cortad 0.3.0-rc.2 → 0.3.0-rc.3
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 +8 -3
- package/lib/mcp.mjs +5 -2
- package/lib/read-text.mjs +91 -0
- package/lib/text.mjs +4 -49
- package/lib/verbs.mjs +7 -3
- package/lib/words.mjs +37 -0
- package/package.json +3 -1
- package/skill/SKILL.md +2 -0
- package/skill/references/results.md +22 -0
package/lib/cli.mjs
CHANGED
|
@@ -4,6 +4,7 @@ import { dirname, join } from "node:path";
|
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
import { homeOf, projectOf, readPending, readRunner, readToken, writePending, writeRunner } from "./home.mjs";
|
|
6
6
|
import { serveMcp } from "./mcp.mjs";
|
|
7
|
+
import { SHOW } from "./read-text.mjs";
|
|
7
8
|
import { cliSpec, VERSION } from "./spec.mjs";
|
|
8
9
|
import { hookOutput, stick, unstick } from "./stick.mjs";
|
|
9
10
|
import { makeVerbs, READY_MS } from "./verbs.mjs";
|
|
@@ -62,13 +63,17 @@ export async function main(argv, { root = process.cwd(), env = process.env, stdo
|
|
|
62
63
|
return out.isError ? 1 : 0;
|
|
63
64
|
}
|
|
64
65
|
|
|
65
|
-
// Positional arguments for the shell face: `verify f1`, `run_status <jobId>`,
|
|
66
|
-
// `dispute f1 "why"`.
|
|
66
|
+
// Positional arguments for the shell face: `status rules 2`, `verify f1`, `run_status <jobId>`,
|
|
67
|
+
// `findings 2`, `dispute f1 "why"`. A bare number is a page; in findings anything else is a run id.
|
|
67
68
|
function argsOf(command, rest) {
|
|
69
|
+
const page = rest.find((w) => /^\d+$/.test(w));
|
|
68
70
|
switch (command.replace(/-/g, "_")) {
|
|
71
|
+
case "status": {
|
|
72
|
+
const show = rest.find((w) => SHOW.includes(w));
|
|
73
|
+
return { ...(show ? { show } : {}), ...(page ? { page: Number(page) } : {}) };
|
|
74
|
+
}
|
|
69
75
|
case "run_status": return rest[0] ? { jobId: rest[0] } : {};
|
|
70
76
|
case "findings": {
|
|
71
|
-
const page = rest.find((w) => /^\d+$/.test(w));
|
|
72
77
|
const jobId = rest.find((w) => w !== "--page" && !/^\d+$/.test(w));
|
|
73
78
|
return { ...(jobId ? { jobId } : {}), ...(page ? { page: Number(page) } : {}) };
|
|
74
79
|
}
|
package/lib/mcp.mjs
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
// dependency. stdout carries protocol messages and nothing else; everything a person might read
|
|
3
3
|
// goes to stderr. The tools are lib/verbs.mjs, one to one.
|
|
4
4
|
|
|
5
|
+
import { SHOW } from "./read-text.mjs";
|
|
6
|
+
|
|
5
7
|
export const PROTOCOL = "2025-06-18";
|
|
6
8
|
|
|
7
9
|
// Codex keeps only the first 512 characters self-contained and Claude Code shares about 4 KB across
|
|
@@ -16,8 +18,9 @@ const none = { type: "object", properties: {}, additionalProperties: false };
|
|
|
16
18
|
const readOnly = { readOnlyHint: true };
|
|
17
19
|
|
|
18
20
|
export const TOOLS = [
|
|
19
|
-
{ name: "status", title: "Cortad status", annotations: readOnly,
|
|
20
|
-
description: "What Cortad read in this repository, the plan with runs left, whether the app is up, and the latest run. Applies at the start of a session, right after a connect, and before a commit that changes prompts, tools, models or retrieval.
|
|
21
|
+
{ name: "status", title: "Cortad status", annotations: readOnly,
|
|
22
|
+
description: "What Cortad read in this repository, the plan with runs left, whether the app is up, and the latest run. Applies at the start of a session, right after a connect, and before a commit that changes prompts, tools, models or retrieval. With show, one section of the read in full, in pages.",
|
|
23
|
+
inputSchema: { type: "object", properties: { show: { type: "string", enum: SHOW, description: "A section of the read to list in full: rules, standards, journeys, endpoints or trials." }, page: { type: "integer", minimum: 1, description: "The page of that list. Default 1." } }, additionalProperties: false } },
|
|
21
24
|
{ name: "run", title: "Start a run", inputSchema: none,
|
|
22
25
|
description: "Starts a run of the simulated conversations against the app on this machine and answers within a second with the run id. 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; a spent plan answers with the numbers and a checkout link for the person." },
|
|
23
26
|
{ name: "run_status", title: "Run progress", annotations: readOnly,
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { at, clip, has, num, pack, pageOf, plural, SIDE } from "./words.mjs";
|
|
2
|
+
|
|
3
|
+
// What Cortad read of the repository: the walkthrough `status` carries, and one section in full
|
|
4
|
+
// for `status` with show. Both use the same lines, so the full list reads like the walkthrough.
|
|
5
|
+
//
|
|
6
|
+
// The full list comes back from GET /mcp/status?show=<name>: read.rules.all, read.machine.misses
|
|
7
|
+
// complete with read.machine.mets, read.journeys.all, read.doors.all, read.trials.all. Where the API
|
|
8
|
+
// has not sent it, what it did send is listed with "n of N listed".
|
|
9
|
+
export const SHOW = ["rules", "standards", "journeys", "endpoints", "trials"];
|
|
10
|
+
|
|
11
|
+
const rulesHead = (rules) => {
|
|
12
|
+
const files = Array.isArray(rules.files) ? rules.files.length : rules.files;
|
|
13
|
+
return `Rules in the code: ${num(rules.count)}${has(files) ? `, in ${plural(files, "file")}` : ""}.`;
|
|
14
|
+
};
|
|
15
|
+
const ruleLine = (e) => ` ${at(e.path, e.line)} "${clip(e.text, 300)}"`;
|
|
16
|
+
const headOf = (label, parts) => (parts.some(Boolean) ? `${label}: ${parts.filter(Boolean).join(", ")}.` : `${label}:`);
|
|
17
|
+
const standardsHead = (m) => headOf("Engineering standards", [has(m.decided) && `${num(m.decided)} decided`, has(m.met) && `${num(m.met)} met`, has(m.missed) && `${num(m.missed)} missed`]);
|
|
18
|
+
const standardLine = (x) => ` ${x.path ? `${at(x.path, x.line)} ` : ""}${x.standard}${x.detail ? `: ${clip(x.detail, 240)}` : ""}${x.decidedBy ? ` (decided by ${x.decidedBy === "code" ? "code" : "a model"})` : ""}`;
|
|
19
|
+
const trialsHead = (t) => headOf("Trials", [has(t.written) && `${num(t.written)} written`, has(t.playable) && `${num(t.playable)} playable`]);
|
|
20
|
+
const heldWhy = (h) => `${SIDE[h.side] ? `, ${SIDE[h.side]}` : ""}${h.why ? `: ${clip(h.why, 300)}` : ""}`;
|
|
21
|
+
const named = (label, group, max = 12) => {
|
|
22
|
+
if (!group) return null;
|
|
23
|
+
const names = group.names ?? [];
|
|
24
|
+
const more = (group.count ?? names.length) - Math.min(names.length, max);
|
|
25
|
+
const list = names.slice(0, max).join(", ");
|
|
26
|
+
return `${label} (${num(group.count ?? names.length)})${list ? `: ${list}${more > 0 ? `, and ${more} more` : ""}` : ""}.`;
|
|
27
|
+
};
|
|
28
|
+
// "Journeys (6):" when every one is here, "Journeys (6), 4 listed:" when not.
|
|
29
|
+
const counted = (label, count, n) => `${label} (${num(count ?? n)})${has(count) && n < count ? `, ${num(n)} listed` : ""}:`;
|
|
30
|
+
|
|
31
|
+
// The walkthrough, in the order the person is walked through it.
|
|
32
|
+
export function readLines(r) {
|
|
33
|
+
const lines = ["What Cortad read:"];
|
|
34
|
+
if (r.rules) {
|
|
35
|
+
const examples = (r.rules.examples ?? []).slice(0, 3);
|
|
36
|
+
lines.push(`${rulesHead(r.rules)}${examples.length ? ` ${examples.length} of them:` : ""}`, ...examples.map(ruleLine));
|
|
37
|
+
}
|
|
38
|
+
for (const line of [named("Journeys", r.journeys), named("Simulated users", r.profiles), named("Endpoints", r.doors)]) if (line) lines.push(line);
|
|
39
|
+
const m = r.machine;
|
|
40
|
+
if (m) {
|
|
41
|
+
const misses = m.misses ?? [];
|
|
42
|
+
lines.push(standardsHead(m), ...misses.slice(0, 12).map(standardLine));
|
|
43
|
+
if (misses.length > 12) lines.push(` and ${misses.length - 12} more.`);
|
|
44
|
+
}
|
|
45
|
+
const q = r.questions;
|
|
46
|
+
if (q) lines.push(`Questions: ${num(q.total)}${has(q.everywhere) ? `; ${num(q.everywhere)} asked in every conversation` : ""}${has(q.placed) ? `, ${num(q.placed)} placed in the situations they fit` : ""}.`);
|
|
47
|
+
const t = r.trials;
|
|
48
|
+
if (t) lines.push(trialsHead(t), ...(t.held ?? []).map((h) => ` ${h.surface}: ${plural(h.n, "trial")} held back${heldWhy(h)}`));
|
|
49
|
+
return lines;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function showText(d, show, page = 1) {
|
|
53
|
+
if (!d.read) return "Cortad is still reading this repository.";
|
|
54
|
+
const { head, lines, continued } = sectionOf(d.read, show);
|
|
55
|
+
return pageOf(pack(head, lines, continued), page, (n) => `status with show ${show} and page ${n}`);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function sectionOf(r, show) {
|
|
59
|
+
switch (show) {
|
|
60
|
+
case "rules": {
|
|
61
|
+
const rules = r.rules ?? { count: 0 };
|
|
62
|
+
const all = rules.all ?? rules.examples ?? [];
|
|
63
|
+
return { head: `${rulesHead(rules)}${all.length < rules.count ? ` ${num(all.length)} of ${num(rules.count)} listed.` : ""}`, lines: all.map(ruleLine), continued: "Rules in the code, continued." };
|
|
64
|
+
}
|
|
65
|
+
case "standards": {
|
|
66
|
+
const m = r.machine ?? {};
|
|
67
|
+
const misses = m.misses ?? [];
|
|
68
|
+
const mets = m.mets ?? [];
|
|
69
|
+
return {
|
|
70
|
+
head: standardsHead(m),
|
|
71
|
+
lines: [misses.length && counted("Missed", m.missed, misses.length), ...misses.map(standardLine), mets.length && counted("Met", m.met, mets.length), ...mets.map(standardLine)].filter(Boolean),
|
|
72
|
+
continued: "Engineering standards, continued.",
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
case "journeys": {
|
|
76
|
+
const all = r.journeys?.all ?? (r.journeys?.names ?? []).map((name) => ({ name }));
|
|
77
|
+
return { head: counted("Journeys", r.journeys?.count, all.length), lines: all.map((j) => ` ${j.name}${j.steps?.length ? `: ${clip(j.steps.join("; "), 600)}` : ""}`), continued: "Journeys, continued." };
|
|
78
|
+
}
|
|
79
|
+
case "endpoints": {
|
|
80
|
+
const all = r.doors?.all ?? (r.doors?.names ?? []).map((name) => ({ name }));
|
|
81
|
+
return { head: counted("Endpoints", r.doors?.count, all.length), lines: all.map((e) => ` ${e.name}${e.path ? `: ${[e.method, e.path].filter(Boolean).join(" ")}` : ""}`), continued: "Endpoints, continued." };
|
|
82
|
+
}
|
|
83
|
+
case "trials":
|
|
84
|
+
default: {
|
|
85
|
+
const t = r.trials ?? {};
|
|
86
|
+
const all = t.all ?? (t.held ?? []).map((h) => ({ surface: h.surface, held: h.n, why: h.why, side: h.side }));
|
|
87
|
+
const line = (x) => ` ${x.surface}: ${[has(x.written) && `${num(x.written)} written`, has(x.playable) && `${num(x.playable)} playable`, x.held && `${num(x.held)} held back`].filter(Boolean).join(", ")}${x.held ? heldWhy(x) : ""}`;
|
|
88
|
+
return { head: trialsHead(t), lines: all.map(line), continued: "Trials, continued." };
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
package/lib/text.mjs
CHANGED
|
@@ -1,31 +1,18 @@
|
|
|
1
|
+
import { readLines } from "./read-text.mjs";
|
|
2
|
+
import { at, clip, has, num, PAGE_CHARS, pageOf, plural, SIDE, upper } from "./words.mjs";
|
|
3
|
+
|
|
1
4
|
// What each verb prints. A result is data for the coding agent: a line meant for the person starts
|
|
2
5
|
// with "For the person:", and a run_status result ends with the next call. A field the API leaves
|
|
3
6
|
// out prints nothing, so the text follows the API as it grows.
|
|
4
7
|
|
|
5
8
|
const DONE = new Set(["succeeded", "failed", "canceled"]);
|
|
6
|
-
const SIDE = { theirs: "on the app's side", ours: "on Cortad's side" };
|
|
7
9
|
const UNIT = { sweeps: ["run", "runs"], runs: ["run", "runs"], trials: ["verify trial", "verify trials"] };
|
|
8
|
-
// Copilot cuts a result past about 10 KB; a findings page stays under this.
|
|
9
|
-
export const PAGE_CHARS = 9000;
|
|
10
10
|
export const STARTING = "Starting your app for the run. Call run_status; it answers as soon as the run has an id.";
|
|
11
11
|
|
|
12
|
-
const has = (v) => v !== undefined && v !== null;
|
|
13
|
-
const num = (x) => (typeof x === "number" ? Math.round(x).toLocaleString("en-US") : String(x));
|
|
14
|
-
export const plural = (n, one, many = `${one}s`) => `${num(n)} ${n === 1 ? one : many}`;
|
|
15
12
|
const signed = (x) => (Math.round(x) > 0 ? `+${num(x)}` : num(x));
|
|
16
|
-
const upper = (s) => `${s.charAt(0).toUpperCase()}${s.slice(1)}`;
|
|
17
|
-
const at = (path, line) => `${path}${has(line) ? `:${line}` : ""}`;
|
|
18
|
-
const clip = (s, max) => (String(s).length > max ? `${String(s).slice(0, max - 3)}...` : String(s));
|
|
19
13
|
const units = (n, unit) => { const [one, many] = UNIT[unit] ?? [unit, unit]; return `${num(n)} ${n === 1 ? one : many}`; };
|
|
20
14
|
const kn = (x) => (has(x?.k) ? `${num(x.k)} of ${num(x.n)}` : num(x));
|
|
21
15
|
const interval = (ci) => (ci && has(ci.low) ? `, interval ${num(ci.low)} to ${num(ci.high)}` : "");
|
|
22
|
-
const named = (label, group, max = 12) => {
|
|
23
|
-
if (!group) return null;
|
|
24
|
-
const names = group.names ?? [];
|
|
25
|
-
const more = (group.count ?? names.length) - Math.min(names.length, max);
|
|
26
|
-
const list = names.slice(0, max).join(", ");
|
|
27
|
-
return `${label} (${num(group.count ?? names.length)})${list ? `: ${list}${more > 0 ? `, and ${more} more` : ""}` : ""}.`;
|
|
28
|
-
};
|
|
29
16
|
|
|
30
17
|
export const finished = (run) => Boolean(run) && (run.finished ?? DONE.has(run.status));
|
|
31
18
|
|
|
@@ -52,33 +39,6 @@ function planLine(p) {
|
|
|
52
39
|
return parts.length ? `${p.name}: ${parts.join(", ")}.` : `Plan: ${p.name}.`;
|
|
53
40
|
}
|
|
54
41
|
|
|
55
|
-
// What Cortad read, in the order the person is walked through it.
|
|
56
|
-
function readLines(r) {
|
|
57
|
-
const lines = ["What Cortad read:"];
|
|
58
|
-
if (r.rules) {
|
|
59
|
-
const files = Array.isArray(r.rules.files) ? r.rules.files.length : r.rules.files;
|
|
60
|
-
const examples = (r.rules.examples ?? []).slice(0, 3);
|
|
61
|
-
lines.push(`Rules in the code: ${num(r.rules.count)}${has(files) ? `, in ${plural(files, "file")}` : ""}.${examples.length ? ` ${examples.length} of them:` : ""}`);
|
|
62
|
-
for (const e of examples) lines.push(` ${at(e.path, e.line)} "${clip(e.text, 200)}"`);
|
|
63
|
-
}
|
|
64
|
-
for (const line of [named("Journeys", r.journeys), named("Simulated users", r.profiles), named("Endpoints", r.doors)]) if (line) lines.push(line);
|
|
65
|
-
const m = r.machine;
|
|
66
|
-
if (m) {
|
|
67
|
-
lines.push(`Engineering standards: ${[has(m.decided) && `${num(m.decided)} decided`, has(m.met) && `${num(m.met)} met`, has(m.missed) && `${num(m.missed)} missed`].filter(Boolean).join(", ")}.`);
|
|
68
|
-
const misses = m.misses ?? [];
|
|
69
|
-
for (const x of misses.slice(0, 12)) lines.push(` ${x.path ? `${at(x.path, x.line)} ` : ""}${x.standard}${x.detail ? `: ${clip(x.detail, 240)}` : ""}${x.decidedBy ? ` (decided by ${x.decidedBy === "code" ? "code" : "a model"})` : ""}`);
|
|
70
|
-
if (misses.length > 12) lines.push(` and ${misses.length - 12} more.`);
|
|
71
|
-
}
|
|
72
|
-
const q = r.questions;
|
|
73
|
-
if (q) lines.push(`Questions: ${num(q.total)}${has(q.everywhere) ? `; ${num(q.everywhere)} asked in every conversation` : ""}${has(q.placed) ? `, ${num(q.placed)} placed in the situations they fit` : ""}.`);
|
|
74
|
-
const t = r.trials;
|
|
75
|
-
if (t) {
|
|
76
|
-
lines.push(`Trials: ${[has(t.written) && `${num(t.written)} written`, has(t.playable) && `${num(t.playable)} playable`].filter(Boolean).join(", ")}.`);
|
|
77
|
-
for (const h of t.held ?? []) lines.push(` ${h.surface}: ${plural(h.n, "trial")} held back${SIDE[h.side] ? `, ${SIDE[h.side]}` : ""}${h.why ? `: ${h.why}` : ""}`);
|
|
78
|
-
}
|
|
79
|
-
return lines;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
42
|
function runLines(d, prefix = "") {
|
|
83
43
|
const done = finished(d);
|
|
84
44
|
const played = d.of ? `${num(d.played ?? 0)} of ${plural(d.of, "trial")} played` : "no trial played yet";
|
|
@@ -190,12 +150,7 @@ export function linesOf(d) {
|
|
|
190
150
|
return [...merged.values()];
|
|
191
151
|
}
|
|
192
152
|
|
|
193
|
-
export
|
|
194
|
-
const pages = findingsPages(d);
|
|
195
|
-
const n = Math.min(Math.max(1, Math.floor(Number(page)) || 1), pages.length);
|
|
196
|
-
const tail = pages.length === 1 ? "" : n < pages.length ? `\npage ${n} of ${pages.length}, call findings with page ${n + 1}` : `\npage ${n} of ${pages.length}`;
|
|
197
|
-
return `${pages[n - 1]}${tail}`;
|
|
198
|
-
}
|
|
153
|
+
export const findingsText = (d, page = 1) => pageOf(findingsPages(d), page, (n) => `findings with page ${n}`);
|
|
199
154
|
|
|
200
155
|
function findingsHead(d) {
|
|
201
156
|
const lines = [`Run ${d.runId}: ${plural(d.findings?.length ?? 0, "finding")}.`];
|
package/lib/verbs.mjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { realpathSync, statSync } from "node:fs";
|
|
2
2
|
import { resolve as resolvePath, sep } from "node:path";
|
|
3
|
+
import { SHOW, showText } from "./read-text.mjs";
|
|
3
4
|
import { fieldConnectText, fieldText, findingsText, linesOf, refusedText, runText, STARTING, startedText, statusText, waitingText } from "./text.mjs";
|
|
4
5
|
|
|
5
6
|
// The eight things a coding agent may ask of Cortad, each a call to the API with this machine's key
|
|
@@ -40,9 +41,12 @@ export function makeVerbs({ api, token, fetchImpl = fetch, startApp, pending, ro
|
|
|
40
41
|
const failed = (res) => ({ text: res.data?.why ?? res.data?.error ?? `Cortad answered ${res.status}.`, data: res.data, isError: true });
|
|
41
42
|
const iso = () => new Date(now()).toISOString();
|
|
42
43
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
44
|
+
// With show, one section of the read in full, paged; a name that is not a section is the plain status.
|
|
45
|
+
const status = async ({ show, page } = {}) => {
|
|
46
|
+
const section = SHOW.includes(show) ? show : null;
|
|
47
|
+
const res = await call("GET", `/mcp/status${section ? `?show=${section}` : ""}`);
|
|
48
|
+
if (!res.ok) return failed(res);
|
|
49
|
+
return { text: section ? showText(res.data, section, page) : statusText(res.data), data: res.data };
|
|
46
50
|
};
|
|
47
51
|
|
|
48
52
|
// Asks the server for the run and keeps its id here, so run_status finds it without one.
|
package/lib/words.mjs
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// The small pieces every rendering shares: numbers with their units, file and line, whose side a
|
|
2
|
+
// stop is on, and the pages a long answer is cut into.
|
|
3
|
+
|
|
4
|
+
// Copilot cuts a result past about 10 KB; a page stays under this.
|
|
5
|
+
export const PAGE_CHARS = 9000;
|
|
6
|
+
export const SIDE = { theirs: "on the app's side", ours: "on Cortad's side" };
|
|
7
|
+
|
|
8
|
+
export const has = (v) => v !== undefined && v !== null;
|
|
9
|
+
export const num = (x) => (typeof x === "number" ? Math.round(x).toLocaleString("en-US") : String(x));
|
|
10
|
+
export const plural = (n, one, many = `${one}s`) => `${num(n)} ${n === 1 ? one : many}`;
|
|
11
|
+
export const upper = (s) => `${s.charAt(0).toUpperCase()}${s.slice(1)}`;
|
|
12
|
+
export const at = (path, line) => `${path}${has(line) ? `:${line}` : ""}`;
|
|
13
|
+
export const clip = (s, max) => (String(s).length > max ? `${String(s).slice(0, max - 3)}...` : String(s));
|
|
14
|
+
|
|
15
|
+
// One page of many, with the call that reads the next one: `next(2)` gives "findings with page 2".
|
|
16
|
+
export function pageOf(pages, page, next) {
|
|
17
|
+
const n = Math.min(Math.max(1, Math.floor(Number(page)) || 1), pages.length);
|
|
18
|
+
const tail = pages.length === 1 ? "" : n < pages.length ? `\npage ${n} of ${pages.length}, call ${next(n + 1)}` : `\npage ${n} of ${pages.length}`;
|
|
19
|
+
return `${pages[n - 1]}${tail}`;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Lines packed into pages under PAGE_CHARS; every page after the first opens with `continued`.
|
|
23
|
+
export function pack(head, lines, continued, budget = PAGE_CHARS - 80) {
|
|
24
|
+
const pages = [];
|
|
25
|
+
let page = head;
|
|
26
|
+
let filled = false;
|
|
27
|
+
for (const line of lines) {
|
|
28
|
+
if (filled && page.length + line.length + 1 > budget) {
|
|
29
|
+
pages.push(page);
|
|
30
|
+
page = continued;
|
|
31
|
+
filled = false;
|
|
32
|
+
}
|
|
33
|
+
page += `\n${line}`;
|
|
34
|
+
filled = true;
|
|
35
|
+
}
|
|
36
|
+
return [...pages, page];
|
|
37
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cortad",
|
|
3
|
-
"version": "0.3.0-rc.
|
|
3
|
+
"version": "0.3.0-rc.3",
|
|
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"
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
"lib/mcp.mjs",
|
|
18
18
|
"lib/mint.mjs",
|
|
19
19
|
"lib/pyhook/sitecustomize.py",
|
|
20
|
+
"lib/read-text.mjs",
|
|
20
21
|
"lib/register.mjs",
|
|
21
22
|
"lib/replay.mjs",
|
|
22
23
|
"lib/sample.mjs",
|
|
@@ -28,6 +29,7 @@
|
|
|
28
29
|
"lib/text.mjs",
|
|
29
30
|
"lib/trace.cjs",
|
|
30
31
|
"lib/verbs.mjs",
|
|
32
|
+
"lib/words.mjs",
|
|
31
33
|
"local.mjs",
|
|
32
34
|
"skill/SKILL.md",
|
|
33
35
|
"skill/references/results.md"
|
package/skill/SKILL.md
CHANGED
|
@@ -29,6 +29,8 @@ The connect command ends with "Go back to the browser". From then on `status` ca
|
|
|
29
29
|
4. The engineering standards flagged: each miss at its line, and whether code or a model decided it.
|
|
30
30
|
5. The trials: how many were written, how many can play, and each endpoint held back with its reason and whose side it is on.
|
|
31
31
|
|
|
32
|
+
Each section's full list is one `status` call away with `show`: `rules`, `standards`, `journeys`, `endpoints` or `trials` (in a shell, `npx {{cortad}} status rules`). A long list comes in pages, and the last line names the next page.
|
|
33
|
+
|
|
32
34
|
Then the findings, then the fixes.
|
|
33
35
|
|
|
34
36
|
## Following a run
|
|
@@ -44,6 +44,28 @@ For the person: https://cortad.com/lab shows this in the browser.
|
|
|
44
44
|
next: run_status 8f2a1c4e-5b6d-4e7f-9a0b-1c2d3e4f5a6b
|
|
45
45
|
```
|
|
46
46
|
|
|
47
|
+
Each section in full, with `show` (`rules`, `standards`, `journeys`, `endpoints` or `trials`). A list longer than one page ends with `page 1 of 3, call status with show rules and page 2`.
|
|
48
|
+
|
|
49
|
+
```
|
|
50
|
+
Rules in the code: 16, in 5 files.
|
|
51
|
+
apps/api/src/agent/prompt.ts:41 "Never state a refund policy the product does not publish."
|
|
52
|
+
apps/api/src/agent/prompt.ts:44 "Point billing questions to the billing page."
|
|
53
|
+
apps/api/src/agent/system.ts:12 "Answer in the language the student writes in."
|
|
54
|
+
apps/api/src/agent/system.ts:15 "Keep an answer for a grade 9 student to grade 9 words."
|
|
55
|
+
apps/api/src/agent/system.ts:19 "Ask which lesson the question is about before answering it."
|
|
56
|
+
apps/api/src/agent/system.ts:22 "Give the method before the answer on homework."
|
|
57
|
+
apps/api/src/agent/system.ts:30 "Leave the student's name out of the reply."
|
|
58
|
+
apps/api/src/tools/search.ts:8 "Cite the lesson a fact comes from."
|
|
59
|
+
apps/api/src/tools/search.ts:14 "Say so when search finds nothing, rather than answering from memory."
|
|
60
|
+
apps/api/src/tools/search.ts:17 "Quote at most two sentences from a lesson."
|
|
61
|
+
apps/api/src/agent/handoff.ts:6 "Hand a refund request to a person."
|
|
62
|
+
apps/api/src/agent/handoff.ts:9 "Hand an account recovery to a person once the email is confirmed."
|
|
63
|
+
apps/api/src/agent/billing.ts:21 "Tell a parent what the plan costs only from the pricing page."
|
|
64
|
+
apps/api/src/agent/billing.ts:27 "Never promise a teacher a feature that is not released."
|
|
65
|
+
apps/web/src/chat/welcome.ts:3 "Greet a first lesson with what the student can ask."
|
|
66
|
+
apps/web/src/chat/welcome.ts:11 "End each homework answer with one practice question."
|
|
67
|
+
```
|
|
68
|
+
|
|
47
69
|
## run
|
|
48
70
|
|
|
49
71
|
The app is up:
|