cortad 0.3.1-rc.10 → 0.3.1-rc.12

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.
@@ -0,0 +1,35 @@
1
+ import { at, clip, plural } from "./words.mjs";
2
+
3
+ // The replies that did not count and the doors that were not sent to, said before the score: one
4
+ // sentence whose parts add up to the count, then the request behind each part that has one. A count
5
+ // on its own ("17 of 33 replies failed") left resumeforge's agent asking what was attempted, whose it
6
+ // was, and how to reproduce it.
7
+
8
+ // A row the run held back names a door and why, or, when no door could take it, only the reason.
9
+ export const heldBackLine = (h) => h.why === "not testable here"
10
+ ? `Held back: ${plural(h.trials, "trial")} cannot be played here: ${clip(h.surface, 200).replace(/\.$/, "")}.`
11
+ : `Held back: ${plural(h.trials, "trial")} on ${h.surface}${h.why ? `, ${clip(h.why, 200)}` : ""}.`;
12
+
13
+ const HELD_SHOWN = 6;
14
+
15
+ export function heldLines(held) {
16
+ const rows = (held ?? []).filter((h) => h.trials > 0);
17
+ const lines = rows.slice(0, HELD_SHOWN).map(heldBackLine);
18
+ if (rows.length > HELD_SHOWN) lines.push(`Held back: ${plural(rows.slice(HELD_SHOWN).reduce((n, h) => n + h.trials, 0), "more trial")} for ${plural(rows.length - HELD_SHOWN, "other reason")}.`);
19
+ return lines;
20
+ }
21
+
22
+ export function failedLines(f) {
23
+ if (!f) return [];
24
+ const lines = f.said ? [f.said] : [];
25
+ for (const row of f.rows ?? []) {
26
+ const sent = row.curl ?? row.request;
27
+ if (!sent && !row.verify) continue;
28
+ const [path, line] = (row.at ?? "").split(/:(?=\d+$)/);
29
+ const where = row.at ? `at ${at(path, Number(line))}` : `from ${row.door}`;
30
+ if (sent) lines.push(` Request for the ${row.count} ${where}: ${clip(sent, 900)}`);
31
+ if (sent && row.response) lines.push(` Answered: ${clip(row.response.replace(/\s+/g, " "), 300)}`);
32
+ if (row.verify) lines.push(sent ? ` verify ${row.verify} replays them.` : ` verify ${row.verify} replays the ${row.count} ${where}.`);
33
+ }
34
+ return [...lines, ...heldLines((f.held ?? []).map((h) => ({ surface: h.door, trials: h.trials, why: h.why })))];
35
+ }
package/lib/health.mjs ADDED
@@ -0,0 +1,53 @@
1
+ // Whether the app a run is using is alive, read the way a person would: is its process still there,
2
+ // does anything still hold its port, and only then does it answer. An app slow to answer because it
3
+ // is answering our own requests is working, not dead: resumeforge held sixteen of them on a database
4
+ // pool of five, missed two probes of 2.5 seconds, and was restarted in the middle of the run.
5
+ import { connect } from "node:net";
6
+
7
+ const FLOOR_MS = 10_000;
8
+ const CAP_MS = 120_000;
9
+ const url = (host, port) => `http://${host === "::1" ? "[::1]" : host}:${port}/`;
10
+
11
+ // Opens the socket and sends nothing. Only a refusal means nobody holds the port: a listener too
12
+ // busy to accept still holds it.
13
+ export const portHeld = (host, port, ms = 3000) => new Promise((done) => {
14
+ const socket = connect({ host, port });
15
+ const end = (held) => { socket.destroy(); done(held); };
16
+ socket.once("connect", () => end(true));
17
+ socket.once("error", (e) => end(e.code !== "ECONNREFUSED"));
18
+ socket.setTimeout(ms, () => end(true));
19
+ });
20
+
21
+ // `gone`: the process this command started has exited. `inFlight`: the run's requests the app has
22
+ // not answered yet. The probe waits three times the app's last reply to it, never under ten seconds
23
+ // or over two minutes.
24
+ export function makeHealth({ host, gone, inFlight }) {
25
+ let took = 0;
26
+ const timeout = () => Math.min(CAP_MS, Math.max(FLOOR_MS, took * 3));
27
+ return {
28
+ timeout,
29
+ // "up", "busy" (slow while the run's requests are open: never restarted), or why it is down:
30
+ // "exited", "released" (nothing holds its port) or "quiet" (no answer with nothing of ours open).
31
+ async check(port) {
32
+ if (gone()) return "exited";
33
+ if (!(await portHeld(host(), port))) return "released";
34
+ const started = Date.now();
35
+ try {
36
+ const res = await fetch(url(host(), port), { method: "GET", signal: AbortSignal.timeout(timeout()), redirect: "manual" });
37
+ await res.body?.cancel();
38
+ took = Date.now() - started;
39
+ return "up";
40
+ } catch {
41
+ return inFlight() > 0 ? "busy" : "quiet";
42
+ }
43
+ },
44
+ };
45
+ }
46
+
47
+ // The runner's own line for a restart, in the person's words.
48
+ export function downLine(state, { port, downMs, crashed = false }) {
49
+ if (crashed) return "your app crashed, starting it again";
50
+ if (state === "exited") return "your app exited, starting it again";
51
+ if (state === "released") return `nothing is listening on port ${port} any more, starting your app again`;
52
+ return `your app has not answered for ${Math.round(downMs / 1000)} seconds while no test conversation was waiting on it, starting it again`;
53
+ }
@@ -964,7 +964,10 @@ def _install():
964
964
  # ran by it, where the read could only name the door. Its frames on this stack outside installed
965
965
  # packages, then, when the call runs in a task another task awaits (LangChain runs each step of a
966
966
  # chain as its own task), the awaiting task's. Relative to the folder the app was started in.
967
+ # Five lines: a provider class's stream, post and retry can be three lines of one file, and the
968
+ # line that says which path ran is past them.
967
969
  root = os.getcwd().replace(os.sep, "/").rstrip("/") + "/"
970
+ callers_max = 5
968
971
  installed = re.compile(r"/(?:site-packages|dist-packages|\.venv|venv|__pypackages__)/|/lib/python\d")
969
972
 
970
973
  def ours(frame, out):
@@ -973,7 +976,7 @@ def _install():
973
976
  at = "%s:%d" % (f[len(root):], frame.f_lineno)
974
977
  if at not in out:
975
978
  out.append(at)
976
- return len(out) >= 3
979
+ return len(out) >= callers_max
977
980
 
978
981
  # The task that waits on this one: its wake-up is among this one's callbacks, directly, or behind
979
982
  # the future an asyncio.gather callback closes over.
package/lib/replay.mjs CHANGED
@@ -124,7 +124,8 @@ const calledOf = (v) => {
124
124
  const passagesOf = (v) => (texts(v).length ? { passages: texts(v).slice(0, 6) } : {});
125
125
  // Which of the app's own lines made the call, nearest first, as "path:line".
126
126
  const AT = /^[^\n\r\t]{1,300}:\d{1,7}$/;
127
- const callerOf = (v) => { const caller = Array.isArray(v) ? v.filter((c) => typeof c === "string" && AT.test(c)).slice(0, 3) : []; return caller.length ? { caller } : {}; };
127
+ const CALLERS_MAX = 5;
128
+ const callerOf = (v) => { const caller = Array.isArray(v) ? v.filter((c) => typeof c === "string" && AT.test(c)).slice(0, CALLERS_MAX) : []; return caller.length ? { caller } : {}; };
128
129
  const meter = (() => {
129
130
  const rows = [];
130
131
  const deps = [];
package/lib/text.mjs CHANGED
@@ -1,3 +1,4 @@
1
+ import { failedLines, heldLines } from "./failed-text.mjs";
1
2
  import { readLines } from "./read-text.mjs";
2
3
  import { at, clip, has, num, PAGE_CHARS, pageOf, plural, SIDE, upper } from "./words.mjs";
3
4
 
@@ -59,15 +60,13 @@ function measuredLines(d) {
59
60
 
60
61
  function scoreText(d, of) {
61
62
  const floor = has(d.decided) && d.decided < 22;
62
- // The replies that failed stand beside the number: a 100 over the replies that answered, with 62 of
63
- // 89 error replies unsaid, read as a clean bill.
64
- const crashed = d.replies && d.replies.crashed > 0 ? `; ${num(d.replies.crashed)} of ${plural(d.replies.total, "reply", "replies")} were failures and are not in it` : "";
63
+ // The replies that did not count stand beside the number, in the same counts the lines above it
64
+ // give: a 100 over the replies that answered, with 62 of 89 error replies unsaid, read as a clean bill.
65
+ const f = d.failedReplies;
66
+ const crashed = f ? (f.notCounted > 0 ? `; the ${num(f.notCounted)} of ${plural(f.replies, "reply", "replies")} that did not count are not in it` : "")
67
+ : d.replies && d.replies.crashed > 0 ? `; ${num(d.replies.crashed)} of ${plural(d.replies.total, "reply", "replies")} were failures and are not in it` : "";
65
68
  return `${floor ? "provisional score" : "score"} ${num(d.score)} of 100${of ? ` on those ${num(d.questionsAsked)}` : ""}${interval(d.ci)}${floor ? `, from ${plural(d.decided, "decided reading")}: under the 22-reading floor, so not a verdict yet` : ""}${crashed}`;
66
69
  }
67
- // A row the run held back names a door and why, or, when no door could take it, only the reason.
68
- const heldBackLine = (h) => h.why === "not testable here"
69
- ? `Held back: ${plural(h.trials, "trial")} cannot be played here: ${clip(h.surface, 200).replace(/\.$/, "")}.`
70
- : `Held back: ${plural(h.trials, "trial")} on ${h.surface}${h.why ? `, ${clip(h.why, 200)}` : ""}.`;
71
70
  const haltLine = (h) => `${h.side === "ours" ? `Stopped ${SIDE.ours}` : "Your app stopped answering"} after ${plural(h.after, "turn")}${h.why ? `: ${clip(h.why, 240)}` : ""}. The score covers the turns before that.`;
72
71
  // The pace a slow app was played at, and the replies our own wait cut off: both said as ours. A
73
72
  // verify says the second in its own paired line.
@@ -86,12 +85,14 @@ function runLines(d, prefix = "") {
86
85
  const played = d.of ? `${num(d.played ?? 0)} of ${plural(d.of, "trial")} played` : "no trial played yet";
87
86
  const lines = [`${upper(`${prefix}${d.kind === "verify" ? "verify" : "run"}`)} ${d.jobId}: ${d.status === "succeeded" || !d.status ? (done ? "finished" : "running") : d.status}, ${played}.`];
88
87
  if (d.tested) lines.push(testedLine(d.tested));
88
+ // Each endpoint's first conversation, played alone before the others: whether it held and why not.
89
+ lines.push(...(d.first ?? []));
89
90
  // Between two counts, what the run is doing now: four minutes of "0 of 15" read as nothing happening.
90
91
  if (!done && d.now?.doing) lines.push(d.now.doing);
91
92
  lines.push(...waitLines(d));
92
93
  // A verify is a move, not a score: "100 of 100" on a replay whose run before decided nothing read
93
94
  // as a clean bill to bloom's agent, beside "zero verified fixes".
94
- if (done && d.kind !== "verify") lines.push(...measuredLines(d));
95
+ if (done && d.kind !== "verify") lines.push(...failedLines(d.failedReplies), ...measuredLines(d));
95
96
  if (done && d.blocked) lines.push(d.blocked);
96
97
  if (done && d.notCounted) lines.push(d.notCounted);
97
98
  if (done && d.halted) lines.push(haltLine(d.halted));
@@ -99,10 +100,9 @@ function runLines(d, prefix = "") {
99
100
  if (done && has(d.findings)) lines.push(`${plural(d.findings, "finding")}.`);
100
101
  const trials = trialsLine(d);
101
102
  if (trials) lines.push(trials);
102
- // Every row, or six and the rest in one line, so the rows always sum to the held-back count.
103
- const heldRows = (d.heldBack ?? []).filter((h) => h.trials > 0);
104
- for (const h of heldRows.slice(0, 6)) lines.push(heldBackLine(h));
105
- if (heldRows.length > 6) lines.push(`Held back: ${plural(heldRows.slice(6).reduce((n, h) => n + h.trials, 0), "more trial")} for ${plural(heldRows.length - 6, "other reason")}.`);
103
+ // Every row, or six and the rest in one line, so the rows always sum to the held-back count. A
104
+ // finished run says them with the replies that did not count, above the score.
105
+ if (!(done && d.failedReplies)) lines.push(...heldLines(d.heldBack));
106
106
  const reads = readingsLine(d);
107
107
  if (reads) lines.push(reads);
108
108
  if (d.stopped) lines.push(stoppedLine(d));
@@ -140,7 +140,8 @@ function readingsLine(d) {
140
140
  // before the first turn has no turn to name, and a reason written as a sentence keeps one full stop.
141
141
  function stoppedLine(d) {
142
142
  const s = d.stopped;
143
- const turn = s.after > 0 ? ` at turn ${s.after}` : "";
143
+ // A stop before the trials, on the endpoints' first conversations, names each one instead of a turn.
144
+ const turn = s.after > 0 && !s.first ? ` at turn ${s.after}` : "";
144
145
  const why = String(s.why).replace(/\.$/, "");
145
146
  const fix = s.fix ? ` ${s.fix}` : "";
146
147
  if (d.of && d.played < d.of) return `Stopped at ${num(d.played)} of ${plural(d.of, "trial")}${SIDE[s.side] ? `, ${SIDE[s.side]}` : ""}: ${why}${turn}.${fix}`;
@@ -222,8 +223,10 @@ export function testedLine(t) {
222
223
  return `App started ${started}${t.reloads ? " and reloads itself on save" : ""}; your last change was at ${clock(t.change.at)}.`;
223
224
  }
224
225
 
225
- export const waitingText = (p, now, limitMs) =>
226
- `Your app is still starting for the ${p.kind === "verify" ? "verify" : "run"}: ${num((now - Date.parse(p.startedAt)) / 1000)} of up to ${num(limitMs / 1000)} seconds.\nnext: run_status pending`;
226
+ // `said`: the status's own word on the app, which knows when it already answers and only its AI
227
+ // endpoint is still being found; bloom's agent read "still starting" beside "your app is answering".
228
+ export const waitingText = (p, now, limitMs, said) =>
229
+ `${said ? String(said).replace(/\.$/, "") : `Your app is still starting for the ${p.kind === "verify" ? "verify" : "run"}`}: ${num((now - Date.parse(p.startedAt)) / 1000)} of up to ${num(limitMs / 1000)} seconds.\nnext: run_status pending`;
227
230
 
228
231
  // A spent plan: what the last run found, what was fixed since, what the next run would play.
229
232
  export function refusedText(d) {
@@ -277,7 +280,7 @@ function findingsHead(d) {
277
280
  if (!d.runId) return ["No run has finished on this repository yet."];
278
281
  const lines = [`Run ${d.runId}: ${plural(d.findings?.length ?? 0, "finding")}.`];
279
282
  if (d.tested) lines.push(testedLine(d.tested));
280
- lines.push(...measuredLines(d));
283
+ lines.push(...failedLines(d.failedReplies), ...measuredLines(d));
281
284
  if (d.blocked) lines.push(d.blocked);
282
285
  if (d.halted) lines.push(haltLine(d.halted));
283
286
  lines.push(...waitLines(d));
@@ -338,7 +341,9 @@ function findingBlock(f, placed) {
338
341
  return lines.join("\n");
339
342
  }
340
343
  const lines = [`${f.id} ${f.asks}`];
341
- if (!placed && f.file) lines.push(` At ${at(f.file, f.line)}`);
344
+ // What the line is when it is not the check's own: the tool that ran, or only the model call.
345
+ if (f.addressed) lines.push(` At ${at(f.file, f.line)}: ${f.addressed}`);
346
+ else if (!placed && f.file) lines.push(` At ${at(f.file, f.line)}`);
342
347
  if (f.criteria) lines.push(` Criteria: ${clip(f.criteria, 600)}`);
343
348
  if (f.door) lines.push(` Endpoint: ${f.door}`);
344
349
  else if (f.doors?.length) lines.push(` Endpoints: ${f.doors.join(", ")}`);
package/lib/trace.cjs CHANGED
@@ -137,13 +137,15 @@ if (FILE) {
137
137
  // The file that registered a route: the first frame outside this hook and outside node_modules.
138
138
  const callerFile = () => { const f = stackFrames()[0]; return f && !f.file.startsWith("..") ? f.file : ""; };
139
139
  // Which of the app's own lines made a model call, nearest first: the run names the code path that
140
- // ran by it, where the read could only name the door.
140
+ // ran by it, where the read could only name the door. Five lines: a provider class's stream, post
141
+ // and retry can be three lines of one file, and the line that says which path ran is past them.
142
+ const CALLERS_MAX = 5;
141
143
  const callers = () => {
142
144
  const out = [];
143
145
  for (const f of stackFrames()) {
144
146
  const at = `${f.file}:${f.line}`;
145
147
  if (!f.file.startsWith("..") && !out.includes(at)) out.push(at);
146
- if (out.length === 3) break;
148
+ if (out.length === CALLERS_MAX) break;
147
149
  }
148
150
  return out.length ? out : undefined;
149
151
  };
package/lib/verbs.mjs CHANGED
@@ -109,7 +109,7 @@ export function makeVerbs({ api, token, fetchImpl = fetch, startApp, pending, ro
109
109
  if (p.jobId) return { id: p.jobId };
110
110
  if (p.error) return { text: `${p.error}\nnext: status`, isError: true };
111
111
  if (stale()) return { text: "The process starting your app for the run ended without a result.\nNothing ran.\nnext: status", isError: true };
112
- return { text: waitingText(p, now(), READY_MS) };
112
+ return { text: waitingText(p, now(), READY_MS, latest.data.app?.state === "starting" && latest.data.app.port ? latest.data.app.said : null) };
113
113
  };
114
114
 
115
115
  const runStatus = async ({ jobId } = {}) => {
package/local.mjs CHANGED
@@ -34,6 +34,7 @@ import { listingUrl } from "./lib/listing.mjs";
34
34
  import { installPlan, missingDependency, startPlan, workspaces } from "./lib/start.mjs";
35
35
  import { openSwitches } from "./lib/switches.mjs";
36
36
  import { keepData } from "./lib/data.mjs";
37
+ import { downLine, makeHealth } from "./lib/health.mjs";
37
38
  import { markRun, writesDirOf } from "./lib/writes.mjs";
38
39
 
39
40
  const argv = process.argv.slice(2);
@@ -275,6 +276,9 @@ let appGone = false;
275
276
  let lastSaid = "";
276
277
  let lastOutputAt = Date.now();
277
278
  let forgetTold = null;
279
+ // The run's requests your app has not answered yet: a slow health probe while any are open is our
280
+ // load on it, not a hang.
281
+ let answering = 0;
278
282
  const mask = (text) => { let s = String(text ?? ""); for (const v of secrets) s = s.split(v).join("[masked]"); return s; };
279
283
 
280
284
  // ---- the wire
@@ -405,12 +409,13 @@ async function verb(job) {
405
409
  const said = Object.fromEntries([...res.headers].filter(([k]) => !/^set-cookie2?$/i.test(k)).map(([k, v]) => [k, mask(v)]));
406
410
  return { status: res.status, headers: said, body: mask(buf.subarray(0, LIMIT).toString("utf8")), truncated: buf.length > LIMIT };
407
411
  };
408
- try { return await ask(); }
412
+ const counted = async () => { answering += 1; try { return await ask(); } finally { answering -= 1; } };
413
+ try { return await counted(); }
409
414
  catch (e) {
410
415
  const code = String(e.cause?.code ?? e.code ?? "");
411
416
  if (!/ECONNREFUSED|ECONNRESET|EPIPE|UND_ERR_SOCKET/.test(code)) return { error: `nothing answered at port ${port}: ${code || e.message}` };
412
417
  for (let i = 0; i < 45 && !(await answers(port)); i++) await new Promise((r) => setTimeout(r, 1000));
413
- try { return { ...(await ask()), heldForRestart: true }; }
418
+ try { return { ...(await counted()), heldForRestart: true }; }
414
419
  catch (again) { return { error: `nothing answered at port ${port}: ${String(again.cause?.code ?? again.message)}` }; }
415
420
  }
416
421
  }
@@ -854,6 +859,7 @@ async function restartApp() {
854
859
  async function restartNow() {
855
860
  if (!launched || !child) return { error: "You started this app yourself, so restart it in your own terminal. Most dev servers reload on save." };
856
861
  const port = app.port;
862
+ say("starting your app again so it runs the code you saved, since it does not reload by itself");
857
863
  await stopApp(child.pid);
858
864
  // Until the port is free, not merely silent: a listener still closing does not answer and still
859
865
  // holds the bind, and the restarted app was moved to the next port while the run kept knocking on
@@ -1073,26 +1079,30 @@ async function appLife() {
1073
1079
  let toldDown = false;
1074
1080
  let appTold = true;
1075
1081
  forgetTold = () => { appTold = false; };
1082
+ const health = makeHealth({ host: () => appHost, gone: () => Boolean(launched && appGone), inFlight: () => answering });
1076
1083
  for (let up = true; !closing;) {
1077
1084
  await new Promise((r) => setTimeout(r, 2000));
1078
1085
  if (closing || restarting) continue;
1079
- const now = await answers(app.port);
1080
- if (now) {
1086
+ const state = await health.check(app.port);
1087
+ if (state === "up") {
1081
1088
  downSince = 0; toldDown = false;
1082
1089
  // Said until it is heard. Said once, it was lost when their app came back while the network
1083
1090
  // was down, and the screen went on showing an app that had stopped while it answered turns.
1084
1091
  if (!up || !appTold) { up = true; appTold = Boolean((await announce().catch(() => null))?.ok); }
1085
1092
  continue;
1086
1093
  }
1094
+ // Running, holding its port, and slow because it is answering the run's own requests: working,
1095
+ // not dead. Nothing is said and nothing restarts, and a silence after it is timed from here.
1096
+ if (state === "busy") { if (!up) downSince = Date.now(); continue; }
1087
1097
  if (up) { up = false; downSince = Date.now(); }
1088
1098
  appTold = false;
1089
1099
  const downFor = Date.now() - downSince;
1090
- // Not answering. A dev server restarting on a save is busy and says so in its output: it is
1091
- // left alone, however long its own startup takes.
1092
- // A watcher that has said its app crashed is not busy, it is waiting, and says so.
1100
+ // Not answering. A dev server restarting on a save says so in its output: it is left alone,
1101
+ // however long its own startup takes.
1102
+ // A watcher that has said its app crashed is not reloading, it is waiting, and says so.
1093
1103
  const crashed = /app crashed - waiting|Failed running|waiting for (?:file )?changes before restart/i.test(lastSaid.slice(-600));
1094
- const busy = launched && !appGone && !crashed && Date.now() - lastOutputAt < 15_000;
1095
- if (busy || downFor < (crashed ? 4_000 : 10_000)) continue;
1104
+ const reloading = launched && !appGone && !crashed && Date.now() - lastOutputAt < 15_000;
1105
+ if (reloading || downFor < (crashed ? 4_000 : 10_000)) continue;
1096
1106
  // Down for real. The screen is told, so it never shows an app that is not there.
1097
1107
  // `exited`: the process this command started is gone or its watcher said it crashed, not a process
1098
1108
  // running and not answering. The run tells the two apart: one is their crash, the other may be our
@@ -1108,7 +1118,7 @@ async function appLife() {
1108
1118
  restarting = true;
1109
1119
  try {
1110
1120
  if (child?.pid) await stopApp(child.pid);
1111
- say("your app stopped answering, starting it again");
1121
+ say(downLine(state, { port: app.port, downMs: downFor, crashed }));
1112
1122
  let back = launched ? await launch(180_000) : await startApp();
1113
1123
  while (!back.port && !closing) {
1114
1124
  await call("POST", `/local/${box}/stopped`, { said: mask(back.tail || back.said || lastSaid).slice(-2000) }).catch(() => {});
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cortad",
3
- "version": "0.3.1-rc.10",
3
+ "version": "0.3.1-rc.12",
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"
@@ -10,7 +10,9 @@
10
10
  "README.md",
11
11
  "lib/cli.mjs",
12
12
  "lib/data.mjs",
13
+ "lib/failed-text.mjs",
13
14
  "lib/fresh.mjs",
15
+ "lib/health.mjs",
14
16
  "lib/home.mjs",
15
17
  "lib/identities.mjs",
16
18
  "lib/keys.mjs",
package/skill/SKILL.md CHANGED
@@ -49,7 +49,7 @@ Take the person through them worst first, each with its file and line. A long li
49
49
 
50
50
  ## Fixing one finding
51
51
 
52
- Each finding ends with what a verify of it can show. When the trials it replays read the question too few times for a rate, the verify plays up to 12 more trials in that situation itself, counted as verify trials, and the finding says how many before one is spent.
52
+ Each finding ends with what a verify of it can show. When the trials it replays read the question too few times for a rate, the verify plays up to 12 more trials itself, in the finding's situations that were read least, counted as verify trials, and the finding says how many before one is spent. A finding listed in several situations is verified in all of them.
53
53
 
54
54
  1. One change, in the file and near the line the finding names.
55
55
  2. `verify <findingId>`. It answers within a second, and `run_status` follows it. When the app's code changed since the app started and nothing reloaded it, Cortad starts the app again first and the answer says so, so the saved edit is what plays; `run_status` names when the app started against the last change. When it also plays new trials in the finding's situation, the answer says how many.
@@ -130,6 +130,21 @@ For the person: the report is at https://cortad.com/lab
130
130
  next: findings
131
131
  ```
132
132
 
133
+ Replies that did not count come before the score, each part saying whose it is, with the request to paste where there is one:
134
+
135
+ ```
136
+ Run 8f2a1c4e-5b6d-4e7f-9a0b-1c2d3e4f5a6b: finished, 31 of 31 trials played.
137
+ 17 of 33 replies did not count: 9 answered HTTP 502 at backend/app/api/resume_writing.py:91 (yours; request below), 6 refused the shape we sent to POST /api/drill/{id}/answer (HTTP 422) (ours; request below), 2 got no answer from /api/chat before we stopped waiting at 90 seconds (ours).
138
+ Request for the 9 at backend/app/api/resume_writing.py:91: curl -X POST 'http://localhost:8004/api/resume/writing' -H 'authorization: <redacted>' -H 'content-type: application/json' --data-raw '{"message":"rewrite my summary"}'
139
+ Answered: Bad Gateway
140
+ verify finding:1 replays them.
141
+ Request for the 6 from /api/drill/{id}/answer: curl -X POST 'http://localhost:8004/api/drill/7/answer' -H 'content-type: application/json' --data-raw '{"message":"hi"}'
142
+ Answered: {"detail":[{"loc":["body","answer"],"msg":"field required"}]}
143
+ 20 of 40 checks measured; score 88 of 100 on those 20, interval 70 to 96; the 17 of 33 replies that did not count are not in it.
144
+ ```
145
+
146
+ A part marked yours is the app failing: run the request, fix what it shows, then verify the finding named. A part marked ours is Cortad's to fix, and nothing in the app changes for it. A masked value (`<redacted>`) is the app's own sign-in or key; put it back before running the request.
147
+
133
148
  Your app stopped during the run and was started again: that is a finding of its own, `crash:1`, with the error your app printed and the turn it happened at. Fix the error it quotes. It has no trials to replay, so `verify` refuses it; the next run that finishes without it is the proof. A `Data:` line says whether the run wrote to a copy of your app's database or into the real one.
134
149
 
135
150
  A verify that holds: both intervals are above zero and the held-out trials moved with the visible ones.