@precode/mcp 0.1.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +69 -10
- package/dist/checks.js +120 -0
- package/dist/index.js +227 -66
- package/dist/store.js +156 -8
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -7,22 +7,36 @@ Install: [`@precode/mcp` on npm](https://www.npmjs.com/package/@precode/mcp). Do
|
|
|
7
7
|
It fixes the loop/goal failure other spec MCPs ship with:
|
|
8
8
|
|
|
9
9
|
- **Goal re-anchoring** — the agent re-reads the goal every phase, so long builds don't drift.
|
|
10
|
-
- **Hard definition-of-done** —
|
|
10
|
+
- **Hard definition-of-done** — when you declare checks in `.precode/checks.json`, **the MCP runs them itself** and the verdict comes from the real exit codes. A fabricated "it passed" cannot move a task to done.
|
|
11
11
|
- **Closed verify → fix → re-verify** — gaps come back with fixes and the loop will not advance past them.
|
|
12
|
-
- **
|
|
12
|
+
- **Honest escapes** — a check that genuinely can't pass here (needs a secret, a deploy, a paid service) is `precode.defer_task`'d with a reason and surfaced in `TODO_FOR_YOU.md`, never silently skipped.
|
|
13
13
|
|
|
14
14
|
At the end it writes an **`IMPLEMENTATION.md`** (what was built) and a **`TODO_FOR_YOU.md`** (what you still need to do — secrets, env, deploy).
|
|
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,19 +86,44 @@ 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
|
-
| `precode.verify` |
|
|
99
|
+
| `precode.verify` | **Runs** the task's `checks.json` checks; marks done only on real exit-0 |
|
|
100
|
+
| `precode.defer_task` | Honest escape for a check that can't pass here (logged to TODO) |
|
|
65
101
|
| `precode.record_implementation` | Append to the implementation ledger |
|
|
66
102
|
| `precode.finalize` | Write `TODO_FOR_YOU.md` |
|
|
67
|
-
| `precode.adopt_spec` | Map a non-PreCode spec into `.precode/` |
|
|
103
|
+
| `precode.adopt_spec` | Map a non-PreCode spec into `.precode/` (+ starter `checks.json`) |
|
|
104
|
+
|
|
105
|
+
## Checks — the executable done-gate
|
|
106
|
+
|
|
107
|
+
Declare checks in `.precode/checks.json`. Each `auto` check is **run by the MCP** from the project root on every `precode.verify`; the task closes only when its real exit code is `0`.
|
|
108
|
+
|
|
109
|
+
```json
|
|
110
|
+
{
|
|
111
|
+
"checks": [
|
|
112
|
+
{ "id": "typecheck", "name": "Type-check", "kind": "auto", "cmd": "npm run typecheck" },
|
|
113
|
+
{ "id": "build", "name": "Build", "kind": "auto", "cmd": "npm run build" },
|
|
114
|
+
{ "id": "test", "name": "Tests", "kind": "auto", "cmd": "npm test", "expect": "/\\d+ passing/" },
|
|
115
|
+
{ "id": "deploy-task-3", "name": "Smoke route /health", "kind": "auto", "cmd": "curl -fsS localhost:3000/health", "taskIndex": 3 },
|
|
116
|
+
{ "id": "secrets", "name": "Prod secrets set in real accounts", "kind": "manual" }
|
|
117
|
+
]
|
|
118
|
+
}
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
- `kind: "auto"` — executed; pass = exit 0 (and `expect` substring/`/regex/` matches output if set). Optional `timeoutMs` (default 120s).
|
|
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
|
+
- `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`. `adopt_spec` writes such placeholders on purpose so a fresh spec can't be "verified" until you point the gate at something real.
|
|
125
|
+
|
|
126
|
+
**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.
|
|
68
127
|
|
|
69
128
|
## Resources
|
|
70
129
|
|
package/dist/checks.js
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
const DEFAULT_TIMEOUT_MS = 120_000;
|
|
3
|
+
const OUTPUT_TAIL = 1600;
|
|
4
|
+
function tail(s, n = OUTPUT_TAIL) {
|
|
5
|
+
const trimmed = s.replace(/\s+$/, "");
|
|
6
|
+
return trimmed.length > n ? "…" + trimmed.slice(-n) : trimmed;
|
|
7
|
+
}
|
|
8
|
+
function matchesExpect(output, expect) {
|
|
9
|
+
if (expect.length > 1 && expect.startsWith("/") && expect.endsWith("/")) {
|
|
10
|
+
try {
|
|
11
|
+
return new RegExp(expect.slice(1, -1)).test(output);
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
// Malformed regex => fall back to substring so a bad spec can't crash the gate.
|
|
15
|
+
return output.includes(expect);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
return output.includes(expect);
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Resolve a spec to one of three states:
|
|
22
|
+
* - "manual": an explicit human gate (surfaced, never auto-passed).
|
|
23
|
+
* - "auto": has a runnable command (executed; authoritative).
|
|
24
|
+
* - "skip": an unconfigured placeholder (no command, not manual) — ignored
|
|
25
|
+
* until the user fills in `cmd`, so an empty starter check never gates.
|
|
26
|
+
*/
|
|
27
|
+
export function checkKind(spec) {
|
|
28
|
+
if (spec.kind === "manual")
|
|
29
|
+
return "manual";
|
|
30
|
+
if (spec.cmd && spec.cmd.trim())
|
|
31
|
+
return "auto";
|
|
32
|
+
return "skip";
|
|
33
|
+
}
|
|
34
|
+
/** Run a single auto check. Manual checks are returned as skipped (human gate). */
|
|
35
|
+
export function runCheck(spec, root) {
|
|
36
|
+
const kind = checkKind(spec);
|
|
37
|
+
if (kind !== "auto") {
|
|
38
|
+
return Promise.resolve({
|
|
39
|
+
id: spec.id,
|
|
40
|
+
name: spec.name,
|
|
41
|
+
kind: kind === "manual" ? "manual" : "auto",
|
|
42
|
+
passed: false,
|
|
43
|
+
skipped: true,
|
|
44
|
+
exitCode: null,
|
|
45
|
+
timedOut: false,
|
|
46
|
+
detail: kind === "manual"
|
|
47
|
+
? "manual check — requires human verification, not auto-passed."
|
|
48
|
+
: "unconfigured placeholder — no command set, ignored.",
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
const timeoutMs = typeof spec.timeoutMs === "number" && spec.timeoutMs > 0
|
|
52
|
+
? spec.timeoutMs
|
|
53
|
+
: DEFAULT_TIMEOUT_MS;
|
|
54
|
+
return new Promise((resolve) => {
|
|
55
|
+
let out = "";
|
|
56
|
+
let settled = false;
|
|
57
|
+
let timedOut = false;
|
|
58
|
+
const child = spawn(spec.cmd, {
|
|
59
|
+
cwd: root,
|
|
60
|
+
shell: true,
|
|
61
|
+
env: process.env,
|
|
62
|
+
});
|
|
63
|
+
const timer = setTimeout(() => {
|
|
64
|
+
timedOut = true;
|
|
65
|
+
child.kill("SIGKILL");
|
|
66
|
+
}, timeoutMs);
|
|
67
|
+
const onData = (d) => {
|
|
68
|
+
out += d.toString();
|
|
69
|
+
if (out.length > OUTPUT_TAIL * 4)
|
|
70
|
+
out = out.slice(-OUTPUT_TAIL * 4);
|
|
71
|
+
};
|
|
72
|
+
child.stdout?.on("data", onData);
|
|
73
|
+
child.stderr?.on("data", onData);
|
|
74
|
+
const finish = (exitCode) => {
|
|
75
|
+
if (settled)
|
|
76
|
+
return;
|
|
77
|
+
settled = true;
|
|
78
|
+
clearTimeout(timer);
|
|
79
|
+
let passed = !timedOut && exitCode === 0;
|
|
80
|
+
let detail;
|
|
81
|
+
if (timedOut) {
|
|
82
|
+
detail = `timed out after ${timeoutMs}ms. ${tail(out)}`;
|
|
83
|
+
}
|
|
84
|
+
else if (exitCode !== 0) {
|
|
85
|
+
detail = `exit ${exitCode}. ${tail(out)}`;
|
|
86
|
+
}
|
|
87
|
+
else if (spec.expect && !matchesExpect(out, spec.expect)) {
|
|
88
|
+
passed = false;
|
|
89
|
+
detail = `exit 0 but output did not match expect=${spec.expect}. ${tail(out)}`;
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
detail = tail(out) || "passed (exit 0).";
|
|
93
|
+
}
|
|
94
|
+
resolve({
|
|
95
|
+
id: spec.id,
|
|
96
|
+
name: spec.name,
|
|
97
|
+
kind,
|
|
98
|
+
passed,
|
|
99
|
+
skipped: false,
|
|
100
|
+
exitCode,
|
|
101
|
+
timedOut,
|
|
102
|
+
detail,
|
|
103
|
+
});
|
|
104
|
+
};
|
|
105
|
+
child.on("error", (err) => {
|
|
106
|
+
// e.g. spawn failure; surface as a real failure rather than throwing.
|
|
107
|
+
finish(null);
|
|
108
|
+
void err;
|
|
109
|
+
});
|
|
110
|
+
child.on("close", (code) => finish(code));
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
/** Run checks sequentially for deterministic, reproducible ordering. */
|
|
114
|
+
export async function runChecks(specs, root) {
|
|
115
|
+
const results = [];
|
|
116
|
+
for (const spec of specs) {
|
|
117
|
+
results.push(await runCheck(spec, root));
|
|
118
|
+
}
|
|
119
|
+
return results;
|
|
120
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -2,8 +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";
|
|
7
|
+
import { checkKind, runChecks } from "./checks.js";
|
|
6
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";
|
|
7
13
|
/**
|
|
8
14
|
* @precode/mcp — drives a phased build → recheck → fix loop over a `.precode/`
|
|
9
15
|
* package. Fixes the loop/goal failure other spec MCPs ship with:
|
|
@@ -21,7 +27,15 @@ function text(s) {
|
|
|
21
27
|
}
|
|
22
28
|
const NO_STORE = text("No `.precode/` package found in this directory. Either drop a PreCode export here, " +
|
|
23
29
|
"or call `precode.adopt_spec` to map a plain SPEC.md / .specify / .spec-workflow folder into one.");
|
|
24
|
-
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
|
+
});
|
|
25
39
|
// --- Tool: get the goal (anti-drift anchor) -------------------------------
|
|
26
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 () => {
|
|
27
41
|
const s = await store();
|
|
@@ -30,6 +44,37 @@ server.tool("precode.get_goal", "Re-read the project goal and progress. Call thi
|
|
|
30
44
|
await recordTelemetry({ eventName: "mcp_get_goal", root: ROOT });
|
|
31
45
|
return text(await s.goal());
|
|
32
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
|
+
});
|
|
33
78
|
// --- Tool: next task ------------------------------------------------------
|
|
34
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 () => {
|
|
35
80
|
const s = await store();
|
|
@@ -90,83 +135,146 @@ server.tool("precode.next_phase", "Alias for precode.next_task. Pulls the next p
|
|
|
90
135
|
].join("\n"));
|
|
91
136
|
});
|
|
92
137
|
// --- Tool: verify (the hard done-gate + fix loop) -------------------------
|
|
93
|
-
server.tool("precode.verify", "
|
|
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.", {
|
|
94
139
|
checkResults: z
|
|
95
140
|
.array(z.object({
|
|
96
141
|
name: z.string().describe("e.g. 'build', 'typecheck', 'lint', 'smoke test'"),
|
|
97
142
|
passed: z.boolean(),
|
|
98
143
|
detail: z.string().optional(),
|
|
99
144
|
}))
|
|
100
|
-
.
|
|
145
|
+
.optional()
|
|
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)."),
|
|
101
147
|
notes: z.string().optional(),
|
|
102
148
|
task: z.string().optional().describe("Optional host-side task identifier or title."),
|
|
103
149
|
}, async ({ checkResults, notes, task: taskLabel }) => {
|
|
104
150
|
const s = await store();
|
|
105
151
|
if (!s)
|
|
106
152
|
return NO_STORE;
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
return text("No open task to verify. Run precode.finalize.");
|
|
110
|
-
if (checkResults.length === 0) {
|
|
111
|
-
return text(`HOLD. Task #${task.index} cannot be marked done — you reported no checks. ` +
|
|
112
|
-
"Run the acceptance checks (build, type-check, lint, smoke test) in your terminal, then report results.");
|
|
153
|
+
if (!(await s.acquireLock())) {
|
|
154
|
+
return text("Another precode.verify is in progress for this package. Wait for it to finish, then retry.");
|
|
113
155
|
}
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
}
|
|
132
|
-
const
|
|
133
|
-
.
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
156
|
+
try {
|
|
157
|
+
const task = await s.nextOpenTask();
|
|
158
|
+
if (!task)
|
|
159
|
+
return text("No open task to verify. Run precode.finalize.");
|
|
160
|
+
const specs = await s.checksForTask(task.index);
|
|
161
|
+
const autoSpecs = specs.filter((c) => checkKind(c) === "auto");
|
|
162
|
+
const manualSpecs = specs.filter((c) => checkKind(c) === "manual");
|
|
163
|
+
const placeholderSpecs = specs.filter((c) => checkKind(c) === "skip");
|
|
164
|
+
// Route manual checks to the human TODO when the task closes — never auto-passed.
|
|
165
|
+
const routeManual = async () => {
|
|
166
|
+
if (!manualSpecs.length)
|
|
167
|
+
return;
|
|
168
|
+
await s.appendTodo([
|
|
169
|
+
`## Task #${task.index} — manual verification required`,
|
|
170
|
+
"",
|
|
171
|
+
...manualSpecs.map((c) => `- [ ] ${c.name}`),
|
|
172
|
+
].join("\n"));
|
|
173
|
+
};
|
|
174
|
+
const closeTask = async (verifiedLine) => {
|
|
175
|
+
await s.markTaskDone(task.index);
|
|
176
|
+
await routeManual();
|
|
177
|
+
await s.appendImplementation([
|
|
178
|
+
`## Task #${task.index}: ${task.text}`,
|
|
179
|
+
taskLabel ? `Host task: ${taskLabel}` : "",
|
|
180
|
+
verifiedLine,
|
|
181
|
+
manualSpecs.length
|
|
182
|
+
? `Manual gates routed to TODO_FOR_YOU.md: ${manualSpecs.map((c) => c.name).join(", ")}`
|
|
183
|
+
: "",
|
|
184
|
+
notes ? `Notes: ${notes}` : "",
|
|
185
|
+
]
|
|
186
|
+
.filter(Boolean)
|
|
187
|
+
.join("\n"));
|
|
188
|
+
const next = await s.nextOpenTask();
|
|
189
|
+
const manualNote = manualSpecs.length
|
|
190
|
+
? ` ${manualSpecs.length} manual check(s) routed to TODO_FOR_YOU.md for you.`
|
|
191
|
+
: "";
|
|
192
|
+
return text((next
|
|
193
|
+
? `PASS. Task #${task.index} marked done.${manualNote} Call precode.next_task for the next step (#${next.index}).`
|
|
194
|
+
: `PASS. Task #${task.index} marked done.${manualNote} All tasks complete — call precode.finalize.`));
|
|
195
|
+
};
|
|
196
|
+
const gateFailure = async (failureLines, fixLines) => {
|
|
197
|
+
const retry = await s.recordVerificationFailure({ task, failures: failureLines });
|
|
198
|
+
await recordTelemetry({
|
|
199
|
+
eventName: "mcp_verify_fail",
|
|
200
|
+
root: ROOT,
|
|
201
|
+
task: taskTelemetry(task),
|
|
202
|
+
metadata: { failedCount: failureLines.length, attempts: retry.attempts, escalated: retry.escalated },
|
|
203
|
+
});
|
|
204
|
+
return text([
|
|
205
|
+
`GATE: Task #${task.index} stays OPEN. ${failureLines.length} check(s) failed:`,
|
|
206
|
+
...failureLines.map((l) => `- ${l}`),
|
|
207
|
+
"",
|
|
208
|
+
"Concrete next fixes:",
|
|
209
|
+
...fixLines.map((l) => `- ${l}`),
|
|
210
|
+
"",
|
|
211
|
+
`Verify attempt ${retry.attempts}/3 for this task.`,
|
|
212
|
+
retry.escalated
|
|
213
|
+
? "Escalated honestly into .precode/progress/TODO_FOR_YOU.md because the retry bound was reached. If a check genuinely cannot pass here (needs a secret, a deploy, an external service), call precode.defer_task with a reason instead of looping."
|
|
214
|
+
: "Fix these, re-run, and call precode.verify again. Do not call precode.next_task until this passes.",
|
|
215
|
+
].join("\n"));
|
|
216
|
+
};
|
|
217
|
+
// --- Authoritative path: the MCP executes the declared auto checks. ---
|
|
218
|
+
if (autoSpecs.length > 0) {
|
|
219
|
+
const runs = await runChecks(autoSpecs, ROOT);
|
|
220
|
+
const failed = runs.filter((r) => !r.passed);
|
|
221
|
+
if (failed.length > 0) {
|
|
222
|
+
return await gateFailure(failed.map((r) => `${r.name}: FAILED — ${r.detail}`), failed.map((r) => `${r.name}: ${fixHint(r.name)}`));
|
|
223
|
+
}
|
|
224
|
+
await recordTelemetry({
|
|
225
|
+
eventName: "mcp_verify_pass",
|
|
226
|
+
root: ROOT,
|
|
227
|
+
task: taskTelemetry(task),
|
|
228
|
+
metadata: { checkCount: runs.length, executed: true },
|
|
229
|
+
});
|
|
230
|
+
return await closeTask(`Verified by MCP execution: ${runs.map((r) => `${r.name} ✓ (exit ${r.exitCode})`).join(", ")}`);
|
|
231
|
+
}
|
|
232
|
+
// --- No auto check ran. A task NEVER closes on a self-reported pass. ---
|
|
233
|
+
const reported = checkResults ?? [];
|
|
234
|
+
const reportedNote = reported.length
|
|
235
|
+
? `Agent-reported (advisory, NOT used to close): ${reported
|
|
236
|
+
.map((c) => `${c.name}=${c.passed ? "pass" : "fail"}`)
|
|
237
|
+
.join(", ")}`
|
|
238
|
+
: "";
|
|
239
|
+
// Unconfigured placeholder checks (declared but no command) — hold and
|
|
240
|
+
// tell the agent to wire a real command, mark it manual, or defer.
|
|
241
|
+
if (placeholderSpecs.length > 0) {
|
|
242
|
+
return text([
|
|
243
|
+
`HOLD. Task #${task.index} is NOT done.`,
|
|
244
|
+
`${placeholderSpecs.length} declared check(s) have no command yet: ${placeholderSpecs
|
|
245
|
+
.map((c) => c.name)
|
|
246
|
+
.join(", ")}.`,
|
|
247
|
+
"A task never closes on a self-reported pass. Do ONE of these in .precode/checks.json:",
|
|
248
|
+
"- set a real shell `cmd` for each check (preferred — the gate then RUNS it), or",
|
|
249
|
+
'- set its `kind` to `manual` if only a human can verify it (routed to TODO_FOR_YOU.md), or',
|
|
250
|
+
"- call precode.defer_task if it genuinely cannot be verified in this environment.",
|
|
251
|
+
reportedNote,
|
|
252
|
+
]
|
|
253
|
+
.filter(Boolean)
|
|
254
|
+
.join("\n"));
|
|
255
|
+
}
|
|
256
|
+
// Only explicit manual gates apply — a legitimate human-verified close.
|
|
257
|
+
if (manualSpecs.length > 0) {
|
|
258
|
+
await recordTelemetry({
|
|
259
|
+
eventName: "mcp_verify_pass",
|
|
260
|
+
root: ROOT,
|
|
261
|
+
task: taskTelemetry(task),
|
|
262
|
+
metadata: { checkCount: manualSpecs.length, executed: false, manualOnly: true },
|
|
263
|
+
});
|
|
264
|
+
return await closeTask("Closed with MANUAL gates only (no automatable check) — routed to TODO_FOR_YOU.md for your verification.");
|
|
265
|
+
}
|
|
266
|
+
// No check defined at all for this task.
|
|
138
267
|
return text([
|
|
139
|
-
`
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
""
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
: "Fix these, re-run the checks, and call precode.verify again. Do not call precode.next_task until this passes.",
|
|
149
|
-
].join("\n"));
|
|
268
|
+
`HOLD. Task #${task.index} has no check defined in .precode/checks.json, so it cannot be verified.`,
|
|
269
|
+
"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.",
|
|
270
|
+
reportedNote,
|
|
271
|
+
]
|
|
272
|
+
.filter(Boolean)
|
|
273
|
+
.join("\n"));
|
|
274
|
+
}
|
|
275
|
+
finally {
|
|
276
|
+
await s.releaseLock();
|
|
150
277
|
}
|
|
151
|
-
await s.markTaskDone(task.index);
|
|
152
|
-
await recordTelemetry({
|
|
153
|
-
eventName: "mcp_verify_pass",
|
|
154
|
-
root: ROOT,
|
|
155
|
-
task: taskTelemetry(task),
|
|
156
|
-
metadata: { checkCount: checkResults.length },
|
|
157
|
-
});
|
|
158
|
-
await s.appendImplementation([
|
|
159
|
-
`## Task #${task.index}: ${task.text}`,
|
|
160
|
-
taskLabel ? `Host task: ${taskLabel}` : "",
|
|
161
|
-
`Verified: ${checkResults.map((c) => `${c.name} ✓`).join(", ")}`,
|
|
162
|
-
notes ? `Notes: ${notes}` : "",
|
|
163
|
-
]
|
|
164
|
-
.filter(Boolean)
|
|
165
|
-
.join("\n"));
|
|
166
|
-
const next = await s.nextOpenTask();
|
|
167
|
-
return text(next
|
|
168
|
-
? `PASS. Task #${task.index} marked done. Call precode.next_task for the next step (#${next.index}).`
|
|
169
|
-
: `PASS. Task #${task.index} marked done. All tasks complete — call precode.finalize.`);
|
|
170
278
|
});
|
|
171
279
|
// --- Tool: record implementation (ledger) ---------------------------------
|
|
172
280
|
server.tool("precode.record_implementation", "Log what you built for a task and any undocumented decisions, into .precode/progress/IMPLEMENTATION.md.", {
|
|
@@ -210,17 +318,29 @@ server.tool("precode.finalize", "Write the handoff: confirm all tasks done and r
|
|
|
210
318
|
if (!s)
|
|
211
319
|
return NO_STORE;
|
|
212
320
|
const tasks = await s.tasks();
|
|
213
|
-
const
|
|
321
|
+
const deferred = await s.deferredEntries();
|
|
322
|
+
const deferredSet = new Set(deferred.map((e) => e.index));
|
|
323
|
+
const open = tasks.filter((t) => !t.done && !deferredSet.has(t.index));
|
|
324
|
+
// Manual gates the human still owns, regenerated from checks.json for done tasks.
|
|
325
|
+
const manualGates = [];
|
|
326
|
+
for (const t of tasks.filter((t) => t.done)) {
|
|
327
|
+
const manual = (await s.checksForTask(t.index)).filter((c) => checkKind(c) === "manual");
|
|
328
|
+
for (const c of manual)
|
|
329
|
+
manualGates.push(`Task #${t.index}: ${c.name}`);
|
|
330
|
+
}
|
|
214
331
|
await recordTelemetry({
|
|
215
332
|
eventName: "mcp_finalize",
|
|
216
333
|
root: ROOT,
|
|
217
334
|
metadata: {
|
|
218
335
|
taskCount: tasks.length,
|
|
219
336
|
openTaskCount: open.length,
|
|
337
|
+
deferredCount: deferred.length,
|
|
338
|
+
manualGateCount: manualGates.length,
|
|
220
339
|
userTodoCount: userTodos.length,
|
|
221
340
|
unresolvedCount: unresolved?.length ?? 0,
|
|
222
341
|
},
|
|
223
342
|
});
|
|
343
|
+
const taskText = (i) => tasks.find((t) => t.index === i)?.text ?? "(unknown)";
|
|
224
344
|
const body = [
|
|
225
345
|
"# What you still need to do",
|
|
226
346
|
"",
|
|
@@ -229,6 +349,14 @@ server.tool("precode.finalize", "Write the handoff: confirm all tasks done and r
|
|
|
229
349
|
"## Your steps",
|
|
230
350
|
...(userTodos.length ? userTodos.map((t) => `- [ ] ${t}`) : ["- [ ] (none reported)"]),
|
|
231
351
|
"",
|
|
352
|
+
"## Manual verification still required",
|
|
353
|
+
...(manualGates.length ? manualGates.map((m) => `- [ ] ${m}`) : ["- (none)"]),
|
|
354
|
+
"",
|
|
355
|
+
"## Deferred tasks (could not be auto-verified here)",
|
|
356
|
+
...(deferred.length
|
|
357
|
+
? deferred.map((e) => `- [ ] Task #${e.index} (${taskText(e.index)}) — ${e.reason}`)
|
|
358
|
+
: ["- (none)"]),
|
|
359
|
+
"",
|
|
232
360
|
"## Not yet satisfied by the build",
|
|
233
361
|
...(unresolved?.length
|
|
234
362
|
? unresolved.map((u) => `- ${u}`)
|
|
@@ -238,9 +366,42 @@ server.tool("precode.finalize", "Write the handoff: confirm all tasks done and r
|
|
|
238
366
|
"",
|
|
239
367
|
].join("\n");
|
|
240
368
|
await s.writeTodo(body);
|
|
241
|
-
|
|
369
|
+
const doneCount = tasks.filter((t) => t.done).length;
|
|
370
|
+
const flags = [
|
|
371
|
+
open.length ? `${open.length} task(s) still OPEN` : "",
|
|
372
|
+
deferred.length ? `${deferred.length} deferred` : "",
|
|
373
|
+
manualGates.length ? `${manualGates.length} manual gate(s) for you` : "",
|
|
374
|
+
].filter(Boolean);
|
|
375
|
+
return text(`Finalized. ${doneCount}/${tasks.length} tasks done. ` +
|
|
242
376
|
`Wrote .precode/progress/TODO_FOR_YOU.md and IMPLEMENTATION.md. ` +
|
|
243
|
-
(
|
|
377
|
+
(flags.length
|
|
378
|
+
? `Surfaced honestly in the TODO: ${flags.join(", ")}.`
|
|
379
|
+
: "All tasks verified, nothing outstanding."));
|
|
380
|
+
});
|
|
381
|
+
// --- Tool: defer a task that genuinely cannot be auto-verified here --------
|
|
382
|
+
server.tool("precode.defer_task", "Honest escape hatch for the CURRENT open task when an auto check cannot pass in this environment (needs a secret, a real deploy, a paid/external service). Records the reason, skips the task in the loop, and surfaces it in TODO_FOR_YOU.md. Use this instead of looping forever — never to dodge a real, fixable failure.", {
|
|
383
|
+
reason: z
|
|
384
|
+
.string()
|
|
385
|
+
.min(8)
|
|
386
|
+
.describe("Why it cannot be auto-verified here. Be specific and honest."),
|
|
387
|
+
}, async ({ reason }) => {
|
|
388
|
+
const s = await store();
|
|
389
|
+
if (!s)
|
|
390
|
+
return NO_STORE;
|
|
391
|
+
const task = await s.nextOpenTask();
|
|
392
|
+
if (!task)
|
|
393
|
+
return text("No open task to defer. Run precode.finalize.");
|
|
394
|
+
await s.deferTask(task.index, reason);
|
|
395
|
+
await recordTelemetry({
|
|
396
|
+
eventName: "mcp_defer_task",
|
|
397
|
+
root: ROOT,
|
|
398
|
+
task: taskTelemetry(task),
|
|
399
|
+
});
|
|
400
|
+
const next = await s.nextOpenTask();
|
|
401
|
+
return text(`Deferred task #${task.index} and logged it to TODO_FOR_YOU.md: ${reason}\n` +
|
|
402
|
+
(next
|
|
403
|
+
? `Next open task is #${next.index}. Call precode.next_task.`
|
|
404
|
+
: "No more open tasks — call precode.finalize."));
|
|
244
405
|
});
|
|
245
406
|
// --- Tool: adopt any spec -------------------------------------------------
|
|
246
407
|
server.tool("precode.adopt_spec", "If there is no .precode/ here, map a plain SPEC.md / .specify / .spec-workflow / docs folder into a minimal .precode/ package so this loop works with ANY spec.", { specPath: z.string().optional().describe("Optional path hint to the spec file or folder.") }, async ({ specPath }) => {
|
package/dist/store.js
CHANGED
|
@@ -65,7 +65,109 @@ export class PrecodeStore {
|
|
|
65
65
|
return tasks;
|
|
66
66
|
}
|
|
67
67
|
async nextOpenTask() {
|
|
68
|
-
|
|
68
|
+
const deferred = new Set(await this.deferredIndices());
|
|
69
|
+
return ((await this.tasks()).find((t) => !t.done && !deferred.has(t.index)) ?? null);
|
|
70
|
+
}
|
|
71
|
+
/** Declared, executable checks (`.precode/checks.json`). [] if absent/malformed. */
|
|
72
|
+
async checks() {
|
|
73
|
+
const raw = await this.read("checks.json");
|
|
74
|
+
if (!raw)
|
|
75
|
+
return [];
|
|
76
|
+
let parsed;
|
|
77
|
+
try {
|
|
78
|
+
parsed = JSON.parse(raw);
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
return [];
|
|
82
|
+
}
|
|
83
|
+
const arr = Array.isArray(parsed)
|
|
84
|
+
? parsed
|
|
85
|
+
: Array.isArray(parsed.checks)
|
|
86
|
+
? parsed.checks
|
|
87
|
+
: [];
|
|
88
|
+
const specs = [];
|
|
89
|
+
arr.forEach((item, i) => {
|
|
90
|
+
if (!item || typeof item !== "object")
|
|
91
|
+
return;
|
|
92
|
+
const o = item;
|
|
93
|
+
const name = typeof o.name === "string" ? o.name : `check ${i + 1}`;
|
|
94
|
+
specs.push({
|
|
95
|
+
id: typeof o.id === "string" ? o.id : `c${i + 1}`,
|
|
96
|
+
name,
|
|
97
|
+
cmd: typeof o.cmd === "string" ? o.cmd : undefined,
|
|
98
|
+
kind: o.kind === "manual" ? "manual" : o.kind === "auto" ? "auto" : undefined,
|
|
99
|
+
expect: typeof o.expect === "string" ? o.expect : undefined,
|
|
100
|
+
timeoutMs: typeof o.timeoutMs === "number" ? o.timeoutMs : undefined,
|
|
101
|
+
taskIndex: typeof o.taskIndex === "number" ? o.taskIndex : undefined,
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
return specs;
|
|
105
|
+
}
|
|
106
|
+
/** Checks that apply to a task: global (no taskIndex) + that task's own. */
|
|
107
|
+
async checksForTask(taskIndex) {
|
|
108
|
+
return (await this.checks()).filter((c) => c.taskIndex === undefined || c.taskIndex === taskIndex);
|
|
109
|
+
}
|
|
110
|
+
// --- Deferred tasks: an honest escape from a check that cannot pass --------
|
|
111
|
+
async deferredEntries() {
|
|
112
|
+
const raw = await this.read("progress/deferred.json");
|
|
113
|
+
if (!raw)
|
|
114
|
+
return [];
|
|
115
|
+
try {
|
|
116
|
+
const v = JSON.parse(raw);
|
|
117
|
+
if (!Array.isArray(v))
|
|
118
|
+
return [];
|
|
119
|
+
return v
|
|
120
|
+
.filter((e) => e && typeof e === "object" && typeof e.index === "number")
|
|
121
|
+
.map((e) => ({ index: e.index, reason: String(e.reason ?? "") }));
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
return [];
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
async deferredIndices() {
|
|
128
|
+
return (await this.deferredEntries()).map((e) => e.index);
|
|
129
|
+
}
|
|
130
|
+
async deferTask(taskIndex, reason) {
|
|
131
|
+
await fs.mkdir(this.file("progress"), { recursive: true });
|
|
132
|
+
const cur = await this.deferredEntries();
|
|
133
|
+
if (!cur.some((e) => e.index === taskIndex)) {
|
|
134
|
+
cur.push({ index: taskIndex, reason });
|
|
135
|
+
}
|
|
136
|
+
await fs.writeFile(this.file("progress/deferred.json"), JSON.stringify(cur, null, 2), "utf8");
|
|
137
|
+
const task = (await this.tasks()).find((t) => t.index === taskIndex);
|
|
138
|
+
await this.appendTodo([
|
|
139
|
+
`## Task #${taskIndex} deferred — needs you`,
|
|
140
|
+
"",
|
|
141
|
+
`Task: ${task?.text ?? "(unknown)"}`,
|
|
142
|
+
`Reason it could not be auto-verified: ${reason}`,
|
|
143
|
+
].join("\n"));
|
|
144
|
+
}
|
|
145
|
+
// --- Verify lock: stop two concurrent verifies racing on tasks.md ----------
|
|
146
|
+
async acquireLock(staleMs = 5 * 60 * 1000) {
|
|
147
|
+
const lock = this.file(".lock");
|
|
148
|
+
try {
|
|
149
|
+
const handle = await fs.open(lock, "wx");
|
|
150
|
+
await handle.writeFile(String(Date.now()));
|
|
151
|
+
await handle.close();
|
|
152
|
+
return true;
|
|
153
|
+
}
|
|
154
|
+
catch {
|
|
155
|
+
// Lock exists — steal it only if it is stale (crashed prior run).
|
|
156
|
+
try {
|
|
157
|
+
const stat = await fs.stat(lock);
|
|
158
|
+
if (Date.now() - stat.mtimeMs > staleMs) {
|
|
159
|
+
await fs.writeFile(lock, String(Date.now()), "utf8");
|
|
160
|
+
return true;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
catch {
|
|
164
|
+
/* race: gone between checks — treat as not acquired */
|
|
165
|
+
}
|
|
166
|
+
return false;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
async releaseLock() {
|
|
170
|
+
await fs.rm(this.file(".lock"), { force: true });
|
|
69
171
|
}
|
|
70
172
|
/** Hard done-gate: only flips a task to [x]. Caller must verify checks first. */
|
|
71
173
|
async markTaskDone(taskIndex) {
|
|
@@ -91,10 +193,14 @@ export class PrecodeStore {
|
|
|
91
193
|
const project = manifest?.project ?? {};
|
|
92
194
|
const tasks = await this.tasks();
|
|
93
195
|
const done = tasks.filter((t) => t.done).length;
|
|
196
|
+
const deferred = await this.deferredIndices();
|
|
197
|
+
const docs = await this.listDocFiles();
|
|
94
198
|
return [
|
|
95
199
|
`Project: ${project.name ?? "(unknown)"} (${project.appType ?? "app"})`,
|
|
96
|
-
`Progress: ${done}/${tasks.length} tasks complete.`,
|
|
97
|
-
|
|
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.",
|
|
98
204
|
].join("\n");
|
|
99
205
|
}
|
|
100
206
|
async appendImplementation(entry) {
|
|
@@ -140,6 +246,12 @@ export class PrecodeStore {
|
|
|
140
246
|
await fs.mkdir(this.file("progress"), { recursive: true });
|
|
141
247
|
await fs.writeFile(this.file("progress/TODO_FOR_YOU.md"), content, "utf8");
|
|
142
248
|
}
|
|
249
|
+
async appendTodo(entry) {
|
|
250
|
+
await fs.mkdir(this.file("progress"), { recursive: true });
|
|
251
|
+
const cur = (await this.read("progress/TODO_FOR_YOU.md")) ??
|
|
252
|
+
"# TODO for you\n\nNo manual follow-up has been recorded yet.\n";
|
|
253
|
+
await fs.writeFile(this.file("progress/TODO_FOR_YOU.md"), `${cur.trimEnd()}\n\n${entry}\n`, "utf8");
|
|
254
|
+
}
|
|
143
255
|
async listDocFiles() {
|
|
144
256
|
try {
|
|
145
257
|
const files = await fs.readdir(this.file("docs"));
|
|
@@ -216,18 +328,35 @@ export async function adoptSpec(searchRoot, specPathHint) {
|
|
|
216
328
|
error: "No spec found. Provide a SPEC.md, a .specify/.spec-workflow folder, or a docs/ folder of markdown.",
|
|
217
329
|
};
|
|
218
330
|
}
|
|
219
|
-
// Derive tasks
|
|
220
|
-
|
|
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.
|
|
336
|
+
const checkboxes = [];
|
|
337
|
+
const bullets = [];
|
|
338
|
+
const headings = [];
|
|
221
339
|
for (const line of specBody.split("\n")) {
|
|
222
340
|
const cb = TASK_RE.exec(line);
|
|
223
341
|
if (cb) {
|
|
224
|
-
|
|
342
|
+
checkboxes.push(`- [ ] ${cb[3]}`);
|
|
343
|
+
continue;
|
|
344
|
+
}
|
|
345
|
+
const bullet = /^\s*(?:[-*]|\d+[.)])\s+(.*\S)\s*$/.exec(line);
|
|
346
|
+
if (bullet) {
|
|
347
|
+
if (bullets.length < 40)
|
|
348
|
+
bullets.push(`- [ ] ${bullet[1]}`);
|
|
225
349
|
continue;
|
|
226
350
|
}
|
|
227
351
|
const h = /^#{2,3}\s+(.*\S)\s*$/.exec(line);
|
|
228
|
-
if (h &&
|
|
229
|
-
|
|
352
|
+
if (h && headings.length < 40)
|
|
353
|
+
headings.push(`- [ ] Implement: ${h[1]}`);
|
|
230
354
|
}
|
|
355
|
+
const tasks = checkboxes.length
|
|
356
|
+
? checkboxes
|
|
357
|
+
: bullets.length
|
|
358
|
+
? bullets
|
|
359
|
+
: headings;
|
|
231
360
|
if (!tasks.length)
|
|
232
361
|
tasks.push("- [ ] Implement the full spec, then verify.");
|
|
233
362
|
await fs.mkdir(path.join(dir, "progress"), { recursive: true });
|
|
@@ -254,5 +383,24 @@ export async function adoptSpec(searchRoot, specPathHint) {
|
|
|
254
383
|
"",
|
|
255
384
|
].join("\n"), "utf8");
|
|
256
385
|
await fs.writeFile(path.join(dir, "manifest.json"), JSON.stringify({ project: { name: "Adopted spec", appType: "app" }, adoptedFrom: found }, null, 2), "utf8");
|
|
386
|
+
// Starter checks.json. Commands are commented placeholders the user edits to
|
|
387
|
+
// their real build — until then the gate runs nothing auto and stamps passes
|
|
388
|
+
// UNVERIFIED, so it never silently claims a verified build.
|
|
389
|
+
await fs.writeFile(path.join(dir, "checks.json"), JSON.stringify({
|
|
390
|
+
$comment: "Each 'auto' check is RUN by the MCP from the project root; the task is " +
|
|
391
|
+
"done only when its real exit code is 0. Replace cmd values with your " +
|
|
392
|
+
"build. 'manual' checks are surfaced for human verification, never auto-passed.",
|
|
393
|
+
checks: [
|
|
394
|
+
{ id: "typecheck", name: "Type-check", kind: "auto", cmd: "" },
|
|
395
|
+
{ id: "lint", name: "Lint", kind: "auto", cmd: "" },
|
|
396
|
+
{ id: "build", name: "Production build", kind: "auto", cmd: "" },
|
|
397
|
+
{ id: "test", name: "Tests", kind: "auto", cmd: "" },
|
|
398
|
+
{
|
|
399
|
+
id: "secrets",
|
|
400
|
+
name: "Secrets & deploy env verified in real accounts",
|
|
401
|
+
kind: "manual",
|
|
402
|
+
},
|
|
403
|
+
],
|
|
404
|
+
}, null, 2), "utf8");
|
|
257
405
|
return { ok: true, dir };
|
|
258
406
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@precode/mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
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",
|
|
@@ -17,8 +17,9 @@
|
|
|
17
17
|
"dev": "tsc --watch",
|
|
18
18
|
"start": "node dist/index.js",
|
|
19
19
|
"test:stdio": "npm run build && node scripts/stdio-smoke.mjs",
|
|
20
|
+
"test": "npm run build && node test/run.mjs",
|
|
20
21
|
"typecheck": "tsc --noEmit",
|
|
21
|
-
"prepublishOnly": "npm run test
|
|
22
|
+
"prepublishOnly": "npm run build && node scripts/stdio-smoke.mjs && node test/run.mjs"
|
|
22
23
|
},
|
|
23
24
|
"keywords": [
|
|
24
25
|
"mcp",
|