cawdev-cli 1.1.1 → 1.2.1
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/finish-check.mjs +9 -1
- package/lib/gate-condition.mjs +11 -0
- package/lib/stage-tools.mjs +33 -2
- package/lib/version-files.mjs +99 -0
- package/mcp/server.mjs +40 -0
- package/package.json +1 -1
- package/runner/README.md +260 -32
- package/runner/attach.mjs +1692 -110
- package/runner/card-import.mjs +115 -0
- package/runner/cawdev.mjs +70 -1
- package/runner/diff-view.mjs +252 -0
- package/runner/git-lock.mjs +73 -0
- package/runner/runner.mjs +415 -29
- package/runner/session-cards.mjs +448 -0
- package/runner/shots.mjs +214 -0
- package/runner/sign-in.mjs +29 -2
- package/runner/split.mjs +272 -64
package/lib/finish-check.mjs
CHANGED
|
@@ -21,6 +21,13 @@ import { execFile } from 'node:child_process';
|
|
|
21
21
|
import { notAllowed, protectedChanges } from './protected.mjs';
|
|
22
22
|
import { findSecret } from './secrets.mjs';
|
|
23
23
|
|
|
24
|
+
// R604. Where a SHOW stage leaves what it wants shown (stage-tools.mjs names it).
|
|
25
|
+
// Never committed: the daemon keeps it out of git with `.git/info/exclude`,
|
|
26
|
+
// deletes it after the upload, and the filter in `leftOver` is the backstop.
|
|
27
|
+
import { SHOTS_DIR } from './stage-tools.mjs';
|
|
28
|
+
|
|
29
|
+
export { SHOTS_DIR };
|
|
30
|
+
|
|
24
31
|
/** The profiles whose run is a branch somebody will build on. */
|
|
25
32
|
export const COMMITS_ITS_WORK = new Set(['CODE']);
|
|
26
33
|
|
|
@@ -57,7 +64,8 @@ export async function leftOver(cwd, branch, { git = runGit } = {}) {
|
|
|
57
64
|
} catch {
|
|
58
65
|
return null;
|
|
59
66
|
}
|
|
60
|
-
const dirty = status.split('\n').map((line) => line.slice(3).trim()).filter(Boolean)
|
|
67
|
+
const dirty = status.split('\n').map((line) => line.slice(3).trim()).filter(Boolean)
|
|
68
|
+
.filter((path) => !path.startsWith(SHOTS_DIR));
|
|
61
69
|
const current = (await git(cwd, ['rev-parse', '--abbrev-ref', 'HEAD']).catch(() => '')).trim();
|
|
62
70
|
const remote = (await git(cwd, ['remote']).catch(() => '')).split('\n')[0]?.trim() || null;
|
|
63
71
|
const exists = await git(cwd, ['rev-parse', '--verify', '-q', `refs/heads/${branch}`])
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// R586 — one failed quality-gate condition in words. Shared by the MCP server
|
|
2
|
+
// (`quality_get`) and `sonar.sh` (tools/quality/scan.mjs) so the two cannot
|
|
3
|
+
// drift. Zero dependencies.
|
|
4
|
+
|
|
5
|
+
/** One failed gate condition, as SonarQube words it: `new_violations 1287 > 0`. A rating's step is its letter. */
|
|
6
|
+
export function describeCondition({ metric, comparator, threshold, actual }) {
|
|
7
|
+
const letter = (step) => ('12345'.includes(step) && step.length === 1 ? 'ABCDE'['12345'.indexOf(step)] : step);
|
|
8
|
+
if (metric.endsWith('_rating')) return `${metric} ${letter(actual)} worse than ${letter(threshold)}`;
|
|
9
|
+
const symbol = { GT: '>', LT: '<', EQ: '=', NE: '!=' }[comparator] ?? comparator;
|
|
10
|
+
return `${metric} ${actual} ${symbol} ${threshold}`;
|
|
11
|
+
}
|
package/lib/stage-tools.mjs
CHANGED
|
@@ -152,6 +152,12 @@ export const CARD_WRITE = `${CAWDEV}roadmap_create`;
|
|
|
152
152
|
*/
|
|
153
153
|
export const SUGGEST = `${CAWDEV}backlog_file`;
|
|
154
154
|
|
|
155
|
+
/**
|
|
156
|
+
* The one folder a SHOW stage may write — R604. Repository-relative, with the
|
|
157
|
+
* trailing slash; `finish-check.mjs` has the same constant for its own reason.
|
|
158
|
+
*/
|
|
159
|
+
export const SHOTS_DIR = '.cawdev/shots/';
|
|
160
|
+
|
|
155
161
|
/**
|
|
156
162
|
* Reads that only mean something once the work exists — R302.
|
|
157
163
|
*
|
|
@@ -312,7 +318,7 @@ export function canRunThings(tool) {
|
|
|
312
318
|
* tools rather than a coding run's. That ordering is what keeps R28's decision
|
|
313
319
|
* the outer one.
|
|
314
320
|
*
|
|
315
|
-
* @param stage one of PLAN, VERIFY, IMPLEMENT, TEST, MEMORY
|
|
321
|
+
* @param stage one of PLAN, VERIFY, IMPLEMENT, TEST, SHOW, MEMORY
|
|
316
322
|
* @param profileTools what the run's profile allows
|
|
317
323
|
* @param testMode R129, and only TEST reads it: `TESTBOOK` narrows the stage to
|
|
318
324
|
* what cannot run anything. Absent — an older platform that does not send it —
|
|
@@ -366,6 +372,20 @@ export function toolsForStage(stage, profileTools, testMode, options = {}) {
|
|
|
366
372
|
case 'VERIFY':
|
|
367
373
|
case 'MEMORY':
|
|
368
374
|
return readOnly(false);
|
|
375
|
+
case 'SHOW':
|
|
376
|
+
// R604. The reads of a read-only stage, plus every Bash the profile
|
|
377
|
+
// allows (SHOW has to start the app and run the repository's own
|
|
378
|
+
// Playwright), plus an Edit narrowed to the shots folder. Bare Write,
|
|
379
|
+
// Edit, MultiEdit and NotebookEdit never pass through. Bash can still
|
|
380
|
+
// write anywhere, and that is said plainly: this list is not the guard.
|
|
381
|
+
// The guard is the daemon's tree check after the stage, and SHOW is
|
|
382
|
+
// spawned with no permission mode so anything off this list goes to a
|
|
383
|
+
// person.
|
|
384
|
+
return [...new Set([
|
|
385
|
+
...readOnly(false),
|
|
386
|
+
...allowed.filter((tool) => /^Bash\b/.test(tool)),
|
|
387
|
+
`Edit(./${SHOTS_DIR}**)`,
|
|
388
|
+
])];
|
|
369
389
|
case 'TEST':
|
|
370
390
|
// R129. The mirror of the claim above, with the other question asked: a
|
|
371
391
|
// stage that may WRITE and cannot RUN. Same filtering of the PROFILE's
|
|
@@ -420,7 +440,7 @@ function readsGit(toolLine) {
|
|
|
420
440
|
* person who needs to know is the one reading the run.
|
|
421
441
|
*/
|
|
422
442
|
export function driftedFrom(stage, toolLine, testMode, readOnlyExperts = [], options = {}) {
|
|
423
|
-
const readOnly = stage === 'PLAN' || stage === 'VERIFY' || stage === 'MEMORY';
|
|
443
|
+
const readOnly = stage === 'PLAN' || stage === 'VERIFY' || stage === 'MEMORY' || stage === 'SHOW';
|
|
424
444
|
// R129. A TEST stage set to write the testbook has its own thing it should
|
|
425
445
|
// not have been able to do, and the same reason for saying so out loud.
|
|
426
446
|
const testbook = stage === 'TEST' && testMode === 'TESTBOOK';
|
|
@@ -445,6 +465,17 @@ export function driftedFrom(stage, toolLine, testMode, readOnlyExperts = [], opt
|
|
|
445
465
|
if (!canChangeThings(name)) {
|
|
446
466
|
return null;
|
|
447
467
|
}
|
|
468
|
+
// R604. SHOW ran something, which it is allowed to (the app, Playwright), or
|
|
469
|
+
// wrote into its own folder, which is what it is for.
|
|
470
|
+
if (stage === 'SHOW') {
|
|
471
|
+
if (/^Bash\b/.test(name)) {
|
|
472
|
+
return null;
|
|
473
|
+
}
|
|
474
|
+
if (/^(Write|Edit|MultiEdit)/.test(name) && String(toolLine).includes(SHOTS_DIR)
|
|
475
|
+
&& !String(toolLine).includes('..')) {
|
|
476
|
+
return null;
|
|
477
|
+
}
|
|
478
|
+
}
|
|
448
479
|
// R147. A delegation to an expert this stage was allowed is the one call
|
|
449
480
|
// `canChangeThings` names a writer that is not one here. `describeCall`
|
|
450
481
|
// writes it as `Agent(<name>) …`, and the name is the CLI's qualified one.
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
// R587: the project's version out of a file that declares it. Moved from
|
|
2
|
+
// tools/release/plan.mjs so the release plan, `sonar.sh` (through
|
|
3
|
+
// tools/quality/scan.mjs) and the daemon's scan (runner.mjs, scanQuality) read
|
|
4
|
+
// a version one way. Pure and zero dependencies, like all of tools/.
|
|
5
|
+
//
|
|
6
|
+
// A file is told apart by its NAME: `pom.xml`, `package.json`, and a `.yaml` /
|
|
7
|
+
// `.yml` with an `info:` block (openapi.yaml). Anything else has no version
|
|
8
|
+
// here: null, never a guess.
|
|
9
|
+
|
|
10
|
+
import { readFile } from 'node:fs/promises';
|
|
11
|
+
import { basename, isAbsolute, resolve, sep } from 'node:path';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* The project's own version in a pom: the first `<version>` OUTSIDE
|
|
15
|
+
* `<parent>`, which carries Spring Boot's, and outside `<dependencies>`,
|
|
16
|
+
* `<build>` and the rest, which carry their libraries'.
|
|
17
|
+
*/
|
|
18
|
+
export function pomVersion(pom) {
|
|
19
|
+
const top = pom
|
|
20
|
+
.replace(/<!--[\s\S]*?-->/g, '')
|
|
21
|
+
.replace(/<(parent|dependencies|dependencyManagement|build|profiles|properties)>[\s\S]*?<\/\1>/g, '');
|
|
22
|
+
const match = /<version>\s*([^<\s]+)\s*<\/version>/.exec(top);
|
|
23
|
+
return match ? match[1] : null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* `info.version` in openapi.yaml: a `version:` key directly under `info:`,
|
|
28
|
+
* at the block's own indentation: `info.description` is a block scalar
|
|
29
|
+
* with blank lines in it, and a deeper `version:` is somebody's schema.
|
|
30
|
+
*/
|
|
31
|
+
export function openapiVersion(yaml) {
|
|
32
|
+
const lines = yaml.split('\n');
|
|
33
|
+
const start = lines.findIndex((line) => /^info:\s*$/.test(line));
|
|
34
|
+
if (start === -1) return null;
|
|
35
|
+
let indent = null;
|
|
36
|
+
for (const line of lines.slice(start + 1)) {
|
|
37
|
+
if (!line.trim()) continue;
|
|
38
|
+
const own = /^(\s*)/.exec(line)[1].length;
|
|
39
|
+
if (own === 0) return null;
|
|
40
|
+
indent ??= own;
|
|
41
|
+
if (own !== indent) continue;
|
|
42
|
+
const match = /^\s*version:\s*['"]?([^'"\s]+)/.exec(line);
|
|
43
|
+
if (match) return match[1];
|
|
44
|
+
}
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** The top-level `version` of a package.json, or null (not JSON, no version). */
|
|
49
|
+
export function packageJsonVersion(text) {
|
|
50
|
+
try {
|
|
51
|
+
const version = JSON.parse(text)?.version;
|
|
52
|
+
return typeof version === 'string' && version.trim() ? version.trim() : null;
|
|
53
|
+
} catch {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** The version `text` declares, by what `path` is called; null for a kind it does not know. */
|
|
59
|
+
export function versionOf(path, text) {
|
|
60
|
+
const name = basename(String(path));
|
|
61
|
+
if (name === 'pom.xml') return pomVersion(text);
|
|
62
|
+
if (name === 'package.json') return packageJsonVersion(text);
|
|
63
|
+
if (/\.ya?ml$/i.test(name)) return openapiVersion(text);
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* The first declared file that yields a version: `{version, file}`, or null.
|
|
69
|
+
* `read(path)` returns the text, or throws for a file that is not there; both
|
|
70
|
+
* a throw and a non-string mean "not this one". Order is the project's.
|
|
71
|
+
*/
|
|
72
|
+
export async function firstVersion(files, read) {
|
|
73
|
+
for (const file of files ?? []) {
|
|
74
|
+
let text;
|
|
75
|
+
try { text = await read(file); } catch { continue; }
|
|
76
|
+
if (typeof text !== 'string') continue;
|
|
77
|
+
const version = versionOf(file, text);
|
|
78
|
+
if (version) return { version, file };
|
|
79
|
+
}
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* R587: the first of the project's declared version files that parses, read
|
|
85
|
+
* from the workspace: `{version, file}` or null. A file that is missing, does
|
|
86
|
+
* not parse, or would climb out of the checkout is skipped, never an error.
|
|
87
|
+
* Only a branch analysis has a new-code period of its own and never touches the
|
|
88
|
+
* project's gate; a MAIN analysis (`whenBranchesRefused: "main"`) is the one
|
|
89
|
+
* the version matters to, since SonarQube reads a versionless one as a change.
|
|
90
|
+
*/
|
|
91
|
+
export async function workspaceVersion(cwd, files) {
|
|
92
|
+
const root = resolve(cwd);
|
|
93
|
+
return firstVersion(files, async (file) => {
|
|
94
|
+
if (isAbsolute(file)) return null;
|
|
95
|
+
const path = resolve(root, file);
|
|
96
|
+
if (path !== root && !path.startsWith(`${root}${sep}`)) return null;
|
|
97
|
+
return readFile(path, 'utf8');
|
|
98
|
+
});
|
|
99
|
+
}
|
package/mcp/server.mjs
CHANGED
|
@@ -26,6 +26,7 @@ import {
|
|
|
26
26
|
} from '../lib/protected.mjs';
|
|
27
27
|
import { findDestructive, withinScope } from '../lib/secrets.mjs';
|
|
28
28
|
import { byCodeUnit } from '../lib/order.mjs';
|
|
29
|
+
import { describeCondition } from '../lib/gate-condition.mjs';
|
|
29
30
|
import { COMMITS_ITS_WORK, isUnfinished, leftOver, notDoneYet } from '../lib/finish-check.mjs';
|
|
30
31
|
|
|
31
32
|
const NAME = 'cawdev';
|
|
@@ -2005,6 +2006,36 @@ async function protectedAllowedNow(config, project, runId, policy) {
|
|
|
2005
2006
|
return allowed;
|
|
2006
2007
|
}
|
|
2007
2008
|
|
|
2009
|
+
/**
|
|
2010
|
+
* R318: what a branch scan made of the finding the card was filed from, in the
|
|
2011
|
+
* words the console uses (frontend `core/proven-words.ts`): still reported
|
|
2012
|
+
* beats not checked beats fixed. Null when the card has no finding to prove.
|
|
2013
|
+
*/
|
|
2014
|
+
function provenLine(row) {
|
|
2015
|
+
const findings = row.findings ?? [];
|
|
2016
|
+
if (!findings.length) return null;
|
|
2017
|
+
const name = (finding) => {
|
|
2018
|
+
const rule = finding.rule ? finding.rule.replace(/^[^:]*:/, '') : finding.key;
|
|
2019
|
+
const file = finding.path ? finding.path.split('/').pop() : null;
|
|
2020
|
+
return file ? `${rule} in ${file}` : rule;
|
|
2021
|
+
};
|
|
2022
|
+
const named = (verdict) => findings.filter((finding) => finding.verdict === verdict).map(name).join(', ');
|
|
2023
|
+
const plural = (n, noun) => `${n} new ${noun}${n === 1 ? '' : 's'}`;
|
|
2024
|
+
const bugs = row.newBugs ?? null;
|
|
2025
|
+
const vulnerabilities = row.newVulnerabilities ?? null;
|
|
2026
|
+
let clause = null;
|
|
2027
|
+
if (bugs !== null || vulnerabilities !== null) {
|
|
2028
|
+
const parts = [];
|
|
2029
|
+
if (bugs) parts.push(plural(bugs, 'bug'));
|
|
2030
|
+
if (vulnerabilities) parts.push(vulnerabilities === 1 ? '1 new vulnerability' : `${vulnerabilities} new vulnerabilities`);
|
|
2031
|
+
clause = parts.length ? parts.join(' and ') : '0 new problems';
|
|
2032
|
+
}
|
|
2033
|
+
const tail = clause ? ` · ${clause}` : '';
|
|
2034
|
+
if (named('STILL_OPEN')) return `Still reported: ${named('STILL_OPEN')}${tail}`;
|
|
2035
|
+
if (named('UNCHECKED')) return `Not checked: ${named('UNCHECKED')} (restart the daemon)`;
|
|
2036
|
+
return named('FIXED') ? `Fixed ${named('FIXED')}${tail}` : null;
|
|
2037
|
+
}
|
|
2038
|
+
|
|
2008
2039
|
/**
|
|
2009
2040
|
* The protected-file gate on `report done` — R562. Null when the branch is
|
|
2010
2041
|
* clean of unallowed protected changes (or nothing can be judged), else the
|
|
@@ -2415,6 +2446,12 @@ function formatQualityReading(reading, branch) {
|
|
|
2415
2446
|
const parts = project.error
|
|
2416
2447
|
? [`the last read failed: ${project.error}`, `before that: gate ${project.gate ?? 'NONE'}`]
|
|
2417
2448
|
: [`gate ${project.gate ?? 'NONE'}`];
|
|
2449
|
+
// R587: which version was analysed, right after the verdict it belongs to.
|
|
2450
|
+
if (project.analysedVersion) parts.push(`v${project.analysedVersion.replace(/^v/i, '')}`);
|
|
2451
|
+
// R587: the conditions the gate failed on, the cause a session would otherwise guess at.
|
|
2452
|
+
if (project.gateConditions?.length) {
|
|
2453
|
+
parts.push(`failing: ${project.gateConditions.map(describeCondition).join(', ')}`);
|
|
2454
|
+
}
|
|
2418
2455
|
parts.push(
|
|
2419
2456
|
`${count(project.bugs)} bugs`,
|
|
2420
2457
|
`${count(project.vulnerabilities)} vulnerabilities`,
|
|
@@ -2423,6 +2460,7 @@ function formatQualityReading(reading, branch) {
|
|
|
2423
2460
|
`coverage ${percent(project.coverage)}`,
|
|
2424
2461
|
);
|
|
2425
2462
|
if (project.analysedAt) parts.push(`analysed ${project.analysedAt}`);
|
|
2463
|
+
if (project.newCodeSince) parts.push(`new code since ${project.newCodeSince}`);
|
|
2426
2464
|
if (project.dashboardUrl) parts.push(project.dashboardUrl);
|
|
2427
2465
|
lines.push(`Default branch: ${parts.join(' · ')}`);
|
|
2428
2466
|
}
|
|
@@ -2452,6 +2490,8 @@ function formatQualityReading(reading, branch) {
|
|
|
2452
2490
|
if (row.dashboardUrl) parts.push(row.dashboardUrl);
|
|
2453
2491
|
lines.push(`Branch ${asked.branch}: ${parts.join(' · ')}`);
|
|
2454
2492
|
if (row.warning) lines.push(` ${row.warning}`);
|
|
2493
|
+
const proven = provenLine(row);
|
|
2494
|
+
if (proven) lines.push(` ${proven}`);
|
|
2455
2495
|
}
|
|
2456
2496
|
} else if (branch === null) {
|
|
2457
2497
|
lines.push('No branch was read: outside a run, name one to see its gate and new problems.');
|
package/package.json
CHANGED
package/runner/README.md
CHANGED
|
@@ -356,18 +356,73 @@ browser anywhere and the terminal collects the session when you approve.
|
|
|
356
356
|
error — it is what an expired session looks like, and the answer is the same as
|
|
357
357
|
having none.
|
|
358
358
|
|
|
359
|
-
### Advanced mode: one
|
|
360
|
-
|
|
361
|
-
`/advanced-mode`
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
359
|
+
### Advanced mode: one card per session
|
|
360
|
+
|
|
361
|
+
`/advanced-mode` gives **one card per session** this machine is running, or
|
|
362
|
+
has run since you attached. The cards sit above **the prompt, which stays at
|
|
363
|
+
the bottom** the way a shell's does. A session's log scrolling past in a corner was neither readable
|
|
364
|
+
nor meant to be read live, so a card shows what a glance needs:
|
|
365
|
+
|
|
366
|
+
```
|
|
367
|
+
┏━ 1 DEV R42 Payments refund flow ━━━━━━━━ ● running · 12m ━┓
|
|
368
|
+
┃ Claude · Opus 5.5 ▸ saasng · …/saasng-auto-2 ┃
|
|
369
|
+
┃ plan ✓ ─ implement ● ─ test ○ ─ verify ○ ┃
|
|
370
|
+
┃ now: Bash npm test -- refund ┃
|
|
371
|
+
┃ リサ ニ 2 3 ノ ニ7 2 3 ヤ ┃
|
|
372
|
+
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
- **The top border** has the card's number, a badge for the kind of session,
|
|
376
|
+
the card and its title, and the status.
|
|
377
|
+
- Badges are reversed so they never read as a status: `PLAN` violet, `DEV`
|
|
378
|
+
terracotta, `REVIEW`/`MERGE`/`RELEASE` light, anything else grey.
|
|
379
|
+
- Statuses: `● running · 12m`, `◆ waiting on you` (or
|
|
380
|
+
`waiting · permission`, `waiting · screenshots`), `◌ queued`, `✓ finished`,
|
|
381
|
+
`✓ planned` (a plan that finished), `✓ finished · nothing built`, `✕ failed`.
|
|
382
|
+
- **The rows inside**, top down:
|
|
383
|
+
- the agent and model, the project and the checkout;
|
|
384
|
+
- the stage walk while the session runs (from the daemon's snapshot, its
|
|
385
|
+
`stage` and `stages`), or the commits once it has finished;
|
|
386
|
+
- ONE line of what it is doing.
|
|
387
|
+
- **The "doing" line.** While it works this is the last tool call (`now: …`),
|
|
388
|
+
even when it has said something since. Before its first tool, it is the last
|
|
389
|
+
thing it said. A queued session says why it waits. An ended session that
|
|
390
|
+
failed or built nothing gives the first line of why. While it waits on you,
|
|
391
|
+
this line holds the question or the tool asking permission.
|
|
392
|
+
- **The last line** says what can be done: an ended session's next step, or
|
|
393
|
+
the keys for what it waits on.
|
|
394
|
+
|
|
395
|
+
A card's frame, and everything inside it, take the status's colour: green
|
|
396
|
+
running or finished, amber waiting or finished having built nothing, grey
|
|
397
|
+
queued, red stopped. The focused card is drawn in heavy lines. **The rows a
|
|
398
|
+
card does not need are rain**: falling glyphs in the status's colour, at its
|
|
399
|
+
speed.
|
|
400
|
+
|
|
401
|
+
| Status | Rain |
|
|
402
|
+
|---|---|
|
|
403
|
+
| running | green, fast, dense |
|
|
404
|
+
| waiting on you | amber, half that speed |
|
|
405
|
+
| queued | grey, slow and sparse |
|
|
406
|
+
| finished | a slow green drizzle |
|
|
407
|
+
| **planned** (a plan that finished) | no rain: a small robot paces along the bottom of the card, waving and blinking, holding up `✓ plan done` |
|
|
408
|
+
| finished, nothing built | slow amber |
|
|
409
|
+
| stopped or failed | slow red |
|
|
410
|
+
|
|
411
|
+
It moves a step every 120 ms, and only while the cards are on screen.
|
|
412
|
+
**`/matrix off`** stills it and is remembered (`prefs.mjs`); `/matrix on` brings
|
|
413
|
+
it back. With it off the rain is gone, but a planned card keeps its robot,
|
|
414
|
+
standing still, because the robot is the news that the card is ready to develop. `session-cards.mjs` is all of it, pure: the grid, a card, the rain at a
|
|
415
|
+
given tick, a function of the session's id, so each session keeps its own drops.
|
|
416
|
+
|
|
417
|
+
**A session's log is `/view`'s.** `/view` takes the focused card's session over
|
|
418
|
+
the whole screen. `/view 2` takes card 2, `/view R42` the session on R42, and
|
|
419
|
+
**`v`** on a card does the same. The log follows the end as it grows. Scroll up
|
|
420
|
+
and it holds still; `G` follows again. **`tab`** goes to the next session's log
|
|
421
|
+
without going back, and `esc` gives the cards back.
|
|
422
|
+
|
|
423
|
+
On the cards, `tab` and `shift+tab` move the focus, `1`–`9` jump to a card,
|
|
424
|
+
and `enter` prompts the focused session exactly as it does outside the mode.
|
|
425
|
+
`/advanced-mode` again (or `/advanced-mode off`) leaves.
|
|
371
426
|
|
|
372
427
|
It is drawn on the terminal's **alternate screen**, like nano or emacs, and
|
|
373
428
|
holds still the way they do: auto-wrap is off, so nothing ever scrolls the
|
|
@@ -378,17 +433,23 @@ for printing into the scrollback still holds, and this mode is something you
|
|
|
378
433
|
ask for rather than the default. On the way out, what the watched session said
|
|
379
434
|
meanwhile is laid into the scrollback, so nothing was lost by watching it here.
|
|
380
435
|
|
|
381
|
-
**The
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
436
|
+
**The roadmap runs across the top, grouped by project** — Considering,
|
|
437
|
+
Planned and In development side by side, one line per card, read again every
|
|
438
|
+
thirty seconds. **`b`** (or **`/board`**) puts the focus into the roadmap
|
|
439
|
+
strip (`❯` marks the focused card and an overflowing column scrolls with it):
|
|
440
|
+
`←`/`→` (or `h`/`l`) step across columns and projects, `↑`/`↓` (or `j`/`k`)
|
|
441
|
+
step through the column's cards, and a preview panel directly below the strip
|
|
442
|
+
shows the focused card's **full untruncated title and description** (read on
|
|
443
|
+
demand from `GET /api/projects/:slug/roadmap/:ref` and cached, scrolled with
|
|
444
|
+
`space` / `PgDn` / `PgUp`). From a focused card in the strip, `p` plans it,
|
|
445
|
+
`d` develops it, `v` (or `enter`) prints the full card record, and `esc` (or
|
|
446
|
+
`b` / `/board off`) returns focus to the session cards. Outside board focus,
|
|
447
|
+
**`d`** lists the PLANNED cards and CONFIRMED issues and develops the one you
|
|
448
|
+
pick, on this machine and on the card's own branch — except on a finished
|
|
449
|
+
plan's window, where it is that plan's `/develop` (below) with no list; `/new
|
|
450
|
+
dev` with nothing after it does the same anywhere, and `/new dev R3` goes
|
|
451
|
+
straight to one. A full-width rule under the strip separates the board from
|
|
452
|
+
the session cards; it comes out of the strip's own rows, never the cards'.
|
|
392
453
|
|
|
393
454
|
**`/refresh`** reads the cards again now rather than on the next tick, and
|
|
394
455
|
says what it found per project — `saasng: 5 considering · 2 planned · 1 in
|
|
@@ -396,14 +457,16 @@ development` — and which project could not be read, and why. A project that
|
|
|
396
457
|
cannot be read keeps its last cards and no other's: one unreadable slug used
|
|
397
458
|
to leave every column of every project at 0, silently.
|
|
398
459
|
|
|
399
|
-
**
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
460
|
+
**As many cards as fit.** Up to three across, as many as the width holds at 38
|
|
461
|
+
columns each, and as many rows as the height holds at six. Each row of cards is
|
|
462
|
+
up to eleven tall, and the prompt has whatever is left. The cards that do not
|
|
463
|
+
fit are named on one line above the prompt (number, kind, state, card, in the
|
|
464
|
+
state's colour), and `tab` (or the number) onto one brings it up in place of
|
|
465
|
+
another. The focused session always has a card, then the ones still running,
|
|
466
|
+
then the ended ones, each most recently looked at first.
|
|
404
467
|
|
|
405
|
-
A
|
|
406
|
-
|
|
468
|
+
A card stays up when its session ends, and its last line says what can be done
|
|
469
|
+
next:
|
|
407
470
|
|
|
408
471
|
- **`d`** or **`/develop`** on a finished plan — the window says `d develops R2
|
|
409
472
|
here, in place of this plan` — starts the development of its card, on
|
|
@@ -436,6 +499,13 @@ done next:
|
|
|
436
499
|
project's runs, kept when `carriesOnHere` — yours, `canCarryOn`, and its
|
|
437
500
|
checkout on this machine or none), listed in the scrollback, and given a
|
|
438
501
|
window that opens on why it stopped.
|
|
502
|
+
- **`/clean`** is offered beside `/continue` when a session stopped on a git
|
|
503
|
+
lock a crash left behind (`Unable to create '…/.git/index.lock': File
|
|
504
|
+
exists`), which `/continue` alone would only fail on again
|
|
505
|
+
(`git-lock.mjs`). It removes the lock named in the failure, and only one
|
|
506
|
+
inside that run's own checkout (or its reading worktree's admin directory),
|
|
507
|
+
older than a minute, on a run this machine took. One `/clean` frees every
|
|
508
|
+
session stopped on the same lock; `/continue` then picks each up.
|
|
439
509
|
- **Finished, having built nothing** — a dev session that ended FINISHED
|
|
440
510
|
with no commit (IMPLEMENT stopped on a plan it would not follow, say) —
|
|
441
511
|
reads `finished · nothing built`, in amber rather than green, offers no
|
|
@@ -476,6 +546,22 @@ sentence; `--anyway` starts it regardless, the same decision the console's
|
|
|
476
546
|
panel asks for. All four commands need you signed in (`/login`), and they work
|
|
477
547
|
outside advanced mode too, on the session you are watching.
|
|
478
548
|
|
|
549
|
+
**Several cards on one line are one batch** (R606): `/new dev R3 R5 R7`,
|
|
550
|
+
`/new plan R3,R5,R7`, `/new plan+dev R3 R5 R7`, in the order written, aimed at
|
|
551
|
+
this machine. It is the console's *Start runs on 3 cards* (`POST
|
|
552
|
+
…/runs/batch`), and the platform holds the order on the runs (`after_run_id`),
|
|
553
|
+
so quitting the terminal does not stop the sequence. `dev` is a branch each,
|
|
554
|
+
queued here; `--in-order` is one branch, each run from the last one's commits,
|
|
555
|
+
named with `--branch <name>` or else the first card's. `plan` plans the cards
|
|
556
|
+
one after another (a plan takes no checkout, so only the chain keeps them in
|
|
557
|
+
turn). `plan+dev` plans a card, builds it as soon as its plan is written (the
|
|
558
|
+
project's hand-off switch is not asked: you asked on the line), and plans the
|
|
559
|
+
next once that build has finished: plan R3, build R3, plan R5, build R5. A build
|
|
560
|
+
that fails or is cancelled holds the rest until it is carried on or started over.
|
|
561
|
+
A card the platform skips is named with its reason (`batchStartedLines`).
|
|
562
|
+
`--again` and `--show` are one card's answers and a batch refuses them;
|
|
563
|
+
`--codex` and `/agent` apply to every card.
|
|
564
|
+
|
|
479
565
|
**The prompt says which window it acts on.** In advanced mode the line at the
|
|
480
566
|
bottom leads with the focused window's number — `[2] cawdev ▸ /pr`, `[2]
|
|
481
567
|
enter prompts “…”` — and a command's answer keeps the number of the window it
|
|
@@ -514,8 +600,8 @@ card — in the browser, on the console this terminal signed in to.
|
|
|
514
600
|
**On a session waiting for you** — a question, or a permission such as a stage
|
|
515
601
|
gate whose summary is the whole plan — the prompt only has room for the start
|
|
516
602
|
of it, and says so: `/view reads it all · /open answers it on cawdev`.
|
|
517
|
-
**`/view`** then opens the READER:
|
|
518
|
-
|
|
603
|
+
**`/view`** then opens the READER: the session's log over the whole screen
|
|
604
|
+
(as above) and, under a rule, the request in full (the question and its
|
|
519
605
|
options; the permission's summary, its input and its keys). It opens on the
|
|
520
606
|
request; `↑`/`↓` (and the wheel), `PgUp`/`PgDn`, `g`/`G` scroll it, `esc`
|
|
521
607
|
closes it, and any other key closes it and does what it always does — `a`
|
|
@@ -571,6 +657,58 @@ a model. Titled by its words (`cardTitleFor`), the whole text in its body;
|
|
|
571
657
|
the project chosen exactly as `/new` chooses it (`@project`, else advanced
|
|
572
658
|
mode narrowed to one, else asked).
|
|
573
659
|
|
|
660
|
+
**`/review`** reads a card's branch and acts on it from here: the focused
|
|
661
|
+
session's card, or `/review R42 (@project)`. The diff is local git
|
|
662
|
+
(`diff-view.mjs`: `git fetch origin <base> <branch>`, which moves refs only,
|
|
663
|
+
then `git diff base...branch` from the merge base, in the session's checkout or
|
|
664
|
+
one the config names), so no diff crosses the API and a working checkout is
|
|
665
|
+
never touched; a failed fetch reads what the checkout has and says it may be
|
|
666
|
+
behind. File bytes are made printable before they are drawn. In advanced mode
|
|
667
|
+
it is the reader (`n`/`p` files, `f` fold; lockfiles, binaries and files over
|
|
668
|
+
`FOLD_OVER_LINES` start folded) with `d` Done, `c` changes requested (the note
|
|
669
|
+
is asked in the prompt; empty goes back to the diff) and `m` merge, offered on
|
|
670
|
+
a Done branch to a person holding Merge and pressed twice. Outside it the diff
|
|
671
|
+
is printed and the acts are `/review done|changes <note>|merge|agent-merge`.
|
|
672
|
+
Each act is the platform's endpoint (`work-items/{id}/verdict`, `/merge`,
|
|
673
|
+
`/agent-merge`); after a merge the card is read every five seconds until it is
|
|
674
|
+
MERGED or the merge FAILED (`followMerge`).
|
|
675
|
+
|
|
676
|
+
**While it merges, the card's sessions are out of reach** (`markMerging`,
|
|
677
|
+
`refuseWhileMerging`). Their cards turn grey and say `⇢ merging` and
|
|
678
|
+
`r42-pay → main`. Instead of rain, a robot looking ahead (`[>_>]`) carries the
|
|
679
|
+
branch (`[■]`) to `▶ main` and walks back for the next load
|
|
680
|
+
(`session-cards.mjs` `mergingRobotLines`). Enter, `d`, `x` and the bare
|
|
681
|
+
commands that act on the branch or its checkout are refused with the reason:
|
|
682
|
+
`/pr`, `/continue`, `/clean`, `/drop`, `/close`, `/cancel`, `/develop` and
|
|
683
|
+
`/review` (`DISABLED_WHILE_MERGING`). `/view` and `/open` still work.
|
|
684
|
+
- **Once it lands**, the card closes by itself (a session still running only
|
|
685
|
+
loses the mark).
|
|
686
|
+
- **If the merge hits a conflict** (the merge's `failure` is `CONFLICT`, or an
|
|
687
|
+
older runner's words say so), the card goes back to development
|
|
688
|
+
(`resolveConflict`).
|
|
689
|
+
- The branch is sent back, Not done, with an order as its note
|
|
690
|
+
(`conflictOrder`): merge `origin/main` into the branch, resolve every
|
|
691
|
+
conflict keeping what both sides meant, change nothing else, run the tests,
|
|
692
|
+
commit and push. The next session on the card is handed that note.
|
|
693
|
+
- That session is started here again (`again`), with the agent that built
|
|
694
|
+
the branch. The old card gives way to the new one, which takes the focus.
|
|
695
|
+
- Once it is Done, `/review` and `m` merge it again. `/review agent-merge`
|
|
696
|
+
is still there for an agent of its own.
|
|
697
|
+
- **If the merge fails any other way** (not permitted, unreachable…), no
|
|
698
|
+
agent can fix it. The card comes back red, `✕ merge failed`, with the
|
|
699
|
+
machine's words and `/review` on its last line, and every act is offered
|
|
700
|
+
again.
|
|
701
|
+
|
|
702
|
+
**`/import <file.json>`** files the cards of a JSON file (`card-import.mjs`,
|
|
703
|
+
over `POST /api/projects/{slug}/roadmap/import`): a list of `{title, body,
|
|
704
|
+
severity, tags, …}` or `{"cards": [...]}`. The project is `@project`, else
|
|
705
|
+
advanced mode narrowed to one, else the one project served; with several it is
|
|
706
|
+
asked for by name. `--dry-run` only checks. A card whose title is taken (on the
|
|
707
|
+
board, or earlier in the file) is skipped and listed with what it repeats;
|
|
708
|
+
`--allow-duplicates` files it anyway. From the shell, `cawdev import
|
|
709
|
+
<file.json>` checks first, prints what would be filed and refused, and asks
|
|
710
|
+
(`--yes` does not; `--project`, `--status`, `--dry-run` as above).
|
|
711
|
+
|
|
574
712
|
**Across the top, the roadmap** (`roadmapStrip`): every project this machine
|
|
575
713
|
serves as a small board — Considering, Planned and In development side by
|
|
576
714
|
side, one line per card, `●` on a card with a session running now; Merged,
|
|
@@ -1214,6 +1352,20 @@ Bitbucket project's pull request or merge to it: the button says "No machine of
|
|
|
1214
1352
|
yours runs a cawdev runner that speaks Bitbucket. Restart the daemon to update
|
|
1215
1353
|
it."
|
|
1216
1354
|
|
|
1355
|
+
**The fix proves itself (R318).** When the run's card was filed from a
|
|
1356
|
+
SonarQube finding, the claim carries it (`findingsToProve`) and the scan then
|
|
1357
|
+
asks the server whether it is still reported on the tree just scanned, and
|
|
1358
|
+
reports `fixedKeys` / `stillOpenKeys` with the reading. SonarQube gives an issue
|
|
1359
|
+
a *new key* when it copies it onto a branch, so on a `BRANCH` or `PROJECT`
|
|
1360
|
+
reading the match is the rule in the file — narrowed to the line's hash when the
|
|
1361
|
+
finding has one, since lines move. Without a hash, another issue of the same rule
|
|
1362
|
+
in that file reads as "still reported": the check can err toward blocking, never
|
|
1363
|
+
toward calling an unfixed finding fixed. On a `MAIN` reading the key itself is
|
|
1364
|
+
asked for. A reading is taken after TEST, so commits from a later stage, or
|
|
1365
|
+
pushed by hand, are not seen. **Restart the daemon** to read `findingsToProve`: a
|
|
1366
|
+
daemon started before R318 sends neither list, and the board then says *Not
|
|
1367
|
+
checked*.
|
|
1368
|
+
|
|
1217
1369
|
### What the agent may push
|
|
1218
1370
|
|
|
1219
1371
|
A coding session may commit on its card's branch and push **that branch and
|
|
@@ -1233,6 +1385,82 @@ shell and your credentials can get round a pattern match (`git -c alias.x='!…'
|
|
|
1233
1385
|
a host CLI called through a script). **The real wall is branch protection on the
|
|
1234
1386
|
host: protect the default branch and require a pull request.** Do that.
|
|
1235
1387
|
|
|
1388
|
+
## Showing a card before its pull request (R604)
|
|
1389
|
+
|
|
1390
|
+
A card with *Show me before the PR* on walks **IMPLEMENT → TEST → SHOW → MEMORY**.
|
|
1391
|
+
SHOW is the agent's chance to show a person what it built, before anything is proposed.
|
|
1392
|
+
|
|
1393
|
+
- **The folder.** The session writes 1–8 files into `.cawdev/shots/` — a PNG or JPEG of
|
|
1394
|
+
the real screen (the repository's own Playwright, or Claude in Chrome), or a *static*
|
|
1395
|
+
self-contained HTML mock — and `.cawdev/shots/shots.json`, `[{"file", "caption"}]`.
|
|
1396
|
+
Each file is 2 MB or smaller. The daemon adds the folder to `.git/info/exclude`, so it
|
|
1397
|
+
is never committed, and deletes it once the round is uploaded.
|
|
1398
|
+
- **The upload is the daemon's**, with the machine's token on `runner:operate`. The
|
|
1399
|
+
session holds the run token and cannot upload what it is judged by.
|
|
1400
|
+
- **The tree check.** SHOW is spawned with a tool list that allows `Edit` only under
|
|
1401
|
+
`.cawdev/shots/`, but its Bash can write anywhere, so the list is not the guard:
|
|
1402
|
+
after the stage the daemon compares the tree with how it began. A commit, or any path
|
|
1403
|
+
changed outside the folder (build output that is not gitignored counts), fails the
|
|
1404
|
+
stage, names the paths, and raises no gate.
|
|
1405
|
+
- **No permission mode.** SHOW is spawned without `--permission-mode` (never
|
|
1406
|
+
`acceptEdits`, never "allow everything") and with the permission prompt kept: a Bash
|
|
1407
|
+
command that is not on the allowed list becomes a question for a person. On a machine
|
|
1408
|
+
with few Bash patterns SHOW is chatty; the answer is a project rule for the app's start
|
|
1409
|
+
command, not a mode. Codex and Antigravity cannot be narrowed this way, so for them
|
|
1410
|
+
the tree check is the only guard.
|
|
1411
|
+
- **The gate.** The daemon raises `stage:SHOW` with the round and its screenshot ids.
|
|
1412
|
+
`attach` shows it as a wait to look at (below); `/open` opens the run page. SHOW is reported DONE only after *Accept*, so a daemon that dies while the
|
|
1413
|
+
gate waits resumes at SHOW. A gate that cannot be raised fails the stage.
|
|
1414
|
+
- **Comments** send the walk back to IMPLEMENT, with the comments leading its prompt,
|
|
1415
|
+
then TEST and SHOW again. There is no cap on rounds.
|
|
1416
|
+
|
|
1417
|
+
The daemon must be restarted to learn the SHOW stage.
|
|
1418
|
+
|
|
1419
|
+
### Looking and deciding from the terminal (R605)
|
|
1420
|
+
|
|
1421
|
+
A run stopped at SHOW is not a permission request, and `attach` does not ask for one.
|
|
1422
|
+
The window's title says `waiting for you` and its subtitle, in amber, reads
|
|
1423
|
+
`waiting for you — 3 screenshots, round 2 · a to look and decide`. The banner says
|
|
1424
|
+
` show ` and offers `a`, `/accept`, `/comments …` and `/open` instead of `y / s / n`.
|
|
1425
|
+
|
|
1426
|
+
- **`a`** (or `/view`) opens one picker: the round's screenshots, numbered, with their
|
|
1427
|
+
captions (control characters stripped — a caption comes from the session); *Open all*;
|
|
1428
|
+
*Accept — open the pull request*; one row per earlier round, with the comments sent on
|
|
1429
|
+
it; and *Send comments…*. Enter on a numbered screenshot opens that one.
|
|
1430
|
+
- **Commands** act on the focused session and say `is not waiting at SHOW` and do
|
|
1431
|
+
nothing when it is not at SHOW: `/shots` (the round listed), `/shots 2`, `/shots all`,
|
|
1432
|
+
`/shots round 1` (an earlier round and its comments), `/shots round 1 all`,
|
|
1433
|
+
`/accept`, and `/comments <text>`. In comments a line starting `#2` is a note on
|
|
1434
|
+
screenshot 2 and every other line is the round's note; on the one-line command, a `#2`
|
|
1435
|
+
anywhere starts the note on screenshot 2, which runs to the next `#n` or the end. An
|
|
1436
|
+
unknown `#n` or an empty comment is refused before anything is sent. In the picker's
|
|
1437
|
+
*Send comments…* an empty line sends; Esc goes back to the picker and keeps the lines.
|
|
1438
|
+
- **`y`, `s`, `Y`, `M` and `n` do nothing at SHOW** and say to use `a`, `/accept` or
|
|
1439
|
+
`/comments`: `s` would otherwise have accepted the round, and opened the pull request,
|
|
1440
|
+
without anybody looking.
|
|
1441
|
+
- **`--show` / `--no-show`** on `/new dev` and `/develop` override the card's switch for
|
|
1442
|
+
one run (on a plan or a question they are refused in words); `/card <idea> --show`
|
|
1443
|
+
writes a card with the switch on.
|
|
1444
|
+
- **Only the screenshots the platform sealed are shown.** A gate's list is the session's
|
|
1445
|
+
own claim, so the picker joins it to `GET …/attachments` for that round; a gate that
|
|
1446
|
+
names an id that was never sealed offers no Accept.
|
|
1447
|
+
|
|
1448
|
+
**The cache.** Images open in the OS viewer (`open` with every path on macOS, so Preview
|
|
1449
|
+
pages through them in one window; `xdg-open` once per file elsewhere) from
|
|
1450
|
+
`~/.cawdev/shots/<runId>/<round>/` (directories 0700, files 0600; `CAWDEV_SHOTS_DIR`
|
|
1451
|
+
moves it). Each file is named by its SHA-256 and an extension taken from the platform's
|
|
1452
|
+
content type, is downloaded the first time and not again while its bytes still match, and
|
|
1453
|
+
is checked against the checksum the list carries. **The terminal is the only writer** —
|
|
1454
|
+
the daemon deletes the checkout's `.cawdev/shots/` once a round is sealed and never
|
|
1455
|
+
writes the cache, because a session could plant a link there. A link at any level is
|
|
1456
|
+
refused.
|
|
1457
|
+
|
|
1458
|
+
**HTML is never opened from disk.** It opens at its sandboxed URL on the platform in the
|
|
1459
|
+
browser (which must be signed in to cawdev), because a `file://` page is a trusted page.
|
|
1460
|
+
|
|
1461
|
+
**Thumbnails.** In iTerm2, WezTerm and kitty, `/shots` also prints each image small into
|
|
1462
|
+
the scrollback — not in advanced mode, not when `plain`, and never to any other terminal.
|
|
1463
|
+
|
|
1236
1464
|
## What a project's rules can ask of it (R40)
|
|
1237
1465
|
|
|
1238
1466
|
A project may decide that a finished run **pushes its branch**, **opens a pull
|