@sylad/cadence 1.17.1 → 1.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/agents/code-reviewer.md +1 -1
- package/dist/archive.js +79 -0
- package/dist/audit.js +7 -1
- package/dist/cli.js +38 -6
- package/dist/config.js +21 -1
- package/dist/gate-record.js +71 -0
- package/dist/orchestrate/briefs.js +13 -2
- package/dist/orchestrate/command.js +17 -8
- package/dist/orchestrate/continue.js +47 -2
- package/dist/orchestrate/cycle.js +23 -7
- package/dist/orchestrate/launch.js +36 -2
- package/dist/orchestrate/registry.js +19 -8
- package/dist/orchestrate/table.js +8 -1
- package/dist/orchestrate/worktree.js +34 -19
- package/dist/plan-commit.js +2 -0
- package/dist/plan.js +196 -22
- package/dist/ref/session.js +2 -1
- package/dist/verify-gate.js +35 -12
- package/docs/cadence-hud.md +1 -1
- package/docs/deliver.md +17 -2
- package/docs/orchestrate.md +22 -6
- package/docs/raf.md +4 -2
- package/docs/whats-new.md +6 -0
- package/package.json +1 -1
- package/skills/lead/SKILL.md +1 -1
- package/templates/orchestrate/fix-docs.md +1 -0
- package/templates/orchestrate/fix-minors.md +1 -0
- package/templates/orchestrate/fix.md +1 -0
- package/templates/orchestrate/implement.md +2 -1
- package/templates/orchestrate/spec-back.md +1 -1
- package/templates/orchestrate/spec.md +1 -1
|
@@ -19,9 +19,9 @@ 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, 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, tmpText } from './briefs.js';
|
|
23
23
|
import { SlotAbandoned } from './registry.js';
|
|
24
|
-
import { journalTokens, peakContext, mcpServersFor, playwrightDir, runSession, trackGroup, writeMcpConfig } from './launch.js';
|
|
24
|
+
import { journalTokens, peakContext, mcpServersFor, playwrightDir, runSession, trackGroup, writeMcpConfig, enabledPlugins, settingsFiles } from './launch.js';
|
|
25
25
|
import { cleanPlaywrightOutput, pushed, remoteAdvances, snapshot } from './guard.js';
|
|
26
26
|
import { sumTokens } from './result.js';
|
|
27
27
|
import { cutReport, EXTENSION_MS, MAX_EXTENSIONS, workTimeoutMs } from './timeouts.js';
|
|
@@ -147,8 +147,11 @@ async function commitPlan(c, message, root = c.lot.repo, before = new Map()) {
|
|
|
147
147
|
const repo = root;
|
|
148
148
|
const ownFile = planOwnFile(c, repo);
|
|
149
149
|
// Les fichiers de plan.onWrite (L346) entrent même non suivis : le premier plan publié n'est pas encore au dépôt.
|
|
150
|
-
|
|
151
|
-
|
|
150
|
+
// Les fichiers d'année de l'archive (L396) aussi : un `raf archive` non commité pendant la vague les crée ou les réécrit, et le plan seul commité laisserait HEAD sans les lots archivés.
|
|
151
|
+
// Chemins relatifs au dépôt du projet (comme `plan.maintained`), même quand `root` est un worktree de lot ; vide pour un plan en lecture seule.
|
|
152
|
+
const plan = c.loadPlan();
|
|
153
|
+
const archives = plan.archiveFiles().map((f) => relative(c.lot.repo, f));
|
|
154
|
+
const dirty = [...new Set([...trackedPaths(await snapshot(repo, { remote: false })), ...dirtyPaths(repo, [...plan.produced, ...archives])])];
|
|
152
155
|
if (dirty.length === 0)
|
|
153
156
|
return null;
|
|
154
157
|
const foreign = dirty.filter((f) => !ownFile(f));
|
|
@@ -488,7 +491,7 @@ function briefFor(c, kind, local = false) {
|
|
|
488
491
|
const l = c.lot;
|
|
489
492
|
const lot = plan.lot(l.lot);
|
|
490
493
|
const tmpDir = c.wave.store.tmpDir(l.project, l.lot);
|
|
491
|
-
const vars = { chemin: wd(c), lot: l.lot, titre: headline(lot, l.goal), objectif: objective(lot, l.goal), commits: '', reponse: '', constats: '', spec: '', ux: uxText(c), choix: choixText(c.lot.choix ?? []), checks: kind === 'review' || kind === 'review-small' ? checksText(c) : '', docs: kind === 'review' || kind === 'review-small' ? docsText(c) : '', news: '', captures: '', tmp: tmpText(tmpDir), workers: kind === 'implement' || kind === 'fix' ? workersText(c) : '', repos: reposText(kind === 'implement' || kind === 'fix' ? 'write' : 'read', l.lot, l.repos ?? []), integration: c.integration ? integrationText(c.integration.base) : '' };
|
|
494
|
+
const vars = { chemin: wd(c), lot: l.lot, titre: headline(lot, l.goal), objectif: objective(lot, l.goal), commits: '', reponse: '', constats: '', spec: '', ux: uxText(c), choix: choixText(c.lot.choix ?? []), checks: kind === 'review' || kind === 'review-small' ? checksText(c) : '', docs: kind === 'review' || kind === 'review-small' ? docsText(c) : '', news: '', captures: '', tmp: tmpText(tmpDir), workers: kind === 'implement' || kind === 'fix' ? workersText(c) : '', gate: kind === 'implement' || kind === 'fix' ? gateText(c.config) : '', repos: reposText(kind === 'implement' || kind === 'fix' ? 'write' : 'read', l.lot, l.repos ?? []), integration: c.integration ? integrationText(c.integration.base) : '' };
|
|
492
495
|
if (l.visible) {
|
|
493
496
|
const pwDir = playwrightDir(c.wave.store.lotDir(l.project, l.lot));
|
|
494
497
|
vars.news = newsText(l.lot, pwDir);
|
|
@@ -746,6 +749,7 @@ async function session(c, kind, local = false) {
|
|
|
746
749
|
tmpDir,
|
|
747
750
|
mcpConfig,
|
|
748
751
|
playwright,
|
|
752
|
+
disablePlugins: enabledPlugins(settingsFiles(w.claudeHome, wd(c))),
|
|
749
753
|
timeoutMs: delay,
|
|
750
754
|
extend,
|
|
751
755
|
}, {
|
|
@@ -1970,10 +1974,22 @@ async function acceptGreen(c) {
|
|
|
1970
1974
|
* (un test déjà rouge au sha de départ n'en est pas un, L213). S'arrête au premier rouge : un build rouge rend le lot sans lancer les tests. Après une écriture (work) et, au sommet rebasé, avant l'avance de la branche du dépôt (R7).
|
|
1971
1975
|
*/
|
|
1972
1976
|
async function programChecks(c, red, runs) {
|
|
1977
|
+
try {
|
|
1978
|
+
await runChecks(c, red, runs);
|
|
1979
|
+
}
|
|
1980
|
+
finally {
|
|
1981
|
+
// L395 : la porte se lit dans l'état du lot tant qu'une commande (ou le rejeu qui la suit) tourne, jamais après.
|
|
1982
|
+
c.lot.gate = undefined;
|
|
1983
|
+
save(c);
|
|
1984
|
+
}
|
|
1985
|
+
}
|
|
1986
|
+
async function runChecks(c, red, runs) {
|
|
1973
1987
|
const l = c.lot;
|
|
1974
1988
|
for (const [label, command] of [['build', c.config.build], ['tests', c.config.test]]) {
|
|
1975
1989
|
if (!command)
|
|
1976
1990
|
continue;
|
|
1991
|
+
l.gate = { label, started: new Date().toISOString() };
|
|
1992
|
+
save(c);
|
|
1977
1993
|
const r = await sh(c, command);
|
|
1978
1994
|
const run = { label, command, code: r.code };
|
|
1979
1995
|
runs.push(run);
|
|
@@ -2285,7 +2301,7 @@ async function work(c, kind) {
|
|
|
2285
2301
|
}
|
|
2286
2302
|
}
|
|
2287
2303
|
l.constats = [];
|
|
2288
|
-
l.checks = runs.length ? { head: git(wd(c), 'rev-parse', 'HEAD'), runs } : undefined;
|
|
2304
|
+
l.checks = runs.length ? { head: git(wd(c), 'rev-parse', 'HEAD'), tree: git(wd(c), 'rev-parse', 'HEAD^{tree}'), runs } : undefined;
|
|
2289
2305
|
l.next = firstReview(c);
|
|
2290
2306
|
save(c);
|
|
2291
2307
|
}
|
|
@@ -2749,7 +2765,7 @@ async function integrateLot(c) {
|
|
|
2749
2765
|
if (tip !== reviewed) {
|
|
2750
2766
|
// Le rebase a changé les commits : tests et build rejoués au sommet rebasé, avant que la branche du dépôt ne bouge (R7).
|
|
2751
2767
|
const { red, runs } = await integratedChecks(c, key);
|
|
2752
|
-
l.checks = runs.length ? { head: tip, runs } : undefined;
|
|
2768
|
+
l.checks = runs.length ? { head: tip, tree: git(t.dir, 'rev-parse', 'HEAD^{tree}'), runs } : undefined;
|
|
2753
2769
|
save(c);
|
|
2754
2770
|
if (red.length)
|
|
2755
2771
|
return refuse(`intégration refusée : tests rouges au sommet intégré (${red.map(redExcerpt).join(' ; ')}) ; branche du dépôt inchangée, ${kept}`);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
2
|
import { randomUUID } from 'node:crypto';
|
|
3
3
|
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
4
|
+
import { homedir } from 'node:os';
|
|
4
5
|
import { delimiter, join, resolve } from 'node:path';
|
|
5
6
|
import { parse } from 'yaml';
|
|
6
7
|
import { RafError } from '../plan.js';
|
|
@@ -54,6 +55,39 @@ function effortArgs(spec) {
|
|
|
54
55
|
}
|
|
55
56
|
/** Retire `run_in_background` de Bash et des sous-agents : `claude -p` attend toute tâche de fond avant de sortir (jusqu'à 10 min d'inactivité), la session finie ne rendait pas la main (L285). */
|
|
56
57
|
export const NO_BACKGROUND_ENV = { CLAUDE_CODE_DISABLE_BACKGROUND_TASKS: '1' };
|
|
58
|
+
/** Dossier de Claude Code de l'utilisateur : `CLAUDE_CONFIG_DIR`, sinon `~/.claude` (le `HOME` de l'environnement donné : les tests en simulent un). */
|
|
59
|
+
export function claudeConfigDir(env = process.env) {
|
|
60
|
+
return env.CLAUDE_CONFIG_DIR || join(env.HOME || homedir(), '.claude');
|
|
61
|
+
}
|
|
62
|
+
/** Les fichiers de réglages dont les plugins activés comptent pour une session lancée dans `cwd` : celui de l'utilisateur (`claudeHome`, s'il est connu), ceux du projet et du dossier. */
|
|
63
|
+
export function settingsFiles(claudeHome, cwd) {
|
|
64
|
+
return [...(claudeHome ? [join(claudeHome, 'settings.json')] : []), join(cwd, '.claude', 'settings.json'), join(cwd, '.claude', 'settings.local.json')];
|
|
65
|
+
}
|
|
66
|
+
/** Identifiants des plugins activés (`enabledPlugins: { "<id>": true }`) dans l'un des fichiers de réglages donnés ; un fichier absent ou illisible ne compte pas. */
|
|
67
|
+
export function enabledPlugins(files) {
|
|
68
|
+
const ids = new Set();
|
|
69
|
+
for (const file of files) {
|
|
70
|
+
let settings;
|
|
71
|
+
try {
|
|
72
|
+
settings = JSON.parse(readFileSync(file, 'utf8'));
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
for (const [id, on] of Object.entries(settings?.enabledPlugins ?? {}))
|
|
78
|
+
if (on === true)
|
|
79
|
+
ids.add(id);
|
|
80
|
+
}
|
|
81
|
+
return [...ids];
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* `--settings` d'une session (L398) : un JSON qui ne porte que les plugins de l'utilisateur et du projet à `false`. Un plugin désactivé n'enregistre
|
|
85
|
+
* ni ses hooks (celui de démarrage de superpowers injectait 3,4 ko) ni ses skills : le contexte qu'une session, qui n'en appelle aucun, ne paie plus
|
|
86
|
+
* (mesure : `docs/mesures/contexte-session-2026-10-28.md`). Rien d'autre n'est touché ni relayé : les réglages de l'utilisateur et du projet
|
|
87
|
+
* s'appliquent avec leur précédence normale (`--settings` l'emporte sur un projet qui active le plugin, vérifié le 2026-10-28), ses hooks compris.
|
|
88
|
+
* En argument, pas dans un fichier : des identifiants seulement (`nom@marketplace`, aucun secret), 1 000 plugins pèsent 50 ko sous la limite d'un argument Linux (128 ko).
|
|
89
|
+
*/
|
|
90
|
+
const settingsArgs = (spec) => (spec.disablePlugins?.length ? ['--settings', JSON.stringify({ enabledPlugins: Object.fromEntries(spec.disablePlugins.map((id) => [id, false])) })] : []);
|
|
57
91
|
export function buildArgs(spec, agents) {
|
|
58
92
|
const args = ['-p', spec.brief, '--output-format', 'json', '--json-schema', JSON.stringify(spec.schema), '--model', spec.model, ...effortArgs(spec)];
|
|
59
93
|
if (spec.agent) {
|
|
@@ -64,7 +98,7 @@ export function buildArgs(spec, agents) {
|
|
|
64
98
|
args.push('--session-id', spec.sessionId, '--permission-mode', spec.permissionMode);
|
|
65
99
|
for (const d of spec.addDirs)
|
|
66
100
|
args.push('--add-dir', d);
|
|
67
|
-
args.push('--disallowedTools', ...DISALLOWED);
|
|
101
|
+
args.push(...settingsArgs(spec), '--disallowedTools', ...DISALLOWED);
|
|
68
102
|
if (spec.mcpConfig)
|
|
69
103
|
args.push('--strict-mcp-config', '--mcp-config', spec.mcpConfig);
|
|
70
104
|
return args;
|
|
@@ -126,7 +160,7 @@ export function buildRetryArgs(spec, sessionId, agents) {
|
|
|
126
160
|
args.push('--resume', sessionId, '--permission-mode', spec.permissionMode);
|
|
127
161
|
for (const d of spec.addDirs)
|
|
128
162
|
args.push('--add-dir', d);
|
|
129
|
-
args.push('--disallowedTools', ...DISALLOWED);
|
|
163
|
+
args.push(...settingsArgs(spec), '--disallowedTools', ...DISALLOWED);
|
|
130
164
|
if (spec.mcpConfig)
|
|
131
165
|
args.push('--strict-mcp-config', '--mcp-config', spec.mcpConfig);
|
|
132
166
|
return args;
|
|
@@ -49,14 +49,24 @@ export function liveWaves(home) {
|
|
|
49
49
|
}
|
|
50
50
|
return out.sort((a, b) => a.started.localeCompare(b.started));
|
|
51
51
|
}
|
|
52
|
+
/**
|
|
53
|
+
* Le plafond que TOUTES les vagues vivantes appliquent (L395) : le plus bas des plafonds connus, `fallback` si aucune n'en déclare.
|
|
54
|
+
* Les créneaux sont communs : deux plafonds différents ne peuvent pas être respectés tous les deux (une vague à 4 à côté d'une
|
|
55
|
+
* vague à 3 portait le total à « 4/3 » et la vague à 3 attendait), le plus bas fait loi pour toutes.
|
|
56
|
+
*/
|
|
57
|
+
export function commonCap(waves, fallback) {
|
|
58
|
+
const caps = waves.map((w) => w.cap).filter((c) => c !== undefined && Number.isFinite(c) && c >= 1);
|
|
59
|
+
return caps.length ? Math.min(fallback, ...caps) : fallback;
|
|
60
|
+
}
|
|
52
61
|
/**
|
|
53
62
|
* Les créneaux que le lead peut encore donner à des sous-agents : `total` moins, pour chaque vague vivante, le
|
|
54
|
-
* plus petit
|
|
63
|
+
* plus petit du plafond commun (`commonCap`) et du nombre de lots qu'elle joue à la fois — un par dépôt qu'elle tient encore, un par lot pour des lots en worktrees (ses pas prennent un créneau et le
|
|
55
64
|
* rendent entre deux étapes : les sessions vivantes à l'instant ne disent pas ce que la vague va reprendre).
|
|
56
65
|
* Une vague sans plafond connu compte pour `total`.
|
|
57
66
|
*/
|
|
58
67
|
export function freeSlots(waves, total, leadSessions = 0) {
|
|
59
|
-
const
|
|
68
|
+
const common = commonCap(waves, total);
|
|
69
|
+
const held = waves.reduce((n, w) => n + Math.min(w.cap !== undefined ? common : total, w.lanes ?? w.repos.length), 0);
|
|
60
70
|
return Math.max(0, total - held - leadSessions);
|
|
61
71
|
}
|
|
62
72
|
const holdsDir = (home) => join(home, 'holds');
|
|
@@ -182,9 +192,9 @@ export class SlotAbandoned extends Error {
|
|
|
182
192
|
}
|
|
183
193
|
/**
|
|
184
194
|
* Attend que le nombre de sessions vivantes, TOUTES vagues confondues et quel que soit l'index de leur créneau,
|
|
185
|
-
* soit sous `cap` (le plafond de cette vague),
|
|
186
|
-
*
|
|
187
|
-
*
|
|
195
|
+
* soit sous le plafond commun — `cap` (le plafond de cette vague) ou, s'il est plus bas, celui d'une autre vague vivante
|
|
196
|
+
* (`commonCap`, relu à chaque tour, L395) —, puis prend le premier créneau libre (un pid mort est repris) et
|
|
197
|
+
* rend sa libération. Toutes les vagues appliquent le même plafond : jamais « 4/3 en cours ».
|
|
188
198
|
* Deux vagues qui prennent en même temps se départagent au recomptage : si le total dépasse `cap` une fois
|
|
189
199
|
* le créneau pris, il est rendu et l'attente reprend.
|
|
190
200
|
*
|
|
@@ -208,15 +218,16 @@ export async function acquireSlot(home, cap, opts) {
|
|
|
208
218
|
throw new SlotAbandoned(why);
|
|
209
219
|
};
|
|
210
220
|
for (;;) {
|
|
221
|
+
const limit = commonCap(liveWaves(home), cap); // relu à chaque tour : une vague au plafond bas qui s'arrête le relève
|
|
211
222
|
if (reportedAt !== null)
|
|
212
223
|
checkAbandon(); // pas à la première tentative : un créneau libre est pris même si une demande est posée
|
|
213
|
-
if (live(home).length <
|
|
224
|
+
if (live(home).length < limit) {
|
|
214
225
|
for (let k = 0; k < MAX_SLOT_INDEX; k++) {
|
|
215
226
|
const file = join(slotsDir(home), `slot-${k}.lock`);
|
|
216
227
|
const got = takeLock(file, { pid: process.pid, wave: opts.wave, started: new Date().toISOString() });
|
|
217
228
|
if (!got.ok)
|
|
218
229
|
continue;
|
|
219
|
-
if (live(home).length <=
|
|
230
|
+
if (live(home).length <= limit) {
|
|
220
231
|
if (reportedAt !== null)
|
|
221
232
|
opts.onGot?.(now() - t0);
|
|
222
233
|
return () => releaseLock(file, process.pid);
|
|
@@ -229,7 +240,7 @@ export async function acquireSlot(home, cap, opts) {
|
|
|
229
240
|
checkAbandon(); // la première tentative a échoué : l'attente commence, la demande déjà posée l'interrompt sans attendre un tour
|
|
230
241
|
if (reportedAt === null || now() - reportedAt >= (opts.reportMs ?? WAIT_REPORT_MS)) {
|
|
231
242
|
reportedAt = now();
|
|
232
|
-
opts.onWait?.(live(home), reportedAt - t0);
|
|
243
|
+
opts.onWait?.(live(home), reportedAt - t0, limit);
|
|
233
244
|
}
|
|
234
245
|
await new Promise((r) => setTimeout(r, pollMs + Math.random() * pollMs * 0.25));
|
|
235
246
|
}
|
|
@@ -64,6 +64,13 @@ function ux(l) {
|
|
|
64
64
|
}
|
|
65
65
|
const commits = (l) => new Set(l.steps.flatMap((s) => s.commits ?? [])).size;
|
|
66
66
|
const counted = (l) => l.steps.reduce((n, s) => n + (s.tokens?.counted ?? 0), 0);
|
|
67
|
+
/** L'état d'un lot (L395) : « porte tests (3 min) » tant que la commande de la porte du programme tourne dans une vague vivante, sinon l'étiquette de son statut. Une porte restée dans l'état d'une vague arrêtée (processus tué) ne tourne plus. */
|
|
68
|
+
const stateText = (l, wave, now) => {
|
|
69
|
+
if (!l.gate || wave.status !== 'running')
|
|
70
|
+
return LABEL[l.status];
|
|
71
|
+
const since = Date.parse(l.gate.started);
|
|
72
|
+
return `porte ${l.gate.label}${Number.isFinite(since) ? ` (${Math.max(0, Math.floor((now - since) / 60_000))} min)` : ''}`;
|
|
73
|
+
};
|
|
67
74
|
const minutes = (l) => Math.round(l.steps.reduce((n, s) => n + (s.ended ? Date.parse(s.ended) - Date.parse(s.started) : 0), 0) / 60_000);
|
|
68
75
|
/** Sha abrégés (7, comme `short` du cycle) des commits que git attribue au lot dans son dépôt, du plus ancien au plus récent (illisible ou sans plan : aucun). */
|
|
69
76
|
function gitCommitsOf(l) {
|
|
@@ -189,7 +196,7 @@ export function resumeAnswerHint(waveId, l) {
|
|
|
189
196
|
}
|
|
190
197
|
/** Le tableau de fin de vague (ou de `--status`), une ligne par lot puis le total et les questions. */
|
|
191
198
|
export function renderTable(wave, lots, deps = {}) {
|
|
192
|
-
const rows = lots.map((l) => [`${l.project}:${l.lot}`,
|
|
199
|
+
const rows = lots.map((l) => [`${l.project}:${l.lot}`, stateText(l, wave, deps.now ?? Date.now()), String(l.pass), review(l), ux(l), String(commits(l)), k(counted(l)), `${minutes(l)} min`]);
|
|
193
200
|
const head = ['lot', 'état', 'passes', 'revue', 'UX', 'commits', 'tokens', 'durée'];
|
|
194
201
|
const widths = head.map((h, i) => Math.max(h.length, ...rows.map((r) => r[i].length)));
|
|
195
202
|
const line = (r) => r.map((c, i) => c.padEnd(widths[i])).join(' ').trimEnd();
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { execFileSync } from 'node:child_process';
|
|
2
2
|
import { createHash } from 'node:crypto';
|
|
3
|
-
import {
|
|
3
|
+
import { existsSync, lstatSync, mkdirSync, readdirSync, rmSync, symlinkSync, realpathSync } from 'node:fs';
|
|
4
|
+
import { cp, readdir, realpath, rm, symlink } from 'node:fs/promises';
|
|
4
5
|
import { basename, dirname, isAbsolute, join, relative, sep } from 'node:path';
|
|
5
6
|
import { lotSlug } from './state.js';
|
|
6
7
|
import { cadenceHome, liveWaves } from './registry.js';
|
|
@@ -129,44 +130,58 @@ export function linkIgnored(repo, dir) {
|
|
|
129
130
|
export function ignoredDeps(repo) {
|
|
130
131
|
return git(repo, 'ls-files', '--others', '--ignored', '--exclude-standard', '--directory').split('\n').map((f) => f.replace(/\/$/, '')).filter((f) => f && !f.startsWith('.cadence') && !f.split('/').some((p) => OUTPUT_DIRS.has(p)));
|
|
131
132
|
}
|
|
133
|
+
/** Les dépendances que `verify --gate` copie quand `orchestrate.worktreeDeps` est absente (L392) : les `node_modules` de tout niveau et le `.venv`. Rien d'autre (données, `.env`) : trop gros, ou secret. */
|
|
134
|
+
export const isCopiedDep = (name) => ['node_modules', '.venv'].includes(basename(name));
|
|
132
135
|
/**
|
|
133
136
|
* Copie dans le worktree `dir` ces dépendances du dépôt vivant (L389), au lieu de les y relier : un outil qui refuse de charger un `node_modules` lié depuis
|
|
134
|
-
* un autre arbre (Astro : « Could not load … ») les trouve chez lui.
|
|
135
|
-
*
|
|
136
|
-
*
|
|
137
|
+
* un autre arbre (Astro : « Could not load … ») les trouve chez lui. Asynchrone (L392) : une copie synchrone de plusieurs centaines de Mo figerait la boucle
|
|
138
|
+
* d'événements, et le Ctrl-C reçu pendant ce temps n'aurait aucun effet. Un dossier qui est lui-même un lien est copié par son contenu (copier le lien ne
|
|
139
|
+
* copierait rien). Les liens relatifs du dossier qui restent dans le dépôt (espaces de travail) sont copiés tels quels : ils pointent dans la copie. Ceux qui en
|
|
140
|
+
* sortent (`file:../voisin`, `link:`) pointeraient dans le vide depuis `dir` : ils sont refaits vers leur cible vivante. Rend les chemins copiés ;
|
|
141
|
+
* un chemin dont le parent n'existe pas dans le worktree est passé.
|
|
137
142
|
*/
|
|
138
|
-
export function copyDeps(repo, dir, names) {
|
|
143
|
+
export async function copyDeps(repo, dir, names) {
|
|
139
144
|
const copied = [];
|
|
145
|
+
const repoReal = realpathSync(repo);
|
|
140
146
|
for (const name of names) {
|
|
141
147
|
const to = join(dir, name);
|
|
142
148
|
if (existsSync(to) || !existsSync(dirname(to)))
|
|
143
149
|
continue;
|
|
144
|
-
|
|
145
|
-
|
|
150
|
+
let from = join(repo, name);
|
|
151
|
+
try {
|
|
152
|
+
from = realpathSync(from);
|
|
153
|
+
}
|
|
154
|
+
catch {
|
|
155
|
+
continue; // lien mort : rien à copier
|
|
156
|
+
}
|
|
157
|
+
await cp(from, to, { recursive: true, verbatimSymlinks: true });
|
|
158
|
+
// Un dossier venu d'hors du dépôt (lien) garde ses liens internes relatifs ; ses liens vers le reste du monde sont refaits vers leur cible vivante.
|
|
159
|
+
const inside = !relative(repoReal, from).startsWith('..');
|
|
160
|
+
await relinkOutside(inside ? repoReal : from, from, to);
|
|
146
161
|
copied.push(name);
|
|
147
162
|
}
|
|
148
163
|
return copied;
|
|
149
164
|
}
|
|
150
|
-
/** Dans la copie `to` de `
|
|
151
|
-
function relinkOutside(
|
|
152
|
-
for (const e of
|
|
153
|
-
const
|
|
165
|
+
/** Dans la copie `to` du dossier de vrai chemin `real`, refait vers leur cible vivante les liens dont la cible sort de `boundary`. */
|
|
166
|
+
async function relinkOutside(boundary, real, to) {
|
|
167
|
+
for (const e of await readdir(real, { withFileTypes: true })) {
|
|
168
|
+
const realEntry = join(real, e.name);
|
|
154
169
|
const copy = join(to, e.name);
|
|
155
170
|
if (e.isSymbolicLink()) {
|
|
156
|
-
let
|
|
171
|
+
let target;
|
|
157
172
|
try {
|
|
158
|
-
|
|
173
|
+
target = await realpath(realEntry);
|
|
159
174
|
}
|
|
160
175
|
catch {
|
|
161
176
|
continue; // lien mort : recopié tel quel
|
|
162
177
|
}
|
|
163
|
-
if (relative(
|
|
164
|
-
|
|
165
|
-
|
|
178
|
+
if (relative(boundary, target).startsWith('..')) {
|
|
179
|
+
await rm(copy);
|
|
180
|
+
await symlink(target, copy);
|
|
166
181
|
}
|
|
167
182
|
}
|
|
168
183
|
else if (e.isDirectory()) {
|
|
169
|
-
relinkOutside(
|
|
184
|
+
await relinkOutside(boundary, realEntry, copy);
|
|
170
185
|
}
|
|
171
186
|
}
|
|
172
187
|
}
|
|
@@ -271,10 +286,10 @@ export function recoverTree(repo, branch, dir, base, planFiles = new Set()) {
|
|
|
271
286
|
}
|
|
272
287
|
return { from: found.branch, unintegrated: found.unintegrated };
|
|
273
288
|
}
|
|
274
|
-
/** Les fichiers du plan qu'une session ne doit pas toucher, relatifs au dépôt : le fichier de plan, la page Gantt
|
|
289
|
+
/** Les fichiers du plan qu'une session ne doit pas toucher, relatifs au dépôt : le fichier de plan, la page Gantt, ce qui est généré avec (`plan.files`, les fichiers de `plan.onWrite`) et les fichiers d'année de l'archive (L396). */
|
|
275
290
|
export function planFileSet(plan, repo) {
|
|
276
291
|
const rel = relative(repo, plan.path);
|
|
277
|
-
return new Set([rel, join(dirname(rel), 'gantt.html'), ...plan.files, ...plan.produced]);
|
|
292
|
+
return new Set([rel, join(dirname(rel), 'gantt.html'), ...plan.files, ...plan.produced, ...plan.archiveFiles().map((f) => relative(repo, f))]);
|
|
278
293
|
}
|
|
279
294
|
/** Les commits de la branche du lot déjà dans la branche du dépôt ? */
|
|
280
295
|
export function integrated(repo, branch, base) {
|
package/dist/plan-commit.js
CHANGED
|
@@ -18,6 +18,8 @@ export const PLAN_ACTIONS = {
|
|
|
18
18
|
minors: (label) => `mineurs de la revue de ${label} notés`,
|
|
19
19
|
/** `raf done` ne commite pas : l'action de la commande de commit qu'il propose (L371). */
|
|
20
20
|
done: 'terminé',
|
|
21
|
+
/** `raf archive` (L396) : le nombre de lots déplacés dans l'archive. */
|
|
22
|
+
archived: (n) => `${n} lot(s) archivé(s)`,
|
|
21
23
|
reviewRecorded: 'revue de code enregistrée',
|
|
22
24
|
reviewNoted: 'revue de code notée',
|
|
23
25
|
/** `cadence deliver` pendant une vague (L388) : le lot de l'article ouvert, ou ses titres ajoutés en note. */
|