@sylad/cadence 1.20.0 → 1.21.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.
@@ -8,7 +8,7 @@
8
8
  {
9
9
  "name": "cadence",
10
10
  "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).",
11
- "version": "1.20.0",
11
+ "version": "1.21.1",
12
12
  "source": "./",
13
13
  "author": {
14
14
  "name": "Sylvain Ladoire"
@@ -16,8 +16,8 @@
16
16
  },
17
17
  {
18
18
  "name": "cadence-hud",
19
- "description": "A status band above the Claude Code prompt: context, 5 h / 7 d quota windows, cost per model, agents of the session and the running cadence orchestrate waves. Works without the cadence CLI, except the per-project plan progress, which needs `cadence` on the PATH.",
20
- "version": "0.5.0",
19
+ "description": "A status band above the Claude Code prompt: context, 5 h / 7 d quota windows, cost per model, agents of the session and the running cadence orchestrate waves, plus a read-only lots panel (`/hud lots`). Works without the cadence CLI, except the per-project plan progress and the lots panel, which need `cadence` on the PATH.",
20
+ "version": "0.6.1",
21
21
  "source": "./plugins/cadence-hud",
22
22
  "author": {
23
23
  "name": "Sylvain Ladoire"
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "cadence",
3
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": "1.20.0",
4
+ "version": "1.21.1",
5
5
  "author": { "name": "Sylvain Ladoire" },
6
6
  "homepage": "https://github.com/Sylad/cadence",
7
7
  "repository": "https://github.com/Sylad/cadence",
package/dist/cli.js CHANGED
@@ -14,6 +14,7 @@ import { ganttData, renderGantt } from './gantt.js';
14
14
  import { PLAN_ACTIONS, planCommitMessage } from './plan-commit.js';
15
15
  import { dirtyPaths, gitRoot, isTracked, readCommits, matchesHead, resolveCommit, stage, stagedFiles, syncIndexWithHead } from './git.js';
16
16
  import { installHook, pendingCommitMessage, REFUSED } from './hook.js';
17
+ import { pushRefusal } from './push-guard.js';
17
18
  import { citedRefs, linkCommits } from './link.js';
18
19
  import { buildNews, loadEntries, newEntry, newsData, newsIssues, publicTitleTooLong, stampEntries } from './news.js';
19
20
  import { invalidResource, Plan, RafError, SCREENS, STATUSES, setPlanNotice } from './plan.js';
@@ -741,7 +742,9 @@ function dispatch(argv, io) {
741
742
  return postCommit(existsSync(planPath) ? loadPlan : null, newsDir, root, today, io, readHookConfig(configPath).autostart);
742
743
  if (rest[0] === 'pre-commit')
743
744
  return preCommit(existsSync(planPath) ? loadPlan : null, readHookConfig(configPath).autostart, root, today, io);
744
- throw new RafError('usage : raf hook install|pre-commit|post-commit');
745
+ if (rest[0] === 'pre-push')
746
+ return prePush(root, io, rest[1]);
747
+ throw new RafError('usage : raf hook install|pre-commit|post-commit|pre-push');
745
748
  }
746
749
  case 'session': {
747
750
  // Le contexte de la session ne dépend d'aucun dépôt : le lead le lit depuis le dossier parent.
@@ -1121,6 +1124,34 @@ function morningEffects(configPath, root) {
1121
1124
  return [`✗ ${e.message}`];
1122
1125
  }
1123
1126
  }
1127
+ /**
1128
+ * `raf hook pre-push` (L405), appelé par le hook que la vague pose : refuse (code 3) un push qui emporte un commit d'un lot pas prêt de la vague vivante
1129
+ * du dépôt, ou dont un lot pas prêt a des commits illisibles (la garde de `deliver --sha`). Sans vague qui tient le dépôt, ou avec un état de vague illisible
1130
+ * (avertissement), un hook ne gêne jamais un push.
1131
+ */
1132
+ function prePush(root, io, remoteName) {
1133
+ const holding = waveHolding(root);
1134
+ if (!holding)
1135
+ return 0;
1136
+ let input = '';
1137
+ try {
1138
+ input = io.stdin ? io.stdin() : readFileSync(0, 'utf8');
1139
+ }
1140
+ catch {
1141
+ return 0;
1142
+ }
1143
+ const lots = waveLotsIn(cadenceHome(), holding.pid, root);
1144
+ if (lots === null) {
1145
+ io.err(`raf: vague ${holding.wave} : état illisible, push non vérifié`);
1146
+ return 0;
1147
+ }
1148
+ const refusal = pushRefusal(root, holding.wave, lots, input, remoteName || undefined);
1149
+ if (refusal) {
1150
+ io.err(`raf: ${refusal}`);
1151
+ return REFUSED;
1152
+ }
1153
+ return 0;
1154
+ }
1124
1155
  /** La vague qui tient le verrou du dépôt tant qu'un de ses lots y travaille encore (état illisible : on la tient pour active) ; null si le dépôt est libre. Partagé par `deliver` et `spec reseal`. */
1125
1156
  function waveHolding(root) {
1126
1157
  const orchestrating = activeLock(join(sharedStateDir(root), REPO_LOCK));
package/dist/lead.js CHANGED
@@ -7,7 +7,8 @@ import { gitRoot, repoStatus } from './git.js';
7
7
  import { linkCommits } from './link.js';
8
8
  import { commitCount, lastActivity, lockStatus } from './session.js';
9
9
  import { parse } from 'yaml';
10
- import { RafError } from './plan.js';
10
+ import { isOpen, RafError } from './plan.js';
11
+ import { isRecurring } from './recurring.js';
11
12
  import { priorityRank, readPriority } from './orchestrate/continue.js';
12
13
  import { readNext, sharedStateDir, stateDir } from './state.js';
13
14
  import { freeLine, readyFree } from './ready-free.js';
@@ -65,6 +66,15 @@ export const TOUR_IDLE = 3;
65
66
  /** Un lot créé depuis moins de ce nombre de jours (aujourd'hui compris : 0 à 6 jours d'écart) compte parmi les « ajoutés cette semaine ». */
66
67
  export const ADDED_DAYS = 7;
67
68
  const empty = (project) => ({ project, doing: [], drift: [], notes: [], next: null, progress: { done: 0, doing: 0, todo: 0, added7: 0 }, repo: [] });
69
+ /** Les lots non clos du plan, hors récurrents, chacun suivi de ses sous-tâches non closes ; celles d'un lot clos ou récurrent sortent avec lui. */
70
+ export function openLots(lots) {
71
+ return lots
72
+ .filter((l) => isOpen(l.status) && !isRecurring(l))
73
+ .flatMap((l) => [
74
+ { id: l.id, title: l.title, status: l.status, after: l.after, ...(l.parent ? { parent: l.parent } : {}) },
75
+ ...l.tasks.filter((t) => isOpen(t.status)).map((t) => ({ id: `${l.id}/${t.id}`, title: t.title, status: t.status, after: [], parent: l.id })),
76
+ ]);
77
+ }
68
78
  export function truncate(text, max) {
69
79
  return text.length <= max ? text : `${text.slice(0, max - 1)}…`;
70
80
  }
@@ -135,6 +145,7 @@ export function tourRow(dir, today, idle = TOUR_IDLE, name = basename(dir)) {
135
145
  todo: counted.filter((l) => l.status === 'todo').length,
136
146
  added7: counted.filter((l) => l.created !== undefined && diffDays(l.created, today) >= 0 && diffDays(l.created, today) < ADDED_DAYS).length,
137
147
  };
148
+ row.lots = openLots(lots);
138
149
  const waiting = gateWaiting(lots, blocked).map((g) => g.lot.id);
139
150
  if (waiting.length)
140
151
  row.waiting = waiting;
@@ -704,6 +704,8 @@ async function session(c, kind, local = false) {
704
704
  if (playwright)
705
705
  l.uxCaptures = join(dirname(mcpConfig), 'playwright');
706
706
  const answerInBrief = l.pendingAnswer; // gardée pour la rendre si la session ne part jamais (créneau abandonné, L332)
707
+ if (answerInBrief && kind === 'fix')
708
+ step.answered = true;
707
709
  l.pendingAnswer = null; // la réponse est dans le brief : elle ne repart pas avec la session suivante
708
710
  const tmpDir = w.store.tmpDir(l.project, l.lot); // TMPDIR de la session, aussi en --add-dir : hors du cwd, un Write y serait refusé en `claude -p`
709
711
  // Délai (L298) : l'étape d'écriture est proportionnée à l'estimate et à la fiche du lot, puis prolongée à chaque échéance où elle a commité.
@@ -2199,12 +2201,19 @@ async function work(c, kind) {
2199
2201
  return;
2200
2202
  }
2201
2203
  // La session peut n'avoir plus rien à commiter (réponse du lead, lot commité avant la vague) : le travail du lot est déjà dans git.
2202
- // Même règle côté fix (L56) : la passe n'avait rien à ajouter, le lot a déjà ses commits de travail → revue neuve ; la passe reste consommée, MAX_PASSES borne la boucle.
2204
+ // Même règle côté fix (L56) : la passe n'avait rien à ajouter, le lot a déjà ses commits de travail → revue neuve, à condition que la session donne sa raison (L406, plus bas) ; la passe reste consommée, MAX_PASSES borne la boucle.
2203
2205
  const alreadyDone = hasWork(plan, wd(c), l);
2204
2206
  if (made === 0 && !alreadyDone && !headMoved) {
2205
2207
  stop(c, 'handed-back', `${kind} sans commit`);
2206
2208
  return;
2207
2209
  }
2210
+ // L406 : une correction qui ne commite rien doit dire pourquoi (« choix », lu par la revue). Sans raison, la revue relirait les mêmes commits et rendrait les mêmes constats : la passe ne compte pas (toFix l'avait comptée), le lot est rendu.
2211
+ const reason = (rep.choix ?? []).some((x) => x.trim()) || !!done.step.answered; // la réponse du lead, que la session a lue dans son brief, vaut raison
2212
+ if (kind === 'fix' && made === 0 && !headMoved && !docsPass && !reason) {
2213
+ l.pass = Math.max(0, l.pass - 1);
2214
+ stop(c, 'handed-back', 'fix sans commit ni raison (choix vide) : la revue aurait relu les mêmes commits');
2215
+ return;
2216
+ }
2208
2217
  // L317 : la passe du bibliothécaire qui laisse l'écart ouvert (rien commité, ou autre chose que la documentation) ne passe pas à la revue : le lot ne finirait pas prêt, `raf check` resterait rouge.
2209
2218
  if (docsPass) {
2210
2219
  const open = openDocGap(c);
@@ -2216,7 +2225,7 @@ async function work(c, kind) {
2216
2225
  if (headMoved)
2217
2226
  l.warnings.push(`${minorsPass ? 'passe des mineurs' : 'correction'} reprise sans nouveau commit : un commit d'une session précédente est relu par la revue`);
2218
2227
  else if (made === 0)
2219
- l.warnings.push(kind === 'fix' ? 'fix sans nouveau commit : revue lancée sur les commits du lot' : `${kind} sans nouveau commit : revue lancée sur les commits déjà faits du lot`);
2228
+ l.warnings.push(kind === 'fix' ? 'fix sans nouveau commit : revue lancée sur les commits du lot, avec la raison de la session (choix)' : `${kind} sans nouveau commit : revue lancée sur les commits déjà faits du lot`);
2220
2229
  const red = [];
2221
2230
  // Tests annoncés rouges (L213) : avec orchestrate.test, le programme rejoue et compare à la ligne de base, la session n'est crue que si les tests y sont rouges autrement que par des échecs de départ ; sans, la liste des échecs de base de la session, non vérifiée.
2222
2231
  const claimed = { source: 'tests', gravite: 'bloquant', texte: `tests annoncés rouges par la session : ${rep.tests.commande} — ${rep.tests.resultat}` };
@@ -13,6 +13,8 @@ if [ -n "$CADENCE_ORCHESTRATED" ]; then
13
13
  echo "cadence orchestrate : push refusé pendant la vague $CADENCE_ORCHESTRATED (le lead pousse)" >&2
14
14
  exit 1
15
15
  fi
16
+ # Le push du lead ou d'une session à la main : refusé s'il emporte un commit d'un lot pas prêt de la vague (code 3 de raf seulement : un raf absent ou en panne le laisse passer).
17
+ if command -v raf >/dev/null 2>&1; then raf hook pre-push "$@"; [ $? -ne 3 ] || exit 1; fi
16
18
  exit 0
17
19
  `;
18
20
  const hookPath = (repo) => join(hooksDir(repo), 'pre-push');
@@ -0,0 +1,42 @@
1
+ import { tryGit } from './git.js';
2
+ import { blockingLots, LABEL } from './orchestrate/table.js';
3
+ const ZERO = /^0+$/;
4
+ /** Combien de commits de la plage on essaie, du plus récent au plus ancien, pour trouver le dernier livrable. */
5
+ const SAFE_SEARCH = 200;
6
+ /**
7
+ * Le refus d'un push (hook pre-push de la vague, L405) : une ligne de l'entrée standard de git (`<réf locale> <sha local> <réf distante> <sha distant>`)
8
+ * dont l'ascendance du sha local (la garde de `deliver --sha`, pas la seule plage `distant..local`) tient un commit d'un lot de la vague qui n'est pas prêt,
9
+ * ou dont un lot pas prêt a des commits illisibles (plan qui ne le connaît plus, dépôt absent) : la vague refuserait ce push de toute façon (`leadDelivery`),
10
+ * le hook le refuse donc aussi, comme `deliver --sha`. Le message nomme les lots et leur état, et donne la commande sûre :
11
+ * `git push <remote> <dernier sha livrable>:<branche distante>` (le remote reçu en argument du hook, la branche de la réf distante) ; un lot illisible
12
+ * n'en laisse aucune, puisqu'aucun sha ne passe la garde tant qu'il l'est ; une réf poussée hors `refs/heads/` (un tag) n'en a jamais non plus (pousser un autre sha sous ce nom publierait un tag qui ne nomme pas son commit). Une suppression de branche, un sha illisible ou une plage sans lot pas prêt passent (null).
13
+ */
14
+ export function pushRefusal(root, wave, lots, input, remoteName = 'origin') {
15
+ for (const entry of input.split('\n')) {
16
+ const [ref, local, remoteRef, remote] = entry.trim().split(/\s+/);
17
+ if (!ref || !local || ZERO.test(local) || !/^[0-9a-f]{7,64}$/.test(local))
18
+ continue;
19
+ const base = remote && !ZERO.test(remote) ? remote : null;
20
+ // La garde de la vague (`leadDelivery`, `deliver --sha`) : toute l'ascendance du sha poussé, pas seulement la plage. Un commit pas prêt déjà sur le distant bloque encore.
21
+ const { blocking, unreadable } = blockingLots(lots, root, local);
22
+ if (blocking.length === 0 && unreadable.length === 0)
23
+ continue;
24
+ const target = remoteRef || ref;
25
+ // Hors refs/heads/ (un tag, une autre réf) : `<sha>:refs/tags/<tag>` créerait sur le distant un tag qui ne nomme pas le commit du tag local. Pas de commande sûre.
26
+ const notBranch = target.startsWith('refs/') && !target.startsWith('refs/heads/');
27
+ const branch = target.replace(/^refs\/heads\//, '');
28
+ const range = unreadable.length > 0 || notBranch ? null : tryGit(root, ['rev-list', '--first-parent', `-n${SAFE_SEARCH}`, base ? `${base}..${local}` : local]);
29
+ const safe = (range ?? '').split('\n').filter(Boolean).find((c) => {
30
+ const g = blockingLots(lots, root, c);
31
+ return g.blocking.length === 0 && g.unreadable.length === 0;
32
+ });
33
+ const name = (l) => `${l.project}:${l.lot} (${LABEL[l.status]})`;
34
+ const why = [
35
+ blocking.length > 0 ? `emporte des commits de lots pas prêts : ${blocking.map(name).join(', ')}` : null,
36
+ unreadable.length > 0 ? `commits illisibles (plan du projet ou dépôt) des lots pas prêts ${unreadable.map(name).join(', ')} : push non vérifiable` : null,
37
+ ].filter(Boolean).join(' ; ');
38
+ const how = notBranch ? `${target} doit nommer son propre commit : attendre que ces lots soient prêts, ou poser la réf sur un commit livrable` : safe ? `pousser seulement le livrable : git push ${remoteName} ${safe}:${branch}` : unreadable.length > 0 ? `rétablir le plan ou attendre la fin de ces lots` : `aucun commit livrable dans cette plage : attendre la fin de ces lots`;
39
+ return `push refusé — vague ${wave} vivante : ${local.slice(0, 7)} ${why} ; ${how} (--no-verify pour passer outre)`;
40
+ }
41
+ return null;
42
+ }
@@ -55,7 +55,31 @@ Install it as a plugin from the same marketplace:
55
55
  `/hud` hides or shows the band. `/hud cmd` lists the background commands being counted (id, tool,
56
56
  origin `session` or `agent <id>`, age, name; `(sans fin vue)` on the ones above). Any other argument
57
57
  `/hud projets` folds the project list to one line, and opens it again. Any other argument
58
- answers `argument inconnu : … (attendu : cmd, projets)` and leaves the band as it is. The source lives in [`plugins/cadence-hud`](../plugins/cadence-hud/README.md)
58
+ answers `argument inconnu : … (attendu : cmd, projets, lots)` and leaves the band as it is.
59
+
60
+ ### The lots panel (`/hud lots`, L401)
61
+
62
+ `/hud lots` opens a read-only **panel** beside the conversation (inline above the prompt on the main screen); the same command closes it. It is never opened on its own, and the command opens it whatever the width of the window, Claude placing it; when Claude cannot place it, the command answers `panneau des lots non placé : <reason>` and nothing opens. For each project of the folder (the order of `priority:`) it draws the open lots as a tree, with their state and their `after:` links:
63
+
64
+ ```
65
+ Lots lecture seule lu à 15:52
66
+ cadence 7 lots non clos
67
+ L401 Panneau des lots (mod cadence-hud, Pane) ▶ en cours
68
+ ├ L401/2 Écrire le panneau et son rafraîchissement ▶ en cours
69
+ └ L401/3 Test du rendu (claude plugin test) et doc ○ à faire
70
+ L402 Réordonner par glisser-déposer after: L400 L401 ⊘ bloqué
71
+ ol 1 lot non clos
72
+ L80 Calendrier des matchs ⟳ relit
73
+ /hud lots pour fermer
74
+ ```
75
+
76
+ States: `▶ en cours` (doing), `○ libre` (todo, no open `after:`), `⊘ bloqué` (todo, an `after:` is still open); a lot in a wave takes the wave's state and words, as the band writes them (`… en attente`, `⟳ implémente`, `⟳ relit`, `⟳ corrige`, `? QUESTION`, `✔ prêt`, `↩ rendu`, `✖ échec`, `⏸ suspendu`). In a native plan the sub-tasks of an open lot (`L401/2`) are its sub-lots (`├`/`└`); they only read `▶ en cours` or `○ à faire`, and are not counted in the project's `N lots non clos`. In a plan kept by another tool the sub-lots come from `parent:`. An `after:` id that is closed or unknown is struck through and blocks nothing. A project the tour could not read shows `illisible : <error>`; the others stay.
77
+
78
+ Before the first reading the panel says `Lecture des plans…`; if `cadence lead tour` does not answer it says `Lots illisibles : cadence lead tour n'a pas répondu. Nouvelle lecture dans une minute.` (a failed re-reading keeps the last table); a `cadence` too old to give the `lots` field says `Lots indisponibles : cadence est trop ancien, mettez-le à jour.`; with no open lot, `Aucun lot non clos dans les projets de ce dossier.`. A tree taller than the panel ends with `… +N lots de plus`. Under 60 columns the `after:` list moves to its own line and the title is cut with `…`, never the id or the state.
79
+
80
+ The data is the `lots` field of `cadence lead tour --json` ([lead tour](lead.md#lead-tour)), read together with the band's `progress` (same call, same folder). While the panel is open, the modification date of each plan (`.cadence/raf.yaml` and `cadence.yaml` of the folder and of its direct sub-folders) is read every 5 s, one `stat` each: a changed plan, like a wave transition, triggers a new reading before the minute is over. Cost of the `lots` field (R18, measured on 2026-10-28 with `cadence lead tour --json` on the parent folder of ten projects, three passes each): before L401 (cadence 1.19.1, no `lots`) 8.0 / 9.1 / 7.3 s and 86 KB of output; after (the L401 code) 7.5 / 7.0 / 8.4 s and 188 KB (192,503 bytes). The output more than doubles in size but the duration stays in the same order, under the 20 s refresh delay of the band, so the rule holds.
81
+
82
+ The source lives in [`plugins/cadence-hud`](../plugins/cadence-hud/README.md)
59
83
  (its own README has the details of every cell); it is not part of the npm package. To work on it:
60
84
  `claude plugin validate plugins/cadence-hud`, `claude plugin test plugins/cadence-hud`, and
61
85
  `tsc -p plugins/cadence-hud` once Claude Code has loaded the plugin at least once (it generates
package/docs/lead.md CHANGED
@@ -167,6 +167,8 @@ The commit count of a lot in progress is the one of `raf commits`: plan commits
167
167
  | `attend` | only when a lot waits on its `gate`: `attend L190` |
168
168
  | `dépôt` | `non commité` (modified or untracked files), `non poussé` (commits ahead of the upstream), `livraison en cours` (live delivery lock); `propre` otherwise |
169
169
 
170
+ With `--json`, each project row also carries `lots` (L401): the open lots (`todo`, `doing`, recurring ones left out) as `{ id, title, status, after, parent? }`, in plan order, `after` as the plan writes it (a closed or unknown id is kept as is). The open sub-tasks of those lots follow their lot as entries of their own (`id` = `<lot>/<n>`, `parent` = the lot, no `after`); those of a closed, dropped or recurring lot are left out with it. In a plan kept by another tool, `parent` is the lot's own `parent:` field. A row in error has no `lots`. The [lots panel](cadence-hud.md#the-lots-panel-hud-lots-l401) of `cadence-hud` reads this field.
171
+
170
172
  After them, one line per project that does not declare `orchestrate.test` (L327): `alpha · porte de tests absente : orchestrate.test n'est pas déclaré, la vague ne lancera pas la suite (seule la revue le fera si elle le décide) · commande probable : npm test (à déclarer en orchestrate.test)`. The probable command is read from `package.json` (`scripts.test`), `pyproject.toml` (pytest) or `pom.xml` (`mvn test`) and omitted when none exists; the lead declares `test:` and `build:` in that project's `cadence.yaml` after checking the command is green, one config commit per project. `--json`: `noTest: { probable }` on the project's row, absent when the key is declared. Same line in [`orchestrate --dry-run`](orchestrate.md).
171
173
 
172
174
  After the project lines the tour lists the **worktrees kept by waves** (L279, `orchestrate.worktrees: true`, on by default in cadence since L320; [`orchestrate.worktrees`](orchestrate.md#one-worktree-per-lot-orchestrateworktrees-l279)): a title, then one line per kept worktree — repository, lot, age, number of unintegrated commits, path — so that a lot handed back with unintegrated commits is never forgotten; nothing is printed when there is none (`--json`: `worktrees` on the project's row). `cadence orchestrate --status` and the final table of a wave print the same lines; the lead removes one with `cadence orchestrate --remove-worktree <project>:<lot> [--abandon-commits]`.
@@ -454,9 +454,13 @@ review) still starts with the implementation session: its brief tells it to read
454
454
  sub-tasks, which carry the findings of any earlier review. If that session ends without a new commit on a lot that
455
455
  already has commits, the wave goes on to the review (warning `implement sans nouveau commit`) instead of handing the
456
456
  lot back; with no commit on the lot at all it is handed back as `implement sans commit`. The same goes for a
457
- correction pass: a `fix` session that adds no commit to a lot that already has work commits goes on to a fresh review
458
- (warning `fix sans nouveau commit : revue lancée sur les commits du lot`), the pass still counted — the cap of two
459
- correction passes stays the guard against a loop; with no commit on the lot it is handed back as `fix sans commit`.
457
+ correction pass: a `fix` session that adds no commit to a lot that already has work commits must give its reason under
458
+ `choix` (the brief says so; a blank entry is no reason). With the reason, the wave goes on to a fresh review, which reads
459
+ it (warning `fix sans nouveau commit : revue lancée sur les commits du lot, avec la raison de la session (choix)`), the
460
+ pass counted — the cap of two correction passes stays the guard against a loop. Without it (L406), a review would reread
461
+ the same commits and return the same findings, so the lot is handed back as `fix sans commit ni raison (choix vide) :
462
+ la revue aurait relu les mêmes commits` and the pass is not counted (measured over the wave states of 2026-10-28 and before: 68 `fix` sessions without commit, 5 with an empty `choix` — about 7 %; the two L401 passes of 2026-10-28 gave a reason and stay counted, the test gate relaunched them, not a review). With no commit on the lot at all it is handed back as
463
+ `fix sans commit`.
460
464
 
461
465
  **Compliant with minor findings**: the minors are not left for a follow-up lot. One **minors pass** runs
462
466
  before concluding: a new Sonnet session with its own brief (`fix-minors.md`) fixes the minors that are right
@@ -766,7 +770,7 @@ still holds (those not yet released), the cap of each wave, the slots in use and
766
770
  **Guards, imposed by the code**: a global cap of simultaneous sessions, one wave per repository at
767
771
  a time (above); `Agent`, `Monitor`, `git push`, `cadence deliver`, `raf done|review|ux` are denied to the sessions, and they run with `CLAUDE_CODE_DISABLE_BACKGROUND_TASKS=1` (no `run_in_background` on Bash or sub-agents: `claude -p` waits for any background task before exiting, up to 10 min of inactivity, so a finished session kept its slot; L285 — the session is **not** killed at its `StructuredOutput`, the real cost is only in the final `result` message); a
768
772
  temporary `pre-push` hook, installed for the duration of the wave and removed at its end (or, for a repository, as soon as all the lots that touch it are finished), refuses any push
769
- from a session (`CADENCE_ORCHESTRATED` is in their environment; a repository that already has another
773
+ from a session (`CADENCE_ORCHESTRATED` is in their environment; L405: the same hook also calls `raf hook pre-push` for every other push, the lead's or a session started by hand: a push whose local sha has in its history — all of it, not only the range `remote..local`: a commit of a not-ready lot already on the remote still blocks, the wave would stop on « push détecté » —, a deleted branch carrying nothing, a commit of a lot of the live wave that is not ready (running, queued, in question, handed back, failed) is refused with exit 3, the message naming the lot, its state and the safe command `git push <remote> <last deliverable sha>:<remote branch>` (the newest commit of the range `remote..local`, first-parent, that holds no commit of a lot that is not ready; « aucun commit livrable » when there is none); a not-ready lot whose commits cannot be read (a plan that no longer knows the lot, a neighbour that is not a git repository) refuses the push as well, naming the lot (« commits illisibles … rétablir le plan ou attendre la fin de ces lots »), with no safe command since no sha passes the guard while it stays unreadable; a ref pushed outside `refs/heads/` (a tag) is refused the same way with no safe command at all, since pushing another sha under that name would publish a tag that does not name its own commit (wait for the lots to be ready, or tag a deliverable commit): the wave would not tolerate that push either and would stop on « push détecté »; a wave state that cannot be read only warns, an absent or broken `raf` lets the push through, `--no-verify` overrides — the same check as the guard of `cadence deliver --sha`; a repository that already has another
770
774
  `pre-push` hook is refused before anything starts — `pushurl` is never touched); after every session the
771
775
  upstream ref and `git ls-remote` are compared with the "before" (a `git checkout <sha>` or `git switch` that only moves `HEAD` — detached `HEAD`, another branch, no upstream any more — is not a push: the upstream ref is compared only when it is the same branch on both sides, `git ls-remote` stays the judge; the write briefs forbid `git checkout` and `git switch` in the repository and point to a throwaway `git worktree add --detach <tmp> <sha>`; should a write session still end off its starting branch, a clean tree with nothing outside the branch gets the branch put back (`git checkout <branch>`, a warning in the log, the lot carries on from the right `HEAD`), anything else — including a put-back that fails (`index.lock`, a conflicting file), cause « HEAD déplacé — remise de la branche <b> impossible : <git message> » — hands the lot back with the cause « HEAD déplacé » and holds the queue of that repository, the other repositories carrying on; a push or a removed guard hook in ANY watched repository, project or neighbour, is checked first and wins over a moved `HEAD`, L214), and a read-only step that left only UNTRACKED files (screenshots written at the root by a script with a relative path) is not an incident: the files are moved to `.cadence/runs/<wave>/<project>--<lot>/stray/` (kept in their tree; for a neighbour repository, under `stray/<its relative path, `/` replaced by `_`>/`, e.g. `stray/.._gitops/`), a warning naming them goes to the lot's warnings and the wave log, the verdict is kept and the lot continues (L162; a move that is impossible, or an untracked file that vanished during the step, stays an incident); a review that changed `HEAD` or a tracked file (a commit, a modified file) is an incident of that lot only: the lot is handed back to the lead, the lots of the other repositories carry on while the lots still queued behind it in the same repository are suspended (resumable, nothing started on a dirty tree), and a verdict the review had already produced is reported in the outcome and the lot's warnings (never recorded in the plan). **The lead's own push is the one exception (L352)**: `git push origin <sha>:main` to deliver a ready lot while the wave runs (the « livrable jusqu'à <sha> » path of the lead skill) is tolerated, because the pushed state is one the lead has re-verified. The check is the guard of `cadence deliver --sha`: every remote branch that moved advanced fast-forward (or was created) to a sha that is an ancestor of the repository's `HEAD` and holds no commit of a lot of the wave that is not ready (running, queued, in question, handed back, failed, or whose commits cannot be read), so no commit of a lot of the wave that is not ready (a commit that cites no lot, such as one of the plan, passes); the journal says `push du lead (livraison de <lot>) toléré` (`livraison du plan` when no ready lot is in the range) and the session carries on. Any other push — a deleted branch, a forced push, a sha outside the history of `HEAD`, a commit of a lot still running — and a removed guard hook still stop the wave. `raf done|ux|review` and `cadence deliver` refuse when
772
776
  `CADENCE_ORCHESTRATED` is set.
package/docs/raf.md CHANGED
@@ -76,6 +76,7 @@ raf gantt # .cadence/gantt.html (next to the plan)
76
76
  | `raf check --ignored` | list the acknowledged commits with their date and reason |
77
77
  | `raf check [--since date] [--idle 7]` | since the plan's adoption date by default (a visible lot without a public title is only a `⚠` warning: it never changes the exit code): commits without a lot (commits touching only plan files, or only version fields, `CHANGELOG.md`, the README's « What's new » section and `docs/whats-new.md` are exempt), unknown ids, `todo` lots that already have commits, idle lots (an open lot whose sub-lots, entries carrying `parent:` in a plan kept by another tool, have a recent commit or are in progress is not idle: L321), `done` lots with open sub-tasks, bad or circular dependencies, and an open lot titled « Revue UX — <screen> » without `visible: true` (`ux-review-not-visible`, an error: such a lot goes through precheck → implement → review with no UX step, so the screen is never reviewed — finance L32 on 2026-10-23; L271; the message names the fix, `raf visible <id> on`, L273; on a read-only plan, plan kept by another tool, it never names a `raf` command, which would refuse: with `plan.fields.visible` mapped it says to set `visible:` with the project's tool, without it it says the plan carries no `visible`, to map the field in `cadence.yaml` (`plan.fields.visible`) if the project's tool writes it, and that otherwise the UX review of that screen happens outside orchestrate, L321) |
78
78
  | `raf gantt [-o file]` | standalone Gantt page |
79
+ | `raf hook pre-push` | called by the temporary `pre-push` hook that `cadence orchestrate` installs (not by `raf hook install`): reads git's lines on the standard input and exits 3 when the push carries a commit of a lot of the live wave that is not ready, naming the lot and the safe `git push <remote> <sha>:<remote branch>`, or when the commits of a not-ready lot cannot be read (the guard of `deliver --sha`: the push is then unverifiable, the lot is named); exits 0 with no live wave on the repository |
79
80
  | `raf hook install` | add the post-commit hook (read-only, never blocks) and the pre-commit hook (does nothing unless `hook.autostart` says so, see below) |
80
81
 
81
82
  ### A commit on a lot that is still `todo`
package/docs/whats-new.md CHANGED
@@ -4,6 +4,8 @@
4
4
 
5
5
  ## In two lines
6
6
 
7
+ - **1.21.1**: `/hud lots` opens on the desktop app whatever the width; a correction session that makes no commit must say why, or the lot is handed back.
8
+ - **1.21.0**: `/hud lots` opens a panel beside the conversation with the open lots of every project; the pre-push hook of a wave refuses a push that carries a lot not ready.
7
9
  - **1.20.0**: `raf move <id> --before|--after <id>` reorders a lot or a sub-task in the plan, refused during a wave of the repository and on a plan kept by another tool.
8
10
  - **1.19.1**: `cadence verify --gate` in the main tree reads `HEAD` and `git status` again after the steps, says what moved and prints no green or red verdict that is not the sha's.
9
11
  - **1.19.0**: `raf archive` moves closed lots out of the plan file (565 KB → 5 KB, same output everywhere); live waves share one session cap; wave sessions skip the user's plugins.
@@ -45,6 +47,10 @@ Each is a paragraph in [docs/whats-new.md](whats-new.md) and a full section in [
45
47
 
46
48
  The long form of « In two lines » above (the README no longer carries a « What's new » section since L326: it links here). The full history, with the date of each version, is in [CHANGELOG.md](../CHANGELOG.md).
47
49
 
50
+ **1.21.1**: `/hud lots` opened nothing on the desktop app, even on a very wide screen (« terminal trop étroit », L408): the mod checked 110 columns itself, while a panel the person asks for is placed at any width; the engine now decides, and says « non placé » with its reason otherwise (cadence-hud 0.6.1). In a wave, a correction session that makes no commit must give its reason under `choix`; without one the lot is handed back instead of replaying the same review (L406, `docs/orchestrate.md`).
51
+
52
+ **1.21.0**: what is in progress, project by project, is now visible beside the conversation. `/hud lots` (cadence-hud 0.6.0, L401, `docs/cadence-hud.md`) opens a read-only panel with, for each project of the parent folder, the tree of its open lots and native sub-tasks, their state (in progress, ready, blocked, in a wave, handed back, in question) and their `after:` links, crossed out once satisfied; it reads `cadence lead tour --json`, which now carries a `lots` field (measured 7.0–8.4 s on ten projects, as before). The temporary `pre-push` hook of a wave also refuses a push of the lead or of a hand-started session that carries a commit of a lot not ready, names the lot and gives the safe command `git push <remote> <sha>:<branch>`; for a tag it refuses without a command, since a tag must name its own commit (L405, `docs/orchestrate.md`).
53
+
48
54
  **1.20.0**: the order of the plan is no longer edited by hand. `raf move <id> --before|--after <id>` (L400, `docs/raf.md`) moves a lot among the lots, or a sub-task among the sub-tasks of its lot; it is refused across different parents, on an archived lot, during a live wave of the repository (the same detection as `cadence deliver`) and on a plan kept by another tool (with that tool's command); a lot already in place leaves the file untouched, and like every plan write it does not commit. A test that read the real plan file directly, red since `raf archive`, now goes through the plan loader, and a guard test refuses any test that reads the real plan without it (L404).
49
55
 
50
56
  **1.19.1**: a verdict of `cadence verify --gate` run in the main tree is always the verdict of the sha it names (L393, `docs/deliver.md`): after the steps, `HEAD` and `git status` are read again; if the tree moved during the gate (a lot integrated, the plan of a wave committed), the output says what moved, the ✓/✗ lines are said not to be those of the sha, no « vert » / « rouge » line is written and the exit code is 2.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sylad/cadence",
3
- "version": "1.20.0",
3
+ "version": "1.21.1",
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",
@@ -25,7 +25,7 @@ A point you cannot hold is written under "choix" or "nonVerifie" of your report,
25
25
 
26
26
  Auto-verification once the tests are green, before the review: when the full test suite and the build pass, re-read the complete diff of your fix commits (`git diff <HEAD before your first fix>..HEAD`, run in each repository where you committed a fix: the project and any neighbouring repository above) against the grid of the code-reviewer agent: code bug; architecture drift; docs/architecture.md (or the project's architecture documentation) up to date; edge cases; one test per changed behaviour. What you find in your own fix, fix it at once (a `fix({{lot}}): …` commit) and rerun the suite and the build; touch nothing else. Then write in "resume" a paragraph that starts with « auto-vérification »: what you re-read, what you found and fixed, what you could not hold. It is one reading of the diff, not a second review: no subagent, no new exploration, no re-reading of files the diff does not touch.
27
27
 
28
- If a finding is wrong, say so under "choix" with the reason. Ask (under "questions") only when a fix needs a decision that passes the rule below.
28
+ If a finding is wrong, say so under "choix" with the reason. If you make no commit at all, "choix" MUST say why: a session with no commit and no reason is handed back to the lead without a new review (the review would reread the same commits). Ask (under "questions") only when a fix needs a decision that passes the rule below.
29
29
  A question is legitimate only if it names what it would change: the scope of the lot, the architecture, or a costly rollback (data, production, public API);
30
30
  otherwise it is a choice: decide, write the alternative you did not take, go on. Minor choices to settle yourself:
31
31
  a spacing, colour or label value when a measurement or a rule justifies it; the URL of a link (the most general official page if a precise one is not certain);