@sylad/cadence 1.11.0 → 1.12.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.11.0",
11
+ "version": "1.12.1",
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.11.0",
4
+ "version": "1.12.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/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);
@@ -86,8 +86,22 @@ function transition(c, status, outcome) {
86
86
  save(c);
87
87
  c.wave.log(`${lotKey(c.lot.project, c.lot.lot)} → ${status}${outcome ? ` — ${outcome}` : ''}`);
88
88
  }
89
- const stop = (c, status, outcome) => {
89
+ /** Une session jouée au modèle d'escalade (L337) : la correction escaladée et la revue neuve qui la suit. L'UX, la fiche, le précheck… gardent leur modèle. */
90
+ const escalatedSession = (c, kind) => c.lot.escalation?.stage === 'fix' && (kind === 'fix' || kind === 'review' || kind === 'review-small');
91
+ /**
92
+ * Rend ou échoue le lot. `noResult` : la session escaladée n'a rien rendu (sortie vide, erreur de lancement, coupure, délai) — seul cas où l'escalade est dite
93
+ * « en échec » (L338) : le modèle n'a pas jugé le lot. Un retrait (--drop) ou un incident du dépôt pendant l'escalade laisse l'escalade sans issue : ce n'est pas
94
+ * un échec du modèle et `cadence perf` ne le compte pas contre lui.
95
+ */
96
+ const stop = (c, status, outcome, noResult = false) => {
90
97
  c.lot.next = null;
98
+ const e = c.lot.escalation;
99
+ if (e && !e.issue && noResult) {
100
+ // Une session escaladée a échoué ou été coupée (L337) : le modèle n'a pas jugé le lot. `toFix` pose `rendu` avant de rendre un lot sur un verdict.
101
+ e.stage = 'done';
102
+ e.issue = 'échec';
103
+ 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`);
104
+ }
91
105
  transition(c, status, outcome);
92
106
  return null;
93
107
  };
@@ -539,6 +553,9 @@ function halted(w, lot) {
539
553
  /** 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
554
  function reviewModel(c, kind) {
541
555
  const { light, full } = c.config.review;
556
+ // Escalade (L337) : la revue neuve qui suit la correction escaladée se joue au modèle de l'escalade ; l'UX garde le sien.
557
+ if (c.lot.escalation?.stage === 'fix' && (kind === 'review' || kind === 'review-small'))
558
+ return c.lot.escalation.model;
542
559
  return c.lot.light && c.lot.pass === 0 && (kind === 'review' || kind === 'review-small') ? light : full;
543
560
  }
544
561
  /** É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 +576,7 @@ async function session(c, kind, local = false) {
559
576
  // 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
577
  if (write && (lotOver(l) || (kind === 'fix' && !fixAffordable(l))))
561
578
  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
579
+ 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
580
  const effort = c.config.effort[kind === 'review-small' ? 'review' : kind];
564
581
  const before = await snapshot(wd(c));
565
582
  const neighbours = l.repos ?? [];
@@ -773,9 +790,9 @@ async function session(c, kind, local = false) {
773
790
  }
774
791
  const report = cutReport({ minutes: Math.round(delay / 60_000), commits, files, numstat });
775
792
  step.cause = report;
776
- return stop(c, files.length || commits ? 'handed-back' : 'failed', `délai dépassé : ${kind} ${report}`);
793
+ return stop(c, files.length || commits ? 'handed-back' : 'failed', `délai dépassé : ${kind} ${report}`, escalatedSession(c, kind));
777
794
  }
778
- return stop(c, 'failed', outcome.cut ? `coupure : ${kind} ${outcome.cause}` : `échec : ${kind} sans résultat : ${outcome.cause}`);
795
+ return stop(c, 'failed', outcome.cut ? `coupure : ${kind} ${outcome.cause}` : `échec : ${kind} sans résultat : ${outcome.cause}`, escalatedSession(c, kind));
779
796
  }
780
797
  if (outcome.kind === 'quota') {
781
798
  if (outcome.firstStdout !== undefined)
@@ -2113,8 +2130,29 @@ function toFix(c, constats, what) {
2113
2130
  const converging = what === 'revue non conforme' && counts.length >= 2 && counts[counts.length - 1] === 1 && counts[counts.length - 1] < counts[counts.length - 2];
2114
2131
  const cap = converging ? MAX_PASSES_CONVERGING : MAX_PASSES;
2115
2132
  if (l.pass >= cap) {
2116
- stop(c, 'handed-back', `${what} après ${l.pass} passe(s) de correction`);
2117
- return;
2133
+ const text = `${what} après ${l.pass} passe(s) de correction`;
2134
+ const esc = c.config.escalation;
2135
+ // 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.
2136
+ if (l.escalation) {
2137
+ if (!l.escalation.issue) {
2138
+ l.escalation.issue = 'rendu';
2139
+ c.wave.log(`${lotKey(l.project, l.lot)} · escalade ${l.escalation.model} : lot rendu malgré l'escalade`);
2140
+ }
2141
+ stop(c, 'handed-back', `${text} ; ${l.escalation.issue === 'conforme' ? `escalade ${l.escalation.model} déjà jouée` : `rendu malgré l'escalade ${l.escalation.model}`}`);
2142
+ return;
2143
+ }
2144
+ if (!esc || (what !== 'revue non conforme' && what !== 'tests rouges')) {
2145
+ stop(c, 'handed-back', text);
2146
+ return;
2147
+ }
2148
+ if (!fixAffordable(l)) {
2149
+ l.escalation = { model: esc.model, stage: 'done', issue: 'non jouée' };
2150
+ c.wave.log(`${lotKey(l.project, l.lot)} · escalade ${esc.model} non jouée : budget du lot (${lotSpent(l)} / ${l.budget} tokens comptés)`);
2151
+ 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)`);
2152
+ return;
2153
+ }
2154
+ l.escalation = { model: esc.model, stage: 'fix' };
2155
+ 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
2156
  }
2119
2157
  l.pass += 1;
2120
2158
  l.next = 'fix';
@@ -2212,6 +2250,14 @@ async function reviewStep(c, kind) {
2212
2250
  }
2213
2251
  l.code = summary;
2214
2252
  const w = c.wave;
2253
+ if (l.escalation?.stage === 'fix') {
2254
+ // La revue neuve de l'escalade (L337) est lue : conforme, le lot reprend sa fin ordinaire ; sinon toFix le rend.
2255
+ l.escalation.stage = 'done';
2256
+ if (summary.conforme) {
2257
+ l.escalation.issue = 'conforme';
2258
+ w.log(`${lotKey(l.project, l.lot)} · escalade ${l.escalation.model} : revue conforme`);
2259
+ }
2260
+ }
2215
2261
  if (l.small && l.visible) {
2216
2262
  // 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
2263
  l.ux = summary;
@@ -16,6 +16,29 @@ 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
+ // Sans issue : l'escalade a été engagée, personne ne l'a tranchée. « en cours » ne se dit que d'un lot qui tourne : un lot retiré (--drop), en échec ou rendu avant le verdict du modèle n'est pas vivant.
30
+ if (l.status === 'handed-back' || l.status === 'failed')
31
+ return 'engagée, lot rendu avant le verdict du modèle (retrait, incident ou erreur : voir la ligne du lot), le modèle ne l\'a pas jugé';
32
+ return e.stage === 'fix' ? 'en cours (une correction puis une revue neuve à ce modèle)' : 'engagée, pas encore tranchée';
33
+ }
34
+ if (e.issue !== 'conforme')
35
+ return ESCALATION_TEXT[e.issue];
36
+ if (l.status === 'ready')
37
+ return 'revue neuve conforme, lot rendu prêt';
38
+ if (l.status === 'handed-back' || l.status === 'failed')
39
+ return 'revue neuve conforme, lot rendu pour un autre motif (voir la ligne du lot)';
40
+ return 'revue neuve conforme, le lot poursuit sa fin ordinaire';
41
+ }
19
42
  function review(l) {
20
43
  if (!l.code)
21
44
  return '—';
@@ -188,6 +211,8 @@ export function renderTable(wave, lots, deps = {}) {
188
211
  for (const l of lots) {
189
212
  if (l.goal)
190
213
  out.push(`${l.project}:${l.lot} — objectif donné : ${l.goal}`);
214
+ if (l.escalation)
215
+ out.push(`${l.project}:${l.lot} — escalade ${l.escalation.model} : ${escalationText(l)}`);
191
216
  if (l.uxNote)
192
217
  out.push(`${l.project}:${l.lot} — ${l.uxNote}`);
193
218
  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));
@@ -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>` (`en cours` only while the lot still runs — the table also serves `--status` and a suspended wave —; a lot withdrawn (`--drop`), failed by an error or stopped by an incident of the repository before the model's verdict says `engagée, lot rendu avant le verdict du modèle`: the model did not judge it and it is not counted against the model —, 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 (empty output, launch error, cut, timeout; the only case said `session escaladée sans résultat`) — 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/perf.md CHANGED
@@ -35,9 +35,10 @@ 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 · e en échec` — 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, withdrawn from the wave (`--drop`) or stopped by an incident of the repository is counted among the lots but in no outcome, never as `en échec`. 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
- - **`--json`** prints one object `{ since, today, windows: { current, previous }, throughput, corrections, interventions, precheck, tokens, delays, durations, cost, orphans, warnings }`; every indicator carries
41
+ - **`--json`** prints one object `{ since, today, windows: { current, previous }, throughput, corrections, interventions, precheck, escalations, tokens, delays, durations, cost, quality, orphans, warnings }`; every indicator carries
41
42
  `{ current, previous, change, warn, twin, lots, note }` (`twin`: the path of its quality twin, e.g. `throughput.escaped`). Same values as the text.
42
43
 
43
44
  ## Tokens, delays, wall time and cost (L226)
package/docs/whats-new.md CHANGED
@@ -4,6 +4,8 @@
4
4
 
5
5
  ## In two lines
6
6
 
7
+ - **1.12.1**: the escalation line of the final table follows the lot's state (a lot withdrawn or failed during its escalation no longer reads « en cours »), and a test that raced on the GitHub runner is deterministic.
8
+ - **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.
7
9
  - **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).
8
10
  - **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`.
9
11
  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.
@@ -30,6 +32,10 @@ Each is a paragraph in [docs/whats-new.md](whats-new.md) and a full section in [
30
32
 
31
33
  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).
32
34
 
35
+ **1.12.1**: fixes after 1.12.0. The « escalade » line of the final table follows the lot's state (L338): a lot handed back or failed during its escalation before the model's verdict (`--resume --drop`, step exception, incident of the repository) reads « engagée, lot rendu avant le verdict du modèle » instead of « en cours », and only an escalated session that returned nothing (model unavailable to the account, cut, timeout) counts as `échec` in the table and in `cadence perf`, whose `--json` key list in `docs/perf.md` now names `escalations` and `quality`. Two tests of the worktree integration that raced on the order in which two lots take the integration lock — one of them failed the first publication run of 1.11.0 — now force that order (L339).
36
+
37
+ **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.
38
+
33
39
  **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.
34
40
 
35
41
  **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).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sylad/cadence",
3
- "version": "1.11.0",
3
+ "version": "1.12.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",
@@ -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; `engagée, lot rendu avant le verdict du modèle` is a lot withdrawn, failed by an error or stopped by an incident during the escalation: the model did not judge it, read the lot line for the reason. 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,