@sylad/cadence 1.27.0 → 1.29.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/README.md +1 -1
- package/agents/doc-keeper.md +17 -0
- package/agents/ux-reviewer.md +18 -0
- package/dist/cli.js +48 -8
- package/dist/config-set.js +2 -1
- package/dist/config.js +16 -1
- package/dist/lead.js +24 -7
- package/dist/orchestrate/briefs.js +9 -0
- package/dist/orchestrate/cycle.js +91 -6
- package/dist/orchestrate/state.js +7 -0
- package/dist/orchestrate/tiers.js +125 -0
- package/dist/priority.js +77 -0
- package/dist/verify-gate.js +6 -6
- package/docs/cadence-hud.md +2 -0
- package/docs/deliver.md +4 -2
- package/docs/fonctionnement-lead-dark.svg +2 -2
- package/docs/fonctionnement-lead-light.svg +2 -2
- package/docs/lead-workflow-dark.svg +2 -2
- package/docs/lead-workflow-light.svg +2 -2
- package/docs/lead.md +12 -4
- package/docs/orchestrate.md +42 -3
- package/docs/raf.md +1 -0
- package/docs/session.md +7 -0
- package/docs/skills.md +4 -0
- package/docs/whats-new.md +6 -0
- package/package.json +1 -1
- package/skills/lead/SKILL.md +8 -5
|
@@ -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.29.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.29.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
|
@@ -48,7 +48,7 @@ Details: [docs/skills.md](docs/skills.md) and [docs/cadence-hud.md](docs/cadence
|
|
|
48
48
|
## Where to read more
|
|
49
49
|
|
|
50
50
|
- [docs/raf.md](docs/raf.md): the plan, its commands and its file.
|
|
51
|
-
- [docs/orchestrate.md](docs/orchestrate.md): waves of sessions and their budgets.
|
|
51
|
+
- [docs/orchestrate.md](docs/orchestrate.md): waves of sessions and their budgets, and which repositories a session may commit in.
|
|
52
52
|
- [docs/deliver.md](docs/deliver.md): wait for the CI, deploy, verify the effect.
|
|
53
53
|
- [docs/spec.md](docs/spec.md): screen and back-end sheets.
|
|
54
54
|
- [docs/ref.md](docs/ref.md): reference documents (specs, norms, mock-ups) and the coverage of their elements by the lots.
|
package/agents/doc-keeper.md
CHANGED
|
@@ -17,3 +17,20 @@ Follow the brief you are given. The method:
|
|
|
17
17
|
5. **A document outside the repository** is yours only when the configuration says so: the brief then lists the entries of `docs.articles` (the cadence.yaml of the project: « this project is also described by this file of that repository », with the absolute path of the repository, open to you for reading). Never guess such a file, never go looking for one. Do not edit it, and never write to the plan of that repository (no `raf add`, no edit of its plan file): write a proposed lot instead — a title and a body that name the repository, the file, the passage and the sentence to change — and put it in the `resume` of your report, starting with « lot proposé ». The lead decides whether it enters that repository's plan.
|
|
18
18
|
|
|
19
19
|
Never push, deliver, or run `raf done|start|note|review|ux`. Never run `git checkout` or `git switch` to another commit or branch in this repository. Do not launch subagents. Do not stop to ask: an interpretation you settle yourself goes under "choix" with the alternative you did not take. A document you could not check against the code goes under "nonVerifie". Give the report in the structured output the brief asks for.
|
|
20
|
+
|
|
21
|
+
## Short reply
|
|
22
|
+
|
|
23
|
+
Only when the brief carries a line `Report file: <absolute path>` (the lead, which keeps its context
|
|
24
|
+
small by reading a report only when it needs to). That line is the only trigger; an orchestrated
|
|
25
|
+
session and a call by hand without it have none.
|
|
26
|
+
|
|
27
|
+
- The full report, with its measurements, is written to that file with Bash. Use the path the brief gives
|
|
28
|
+
and no other: it is outside the repository, so the working tree stays as you found it. The
|
|
29
|
+
verdict is the last line of the file. Write the file before you reply.
|
|
30
|
+
- Reply to the caller with 15 lines at most, and nothing before or after them:
|
|
31
|
+
`Verdict: <the one-line verdict>`; `Findings:` one line per finding, most severe first, with its rank and the rule or the
|
|
32
|
+
measure it rests on (e.g. `README.md:12 — still says the old flag, `git show <sha>` line 40`); `Not verified: <what could not be run or seen>`
|
|
33
|
+
(`Not verified: none` when nothing); `Sub-tasks: <n> — <the lines to add to the plan, by their first words>`; `Report: <the path>`. A « lot proposé » for a document outside the repository goes in `Sub-tasks:` (title first), its body in the file.
|
|
34
|
+
- If you cannot write the file, say so on the first line of the reply and give the whole report in the reply, as if
|
|
35
|
+
the brief had no report file: a verdict whose evidence nobody can read is lost.
|
|
36
|
+
- With no `Report file:` line in the brief, give the complete report in your reply.
|
package/agents/ux-reviewer.md
CHANGED
|
@@ -98,3 +98,21 @@ A short report:
|
|
|
98
98
|
- Judge on taste, or propose a new visual identity.
|
|
99
99
|
- Report a finding you have not seen in a capture or measured.
|
|
100
100
|
- Edit code, commit, or record `raf ux` yourself: the session that owns the lot does it.
|
|
101
|
+
|
|
102
|
+
## Short reply
|
|
103
|
+
|
|
104
|
+
Only when the brief carries a line `Report file: <absolute path>` (the lead, which keeps its context
|
|
105
|
+
small by reading a report only when it needs to). That line is the only trigger; an orchestrated
|
|
106
|
+
session and a call by hand without it have none.
|
|
107
|
+
|
|
108
|
+
- The full report, with its measurements, is written to that file with Bash. Use the path the brief gives
|
|
109
|
+
and no other: it is outside the repository, so the working tree stays as you found it. The
|
|
110
|
+
verdict is the last line of the file. Write the file before you reply.
|
|
111
|
+
- Reply to the caller with 15 lines at most, and nothing before or after them:
|
|
112
|
+
`Verdict: <the one-line verdict>`; `Findings:` one line per finding, most severe first, with its rank and the rule or the
|
|
113
|
+
measure it rests on (e.g. `major — /players: target 18×18 px, WCAG 2.5.8`); `Not verified: <what could not be run or seen>`
|
|
114
|
+
(`Not verified: none` when nothing); `Sub-tasks: <n> — <the lines to add to the plan, by their first words>`; `Report: <the path>`.
|
|
115
|
+
Add a last line `Captures: <the absolute path of the capture folder>` (the folder the brief names): the report file does not replace the captures, which the lead opens itself.
|
|
116
|
+
- If you cannot write the file, say so on the first line of the reply and give the whole report in the reply, as if
|
|
117
|
+
the brief had no report file: a verdict whose evidence nobody can read is lost.
|
|
118
|
+
- With no `Report file:` line in the brief, give the complete report in your reply.
|
package/dist/cli.js
CHANGED
|
@@ -22,7 +22,8 @@ import { dueLine, isRecurring, recurringByDue } from './recurring.js';
|
|
|
22
22
|
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
|
-
import {
|
|
25
|
+
import { movePriority } from './priority.js';
|
|
26
|
+
import { findProjects, readDepth, leadTour, TITLE_MAX, truncate, readLeadUx, tourFreeLine, readLeadContextThreshold, tourLine, tourNoTestLines, tourWorktreeLines } from './lead.js';
|
|
26
27
|
import { configuredCap, expiredHoldLines, slotsLine, orchestrate, realOrchestrateDeps } from './orchestrate/command.js';
|
|
27
28
|
import { addHold, releaseHold } from './orchestrate/registry.js';
|
|
28
29
|
import { activeLock, REPO_LOCK } from './orchestrate/lock.js';
|
|
@@ -53,6 +54,7 @@ const HELP = `raf — plan « reste à faire » versionné dans le dépôt, reli
|
|
|
53
54
|
raf start <id> raf done <id> [--force] raf drop <id> [--reason texte]
|
|
54
55
|
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)
|
|
55
56
|
raf note <id> "texte"
|
|
57
|
+
--verbose avec done, ux, review, note, move ou public : imprime la ligne « id état titre entier » du lot touché ; avec hook post-commit : le titre entier (sinon 60 caractères au plus)
|
|
56
58
|
raf title <id> "nouveau titre" remplace le titre d'un lot, de tout statut (done compris), et note l'ancien dans une note datée
|
|
57
59
|
raf did <id> ["texte"] un lot récurrent (--every) a été refait : le « dû depuis N j » repart de aujourd'hui
|
|
58
60
|
raf ux enable revue UX obligatoire avant « done » pour les lots --visible
|
|
@@ -74,6 +76,7 @@ const HELP = `raf — plan « reste à faire » versionné dans le dépôt, reli
|
|
|
74
76
|
cadence orchestrate <projet>:<lot>[@modèle]… [--budget 2M] [--goal "…"] [--dry-run] [--wave nom] [--continue [--until HH:MM] [--priority a,b]] une session claude neuve par étape ; --status [vague] [--watch [--interval s]] ; --resume [vague | --wave vague] [--budget 1M] [--budget projet:lot=+300k] [--answer projet:lot "réponse"] [--drop projet:lot] ; --drop projet:lot [--wave vague] ; --stop-after-current [--wave vague] ; --waits [--since AAAA-MM-JJ]
|
|
75
77
|
mode worktree par lot (orchestrate.worktrees: true) (activé par défaut dans cadence, désactivé ailleurs tant que la clé est absente) ; ses options : --resume --wave <vague> --integrate projet:lot (rejoue un lot rendu à l'intégration) · --resume --wave <vague> --replay projet:lot (idem, et après une écriture de plan refusée) · --remove-worktree projet:lot [--abandon-commits] (retire le worktree et la branche conservés) ; la liste des worktrees conservés : orchestrate --status et lead tour
|
|
76
78
|
cadence lead tour [dossier] [--idle 3] [--depth n] [--json] le tableau du lead, sans modèle : une ligne par sous-dossier qui a un plan, ou la ligne du dossier seul s'il est lui-même un projet (en cours, dérive, notes de clôture, prochain lot prêt, dépôt), précédées de l'en-tête perf : une ligne par ⚠ de cadence perf, rien si tout est stable
|
|
79
|
+
cadence lead priority <projet> --before|--after <projet> [dossier] [--depth n] range un projet dans la liste priority: du cadence.yaml du dossier (ordre du tour, de orchestrate --continue et du panneau des lots), commentaires gardés ; ne commite pas
|
|
77
80
|
cadence lead hold <étiquette> [--ttl minutes] | lead release <étiquette> déclare (60 min par défaut), puis rend, une session lancée à la main par le lead : elle est soustraite des créneaux libres de orchestrate --status et de lead tour ; au TTL échu, les deux la signalent en ⚠ jusqu'au release
|
|
78
81
|
cadence corrections [dossier] [--classify [--model m]] [--cache fichier] [--cache-sous-causes fichier] [--sample n] [--until date] [--since date] [--json] mesure des passes de correction des journaux dossier/.cadence/runs : lots à ≥1 et ≥2 corrections, par projet, visibles ou non, passes par cause (classées par Ollama avec --classify, hors quota)
|
|
79
82
|
cadence perf [dossier] [--since date] [--json] tableau de bord du workflow, sans modèle : deux fenêtres (7 derniers jours contre les 7 précédents) lues dans dossier/.cadence/runs et les plans, chaque vitesse à côté de sa qualité, ⚠ au-delà de 25 % de dégradation ; tokens par lot selon l'estimate, part de la revue, délais création → done → livraison, durée de mur par étape, coût en euros par lot livré (grille cadence.yaml perf.prices) ; --since est un jour qui tronque les fenêtres
|
|
@@ -197,6 +200,7 @@ function dispatch(argv, io) {
|
|
|
197
200
|
ignored: { type: 'boolean' },
|
|
198
201
|
notes: { type: 'boolean' },
|
|
199
202
|
json: { type: 'boolean' },
|
|
203
|
+
verbose: { type: 'boolean' },
|
|
200
204
|
classify: { type: 'boolean' },
|
|
201
205
|
model: { type: 'string' },
|
|
202
206
|
cache: { type: 'string' },
|
|
@@ -230,10 +234,12 @@ function dispatch(argv, io) {
|
|
|
230
234
|
if (command === 'lead') {
|
|
231
235
|
if (rest[0] === 'hold' || rest[0] === 'release')
|
|
232
236
|
return leadHold(rest, values.ttl, io, today);
|
|
237
|
+
if (rest[0] === 'priority')
|
|
238
|
+
return leadPriority(rest, values.before, values.after, values.depth, io);
|
|
233
239
|
if (values.ttl !== undefined)
|
|
234
240
|
throw new RafError('--ttl ne va qu\'avec lead hold');
|
|
235
241
|
if (rest[0] !== 'tour' || rest.length > 2)
|
|
236
|
-
throw new RafError('usage : cadence lead tour [dossier] [--idle 3] [--depth n] [--json] | lead hold <étiquette> [--ttl minutes] | lead release <étiquette>');
|
|
242
|
+
throw new RafError('usage : cadence lead tour [dossier] [--idle 3] [--depth n] [--json] | lead priority <projet> --before|--after <projet> [dossier] | lead hold <étiquette> [--ttl minutes] | lead release <étiquette>');
|
|
237
243
|
const idle = values.idle === undefined ? undefined : Number(values.idle);
|
|
238
244
|
if (idle !== undefined && !(Number.isInteger(idle) && idle >= 0))
|
|
239
245
|
throw new RafError(`--idle invalide : ${values.idle}`);
|
|
@@ -460,6 +466,7 @@ function dispatch(argv, io) {
|
|
|
460
466
|
io.out(`${rest[0]} → ${status}`);
|
|
461
467
|
if (status === 'done')
|
|
462
468
|
warnIntegrating(root, io.err);
|
|
469
|
+
touchedLot(plan, rest[0], values.verbose, io);
|
|
463
470
|
if (status === 'done' && !rest[0].includes('/')) {
|
|
464
471
|
const lot = plan.lot(rest[0]);
|
|
465
472
|
if (lot.visible && !loadEntries(newsDir).some((e) => e.lots.includes(lot.id))) {
|
|
@@ -515,6 +522,7 @@ function dispatch(argv, io) {
|
|
|
515
522
|
plan.note(rest[0], rest.slice(1).join(' '), today);
|
|
516
523
|
plan.save();
|
|
517
524
|
warnIntegrating(root, io.err);
|
|
525
|
+
touchedLot(plan, rest[0], values.verbose, io);
|
|
518
526
|
return 0;
|
|
519
527
|
}
|
|
520
528
|
case 'did': {
|
|
@@ -551,6 +559,7 @@ function dispatch(argv, io) {
|
|
|
551
559
|
if (holding)
|
|
552
560
|
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`);
|
|
553
561
|
plan.save();
|
|
562
|
+
touchedLot(plan, rest[0], values.verbose, io);
|
|
554
563
|
return 0;
|
|
555
564
|
}
|
|
556
565
|
case 'public': {
|
|
@@ -562,6 +571,7 @@ function dispatch(argv, io) {
|
|
|
562
571
|
refuseLongTitle(rest.slice(1).join(' '));
|
|
563
572
|
plan.setPublic(rest[0], values.clear ? null : rest.slice(1).join(' '));
|
|
564
573
|
plan.save();
|
|
574
|
+
touchedLot(plan, rest[0], values.verbose, io);
|
|
565
575
|
return 0;
|
|
566
576
|
}
|
|
567
577
|
case 'screen': {
|
|
@@ -600,7 +610,7 @@ function dispatch(argv, io) {
|
|
|
600
610
|
}
|
|
601
611
|
case 'ux':
|
|
602
612
|
case 'review':
|
|
603
|
-
return gate(command, rest, loadPlan, root, today, io);
|
|
613
|
+
return gate(command, rest, loadPlan, root, today, io, !!values.verbose);
|
|
604
614
|
case 'commits': {
|
|
605
615
|
need(1, 'commits <lot>');
|
|
606
616
|
const plan = loadPlan();
|
|
@@ -749,7 +759,7 @@ function dispatch(argv, io) {
|
|
|
749
759
|
return 0;
|
|
750
760
|
}
|
|
751
761
|
if (rest[0] === 'post-commit')
|
|
752
|
-
return postCommit(existsSync(planPath) ? loadPlan : null, newsDir, root, today, io, readHookConfig(configPath).autostart);
|
|
762
|
+
return postCommit(existsSync(planPath) ? loadPlan : null, newsDir, root, today, io, readHookConfig(configPath).autostart, !!values.verbose);
|
|
753
763
|
if (rest[0] === 'pre-commit')
|
|
754
764
|
return preCommit(existsSync(planPath) ? loadPlan : null, readHookConfig(configPath).autostart, root, today, io);
|
|
755
765
|
if (rest[0] === 'pre-push')
|
|
@@ -854,7 +864,7 @@ const GATES = {
|
|
|
854
864
|
review: { on: 'revue de code obligatoire pour les lots à commits', already: 'revue de code déjà active' },
|
|
855
865
|
};
|
|
856
866
|
/** « raf ux … » et « raf review … » ont la même forme : activer la porte, ou noter le verdict d'un lot. */
|
|
857
|
-
function gate(kind, rest, loadPlan, root, today, io) {
|
|
867
|
+
function gate(kind, rest, loadPlan, root, today, io, verbose) {
|
|
858
868
|
if (rest.length < 1)
|
|
859
869
|
throw new RafError(`usage : raf ${kind} enable | ${kind} <lot> "verdict"`);
|
|
860
870
|
const plan = loadPlan();
|
|
@@ -879,8 +889,19 @@ function gate(kind, rest, loadPlan, root, today, io) {
|
|
|
879
889
|
plan.recordReview(rest[0], verdict, today, plan.readonly ? null : (lotWork(plan, root, rest[0])[0]?.sha ?? null), neighbours?.repos.length ? repoShas(plan, neighbours.repos, rest[0]) : undefined);
|
|
880
890
|
}
|
|
881
891
|
plan.save();
|
|
892
|
+
touchedLot(plan, rest[0], verbose, io);
|
|
882
893
|
return 0;
|
|
883
894
|
}
|
|
895
|
+
/** Un titre de lot n'est jamais rendu en entier par défaut (L453) : ~300 caractères répétés à chaque commande, chaque commit de plan. */
|
|
896
|
+
function shortTitle(title, verbose) {
|
|
897
|
+
return verbose ? title : truncate(title, TITLE_MAX);
|
|
898
|
+
}
|
|
899
|
+
/** `--verbose` (L453) : la ligne `id état titre entier` du lot touché par `done`, `ux`, `review`, `note`, `move`, `public` ; sans lui, rien de plus (une sous-tâche n'a pas de ligne). */
|
|
900
|
+
function touchedLot(plan, id, verbose, io) {
|
|
901
|
+
const lot = verbose ? plan.lots().find((l) => l.id === id) : undefined;
|
|
902
|
+
if (lot)
|
|
903
|
+
io.out(`${lot.id} ${lot.status} ${lot.title}`);
|
|
904
|
+
}
|
|
884
905
|
function knownLot(plan, id, subtask) {
|
|
885
906
|
const lot = plan.lots().find((l) => l.id === id);
|
|
886
907
|
if (!lot)
|
|
@@ -1021,7 +1042,7 @@ function preCommit(load, mode, root, today, io) {
|
|
|
1021
1042
|
}
|
|
1022
1043
|
return 0;
|
|
1023
1044
|
}
|
|
1024
|
-
function postCommit(load, newsDir, root, today, io, mode) {
|
|
1045
|
+
function postCommit(load, newsDir, root, today, io, mode, verbose) {
|
|
1025
1046
|
if (!load)
|
|
1026
1047
|
return 0;
|
|
1027
1048
|
try {
|
|
@@ -1050,7 +1071,7 @@ function postCommit(load, newsDir, root, today, io, mode) {
|
|
|
1050
1071
|
else if (lot.status === 'todo' && !planning)
|
|
1051
1072
|
io.err(`raf: ${r.lot} est encore todo${plan.readonly ? '' : ` — raf start ${r.lot}`}`);
|
|
1052
1073
|
else
|
|
1053
|
-
io.err(`raf: ${r.lot} (${lot.status}) ${lot.title}`);
|
|
1074
|
+
io.err(`raf: ${r.lot} (${lot.status}) ${shortTitle(lot.title, verbose)}`);
|
|
1054
1075
|
}
|
|
1055
1076
|
const entries = loadEntries(newsDir);
|
|
1056
1077
|
for (const id of new Set(refs.map((r) => r.lot))) {
|
|
@@ -1445,6 +1466,23 @@ function specReseal(args, reason, porteur, ctx, io) {
|
|
|
1445
1466
|
const HOLD_TTL_MINUTES = 60;
|
|
1446
1467
|
/** Longueur maximale de l'étiquette encodée : un nom de fichier tient en 255 octets, `.json.tmp` en prend 9. */
|
|
1447
1468
|
const HOLD_LABEL_MAX = 200;
|
|
1469
|
+
/** `cadence lead priority <projet> --before|--after <projet> [dossier]` (L449) : range un projet dans la liste `priority:` du cadence.yaml du dossier (celui du tour), sans modèle ni commit. */
|
|
1470
|
+
function leadPriority(rest, before, after, depth, io) {
|
|
1471
|
+
const usage = 'usage : cadence lead priority <projet> --before|--after <projet> [dossier] [--depth n]';
|
|
1472
|
+
if (rest.length < 2 || rest.length > 3 || (before === undefined) === (after === undefined))
|
|
1473
|
+
throw new RafError(usage);
|
|
1474
|
+
const place = before !== undefined ? 'before' : 'after';
|
|
1475
|
+
const levels = depth === undefined ? undefined : Number(depth);
|
|
1476
|
+
if (levels !== undefined && !(Number.isInteger(levels) && levels >= 1))
|
|
1477
|
+
throw new RafError(`--depth invalide : ${depth} (un entier ≥ 1)`);
|
|
1478
|
+
const parent = resolve(io.cwd, rest[2] ?? '.');
|
|
1479
|
+
if (!existsSync(parent))
|
|
1480
|
+
throw new RafError(`lead priority : dossier introuvable : ${parent}`);
|
|
1481
|
+
const target = (before ?? after);
|
|
1482
|
+
const moved = movePriority(parent, rest[1], place, target, levels);
|
|
1483
|
+
io.out(moved.changed ? `${rest[1]} placé ${place === 'before' ? 'avant' : 'après'} ${target} dans priority: de ${join(parent, 'cadence.yaml')} (ordre : ${moved.order.join(' > ')}) — le fichier est modifié, à commiter` : `${rest[1]} est déjà ${place === 'before' ? 'avant' : 'après'} ${target} : rien à changer`);
|
|
1484
|
+
return 0;
|
|
1485
|
+
}
|
|
1448
1486
|
/** `cadence lead hold <étiquette> [--ttl minutes]` / `lead release <étiquette>` (L255) : inscrit, ou rend, une session lancée à la main par le lead, qui compte dans les créneaux libres. */
|
|
1449
1487
|
function leadHold(rest, ttl, io, today) {
|
|
1450
1488
|
const [verb, label] = rest;
|
|
@@ -1702,12 +1740,14 @@ async function sessionAll(rest, values, io) {
|
|
|
1702
1740
|
}
|
|
1703
1741
|
return worst;
|
|
1704
1742
|
}
|
|
1705
|
-
/** Le contexte de la session de `--session <id>`, sinon de `CLAUDE_CODE_SESSION_ID`, sinon (variable absente) de la seule session du dossier courant, tel que la bande cadence-hud le publie ; illisible = code 2, à lire comme « au seuil ». */
|
|
1743
|
+
/** Le contexte (et sa position par rapport à `lead.contextThreshold`, en tokens) de la session de `--session <id>`, sinon de `CLAUDE_CODE_SESSION_ID`, sinon (variable absente) de la seule session du dossier courant, tel que la bande cadence-hud le publie ; illisible = code 2, à lire comme « au seuil ». */
|
|
1706
1744
|
function sessionContext(io, session) {
|
|
1707
1745
|
const r = readHudContext(io.env, io.now().getTime(), { cwd: io.cwd, session });
|
|
1708
1746
|
if (!r.ok)
|
|
1709
1747
|
throw new RafError(`session context : contexte illisible — ${r.reason} ; à traiter comme au seuil`);
|
|
1710
1748
|
io.out(`ctx ${Math.round(r.ctx.percent)} % (${r.ctx.tokens}/${r.ctx.window})`);
|
|
1749
|
+
const threshold = readLeadContextThreshold(io.cwd);
|
|
1750
|
+
io.out(`seuil ${threshold} tokens : ${r.ctx.tokens >= threshold ? 'atteint' : 'sous le seuil'}`);
|
|
1711
1751
|
return 0;
|
|
1712
1752
|
}
|
|
1713
1753
|
function session([sub, ...args], ctx, values) {
|
package/dist/config-set.js
CHANGED
|
@@ -6,7 +6,7 @@ import { isNode, isScalar, parse, parseDocument } from 'yaml';
|
|
|
6
6
|
import { defaultPlan, readDocsConfig, readHookConfig, readMigrateConfig, readNewsConfig, readOrchestrateConfig, readPerfConfig, readPlanConfig, readReferencesConfig, readSessionConfig } from './config.js';
|
|
7
7
|
import { parseDeliverConfig } from './deliver.js';
|
|
8
8
|
import { commitPaths, dirtyPaths, gitRoot, restorePaths } from './git.js';
|
|
9
|
-
import { findProjects, isProject, readDepth, readLeadUx } from './lead.js';
|
|
9
|
+
import { findProjects, isProject, readDepth, readLeadContextThreshold, readLeadUx } from './lead.js';
|
|
10
10
|
import { readPriority } from './orchestrate/continue.js';
|
|
11
11
|
import { Plan, RafError } from './plan.js';
|
|
12
12
|
const KEY = /^[A-Za-z0-9_-]+(\.[A-Za-z0-9_-]+)*$/;
|
|
@@ -26,6 +26,7 @@ export function validateConfig(file, root) {
|
|
|
26
26
|
readReferencesConfig(file);
|
|
27
27
|
readMigrateConfig(file);
|
|
28
28
|
readLeadUx(dirname(file));
|
|
29
|
+
readLeadContextThreshold(dirname(file));
|
|
29
30
|
readDepth(dirname(file));
|
|
30
31
|
readPriority(dirname(file));
|
|
31
32
|
const text = readFileSync(file, 'utf8');
|
package/dist/config.js
CHANGED
|
@@ -338,7 +338,7 @@ export function readVerifyConfig(file) {
|
|
|
338
338
|
return { inPlace: v.inPlace };
|
|
339
339
|
}
|
|
340
340
|
const REVIEW_KEYS = ['threshold', 'light', 'full'];
|
|
341
|
-
const ORCH_KEYS = ['start', 'verdict', 'test', 'testReplay', 'build', 'worktreeDeps', 'accept', 'maquette', 'maquetteReviews', 'fiche', 'ux', 'precheck', 'prodAccess', 'worktrees', 'testGate', 'spec', 'review', 'escalation', 'continue', 'effort', 'permissionMode', 'addDirs', 'timeouts'];
|
|
341
|
+
const ORCH_KEYS = ['start', 'verdict', 'test', 'testReplay', 'build', 'worktreeDeps', 'gate', 'accept', 'maquette', 'maquetteReviews', 'fiche', 'ux', 'precheck', 'prodAccess', 'worktrees', 'testGate', 'spec', 'review', 'escalation', 'continue', 'effort', 'permissionMode', 'addDirs', 'timeouts'];
|
|
342
342
|
/** Clé `orchestrate:` de cadence.yaml. Absente : les défauts (auto, 45 min d'implémentation, 25 min de revue). */
|
|
343
343
|
export function readOrchestrateConfig(file) {
|
|
344
344
|
const config = { precheck: true, maquette: true, maquetteReviews: 3, fiche: true, precheckLocal: false, prodAccess: false, worktrees: false, spec: { dir: 'docs/specs' }, review: { threshold: 0.25, light: 'sonnet', full: 'opus' }, effort: { precheck: 'low', spec: 'medium', maquette: 'high', fiche: 'medium', accept: 'medium', implement: 'medium', fix: 'medium', review: 'high', ux: 'high' }, permissionMode: 'auto', addDirs: [], timeouts: { work: 45 * 60_000, review: 25 * 60_000 } };
|
|
@@ -392,6 +392,21 @@ export function readOrchestrateConfig(file) {
|
|
|
392
392
|
throw bad('worktreeDeps : liste de chemins relatifs au dépôt, sans « .. » (node_modules, .venv…)');
|
|
393
393
|
config.worktreeDeps = deps.map((x) => x.trim().replace(/\/$/, ''));
|
|
394
394
|
}
|
|
395
|
+
if (o.gate != null) {
|
|
396
|
+
if (!isObject(o.gate))
|
|
397
|
+
throw bad('gate : objet attendu (gate: { readsPlan: true })');
|
|
398
|
+
for (const k of Object.keys(o.gate))
|
|
399
|
+
if (k !== 'readsPlan')
|
|
400
|
+
throw bad(`gate.${k} inconnu (attendu : readsPlan)`);
|
|
401
|
+
const r = o.gate.readsPlan;
|
|
402
|
+
const msg = 'gate.readsPlan : true, false ou liste de chemins de tests relatifs au dépôt, sans « .. »';
|
|
403
|
+
if (r === true)
|
|
404
|
+
config.gate = { readsPlan: true };
|
|
405
|
+
else if (Array.isArray(r) && r.length && r.every((x) => typeof x === 'string' && x.trim() && !isAbsolute(x) && !x.split(/[\\/]/).includes('..')))
|
|
406
|
+
config.gate = { readsPlan: r.map((x) => x.trim()) };
|
|
407
|
+
else if (r !== false)
|
|
408
|
+
throw bad(msg);
|
|
409
|
+
}
|
|
395
410
|
if (o.maquette != null) {
|
|
396
411
|
if (typeof o.maquette !== 'boolean')
|
|
397
412
|
throw bad('maquette : true ou false attendu');
|
package/dist/lead.js
CHANGED
|
@@ -16,11 +16,11 @@ import { cadenceHome } from './orchestrate/registry.js';
|
|
|
16
16
|
import { staleNotes, staleText } from './stale-notes.js';
|
|
17
17
|
import { noTestGate, noTestText } from './orchestrate/no-test.js';
|
|
18
18
|
import { KEPT_TITLE, keptLine, keptTrees } from './orchestrate/worktree.js';
|
|
19
|
-
/**
|
|
20
|
-
|
|
19
|
+
/** Le bloc `lead:` du cadence.yaml du dossier parent (absent = `undefined`) ; un `lead` qui n'est pas une table est refusé en nommant `key`. */
|
|
20
|
+
function readLeadBlock(parent, key, expected) {
|
|
21
21
|
const file = join(parent, 'cadence.yaml');
|
|
22
22
|
if (!existsSync(file))
|
|
23
|
-
return
|
|
23
|
+
return undefined;
|
|
24
24
|
let raw;
|
|
25
25
|
try {
|
|
26
26
|
raw = parse(readFileSync(file, 'utf8'));
|
|
@@ -30,16 +30,33 @@ export function readLeadUx(parent) {
|
|
|
30
30
|
}
|
|
31
31
|
const l = raw?.lead;
|
|
32
32
|
if (l == null)
|
|
33
|
-
return
|
|
34
|
-
const ux = l.ux;
|
|
33
|
+
return undefined;
|
|
35
34
|
if (typeof l !== 'object' || Array.isArray(l))
|
|
36
|
-
throw new RafError(`${file} :
|
|
35
|
+
throw new RafError(`${file} : ${key} doit être ${expected}`);
|
|
36
|
+
return { file, block: l };
|
|
37
|
+
}
|
|
38
|
+
/** Clé `lead.ux` du cadence.yaml du dossier parent (L197) : qui pose le verdict UX (`raf ux`) d'un lot visible. `human` (défaut) : l'humain ; `lead` : le lead, l'humain n'étant consulté qu'en cas de doute. */
|
|
39
|
+
export function readLeadUx(parent) {
|
|
40
|
+
const found = readLeadBlock(parent, 'lead.ux', '« lead » ou « human »');
|
|
41
|
+
const ux = found?.block.ux;
|
|
37
42
|
if (ux == null)
|
|
38
43
|
return 'human';
|
|
39
44
|
if (ux !== 'lead' && ux !== 'human')
|
|
40
|
-
throw new RafError(`${file} : lead.ux doit être « lead » ou « human »`);
|
|
45
|
+
throw new RafError(`${found.file} : lead.ux doit être « lead » ou « human »`);
|
|
41
46
|
return ux;
|
|
42
47
|
}
|
|
48
|
+
/** Seuil de contexte du lead quand `lead.contextThreshold` n'est pas posé (L437). */
|
|
49
|
+
export const CONTEXT_THRESHOLD_DEFAULT = 150_000;
|
|
50
|
+
/** 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. */
|
|
51
|
+
export function readLeadContextThreshold(parent) {
|
|
52
|
+
const found = readLeadBlock(parent, 'lead.contextThreshold', 'un entier ≥ 1 (tokens)');
|
|
53
|
+
const t = found?.block.contextThreshold;
|
|
54
|
+
if (t == null)
|
|
55
|
+
return CONTEXT_THRESHOLD_DEFAULT;
|
|
56
|
+
if (typeof t !== 'number' || !Number.isInteger(t) || t < 1)
|
|
57
|
+
throw new RafError(`${found.file} : lead.contextThreshold doit être un entier ≥ 1 (tokens), pas ${JSON.stringify(t)}`);
|
|
58
|
+
return t;
|
|
59
|
+
}
|
|
43
60
|
/** Clé `depth:` du cadence.yaml du dossier parent (L201, issue #8), à côté de `priority:` : combien de niveaux `lead tour` parcourt (1 : sous-dossiers directs, le défaut). */
|
|
44
61
|
export function readDepth(parent) {
|
|
45
62
|
const file = join(parent, 'cadence.yaml');
|
|
@@ -2,6 +2,7 @@ import { readFileSync } from 'node:fs';
|
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { fileURLToPath } from 'node:url';
|
|
4
4
|
import { RafError } from '../plan.js';
|
|
5
|
+
import { wipBranch } from './tiers.js';
|
|
5
6
|
/** Gabarits livrés avec le paquet (à la racine, à côté de dist/ et src/). */
|
|
6
7
|
export const TEMPLATES_DIR = fileURLToPath(new URL('../../templates/orchestrate', import.meta.url));
|
|
7
8
|
/** Consigne sur les dépôts voisins d'un lot (L62) : les sessions d'écriture y travaillent, les relectures y lisent les commits qui citent le lot. */
|
|
@@ -24,6 +25,14 @@ export function reposText(role, lot, repos) {
|
|
|
24
25
|
`\`raf commits ${lot}\` lists them all; read the diff of the neighbouring ones with \`git -C <path> log\` / \`git -C <path> show\` (${repos.map((r) => `git -C ${r.path} log`).join(' ; ')}), and review them like the rest. You are read-only there too.`,
|
|
25
26
|
].join('\n');
|
|
26
27
|
}
|
|
28
|
+
/**
|
|
29
|
+
* Consigne des dépôts tiers (L456) d'une session d'écriture : jamais de commit sur la branche principale d'un autre dépôt que celui du lot et que ses voisins déclarés,
|
|
30
|
+
* cité par le lot ou non (cluster-sante L1 : 3 commits sur developpeur-gitops main le 30-10) ; la voie est `wip/<projet>-<lot>`. Le programme le contrôle en fin de session.
|
|
31
|
+
*/
|
|
32
|
+
export function tiersText(project, lot) {
|
|
33
|
+
const wip = wipBranch(project, lot);
|
|
34
|
+
return `Never commit on the main branch (\`origin/HEAD\`, \`main\` or \`master\`) of any repository other than this lot's own and the ones it declares with \`repos:\`, whether the lot's text cites that repository or not: a sibling repository of the projects folder is not yours to move (a GitOps repository can be one script away from production). If the lot's work needs a change there, create the branch \`${wip}\` in that repository and commit on it, never push, and say it under "choix". At the end of the session the program checks those repositories: a moved main branch hands the lot back to the lead, and the commit is not undone.`;
|
|
35
|
+
}
|
|
27
36
|
/** Revue courte du diff rebasé (L279, R17) : la branche du lot vient d'être rebasée sur celle du dépôt et son diff n'est plus celui que la revue a lu. */
|
|
28
37
|
export function integrationText(base) {
|
|
29
38
|
return `Integration re-review: this lot's branch was just rebased onto \`${base}\`, which moved while the lot was being written, and the diff of the lot is no longer the one an earlier review read (context changed or a conflict was resolved). Review the rebased diff only (\`git diff ${base}...HEAD\`): report a finding only for what the rebase changed or broke in the lot's own change. The program has already re-run the tests and the build on the rebased tip.`;
|
|
@@ -2,11 +2,11 @@ import { spawn } from 'node:child_process';
|
|
|
2
2
|
import { parse } from 'yaml';
|
|
3
3
|
import { toolBinOf, withoutLaunchVars } from './snapshot.js';
|
|
4
4
|
import { randomUUID } from 'node:crypto';
|
|
5
|
-
import { delimiter, dirname, isAbsolute, join, posix, relative, resolve } from 'node:path';
|
|
5
|
+
import { basename, delimiter, dirname, isAbsolute, join, posix, relative, resolve } from 'node:path';
|
|
6
6
|
import { copyFileSync, existsSync, lstatSync, mkdirSync, mkdtempSync, readFileSync, renameSync, rmSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
7
7
|
import { tmpdir } from 'node:os';
|
|
8
8
|
import { coveredTasks, lotCommits, lotWork } from '../audit.js';
|
|
9
|
-
import { readDocsConfig, readPlanConfig } from '../config.js';
|
|
9
|
+
import { planLoader, readDocsConfig, readPlanConfig } from '../config.js';
|
|
10
10
|
import { PLAN_ACTIONS, leadAnswerMessage, planCommitMessage } from '../plan-commit.js';
|
|
11
11
|
import { articleRepoDir, articlesBrief, docSyncBrief, docSyncGaps, filesOf, gapMessage } from '../docsync.js';
|
|
12
12
|
import { testGateMissing } from '../testgate.js';
|
|
@@ -19,7 +19,7 @@ import { isPlanOnly } from '../audit.js';
|
|
|
19
19
|
import { compareBaseline, failedTests, hasUnnamedFailure } from './baseline.js';
|
|
20
20
|
import { replayRed, TEST_WORKERS_ENV, testWorkersText } from './workers.js';
|
|
21
21
|
import { APP_TIMEOUT_S, startApp } from './app.js';
|
|
22
|
-
import { capturesText, gateText, headline, integrationText, localReading, newsText, objective, renderBrief, reposText, shotsText, TEMPLATES_DIR, tmpText } from './briefs.js';
|
|
22
|
+
import { capturesText, gateText, headline, integrationText, localReading, newsText, objective, renderBrief, reposText, shotsText, TEMPLATES_DIR, tiersText, tmpText } from './briefs.js';
|
|
23
23
|
import { SlotAbandoned } from './registry.js';
|
|
24
24
|
import { journalTokens, peakContext, mcpServersFor, playwrightDir, runSession, trackGroup, writeMcpConfig, enabledPlugins, settingsFiles } from './launch.js';
|
|
25
25
|
import { cleanPlaywrightOutput, pushed, remoteAdvances, snapshot } from './guard.js';
|
|
@@ -36,6 +36,7 @@ import { decisionsText, humanDecisions } from './decisions.js';
|
|
|
36
36
|
import { LOCAL_PRECHECK_UNREADABLE, PRECHECK_UNREADABLE, lotKey, lotRepoPaths, neighbourDirs } from './state.js';
|
|
37
37
|
import { advance, baseMoved, INTEGRATE_WAIT_MS, MAX_REPLAYS, mergeBase, patchId, rebaseOnto, repoBranch, takeRepoWrite } from './integrate.js';
|
|
38
38
|
import { mutexFor } from './mutex.js';
|
|
39
|
+
import { readTier, takeTiers, tierMoved, tierRepos, wipBranch } from './tiers.js';
|
|
39
40
|
import { branchExists, git, gitFailure, integrated, linkIgnored, lotBranch, planFileSet, planSeenFrom, prepareTree, removeTree, trackedDirty } from './worktree.js';
|
|
40
41
|
/** Passes de correction au plus, puis la main est rendue au lead. */
|
|
41
42
|
export const MAX_PASSES = 2;
|
|
@@ -571,7 +572,9 @@ function briefFor(c, kind, local = false) {
|
|
|
571
572
|
if (l.pendingAnswer)
|
|
572
573
|
vars.reponse = `Answer from the human to your earlier question: ${l.pendingAnswer}`;
|
|
573
574
|
}
|
|
574
|
-
|
|
575
|
+
const brief = renderBrief(briefName(l, kind, !!c.integration), vars, c.wave.templates);
|
|
576
|
+
// Les dépôts tiers (L456) : la consigne s'ajoute à tout brief d'écriture, sans passer par un gabarit (un gabarit modifié change l'empreinte que d'autres tests gardent).
|
|
577
|
+
return kind === 'implement' || kind === 'fix' || kind === 'spec' || kind === 'accept' ? `${brief}\n${tiersText(l.project, l.lot)}\n` : brief;
|
|
575
578
|
}
|
|
576
579
|
/** Tokens comptés par les sessions d'un lot (un lot repris compte ses étapes d'avant). */
|
|
577
580
|
export const lotSpent = (l) => l.steps.reduce((n, s) => n + (s.tokens?.counted ?? 0), 0);
|
|
@@ -683,6 +686,7 @@ async function session(c, kind, local = false) {
|
|
|
683
686
|
const before = await snapshot(wd(c));
|
|
684
687
|
const neighbours = l.repos ?? [];
|
|
685
688
|
const beforeOthers = await Promise.all(neighbours.map((r) => snapshot(r.path)));
|
|
689
|
+
const tiersBefore = write ? takeTiers(thirdRepos(c), (line) => w.log(`${lotKey(l.project, l.lot)} · ${line}`)) : null;
|
|
686
690
|
const n = l.steps.length + 1;
|
|
687
691
|
const sessionId = randomUUID();
|
|
688
692
|
const step = { n, kind, model, ...(effort !== 'default' ? { effort } : {}), status: 'running', sessionId, started: new Date().toISOString(), headBefore: before.head ?? undefined };
|
|
@@ -873,6 +877,9 @@ async function session(c, kind, local = false) {
|
|
|
873
877
|
writeFileSync(join(dir, `${base}.json`), outcome.stdout);
|
|
874
878
|
writeFileSync(join(dir, `${base}.err`), outcome.stderr);
|
|
875
879
|
step.report = `${base}.json`;
|
|
880
|
+
// Un commit sur la branche principale d'un dépôt tiers se dit aussi quand la session est en échec ou coupée au délai : sinon la relance le prendrait pour référence.
|
|
881
|
+
if (tiersBefore && checkTiers(c, kind, step, tiersBefore))
|
|
882
|
+
return null;
|
|
876
883
|
step.status = 'failed';
|
|
877
884
|
step.cause = outcome.cause;
|
|
878
885
|
if (write && outcome.cause === 'délai dépassé') {
|
|
@@ -903,9 +910,11 @@ async function session(c, kind, local = false) {
|
|
|
903
910
|
if (outcome.kind === 'quota') {
|
|
904
911
|
if (outcome.firstStdout !== undefined)
|
|
905
912
|
writeFileSync(join(dir, `${base}.first.json`), outcome.firstStdout);
|
|
913
|
+
w.quota = { hit: true, message: outcome.message };
|
|
914
|
+
if (tiersBefore && checkTiers(c, kind, step, tiersBefore))
|
|
915
|
+
return null;
|
|
906
916
|
step.status = 'interrupted';
|
|
907
917
|
step.cause = outcome.message;
|
|
908
|
-
w.quota = { hit: true, message: outcome.message };
|
|
909
918
|
w.saveWave();
|
|
910
919
|
c.lot.next = kind;
|
|
911
920
|
transition(c, 'suspended', 'quota atteint');
|
|
@@ -953,6 +962,8 @@ async function session(c, kind, local = false) {
|
|
|
953
962
|
return stop(c, 'failed', `incident : ${w.incident}`);
|
|
954
963
|
}
|
|
955
964
|
}
|
|
965
|
+
if (tiersBefore && checkTiers(c, kind, step, tiersBefore))
|
|
966
|
+
return null;
|
|
956
967
|
for (const { where, path, before: b, after: a } of watched) {
|
|
957
968
|
// Session d'écriture finie sur une autre branche ou en HEAD détaché (L214) : pas un push, mais les contrôles et le lot suivant partiraient d'un HEAD qui n'est plus la branche du lot.
|
|
958
969
|
if (write && b.branch !== a.branch) {
|
|
@@ -1019,6 +1030,53 @@ async function session(c, kind, local = false) {
|
|
|
1019
1030
|
save(c);
|
|
1020
1031
|
return { step, report: res.structured, before, after, others };
|
|
1021
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 d'une autre vague en cours du même dossier (leur branche principale bouge légitimement, R14). */
|
|
1034
|
+
function thirdRepos(c) {
|
|
1035
|
+
const mine = lotKey(c.lot.project, c.lot.lot);
|
|
1036
|
+
const others = [...c.wave.store.lots().filter((x) => lotKey(x.project, x.lot) !== mine), ...c.wave.store.liveWaveLots()].flatMap(lotRepoPaths);
|
|
1037
|
+
return tierRepos(c.lot.repo, [...neighbourDirs(c.lot), ...others]);
|
|
1038
|
+
}
|
|
1039
|
+
/**
|
|
1040
|
+
* Fin d'une session d'écriture (L456) : la branche principale d'un dépôt tiers a-t-elle bougé ? Alors le lot est rendu avec le constat, le commit n'est pas défait,
|
|
1041
|
+
* le dépôt est marqué sale pour la vague. Un tiers resté sur la branche wip du lot est dit en avertissement, sans être remis. Un dépôt illisible ou sans branche
|
|
1042
|
+
* principale des deux côtés est ignoré, au journal. Vrai quand le lot est rendu : la session s'arrête là.
|
|
1043
|
+
*/
|
|
1044
|
+
function checkTiers(c, kind, step, before) {
|
|
1045
|
+
const l = c.lot;
|
|
1046
|
+
const w = c.wave;
|
|
1047
|
+
const key = lotKey(l.project, l.lot);
|
|
1048
|
+
const wip = wipBranch(l.project, l.lot);
|
|
1049
|
+
for (const [repo, b] of before) {
|
|
1050
|
+
const name = basename(repo);
|
|
1051
|
+
let a;
|
|
1052
|
+
try {
|
|
1053
|
+
a = readTier(repo);
|
|
1054
|
+
}
|
|
1055
|
+
catch (e) {
|
|
1056
|
+
w.log(`${key} · dépôt tiers ${name} ignoré : lecture impossible (${gitFailure(e)})`);
|
|
1057
|
+
continue;
|
|
1058
|
+
}
|
|
1059
|
+
if (!Object.keys(b.mains).length && !Object.keys(a.mains).length) {
|
|
1060
|
+
w.log(`${key} · dépôt tiers ${name} ignoré : aucune branche principale`);
|
|
1061
|
+
}
|
|
1062
|
+
else {
|
|
1063
|
+
const moved = tierMoved(repo, b, a, relative(l.repo, c.loadPlan().path));
|
|
1064
|
+
if (moved) {
|
|
1065
|
+
step.status = 'failed';
|
|
1066
|
+
step.cause = `commit sur la branche principale d'un autre dépôt (${name}, ${moved.branch})`;
|
|
1067
|
+
(w.dirtyRepos ??= new Map()).set(repo, key);
|
|
1068
|
+
stop(c, 'handed-back', `incident : commit sur la branche principale d'un autre dépôt (${name}, ${moved.branch}) pendant ${key} (${kind}) : ${moved.detail} ; la voie est la branche ${wip}`);
|
|
1069
|
+
return true;
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
if (a.branch === wip && b.branch !== wip) {
|
|
1073
|
+
const text = `${kind} : la session a laissé le dépôt tiers ${name} sur la branche ${wip} ; elle n'est pas remise`;
|
|
1074
|
+
l.warnings.push(text);
|
|
1075
|
+
w.log(`${key} · ${text}`);
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
return false;
|
|
1079
|
+
}
|
|
1022
1080
|
/**
|
|
1023
1081
|
* Une session d'écriture a-t-elle touché, dans son worktree, le fichier de plan ou un fichier généré avec lui (plan.files, la page Gantt) ? (R21) Le plan n'est
|
|
1024
1082
|
* écrit que par le programme, dans le dépôt du projet : un commit de plan sur la branche du lot reviendrait en conflit au rebase. Rend « (fichier, commit sha) »
|
|
@@ -2318,15 +2376,42 @@ async function work(c, kind) {
|
|
|
2318
2376
|
const noCommitNote = (l) => (l.warnings.some((w) => w.startsWith('passe des mineurs sans commit')) ? ' sans commit' : '');
|
|
2319
2377
|
/** Revue de code qui clôt le cycle : complète, ou courte une fois la passe des mineurs jouée. */
|
|
2320
2378
|
const codeReview = (l) => (l.minorPass ? 'review-small' : 'review');
|
|
2379
|
+
/** Note d'un lot que sa session a rendu `visible` après le lancement de la vague (L454) : son verdict UX n'existe pas encore, le lead le sait. */
|
|
2380
|
+
const VISIBLE_ADDED = 'visible ajouté en cours de vague, étape UX à jouer';
|
|
2381
|
+
/**
|
|
2382
|
+
* Relit `visible` dans le plan du worktree après une session d'écriture (L454) : un lot que sa session a rendu visible (warhammer L37 et L43,
|
|
2383
|
+
* 30-10) est lancé non visible, et sortait « prêt » sans étape UX. Il le devient dans l'état du lot, donc l'UX est jouée (`uxAfterReview`, ou la
|
|
2384
|
+
* revue courte d'un petit lot) ; sans application déclarée, il est rendu avec la note. Un plan illisible, ou un lot qui n'est plus visible
|
|
2385
|
+
* dans le plan, laisse l'état tel quel (retirer `visible` en cours de vague n'est pas traité).
|
|
2386
|
+
*/
|
|
2387
|
+
function addedVisible(c) {
|
|
2388
|
+
const l = c.lot;
|
|
2389
|
+
if (l.visible)
|
|
2390
|
+
return;
|
|
2391
|
+
let now;
|
|
2392
|
+
try {
|
|
2393
|
+
now = (wd(c) === l.repo ? c.loadPlan() : planLoader(wd(c))()).lot(l.lot).visible;
|
|
2394
|
+
}
|
|
2395
|
+
catch {
|
|
2396
|
+
return;
|
|
2397
|
+
}
|
|
2398
|
+
if (!now)
|
|
2399
|
+
return;
|
|
2400
|
+
l.visible = true;
|
|
2401
|
+
if (!c.config.ux)
|
|
2402
|
+
l.uxNote = `${VISIBLE_ADDED} (aucune application déclarée : cadence.yaml orchestrate.ux)`;
|
|
2403
|
+
c.wave.log(`${lotKey(l.project, l.lot)} · ${VISIBLE_ADDED}`);
|
|
2404
|
+
}
|
|
2321
2405
|
/**
|
|
2322
2406
|
* Première étape après un travail d'écriture (L221) : la revue de code, jamais l'UX. La revue UX d'un lot visible est jouée une seule fois,
|
|
2323
2407
|
* après la dernière revue de code conforme (`uxAfterReview`), sur le code qui sera livré.
|
|
2324
2408
|
*/
|
|
2325
2409
|
function firstReview(c) {
|
|
2326
2410
|
const l = c.lot;
|
|
2411
|
+
addedVisible(c);
|
|
2327
2412
|
if (l.small)
|
|
2328
2413
|
return l.visible || l.minorPass ? 'review-small' : 'review'; // passe unique code + ergonomie ; sans écran, une revue de code
|
|
2329
|
-
if (l.visible && !c.config.ux)
|
|
2414
|
+
if (l.visible && !c.config.ux && !l.uxNote?.startsWith(VISIBLE_ADDED))
|
|
2330
2415
|
l.uxNote = 'UX à faire par le lead (aucune application déclarée : cadence.yaml orchestrate.ux)';
|
|
2331
2416
|
return codeReview(l);
|
|
2332
2417
|
}
|
|
@@ -3,6 +3,7 @@ import { appendFileSync, existsSync, lstatSync, mkdirSync, readFileSync, readdir
|
|
|
3
3
|
import { tmpdir } from 'node:os';
|
|
4
4
|
import { join, relative } from 'node:path';
|
|
5
5
|
import { gitCommonDir, gitRoot } from '../git.js';
|
|
6
|
+
import { pidAlive } from '../state.js';
|
|
6
7
|
/** Début de l'avertissement d'un rapport de précheck illisible (Sonnet) : le journal le garde, `cadence perf` s'en sert pour ne pas compter ce lot « non » (L293). */
|
|
7
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). */
|
|
8
9
|
export const LOCAL_PRECHECK_UNREADABLE = 'contrôle préalable : rapport du modèle local illisible';
|
|
@@ -127,6 +128,12 @@ export class RunStore {
|
|
|
127
128
|
const f = join(this.dir, lotFile(project, lot));
|
|
128
129
|
return existsSync(f) ? JSON.parse(readFileSync(f, 'utf8')) : null;
|
|
129
130
|
}
|
|
131
|
+
/** Les lots des AUTRES vagues en cours du même dossier de lancement (L456) : vague `running` dont le processus vit. Leurs dépôts avancent sans que cette vague y soit pour rien. */
|
|
132
|
+
liveWaveLots() {
|
|
133
|
+
return RunStore.all(this.launchDir)
|
|
134
|
+
.filter(({ store, wave }) => store.id !== this.id && wave?.status === 'running' && pidAlive(wave.pid))
|
|
135
|
+
.flatMap(({ store }) => store.lots());
|
|
136
|
+
}
|
|
130
137
|
/** Les lots de la vague, dans l'ordre donné au lancement (`wave.lots`) : une reprise rejoue les dépendances avant les dépendants. */
|
|
131
138
|
lots() {
|
|
132
139
|
const all = readdirSync(this.dir)
|