@sylad/cadence 0.18.0 → 0.19.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 +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +18 -14
- package/dist/cli.js +1 -1
- package/dist/orchestrate/command.js +30 -6
- package/dist/orchestrate/cycle.js +35 -10
- package/dist/orchestrate/pool.js +4 -2
- package/dist/orchestrate/registry.js +9 -0
- package/dist/session.js +3 -4
- package/package.json +1 -1
- package/skills/session-close/SKILL.md +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": "0.
|
|
11
|
+
"version": "0.19.0",
|
|
12
12
|
"source": "./",
|
|
13
13
|
"author": {
|
|
14
14
|
"name": "Sylvain Ladoire"
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
{
|
|
18
18
|
"name": "cadence-hud",
|
|
19
19
|
"description": "A status band above the Claude Code prompt: context, 5 h / 7 d quota windows, cost per model, agents of the session and the running cadence orchestrate waves. No CLI needed.",
|
|
20
|
-
"version": "0.
|
|
20
|
+
"version": "0.3.1",
|
|
21
21
|
"source": "./plugins/cadence-hud",
|
|
22
22
|
"author": {
|
|
23
23
|
"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": "0.
|
|
4
|
+
"version": "0.19.0",
|
|
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
|
@@ -24,7 +24,7 @@ Code prompt that shows the session's context, quota, cost and the orchestrate wa
|
|
|
24
24
|
|
|
25
25
|
## What's new
|
|
26
26
|
|
|
27
|
-
**0.18.0**: `cadence session next` prints what it records (issue #6); `cadence session close` covers the period since the last `session start` (or the last close) and says so in its title, `--since` still wins (issue #7). **0.17.0**: `cadence-hud` shows an `N cmd` segment next to the agents, the background commands of the session (Bash run in the background and Monitor, subagents' included), tracked from the tool results and ended by the task's notification or `TaskStop`. **0.16.0**: a lot can declare **neighbouring repositories** (`repos:` key of the lot, `{ path, cite }` for a repository shared between projects): `cadence orchestrate` refuses, locks and guards them, gives them to every session as `--add-dir`, and `raf commits`, `raf show` and the review verdict read the commits that cite the lot there too. **0.15.0**: `hook.autostart` in `cadence.yaml` (`warn` by default, `refuse` or `start`): a pre-commit hook that refuses, or starts in the same commit, a lot still todo that the commit cites; `cadence session start|close --all [--depth n]`, one section per project under a folder. **0.14.0**: `cadence lead tour`, the lead's morning table of every project in a folder, printed by a program (one line per project: lots in progress, gaps, last notes, next ready lot, repository) in place of one subagent per project; the `lead` skill calls it. **0.13.1**: `cadence-hud` takes back the segments that fit once a big one has dropped (reset times, then the per-model consumption, now short: `opus 1.2M sonnet 800k`); `CADENCE_HUD_AMBIGUOUS=1` for a terminal that draws `▰ ▱ ⚙ │ ↻` in one cell. **0.13.0**: `cadence orchestrate --status --watch` (a wave's table redrawn in the terminal, which
|
|
27
|
+
**0.19.0**: `cadence orchestrate` lifts a repository's lock and `pre-push` guard as soon as every lot of the wave that touches it is finished, so `cadence deliver` works there while the wave continues elsewhere (L132); a review that leaves a repository dirty hands back that lot only, suspends the lots still queued in that repository and keeps the other repositories running, its verdict reported instead of lost (L133); `cadence session close` re-run without a `session start` keeps the window since the last opening (L131); `cadence-hud` draws the lot rows of a wave in aligned columns on every surface (L134). **0.18.0**: `cadence session next` prints what it records (issue #6); `cadence session close` covers the period since the last `session start` (or the last close) and says so in its title, `--since` still wins (issue #7). **0.17.0**: `cadence-hud` shows an `N cmd` segment next to the agents, the background commands of the session (Bash run in the background and Monitor, subagents' included), tracked from the tool results and ended by the task's notification or `TaskStop`. **0.16.0**: a lot can declare **neighbouring repositories** (`repos:` key of the lot, `{ path, cite }` for a repository shared between projects): `cadence orchestrate` refuses, locks and guards them, gives them to every session as `--add-dir`, and `raf commits`, `raf show` and the review verdict read the commits that cite the lot there too. **0.15.0**: `hook.autostart` in `cadence.yaml` (`warn` by default, `refuse` or `start`): a pre-commit hook that refuses, or starts in the same commit, a lot still todo that the commit cites; `cadence session start|close --all [--depth n]`, one section per project under a folder. **0.14.0**: `cadence lead tour`, the lead's morning table of every project in a folder, printed by a program (one line per project: lots in progress, gaps, last notes, next ready lot, repository) in place of one subagent per project; the `lead` skill calls it. **0.13.1**: `cadence-hud` takes back the segments that fit once a big one has dropped (reset times, then the per-model consumption, now short: `opus 1.2M sonnet 800k`); `CADENCE_HUD_AMBIGUOUS=1` for a terminal that draws `▰ ▱ ⚙ │ ↻` in one cell. **0.13.0**: `cadence orchestrate --status --watch` (a wave's table redrawn in the terminal, which
|
|
28
28
|
stops with the wave) and the lead skill that names the wave and follows its journal; lot ids with `/`
|
|
29
29
|
(sub-tasks of a read-only plan) accepted by `orchestrate`; `cadence-hud`'s first line measured in
|
|
30
30
|
terminal cells and kept to ASCII; `publish.yml` on the v5 actions. **0.12.0**: the `cadence-hud` plugin (a status band above the Claude Code prompt),
|
|
@@ -414,7 +414,7 @@ URL. The agent then returns a draft for you to correct — it never writes the f
|
|
|
414
414
|
cadence session start # notes from the last close, lots in progress (silent ones flagged),
|
|
415
415
|
# work done since yesterday by lot, drift, repo state, 3 proposals
|
|
416
416
|
cadence session start --since "3 days ago" --idle 2
|
|
417
|
-
cadence session close # commits by lot since the last `session start
|
|
417
|
+
cadence session close # commits by lot since the last `session start`,
|
|
418
418
|
# commits without a lot, lots in progress with no commit in that window,
|
|
419
419
|
# drift, uncommitted / unpushed work; exit 1 while something is still open
|
|
420
420
|
cadence session close --since "2 days ago" # --since overrides the window
|
|
@@ -425,12 +425,13 @@ cadence session next "finish L3" "review L4" # shown by the next session star
|
|
|
425
425
|
cadence session next --clear # erase those notes, on purpose
|
|
426
426
|
```
|
|
427
427
|
|
|
428
|
-
`session start` records its time in the worktree's state (`session.json`, under the git dir)
|
|
429
|
-
`session close`
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
428
|
+
`session start` records its time in the worktree's state (`session.json`, under the git dir); the next
|
|
429
|
+
`session close` takes it as its window and names it in the section title: `Commits de la période (depuis
|
|
430
|
+
l'ouverture de 09:12)` (with the date when it is not today). A close never moves the boundary, refused
|
|
431
|
+
or not: run again without a `session start` in between (the `session-close` skill does, after fixing a
|
|
432
|
+
point), it still reports since that opening instead of an empty period. With no recorded opening, or an
|
|
433
|
+
unreadable file, the window is the current day, said as such (`depuis 2026-09-28 00:00`); `--since` is
|
|
434
|
+
quoted as given. (A `session.json` written by 0.18.0 after a close, `depuis la clôture de …`, is still read.)
|
|
434
435
|
|
|
435
436
|
On success `cadence session next` prints what it recorded (`2 lignes enregistrées pour la prochaine
|
|
436
437
|
ouverture`, then the lines as written, blank ones dropped), so `session-close` can report
|
|
@@ -504,7 +505,7 @@ An entry is proposed only if it could be measured entirely. Never proposed:
|
|
|
504
505
|
The command deletes nothing, never fails on the cleanup and does not change the exit code: the
|
|
505
506
|
`session-close` skill shows the list and removes the entries only after the human agrees.
|
|
506
507
|
|
|
507
|
-
The commands get `CADENCE_SINCE` and `CADENCE_TODAY`. `CADENCE_SINCE` is the `--since` as given; on `session close` without `--since` it is the instant of the last
|
|
508
|
+
The commands get `CADENCE_SINCE` and `CADENCE_TODAY`. `CADENCE_SINCE` is the `--since` as given; on `session close` without `--since` it is the instant of the last `session start` as an ISO 8601 UTC string (`2026-09-28T07:12:00.000Z`), or `<today> 00:00` when no mark exists.
|
|
508
509
|
The commands add facts and decide nothing: a failing command is reported and changes neither
|
|
509
510
|
the exit code nor the verdict.
|
|
510
511
|
|
|
@@ -880,7 +881,7 @@ the name `repos`). The effect, for `cadence orchestrate`:
|
|
|
880
881
|
- **Lock and guard**: the neighbour gets the repository lock (`orchestrate.lock`, so `cadence deliver` refuses it too, and no
|
|
881
882
|
other wave can hold it), the temporary `pre-push` hook, and the same after-session checks (push, tampered hook, a review
|
|
882
883
|
that changed it). Two lots that share a repository, as project or as neighbour, run one after the other, never two
|
|
883
|
-
sessions in one repository; `--status` lists the neighbours among the repositories a wave holds.
|
|
884
|
+
sessions in one repository; `--status` lists the neighbours among the repositories a wave still holds (a repository released once all its lots are finished no longer appears).
|
|
884
885
|
- **Sessions**: every session gets each neighbour as `--add-dir`, and the briefs name them: the implementation and the
|
|
885
886
|
corrections must commit there too, with a message that cites the lot (`feat(B64): …`), and never push; the reviewers read
|
|
886
887
|
the commits that cite the lot in the project **and** in each neighbour (`raf commits <lot>` lists them all; in a neighbour only the commits that cite the lot and are dated within the lot's period count, and also contain the `cite:` string when the lot declares one for it — a `fix(B64): …` outside the period, or one without the `cite:` string, is not listed.) A lot whose
|
|
@@ -899,7 +900,10 @@ verdict and screenshots, it does not record it), the push and the deliveries, on
|
|
|
899
900
|
`orchestrate.lock`, which also makes `cadence deliver` refuse that repository), not per folder. A wave is
|
|
900
901
|
refused only when one of its repositories is held by a live wave (`<repo> : une orchestration y est déjà en
|
|
901
902
|
cours`; the lock of a dead process is detected and cleared); two waves on different repositories run side by
|
|
902
|
-
side, even when started from the same parent folder.
|
|
903
|
+
side, even when started from the same parent folder. A repository is released **as soon as every lot of the wave that
|
|
904
|
+
touches it is finished** (ready, handed back or failed): its lock and its `pre-push` guard are lifted while the wave goes
|
|
905
|
+
on elsewhere, so `cadence deliver` accepts it (a repository with a lot still to play, a question or a suspended lot stays
|
|
906
|
+
held until the wave ends). They share a **cap on simultaneous sessions**, counted
|
|
903
907
|
across all live waves: 2 by default, `--max-sessions N` (or `CADENCE_MAX_SESSIONS=N`) to change it — give
|
|
904
908
|
every wave the same value: each wave counts ALL live sessions, whatever their slot, and waits while that
|
|
905
909
|
count has reached ITS OWN cap, so with different caps the highest one can push the total past the lowest
|
|
@@ -909,15 +913,15 @@ A slot or a registry entry is owned by a pid **and** its start time: a reused pi
|
|
|
909
913
|
identifiers are reserved atomically (`-2`, `-3` suffix when two waves start in the same minute; an existing
|
|
910
914
|
`--wave` is refused). The registry of live waves and the slots live under `~/.cadence/orchestrate/` (`CADENCE_HOME`
|
|
911
915
|
to move it): `cadence orchestrate --status` lists, from any folder, the live waves, the repositories each
|
|
912
|
-
holds, the cap of each wave and the slots in use.
|
|
916
|
+
still holds (those not yet released), the cap of each wave and the slots in use.
|
|
913
917
|
|
|
914
918
|
**Guards, imposed by the code**: a global cap of simultaneous sessions, one wave per repository at
|
|
915
919
|
a time (above); `Agent`, `git push`, `cadence deliver`, `raf done|review|ux` are denied to the sessions; a
|
|
916
|
-
temporary `pre-push` hook, installed for the duration of the wave and removed at its end, refuses any push
|
|
920
|
+
temporary `pre-push` hook, installed for the duration of the wave and removed at its end (or, for a repository, as soon as all the lots that touch it are finished), refuses any push
|
|
917
921
|
from a session (`CADENCE_ORCHESTRATED` is in their environment; a repository that already has another
|
|
918
922
|
`pre-push` hook is refused before anything starts — `pushurl` is never touched); after every session the
|
|
919
923
|
upstream ref and `git ls-remote` are compared with the "before", and a review that changed `HEAD` or the
|
|
920
|
-
tree is an incident that stops the wave. `raf done|ux|review` and `cadence deliver` refuse when
|
|
924
|
+
tree (screenshots left at the root, a commit) is an incident of that lot only: the lot is handed back to the lead, the lots of the other repositories carry on while the lots still queued behind it in the same repository are suspended (resumable, nothing started on a dirty tree), and a verdict the review had already produced is reported in the outcome and the lot's warnings (never recorded in the plan). A push or a removed guard hook still stops the wave. `raf done|ux|review` and `cadence deliver` refuse when
|
|
921
925
|
`CADENCE_ORCHESTRATED` is set.
|
|
922
926
|
|
|
923
927
|
**Budget**: the wave counts input + cache writes + output tokens (default 2 M); cache reads are kept and
|
package/dist/cli.js
CHANGED
|
@@ -421,7 +421,7 @@ function dispatch(argv, io) {
|
|
|
421
421
|
throw new RafError('deliver : à lancer dans un dépôt git');
|
|
422
422
|
const orchestrating = activeLock(join(sharedStateDir(root), REPO_LOCK));
|
|
423
423
|
if (orchestrating)
|
|
424
|
-
throw new RafError(`deliver : orchestration en cours (vague ${orchestrating.wave}, pid ${orchestrating.pid}) — livrer une fois
|
|
424
|
+
throw new RafError(`deliver : orchestration en cours (vague ${orchestrating.wave}, pid ${orchestrating.pid}) — livrer une fois les lots de ce dépôt finis (le verrou tombe dès que tous sont prêts ou rendus)`);
|
|
425
425
|
if (!existsSync(configPath))
|
|
426
426
|
throw new RafError(`pas de configuration de livraison : ${configPath} (voir « cadence.yaml » dans le README)`);
|
|
427
427
|
const config = parseDeliverConfig(readFileSync(configPath, 'utf8'), configPath);
|
|
@@ -10,7 +10,7 @@ import { stopApps } from './app.js';
|
|
|
10
10
|
import { Plan, RafError, isOpen } from '../plan.js';
|
|
11
11
|
import { AGENTS_DIR } from '../skills.js';
|
|
12
12
|
import { pidAlive, sharedStateDir } from '../state.js';
|
|
13
|
-
import { acquireSlot, cadenceHome, liveSlots, liveWaves, registerWave, unregisterWave } from './registry.js';
|
|
13
|
+
import { acquireSlot, cadenceHome, liveSlots, liveWaves, registerWave, unregisterWave, updateWaveRepos } from './registry.js';
|
|
14
14
|
import { loadTemplates, newsText, objective, renderBrief } from './briefs.js';
|
|
15
15
|
import { Budget, MAX_PASSES, countInterrupted, needsPrecheck } from './cycle.js';
|
|
16
16
|
import { canInstallPrePush, installPrePush, removePrePush, snapshot } from './guard.js';
|
|
@@ -472,12 +472,28 @@ async function execute(wave, lots, store, io, deps, today, cap, all = lots) {
|
|
|
472
472
|
const home = cadenceHome();
|
|
473
473
|
const held = [];
|
|
474
474
|
const repos = [...new Set(lots.flatMap(lotRepoPaths))];
|
|
475
|
+
/** Libère un dépôt : son verrou et sa garde de push (L132 : dès que ses lots sont finis, pas à la fin de la vague). */
|
|
476
|
+
const freed = new Set();
|
|
477
|
+
const freeRepo = (r) => {
|
|
478
|
+
freed.add(r);
|
|
479
|
+
const file = join(sharedStateDir(r), REPO_LOCK);
|
|
480
|
+
releaseLock(file, process.pid);
|
|
481
|
+
held.splice(0, held.length, ...held.filter((f) => f !== file));
|
|
482
|
+
removePrePush(r, wave.id);
|
|
483
|
+
try {
|
|
484
|
+
updateWaveRepos(home, process.pid, repos.filter((x) => !freed.has(x)));
|
|
485
|
+
}
|
|
486
|
+
catch {
|
|
487
|
+
// registre illisible : `--status` garde la liste de départ, le verrou est bien tombé
|
|
488
|
+
}
|
|
489
|
+
};
|
|
475
490
|
const release = () => {
|
|
476
491
|
for (const f of held)
|
|
477
492
|
releaseLock(f, process.pid);
|
|
478
493
|
held.length = 0;
|
|
479
494
|
for (const r of repos)
|
|
480
|
-
|
|
495
|
+
if (!freed.has(r))
|
|
496
|
+
removePrePush(r, wave.id);
|
|
481
497
|
try {
|
|
482
498
|
unregisterWave(home, process.pid);
|
|
483
499
|
}
|
|
@@ -575,14 +591,22 @@ async function execute(wave, lots, store, io, deps, today, cap, all = lots) {
|
|
|
575
591
|
saveWave();
|
|
576
592
|
release();
|
|
577
593
|
});
|
|
594
|
+
const finished = (l) => l.status === 'ready' || l.status === 'handed-back' || l.status === 'failed';
|
|
578
595
|
try {
|
|
579
|
-
await runPool(ctxs, cap, all)
|
|
596
|
+
await runPool(ctxs, cap, all, (c) => {
|
|
597
|
+
// Un dépôt dont tous les lots de la vague (reprise comprise) sont prêts ou rendus peut être livré pendant que la vague continue ailleurs.
|
|
598
|
+
if (wctx.incident)
|
|
599
|
+
return;
|
|
600
|
+
for (const r of lotRepoPaths(c.lot)) {
|
|
601
|
+
if (held.includes(join(sharedStateDir(r), REPO_LOCK)) && all.filter((l) => lotRepoPaths(l).includes(r)).every(finished))
|
|
602
|
+
freeRepo(r);
|
|
603
|
+
}
|
|
604
|
+
});
|
|
580
605
|
}
|
|
581
606
|
finally {
|
|
582
607
|
forget();
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
wave.status = wctx.incident ? 'interrupted' : wctx.quota.hit ? 'suspended-quota' : lots.some((l) => l.status === 'suspended') ? 'suspended-budget' : all.every(finished) ? 'done' : 'interrupted';
|
|
608
|
+
// « done » = plus rien à reprendre : une question en attente ou un lot suspendu garde la vague reprenable. Des lots suspendus parce qu'une revue a sali leur dépôt (L133), budget intact, ne sont pas un manque de budget : « interrupted », le lead nettoie puis reprend.
|
|
609
|
+
wave.status = wctx.incident ? 'interrupted' : wctx.quota.hit ? 'suspended-quota' : lots.some((l) => l.status === 'suspended') && (budget.exhausted || !wctx.dirtyRepos?.size) ? 'suspended-budget' : all.every(finished) ? 'done' : 'interrupted';
|
|
586
610
|
saveWave();
|
|
587
611
|
release();
|
|
588
612
|
}
|
|
@@ -14,7 +14,7 @@ import { capturesText, newsText, objective, renderBrief, reposText } from './bri
|
|
|
14
14
|
import { journalTokens, peakContext, mcpServersFor, playwrightDir, runSession, trackGroup, writeMcpConfig } from './launch.js';
|
|
15
15
|
import { cleanPlaywrightOutput, pushed, snapshot } from './guard.js';
|
|
16
16
|
import { checkShape, PRECHECK_SCHEMA, REVIEW_SCHEMA, schemaFor, WORK_SCHEMA } from './schemas.js';
|
|
17
|
-
import { lotKey, neighbourDirs } from './state.js';
|
|
17
|
+
import { lotKey, lotRepoPaths, neighbourDirs } from './state.js';
|
|
18
18
|
/** Passes de correction au plus, puis la main est rendue au lead. */
|
|
19
19
|
export const MAX_PASSES = 2;
|
|
20
20
|
export class Budget {
|
|
@@ -240,9 +240,16 @@ export const lotSpent = (l) => l.steps.reduce((n, s) => n + (s.tokens?.counted ?
|
|
|
240
240
|
/** Le lot a dépensé son propre budget (L78) : il ne compte plus que sur le lead, la vague garde le sien pour les autres. */
|
|
241
241
|
const lotOver = (l) => l.budget !== undefined && lotSpent(l) >= l.budget;
|
|
242
242
|
/** Pourquoi plus aucune session ne doit partir (incident, quota, budget), sinon null. */
|
|
243
|
-
function halted(w) {
|
|
243
|
+
function halted(w, lot) {
|
|
244
244
|
if (w.incident)
|
|
245
245
|
return `vague arrêtée : ${w.incident}`;
|
|
246
|
+
if (lot && w.dirtyRepos) {
|
|
247
|
+
for (const r of lotRepoPaths(lot)) {
|
|
248
|
+
const by = w.dirtyRepos.get(r);
|
|
249
|
+
if (by)
|
|
250
|
+
return `dépôt ${r} sali par la revue de ${by} : à nettoyer par le lead avant de reprendre`;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
246
253
|
if (w.quota.hit)
|
|
247
254
|
return 'quota atteint';
|
|
248
255
|
if (w.budget.exhausted)
|
|
@@ -258,7 +265,7 @@ function reviewModel(c, kind) {
|
|
|
258
265
|
async function session(c, kind) {
|
|
259
266
|
const w = c.wave;
|
|
260
267
|
const l = c.lot;
|
|
261
|
-
const halt = halted(w);
|
|
268
|
+
const halt = halted(w, l);
|
|
262
269
|
if (halt)
|
|
263
270
|
return suspend(c, halt);
|
|
264
271
|
if (lotOver(l))
|
|
@@ -370,8 +377,8 @@ async function session(c, kind) {
|
|
|
370
377
|
const others = neighbours.map((repo, i) => ({ repo, before: beforeOthers[i], after: afterOthers[i] }));
|
|
371
378
|
step.headAfter = after.head ?? undefined;
|
|
372
379
|
// Le dépôt du projet d'abord, puis chaque voisin : mêmes contrôles, l'incident nomme le dépôt voisin.
|
|
373
|
-
const watched = [{ where: '', before, after }, ...others.map((o) => ({ where: ` dans ${o.repo.rel}`, before: o.before, after: o.after }))];
|
|
374
|
-
for (const { where, before: b, after: a } of watched) {
|
|
380
|
+
const watched = [{ where: '', path: l.repo, before, after }, ...others.map((o) => ({ where: ` dans ${o.repo.rel}`, path: o.repo.path, before: o.before, after: o.after }))];
|
|
381
|
+
for (const { where, path, before: b, after: a } of watched) {
|
|
375
382
|
if (pushed(b, a)) {
|
|
376
383
|
step.status = 'failed';
|
|
377
384
|
step.cause = `push détecté${where}`;
|
|
@@ -387,17 +394,35 @@ async function session(c, kind) {
|
|
|
387
394
|
return stop(c, 'failed', `incident : ${w.incident}`);
|
|
388
395
|
}
|
|
389
396
|
if (!write && (b.head !== a.head || b.tracked.join() !== a.tracked.join() || b.untracked.join() !== a.untracked.join())) {
|
|
397
|
+
// Incident du LOT (L133) : la revue a laissé des traces (captures, commit), ni push ni garde supprimée — les lots des autres dépôts continuent.
|
|
390
398
|
step.status = 'failed';
|
|
391
399
|
step.cause = `le dépôt${where} a changé pendant une revue`;
|
|
392
|
-
|
|
393
|
-
w.
|
|
394
|
-
|
|
400
|
+
const traces = [...trackedPaths(a).filter((f) => !trackedPaths(b).includes(f)), ...a.untracked.filter((f) => !b.untracked.includes(f))];
|
|
401
|
+
(w.dirtyRepos ??= new Map()).set(path, lotKey(l.project, l.lot));
|
|
402
|
+
const what = traces.length ? ` (${traces.join(', ')})` : b.head !== a.head ? ' (commit)' : '';
|
|
403
|
+
return stop(c, 'handed-back', `incident : ${kind} de ${lotKey(l.project, l.lot)} a modifié le dépôt${where}${what}${lostVerdict(c, kind, res.structured)}`);
|
|
395
404
|
}
|
|
396
405
|
}
|
|
397
406
|
step.status = 'ok';
|
|
398
407
|
save(c);
|
|
399
408
|
return { step, report: res.structured, before, after, others };
|
|
400
409
|
}
|
|
410
|
+
/** Verdict d'une revue interrompue par un incident du lot : rapporté au lead plutôt que perdu (L133). Rien n'est enregistré dans le plan. */
|
|
411
|
+
function lostVerdict(c, kind, report) {
|
|
412
|
+
if (kind === 'precheck')
|
|
413
|
+
return '';
|
|
414
|
+
let rep;
|
|
415
|
+
try {
|
|
416
|
+
rep = checkShape(report, REVIEW_SCHEMA);
|
|
417
|
+
}
|
|
418
|
+
catch {
|
|
419
|
+
return ' ; la revue n\'a pas rendu de verdict exploitable';
|
|
420
|
+
}
|
|
421
|
+
const s = summarize(rep, '');
|
|
422
|
+
const text = `verdict de la revue avant l'incident : ${s.conforme ? 'conforme' : 'non conforme'} (${s.bloquants} bloquant(s), ${s.majeurs} majeur(s), ${s.mineurs} mineur(s)) — « ${s.verdict} » — non enregistré dans le plan, à confirmer par le lead`;
|
|
423
|
+
c.lot.warnings.push(text);
|
|
424
|
+
return ` ; ${text}`;
|
|
425
|
+
}
|
|
401
426
|
/** Budget du lot atteint avant une session : le lot est rendu au lead (reprendre ne lui rendrait pas de budget), les autres continuent. */
|
|
402
427
|
function overBudget(c) {
|
|
403
428
|
const l = c.lot;
|
|
@@ -657,7 +682,7 @@ async function review(c, kind) {
|
|
|
657
682
|
const ux = c.config.ux;
|
|
658
683
|
if (!ux?.command || !ux.url || !seesApp(l, kind))
|
|
659
684
|
return reviewStep(c, kind);
|
|
660
|
-
const halt = halted(c.wave);
|
|
685
|
+
const halt = halted(c.wave, l);
|
|
661
686
|
if (halt) {
|
|
662
687
|
suspend(c, halt); // la session n'aurait pas lieu : l'application ne se lance pas pour rien
|
|
663
688
|
return;
|
|
@@ -841,7 +866,7 @@ export async function runLot(c) {
|
|
|
841
866
|
return;
|
|
842
867
|
if (l.next === null && l.steps.length === 0) {
|
|
843
868
|
// Pas de raf start ni de commit du plan pour un lot qu'aucune session ne suivrait.
|
|
844
|
-
const halt = halted(c.wave);
|
|
869
|
+
const halt = halted(c.wave, l);
|
|
845
870
|
if (halt) {
|
|
846
871
|
suspend(c, halt);
|
|
847
872
|
return;
|
package/dist/orchestrate/pool.js
CHANGED
|
@@ -6,9 +6,9 @@ export const SLOTS = 2;
|
|
|
6
6
|
* 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` dépôts
|
|
7
7
|
* au plus en parallèle. Deux lots qui partagent un dépôt (le leur, ou un dépôt voisin déclaré par `repos:`, L62) sont dans la
|
|
8
8
|
* même file : jamais deux sessions dans un même dépôt (le plafond global des sessions de toutes
|
|
9
|
-
* les vagues est tenu par command.ts). Le budget, le quota et les incidents sont vérifiés par le cycle avant chaque session.
|
|
9
|
+
* les vagues est tenu par command.ts). `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.
|
|
10
10
|
*/
|
|
11
|
-
export async function runPool(ctxs, slots = SLOTS, known = []) {
|
|
11
|
+
export async function runPool(ctxs, slots = SLOTS, known = [], onSettled = () => { }) {
|
|
12
12
|
const groups = [];
|
|
13
13
|
for (const c of ctxs) {
|
|
14
14
|
const mine = lotRepoPaths(c.lot);
|
|
@@ -51,12 +51,14 @@ export async function runPool(ctxs, slots = SLOTS, known = []) {
|
|
|
51
51
|
c.wave.store.writeLot(c.lot);
|
|
52
52
|
c.wave.log(`${lotKey(c.lot.project, c.lot.lot)} → handed-back — ${c.lot.outcome}`);
|
|
53
53
|
progress = true;
|
|
54
|
+
onSettled(c);
|
|
54
55
|
continue;
|
|
55
56
|
}
|
|
56
57
|
if (c.lot.outcome?.startsWith('en attente de : '))
|
|
57
58
|
c.lot.outcome = null;
|
|
58
59
|
progress = true;
|
|
59
60
|
await runLot(c);
|
|
61
|
+
onSettled(c);
|
|
60
62
|
}
|
|
61
63
|
todo = waiting;
|
|
62
64
|
}
|
|
@@ -59,6 +59,15 @@ export function registerWave(home, given) {
|
|
|
59
59
|
writeFileSync(`${file}.tmp`, JSON.stringify(w));
|
|
60
60
|
renameSync(`${file}.tmp`, file);
|
|
61
61
|
}
|
|
62
|
+
/** Met à jour la liste des dépôts tenus par la vague (un dépôt libéré en cours de vague n'y figure plus). */
|
|
63
|
+
export function updateWaveRepos(home, pid, repos) {
|
|
64
|
+
const file = join(wavesDir(home), `${pid}.json`);
|
|
65
|
+
const w = readWave(file);
|
|
66
|
+
if (!w)
|
|
67
|
+
return;
|
|
68
|
+
writeFileSync(`${file}.tmp`, JSON.stringify({ ...w, repos }));
|
|
69
|
+
renameSync(`${file}.tmp`, file);
|
|
70
|
+
}
|
|
62
71
|
export function unregisterWave(home, pid) {
|
|
63
72
|
rmSync(join(wavesDir(home), `${pid}.json`), { force: true });
|
|
64
73
|
}
|
package/dist/session.js
CHANGED
|
@@ -83,7 +83,7 @@ export function lastActivity(lot, commits) {
|
|
|
83
83
|
}
|
|
84
84
|
const hhmm = (d) => `${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}`;
|
|
85
85
|
/**
|
|
86
|
-
* Fenêtre de la clôture : --since s'il est donné ; sinon depuis la dernière ouverture
|
|
86
|
+
* Fenêtre de la clôture : --since s'il est donné ; sinon depuis la dernière ouverture du
|
|
87
87
|
* projet, dite avec son heure ; sinon le jour même.
|
|
88
88
|
*/
|
|
89
89
|
function closeWindow(ctx, since) {
|
|
@@ -174,8 +174,7 @@ export function sessionClose(ctx, opts) {
|
|
|
174
174
|
}
|
|
175
175
|
const open = issues.filter((i) => !i.warning).length + repo.open + (lock?.live ? 1 : 0);
|
|
176
176
|
out(open === 0 ? '\n✓ prêt à fermer' : `\n✗ pas fermé : ${open} point(s)`);
|
|
177
|
-
//
|
|
178
|
-
|
|
179
|
-
writeSessionMark(ctx.state, { kind: 'close', at: (ctx.now ?? new Date()).toISOString() });
|
|
177
|
+
// La clôture ne déplace jamais la borne : relancée sans ouverture entre-temps (le skill la relance exprès),
|
|
178
|
+
// elle rapporte encore depuis l'ouverture. Seule une ouverture pose la marque.
|
|
180
179
|
return open === 0 ? 0 : 1;
|
|
181
180
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sylad/cadence",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.19.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",
|
|
@@ -18,7 +18,7 @@ With no project named, run `cadence session close --all` from the parent folder,
|
|
|
18
18
|
|
|
19
19
|
## Steps
|
|
20
20
|
|
|
21
|
-
1. Run `cadence session close` (the window runs from the last `session start
|
|
21
|
+
1. Run `cadence session close` (the window runs from the last `session start`, even when the close is re-run, and is named in the "Commits de la période" title; over several days: `--since "2 days ago"`). Exit code 1 means
|
|
22
22
|
*not closed*: something below is still open.
|
|
23
23
|
2. **Plan hygiene**, lot by lot:
|
|
24
24
|
- a `doing` lot with no commit in the period → `raf done <id>` if its tests are green,
|