cortad 0.2.1 → 0.2.2

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/lib/cli.mjs CHANGED
@@ -22,11 +22,11 @@ export async function main(argv, { root = process.cwd(), env = process.env, stdo
22
22
  if (!trusted) { stderr.write(`cortad refusing: ${origin.host} is not Cortad.\n`); return 1; }
23
23
  const api = `${origin.origin}/api`;
24
24
  const project = projectOf(root);
25
- const token = readToken(project);
26
25
  const [command, ...rest] = argv;
27
26
  const mcp = command === "mcp";
28
27
  const children = [];
29
- const verbs = makeVerbs({ api, token, ensureRunner: () => ensureRunner({ root, project, env, keep: mcp, children, say: (line) => stderr.write(`cortad ${line}\n`) }) });
28
+ // The key is read at every call: the MCP process outlives a connect that writes it after the process started.
29
+ const verbs = makeVerbs({ api, token: () => readToken(project), ensureRunner: () => ensureRunner({ root, project, env, keep: mcp, children, say: (line) => stderr.write(`cortad ${line}\n`) }) });
30
30
 
31
31
  if (mcp) {
32
32
  const code = await serveMcp({ verbs, version: VERSION, log: (line) => stderr.write(`${line}\n`) });
package/lib/mcp.mjs CHANGED
@@ -5,7 +5,7 @@
5
5
  export const PROTOCOL = "2025-06-18";
6
6
 
7
7
  export const INSTRUCTIONS = [
8
- "Cortad is an independent behavior test lab for the AI app in this repository. It runs simulated users through the app on this machine and grades every reply with about 100 checks. It is already connected; never run `npx cortad <code>` again.",
8
+ "Cortad is an independent behavior test lab for the AI app in this repository. It runs simulated users through the app on this machine and grades every reply with about 100 checks. It is already connected; never run `npx cortad <code>` again. The first run starts by itself after a connect; the skill says what to tell the person then.",
9
9
  "The loop: status, then run only when the person asks (the first run is free; after that run returns a checkout link, show it in one sentence and wait), poll run_status every 30 seconds and stay quiet unless the count moved, findings, fix one finding at a time in the repo, verify <id>, read the move.",
10
10
  "Rules: a move inside the interval is not a fix. Never change or remove a case to move a number; never make the app recognise test traffic; if a check reads wrong, call dispute with why. Cases, checks, seeds and the holdout are not yours to edit, and Cortad refuses it anyway.",
11
11
  ].join("\n\n");
package/lib/verbs.mjs CHANGED
@@ -6,12 +6,13 @@ const NOT_CONNECTED = "This project is not connected to Cortad yet. Ask the pers
6
6
 
7
7
  export function makeVerbs({ api, token, fetchImpl = fetch, ensureRunner = async () => ({ ok: true }) }) {
8
8
  const call = async (method, path, body) => {
9
- if (!token) return { status: 0, ok: false, data: { error: NOT_CONNECTED } };
9
+ const key = typeof token === "function" ? token() : token;
10
+ if (!key) return { status: 0, ok: false, data: { error: NOT_CONNECTED } };
10
11
  let res;
11
12
  try {
12
13
  res = await fetchImpl(`${api}${path}`, {
13
14
  method,
14
- headers: { authorization: `Bearer ${token}`, ...(body === undefined ? {} : { "content-type": "application/json" }) },
15
+ headers: { authorization: `Bearer ${key}`, ...(body === undefined ? {} : { "content-type": "application/json" }) },
15
16
  body: body === undefined ? undefined : JSON.stringify(body),
16
17
  signal: AbortSignal.timeout(60_000),
17
18
  });
@@ -108,6 +109,8 @@ export function runText(d) {
108
109
  const tail = [];
109
110
  if (d.finished && typeof d.score === "number") tail.push(`Score ${d.score} of 100.`);
110
111
  if (d.finished && typeof d.findings === "number") tail.push(d.findings ? `${plural(d.findings, "finding")}; call findings.` : "No findings stood out.");
112
+ if (d.stopped) tail.push(`Stopped after ${d.stopped.after} of ${d.of}: ${d.stopped.why}${d.stopped.side === "theirs" ? " (their side)" : d.stopped.side === "ours" ? " (Cortad's side)" : ""}. ${d.stopped.fix}`);
113
+ for (const f of d.faults ?? []) tail.push(`${f.what} ${f.fix}`);
111
114
  if (d.error) tail.push(`Error: ${d.error}`);
112
115
  if (d.verify) tail.push(verifyText(d.verify));
113
116
  if (!d.finished) tail.push("Poll again in 30 seconds.");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cortad",
3
- "version": "0.2.1",
3
+ "version": "0.2.2",
4
4
  "description": "Connects the AI app on your machine to Cortad for test conversations, and gives your coding agent the MCP and skill to run them. No dependencies.",
5
5
  "bin": {
6
6
  "cortad": "local.mjs"
package/skill/SKILL.md CHANGED
@@ -6,9 +6,17 @@ Cortad is connected to this repository. Do not run `npx cortad <code>` again.
6
6
 
7
7
  If the `cortad` MCP tools are in your tool list, use them. Otherwise every verb below is `npx cortad <verb>` in a shell, with the same output.
8
8
 
9
+ ## Right after connect
10
+ The first run starts by itself: never call `run` for it. As soon as the command says "Go back to the browser", tell the person, in your own words, all of this:
11
+ - Cortad has started their app on this machine and is reading their code to write realistic users of it, with situations that move.
12
+ - It will play those users through the app and grade every reply against about 100 checks: their own rules, and what a good reply is.
13
+ - The report lands in the browser, with each finding at a file and line, and you can fix and verify from here.
14
+ - They should go back to the browser now and finish the two onboarding questions while it plays; you will report when it is done.
15
+ Then `status` every 30 seconds, quietly, until the latest run is finished. Report the score, the number of findings and the link.
16
+
9
17
  ## The loop
10
18
  1. `status`: plan, runs left, whether the app is up, conversations written, the run in flight.
11
- 2. `run`: only when the person asks. The first run is free. After that it returns a checkout link; show it in one sentence and wait for the person.
19
+ 2. `run`: only when the person asks. The first run started by itself and was free. After that `run` returns a checkout link; show it in one sentence and wait for the person.
12
20
  3. `run_status <jobId>` every 30 seconds; say nothing unless the count moved. When it finishes, say the score, how many findings, and the link.
13
21
  4. `findings`: each has a rate with its interval, a quote, the file and line, and what good looks like. Start from the worst rate.
14
22
  5. Fix ONE finding: the smallest change in the file it names. Then `verify <findingId>`.