@sylad/cadence 1.12.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.12.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.12.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",
@@ -86,11 +86,18 @@ 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;
91
98
  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.
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.
94
101
  e.stage = 'done';
95
102
  e.issue = 'échec';
96
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`);
@@ -783,9 +790,9 @@ async function session(c, kind, local = false) {
783
790
  }
784
791
  const report = cutReport({ minutes: Math.round(delay / 60_000), commits, files, numstat });
785
792
  step.cause = report;
786
- 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));
787
794
  }
788
- 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));
789
796
  }
790
797
  if (outcome.kind === 'quota') {
791
798
  if (outcome.firstStdout !== undefined)
@@ -25,8 +25,12 @@ const ESCALATION_TEXT = {
25
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
26
  function escalationText(l) {
27
27
  const e = l.escalation;
28
- if (!e.issue)
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é';
29
32
  return e.stage === 'fix' ? 'en cours (une correction puis une revue neuve à ce modèle)' : 'engagée, pas encore tranchée';
33
+ }
30
34
  if (e.issue !== 'conforme')
31
35
  return ESCALATION_TEXT[e.issue];
32
36
  if (l.status === 'ready')
@@ -192,7 +192,7 @@ orchestrate:
192
192
 
193
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
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`.
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
196
 
197
197
  **Code changed without a test (L287)**: before any review session, the program checks the diff of the lot (every commit that
198
198
  cites it, all sessions included). If it touches code (`testGate.code`, minus `testGate.except`) without touching a single test
package/docs/perf.md CHANGED
@@ -35,10 +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` — 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
+ - **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.
39
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 `⚠`).
40
40
  It is **not** the instant of `cadence corrections --since` (waves created strictly after).
41
- - **`--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
42
42
  `{ current, previous, change, warn, twin, lots, note }` (`twin`: the path of its quality twin, e.g. `throughput.escaped`). Same values as the text.
43
43
 
44
44
  ## Tokens, delays, wall time and cost (L226)
package/docs/whats-new.md CHANGED
@@ -4,6 +4,7 @@
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.
7
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.
8
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).
9
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`.
@@ -31,6 +32,8 @@ Each is a paragraph in [docs/whats-new.md](whats-new.md) and a full section in [
31
32
 
32
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).
33
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
+
34
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.
35
38
 
36
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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sylad/cadence",
3
- "version": "1.12.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,7 +141,7 @@ 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.
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
145
 
146
146
  **The threshold.** Before every new wave, read the context of the lead session with `cadence session context`: the
147
147
  `cadence-hud` mod publishes the context of each session, one file per session named after its id, and the command reads the