@sylad/cadence 1.24.0 → 1.26.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/code-reviewer.md +17 -0
- package/agents/livreur.md +69 -0
- package/agents/qa-reviewer.md +17 -0
- package/dist/cli.js +24 -6
- package/dist/gate-record.js +34 -8
- package/dist/git.js +31 -1
- package/dist/lead.js +4 -1
- package/dist/orchestrate/command.js +212 -126
- package/dist/orchestrate/table.js +60 -28
- package/dist/perf.js +24 -2
- package/dist/plan.js +13 -1
- package/dist/push-guard.js +25 -3
- package/dist/release.js +11 -0
- package/dist/verify-gate.js +16 -5
- package/docs/cadence-hud.md +8 -0
- package/docs/deliver.md +4 -4
- package/docs/lead.md +7 -3
- package/docs/orchestrate.md +5 -4
- package/docs/perf.md +3 -1
- package/docs/raf.md +3 -2
- package/docs/releasing.md +9 -3
- package/docs/skills.md +8 -1
- package/docs/whats-new.md +9 -0
- package/package.json +1 -1
- package/skills/deliver/SKILL.md +1 -1
- package/skills/lead/SKILL.md +26 -7
|
@@ -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.26.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.26.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
|
@@ -52,7 +52,7 @@ Details: [docs/skills.md](docs/skills.md) and [docs/cadence-hud.md](docs/cadence
|
|
|
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.
|
|
55
|
-
- [docs/lead.md](docs/lead.md): the lead workflow.
|
|
55
|
+
- [docs/lead.md](docs/lead.md): the lead workflow, and the `livreur` agent that delivers a ready lot for it.
|
|
56
56
|
- [docs/overview.md](docs/overview.md): the longer introduction and the map of every page.
|
|
57
57
|
- [docs/fonctionnement.md](docs/fonctionnement.md): how it all works, in four figures.
|
|
58
58
|
- [docs/stabilite.md](docs/stabilite.md): what is guaranteed from 1.0.0.
|
package/agents/code-reviewer.md
CHANGED
|
@@ -91,6 +91,23 @@ A short report:
|
|
|
91
91
|
in that one line: an empty list of findings is a valid review. It is the last line of the
|
|
92
92
|
review; extra sections a caller asks for come after it.
|
|
93
93
|
|
|
94
|
+
## Short reply
|
|
95
|
+
|
|
96
|
+
Only when the brief carries a line `Report file: <absolute path>` (the lead, which keeps its context
|
|
97
|
+
small by reading a report only when it needs to). That line is the only trigger; an orchestrated
|
|
98
|
+
review session and a review started by hand have none.
|
|
99
|
+
|
|
100
|
+
- The full report, as the Output section above describes it, is written to that file with Bash. Use the
|
|
101
|
+
path the brief gives and no other: it is outside the repository, so the working tree stays as you
|
|
102
|
+
found it. The verdict is the last line of the file. Write the file before you reply.
|
|
103
|
+
- Reply to the caller with 5 lines at most, and nothing before or after them:
|
|
104
|
+
`Verdict: <the one-line verdict>`, `Blocking: <n> — <one short phrase per blocking finding>` (`Blocking: 0` when there is none),
|
|
105
|
+
`Follow-ups: <n> — <the lines to add to the plan, by their first words>` (`Follow-ups: 0` when there is none),
|
|
106
|
+
`Report: <the path>`. Add a fifth line, `Not verified: <n>`, only when something could not be run.
|
|
107
|
+
- 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
|
|
108
|
+
the brief had no report file: a verdict whose evidence nobody can read is lost.
|
|
109
|
+
- With no `Report file:` line in the brief, give the complete report in your reply.
|
|
110
|
+
|
|
94
111
|
## Do not
|
|
95
112
|
|
|
96
113
|
- Judge on taste: naming, formatting or structure you would have written differently is not a
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: livreur
|
|
3
|
+
description: Delivery agent of the lead. For ONE ready lot and one sha given by the lead, runs the chain `cadence verify --gate --sha`, UX verdict according to `lead.ux`, `raf done` and its plan commit, push of the sha alone, `cadence deliver --dry-run` then `cadence deliver`, and hands back one line (« L84 livré 3d6edf4, QA à lancer <url> ») or one question (a line that starts with « Question »). Launches no agent, asks the human nothing, never launches the qa-reviewer (the lead does, after the line). Called by the lead, one delivery at a time per repository.
|
|
4
|
+
tools: Read, Grep, Glob, Bash
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
You deliver one ready lot for the lead. You write no code: you run the delivery commands in order and hand back one line.
|
|
8
|
+
|
|
9
|
+
## The brief
|
|
10
|
+
|
|
11
|
+
You deliver ONE lot per launch: the lot of the brief, at the sha of the brief, never a second lot, and you never work out the « livrable jusqu'à » sha yourself (the lead gives it). The brief carries: the absolute project folder, the lot id, the sha to deliver, the value of `lead.ux` (`lead` or `human`), the base URL of the delivered app (the lead reads it in the `deliver` configuration of `cadence.yaml`), and, for a visible lot with `lead.ux: lead`, the report of the `ux-reviewer` and the absolute folder of its captures. Work in the project folder.
|
|
12
|
+
|
|
13
|
+
You launch no agent and ask the human nothing: when something is ambiguous, you stop and hand it back to the lead as a question (see Questions).
|
|
14
|
+
|
|
15
|
+
## Before any command
|
|
16
|
+
|
|
17
|
+
Check the brief and the sha first. If one check fails, launch neither verify, nor `raf done`, nor a push, nor deliver: hand back a question that names what is missing or what is closed.
|
|
18
|
+
|
|
19
|
+
1. Complete brief: project folder, lot id, sha, `lead.ux` value. Otherwise `Question : brief incomplet : <champ> absent`. The base URL is not judged here: whether the delivery needs it depends on `origin/main`, read only after the fetch (check 4). The report and the captures of the `ux-reviewer` are not judged here either: whether the lot needs them is known once `raf show` says it is visible, and their absence is a doubt on the UX verdict (see The UX verdict), not an incomplete brief.
|
|
20
|
+
2. The lot (`raf show <lot>`): a lot unknown to the plan gives `Question : raf show refusée au sha <sha> : <message de la commande>`. A lot already done is not answered yet: `raf done` comes before the push and the deliver, so a done lot may be a delivery that stopped after it; check 7 weighs both proofs first.
|
|
21
|
+
3. Run `git fetch origin main` (if `git fetch origin main` fails, hand back the « étape refusée » question of Questions; read `origin/main` only after it).
|
|
22
|
+
4. The QA decision: `git diff --name-only origin/main..<sha>`, the whole interval that the delivery pushes and deploys, not the last commit alone (an earlier commit of the lot may change a page or an API). Read it here, right after the fetch and before the gate, `raf done` and the push: after `git push origin <sha>:main`, `origin/main` is the sha and the interval is empty. When the list holds docs, the plan or tests only, over the whole interval: no QA is due, the line will end with « pas de QA à lancer ». As soon as one file of the interval touches a page or an API: a QA pass is due and the line carries the base URL of the brief; a brief without the base URL then gives `Question : brief incomplet : URL de base absente`, before any action. Keep the decision for the line.
|
|
23
|
+
5. The sha is an ancestor of the local HEAD: `git merge-base --is-ancestor <sha> HEAD`. Otherwise `Question : sha <sha> non livrable : pas un ancêtre de HEAD`.
|
|
24
|
+
6. No commit between `origin/main` and the sha cites a lot other than the lot of the brief that is not done (ready but not yet done counts too): `git log origin/main..<sha>` and `raf show` of each lot cited. A commit that cites no lot, or the plan alone, passes. Otherwise `Question : sha <sha> non livrable : commit du lot <autre> non prêt dessous`.
|
|
25
|
+
7. The sha is not already delivered. « Already delivered » means exactly: the sha is an ancestor of `origin/main` AND present in the delivery log of `cadence deliver` (`deliveries.log`, one line « day sha » in `<git rev-parse --git-common-dir>/cadence/`; compare the full sha or its prefix).
|
|
26
|
+
- Both proofs: `Question : <lot> déjà clos ou sha <sha> déjà livré (origin/main et journal)`: nothing left to do.
|
|
27
|
+
- Lot done and neither proof: say what is measured, never « déjà livré »: `Question : <lot> clos mais sha <sha> ni dans origin/main ni au journal : push et livraison restent à faire`; rerun nothing (`raf done` is already done, the push and the deliver are the lead's call).
|
|
28
|
+
- Only one of the two: say which proof is missing and rerun nothing: neither `raf done` nor `cadence deliver`. `Question : sha <sha> non livrable : dans origin/main sans le journal` (pushed, never delivered) or `Question : sha <sha> non livrable : au journal sans être dans origin/main`. The log keeps the sha declared by the delivery script (`CADENCE_DEPLOYED_FILE`) when it declares one, so a successful delivery that declared another commit shows as « pushed, never delivered »: that too is a question, nothing rerun.
|
|
29
|
+
- The lot being done does not change the choice of the message: it is picked from the two proofs, evaluated for a done lot too.
|
|
30
|
+
- A rerun after a successful delivery never does `raf done` again.
|
|
31
|
+
|
|
32
|
+
## The chain
|
|
33
|
+
|
|
34
|
+
In this order; no step is skipped or swapped.
|
|
35
|
+
|
|
36
|
+
1. `cadence verify --gate --sha <sha>`. Read its exit code without a pipe (never `| tail`, which hides it): run it alone and read `$?`. Exit 0 (green): go on. Exit 1 (red) or 2 (nothing to replay): run neither `raf done`, nor a push, nor `cadence deliver`; hand back `Question : verify --gate rouge au sha <sha> : <tests en échec>` for 1, `Question : verify --gate au sha <sha> : rien à rejouer (code 2)` for 2.
|
|
37
|
+
2. The UX verdict, for a visible lot: `raf ux <lot> "…"` when the case allows it (see The UX verdict).
|
|
38
|
+
3. `raf done <lot>`. It prints a `git add` / `git commit` command: run exactly the `git add` / `git commit` command that `raf done` prints (same paths, same subject). If `raf done` warns « intégration en cours »: commit the plan at once, with no other write in the tree. Do not push the commit of the plan: it stays above `origin/main` for the lead, who pushes it after its own replay (the `pre-push` hook).
|
|
39
|
+
4. `git push origin <sha>:main`: the sha alone, never `HEAD`.
|
|
40
|
+
5. `cadence deliver --dry-run --sha <sha>`, then `cadence deliver --sha <sha>`.
|
|
41
|
+
6. Hand back the line (see The line).
|
|
42
|
+
|
|
43
|
+
Never `--force`, never `CADENCE_PUSH_UNGATED`, no second attempt after a refusal (`raf done`, `pre-push` hook exit 3, `cadence deliver`): stop and hand back `Question : <étape> refusée au sha <sha> : <message de la commande>`.
|
|
44
|
+
|
|
45
|
+
## The UX verdict
|
|
46
|
+
|
|
47
|
+
For a visible lot only, after the green gate and before `raf done`. Whether the lot is visible is read in `raf show <lot>`: a visible lot prints a line `visible : oui` (followed by `screen : …` when it has one) and a line `ux : <verdict>` or `ux : absent`; a lot that prints neither line is not visible, there is no UX verdict to form or to check: go on to `raf done`.
|
|
48
|
+
|
|
49
|
+
- `lead.ux: lead`: open the captures yourself (read the images of the captures folder, one by one) and read the report of the `ux-reviewer`; do not trust the words of the report. Record `raf ux <lot> "…"` with the last line of the report brought up to date, only when the reviewer has no reservation left and the captures show what the lot promised.
|
|
50
|
+
- At the slightest doubt (a reservation you cannot settle, a capture you cannot read, a layout or wording choice that is a matter of taste, a state the captures do not show): records no `raf ux`, closes nothing, hand back `Question : doute sur le verdict UX de <lot> : <réserve> (captures : <chemin>)`.
|
|
51
|
+
- Report or captures missing from the brief, for a visible lot with `lead.ux: lead` (known once `raf show` prints `visible : oui`): the same doubt, one message only: records no `raf ux`, closes nothing, hand back `Question : doute sur le verdict UX de <lot> : rapport ou captures absents du brief (captures : <chemin reçu, ou aucun>)`. Never « brief incomplet » for them.
|
|
52
|
+
- `lead.ux: human` (or no key): never form the verdict yourself. Read the `ux` line of `raf show <lot>`: `ux : <verdict>` says the human's verdict is recorded, go on; `ux : absent` says none is: close nothing and hand back `Question : verdict UX absent pour <lot> (lead.ux: human)`.
|
|
53
|
+
|
|
54
|
+
## The line
|
|
55
|
+
|
|
56
|
+
The last thing you hand back is one line, once the delivery is green. The base URL comes from the brief; never deduce or invent it.
|
|
57
|
+
|
|
58
|
+
- Delivery that changes a page or an API: `<lot> livré <sha court>, QA à lancer <url>` (for instance `L84 livré 3d6edf4, QA à lancer https://app.example`).
|
|
59
|
+
- Delivery that changes neither a page nor an API: `<lot> livré <sha court>, pas de QA à lancer`. The decision is the one of check 4 (`git diff --name-only origin/main..<sha>`, read after `git fetch origin main` and before the push: after `git push origin <sha>:main` the interval is empty): docs, the plan or tests only over the whole interval, no QA; as soon as one file of the interval touches a page or an API: the line carries the URL.
|
|
60
|
+
|
|
61
|
+
You never launch the `qa-reviewer`: the lead launches it after your line, with the URL it carries.
|
|
62
|
+
|
|
63
|
+
## Questions
|
|
64
|
+
|
|
65
|
+
A question starts with « Question : » and fits in a few lines: the step reached, the sha, the cause and the command to rerun. It says the state of the repository left by that step (for instance « raf done fait, commit du plan non fait »): you leave it as the step reached left it, and do not clean up. A step that fails or is refused (`git fetch origin main`, `raf done`, the commit of the plan, the push, `cadence deliver`) is handed back as `Question : <étape> refusée au sha <sha> : <message de la commande>`.
|
|
66
|
+
|
|
67
|
+
## Never
|
|
68
|
+
|
|
69
|
+
Never change code, never `raf review`, never `raf start`, never `raf note`; `raf ux` only in the case of the UX verdict. Never push the plan, never `HEAD`, never `--force`, never `CADENCE_PUSH_UNGATED`. Never `git checkout` or `git switch` to another commit or branch in this repository. Launch no agent. Do not stop to ask the human anything: a question goes in your reply to the lead.
|
package/agents/qa-reviewer.md
CHANGED
|
@@ -169,6 +169,23 @@ Give screenshots and snapshots the absolute path of the capture directory your b
|
|
|
169
169
|
the caller names; remove them, or list their paths in the report — except the QA browser profile, which is kept for the next pass. The working tree is left as you
|
|
170
170
|
found it.
|
|
171
171
|
|
|
172
|
+
## Short reply
|
|
173
|
+
|
|
174
|
+
Only when the brief carries a line `Report file: <absolute path>` (the lead, which keeps its context
|
|
175
|
+
small by reading a report only when it needs to). That line is the only trigger; an orchestrated
|
|
176
|
+
review session and a review started by hand have none.
|
|
177
|
+
|
|
178
|
+
- The full report, as the Output section above describes it, is written to that file with Bash. Use the
|
|
179
|
+
path the brief gives and no other: it is outside the repository, so the working tree stays as you
|
|
180
|
+
found it. The verdict is the last line of the file. Write the file before you reply.
|
|
181
|
+
- Reply to the caller with 5 lines at most, and nothing before or after them:
|
|
182
|
+
`Verdict: <the one-line verdict>`, `Blocking: <n> — <one short phrase per blocking finding>` (`Blocking: 0` when there is none),
|
|
183
|
+
`Follow-ups: <n> — <the lines to add to the plan, by their first words>` (`Follow-ups: 0` when there is none),
|
|
184
|
+
`Report: <the path>`. Add a fifth line, `Not walked: <n> pages`, only when pages were not reached.
|
|
185
|
+
- 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
|
|
186
|
+
the brief had no report file: a verdict whose evidence nobody can read is lost.
|
|
187
|
+
- With no `Report file:` line in the brief, give the complete report in your reply.
|
|
188
|
+
|
|
172
189
|
## Do not
|
|
173
190
|
|
|
174
191
|
- Report an impression: a finding you have not measured in the browser is not a finding.
|
package/dist/cli.js
CHANGED
|
@@ -59,7 +59,7 @@ const HELP = `raf — plan « reste à faire » versionné dans le dépôt, reli
|
|
|
59
59
|
raf ux <id> "verdict" enregistre la revue d'ergonomie du lot (agent ux-reviewer)
|
|
60
60
|
raf review enable revue de code obligatoire avant « done » pour les lots qui ont des commits
|
|
61
61
|
raf review <id> "verdict" enregistre la revue de code du lot (agent code-reviewer)
|
|
62
|
-
raf show <id> [--notes] le lot : statut, dates, after, titre public, notes datées, commits comptés ; --notes : les notes seules
|
|
62
|
+
raf show <id> [--notes] le lot : statut, dates, after, titre public, visible et verdict ux (lot visible), notes datées, commits comptés ; --notes : les notes seules
|
|
63
63
|
raf commits <id> les commits du lot que compte la porte de revue de code, du plus ancien au plus récent
|
|
64
64
|
raf now ce qui est en cours, la suite, les derniers terminés
|
|
65
65
|
raf list [--status todo|doing|done|dropped]
|
|
@@ -632,8 +632,13 @@ function dispatch(argv, io) {
|
|
|
632
632
|
io.out(`after : ${lot.after.join(', ')}`);
|
|
633
633
|
if (lot.public)
|
|
634
634
|
io.out(`public : ${lot.public}`);
|
|
635
|
+
// L439 : le livreur lit ici si le lot est visible et s'il porte un verdict UX ; deux lignes stables, absentes d'un lot non visible.
|
|
636
|
+
if (lot.visible)
|
|
637
|
+
io.out('visible : oui');
|
|
635
638
|
if (lot.screen)
|
|
636
639
|
io.out(`screen : ${lot.screen}`);
|
|
640
|
+
if (lot.visible)
|
|
641
|
+
io.out(lot.ux ? `ux : ${lot.ux.verdict}` : 'ux : absent');
|
|
637
642
|
if (lot.spec)
|
|
638
643
|
io.out('spec : true');
|
|
639
644
|
if (lot.uses)
|
|
@@ -747,7 +752,7 @@ function dispatch(argv, io) {
|
|
|
747
752
|
if (rest[0] === 'pre-commit')
|
|
748
753
|
return preCommit(existsSync(planPath) ? loadPlan : null, readHookConfig(configPath).autostart, root, today, io);
|
|
749
754
|
if (rest[0] === 'pre-push')
|
|
750
|
-
return prePush(root, io, rest[1], configPath);
|
|
755
|
+
return prePush(root, io, rest[1], configPath, planFileIn(root, planPath));
|
|
751
756
|
throw new RafError('usage : raf hook install|pre-commit|post-commit|pre-push');
|
|
752
757
|
}
|
|
753
758
|
case 'session': {
|
|
@@ -1128,13 +1133,17 @@ function morningEffects(configPath, root) {
|
|
|
1128
1133
|
return [`✗ ${e.message}`];
|
|
1129
1134
|
}
|
|
1130
1135
|
}
|
|
1136
|
+
/** Le plan, relatif à la racine du dépôt git (comme les fichiers d'un commit), séparateurs `/`. */
|
|
1137
|
+
function planFileIn(root, planPath) {
|
|
1138
|
+
return relative(gitRoot(root) ?? root, planPath).split('\\').join('/');
|
|
1139
|
+
}
|
|
1131
1140
|
/**
|
|
1132
1141
|
* `raf hook pre-push` (L405), appelé par le hook que la vague pose : refuse (code 3) un push qui emporte un commit d'un lot pas prêt de la vague vivante
|
|
1133
1142
|
* du dépôt, ou dont un lot pas prêt a des commits illisibles (la garde de `deliver --sha`). Puis (L426), quand le projet déclare une porte, refuse un push dont
|
|
1134
|
-
* le sha de tête n'a pas de verdict vert de `cadence verify --gate` (`CADENCE_PUSH_UNGATED=1` passe outre, et le dit). Sans vague qui tient le dépôt, ou avec un état de vague illisible
|
|
1143
|
+
* le sha de tête n'a pas de verdict vert de `cadence verify --gate` (`CADENCE_PUSH_UNGATED=1` passe outre, et le dit) ; un commit qui ne change que le plan reprend le verdict de son parent (L432). Sans vague qui tient le dépôt, ou avec un état de vague illisible
|
|
1135
1144
|
* (avertissement), la première garde ne gêne jamais un push ; une config illisible laisse passer la seconde.
|
|
1136
1145
|
*/
|
|
1137
|
-
function prePush(root, io, remoteName, configPath) {
|
|
1146
|
+
function prePush(root, io, remoteName, configPath, planFile) {
|
|
1138
1147
|
let input = '';
|
|
1139
1148
|
try {
|
|
1140
1149
|
input = io.stdin ? io.stdin() : readFileSync(0, 'utf8');
|
|
@@ -1165,7 +1174,7 @@ function prePush(root, io, remoteName, configPath) {
|
|
|
1165
1174
|
catch {
|
|
1166
1175
|
return 0;
|
|
1167
1176
|
}
|
|
1168
|
-
const refusal = gateRefusal(root, gate, input);
|
|
1177
|
+
const refusal = gateRefusal(root, gate, input, planFile);
|
|
1169
1178
|
if (!refusal)
|
|
1170
1179
|
return 0;
|
|
1171
1180
|
if (io.env[UNGATED_ENV] === '1') {
|
|
@@ -1603,7 +1612,16 @@ async function verifyGateCommand(values, io) {
|
|
|
1603
1612
|
const sha = resolveCommit(top, values.sha);
|
|
1604
1613
|
if (!sha)
|
|
1605
1614
|
throw new RafError(`verify --gate : --sha ${values.sha} : commit introuvable dans ${top}`);
|
|
1606
|
-
|
|
1615
|
+
// Le plan du projet (L444) : les commits qui ne changent que lui au-dessus du sha reprennent la porte du sha dessous. Illisible, il n'est pas cherché : la porte se rejoue.
|
|
1616
|
+
let planFile;
|
|
1617
|
+
try {
|
|
1618
|
+
const configured = readPlanConfig(configPath, top)?.path;
|
|
1619
|
+
planFile = planFileIn(top, configured !== undefined ? resolve(top, configured) : defaultPlan(top).path);
|
|
1620
|
+
}
|
|
1621
|
+
catch {
|
|
1622
|
+
planFile = undefined;
|
|
1623
|
+
}
|
|
1624
|
+
return verifyGate({ repo: top, sha, config: readOrchestrateConfig(configPath), verify: readVerifyConfig(configPath), launchDirs: [io.cwd, top, dirname(top)], planFile, now: io.now, out: io.out });
|
|
1607
1625
|
}
|
|
1608
1626
|
/** `session start|close --all [--depth n]` : la commande jouée dans chaque projet sous le dossier courant, une section par projet. */
|
|
1609
1627
|
async function sessionAll(rest, values, io) {
|
package/dist/gate-record.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { existsSync, readdirSync, realpathSync } from 'node:fs';
|
|
2
|
+
import { changedFiles, tryGit } from './git.js';
|
|
2
3
|
import { git } from './orchestrate/worktree.js';
|
|
3
4
|
import { RunStore } from './orchestrate/state.js';
|
|
4
5
|
const realOrSelf = (p) => {
|
|
@@ -9,14 +10,36 @@ const realOrSelf = (p) => {
|
|
|
9
10
|
return p;
|
|
10
11
|
}
|
|
11
12
|
};
|
|
13
|
+
const PLAN_HOPS = 50;
|
|
14
|
+
/**
|
|
15
|
+
* Les shas dont la porte vaut pour `sha` : `sha` lui-même, puis, tant que le commit ne change que le plan (`planFile`, relatif à la racine du dépôt) et n'a qu'un parent, son parent, de proche en proche
|
|
16
|
+
* (même règle que le hook pre-push, L432 : le commit de revue et celui de `raf done` posés au-dessus d'un lot intégré ne changent pas ce que la porte a contrôlé).
|
|
17
|
+
* Une fusion, un commit racine, un commit qui touche autre chose que le plan ou qui ne touche rien est le dernier de la liste.
|
|
18
|
+
*/
|
|
19
|
+
function planChain(repo, sha, planFile) {
|
|
20
|
+
const chain = [sha];
|
|
21
|
+
if (planFile === undefined)
|
|
22
|
+
return chain;
|
|
23
|
+
for (let hop = 0; hop < PLAN_HOPS; hop++) {
|
|
24
|
+
const at = chain[chain.length - 1];
|
|
25
|
+
const parents = (tryGit(repo, ['rev-list', '--parents', '-n1', at]) ?? '').trim().split(/\s+/).slice(1);
|
|
26
|
+
const files = parents.length === 1 ? changedFiles(repo, at) : [];
|
|
27
|
+
if (files.length !== 1 || files[0] !== planFile)
|
|
28
|
+
break;
|
|
29
|
+
chain.push(parents[0]);
|
|
30
|
+
}
|
|
31
|
+
return chain;
|
|
32
|
+
}
|
|
12
33
|
/**
|
|
13
34
|
* La porte que le programme a déjà jouée pour `sha` (L397), pour que `verify --gate` la lise au lieu de la rejouer (≈ 5 min par lot prêt).
|
|
14
35
|
* Une vague range l'état de ses lots sous `<dossier de lancement>/.cadence/runs/<vague>` : `launchDirs` sont ceux à regarder (dossier courant, racine du dépôt, son parent), les vagues les plus récentes d'abord.
|
|
15
36
|
* Un enregistrement est repris quand ses contrôles ont tourné AU sha (même commit), que l'arbre de ce commit est celui enregistré, et qu'il couvre chaque étape de `steps`
|
|
16
|
-
* (même étiquette, même commande, code 0 : une étape rouge, absente ou rejouée avec une autre commande ne prouve rien). Sinon `reason` dit pourquoi la porte est rejouée.
|
|
37
|
+
* (même étiquette, même commande, code 0 : une étape rouge, absente ou rejouée avec une autre commande ne prouve rien). `planFile` : les commits qui ne changent que le plan au-dessus de `sha` sont traversés (`planBase`, L444). Sinon `reason` dit pourquoi la porte est rejouée.
|
|
17
38
|
*/
|
|
18
|
-
export function recordedGate(launchDirs, repo, sha, steps) {
|
|
19
|
-
const
|
|
39
|
+
export function recordedGate(launchDirs, repo, sha, steps, planFile) {
|
|
40
|
+
const chain = planChain(repo, sha, planFile);
|
|
41
|
+
const trees = new Map();
|
|
42
|
+
const treeOf = (c) => trees.get(c) ?? trees.set(c, git(repo, 'rev-parse', `${c}^{tree}`)).get(c);
|
|
20
43
|
const repoReal = realOrSelf(repo);
|
|
21
44
|
let reason = 'aucune porte de vague enregistrée pour ce sha';
|
|
22
45
|
const seen = new Set();
|
|
@@ -46,24 +69,27 @@ export function recordedGate(launchDirs, repo, sha, steps) {
|
|
|
46
69
|
continue; // état illisible (écriture en cours) : cette vague ne fournit rien
|
|
47
70
|
}
|
|
48
71
|
for (const l of lots) {
|
|
49
|
-
|
|
72
|
+
const checks = l.checks;
|
|
73
|
+
const checked = checks === undefined ? undefined : chain.find((c) => c === checks.head);
|
|
74
|
+
if (checks === undefined || checked === undefined || realOrSelf(l.repo) !== repoReal)
|
|
50
75
|
continue;
|
|
76
|
+
const tree = treeOf(checked);
|
|
51
77
|
const where = `vague ${store.id}, lot ${l.lot}`;
|
|
52
|
-
if (
|
|
78
|
+
if (checks.tree === undefined) {
|
|
53
79
|
reason = `arbre non enregistré (${where}, enregistrement antérieur à L397)`;
|
|
54
80
|
continue;
|
|
55
81
|
}
|
|
56
|
-
if (
|
|
82
|
+
if (checks.tree !== tree) {
|
|
57
83
|
reason = `arbre différent de celui contrôlé (${where})`;
|
|
58
84
|
continue;
|
|
59
85
|
}
|
|
60
86
|
const label = (name) => (name === 'build' ? 'build' : 'tests');
|
|
61
|
-
const missing = steps.filter((s) => !
|
|
87
|
+
const missing = steps.filter((s) => !checks.runs.some((r) => r.label === label(s.name) && r.command === s.cmd && r.code === 0));
|
|
62
88
|
if (missing.length) {
|
|
63
89
|
reason = `l'enregistrement (${where}) ne couvre pas en vert ${missing.map((s) => `${s.name} (${s.cmd})`).join(', ')}`;
|
|
64
90
|
continue;
|
|
65
91
|
}
|
|
66
|
-
return { found: { wave: store.id, lot: l.lot, tree, runs:
|
|
92
|
+
return { found: { wave: store.id, lot: l.lot, tree, checked, runs: checks.runs } };
|
|
67
93
|
}
|
|
68
94
|
}
|
|
69
95
|
}
|
package/dist/git.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { execFileSync } from 'node:child_process';
|
|
2
|
-
import { rmSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
3
3
|
import { basename, dirname, resolve } from 'node:path';
|
|
4
4
|
function git(cwd, args) {
|
|
5
5
|
// Le journal complet d'un gros dépôt dépasse le Mo par défaut : sans marge, il serait lu comme vide.
|
|
@@ -248,6 +248,36 @@ export function dirtyPaths(root, paths) {
|
|
|
248
248
|
const out = git(root, ['-c', 'core.quotepath=off', 'status', '--porcelain', '-z', '--', ...paths]);
|
|
249
249
|
return out.split('\0').filter(Boolean).map((l) => l.slice(3));
|
|
250
250
|
}
|
|
251
|
+
/**
|
|
252
|
+
* L'état des fichiers que git voit modifiés, stagés ou non suivis dans tout le dépôt : chemin (relatif à la racine) → contenu.
|
|
253
|
+
* Deux états se comparent pour savoir ce qu'une commande a écrit entre les deux. Vide hors d'un dépôt git.
|
|
254
|
+
*/
|
|
255
|
+
export function dirtyContents(root) {
|
|
256
|
+
const state = new Map();
|
|
257
|
+
let out;
|
|
258
|
+
try {
|
|
259
|
+
out = git(root, ['-c', 'core.quotepath=off', 'status', '--porcelain', '-z', '-uall']);
|
|
260
|
+
}
|
|
261
|
+
catch {
|
|
262
|
+
return state;
|
|
263
|
+
}
|
|
264
|
+
const top = tryGit(root, ['rev-parse', '--show-toplevel'])?.trim() ?? root;
|
|
265
|
+
const entries = out.split('\0').filter(Boolean);
|
|
266
|
+
for (let i = 0; i < entries.length; i++) {
|
|
267
|
+
const file = entries[i].slice(3);
|
|
268
|
+
if (entries[i][0] === 'R' || entries[i][0] === 'C')
|
|
269
|
+
i++; // un renommage porte l'ancien chemin dans l'entrée suivante
|
|
270
|
+
let content = '';
|
|
271
|
+
try {
|
|
272
|
+
content = readFileSync(resolve(top, file)).toString('base64');
|
|
273
|
+
}
|
|
274
|
+
catch {
|
|
275
|
+
content = '(absent)';
|
|
276
|
+
}
|
|
277
|
+
state.set(file, content);
|
|
278
|
+
}
|
|
279
|
+
return state;
|
|
280
|
+
}
|
|
251
281
|
/** Commit de `paths` seuls (à chemin explicite : le reste de l'index n'y entre pas). Rend le sha court. Lève l'erreur de git (hook refusé compris). */
|
|
252
282
|
export function commitPaths(root, paths, message) {
|
|
253
283
|
git(root, ['add', '--', ...paths]);
|
package/dist/lead.js
CHANGED
|
@@ -71,7 +71,7 @@ export function openLots(lots) {
|
|
|
71
71
|
return lots
|
|
72
72
|
.filter((l) => isOpen(l.status) && !isRecurring(l))
|
|
73
73
|
.flatMap((l) => [
|
|
74
|
-
{ id: l.id, title: l.title, status: l.status, after: l.after, ...(l.parent ? { parent: l.parent } : {}) },
|
|
74
|
+
{ id: l.id, title: l.title, status: l.status, after: l.after, ...(l.parent ? { parent: l.parent } : {}), ...(l.review ? { reviewed: true } : {}) },
|
|
75
75
|
...l.tasks.filter((t) => isOpen(t.status)).map((t) => ({ id: `${l.id}/${t.id}`, title: t.title, status: t.status, after: [], parent: l.id })),
|
|
76
76
|
]);
|
|
77
77
|
}
|
|
@@ -146,6 +146,9 @@ export function tourRow(dir, today, idle = TOUR_IDLE, name = basename(dir)) {
|
|
|
146
146
|
added7: counted.filter((l) => l.created !== undefined && diffDays(l.created, today) >= 0 && diffDays(l.created, today) < ADDED_DAYS).length,
|
|
147
147
|
};
|
|
148
148
|
row.lots = openLots(lots);
|
|
149
|
+
const closed = lots.filter((l) => l.status === 'done' && l.every === undefined && l.finished !== undefined).map((l) => ({ id: l.id, finished: l.finished }));
|
|
150
|
+
if (closed.length)
|
|
151
|
+
row.closed = closed;
|
|
149
152
|
const waiting = gateWaiting(lots, blocked).map((g) => g.lot.id);
|
|
150
153
|
if (waiting.length)
|
|
151
154
|
row.waiting = waiting;
|