cortad 0.3.1-rc.5 → 0.3.1-rc.7
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 +1 -1
- package/lib/text.mjs +25 -0
- package/lib/verbs.mjs +1 -1
- package/lib/writes.mjs +10 -6
- package/local.mjs +4 -0
- package/package.json +1 -1
package/lib/cli.mjs
CHANGED
|
@@ -36,7 +36,7 @@ export async function main(argv, { root = process.cwd(), env = process.env, stdo
|
|
|
36
36
|
const children = [];
|
|
37
37
|
const pending = { read: () => readPending(project), write: (p) => writePending(project, p) };
|
|
38
38
|
// The key is read at every call: the MCP process outlives a connect that writes it after the process started.
|
|
39
|
-
const writes = { mark: (id) => markRun(writesDirOf(homeOf(project)), id), putBack: (
|
|
39
|
+
const writes = { mark: (id) => markRun(writesDirOf(homeOf(project)), id), putBack: () => putBack(writesDirOf(homeOf(project)), root) };
|
|
40
40
|
const verbs = makeVerbs({
|
|
41
41
|
api: `${origin.origin}/api`,
|
|
42
42
|
root,
|
package/lib/text.mjs
CHANGED
|
@@ -144,6 +144,17 @@ const moveText = (m) => `held ${kn(m.before)} readings before, ${kn(m.after)} af
|
|
|
144
144
|
// Over zero pairs there is no move to print: the line says verification is unavailable and why,
|
|
145
145
|
// never "no change, 0 points" measured on nothing. The visible side's reason is the said line.
|
|
146
146
|
function verifyLines(v) {
|
|
147
|
+
// An access verify is a re-knock, not a trial move: the routes side by side, then the count.
|
|
148
|
+
if (v.access) {
|
|
149
|
+
const lines = [`Verify of ${v.findingId}.`];
|
|
150
|
+
for (const r of v.routes ?? []) {
|
|
151
|
+
const then = r.wasOpen ? `answered ${r.before} with no sign-in` : `refused (${r.before || "no answer"})`;
|
|
152
|
+
const now = r.nowOpen ? `answers ${r.after} with no sign-in` : r.after ? `refuses (${r.after})` : "did not answer";
|
|
153
|
+
lines.push(` ${r.method} ${r.route}: ${then}, now ${now}.`);
|
|
154
|
+
}
|
|
155
|
+
if (v.said) lines.push(v.said);
|
|
156
|
+
return lines;
|
|
157
|
+
}
|
|
147
158
|
const lines = [`Verify of ${v.findingId}${v.path ? ` at ${at(v.path, v.line)}` : ""}.`];
|
|
148
159
|
if (v.visible && !v.visible.unavailable) lines.push(`Visible trials: ${moveText(v.visible)}`);
|
|
149
160
|
if (v.holdout?.unavailable) lines.push(`Held-out trials: verification unavailable: ${v.holdout.unavailable}.`);
|
|
@@ -233,6 +244,9 @@ function findingsHead(d) {
|
|
|
233
244
|
if (reads) lines.push(reads);
|
|
234
245
|
if (d.baseRunId) lines.push(`Compared with run ${d.baseRunId}.`);
|
|
235
246
|
for (const s of [d.read, d.heldBack, d.findings?.length ? null : d.why]) if (s) lines.push(s);
|
|
247
|
+
// A finding in a situation the run keeps back: what failed and where, so the rule can be fixed;
|
|
248
|
+
// its inputs and trials stay unseen, and verify replays it all the same.
|
|
249
|
+
for (const k of d.keptBack ?? []) lines.push(`Kept back ${k.id}: "${clip(k.asks, 160)}"${k.file ? ` at ${at(k.file, k.line)}` : ""}, held ${kn(k.rate)} at reply ${num(k.reply)}${k.quote ? `; the reply said "${clip(k.quote, 120)}"` : ""}. Its inputs are not shown; verify ${k.id} replays them.`);
|
|
236
250
|
return lines.join("\n");
|
|
237
251
|
}
|
|
238
252
|
|
|
@@ -271,6 +285,17 @@ function findingsPages(d, budget = PAGE_CHARS - 60) {
|
|
|
271
285
|
}
|
|
272
286
|
|
|
273
287
|
function findingBlock(f, placed) {
|
|
288
|
+
// A gate the run knocked open: the route, the address, the request sent and the status it answered
|
|
289
|
+
// with no sign-in. Its own words, then how to prove the fix.
|
|
290
|
+
if (f.kind === "access-open") {
|
|
291
|
+
const lines = [`${f.id} ${f.says}`];
|
|
292
|
+
if (f.path) lines.push(` At ${at(f.path, f.line)}`);
|
|
293
|
+
if (f.standard) lines.push(` Standard: ${clip(f.standard, 200)}`);
|
|
294
|
+
lines.push(` Sent: ${f.request.method} ${f.request.path}${f.request.body ? ` ${clip(f.request.body, 200)}` : ""}${f.as === "another-user" ? " as another user" : " with no sign-in"}`);
|
|
295
|
+
lines.push(` Answered: ${f.status}${has(f.response) && f.response ? `, ${clip(f.response, 400)}` : ""}`);
|
|
296
|
+
lines.push(` Replay: verify ${f.id}`);
|
|
297
|
+
return lines.join("\n");
|
|
298
|
+
}
|
|
274
299
|
const lines = [`${f.id} ${f.asks}`];
|
|
275
300
|
if (!placed && f.file) lines.push(` At ${at(f.file, f.line)}`);
|
|
276
301
|
if (f.criteria) lines.push(` Criteria: ${clip(f.criteria, 600)}`);
|
package/lib/verbs.mjs
CHANGED
|
@@ -114,7 +114,7 @@ export function makeVerbs({ api, token, fetchImpl = fetch, startApp, pending, ro
|
|
|
114
114
|
}
|
|
115
115
|
// The run is over: what the app wrote into its folder during it is put back, said before the next step.
|
|
116
116
|
let back = "";
|
|
117
|
-
if (writes && finished(res.data)) { try { back = writes.putBack(
|
|
117
|
+
if (writes && finished(res.data)) { try { back = writes.putBack(); } catch { back = ""; } }
|
|
118
118
|
return { text: back ? withBack(runText(res.data), back) : runText(res.data), data: res.data };
|
|
119
119
|
};
|
|
120
120
|
|
package/lib/writes.mjs
CHANGED
|
@@ -8,19 +8,23 @@ import { dirname, join, resolve, sep } from "node:path";
|
|
|
8
8
|
|
|
9
9
|
export const writesDirOf = (home) => join(home, "writes");
|
|
10
10
|
|
|
11
|
-
// A run has started: from now until it ends, the hook records what the app writes.
|
|
12
|
-
|
|
11
|
+
// A run has started: from now until it ends, the hook records what the app writes. A record that
|
|
12
|
+
// is already open is kept when `keep` is set: an app started again mid-run keeps the copies made
|
|
13
|
+
// before it went down.
|
|
14
|
+
export function markRun(dir, jobId, { keep = false } = {}) {
|
|
15
|
+
if (keep && existsSync(join(dir, "run"))) return;
|
|
13
16
|
try { rmSync(dir, { recursive: true, force: true }); } catch { /* fresh */ }
|
|
14
17
|
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
15
18
|
writeFileSync(join(dir, "run"), String(jobId), { mode: 0o600 });
|
|
16
19
|
}
|
|
17
20
|
|
|
18
|
-
// The run has ended: every file
|
|
19
|
-
//
|
|
20
|
-
|
|
21
|
+
// The run has ended: every file written since the record began is put back, and the line says
|
|
22
|
+
// which. The record begins when the command starts the app (the first run starts itself on the
|
|
23
|
+
// server's side, so no verb marks it) and again at every run a verb starts, so what a person wrote
|
|
24
|
+
// between two runs is dropped from the record rather than undone.
|
|
25
|
+
export function putBack(dir, root) {
|
|
21
26
|
let rows = [];
|
|
22
27
|
try {
|
|
23
|
-
if (readFileSync(join(dir, "run"), "utf8").trim() !== String(jobId)) return "";
|
|
24
28
|
rows = readFileSync(join(dir, "written.jsonl"), "utf8").split("\n").filter(Boolean)
|
|
25
29
|
.map((line) => { try { return JSON.parse(line); } catch { return null; } }).filter(Boolean);
|
|
26
30
|
} catch { /* the run wrote nothing */ }
|
package/local.mjs
CHANGED
|
@@ -33,6 +33,7 @@ import { listingUrl } from "./lib/listing.mjs";
|
|
|
33
33
|
import { installPlan, missingDependency, startPlan, workspaces } from "./lib/start.mjs";
|
|
34
34
|
import { openSwitches } from "./lib/switches.mjs";
|
|
35
35
|
import { keepData } from "./lib/data.mjs";
|
|
36
|
+
import { markRun, writesDirOf } from "./lib/writes.mjs";
|
|
36
37
|
|
|
37
38
|
const argv = process.argv.slice(2);
|
|
38
39
|
// The two faces a coding agent uses after the first connect (lib/cli.mjs): the MCP server the
|
|
@@ -768,6 +769,9 @@ function dataOnce(started) {
|
|
|
768
769
|
|
|
769
770
|
async function start(waitMs) {
|
|
770
771
|
const { cmd, lifted } = await freed(launched.cmd, launched.lifted ?? {});
|
|
772
|
+
// The record of what the app writes opens with the app: the first run starts itself on the
|
|
773
|
+
// server's side, with no verb here to mark it, and a restart mid-run keeps the record open.
|
|
774
|
+
try { markRun(writesDirOf(homeOf(projectOf(root))), "session", { keep: true }); } catch { /* the app's writes go unrecorded */ }
|
|
771
775
|
child = spawn("/bin/sh", ["-c", cmd], { cwd: appDir, env: { ...envExports(envFiles), ...process.env, ...lifted, ...dataOnce(true).env, ...(capture ? capture.env(process.env) : {}), FORCE_COLOR: "0", ...(pinned.bin ? { PATH: `${pinned.bin}:${process.env.PATH ?? ""}` } : {}) }, stdio: ["ignore", "pipe", "pipe"], detached: true });
|
|
772
776
|
const mine = child;
|
|
773
777
|
let seen = "";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cortad",
|
|
3
|
-
"version": "0.3.1-rc.
|
|
3
|
+
"version": "0.3.1-rc.7",
|
|
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"
|