cortad 0.3.1-rc.9 → 0.3.2
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/README.md +8 -5
- package/lib/app-port.mjs +9 -0
- package/lib/auto-reach.mjs +88 -0
- package/lib/backing.mjs +230 -0
- package/lib/calls.mjs +23 -0
- package/lib/cli.mjs +115 -30
- package/lib/contact-text.mjs +167 -0
- package/lib/data.mjs +16 -22
- package/lib/dissection-text.mjs +232 -0
- package/lib/document.mjs +75 -0
- package/lib/env-secrets.mjs +48 -0
- package/lib/failed-text.mjs +36 -0
- package/lib/fresh.mjs +33 -12
- package/lib/health.mjs +61 -0
- package/lib/held.mjs +67 -0
- package/lib/home.mjs +55 -17
- package/lib/hosted.mjs +201 -0
- package/lib/identities.mjs +1 -1
- package/lib/keeping.mjs +162 -0
- package/lib/ledger.mjs +20 -0
- package/lib/lock.mjs +6 -1
- package/lib/mcp.mjs +52 -18
- package/lib/mint.mjs +179 -38
- package/lib/mongo.mjs +74 -0
- package/lib/numbers-text.mjs +68 -0
- package/lib/pg.mjs +118 -0
- package/lib/proc.mjs +100 -0
- package/lib/proof.mjs +1014 -0
- package/lib/proxy.mjs +184 -0
- package/lib/pyhook/cortad_sites.py +218 -0
- package/lib/pyhook/cortad_tied.py +291 -0
- package/lib/pyhook/sitecustomize.py +1175 -166
- package/lib/qdrant.mjs +47 -0
- package/lib/reach.mjs +223 -0
- package/lib/read-text.mjs +78 -24
- package/lib/redis.mjs +130 -0
- package/lib/register.mjs +36 -27
- package/lib/replay.mjs +464 -34
- package/lib/runner.mjs +85 -0
- package/lib/service.mjs +25 -0
- package/lib/spec.mjs +7 -4
- package/lib/start.mjs +33 -2
- package/lib/stick.mjs +65 -25
- package/lib/stores.mjs +347 -0
- package/lib/switches.mjs +36 -0
- package/lib/tether.mjs +9 -0
- package/lib/text.mjs +314 -179
- package/lib/tied.cjs +217 -0
- package/lib/trace.cjs +386 -514
- package/lib/verbs.mjs +155 -38
- package/lib/wire.cjs +882 -0
- package/lib/words.mjs +2 -0
- package/lib/writes.mjs +2 -2
- package/local.mjs +661 -297
- package/package.json +31 -1
- package/skill/SKILL.md +49 -42
- package/skill/references/results.md +279 -102
package/lib/verbs.mjs
CHANGED
|
@@ -1,25 +1,44 @@
|
|
|
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";
|
|
4
|
-
import {
|
|
3
|
+
import { machineText, SHOW, showText } from "./read-text.mjs";
|
|
4
|
+
import { numbersText } from "./numbers-text.mjs";
|
|
5
|
+
import { askReach } from "./auto-reach.mjs";
|
|
6
|
+
import { homeOf, projectOf } from "./home.mjs";
|
|
7
|
+
import { reach as reachApp } from "./reach.mjs";
|
|
8
|
+
import { fieldConnectText, fieldText, findingsText, finished, linesOf, notStartedText, refusedText, restartedText, runText, STARTING, startedText, statusText, testedLine, upWaitingText, waitingText } from "./text.mjs";
|
|
5
9
|
|
|
6
|
-
// The
|
|
7
|
-
// and a rendering from lib/text.mjs
|
|
8
|
-
// functions, so both faces say the same thing.
|
|
10
|
+
// The ten things a coding agent may ask of Cortad, each a call to the API with this machine's key
|
|
11
|
+
// and a rendering from lib/text.mjs; `reach` also sends to the app on this machine. The MCP tools
|
|
12
|
+
// and the `npx cortad <verb>` commands are the same functions, so both faces say the same thing.
|
|
9
13
|
//
|
|
10
|
-
// Codex, Cursor CLI and Copilot CLI cut an MCP call at about 60 seconds. `
|
|
11
|
-
//
|
|
14
|
+
// Codex, Cursor CLI and Copilot CLI cut an MCP call at about 60 seconds. `run_status` holds for up
|
|
15
|
+
// to 45 seconds on the server and 58 in all. `run` and `verify` answer within a second, except while
|
|
16
|
+
// the run before them has its numbers in and its job is still ending: the server holds them until it
|
|
17
|
+
// has, up to the same 45 seconds.
|
|
12
18
|
|
|
13
19
|
const NOT_CONNECTED = "This folder is not connected to Cortad.\nFor the person: sign in at cortad.com and run the command the connect screen shows, in this folder.";
|
|
14
20
|
const HOLD_S = 45;
|
|
21
|
+
// `wait`: how often it asks whether a run has started, how long the connect command may be gone
|
|
22
|
+
// before no run can start, and how long it waits for one at all.
|
|
23
|
+
const WAIT_POLL_MS = 10_000;
|
|
24
|
+
const GONE_MS = 60_000;
|
|
25
|
+
const IDLE_MS = 6 * 3600_000;
|
|
15
26
|
// What the process that starts a run for an app that is still coming up allows it; lib/cli.mjs.
|
|
16
27
|
export const READY_MS = 240_000;
|
|
28
|
+
// The most changed files a run is scoped by; past it the run replays everything.
|
|
29
|
+
const MOST_CHANGED = 2000;
|
|
17
30
|
|
|
18
31
|
// `pending` keeps the run this machine asked for in ~/.cortad/<project>/pending.json: { kind,
|
|
19
32
|
// startedAt, after } while the app comes up, then its jobId, or the error that ended it. `after` is
|
|
20
33
|
// the latest run at the time, so a newer run on the server outranks a stale file. A run this machine
|
|
21
34
|
// posted also keeps `tested`, which code it played (lib/fresh.mjs), for its run_status and findings.
|
|
22
|
-
|
|
35
|
+
// `runner` is the record of the process holding the app up on this machine (lib/runner.mjs): the
|
|
36
|
+
// app's state is read there and nowhere else. `seen` is told each run the agent has now read to its
|
|
37
|
+
// end, so what was kept to tell it of that run is not said again. `wasRead` says whether the agent has read a run, and
|
|
38
|
+
// `spec` is the package the agent calls back, npx -y <spec>.
|
|
39
|
+
// `appLog` is the file the connect command keeps the app's output in; its path is said here, on this
|
|
40
|
+
// machine, beside a failure, and never sent.
|
|
41
|
+
export function makeVerbs({ api, token, runner = () => null, fetchImpl = fetch, startApp, pending, root, writes = null, appLog = null, code = null, seen = () => {}, wasRead = () => false, spec = "cortad", now = Date.now, sleep = (ms) => new Promise((r) => setTimeout(r, ms)) }) {
|
|
23
42
|
const call = async (method, path, body, timeoutMs = 30_000) => {
|
|
24
43
|
const key = typeof token === "function" ? token() : token;
|
|
25
44
|
if (!key) return { status: 0, ok: false, data: { error: NOT_CONNECTED } };
|
|
@@ -39,8 +58,17 @@ export function makeVerbs({ api, token, fetchImpl = fetch, startApp, pending, ro
|
|
|
39
58
|
try { data = text ? JSON.parse(text) : null; } catch { data = { error: text.slice(0, 200) }; }
|
|
40
59
|
return { status: res.status, ok: res.ok, data };
|
|
41
60
|
};
|
|
42
|
-
|
|
61
|
+
// `code` and `why` are what lib/calls.mjs counts a failure under. `why` is set only where the
|
|
62
|
+
// sentence is Cortad's own: a failure that quotes the app's output carries a code and no words.
|
|
63
|
+
const failed = (res) => {
|
|
64
|
+
const text = res.data?.why ?? res.data?.error ?? `Cortad answered ${res.status}.`;
|
|
65
|
+
const code = res.status ? `http_${res.status}` : text === NOT_CONNECTED ? "not_connected" : "unreachable";
|
|
66
|
+
return { text, data: res.data, isError: true, code, why: text };
|
|
67
|
+
};
|
|
43
68
|
const iso = () => new Date(now()).toISOString();
|
|
69
|
+
const heard = (id) => { try { seen(String(id)); } catch { /* said once more by the hook */ } };
|
|
70
|
+
// Empty for an app the connect command found already running.
|
|
71
|
+
const logSaid = (failing) => (failing && appLog && sizeOf(appLog) > 0 ? `Your app's whole output from this session is in ${appLog}.` : "");
|
|
44
72
|
const withTested = (d, id) => {
|
|
45
73
|
const kept = pending.read();
|
|
46
74
|
return kept?.tested && kept.jobId === id ? { ...d, tested: kept.tested } : d;
|
|
@@ -48,16 +76,37 @@ export function makeVerbs({ api, token, fetchImpl = fetch, startApp, pending, ro
|
|
|
48
76
|
|
|
49
77
|
// With show, one section of the read in full, paged; a name that is not a section is the plain status.
|
|
50
78
|
const status = async ({ show, page } = {}) => {
|
|
79
|
+
// What the connect command does here is this machine's to say: nothing is sent.
|
|
80
|
+
if (show === "machine") return { text: machineText(runner()?.machine ?? null).join("\n") };
|
|
51
81
|
const section = SHOW.includes(show) ? show : null;
|
|
52
|
-
|
|
82
|
+
// The plain status is the one read that prints the server's news, and says so; a section prints
|
|
83
|
+
// none, so it takes none.
|
|
84
|
+
// The reviews are written when the read ends: asked for by name, the answer holds for that.
|
|
85
|
+
const hold = section === "reviews" ? 45 : 0;
|
|
86
|
+
const res = await call("GET", `/mcp/status${section ? `?show=${section}&quiet=1${hold ? `&wait=${hold}` : ""}` : "?news=1"}`, undefined, hold ? (hold + 13) * 1000 : undefined);
|
|
53
87
|
if (!res.ok) return failed(res);
|
|
54
|
-
|
|
88
|
+
// Status says a finished run itself, so the line kept to tell the agent of it is not said again.
|
|
89
|
+
if (finished(res.data?.run) && res.data.run.jobId) heard(res.data.run.jobId);
|
|
90
|
+
const here = runner();
|
|
91
|
+
const data = here ? { ...res.data, runner: here } : res.data;
|
|
92
|
+
const log = section ? "" : logSaid(appFailed(data.run) || (data.contact?.proven ?? []).some((v) => v.problems?.length));
|
|
93
|
+
const text = section ? showText(data, section, page) : statusText(data);
|
|
94
|
+
return { text: log ? withBack(text, log) : text, data };
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
// The files saved since the run the next one is compared with (status `baseline`), which scope a
|
|
98
|
+
// run after a change on Cortad's side. None when this machine cannot say: the run replays everything.
|
|
99
|
+
const changeOf = async (seen) => {
|
|
100
|
+
const base = (seen ?? (await call("GET", "/mcp/status?quiet=1")).data)?.baseline;
|
|
101
|
+
const files = base?.jobId ? code?.saved?.(base.startedAt) : null;
|
|
102
|
+
return files && files.length <= MOST_CHANGED ? { since: base.jobId, files } : null;
|
|
55
103
|
};
|
|
56
104
|
|
|
57
105
|
// Asks the server for the run and keeps its id here, so run_status finds it without one.
|
|
58
|
-
const post = async (args, kind, restarted = []) => {
|
|
106
|
+
const post = async (args, kind, restarted = [], seen = null) => {
|
|
59
107
|
const tested = code?.tested() ?? null;
|
|
60
|
-
const
|
|
108
|
+
const change = kind === "run" ? await changeOf(seen) : null;
|
|
109
|
+
const res = await call("POST", "/mcp/run", change ? { ...args, change } : args, (HOLD_S + 13) * 1000);
|
|
61
110
|
if (res.status === 202 || (res.ok && res.data?.joined)) {
|
|
62
111
|
const mine = res.status === 202 ? tested : null;
|
|
63
112
|
pending.write({ jobId: res.data.jobId, kind: res.data.kind ?? kind, startedAt: iso(), ...(mine ? { tested: mine } : {}) });
|
|
@@ -65,30 +114,33 @@ export function makeVerbs({ api, token, fetchImpl = fetch, startApp, pending, ro
|
|
|
65
114
|
const said = [restarted.length ? restartedText(restarted) : "", mine?.stale ? testedLine(mine) : ""];
|
|
66
115
|
return { text: startedText(res.data, kind, args.findingId, said), data: res.data };
|
|
67
116
|
}
|
|
68
|
-
if (res.status === 402) return { text: refusedText(res.data), data: res.data, isError: true };
|
|
69
|
-
if (res.status === 409) return { text: `${res.data?.why ?? "The run could not start."}\nNothing ran.`, data: res.data, isError: true };
|
|
117
|
+
if (res.status === 402) return { text: refusedText(res.data), data: res.data, isError: true, code: "plan_spent" };
|
|
118
|
+
if (res.status === 409) return { text: `${res.data?.why ?? "The run could not start."}\nNothing ran.`, data: res.data, isError: true, code: "run_refused" };
|
|
70
119
|
return failed(res);
|
|
71
120
|
};
|
|
72
121
|
|
|
73
|
-
// An app
|
|
74
|
-
// reloaded it; never under a run already playing, which is
|
|
75
|
-
//
|
|
122
|
+
// An app the runner says is up, and the server is ready to play, gets the run now, started again
|
|
123
|
+
// first when its code changed and nothing reloaded it; never under a run already playing, which is
|
|
124
|
+
// joined. One that could not start says why and nothing is posted. Otherwise the runner is started
|
|
125
|
+
// if none holds the app, and the run is posted in the background once it can be.
|
|
76
126
|
const start = async (args, kind) => {
|
|
77
|
-
const
|
|
127
|
+
const here = runner();
|
|
128
|
+
if (here?.state === "failed") return { text: notStartedText(here), data: { app: here }, isError: true, code: "app_not_started" };
|
|
129
|
+
const before = await call("GET", "/mcp/status?quiet=1");
|
|
78
130
|
if (!before.ok) return failed(before);
|
|
79
|
-
if (before.data.app?.state === "ready-to-test") {
|
|
131
|
+
if (here?.state === "up" && before.data.app?.state === "ready-to-test") {
|
|
80
132
|
const idle = !before.data.run || finished(before.data.run);
|
|
81
133
|
const got = code && idle ? await code.fresh() : {};
|
|
82
|
-
if (got.error) return { text: `${got.error}\nNothing ran.`, isError: true };
|
|
83
|
-
return post(args, kind, got.restarted);
|
|
134
|
+
if (got.error) return { text: `${got.error}\nNothing ran.`, isError: true, code: "app_not_started" };
|
|
135
|
+
return post(args, kind, got.restarted, before.data);
|
|
84
136
|
}
|
|
85
137
|
pending.write({ kind, startedAt: iso(), after: before.data.run?.jobId ?? null });
|
|
86
138
|
const began = await startApp(args, kind);
|
|
87
139
|
if (!began.ok) {
|
|
88
140
|
pending.write({ kind, startedAt: iso(), error: began.why });
|
|
89
|
-
return { text: began.why, isError: true };
|
|
141
|
+
return { text: began.why, isError: true, code: "app_not_started" };
|
|
90
142
|
}
|
|
91
|
-
return { text: STARTING, data: { pending: true } };
|
|
143
|
+
return { text: here?.state === "up" ? upWaitingText(here, before.data.app) : STARTING, data: { pending: true } };
|
|
92
144
|
};
|
|
93
145
|
const run = () => start({}, "run");
|
|
94
146
|
const verify = ({ findingId, jobId } = {}) => start({ findingId, ...(jobId ? { jobId } : {}) }, "verify");
|
|
@@ -96,7 +148,7 @@ export function makeVerbs({ api, token, fetchImpl = fetch, startApp, pending, ro
|
|
|
96
148
|
// With no id: the run this machine is still starting, held here until it has an id, or else the
|
|
97
149
|
// latest run on the server.
|
|
98
150
|
const resolve = async (until) => {
|
|
99
|
-
const latest = await call("GET", "/mcp/status");
|
|
151
|
+
const latest = await call("GET", "/mcp/status?quiet=1");
|
|
100
152
|
if (!latest.ok) return failed(latest);
|
|
101
153
|
const newest = latest.data.run?.jobId ?? null;
|
|
102
154
|
let p = pending.read();
|
|
@@ -107,9 +159,9 @@ export function makeVerbs({ api, token, fetchImpl = fetch, startApp, pending, ro
|
|
|
107
159
|
p = pending.read() ?? p;
|
|
108
160
|
}
|
|
109
161
|
if (p.jobId) return { id: p.jobId };
|
|
110
|
-
if (p.error) return { text: `${p.error}\nnext: status`, isError: true };
|
|
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) };
|
|
162
|
+
if (p.error) return { text: `${p.error}\nnext: status`, isError: true, code: "app_not_started" };
|
|
163
|
+
if (stale()) return { text: "The process starting your app for the run ended without a result.\nNothing ran.\nnext: status", isError: true, code: "app_not_started" };
|
|
164
|
+
return { text: waitingText(p, now(), READY_MS, runner(), latest.data.app) };
|
|
113
165
|
};
|
|
114
166
|
|
|
115
167
|
const runStatus = async ({ jobId } = {}) => {
|
|
@@ -124,13 +176,48 @@ export function makeVerbs({ api, token, fetchImpl = fetch, startApp, pending, ro
|
|
|
124
176
|
const res = await call("GET", `/mcp/run/${encodeURIComponent(id)}${hold ? `?wait=${hold}` : ""}`, undefined, (hold + 13) * 1000);
|
|
125
177
|
if (!res.ok) {
|
|
126
178
|
const out = failed(res);
|
|
127
|
-
|
|
179
|
+
// Unreached is this terminal's side only: the run is on Cortad's servers and goes on without it.
|
|
180
|
+
const said = res.status === 0 ? `${out.text} The run is on Cortad's side and goes on without this call; nothing was lost or charged by it.` : out.text;
|
|
181
|
+
return { ...out, text: `${said}\nnext: run_status ${id}` };
|
|
128
182
|
}
|
|
183
|
+
// A finished run's dissection is also handed over as data, for a client that reads structured results.
|
|
184
|
+
return { text: shown(res.data, id), data: res.data, ...(res.data?.dissection ? { structured: res.data.dissection } : {}) };
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
const shown = (d, id) => {
|
|
129
188
|
// The run is over: what the app wrote into its folder during it is put back, said before the next step.
|
|
130
189
|
let back = "";
|
|
131
|
-
if (writes && finished(
|
|
132
|
-
|
|
133
|
-
|
|
190
|
+
if (writes && finished(d)) { try { back = writes.putBack(); } catch { back = ""; } }
|
|
191
|
+
// The id asked for and the run the answer names: a verify that played another round names its latest.
|
|
192
|
+
if (finished(d)) { heard(id); if (d?.jobId && d.jobId !== id) heard(d.jobId); }
|
|
193
|
+
return [logSaid(appFailed(d)), back].filter(Boolean).reduce(withBack, runText(withTested(d, id)));
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
// `npx cortad wait`: returns when the next run ends, started from the browser or by the agent, with
|
|
197
|
+
// what it found. A coding agent starts it in the background and is woken when it exits; it is a shell
|
|
198
|
+
// verb alone, since an MCP call is cut at about 60 seconds. A run playing when it starts is that run,
|
|
199
|
+
// and so is a finished one the agent has not read: a first run can end before the agent starts this.
|
|
200
|
+
const wait = async () => {
|
|
201
|
+
if (!(typeof token === "function" ? token() : token)) return { text: NOT_CONNECTED, isError: true, code: "not_connected", why: NOT_CONNECTED };
|
|
202
|
+
const began = now();
|
|
203
|
+
let id = null;
|
|
204
|
+
let goneAt = null;
|
|
205
|
+
for (;;) {
|
|
206
|
+
if (runner()) goneAt = null;
|
|
207
|
+
else if (now() - (goneAt ??= now()) >= GONE_MS) return { text: "The connect command stopped, so no run can start from the browser.\nFor the person: run the connect command again in this folder.\nnext: status", isError: true };
|
|
208
|
+
if (now() - began >= IDLE_MS) return { text: `No run ended in 6 hours.\nnext: npx -y ${spec} wait` };
|
|
209
|
+
const res = id ? await call("GET", `/mcp/run/${encodeURIComponent(id)}?wait=${HOLD_S}`, undefined, (HOLD_S + 13) * 1000) : await call("GET", "/mcp/status?quiet=1");
|
|
210
|
+
// A key that no longer opens this repository, or a run that is gone, does not come back by asking again.
|
|
211
|
+
if ([401, 403, 404].includes(res.status)) return failed(res);
|
|
212
|
+
if (res.ok && id && finished(res.data)) {
|
|
213
|
+
const text = shown(res.data, id).replace(/\nnext: [^\n]*$/, "");
|
|
214
|
+
return { text: `${text}\nnext: findings ${res.data.jobId ?? id}`, data: res.data };
|
|
215
|
+
}
|
|
216
|
+
const r = res.ok && !id ? res.data?.run : null;
|
|
217
|
+
if (r?.jobId && r.kind !== "verify" && (!finished(r) || !wasRead(r.jobId))) { id = r.jobId; continue; }
|
|
218
|
+
// A run being followed holds on the server; a failed call, or no run yet, is asked again later.
|
|
219
|
+
if (!id || !res.ok) await sleep(WAIT_POLL_MS);
|
|
220
|
+
}
|
|
134
221
|
};
|
|
135
222
|
|
|
136
223
|
// Every server page of a run's findings, so the pages the agent reads are cut by size here.
|
|
@@ -157,8 +244,15 @@ export function makeVerbs({ api, token, fetchImpl = fetch, startApp, pending, ro
|
|
|
157
244
|
return first;
|
|
158
245
|
};
|
|
159
246
|
|
|
160
|
-
|
|
247
|
+
// With show numbers: every number the run measured, whole, paged here.
|
|
248
|
+
const findings = async ({ jobId, page, show } = {}) => {
|
|
249
|
+
if (show === "numbers") {
|
|
250
|
+
const q = new URLSearchParams({ show, ...(jobId ? { jobId } : {}) }).toString();
|
|
251
|
+
const res = await call("GET", `/mcp/findings?${q}`);
|
|
252
|
+
return res.ok ? { text: numbersText(res.data, page), data: res.data } : failed(res);
|
|
253
|
+
}
|
|
161
254
|
const res = await gather(jobId);
|
|
255
|
+
if (res.ok && res.data.runId) heard(res.data.runId);
|
|
162
256
|
return res.ok ? { text: findingsText(withTested(res.data, res.data.runId), page), data: res.data } : failed(res);
|
|
163
257
|
};
|
|
164
258
|
|
|
@@ -167,6 +261,23 @@ export function makeVerbs({ api, token, fetchImpl = fetch, startApp, pending, ro
|
|
|
167
261
|
return res.ok ? { text: res.data.said, data: res.data } : failed(res);
|
|
168
262
|
};
|
|
169
263
|
|
|
264
|
+
// A note about Cortad itself, filed where the browser's feedback box files, for the Cortad team to read.
|
|
265
|
+
const feedback = async ({ about, kind, needed, got, tried }) => {
|
|
266
|
+
const res = await call("POST", "/mcp/feedback", { about, kind, needed, ...(got ? { got } : {}), ...(tried ? { tried } : {}) });
|
|
267
|
+
return res.ok ? { text: res.data.said, data: res.data } : failed(res);
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
// Cortad's test request to each endpoint not yet answered, from this machine (lib/reach.mjs). The
|
|
271
|
+
// connect command sends it, signed in where it signed a test account in; the server is asked only
|
|
272
|
+
// for the list.
|
|
273
|
+
const reach = async ({ paths } = {}) => {
|
|
274
|
+
const here = runner();
|
|
275
|
+
if (here?.state === "up" && here.reach === "ask" && root) return askReach(homeOf(projectOf(root)), paths);
|
|
276
|
+
const res = here?.state === "up" ? await call("GET", "/mcp/status?quiet=1") : null;
|
|
277
|
+
if (res && !res.ok) return failed(res);
|
|
278
|
+
return reachApp({ runner: here, contact: res?.data?.contact, paths, fetchImpl });
|
|
279
|
+
};
|
|
280
|
+
|
|
170
281
|
const fieldConnect = async () => {
|
|
171
282
|
const res = await call("POST", "/mcp/field/connect");
|
|
172
283
|
return res.ok ? { text: fieldConnectText(res.data), data: res.data } : failed(res);
|
|
@@ -178,9 +289,10 @@ export function makeVerbs({ api, token, fetchImpl = fetch, startApp, pending, ro
|
|
|
178
289
|
};
|
|
179
290
|
|
|
180
291
|
// `status --changed`: the files Cortad cites that changed since the latest run started, with the
|
|
181
|
-
// findings at them. Empty when there is nothing to say, or nothing could be asked.
|
|
292
|
+
// findings at them. Empty when there is nothing to say, or nothing could be asked. The code review's
|
|
293
|
+
// files are those of the review shown: the read's misses, or the lines Cortad's team's review cites.
|
|
182
294
|
const changed = async () => {
|
|
183
|
-
const [s, f] = await Promise.all([call("GET", "/mcp/status"), gather()]);
|
|
295
|
+
const [s, f] = await Promise.all([call("GET", "/mcp/status?quiet=1"), gather()]);
|
|
184
296
|
const r = s.ok ? s.data?.run : null;
|
|
185
297
|
if (!r) return { text: "" };
|
|
186
298
|
const kept = pending.read();
|
|
@@ -190,18 +302,23 @@ export function makeVerbs({ api, token, fetchImpl = fetch, startApp, pending, ro
|
|
|
190
302
|
const paths = [
|
|
191
303
|
...lines.map((l) => l.path),
|
|
192
304
|
...(read.rules?.examples ?? []).map((e) => e.path),
|
|
193
|
-
...(read.machine?.misses ?? []).map((m) => m.path),
|
|
305
|
+
...(read.machine?.misses ?? read.machine?.cited ?? []).map((m) => m.path),
|
|
194
306
|
...(Array.isArray(read.rules?.files) ? read.rules.files : []),
|
|
195
307
|
].filter((p) => typeof p === "string" && p);
|
|
196
308
|
return { text: changedLine({ root, since, runId: r.jobId, paths, lines }) };
|
|
197
309
|
};
|
|
198
310
|
|
|
199
311
|
// `post` and `changed` serve lib/cli.mjs; the MCP answers only the names in TOOLS.
|
|
200
|
-
return { status, run, run_status: runStatus, findings, verify, dispute, field_connect: fieldConnect, field, post, changed };
|
|
312
|
+
return { status, reach, run, run_status: runStatus, wait, findings, verify, dispute, feedback, field_connect: fieldConnect, field, post, changed };
|
|
201
313
|
}
|
|
202
314
|
|
|
315
|
+
const sizeOf = (path) => { try { return statSync(path).size; } catch { return 0; } };
|
|
316
|
+
|
|
317
|
+
// A run whose replies failed, or with a fault on the app's side, is one whose app output is worth reading.
|
|
318
|
+
const appFailed = (run) => Boolean(run?.failedReplies?.rows?.length || run?.replies?.crashed > 0 || (run?.faults ?? []).some((f) => f.side === "theirs"));
|
|
319
|
+
|
|
203
320
|
// Only paths inside the repository are looked at; a path from the server never reaches outside it.
|
|
204
|
-
//
|
|
321
|
+
// A line said after a result sits above the next step, never after it.
|
|
205
322
|
const withBack = (text, back) => {
|
|
206
323
|
const lines = text.split("\n");
|
|
207
324
|
const at = lines.findIndex((l) => l.startsWith("next:"));
|