cawdev-cli 1.2.0 → 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.
@@ -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
+ }
@@ -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.
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
@@ -2390,14 +2421,6 @@ function formatFileDeps(map, path) {
2390
2421
  /** How many finding lines a reading prints before saying "and N more" — R302. */
2391
2422
  const FINDING_LINES = 50;
2392
2423
 
2393
- /** One failed gate condition, as SonarQube words it: `new_violations 1287 > 0`. A rating's step is its letter. */
2394
- function describeCondition({ metric, comparator, threshold, actual }) {
2395
- const letter = (step) => ('12345'.includes(step) && step.length === 1 ? 'ABCDE'['12345'.indexOf(step)] : step);
2396
- if (metric.endsWith('_rating')) return `${metric} ${letter(actual)} worse than ${letter(threshold)}`;
2397
- const symbol = { GT: '>', LT: '<', EQ: '=', NE: '!=' }[comparator] ?? comparator;
2398
- return `${metric} ${actual} ${symbol} ${threshold}`;
2399
- }
2400
-
2401
2424
  /**
2402
2425
  * The reading as a session should read it — R302: one line for the default
2403
2426
  * branch, one for the branch asked about, then the open findings.
@@ -2467,6 +2490,8 @@ function formatQualityReading(reading, branch) {
2467
2490
  if (row.dashboardUrl) parts.push(row.dashboardUrl);
2468
2491
  lines.push(`Branch ${asked.branch}: ${parts.join(' · ')}`);
2469
2492
  if (row.warning) lines.push(` ${row.warning}`);
2493
+ const proven = provenLine(row);
2494
+ if (proven) lines.push(` ${proven}`);
2470
2495
  }
2471
2496
  } else if (branch === null) {
2472
2497
  lines.push('No branch was read: outside a run, name one to see its gate and new problems.');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cawdev-cli",
3
- "version": "1.2.0",
3
+ "version": "1.2.1",
4
4
  "description": "cawdev's terminal client and runner daemon — zero dependencies, on purpose",
5
5
  "type": "module",
6
6
  "bin": {
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 window per session
360
-
361
- `/advanced-mode` splits the terminal the way emacs does: **one window per
362
- session** this machine is running — or ran since you attached — above **the
363
- prompt, which stays at the bottom** the way a shell's does. Each window's title says what kind of
364
- session it is (`plan`, `dev`, `review`, `ask`…), where it stands, the card and
365
- the project, and its body is that session's output as it arrives. A frame's
366
- colour is its session's state — **red while it runs, green once it finished**
367
- (a plan planned, a dev developed), amber if it stopped any other way — and
368
- the focused window is drawn in heavy lines; `tab` and `shift+tab` move the focus, `1`–`9`
369
- jump to a window, and `enter` prompts the focused session exactly as it does
370
- outside the mode. `/advanced-mode` again (or `/advanced-mode off`) leaves.
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 PLANNED cards run across the top, grouped by project** — a line with
382
- each project's name over its cards and a rule between two projects, every
383
- project this machine serves included ("nothing planned" is an answer) — one
384
- small box each, titled with the card and its status — `R3 (Planned)` — the cards of
385
- every project this machine serves that are ready to be developed, read again
386
- every thirty seconds. **`d`** lists them and develops the one you pick, on this
387
- machine and on the card's own branch — except on a finished plan's window, where
388
- it is that plan's `/develop` (below) with no list; `/new dev` with nothing after it does the
389
- same anywhere, and `/new dev R3` goes straight to one. A full-width rule
390
- under the strip separates the board from the session windows; it comes out of
391
- the strip's own rows, never the windows'.
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
- **Four windows at most.** Past four, the rest are named on one line above the
400
- prompt — number, kind, state, card, in the state's colour — and `tab` (or the
401
- number) onto one brings it up in place of another. The focused session always
402
- has a window, then the ones still running, then the ended ones, each most
403
- recently looked at first.
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 window stays up when its session ends, and its last line says what can be
406
- done next:
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: one window over the whole screen with the
518
- session's output and, under a rule, the request in full (the question and its
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