@sylad/cadence 1.36.0 → 1.38.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 +4 -4
- package/bin/cadence.js +4 -1
- package/dist/cli.js +32 -55
- package/dist/lead.js +2 -0
- package/dist/local-task.js +143 -0
- package/dist/orchestrate/ajout.js +2 -1
- package/dist/orchestrate/briefs.js +35 -4
- package/dist/orchestrate/command.js +18 -12
- package/dist/orchestrate/cycle.js +29 -21
- package/dist/orchestrate/depot.js +2 -1
- package/dist/orchestrate/express.js +10 -0
- package/dist/orchestrate/gate-notes.js +7 -1
- package/dist/orchestrate/lock.js +7 -3
- package/dist/orchestrate/no-finding.js +19 -0
- package/dist/orchestrate/precheck.js +34 -0
- package/dist/orchestrate/probe.js +1 -1
- package/dist/orchestrate/schemas.js +2 -2
- package/dist/orchestrate/state.js +2 -0
- package/dist/orchestrate/table.js +2 -2
- package/dist/plan.js +4 -8
- package/dist/repo-busy.js +47 -0
- package/docs/corrections.md +2 -0
- package/docs/mesures/brief-implementation-enrichi-avant-apres.md +36 -0
- package/docs/orchestrate.md +19 -3
- package/docs/raf.md +13 -1
- package/docs/whats-new.md +6 -0
- package/package.json +2 -1
- package/templates/orchestrate/fix-docs.md +1 -1
- package/templates/orchestrate/fix-minors.md +1 -1
- package/templates/orchestrate/fix.md +5 -4
- package/templates/orchestrate/implement.md +6 -4
- package/templates/orchestrate/precheck.md +1 -0
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
{
|
|
9
9
|
"name": "cadence",
|
|
10
10
|
"description": "Session start and close rituals driven by a versioned plan (raf), and deliveries proven by their effect. Needs the cadence CLI (npm i -g @sylad/cadence).",
|
|
11
|
-
"version": "1.
|
|
11
|
+
"version": "1.38.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.38.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
|
@@ -47,14 +47,14 @@ Details: [docs/skills.md](docs/skills.md) and [docs/cadence-hud.md](docs/cadence
|
|
|
47
47
|
|
|
48
48
|
## Where to read more
|
|
49
49
|
|
|
50
|
-
- [docs/raf.md](docs/raf.md): the plan, its commands and
|
|
51
|
-
- [
|
|
50
|
+
- [docs/raf.md](docs/raf.md): the plan, its commands, its file, and [a lot of the lead](docs/raf.md#a-lot-of-the-lead-local-l519).
|
|
51
|
+
- [Waves](docs/orchestrate.md): [probe](docs/orchestrate.md#the-edge-case-probe-orchestrateprobe-l497), [express](docs/orchestrate.md#express-lane-l519).
|
|
52
|
+
- [Brief grid and pre-check reading](docs/mesures/brief-implementation-enrichi-avant-apres.md): grid of the code-reviewer in the `implement` brief (L523).
|
|
52
53
|
- [docs/deliver.md](docs/deliver.md): wait for the CI, deploy, verify the effect; [docs/remote-gate.md](docs/remote-gate.md): the gate on another machine.
|
|
53
54
|
- [docs/spec.md](docs/spec.md): screen and back-end sheets.
|
|
54
55
|
- [docs/ref.md](docs/ref.md): reference documents (specs, norms, mock-ups) and the coverage of their elements by the lots.
|
|
55
56
|
- [docs/lead.md](docs/lead.md): the lead workflow, and the `livreur` agent that delivers a ready lot for it.
|
|
56
|
-
- [docs/overview.md](docs/overview.md): the longer introduction and the map of every page.
|
|
57
|
-
- [docs/fonctionnement.md](docs/fonctionnement.md): how it all works, in four figures.
|
|
57
|
+
- [docs/overview.md](docs/overview.md): the longer introduction and the map of every page; [docs/fonctionnement.md](docs/fonctionnement.md): how it works.
|
|
58
58
|
- [docs/stabilite.md](docs/stabilite.md): what is guaranteed from 1.0.0; [docs/qualite.md](docs/qualite.md): the code-quality ratchet.
|
|
59
59
|
- [docs/whats-new.md](docs/whats-new.md) and [CHANGELOG.md](CHANGELOG.md): what changed.
|
|
60
60
|
|
package/bin/cadence.js
CHANGED
|
@@ -12,7 +12,7 @@ const io = {
|
|
|
12
12
|
|
|
13
13
|
if (tool === 'raf') {
|
|
14
14
|
process.exitCode = await run(args, io);
|
|
15
|
-
} else if (['news', 'session', 'deliver', 'verify', 'skills', 'orchestrate', 'lead', 'corrections', 'perf', 'spec', 'ref', 'config', 'release'].includes(tool)) {
|
|
15
|
+
} else if (['news', 'session', 'deliver', 'verify', 'skills', 'orchestrate', 'lead', 'corrections', 'perf', 'spec', 'ref', 'config', 'release', 'local'].includes(tool)) {
|
|
16
16
|
process.exitCode = await run([tool, ...args], io);
|
|
17
17
|
} else if (tool === '--version' || tool === '-v') {
|
|
18
18
|
process.exitCode = await run([tool], io);
|
|
@@ -37,6 +37,9 @@ if (tool === 'raf') {
|
|
|
37
37
|
cadence ref measure <slug> <page> <page | x,y | x,y,l,h> [--quoi couleurs|contraste|boites|texte] [--largeur 1440] [--json]
|
|
38
38
|
mesure une zone d'une page rendue : couleurs, contraste WCAG 2.2, boîtes, mots OCR et taille de police estimée,
|
|
39
39
|
ramenés à 1440 px de large, tolérance déclarée
|
|
40
|
+
cadence local start|done <lot>
|
|
41
|
+
tâche locale du lead sur un lot posé par raf add --local : start verrouille le dépôt comme une vague,
|
|
42
|
+
done joue la porte une fois et libère le dépôt si elle est verte
|
|
40
43
|
cadence release patch|minor|major [--dry-run]
|
|
41
44
|
prépare le commit de version (fichiers de version, CHANGELOG) sans rien commiter ni publier ;
|
|
42
45
|
imprime les étapes du lead dans l'ordre et vérifie docs/whats-new.md
|
package/dist/cli.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, readFileSync, realpathSync,
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, realpathSync, writeFileSync } from 'node:fs';
|
|
2
2
|
import { execFileSync } from 'node:child_process';
|
|
3
3
|
import { basename, dirname, join, posix, relative, resolve } from 'node:path';
|
|
4
4
|
import { parseArgs } from 'node:util';
|
|
@@ -24,12 +24,10 @@ import { schedule } from './schedule.js';
|
|
|
24
24
|
import { AGENTS_DIR, installAgents, installSkills, SKILLS_DIR } from './skills.js';
|
|
25
25
|
import { configSet } from './config-set.js';
|
|
26
26
|
import { movePriority } from './priority.js';
|
|
27
|
-
import { findProjects, readDepth, leadTour, TITLE_MAX, truncate, readLeadUx, tourFreeLine, readLeadContextThreshold, findLeadDir, tourLine, tourNoTestLines, tourWorktreeLines } from './lead.js';
|
|
27
|
+
import { findProjects, isProject, readDepth, leadTour, TITLE_MAX, truncate, readLeadUx, tourFreeLine, readLeadContextThreshold, findLeadDir, tourLine, tourNoTestLines, tourWorktreeLines } from './lead.js';
|
|
28
28
|
import { configuredCap, expiredHoldLines, slotsLine, orchestrate, realOrchestrateDeps } from './orchestrate/command.js';
|
|
29
29
|
import { addHold, releaseHold } from './orchestrate/registry.js';
|
|
30
|
-
import {
|
|
31
|
-
import { INTEGRATION_LOCK } from './orchestrate/integrate.js';
|
|
32
|
-
import { mainTreeWorkers } from './orchestrate/main-tree.js';
|
|
30
|
+
import { waveHolding, warnIntegrating, warnWaveWorking } from './repo-busy.js';
|
|
33
31
|
import { cadenceHome, liveWaves, openLotsIn, openWavesIn, waveLotsIn } from './orchestrate/registry.js';
|
|
34
32
|
import { blockingLots, LABEL, resumeCommand } from './orchestrate/table.js';
|
|
35
33
|
import { renderTestsSansCode, testsSansCode } from './corrections-sans-code.js';
|
|
@@ -41,19 +39,21 @@ import { coverageCommand } from './ref/coverage.js';
|
|
|
41
39
|
import { release } from './release.js';
|
|
42
40
|
import { applyMigration, describeMigration, planMigration, unhandledError } from './migrate.js';
|
|
43
41
|
import { checkFiche, deriveQa, readFiche, summary } from './spec.js';
|
|
42
|
+
import { localCommand, localLotOf, localOverdueLines } from './local-task.js';
|
|
44
43
|
import { readHudContext } from './hud-context.js';
|
|
45
44
|
import { sessionClose, sessionStart } from './session.js';
|
|
46
45
|
import { clearNext, readNext, sharedStateDir, stateDir, writeNext } from './state.js';
|
|
47
46
|
const HELP = `raf — plan « reste à faire » versionné dans le dépôt, relié aux commits
|
|
48
47
|
|
|
49
48
|
raf init [--project nom] [--prefix L] [--no-hook]
|
|
50
|
-
raf add "titre" [--estimate j] [--quickwin] [--visible [--screen page|component|journey]] [--spec] [--public "titre public"] [--every jours] [--after L2,L4] [--uses banc,passerelle] [--parent L3]
|
|
49
|
+
raf add "titre" [--estimate j] [--quickwin] [--visible [--screen page|component|journey]] [--spec] [--local] [--public "titre public"] [--every jours] [--after L2,L4] [--uses banc,passerelle] [--parent L3]
|
|
51
50
|
raf public <id> "titre public" | --clear titre du lot dans le langage du public (pages Nouveautés et Plan)
|
|
52
51
|
raf screen <id> page|component|journey|none pose, remplace ou retire screen: d'un lot visible existant (seule la valeur page adopte l'étape spec d'orchestrate)
|
|
53
52
|
raf visible <id> on|off pose ou retire visible: d'un lot existant (changement visible : entrée Nouveautés et étape UX d'orchestrate) ; off est refusé tant que le lot a un screen:
|
|
54
53
|
raf spec <id> on|off pose ou retire spec: d'un lot existant (le lot change un comportement : étape spec back d'orchestrate)
|
|
55
54
|
raf move <id> --before|--after <id> réordonne un lot (ou une sous-tâche) dans le plan, parmi ceux du même parent ; refusé pendant une vague du dépôt, sur un lot archivé et sur un plan en lecture seule
|
|
56
55
|
raf uses <lot> [<ressource>…] remplace les ressources partagées du lot (uses:, déclarées par resources: dans le cadence.yaml du dossier parent ; raf check refuse un nom non déclaré) ; sans nom, retire la clé
|
|
56
|
+
cadence local start <lot> [--ttl minutes] | local done <lot> | local abort <lot> tâche locale du lead (lot posé par raf add --local) : start verrouille le dépôt comme une vague (aucune vague n'y démarre, --add y est refusé) et passe le lot à doing, refusé si une orchestration ou un autre lot du dépôt est en cours ; le verrou est déclaré (60 min par défaut), tenu sans processus jusqu'à done ou abort, signalé en ⚠ par lead tour et orchestrate --status au-delà du délai ; done joue la porte du projet UNE fois, libère le dépôt si elle est verte (la capture, la revue du diff, cadence deliver et raf done restent au lead) et le garde verrouillé sinon ; abort libère sans porte
|
|
57
57
|
raf start <id> raf done <id> [--force] raf drop <id> [--reason texte]
|
|
58
58
|
raf todo <id> [--reason texte] remet en attente un lot démarré sans commit de travail (doing → todo, started retiré, note datée) ; refusé s'il a des commits ; rouvre un lot done ou dropped (→ todo, finished retiré, note « rouvert ») — la seule façon de rouvrir, archivé compris (il revient dans le fichier du plan)
|
|
59
59
|
raf note <id> "texte"
|
|
@@ -175,6 +175,7 @@ function dispatch(argv, io) {
|
|
|
175
175
|
visible: { type: 'boolean' },
|
|
176
176
|
screen: { type: 'string' },
|
|
177
177
|
spec: { type: 'boolean' },
|
|
178
|
+
local: { type: 'boolean' },
|
|
178
179
|
public: { type: 'string' },
|
|
179
180
|
every: { type: 'string' },
|
|
180
181
|
dir: { type: 'string' },
|
|
@@ -282,7 +283,8 @@ function dispatch(argv, io) {
|
|
|
282
283
|
if (free)
|
|
283
284
|
io.out(free);
|
|
284
285
|
// une session du lead dont le TTL est échu (L278) : rien ne l'arrête, le tour la signale (hors --json, comme les créneaux)
|
|
285
|
-
|
|
286
|
+
// … et une tâche locale (L519) au-delà de son délai : le dépôt reste verrouillé, le tour le dit au lieu de le libérer en silence
|
|
287
|
+
for (const l of [...expiredHoldLines(io.now()), ...localOverdueLines(tourRoots(parent, depth), io.now())])
|
|
286
288
|
io.out(l);
|
|
287
289
|
// le réglage lead.ux du cadence.yaml du dossier (L197) : le lead lit ici qui pose le verdict UX, au lieu d'ouvrir le fichier
|
|
288
290
|
try {
|
|
@@ -396,14 +398,10 @@ function dispatch(argv, io) {
|
|
|
396
398
|
const plan = loadPlan();
|
|
397
399
|
let id;
|
|
398
400
|
if (values.parent) {
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
if (values.public !== undefined)
|
|
404
|
-
throw new RafError('--public se pose sur un lot, pas sur une sous-tâche');
|
|
405
|
-
if (values.every !== undefined)
|
|
406
|
-
throw new RafError('--every se pose sur un lot, pas sur une sous-tâche');
|
|
401
|
+
for (const [flag, given] of [['screen', values.screen !== undefined], ['spec', values.spec], ['local', values.local], ['public', values.public !== undefined], ['every', values.every !== undefined]]) {
|
|
402
|
+
if (given)
|
|
403
|
+
throw new RafError(`--${flag} se pose sur un lot, pas sur une sous-tâche`);
|
|
404
|
+
}
|
|
407
405
|
if (values.uses !== undefined)
|
|
408
406
|
throw new RafError(`uses se pose sur un lot, pas sur une sous-tâche : ${values.parent}`);
|
|
409
407
|
id = plan.addTask(values.parent, rest.join(' '));
|
|
@@ -421,7 +419,7 @@ function dispatch(argv, io) {
|
|
|
421
419
|
const uses = values.uses?.split(',').map((n) => n.trim()).filter(Boolean);
|
|
422
420
|
if (uses?.length === 0)
|
|
423
421
|
throw new RafError(invalidResource(''));
|
|
424
|
-
id = plan.add(rest.join(' '), today, { estimate, quickwin: values.quickwin, visible: values.visible, screen: values.screen, spec: values.spec, public: values.public, every, after, uses });
|
|
422
|
+
id = plan.add(rest.join(' '), today, { estimate, quickwin: values.quickwin, visible: values.visible, screen: values.screen, spec: values.spec, local: values.local, public: values.public, every, after, uses });
|
|
425
423
|
}
|
|
426
424
|
plan.save();
|
|
427
425
|
io.out(id);
|
|
@@ -858,6 +856,8 @@ function dispatch(argv, io) {
|
|
|
858
856
|
io.out(base);
|
|
859
857
|
return 0;
|
|
860
858
|
}
|
|
859
|
+
case 'local':
|
|
860
|
+
return localCommand(rest, values.ttl, { repo: gitRoot(root) ?? root, loadPlan, today, now: io.now, gate: (sha) => verifyGateCommand({ sha }, io), out: io.out, err: io.err });
|
|
861
861
|
case 'news':
|
|
862
862
|
return news(rest, loadPlan(), newsDir, today, values, io, refuseLongTitle, () => hypothesesOf(root, configPath));
|
|
863
863
|
default:
|
|
@@ -1164,7 +1164,8 @@ function prePush(root, io, remoteName, configPath, planFile) {
|
|
|
1164
1164
|
return 0;
|
|
1165
1165
|
}
|
|
1166
1166
|
const holding = waveHolding(root);
|
|
1167
|
-
|
|
1167
|
+
// Une tâche locale du lead (L519) tient le dépôt comme une vague, mais n'a pas de lots de vague : rien à vérifier pour la première garde, et pas de faux « état illisible ».
|
|
1168
|
+
if (holding && !localLotOf(holding.wave)) {
|
|
1168
1169
|
const lots = waveLotsIn(cadenceHome(), holding.pid, root);
|
|
1169
1170
|
if (lots === null) {
|
|
1170
1171
|
io.err(`raf: vague ${holding.wave} : état illisible, push non vérifié`);
|
|
@@ -1196,44 +1197,6 @@ function prePush(root, io, remoteName, configPath, planFile) {
|
|
|
1196
1197
|
io.err(`raf: ${refusal}`);
|
|
1197
1198
|
return REFUSED;
|
|
1198
1199
|
}
|
|
1199
|
-
/**
|
|
1200
|
-
* Avertit (stderr, sans refuser) quand une intégration du dépôt est en cours (L425) : le plan écrit dans l'arbre principal est un fichier suivi modifié, et
|
|
1201
|
-
* l'intégration en cours le voit et refuse son lot (« arbre du dépôt modifié »). Seul l'arbre principal est concerné : dans le worktree d'un lot, l'écriture
|
|
1202
|
-
* ne change pas l'arbre que l'intégration contrôle.
|
|
1203
|
-
*/
|
|
1204
|
-
function warnIntegrating(root, err) {
|
|
1205
|
-
const top = gitRoot(root);
|
|
1206
|
-
if (!top || !statSync(join(top, '.git')).isDirectory())
|
|
1207
|
-
return;
|
|
1208
|
-
const holder = activeLock(join(sharedStateDir(root), INTEGRATION_LOCK));
|
|
1209
|
-
if (holder)
|
|
1210
|
-
err(`avertissement : intégration en cours (vague ${holder.wave}, pid ${holder.pid}) — ce plan est un fichier suivi modifié de l'arbre principal, l'intégration en cours peut refuser son lot (arbre du dépôt modifié) : committer le plan sans attendre`);
|
|
1211
|
-
}
|
|
1212
|
-
/**
|
|
1213
|
-
* Avertit (stderr, sans refuser) quand une session de vague écrit dans l'arbre principal du dépôt (L475) : un lot en implémentation, revue ou correction sans worktree.
|
|
1214
|
-
* Un commit de plan du lead à ce moment-là rend le lot (cas du 30-10 : cadence L458 rendu par le commit du plan). Sans effet dans le worktree d'un lot, ou depuis une session de la vague (c'est elle qui tient l'arbre).
|
|
1215
|
-
*/
|
|
1216
|
-
function warnWaveWorking(root, env, err) {
|
|
1217
|
-
if (env.CADENCE_ORCHESTRATED)
|
|
1218
|
-
return;
|
|
1219
|
-
const top = gitRoot(root);
|
|
1220
|
-
if (!top || !statSync(join(top, '.git')).isDirectory())
|
|
1221
|
-
return;
|
|
1222
|
-
const holder = activeLock(join(sharedStateDir(root), REPO_LOCK));
|
|
1223
|
-
if (!holder)
|
|
1224
|
-
return;
|
|
1225
|
-
const working = mainTreeWorkers(cadenceHome(), holder.pid, top);
|
|
1226
|
-
if (working?.length)
|
|
1227
|
-
err(`avertissement : la vague ${holder.wave} (pid ${holder.pid}) travaille dans l'arbre principal de ce dépôt (${working.join(', ')}) — un commit de plan maintenant rendrait ces lots à la revue ; attendre la fin de leur étape, ou écrire le plan sans le committer`);
|
|
1228
|
-
}
|
|
1229
|
-
/** La vague qui tient le verrou du dépôt tant qu'un de ses lots y travaille encore (état illisible : on la tient pour active) ; null si le dépôt est libre. Partagé par `deliver` et `spec reseal`. */
|
|
1230
|
-
function waveHolding(root) {
|
|
1231
|
-
const orchestrating = activeLock(join(sharedStateDir(root), REPO_LOCK));
|
|
1232
|
-
if (!orchestrating)
|
|
1233
|
-
return null;
|
|
1234
|
-
const stillOpen = openLotsIn(cadenceHome(), orchestrating.pid, root);
|
|
1235
|
-
return stillOpen === null || stillOpen.length > 0 ? orchestrating : null;
|
|
1236
|
-
}
|
|
1237
1200
|
/**
|
|
1238
1201
|
* `cadence spec reseal <lot> <test> --reason texte` (L200) : le chemin du lead quand un test d'acceptation scellé du lot EN COURS contredit la fiche.
|
|
1239
1202
|
* Le test est corrigé dans l'arbre de travail (fiche inchangée, ou amendée et commitée par des commits du lot, L217) ; la commande refait l'empreinte de ce seul test, journalise la raison dans
|
|
@@ -1469,6 +1432,20 @@ function specReseal(args, reason, porteur, ctx, io) {
|
|
|
1469
1432
|
io.out(`${lotId} : ${norm} rescellé (${r.avant.slice(0, 7)} → ${r.apres.slice(0, 7)}), commit ${sha}${r.ficheAmendee ? `, fiche amendée en ${r.ficheAmendee}` : ''} ; raison dans ${json} et en note du lot ${cite}${tracked ? '' : ' (plan à commiter)'}`);
|
|
1470
1433
|
return 0;
|
|
1471
1434
|
}
|
|
1435
|
+
/** Les racines git des projets du tour (la clé `depth:` du dossier, un seul niveau si elle est illisible : le tour a déjà sa ligne d'erreur). */
|
|
1436
|
+
function tourRoots(parent, depth) {
|
|
1437
|
+
let levels = depth;
|
|
1438
|
+
if (levels === undefined) {
|
|
1439
|
+
try {
|
|
1440
|
+
levels = readDepth(parent);
|
|
1441
|
+
}
|
|
1442
|
+
catch {
|
|
1443
|
+
levels = 1;
|
|
1444
|
+
}
|
|
1445
|
+
}
|
|
1446
|
+
const dirs = findProjects(parent, levels);
|
|
1447
|
+
return (dirs.length === 0 && isProject(parent) ? [parent] : dirs).map((d) => gitRoot(d) ?? d);
|
|
1448
|
+
}
|
|
1472
1449
|
/** Durée par défaut (minutes) d'une session du lead inscrite par `lead hold` : de quoi finir un correctif ou une revue, pas de quoi oublier la réservation toute la journée. */
|
|
1473
1450
|
const HOLD_TTL_MINUTES = 60;
|
|
1474
1451
|
/** Longueur maximale de l'étiquette encodée : un nom de fichier tient en 255 octets, `.json.tmp` en prend 9. */
|
package/dist/lead.js
CHANGED
|
@@ -3,6 +3,7 @@ import { basename, dirname, join, relative, sep } from 'node:path';
|
|
|
3
3
|
import { audit, gateWaiting, nextUp, planCommits, workByLot } from './audit.js';
|
|
4
4
|
import { PLAN_DEFAULT, PLAN_LEGACY, planLoader, readPlanConfig } from './config.js';
|
|
5
5
|
import { diffDays } from './dates.js';
|
|
6
|
+
import { localRepoNotes } from './local-task.js';
|
|
6
7
|
import { gitRoot, repoStatus } from './git.js';
|
|
7
8
|
import { linkCommits } from './link.js';
|
|
8
9
|
import { commitCount, lastActivity, lockStatus } from './session.js';
|
|
@@ -195,6 +196,7 @@ export function tourRow(dir, today, idle = TOUR_IDLE, name = basename(dir)) {
|
|
|
195
196
|
row.repo.push('non poussé');
|
|
196
197
|
if (lockStatus(sharedStateDir(root))?.live)
|
|
197
198
|
row.repo.push('livraison en cours');
|
|
199
|
+
row.repo.push(...localRepoNotes(root, new Date()));
|
|
198
200
|
const kept = keptTrees(root);
|
|
199
201
|
if (kept.length)
|
|
200
202
|
row.worktrees = kept;
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { basename, join } from 'node:path';
|
|
2
|
+
import { REPO_LOCK, activeLock, lockOverdue, releaseLock, takeLock } from './orchestrate/lock.js';
|
|
3
|
+
import { longDuration } from './orchestrate/guard.js';
|
|
4
|
+
import { RafError } from './plan.js';
|
|
5
|
+
import { tryGit } from './git.js';
|
|
6
|
+
import { sharedStateDir } from './state.js';
|
|
7
|
+
/** Nom de « vague » sous lequel la tâche locale d'un lot tient le verrou du dépôt : les refus des vagues le citent tel quel. */
|
|
8
|
+
export const localWave = (lot) => `local:${lot}`;
|
|
9
|
+
/** Le lot d'un verrou de tâche locale (`local:<lot>`), null pour le verrou d'une vague. */
|
|
10
|
+
export const localLotOf = (wave) => (wave.startsWith('local:') ? wave.slice('local:'.length) : null);
|
|
11
|
+
/** Durée annoncée (minutes) d'une tâche locale sans `--ttl` : celle de `lead hold` — de quoi finir une retouche, pas de quoi oublier le dépôt verrouillé toute la journée. */
|
|
12
|
+
export const LOCAL_TTL_MINUTES = 60;
|
|
13
|
+
const lockFile = (repo) => join(sharedStateDir(repo), REPO_LOCK);
|
|
14
|
+
/** Le refus qu'un verrou tenu oppose à une tâche locale ou à une vague : une tâche locale est nommée comme telle, avec sa sortie. */
|
|
15
|
+
export function heldRefusal(repo, held) {
|
|
16
|
+
const local = localLotOf(held.wave);
|
|
17
|
+
if (local)
|
|
18
|
+
return `${repoName(repo)} : une tâche locale du lead y est en cours (${local}, depuis ${held.started}) — la finir (cadence local done ${local}) ou l'abandonner (cadence local abort ${local})`;
|
|
19
|
+
return `${repoName(repo)} : une orchestration y est déjà en cours (${held.wave}, pid ${held.pid})`;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* `cadence local start <lot>` : le lead va modifier lui-même ce dépôt pour un lot marqué `local`. Le dépôt est verrouillé comme par une vague (aucune vague n'y démarre,
|
|
23
|
+
* `--add` y est refusé par le même contrôle de dépôt) et le lot passe à `doing`. Le verrou est DÉCLARÉ (`until`) : le shell qui lance la commande meurt à la fin de l'appel de l'outil
|
|
24
|
+
* Bash et la session du lead (Claude desktop) n'a pas de nom de processus stable — aucun processus ne le porte, il tient jusqu'à `local done` ou `local abort`, et au TTL échu il se
|
|
25
|
+
* signale (⚠) au lieu de tomber. Refus (`RafError`, rien n'est verrouillé ni écrit ; si l'écriture du plan échoue, le verrou pris est relâché) : lot inconnu, non marqué local, déjà
|
|
26
|
+
* fini, une orchestration ou une autre tâche locale qui tient le dépôt, un autre lot du dépôt en cours.
|
|
27
|
+
*/
|
|
28
|
+
export function startLocal(repo, plan, lotId, d) {
|
|
29
|
+
const lot = plan.lots().find((l) => l.id === lotId);
|
|
30
|
+
if (!lot)
|
|
31
|
+
throw new RafError(`lot inconnu : ${lotId}`);
|
|
32
|
+
if (!lot.local)
|
|
33
|
+
throw new RafError(`${lotId} n'est pas marqué local : le lead ne modifie pas lui-même le dépôt pour un lot de vague (raf add --local pour en créer un)`);
|
|
34
|
+
if (lot.status === 'done' || lot.status === 'dropped')
|
|
35
|
+
throw new RafError(`${lotId} est ${lot.status} : une tâche locale ne reprend pas un lot fini`);
|
|
36
|
+
const held = activeLock(lockFile(repo));
|
|
37
|
+
if (held)
|
|
38
|
+
throw new RafError(held.wave === localWave(lotId) ? `${lotId} : la tâche locale est déjà en cours (depuis ${held.started}) — cadence local done ${lotId} à sa fin` : `${heldRefusal(repo, held)} — attendre sa fin`);
|
|
39
|
+
const others = plan.lots().filter((l) => l.status === 'doing' && l.id !== lotId);
|
|
40
|
+
if (others.length)
|
|
41
|
+
throw new RafError(`${repoName(repo)} : un lot du dépôt est en cours (${others.map((l) => l.id).join(', ')}) — le finir, ou raf todo, avant la tâche locale`);
|
|
42
|
+
const now = d.now();
|
|
43
|
+
const until = new Date(now.getTime() + (d.ttlMinutes ?? LOCAL_TTL_MINUTES) * 60_000).toISOString();
|
|
44
|
+
const taken = takeLock(lockFile(repo), { pid: d.pid, wave: localWave(lotId), started: now.toISOString(), until });
|
|
45
|
+
if (!taken.ok)
|
|
46
|
+
throw new RafError(`${heldRefusal(repo, taken.held)} — attendre sa fin`);
|
|
47
|
+
if (lot.status === 'todo') {
|
|
48
|
+
try {
|
|
49
|
+
plan.setStatus(lotId, 'doing', d.today);
|
|
50
|
+
plan.save();
|
|
51
|
+
}
|
|
52
|
+
catch (e) {
|
|
53
|
+
releaseLock(lockFile(repo), d.pid);
|
|
54
|
+
throw e;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
/** Le verrou de la tâche locale de `lotId` s'il tient le dépôt ; `RafError` sinon (dépôt libre, vague, ou tâche d'un autre lot). */
|
|
59
|
+
function localHeld(repo, lotId) {
|
|
60
|
+
const held = activeLock(lockFile(repo));
|
|
61
|
+
if (!held || held.wave !== localWave(lotId))
|
|
62
|
+
throw new RafError(`aucune tâche locale en cours pour ${lotId} (cadence local start ${lotId})`);
|
|
63
|
+
return held;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* `cadence local done <lot>` : la tâche locale du lead est finie. La porte du projet est jouée UNE fois ; verte, le verrou tombe (la capture, la revue du diff par le lead,
|
|
67
|
+
* la livraison et `raf done` restent au lead, dans cet ordre) ; rouge ou sans verdict, le verrou reste : le lead corrige et rejoue. Refus (`RafError`) quand ce n'est pas la tâche locale de ce lot qui tient le dépôt.
|
|
68
|
+
*/
|
|
69
|
+
export async function finishLocal(repo, lotId, d) {
|
|
70
|
+
const held = localHeld(repo, lotId);
|
|
71
|
+
const code = await d.gate();
|
|
72
|
+
if (code === 1) {
|
|
73
|
+
d.err(`${lotId} : porte rouge — le dépôt reste verrouillé ; corriger, puis cadence local done ${lotId}`);
|
|
74
|
+
return 1;
|
|
75
|
+
}
|
|
76
|
+
if (code !== 0) {
|
|
77
|
+
d.err(`${lotId} : la porte n'a rien rejoué (orchestrate.build / orchestrate.test absents ?) — le dépôt reste verrouillé`);
|
|
78
|
+
return 2;
|
|
79
|
+
}
|
|
80
|
+
releaseLock(lockFile(repo), held.pid);
|
|
81
|
+
d.out(`${lotId} : porte verte, dépôt libéré. Suite : la capture d'un lot visible, relire le diff du lot et poser son verdict (raf review ${lotId} "…" — raf done le demande), cadence deliver, puis raf done ${lotId}`);
|
|
82
|
+
return 0;
|
|
83
|
+
}
|
|
84
|
+
/** `cadence local abort <lot>` : lève le verrou de la tâche locale sans jouer la porte ; le plan n'est pas touché (le lot reste `doing` : `raf todo <lot>` pour l'abandonner). */
|
|
85
|
+
export function abortLocal(repo, lotId, out = () => { }) {
|
|
86
|
+
const held = localHeld(repo, lotId);
|
|
87
|
+
releaseLock(lockFile(repo), held.pid);
|
|
88
|
+
out(`${lotId} : tâche locale abandonnée, dépôt libéré sans porte ; le lot reste doing (raf todo ${lotId} pour le remettre à todo)`);
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Une ligne ⚠ par dépôt dont une tâche locale a dépassé son TTL à `now` (lead tour, orchestrate --status) : le verrou tient toujours, le lead le finit ou l'abandonne. Vide sans tâche échue.
|
|
92
|
+
* Un dépôt illisible est sauté.
|
|
93
|
+
*/
|
|
94
|
+
export function localOverdueLines(repos, now) {
|
|
95
|
+
const lines = [];
|
|
96
|
+
for (const repo of new Set(repos)) {
|
|
97
|
+
let held;
|
|
98
|
+
try {
|
|
99
|
+
held = activeLock(lockFile(repo));
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
const lot = held && localLotOf(held.wave);
|
|
105
|
+
if (!held || !lot || !lockOverdue(held, now))
|
|
106
|
+
continue;
|
|
107
|
+
const since = Date.parse(held.started);
|
|
108
|
+
const age = Number.isFinite(since) ? `démarrée depuis ${longDuration(now.getTime() - since)}, ` : '';
|
|
109
|
+
lines.push(`⚠ tâche locale échue : ${lot} (${basename(repo)} · ${age}échue depuis ${longDuration(now.getTime() - Date.parse(held.until))}) — le dépôt reste verrouillé : la finir (cadence local done ${lot}) ou l'abandonner (cadence local abort ${lot})`);
|
|
110
|
+
}
|
|
111
|
+
return lines;
|
|
112
|
+
}
|
|
113
|
+
/** Ce que le tableau du lead dit du dépôt : « tâche locale L1 », avec « (échue) » au TTL passé ; vide sans tâche locale. */
|
|
114
|
+
export function localRepoNotes(repo, now) {
|
|
115
|
+
const held = activeLock(lockFile(repo));
|
|
116
|
+
const lot = held && localLotOf(held.wave);
|
|
117
|
+
return held && lot ? [`tâche locale ${lot}${lockOverdue(held, now) ? ' (échue)' : ''}`] : [];
|
|
118
|
+
}
|
|
119
|
+
/** Le refus qu'un lot local oppose à une vague (lancement, `--add`, `--continue`) : il est au lead ; null pour un lot de vague. */
|
|
120
|
+
export const localLotRefusal = (lot) => (lot.local ? `lot local (raf add --local) : le lead le fait lui-même (cadence local start ${lot.id}), une vague ne le joue pas` : null);
|
|
121
|
+
const repoName = (repo) => repo.split('/').filter(Boolean).pop() ?? repo;
|
|
122
|
+
/** `cadence local start|done|abort <lot> [--ttl minutes]` : l'analyse de la commande, puis `startLocal`, `finishLocal` ou `abortLocal` ; `gate(sha)` joue la porte du projet au sha. */
|
|
123
|
+
export async function localCommand(rest, ttl, c) {
|
|
124
|
+
const usage = 'usage : cadence local start <lot> [--ttl minutes] | cadence local done <lot> | cadence local abort <lot>';
|
|
125
|
+
if (rest.length !== 2 || (rest[0] !== 'start' && rest[0] !== 'done' && rest[0] !== 'abort'))
|
|
126
|
+
throw new RafError(usage);
|
|
127
|
+
const [sub, lot] = rest;
|
|
128
|
+
if (ttl !== undefined && sub !== 'start')
|
|
129
|
+
throw new RafError(`--ttl ne va qu'avec local start (${usage})`);
|
|
130
|
+
const minutes = ttl === undefined ? undefined : Number(ttl);
|
|
131
|
+
if (minutes !== undefined && !(Number.isInteger(minutes) && minutes >= 1))
|
|
132
|
+
throw new RafError(`--ttl invalide : ${ttl} (un nombre entier de minutes ≥ 1)`);
|
|
133
|
+
const deps = { ...c, pid: process.pid, ttlMinutes: minutes, gate: () => c.gate(tryGit(c.repo, ['rev-parse', 'HEAD']) ?? 'HEAD') };
|
|
134
|
+
if (sub === 'done')
|
|
135
|
+
return finishLocal(c.repo, lot, deps);
|
|
136
|
+
if (sub === 'abort') {
|
|
137
|
+
abortLocal(c.repo, lot, c.out);
|
|
138
|
+
return 0;
|
|
139
|
+
}
|
|
140
|
+
startLocal(c.repo, c.loadPlan(), lot, deps);
|
|
141
|
+
c.out(`${lot} : dépôt verrouillé (tâche locale, ${minutes ?? LOCAL_TTL_MINUTES} min annoncées) — aucune vague n'y démarre, le verrou tient jusqu'à cadence local done ${lot} (ou local abort ${lot}) ; au-delà du délai il se signale en ⚠`);
|
|
142
|
+
return 0;
|
|
143
|
+
}
|
|
@@ -2,6 +2,7 @@ import { basename, join, relative, resolve } from 'node:path';
|
|
|
2
2
|
import { lotFinished, lotKey, lotRepoPaths } from './state.js';
|
|
3
3
|
import { cadenceHome, liveWaves } from './registry.js';
|
|
4
4
|
import { REPO_LOCK, takeLock } from './lock.js';
|
|
5
|
+
import { heldRefusal } from '../local-task.js';
|
|
5
6
|
import { installPrePush } from './guard.js';
|
|
6
7
|
import { sharedStateDir } from '../state.js';
|
|
7
8
|
import { waveReadsAdds } from './snapshot.js';
|
|
@@ -116,7 +117,7 @@ export class AddFeed {
|
|
|
116
117
|
return null;
|
|
117
118
|
const l = takeLock(file, { pid: process.pid, wave: e.wave.id, started: new Date().toISOString() });
|
|
118
119
|
if (!l.ok && l.held.pid !== process.pid)
|
|
119
|
-
return
|
|
120
|
+
return heldRefusal(repo, l.held);
|
|
120
121
|
e.held.push(file);
|
|
121
122
|
fresh.push(repo);
|
|
122
123
|
if (l.ok && l.stale)
|
|
@@ -53,6 +53,15 @@ export function newsText(lot, captureDir) {
|
|
|
53
53
|
'Commit the entry with the lot (`cadence news check` must pass).',
|
|
54
54
|
].join('\n');
|
|
55
55
|
}
|
|
56
|
+
/** Consigne `{{express}}` du brief d'implémentation d'un lot express ; vide ailleurs. */
|
|
57
|
+
export const EXPRESS_BRIEF = [
|
|
58
|
+
'Express lane (touch-up of at most 0.1 day): this is the only session of the lot. There is no precheck, no edge-case probe and no separate code review: the lead reads the diff and looks at the capture. So name the file and the scope you touched in your report, keep the diff to what the goal says, and do not widen it.',
|
|
59
|
+
'No new test for a purely visual change (the existing suite runs and must stay green); a change of behaviour still gets its test first. The full suite and the build run once, at the end.',
|
|
60
|
+
].join('\n');
|
|
61
|
+
/** Consigne `{{news}}` d'un lot express `visible` : pas d'entrée Nouveautés par lot, la capture seule (le lead regroupe les entrées à la livraison). */
|
|
62
|
+
export function expressNewsText(captureDir) {
|
|
63
|
+
return `No News entry for this lot (express lane): the lead groups the entries at delivery. Do not run \`cadence news new\`. Take a capture of the result (it lands in the wave's output directory, \`${captureDir}\`): the lead looks at it instead of a separate review.`;
|
|
64
|
+
}
|
|
56
65
|
/** Consigne des briefs de correction d'un lot `visible` : où la vague range les captures Playwright (même dossier que l'étape implement). */
|
|
57
66
|
export function capturesText(captureDir) {
|
|
58
67
|
return `A Playwright capture taken with the absolute path given above lands in \`${captureDir}\` (outside the repository; this session may read it): a screenshot a finding asks for is copied from there into \`docs/nouveautes/captures/\`.`;
|
|
@@ -82,6 +91,23 @@ export function localReading(lot) {
|
|
|
82
91
|
'Keep every read small (`grep -n`, `git log --oneline -S`, a few dozen lines at a time, never a whole large file),',
|
|
83
92
|
].join('\n');
|
|
84
93
|
}
|
|
94
|
+
/**
|
|
95
|
+
* Ce que le précheck apporte au brief de l'implémentation : son constat (« partiel ») puis, séparée par une ligne vide, ce qu'il a lu
|
|
96
|
+
* (L523), pour que la session parte de là au lieu de tout relire à froid. `lu` n'est passé qu'à la première passe : la lecture date du
|
|
97
|
+
* début du lot, une reprise après interruption a des commits à relire (l'appelant ne le passe pas). Sans constat ni lecture, `commits` est rendu tel quel.
|
|
98
|
+
*/
|
|
99
|
+
export function precheckBrief(commits, finding, lu) {
|
|
100
|
+
const withFinding = finding ? [commits, finding].filter(Boolean).join('\n') : commits;
|
|
101
|
+
if (!lu)
|
|
102
|
+
return withFinding;
|
|
103
|
+
const reading = [
|
|
104
|
+
'Pre-check reading (L523): the read-only pre-check of this lot, at the start of the lot, looked in the repository and read this.',
|
|
105
|
+
'Start from these files, the neighbouring code to imitate and the existing tests it names, without searching for them again;',
|
|
106
|
+
'reopen a file only to modify it or when this reading is not enough, and re-read the lines you edit (the reading dates from the start of the lot).',
|
|
107
|
+
lu,
|
|
108
|
+
].join('\n');
|
|
109
|
+
return [withFinding, reading].filter(Boolean).join('\n\n');
|
|
110
|
+
}
|
|
85
111
|
const FILES = {
|
|
86
112
|
precheck: 'precheck.md',
|
|
87
113
|
spec: 'spec.md',
|
|
@@ -100,6 +126,8 @@ const FILES = {
|
|
|
100
126
|
'review-small': 'review-small.md',
|
|
101
127
|
'review-recheck': 'review-recheck.md',
|
|
102
128
|
};
|
|
129
|
+
/** Les variables dont l'absence se rend par un texte vide : le brief d'une étape qui n'en a pas l'usage ne les fournit pas. */
|
|
130
|
+
const BLANK_WHEN_ABSENT = ['repos', 'integration', 'docs', 'elsewhere', 'ack', 'spec', 'decisions', 'accept', 'maquette', 'tmp', 'workers', 'gate', 'shots', 'fiche', 'express'];
|
|
103
131
|
/** Lit tous les gabarits du dossier. Un gabarit absent est une erreur, avant que quoi que ce soit ne parte. */
|
|
104
132
|
export function loadTemplates(dir = TEMPLATES_DIR) {
|
|
105
133
|
const out = {};
|
|
@@ -120,7 +148,7 @@ export function loadTemplates(dir = TEMPLATES_DIR) {
|
|
|
120
148
|
export function renderBrief(kind, vars, source = TEMPLATES_DIR) {
|
|
121
149
|
const text = typeof source === 'string' ? loadTemplates(source)[kind] : source[kind];
|
|
122
150
|
const out = text.replace(/\{\{(\w+)\}\}/g, (_m, name) => {
|
|
123
|
-
const v = { ...vars,
|
|
151
|
+
const v = { ...vars, ...Object.fromEntries(BLANK_WHEN_ABSENT.map((k) => [k, vars[k] ?? ''])), lecture: vars.lecture ?? PLAN_READING }[name];
|
|
124
152
|
if (v === undefined)
|
|
125
153
|
throw new RafError(`gabarit ${FILES[kind]} : valeur manquante pour {{${name}}}`);
|
|
126
154
|
return v;
|
|
@@ -129,9 +157,9 @@ export function renderBrief(kind, vars, source = TEMPLATES_DIR) {
|
|
|
129
157
|
return `${out.replace(/[ \t]+\n/g, '\n').replace(/\n{3,}/g, '\n\n').trim()}\n`;
|
|
130
158
|
}
|
|
131
159
|
/**
|
|
132
|
-
* Consigne `{{gate}}` (L399) : le programme
|
|
160
|
+
* Consigne `{{gate}}` (L399, L518) : le programme joue une fois après l'écriture `orchestrate.test` et `orchestrate.build` ; la session ne joue que les tests ciblés du code touché (la suite entière rejouée en session coûtait 2,7 suites par lot, étude L517) et le build tel quel, pas la commande qu'elle juge équivalente.
|
|
133
161
|
* Du 22 au 28-10, des lots annoncés verts arrivaient rouges à la porte parce que la session avait joué `npm run build` (tsconfig.build.json : `src` seul)
|
|
134
|
-
* alors que la porte joue `npx tsc --noEmit` (test et scripts compris) : L330, L353. Vide sans commande déclarée.
|
|
162
|
+
* alors que la porte joue `npx tsc --noEmit` (test et scripts compris) : L330, L353. Vide sans commande déclarée ; sans `orchestrate.test`, la session joue elle-même la suite complète, une fois.
|
|
135
163
|
*/
|
|
136
164
|
export function gateText(gate) {
|
|
137
165
|
// Porte en parties (L512) : une ligne par partie, avec son dossier, jamais la liste sérialisée.
|
|
@@ -139,7 +167,10 @@ export function gateText(gate) {
|
|
|
139
167
|
const lines = [...tests, gate.build ? `- build: \`${gate.build}\`` : ''].filter(Boolean);
|
|
140
168
|
if (!lines.length)
|
|
141
169
|
return '';
|
|
142
|
-
|
|
170
|
+
// Sans `orchestrate.test`, le programme ne joue aucune suite (runChecks la saute) : la session est la seule à pouvoir la jouer.
|
|
171
|
+
if (!gate.test)
|
|
172
|
+
return `The program's gate, played once by the program after your writing session, is exactly:\n${lines.join('\n')}\nThe program plays no test suite for this project: play the full suite yourself, once, before committing, and the build command as written, not a narrower one (\`npm run build\` may cover less than the gate). A red at the gate sends the lot back to a correction pass even when your own run was green.`;
|
|
173
|
+
return `The program's gate, played once by the program after your writing session, is exactly:\n${lines.join('\n')}\nPlay only the tests of the code you touch (targeted: one file or filter, never the full suite), once, before committing: the full suite is not yours to replay (L518). Play the build command as written, not a narrower one (\`npm run build\` may cover less than the gate). A red at the gate sends the lot back to a correction pass even when your own run was green.`;
|
|
143
174
|
}
|
|
144
175
|
/**
|
|
145
176
|
* L'objectif d'un lot (L258) : celui gardé dans l'état du lot (`LotState.goal`, fixé au lancement : `--goal`, sinon la clé `goal:` du plan),
|
|
@@ -8,6 +8,7 @@ import { PLAN_DEFAULT, PLAN_LEGACY, planLoader, readOrchestrateConfig, readPlanC
|
|
|
8
8
|
import { PLAN_ACTIONS, commitMsgHook, leadAnswerMessage, commitMsgRefusal, planCommitMessage } from '../plan-commit.js';
|
|
9
9
|
import { isDay, toDay } from '../dates.js';
|
|
10
10
|
import { gitRoot } from '../git.js';
|
|
11
|
+
import { heldRefusal, localLotRefusal, localOverdueLines } from '../local-task.js';
|
|
11
12
|
import { onTermination } from '../proc.js';
|
|
12
13
|
import { stopApps } from './app.js';
|
|
13
14
|
import { RafError, isOpen } from '../plan.js';
|
|
@@ -39,6 +40,7 @@ import { baseBranchOf, keptLines, lotBranch, lotTreeDir, planFileSet, recoverTre
|
|
|
39
40
|
import { agentOf, schemaFor } from './schemas.js';
|
|
40
41
|
import { resolveLotRepos } from '../repos.js';
|
|
41
42
|
import { excludeState, lotFinished, lotKey, lotRepoPaths, lotSlug, neighbourDirs, newLot, RunStore } from './state.js';
|
|
43
|
+
import { isExpress } from './express.js';
|
|
42
44
|
import { linkNodeBin, nvmVersionsDir, resolveNode } from './node-env.js';
|
|
43
45
|
import { quotaText, renderDetail, renderTable, resumeCommand } from './table.js';
|
|
44
46
|
import { PACKAGE_ROOT, RESERVED_ENV, SnapshotRefusal, isSnapshotChild, resolveModulesDir, repairSnapshotModules, snapshotExists, waveReadsAnswerQueue, waveReadsControl, waveReadsIntegrate, waveReadsLotBudget, waveReadsReplay, withoutLaunchVars, spawnReexec, takeSnapshot, toolDirOf } from './snapshot.js';
|
|
@@ -274,8 +276,9 @@ export async function preflight(args, targets, io, deps, launch, opts = {}) {
|
|
|
274
276
|
refusals.push(`${key} : lot inconnu`);
|
|
275
277
|
continue;
|
|
276
278
|
}
|
|
277
|
-
|
|
278
|
-
|
|
279
|
+
const shut = !isOpen(lot.status) ? `le lot est ${lot.status}` : localLotRefusal(lot); // un lot local (L519) est au lead, jamais à une vague
|
|
280
|
+
if (shut) {
|
|
281
|
+
refusals.push(`${key} : ${shut}`);
|
|
279
282
|
continue;
|
|
280
283
|
}
|
|
281
284
|
if (plan.readonly && lot.status === 'todo' && !env.config.start) {
|
|
@@ -322,6 +325,8 @@ export async function preflight(args, targets, io, deps, launch, opts = {}) {
|
|
|
322
325
|
if (neighbours.repos.length)
|
|
323
326
|
state.repos = neighbours.repos;
|
|
324
327
|
state.estimate = lot.estimate;
|
|
328
|
+
if (isExpress({ estimate: lot.estimate, screen, specLot: state.specLot }))
|
|
329
|
+
state.express = true;
|
|
325
330
|
state.light = lot.estimate <= env.config.review.threshold;
|
|
326
331
|
const escalating = !!env.config.escalation;
|
|
327
332
|
state.budget = lotBudget(lot.estimate, screen ? 'screen' : lot.spec ? 'spec' : undefined, escalating);
|
|
@@ -334,9 +339,7 @@ export async function preflight(args, targets, io, deps, launch, opts = {}) {
|
|
|
334
339
|
}
|
|
335
340
|
return { refusals, lots };
|
|
336
341
|
}
|
|
337
|
-
|
|
338
|
-
return readAgents(deps.agentsDir);
|
|
339
|
-
}
|
|
342
|
+
const agentsOf = (deps) => readAgents(deps.agentsDir);
|
|
340
343
|
const WAVE_ID_RE = /^[\w.-]+$/;
|
|
341
344
|
/** Identifiant par défaut : jour et minute de lancement ; le suffixe qui départage deux vagues vient de `RunStore.reserve`. */
|
|
342
345
|
function defaultWaveBase(io) {
|
|
@@ -463,23 +466,26 @@ function dryRun(lots, io, deps, budget, id) {
|
|
|
463
466
|
steps.unshift({ kind: 'fiche', model: 'sonnet' });
|
|
464
467
|
if (specStage)
|
|
465
468
|
steps.unshift({ kind: 'spec', model: 'sonnet' });
|
|
466
|
-
if (needsPrecheck(env.config, env.loadPlan(), l.repo, l.lot, l.repos, l.goal)) {
|
|
469
|
+
if (!l.express && needsPrecheck(env.config, env.loadPlan(), l.repo, l.lot, l.repos, l.goal)) {
|
|
467
470
|
const nothingToFind = precheckNothingToFind(env.loadPlan(), l.repo, l.lot, l.repos, l.goal);
|
|
468
471
|
if (nothingToFind)
|
|
469
472
|
io.out(` contrôle préalable sauté : ${nothingToFind}`);
|
|
470
473
|
else
|
|
471
474
|
steps.unshift({ kind: 'precheck', model: env.config.precheckLocal ? 'local' : 'sonnet' });
|
|
472
475
|
}
|
|
473
|
-
steps.push(...(env.config.probe ? [{ kind: 'probe', model: 'sonnet' }] : [])); // L497 : entre l'écriture et la revue
|
|
476
|
+
steps.push(...(env.config.probe && !l.express ? [{ kind: 'probe', model: 'sonnet' }] : [])); // L497 : entre l'écriture et la revue
|
|
474
477
|
const { light, full } = env.config.review;
|
|
475
|
-
if (l.
|
|
478
|
+
if (l.express) {
|
|
479
|
+
// L519 : une seule session, le lead lit le diff
|
|
480
|
+
}
|
|
481
|
+
else if (l.small)
|
|
476
482
|
steps.push({ kind: l.visible ? 'review-small' : 'review', model: l.light ? light : full });
|
|
477
483
|
else {
|
|
478
484
|
steps.push({ kind: 'review', model: l.light ? light : full });
|
|
479
485
|
if (l.visible && env.config.ux)
|
|
480
486
|
steps.push({ kind: 'ux', model: full }); // L221 : l'UX est jouée une fois, après la revue de code conforme
|
|
481
487
|
}
|
|
482
|
-
io.out(` étapes : ${steps.map((s) => `${s.kind} (${s.model})`).join(' → ')}${l.small ? ' — petit lot' : ''}${l.light ? ` — revue légère (${light}, pas de passe des mineurs ; un bloquant ou un majeur → correction puis revue ${full})` : ''}${l.visible && !env.config.ux ? ' — UX à faire par le lead (orchestrate.ux absent)' : ''}`);
|
|
488
|
+
io.out(` étapes : ${steps.map((s) => `${s.kind} (${s.model})`).join(' → ')}${l.small ? ' — petit lot' : ''}${l.express ? ' — voie express (pas de précheck, de sonde ni de revue séparée : le lead lit le diff)' : ''}${l.light && !l.express ? ` — revue légère (${light}, pas de passe des mineurs ; un bloquant ou un majeur → correction puis revue ${full})` : ''}${l.visible && !env.config.ux ? ' — UX à faire par le lead (orchestrate.ux absent)' : ''}`);
|
|
483
489
|
for (const m of new Set(steps.map((s) => s.model))) {
|
|
484
490
|
if (m !== 'local' && !MODEL_ALIASES.includes(m))
|
|
485
491
|
io.out(` ⚠ modèle « ${m} » hors des alias ${MODEL_ALIASES.join(', ')} : passé tel quel à claude --model (un nom que le compte ne connaît pas fait échouer la session)`);
|
|
@@ -592,7 +598,7 @@ const ANSWER_POLL_MS = 2000;
|
|
|
592
598
|
function statusView(args, launch, cap, now) {
|
|
593
599
|
const id = statusWave(args);
|
|
594
600
|
const store = id !== undefined ? RunStore.find(launch, id) : RunStore.last(launch);
|
|
595
|
-
const live = [...liveLines(cap, now), ...expiredHoldLines(now)];
|
|
601
|
+
const live = [...liveLines(cap, now), ...expiredHoldLines(now), ...localOverdueLines((isLeadProject(launch) ? [launch] : findProjects(launch)).map((d) => gitRoot(d) ?? d), now)];
|
|
596
602
|
const waveLive = liveWaves(cadenceHome()).length > 0;
|
|
597
603
|
if (!store && live.length && id === undefined)
|
|
598
604
|
return { live, running: waveLive };
|
|
@@ -1082,7 +1088,7 @@ async function execute(wave, lots, store, io, deps, today, cap, all = lots) {
|
|
|
1082
1088
|
const l = takeLock(file, { pid: process.pid, wave: wave.id, started: new Date().toISOString() });
|
|
1083
1089
|
// Le verrou déjà à ce pid : pris par la reprise qui a attendu l'autre vague (L443).
|
|
1084
1090
|
if (!l.ok && l.held.pid !== process.pid) {
|
|
1085
|
-
io.err(`orchestrate : ${
|
|
1091
|
+
io.err(`orchestrate : ${heldRefusal(r, l.held)}`);
|
|
1086
1092
|
release();
|
|
1087
1093
|
return 2;
|
|
1088
1094
|
}
|
|
@@ -1653,7 +1659,7 @@ async function resume(args, argv, io, deps, launch, today) {
|
|
|
1653
1659
|
}
|
|
1654
1660
|
// Un refus déjà posé sur un dépôt précédent (même vague, attente échue) : les suivants ne font plus attendre, la reprise sort en 2.
|
|
1655
1661
|
if (!waitFor || refusals.length || Date.now() >= deadline) {
|
|
1656
|
-
refusals.push(
|
|
1662
|
+
refusals.push(heldRefusal(repo, busy));
|
|
1657
1663
|
break;
|
|
1658
1664
|
}
|
|
1659
1665
|
if (!waiting) {
|