cortad 0.3.0-rc.8 → 0.3.0-rc.9

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/text.mjs CHANGED
@@ -39,11 +39,28 @@ function planLine(p) {
39
39
  return parts.length ? `${p.name}: ${parts.join(", ")}.` : `Plan: ${p.name}.`;
40
40
  }
41
41
 
42
+ // The number with what it stands on: a 100 beside an app that stopped, or from five readings, read
43
+ // as a verdict to two agents. Under the 22-reading floor it is provisional and says so.
44
+ function scoreLine(d) {
45
+ const floor = has(d.decided) && d.decided < 22;
46
+ return `${floor ? "Provisional score" : "Score"} ${num(d.score)} of 100${interval(d.ci)}${floor ? `, from ${plural(d.decided, "decided reading")}: under the 22-reading floor, so not a verdict yet` : ""}.`;
47
+ }
48
+ const haltLine = (h) => `${h.side === "ours" ? "We lost your app" : "Your app stopped answering"} after ${plural(h.after, "turn")}${h.why ? `: ${clip(h.why, 200)}` : ""}. The score covers the turns before that.`;
49
+ const LAYER_WORDS = {
50
+ retrieval: "what the store returned behind the reply, not how the reply was written",
51
+ tools: "the tool call behind the reply",
52
+ memory: "what the app kept between turns",
53
+ application: "an error in the app around the model",
54
+ instructions: "the model against its own instructions",
55
+ upsell: "what the reply offered beyond the ask",
56
+ };
57
+
42
58
  function runLines(d, prefix = "") {
43
59
  const done = finished(d);
44
60
  const played = d.of ? `${num(d.played ?? 0)} of ${plural(d.of, "trial")} played` : "no trial played yet";
45
61
  const lines = [`${upper(`${prefix}${d.kind === "verify" ? "verify" : "run"}`)} ${d.jobId}: ${d.status === "succeeded" || !d.status ? (done ? "finished" : "running") : d.status}, ${played}.`];
46
- if (done && typeof d.score === "number") lines.push(`Score ${num(d.score)} of 100${interval(d.ci)}.`);
62
+ if (done && typeof d.score === "number") lines.push(scoreLine(d));
63
+ if (done && d.halted) lines.push(haltLine(d.halted));
47
64
  if (done && has(d.findings)) lines.push(`${plural(d.findings, "finding")}.`);
48
65
  const reads = readingsLine(d);
49
66
  if (reads) lines.push(reads);
@@ -58,7 +75,7 @@ function runLines(d, prefix = "") {
58
75
  function readingsLine(d) {
59
76
  if (!has(d.readings) && !has(d.questionsAsked)) return null;
60
77
  const r = has(d.readings) ? plural(d.readings, "reading") : null;
61
- const q = has(d.questionsAsked) ? plural(d.questionsAsked, "question") : null;
78
+ const q = has(d.questionsAsked) ? `${plural(d.questionsAsked, "question")}${has(d.questionsOf) && d.questionsOf > d.questionsAsked ? ` (${num(d.questionsOf - d.questionsAsked)} of the ${num(d.questionsOf)} in the set never came up)` : ""}` : null;
62
79
  const split = [has(d.decided) && `${num(d.decided)} decided`, has(d.unclear) && `${num(d.unclear)} unclear`].filter(Boolean).join(", ");
63
80
  return `${r && q ? `${r} of ${q}` : r ?? `${q} asked`}${split ? `: ${split}` : ""}.`;
64
81
  }
@@ -154,7 +171,8 @@ export const findingsText = (d, page = 1) => pageOf(findingsPages(d), page, (n)
154
171
 
155
172
  function findingsHead(d) {
156
173
  const lines = [`Run ${d.runId}: ${plural(d.findings?.length ?? 0, "finding")}.`];
157
- if (typeof d.score === "number") lines.push(`Score ${num(d.score)} of 100${interval(d.ci)}.`);
174
+ if (typeof d.score === "number") lines.push(scoreLine(d));
175
+ if (d.halted) lines.push(haltLine(d.halted));
158
176
  const reads = readingsLine(d);
159
177
  if (reads) lines.push(reads);
160
178
  if (d.baseRunId) lines.push(`Compared with run ${d.baseRunId}.`);
@@ -201,7 +219,9 @@ function findingBlock(f, placed) {
201
219
  if (!placed && f.file) lines.push(` At ${at(f.file, f.line)}`);
202
220
  if (f.criteria) lines.push(` Criteria: ${clip(f.criteria, 600)}`);
203
221
  if (f.door) lines.push(` Endpoint: ${f.door}`);
222
+ else if (f.doors?.length) lines.push(` Endpoints: ${f.doors.join(", ")}`);
204
223
  if (f.where) lines.push(` Situation: ${f.where}`);
224
+ if (f.layer && LAYER_WORDS[f.layer]) lines.push(` Layer: ${f.layer}, ${LAYER_WORDS[f.layer]}.`);
205
225
  const r = f.rate;
206
226
  // The break count, whichever way the question is phrased: "held in 21 of 33" under a question
207
227
  // that asks whether the passages MISS the point left an agent unable to tell pass from fail.
package/local.mjs CHANGED
@@ -687,14 +687,17 @@ async function installOnce(said) {
687
687
 
688
688
  // The port their start command names, moved when something else already holds it: two apps on one
689
689
  // laptop both said --port 8000, and the second attached to the first's server as its own.
690
+ // Ports this program tried and lost: two connects on one machine can pick the same free port in
691
+ // the same second, and the one that binds second sees "address already in use".
692
+ const lost = new Set();
690
693
  async function freePortAbove(port) {
691
- for (let next = port + 1; next < port + 30; next++) if (!(await listenerOn(next))) return next;
694
+ for (let next = port + 1; next < port + 30; next++) if (!lost.has(next) && !(await listenerOn(next))) return next;
692
695
  return null;
693
696
  }
694
697
  async function freed(cmd, lifted) {
695
698
  const m = /(--port[= ]|-p |\bPORT=)(\d{4,5})\b/.exec(cmd);
696
699
  const named = m ? Number(m[2]) : Number(lifted.PORT) || 0;
697
- if (!named || !(await listenerOn(named))) return { cmd, lifted };
700
+ if (!named || (!lost.has(named) && !(await listenerOn(named)))) return { cmd, lifted };
698
701
  const port = await freePortAbove(named);
699
702
  if (!port) return { cmd, lifted };
700
703
  say(`port ${named} is taken on this machine, so your app starts on ${port}`);
@@ -723,6 +726,10 @@ async function start(waitMs) {
723
726
  // that holds the port is theirs and already running, which startApp turns into attaching to it.
724
727
  if (/app crashed - waiting for file changes|waiting for (?:file )?changes before restart|Failed running|EADDRINUSE|address already in use/i.test(seen)) {
725
728
  await stopApp(mine.pid);
729
+ // The port this start named was taken between the check and the bind: the next free one, at
730
+ // most three times, before the failure is theirs to read.
731
+ const named = /(?:--port[= ]|-p |\bPORT=)(\d{4,5})\b/.exec(cmd)?.[1] ?? lifted.PORT;
732
+ if (/EADDRINUSE|address already in use/i.test(seen) && named && lost.size < 3) { lost.add(Number(named)); return start(waitMs); }
726
733
  return { port: null, exited: 1, tail: tail() };
727
734
  }
728
735
  // The port is what the app's own process group listens on. Never a guess: a developer's
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cortad",
3
- "version": "0.3.0-rc.8",
3
+ "version": "0.3.0-rc.9",
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"