@sylad/cadence 1.11.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.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/dist/config.js +12 -1
- package/dist/corrections.js +3 -0
- package/dist/orchestrate/cycle.js +42 -3
- package/dist/orchestrate/table.js +21 -0
- package/dist/perf.js +10 -1
- package/docs/orchestrate.md +11 -0
- package/docs/perf.md +1 -0
- package/docs/whats-new.md +3 -0
- package/package.json +1 -1
- package/skills/lead/SKILL.md +2 -0
|
@@ -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
|
+
"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.
|
|
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 }');
|
package/dist/corrections.js
CHANGED
|
@@ -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
|
-
|
|
2117
|
-
|
|
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/orchestrate.md
CHANGED
|
@@ -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/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/whats-new.md
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
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.
|
|
7
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).
|
|
8
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`.
|
|
9
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.
|
|
@@ -30,6 +31,8 @@ Each is a paragraph in [docs/whats-new.md](whats-new.md) and a full section in [
|
|
|
30
31
|
|
|
31
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).
|
|
32
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
|
+
|
|
33
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.
|
|
34
37
|
|
|
35
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).
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sylad/cadence",
|
|
3
|
-
"version": "1.
|
|
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",
|
package/skills/lead/SKILL.md
CHANGED
|
@@ -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,
|