@sylad/cadence 0.5.0 → 0.7.0

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.
@@ -6,7 +6,7 @@
6
6
  {
7
7
  "name": "cadence",
8
8
  "description": "Session start and close rituals driven by a versioned plan (raf), and deliveries proven by their effect. Needs the cadence CLI (npm i -g @sylad/cadence).",
9
- "version": "0.5.0",
9
+ "version": "0.7.0",
10
10
  "source": "./",
11
11
  "author": { "name": "Sylvain Ladoire" }
12
12
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "cadence",
3
- "description": "A repo-native working method: session start and close rituals driven by a versioned plan (raf), deliveries proven by their effect, and two reviewer agents (UX, code).",
4
- "version": "0.5.0",
3
+ "description": "A repo-native working method: session start and close rituals driven by a versioned plan (raf), deliveries proven by their effect, and three reviewer agents (UX, code, QA).",
4
+ "version": "0.7.0",
5
5
  "author": { "name": "Sylvain Ladoire" },
6
6
  "homepage": "https://github.com/Sylad/cadence",
7
7
  "repository": "https://github.com/Sylad/cadence",
package/README.md CHANGED
@@ -13,9 +13,11 @@ Four tools:
13
13
 
14
14
  And four [Claude Code](https://claude.com/claude-code) skills that turn them
15
15
  into rituals — `session-start`, `session-close`, `deliver`, and `lead` to pilot
16
- several projects through subagents — plus two reviewer agents, each behind an
17
- opt-in gate: `ux-reviewer` (no user-facing change is done before its usability
18
- review) and `code-reviewer` (no lot with commits is done before its code review).
16
+ several projects through subagents — plus three reviewer agents: `ux-reviewer`
17
+ (no user-facing change is done before its usability review) and `code-reviewer`
18
+ (no lot with commits is done before its code review), each behind an opt-in
19
+ gate, and `qa-reviewer`, which walks the delivered app in a real browser and
20
+ reports a page left empty or in error.
19
21
 
20
22
  ## raf
21
23
 
@@ -23,8 +25,17 @@ review) and `code-reviewer` (no lot with commits is done before its code review)
23
25
  Your comments and hand edits are preserved.
24
26
  - A commit belongs to a lot when its message cites the id: `feat(L3): …`,
25
27
  `fix: L3/t1 …`. The link is **computed from `git log`**, never stored, so
26
- committing never dirties the plan.
28
+ committing never dirties the plan. The id is read as a whole word: `XL3`,
29
+ `L3x` and `L3.4` do not cite `L3`, while `L3.` at the end of a sentence does.
27
30
  - `raf check` audits drift between the plan and the history.
31
+ - Plan upkeep needs no lot. A commit is plan upkeep when **every file it
32
+ touches is a plan file**: the plan itself, its Gantt page, or a file the
33
+ project lists under `plan.files` in `cadence.yaml` (a page it generates from
34
+ the plan, a journal). Such a commit is never a "commit without a lot", and it
35
+ does not count as work on the lots it cites: it is absent from `raf commits`,
36
+ does not start a `todo` lot and does not make a code review stale. The files
37
+ decide, never the subject: a `chore(plan): …` commit that touches a source
38
+ file is a commit like any other.
28
39
  - `raf gantt` writes a single self-contained HTML page (no server, no CDN).
29
40
 
30
41
  ```sh
@@ -54,7 +65,7 @@ raf gantt # docs/plan/gantt.html
54
65
  | `raf commits <id>` | the commits counted for a lot (the set the code review gate uses), one `<sha> <subject>` per line, oldest first |
55
66
  | `raf now` | what to do next |
56
67
  | `raf list [--status s]` | flat list |
57
- | `raf check [--since date] [--idle 7]` | since the plan's adoption date by default: commits without a lot (commits touching only the plan are exempt), unknown ids, `todo` lots that already have commits, idle lots, `done` lots with open sub-tasks, bad or circular dependencies |
68
+ | `raf check [--since date] [--idle 7]` | since the plan's adoption date by default: commits without a lot (commits touching only plan files are exempt), unknown ids, `todo` lots that already have commits, idle lots, `done` lots with open sub-tasks, bad or circular dependencies |
58
69
  | `raf gantt [-o file]` | standalone Gantt page |
59
70
  | `raf hook install` | add the (non-blocking, read-only) post-commit hook |
60
71
 
@@ -94,6 +105,18 @@ format, and the plan stays writable:
94
105
  plan: planning/todo.yaml
95
106
  ```
96
107
 
108
+ A project that publishes its plan (a JSON generated from it and committed with it)
109
+ declares that file, so a commit touching only the plan and its published copy is
110
+ plan upkeep; the plan keeps raf's format and stays writable:
111
+
112
+ ```yaml
113
+ plan:
114
+ files: [frontend/public/plan-data/plan.json]
115
+ ```
116
+
117
+ Until the file is declared, such a commit is reported as a "commit without a
118
+ lot" when it cites none, and counts as work on the lots it cites.
119
+
97
120
  A project that already keeps its plan with its own tool is read **without migrating it**: describe
98
121
  the file, and `raf now`, `raf list`, `raf commits`, `raf check`, `raf gantt` and
99
122
  `cadence session start|close` work on it. Such a plan is **read-only** —
@@ -175,6 +198,15 @@ captures: [captures/l8.png]
175
198
  Imported statements now read **3.000** as three thousand, not three.
176
199
  ```
177
200
 
201
+ A screenshot can say what it shows: write it as `{ file, alt }` instead of a
202
+ bare path, one text per screenshot.
203
+
204
+ ```yaml
205
+ captures:
206
+ - { file: captures/l8-before.png, alt: "Statement total read as 3 instead of 3,000" }
207
+ - captures/l8-after.png # a bare path still works: no alternative text
208
+ ```
209
+
178
210
  | Command | Effect |
179
211
  |---|---|
180
212
  | `cadence news new <lot…> [--title t]` | entry skeleton, dated and timed now (`date`, `created`), titled after the lot |
@@ -187,7 +219,11 @@ Imported statements now read **3.000** as three thousand, not three.
187
219
  The Markdown is deliberately small: paragraphs, `-` lists, `**bold**`,
188
220
  `` `code` ``, `[links](url)`; everything else is escaped text. The JSON holds
189
221
  `{ project, generated, entries: [{ slug, title, date, lots, captures, html }] }`,
190
- with screenshot paths relative to the JSON file.
222
+ with screenshot paths relative to the JSON file. `captures` is always a list of
223
+ paths; an entry that gives at least one alternative text also carries `alts`,
224
+ the texts in the same order (`""` for a screenshot without one) — an entry
225
+ without any keeps exactly the shape above. The built page puts the text in the
226
+ image's `alt`, and falls back to "Capture : <title>".
191
227
 
192
228
  **Order.** Everywhere (`list`, `build`, the JSON), entries are strictly newest
193
229
  first: by `date`, then, on the same day, by creation time. Every entry carries
@@ -214,8 +250,8 @@ raf ux L8 "no screen: calculation fix"
214
250
 
215
251
  With the rule on, `raf done` refuses a visible lot without a review (`--force`
216
252
  to override) and `raf check` reports visible lots finished after the `uxSince`
217
- day without one. An empty verdict is refused. Plans without `uxSince` are not
218
- affected.
253
+ day without one. An empty verdict is refused, and one left empty or blank by hand
254
+ in the YAML counts as no review. Plans without `uxSince` are not affected.
219
255
 
220
256
  ### Code review
221
257
 
@@ -229,8 +265,8 @@ The counterpart of the UX review, off by default. With the rule on, `raf done`
229
265
  refuses a lot that has at least one commit citing it and no recorded verdict
230
266
  (`--force` to override), and `raf check` reports such lots finished after the
231
267
  `reviewSince` day. A lot with no commit has nothing to review; neither does a
232
- lot whose only commits touch the plan itself, predate the plan's `since` or
233
- match an `ignore:` pattern — `raf commits <id>` prints exactly the counted set.
268
+ lot whose only commits touch plan files alone (the plan, or a file listed under
269
+ `plan.files`), predate the plan's `since` or match an `ignore:` pattern — `raf commits <id>` prints exactly the counted set.
234
270
 
235
271
  The verdict is tied to what was reviewed: `raf review` stores it on the lot with
236
272
  the sha of the lot's latest counted commit (`review: { date, verdict, commit }`,
@@ -239,7 +275,75 @@ makes the review stale: `raf done` refuses (`--force` to override), and
239
275
  `raf check` reports a finished lot, until the lot is reviewed again and
240
276
  `raf review` is rerun. A verdict
241
277
  written by hand without a `commit` field is not checked for staleness. An empty
242
- verdict is refused. Plans without `reviewSince` are not affected.
278
+ verdict is refused, and one left empty or blank by hand in the YAML counts as no
279
+ review. Plans without `reviewSince` are not affected.
280
+
281
+ ### QA review
282
+
283
+ No gate and no command here: the QA review comes **after** a delivery, and
284
+ `raf done` does not wait for it. It follows any delivery that changes what a
285
+ page shows or what it is served (screen, API, data source, configuration of
286
+ either) — in practice every delivery except docs-, plan- or tests-only ones: a
287
+ backend-only lot can empty a page without touching a screen, and the agent then
288
+ starts with the pages that call the changed endpoints. The `qa-reviewer` agent
289
+ opens each page of the running app in a real browser and judges it from the
290
+ user's side. A page can be empty while everything else is green — no code
291
+ changed, a data source went down upstream, the unit tests replace the network,
292
+ `/api/health` answers ok, and the "nothing found" on screen is the message the
293
+ code was written to show.
294
+
295
+ The agent cannot tell such an empty state from a normal one by itself: the
296
+ project says what each page must show, in `docs/qa/expectations.md` — one
297
+ `## <route>` section per page, three kinds of lines:
298
+
299
+ ```markdown
300
+ # QA expectations
301
+
302
+ ## *
303
+ - shows: the header and the navigation links
304
+ - never: "Loading failed", "Too Many Requests"
305
+ - api: /api/live/current — may be empty when no match is within 24 hours
306
+
307
+ ## /players
308
+ - shows: the squad of the last match — at least 11 players
309
+ - shows: the season statistics table, 8 columns — 1440 only
310
+ - never: "No recent line-up found"
311
+ - api: /api/squad/last — a non-empty list
312
+
313
+ ## /fixtures/:id (the first match linked from /fixtures)
314
+ - shows: both team names, the date, the score once the match is played
315
+ - never: "Unknown match"
316
+ - api: /api/fixtures/:id
317
+ ```
318
+
319
+ - `shows:` — content that must be present and non-empty, with a count where one exists;
320
+ - `never:` — texts that must not appear: error messages, and empty-state messages that mean
321
+ missing data;
322
+ - `api:` — the calls the page depends on: each must answer 2xx with a non-empty body (a 200 with
323
+ `[]`, `{}` or `null` is a failure, unless its line says `may be empty when …`).
324
+
325
+ An optional `## *` section holds what every page must show, never show and call. A line may end
326
+ with a condition in plain words, which the agent honours: `may be empty when …`, `1440 only`,
327
+ `390 only` (a line without a width holds at both). Content hidden on the phone by design is not a
328
+ defect unless a `shows:` line requires it at 390; content pushed outside the visible area (it
329
+ needs a sideways scroll) is reported as suspect and handed to `ux-reviewer` in one line.
330
+
331
+ The rest is free text, written for a reader: a line can be repeated, and a route with a parameter
332
+ names a real value to visit or says where to find one. The file can live elsewhere:
333
+
334
+ ```yaml
335
+ # cadence.yaml
336
+ qa:
337
+ expectations: docs/quality/pages.md
338
+ ```
339
+
340
+ Only the agent reads that key; the CLI does not use it. Without an expectations file the agent walks
341
+ the routes it discovers and still runs its universal checks: an error shown, a failed API call
342
+ whose content is missing on screen, a broken or missing content image are defects with or without a
343
+ file; an empty 2xx body, like whatever else would need an expectation to judge, is suspect at most
344
+ (it may be a normal absence). For a route with a
345
+ parameter, it finds a real value in the app's links or its API responses and says how it built the
346
+ URL. The agent then returns a draft for you to correct — it never writes the file itself.
243
347
 
244
348
  ## session
245
349
 
@@ -250,9 +354,13 @@ cadence session start --since "3 days ago" --idle 2
250
354
  cadence session close # today's commits by lot, commits without a lot, lots in progress
251
355
  # with no commit today, drift, uncommitted / unpushed work
252
356
  # exit 1 while something is still open
253
- cadence session next "finish L3" "review L4" # shown by the next session start
357
+ cadence session next "finish L3" "review L4" # shown by the next session start; replaces the previous notes
358
+ cadence session next --clear # erase those notes, on purpose
254
359
  ```
255
360
 
361
+ `cadence session next` without a line refuses (exit 2) and leaves the notes of the
362
+ last close as they are — it used to erase them silently; erasing is `--clear`.
363
+
256
364
  Proposals come from the plan only: lots in progress, then ready lots (dependencies
257
365
  done), quick wins first. Local state lives in the git directory, never committed:
258
366
  the close notes per worktree, the delivery lock and log in `.git/cadence/`, shared
@@ -311,7 +419,10 @@ cadence deliver # 0 delivered and verified · 1 a step failed · 2
311
419
  - Commands get `CADENCE_SHA`, `CADENCE_SHORT` (7 characters) and `CADENCE_BRANCH`;
312
420
  `${SHA}` and `${SHORT}` are replaced in `url` and `contains`.
313
421
  - On success the lots cited by the commits since the previous delivery are
314
- listed, so you can `raf done` those whose effect you have seen.
422
+ listed, so you can `raf done` those whose effect you have seen. With a
423
+ read-only plan, only the lots that were in progress when the delivery started
424
+ are listed: an id quoted in a message for context (a finished lot, a
425
+ reservation number that looks like one) is not a delivered lot.
315
426
 
316
427
  ### A project with its own delivery script
317
428
 
@@ -354,7 +465,7 @@ As a plugin:
354
465
  ```
355
466
 
356
467
  gives `/cadence:session-start`, `/cadence:session-close`, `/cadence:deliver`,
357
- `/cadence:lead` and the `ux-reviewer` and `code-reviewer` agents. Or copy them into the
468
+ `/cadence:lead` and the `ux-reviewer`, `code-reviewer` and `qa-reviewer` agents. Or copy them into the
358
469
  repository with `cadence skills install` (to `.claude/skills/cadence-*` and
359
470
  `.claude/agents/cadence-*.md`; `--dir` for another `.claude` folder,
360
471
  `--force` to overwrite local edits).
@@ -369,12 +480,16 @@ repository with `cadence skills install` (to `.claude/skills/cadence-*` and
369
480
  morning and evening scripts feed the session report (`session:`), and its own
370
481
  skills can become one-line aliases of `session-start` / `session-close`.
371
482
  - **deliver**: dry run, delivery, and on failure the cause fixed rather than a
372
- blind retry.
483
+ blind retry; after a green delivery that changes what a page shows or what it
484
+ is served, the `qa-reviewer` agent walks the delivered app.
373
485
  - **lead**: from a folder holding several projects, one subagent per project
374
486
  gathers the facts, you choose the priorities, each lot is delegated to a
375
487
  subagent with a standard brief (test first, commits citing the lot, no push),
376
488
  reviewed by the `code-reviewer` agent, re-verified by the lead, then delivered
377
- one project at a time. Two subagents at most, never two in the same repository.
489
+ one project at a time; a delivery that changes what a page shows or what it is
490
+ served is then checked in the running app by the `qa-reviewer` agent, whose
491
+ blocking findings come back to you. Two
492
+ subagents at most, never two in the same repository.
378
493
  - **ux-reviewer** (agent): captures at 1440 and 390 px, findings grounded in a
379
494
  named rule (Nielsen, WCAG 2.2 AA) or a measurement, ranked, turned into
380
495
  `raf add --parent` sub-tasks, and a one-line verdict for `raf ux`. It never
@@ -387,6 +502,20 @@ repository with `cadence skills install` (to `.claude/skills/cadence-*` and
387
502
  scenario; real defects only, ranked, what it could not verify, and a one-line
388
503
  verdict for `raf review`. It takes the lot's commits from `raf commits`, never
389
504
  runs a build whose output is used live, and never edits code.
505
+ - **qa-reviewer** (agent): any web app; given a repository and a base URL (and
506
+ optionally a lot id, to start with the pages it touched — for a backend-only
507
+ lot, those that call the changed endpoints), it opens each page of
508
+ the project's expectations file in a real browser at 1440 and 390 px and
509
+ measures: expected content present and non-empty, no error or missing-data
510
+ message, every API call answered 2xx with a non-empty body, no console error,
511
+ no broken content image. Findings are defects (a line of the expectations
512
+ broken, or a universal check failing with a visible effect, with or without an
513
+ expectations file), suspects (it looks like missing or wrong data and no
514
+ expectation settles it) or noise (a console error or a failed request with no
515
+ visible effect, ranked minor), ranked, each with
516
+ the route, what was expected, what was measured and the evidence; pages checked
517
+ N/N, follow-ups as `raf add` lines, what it could not verify, a one-line
518
+ verdict. Read-only: GET only, no login, nothing submitted; it stops at a PIN.
390
519
 
391
520
  ## Releasing
392
521
 
@@ -0,0 +1,122 @@
1
+ ---
2
+ name: qa-reviewer
3
+ description: QA reviewer for any web app — after a delivery, walks the pages of the running app in a real browser, from the user's side, and reports empty states, wrong data, error messages, failed or empty API calls, console errors and broken images. Given a repository path and a base URL, it checks each page against the project's expectations file (`docs/qa/expectations.md` — per route, what the user must find, what must never appear, the API calls the page depends on) at a desktop and a phone width; every finding names what it measured (selector or text, count, status code, response size), never an impression; without an expectations file it still runs its universal checks, reports what it saw and returns a draft one. Use after any delivery that changes what a page shows or what it is served (screen, API, data source, configuration of either) — in practice every delivery except docs-, plan- or tests-only ones — or to re-check a deployed app. Read-only — does not modify code, log in or submit anything.
4
+ ---
5
+
6
+ You check a running web app the way its user meets it: page by page, in a real browser. You
7
+ report; you never edit code, the plan or the expectations.
8
+
9
+ A page can be empty while everything else is green: no code changed, a data source went down
10
+ upstream, the unit tests replace the network, the health endpoint answers ok, and the message on
11
+ screen is exactly the one the code was written to show. Neither a test nor a code review calls
12
+ that a defect. You do: a players page with no players is a defect, whatever the cause.
13
+
14
+ ## Inputs
15
+
16
+ The absolute path of the repository and the base URL of the app — deployed, or a local server the
17
+ caller started. Optionally a lot id: then start with the pages that lot touched (its title and
18
+ notes in the plan, and `raf commits <id>`, tell which) — when the lot touched only the backend,
19
+ the pages that call the changed endpoints — and walk the others after. If the path or the URL is
20
+ missing, or the URL does not answer, say so and stop.
21
+
22
+ ## Method
23
+
24
+ 1. **Read how to reach the app**: the project's CLAUDE.md, then its README — the routes, the demo
25
+ data, what sits behind a PIN or a login.
26
+ 2. **Read the expectations**: `docs/qa/expectations.md`, or the file named by `qa.expectations` in
27
+ `cadence.yaml`. One `## <route>` section per page: what the page `shows:` (the content that
28
+ must be present and non-empty, with a count where one exists), what must `never:` appear (error
29
+ texts, empty-state messages that mean missing data), and the `api:` calls it depends on (each
30
+ must answer 2xx with a non-empty body). An optional `## *` section holds what every page must
31
+ show, never show and call. A line may end with a condition in plain words, which you honour:
32
+ `may be empty when …`, `1440 only`, `390 only` (a line without a width holds at both). A route
33
+ with a parameter names a real value to visit, or says where to find one.
34
+ 3. **No expectations file: do not guess silently.** Discover the routes (router file, sitemap,
35
+ navigation links); for a route with a parameter, find a real value in the app's links or its API
36
+ responses and say how you built the URL. Walk them as in step 4 and report what you saw: the
37
+ universal checks hold without a file, and anything that would need an expectation to judge is
38
+ *suspect* at most. Return a DRAFT expectations file as text, for the human to correct: you do not
39
+ write it into the repository. Say plainly that without expectations an empty state cannot be told
40
+ from a normal one.
41
+ 4. **Open each page in a real browser** (Playwright, or the browser tool available), at **1440 px**
42
+ and **390 px** wide. Let it settle: after `load`, wait a fixed few seconds, scroll through the
43
+ page (lazy images), wait again — never for network idle, which streams and polling never reach.
44
+ Then measure:
45
+ - the expected content is present and non-empty — name the selector or the text found and its
46
+ count (`.player-card` ×14), not "the list looks fine";
47
+ - no `never:` text on screen, and no other error or missing-data message;
48
+ - every API call of the page — those listed, and those you saw it make to its own backend —
49
+ answered 2xx with a non-empty body: note the status and the response size (decoded body bytes;
50
+ streams — SSE, websockets — are exempt from the size rule). A 200 with an empty or null body
51
+ (`[]`, `{}`, `null`, 0 bytes) is a failure, unless its line says `may be empty when …`. This
52
+ takes a tool that listens to responses (e.g. a Playwright `page.on('response')` listener): if
53
+ yours cannot give status and size, say so under "not verified" instead of pretending;
54
+ - no console error: quote the first line of each;
55
+ - no broken image among the content images (a failed request, or `naturalWidth` 0): count the
56
+ items that should carry an image and have no loaded `<img>` — a fallback badge replacing a
57
+ failed image has no `<img>` at all;
58
+ - at 390 px, content hidden on the phone by design is not a defect unless a `shows:` line
59
+ requires it at 390; content pushed outside the visible area (it needs a sideways scroll) is
60
+ reported as suspect and handed to `ux-reviewer` in one line;
61
+ - states behind controls: tabs, filters and other controls that only change the view may be used
62
+ and are part of the page (a tab that triggers its own API call is checked like a page); a
63
+ control that writes is never used;
64
+ - pacing: pause between pages; when a 429 (or any rate-limit answer) appears, re-run that page
65
+ ALONE after a quiet minute before concluding — if it reproduces, an ordinary visitor gets it;
66
+ if not, it was your own pace and it is not a finding;
67
+ - the frontend source may be read to LOCATE a cause after a measurement, never as evidence.
68
+ 5. **GET only, and nothing that writes**: never log in, never submit a form that writes, never
69
+ click a control that changes data, never send a POST, PUT, PATCH or DELETE yourself. If a PIN
70
+ or a login wall is met, say so and stop there for those pages: they go under "not verified",
71
+ they are neither a finding nor a page checked.
72
+ 6. **Classify** what you see:
73
+ - *defect* — a line of the expectations is broken, or a universal check fails with a visible
74
+ effect on the page: an error message shown, a failed API call whose content is missing on
75
+ screen, a broken or missing content image. Universal checks need no expectations file: such
76
+ a failure is a defect even without one. An API call that answers 2xx with an empty body is a
77
+ defect only when an expectation says data is due there; without one it is suspect at most
78
+ (it may be a normal absence);
79
+ - *suspect* — something that looks like missing or wrong data and that no expectation
80
+ settles: an empty list under a heading, a "nothing found" message, a status or label
81
+ contradicted by the page's own data ("eliminated" beside a won match), a stale season
82
+ label. Say why, and propose the line of expectations that would settle it;
83
+ - *noise* — a console error or a failed request with no visible effect: reported, ranked minor;
84
+ - *out of scope* — usability and accessibility belong to `ux-reviewer`, code quality to
85
+ `code-reviewer`: one line at most, never a finding.
86
+ 7. **Rank** each finding: *blocking* (a page's main content is missing, its main information is
87
+ false, or an error is shown to the user), *major* (secondary content missing or wrong, a section
88
+ silently dropped after a failed or empty API call, a broken content image), *minor* (noise).
89
+
90
+ ## Output
91
+
92
+ A short report:
93
+
94
+ - **Pages checked N/N**, with the base URL and the date and time of the run, and the two widths. The
95
+ second N is every page of the expectations (or every route discovered): a page you could not open
96
+ is counted and named, never dropped. A page counts as checked when both widths were measured; a
97
+ page checked partially (one width, tabs not opened) is counted and named as partial.
98
+ - **Findings**, most severe first, each with: the route, its kind and rank, what was expected —
99
+ quote the line of the expectations, or name the universal check, or, for a suspect, give the
100
+ expectation line you propose —, what was measured, and the evidence — status code, response
101
+ size, the text on screen, the capture. No finding without a measurement.
102
+ - **Not verified**: pages behind a PIN or a login, states that need data you could not get, a
103
+ browser tool that was missing or could not give status and size — stated plainly.
104
+ - **Proposed follow-ups**: one `raf add "…"` line per finding worth doing; on a read-only plan
105
+ (`cadence.yaml` maps the fields of a file kept by another tool), plain lines for the project's
106
+ own tool instead. Without an expectations file, the draft comes here.
107
+ - **Verdict**, one line, alone — e.g. "6/6 pages as expected", "not as expected: 1 blocking
108
+ (/players shows no player)", "no expectations file: 13 pages walked, 1 defect, 8 suspects, draft
109
+ returned". It is the last line of the report.
110
+
111
+ Captures and temporary files go in a temporary directory outside the repository, or in the one
112
+ the caller names; remove them, or list their paths in the report. The working tree is left as you
113
+ found it.
114
+
115
+ ## Do not
116
+
117
+ - Report an impression: a finding you have not measured in the browser is not a finding.
118
+ - Take a green health endpoint, a passing test suite, or "the code shows this message on purpose"
119
+ as proof that a page is fine.
120
+ - Excuse an empty page by its cause: an upstream outage explains a defect, it does not remove it.
121
+ - Edit code, the plan or the expectations file, commit, or mark anything done: the session that
122
+ called you does it.
package/bin/cadence.js CHANGED
@@ -26,7 +26,8 @@ if (tool === 'raf') {
26
26
  cadence session start [--since "24 hours ago"] [--idle 2]
27
27
  faits de reprise : notes de la veille, en cours, fait depuis, écarts, propositions
28
28
  cadence session close [--since …] faits de clôture ; code 1 tant que ce n'est pas fermé
29
- cadence session next "ligne" … notes pour la prochaine session (sans argument : efface)
29
+ cadence session next "ligne" … notes pour la prochaine session (remplacent les précédentes)
30
+ cadence session next --clear efface ces notes ; sans ligne ni --clear, la commande refuse
30
31
  cadence deliver [--dry-run] [--sha rév] [--config cadence.yaml] [-- arguments du script du projet]
31
32
  CI du sha poussé → déploiement → vérifications de l'effet ;
32
33
  ou le script de livraison du projet (deliver.script), sous verrou et journal
package/dist/audit.js CHANGED
@@ -14,15 +14,18 @@ export function planCommits(plan, root, opts = {}) {
14
14
  const commits = readCommits(root, opts);
15
15
  return patterns.length ? commits.filter((c) => !patterns.some((re) => re.test(c.subject))) : commits;
16
16
  }
17
- /** Le commit ne touche-t-il que le plan (ou la page Gantt) ? */
17
+ /** Commit d'entretien du plan : ne touche-t-il que des fichiers du plan (plan, page Gantt, plan.files) ? */
18
18
  export function isPlanOnly(sha, plan, root) {
19
19
  const own = ownFiles(plan, root);
20
20
  const files = changedFiles(root, sha);
21
21
  return files.length > 0 && files.every((f) => own.has(f));
22
22
  }
23
23
  /**
24
- * N'ont pas besoin de citer un lot : un commit qui ne touche que le plan (ou la page Gantt), et un
25
- * commit automatique dont le sujet correspond à un motif `ignore:` du plan.
24
+ * N'ont pas besoin de citer un lot : un commit d'entretien du plan — TOUS ses fichiers sont des fichiers
25
+ * du plan (le plan, sa page Gantt, ceux que le projet déclare sous plan.files, son plan publié par
26
+ * exemple) — et un commit automatique dont le sujet correspond à un motif `ignore:` du plan.
27
+ * Les fichiers décident, jamais le sujet : « chore(plan): … » qui touche un fichier source est un
28
+ * commit comme un autre.
26
29
  */
27
30
  export function exemptPlanOnly(linked, plan, root) {
28
31
  const own = ownFiles(plan, root);
package/dist/cli.js CHANGED
@@ -15,7 +15,7 @@ import { Plan, RafError, STATUSES } from './plan.js';
15
15
  import { schedule } from './schedule.js';
16
16
  import { AGENTS_DIR, installAgents, installSkills, SKILLS_DIR } from './skills.js';
17
17
  import { sessionClose, sessionStart } from './session.js';
18
- import { sharedStateDir, stateDir, writeNext } from './state.js';
18
+ import { clearNext, readNext, sharedStateDir, stateDir, writeNext } from './state.js';
19
19
  const HELP = `raf — plan « reste à faire » versionné dans le dépôt, relié aux commits
20
20
 
21
21
  raf init [--project nom] [--prefix L] [--no-hook]
@@ -35,6 +35,8 @@ const HELP = `raf — plan « reste à faire » versionné dans le dépôt, reli
35
35
  raf news new <lot…> [--title t] | list | check | stamp | build [-o dossier] (aussi « cadence news … »)
36
36
 
37
37
  Un commit appartient à un lot quand son message cite l'identifiant : « feat(L3): … », « L3/t1 ».
38
+ Un commit qui ne touche que le plan (et les fichiers déclarés sous plan.files dans cadence.yaml, un plan
39
+ publié par exemple) n'a pas à en citer, et ne compte pas pour les lots qu'il cite ; le sujet n'y change rien.
38
40
  Un lot --visible attend une entrée Nouveautés (docs/nouveautes/, --dir) avec capture ; raf check le vérifie.
39
41
  Un texte qui commence par « - » se passe après « -- » : raf note L1 -- "-5 %".
40
42
  Le plan est docs/plan/raf.yaml, ou celui que nomme « plan: » dans cadence.yaml ; un plan tenu par un
@@ -85,6 +87,7 @@ function dispatch(argv, io) {
85
87
  config: { type: 'string' },
86
88
  'dry-run': { type: 'boolean' },
87
89
  sha: { type: 'string' },
90
+ clear: { type: 'boolean' },
88
91
  help: { type: 'boolean', short: 'h' },
89
92
  },
90
93
  });
@@ -429,10 +432,26 @@ function session([sub, ...args], ctx, values) {
429
432
  }
430
433
  case 'close':
431
434
  return sessionClose(ctx, { since: values.since ?? `${ctx.today} 00:00` });
432
- case 'next':
433
- writeNext(ctx.state, ctx.today, args);
435
+ case 'next': {
436
+ const lines = args.filter((l) => l.trim() !== '');
437
+ if (values.clear) {
438
+ if (lines.length)
439
+ throw new RafError('session next --clear efface les notes : ne pas lui passer de ligne');
440
+ const gone = clearNext(ctx.state);
441
+ ctx.out(gone ? `notes effacées (${gone.lines.length} ligne(s) du ${gone.date})` : 'aucune note à effacer');
442
+ return 0;
443
+ }
444
+ // Lancée sans ligne (variable vide dans un script, agent pressé), la commande effaçait en silence
445
+ // les notes de la dernière clôture : effacer se demande exprès.
446
+ if (lines.length === 0) {
447
+ const kept = readNext(ctx.state);
448
+ throw new RafError(`session next : aucune ligne — ${kept ? `${kept.lines.length} ligne(s) du ${kept.date} conservée(s)` : "rien n'est écrit"} ; ` +
449
+ 'usage : cadence session next "ligne" … (pour effacer les notes exprès : cadence session next --clear)');
450
+ }
451
+ writeNext(ctx.state, ctx.today, lines);
434
452
  return 0;
453
+ }
435
454
  default:
436
- throw new RafError('usage : cadence session start [--since …] [--idle 2] | close [--since …] | next "ligne" …');
455
+ throw new RafError('usage : cadence session start [--since …] [--idle 2] | close [--since …] | next "ligne" … | next --clear');
437
456
  }
438
457
  }
package/dist/deliver.js CHANGED
@@ -343,14 +343,22 @@ async function verifyAll(ctx, deps, sha, env) {
343
343
  }
344
344
  return null;
345
345
  }
346
- /** Ligne des lots cités depuis la livraison précédente, ou null (première livraison, rien de cité). */
346
+ /**
347
+ * Ligne des lots cités depuis la livraison précédente, ou null (première livraison, rien de cité).
348
+ *
349
+ * Plan en lecture seule : seuls les lots EN COURS sont annoncés. Ses identifiants sont de forme libre, et
350
+ * un message cite volontiers un numéro qui en a la forme (réserve « R1 » d'une revue) ou un lot clos
351
+ * nommé pour le contexte — les annoncer « livrés » ferait fermer à tort. L'état est celui du départ de
352
+ * la livraison : le plan a été lu avant que le script du projet ne ferme lui-même les lots qu'il livre.
353
+ */
347
354
  function deliveredLots(ctx, prev, sha) {
348
355
  if (!ctx.plan || !prev || prev === sha)
349
356
  return null;
350
357
  if (!isAncestor(ctx.root, prev, sha)) {
351
358
  return `livraison précédente (${prev.slice(0, 7)}) hors de l'historique de ${sha.slice(0, 7)} (réécrit ?) : lots livrés non calculés`;
352
359
  }
353
- const known = new Set(ctx.plan.lots().map((l) => l.id));
360
+ const lots = ctx.plan.lots();
361
+ const known = new Set((ctx.plan.readonly ? lots.filter((l) => l.status === 'doing') : lots).map((l) => l.id));
354
362
  const ids = new Set();
355
363
  for (const c of readCommits(ctx.root, { range: `${prev}..${sha}` })) {
356
364
  for (const r of ctx.plan.refs(`${c.subject}\n${c.body}`))
package/dist/news.js CHANGED
@@ -16,8 +16,52 @@ function validStamp(s) {
16
16
  return (!Number.isNaN(Date.parse(s.replace(' ', 'T'))) && day.getUTCMonth() === mo - 1 && day.getUTCDate() === d && h < 24 && mi < 60 && sec < 60);
17
17
  }
18
18
  const list = (v) => (v == null ? [] : Array.isArray(v) ? v.map(String) : [String(v)]);
19
+ const CAPTURE_KEYS = ['file', 'alt'];
20
+ const CAPTURE_SHAPE = '{ file: chemin, alt: "texte alternatif" }';
21
+ /**
22
+ * Captures de l'en-tête : un chemin, ou `{ file, alt }` quand l'entrée dit ce que l'image montre.
23
+ * Les chemins restent une liste de chaînes (ce que lit déjà le JSON publié), les textes les suivent un à un.
24
+ */
25
+ function readCaptures(raw, problems) {
26
+ const captures = [];
27
+ const alts = [];
28
+ for (const [i, item] of (raw == null ? [] : Array.isArray(raw) ? raw : [raw]).entries()) {
29
+ let file;
30
+ let alt = '';
31
+ if (typeof item === 'string' || typeof item === 'number') {
32
+ file = String(item);
33
+ }
34
+ else if (item && typeof item === 'object' && !Array.isArray(item)) {
35
+ const map = item;
36
+ if (map.file == null || String(map.file).trim() === '') {
37
+ problems.push(`capture n° ${i + 1} : fichier absent — ${CAPTURE_SHAPE}`);
38
+ continue;
39
+ }
40
+ file = String(map.file);
41
+ // Une ligne : le texte part tel quel dans un attribut alt.
42
+ alt = map.alt == null ? '' : String(map.alt).replace(/\s+/g, ' ').trim();
43
+ for (const k of Object.keys(map)) {
44
+ if (!CAPTURE_KEYS.includes(k))
45
+ problems.push(`capture ${file} : clé inconnue « ${k} » (attendu : ${CAPTURE_KEYS.join(', ')})`);
46
+ }
47
+ }
48
+ else {
49
+ problems.push(`capture n° ${i + 1} illisible : un chemin ou ${CAPTURE_SHAPE}`);
50
+ continue;
51
+ }
52
+ // Copiées telles quelles sous le dossier de build : un chemin qui sort du dossier écrirait ailleurs.
53
+ if (isAbsolute(file) || file.split(/[\\/]/).includes('..'))
54
+ problems.push(`capture hors du dossier des Nouveautés : ${file}`);
55
+ // Noms sages : utilisables tels quels dans une URL, et seulement des images.
56
+ else if (!CAPTURE.test(file))
57
+ problems.push(`capture ${file} : image .png, .jpg, .webp ou .gif, nom en lettres, chiffres, « . _ - / »`);
58
+ captures.push(file);
59
+ alts.push(alt);
60
+ }
61
+ return { captures, alts };
62
+ }
19
63
  export function parseEntry(file, text) {
20
- const entry = { file, slug: basename(file).replace(/\.md$/, ''), title: '', date: '', lots: [], captures: [], body: '', problems: [] };
64
+ const entry = { file, slug: basename(file).replace(/\.md$/, ''), title: '', date: '', lots: [], captures: [], alts: [], body: '', problems: [] };
21
65
  const m = FRONT.exec(text.replace(/^\uFEFF/, '')); // BOM des éditeurs Windows
22
66
  if (!m) {
23
67
  entry.problems.push('en-tête YAML absent (--- … ---)');
@@ -48,18 +92,10 @@ export function parseEntry(file, text) {
48
92
  entry.created = created.replace(' ', 'T');
49
93
  }
50
94
  entry.lots = list(head.lots);
51
- entry.captures = list(head.captures);
52
95
  if (head.nocapture != null && String(head.nocapture).trim())
53
96
  entry.nocapture = String(head.nocapture).trim();
54
97
  entry.body = m[2].trim();
55
- for (const c of entry.captures) {
56
- // Copiées telles quelles sous le dossier de build : un chemin qui sort du dossier écrirait ailleurs.
57
- if (isAbsolute(c) || c.split(/[\\/]/).includes('..'))
58
- entry.problems.push(`capture hors du dossier des Nouveautés : ${c}`);
59
- // Noms sages : utilisables tels quels dans une URL, et seulement des images.
60
- else if (!CAPTURE.test(c))
61
- entry.problems.push(`capture ${c} : image .png, .jpg, .webp ou .gif, nom en lettres, chiffres, « . _ - / »`);
62
- }
98
+ Object.assign(entry, readCaptures(head.captures, entry.problems));
63
99
  if (!entry.title)
64
100
  entry.problems.push('title vide');
65
101
  if (!isDay(entry.date))
@@ -136,7 +172,7 @@ export function newEntry(dir, lots, rawTitle, today, now) {
136
172
  if (existsSync(path))
137
173
  throw new RafError(`${path} existe déjà`);
138
174
  mkdirSync(dir, { recursive: true });
139
- writeFileSync(path, `---\ntitle: ${stringify(title).trimEnd()}\ndate: ${today}\ncreated: ${toStamp(now)}\nlots: [${lots.join(', ')}]\ncaptures: []\n# nocapture: raison, quand une capture n'a pas de sens\n---\nCe qui change pour l'utilisateur.\n`);
175
+ writeFileSync(path, `---\ntitle: ${stringify(title).trimEnd()}\ndate: ${today}\ncreated: ${toStamp(now)}\nlots: [${lots.join(', ')}]\ncaptures: []\n# une capture peut dire ce qu'elle montre : captures: [${CAPTURE_SHAPE.replace('chemin', 'captures/x.png')}]\n# nocapture: raison, quand une capture n'a pas de sens\n---\nCe qui change pour l'utilisateur.\n`);
140
176
  return path;
141
177
  }
142
178
  /**
@@ -210,7 +246,16 @@ export function newsData(project, entries, generated) {
210
246
  return {
211
247
  project,
212
248
  generated,
213
- entries: entries.map((e) => ({ slug: e.slug, title: e.title, date: e.date, lots: e.lots, captures: e.captures, html: renderMarkdown(e.body) })),
249
+ entries: entries.map((e) => ({
250
+ slug: e.slug,
251
+ title: e.title,
252
+ date: e.date,
253
+ lots: e.lots,
254
+ captures: e.captures,
255
+ // Clé ajoutée seulement quand elle dit quelque chose : une entrée sans texte garde sa forme d'avant.
256
+ ...(e.alts.some(Boolean) ? { alts: e.alts } : {}),
257
+ html: renderMarkdown(e.body),
258
+ })),
214
259
  };
215
260
  }
216
261
  /** Écrit nouveautes.json, index.html et copie les captures sous `out`. */
@@ -237,7 +282,7 @@ function renderNewsPage(data) {
237
282
  <h2>${escapeText(e.title)}</h2>
238
283
  <p class="meta"><time datetime="${e.date}">${e.date}</time> · ${e.lots.map(escapeText).join(', ')}</p>
239
284
  ${e.html}
240
- ${e.captures.map((c) => `<a href="${escapeText(c)}"><img src="${escapeText(c)}" alt="Capture : ${escapeText(e.title)}" loading="lazy"></a>`).join('\n ')}
285
+ ${e.captures.map((c, i) => `<a href="${escapeText(c)}"><img src="${escapeText(c)}" alt="${escapeText(e.alts?.[i] || `Capture : ${e.title}`)}" loading="lazy"></a>`).join('\n ')}
241
286
  </article>`)
242
287
  .join('\n');
243
288
  return `<!doctype html>
package/dist/plan.js CHANGED
@@ -12,9 +12,15 @@ export function isOpen(status) {
12
12
  function escapeRe(s) {
13
13
  return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
14
14
  }
15
+ /**
16
+ * Ce qui ne peut pas suivre un identifiant cité : un point suivi d'un caractère de mot — « L1.4 » n'est
17
+ * pas le lot L1, « NC2.4 » n'est pas NC2 — alors que le point qui finit une phrase (« voir L1. ») passe.
18
+ * Même garde pour le format de raf et pour les identifiants d'un plan en lecture seule.
19
+ */
20
+ const DOTTED = '\\.\\w';
15
21
  /** Matches `L3` or `L3/t1` as whole words. Group 1 = lot id, group 2 = task id. */
16
22
  export function refPattern(prefix) {
17
- return new RegExp(`(?<![\\w/])(${escapeRe(prefix)}\\d+)(?:/(t\\d+))?(?![\\w])`, 'g');
23
+ return new RegExp(`(?<![\\w/])(${escapeRe(prefix)}\\d+)(?:/(t\\d+))?(?!\\w|${DOTTED})`, 'g');
18
24
  }
19
25
  export function extractRefs(text, prefix) {
20
26
  return [...text.matchAll(refPattern(prefix))].map((m) => ({ lot: m[1], task: m[2] }));
@@ -149,7 +155,7 @@ export class Plan {
149
155
  ids: new Set(ids),
150
156
  tasks: new Set(lots.flatMap((l) => l.tasks.map((t) => `${l.id}/${t.id}`))),
151
157
  refs: ids.length
152
- ? new RegExp(`(?<![\\w/.-])(${ids.map(escapeRe).join('|')})(?:/([\\w-]+(?:\\.[\\w-]+)*))?(?![\\w-]|\\.\\w)`, 'g')
158
+ ? new RegExp(`(?<![\\w/.-])(${ids.map(escapeRe).join('|')})(?:/([\\w-]+(?:\\.[\\w-]+)*))?(?![\\w-]|${DOTTED})`, 'g')
153
159
  : null,
154
160
  };
155
161
  }
@@ -431,7 +437,11 @@ function asVerdict(raw) {
431
437
  if (!raw || typeof raw !== 'object')
432
438
  return undefined;
433
439
  const v = raw;
434
- const verdict = { date: String(v.date ?? ''), verdict: String(v.verdict ?? '') };
440
+ // Écrit à la main sans verdict (clé absente, vide ou blanche) : rien n'a été dit de la revue, la porte
441
+ // reste fermée — comme `raf ux` et `raf review` refusent d'enregistrer un verdict vide.
442
+ if (String(v.verdict ?? '').trim() === '')
443
+ return undefined;
444
+ const verdict = { date: String(v.date ?? ''), verdict: String(v.verdict) };
435
445
  // Champ présent mais vide : relu « jusqu'à rien », comme un verdict noté sans commit.
436
446
  if ('commit' in v)
437
447
  verdict.commit = v.commit == null || String(v.commit).trim() === '' ? null : String(v.commit).trim();
package/dist/state.js CHANGED
@@ -21,14 +21,17 @@ export function readNext(dir) {
21
21
  const lines = rest.map((l) => l.replace(/^- /, '')).filter(Boolean);
22
22
  return { date: head.replace(/^# /, '').trim(), lines };
23
23
  }
24
- /** Remplace les notes pour la prochaine session ; sans ligne, les efface. */
24
+ /** Remplace les notes pour la prochaine session. Ne les efface jamais : c'est `clearNext`, demandé exprès. */
25
25
  export function writeNext(dir, date, lines) {
26
- const file = join(dir, 'next.md');
27
- if (lines.length === 0) {
28
- rmSync(file, { force: true });
29
- return;
30
- }
31
- writeFileSync(file, `# ${date}\n${lines.map((l) => `- ${l.replace(/\n/g, ' ')}`).join('\n')}\n`);
26
+ if (lines.length === 0)
27
+ throw new Error('writeNext : aucune ligne');
28
+ writeFileSync(join(dir, 'next.md'), `# ${date}\n${lines.map((l) => `- ${l.replace(/\n/g, ' ')}`).join('\n')}\n`);
29
+ }
30
+ /** Efface les notes pour la prochaine session ; rend celles qui s'y trouvaient, null s'il n'y en avait pas. */
31
+ export function clearNext(dir) {
32
+ const previous = readNext(dir);
33
+ rmSync(join(dir, 'next.md'), { force: true });
34
+ return previous;
32
35
  }
33
36
  export function lockPath(dir) {
34
37
  return join(dir, 'deliver.lock');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sylad/cadence",
3
- "version": "0.5.0",
3
+ "version": "0.7.0",
4
4
  "description": "A small, repo-native working method: a versioned plan linked to your commits, a changelog with screenshots, session rituals and deliveries proven by their effect.",
5
5
  "license": "MIT",
6
6
  "author": "Sylvain Ladoire",
@@ -51,7 +51,18 @@ Deliver one project at a time: the one the human names, or ask.
51
51
  4. On failure: read which step failed and why. Fix the cause, commit, push, deliver again. Never rerun
52
52
  blindly, never skip a check to make it pass.
53
53
  5. On success: `raf done <id>` (or the project's own tool when its plan is read-only) for the lots it lists **whose effect you have seen**; if one of them is
54
- `visible`, `cadence news build` and deliver the news too.
54
+ `visible`, `cadence news build` and deliver the news too. With a read-only plan the list holds only
55
+ the lots that were in progress when the delivery started; the project's own tool has the last word
56
+ on what it marked delivered.
57
+ 6. After a green delivery that changes what a page shows or what it is served (screen, API, data
58
+ source, configuration of either) — in practice every delivery except docs-, plan- or tests-only
59
+ ones — have the `qa-reviewer` agent walk the delivered app in a real browser, whether the lot
60
+ is `visible` or not: give it the repository path, the base URL and the lot id. When the lot
61
+ touched only the backend, the agent starts with the pages that call the changed endpoints. It
62
+ checks each page against `docs/qa/expectations.md` — what the user must find there — and
63
+ reports a page left empty, an error shown, an API call that failed or came back empty: what
64
+ the checks of `cadence.yaml` do not see. Bring its blocking findings to the human. It is not a
65
+ gate: the delivery stays done, a finding becomes a new lot.
55
66
 
56
67
  ## Rules
57
68
 
@@ -74,6 +74,20 @@ proposed sub-tasks back to the human.
74
74
  One project at a time, by the lead: push, then the `deliver` skill (`cadence deliver --dry-run`, then
75
75
  `cadence deliver`). Follow the human's standing instructions about confirmation before production.
76
76
 
77
+ After a green delivery that changes what a page shows or what it is served (screen, API, data
78
+ source, configuration of either) — in practice every delivery except docs-, plan- or tests-only ones
79
+ — have the `qa-reviewer` agent check the delivered app, as a fresh subagent: give it the absolute
80
+ path of the project, the base URL of the delivered app and the lot id. The lot need not be
81
+ `visible`: a backend-only lot can empty a page without changing a screen. When the lot touched only
82
+ the backend, the agent starts with the pages that call the changed endpoints. It walks the pages in
83
+ a real browser against the project's expectations (`docs/qa/expectations.md`: per page, what the
84
+ user must find there) and returns measured findings; it reads only, and never logs in. Bring its
85
+ blocking findings back to the human — a page whose main content is missing, or that shows an error,
86
+ is a defect even when the delivery checks are green — with its proposed follow-up lines. A project
87
+ without an expectations file gets a draft back: show it to the human, who corrects it and decides
88
+ whether it is committed. It is not a gate: `raf done` does not wait for it, and a finding becomes a
89
+ new lot, not a reopened one.
90
+
77
91
  ## 5. Close
78
92
 
79
93
  At the end, the `session-close` routine in each project touched, and `cadence session next` lines in
@@ -24,6 +24,9 @@ With no project named, run `cadence session close` in each project touched durin
24
24
  otherwise `raf note <id> "where it stands, what blocks"`;
25
25
  - a commit without a lot that belongs to one → `raf note <id> "commits: <sha> …"`; nothing if it
26
26
  is genuinely outside the plan (docs, chores);
27
+ - a plan commit reported because it also touches a file generated from the plan (a published
28
+ plan) → propose to declare that file under `plan.files` in `cadence.yaml`; the files of a commit
29
+ decide whether it is plan upkeep, never its subject;
27
30
  - a finished lot marked `visible` without a news entry → `cadence news new <id>`, written for the
28
31
  user, with a screenshot;
29
32
  - a lot that `raf done` refuses, or that the check reports as finished, for lack of a review —
@@ -43,6 +46,8 @@ With no project named, run `cadence session close` in each project touched durin
43
46
  5. **Clean state**: everything committed and pushed, no delivery running. If the command still exits 1,
44
47
  say what remains and do NOT say the session is closed.
45
48
  6. **Three lines for next time**: `cadence session next "…" "…" "…"` — the next `session-start` shows them.
49
+ The lines replace the previous notes. Without a line the command refuses and keeps them; erase
50
+ them on purpose with `cadence session next --clear`, only when nothing is left to say.
46
51
 
47
52
  ## Do not
48
53