@precode/mcp 0.2.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 +44 -8
- package/dist/index.js +124 -47
- package/dist/store.js +81 -13
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -15,14 +15,28 @@ At the end it writes an **`IMPLEMENTATION.md`** (what was built) and a **`TODO_F
|
|
|
15
15
|
|
|
16
16
|
## Install
|
|
17
17
|
|
|
18
|
-
Runs over stdio. Point any MCP host at
|
|
18
|
+
Runs over stdio. Point any MCP host at `npx -y @precode/mcp`. Full per-host copy-paste configs: [useprecode.vercel.app/mcp](https://useprecode.vercel.app/mcp).
|
|
19
19
|
|
|
20
|
-
|
|
20
|
+
| Host | Config location | How |
|
|
21
|
+
|------|-----------------|-----|
|
|
22
|
+
| **Cursor** | `.cursor/mcp.json` | JSON — see below |
|
|
23
|
+
| **Claude Code** | `.mcp.json` (project) | `claude mcp add --env PRECODE_TELEMETRY_DISABLED=1 --transport stdio --scope project precode -- npx -y @precode/mcp` |
|
|
24
|
+
| **Claude Desktop** | `~/Library/Application Support/Claude/claude_desktop_config.json` | JSON — same shape as Cursor |
|
|
25
|
+
| **Codex CLI** | `~/.codex/config.toml` | `codex mcp add precode -- npx -y @precode/mcp` |
|
|
26
|
+
| **VS Code** | `.vscode/mcp.json` | JSON — uses `"servers"` not `"mcpServers"` |
|
|
27
|
+
| **Windsurf** | `~/.codeium/windsurf/mcp_config.json` | JSON — same shape as Cursor |
|
|
28
|
+
| **Antigravity** | `~/.gemini/antigravity/mcp_config.json` | Agent panel → Manage MCP Servers → View raw config |
|
|
29
|
+
|
|
30
|
+
**Cursor / Claude Desktop / Windsurf / Antigravity** — `mcpServers` JSON:
|
|
21
31
|
|
|
22
32
|
```json
|
|
23
33
|
{
|
|
24
34
|
"mcpServers": {
|
|
25
|
-
"precode": {
|
|
35
|
+
"precode": {
|
|
36
|
+
"command": "npx",
|
|
37
|
+
"args": ["-y", "@precode/mcp"],
|
|
38
|
+
"env": { "PRECODE_TELEMETRY_DISABLED": "1" }
|
|
39
|
+
}
|
|
26
40
|
}
|
|
27
41
|
}
|
|
28
42
|
```
|
|
@@ -30,10 +44,30 @@ Runs over stdio. Point any MCP host at it.
|
|
|
30
44
|
**Claude Code:**
|
|
31
45
|
|
|
32
46
|
```bash
|
|
33
|
-
claude mcp add precode -- npx -y @precode/mcp
|
|
47
|
+
claude mcp add --env PRECODE_TELEMETRY_DISABLED=1 --transport stdio --scope project precode -- npx -y @precode/mcp
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
**Codex CLI:**
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
codex mcp add precode -- npx -y @precode/mcp
|
|
34
54
|
```
|
|
35
55
|
|
|
36
|
-
**
|
|
56
|
+
**VS Code** — note `"servers"` wrapper and optional `"cwd": "${workspaceFolder}"`:
|
|
57
|
+
|
|
58
|
+
```json
|
|
59
|
+
{
|
|
60
|
+
"servers": {
|
|
61
|
+
"precode": {
|
|
62
|
+
"type": "stdio",
|
|
63
|
+
"command": "npx",
|
|
64
|
+
"args": ["-y", "@precode/mcp"],
|
|
65
|
+
"cwd": "${workspaceFolder}",
|
|
66
|
+
"env": { "PRECODE_TELEMETRY_DISABLED": "1" }
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
```
|
|
37
71
|
|
|
38
72
|
The server reads `.precode/` from the directory the host runs it in (`PRECODE_ROOT` overrides). Local stdio only — the build loop needs your filesystem.
|
|
39
73
|
|
|
@@ -52,13 +86,14 @@ Telemetry is off unless `PRECODE_TELEMETRY_URL` is set. When enabled, the server
|
|
|
52
86
|
npm run test:stdio
|
|
53
87
|
```
|
|
54
88
|
|
|
55
|
-
The smoke test launches the built MCP server over stdio, verifies the tool list, confirms
|
|
89
|
+
The smoke test launches the built MCP server over stdio, verifies the tool list, runs a real executable check, confirms a failing check (and a fabricated passing report) keep the task open, confirms the task closes only once the real check passes, reads `precode://docs`, and writes the final `TODO_FOR_YOU.md`. `npm test` runs the full edge-case suite (`test/run.mjs`).
|
|
56
90
|
|
|
57
91
|
## Tools
|
|
58
92
|
|
|
59
93
|
| Tool | Does |
|
|
60
94
|
|------|------|
|
|
61
95
|
| `precode.get_goal` | Re-anchor on the goal + progress |
|
|
96
|
+
| `precode.status` | Read-only snapshot of tasks, checks, and deferred work |
|
|
62
97
|
| `precode.next_phase` | Alias for the next phased build step |
|
|
63
98
|
| `precode.next_task` | Pull the next step + acceptance criteria |
|
|
64
99
|
| `precode.verify` | **Runs** the task's `checks.json` checks; marks done only on real exit-0 |
|
|
@@ -84,9 +119,10 @@ Declare checks in `.precode/checks.json`. Each `auto` check is **run by the MCP*
|
|
|
84
119
|
```
|
|
85
120
|
|
|
86
121
|
- `kind: "auto"` — executed; pass = exit 0 (and `expect` substring/`/regex/` matches output if set). Optional `timeoutMs` (default 120s).
|
|
87
|
-
- `kind: "manual"` — never auto-passed; surfaced as a human gate in `TODO_FOR_YOU.md
|
|
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).
|
|
88
123
|
- `taskIndex` — scope a check to one task; omit for a global check that runs on every task.
|
|
89
|
-
-
|
|
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.
|
|
90
126
|
|
|
91
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.
|
|
92
128
|
|
package/dist/index.js
CHANGED
|
@@ -2,9 +2,14 @@
|
|
|
2
2
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
3
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
4
|
import { z } from "zod";
|
|
5
|
+
import { createRequire } from "node:module";
|
|
5
6
|
import { PrecodeStore, adoptSpec } from "./store.js";
|
|
6
7
|
import { checkKind, runChecks } from "./checks.js";
|
|
7
8
|
import { recordTelemetry, taskTelemetry } from "./telemetry.js";
|
|
9
|
+
// Single source of truth for the version the handshake reports — read from
|
|
10
|
+
// package.json so it can never drift from the published package again.
|
|
11
|
+
const VERSION = createRequire(import.meta.url)("../package.json")
|
|
12
|
+
.version ?? "0.0.0";
|
|
8
13
|
/**
|
|
9
14
|
* @precode/mcp — drives a phased build → recheck → fix loop over a `.precode/`
|
|
10
15
|
* package. Fixes the loop/goal failure other spec MCPs ship with:
|
|
@@ -22,7 +27,15 @@ function text(s) {
|
|
|
22
27
|
}
|
|
23
28
|
const NO_STORE = text("No `.precode/` package found in this directory. Either drop a PreCode export here, " +
|
|
24
29
|
"or call `precode.adopt_spec` to map a plain SPEC.md / .specify / .spec-workflow folder into one.");
|
|
25
|
-
const server = new McpServer({ name: "precode", version:
|
|
30
|
+
const server = new McpServer({ name: "precode", version: VERSION }, {
|
|
31
|
+
instructions: "PreCode drives a spec → build → verify loop with a hard, SELF-EXECUTING done-gate. " +
|
|
32
|
+
"Flow: ensure a .precode/ package exists (precode.adopt_spec maps any SPEC.md / .specify / .spec-workflow). " +
|
|
33
|
+
"Then loop: precode.get_goal to anchor on the goal → precode.next_task → build ONLY that task → precode.verify. " +
|
|
34
|
+
"On verify the MCP itself RUNS the commands in .precode/checks.json; a task closes only on real exit-0, so self-reported results cannot fake a pass. " +
|
|
35
|
+
"A failing or unconfigured check keeps the task OPEN with concrete fixes. If a check genuinely cannot pass here (needs a secret/deploy/external service), call precode.defer_task with a reason. " +
|
|
36
|
+
"When every task is done, call precode.finalize. Call precode.status anytime for a read-only progress snapshot. " +
|
|
37
|
+
"Resources precode://goal, ://tasks, ://docs, ://acceptance, ://progress are re-readable each phase.",
|
|
38
|
+
});
|
|
26
39
|
// --- Tool: get the goal (anti-drift anchor) -------------------------------
|
|
27
40
|
server.tool("precode.get_goal", "Re-read the project goal and progress. Call this at the start of every phase so the build does not drift from the spec.", {}, async () => {
|
|
28
41
|
const s = await store();
|
|
@@ -31,6 +44,37 @@ server.tool("precode.get_goal", "Re-read the project goal and progress. Call thi
|
|
|
31
44
|
await recordTelemetry({ eventName: "mcp_get_goal", root: ROOT });
|
|
32
45
|
return text(await s.goal());
|
|
33
46
|
});
|
|
47
|
+
// --- Tool: status (read-only snapshot) ------------------------------------
|
|
48
|
+
server.tool("precode.status", "Read-only snapshot of the build: task counts, the current open task, deferred tasks, and whether real checks are configured. Has no side effects — safe to call anytime.", {}, async () => {
|
|
49
|
+
const s = await store();
|
|
50
|
+
if (!s)
|
|
51
|
+
return NO_STORE;
|
|
52
|
+
const tasks = await s.tasks();
|
|
53
|
+
const deferred = await s.deferredEntries();
|
|
54
|
+
const deferredSet = new Set(deferred.map((e) => e.index));
|
|
55
|
+
const done = tasks.filter((t) => t.done).length;
|
|
56
|
+
const open = tasks.filter((t) => !t.done && !deferredSet.has(t.index));
|
|
57
|
+
const next = open[0];
|
|
58
|
+
const allChecks = await s.checks();
|
|
59
|
+
const autoConfigured = allChecks.filter((c) => checkKind(c) === "auto").length;
|
|
60
|
+
const placeholders = allChecks.filter((c) => checkKind(c) === "skip").length;
|
|
61
|
+
const manual = allChecks.filter((c) => checkKind(c) === "manual").length;
|
|
62
|
+
return text([
|
|
63
|
+
`Tasks: ${done}/${tasks.length} done, ${open.length} open, ${deferred.length} deferred.`,
|
|
64
|
+
next ? `Next open task: #${next.index} — ${next.text}` : "No open tasks — call precode.finalize.",
|
|
65
|
+
`Checks in checks.json: ${autoConfigured} auto (executed), ${manual} manual, ${placeholders} unconfigured placeholder(s).`,
|
|
66
|
+
placeholders > 0
|
|
67
|
+
? "WARNING: placeholder checks have no command — tasks they apply to will HOLD until you set a real cmd, mark them manual, or defer."
|
|
68
|
+
: autoConfigured === 0 && manual === 0
|
|
69
|
+
? "WARNING: no checks defined — add an auto check with a real cmd so the gate can verify."
|
|
70
|
+
: "Gate is backed by real executed checks.",
|
|
71
|
+
deferred.length
|
|
72
|
+
? `Deferred: ${deferred.map((e) => `#${e.index} (${e.reason})`).join("; ")}`
|
|
73
|
+
: "",
|
|
74
|
+
]
|
|
75
|
+
.filter(Boolean)
|
|
76
|
+
.join("\n"));
|
|
77
|
+
});
|
|
34
78
|
// --- Tool: next task ------------------------------------------------------
|
|
35
79
|
server.tool("precode.next_task", "Pull the next unchecked build step plus the goal and acceptance criteria. Build ONLY this task, then call precode.verify. Do not skip ahead.", {}, async () => {
|
|
36
80
|
const s = await store();
|
|
@@ -91,7 +135,7 @@ server.tool("precode.next_phase", "Alias for precode.next_task. Pulls the next p
|
|
|
91
135
|
].join("\n"));
|
|
92
136
|
});
|
|
93
137
|
// --- Tool: verify (the hard done-gate + fix loop) -------------------------
|
|
94
|
-
server.tool("precode.verify", "Close the done-gate for the current task.
|
|
138
|
+
server.tool("precode.verify", "Close the done-gate for the current task. The MCP RUNS the task's auto checks from .precode/checks.json itself and the verdict comes from real exit codes — your reported results CANNOT fake a pass and never close a task on their own. A task is marked done only when every auto check passes (or it has only manual gates, which route to TODO). If a declared check has no command yet, or no check is defined, you get a HOLD telling you to wire a real command, mark it manual, or precode.defer_task. Fix and call precode.verify again.", {
|
|
95
139
|
checkResults: z
|
|
96
140
|
.array(z.object({
|
|
97
141
|
name: z.string().describe("e.g. 'build', 'typecheck', 'lint', 'smoke test'"),
|
|
@@ -99,7 +143,7 @@ server.tool("precode.verify", "Close the done-gate for the current task. If the
|
|
|
99
143
|
detail: z.string().optional(),
|
|
100
144
|
}))
|
|
101
145
|
.optional()
|
|
102
|
-
.describe("
|
|
146
|
+
.describe("ADVISORY ONLY. Never used to mark a task done — a task closes only when the MCP executes its checks.json checks and they pass (or via a manual gate / precode.defer_task)."),
|
|
103
147
|
notes: z.string().optional(),
|
|
104
148
|
task: z.string().optional().describe("Optional host-side task identifier or title."),
|
|
105
149
|
}, async ({ checkResults, notes, task: taskLabel }) => {
|
|
@@ -116,14 +160,17 @@ server.tool("precode.verify", "Close the done-gate for the current task. If the
|
|
|
116
160
|
const specs = await s.checksForTask(task.index);
|
|
117
161
|
const autoSpecs = specs.filter((c) => checkKind(c) === "auto");
|
|
118
162
|
const manualSpecs = specs.filter((c) => checkKind(c) === "manual");
|
|
119
|
-
|
|
163
|
+
const placeholderSpecs = specs.filter((c) => checkKind(c) === "skip");
|
|
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);
|
|
120
167
|
const routeManual = async () => {
|
|
121
|
-
if (!
|
|
168
|
+
if (!taskManual.length)
|
|
122
169
|
return;
|
|
123
170
|
await s.appendTodo([
|
|
124
171
|
`## Task #${task.index} — manual verification required`,
|
|
125
172
|
"",
|
|
126
|
-
...
|
|
173
|
+
...taskManual.map((c) => `- [ ] ${c.name}`),
|
|
127
174
|
].join("\n"));
|
|
128
175
|
};
|
|
129
176
|
const closeTask = async (verifiedLine) => {
|
|
@@ -133,16 +180,16 @@ server.tool("precode.verify", "Close the done-gate for the current task. If the
|
|
|
133
180
|
`## Task #${task.index}: ${task.text}`,
|
|
134
181
|
taskLabel ? `Host task: ${taskLabel}` : "",
|
|
135
182
|
verifiedLine,
|
|
136
|
-
|
|
137
|
-
? `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(", ")}`
|
|
138
185
|
: "",
|
|
139
186
|
notes ? `Notes: ${notes}` : "",
|
|
140
187
|
]
|
|
141
188
|
.filter(Boolean)
|
|
142
189
|
.join("\n"));
|
|
143
190
|
const next = await s.nextOpenTask();
|
|
144
|
-
const manualNote =
|
|
145
|
-
? ` ${
|
|
191
|
+
const manualNote = taskManual.length
|
|
192
|
+
? ` ${taskManual.length} manual check(s) routed to TODO_FOR_YOU.md for you.`
|
|
146
193
|
: "";
|
|
147
194
|
return text((next
|
|
148
195
|
? `PASS. Task #${task.index} marked done.${manualNote} Call precode.next_task for the next step (#${next.index}).`
|
|
@@ -174,7 +221,16 @@ server.tool("precode.verify", "Close the done-gate for the current task. If the
|
|
|
174
221
|
const runs = await runChecks(autoSpecs, ROOT);
|
|
175
222
|
const failed = runs.filter((r) => !r.passed);
|
|
176
223
|
if (failed.length > 0) {
|
|
177
|
-
|
|
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
|
+
}));
|
|
178
234
|
}
|
|
179
235
|
await recordTelemetry({
|
|
180
236
|
eventName: "mcp_verify_pass",
|
|
@@ -184,26 +240,48 @@ server.tool("precode.verify", "Close the done-gate for the current task. If the
|
|
|
184
240
|
});
|
|
185
241
|
return await closeTask(`Verified by MCP execution: ${runs.map((r) => `${r.name} ✓ (exit ${r.exitCode})`).join(", ")}`);
|
|
186
242
|
}
|
|
187
|
-
// ---
|
|
188
|
-
// accepted but explicitly stamped UNVERIFIED so it is never mistaken
|
|
189
|
-
// for a machine-verified pass. ---
|
|
243
|
+
// --- No auto check ran. A task NEVER closes on a self-reported pass. ---
|
|
190
244
|
const reported = checkResults ?? [];
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
"
|
|
245
|
+
const reportedNote = reported.length
|
|
246
|
+
? `Agent-reported (advisory, NOT used to close): ${reported
|
|
247
|
+
.map((c) => `${c.name}=${c.passed ? "pass" : "fail"}`)
|
|
248
|
+
.join(", ")}`
|
|
249
|
+
: "";
|
|
250
|
+
// Unconfigured placeholder checks (declared but no command) — hold and
|
|
251
|
+
// tell the agent to wire a real command, mark it manual, or defer.
|
|
252
|
+
if (placeholderSpecs.length > 0) {
|
|
253
|
+
return text([
|
|
254
|
+
`HOLD. Task #${task.index} is NOT done.`,
|
|
255
|
+
`${placeholderSpecs.length} declared check(s) have no command yet: ${placeholderSpecs
|
|
256
|
+
.map((c) => c.name)
|
|
257
|
+
.join(", ")}.`,
|
|
258
|
+
"A task never closes on a self-reported pass. Do ONE of these in .precode/checks.json:",
|
|
259
|
+
"- set a real shell `cmd` for each check (preferred — the gate then RUNS it), or",
|
|
260
|
+
'- set its `kind` to `manual` if only a human can verify it (routed to TODO_FOR_YOU.md), or',
|
|
261
|
+
"- call precode.defer_task if it genuinely cannot be verified in this environment.",
|
|
262
|
+
reportedNote,
|
|
263
|
+
]
|
|
264
|
+
.filter(Boolean)
|
|
265
|
+
.join("\n"));
|
|
195
266
|
}
|
|
196
|
-
|
|
197
|
-
if (
|
|
198
|
-
|
|
267
|
+
// Only explicit manual gates apply — a legitimate human-verified close.
|
|
268
|
+
if (manualSpecs.length > 0) {
|
|
269
|
+
await recordTelemetry({
|
|
270
|
+
eventName: "mcp_verify_pass",
|
|
271
|
+
root: ROOT,
|
|
272
|
+
task: taskTelemetry(task),
|
|
273
|
+
metadata: { checkCount: manualSpecs.length, executed: false, manualOnly: true },
|
|
274
|
+
});
|
|
275
|
+
return await closeTask("Closed with MANUAL gates only (no automatable check) — routed to TODO_FOR_YOU.md for your verification.");
|
|
199
276
|
}
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
task:
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
277
|
+
// No check defined at all for this task.
|
|
278
|
+
return text([
|
|
279
|
+
`HOLD. Task #${task.index} has no check defined in .precode/checks.json, so it cannot be verified.`,
|
|
280
|
+
"A task never closes on a self-reported pass. Add at least one check (preferred: an `auto` check with a real `cmd` the gate runs), mark it `manual`, or call precode.defer_task.",
|
|
281
|
+
reportedNote,
|
|
282
|
+
]
|
|
283
|
+
.filter(Boolean)
|
|
284
|
+
.join("\n"));
|
|
207
285
|
}
|
|
208
286
|
finally {
|
|
209
287
|
await s.releaseLock();
|
|
@@ -255,11 +333,15 @@ server.tool("precode.finalize", "Write the handoff: confirm all tasks done and r
|
|
|
255
333
|
const deferredSet = new Set(deferred.map((e) => e.index));
|
|
256
334
|
const open = tasks.filter((t) => !t.done && !deferredSet.has(t.index));
|
|
257
335
|
// Manual gates the human still owns, regenerated from checks.json for done tasks.
|
|
258
|
-
|
|
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);
|
|
259
341
|
for (const t of tasks.filter((t) => t.done)) {
|
|
260
|
-
const
|
|
261
|
-
for (const c of manual)
|
|
342
|
+
for (const c of allChecks.filter((c) => checkKind(c) === "manual" && c.taskIndex === t.index)) {
|
|
262
343
|
manualGates.push(`Task #${t.index}: ${c.name}`);
|
|
344
|
+
}
|
|
263
345
|
}
|
|
264
346
|
await recordTelemetry({
|
|
265
347
|
eventName: "mcp_finalize",
|
|
@@ -324,6 +406,17 @@ server.tool("precode.defer_task", "Honest escape hatch for the CURRENT open task
|
|
|
324
406
|
const task = await s.nextOpenTask();
|
|
325
407
|
if (!task)
|
|
326
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
|
+
}
|
|
327
420
|
await s.deferTask(task.index, reason);
|
|
328
421
|
await recordTelemetry({
|
|
329
422
|
eventName: "mcp_defer_task",
|
|
@@ -401,19 +494,3 @@ main().catch((err) => {
|
|
|
401
494
|
console.error("[precode-mcp] fatal:", err);
|
|
402
495
|
process.exit(1);
|
|
403
496
|
});
|
|
404
|
-
function fixHint(checkName) {
|
|
405
|
-
const lower = checkName.toLowerCase();
|
|
406
|
-
if (lower.includes("type") || lower.includes("tsc")) {
|
|
407
|
-
return "open the TypeScript diagnostics, fix the reported type errors, then rerun the same type-check command.";
|
|
408
|
-
}
|
|
409
|
-
if (lower.includes("lint") || lower.includes("eslint")) {
|
|
410
|
-
return "fix the reported lint violations without changing spec scope, then rerun lint.";
|
|
411
|
-
}
|
|
412
|
-
if (lower.includes("build")) {
|
|
413
|
-
return "inspect the build error, fix the first failing route/module/env issue, then rerun the production build.";
|
|
414
|
-
}
|
|
415
|
-
if (lower.includes("test") || lower.includes("smoke") || lower.includes("e2e")) {
|
|
416
|
-
return "reproduce the failing flow, align behavior to the acceptance criteria, then rerun the test.";
|
|
417
|
-
}
|
|
418
|
-
return "inspect the failing command output, make the smallest spec-aligned fix, then rerun this exact check.";
|
|
419
|
-
}
|
package/dist/store.js
CHANGED
|
@@ -193,10 +193,14 @@ export class PrecodeStore {
|
|
|
193
193
|
const project = manifest?.project ?? {};
|
|
194
194
|
const tasks = await this.tasks();
|
|
195
195
|
const done = tasks.filter((t) => t.done).length;
|
|
196
|
+
const deferred = await this.deferredIndices();
|
|
197
|
+
const docs = await this.listDocFiles();
|
|
196
198
|
return [
|
|
197
199
|
`Project: ${project.name ?? "(unknown)"} (${project.appType ?? "app"})`,
|
|
198
|
-
`Progress: ${done}/${tasks.length} tasks complete.`,
|
|
199
|
-
|
|
200
|
+
`Progress: ${done}/${tasks.length} tasks complete${deferred.length ? `, ${deferred.length} deferred` : ""}.`,
|
|
201
|
+
docs.length
|
|
202
|
+
? `Build strictly to the spec docs in .precode/: ${docs.join(", ")}. Re-read the relevant one before each task; do not invent scope.`
|
|
203
|
+
: "No spec docs found in .precode/docs/. Work from the tasks and acceptance criteria; do not invent scope.",
|
|
200
204
|
].join("\n");
|
|
201
205
|
}
|
|
202
206
|
async appendImplementation(entry) {
|
|
@@ -324,9 +328,13 @@ export async function adoptSpec(searchRoot, specPathHint) {
|
|
|
324
328
|
error: "No spec found. Provide a SPEC.md, a .specify/.spec-workflow folder, or a docs/ folder of markdown.",
|
|
325
329
|
};
|
|
326
330
|
}
|
|
327
|
-
// Derive tasks
|
|
328
|
-
//
|
|
331
|
+
// Derive tasks, in priority order so a real list is never collapsed into one:
|
|
332
|
+
// 1. explicit checkboxes - [ ] ...
|
|
333
|
+
// 2. plain bullets / numbered items - x * x 1. x
|
|
334
|
+
// 3. section headings ## / ###
|
|
335
|
+
// 4. a single catch-all when the spec has no structure at all.
|
|
329
336
|
const checkboxes = [];
|
|
337
|
+
const bullets = [];
|
|
330
338
|
const headings = [];
|
|
331
339
|
for (const line of specBody.split("\n")) {
|
|
332
340
|
const cb = TASK_RE.exec(line);
|
|
@@ -334,11 +342,21 @@ export async function adoptSpec(searchRoot, specPathHint) {
|
|
|
334
342
|
checkboxes.push(`- [ ] ${cb[3]}`);
|
|
335
343
|
continue;
|
|
336
344
|
}
|
|
345
|
+
const bullet = /^\s*(?:[-*]|\d+[.)])\s+(.*\S)\s*$/.exec(line);
|
|
346
|
+
if (bullet) {
|
|
347
|
+
if (bullets.length < 40)
|
|
348
|
+
bullets.push(`- [ ] ${bullet[1]}`);
|
|
349
|
+
continue;
|
|
350
|
+
}
|
|
337
351
|
const h = /^#{2,3}\s+(.*\S)\s*$/.exec(line);
|
|
338
352
|
if (h && headings.length < 40)
|
|
339
353
|
headings.push(`- [ ] Implement: ${h[1]}`);
|
|
340
354
|
}
|
|
341
|
-
const tasks = checkboxes.length
|
|
355
|
+
const tasks = checkboxes.length
|
|
356
|
+
? checkboxes
|
|
357
|
+
: bullets.length
|
|
358
|
+
? bullets
|
|
359
|
+
: headings;
|
|
342
360
|
if (!tasks.length)
|
|
343
361
|
tasks.push("- [ ] Implement the full spec, then verify.");
|
|
344
362
|
await fs.mkdir(path.join(dir, "progress"), { recursive: true });
|
|
@@ -365,18 +383,25 @@ export async function adoptSpec(searchRoot, specPathHint) {
|
|
|
365
383
|
"",
|
|
366
384
|
].join("\n"), "utf8");
|
|
367
385
|
await fs.writeFile(path.join(dir, "manifest.json"), JSON.stringify({ project: { name: "Adopted spec", appType: "app" }, adoptedFrom: found }, null, 2), "utf8");
|
|
368
|
-
//
|
|
369
|
-
//
|
|
370
|
-
//
|
|
371
|
-
await
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
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
|
+
: [
|
|
376
393
|
{ id: "typecheck", name: "Type-check", kind: "auto", cmd: "" },
|
|
377
394
|
{ id: "lint", name: "Lint", kind: "auto", cmd: "" },
|
|
378
395
|
{ id: "build", name: "Production build", kind: "auto", cmd: "" },
|
|
379
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,
|
|
380
405
|
{
|
|
381
406
|
id: "secrets",
|
|
382
407
|
name: "Secrets & deploy env verified in real accounts",
|
|
@@ -386,3 +411,46 @@ export async function adoptSpec(searchRoot, specPathHint) {
|
|
|
386
411
|
}, null, 2), "utf8");
|
|
387
412
|
return { ok: true, dir };
|
|
388
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
|
+
"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",
|