@sylad/cadence 1.29.0 → 1.30.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/dist/cli.js +5 -3
- package/dist/lead.js +19 -1
- package/dist/orchestrate/command.js +104 -7
- package/dist/orchestrate/cycle.js +2 -2
- package/dist/orchestrate/pool.js +26 -11
- package/dist/orchestrate/snapshot.js +13 -0
- package/dist/orchestrate/state.js +36 -0
- package/dist/orchestrate/tiers.js +8 -12
- package/docs/cadence-hud.md +1 -1
- package/docs/lead.md +3 -1
- package/docs/orchestrate.md +14 -6
- package/docs/session.md +4 -1
- package/docs/whats-new.md +3 -0
- package/package.json +1 -1
|
@@ -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.30.0",
|
|
12
12
|
"source": "./",
|
|
13
13
|
"author": {
|
|
14
14
|
"name": "Sylvain Ladoire"
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cadence",
|
|
3
3
|
"description": "A repo-native working method: session start and close rituals driven by a versioned plan (raf), deliveries proven by their effect, and three reviewer agents (UX, code, QA).",
|
|
4
|
-
"version": "1.
|
|
4
|
+
"version": "1.30.0",
|
|
5
5
|
"author": { "name": "Sylvain Ladoire" },
|
|
6
6
|
"homepage": "https://github.com/Sylad/cadence",
|
|
7
7
|
"repository": "https://github.com/Sylad/cadence",
|
package/dist/cli.js
CHANGED
|
@@ -23,7 +23,7 @@ import { schedule } from './schedule.js';
|
|
|
23
23
|
import { AGENTS_DIR, installAgents, installSkills, SKILLS_DIR } from './skills.js';
|
|
24
24
|
import { configSet } from './config-set.js';
|
|
25
25
|
import { movePriority } from './priority.js';
|
|
26
|
-
import { findProjects, readDepth, leadTour, TITLE_MAX, truncate, readLeadUx, tourFreeLine, readLeadContextThreshold, tourLine, tourNoTestLines, tourWorktreeLines } from './lead.js';
|
|
26
|
+
import { findProjects, readDepth, leadTour, TITLE_MAX, truncate, readLeadUx, tourFreeLine, readLeadContextThreshold, findLeadDir, tourLine, tourNoTestLines, tourWorktreeLines } from './lead.js';
|
|
27
27
|
import { configuredCap, expiredHoldLines, slotsLine, orchestrate, realOrchestrateDeps } from './orchestrate/command.js';
|
|
28
28
|
import { addHold, releaseHold } from './orchestrate/registry.js';
|
|
29
29
|
import { activeLock, REPO_LOCK } from './orchestrate/lock.js';
|
|
@@ -1746,8 +1746,10 @@ function sessionContext(io, session) {
|
|
|
1746
1746
|
if (!r.ok)
|
|
1747
1747
|
throw new RafError(`session context : contexte illisible — ${r.reason} ; à traiter comme au seuil`);
|
|
1748
1748
|
io.out(`ctx ${Math.round(r.ctx.percent)} % (${r.ctx.tokens}/${r.ctx.window})`);
|
|
1749
|
-
|
|
1750
|
-
|
|
1749
|
+
// Lancée d'un sous-projet, la commande lit le cadence.yaml du dossier du lead (L457) et nomme le dossier lu.
|
|
1750
|
+
const leadDir = findLeadDir(io.cwd);
|
|
1751
|
+
const threshold = readLeadContextThreshold(leadDir);
|
|
1752
|
+
io.out(`seuil ${threshold} tokens : ${r.ctx.tokens >= threshold ? 'atteint' : 'sous le seuil'}${leadDir === io.cwd ? '' : ` (lu dans ${join(leadDir, 'cadence.yaml')})`}`);
|
|
1751
1753
|
return 0;
|
|
1752
1754
|
}
|
|
1753
1755
|
function session([sub, ...args], ctx, values) {
|
package/dist/lead.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
|
|
2
|
-
import { basename, join, relative, sep } from 'node:path';
|
|
2
|
+
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';
|
|
@@ -45,6 +45,24 @@ export function readLeadUx(parent) {
|
|
|
45
45
|
throw new RafError(`${found.file} : lead.ux doit être « lead » ou « human »`);
|
|
46
46
|
return ux;
|
|
47
47
|
}
|
|
48
|
+
/** Le dossier du lead pour `start` (L457) : `start` ou le premier parent dont le cadence.yaml porte la clé `lead:` ou `priority:` ; aucun ne la porte (ou un yaml illisible en chemin) = `start` lui-même. */
|
|
49
|
+
export function findLeadDir(start) {
|
|
50
|
+
for (let dir = start;; dir = dirname(dir)) {
|
|
51
|
+
const file = join(dir, 'cadence.yaml');
|
|
52
|
+
if (existsSync(file)) {
|
|
53
|
+
try {
|
|
54
|
+
const raw = parse(readFileSync(file, 'utf8'));
|
|
55
|
+
if (raw && typeof raw === 'object' && ('lead' in raw || 'priority' in raw))
|
|
56
|
+
return dir;
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
// un yaml illisible n'est pas celui d'un lead qu'on puisse lire : on continue de remonter
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
if (dirname(dir) === dir)
|
|
63
|
+
return start;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
48
66
|
/** Seuil de contexte du lead quand `lead.contextThreshold` n'est pas posé (L437). */
|
|
49
67
|
export const CONTEXT_THRESHOLD_DEFAULT = 150_000;
|
|
50
68
|
/** Clé `lead.contextThreshold` du cadence.yaml du dossier (L437) : le nombre de tokens de contexte à partir duquel le lead ne lance plus de vague. En tokens et non en % : 60 % d'une fenêtre de 1M valait 600k. Un entier ≥ 1, sinon refus. */
|
|
@@ -38,7 +38,7 @@ import { resolveLotRepos } from '../repos.js';
|
|
|
38
38
|
import { excludeState, lotFinished, lotKey, lotRepoPaths, lotSlug, neighbourDirs, newLot, RunStore } from './state.js';
|
|
39
39
|
import { linkNodeBin, nvmVersionsDir, resolveNode } from './node-env.js';
|
|
40
40
|
import { quotaText, renderDetail, renderTable, resumeCommand } from './table.js';
|
|
41
|
-
import { PACKAGE_ROOT, RESERVED_ENV, SnapshotRefusal, isSnapshotChild, resolveModulesDir, repairSnapshotModules, snapshotExists, waveReadsControl, waveReadsIntegrate, waveReadsLotBudget, waveReadsReplay, withoutLaunchVars, spawnReexec, takeSnapshot, toolDirOf } from './snapshot.js';
|
|
41
|
+
import { PACKAGE_ROOT, RESERVED_ENV, SnapshotRefusal, isSnapshotChild, resolveModulesDir, repairSnapshotModules, snapshotExists, waveReadsAnswerQueue, waveReadsControl, waveReadsIntegrate, waveReadsLotBudget, waveReadsReplay, withoutLaunchVars, spawnReexec, takeSnapshot, toolDirOf } from './snapshot.js';
|
|
42
42
|
const REPO_WAIT_MS = 3 * 60 * 60_000;
|
|
43
43
|
const REPO_WAIT_POLL_MS = 5_000;
|
|
44
44
|
export const DEFAULT_BUDGET = 2_000_000;
|
|
@@ -634,6 +634,8 @@ function freeLines(launch, drawn) {
|
|
|
634
634
|
/** Efface l'écran et replace le curseur en haut : l'entrée d'un rendu de `--status --watch`. */
|
|
635
635
|
const CLEAR_SCREEN = '\x1b[H\x1b[2J';
|
|
636
636
|
const DEFAULT_WATCH_INTERVAL = 10;
|
|
637
|
+
/** Attente (ms) entre deux lectures de la file de réponses d'une vague vivante (L407) quand aucune file de lots ne finit. */
|
|
638
|
+
const ANSWER_POLL_MS = 2000;
|
|
637
639
|
/** Ce que `--status` montre à un instant : les vagues vivantes, puis le tableau (absent si seules les vagues vivantes comptent). */
|
|
638
640
|
function statusView(args, launch, cap, now) {
|
|
639
641
|
const id = statusWave(args);
|
|
@@ -805,6 +807,8 @@ export async function orchestrate(argv, io, deps) {
|
|
|
805
807
|
return resume(args, argv, io, deps, launch, today);
|
|
806
808
|
if (args.drop.length || args.stopAfterCurrent)
|
|
807
809
|
return control(args, io);
|
|
810
|
+
if (args.answers.length)
|
|
811
|
+
return answerOnly(args, io);
|
|
808
812
|
if (args.lots.length === 0 && !args.continue)
|
|
809
813
|
throw new RafError('usage : cadence orchestrate <projet>:<lot>… [--budget 2M] [--max-sessions 2] [--dry-run] [--continue [--until 18:00] [--priority a,b]] | --waits [--since AAAA-MM-JJ] | --status [vague] [--detail projet:lot | --watch [--interval 10]] | --resume [vague] [--answer projet:lot "réponse"] [--integrate projet:lot] [--replay projet:lot] | --drop projet:lot | --stop-after-current | --remove-worktree projet:lot [--abandon-commits]');
|
|
810
814
|
const refusals = [];
|
|
@@ -1286,7 +1290,41 @@ async function execute(wave, lots, store, io, deps, today, cap, all = lots) {
|
|
|
1286
1290
|
if (held.includes(join(sharedStateDir(r), REPO_LOCK)) && all.filter((l) => lotRepoPaths(l).includes(r)).every(finished))
|
|
1287
1291
|
freeRepo(r);
|
|
1288
1292
|
}
|
|
1289
|
-
}, (lots) => lots.every((c) => c.tree)
|
|
1293
|
+
}, (lots) => lots.every((c) => c.tree), {
|
|
1294
|
+
pollMs: deps.slotPollMs ?? ANSWER_POLL_MS,
|
|
1295
|
+
// L407 : les réponses déposées par `--answer` pendant la vague sont prises entre deux étapes ; un lot qui n'est plus en question ne rejoue rien.
|
|
1296
|
+
take: (canRun) => {
|
|
1297
|
+
const taken = [];
|
|
1298
|
+
for (const e of store.answerQueue()) {
|
|
1299
|
+
const c = ctxs.find((x) => lotKey(x.lot.project, x.lot.lot) === e.lot);
|
|
1300
|
+
const why = !c ? 'lot inconnu' : c.lot.status !== 'question' ? `le lot n'attend plus de réponse (${c.lot.status})` : c.lot.pendingAnswer ? 'une réponse est déjà en cours de jeu' : null;
|
|
1301
|
+
if (why) {
|
|
1302
|
+
wctx.log(`${e.lot} : réponse écartée (${why})`);
|
|
1303
|
+
store.markAnswerTaken(e.id);
|
|
1304
|
+
}
|
|
1305
|
+
else if (c && canRun(c)) {
|
|
1306
|
+
c.lot.pendingAnswer = e.text;
|
|
1307
|
+
c.lot.answers.push(e.text);
|
|
1308
|
+
c.lot.questions = [];
|
|
1309
|
+
store.writeLot(c.lot);
|
|
1310
|
+
store.markAnswerTaken(e.id);
|
|
1311
|
+
wctx.log(`${e.lot} : réponse du lead prise, le lot repart`);
|
|
1312
|
+
// Le lot avait fini de jouer à sa question (`settled`) : il rejoue, il tient de nouveau un créneau — sans cela `lanes()` le sous-compte et le registre annonce un créneau libre de trop (L433).
|
|
1313
|
+
settled.delete(c);
|
|
1314
|
+
taken.push(c);
|
|
1315
|
+
}
|
|
1316
|
+
}
|
|
1317
|
+
if (taken.length) {
|
|
1318
|
+
try {
|
|
1319
|
+
updateWaveRepos(home, process.pid, repos.filter((x) => !freed.has(x)), lanes());
|
|
1320
|
+
}
|
|
1321
|
+
catch {
|
|
1322
|
+
// registre illisible : le décompte reste celui du dernier écrit
|
|
1323
|
+
}
|
|
1324
|
+
}
|
|
1325
|
+
return taken;
|
|
1326
|
+
},
|
|
1327
|
+
});
|
|
1290
1328
|
handBackDroppedQuestions(ctxs); // un lot en question retiré pendant la vague : rendu avant de fixer le statut de la vague
|
|
1291
1329
|
}
|
|
1292
1330
|
finally {
|
|
@@ -1308,11 +1346,57 @@ async function execute(wave, lots, store, io, deps, today, cap, all = lots) {
|
|
|
1308
1346
|
return 3;
|
|
1309
1347
|
return all.every((l) => l.status === 'ready') ? 0 : 1;
|
|
1310
1348
|
}
|
|
1311
|
-
/**
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1349
|
+
/**
|
|
1350
|
+
* `--answer` sur une vague VIVANTE (L407) : la réponse est déposée dans la file de la vague (`answers.log`), qui la prend entre deux étapes et rejoue le lot dès qu'un créneau est libre.
|
|
1351
|
+
* Sans cela le lead attendait la fin de la vague (15 min d'un lot bloqué le 28-10). Les lots visés sont contrôlés comme à la reprise : connus, en question.
|
|
1352
|
+
*/
|
|
1353
|
+
function answerLive(args, io, store) {
|
|
1354
|
+
const lots = store.lots();
|
|
1355
|
+
const refusals = [];
|
|
1356
|
+
const keys = [];
|
|
1357
|
+
for (const a of args.answers) {
|
|
1358
|
+
const matches = lots.filter((l) => l.lot === a.lot && (a.project ? l.project === a.project : true));
|
|
1359
|
+
const where = `${a.project ? `${a.project}:` : ''}${a.lot}`;
|
|
1360
|
+
if (matches.length !== 1)
|
|
1361
|
+
refusals.push(matches.length ? `--answer ${where} : plusieurs projets portent ce lot, précisez projet:lot` : `--answer ${where} : lot inconnu dans la vague ${store.id}`);
|
|
1362
|
+
else if (matches[0].status !== 'question')
|
|
1363
|
+
refusals.push(`--answer ${lotKey(matches[0].project, matches[0].lot)} : le lot n'attend pas de réponse (${matches[0].status})`);
|
|
1364
|
+
else
|
|
1365
|
+
keys.push({ key: lotKey(matches[0].project, matches[0].lot), text: a.text });
|
|
1366
|
+
}
|
|
1367
|
+
if (!waveReadsAnswerQueue(store.dir))
|
|
1368
|
+
refusals.push(`la vague ${store.id} ne lit pas la file de réponses (lancée avec une version de cadence d'avant L407) : attendre sa fin, puis ${resumeCommand(store.id, '--answer', '<projet>:<lot>', '"…"')}`);
|
|
1369
|
+
if (refusals.length) {
|
|
1370
|
+
for (const r of refusals)
|
|
1371
|
+
io.err(`orchestrate : ${r}`);
|
|
1372
|
+
return 2;
|
|
1373
|
+
}
|
|
1374
|
+
for (const { key, text } of keys) {
|
|
1375
|
+
const id = store.requestAnswer(key, text);
|
|
1376
|
+
// La vague a pu finir entre le contrôle et le dépôt : sa file ne sera plus lue, la réponse est retirée et le lead reprend à la main.
|
|
1377
|
+
if (!liveWaves(cadenceHome()).some((w) => w.wave === store.id)) {
|
|
1378
|
+
store.markAnswerTaken(id);
|
|
1379
|
+
io.err(`orchestrate : ${key} : la vague ${store.id} vient de finir, réponse non prise : ${resumeCommand(store.id, '--answer', key, '"…"')}`);
|
|
1380
|
+
return 2;
|
|
1381
|
+
}
|
|
1382
|
+
store.journal(`demande : réponse du lead pour ${key}`);
|
|
1383
|
+
io.out(`${key} : réponse déposée — la vague ${store.id} la prend entre deux étapes et rejoue le lot dès qu'un créneau est libre`);
|
|
1384
|
+
}
|
|
1385
|
+
return 0;
|
|
1386
|
+
}
|
|
1387
|
+
/** La vague vivante, pour `--answer` seul (sans --resume) : la même recherche que `--drop`. */
|
|
1388
|
+
function answerOnly(args, io) {
|
|
1389
|
+
if (args.drop.length || args.stopAfterCurrent || args.budget !== undefined || args.dryRun || args.continue || args.lots.length)
|
|
1390
|
+
throw new RafError("--answer ne se combine pas avec --drop, --stop-after-current, --budget, --dry-run, --continue ni des lots à lancer (sur une vague arrêtée : --resume --answer)");
|
|
1391
|
+
const store = liveStore('--answer', args, io);
|
|
1392
|
+
if (typeof store === 'number') {
|
|
1393
|
+
io.err(`orchestrate : --answer : une vague arrêtée se reprend avec ${resumeCommand('<vague>', '--answer', '<projet>:<lot>', '"…"')}`);
|
|
1394
|
+
return store;
|
|
1395
|
+
}
|
|
1396
|
+
return answerLive(args, io, store);
|
|
1397
|
+
}
|
|
1398
|
+
/** La vague vivante lancée depuis ce dossier que vise `--wave` (ou la seule) ; sinon le refus (code 2) déjà écrit. */
|
|
1399
|
+
function liveStore(flags, args, io) {
|
|
1316
1400
|
const live = liveWaves(cadenceHome()).filter((w) => w.cwd === io.cwd && (args.wave === undefined || w.wave === args.wave));
|
|
1317
1401
|
if (live.length === 0) {
|
|
1318
1402
|
io.err(`orchestrate : ${flags} : aucune vague en cours${args.wave ? ` (${args.wave})` : ''} lancée depuis ce dossier`);
|
|
@@ -1327,6 +1411,16 @@ function control(args, io) {
|
|
|
1327
1411
|
io.err(`orchestrate : ${flags} : dossier d'état de la vague ${live[0].wave} introuvable`);
|
|
1328
1412
|
return 2;
|
|
1329
1413
|
}
|
|
1414
|
+
return store;
|
|
1415
|
+
}
|
|
1416
|
+
/** `--drop` / `--stop-after-current` (L79) : une demande écrite dans le dossier d'état de la vague vivante ; c'est elle qui la lit avant sa prochaine session. */
|
|
1417
|
+
function control(args, io) {
|
|
1418
|
+
const flags = '--drop / --stop-after-current';
|
|
1419
|
+
if (args.lots.length || args.continue || args.dryRun || args.budget !== undefined || args.answers.length)
|
|
1420
|
+
throw new RafError(`${flags} ne se combinent pas avec des lots à lancer, --continue, --dry-run, --budget ou --answer`);
|
|
1421
|
+
const store = liveStore(flags, args, io);
|
|
1422
|
+
if (typeof store === 'number')
|
|
1423
|
+
return store;
|
|
1330
1424
|
const lots = store.lots();
|
|
1331
1425
|
const refusals = [];
|
|
1332
1426
|
const keys = [];
|
|
@@ -1427,6 +1521,9 @@ async function resume(args, argv, io, deps, launch, today) {
|
|
|
1427
1521
|
io.err(`orchestrate : vague ${store.id} sans instantané (antérieure à L61) : reprise avec le dist/ et les gabarits courants`);
|
|
1428
1522
|
}
|
|
1429
1523
|
}
|
|
1524
|
+
// L407 : la forme documentée sur une vague qui tourne encore dépose la réponse dans sa file au lieu de refuser ; avec une autre option de reprise, le refus ci-dessous reste.
|
|
1525
|
+
if (args.answers.length && !args.lotBudgets.length && !args.drop.length && !args.integrate.length && args.budget === undefined && !args.continue && liveWaves(cadenceHome()).some((w) => w.wave === store.id && w.cwd === launch && w.pid !== process.pid))
|
|
1526
|
+
return answerLive(args, io, store);
|
|
1430
1527
|
const wave = store.readWave();
|
|
1431
1528
|
const refusals = [];
|
|
1432
1529
|
const info = deps.claudeInfo();
|
|
@@ -1030,7 +1030,7 @@ async function session(c, kind, local = false) {
|
|
|
1030
1030
|
save(c);
|
|
1031
1031
|
return { step, report: res.structured, before, after, others };
|
|
1032
1032
|
}
|
|
1033
|
-
/** Les dépôts tiers d'un lot (L456) : frères du dépôt du lot, hors voisins déclarés et dépôts des autres lots de la vague, ou
|
|
1033
|
+
/** Les dépôts tiers d'un lot (L456) : frères du dépôt du lot, hors voisins déclarés et dépôts des autres lots de la vague (leur branche principale bouge légitimement, R14). Une vague lancée après ce relevé, comme le lead ou une livraison, n'est pas dans la liste : seuls les commits qui citent un lot de cette vague sont constatés (L458) ; exclure les vagues vivantes déjà connues évite qu'un identifiant de lot commun (deux projets, chacun son L1) fasse passer leurs commits pour les nôtres. */
|
|
1034
1034
|
function thirdRepos(c) {
|
|
1035
1035
|
const mine = lotKey(c.lot.project, c.lot.lot);
|
|
1036
1036
|
const others = [...c.wave.store.lots().filter((x) => lotKey(x.project, x.lot) !== mine), ...c.wave.store.liveWaveLots()].flatMap(lotRepoPaths);
|
|
@@ -1060,7 +1060,7 @@ function checkTiers(c, kind, step, before) {
|
|
|
1060
1060
|
w.log(`${key} · dépôt tiers ${name} ignoré : aucune branche principale`);
|
|
1061
1061
|
}
|
|
1062
1062
|
else {
|
|
1063
|
-
const moved = tierMoved(repo, b, a,
|
|
1063
|
+
const moved = tierMoved(repo, b, a, w.store.lots().map((x) => x.lot));
|
|
1064
1064
|
if (moved) {
|
|
1065
1065
|
step.status = 'failed';
|
|
1066
1066
|
step.cause = `commit sur la branche principale d'un autre dépôt (${name}, ${moved.branch})`;
|
package/dist/orchestrate/pool.js
CHANGED
|
@@ -25,14 +25,7 @@ export function groupLots(ctxs) {
|
|
|
25
25
|
}
|
|
26
26
|
return groups.map((g) => g.lots);
|
|
27
27
|
}
|
|
28
|
-
|
|
29
|
-
* Ordonnanceur : une file par dépôt, les lots d'un dépôt l'un après l'autre dans l'ordre donné, `slots` files au plus en parallèle. Deux lots qui partagent un dépôt
|
|
30
|
-
* sont dans la même file : jamais deux sessions dans un même dépôt (le plafond global des sessions de toutes les vagues est tenu par command.ts).
|
|
31
|
-
* `parallel(groupe)` (L279) : les lots de ce groupe sont chacun leur propre file — ils jouent ensemble, chacun dans son worktree, dans la limite de `slots` ;
|
|
32
|
-
* un lot qui dépend d'un lot du groupe attend qu'il soit fini (ready après son intégration) avant de partir, et le lot en attente d'une place n'est pas commencé
|
|
33
|
-
* (son worktree n'existe pas encore, R3). `onSettled` est appelé quand un lot a fini de jouer (L132 : command.ts libère alors les dépôts que plus aucun lot ne tient). Le budget, le quota et les incidents sont vérifiés par le cycle avant chaque session.
|
|
34
|
-
*/
|
|
35
|
-
export async function runPool(ctxs, slots = SLOTS, known = [], onSettled = () => { }, parallel = () => false) {
|
|
28
|
+
export async function runPool(ctxs, slots = SLOTS, known = [], onSettled = () => { }, parallel = () => false, answers) {
|
|
36
29
|
const pending = groupLots(ctxs).flatMap((g) => (parallel(g) ? g.map((c) => [c]) : [g]));
|
|
37
30
|
// Les lots pas encore finis de jouer (en file ou en cours) : un lot ne part pas tant que l'un d'eux est sa dépendance.
|
|
38
31
|
const live = new Set(ctxs);
|
|
@@ -82,12 +75,28 @@ export async function runPool(ctxs, slots = SLOTS, known = [], onSettled = () =>
|
|
|
82
75
|
const dep = ctxs.find((o) => o.lot.project === queue[0].lot.project && o.lot.lot === id);
|
|
83
76
|
return !!dep && live.has(dep) && dep.lot.status !== 'ready';
|
|
84
77
|
});
|
|
85
|
-
const running = new
|
|
78
|
+
const running = new Map();
|
|
86
79
|
const start = (queue) => {
|
|
87
80
|
const p = runQueue(queue).finally(() => running.delete(p));
|
|
88
|
-
running.
|
|
81
|
+
running.set(p, queue);
|
|
82
|
+
};
|
|
83
|
+
// Sans worktree, un lot repris ne joue pas pendant qu'un autre lot du même dépôt joue, attend un créneau ou vient d'être accepté dans ce même tour (jamais deux sessions dans un dépôt).
|
|
84
|
+
const admitted = [];
|
|
85
|
+
const sameRepo = (a, b) => lotRepoPaths(a.lot).some((r) => lotRepoPaths(b.lot).includes(r));
|
|
86
|
+
const canRun = (c) => {
|
|
87
|
+
if (c.tree)
|
|
88
|
+
return true;
|
|
89
|
+
if ([...running.values(), ...pending].some((q) => q.some((o) => sameRepo(c, o))) || admitted.some((o) => sameRepo(c, o)))
|
|
90
|
+
return false;
|
|
91
|
+
admitted.push(c);
|
|
92
|
+
return true;
|
|
89
93
|
};
|
|
90
94
|
for (;;) {
|
|
95
|
+
admitted.length = 0;
|
|
96
|
+
for (const c of answers?.take(canRun) ?? []) {
|
|
97
|
+
live.add(c);
|
|
98
|
+
pending.push([c]);
|
|
99
|
+
}
|
|
91
100
|
while (running.size < slots) {
|
|
92
101
|
const next = pending.findIndex((q) => !blocked(q));
|
|
93
102
|
if (next < 0)
|
|
@@ -101,6 +110,12 @@ export async function runPool(ctxs, slots = SLOTS, known = [], onSettled = () =>
|
|
|
101
110
|
start(pending.shift());
|
|
102
111
|
continue;
|
|
103
112
|
}
|
|
104
|
-
|
|
113
|
+
if (!answers)
|
|
114
|
+
await Promise.race(running.keys());
|
|
115
|
+
else {
|
|
116
|
+
let timer;
|
|
117
|
+
await Promise.race([...running.keys(), new Promise((res) => (timer = setTimeout(res, answers.pollMs)))]);
|
|
118
|
+
clearTimeout(timer);
|
|
119
|
+
}
|
|
105
120
|
}
|
|
106
121
|
}
|
|
@@ -117,6 +117,19 @@ export function waveReadsControl(waveDir) {
|
|
|
117
117
|
return false;
|
|
118
118
|
}
|
|
119
119
|
}
|
|
120
|
+
/**
|
|
121
|
+
* La vague lit-elle la file de réponses `answers.log` (L407) ? Une vague lancée avant L407 ne la lirait jamais : la réponse y dormirait. Sans instantané : on suppose que oui.
|
|
122
|
+
*/
|
|
123
|
+
export function waveReadsAnswerQueue(waveDir) {
|
|
124
|
+
if (!snapshotExists(waveDir))
|
|
125
|
+
return true;
|
|
126
|
+
try {
|
|
127
|
+
return readFileSync(join(toolDirOf(waveDir), 'dist', 'orchestrate', 'state.js'), 'utf8').includes('answers.log');
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
return false;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
120
133
|
/**
|
|
121
134
|
* L'instantané de la vague connaît-il `--budget projet:lot=+N` (L223) ? Un instantané plus ancien relit l'argument avec son `parseBudget` et le refuse
|
|
122
135
|
* comme budget invalide. Sans instantané : rien à relire, on suppose que oui.
|
|
@@ -167,6 +167,42 @@ export class RunStore {
|
|
|
167
167
|
const kept = readFileSync(f, 'utf8').split('\n').filter((l) => l.trim() !== '' && l.trim() !== 'stop-after-current');
|
|
168
168
|
writeFileSync(f, kept.map((l) => `${l}\n`).join(''));
|
|
169
169
|
}
|
|
170
|
+
/**
|
|
171
|
+
* File des réponses du lead à une vague vivante (L407) : `answers.log`, en ajout seul, une ligne JSON par réponse déposée (`{id, lot, text}`) ou par réponse prise (`{taken: id}`).
|
|
172
|
+
* La vague la relit entre deux étapes ; une réponse prise ou écartée n'y figure plus. Une ligne illisible est ignorée. Pas de `.json` : `lots()` le prendrait pour un lot.
|
|
173
|
+
*/
|
|
174
|
+
answerQueue() {
|
|
175
|
+
const f = join(this.dir, 'answers.log');
|
|
176
|
+
if (!existsSync(f))
|
|
177
|
+
return [];
|
|
178
|
+
const entries = [];
|
|
179
|
+
const taken = new Set();
|
|
180
|
+
for (const line of readFileSync(f, 'utf8').split('\n')) {
|
|
181
|
+
try {
|
|
182
|
+
const o = JSON.parse(line);
|
|
183
|
+
if (typeof o.taken === 'number')
|
|
184
|
+
taken.add(o.taken);
|
|
185
|
+
else if (typeof o.id === 'number' && typeof o.lot === 'string' && typeof o.text === 'string')
|
|
186
|
+
entries.push({ id: o.id, lot: o.lot, text: o.text });
|
|
187
|
+
}
|
|
188
|
+
catch {
|
|
189
|
+
// ligne vide ou illisible : ignorée
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
return entries.filter((e) => !taken.has(e.id));
|
|
193
|
+
}
|
|
194
|
+
/** Dépose une réponse dans la file ; rend son numéro. */
|
|
195
|
+
requestAnswer(lot, text) {
|
|
196
|
+
const f = join(this.dir, 'answers.log');
|
|
197
|
+
const last = existsSync(f) ? [...readFileSync(f, 'utf8').matchAll(/^\{"id":(\d+),/gm)].map((m) => Number(m[1])) : [];
|
|
198
|
+
const id = Math.max(0, ...last) + 1;
|
|
199
|
+
appendFileSync(join(this.dir, 'answers.log'), `${JSON.stringify({ id, lot, text })}\n`);
|
|
200
|
+
return id;
|
|
201
|
+
}
|
|
202
|
+
/** La réponse `id` est prise (jouée, écartée ou retirée par son auteur) : elle ne sera plus relue. */
|
|
203
|
+
markAnswerTaken(id) {
|
|
204
|
+
appendFileSync(join(this.dir, 'answers.log'), `${JSON.stringify({ taken: id })}\n`);
|
|
205
|
+
}
|
|
170
206
|
journal(line) {
|
|
171
207
|
appendFileSync(join(this.dir, 'journal.log'), `${new Date().toISOString()} ${line}\n`);
|
|
172
208
|
}
|
|
@@ -87,20 +87,16 @@ export function takeTiers(repos, log) {
|
|
|
87
87
|
}
|
|
88
88
|
/** Plus de commits que cela dans le constat : le reste est compté. */
|
|
89
89
|
const SHOWN = 5;
|
|
90
|
-
/**
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
function isLeadCommit(repo, line, planFile) {
|
|
94
|
-
if (!LEAD_SUBJECT.test(line))
|
|
95
|
-
return false;
|
|
96
|
-
const files = (tryGit(repo, 'diff-tree', '--root', '--no-commit-id', '--name-only', '-r', line.split(' ')[0]) ?? '').split('\n').filter(Boolean);
|
|
97
|
-
return files.length > 0 && files.every((f) => f === planFile);
|
|
90
|
+
/** Le sujet d'un commit cite-t-il l'un de ces lots ? Identifiant entier : « L1 » n'est ni dans « L10 » ni dans « L1.2 », mais l'est dans « feat(L1): » ou « demo:L1 ». */
|
|
91
|
+
export function citesLot(subject, lots) {
|
|
92
|
+
return lots.some((id) => new RegExp(`(?<![\\w.-])${id.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}(?![\\w-]|\\.\\w)`).test(subject));
|
|
98
93
|
}
|
|
99
94
|
/**
|
|
100
95
|
* Première branche principale dont le sha a bougé entre deux relevés, avec ce qu'il faut en dire : les commits ajoutés (sha court et sujet), ou la branche créée,
|
|
101
|
-
* supprimée ou déplacée sans commit nouveau
|
|
96
|
+
* supprimée ou déplacée sans commit nouveau. Seuls comptent les commits dont le sujet cite un lot de la vague (`lots`, L458) : une livraison (bump de tag de deploy.sh), un commit du lead
|
|
97
|
+
* ou d'une autre vague ne citent aucun d'eux et passent. Une branche absente d'un seul côté a bougé (créée ou supprimée pendant la session). Null : rien n'a bougé.
|
|
102
98
|
*/
|
|
103
|
-
export function tierMoved(repo, before, after,
|
|
99
|
+
export function tierMoved(repo, before, after, lots) {
|
|
104
100
|
for (const branch of [...new Set([...Object.keys(before.mains), ...Object.keys(after.mains)])].sort()) {
|
|
105
101
|
const b = before.mains[branch];
|
|
106
102
|
const a = after.mains[branch];
|
|
@@ -111,9 +107,9 @@ export function tierMoved(repo, before, after, planFile) {
|
|
|
111
107
|
// branche créée : seuls comptent les commits que aucune branche principale d'avant ne portait déjà
|
|
112
108
|
const range = b ? [`${b}..${a}`] : [a, '--not', ...Object.values(before.mains)];
|
|
113
109
|
const all = (tryGit(repo, 'log', '--abbrev=7', '--format=%h %s', ...range) ?? '').split('\n').filter(Boolean);
|
|
114
|
-
const lines = all.filter((l) =>
|
|
110
|
+
const lines = all.filter((l) => citesLot(l.slice(l.indexOf(' ') + 1), lots));
|
|
115
111
|
if (all.length && !lines.length)
|
|
116
|
-
continue; //
|
|
112
|
+
continue; // aucun commit ne cite un lot de la vague
|
|
117
113
|
if (!lines.length && !b)
|
|
118
114
|
return { branch, detail: `branche créée (${a.slice(0, 7)})` };
|
|
119
115
|
if (!lines.length)
|
package/docs/cadence-hud.md
CHANGED
|
@@ -93,7 +93,7 @@ Before the first reading the panel says `Lecture des plans…`; if `cadence lead
|
|
|
93
93
|
|
|
94
94
|
**Last delivery of each project (L415).** Under the lots in progress, `Dernière livraison par projet (N)` shows one line per project that has delivered: `✔ finance 3f9a1c2 · il y a 2 j · verte · 2 lots clos non livrés : L12, L13`, or `✖ … · rouge` in red when the last delivery failed; `rien à livrer` when every closed lot was delivered, and nothing after `verte` when the plan is not known. It reads the journal `cadence deliver` writes (`.git/cadence/deliveries.log`, one `day sha` line per green delivery, and `deliveries-failed.log`) through the `LAST_DELIVERIES` script (python3, the folder and each direct sub-folder whose `.git` is a folder, like the failed-delivery script; run at each 5 s refresh **while the panel is open**): the last delivery is the red one when `deliveries-failed.log` was modified after `deliveries.log` (the files' modification date decides, as for the failed deliveries), the green one otherwise; a repository with no readable line is absent. The age counts days from the journal's day (the journal has no hour): `aujourd’hui`, `hier`, `il y a N j`. **Closed lots not delivered**: `cadence lead tour --json` gives each project row `closed`, the closed lots (`done`, archived ones included, lots with an `every:` left out) as `{ id, finished }`; a lot is delivered when a green delivery happened on the day it was closed or after (the rule of the delivery delay in `cadence perf`), so it is not delivered when closed **after the day of the last green delivery** — a red delivery delivers nothing, and a project that never delivered green counts all its closed lots. A project absent from the tour, in error, or read by a `cadence` too old to give `closed` shows no count (not « rien à livrer »: we do not know). Order: red first, then the most undelivered lots, then the name. Height (`deliveriesHeight`): the title and one line per project, within what the decisions and the lots in progress leave of the half of the body, `… +N projets` when they do not all fit, nothing under two lines; the tree keeps the rest. Calculation: `hooks/deliveries.ts` (`deliveriesOf`, `buildDeliveries`, `parseLastDeliveries`). Tested by `tests/deliveries.test.ts` (L415 R4 to R11), `tests/lots-pane.test.ts` (R12 to R14, panel mounted), `test/hud-livraisons-l415.test.ts` (R2, R3: the real script on real journals) and `test/lead-tour-lots.test.ts` (R1: the `closed` field).
|
|
95
95
|
|
|
96
|
-
**What is waiting for a decision (L412).** Above the tree, a block `Ce qui attend une décision (N)` gathers what the person has to settle; it exists only when there is something to settle. It reads the band's waves (live ones, and the last finished one only when none is live), the finished waves of the folder that the collector returns apart in `ended` (whether a wave is live or not, without the band's 30-minute delay: **all** the finished waves of the last 7 days, with no cap on their number (a cost guard of 500 waves, far above the 162 waves per 7 days measured, is not a rule), newest first by the date of their `wave.json` — a question or a handed-back lot waits as long as the plan keeps the lot open and its wave is less than 7 days old (beyond that, the decision leaves the block even if the lot is still open in the plan), a newer wave finishing without that lot does not erase it; a live wave is not among them: its `wave.json`, always the most recent of the folder since orchestrate rewrites `consumed` in it, is skipped by the collector; the band and the tree keep reading `waves` only, their drawing does not change) and the failed deliveries; a lot counts once, in the first wave carrying it, live waves first then the finished ones newest first (taken over in a newer wave, that wave decides). Four kinds: `? project:lot <questions>` (a lot in the `question` state; the short text is the first line of each question, joined by ` / `) with, below it in grey, the command that answers it (`cd <launch folder of the wave> && cadence orchestrate --resume --wave <wave> --answer project:lot "…"`, the one the end-of-wave table prints, preceded by a `cd` to the wave's `cwd`: orchestrate finds the wave only from there) — while the wave is live
|
|
96
|
+
**What is waiting for a decision (L412).** Above the tree, a block `Ce qui attend une décision (N)` gathers what the person has to settle; it exists only when there is something to settle. It reads the band's waves (live ones, and the last finished one only when none is live), the finished waves of the folder that the collector returns apart in `ended` (whether a wave is live or not, without the band's 30-minute delay: **all** the finished waves of the last 7 days, with no cap on their number (a cost guard of 500 waves, far above the 162 waves per 7 days measured, is not a rule), newest first by the date of their `wave.json` — a question or a handed-back lot waits as long as the plan keeps the lot open and its wave is less than 7 days old (beyond that, the decision leaves the block even if the lot is still open in the plan), a newer wave finishing without that lot does not erase it; a live wave is not among them: its `wave.json`, always the most recent of the folder since orchestrate rewrites `consumed` in it, is skipped by the collector; the band and the tree keep reading `waves` only, their drawing does not change) and the failed deliveries; a lot counts once, in the first wave carrying it, live waves first then the finished ones newest first (taken over in a newer wave, that wave decides). Four kinds: `? project:lot <questions>` (a lot in the `question` state; the short text is the first line of each question, joined by ` / `) with, below it in grey, the command that answers it (`cd <launch folder of the wave> && cadence orchestrate --resume --wave <wave> --answer project:lot "…"`, the one the end-of-wave table prints, preceded by a `cd` to the wave's `cwd`: orchestrate finds the wave only from there) — the same command while the wave is live (L407: orchestrate then drops the answer in the wave's queue, which the live wave reads between two steps, instead of refusing « la vague tourne encore »); `↩ project:lot <cause>` (a `handed-back` or `failed` lot: the first line of its outcome, the status in words when empty; a `suspended` lot waits for nothing from the person), in red; `◐ project:lot verdict UX : <verdict>` (a `ready`, visible lot whose review returned a verdict) with `cd <repository of the lot> && raf ux <lot> "verdict"` below (the `repo` of the lot state: from the parent folder raf answers « pas de plan », from another project it would record the verdict on that project's lot of the same id) — the commands to copy start with `cd` and run as they are from any folder, a path with a space or a quote is quoted for sh; `✖ project livraison : <short sha> · <reason> (<day>)` in red (7 characters: deliver writes the full 40-character sha in the log). The block never takes more than half of the body (3 lines at least): past that, the first decisions that fit, then `… +N décisions de plus`. Failed deliveries come from the `DELIVERIES` script (collect.ts, python3), run at every 5 s refresh **while the panel is open**: for the folder and each direct sub-folder whose `.git` is a directory, the last line of `.git/cadence/deliveries-failed.log`, unless `deliveries.log` was modified after it (a green delivery clears the failure; file modification times decide, the day on the line cannot order two writes of the same day). The plan decides: the panel's board (read from `cadence lead tour --json`) drops any decision whose lot is no longer open in a project the tour read, so a UX verdict recorded then `raf done`, or a handed-back lot taken over and closed, disappears at the next plan read; a project missing from the board, in error or without lots keeps its decisions. The command to copy is wrapped over several lines at the panel's width, never cut, every line but the last ending with the shell continuation ` \` (`\` glued to the piece when a word longer than the line is cut): pasted as they are into a shell, the lines run one command, the whole one (without it, a first line `cadence orchestrate --resume --wave <wave>` ran alone, without its `--answer`); the wrapped lines count in the block's ceiling. Details in the plugin's README.
|
|
97
97
|
|
|
98
98
|
The data is the `lots` field of `cadence lead tour --json` ([lead tour](lead.md#lead-tour)), read together with the band's `progress` (same call, same folder). While the panel is open, the modification date of each plan (`.cadence/raf.yaml` and `cadence.yaml` of the folder and of its direct sub-folders) is read every 5 s, one `stat` each: a changed plan, like a wave transition, triggers a new reading before the minute is over. Cost of the `lots` field (R18, measured on 2026-10-28 with `cadence lead tour --json` on the parent folder of ten projects, three passes each): before L401 (cadence 1.19.1, no `lots`) 8.0 / 9.1 / 7.3 s and 86 KB of output; after (the L401 code) 7.5 / 7.0 / 8.4 s and 188 KB (192,503 bytes). The output more than doubles in size but the duration stays in the same order, under the 20 s refresh delay of the band, so the rule holds.
|
|
99
99
|
|
package/docs/lead.md
CHANGED
|
@@ -55,7 +55,9 @@ picture, not its source.
|
|
|
55
55
|
question from a session comes back to the human at once; the answer goes in with
|
|
56
56
|
`cadence orchestrate --resume --wave <wave> --answer <project>:<lot> "…"` (the wave is named: the
|
|
57
57
|
command is printed under each question of the table, ready to run; without a wave, `--resume` is refused as
|
|
58
|
-
soon as the folder keeps several unfinished waves).
|
|
58
|
+
soon as the folder keeps several unfinished waves). While the wave is still running the same command (or
|
|
59
|
+
`cadence orchestrate --answer <project>:<lot> "…"`) queues the answer and the live wave replays the lot at
|
|
60
|
+
once, whatever the other lots do (L407). The lead never answers in the
|
|
59
61
|
human's place. `--drop` and `--stop-after-current` steer a wave that is running.
|
|
60
62
|
4. **Review.** The final table of the wave lists the lots ready, handed back, and the line
|
|
61
63
|
« livrable jusqu'à <sha> » for a ready lot stacked under a handed-back one. Minor findings come back in
|
package/docs/orchestrate.md
CHANGED
|
@@ -35,6 +35,7 @@ cadence orchestrate --resume … --dry-run # refused: a
|
|
|
35
35
|
# a resume running on the wave's snapshot receives `--resume <wave>` in place of `--wave <wave>`, so an older snapshot cannot pick another wave; a wave whose id looks like a lot (`suite2`) cannot be named that way, so it is resumed with the current `dist/` and templates
|
|
36
36
|
cadence orchestrate --drop ccc:L29 [--wave <wave>] # from another terminal: take a lot out of the running wave
|
|
37
37
|
cadence orchestrate --stop-after-current [--wave <wave>] # from another terminal: let the running sessions finish, start nothing else
|
|
38
|
+
cadence orchestrate --answer ol-companion:L22 "reply" [--wave <wave>] # from another terminal: answer a lot of the running wave that is waiting for it (below)
|
|
38
39
|
```
|
|
39
40
|
|
|
40
41
|
**Claude Code must be installed and logged in** (L378). Before starting a lot, `cadence orchestrate` runs `claude auth status` (no model, no cost); when it says `loggedIn: false` the command is refused (exit 2) before anything is written: no `raf start`, no plan commit, the lot stays `todo`. The message says what to do: « claude n'est pas connecté : lancer `claude` puis /login (état : `claude auth status`) ». `--dry-run` starts no session, so it is not refused: it prints the same message after its first line, as a `⚠`. `claude auth status` is read once per launch, so with `--continue` the rounds of a wave do not run it again for every candidate lot. An unreadable answer (a `claude` without `auth status`) is not a refusal.
|
|
@@ -60,6 +61,8 @@ this way is left out of the resume checks (repository lock, `.nvmrc` Node, dirty
|
|
|
60
61
|
refuse the resume, and a refusal writes nothing to `control.log`.
|
|
61
62
|
`--resume --wave <wave> --stop-after-current` is refused (exit 2): resuming contradicts stopping.
|
|
62
63
|
|
|
64
|
+
**Answering on a live wave (L407).** `--answer <project:lot> "…"` (from the folder that launched the wave, `--wave` when several are live) no longer waits for the wave to end: on 28-10 a lot waited about 15 minutes for its answer because another lot of the wave was still running, and `--resume` refuses a wave that is running. The answer is appended to the wave's queue (`answers.log` in `.cadence/runs/<wave>/`, one JSON line per answer and per answer taken), the command prints `<lot> : réponse déposée` and returns at once. The live wave reads the queue every 2 seconds and each time a lot finishes, applies the answer to the lot (the same state as `--resume --answer`: the answer joins the brief of its next session) and replays it as one more line of the pool, as soon as a session slot is free; a lot with no worktree never plays together with another lot of its repository. The documented form `--resume --wave <wave> --answer …` does the same when that wave is still running (it used to be refused with `tourne encore`); combined with another resume option (`--budget`, `--drop`, `--integrate`, `--continue`) it is still refused. Refused (exit 2) before anything is written: a lot that is unknown, that several projects carry, or that is not waiting for an answer (`question`); no live wave in that folder (the message gives the `--resume --wave <wave> --answer` form for a stopped wave); a wave started before L407, whose snapshot does not read the queue. `--answer` alone does not combine with `--drop`, `--stop-after-current`, `--budget`, `--dry-run`, `--continue` or lots to launch. An answer whose lot is no longer waiting when the wave reads it (dropped, or already answered by an earlier line) is discarded and the journal says so (`réponse écartée`), it is never played twice; if the wave finishes between the check and the write, the answer is withdrawn and the command says to use `--resume`. Not covered: a lot that depends (`after`) on the answered lot and was left waiting in the pool is not started by this wave when the answer makes the lot ready; `--resume` plays it.
|
|
65
|
+
|
|
63
66
|
**`--goal "…"` and `goal:` — the objective of the briefs when the title is stale (L258)**: the first line of the `{{objectif}}` of every
|
|
64
67
|
brief of a lot (and the title quoted in their first sentence) is the lot's title; when a change of scope made it stale, the lead no longer
|
|
65
68
|
has to delegate by hand. Three ways to give the goal, in this order of precedence (`--goal`, then the plan's key, then the title):
|
|
@@ -999,10 +1002,13 @@ main branch of any other repository**, whether the lot's text cites it or not: o
|
|
|
999
1002
|
`wip/<project>-<lot>` in that repository, commit on it, never push, and say it under « choix ».
|
|
1000
1003
|
|
|
1001
1004
|
**What the program checks.** Before and after each writing session it reads, with no network (one `git for-each-ref` per repository, no `ls-remote`, no `fetch`),
|
|
1002
|
-
the sha of the main branches of the other repositories. The main branches of a repository are, among its local branches, the one `origin/HEAD` names, `main` and
|
|
1005
|
+
the sha of the main branches of the other repositories (the commits it reports are filtered, see below). The main branches of a repository are, among its local branches, the one `origin/HEAD` names, `main` and
|
|
1003
1006
|
`master`; one that moves is enough. When one moved, the lot is handed back to the lead (`handed-back`, the step `failed`) before the checks and the next lot, with
|
|
1004
1007
|
a finding: `incident : commit sur la branche principale d'un autre dépôt (<repository>, <branch>) pendant <project>:<lot> (<step>) : <short sha> <subject> ; la voie
|
|
1005
|
-
est la branche wip/<project>-<lot>`.
|
|
1008
|
+
est la branche wip/<project>-<lot>`. **Only a commit whose subject cites a lot of the wave is reported** (`feat(L1): chart cluster-sante`; the whole identifier counts, `L10` and `L1.2` do not cite `L1`; the
|
|
1009
|
+
body of the message is not read). Every other commit on that main branch passes: a delivery (the tag bump of a `deploy.sh` in a GitOps repository), a commit of the lead
|
|
1010
|
+
(`plan: …`), a commit of another wave, whenever it was launched, a commit that cites no lot. The rule needs no registry of the waves, so it holds
|
|
1011
|
+
for a wave started after the session began. The repository is marked dirty for the wave. A branch created or deleted during the session counts as a moved sha. The comparison
|
|
1006
1012
|
is on the branch, not on `HEAD`: a commit made then left with `HEAD` elsewhere is found too. The program **does not undo the commit** (no reset, no revert, no move of
|
|
1007
1013
|
the branch): it states and hands back, the lead decides. The check also runs when the session ends in failure, at the time limit or at the quota, not only on a
|
|
1008
1014
|
successful one: otherwise the next attempt would take the moved sha as its reference and the commit would never be named.
|
|
@@ -1014,10 +1020,12 @@ That makes every sibling repository, cited by the lot or not.
|
|
|
1014
1020
|
|
|
1015
1021
|
- the lot's own repository, and the neighbours declared by `repos:`: they keep their behaviour (the session commits on their current branch, a moved `HEAD` is put back
|
|
1016
1022
|
with a warning, `raf commits` and the review read them as before);
|
|
1017
|
-
- the repositories of the **other lots of the same wave**, and their neighbours
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1023
|
+
- the repositories of the **other lots of the same wave**, and their neighbours: their main branch moves legitimately through their own sessions and their integration, and
|
|
1024
|
+
those commits cite the lots of the wave; likewise those of the lots of a wave **running at the same time** that the registry shows when the session starts (two projects
|
|
1025
|
+
can both have a `L1`: the citation alone would take the other wave's commits for the lot's own). A wave launched later is not in that list: its commits pass because
|
|
1026
|
+
they cite no lot of this wave;
|
|
1027
|
+
- the commits that **cite no lot of the wave** (see above): deliveries, the lead's `plan: ` commits, another wave's commits. A branch deleted, created or moved with no new
|
|
1028
|
+
commit is still reported;
|
|
1021
1029
|
- a repository two levels below the folder (`folder/group/repository`), a child that is not a git repository, and a **linked worktree** (a child whose `.git` is a file);
|
|
1022
1030
|
- a symbolic link to a repository already counted (each repository is counted once, under its real path), and the folder itself when it is a git repository;
|
|
1023
1031
|
- a repository whose references cannot be read, or that has no main branch before and after the session: it is skipped with a line of the wave's log
|
package/docs/session.md
CHANGED
|
@@ -24,7 +24,10 @@ cadence session context # `ctx 42 % (84000/200000)`: the context of Y
|
|
|
24
24
|
# `seuil 150000 tokens : sous le seuil` or `… : atteint` (tokens ≥ threshold)
|
|
25
25
|
```
|
|
26
26
|
|
|
27
|
-
`lead.contextThreshold` is read in the `cadence.yaml` of the folder where `session context` runs
|
|
27
|
+
`lead.contextThreshold` is read in the `cadence.yaml` of the lead's folder: the folder where `session context` runs, or else the first
|
|
28
|
+
parent whose `cadence.yaml` carries a `lead:` or `priority:` key (run from a sub-project it no longer falls back on the default). When it
|
|
29
|
+
is read in a parent, the verdict line names the file: `seuil 500000 tokens : sous le seuil (lu dans /…/cadence.yaml)`. With no such
|
|
30
|
+
parent, the current folder and the default apply. The value is an integer ≥ 1,
|
|
28
31
|
in tokens, because a percentage moves with the window (60 % is 120k on 200k, 600k on 1M). A zero, a negative, a decimal or
|
|
29
32
|
a non-number is refused with the key's name (exit 2, read as at the threshold). It is not scaled to the window: on a window
|
|
30
33
|
smaller than the threshold the verdict never reads `atteint`, so set a lower one there.
|
package/docs/whats-new.md
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
## In two lines
|
|
6
6
|
|
|
7
|
+
- **1.30.0**: a question can be answered while its wave still runs; the sibling-repository check of 1.29.0 only flags commits that cite a lot of the wave; `session context` reads the lead's threshold from the lead's folder.
|
|
7
8
|
- **1.29.0**: a wave session can no longer commit on the main branch of another repository than its lot's (the lot is handed back with the finding); `verify --gate` no longer links data folders into its throwaway copy.
|
|
8
9
|
- **1.28.0**: projects are reordered from the lots panel or `cadence lead priority`; the lead's context threshold is in tokens; reviewers answer the lead in a few lines; the gate replays under plan commits when tests read the plan.
|
|
9
10
|
- **1.27.0**: `cadence corrections --classify` names the sub-cause of each code-bug pass and the dominant link of the chain, `cadence perf` shows it; the mockup review checks hierarchy, tokens, texts and extra accessibility.
|
|
@@ -56,6 +57,8 @@ Each is a paragraph in [docs/whats-new.md](whats-new.md) and a full section in [
|
|
|
56
57
|
|
|
57
58
|
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).
|
|
58
59
|
|
|
60
|
+
**1.30.0**: `cadence orchestrate --answer <project>:<lot> "…"` (or `--resume --wave <wave> --answer`) is accepted while the wave is still running (L407, `docs/orchestrate.md`): the answer goes into a queue the live wave reads, the lot takes a slot again under the wave's cap, plays in its own worktree when the mode is on, and never runs beside another session of the same repository; the registry counts it at once, so « créneaux libres » stays right. Before, the lead had to wait for the end of the wave. The check of 1.29.0 against commits on the main branch of a sibling repository (L456) now flags only the commits that cite a lot of the wave (L458): a delivery's tag bump in a GitOps repository, the lead's plan commits and the lots of another live wave launched later no longer hand a lot back — three false positives on 30-10. `cadence session context` reads `lead.contextThreshold` from the lead's folder, the first parent holding `lead:` or `priority:`, instead of the current folder (L457).
|
|
61
|
+
|
|
59
62
|
**1.29.0**: two incidents of 30-10 turned into mechanisms. A writing session of a wave no longer commits on the main branch of a repository its lot does not declare (L456, `docs/orchestrate.md`): the brief forbids it and names the way, a branch `wip/<project>-<lot>`, and at the end of each session the program compares the main branches of the sibling repositories of the folder; a commit found there hands the lot back with the repository, the branch and the shas, and never moves or reverts anything — the lead moves the commits. Commits of the lead (subject `plan:`, plan file only) and commits of lots of another live wave of the folder are tolerated; repositories declared by the lot (`repos:`) keep their behaviour. The case was a session that committed a chart on the production branch of a GitOps repository. `cadence verify --gate` no longer links the ignored folders of the main tree that are not dependencies (`data/`, `data-local/`, `.env`) into its throwaway copy (L455, `docs/deliver.md`): a test comparing paths by `realpath` went red in the copy and green in the main tree at the same sha, and stopped a delivery.
|
|
60
63
|
|
|
61
64
|
**1.28.0**: the lead spends less of its context, and the projects are ordered where they are read. `cadence lead priority <project> --before|--after <project>` rewrites the `priority:` list of the folder's `cadence.yaml`, comments kept, and the lots panel does the same by drag and drop or Alt+↑/↓ in a region « ordre des projets » above the tree (L449, `docs/lead.md`, `docs/cadence-hud.md`); a move it cannot write back faithfully (a name that prefixes another) is refused with its reason. The lead's context threshold is a number of tokens, `lead.contextThreshold` (150000 by default), read by `cadence session context` (L437). `ux-reviewer`, `doc-keeper` and the reviewers answer the lead in a few lines and keep the full report in the file named by `Report file:` (L452), and `raf` cuts the titles it echoes to 60 characters (L453). `orchestrate.gate.readsPlan` lets a project whose tests read the plan say so: `cadence verify --gate` then replays the suite under plan-only commits instead of taking over the wave's verdict (L450). A lot made `visible` by its own session during a wave now goes through the UX step instead of coming out ready without it (L454).
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sylad/cadence",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.30.0",
|
|
4
4
|
"description": "A small, repo-native working method: a versioned plan linked to your commits, a changelog with screenshots, session rituals and deliveries proven by their effect.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Sylvain Ladoire",
|