@sylad/cadence 1.19.1 → 1.21.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.
@@ -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.19.1",
11
+ "version": "1.21.0",
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.0",
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.19.1",
4
+ "version": "1.21.0",
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';
@@ -46,6 +47,7 @@ const HELP = `raf — plan « reste à faire » versionné dans le dépôt, reli
46
47
  raf screen <id> page|component|journey|none pose, remplace ou retire screen: d'un lot visible existant (seule la valeur page adopte l'étape spec d'orchestrate)
47
48
  raf visible <id> on|off pose ou retire visible: d'un lot existant (changement visible : entrée Nouveautés et étape UX d'orchestrate) ; off est refusé tant que le lot a un screen:
48
49
  raf spec <id> on|off pose ou retire spec: d'un lot existant (le lot change un comportement : étape spec back d'orchestrate)
50
+ raf move <id> --before|--after <id> réordonne un lot (ou une sous-tâche) dans le plan, parmi ceux du même parent ; refusé pendant une vague du dépôt, sur un lot archivé et sur un plan en lecture seule
49
51
  raf uses <lot> [<ressource>…] remplace les ressources partagées du lot (uses:, déclarées par resources: dans le cadence.yaml du dossier parent ; raf check refuse un nom non déclaré) ; sans nom, retire la clé
50
52
  raf start <id> raf done <id> [--force] raf drop <id> [--reason texte]
51
53
  raf todo <id> [--reason texte] remet en attente un lot démarré sans commit de travail (doing → todo, started retiré, note datée) ; refusé s'il a des commits ; rouvre un lot done ou dropped (→ todo, finished retiré, note « rouvert ») — la seule façon de rouvrir, archivé compris (il revient dans le fichier du plan)
@@ -532,6 +534,20 @@ function dispatch(argv, io) {
532
534
  plan.save();
533
535
  return 0;
534
536
  }
537
+ case 'move': {
538
+ // Une seule position : --before ou --after, avec une cible (« after » est aussi le drapeau de « add », « before » celui de « archive »).
539
+ const place = values.before !== undefined && values.after === undefined ? 'before' : values.after !== undefined && values.before === undefined ? 'after' : null;
540
+ if (rest.length !== 1 || !place)
541
+ throw new RafError('usage : raf move <id> --before <id> | raf move <id> --after <id>');
542
+ const plan = loadPlan();
543
+ plan.move(rest[0], place, values[place]); // en mémoire : les refus de la commande (plan en lecture seule, parent, archive) passent avant celui de la vague
544
+ // Un lot de la vague s'appuie sur l'ordre du plan qu'il a lu : le réordonner sous ses pieds est refusé tant que la vague tient le dépôt.
545
+ const holding = waveHolding(root);
546
+ if (holding)
547
+ throw new RafError(`move : orchestration en cours (vague ${holding.wave}, pid ${holding.pid}) — attendre que les lots de ce dépôt soient finis (le verrou tombe dès que tous sont prêts ou rendus), puis relancer`);
548
+ plan.save();
549
+ return 0;
550
+ }
535
551
  case 'public': {
536
552
  need(values.clear ? 1 : 2, 'public <id> "titre public" | public <id> --clear');
537
553
  if (values.clear && rest.length > 1)
@@ -726,7 +742,9 @@ function dispatch(argv, io) {
726
742
  return postCommit(existsSync(planPath) ? loadPlan : null, newsDir, root, today, io, readHookConfig(configPath).autostart);
727
743
  if (rest[0] === 'pre-commit')
728
744
  return preCommit(existsSync(planPath) ? loadPlan : null, readHookConfig(configPath).autostart, root, today, io);
729
- 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');
730
748
  }
731
749
  case 'session': {
732
750
  // Le contexte de la session ne dépend d'aucun dépôt : le lead le lit depuis le dossier parent.
@@ -1106,6 +1124,34 @@ function morningEffects(configPath, root) {
1106
1124
  return [`✗ ${e.message}`];
1107
1125
  }
1108
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
+ }
1109
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`. */
1110
1156
  function waveHolding(root) {
1111
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;
@@ -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');
package/dist/plan.js CHANGED
@@ -625,6 +625,43 @@ export class Plan {
625
625
  const after = keys.indexOf('after');
626
626
  node.items.splice(created >= 0 ? created : after >= 0 ? after + 1 : keys.length, 0, this.doc.createPair('uses', list));
627
627
  }
628
+ /**
629
+ * Place un lot (ou une sous-tâche) juste avant ou juste après un autre du même parent (L400) : les lots entre eux, les sous-tâches d'un même lot entre elles.
630
+ * Seul l'ordre du fichier change — le nœud voyage avec ses clés et ses commentaires. Un lot archivé n'a pas de place dans le fichier du plan
631
+ * (l'archive se présente dans l'ordre d'avant l'archivage, jamais déplacée) : refusé. Déjà à sa place : rien à écrire.
632
+ */
633
+ move(ref, place, target) {
634
+ this.writable();
635
+ if (ref === target)
636
+ throw new RafError(`move : ${ref} ne peut pas être placé par rapport à lui-même`);
637
+ const parent = (r) => (r.includes('/') ? r.slice(0, r.indexOf('/')) : null);
638
+ const siblings = (r) => {
639
+ const owner = parent(r);
640
+ const top = this.doc.get('lots');
641
+ const lotId = owner ?? r;
642
+ if (!top.items.some((n) => isMap(n) && String(n.get('id')) === lotId)) {
643
+ this.lotNode(lotId); // inconnu : « lot inconnu », même dans l'archive
644
+ throw new RafError(`move : ${lotId} est archivé — l'ordre du plan ne compte que les lots du fichier du plan`);
645
+ }
646
+ if (!owner)
647
+ return top;
648
+ this.taskNode(owner, r.slice(owner.length + 1)); // sous-tâche inconnue
649
+ return this.lotNode(owner).get('tasks');
650
+ };
651
+ const from = siblings(ref);
652
+ const to = siblings(target);
653
+ if (parent(ref) !== parent(target))
654
+ throw new RafError(`move : ${ref} et ${target} n'ont pas le même parent — un lot se place parmi les lots, une sous-tâche parmi celles de son lot`);
655
+ const idOf = (r) => (parent(r) ? r.slice(r.indexOf('/') + 1) : r);
656
+ const at = (seq, r) => seq.items.findIndex((n) => isMap(n) && String(n.get('id')) === idOf(r));
657
+ const node = from.items[at(from, ref)];
658
+ const without = from.items.filter((n) => n !== node);
659
+ const wanted = without.findIndex((n) => n === to.items[at(to, target)]) + (place === 'after' ? 1 : 0);
660
+ if (at(from, ref) === wanted)
661
+ return;
662
+ without.splice(wanted, 0, node);
663
+ from.items = without;
664
+ }
628
665
  addTask(lotId, title) {
629
666
  this.writable();
630
667
  const lot = this.lotNode(lotId);
@@ -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 a terminal under 110 columns answers `terminal trop étroit pour le panneau (110 colonnes au moins)` and opens nothing. 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]`.
@@ -766,7 +766,7 @@ still holds (those not yet released), the cap of each wave, the slots in use and
766
766
  **Guards, imposed by the code**: a global cap of simultaneous sessions, one wave per repository at
767
767
  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
768
  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
769
+ 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
770
  `pre-push` hook is refused before anything starts — `pushurl` is never touched); after every session the
771
771
  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
772
  `CADENCE_ORCHESTRATED` is set.
package/docs/raf.md CHANGED
@@ -58,6 +58,7 @@ raf gantt # .cadence/gantt.html (next to the plan)
58
58
  | `raf visible <id> on\|off` | set or remove `visible:` on an existing lot (a user-facing change: a news entry is expected, and `cadence orchestrate` adds its UX step), written right after `quickwin:`, `estimate:` or `status:` (whichever comes last); `on` on a visible lot changes nothing, `off` on a lot without the key neither; refused on a sub-task, and `off` is refused while the lot has a `screen:` (`raf screen <id> none` first: a screen is a visible change). It is the fix `raf check` points to for an open « Revue UX — » lot without `visible:` (L273) |
59
59
  | `raf spec <id> on\|off` | set or remove `spec:` on an existing lot (a lot that changes a behaviour, for the back-end spec step of `cadence orchestrate`, see [The spec step of a lot with `spec: true`](orchestrate.md#the-spec-step-of-a-lot-with-spec-true)); refused on a sub-task; `off` is refused on a lot that has `screen: page` (the page implies the spec: remove `screen:` first) and `on` there only says so and changes nothing; on `component` and `journey` (ordinary path since L209) both work and set or remove the back-end `spec:`. `raf spec` only takes `on`/`off`: `cadence spec check\|qa <fiche.yaml>` keeps its own subcommands. `raf check` reports a `spec:` that is neither `true` nor `false` (`spec : true ou false attendu`) |
60
60
  | `raf uses <lot> [<resource>…]` | replace the `uses:` list of a lot of any status (`done` and `dropped` included) by the names given (duplicates removed, order kept); with no name, remove the key. Prints nothing. A name is a word of lowercase letters, digits, `-` and `_` (else `ressource invalide : <name> (lettres minuscules, chiffres, - et _)`, plan unchanged); a sub-task is refused (`uses se pose sur un lot, pas sur une sous-tâche : L1/t1`); an unknown lot is `lot inconnu : <id>`. A name not yet declared is accepted: `raf check` refuses it (L251) |
61
+ | `raf move <id> --before\|--after <id>` | reorder a lot in the plan file (L400): `<id>` is placed right before or right after the other lot, the others keep their order, and the lot travels with all its keys, its sub-tasks and its comments. Only among siblings: a lot among lots, a sub-task (`L1/t1`) among the sub-tasks of its own lot (`move : L2 et L1/t1 n'ont pas le même parent …`, plan unchanged). Exactly one of `--before` / `--after`, one id (else `usage : raf move <id> --before <id> \| raf move <id> --after <id>`); a lot placed where it already is, or an unknown id (`lot inconnu : <id>`), changes nothing. **Refused** on a read-only plan, plan kept by another tool (`plan en lecture seule : … — le modifier avec l'outil du projet`: reorder it with the project's own tool), on an archived lot (`move : L1 est archivé …`: the archive is presented in its former order and the order of the plan file never moves, see `raf archive`) and while a wave holds the repository (`move : orchestration en cours (vague …, pid …) — attendre …`: the lots of the wave rely on the order they read; the lock falls as soon as all of them are ready or handed back). The order is the order of the file, so it is the order `raf list`, `raf now`, the Gantt page and the draw of `cadence orchestrate` follow; the command only writes the plan, it does not commit it (`plan.commit` prints the command, as for every plan write) |
61
62
  | `raf public <id> "title"` · `raf public <id> --clear` | set, replace or remove the public title of a lot |
62
63
  | `raf start <id>` · `raf done <id> [--force]` · `raf drop <id> [--reason text]` | dated transitions (`done` refuses open sub-tasks unless `--force`, and a lot whose `acceptation-<lot>.json` is still `vert: false`, L366: closed like that it would lock every later wave of the repository, see `cadence spec green`; with `--force` it only warns on stderr. Like every `raf` command it only writes the plan file and **never commits** it: when the plan or a file `plan.onWrite` produces is left with changes, `done` prints the `git add` / `git commit` command that takes them, the subject from the `plan.commit` template, L371) |
63
64
  | `cadence spec green <lot>` | L366: run the dedicated acceptance command (`orchestrate.accept`) on a lot driven or closed by hand; green, set `vert: true` in its `acceptation-<lot>.json` and commit that file alone (subject `chore(<lot>): acceptation verte, vert: true posé par cadence spec green` or the `plan.commit` template; the resealed tests are listed in the commit body, never in the subject), resealing the fingerprints of the listed tests refined since the red commit (journal `resceaux`) so the lock goes quiet; a listed test changed but not committed is refused. Red: exit 1, nothing changed. Refused for a `todo` lot, during a wave, and while an unfinished wave keeps a lot of the repository open; see [orchestrate](orchestrate.md) |
@@ -75,6 +76,7 @@ raf gantt # .cadence/gantt.html (next to the plan)
75
76
  | `raf check --ignored` | list the acknowledged commits with their date and reason |
76
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) |
77
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 |
78
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) |
79
81
 
80
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.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.
8
+ - **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.
7
9
  - **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.
8
10
  - **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.
9
11
  - **1.18.0**: `cadence verify --gate` reuses the green gate of the wave instead of replaying it; `orchestrate.continue` keeps `--continue` from drawing a big lot; `raf check` and `raf now` run about 5 times faster.
@@ -44,6 +46,10 @@ Each is a paragraph in [docs/whats-new.md](whats-new.md) and a full section in [
44
46
 
45
47
  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).
46
48
 
49
+ **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`).
50
+
51
+ **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).
52
+
47
53
  **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.
48
54
 
49
55
  **1.19.0**: a plan that only grows no longer weighs on every session. `raf archive [--before date] [--dry-run]` moves the `done` and `dropped` lots into `.cadence/archive/<year>.yaml` (L396, `docs/raf.md`): measured on the cadence plan, 565 KB → 5 KB, and `raf show|check|commits|now|gantt`, `cadence news`, `perf`, `session start`, `lead tour`, `deliver` and `orchestrate` print the same thing before and after (one transverse test); the archiving is lossless whatever fails, the command to commit plan and archive together is printed, a wave's plan commit takes the year files too, and the briefs tell the sessions never to read the archive. `raf todo <id>` reopens a closed lot, archived or not. All live waves now apply the same session cap, the lowest one declared, and the test gate is a visible step of a lot (L395, `docs/orchestrate.md`). Wave sessions start without the user's plugins (`--settings` with `enabledPlugins` false, L398): the first turn of an `implement` session went from 42,541 to 38,330 tokens.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sylad/cadence",
3
- "version": "1.19.1",
3
+ "version": "1.21.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",