cawdev-cli 1.2.1 → 1.2.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/lib/sonar-analysis.mjs +55 -9
- package/mcp/server.mjs +11 -4
- package/package.json +1 -1
- package/runner/README.md +34 -1
- package/runner/attach.mjs +255 -33
- package/runner/runner.mjs +116 -14
- package/runner/session-cards.mjs +228 -16
- package/runner/split.mjs +35 -10
package/lib/sonar-analysis.mjs
CHANGED
|
@@ -4,14 +4,48 @@
|
|
|
4
4
|
// for the server's analysis task. One place, so the two cannot drift (R584).
|
|
5
5
|
// Zero dependencies, like all of tools/.
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
7
|
+
export const SONAR_REQUEST_TIMEOUT_MS = 30_000;
|
|
8
|
+
|
|
9
|
+
const isAbort = (error) =>
|
|
10
|
+
['TimeoutError', 'AbortError'].includes(error?.name) || ['TimeoutError', 'AbortError'].includes(error?.cause?.name);
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* A GET against the SonarQube server a scan just reported to, authorised with the
|
|
14
|
+
* scanner's own token. Every request is bounded (R622): by `requestTimeoutMs`, by
|
|
15
|
+
* the asker's `deadline`, and by a per-call `timeoutMs`, whichever is smallest.
|
|
16
|
+
* A cut-off request throws an error with `code === 'SONAR_TIMEOUT'`, and
|
|
17
|
+
* `byDeadline` true when the deadline or the per-call limit set the limit.
|
|
18
|
+
*/
|
|
19
|
+
export function sonarAsker(serverUrl, token, { requestTimeoutMs = SONAR_REQUEST_TIMEOUT_MS, deadline = Infinity } = {}) {
|
|
20
|
+
return async (path, { timeoutMs } = {}) => {
|
|
21
|
+
const pathOnly = path.split('?')[0];
|
|
22
|
+
const left = Math.min(deadline - Date.now(), timeoutMs ?? Infinity);
|
|
23
|
+
const byDeadline = left < requestTimeoutMs;
|
|
24
|
+
const limit = Math.max(1, Math.min(requestTimeoutMs, left));
|
|
25
|
+
try {
|
|
26
|
+
const response = await fetch(`${serverUrl}${path}`, {
|
|
27
|
+
headers: { authorization: `Bearer ${token}` },
|
|
28
|
+
signal: AbortSignal.timeout(limit),
|
|
29
|
+
});
|
|
30
|
+
if (!response.ok) {
|
|
31
|
+
await response.body?.cancel().catch(() => {});
|
|
32
|
+
throw new Error(`SonarQube answered ${response.status} to ${pathOnly}`);
|
|
33
|
+
}
|
|
34
|
+
return await response.json();
|
|
35
|
+
} catch (error) {
|
|
36
|
+
if (!isAbort(error)) {
|
|
37
|
+
throw error;
|
|
38
|
+
}
|
|
39
|
+
const timeout = new Error(
|
|
40
|
+
byDeadline
|
|
41
|
+
? `SonarQube did not answer ${pathOnly} before the reading's time ran out`
|
|
42
|
+
: `SonarQube did not answer ${pathOnly} within ${Math.ceil(limit / 1000)}s`,
|
|
43
|
+
);
|
|
44
|
+
timeout.code = 'SONAR_TIMEOUT';
|
|
45
|
+
timeout.limitMs = limit;
|
|
46
|
+
timeout.byDeadline = byDeadline;
|
|
47
|
+
throw timeout;
|
|
13
48
|
}
|
|
14
|
-
return response.json();
|
|
15
49
|
};
|
|
16
50
|
}
|
|
17
51
|
|
|
@@ -19,16 +53,28 @@ export function sonarAsker(serverUrl, token) {
|
|
|
19
53
|
* Waits for a queued analysis to settle — shared by every scan path, BRANCH,
|
|
20
54
|
* MAIN and PROJECT alike. Throws rather than reports: every caller runs
|
|
21
55
|
* inside `scanQuality`'s own try/catch, which turns it into an error reading.
|
|
56
|
+
* Each poll is limited to the time left (R622), so one unanswered request
|
|
57
|
+
* cannot outlive the deadline.
|
|
22
58
|
*/
|
|
23
59
|
export async function waitForAnalysis(ask, ceTaskId, deadline, timeoutSeconds) {
|
|
24
60
|
let settled = null;
|
|
25
61
|
while (Date.now() < deadline) {
|
|
26
|
-
|
|
62
|
+
let current;
|
|
63
|
+
try {
|
|
64
|
+
({ task: current } = await ask(`/api/ce/task?id=${encodeURIComponent(ceTaskId)}`, {
|
|
65
|
+
timeoutMs: deadline - Date.now(),
|
|
66
|
+
}));
|
|
67
|
+
} catch (error) {
|
|
68
|
+
if (error?.code === 'SONAR_TIMEOUT' && error.byDeadline) {
|
|
69
|
+
break;
|
|
70
|
+
}
|
|
71
|
+
throw error;
|
|
72
|
+
}
|
|
27
73
|
if (['SUCCESS', 'FAILED', 'CANCELED'].includes(current?.status)) {
|
|
28
74
|
settled = current;
|
|
29
75
|
break;
|
|
30
76
|
}
|
|
31
|
-
await new Promise((wake) => setTimeout(wake, 3000));
|
|
77
|
+
await new Promise((wake) => setTimeout(wake, Math.min(3000, Math.max(0, deadline - Date.now()))));
|
|
32
78
|
}
|
|
33
79
|
if (!settled) {
|
|
34
80
|
throw new Error(`SonarQube had not finished analysing after ${timeoutSeconds}s.`);
|
package/mcp/server.mjs
CHANGED
|
@@ -301,10 +301,14 @@ function renderRounds(where) {
|
|
|
301
301
|
* by counting is an agent one skipped question away from acting on the wrong
|
|
302
302
|
* one.
|
|
303
303
|
*/
|
|
304
|
+
/** R612: the platform closes a question with no person behind it; it reaches us as this. */
|
|
305
|
+
const THE_PLATFORM = '(the platform)';
|
|
306
|
+
const bySomeone = (email) => Boolean(email) && email !== THE_PLATFORM;
|
|
307
|
+
|
|
304
308
|
function renderRound(asked, answered) {
|
|
305
309
|
const title = asked?.title ?? 'The round';
|
|
306
310
|
const lines = answered.map((question, at) => {
|
|
307
|
-
const said = question.answeredByEmail
|
|
311
|
+
const said = bySomeone(question.answeredByEmail)
|
|
308
312
|
? `${question.answeredByEmail}: ${question.answer}`
|
|
309
313
|
: question.answer;
|
|
310
314
|
const opinions = (question.opinions ?? [])
|
|
@@ -329,7 +333,10 @@ function renderAnswer(answered) {
|
|
|
329
333
|
.map((opinion) => ` ${opinion.authorEmail}: ${opinion.body}`)
|
|
330
334
|
.join('\n');
|
|
331
335
|
|
|
332
|
-
|
|
336
|
+
// R612: no answeredByEmail means the platform closed it, not a person.
|
|
337
|
+
const answer = bySomeone(answered.answeredByEmail)
|
|
338
|
+
? `${answered.answeredByEmail} answered:\n\n${answered.answer}`
|
|
339
|
+
: `Nobody answered — the question was closed:\n\n${answered.answer}`;
|
|
333
340
|
return said ? `${answer}\n\nWhat people said before deciding:\n${said}` : answer;
|
|
334
341
|
}
|
|
335
342
|
|
|
@@ -1693,7 +1700,7 @@ const TOOLS = [
|
|
|
1693
1700
|
if (answered) {
|
|
1694
1701
|
return renderRound(null, answered);
|
|
1695
1702
|
}
|
|
1696
|
-
return `Still unfinished. Call await_group again with group_id ${args.group_id}.`;
|
|
1703
|
+
return `Still unfinished. Call await_group again with group_id ${args.group_id} to keep waiting. If you carry on instead, the round stays in their inbox — and the run reads as waiting on them — until this stage ends; then it is closed, and an answer given after that is refused.`;
|
|
1697
1704
|
},
|
|
1698
1705
|
},
|
|
1699
1706
|
|
|
@@ -1774,7 +1781,7 @@ const TOOLS = [
|
|
|
1774
1781
|
if (answered) {
|
|
1775
1782
|
return renderAnswer(answered);
|
|
1776
1783
|
}
|
|
1777
|
-
return `Still nothing. Call await_answer again with question_id ${args.question_id}.`;
|
|
1784
|
+
return `Still nothing. Call await_answer again with question_id ${args.question_id} to keep waiting. If you carry on instead, the question stays in their inbox — and the run reads as waiting on them — until this stage ends; then it is closed, and an answer given after that is refused.`;
|
|
1778
1785
|
},
|
|
1779
1786
|
},
|
|
1780
1787
|
|
package/package.json
CHANGED
package/runner/README.md
CHANGED
|
@@ -358,6 +358,33 @@ having none.
|
|
|
358
358
|
|
|
359
359
|
### Advanced mode: one card per session
|
|
360
360
|
|
|
361
|
+
**Advanced mode is the default terminal (R624).** `cawdev` opens in it wherever
|
|
362
|
+
the terminal can draw it — a TTY, not `TERM=dumb`, at least 80×24. Below that,
|
|
363
|
+
or through a pipe (`--watch-only | …`), it is the scrollback, with one line
|
|
364
|
+
saying why. `cawdev --simple` and `/simple` are the scrollback terminal;
|
|
365
|
+
`/advanced-mode` is the way back; the choice is remembered per platform in
|
|
366
|
+
`prefs.mjs` (`mode: simple|advanced`) beside `/agent` and `/matrix`. This does
|
|
367
|
+
not reopen R81 — it weighs it again: with several sessions and a roadmap on one
|
|
368
|
+
machine, R81's reason (a full-screen repaint eats the scrollback) no longer
|
|
369
|
+
wins, and the scrollback is one key away. `/export` (or `/export 3`,
|
|
370
|
+
`/export R29`) lays a session's log into the normal screen so the terminal's own
|
|
371
|
+
search and copy reach it; quitting and `/simple` lay in what the focused
|
|
372
|
+
session said.
|
|
373
|
+
|
|
374
|
+
**The motion is a spinner and a robot, not rain.** The rain is **off by
|
|
375
|
+
default** (`/matrix on` brings it back exactly as it was, remembered). A
|
|
376
|
+
running card's status mark, and the mark on the current stage of its walk,
|
|
377
|
+
turn: `◴ ◵ ◶ ◷` for a stage that writes (plan, code, merge, release, roadmap,
|
|
378
|
+
interview), `◜ ◝ ◞ ◟` for one that only reads (test, ask, audit, scope), a
|
|
379
|
+
frame every two ticks of the 120 ms clock. A waiting card holds `◆`, a queued
|
|
380
|
+
one rests on the spinner's first frame. A status that changes is drawn
|
|
381
|
+
reversed for four ticks. The robots (a finished plan, a finished development
|
|
382
|
+
docked at `▶ review`, nothing built, a landing at `▶ main`, a merge in flight)
|
|
383
|
+
move whether the rain is on or off. `n`/`N` move to the next/previous card
|
|
384
|
+
that needs you (`n` still refuses when the focused card holds a permission
|
|
385
|
+
request); `/close 3`, `/close all` and `w` close ended cards. A running card is
|
|
386
|
+
refused with `that session is still running · x x cancels it`.
|
|
387
|
+
|
|
361
388
|
`/advanced-mode` gives **one card per session** this machine is running, or
|
|
362
389
|
has run since you attached. The cards sit above **the prompt, which stays at
|
|
363
390
|
the bottom** the way a shell's does. A session's log scrolling past in a corner was neither readable
|
|
@@ -1116,7 +1143,13 @@ files at all. R47 removes the need for this by giving each run a workspace.
|
|
|
1116
1143
|
and leaves the run exactly as the platform has it; if it is QUEUED, the next
|
|
1117
1144
|
poll offers it again and it is claimed properly. Only 400, 404, 409 and 410
|
|
1118
1145
|
count as a refusal; any other status (401, 403, 408, 429), a network error or
|
|
1119
|
-
a 5xx still fails the run.
|
|
1146
|
+
a 5xx still fails the run. For a MERGE run (R621), the merge the daemon
|
|
1147
|
+
prepared is undone before it lets go, on a refusal and on a failure alike
|
|
1148
|
+
(`git merge --abort`, or a `reset --keep` past its own merge commit), so the
|
|
1149
|
+
checkout is offered again as it was; the FAILED summary says so. A checkout
|
|
1150
|
+
found mid-merge that the daemon did not start is refused with a sentence
|
|
1151
|
+
naming `git commit` and `git merge --abort`, and is not touched; a MERGE run
|
|
1152
|
+
carrying on its own branch is the exception.
|
|
1120
1153
|
3. **Spawns the agent** in that directory, in its own process group, with the
|
|
1121
1154
|
run token in its environment and an `.mcp.json` pointing at cawdev's MCP
|
|
1122
1155
|
server. **Your own token never reaches the child.**
|