@sylad/cadence 1.37.0 → 1.38.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.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +2 -2
- package/bin/cadence.js +4 -1
- package/dist/cli.js +46 -66
- package/dist/lead.js +2 -0
- package/dist/local-task.js +143 -0
- package/dist/orchestrate/accept.js +22 -7
- package/dist/orchestrate/ajout.js +2 -1
- package/dist/orchestrate/briefs.js +12 -1
- package/dist/orchestrate/command.js +18 -12
- package/dist/orchestrate/cycle.js +9 -6
- package/dist/orchestrate/depot.js +2 -1
- package/dist/orchestrate/express.js +10 -0
- package/dist/orchestrate/lock.js +7 -3
- package/dist/orchestrate/probe.js +1 -1
- package/dist/orchestrate/state.js +2 -0
- package/dist/plan-commit.js +2 -0
- package/dist/plan.js +4 -8
- package/dist/repo-busy.js +51 -0
- package/docs/orchestrate.md +12 -0
- package/docs/raf.md +14 -2
- package/docs/spec.md +1 -1
- package/docs/whats-new.md +7 -1
- package/package.json +1 -1
- package/templates/orchestrate/implement.md +1 -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.38.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.
|
|
4
|
+
"version": "1.38.1",
|
|
5
5
|
"author": { "name": "Sylvain Ladoire" },
|
|
6
6
|
"homepage": "https://github.com/Sylad/cadence",
|
|
7
7
|
"repository": "https://github.com/Sylad/cadence",
|
package/README.md
CHANGED
|
@@ -47,8 +47,8 @@ Details: [docs/skills.md](docs/skills.md) and [docs/cadence-hud.md](docs/cadence
|
|
|
47
47
|
|
|
48
48
|
## Where to read more
|
|
49
49
|
|
|
50
|
-
- [docs/raf.md](docs/raf.md): the plan, its commands and
|
|
51
|
-
- [
|
|
50
|
+
- [docs/raf.md](docs/raf.md): the plan, its commands, its file, and [a lot of the lead](docs/raf.md#a-lot-of-the-lead-local-l519).
|
|
51
|
+
- [Waves](docs/orchestrate.md): [probe](docs/orchestrate.md#the-edge-case-probe-orchestrateprobe-l497), [express](docs/orchestrate.md#express-lane-l519).
|
|
52
52
|
- [Brief grid and pre-check reading](docs/mesures/brief-implementation-enrichi-avant-apres.md): grid of the code-reviewer in the `implement` brief (L523).
|
|
53
53
|
- [docs/deliver.md](docs/deliver.md): wait for the CI, deploy, verify the effect; [docs/remote-gate.md](docs/remote-gate.md): the gate on another machine.
|
|
54
54
|
- [docs/spec.md](docs/spec.md): screen and back-end sheets.
|
package/bin/cadence.js
CHANGED
|
@@ -12,7 +12,7 @@ const io = {
|
|
|
12
12
|
|
|
13
13
|
if (tool === 'raf') {
|
|
14
14
|
process.exitCode = await run(args, io);
|
|
15
|
-
} else if (['news', 'session', 'deliver', 'verify', 'skills', 'orchestrate', 'lead', 'corrections', 'perf', 'spec', 'ref', 'config', 'release'].includes(tool)) {
|
|
15
|
+
} else if (['news', 'session', 'deliver', 'verify', 'skills', 'orchestrate', 'lead', 'corrections', 'perf', 'spec', 'ref', 'config', 'release', 'local'].includes(tool)) {
|
|
16
16
|
process.exitCode = await run([tool, ...args], io);
|
|
17
17
|
} else if (tool === '--version' || tool === '-v') {
|
|
18
18
|
process.exitCode = await run([tool], io);
|
|
@@ -37,6 +37,9 @@ if (tool === 'raf') {
|
|
|
37
37
|
cadence ref measure <slug> <page> <page | x,y | x,y,l,h> [--quoi couleurs|contraste|boites|texte] [--largeur 1440] [--json]
|
|
38
38
|
mesure une zone d'une page rendue : couleurs, contraste WCAG 2.2, boîtes, mots OCR et taille de police estimée,
|
|
39
39
|
ramenés à 1440 px de large, tolérance déclarée
|
|
40
|
+
cadence local start|done <lot>
|
|
41
|
+
tâche locale du lead sur un lot posé par raf add --local : start verrouille le dépôt comme une vague,
|
|
42
|
+
done joue la porte une fois et libère le dépôt si elle est verte
|
|
40
43
|
cadence release patch|minor|major [--dry-run]
|
|
41
44
|
prépare le commit de version (fichiers de version, CHANGELOG) sans rien commiter ni publier ;
|
|
42
45
|
imprime les étapes du lead dans l'ordre et vérifie docs/whats-new.md
|
package/dist/cli.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, readFileSync, realpathSync,
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, realpathSync, writeFileSync } from 'node:fs';
|
|
2
2
|
import { execFileSync } from 'node:child_process';
|
|
3
3
|
import { basename, dirname, join, posix, relative, resolve } from 'node:path';
|
|
4
4
|
import { parseArgs } from 'node:util';
|
|
@@ -24,36 +24,36 @@ import { schedule } from './schedule.js';
|
|
|
24
24
|
import { AGENTS_DIR, installAgents, installSkills, SKILLS_DIR } from './skills.js';
|
|
25
25
|
import { configSet } from './config-set.js';
|
|
26
26
|
import { movePriority } from './priority.js';
|
|
27
|
-
import { findProjects, readDepth, leadTour, TITLE_MAX, truncate, readLeadUx, tourFreeLine, readLeadContextThreshold, findLeadDir, tourLine, tourNoTestLines, tourWorktreeLines } from './lead.js';
|
|
27
|
+
import { findProjects, isProject, readDepth, leadTour, TITLE_MAX, truncate, readLeadUx, tourFreeLine, readLeadContextThreshold, findLeadDir, tourLine, tourNoTestLines, tourWorktreeLines } from './lead.js';
|
|
28
28
|
import { configuredCap, expiredHoldLines, slotsLine, orchestrate, realOrchestrateDeps } from './orchestrate/command.js';
|
|
29
29
|
import { addHold, releaseHold } from './orchestrate/registry.js';
|
|
30
|
-
import {
|
|
31
|
-
import { INTEGRATION_LOCK } from './orchestrate/integrate.js';
|
|
32
|
-
import { mainTreeWorkers } from './orchestrate/main-tree.js';
|
|
30
|
+
import { waveHolding, warnIntegrating, warnWaveWorking } from './repo-busy.js';
|
|
33
31
|
import { cadenceHome, liveWaves, openLotsIn, openWavesIn, waveLotsIn } from './orchestrate/registry.js';
|
|
34
32
|
import { blockingLots, LABEL, resumeCommand } from './orchestrate/table.js';
|
|
35
33
|
import { renderTestsSansCode, testsSansCode } from './corrections-sans-code.js';
|
|
36
34
|
import { bugCodeOf, bugCodePasses, classifyAll, classifySous, measure, ollamaClassifier, ollamaSousClassifier, readCache, readJournals, readRuns, readSousCache, renderReport, sampleKeys, serializeCache, serializeSousCache } from './corrections.js';
|
|
37
35
|
import { computePerf, perfHeader, perfWindows, readPlans, readProjectDeliveries, readProjectFailedEntries, renderPerf } from './perf.js';
|
|
38
|
-
import { findAcceptation, parseAcceptation, redAcceptations, resealAcceptation, resealForGreen, runAcceptation, setVert } from './orchestrate/accept.js';
|
|
36
|
+
import { findAcceptation, parseAcceptation, redAcceptations, resealAcceptation, resealForGreen, runAcceptation, modeSpecGreen, setVert } from './orchestrate/accept.js';
|
|
39
37
|
import { refCommand } from './ref/command.js';
|
|
40
38
|
import { coverageCommand } from './ref/coverage.js';
|
|
41
39
|
import { release } from './release.js';
|
|
42
40
|
import { applyMigration, describeMigration, planMigration, unhandledError } from './migrate.js';
|
|
43
41
|
import { checkFiche, deriveQa, readFiche, summary } from './spec.js';
|
|
42
|
+
import { localCommand, localLotOf, localOverdueLines } from './local-task.js';
|
|
44
43
|
import { readHudContext } from './hud-context.js';
|
|
45
44
|
import { sessionClose, sessionStart } from './session.js';
|
|
46
45
|
import { clearNext, readNext, sharedStateDir, stateDir, writeNext } from './state.js';
|
|
47
46
|
const HELP = `raf — plan « reste à faire » versionné dans le dépôt, relié aux commits
|
|
48
47
|
|
|
49
48
|
raf init [--project nom] [--prefix L] [--no-hook]
|
|
50
|
-
raf add "titre" [--estimate j] [--quickwin] [--visible [--screen page|component|journey]] [--spec] [--public "titre public"] [--every jours] [--after L2,L4] [--uses banc,passerelle] [--parent L3]
|
|
49
|
+
raf add "titre" [--estimate j] [--quickwin] [--visible [--screen page|component|journey]] [--spec] [--local] [--public "titre public"] [--every jours] [--after L2,L4] [--uses banc,passerelle] [--parent L3]
|
|
51
50
|
raf public <id> "titre public" | --clear titre du lot dans le langage du public (pages Nouveautés et Plan)
|
|
52
51
|
raf screen <id> page|component|journey|none pose, remplace ou retire screen: d'un lot visible existant (seule la valeur page adopte l'étape spec d'orchestrate)
|
|
53
52
|
raf visible <id> on|off pose ou retire visible: d'un lot existant (changement visible : entrée Nouveautés et étape UX d'orchestrate) ; off est refusé tant que le lot a un screen:
|
|
54
53
|
raf spec <id> on|off pose ou retire spec: d'un lot existant (le lot change un comportement : étape spec back d'orchestrate)
|
|
55
54
|
raf move <id> --before|--after <id> réordonne un lot (ou une sous-tâche) dans le plan, parmi ceux du même parent ; refusé pendant une vague du dépôt, sur un lot archivé et sur un plan en lecture seule
|
|
56
55
|
raf uses <lot> [<ressource>…] remplace les ressources partagées du lot (uses:, déclarées par resources: dans le cadence.yaml du dossier parent ; raf check refuse un nom non déclaré) ; sans nom, retire la clé
|
|
56
|
+
cadence local start <lot> [--ttl minutes] | local done <lot> | local abort <lot> tâche locale du lead (lot posé par raf add --local) : start verrouille le dépôt comme une vague (aucune vague n'y démarre, --add y est refusé) et passe le lot à doing, refusé si une orchestration ou un autre lot du dépôt est en cours ; le verrou est déclaré (60 min par défaut), tenu sans processus jusqu'à done ou abort, signalé en ⚠ par lead tour et orchestrate --status au-delà du délai ; done joue la porte du projet UNE fois, libère le dépôt si elle est verte (la capture, la revue du diff, cadence deliver et raf done restent au lead) et le garde verrouillé sinon ; abort libère sans porte
|
|
57
57
|
raf start <id> raf done <id> [--force] raf drop <id> [--reason texte]
|
|
58
58
|
raf todo <id> [--reason texte] remet en attente un lot démarré sans commit de travail (doing → todo, started retiré, note datée) ; refusé s'il a des commits ; rouvre un lot done ou dropped (→ todo, finished retiré, note « rouvert ») — la seule façon de rouvrir, archivé compris (il revient dans le fichier du plan)
|
|
59
59
|
raf note <id> "texte"
|
|
@@ -175,6 +175,7 @@ function dispatch(argv, io) {
|
|
|
175
175
|
visible: { type: 'boolean' },
|
|
176
176
|
screen: { type: 'string' },
|
|
177
177
|
spec: { type: 'boolean' },
|
|
178
|
+
local: { type: 'boolean' },
|
|
178
179
|
public: { type: 'string' },
|
|
179
180
|
every: { type: 'string' },
|
|
180
181
|
dir: { type: 'string' },
|
|
@@ -282,7 +283,8 @@ function dispatch(argv, io) {
|
|
|
282
283
|
if (free)
|
|
283
284
|
io.out(free);
|
|
284
285
|
// une session du lead dont le TTL est échu (L278) : rien ne l'arrête, le tour la signale (hors --json, comme les créneaux)
|
|
285
|
-
|
|
286
|
+
// … et une tâche locale (L519) au-delà de son délai : le dépôt reste verrouillé, le tour le dit au lieu de le libérer en silence
|
|
287
|
+
for (const l of [...expiredHoldLines(io.now()), ...localOverdueLines(tourRoots(parent, depth), io.now())])
|
|
286
288
|
io.out(l);
|
|
287
289
|
// le réglage lead.ux du cadence.yaml du dossier (L197) : le lead lit ici qui pose le verdict UX, au lieu d'ouvrir le fichier
|
|
288
290
|
try {
|
|
@@ -396,14 +398,10 @@ function dispatch(argv, io) {
|
|
|
396
398
|
const plan = loadPlan();
|
|
397
399
|
let id;
|
|
398
400
|
if (values.parent) {
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
if (values.public !== undefined)
|
|
404
|
-
throw new RafError('--public se pose sur un lot, pas sur une sous-tâche');
|
|
405
|
-
if (values.every !== undefined)
|
|
406
|
-
throw new RafError('--every se pose sur un lot, pas sur une sous-tâche');
|
|
401
|
+
for (const [flag, given] of [['screen', values.screen !== undefined], ['spec', values.spec], ['local', values.local], ['public', values.public !== undefined], ['every', values.every !== undefined]]) {
|
|
402
|
+
if (given)
|
|
403
|
+
throw new RafError(`--${flag} se pose sur un lot, pas sur une sous-tâche`);
|
|
404
|
+
}
|
|
407
405
|
if (values.uses !== undefined)
|
|
408
406
|
throw new RafError(`uses se pose sur un lot, pas sur une sous-tâche : ${values.parent}`);
|
|
409
407
|
id = plan.addTask(values.parent, rest.join(' '));
|
|
@@ -421,7 +419,7 @@ function dispatch(argv, io) {
|
|
|
421
419
|
const uses = values.uses?.split(',').map((n) => n.trim()).filter(Boolean);
|
|
422
420
|
if (uses?.length === 0)
|
|
423
421
|
throw new RafError(invalidResource(''));
|
|
424
|
-
id = plan.add(rest.join(' '), today, { estimate, quickwin: values.quickwin, visible: values.visible, screen: values.screen, spec: values.spec, public: values.public, every, after, uses });
|
|
422
|
+
id = plan.add(rest.join(' '), today, { estimate, quickwin: values.quickwin, visible: values.visible, screen: values.screen, spec: values.spec, local: values.local, public: values.public, every, after, uses });
|
|
425
423
|
}
|
|
426
424
|
plan.save();
|
|
427
425
|
io.out(id);
|
|
@@ -560,7 +558,7 @@ function dispatch(argv, io) {
|
|
|
560
558
|
const plan = loadPlan();
|
|
561
559
|
plan.move(rest[0], place, values[place]); // en mémoire : les refus de la commande (plan en lecture seule, parent, archive) passent avant celui de la vague
|
|
562
560
|
// Un lot de la vague s'appuie sur l'ordre du plan qu'il a lu : le réordonner sous ses pieds est refusé tant que la vague tient le dépôt.
|
|
563
|
-
const holding = waveHolding(root);
|
|
561
|
+
const holding = waveHolding(root, 'move');
|
|
564
562
|
if (holding)
|
|
565
563
|
throw new RafError(`move : orchestration en cours (vague ${holding.wave}, pid ${holding.pid}) — attendre que les lots de ce dépôt soient finis (le verrou tombe dès que tous sont prêts ou rendus), puis relancer`);
|
|
566
564
|
plan.save();
|
|
@@ -793,7 +791,7 @@ function dispatch(argv, io) {
|
|
|
793
791
|
case 'deliver': {
|
|
794
792
|
if (!gitRoot(io.cwd))
|
|
795
793
|
throw new RafError('deliver : à lancer dans un dépôt git');
|
|
796
|
-
const orchestrating = waveHolding(root);
|
|
794
|
+
const orchestrating = waveHolding(root, 'deliver');
|
|
797
795
|
if (orchestrating) {
|
|
798
796
|
const refusal = `deliver : orchestration en cours (vague ${orchestrating.wave}, pid ${orchestrating.pid})`;
|
|
799
797
|
if (values.sha === undefined)
|
|
@@ -858,6 +856,8 @@ function dispatch(argv, io) {
|
|
|
858
856
|
io.out(base);
|
|
859
857
|
return 0;
|
|
860
858
|
}
|
|
859
|
+
case 'local':
|
|
860
|
+
return localCommand(rest, values.ttl, { repo: gitRoot(root) ?? root, loadPlan, today, now: io.now, gate: (sha) => verifyGateCommand({ sha }, io), out: io.out, err: io.err });
|
|
861
861
|
case 'news':
|
|
862
862
|
return news(rest, loadPlan(), newsDir, today, values, io, refuseLongTitle, () => hypothesesOf(root, configPath));
|
|
863
863
|
default:
|
|
@@ -1164,7 +1164,8 @@ function prePush(root, io, remoteName, configPath, planFile) {
|
|
|
1164
1164
|
return 0;
|
|
1165
1165
|
}
|
|
1166
1166
|
const holding = waveHolding(root);
|
|
1167
|
-
|
|
1167
|
+
// Une tâche locale du lead (L519) tient le dépôt comme une vague, mais n'a pas de lots de vague : rien à vérifier pour la première garde, et pas de faux « état illisible ».
|
|
1168
|
+
if (holding && !localLotOf(holding.wave)) {
|
|
1168
1169
|
const lots = waveLotsIn(cadenceHome(), holding.pid, root);
|
|
1169
1170
|
if (lots === null) {
|
|
1170
1171
|
io.err(`raf: vague ${holding.wave} : état illisible, push non vérifié`);
|
|
@@ -1196,44 +1197,6 @@ function prePush(root, io, remoteName, configPath, planFile) {
|
|
|
1196
1197
|
io.err(`raf: ${refusal}`);
|
|
1197
1198
|
return REFUSED;
|
|
1198
1199
|
}
|
|
1199
|
-
/**
|
|
1200
|
-
* Avertit (stderr, sans refuser) quand une intégration du dépôt est en cours (L425) : le plan écrit dans l'arbre principal est un fichier suivi modifié, et
|
|
1201
|
-
* l'intégration en cours le voit et refuse son lot (« arbre du dépôt modifié »). Seul l'arbre principal est concerné : dans le worktree d'un lot, l'écriture
|
|
1202
|
-
* ne change pas l'arbre que l'intégration contrôle.
|
|
1203
|
-
*/
|
|
1204
|
-
function warnIntegrating(root, err) {
|
|
1205
|
-
const top = gitRoot(root);
|
|
1206
|
-
if (!top || !statSync(join(top, '.git')).isDirectory())
|
|
1207
|
-
return;
|
|
1208
|
-
const holder = activeLock(join(sharedStateDir(root), INTEGRATION_LOCK));
|
|
1209
|
-
if (holder)
|
|
1210
|
-
err(`avertissement : intégration en cours (vague ${holder.wave}, pid ${holder.pid}) — ce plan est un fichier suivi modifié de l'arbre principal, l'intégration en cours peut refuser son lot (arbre du dépôt modifié) : committer le plan sans attendre`);
|
|
1211
|
-
}
|
|
1212
|
-
/**
|
|
1213
|
-
* Avertit (stderr, sans refuser) quand une session de vague écrit dans l'arbre principal du dépôt (L475) : un lot en implémentation, revue ou correction sans worktree.
|
|
1214
|
-
* Un commit de plan du lead à ce moment-là rend le lot (cas du 30-10 : cadence L458 rendu par le commit du plan). Sans effet dans le worktree d'un lot, ou depuis une session de la vague (c'est elle qui tient l'arbre).
|
|
1215
|
-
*/
|
|
1216
|
-
function warnWaveWorking(root, env, err) {
|
|
1217
|
-
if (env.CADENCE_ORCHESTRATED)
|
|
1218
|
-
return;
|
|
1219
|
-
const top = gitRoot(root);
|
|
1220
|
-
if (!top || !statSync(join(top, '.git')).isDirectory())
|
|
1221
|
-
return;
|
|
1222
|
-
const holder = activeLock(join(sharedStateDir(root), REPO_LOCK));
|
|
1223
|
-
if (!holder)
|
|
1224
|
-
return;
|
|
1225
|
-
const working = mainTreeWorkers(cadenceHome(), holder.pid, top);
|
|
1226
|
-
if (working?.length)
|
|
1227
|
-
err(`avertissement : la vague ${holder.wave} (pid ${holder.pid}) travaille dans l'arbre principal de ce dépôt (${working.join(', ')}) — un commit de plan maintenant rendrait ces lots à la revue ; attendre la fin de leur étape, ou écrire le plan sans le committer`);
|
|
1228
|
-
}
|
|
1229
|
-
/** La vague qui tient le verrou du dépôt tant qu'un de ses lots y travaille encore (état illisible : on la tient pour active) ; null si le dépôt est libre. Partagé par `deliver` et `spec reseal`. */
|
|
1230
|
-
function waveHolding(root) {
|
|
1231
|
-
const orchestrating = activeLock(join(sharedStateDir(root), REPO_LOCK));
|
|
1232
|
-
if (!orchestrating)
|
|
1233
|
-
return null;
|
|
1234
|
-
const stillOpen = openLotsIn(cadenceHome(), orchestrating.pid, root);
|
|
1235
|
-
return stillOpen === null || stillOpen.length > 0 ? orchestrating : null;
|
|
1236
|
-
}
|
|
1237
1200
|
/**
|
|
1238
1201
|
* `cadence spec reseal <lot> <test> --reason texte` (L200) : le chemin du lead quand un test d'acceptation scellé du lot EN COURS contredit la fiche.
|
|
1239
1202
|
* Le test est corrigé dans l'arbre de travail (fiche inchangée, ou amendée et commitée par des commits du lot, L217) ; la commande refait l'empreinte de ce seul test, journalise la raison dans
|
|
@@ -1305,7 +1268,9 @@ function specGreen(args, ctx, io) {
|
|
|
1305
1268
|
const doc = parseAcceptation(readFileSync(join(repo, json), 'utf8'));
|
|
1306
1269
|
if (typeof doc === 'string')
|
|
1307
1270
|
throw new RafError(`${json} : ${doc}`);
|
|
1308
|
-
|
|
1271
|
+
// Vert mais sans empreintes (L526, issue #20) : clos avant le scellement, il tient le verrou total sans qu'aucun chemin ne le scelle. On rejoue l'acceptation et, verte, on pose les empreintes.
|
|
1272
|
+
const mode = modeSpecGreen(doc);
|
|
1273
|
+
if (mode.deja) {
|
|
1309
1274
|
io.out(`${lotId} : ${json} est déjà vert, rien à faire`);
|
|
1310
1275
|
return 0;
|
|
1311
1276
|
}
|
|
@@ -1320,25 +1285,26 @@ function specGreen(args, ctx, io) {
|
|
|
1320
1285
|
throw new RafError(`fichiers listés dans ${json} sous la racine d'aucune commande de orchestrate.accept : ${orphans.join(', ')}`);
|
|
1321
1286
|
const failed = runs.filter((r) => r.code !== 0);
|
|
1322
1287
|
if (failed.length) {
|
|
1323
|
-
io.err(`${lotId} : tests d'acceptation encore rouges (${failed.map((r) => `${r.command}, code ${r.code}`).join(' ; ')}), ${json} reste
|
|
1288
|
+
io.err(`${lotId} : tests d'acceptation encore rouges (${failed.map((r) => `${r.command}, code ${r.code}`).join(' ; ')}), ${json} ${mode.reste}`);
|
|
1324
1289
|
for (const r of failed)
|
|
1325
1290
|
io.err(r.output.trim().split('\n').slice(-40).join('\n'));
|
|
1326
1291
|
return 1;
|
|
1327
1292
|
}
|
|
1328
1293
|
const before = readFileSync(join(repo, json), 'utf8');
|
|
1329
|
-
let
|
|
1294
|
+
let sealed;
|
|
1330
1295
|
try {
|
|
1331
|
-
|
|
1296
|
+
sealed = resealForGreen(repo, json, ctx.today);
|
|
1332
1297
|
}
|
|
1333
1298
|
catch (e) {
|
|
1334
1299
|
throw new RafError(`${lotId} : ${e.message}`);
|
|
1335
1300
|
}
|
|
1336
|
-
|
|
1301
|
+
const resceles = sealed.refaits;
|
|
1302
|
+
setVert(repo, json); // déjà vrai pour un lot vert sans empreintes : le fichier garde ce qu'il avait
|
|
1337
1303
|
try {
|
|
1338
1304
|
git(repo, ['add', '--', json]);
|
|
1339
1305
|
// Le sujet reste court et fixe (le gabarit `plan.commit` du projet s'il y en a un, comme tout commit écrit par cadence, L343) ; les tests rescellés, sans borne, vont dans le corps, une ligne chacun.
|
|
1340
1306
|
const template = readPlanConfig(join(repo, 'cadence.yaml'), repo)?.commit;
|
|
1341
|
-
const subject = template ? planCommitMessage(template, { lot: lotId, action:
|
|
1307
|
+
const subject = template ? planCommitMessage(template, { lot: lotId, action: mode.action }) : `chore(${lotId}): ${mode.action}`;
|
|
1342
1308
|
const body = resceles.length ? ['empreintes rescellées (tests mis au point depuis le commit rouge) :', ...resceles.map((f) => `- ${f}`)].join('\n') : null;
|
|
1343
1309
|
git(repo, ['commit', '-q', '-m', subject, ...(body ? ['-m', body] : []), '--', json]);
|
|
1344
1310
|
}
|
|
@@ -1354,14 +1320,14 @@ function specGreen(args, ctx, io) {
|
|
|
1354
1320
|
const out = e;
|
|
1355
1321
|
throw new RafError(`spec green : le commit a échoué, ${json} est revenu tel qu'avant la commande — ${(out.stderr || out.stdout || out.message).trim().split('\n').slice(-5).join(' | ')}`);
|
|
1356
1322
|
}
|
|
1357
|
-
io.out(`${lotId} : ${json} →
|
|
1323
|
+
io.out(`${lotId} : ${json} → ${mode.fait}${sealed.scelles.length ? ` (${sealed.scelles.join(', ')})` : ''}, commit ${git(repo, ['rev-parse', '--short', 'HEAD']).trim()}${resceles.length ? ` ; empreintes rescellées (tests mis au point depuis le commit rouge) : ${resceles.join(', ')}` : ''}`);
|
|
1358
1324
|
return 0;
|
|
1359
1325
|
}
|
|
1360
1326
|
/** Un commit du lead en pleine passe ferait signaler au lot suivant un écart qui n'est pas le sien : tant qu'une vague tient le verrou du dépôt, `spec reseal` et `spec green` refusent. */
|
|
1361
1327
|
function refuseWhileWaveHolds(ctx, command) {
|
|
1362
1328
|
// Comme `deliver` : la vague tient le verrou du dépôt tant qu'un de ses lots y travaille (état illisible : on refuse).
|
|
1363
1329
|
// Le refus est UN message : la vague vivante qui tient le verrou (à attendre) et les vagues arrêtées du dépôt, avec leurs dossiers et leurs commandes (L238).
|
|
1364
|
-
const orchestrating = waveHolding(ctx.root);
|
|
1330
|
+
const orchestrating = waveHolding(ctx.root, command);
|
|
1365
1331
|
if (orchestrating) {
|
|
1366
1332
|
const repo = gitRoot(ctx.root) ?? ctx.root;
|
|
1367
1333
|
let open = [];
|
|
@@ -1469,6 +1435,20 @@ function specReseal(args, reason, porteur, ctx, io) {
|
|
|
1469
1435
|
io.out(`${lotId} : ${norm} rescellé (${r.avant.slice(0, 7)} → ${r.apres.slice(0, 7)}), commit ${sha}${r.ficheAmendee ? `, fiche amendée en ${r.ficheAmendee}` : ''} ; raison dans ${json} et en note du lot ${cite}${tracked ? '' : ' (plan à commiter)'}`);
|
|
1470
1436
|
return 0;
|
|
1471
1437
|
}
|
|
1438
|
+
/** Les racines git des projets du tour (la clé `depth:` du dossier, un seul niveau si elle est illisible : le tour a déjà sa ligne d'erreur). */
|
|
1439
|
+
function tourRoots(parent, depth) {
|
|
1440
|
+
let levels = depth;
|
|
1441
|
+
if (levels === undefined) {
|
|
1442
|
+
try {
|
|
1443
|
+
levels = readDepth(parent);
|
|
1444
|
+
}
|
|
1445
|
+
catch {
|
|
1446
|
+
levels = 1;
|
|
1447
|
+
}
|
|
1448
|
+
}
|
|
1449
|
+
const dirs = findProjects(parent, levels);
|
|
1450
|
+
return (dirs.length === 0 && isProject(parent) ? [parent] : dirs).map((d) => gitRoot(d) ?? d);
|
|
1451
|
+
}
|
|
1472
1452
|
/** Durée par défaut (minutes) d'une session du lead inscrite par `lead hold` : de quoi finir un correctif ou une revue, pas de quoi oublier la réservation toute la journée. */
|
|
1473
1453
|
const HOLD_TTL_MINUTES = 60;
|
|
1474
1454
|
/** Longueur maximale de l'étiquette encodée : un nom de fichier tient en 255 octets, `.json.tmp` en prend 9. */
|
package/dist/lead.js
CHANGED
|
@@ -3,6 +3,7 @@ import { basename, dirname, join, relative, sep } from 'node:path';
|
|
|
3
3
|
import { audit, gateWaiting, nextUp, planCommits, workByLot } from './audit.js';
|
|
4
4
|
import { PLAN_DEFAULT, PLAN_LEGACY, planLoader, readPlanConfig } from './config.js';
|
|
5
5
|
import { diffDays } from './dates.js';
|
|
6
|
+
import { localRepoNotes } from './local-task.js';
|
|
6
7
|
import { gitRoot, repoStatus } from './git.js';
|
|
7
8
|
import { linkCommits } from './link.js';
|
|
8
9
|
import { commitCount, lastActivity, lockStatus } from './session.js';
|
|
@@ -195,6 +196,7 @@ export function tourRow(dir, today, idle = TOUR_IDLE, name = basename(dir)) {
|
|
|
195
196
|
row.repo.push('non poussé');
|
|
196
197
|
if (lockStatus(sharedStateDir(root))?.live)
|
|
197
198
|
row.repo.push('livraison en cours');
|
|
199
|
+
row.repo.push(...localRepoNotes(root, new Date()));
|
|
198
200
|
const kept = keptTrees(root);
|
|
199
201
|
if (kept.length)
|
|
200
202
|
row.worktrees = kept;
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { basename, join } from 'node:path';
|
|
2
|
+
import { REPO_LOCK, activeLock, lockOverdue, releaseLock, takeLock } from './orchestrate/lock.js';
|
|
3
|
+
import { longDuration } from './orchestrate/guard.js';
|
|
4
|
+
import { RafError } from './plan.js';
|
|
5
|
+
import { tryGit } from './git.js';
|
|
6
|
+
import { sharedStateDir } from './state.js';
|
|
7
|
+
/** Nom de « vague » sous lequel la tâche locale d'un lot tient le verrou du dépôt : les refus des vagues le citent tel quel. */
|
|
8
|
+
export const localWave = (lot) => `local:${lot}`;
|
|
9
|
+
/** Le lot d'un verrou de tâche locale (`local:<lot>`), null pour le verrou d'une vague. */
|
|
10
|
+
export const localLotOf = (wave) => (wave.startsWith('local:') ? wave.slice('local:'.length) : null);
|
|
11
|
+
/** Durée annoncée (minutes) d'une tâche locale sans `--ttl` : celle de `lead hold` — de quoi finir une retouche, pas de quoi oublier le dépôt verrouillé toute la journée. */
|
|
12
|
+
export const LOCAL_TTL_MINUTES = 60;
|
|
13
|
+
const lockFile = (repo) => join(sharedStateDir(repo), REPO_LOCK);
|
|
14
|
+
/** Le refus qu'un verrou tenu oppose à une tâche locale ou à une vague : une tâche locale est nommée comme telle, avec sa sortie. */
|
|
15
|
+
export function heldRefusal(repo, held) {
|
|
16
|
+
const local = localLotOf(held.wave);
|
|
17
|
+
if (local)
|
|
18
|
+
return `${repoName(repo)} : une tâche locale du lead y est en cours (${local}, depuis ${held.started}) — la finir (cadence local done ${local}) ou l'abandonner (cadence local abort ${local})`;
|
|
19
|
+
return `${repoName(repo)} : une orchestration y est déjà en cours (${held.wave}, pid ${held.pid})`;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* `cadence local start <lot>` : le lead va modifier lui-même ce dépôt pour un lot marqué `local`. Le dépôt est verrouillé comme par une vague (aucune vague n'y démarre,
|
|
23
|
+
* `--add` y est refusé par le même contrôle de dépôt) et le lot passe à `doing`. Le verrou est DÉCLARÉ (`until`) : le shell qui lance la commande meurt à la fin de l'appel de l'outil
|
|
24
|
+
* Bash et la session du lead (Claude desktop) n'a pas de nom de processus stable — aucun processus ne le porte, il tient jusqu'à `local done` ou `local abort`, et au TTL échu il se
|
|
25
|
+
* signale (⚠) au lieu de tomber. Refus (`RafError`, rien n'est verrouillé ni écrit ; si l'écriture du plan échoue, le verrou pris est relâché) : lot inconnu, non marqué local, déjà
|
|
26
|
+
* fini, une orchestration ou une autre tâche locale qui tient le dépôt, un autre lot du dépôt en cours.
|
|
27
|
+
*/
|
|
28
|
+
export function startLocal(repo, plan, lotId, d) {
|
|
29
|
+
const lot = plan.lots().find((l) => l.id === lotId);
|
|
30
|
+
if (!lot)
|
|
31
|
+
throw new RafError(`lot inconnu : ${lotId}`);
|
|
32
|
+
if (!lot.local)
|
|
33
|
+
throw new RafError(`${lotId} n'est pas marqué local : le lead ne modifie pas lui-même le dépôt pour un lot de vague (raf add --local pour en créer un)`);
|
|
34
|
+
if (lot.status === 'done' || lot.status === 'dropped')
|
|
35
|
+
throw new RafError(`${lotId} est ${lot.status} : une tâche locale ne reprend pas un lot fini`);
|
|
36
|
+
const held = activeLock(lockFile(repo));
|
|
37
|
+
if (held)
|
|
38
|
+
throw new RafError(held.wave === localWave(lotId) ? `${lotId} : la tâche locale est déjà en cours (depuis ${held.started}) — cadence local done ${lotId} à sa fin` : `${heldRefusal(repo, held)} — attendre sa fin`);
|
|
39
|
+
const others = plan.lots().filter((l) => l.status === 'doing' && l.id !== lotId);
|
|
40
|
+
if (others.length)
|
|
41
|
+
throw new RafError(`${repoName(repo)} : un lot du dépôt est en cours (${others.map((l) => l.id).join(', ')}) — le finir, ou raf todo, avant la tâche locale`);
|
|
42
|
+
const now = d.now();
|
|
43
|
+
const until = new Date(now.getTime() + (d.ttlMinutes ?? LOCAL_TTL_MINUTES) * 60_000).toISOString();
|
|
44
|
+
const taken = takeLock(lockFile(repo), { pid: d.pid, wave: localWave(lotId), started: now.toISOString(), until });
|
|
45
|
+
if (!taken.ok)
|
|
46
|
+
throw new RafError(`${heldRefusal(repo, taken.held)} — attendre sa fin`);
|
|
47
|
+
if (lot.status === 'todo') {
|
|
48
|
+
try {
|
|
49
|
+
plan.setStatus(lotId, 'doing', d.today);
|
|
50
|
+
plan.save();
|
|
51
|
+
}
|
|
52
|
+
catch (e) {
|
|
53
|
+
releaseLock(lockFile(repo), d.pid);
|
|
54
|
+
throw e;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
/** Le verrou de la tâche locale de `lotId` s'il tient le dépôt ; `RafError` sinon (dépôt libre, vague, ou tâche d'un autre lot). */
|
|
59
|
+
function localHeld(repo, lotId) {
|
|
60
|
+
const held = activeLock(lockFile(repo));
|
|
61
|
+
if (!held || held.wave !== localWave(lotId))
|
|
62
|
+
throw new RafError(`aucune tâche locale en cours pour ${lotId} (cadence local start ${lotId})`);
|
|
63
|
+
return held;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* `cadence local done <lot>` : la tâche locale du lead est finie. La porte du projet est jouée UNE fois ; verte, le verrou tombe (la capture, la revue du diff par le lead,
|
|
67
|
+
* la livraison et `raf done` restent au lead, dans cet ordre) ; rouge ou sans verdict, le verrou reste : le lead corrige et rejoue. Refus (`RafError`) quand ce n'est pas la tâche locale de ce lot qui tient le dépôt.
|
|
68
|
+
*/
|
|
69
|
+
export async function finishLocal(repo, lotId, d) {
|
|
70
|
+
const held = localHeld(repo, lotId);
|
|
71
|
+
const code = await d.gate();
|
|
72
|
+
if (code === 1) {
|
|
73
|
+
d.err(`${lotId} : porte rouge — le dépôt reste verrouillé ; corriger, puis cadence local done ${lotId}`);
|
|
74
|
+
return 1;
|
|
75
|
+
}
|
|
76
|
+
if (code !== 0) {
|
|
77
|
+
d.err(`${lotId} : la porte n'a rien rejoué (orchestrate.build / orchestrate.test absents ?) — le dépôt reste verrouillé`);
|
|
78
|
+
return 2;
|
|
79
|
+
}
|
|
80
|
+
releaseLock(lockFile(repo), held.pid);
|
|
81
|
+
d.out(`${lotId} : porte verte, dépôt libéré. Suite : la capture d'un lot visible, relire le diff du lot et poser son verdict (raf review ${lotId} "…" — raf done le demande), cadence deliver, puis raf done ${lotId}`);
|
|
82
|
+
return 0;
|
|
83
|
+
}
|
|
84
|
+
/** `cadence local abort <lot>` : lève le verrou de la tâche locale sans jouer la porte ; le plan n'est pas touché (le lot reste `doing` : `raf todo <lot>` pour l'abandonner). */
|
|
85
|
+
export function abortLocal(repo, lotId, out = () => { }) {
|
|
86
|
+
const held = localHeld(repo, lotId);
|
|
87
|
+
releaseLock(lockFile(repo), held.pid);
|
|
88
|
+
out(`${lotId} : tâche locale abandonnée, dépôt libéré sans porte ; le lot reste doing (raf todo ${lotId} pour le remettre à todo)`);
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Une ligne ⚠ par dépôt dont une tâche locale a dépassé son TTL à `now` (lead tour, orchestrate --status) : le verrou tient toujours, le lead le finit ou l'abandonne. Vide sans tâche échue.
|
|
92
|
+
* Un dépôt illisible est sauté.
|
|
93
|
+
*/
|
|
94
|
+
export function localOverdueLines(repos, now) {
|
|
95
|
+
const lines = [];
|
|
96
|
+
for (const repo of new Set(repos)) {
|
|
97
|
+
let held;
|
|
98
|
+
try {
|
|
99
|
+
held = activeLock(lockFile(repo));
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
const lot = held && localLotOf(held.wave);
|
|
105
|
+
if (!held || !lot || !lockOverdue(held, now))
|
|
106
|
+
continue;
|
|
107
|
+
const since = Date.parse(held.started);
|
|
108
|
+
const age = Number.isFinite(since) ? `démarrée depuis ${longDuration(now.getTime() - since)}, ` : '';
|
|
109
|
+
lines.push(`⚠ tâche locale échue : ${lot} (${basename(repo)} · ${age}échue depuis ${longDuration(now.getTime() - Date.parse(held.until))}) — le dépôt reste verrouillé : la finir (cadence local done ${lot}) ou l'abandonner (cadence local abort ${lot})`);
|
|
110
|
+
}
|
|
111
|
+
return lines;
|
|
112
|
+
}
|
|
113
|
+
/** Ce que le tableau du lead dit du dépôt : « tâche locale L1 », avec « (échue) » au TTL passé ; vide sans tâche locale. */
|
|
114
|
+
export function localRepoNotes(repo, now) {
|
|
115
|
+
const held = activeLock(lockFile(repo));
|
|
116
|
+
const lot = held && localLotOf(held.wave);
|
|
117
|
+
return held && lot ? [`tâche locale ${lot}${lockOverdue(held, now) ? ' (échue)' : ''}`] : [];
|
|
118
|
+
}
|
|
119
|
+
/** Le refus qu'un lot local oppose à une vague (lancement, `--add`, `--continue`) : il est au lead ; null pour un lot de vague. */
|
|
120
|
+
export const localLotRefusal = (lot) => (lot.local ? `lot local (raf add --local) : le lead le fait lui-même (cadence local start ${lot.id}), une vague ne le joue pas` : null);
|
|
121
|
+
const repoName = (repo) => repo.split('/').filter(Boolean).pop() ?? repo;
|
|
122
|
+
/** `cadence local start|done|abort <lot> [--ttl minutes]` : l'analyse de la commande, puis `startLocal`, `finishLocal` ou `abortLocal` ; `gate(sha)` joue la porte du projet au sha. */
|
|
123
|
+
export async function localCommand(rest, ttl, c) {
|
|
124
|
+
const usage = 'usage : cadence local start <lot> [--ttl minutes] | cadence local done <lot> | cadence local abort <lot>';
|
|
125
|
+
if (rest.length !== 2 || (rest[0] !== 'start' && rest[0] !== 'done' && rest[0] !== 'abort'))
|
|
126
|
+
throw new RafError(usage);
|
|
127
|
+
const [sub, lot] = rest;
|
|
128
|
+
if (ttl !== undefined && sub !== 'start')
|
|
129
|
+
throw new RafError(`--ttl ne va qu'avec local start (${usage})`);
|
|
130
|
+
const minutes = ttl === undefined ? undefined : Number(ttl);
|
|
131
|
+
if (minutes !== undefined && !(Number.isInteger(minutes) && minutes >= 1))
|
|
132
|
+
throw new RafError(`--ttl invalide : ${ttl} (un nombre entier de minutes ≥ 1)`);
|
|
133
|
+
const deps = { ...c, pid: process.pid, ttlMinutes: minutes, gate: () => c.gate(tryGit(c.repo, ['rev-parse', 'HEAD']) ?? 'HEAD') };
|
|
134
|
+
if (sub === 'done')
|
|
135
|
+
return finishLocal(c.repo, lot, deps);
|
|
136
|
+
if (sub === 'abort') {
|
|
137
|
+
abortLocal(c.repo, lot, c.out);
|
|
138
|
+
return 0;
|
|
139
|
+
}
|
|
140
|
+
startLocal(c.repo, c.loadPlan(), lot, deps);
|
|
141
|
+
c.out(`${lot} : dépôt verrouillé (tâche locale, ${minutes ?? LOCAL_TTL_MINUTES} min annoncées) — aucune vague n'y démarre, le verrou tient jusqu'à cadence local done ${lot} (ou local abort ${lot}) ; au-delà du délai il se signale en ⚠`);
|
|
142
|
+
return 0;
|
|
143
|
+
}
|
|
@@ -3,6 +3,7 @@ import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
|
3
3
|
import { isAbsolute, join, posix } from 'node:path';
|
|
4
4
|
import { parse } from 'yaml';
|
|
5
5
|
import { citedRefs } from '../link.js';
|
|
6
|
+
import { PLAN_ACTIONS } from '../plan-commit.js';
|
|
6
7
|
import { isParts } from './gate-parts.js';
|
|
7
8
|
const isObj = (v) => typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
8
9
|
function git(repo, ...args) {
|
|
@@ -215,7 +216,8 @@ export function runAcceptation(repo, json, accept) {
|
|
|
215
216
|
/**
|
|
216
217
|
* `cadence spec green` (L366) : la commande d'acceptation vient de passer au vert sur les tests tels qu'ils sont. Ceux qu'un lot mené à la main a mis au point
|
|
217
218
|
* après le commit rouge ont une empreinte scellée périmée : le verrou les signalerait encore à chaque lot suivant. Chaque empreinte différente est refaite et
|
|
218
|
-
* journalisée dans `resceaux` (comme `spec reseal`, sans fiche amendée) ; le commit qui pose `vert: true` devient ainsi le commit rouge.
|
|
219
|
+
* journalisée dans `resceaux` (comme `spec reseal`, sans fiche amendée) ; le commit qui pose `vert: true` devient ainsi le commit rouge. Un test SANS empreinte (lot clos avant le scellement, L526)
|
|
220
|
+
* n'a rien de périmé : il est scellé (`sealAcceptation`, sans entrée de journal). Rend les tests rescellés (`refaits`) et ceux qui viennent d'être scellés (`scelles`).
|
|
219
221
|
* L'appelant s'est assuré que les tests listés sont commités (l'empreinte rescellée est celle de l'arbre de travail) ; un test absent est refusé.
|
|
220
222
|
*/
|
|
221
223
|
export function resealForGreen(repo, path, today) {
|
|
@@ -223,25 +225,38 @@ export function resealForGreen(repo, path, today) {
|
|
|
223
225
|
const a = parseAcceptation(readFileSync(file, 'utf8'));
|
|
224
226
|
if (typeof a === 'string')
|
|
225
227
|
throw new Error(`${path} : ${a}`);
|
|
226
|
-
const fichiers = a.fichiers.map((f) => posix.normalize(f));
|
|
228
|
+
const fichiers = [...new Set(a.fichiers.map((f) => posix.normalize(f)))];
|
|
227
229
|
const absents = fichiers.filter((f) => !existsSync(join(repo, f)));
|
|
228
230
|
if (absents.length)
|
|
229
231
|
throw new Error(`test(s) listé(s) absent(s) de ${path} : ${absents.join(', ')}`);
|
|
230
232
|
const doc = JSON.parse(readFileSync(file, 'utf8'));
|
|
231
233
|
const empreintes = isObj(doc.empreintes) ? doc.empreintes : {};
|
|
232
234
|
const journal = Array.isArray(doc.resceaux) ? doc.resceaux : [];
|
|
233
|
-
const
|
|
235
|
+
const scelles = fichiers.filter((f) => !empreintes[f]);
|
|
236
|
+
const refaits = fichiers.filter((f) => empreintes[f] && empreintes[f] !== git(repo, 'hash-object', '--', f).trim());
|
|
237
|
+
for (const f of scelles)
|
|
238
|
+
empreintes[f] = git(repo, 'hash-object', '--', f).trim();
|
|
234
239
|
for (const f of refaits) {
|
|
235
240
|
const apres = git(repo, 'hash-object', '--', f).trim();
|
|
236
|
-
journal.push({ test: f, raison: 'cadence spec green : tests mis au point après le commit rouge, acceptation verte sur ces tests', date: today, avant: empreintes[f]
|
|
241
|
+
journal.push({ test: f, raison: 'cadence spec green : tests mis au point après le commit rouge, acceptation verte sur ces tests', date: today, avant: empreintes[f], apres });
|
|
237
242
|
empreintes[f] = apres;
|
|
238
243
|
}
|
|
239
|
-
if (refaits.length) {
|
|
244
|
+
if (refaits.length || scelles.length) {
|
|
240
245
|
doc.empreintes = empreintes;
|
|
241
|
-
|
|
246
|
+
if (refaits.length)
|
|
247
|
+
doc.resceaux = journal;
|
|
242
248
|
writeFileSync(file, `${JSON.stringify(doc, null, 2)}\n`);
|
|
243
249
|
}
|
|
244
|
-
return refaits;
|
|
250
|
+
return { refaits, scelles };
|
|
251
|
+
}
|
|
252
|
+
/** Les fichiers listés qui n'ont pas d'empreinte scellée (L526 : lot clos à `vert: true` avant le scellement, ou scellé en partie). */
|
|
253
|
+
export function sansEmpreinte(a) {
|
|
254
|
+
return a.fichiers.map((f) => posix.normalize(f)).filter((f) => !a.empreintes?.[f]);
|
|
255
|
+
}
|
|
256
|
+
/** Ce que `cadence spec green` fait d'une acceptation (L526) : rien si elle est verte et scellée ; sinon rejouer, puis poser `vert: true` (rouge) ou seulement les empreintes (verte, clos avant le scellement). */
|
|
257
|
+
export function modeSpecGreen(a) {
|
|
258
|
+
const sansSceau = a.vert && sansEmpreinte(a).length > 0;
|
|
259
|
+
return { deja: a.vert && !sansSceau, action: sansSceau ? PLAN_ACTIONS.specGreenScelle : PLAN_ACTIONS.specGreen, reste: sansSceau ? 'reste sans empreintes' : 'reste à vert: false', fait: sansSceau ? 'empreintes posées' : 'vert: true' };
|
|
245
260
|
}
|
|
246
261
|
/** Pose `vert: true` dans le fichier (le cycle, jamais une session) ; le commit est à faire. */
|
|
247
262
|
export function setVert(repo, path) {
|
|
@@ -2,6 +2,7 @@ import { basename, join, relative, resolve } from 'node:path';
|
|
|
2
2
|
import { lotFinished, lotKey, lotRepoPaths } from './state.js';
|
|
3
3
|
import { cadenceHome, liveWaves } from './registry.js';
|
|
4
4
|
import { REPO_LOCK, takeLock } from './lock.js';
|
|
5
|
+
import { heldRefusal } from '../local-task.js';
|
|
5
6
|
import { installPrePush } from './guard.js';
|
|
6
7
|
import { sharedStateDir } from '../state.js';
|
|
7
8
|
import { waveReadsAdds } from './snapshot.js';
|
|
@@ -116,7 +117,7 @@ export class AddFeed {
|
|
|
116
117
|
return null;
|
|
117
118
|
const l = takeLock(file, { pid: process.pid, wave: e.wave.id, started: new Date().toISOString() });
|
|
118
119
|
if (!l.ok && l.held.pid !== process.pid)
|
|
119
|
-
return
|
|
120
|
+
return heldRefusal(repo, l.held);
|
|
120
121
|
e.held.push(file);
|
|
121
122
|
fresh.push(repo);
|
|
122
123
|
if (l.ok && l.stale)
|
|
@@ -53,6 +53,15 @@ export function newsText(lot, captureDir) {
|
|
|
53
53
|
'Commit the entry with the lot (`cadence news check` must pass).',
|
|
54
54
|
].join('\n');
|
|
55
55
|
}
|
|
56
|
+
/** Consigne `{{express}}` du brief d'implémentation d'un lot express ; vide ailleurs. */
|
|
57
|
+
export const EXPRESS_BRIEF = [
|
|
58
|
+
'Express lane (touch-up of at most 0.1 day): this is the only session of the lot. There is no precheck, no edge-case probe and no separate code review: the lead reads the diff and looks at the capture. So name the file and the scope you touched in your report, keep the diff to what the goal says, and do not widen it.',
|
|
59
|
+
'No new test for a purely visual change (the existing suite runs and must stay green); a change of behaviour still gets its test first. The full suite and the build run once, at the end.',
|
|
60
|
+
].join('\n');
|
|
61
|
+
/** Consigne `{{news}}` d'un lot express `visible` : pas d'entrée Nouveautés par lot, la capture seule (le lead regroupe les entrées à la livraison). */
|
|
62
|
+
export function expressNewsText(captureDir) {
|
|
63
|
+
return `No News entry for this lot (express lane): the lead groups the entries at delivery. Do not run \`cadence news new\`. Take a capture of the result (it lands in the wave's output directory, \`${captureDir}\`): the lead looks at it instead of a separate review.`;
|
|
64
|
+
}
|
|
56
65
|
/** Consigne des briefs de correction d'un lot `visible` : où la vague range les captures Playwright (même dossier que l'étape implement). */
|
|
57
66
|
export function capturesText(captureDir) {
|
|
58
67
|
return `A Playwright capture taken with the absolute path given above lands in \`${captureDir}\` (outside the repository; this session may read it): a screenshot a finding asks for is copied from there into \`docs/nouveautes/captures/\`.`;
|
|
@@ -117,6 +126,8 @@ const FILES = {
|
|
|
117
126
|
'review-small': 'review-small.md',
|
|
118
127
|
'review-recheck': 'review-recheck.md',
|
|
119
128
|
};
|
|
129
|
+
/** Les variables dont l'absence se rend par un texte vide : le brief d'une étape qui n'en a pas l'usage ne les fournit pas. */
|
|
130
|
+
const BLANK_WHEN_ABSENT = ['repos', 'integration', 'docs', 'elsewhere', 'ack', 'spec', 'decisions', 'accept', 'maquette', 'tmp', 'workers', 'gate', 'shots', 'fiche', 'express'];
|
|
120
131
|
/** Lit tous les gabarits du dossier. Un gabarit absent est une erreur, avant que quoi que ce soit ne parte. */
|
|
121
132
|
export function loadTemplates(dir = TEMPLATES_DIR) {
|
|
122
133
|
const out = {};
|
|
@@ -137,7 +148,7 @@ export function loadTemplates(dir = TEMPLATES_DIR) {
|
|
|
137
148
|
export function renderBrief(kind, vars, source = TEMPLATES_DIR) {
|
|
138
149
|
const text = typeof source === 'string' ? loadTemplates(source)[kind] : source[kind];
|
|
139
150
|
const out = text.replace(/\{\{(\w+)\}\}/g, (_m, name) => {
|
|
140
|
-
const v = { ...vars,
|
|
151
|
+
const v = { ...vars, ...Object.fromEntries(BLANK_WHEN_ABSENT.map((k) => [k, vars[k] ?? ''])), lecture: vars.lecture ?? PLAN_READING }[name];
|
|
141
152
|
if (v === undefined)
|
|
142
153
|
throw new RafError(`gabarit ${FILES[kind]} : valeur manquante pour {{${name}}}`);
|
|
143
154
|
return v;
|
|
@@ -8,6 +8,7 @@ import { PLAN_DEFAULT, PLAN_LEGACY, planLoader, readOrchestrateConfig, readPlanC
|
|
|
8
8
|
import { PLAN_ACTIONS, commitMsgHook, leadAnswerMessage, commitMsgRefusal, planCommitMessage } from '../plan-commit.js';
|
|
9
9
|
import { isDay, toDay } from '../dates.js';
|
|
10
10
|
import { gitRoot } from '../git.js';
|
|
11
|
+
import { heldRefusal, localLotRefusal, localOverdueLines } from '../local-task.js';
|
|
11
12
|
import { onTermination } from '../proc.js';
|
|
12
13
|
import { stopApps } from './app.js';
|
|
13
14
|
import { RafError, isOpen } from '../plan.js';
|
|
@@ -39,6 +40,7 @@ import { baseBranchOf, keptLines, lotBranch, lotTreeDir, planFileSet, recoverTre
|
|
|
39
40
|
import { agentOf, schemaFor } from './schemas.js';
|
|
40
41
|
import { resolveLotRepos } from '../repos.js';
|
|
41
42
|
import { excludeState, lotFinished, lotKey, lotRepoPaths, lotSlug, neighbourDirs, newLot, RunStore } from './state.js';
|
|
43
|
+
import { isExpress } from './express.js';
|
|
42
44
|
import { linkNodeBin, nvmVersionsDir, resolveNode } from './node-env.js';
|
|
43
45
|
import { quotaText, renderDetail, renderTable, resumeCommand } from './table.js';
|
|
44
46
|
import { PACKAGE_ROOT, RESERVED_ENV, SnapshotRefusal, isSnapshotChild, resolveModulesDir, repairSnapshotModules, snapshotExists, waveReadsAnswerQueue, waveReadsControl, waveReadsIntegrate, waveReadsLotBudget, waveReadsReplay, withoutLaunchVars, spawnReexec, takeSnapshot, toolDirOf } from './snapshot.js';
|
|
@@ -274,8 +276,9 @@ export async function preflight(args, targets, io, deps, launch, opts = {}) {
|
|
|
274
276
|
refusals.push(`${key} : lot inconnu`);
|
|
275
277
|
continue;
|
|
276
278
|
}
|
|
277
|
-
|
|
278
|
-
|
|
279
|
+
const shut = !isOpen(lot.status) ? `le lot est ${lot.status}` : localLotRefusal(lot); // un lot local (L519) est au lead, jamais à une vague
|
|
280
|
+
if (shut) {
|
|
281
|
+
refusals.push(`${key} : ${shut}`);
|
|
279
282
|
continue;
|
|
280
283
|
}
|
|
281
284
|
if (plan.readonly && lot.status === 'todo' && !env.config.start) {
|
|
@@ -322,6 +325,8 @@ export async function preflight(args, targets, io, deps, launch, opts = {}) {
|
|
|
322
325
|
if (neighbours.repos.length)
|
|
323
326
|
state.repos = neighbours.repos;
|
|
324
327
|
state.estimate = lot.estimate;
|
|
328
|
+
if (isExpress({ estimate: lot.estimate, screen, specLot: state.specLot }))
|
|
329
|
+
state.express = true;
|
|
325
330
|
state.light = lot.estimate <= env.config.review.threshold;
|
|
326
331
|
const escalating = !!env.config.escalation;
|
|
327
332
|
state.budget = lotBudget(lot.estimate, screen ? 'screen' : lot.spec ? 'spec' : undefined, escalating);
|
|
@@ -334,9 +339,7 @@ export async function preflight(args, targets, io, deps, launch, opts = {}) {
|
|
|
334
339
|
}
|
|
335
340
|
return { refusals, lots };
|
|
336
341
|
}
|
|
337
|
-
|
|
338
|
-
return readAgents(deps.agentsDir);
|
|
339
|
-
}
|
|
342
|
+
const agentsOf = (deps) => readAgents(deps.agentsDir);
|
|
340
343
|
const WAVE_ID_RE = /^[\w.-]+$/;
|
|
341
344
|
/** Identifiant par défaut : jour et minute de lancement ; le suffixe qui départage deux vagues vient de `RunStore.reserve`. */
|
|
342
345
|
function defaultWaveBase(io) {
|
|
@@ -463,23 +466,26 @@ function dryRun(lots, io, deps, budget, id) {
|
|
|
463
466
|
steps.unshift({ kind: 'fiche', model: 'sonnet' });
|
|
464
467
|
if (specStage)
|
|
465
468
|
steps.unshift({ kind: 'spec', model: 'sonnet' });
|
|
466
|
-
if (needsPrecheck(env.config, env.loadPlan(), l.repo, l.lot, l.repos, l.goal)) {
|
|
469
|
+
if (!l.express && needsPrecheck(env.config, env.loadPlan(), l.repo, l.lot, l.repos, l.goal)) {
|
|
467
470
|
const nothingToFind = precheckNothingToFind(env.loadPlan(), l.repo, l.lot, l.repos, l.goal);
|
|
468
471
|
if (nothingToFind)
|
|
469
472
|
io.out(` contrôle préalable sauté : ${nothingToFind}`);
|
|
470
473
|
else
|
|
471
474
|
steps.unshift({ kind: 'precheck', model: env.config.precheckLocal ? 'local' : 'sonnet' });
|
|
472
475
|
}
|
|
473
|
-
steps.push(...(env.config.probe ? [{ kind: 'probe', model: 'sonnet' }] : [])); // L497 : entre l'écriture et la revue
|
|
476
|
+
steps.push(...(env.config.probe && !l.express ? [{ kind: 'probe', model: 'sonnet' }] : [])); // L497 : entre l'écriture et la revue
|
|
474
477
|
const { light, full } = env.config.review;
|
|
475
|
-
if (l.
|
|
478
|
+
if (l.express) {
|
|
479
|
+
// L519 : une seule session, le lead lit le diff
|
|
480
|
+
}
|
|
481
|
+
else if (l.small)
|
|
476
482
|
steps.push({ kind: l.visible ? 'review-small' : 'review', model: l.light ? light : full });
|
|
477
483
|
else {
|
|
478
484
|
steps.push({ kind: 'review', model: l.light ? light : full });
|
|
479
485
|
if (l.visible && env.config.ux)
|
|
480
486
|
steps.push({ kind: 'ux', model: full }); // L221 : l'UX est jouée une fois, après la revue de code conforme
|
|
481
487
|
}
|
|
482
|
-
io.out(` étapes : ${steps.map((s) => `${s.kind} (${s.model})`).join(' → ')}${l.small ? ' — petit lot' : ''}${l.light ? ` — revue légère (${light}, pas de passe des mineurs ; un bloquant ou un majeur → correction puis revue ${full})` : ''}${l.visible && !env.config.ux ? ' — UX à faire par le lead (orchestrate.ux absent)' : ''}`);
|
|
488
|
+
io.out(` étapes : ${steps.map((s) => `${s.kind} (${s.model})`).join(' → ')}${l.small ? ' — petit lot' : ''}${l.express ? ' — voie express (pas de précheck, de sonde ni de revue séparée : le lead lit le diff)' : ''}${l.light && !l.express ? ` — revue légère (${light}, pas de passe des mineurs ; un bloquant ou un majeur → correction puis revue ${full})` : ''}${l.visible && !env.config.ux ? ' — UX à faire par le lead (orchestrate.ux absent)' : ''}`);
|
|
483
489
|
for (const m of new Set(steps.map((s) => s.model))) {
|
|
484
490
|
if (m !== 'local' && !MODEL_ALIASES.includes(m))
|
|
485
491
|
io.out(` ⚠ modèle « ${m} » hors des alias ${MODEL_ALIASES.join(', ')} : passé tel quel à claude --model (un nom que le compte ne connaît pas fait échouer la session)`);
|
|
@@ -592,7 +598,7 @@ const ANSWER_POLL_MS = 2000;
|
|
|
592
598
|
function statusView(args, launch, cap, now) {
|
|
593
599
|
const id = statusWave(args);
|
|
594
600
|
const store = id !== undefined ? RunStore.find(launch, id) : RunStore.last(launch);
|
|
595
|
-
const live = [...liveLines(cap, now), ...expiredHoldLines(now)];
|
|
601
|
+
const live = [...liveLines(cap, now), ...expiredHoldLines(now), ...localOverdueLines((isLeadProject(launch) ? [launch] : findProjects(launch)).map((d) => gitRoot(d) ?? d), now)];
|
|
596
602
|
const waveLive = liveWaves(cadenceHome()).length > 0;
|
|
597
603
|
if (!store && live.length && id === undefined)
|
|
598
604
|
return { live, running: waveLive };
|
|
@@ -1082,7 +1088,7 @@ async function execute(wave, lots, store, io, deps, today, cap, all = lots) {
|
|
|
1082
1088
|
const l = takeLock(file, { pid: process.pid, wave: wave.id, started: new Date().toISOString() });
|
|
1083
1089
|
// Le verrou déjà à ce pid : pris par la reprise qui a attendu l'autre vague (L443).
|
|
1084
1090
|
if (!l.ok && l.held.pid !== process.pid) {
|
|
1085
|
-
io.err(`orchestrate : ${
|
|
1091
|
+
io.err(`orchestrate : ${heldRefusal(r, l.held)}`);
|
|
1086
1092
|
release();
|
|
1087
1093
|
return 2;
|
|
1088
1094
|
}
|
|
@@ -1653,7 +1659,7 @@ async function resume(args, argv, io, deps, launch, today) {
|
|
|
1653
1659
|
}
|
|
1654
1660
|
// Un refus déjà posé sur un dépôt précédent (même vague, attente échue) : les suivants ne font plus attendre, la reprise sort en 2.
|
|
1655
1661
|
if (!waitFor || refusals.length || Date.now() >= deadline) {
|
|
1656
|
-
refusals.push(
|
|
1662
|
+
refusals.push(heldRefusal(repo, busy));
|
|
1657
1663
|
break;
|
|
1658
1664
|
}
|
|
1659
1665
|
if (!waiting) {
|
|
@@ -24,7 +24,7 @@ import { TEST_WORKERS_ENV, testWorkersText } from './workers.js';
|
|
|
24
24
|
import { baselineCandidates, gateLabel, gateReplayable, gateUnnamed, redAloneGate, redCandidates, replayGate, runGate } from './gate-parts.js';
|
|
25
25
|
import { withPlace } from './gate-place.js';
|
|
26
26
|
import { APP_TIMEOUT_S, startApp } from './app.js';
|
|
27
|
-
import { capturesText, gateText, headline, integrationText, localReading, precheckBrief, newsText, objective, renderBrief, reposText, shotsText, TEMPLATES_DIR, tiersText, tmpText } from './briefs.js';
|
|
27
|
+
import { capturesText, EXPRESS_BRIEF, expressNewsText, gateText, headline, integrationText, localReading, precheckBrief, newsText, objective, renderBrief, reposText, shotsText, TEMPLATES_DIR, tiersText, tmpText } from './briefs.js';
|
|
28
28
|
import { SlotAbandoned } from './registry.js';
|
|
29
29
|
import { journalTokens, peakContext, mcpServersFor, playwrightDir, runSession, trackGroup, writeMcpConfig, enabledPlugins, settingsFiles } from './launch.js';
|
|
30
30
|
import { cleanPlaywrightOutput, pushed, remoteAdvances, snapshot } from './guard.js';
|
|
@@ -39,7 +39,7 @@ import { bulletLines, maquetteText, reviewedText } from './maquette.js';
|
|
|
39
39
|
import { answeredText, checkSpec, costlyHypotheses, ficheHasBack, ficheReady, findFiche, hypothesisLines, answerAltersRule, hypothesisNote, LEAD_SOURCE, ruleIdsSourcedByLead, sourceRules, specText } from './spec.js';
|
|
40
40
|
import { handBackWithoutFinding } from './no-finding.js';
|
|
41
41
|
import { decisionsText, humanDecisions } from './decisions.js';
|
|
42
|
-
import { LOCAL_PRECHECK_UNREADABLE, PRECHECK_UNREADABLE, lotKey, lotRepoPaths, neighbourDirs } from './state.js';
|
|
42
|
+
import { LOCAL_PRECHECK_UNREADABLE, PRECHECK_UNREADABLE, expressSummary, lotKey, lotRepoPaths, neighbourDirs } from './state.js';
|
|
43
43
|
import { advance, baseMoved, INTEGRATE_WAIT_MS, MAX_REPLAYS, mergeBase, patchId, rebaseOnto, repoBranch, takeRepoWrite } from './integrate.js';
|
|
44
44
|
import { mutexFor } from './mutex.js';
|
|
45
45
|
import { readTier, stillThird, takeTiers, tierMoved, tierRepos, wipBranch } from './tiers.js';
|
|
@@ -517,10 +517,10 @@ function briefFor(c, kind, local = false) {
|
|
|
517
517
|
const l = c.lot;
|
|
518
518
|
const lot = plan.lot(l.lot);
|
|
519
519
|
const tmpDir = c.wave.store.tmpDir(l.project, l.lot);
|
|
520
|
-
const vars = { chemin: wd(c), lot: l.lot, titre: headline(lot, l.goal), objectif: objective(lot, l.goal), commits: '', reponse: '', constats: '', spec: '', ux: uxText(c), choix: choixText(c.lot.choix ?? []), checks: kind === 'review' || kind === 'review-small' ? checksText(c) : '', docs: kind === 'review' || kind === 'review-small' ? docsText(c) : '', news: '', captures: '', tmp: tmpText(tmpDir), workers: writesCode(kind) ? workersText(c) : '', gate: writesCode(kind) ? gateText(c.config) : '', repos: reposText(kind === 'implement' || kind === 'fix' ? 'write' : 'read', l.lot, l.repos ?? []), integration: c.integration ? integrationText(c.integration.base) : '' };
|
|
520
|
+
const vars = { chemin: wd(c), lot: l.lot, titre: headline(lot, l.goal), objectif: objective(lot, l.goal), commits: '', reponse: '', constats: '', spec: '', ux: uxText(c), choix: choixText(c.lot.choix ?? []), checks: kind === 'review' || kind === 'review-small' ? checksText(c) : '', docs: kind === 'review' || kind === 'review-small' ? docsText(c) : '', news: '', captures: '', express: kind === 'implement' && l.express ? EXPRESS_BRIEF : '', tmp: tmpText(tmpDir), workers: writesCode(kind) ? workersText(c) : '', gate: writesCode(kind) ? gateText(c.config) : '', repos: reposText(kind === 'implement' || kind === 'fix' ? 'write' : 'read', l.lot, l.repos ?? []), integration: c.integration ? integrationText(c.integration.base) : '' };
|
|
521
521
|
if (l.visible) {
|
|
522
522
|
const pwDir = playwrightDir(c.wave.store.lotDir(l.project, l.lot));
|
|
523
|
-
vars.news = newsText(l.lot, pwDir);
|
|
523
|
+
vars.news = l.express ? expressNewsText(pwDir) : newsText(l.lot, pwDir);
|
|
524
524
|
vars.captures = capturesText(pwDir);
|
|
525
525
|
}
|
|
526
526
|
if (mcpServersFor(kind, l.visible, '', l.small, !!l.screen).playwright)
|
|
@@ -1411,7 +1411,7 @@ async function precheck(c) {
|
|
|
1411
1411
|
/** Le lot passe-t-il par le contrôle préalable ? Un lot qui n'a rien à retrouver (L322) saute l'étape, et le journal du lot le dit. */
|
|
1412
1412
|
function precheckOrSkip(c) {
|
|
1413
1413
|
const plan = c.loadPlan();
|
|
1414
|
-
if (!needsPrecheck(c.config, plan, c.lot.repo, c.lot.lot, c.lot.repos, c.lot.goal))
|
|
1414
|
+
if (c.lot.express || !needsPrecheck(c.config, plan, c.lot.repo, c.lot.lot, c.lot.repos, c.lot.goal))
|
|
1415
1415
|
return false;
|
|
1416
1416
|
const nothing = precheckNothingToFind(histPlan(c), wd(c), c.lot.lot, c.lot.repos, c.lot.goal);
|
|
1417
1417
|
if (nothing)
|
|
@@ -2293,7 +2293,7 @@ async function work(c, kind) {
|
|
|
2293
2293
|
}
|
|
2294
2294
|
// Porte « code modifié sans test » (L287) : le diff du lot (son dépôt et ses voisins, fichiers supprimés exclus), toutes sessions comprises, touche du code sans fichier de test → correction directe, sans session de revue. Une seule fois par lot, jamais pendant la passe des mineurs, ni quand la correction prendrait la dernière passe (la revue n'en aurait plus), ni quand le budget du lot ne paie plus la correction (la revue, elle, part toujours).
|
|
2295
2295
|
const gate = c.config.testGate;
|
|
2296
|
-
if (gate && !l.testGate && !minorsPass) {
|
|
2296
|
+
if (gate && !l.testGate && !minorsPass && !l.express) {
|
|
2297
2297
|
// Le diff du lot : son dépôt et ses dépôts voisins (comme hasWork), sans les fichiers que le lot supprime (du code mort supprimé n'a pas de test à écrire).
|
|
2298
2298
|
const touched = new Set([
|
|
2299
2299
|
...filesOf(wd(c), lotWork(plan, wd(c), l.lot), { withoutDeleted: true }),
|
|
@@ -2341,6 +2341,9 @@ async function toReview(c, runs, minorsPass) {
|
|
|
2341
2341
|
}
|
|
2342
2342
|
}
|
|
2343
2343
|
l.constats = [];
|
|
2344
|
+
// Voie express (L519) : pas de revue séparée, le lead lit le diff ; le lot conclut sur le HEAD que la porte du programme vient de jouer.
|
|
2345
|
+
if (l.express)
|
|
2346
|
+
return conclude(c, expressSummary(git(wd(c), 'rev-parse', 'HEAD')));
|
|
2344
2347
|
l.checks = runs.length ? { head: git(wd(c), 'rev-parse', 'HEAD'), tree: git(wd(c), 'rev-parse', 'HEAD^{tree}'), runs } : undefined;
|
|
2345
2348
|
l.next = firstReview(c);
|
|
2346
2349
|
save(c);
|
|
@@ -3,6 +3,7 @@ import { sharedStateDir } from '../state.js';
|
|
|
3
3
|
import { addHint } from './ajout.js';
|
|
4
4
|
import { canInstallPrePush, gitProcessRunning, indexLockRefusal, snapshot } from './guard.js';
|
|
5
5
|
import { activeLock, REPO_LOCK } from './lock.js';
|
|
6
|
+
import { heldRefusal } from '../local-task.js';
|
|
6
7
|
/** Les fichiers suivis modifiés d'un dépôt. */
|
|
7
8
|
export async function trackedDirty(repo) {
|
|
8
9
|
return (await snapshot(repo, { remote: false })).tracked.map((l) => l.slice(3));
|
|
@@ -28,6 +29,6 @@ export async function repoRefusals(repo, t, run, opts) {
|
|
|
28
29
|
out.push(`${name} : ${hook}`);
|
|
29
30
|
}
|
|
30
31
|
if (busy && !opts.resume && !ours)
|
|
31
|
-
out.push(`${
|
|
32
|
+
out.push(`${heldRefusal(repo, busy)}${addHint(t, busy.wave, run.launch)}`);
|
|
32
33
|
return out;
|
|
33
34
|
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** Estimation, en jours, au plus de laquelle un lot est une retouche : l'étude L517 (levier 2) mesure 15,5 min et 116 k jetons pour un tel lot, dont 44 % en revue et préparation. */
|
|
2
|
+
export const EXPRESS_MAX_ESTIMATE = 0.1;
|
|
3
|
+
/**
|
|
4
|
+
* Voie express (L519) : une retouche (estimate ≤ 0,1 j) sans écran ni spec se joue en une seule session d'écriture, sans contrôle préalable, sans sonde
|
|
5
|
+
* ni revue séparée (le lead lit le diff), sans porte « code sans test » ni entrée Nouveautés par lot. Une estimation absente, nulle ou illisible n'en est pas une.
|
|
6
|
+
*/
|
|
7
|
+
export function isExpress(lot) {
|
|
8
|
+
const e = lot.estimate;
|
|
9
|
+
return typeof e === 'number' && e > 0 && e <= EXPRESS_MAX_ESTIMATE && !lot.screen && !lot.specLot;
|
|
10
|
+
}
|
package/dist/orchestrate/lock.js
CHANGED
|
@@ -16,6 +16,8 @@ export function holderAlive(pid, start) {
|
|
|
16
16
|
}
|
|
17
17
|
/** Nom du verrou d'orchestration d'un dépôt, dans l'état commun aux worktrees (celui de `deliver`). */
|
|
18
18
|
export const REPO_LOCK = 'orchestrate.lock';
|
|
19
|
+
/** Un champ facultatif du verrou, tel qu'écrit ; absent : absent. */
|
|
20
|
+
const optional = (v) => (v === undefined ? undefined : String(v));
|
|
19
21
|
function read(file) {
|
|
20
22
|
let text;
|
|
21
23
|
let ageMs;
|
|
@@ -28,13 +30,15 @@ function read(file) {
|
|
|
28
30
|
}
|
|
29
31
|
try {
|
|
30
32
|
const raw = JSON.parse(text);
|
|
31
|
-
return { pid: Number(raw.pid), wave: String(raw.wave), started: String(raw.started), start: raw.start
|
|
33
|
+
return { pid: Number(raw.pid), wave: String(raw.wave), started: String(raw.started), start: optional(raw.start), until: optional(raw.until), ageMs };
|
|
32
34
|
}
|
|
33
35
|
catch {
|
|
34
36
|
return { pid: 0, wave: '?', started: '?', unreadable: true, ageMs };
|
|
35
37
|
}
|
|
36
38
|
}
|
|
37
|
-
const alive = (l) => (l.unreadable ? l.ageMs < 5_000 : holderAlive(l.pid, l.start));
|
|
39
|
+
const alive = (l) => (l.unreadable ? l.ageMs < 5_000 : l.until !== undefined || holderAlive(l.pid, l.start));
|
|
40
|
+
/** Un verrou déclaré dont l'échéance est passée à `now` ; faux pour un verrou de processus (il n'a pas d'échéance). */
|
|
41
|
+
export const lockOverdue = (l, now) => l.until !== undefined && Date.parse(l.until) <= now.getTime();
|
|
38
42
|
/** Le verrou s'il est tenu par un processus vivant, sinon null (absent ou périmé). */
|
|
39
43
|
export function activeLock(file) {
|
|
40
44
|
const l = read(file);
|
|
@@ -75,7 +79,7 @@ export function takeLock(file, given) {
|
|
|
75
79
|
}
|
|
76
80
|
const now = read(aside);
|
|
77
81
|
if (now && now.pid === seen.pid && now.wave === seen.wave && now.started === seen.started && now.start === seen.start) {
|
|
78
|
-
stale = { pid: seen.pid, wave: seen.wave, started: seen.started, start: seen.start };
|
|
82
|
+
stale = { pid: seen.pid, wave: seen.wave, started: seen.started, start: seen.start, until: seen.until };
|
|
79
83
|
rmSync(aside, { force: true });
|
|
80
84
|
}
|
|
81
85
|
else {
|
|
@@ -15,7 +15,7 @@ const PROBE_FINDING = 'sonde de cas limites : ';
|
|
|
15
15
|
* Sans la clé `orchestrate.probe`, rien : ni champ ni warning.
|
|
16
16
|
*/
|
|
17
17
|
export function scheduleProbe(config, l, o) {
|
|
18
|
-
if (!config.probe || l.probe || o.skip)
|
|
18
|
+
if (!config.probe || l.probe || o.skip || l.express)
|
|
19
19
|
return false;
|
|
20
20
|
l.probe = true;
|
|
21
21
|
if (!o.late && o.pays) {
|
|
@@ -8,6 +8,8 @@ import { pidAlive } from '../state.js';
|
|
|
8
8
|
/** Avertissement d'un rapport du précheck local illisible : l'étape reste `ok`, le lot se replie sur Sonnet (L322, lu par `cadence perf` pour compter l'échec du local). */
|
|
9
9
|
export const LOCAL_PRECHECK_UNREADABLE = 'contrôle préalable : rapport du modèle local illisible';
|
|
10
10
|
export const PRECHECK_UNREADABLE = 'contrôle préalable illisible, implémentation lancée';
|
|
11
|
+
/** Ce que la voie express (L519) enregistre à la place d'une revue : conforme sans constat, la lecture du diff étant celle du lead. `head` : le HEAD que la porte du programme vient de jouer. */
|
|
12
|
+
export const expressSummary = (head) => ({ conforme: true, bloquants: 0, majeurs: 0, mineurs: 0, verdict: 'conforme : voie express (retouche de 0,1 j au plus), pas de revue séparée — le lead lit le diff et regarde la capture', sousTaches: [], nonVerifie: [], head });
|
|
11
13
|
export function newLot(o) {
|
|
12
14
|
return { ...o, status: 'queued', pass: 0, next: null, steps: [], constats: [], verdict: null, uxVerdict: null, questions: [], choix: [], answers: [], proposals: [], warnings: [], instables: undefined, delaisJustes: undefined, outcome: null };
|
|
13
15
|
}
|
package/dist/plan-commit.js
CHANGED
|
@@ -29,6 +29,8 @@ export const PLAN_ACTIONS = {
|
|
|
29
29
|
articleNoted: 'article à rafraîchir : titres livrés notés',
|
|
30
30
|
/** `spec green` (L366) : jamais la liste des tests rescellés, elle va dans le corps du commit (header-max-length de commitlint). */
|
|
31
31
|
specGreen: 'acceptation verte, vert: true posé par cadence spec green',
|
|
32
|
+
/** `cadence spec green` sur un lot déjà `vert: true` mais sans empreintes (L526). */
|
|
33
|
+
specGreenScelle: 'empreintes de l\'acceptation posées par cadence spec green',
|
|
32
34
|
resealed: (norm) => `test d'acceptation rescellé (${norm})`,
|
|
33
35
|
/** En minuscule : `subject-case` de config-conventional refuse une majuscule initiale (L343). */
|
|
34
36
|
leadAnswer: (ids) => `${ids.length > 1 ? 'règles' : 'règle'} ${ids.join(', ')} ${ids.length > 1 ? 'sourcées' : 'sourcée'} par la réponse du lead`,
|
package/dist/plan.js
CHANGED
|
@@ -38,6 +38,7 @@ function cleanUses(names) {
|
|
|
38
38
|
throw new RafError(invalidResource(n));
|
|
39
39
|
return [...new Set(names)];
|
|
40
40
|
}
|
|
41
|
+
const trueFlags = (flags) => Object.fromEntries(Object.entries(flags).filter(([, on]) => on).map(([k]) => [k, true])); // les drapeaux vrais, chacun à `true` : les absents et les faux ne s'écrivent pas
|
|
41
42
|
export const FIELDS = ['id', 'title', 'status', 'estimate', 'quickwin', 'visible', 'screen', 'spec', 'public', 'every', 'last', 'after', 'created', 'started', 'finished', 'notes', 'repos', 'parent', 'gate', 'goal'];
|
|
42
43
|
/** Nature de l'écran d'un lot visible : nouvelle page, nouveau composant, parcours modifié. */
|
|
43
44
|
export const SCREENS = ['page', 'component', 'journey'];
|
|
@@ -562,15 +563,10 @@ export class Plan {
|
|
|
562
563
|
const entry = { id, title };
|
|
563
564
|
if (pub !== undefined)
|
|
564
565
|
entry.public = pub;
|
|
565
|
-
Object.assign(entry, { status: 'todo', estimate: opts.estimate ?? 1 });
|
|
566
|
-
if (opts.quickwin)
|
|
567
|
-
entry.quickwin = true;
|
|
568
|
-
if (opts.visible)
|
|
569
|
-
entry.visible = true;
|
|
566
|
+
Object.assign(entry, { status: 'todo', estimate: opts.estimate ?? 1 }, trueFlags({ quickwin: opts.quickwin, visible: opts.visible }));
|
|
570
567
|
if (opts.screen)
|
|
571
568
|
entry.screen = opts.screen;
|
|
572
|
-
|
|
573
|
-
entry.spec = true;
|
|
569
|
+
Object.assign(entry, trueFlags({ spec: opts.spec, local: opts.local }));
|
|
574
570
|
if (opts.every !== undefined)
|
|
575
571
|
entry.every = opts.every;
|
|
576
572
|
if (opts.after?.length)
|
|
@@ -1070,7 +1066,7 @@ function normalizeLot(raw, problems = []) {
|
|
|
1070
1066
|
quickwin: raw.quickwin === true,
|
|
1071
1067
|
visible: raw.visible === true,
|
|
1072
1068
|
...(screen ? { screen } : {}),
|
|
1073
|
-
...(spec ? { spec } : {}),
|
|
1069
|
+
...(spec ? { spec } : {}), ...(raw.local === true ? { local: true } : {}),
|
|
1074
1070
|
...(gate ? { gate } : {}),
|
|
1075
1071
|
...(typeof raw.public === 'string' && raw.public.trim() !== '' ? { public: raw.public.trim() } : {}),
|
|
1076
1072
|
...(typeof raw.goal === 'string' && raw.goal.trim() !== '' ? { goal: raw.goal.trim() } : {}),
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { statSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { gitRoot } from './git.js';
|
|
4
|
+
import { activeLock, REPO_LOCK } from './orchestrate/lock.js';
|
|
5
|
+
import { INTEGRATION_LOCK } from './orchestrate/integrate.js';
|
|
6
|
+
import { mainTreeWorkers } from './orchestrate/main-tree.js';
|
|
7
|
+
import { cadenceHome, openLotsIn } from './orchestrate/registry.js';
|
|
8
|
+
import { sharedStateDir } from './state.js';
|
|
9
|
+
import { heldRefusal, localLotOf } from './local-task.js';
|
|
10
|
+
import { RafError } from './plan.js';
|
|
11
|
+
// Ce que `raf`, `deliver` et `spec` lisent du verrou du dépôt : une orchestration (ou une tâche locale du lead, L519) qui le tient, ou une intégration en cours.
|
|
12
|
+
/**
|
|
13
|
+
* Avertit (stderr, sans refuser) quand une intégration du dépôt est en cours (L425) : le plan écrit dans l'arbre principal est un fichier suivi modifié, et
|
|
14
|
+
* l'intégration en cours le voit et refuse son lot (« arbre du dépôt modifié »). Seul l'arbre principal est concerné : dans le worktree d'un lot, l'écriture
|
|
15
|
+
* ne change pas l'arbre que l'intégration contrôle.
|
|
16
|
+
*/
|
|
17
|
+
export function warnIntegrating(root, err) {
|
|
18
|
+
const top = gitRoot(root);
|
|
19
|
+
if (!top || !statSync(join(top, '.git')).isDirectory())
|
|
20
|
+
return;
|
|
21
|
+
const holder = activeLock(join(sharedStateDir(root), INTEGRATION_LOCK));
|
|
22
|
+
if (holder)
|
|
23
|
+
err(`avertissement : intégration en cours (vague ${holder.wave}, pid ${holder.pid}) — ce plan est un fichier suivi modifié de l'arbre principal, l'intégration en cours peut refuser son lot (arbre du dépôt modifié) : committer le plan sans attendre`);
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Avertit (stderr, sans refuser) quand une session de vague écrit dans l'arbre principal du dépôt (L475) : un lot en implémentation, revue ou correction sans worktree.
|
|
27
|
+
* Un commit de plan du lead à ce moment-là rend le lot (cas du 30-10 : cadence L458 rendu par le commit du plan). Sans effet dans le worktree d'un lot, ou depuis une session de la vague (c'est elle qui tient l'arbre).
|
|
28
|
+
*/
|
|
29
|
+
export function warnWaveWorking(root, env, err) {
|
|
30
|
+
if (env.CADENCE_ORCHESTRATED)
|
|
31
|
+
return;
|
|
32
|
+
const top = gitRoot(root);
|
|
33
|
+
if (!top || !statSync(join(top, '.git')).isDirectory())
|
|
34
|
+
return;
|
|
35
|
+
const holder = activeLock(join(sharedStateDir(root), REPO_LOCK));
|
|
36
|
+
if (!holder)
|
|
37
|
+
return;
|
|
38
|
+
const working = mainTreeWorkers(cadenceHome(), holder.pid, top);
|
|
39
|
+
if (working?.length)
|
|
40
|
+
err(`avertissement : la vague ${holder.wave} (pid ${holder.pid}) travaille dans l'arbre principal de ce dépôt (${working.join(', ')}) — un commit de plan maintenant rendrait ces lots à la revue ; attendre la fin de leur étape, ou écrire le plan sans le committer`);
|
|
41
|
+
}
|
|
42
|
+
/** La vague qui tient le verrou du dépôt tant qu'un de ses lots y travaille encore (état illisible : on la tient pour active) ; null si le dépôt est libre. Une tâche locale du lead (L519) la tient toujours (elle n'a pas de lots de vague à lire). Avec `refusing` (le nom de la commande), une tâche locale refuse la commande d'un `RafError` qui la nomme et dit comment la finir, sans lire de vague ni conseiller `--sha` : `deliver`, `raf move`, `spec reseal` et `spec green`. Partagé par `deliver`, `raf move`, le hook pre-push, `spec reseal` et `spec green`. */
|
|
43
|
+
export function waveHolding(root, refusing) {
|
|
44
|
+
const orchestrating = activeLock(join(sharedStateDir(root), REPO_LOCK));
|
|
45
|
+
if (!orchestrating)
|
|
46
|
+
return null;
|
|
47
|
+
if (refusing && localLotOf(orchestrating.wave))
|
|
48
|
+
throw new RafError(`${refusing} : ${heldRefusal(root, orchestrating)}`);
|
|
49
|
+
const stillOpen = openLotsIn(cadenceHome(), orchestrating.pid, root);
|
|
50
|
+
return stillOpen === null || stillOpen.length > 0 ? orchestrating : null;
|
|
51
|
+
}
|
package/docs/orchestrate.md
CHANGED
|
@@ -407,6 +407,7 @@ session, `accept` (L173): `precheck → spec → maquette → accept → impleme
|
|
|
407
407
|
file, deletes it, creates one outside the accept step, or modifies a locked test hands the lot back (`verrou des tests d'acceptation : …`) — for a modified test the message ends with `; premier commit fautif : <sha7> <subject>`, the oldest commit after the reference (red commit, or the head before the lot) that touched the sealed path (a deletion or a rename included, renames not followed), and names no commit when the deviation is only in the working tree (L231) — a useful change is a question to the lead or a review finding, never a silent gesture. The lock does not depend on `orchestrate.accept` being declared. **Exception — a delivered lot (`vert: true`):** a later lot that makes the rule evolve may modify the listed tests of that acceptation when **a commit that cites the lot in progress amends the sheet of the screen and comes after the red commit** (a strict descendant of it; the commit may have been made before the wave, and once one exists the exception covers the whole acceptation: the tests may change in the same commit as the sheet, in a later one or in a following session, a `fix` after the `implement` for instance, L231): the sheet is what evolves the rules, and tests and sheet change together. A sheet amended by another lot, by a commit that cites no lot, by a commit that is not after the red commit, or only in the working tree (not committed) opens nothing. The exception never covers the acceptation of the lot in progress (its `vert: true` is set right after the implement): that one keeps the whole lock. Without a sheet change the difference is still a deviation — except one narrow case (L184): a listed file whose content is identical to what it was at the head before the lot's first **work** commit (commits found by the plan's refs) is not a deviation, the lot having taken its own addition back (a lot handed back for writing in a locked file and resumed in a new wave, which then removes that addition). This applies to every acceptation, `vert: false` included, but only to a file that **exists** at that head and now: a file absent at that head (e.g. created by the lot's own red commit) or deleted since is never exempted, so deleting a locked test stays a deviation; nor when the sheet of the screen has changed since that head (an evolution admitted for another lot: going back to the old content would cancel it, not the lot's addition); apart from that, with `vert: false` the whole lock applies, sheet change or not; `acceptation.json` itself stays locked (only its `vert` key moves).
|
|
408
408
|
- **A sealed test that contradicts the sheet: `cadence spec reseal` (L200).** The lock is total for the lot in progress (the delivered-lot exception above never covers it; for a delivered lot, see the end of this item), so a test that is wrong against its sheet — e.g. the regex `/\d{1,2} /` of an acceptance test rejecting « T3 2027 annoncée », which the sheet allows — would leave the lot handed back with no way out. The way out is the lead's, outside any wave: correct the test in the working tree (the sheet is left as it is: unchanged since the red commit, or amended by commits of the lot and committed, see « A sheet amended in the same lot » below), then `cadence spec reseal <lot> <test> --reason "why the test contradicts the sheet"`. The program checks that the lot is in progress (`doing`; for a delivered lot, see the end of this item), that the test is listed in the lot's sealed `acceptation-<lot>.json`, that the sheet is unchanged since the red commit or amended by commits of the lot (below), that **no other listed test** changed (the lock keeps everything else) and that the test did change; it then redoes the fingerprint of that one test, appends `{ test, raison, date, avant, apres }` (plus `ficheAmendee` for an amended sheet, below) to the `resceaux` key of `acceptation.json`, commits the test and `acceptation.json` (`test(L35): acceptation rescellée, <test> : <reason>`, the new red commit) and the lot's note with the reason (`plan: …`). It is refused during an orchestrated wave (`CADENCE_ORCHESTRATED`, or the repository's wave lock while one of its lots still works, like `cadence deliver`), **while a wave that is not `done` (interrupted, suspended, or still alive with a lot waiting on a question) keeps open any lot of this repository that is still `doing` in this repository's plan (L238)** — the resealed lot, the `--porteur` or any other lot of the repository: the lock hands back every open lot of the repository, so a reseal committed in the meantime was handed back to any of them as the « first offending commit » of the lock (ol-companion L172, then L173). Open means any non-final state in the wave (a question waiting for its `--answer`, a lot suspended by the quota or the budget, a queued lot, a session running): a stopped wave holds no lock, but its `--resume` replays those lots. The filter is by repository (the repository of the lot's project, not its neighbour repositories: lot ids belong to each plan, so a finance-tracker L172 never blocks an ol-companion L172) and by plan: a lot that is `done` or absent in the plan never refuses, so an old interrupted wave whose lots are long delivered blocks nothing, and neither does a `done` wave. The error is a single message that names **every** wave concerned, with its launch folder and its open lots, the wave that holds the repository lock included (alive: while it has an open lot the lock refuses first, so the same message lists it together with the stopped waves of the repository, not the bare « orchestration en cours » line `cadence deliver` gives), and gives for each stopped wave ONE command, runnable as it stands, with a `--drop` per open lot of the repository in that wave, the queued ones that are not `doing` in the plan included (L239: `--resume` would relaunch them, and the lot would take the lock back and find the tree dirty with the corrected test): `cd <launch folder> && cadence orchestrate --resume --wave <wave> --drop <project>:<lot> --drop <project>:<lot>` (`--resume` replays the whole wave except the dropped lots, so one command per lot would start sessions on the others; it looks for the wave in the folder it is run from only); a wave that is still alive is not called suspended, the error says it is running and is to be waited for. A repository reached by another path (a worktree, a symbolic link) is the same repository: the `.git` directories are compared by their real path. The waves read are those of `.cadence/runs/` of the repository root, of the plan's folder and of every folder above them (a wave keeps its state where `cadence orchestrate` was launched, usually the parent folder of the projects), with no `--reason`, and on a lot that is neither in progress nor delivered with a green acceptation. Then, depending on how the lot stands: a lot waiting on a `question` sits in a wave that is not `done`, which refuses the reseal (L238): answer it first (`--resume --wave <wave> --answer`, the lot then goes on or is handed back) or close it (`--resume --wave <wave> --drop`), reseal afterwards if the lot is handed back (a rewrite of `acceptation.json` without a `resceaux` entry stays a violation of the lock); a lot handed back with `verrou des tests d'acceptation` is finished for `--resume`, so it is replayed by a new wave `cadence orchestrate <project>:<lot>`, which starts again from the red commit. A session never does it, three guards: (1) `cadence spec reseal` is in the sessions' `--disallowedTools` (like `raf done`/`review`/`ux`); (2) a resealed commit is adopted as the new red commit only if it belongs to **no session of the wave** (commits listed in each `steps[].commits`, or in `headBefore..headAfter` of any session of any lot of the wave) — otherwise it stays a violation of the lock; (3) the lock is checked at the end of **every** work session, a session that ends with a question included, so a session that replaces the test, forges the fingerprint, commits and asks a question is handed back with `verrou des tests d'acceptation` without waiting for the lead's `--answer`. The reseal is atomic: if the commit fails (a `pre-commit` hook exiting 1, say) `acceptation.json` and the test are put back as they were before the command (the lead's correction in the working tree is kept, nothing staged) and a readable error says nothing was resealed. With a plan kept read-only by another tool, the reseal itself succeeds but the plan is not touched: the note of the lot is left to the lead (the reason stays in `acceptation.json` and in the commit message; the command only says a note is to be added). With a plan outside the repository or untracked, the note **is** written in the plan file but not committed (the command says `plan à commiter`); do not add it a second time. If the commit of the note is refused (a hook), the reseal stays done: the error gives its commit and says the note, already written, is to be committed by hand. **A sheet amended in the same lot (L217):** a sheet amended since the red commit is accepted, for a lot in progress as for a delivered one (the sheet is then not put back to its red state to be restored afterwards). When its net difference with the red commit (`git diff <red> -- <sheet>`) is not empty, the command reads `git log <red>..HEAD -- <sheet>`: every commit that touched it must cite the lot (read like `raf commits`, i.e. `citedRefs`: the scope of the subject first, the body otherwise; for a delivered lot, the lot, the `--porteur` or any delivered lot of the plan, status `done`, L229: the living sheet is the reference; one cited delivered lot among several is enough), and the sheet must be committed. The fingerprint of the test is then redone against the current sheet and the `resceaux` entry gains the optional key `ficheAmendee` (short sha of the last commit that touched the sheet); the lot's note and the commit message say `fiche amendée en <sha>` instead of `fiche inchangée`. Still refused, with nothing written: a sheet modified by a commit that cites neither the lot nor (delivered lot) the `--porteur` or a delivered lot (`la fiche … a été modifiée depuis le commit rouge … par un commit qui ne cite pas <lot> : <sha>`; for a delivered lot `… ne cite ni <lot>, ni le porteur <porteur>, ni un lot livré : <sha> (cite : <lots ou aucun lot>) — faire citer le porteur ou un lot livré par ce commit (le réécrire avant qu'il soit poussé) ou, sinon, rescelle à la main avec le lead`), a sheet deleted or renamed since the red commit, a sheet with uncommitted changes (working tree or index: `a des modifications non commitées : la commiter (en citant <lot>) avant le rescellé`), as before any other listed test touched, a test left unchanged, an orchestrated wave and a missing `--reason`; the atomicity is unchanged. A sheet amended then brought back to the identical content is treated as unchanged, whoever committed in between. After the reseal the new red commit is the reseal commit, so a second reseal sees the sheet as unchanged. **A delivered lot (L212):** a lot whose acceptation is green (`vert: true`) and whose sealed test turns out unstable while the contract does not change (seen: a tab read as « blank » in CI) had no way out but the « sheet modified in the same lot » exception. `cadence spec reseal <lot> <test> --reason "why" --porteur <lot in progress>` now covers it, with the same guards (the sheet unchanged since the red commit, or amended as above by commits that cite the delivered lot, the carrier or another delivered lot; no other listed test touched, no wave, `--reason`) and three differences: `--porteur` is required and names the lot in progress that carries the fix (refused if it is the delivered lot itself, unknown or not `doing`); the commit cites the **carrier** (`test(L113): acceptation rescellée du lot livré L47, <test> : <reason>`) and so does the plan note and its commit, the delivered lot being closed; the dated `resceaux` entry also records `porteur`, and `vert` stays `true`. `--porteur` on a lot in progress, or `spec reseal` on a delivered lot without `--porteur`, is refused.
|
|
409
409
|
- **A closed lot left at `vert: false`: `cadence spec green` (L366).** The cycle sets `vert: true` itself; a lot driven and closed by hand (`raf done`) never gets it, and its `acceptation-<lot>.json` then keeps the lock total on its tests: every later wave of the repository is handed back with `verrou des tests d'acceptation … premier commit fautif` on lots that never touched them. Three guards:
|
|
410
|
+
- A lot already at `vert: true` but with no fingerprint for one of its listed tests (closed before the seal, L526, issue #20) is not « already green »: `cadence spec green <lot>` replays the command and, green, seals the missing fingerprints (the others are kept, no `resceaux` entry) in a commit of the file alone citing the lot (`chore(L12): empreintes de l'acceptation posées par cadence spec green`, or the `plan.commit` template); red: nothing changes, exit 1. A lot green and fully sealed still prints « déjà vert » and runs nothing.
|
|
410
411
|
- `cadence spec green <lot>` runs the dedicated command (`orchestrate.accept`, same rules as the cycle: the commands whose root holds a listed file, `{acceptation}` and `CADENCE_ACCEPTATION` set; a listed file under no command's root is refused) and, when it is green, sets `vert: true` and commits the file alone, citing the lot (`chore(L12): acceptation verte, vert: true posé par cadence spec green`, or the `plan.commit` template of the project when it has one). The subject stays short and fixed: the resealed tests are listed in the commit body, one `- <path>` line each, so a `commit-msg` hook bounding the header (commitlint `header-max-length` 100) accepts the commit. Tests refined by the lot after the red commit (the case of issue #18: its own fix commits changed them) would still be reported by the lock, whose reference is the red commit: the command therefore reseals every listed test whose fingerprint changed (the `empreintes` are redone, one `resceaux` entry each) in that same commit, which becomes the red commit; a listed test changed but not committed is refused before anything runs. Red: the tail of the output on stderr, exit 1, nothing changed. Already green: nothing to do. Refused for a `todo` lot, with `CADENCE_ORCHESTRATED` set, while a wave holds the repository lock and while an unfinished wave keeps a lot of the repository open (the same refusals as `spec reseal`, worded for `spec green`: « spec green : refusé … puis relancer spec green »).
|
|
411
412
|
- `raf done <lot>` refuses a lot whose acceptance is `vert: false` (the message names the file and the command above); `--force` closes it anyway and warns on stderr.
|
|
412
413
|
- `cadence orchestrate … --dry-run` prints once per repository `⚠ <repo> · acceptation rouge d'un lot clos : L12 (<file>, vert: false)` for each `done` lot left that way, before the wave starts.
|
|
@@ -558,6 +559,17 @@ except a copy that is still needed, which stays and is said: a copy that a live
|
|
|
558
559
|
The waves that count are those of the launch folders the script knows (`<folder>/.cadence/runs/<wave>/wave.json`): the repository itself and the working directory of every wave the orchestrator's registry still lists (`~/.cadence/orchestrate/waves/<pid>.json`; the registry drops a dead wave when another registers, so it does not know suspended waves by itself). There is no search over the whole disk: a suspended wave launched from another folder is not protected here, and `--resume` repairs its snapshot instead (above). A wave never marked `done` keeps its copy until it is, or until its folder is removed. A failure of the purge (unreadable copy, busy file) once `service` and the links have switched is a warning on the error output (`avertissement : purge des anciennes copies interrompue …`): the new version is in service, the fingerprint is printed, the exit code is 0, the next put in service tries again. The same run updates the **local copy of the `cadence-hud` mod** (L419), the one `CLAUDE_CODE_PLUGIN_DIRS` makes Claude Code load (`~/.claude/mods/cadence-hud`, `--mod-dir` for another folder): `plugins/cadence-hud` of the copy just put in service (not the working repository) is synchronised into it with `rsync -a --delete`, which leaves out `.claude-plugin/types` (generated by Claude Code in the copy). The output says `mod cadence-hud : copie locale mise à jour (<dir>)`. A machine without that folder gets no copy created (`copie locale absente … non mise à jour`), and a failed `rsync` is a warning on the error output (`avertissement : mod cadence-hud non mis à jour …`), exit code 0 like the purge: the version is in service. Before L419 the copy was updated by hand at each release and a forgotten one left the old mod loaded. The first run is also what installs the links in `~/.local/bin`. It prints the fingerprint of the copy
|
|
559
560
|
(bin, dist, package.json).
|
|
560
561
|
|
|
562
|
+
## Express lane (L519)
|
|
563
|
+
|
|
564
|
+
A lot of **at most 0.1 day** (`estimate: 0.1` or less, no `screen:`, no `spec: true`) takes the express lane, with no key to set. The L517 study ([docs/mesures/temps-d-un-lot.md](mesures/temps-d-un-lot.md), lever 2) measures 15.5 min and 116 k counted tokens for such a lot, 44 % of them in review and preparation: the lane cuts about 4.9 min and 51 k tokens per lot. What changes:
|
|
565
|
+
|
|
566
|
+
- **One writing session.** No precheck (even on the local model), no edge-case probe (`orchestrate.probe` is ignored for the lot), no separate code review and no UX review: the lead reads the diff and looks at the capture. After the program's gate the lot is `ready`, with the verdict `conforme : voie express …` recorded in the plan at the sha of its last commit (so `raf done` and the review gate accept it).
|
|
567
|
+
- **The full gate plays once** (`orchestrate.build`, then `orchestrate.test`, as for any lot): a red test still sends the lot to a correction pass. Only the « code sans test » gate (`orchestrate.testGate`) is off, because a purely visual change has no new test to write; the `implement` brief says so (« Express lane »: a change of behaviour still gets its test first).
|
|
568
|
+
- **No News entry per lot.** The brief of a visible express lot asks for a capture and no `cadence news new`; the lead groups the entries at delivery. `raf check` still wants an entry for a visible lot, so write the grouped entry before `raf done`.
|
|
569
|
+
- `--dry-run` prints `voie express` and the single step `implement`.
|
|
570
|
+
|
|
571
|
+
A lot above 0.1 day, or with a screen or a spec sheet, keeps the full cycle. The threshold is `EXPRESS_MAX_ESTIMATE` in `src/orchestrate/express.ts`.
|
|
572
|
+
|
|
561
573
|
## The edge-case probe (`orchestrate.probe`, L497)
|
|
562
574
|
|
|
563
575
|
Code bugs are the first cause of correction passes, and an edge case is the first sub-cause of those (64 % of the `bug-code` passes since the instruction of L447, 69 % before: writing it in the brief changed nothing). `orchestrate.probe: true` adds a step between the writing and the review: a **fresh Sonnet session** (`probe`, brief `templates/orchestrate/probe.md`, `effort.probe` medium, the delay of a write step `timeouts.work`) that has not written the code, reads the lot's diff, and tries to break it with tests at the edges: empty, absent, twice, wrong type, refused, the real shape of the data, the other callers. The diff is given as a command (`git diff <start sha>..HEAD`), never pasted; the sheet of the lot is named when there is one. Like `implement` and `fix`, the brief gives the program's exact gate (L399: the probe commits tests the gate replays, `tsc` and the quality script included) and the session's machine share (L350). The session writes its tests in new files and commits them all, the passing and the failing, with `probe(L<n>): …`; it does not touch the application code nor the sealed acceptance tests.
|
package/docs/raf.md
CHANGED
|
@@ -52,7 +52,7 @@ raf gantt # .cadence/gantt.html (next to the plan)
|
|
|
52
52
|
|---|---|
|
|
53
53
|
| `raf --version` · `raf -v` | print the version of the package, as `cadence --version` does (L376); only as the first argument, `raf show --version` stays an unknown option |
|
|
54
54
|
| `raf init [--project name] [--prefix L] [--no-hook]` | create the plan, install the hook, add `.playwright-mcp/` to `.gitignore` |
|
|
55
|
-
| `raf add "title" [--estimate d] [--quickwin] [--visible [--screen page\|component\|journey]] [--spec] [--public "title"] [--after L2,L4] [--uses banc,passerelle] [--parent L3]` | add a lot or a sub-task, print its id (`--public`: the lot's title in the public's words, written as `public:` right after `title:`; `--screen`: the screen a visible lot creates or changes — a new `page`, a new `component` or a modified `journey` — written as `screen:` right after `visible:`, shown by `raf show`; it requires `--visible`, and `raf check` reports a `screen:` without `visible:` or with another value as an error. Only `page` triggers the spec step of `orchestrate` (L209), never inferred from the title; `--spec`: the lot changes a behaviour (something a user, an API client or another service can observe), written as `spec: true` right after `screen:`, `visible:`, `quickwin:` or `estimate:` (whichever comes last), shown by `raf show`, refused on a sub-task. Also set by the lead at planning time, never inferred from the title); `--uses`: the shared resources the lot occupies, written as `uses: [banc, passerelle]` after `after:` and before `created:` ([shared resources](#shared-resources-uses-and-resources), L251), refused on a sub-task) |
|
|
55
|
+
| `raf add "title" [--estimate d] [--quickwin] [--visible [--screen page\|component\|journey]] [--spec] [--local] [--public "title"] [--after L2,L4] [--uses banc,passerelle] [--parent L3]` | add a lot or a sub-task, print its id (`--public`: the lot's title in the public's words, written as `public:` right after `title:`; `--screen`: the screen a visible lot creates or changes — a new `page`, a new `component` or a modified `journey` — written as `screen:` right after `visible:`, shown by `raf show`; it requires `--visible`, and `raf check` reports a `screen:` without `visible:` or with another value as an error. Only `page` triggers the spec step of `orchestrate` (L209), never inferred from the title; `--spec`: the lot changes a behaviour (something a user, an API client or another service can observe), written as `spec: true` right after `screen:`, `visible:`, `quickwin:` or `estimate:` (whichever comes last), shown by `raf show`, refused on a sub-task. Also set by the lead at planning time, never inferred from the title); `--local`: the lead edits this repository by hand for this lot, written as `local: true` right after `spec:` (or the last key before it), refused on a sub-task ([a lot of the lead](#a-lot-of-the-lead-local-l519)); `--uses`: the shared resources the lot occupies, written as `uses: [banc, passerelle]` after `after:` and before `created:` ([shared resources](#shared-resources-uses-and-resources), L251), refused on a sub-task) |
|
|
56
56
|
| `raf add "title" --every <days>` · `raf did <id> ["text"]` | recurring lot: `every` (days) and `last` (last time done) fields; the lot stays `todo`, `raf now` and `session start` list it under « Récurrent » with « dû depuis N j » / « prochain dans N j »; `raf did` resets the count (spec: `docs/superpowers/specs/2026-10-04-L11-tache-recurrente.md`) |
|
|
57
57
|
| `raf screen <id> page\|component\|journey\|none` | set, replace or remove `screen:` on an existing lot, to adopt the [spec step](orchestrate.md#the-spec-step-of-a-lot-with-screen) on a plan already written (only `page` adopts the [spec step](orchestrate.md#the-spec-step-of-a-lot-with-screen); `component` and `journey` do not); refused on a lot without `visible:` (like `raf add --screen`) and on a sub-task; `none` removes the key and leaves `visible:` alone |
|
|
58
58
|
| `raf visible <id> on\|off` | set or remove `visible:` on an existing lot (a user-facing change: a news entry is expected, and `cadence orchestrate` adds its UX step), written right after `quickwin:`, `estimate:` or `status:` (whichever comes last); `on` on a visible lot changes nothing, `off` on a lot without the key neither; refused on a sub-task, and `off` is refused while the lot has a `screen:` (`raf screen <id> none` first: a screen is a visible change). It is the fix `raf check` points to for an open « Revue UX — » lot without `visible:` (L273) |
|
|
@@ -63,7 +63,7 @@ raf gantt # .cadence/gantt.html (next to the plan)
|
|
|
63
63
|
| `raf add`, `raf note`, `raf done` while a wave works in the main tree (L475) | when the repository's orchestration lock is held by a live wave and one of its lots is in `implementing`, `reviewing` or `fixing` **without a worktree** (a wave without worktrees, or a lot whose worktree folder does not exist), these commands write the plan as usual and print on stderr `avertissement : la vague … travaille dans l'arbre principal de ce dépôt (projet:lot) — …`: a plan commit of the lead then hands the lot back at review (2026-10-30: cadence L458 handed back by a plan commit during its review). Write the plan without committing it, or commit once the step ends. Exit code 0; nothing is printed for lots that are queued, in question, ready, handed back, failed or suspended, for lots playing in their worktree, from a worktree of the repository, from a session of the wave itself (`CADENCE_ORCHESTRATED` set), when the lock belongs to a dead process, or when the wave's state is unreadable |
|
|
64
64
|
| `raf public <id> "title"` · `raf public <id> --clear` | set, replace or remove the public title of a lot |
|
|
65
65
|
| `raf start <id>` · `raf done <id> [--force]` · `raf drop <id> [--reason text]` | dated transitions (`done` refuses open sub-tasks unless `--force`, with the code-review gate active (`raf review enable`) a lot whose commits its review does not cover, **and a lot whose recorded review verdict says `non conforme` / `not compliant` even when it covers every commit** (L476: `non conforme : 1 bloquant` at the last commit no longer closes; `conforme après 1 correction` is compliant; redo the review with `raf review <id> "verdict"` after the fix, or `--force`), and a lot whose `acceptation-<lot>.json` is still `vert: false`, L366: closed like that it would lock every later wave of the repository, see `cadence spec green`; with `--force` it only warns on stderr. Like every `raf` command it only writes the plan file and **never commits** it: when the plan or a file `plan.onWrite` produces is left with changes, `done`, `ux <lot>` and `review <lot>` print the `git add` / `git commit` command that takes them, the subject from the `plan.commit` template, L371) |
|
|
66
|
-
| `cadence spec green <lot>` | L366: run the dedicated acceptance command (`orchestrate.accept`) on a lot driven or closed by hand; green, set `vert: true` in its `acceptation-<lot>.json` and commit that file alone (subject `chore(<lot>): acceptation verte, vert: true posé par cadence spec green` or the `plan.commit` template; the resealed tests are listed in the commit body, never in the subject), resealing the fingerprints of the listed tests refined since the red commit (journal `resceaux`) so the lock goes quiet; a listed test changed but not committed is refused. Red: exit 1, nothing changed. Refused for a `todo` lot, during a wave, and while an unfinished wave keeps a lot of the repository open; see [orchestrate](orchestrate.md) |
|
|
66
|
+
| `cadence spec green <lot>` | L366: run the dedicated acceptance command (`orchestrate.accept`) on a lot driven or closed by hand; green, set `vert: true` in its `acceptation-<lot>.json` and commit that file alone (subject `chore(<lot>): acceptation verte, vert: true posé par cadence spec green` or the `plan.commit` template; the resealed tests are listed in the commit body, never in the subject), resealing the fingerprints of the listed tests refined since the red commit (journal `resceaux`) so the lock goes quiet; a listed test changed but not committed is refused. Red: exit 1, nothing changed. A lot already `vert: true` whose tests lack fingerprints (L526) is replayed and, green, sealed (subject `chore(<lot>): empreintes de l'acceptation posées par cadence spec green`). Refused for a `todo` lot, during a wave, and while an unfinished wave keeps a lot of the repository open; see [orchestrate](orchestrate.md) |
|
|
67
67
|
| `raf todo <id> [--reason text]` | put a started lot back to `todo` (`doing` → `todo`, `started:` removed, dated note « remis en attente : reason »): refused when work commits cite the lot (plan-only and version commits do not count, as `raf commits`), or on a sub-task; sub-tasks and other keys are left as they are. **Reopening** (L396): on a `done` or `dropped` lot, `raf todo` is THE command — never the YAML — that reopens it (`→ todo`, `finished:` removed, `started:` kept so its commits stay its own, dated note « rouvert (était done) : reason »); `raf start`/`raf done`/`raf drop` on a closed lot say so (`L3 est done ; le rouvrir si c'est voulu : raf todo L3`). An archived lot reopened this way comes back into the plan file at that write |
|
|
68
68
|
| `raf migrate [--dry-run]` | move `docs/plan/raf.yaml` to `.cadence/raf.yaml` and rewrite the readers of the old path in the repository, see [Moving the plan: `raf migrate`](#moving-the-plan-raf-migrate) |
|
|
69
69
|
| `raf note <id> "text"` | dated note — keep decisions next to the work |
|
|
@@ -108,6 +108,18 @@ pre-commit block to an existing repository. A read-only plan is never started by
|
|
|
108
108
|
Global options: `--file path` or `RAF_FILE`; `RAF_TODAY=YYYY-MM-DD` to freeze
|
|
109
109
|
the date. `cadence raf …` is the same as `raf …`.
|
|
110
110
|
|
|
111
|
+
## A lot of the lead (local, L519)
|
|
112
|
+
|
|
113
|
+
`raf add "…" --local` marks a lot (`local: true`) that the lead does by hand in this repository instead of giving it to a wave (a touch-up the lead would rather see in the editor, a change that needs the lead's eyes all along). Two commands carry it:
|
|
114
|
+
|
|
115
|
+
| Command | What it does |
|
|
116
|
+
|---|---|
|
|
117
|
+
| `cadence local start <lot> [--ttl minutes]` | locks the repository as a wave does (`orchestrate.lock`, under the name `local:<lot>`): no wave starts in it, `orchestrate --resume` and `orchestrate --add` are refused with « une tâche locale du lead y est en cours (<lot>, depuis …) », and `deliver` and `raf move` are refused with the same message (the task is named with its exit `local done` / `local abort`; no `--sha` advice, a local task has no wave lots). The lot goes to `doing`. The lock is **declared**, like `cadence lead hold`: no process carries it (the shell that runs the command dies with the Bash tool call, and the lead's session has no stable process name), it holds until `local done` or `local abort`, and `--ttl` (60 min by default) is only the announced duration — past it, the lock still holds and `cadence lead tour` and `orchestrate --status` print « ⚠ tâche locale échue : <lot> … » until the lead finishes or aborts it; it never lapses silently. Refused, with nothing locked or written, when the lot is unknown, not marked `local`, `done` or `dropped`, when a wave or another local task holds the repository, or when another lot of the plan is `doing` (it is named). |
|
|
118
|
+
| `cadence local done <lot>` | plays the project's gate **once** (`orchestrate.build`, then `orchestrate.test`, at `HEAD`, as `cadence verify --gate`) and, if it is green, frees the repository. Red (code 1) or nothing to replay (code 2): the lock stays, fix and run it again. Refused when this lot's local task does not hold the lock. |
|
|
119
|
+
| `cadence local abort <lot>` | frees the repository without playing the gate (the task is given up or will be resumed later); the plan is not touched — the lot stays `doing`, `raf todo <lot>` puts it back. Refused when this lot's local task does not hold the lock. |
|
|
120
|
+
|
|
121
|
+
`local start` writes the plan (the lot goes to `doing`): commit it with the first commit of the lot. After `local done`: take the capture of a visible lot, read the lot's diff and record the verdict yourself (`raf review <lot> "…"` — on a plan with `reviewSince`, `raf done` refuses a lot with commits and no verdict; the express lane records its own verdict because a session did the work and the lead reads the diff at delivery, here the lead did the work, so the verdict is the lead's own reading and is written by hand), `cadence deliver`, then `raf done <lot>` — the lot is traced in the plan like any other (its commits cite it, `feat(L12): …`). A local lot is never played by a wave: `cadence orchestrate <project>:<lot>` (launch, `--add`, `--continue`) refuses it by name. During the task the pre-push hook sees the lock as a local task, not as a wave: no wave check, no « état illisible » warning; the gate check (`CADENCE_PUSH_UNGATED`) applies as usual.
|
|
122
|
+
|
|
111
123
|
## Shared resources: `uses:` and `resources:`
|
|
112
124
|
|
|
113
125
|
Some projects of a folder share one thing that only one lot at a time can use (a test bench, a gateway, a
|
package/docs/spec.md
CHANGED
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
|
|
29
29
|
`cadence spec reseal <lot> <test> --reason texte` is the lead's way to correct one sealed acceptance test of the lot in progress that contradicts its sheet (unchanged, or amended by commits of the lot, L217; of a delivered lot, also by commits of another delivered lot, L229) (of a delivered lot: add `--porteur <lot in progress>`); refused while a wave of the repository is running, or a wave that is not `done` keeps open (question waiting, suspended, queued) any lot of this repository still `doing` in its plan, the resealed lot or another (L238: one error, also when a live wave holds the lock, names each wave (live ones to wait for), its launch folder and, for each stopped wave, the single `cd <folder> && cadence orchestrate --resume --wave <wave> --drop <project>:<lot> --drop …` that closes all its open lots of the repository, the queued ones that are not `doing` in the plan included (L239); a lot `done` or absent in the plan never refuses), see [The accept step](orchestrate.md#the-accept-step-red-acceptance-tests-and-their-lock).
|
|
30
30
|
|
|
31
|
-
`cadence spec green <lot>` (L366, GitHub issue #18) is the lead's way to put back to green a lot driven and closed by hand (`raf done`) whose `acceptation-<lot>.json` stayed at `vert: false`: closed like that, its tests keep the lock total and every later wave of the repository is handed back with `verrou des tests d'acceptation … premier commit fautif` on lots that never touched them, and `spec reseal` does not cover it (it takes a lot `doing`, or a delivered lot at `vert: true` with `--porteur`). The program runs the lot's acceptance command (`orchestrate.accept`, same rules as the cycle) and, green, sets `vert: true`, reseals the listed tests refined since the red commit (one `resceaux` entry each) and commits the file alone, citing the lot (`chore(<lot>): acceptation verte, vert: true posé par cadence spec green`, or the project's `plan.commit` template when it has one; the subject stays short and fixed, the tests resealed are listed in the commit body, one `- <path>` line each, so a `header-max-length` hook accepts it); red, the tail of the output goes to stderr, exit 1, nothing changed; already green, nothing to do. Refused for a `todo` lot, from a session (`CADENCE_ORCHESTRATED`), during a wave of the repository and while an unfinished wave keeps a lot of the repository open (the same refusals as `spec reseal`, worded for `spec green`); a listed test changed but not committed is refused before anything runs. `raf done` refuses such a lot without `--force`, and `cadence orchestrate --dry-run` prints `acceptation rouge d'un lot clos` once per repository, see [A closed lot left at vert: false](orchestrate.md#the-accept-step-red-acceptance-tests-and-their-lock).
|
|
31
|
+
`cadence spec green <lot>` (L366, GitHub issue #18) is the lead's way to put back to green a lot driven and closed by hand (`raf done`) whose `acceptation-<lot>.json` stayed at `vert: false`: closed like that, its tests keep the lock total and every later wave of the repository is handed back with `verrou des tests d'acceptation … premier commit fautif` on lots that never touched them, and `spec reseal` does not cover it (it takes a lot `doing`, or a delivered lot at `vert: true` with `--porteur`). The program runs the lot's acceptance command (`orchestrate.accept`, same rules as the cycle) and, green, sets `vert: true`, reseals the listed tests refined since the red commit (one `resceaux` entry each) and commits the file alone, citing the lot (`chore(<lot>): acceptation verte, vert: true posé par cadence spec green`, or the project's `plan.commit` template when it has one; the subject stays short and fixed, the tests resealed are listed in the commit body, one `- <path>` line each, so a `header-max-length` hook accepts it); red, the tail of the output goes to stderr, exit 1, nothing changed; already green and sealed, nothing to do; already `vert: true` but with a listed test lacking its fingerprint (closed before the seal, L526, issue #20), the command is replayed too and, green, only the missing fingerprints are sealed in a commit citing the lot (`chore(<lot>): empreintes de l'acceptation posées par cadence spec green`), red nothing changes. Refused for a `todo` lot, from a session (`CADENCE_ORCHESTRATED`), during a wave of the repository and while an unfinished wave keeps a lot of the repository open (the same refusals as `spec reseal`, worded for `spec green`); a listed test changed but not committed is refused before anything runs. `raf done` refuses such a lot without `--force`, and `cadence orchestrate --dry-run` prints `acceptation rouge d'un lot clos` once per repository, see [A closed lot left at vert: false](orchestrate.md#the-accept-step-red-acceptance-tests-and-their-lock).
|
|
32
32
|
|
|
33
33
|
`cadence spec qa <fiche.yaml>` derives from a valid sheet the section of `docs/qa/expectations.md` for its screen and prints it on the standard output
|
|
34
34
|
(it never writes the file: you paste the section, and the lead commits it like the rest of the plan files). A sheet with violations prints them on the
|
package/docs/whats-new.md
CHANGED
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
|
|
5
5
|
## In two lines
|
|
6
6
|
|
|
7
|
+
- **1.38.1**: `spec green` seals a lot closed at `vert: true` without fingerprints (issue #20); `deliver`, `raf move`, `spec reseal` and `spec green` refuse cleanly during a local task.
|
|
8
|
+
- **1.38.0**: an express lane plays a touch-up of at most 0.1 day in one session; `raf add --local` and `cadence local start|done|abort` lock a repository while the lead edits it by hand.
|
|
7
9
|
- **1.37.0**: one full test suite per lot, played by the gate (`suites` column); no correction pass without a finding; a richer implementation brief; `scripts/temps-d-un-lot.mjs` measures where a lot's time goes.
|
|
8
10
|
- **1.36.0**: `cadence deliver` no longer waits for the CI of a sha whose commits only touch the plan, with timed steps and a `deliveries-steps.log` journal; `orchestrate --add` puts a lot in a live wave.
|
|
9
11
|
- **1.35.0**: a test already red before a lot, or that only needs more time, no longer costs correction passes, including on a gate declared in parts; QA passes run in browsers of their own; design agents check contrast and AI tics.
|
|
@@ -65,7 +67,11 @@ Each is a paragraph in [docs/whats-new.md](whats-new.md) and a full section in [
|
|
|
65
67
|
|
|
66
68
|
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).
|
|
67
69
|
|
|
68
|
-
**1.
|
|
70
|
+
**1.38.1**: `cadence spec green <lot>` now handles a lot closed at `vert: true` with no fingerprints (closed before the seal, GitHub issue #20): it replays the lot's acceptance command and, green, writes the fingerprints in a commit that cites the lot, so a later lot that amends one of its tests no longer has to be finished outside a wave (L526, `docs/orchestrate.md`). During a local task of the lead (1.38.0), `cadence deliver`, `raf move`, `cadence spec reseal` and `cadence spec green` refuse with a message that names the task and how to end it, instead of « état illisible » or an impossible `--sha` hint (L525, `docs/raf.md`).
|
|
71
|
+
|
|
72
|
+
**1.38.0**: two ways to make a small change cheaper (L519, decided on 01-11 after a one-line UI separator cost 245 s of implementation). The **express lane**: a lot of at most 0.1 day with no screen sheet nor spec is played by `cadence orchestrate` in a single writing session, without precheck, edge-case probe, separate code review or « code sans test » gate, and without a News entry per lot; the lead reads the diff and the capture, groups the entries at delivery, and the program's gate still plays once (`docs/orchestrate.md`). The **local task**: `raf add --local` marks a lot the lead does by hand, and `cadence local start <lot>` locks the repository like a wave (no wave starts there, `--add` and `--continue` refuse it, and the task itself is refused while a wave or another lot of the repository runs). The lock is declared with a `--ttl` (60 min by default, like `lead hold`) and carried by no process; past its TTL it still holds and `lead tour` and `orchestrate --status` show a ⚠ until `cadence local done` (gate green) or `cadence local abort`. A wave refuses a `local` lot, and the pre-push hook recognises the task instead of reporting an unreadable wave (`docs/raf.md`).
|
|
73
|
+
|
|
74
|
+
**1.37.0**: the first release of the efficiency work, sized by a measure of 721 lots (L517, `docs/mesures/temps-d-un-lot.md`, script `node scripts/temps-d-un-lot.mjs`): a lot took 25 minutes and 265k tokens, tests were 62 % of the writing sessions' wall time and a correction pass cost about 12 minutes. The writing sessions of `cadence orchestrate` now play only the targeted tests of the code they touch; the full suite is played once by the program as the gate, `cadence verify --gate` takes its verdict back, and the final table and `--status` count the full suites of each lot in a `suites` column (L518, `docs/orchestrate.md`). A non-compliant review that lists no blocking or major finding no longer opens a correction pass: the lot is handed back with the announced totals as the cause; a test red in the suite and green alone is proposed as a lot to stabilise, and a green program gate always outweighs a session's claim of red tests (L522). The `implement` brief carries the code reviewer's grid, « edge-case tests first, seen failing, then code » and a re-reading of the diff before the last commit; the pre-check passes what it read (`precheckLu`) to the first implementation so it does not read the repository again (L523). The tests workflow tolerates the npm « previously published » refusal of its publish dry-run on a non-release commit, and nothing else (L515).
|
|
69
75
|
|
|
70
76
|
**1.36.0**: `cadence deliver` stops waiting for a CI that proves nothing. When every commit since the previous delivery touches only the plan, it says `→ CI : non attendue (aucun fichier hors plan depuis <sha>)`, then runs the deploy and the checks; as soon as one commit touches anything else, or the proof is missing (no previous delivery, rewritten history, no plan, empty range), it waits as before (L516, `docs/deliver.md`). 42 % of the app deliveries measured on 2026-11-01 were such shas. Every step line of the output now starts with its UTC time and `✓ livré` ends with its duration, and each step played appends a JSON line to `deliveries-steps.log`, so a slow delivery can be read step by step. `cadence orchestrate --add <project:lot> [--wave <wave>]` adds a lot to a live wave instead of launching a second one: the lot is checked like a launched one, queued in the wave's `adds.log` and played as soon as a session slot is free; the refusal of a second wave in the same repository now names this command, with the lot and the wave (L421, `docs/orchestrate.md`). A session running in a sibling repository is no longer handed back for a commit on another repository's main branch once a lot was added to the wave.
|
|
71
77
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sylad/cadence",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.38.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",
|
|
@@ -4,6 +4,7 @@ Goal: {{objectif}}
|
|
|
4
4
|
{{repos}}
|
|
5
5
|
{{fiche}}
|
|
6
6
|
{{decisions}}
|
|
7
|
+
{{express}}
|
|
7
8
|
Rules: test first; commit each sub-part as soon as its tests pass, with explicit paths (never
|
|
8
9
|
`git add -A` or `commit -a`), and a message that cites the lot (`feat({{lot}}): …`); run the targeted tests of the code you touch (once, before committing) and the build, before reporting: the full suite is played once by the program as the gate given below, do not replay it (with no gate below, play the full suite yourself, once); do not push, deliver, run `raf done`, `raf ux` or
|
|
9
10
|
`raf review`.
|