@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 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 it.
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
- **Cursor** `.cursor/mcp.json`:
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": { "command": "npx", "args": ["-y", "@precode/mcp"] }
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
- **Codex / Antigravity / VS Code** — add the same `command: npx`, `args: ["-y", "@precode/mcp"]` to their MCP config.
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 failed checks keep a task open, confirms passing checks mark it done, reads `precode://docs`, and writes the final `TODO_FOR_YOU.md`.
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
- - No auto check for a task? `verify` falls back to your self-reported results but stamps the pass **UNVERIFIED**, so it's never mistaken for a machine-verified one.
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: "0.1.0" });
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. If the project declares checks in .precode/checks.json, the MCP RUNS them itself and the verdict comes from real exit codes — your reported results cannot fake a pass. A task is marked done only when every auto check it runs passes. Otherwise the gaps come back and the task stays OPEN fix and call precode.verify again.", {
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("Optional supplementary notes about checks you ran. Authoritative only as a FALLBACK when checks.json defines no auto check for this task."),
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
- // Route manual checks to the human TODO when the task closes — never auto-passed.
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 (!manualSpecs.length)
168
+ if (!taskManual.length)
122
169
  return;
123
170
  await s.appendTodo([
124
171
  `## Task #${task.index} — manual verification required`,
125
172
  "",
126
- ...manualSpecs.map((c) => `- [ ] ${c.name}`),
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
- manualSpecs.length
137
- ? `Manual gates routed to TODO_FOR_YOU.md: ${manualSpecs.map((c) => c.name).join(", ")}`
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 = manualSpecs.length
145
- ? ` ${manualSpecs.length} manual check(s) routed to TODO_FOR_YOU.md for you.`
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
- return await gateFailure(failed.map((r) => `${r.name}: FAILED — ${r.detail}`), failed.map((r) => `${r.name}: ${fixHint(r.name)}`));
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
- // --- Fallback: no auto check defined for this task. Self-report is
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
- if (reported.length === 0) {
192
- return text(`HOLD. Task #${task.index} has no executable check in .precode/checks.json and you reported none. ` +
193
- "Add a real command to checks.json (preferred — then the gate runs it), or report the checks you ran. " +
194
- "Nothing is marked done on an empty report.");
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
- const failedReported = reported.filter((c) => !c.passed);
197
- if (failedReported.length > 0) {
198
- return await gateFailure(failedReported.map((c) => `${c.name}: FAILED${c.detail ? ` — ${c.detail}` : ""}`), failedReported.map((c) => `${c.name}: ${fixHint(c.name)}`));
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
- await recordTelemetry({
201
- eventName: "mcp_verify_pass",
202
- root: ROOT,
203
- task: taskTelemetry(task),
204
- metadata: { checkCount: reported.length, executed: false },
205
- });
206
- return await closeTask(`UNVERIFIED (self-reported, no checks.json command): ${reported.map((c) => `${c.name} ✓`).join(", ")}`);
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
- const manualGates = [];
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 manual = (await s.checksForTask(t.index)).filter((c) => checkKind(c) === "manual");
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
- "Build strictly to the spec in `.precode/docs/`. Do not invent scope. Re-read the relevant doc before each task.",
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: prefer explicit checkboxes; fall back to headings only when
328
- // the spec has none, so heading text never pollutes a real task list.
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 ? checkboxes : headings;
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
- // Starter checks.json. Commands are commented placeholders the user edits to
369
- // their real build until then the gate runs nothing auto and stamps passes
370
- // UNVERIFIED, so it never silently claims a verified build.
371
- await fs.writeFile(path.join(dir, "checks.json"), JSON.stringify({
372
- $comment: "Each 'auto' check is RUN by the MCP from the project root; the task is " +
373
- "done only when its real exit code is 0. Replace cmd values with your " +
374
- "build. 'manual' checks are surfaced for human verification, never auto-passed.",
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.2.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",