@sylad/cadence 1.10.0 → 1.12.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.10.0",
11
+ "version": "1.12.0",
12
12
  "source": "./",
13
13
  "author": {
14
14
  "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.10.0",
4
+ "version": "1.12.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/config.js CHANGED
@@ -283,7 +283,7 @@ const EFFORTS = ['default', 'low', 'medium', 'high', 'xhigh', 'max'];
283
283
  export const EFFORT_STEPS = ['precheck', 'spec', 'maquette', 'fiche', 'accept', 'implement', 'fix', 'review', 'ux'];
284
284
  const MODELS = ['haiku', 'sonnet', 'opus'];
285
285
  const REVIEW_KEYS = ['threshold', 'light', 'full'];
286
- const ORCH_KEYS = ['start', 'verdict', 'test', 'build', 'accept', 'maquette', 'maquetteReviews', 'fiche', 'ux', 'precheck', 'prodAccess', 'worktrees', 'testGate', 'spec', 'review', 'effort', 'permissionMode', 'addDirs', 'timeouts'];
286
+ const ORCH_KEYS = ['start', 'verdict', 'test', 'build', 'accept', 'maquette', 'maquetteReviews', 'fiche', 'ux', 'precheck', 'prodAccess', 'worktrees', 'testGate', 'spec', 'review', 'escalation', 'effort', 'permissionMode', 'addDirs', 'timeouts'];
287
287
  /** Clé `orchestrate:` de cadence.yaml. Absente : les défauts (auto, 45 min d'implémentation, 25 min de revue). */
288
288
  export function readOrchestrateConfig(file) {
289
289
  const config = { precheck: true, maquette: true, maquetteReviews: 3, fiche: true, precheckLocal: false, prodAccess: false, worktrees: false, spec: { dir: 'docs/specs' }, review: { threshold: 0.25, light: 'sonnet', full: 'opus' }, effort: { precheck: 'low', spec: 'medium', maquette: 'high', fiche: 'medium', accept: 'medium', implement: 'medium', fix: 'medium', review: 'high', ux: 'high' }, permissionMode: 'auto', addDirs: [], timeouts: { work: 45 * 60_000, review: 25 * 60_000 } };
@@ -421,6 +421,17 @@ export function readOrchestrateConfig(file) {
421
421
  config.review[k] = m;
422
422
  }
423
423
  }
424
+ if (o.escalation != null) {
425
+ if (!isObject(o.escalation))
426
+ throw bad('escalation doit être un objet { model }');
427
+ for (const k of Object.keys(o.escalation))
428
+ if (k !== 'model')
429
+ throw bad(`escalation.${k} inconnu (attendu : model)`);
430
+ const m = o.escalation.model;
431
+ if (typeof m !== 'string' || !m.trim())
432
+ throw bad('escalation.model : nom de modèle non vide attendu (passé tel quel à claude --model)');
433
+ config.escalation = { model: m.trim() };
434
+ }
424
435
  if (o.effort != null) {
425
436
  if (!isObject(o.effort))
426
437
  throw bad('effort doit être un objet { precheck, spec, maquette, fiche, accept, implement, fix, review, ux }');
@@ -106,6 +106,8 @@ export function readJournals(runsDir, until, since) {
106
106
  return { key: `${wave}/${name}/${fix.n}`, project: rec.project, lot: rec.lot, visible: !!rec.visible, findings, commits: fix.commits ?? [], ...(fix.ended ? { fixEnded: fix.ended } : {}), ...(findings.length > 0 && review ? { reviewKind: review.kind } : {}) };
107
107
  });
108
108
  const precheck = precheckVerdictOf(rec, steps);
109
+ const esc = rec.escalation;
110
+ const escalation = esc && typeof esc === 'object' && typeof esc.model === 'string' ? { model: esc.model, issue: esc.issue === 'conforme' || esc.issue === 'rendu' || esc.issue === 'non jouée' || esc.issue === 'échec' ? esc.issue : null } : undefined;
109
111
  journals.push({
110
112
  wave,
111
113
  ...(waveCreated ? { waveCreated } : {}),
@@ -117,6 +119,7 @@ export function readJournals(runsDir, until, since) {
117
119
  status: String(rec.status),
118
120
  ...(typeof rec.budget === 'number' ? { budget: rec.budget } : {}),
119
121
  ...(precheck ? { precheck } : {}),
122
+ ...(escalation ? { escalation } : {}),
120
123
  ...(Array.isArray(rec.warnings) && rec.warnings.some((w) => typeof w === 'string' && w.startsWith(LOCAL_PRECHECK_UNREADABLE)) ? { localUnreadable: true } : {}),
121
124
  steps: steps.map((s) => {
122
125
  const usage = usageOf(s.tokens);
@@ -88,6 +88,13 @@ function transition(c, status, outcome) {
88
88
  }
89
89
  const stop = (c, status, outcome) => {
90
90
  c.lot.next = null;
91
+ const e = c.lot.escalation;
92
+ if (e && !e.issue) {
93
+ // Le lot s'arrête alors que l'escalade était engagée et sans verdict (L337) : une session escaladée a échoué ou été coupée, le modèle n'a pas jugé le lot. `toFix` pose `rendu` avant de rendre un lot sur un verdict.
94
+ e.stage = 'done';
95
+ e.issue = 'échec';
96
+ c.wave.log(`${lotKey(c.lot.project, c.lot.lot)} · escalade ${e.model} : session sans résultat, le lot n'a pas été jugé par ce modèle`);
97
+ }
91
98
  transition(c, status, outcome);
92
99
  return null;
93
100
  };
@@ -539,6 +546,9 @@ function halted(w, lot) {
539
546
  /** Modèle d'une revue (L108) : la revue légère d'un lot léger tant qu'aucune correction n'a eu lieu, sinon la revue complète (UX comprise). */
540
547
  function reviewModel(c, kind) {
541
548
  const { light, full } = c.config.review;
549
+ // Escalade (L337) : la revue neuve qui suit la correction escaladée se joue au modèle de l'escalade ; l'UX garde le sien.
550
+ if (c.lot.escalation?.stage === 'fix' && (kind === 'review' || kind === 'review-small'))
551
+ return c.lot.escalation.model;
542
552
  return c.lot.light && c.lot.pass === 0 && (kind === 'review' || kind === 'review-small') ? light : full;
543
553
  }
544
554
  /** Écarte le modèle local pour le reste de la vague (L322) : le premier échec suffit, les lots suivants n'attendent pas de le constater à leur tour. */
@@ -559,7 +569,7 @@ async function session(c, kind, local = false) {
559
569
  // Le budget du lot borne l'écriture, jamais la revue (L145) : une revue est jouée dès que le code est à relire (sauf tests rouges après une écriture, cf. REVIEW_RESERVE), la passe fix réserve son coût.
560
570
  if (write && (lotOver(l) || (kind === 'fix' && !fixAffordable(l))))
561
571
  return overBudget(c, kind === 'fix' && !lotOver(l));
562
- const model = kind === 'spec' || kind === 'accept' || kind === 'fiche' ? 'sonnet' : write ? l.model : local ? 'local' : kind === 'precheck' ? 'sonnet' : reviewModel(c, kind); // le contrôle préalable ne fait que lire : pas d'Opus
572
+ const model = kind === 'spec' || kind === 'accept' || kind === 'fiche' ? 'sonnet' : write ? (kind === 'fix' && l.escalation?.stage === 'fix' ? l.escalation.model : l.model) : local ? 'local' : kind === 'precheck' ? 'sonnet' : reviewModel(c, kind); // le contrôle préalable ne fait que lire : pas d'Opus
563
573
  const effort = c.config.effort[kind === 'review-small' ? 'review' : kind];
564
574
  const before = await snapshot(wd(c));
565
575
  const neighbours = l.repos ?? [];
@@ -2113,8 +2123,29 @@ function toFix(c, constats, what) {
2113
2123
  const converging = what === 'revue non conforme' && counts.length >= 2 && counts[counts.length - 1] === 1 && counts[counts.length - 1] < counts[counts.length - 2];
2114
2124
  const cap = converging ? MAX_PASSES_CONVERGING : MAX_PASSES;
2115
2125
  if (l.pass >= cap) {
2116
- stop(c, 'handed-back', `${what} après ${l.pass} passe(s) de correction`);
2117
- return;
2126
+ const text = `${what} après ${l.pass} passe(s) de correction`;
2127
+ const esc = c.config.escalation;
2128
+ // Escalade (L337) : une seule fois, pour une revue de code non conforme ou des tests rouges (l'UX garde sa règle) ; déjà engagée, le lot est rendu.
2129
+ if (l.escalation) {
2130
+ if (!l.escalation.issue) {
2131
+ l.escalation.issue = 'rendu';
2132
+ c.wave.log(`${lotKey(l.project, l.lot)} · escalade ${l.escalation.model} : lot rendu malgré l'escalade`);
2133
+ }
2134
+ stop(c, 'handed-back', `${text} ; ${l.escalation.issue === 'conforme' ? `escalade ${l.escalation.model} déjà jouée` : `rendu malgré l'escalade ${l.escalation.model}`}`);
2135
+ return;
2136
+ }
2137
+ if (!esc || (what !== 'revue non conforme' && what !== 'tests rouges')) {
2138
+ stop(c, 'handed-back', text);
2139
+ return;
2140
+ }
2141
+ if (!fixAffordable(l)) {
2142
+ l.escalation = { model: esc.model, stage: 'done', issue: 'non jouée' };
2143
+ c.wave.log(`${lotKey(l.project, l.lot)} · escalade ${esc.model} non jouée : budget du lot (${lotSpent(l)} / ${l.budget} tokens comptés)`);
2144
+ stop(c, 'handed-back', `${text} ; escalade ${esc.model} non jouée (budget du lot : ${lotSpent(l)} / ${l.budget} tokens comptés, il ne reste pas de quoi payer la correction et la revue)`);
2145
+ return;
2146
+ }
2147
+ l.escalation = { model: esc.model, stage: 'fix' };
2148
+ c.wave.log(`${lotKey(l.project, l.lot)} · escalade ${esc.model} : ${what} après ${l.pass} passe(s), une correction puis une revue neuve`);
2118
2149
  }
2119
2150
  l.pass += 1;
2120
2151
  l.next = 'fix';
@@ -2212,6 +2243,14 @@ async function reviewStep(c, kind) {
2212
2243
  }
2213
2244
  l.code = summary;
2214
2245
  const w = c.wave;
2246
+ if (l.escalation?.stage === 'fix') {
2247
+ // La revue neuve de l'escalade (L337) est lue : conforme, le lot reprend sa fin ordinaire ; sinon toFix le rend.
2248
+ l.escalation.stage = 'done';
2249
+ if (summary.conforme) {
2250
+ l.escalation.issue = 'conforme';
2251
+ w.log(`${lotKey(l.project, l.lot)} · escalade ${l.escalation.model} : revue conforme`);
2252
+ }
2253
+ }
2215
2254
  if (l.small && l.visible) {
2216
2255
  // La passe unique d'un petit lot porte aussi l'ergonomie : son verdict vaut pour les deux. Sur un lot non petit, le verdict UX reste celui de l'agent UX.
2217
2256
  l.ux = summary;
@@ -16,6 +16,25 @@ export const LABEL = {
16
16
  failed: 'échec',
17
17
  suspended: 'suspendu',
18
18
  };
19
+ /** L'issue d'une escalade de modèle (L337), telle que le tableau la dit. */
20
+ const ESCALATION_TEXT = {
21
+ rendu: "lot rendu malgré l'escalade : à l'humain ou à un arbitrage",
22
+ 'non jouée': 'non jouée, le budget du lot ne la couvrait pas',
23
+ échec: "session escaladée sans résultat : le modèle n'a pas jugé le lot (est-il accessible à ce compte ?), reprise par le lead",
24
+ };
25
+ /** La ligne « escalade » d'un lot : l'issue de l'escalade, lue avec l'état du lot (le tableau sert aussi à `--status` et à une vague suspendue). */
26
+ function escalationText(l) {
27
+ const e = l.escalation;
28
+ if (!e.issue)
29
+ return e.stage === 'fix' ? 'en cours (une correction puis une revue neuve à ce modèle)' : 'engagée, pas encore tranchée';
30
+ if (e.issue !== 'conforme')
31
+ return ESCALATION_TEXT[e.issue];
32
+ if (l.status === 'ready')
33
+ return 'revue neuve conforme, lot rendu prêt';
34
+ if (l.status === 'handed-back' || l.status === 'failed')
35
+ return 'revue neuve conforme, lot rendu pour un autre motif (voir la ligne du lot)';
36
+ return 'revue neuve conforme, le lot poursuit sa fin ordinaire';
37
+ }
19
38
  function review(l) {
20
39
  if (!l.code)
21
40
  return '—';
@@ -188,6 +207,8 @@ export function renderTable(wave, lots, deps = {}) {
188
207
  for (const l of lots) {
189
208
  if (l.goal)
190
209
  out.push(`${l.project}:${l.lot} — objectif donné : ${l.goal}`);
210
+ if (l.escalation)
211
+ out.push(`${l.project}:${l.lot} — escalade ${l.escalation.model} : ${escalationText(l)}`);
191
212
  if (l.uxNote)
192
213
  out.push(`${l.project}:${l.lot} — ${l.uxNote}`);
193
214
  if (l.uxCaptures)
package/dist/perf.js CHANGED
@@ -415,7 +415,7 @@ export function computePerf(input) {
415
415
  redDeliveries: mk(reds, deliveriesOf, 'up', 'throughput.delivered'),
416
416
  qaBlocking: mk(both(windows, blockingOf), both(windows, deliveredCount), 'up', twinEscaped, escapedNote),
417
417
  };
418
- const result = { since: input.since ?? null, today, windows, throughput, corrections: correctionsInd, interventions, precheck, tokens, delays, durations, cost, quality, orphans, warnings: [] };
418
+ const result = { since: input.since ?? null, today, windows, throughput, corrections: correctionsInd, interventions, precheck, escalations: both(windows, (w) => escalationCounts(rendered(w))), tokens, delays, durations, cost, quality, orphans, warnings: [] };
419
419
  const bottleneck = bottleneckOf(causes.current);
420
420
  for (const [key, ind] of indicatorLines(result))
421
421
  if (ind.warn)
@@ -424,6 +424,12 @@ export function computePerf(input) {
424
424
  result.warnings.push(bottleneck);
425
425
  return result;
426
426
  }
427
+ /** Les escalades de modèle des lots donnés (L337), par issue ; une escalade engagée dont le journal n'a pas d'issue (lot interrompu) n'est classée dans aucune. */
428
+ function escalationCounts(set) {
429
+ const escalated = set.filter((d) => d.j.escalation);
430
+ const by = (issue) => escalated.filter((d) => d.j.escalation.issue === issue).length;
431
+ return { lots: escalated.length, conforme: by('conforme'), rendu: by('rendu'), nonJouee: by('non jouée'), echec: by('échec') };
432
+ }
427
433
  const minutesOf = (s) => {
428
434
  const from = Date.parse(s.started ?? '');
429
435
  const to = Date.parse(s.ended ?? '');
@@ -642,6 +648,9 @@ export function renderPerf(r) {
642
648
  out.push(` lots rendus puis terminés hors vague : ${h.current ?? 'n/d'} (précédente ${h.previous ?? 'n/d'})`);
643
649
  section('Contrôle préalable', ['part de « oui » du précheck']);
644
650
  out.push(...precheckLines(r.precheck));
651
+ const e = r.escalations.current;
652
+ if (e.lots > 0)
653
+ out.push('', 'Escalade de modèle', ` escalades de modèle : ${plural(e.lots, 'lot', 'lots')} · ${e.conforme} conforme · ${e.rendu} rendu malgré l'escalade · ${e.nonJouee} non jouée · ${e.echec} en échec`);
645
654
  out.push('', 'Tokens, délais et durées (médianes par lot ou par étape)');
646
655
  for (const l of lines.slice(LEGACY_LINES))
647
656
  out.push(row(l));
package/docs/deliver.md CHANGED
@@ -165,7 +165,7 @@ cadence deliver --sha 6b0d9aa -- api # an earli
165
165
  ```
166
166
 
167
167
  `--sha` (any mode) delivers a pushed commit other than `HEAD` — for a CI that
168
- builds each service only on the commit that touched it. `allowDirty` suits a
168
+ builds each service only on the commit that touched it. To deliver an earlier commit while `HEAD` holds work that must not ship, push that sha alone first (`git push origin <sha>:main`), then `cadence deliver --dry-run --sha <sha>` and `cadence deliver --sha <sha>`: with `ci: github` the wait is for the run of that exact sha, and GitHub only builds the tip of a push (L334, the `deliver` skill says so). The push goes to the branch delivered (`main` here); skip it when the sha is already on a remote branch, and when it is refused as non-fast-forward never force it (that would rewind the remote branch): the sha is not deliverable that way, stop and ask. `allowDirty` suits a
169
169
  working tree shared by several sessions when the delivery starts from a pushed
170
170
  sha and never from local files; without it a modified tree is refused. `--sha`
171
171
  itself never reads the working tree: a tracked file modified outside the delivered
@@ -183,6 +183,17 @@ orchestrate:
183
183
 
184
184
  `full` is also the model of the UX review and of the single pass of a small lot that is not light.
185
185
 
186
+ **Model escalation before a lot is handed back (L337, `orchestrate.escalation`)**: on 2026-10-25 two lots (maritime I-CAD2, cadence L332) were handed back after two correction passes and taken up by hand on Sonnet/Opus, while the lead's rule (17-10, 22-10) was to play a stronger model first — a rule forgotten twice. The wave now plays it:
187
+
188
+ ```yaml
189
+ orchestrate:
190
+ escalation: { model: fable } # no default: absent = nothing changes
191
+ ```
192
+
193
+ The name is passed as it stands to `claude --model` — no closed list `haiku|sonnet|opus`, and no default value, because the account may not have the model. When the review is still not compliant after the planned passes (`MAX_PASSES`, or `MAX_PASSES_CONVERGING` when it converges), or when the lot would be handed back « tests rouges » after them, the wave plays **one** correction and then **one** fresh code review, both on that model, before handing the lot back. The lot is handed back only if that escalated review is not compliant (`… ; rendu malgré l'escalade <model>`); the escalated correction that leaves the tests red hands it back without a review. A compliant escalated review ends the lot as any compliant review does (minors pass, UX review, `ready`). The UX review keeps its own rule: a lot handed back for a non-compliant UX review is not escalated. The escalation counts as a correction pass (`passes` of the table) and is played at most once per lot, a resume included (`escalation` in the lot's journal).
194
+
195
+ The lot's budget is respected: the escalation needs what a correction and its review need (`REVIEW_RESERVE`, as for any correction pass). When the budget does not cover it, it is **not played** and the outcome says so (`escalade <model> non jouée (budget du lot …)`); the lot is handed back to the lead with its findings. The final table prints one line per escalated lot, `<project>:<lot> — escalade <model> : <outcome>` (in progress while the lot still runs — the table also serves `--status` and a suspended wave —, compliant review, lot handed back despite the escalation — for the human or an arbitration of the lead —, not played, or an escalated session that returned nothing: model unavailable to the account, cut, timeout — then the model never judged the lot and the lead takes it up, `rendu malgré l'escalade` is said only for a lot handed back on a verdict). A lot whose escalated review was compliant but is handed back later (non-compliant UX review, refused integration) keeps `revue neuve conforme` and says its lot line is the reason, the wave log traces the three moments, and `cadence perf` counts the escalated lots of each window and their outcome (`docs/perf.md`). The price grid of `cadence perf` knows `sonnet`, `opus` and `haiku`: the cost of an escalated step on another model reads `n/d`.
196
+
186
197
  **Code changed without a test (L287)**: before any review session, the program checks the diff of the lot (every commit that
187
198
  cites it, all sessions included). If it touches code (`testGate.code`, minus `testGate.except`) without touching a single test
188
199
  file (`testGate.tests`), the lot goes straight to a correction pass — no review session is spent finding what a file list
package/docs/overview.md CHANGED
@@ -51,7 +51,7 @@ The repository is its own [Claude Code](https://claude.com/claude-code) plugin m
51
51
 
52
52
  ```
53
53
  /plugin marketplace add Sylad/cadence
54
- /plugin install cadence@cadence # skills /cadence:session-start, session-close, deliver, lead + the three reviewer agents
54
+ /plugin install cadence@cadence # skills /cadence:session-start, session-close, deliver, lead, orchestrate + the three reviewer agents
55
55
  /plugin install cadence-hud@cadence # the status band above the prompt
56
56
  ```
57
57
 
package/docs/perf.md CHANGED
@@ -35,6 +35,7 @@ current window, with at least 10 classified passes) is flagged by a `⚠` even w
35
35
  `part des lots visibles revus par une étape ux` is the share of the visible lots rendered in the window with at least one `ux` step that ended `ok` (so a small visible lot, reviewed in `review-small`, is not in it). It is informational: no `⚠`, no quality twin (`interventions.dedicatedUx` in `--json`), `n/d` without a visible lot.
36
36
  - **Pre-check share of `oui`** (L288). `part de « oui » du précheck` is the share of the lots rendered in the window whose journal records a pre-check verdict (`precheckVerdict`) that is `oui` (the deliverable was already there; an unproven `oui` is recorded `partiel`). Compare the current window with the previous one before and after a change of `orchestrate.precheck` or of the pre-check criterion (the title rule of L288): a share that stays low means the pre-check costs more than it saves. A journal older than L288 has no `precheckVerdict`: its verdict is read back from the lot (`livrable déjà présent` → `oui`, pre-check finding in the brief → `partiel`, otherwise `non`), so the window before the change is measurable. A journal written since L288 whose pre-check report was unreadable (it keeps the `contrôle préalable illisible` warning, no `precheckVerdict`) has no verdict and is not counted, not read back as `non` (L293). Lots with no successful `precheck` step are not counted; informational, no `⚠`, no quality twin (`precheck.yes` in `--json`), `n/d` without a verdict.
37
37
  - **Pre-check measured per lot** (L322). Under the section « Contrôle préalable », for the current window: the split `oui` / `partiel` / `non` of the rendered lots (`précheck : oui 1 · partiel 0 · non 1`); the local sessions (`précheck local : n sessions, k échecs, m min`: how many, how many ended without a usable report, wall time; a local session that ended `ok` but whose report was unreadable, so that the lot fell back on Sonnet, counts as a failure, and its line says `local échec`, `--json` `local.status: "illisible"`); the Sonnet sessions of the pre-check, which are the fallback of a local failure and the confirmation of a local `oui` (`précheck Sonnet : n sessions, t k tokens, m min`; the local model counts no token); the gain of the `oui` (`gain des « oui » : n sessions d'implémentation évitées (≈ t k tokens)`: the lots handed back without implementation, times the median of the `implement` steps of the window; without any, the tokens read `(gain en tokens : n/d)` and are `null` in `--json`); then one line per lot (`projet:lot · verdict · local échec 10 min · Sonnet 2 min, 20 k tokens`). Nothing is printed for a window without pre-check session or verdict. In `--json`: `precheck.verdicts`, `precheck.cost` (`local`, `sonnet`, `avoided`, each `{ current, previous }` as a whole) and `precheck.lots` (current window). Read it before changing `orchestrate.precheck` or the lots it applies to: the cost of the pre-check is what it spends (`local` minutes, `sonnet` tokens), its worth is `avoided`.
38
+ - **Model escalations** (L337). Under the section « Escalade de modèle », for the current window: `escalades de modèle : n lots · k conforme · r rendu malgré l'escalade · b non jouée` — the finished lots whose journal carries an `escalation` (`orchestrate.escalation`, `docs/orchestrate.md`) and how it ended: the fresh review on the escalation model was compliant (`conforme`), the lot was handed back despite it (`rendu`), the lot's budget did not cover it (`non jouée`), or an escalated session returned nothing — model unavailable to the account, cut, timeout (`en échec`, the model never judged the lot); a lot interrupted during its escalation is counted among the lots but in no outcome. Nothing is printed without an escalation. In `--json`: `escalations` (`{ current, previous }`, each `{ lots, conforme, rendu, nonJouee, echec }`). Read it to see whether the escalation model earns its cost: many `rendu` means the stronger model does not settle what the passes could not, so the brief or the sheet is at fault.
38
39
  - **`--since date`** is an ISO date that ignores everything before that day in both windows (a wave created before that day is not read, even if its lots end after it); a window starting before it is *truncated* (its per-day rates divide by the days covered, no `⚠`).
39
40
  It is **not** the instant of `cadence corrections --since` (waves created strictly after).
40
41
  - **`--json`** prints one object `{ since, today, windows: { current, previous }, throughput, corrections, interventions, precheck, tokens, delays, durations, cost, orphans, warnings }`; every indicator carries
package/docs/skills.md CHANGED
@@ -10,7 +10,7 @@ As a plugin:
10
10
  ```
11
11
 
12
12
  gives `/cadence:session-start`, `/cadence:session-close`, `/cadence:deliver`,
13
- `/cadence:lead` and the `ux-reviewer`, `code-reviewer` and `qa-reviewer` agents. Or copy them into the
13
+ `/cadence:lead`, `/cadence:orchestrate` and the `ux-reviewer`, `code-reviewer` and `qa-reviewer` agents. Or copy them into the
14
14
  repository with `cadence skills install` (to `.claude/skills/cadence-*` and
15
15
  `.claude/agents/cadence-*.md`; `--dir` for another `.claude` folder,
16
16
  `--force` to overwrite local edits).
@@ -43,6 +43,7 @@ repository with `cadence skills install` (to `.claude/skills/cadence-*` and
43
43
  subagents as the session cap at most (2 by default, `CADENCE_MAX_SESSIONS`), the ones it starts by hand declared with `cadence lead hold`, never two in the same repository. The whole flow is drawn in
44
44
  [docs/lead.md](lead.md); how a lot, a wave, the lead and the service of cadence itself fit together, in four figures, is in
45
45
  [docs/fonctionnement.md](fonctionnement.md).
46
+ - **orchestrate**: the command line of a wave without going through `lead` — `cadence orchestrate <project>:<lot>[@<model>]` (a lot id with « / » kept as is, no `--lot` or `--model` option) and the resume with `--resume [--wave <wave>]`; the description triggers on « lancer une vague orchestrate ».
46
47
  - **ux-reviewer** (agent): captures at 1440 and 390 px, findings grounded in a
47
48
  named rule (Nielsen, WCAG 2.2 AA) or a measurement, ranked, turned into
48
49
  `raf add --parent` sub-tasks, and a one-line verdict for `raf ux`. It never
package/docs/whats-new.md CHANGED
@@ -4,6 +4,8 @@
4
4
 
5
5
  ## In two lines
6
6
 
7
+ - **1.12.0**: `orchestrate.escalation: { model: fable }` plays one correction and one fresh review on that model before a wave hands a lot back; absent, nothing changes.
8
+ - **1.11.0**: a new `orchestrate` skill gives the command line of a wave without `/lead`; the `deliver` skill says how to deliver a commit that is not `HEAD` (push that sha alone first, never forced).
7
9
  - **1.10.0**: the worktree mode of `cadence orchestrate` needs only the `orchestrate.worktrees` key and is on for cadence itself; the gate plays `orchestrate.build` before the tests, and `lead tour` flags a missing `orchestrate.test`.
8
10
  A lot waiting for a slot obeys `--drop` and `--stop-after-current`, the pre-check is back on Sonnet, `cadence perf` classes red deliveries, and the README is a short home page again.
9
11
  - **1.9.0**: the plan lives at `.cadence/raf.yaml` (`raf migrate`); `cadence ref` reads reference documents; `doc-keeper` agent, `orchestrate.testGate`, `cadence lead hold` shown by `cadence-hud`.
@@ -29,6 +31,10 @@ Each is a paragraph in [docs/whats-new.md](whats-new.md) and a full section in [
29
31
 
30
32
  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).
31
33
 
34
+ **1.12.0**: a wave no longer hands a lot back after its planned correction passes without trying a stronger model, when the project asks for it (L337, `docs/orchestrate.md`). With `orchestrate.escalation: { model: <name> }` in `cadence.yaml` (no default: absent, nothing changes, since an account may not have the model; the name goes as it stands to `claude --model`), a review still not compliant after the planned passes — or tests still red — gets one more correction and one fresh review on that model; the lot is handed back only if that review is not compliant either. The final table says « escalade <model> » and its outcome, and `cadence perf` counts the escalations and how they ended. On 2026-10-25 two lots handed back after two passes had been taken up by hand on the ordinary models while the lead's rule was to escalate.
35
+
36
+ **1.11.0**: the plugin eval of 1.10.0 found two gaps in the skills, both closed and replayed at 1.00. A new `orchestrate` skill (L335, `docs/skills.md`) carries the command line of `cadence orchestrate` — `<project>:<lot>[@<model>]`, a lot id with « / » kept as is, `--resume` with or without `--wave` — so that an agent reaches it without `/lead` instead of inventing `--lot` and `--model`. The `deliver` skill (L334, `docs/deliver.md`) says how to deliver a commit that is not `HEAD`: push that sha alone (`git push origin <sha>:main`), then `cadence deliver --dry-run --sha <sha>` and `cadence deliver --sha <sha>`, because with `ci: github` the wait is for the run of that exact sha and GitHub only builds the tip of a push; skip the push when the sha is already on a remote branch, and never force a push refused as non-fast-forward.
37
+
32
38
  **1.10.0**: the worktree mode of `cadence orchestrate` (one git worktree per lot, independent lots of one repository in parallel) is no longer behind `CADENCE_EXPERIMENTAL_WORKTREES=1`: the `orchestrate.worktrees` key alone is the switch, on start and on `--resume`, `--integrate`, `--replay` and `--continue`, and cadence's own `cadence.yaml` turns it on after the trials L310 and L316 (L320); a red at the integrated tip is played once more before the lot is handed back, and the refusal names the red tests (L328); a union of the `CHANGELOG.md` entries of parallel lots, which accepts that a lot rewrites its own entry (L314, L323), and a kept branch recovered without the pre-check (L311). The gate of the wave plays `orchestrate.build` before `orchestrate.test`, a red build handing the lot back as a « build » finding (L333); `cadence lead tour` and `cadence orchestrate --dry-run` flag a project without `orchestrate.test` and name the probable command (L327). The pre-check is back on Sonnet in cadence's own configuration after the local model failed on about one lot in three, skips a lot created after the last code commit of the repository, and `cadence perf` measures it per lot (L322, L328). A lot waiting for a session slot obeys `--drop` and `--stop-after-current` (L332); `--resume` without a wave resumes the only unfinished one and is refused when several are (L309). `cadence perf` classes the red deliveries written before the class from their text (L331) and names the step that carries a rise of the tokens per lot (L312). `npm run service` purges the old copies put in service (L330), `cadence deliver` keeps a Ctrl-C (L325, L329), and `raf docs-ok` acknowledges a `docs.sync` gap (L317); `raf check` no longer reports two false drifts (L321). `cadence ref analyse` reads a reference document, `cadence ref diff` reconciles two readings and `--double` reads twice, an option (L265, L266); `cadence ref add` cites a range of figures and no longer reads « code figure n » as a citation (L274, L313); a failing PDF tool is a refusal, not a stack trace (L275). The README is a short home page again, the detail having moved to `docs/overview.md` (L326). Details: [CHANGELOG.md](../CHANGELOG.md).
33
39
 
34
40
  **1.9.0**: the default plan is `.cadence/raf.yaml`, a hidden folder git versions and `docs/` never publishes (L246), and `raf migrate [--dry-run]` moves a legacy plan and the readers of the old path (L284, L305); `raf title`, `raf visible`, `uses:` resources shown by `lead tour` and the lot `goal:` (L244, L273, L251–L253, L258). `cadence ref add|measure|check|corpus|eval` registers reference documents outside the repository, measures a rendered zone, validates the inventory schema and scores a reading against a known truth (L261–L264). In `cadence orchestrate`: the `doc-keeper` agent and `docs.articles` (L306, L308), the mechanical `orchestrate.testGate` before the review (L287), `precheck: local` on every lot with no commit (L296), a write-step time limit proportioned to the lot (L298), sessions that no longer hold their slot after their report (L285), the real weekly-limit message recognised (L286), and an experimental worktree mode with `--resume --integrate|--replay` and `--remove-worktree` (L279, L299–L304). `cadence lead hold|release` counts the lead's hand-started sessions, an expired one is flagged by `lead tour`, `--status` and the `cadence-hud` band (0.5.0; L255, L276, L278). `cadence deliver --sha` is accepted while a wave runs (L277), `CADENCE_DEPLOYED_FILE` names the deployed sha (L277, L281) and `deliveries-failed.log` keeps the class and last lines of a red delivery, secrets masked (L282, L289, L294). `cadence perf` breaks red deliveries down and counts the `review-small` UX (L282, L283). Details: [CHANGELOG.md](../CHANGELOG.md).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sylad/cadence",
3
- "version": "1.10.0",
3
+ "version": "1.12.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",
@@ -32,10 +32,33 @@ writes that sha into the file named by `$CADENCE_DEPLOYED_FILE`: cadence logs it
32
32
  (a content that is not a hexadecimal commit sha — `HEAD` or a branch name included — fails the delivery; empty during a wave, the log keeps `$CADENCE_SHA` and a warning
33
33
  says the head moved).
34
34
 
35
- Its arguments go after `--`: `cadence deliver -- api frontend` (`--sha <rev>` to deliver a pushed
36
- commit other than HEAD). Ask the human (or the project's own
35
+ Its arguments go after `--`: `cadence deliver -- api frontend` (`--sha <rev>` to deliver a commit
36
+ other than HEAD, see « Delivering a commit that is not HEAD »). Ask the human (or the project's own
37
37
  delivery skill) which arguments this delivery needs; never guess them.
38
38
 
39
+ ## Delivering a commit that is not HEAD
40
+
41
+ When HEAD holds work that must not ship and an earlier commit must, push **that sha alone** first,
42
+ then deliver it:
43
+
44
+ ```sh
45
+ git push origin <sha>:main
46
+ cadence deliver --dry-run --sha <sha>
47
+ cadence deliver --sha <sha>
48
+ ```
49
+
50
+ The order matters. With `ci: github`, cadence waits for the run of that exact sha, and GitHub only
51
+ builds the tip of a push: a sha that was never pushed as the tip has no run to wait for. Pushing HEAD
52
+ instead would ship the unfinished work; never reset HEAD to get there.
53
+
54
+ The push targets the branch being delivered (`main` here). Two cases where it does not apply:
55
+
56
+ - The sha is already on a remote branch: skip the push, it is useless. A sha that was pushed earlier has a CI run
57
+ only if it was the tip of a push; if it never was, no run exists to wait for.
58
+ - The push is refused as non-fast-forward (the sha is not a descendant of `origin/main`, or `origin/main`
59
+ has moved past it): **never force it**, `--force` would rewind the remote branch. The sha cannot be delivered
60
+ that way: stop and ask the human.
61
+
39
62
  ## Where to run
40
63
 
41
64
  Every `cadence` / `raf` command works on the git repository of the current directory. When the
@@ -141,6 +141,8 @@ of the plan itself (section 2b), and the limits of section « Limits that always
141
141
  handed back ready (refused, review not compliant after the correction passes, red delivery, blocking QA finding) is
142
142
  not chained over: it is reported, and the chain goes on with the lots that do not depend on it.
143
143
 
144
+ **A lot handed back despite the escalation** (L337). When the project declares `orchestrate.escalation: { model }`, the wave itself plays one correction and one fresh review on that model before handing a lot back for a non-compliant review or red tests: do not take such a lot up by hand on a model of your own choice first, and do not replay the escalation yourself. The table says `escalade <model> : …` under the lot. A lot handed back **despite** the escalation (`rendu malgré l'escalade`) is for the human or for an arbitration (the sheet, the brief or the lot is at fault, not the model): report it with the remaining findings and go on with the other lots. `escalade … non jouée` means the lot's budget did not cover it: the lot is yours, with its findings. `escalade … session escaladée sans résultat` means the escalated session itself failed (often a model the account does not have): the model never judged the lot, fix the key or take the lot up, do not read it as the lot's fault. A line `en cours` is a lot still running. Without the key, nothing changes; the model has no default, as the account may not have the strongest one.
145
+
144
146
  **The threshold.** Before every new wave, read the context of the lead session with `cadence session context`: the
145
147
  `cadence-hud` mod publishes the context of each session, one file per session named after its id, and the command reads the
146
148
  one named by `$CLAUDE_CODE_SESSION_ID` (Claude Code sets it in the environment of your Bash, so it is your own session,
@@ -0,0 +1,39 @@
1
+ ---
2
+ name: orchestrate
3
+ description: The exact command line of `cadence orchestrate` — launch a wave ("lancer une vague orchestrate", "start an orchestrate wave", run lot X with model Haiku) and resume it with `--resume [--wave <wave>]`. Use it before writing any `cadence orchestrate` command, without going through /lead. Syntax `<project>:<lot>[@<model>]`, a lot id with « / » kept as is.
4
+ ---
5
+
6
+ # orchestrate — the command line of a wave
7
+
8
+ `cadence orchestrate` runs one fresh short session per step of each lot (write, review, correction, delivery of
9
+ the sheet). This page is only the syntax; the lead's workflow is the `cadence:lead` skill and every option is in
10
+ `docs/orchestrate.md` of the cadence package.
11
+
12
+ ## Launch
13
+
14
+ ```sh
15
+ cadence orchestrate <project>:<lot>[@<model>] [<project>:<lot>[@<model>] …] [--wave <id>] [--budget 2M]
16
+ cadence orchestrate maritime-atlas:Q4/accueil-4-ux12@haiku
17
+ cadence orchestrate finance-tracker:L41 cadence:L18@haiku --budget 1.5M
18
+ ```
19
+
20
+ - One positional argument per lot: `<project>:<lot>`, then `@<model>` to give that lot a model (`@haiku`, `@sonnet`,
21
+ `@opus`). The project is what comes before the **first `:`**, the model what comes after the **last `@`**.
22
+ - **A lot id may contain « / »** (sub-tasks of a plan kept read-only by another tool, `Q4/accueil-4-ux12`): write it as is, never escape
23
+ it, never replace it, never split it. Quote the whole argument only if the shell needs it.
24
+ - There is no `--lot` and no `--model`: the lot and its model are always in the same argument, `<project>:<lot>@<model>`.
25
+ From inside a project, `cadence orchestrate L18` is enough.
26
+ - `--dry-run` first when a precondition is in doubt (nothing is started); `--budget` takes `1.5M`, `800k` or `1500000`;
27
+ `--goal "<what done looks like>"` for a single lot whose title is stale.
28
+
29
+ ## Resume
30
+
31
+ A wave that stopped (a question, the quota, an interruption) is resumed with the same program, never with a new wave:
32
+
33
+ ```sh
34
+ cadence orchestrate --resume # the only unfinished wave of the folder
35
+ cadence orchestrate --resume --wave <wave> # a named wave (required when several are unfinished)
36
+ cadence orchestrate --resume --wave <wave> --answer <project>:<lot> "reply"
37
+ ```
38
+
39
+ `--resume` takes no lot: it replays the lots of the wave. `cadence orchestrate --status` shows the live waves of the machine and the table of the folder's latest wave; `--resume` with no wave lists the unfinished ones when there are several.