cortad 0.4.3 → 0.4.5
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 +23 -5
- package/lib/contact-text.mjs +7 -1
- package/lib/mcp.mjs +5 -2
- package/lib/proc.mjs +7 -2
- package/lib/read-text.mjs +7 -0
- package/lib/text.mjs +6 -0
- package/lib/verbs.mjs +8 -1
- package/local.mjs +20 -7
- package/package.json +1 -1
- package/skill/SKILL.md +2 -2
- package/skill/references/results.md +2 -1
package/lib/cli.mjs
CHANGED
|
@@ -9,10 +9,10 @@ import { markRun, putBack, writesDirOf } from "./writes.mjs";
|
|
|
9
9
|
import { makeReport } from "./calls.mjs";
|
|
10
10
|
import { serveMcp, TOOLS } from "./mcp.mjs";
|
|
11
11
|
import { SHOW } from "./read-text.mjs";
|
|
12
|
-
import { answeringText, notStartedText } from "./text.mjs";
|
|
12
|
+
import { answeringText, heldRunText, notStartedText } from "./text.mjs";
|
|
13
13
|
import { cliSpec, npxName, VERSION } from "./spec.mjs";
|
|
14
14
|
import { hookOutput, stick, unstick } from "./stick.mjs";
|
|
15
|
-
import { makeVerbs, READY_MS } from "./verbs.mjs";
|
|
15
|
+
import { cappedAt, makeVerbs, READY_MS } from "./verbs.mjs";
|
|
16
16
|
|
|
17
17
|
// `npx cortad mcp`, `npx cortad <verb>` and `npx cortad stick`: what a coding agent uses after the
|
|
18
18
|
// first connect. None of them uploads or edits anything by itself. When a run needs the app up and
|
|
@@ -102,10 +102,26 @@ const jsonArgs = (raw) => {
|
|
|
102
102
|
// old server while the connect command runs the new one. Each tool then runs as the version the
|
|
103
103
|
// running connect is, and as this one only when that cannot be done.
|
|
104
104
|
const RELEASE = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/;
|
|
105
|
+
// The first cortad that holds a run to a number of conversations. An older one takes the number,
|
|
106
|
+
// plays every conversation the run chose and spends the person's whole key on it, so a run it cannot
|
|
107
|
+
// hold is refused here rather than handed to it (lib/verbs.mjs run).
|
|
108
|
+
const CAP_FROM = "0.4.5";
|
|
109
|
+
// In release order on the numbers, with a prerelease counting below the release it leads to: a
|
|
110
|
+
// 0.4.5 build from before this is not 0.4.5. A version this cannot read counts as older, never as
|
|
111
|
+
// new enough.
|
|
112
|
+
const olderThan = (version, floor) => {
|
|
113
|
+
const parts = (v) => { const m = /^(\d+)\.(\d+)\.(\d+)(?:-(.+))?$/.exec(v); return m ? [+m[1], +m[2], +m[3], m[4] ? 0 : 1] : null; };
|
|
114
|
+
const [a, b] = [parts(version), parts(floor)];
|
|
115
|
+
if (!a) return true;
|
|
116
|
+
for (let i = 0; i < 4; i += 1) if (a[i] !== b[i]) return a[i] < b[i];
|
|
117
|
+
return false;
|
|
118
|
+
};
|
|
105
119
|
export function asRunner(verbs, { runnerVersion, runAt: other, own = VERSION }) {
|
|
106
120
|
return Object.fromEntries(TOOLS.map(({ name }) => [name, async (args) => {
|
|
107
121
|
const version = runnerVersion();
|
|
108
122
|
if (!version || version === own || !RELEASE.test(version)) return verbs[name](args);
|
|
123
|
+
const cap = name === "run" ? cappedAt(args?.conversations).conversations : undefined;
|
|
124
|
+
if (cap && olderThan(version, CAP_FROM)) return { text: heldRunText(version, cap, CAP_FROM), isError: true, code: "older_command" };
|
|
109
125
|
return (await other(version, name, args)) ?? verbs[name](args);
|
|
110
126
|
}]));
|
|
111
127
|
}
|
|
@@ -172,14 +188,16 @@ export function argsOf(command, rest) {
|
|
|
172
188
|
}
|
|
173
189
|
}
|
|
174
190
|
|
|
175
|
-
// What the waiter was asked for, from its own command line: a run, or a verify of one finding.
|
|
176
|
-
|
|
191
|
+
// What the waiter was asked for, from its own command line: a run, or a verify of one finding. An
|
|
192
|
+
// app that has to come up first is the ordinary first run, and this is the only place the run's own
|
|
193
|
+
// arguments survive to be posted, so a cap asked for here is carried and not quietly dropped.
|
|
194
|
+
export function requestOf(raw) {
|
|
177
195
|
let value = {};
|
|
178
196
|
try { value = JSON.parse(raw ?? "{}"); } catch { /* a run */ }
|
|
179
197
|
const pick = (v) => (typeof v === "string" && v.length <= 200 ? v : undefined);
|
|
180
198
|
const findingId = pick(value.args?.findingId);
|
|
181
199
|
const jobId = pick(value.args?.jobId);
|
|
182
|
-
return findingId ? { kind: "verify", args: { findingId, ...(jobId ? { jobId } : {}) } } : { kind: "run", args:
|
|
200
|
+
return findingId ? { kind: "verify", args: { findingId, ...(jobId ? { jobId } : {}) } } : { kind: "run", args: cappedAt(value.args?.conversations) };
|
|
183
201
|
}
|
|
184
202
|
|
|
185
203
|
// Returns at once. The runner holds the app up: from the MCP it lives as long as the MCP does;
|
package/lib/contact-text.mjs
CHANGED
|
@@ -130,6 +130,10 @@ function reachLine(missing) {
|
|
|
130
130
|
const ready = missing.filter((m) => m.request && !m.unsure && !m.excluded);
|
|
131
131
|
return `Send each one message as a user would, signed in where your app needs it${ready.length ? `, or call reach to send the request Cortad read for ${ready.length === missing.length ? "them" : `${num(ready.length)} of them`}` : ""}.`;
|
|
132
132
|
}
|
|
133
|
+
// A customer sent his one message, his app handed back a running turn, and this list went on naming
|
|
134
|
+
// his endpoint: he read it as his endpoint being broken. What Cortad is waiting for is said here, so
|
|
135
|
+
// the wait is the model's answer and not his question again.
|
|
136
|
+
const LATER_WAIT = "An endpoint that hands back a turn and answers afterwards stays on this list until Cortad has seen the answer its model wrote: the first reply alone does not take it off.";
|
|
133
137
|
|
|
134
138
|
// `data`: what the command did with the app's stores and services, said right under the run it is
|
|
135
139
|
// about, so what a run writes into for real is read before Run is pressed.
|
|
@@ -144,7 +148,9 @@ export function contactLines(c, data = [], receipts = []) {
|
|
|
144
148
|
const head = alike ? `Endpoints that answered (${num(proven.length)}), each only Cortad's own test request so far: ${proven[0].why}` : `Endpoints that answered (${num(proven.length)}):`;
|
|
145
149
|
const lines = proven.length ? [head, ...proven.flatMap((v) => doorLines(alike ? { ...v, standing: null } : v))] : [];
|
|
146
150
|
const missing = unreached(c, receipts);
|
|
147
|
-
|
|
151
|
+
// Not "have not answered yet", which a customer read as his endpoint being broken: what is missing
|
|
152
|
+
// is an answer Cortad has seen.
|
|
153
|
+
if (missing.length) lines.push(`Endpoints found in your code that Cortad has seen no answer from yet (${num(missing.length)}):`, ...missing.map(unreachedLine), ...[reachLine(missing), LATER_WAIT].filter(Boolean));
|
|
148
154
|
for (const line of [readyLine(c.ready), noRulesLine(c.ready)]) if (line) lines.push(line);
|
|
149
155
|
// What holds Run is the server's next line (src/local/contact.ts heldLine), said once: printed here
|
|
150
156
|
// as well, ulaim's hold on its Qdrant was read twice in one status.
|
package/lib/mcp.mjs
CHANGED
|
@@ -30,8 +30,11 @@ export const TOOLS = [
|
|
|
30
30
|
inputSchema: { type: "object", properties: {
|
|
31
31
|
paths: { type: "array", items: { type: "string" }, description: "Only these endpoints, by path. Default: every endpoint status lists as failed or not yet answered." },
|
|
32
32
|
}, additionalProperties: false } },
|
|
33
|
-
{ name: "run", title: "Start a run",
|
|
34
|
-
description: "Starts a run of conversations against the app on this machine and answers within a second with the run id;
|
|
33
|
+
{ name: "run", title: "Start a run",
|
|
34
|
+
description: "Starts a run of conversations against the app on this machine and answers within a second with the run id; with conversations it plays no more than that many, so a first look spends less on the person's own model key than a whole run does. After the first run it plays what the files saved since the last full run reach, plus new conversations written for the change, and its result leads with what moved; when the app is down it is started first, and run_status gives the id once the run has one. Uses one run from the plan, capped or not; a spent plan answers with the numbers and a checkout link for the person.",
|
|
35
|
+
inputSchema: { type: "object", properties: {
|
|
36
|
+
conversations: { type: "integer", minimum: 1, description: "The most conversations this run may play, for example 10. Default: as many as the run would choose, which status prices before it starts." },
|
|
37
|
+
}, additionalProperties: false } },
|
|
35
38
|
{ name: "run_status", title: "Run progress", annotations: readOnly,
|
|
36
39
|
description: "Where a run or verify stands: conversations played of the total, the findings, what the app's rules and users measured, and for a verify whether the failure is gone, shows less often, stayed or cannot be told yet. The call holds up to 45 seconds until the count moves, and ends with the next call. With no jobId it follows the run this machine started last, or the latest run.",
|
|
37
40
|
inputSchema: { type: "object", properties: { jobId: str("A run or verify id, or the word pending. Omit for the run this machine started last.") }, additionalProperties: false } },
|
package/lib/proc.mjs
CHANGED
|
@@ -79,7 +79,9 @@ export async function holderOf(port) {
|
|
|
79
79
|
export async function stopTree(pid) {
|
|
80
80
|
const family = await familyOf(pid).catch(() => [pid]);
|
|
81
81
|
const signal = (sig) => { for (const p of family) { try { process.kill(p, sig); } catch { /* already gone */ } } try { process.kill(-pid, sig); } catch { /* no group left */ } };
|
|
82
|
-
|
|
82
|
+
// The group counts as alive as well: once its leader has exited, a server the leader started is in
|
|
83
|
+
// the group and in no family read from the leader's pid, and asking alone never stopped it.
|
|
84
|
+
const alive = () => [...family, -pid].some((p) => { try { process.kill(p, 0); return true; } catch { return false; } });
|
|
83
85
|
signal("SIGTERM");
|
|
84
86
|
for (let i = 0; i < 15 && alive(); i++) await new Promise((r) => setTimeout(r, 200));
|
|
85
87
|
if (alive()) signal("SIGKILL");
|
|
@@ -97,6 +99,9 @@ export function spawnTied(cmd, { cwd, env }) {
|
|
|
97
99
|
dog.stdin.unref?.();
|
|
98
100
|
dog.stdin.on("error", () => { /* the watchdog is gone; the runner stops the app itself */ });
|
|
99
101
|
// An app that ended while this process lives is this process's to clean up, not the watchdog's.
|
|
100
|
-
|
|
102
|
+
// Its group can hold more than it: a start command that hands the port to a child and exits leaves
|
|
103
|
+
// that child serving, which is a second copy of the app as soon as the next start comes up. It is
|
|
104
|
+
// stopped here, while the group still names it, and the watchdog goes once there is nothing left.
|
|
105
|
+
app.once("exit", () => { void stopTree(app.pid).finally(() => { try { dog.kill("SIGKILL"); } catch { /* gone */ } }); });
|
|
101
106
|
return app;
|
|
102
107
|
}
|
package/lib/read-text.mjs
CHANGED
|
@@ -111,6 +111,13 @@ export function machineText(f) {
|
|
|
111
111
|
// What the terminal says once the app answers: the one line the person needs.
|
|
112
112
|
export const connectedText = (f) => machineText(f).slice(0, 1);
|
|
113
113
|
|
|
114
|
+
// The last line a connect prints, and the last line when the app never started. A customer read the
|
|
115
|
+
// failure as the ready signal: both opened on the same clause and sent him to a browser, and his
|
|
116
|
+
// whole session was a terminal and a coding agent, which never had one open. Each opens on the state
|
|
117
|
+
// it is reporting, and the one place that session can act on is the agent.
|
|
118
|
+
export const CONNECTED = "connected: Cortad is watching your app. Ask your coding agent for Cortad's status to see what a run would play; Ctrl-C disconnects.";
|
|
119
|
+
export const NOT_RUNNING = "your app is not running, so Cortad has nothing to test. What stopped it is the line above, from starting your app, not from Cortad.";
|
|
120
|
+
|
|
114
121
|
export function showText(d, show, page = 1) {
|
|
115
122
|
if (!d.read || (d.read.complete === false && show !== "reviews")) return "Cortad is still reading this repository.";
|
|
116
123
|
const { head, lines, continued } = sectionOf(d.read, show);
|
package/lib/text.mjs
CHANGED
|
@@ -308,6 +308,12 @@ export function startedText(d, kind, findingId, said = []) {
|
|
|
308
308
|
return [`${what} ${d.joined ? "already playing" : "started"}: ${d.jobId}.`, ...said, d.note, `next: run_status ${d.jobId}`].filter(Boolean).join("\n");
|
|
309
309
|
}
|
|
310
310
|
|
|
311
|
+
// A run held to a number of conversations, asked of a connect command older than the one that can
|
|
312
|
+
// hold it: that command would take the number, play every conversation and spend the whole of the
|
|
313
|
+
// person's own key, so the run is refused instead and the one thing to do about it is said.
|
|
314
|
+
export const heldRunText = (version, conversations, from) =>
|
|
315
|
+
`The cortad command holding your app up is ${version} and cannot hold a run to ${plural(conversations, "conversation")}; ${from} and newer can, so stop that command and run it again.\nNothing ran.`;
|
|
316
|
+
|
|
311
317
|
export const restartedText = (paths) =>
|
|
312
318
|
`Your app was started again so this run plays your change (${plural(paths.length, "file")} changed since it started: ${paths.slice(0, 3).join(", ")}${paths.length > 3 ? ", ..." : ""}).`;
|
|
313
319
|
|
package/lib/verbs.mjs
CHANGED
|
@@ -28,6 +28,10 @@ const IDLE_MS = 6 * 3600_000;
|
|
|
28
28
|
export const READY_MS = 240_000;
|
|
29
29
|
// The most changed files a run is scoped by; past it the run replays everything.
|
|
30
30
|
const MOST_CHANGED = 2000;
|
|
31
|
+
// The most conversations a run may play, as the person asked for it, on its way to the server: a
|
|
32
|
+
// whole number of at least one, or nothing at all. How many a run is allowed is the server's to
|
|
33
|
+
// decide and to refuse, so no ceiling is kept on this side to drift from the one it holds.
|
|
34
|
+
export const cappedAt = (n) => (Number.isSafeInteger(n) && n >= 1 ? { conversations: n } : {});
|
|
31
35
|
|
|
32
36
|
// `pending` keeps the run this machine asked for in ~/.cortad/<project>/pending.json: { kind,
|
|
33
37
|
// startedAt, after } while the app comes up, then its jobId, or the error that ended it. `after` is
|
|
@@ -143,7 +147,10 @@ export function makeVerbs({ api, token, runner = () => null, fetchImpl = fetch,
|
|
|
143
147
|
}
|
|
144
148
|
return { text: here?.state === "up" ? upWaitingText(here, before.data.app) : STARTING, data: { pending: true } };
|
|
145
149
|
};
|
|
146
|
-
|
|
150
|
+
// `conversations`: the most this run may play, asked for by the person who is paying for the
|
|
151
|
+
// replies. Whether a number is allowed is the server's to say (it holds the range), so only its
|
|
152
|
+
// shape is read here and the server's refusal speaks for the rest.
|
|
153
|
+
const run = ({ conversations } = {}) => start(cappedAt(conversations), "run");
|
|
147
154
|
const verify = ({ findingId, jobId } = {}) => start({ findingId, ...(jobId ? { jobId } : {}) }, "verify");
|
|
148
155
|
|
|
149
156
|
// With no id: the run this machine is still starting, held here until it has an id, or else the
|
package/local.mjs
CHANGED
|
@@ -30,7 +30,7 @@ import { changesOf, commandText, RELOADER, sourceOf } from "./lib/fresh.mjs";
|
|
|
30
30
|
import { chainOf, elapsedMs, holderOf, listening, listenerOn, portInError, spawnTied, stopTree } from "./lib/proc.mjs";
|
|
31
31
|
import { claimRunner, readRunner, releaseRunner, replaceRunner, startedAtOf, writeRunner } from "./lib/runner.mjs";
|
|
32
32
|
import { registerAll } from "./lib/register.mjs";
|
|
33
|
-
import { connectedText } from "./lib/read-text.mjs";
|
|
33
|
+
import { CONNECTED, connectedText, NOT_RUNNING } from "./lib/read-text.mjs";
|
|
34
34
|
import { VERSION } from "./lib/spec.mjs";
|
|
35
35
|
import { credentialShaped, maskedLines, maskWith, secretValues } from "./lib/env-secrets.mjs";
|
|
36
36
|
import { finished, unseenText } from "./lib/text.mjs";
|
|
@@ -822,9 +822,14 @@ function restartOnce(work) {
|
|
|
822
822
|
})();
|
|
823
823
|
return restartDone;
|
|
824
824
|
}
|
|
825
|
+
const SAVED_CODE = "starting your app again so it runs the code you saved, since it does not reload by itself";
|
|
826
|
+
// Your app read its env files when it started and nothing reloads them, so a value saved into one is
|
|
827
|
+
// a value it is not running on. A customer changed one and had to stop this command and run it again
|
|
828
|
+
// for every change; one of his apps was left running each time.
|
|
829
|
+
const SAVED_ENV = "starting your app again so it reads the value you saved in your env file, which your app reads only when it starts";
|
|
825
830
|
// The same command again, for an app that does not reload on save. Only an app this program
|
|
826
831
|
// started: one you started yourself is yours to restart. New requests wait at the gate meanwhile.
|
|
827
|
-
function restartApp(why =
|
|
832
|
+
function restartApp(why = SAVED_CODE) {
|
|
828
833
|
if (!launched || !child) return Promise.resolve({ error: "You started this app yourself, so restart it in your own terminal. Most dev servers reload on save." });
|
|
829
834
|
return restartOnce(async () => {
|
|
830
835
|
hold();
|
|
@@ -840,13 +845,20 @@ function restartApp(why = "starting your app again so it runs the code you saved
|
|
|
840
845
|
const savedSinceStart = () => Boolean(loaded?.files) && changesOf({
|
|
841
846
|
root, app: { startedAt: new Date(loaded.at).toISOString(), files: loaded.files }, skip: writtenPaths(writesDirOf(homeOf(project))),
|
|
842
847
|
}).changed.length > 0;
|
|
848
|
+
// An env file of the app's own, saved since it started: compared by the time on the file, since what
|
|
849
|
+
// it holds is never read into this command's own record. An example file is nobody's settings.
|
|
850
|
+
const envSavedSinceStart = () => Boolean(loaded?.own && loaded.at) && envFiles.some((f) => {
|
|
851
|
+
try { return !/\.(example|sample)$/.test(f) && statSync(f).mtimeMs > loaded.at; } catch { return false; }
|
|
852
|
+
});
|
|
853
|
+
// Why the app has to start again now, or nothing when it is running what is on disk.
|
|
854
|
+
const whyRestart = () => (savedSinceStart() ? SAVED_CODE : envSavedSinceStart() ? SAVED_ENV : null);
|
|
843
855
|
// Every save that changes what your app runs starts it again, when it does not reload by itself: an
|
|
844
856
|
// agent that saved a fix and asked the app eight seconds later was answered by the old code. Looked
|
|
845
857
|
// at again after each restart, for a save made while it was under way, and before the first wait too:
|
|
846
858
|
// the app starts while the code is uploaded, and a save made before this watch began was otherwise never seen.
|
|
847
859
|
async function restartOnSave() {
|
|
848
860
|
while (!closing) {
|
|
849
|
-
|
|
861
|
+
for (let why = whyRestart(); !closing && launched && child && !appGone && !loaded?.reloads && why; why = whyRestart()) await restartApp(why);
|
|
850
862
|
await sourceChanged();
|
|
851
863
|
}
|
|
852
864
|
}
|
|
@@ -1108,9 +1120,10 @@ async function appLife(firstStart) {
|
|
|
1108
1120
|
failedWith(got.why, got.said);
|
|
1109
1121
|
await call("POST", `/local/${box}/stopped`, { said: mask(got.said || got.why).slice(-2000) }).catch(() => {});
|
|
1110
1122
|
say(got.why);
|
|
1111
|
-
//
|
|
1112
|
-
// the failure instead of waiting for a line that
|
|
1113
|
-
|
|
1123
|
+
// An agent holding this terminal watches for one of these two lines (skill/SKILL.md), so the
|
|
1124
|
+
// failure carries a line of its own: it reports the failure instead of waiting for a line that
|
|
1125
|
+
// never comes, and no reader takes it for the connected one.
|
|
1126
|
+
if (first) say(NOT_RUNNING);
|
|
1114
1127
|
await sourceChanged();
|
|
1115
1128
|
become("starting");
|
|
1116
1129
|
say("saw your change, starting your app again");
|
|
@@ -1128,7 +1141,7 @@ async function appLife(firstStart) {
|
|
|
1128
1141
|
if (launched) say(`your app's output for this session is kept in ${bootLog}, with the values from your env files masked`);
|
|
1129
1142
|
if (proxy) say(`send requests to port ${proxy.port}: it passes them to your app and Cortad sees each one. Requests sent straight to port ${app.port} are not seen`);
|
|
1130
1143
|
for (const line of connectedText(machineOf())) say(line);
|
|
1131
|
-
say(
|
|
1144
|
+
say(CONNECTED);
|
|
1132
1145
|
let downSince = 0;
|
|
1133
1146
|
let toldDown = false;
|
|
1134
1147
|
let appTold = true;
|
package/package.json
CHANGED
package/skill/SKILL.md
CHANGED
|
@@ -38,7 +38,7 @@ Cortad does not guess how the app runs: you tell it, in one line, from the repos
|
|
|
38
38
|
- A folder connected before reconnects with the key and the start command it kept. A first connect prints a link and four characters, good for ten minutes: give the person both as printed; they approve the link in their browser, signed in to Cortad.
|
|
39
39
|
- When a result says no connect command is running, run the same line again in this folder.
|
|
40
40
|
|
|
41
|
-
|
|
41
|
+
The output ends on one of two lines. "Cortad is watching your app" means it is connected, and Cortad lists the AI endpoints it found in the code. "your app is not running" means the start failed: give the person that line and the output above it, which is their app's own, and stop there. Once it is connected, send each endpoint one message the way a user would: through the app's own page, or a request from this machine, signed in as a real test account where the app needs one (ask the person which account; Cortad makes none). That one request is what Cortad learns the endpoint from: its route, its body, its sign-in and where the answer comes back. Cortad sends nothing to the app before that, and nothing of its own before the person's yes on the card.
|
|
42
42
|
|
|
43
43
|
1. Call `status` and tell the person, in three or four short lines:
|
|
44
44
|
- that their app is connected, or the one line that says why it did not start (what the app printed is the app's own output);
|
|
@@ -47,7 +47,7 @@ Once it prints "Go back to the browser", Cortad lists the AI endpoints it found
|
|
|
47
47
|
- what happens next: they press Run on the card in the browser, or tell you to start it.
|
|
48
48
|
|
|
49
49
|
The endpoints, the files and the audit's rows are yours to work from. They go to the person when they ask, or when you propose a fix.
|
|
50
|
-
2. An endpoint `status` lists as
|
|
50
|
+
2. An endpoint `status` lists as one Cortad has seen no answer from is waiting for your one message to it. One it lists as `Needs a signed-in user` was refused: send it again signed in as a test account the person names, and call `status` again. An endpoint that hands back a turn and answers afterwards stays on that list until Cortad has seen the answer its model wrote, so fetch the finished answer the way the app's own client does rather than sending the question again.
|
|
51
51
|
3. A failure on the app's side that every conversation would repeat, such as a model provider refusing, a retrieval with no answer or a tool returning an error: say what you would change, change it when the person agrees, then send that endpoint one message again, or call `reach` to have Cortad send the request it read for it.
|
|
52
52
|
4. The one question on the card in the browser is where test conversations will write: into the databases the app wrote into for your message, named there, with nothing copied. Say it to the person once, in one line. The first run starts when the person presses Run on the card, or asks you, and then you call `run`. Before you stop, start `npx -y {{cortad}} wait` as a background command (in Claude Code: Bash with run_in_background). It returns when the next run or verify ends, with what it found, and its ending wakes you. Then follow "After a run".
|
|
53
53
|
|
|
@@ -31,10 +31,11 @@ Endpoints that answered (1):
|
|
|
31
31
|
Passages handed to the model: 3.
|
|
32
32
|
Problem at apps/api/src/agent/system.ts:30: The reply carried "</student_profile>", markup from the prompt your app sent the model.
|
|
33
33
|
Last message: "and the second question?".
|
|
34
|
-
Endpoints found in your code that
|
|
34
|
+
Endpoints found in your code that Cortad has seen no answer from yet (2):
|
|
35
35
|
POST /api/homework/explain apps/api/src/routes/homework.ts:18 Cortad's test request was answered with HTTP 500.
|
|
36
36
|
POST /api/progress/summary apps/api/src/routes/progress.ts:9 Needs a signed-in user.
|
|
37
37
|
Send each one message as a user would, signed in where your app needs it, or call reach to send the request Cortad read for them.
|
|
38
|
+
An endpoint that hands back a turn and answers afterwards stays on this list until Cortad has seen the answer its model wrote: the first reply alone does not take it off.
|
|
38
39
|
A run plays 36 simulated users in about 6 minutes. About $0.38 on your OpenAI key.
|
|
39
40
|
A run starts when the person presses Run on the card in the browser or asks you for one; if they say they pressed it, call run rather than ask them again.
|
|
40
41
|
```
|