@sylad/cadence 1.41.0 → 1.42.1
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/dist/archive.js +103 -6
- package/dist/audit.js +2 -2
- package/dist/clean.js +0 -4
- package/dist/cli/context.js +129 -0
- package/dist/cli/deliver-commands.js +114 -0
- package/dist/cli/gates.js +234 -0
- package/dist/cli/lead-commands.js +161 -0
- package/dist/cli/lot-commands.js +257 -0
- package/dist/cli/read-commands.js +227 -0
- package/dist/cli/session-commands.js +152 -0
- package/dist/cli/setup-commands.js +72 -0
- package/dist/cli/spec-commands.js +298 -0
- package/dist/cli/stats-commands.js +138 -0
- package/dist/cli-add.js +37 -0
- package/dist/cli.js +82 -1651
- package/dist/config.js +16 -103
- package/dist/corrections.js +28 -42
- package/dist/docsync.js +15 -3
- package/dist/lead.js +6 -24
- package/dist/orchestrate/attente.js +82 -0
- package/dist/orchestrate/command.js +18 -17
- package/dist/orchestrate/continue.js +2 -12
- package/dist/orchestrate/cycle.js +5 -5
- package/dist/orchestrate/integration-vivante.js +138 -0
- package/dist/orchestrate/launch.js +6 -18
- package/dist/orchestrate/lock.js +3 -20
- package/dist/orchestrate/registry.js +8 -1
- package/dist/orchestrate/schemas.js +5 -1
- package/dist/orchestrate/snapshot.js +13 -0
- package/dist/orchestrate/state.js +17 -0
- package/dist/plan-extras.js +3 -2
- package/dist/plan-ids.js +95 -0
- package/dist/plan-read.js +2 -0
- package/dist/plan.js +29 -34
- package/dist/ref/command.js +2 -3
- package/dist/resources.js +3 -9
- package/dist/state.js +8 -8
- package/dist/verify-gate.js +33 -2
- package/dist/yaml-keys.js +19 -0
- package/docs/cadence-hud.md +1 -1
- package/docs/deliver.md +1 -1
- package/docs/lead.md +2 -2
- package/docs/news.md +2 -0
- package/docs/orchestrate.md +6 -2
- package/docs/qualite.md +1 -1
- package/docs/raf.md +22 -4
- package/docs/whats-new.md +6 -0
- package/package.json +1 -1
- package/skills/lead/SKILL.md +1 -1
package/dist/config.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import { existsSync,
|
|
1
|
+
import { existsSync, realpathSync, statSync } from 'node:fs';
|
|
2
2
|
import { basename, dirname, isAbsolute, join, posix, relative, resolve } from 'node:path';
|
|
3
|
-
import { parse } from 'yaml';
|
|
4
3
|
import { isDay } from './dates.js';
|
|
5
4
|
import { readGate } from './gate-config.js';
|
|
6
5
|
import { DEFAULT_TEST_PATTERNS } from './testgate.js';
|
|
@@ -9,9 +8,9 @@ import { gitRoot } from './git.js';
|
|
|
9
8
|
import { planCommitProblem } from './plan-commit.js';
|
|
10
9
|
import { parseStates } from './plan-states.js';
|
|
11
10
|
import { checkTestReplay, readGateTest } from './orchestrate/gate-parts.js';
|
|
12
|
-
import { booleanKey, isObject, list } from './yaml-keys.js';
|
|
13
|
-
import { FIELDS, Plan, RafError, STATUSES } from './plan.js';
|
|
14
|
-
const KEYS = ['path', 'project', 'since', 'ignore', 'files', 'lots', 'fields', 'statuses', 'estimates', 'add', 'onWrite', 'commit', 'states', 'transitions', 'doneState', 'blockedState', 'extras'];
|
|
11
|
+
import { booleanKey, isObject, list, readYamlFile, readYamlKey } from './yaml-keys.js';
|
|
12
|
+
import { FIELDS, Plan, RafError, readIdPattern, STATUSES } from './plan.js';
|
|
13
|
+
const KEYS = ['path', 'project', 'since', 'ignore', 'files', 'lots', 'fields', 'statuses', 'estimates', 'add', 'onWrite', 'commit', 'states', 'transitions', 'doneState', 'blockedState', 'extras', 'idPattern'];
|
|
15
14
|
const ON_WRITE_KEYS = ['command', 'files'];
|
|
16
15
|
const FORMAT_KEYS = ['lots', 'fields', 'statuses', 'estimates'];
|
|
17
16
|
/** Chemin réel (liens symboliques résolus) du plus proche ancêtre existant, le reste rattaché tel quel. */
|
|
@@ -72,15 +71,9 @@ function readOnWrite(v, cwd, bad) {
|
|
|
72
71
|
* Null quand le fichier ou la clé manque — le plan est alors .cadence/raf.yaml (ou docs/plan/raf.yaml hérité) au format de raf.
|
|
73
72
|
*/
|
|
74
73
|
export function readPlanConfig(file, root) {
|
|
75
|
-
|
|
74
|
+
const raw = readYamlFile(file);
|
|
75
|
+
if (raw === undefined)
|
|
76
76
|
return null;
|
|
77
|
-
let raw;
|
|
78
|
-
try {
|
|
79
|
-
raw = parse(readFileSync(file, 'utf8'));
|
|
80
|
-
}
|
|
81
|
-
catch (e) {
|
|
82
|
-
throw new RafError(`${file} illisible : ${e.message.split('\n')[0]}`);
|
|
83
|
-
}
|
|
84
77
|
const doc = raw;
|
|
85
78
|
const qa = readQa(doc?.qa, file, root);
|
|
86
79
|
const p = doc?.plan;
|
|
@@ -112,7 +105,7 @@ export function readPlanConfig(file, root) {
|
|
|
112
105
|
settings.onWrite = readOnWrite(p.onWrite, root ?? dirname(resolve(file)), bad);
|
|
113
106
|
if (FORMAT_KEYS.some((k) => p[k] != null))
|
|
114
107
|
settings.format = parseFormat(p, bad);
|
|
115
|
-
Object.assign(settings, parseStates(p, bad, settings.format !== undefined), readExtras(p.extras, !!settings.format, bad));
|
|
108
|
+
Object.assign(settings, parseStates(p, bad, settings.format !== undefined), readExtras(p.extras, !!settings.format, bad), readIdPattern(p.idPattern, bad));
|
|
116
109
|
if (p.path != null && String(p.path).trim() === '')
|
|
117
110
|
throw bad('path est vide');
|
|
118
111
|
if (p.add != null && (typeof p.add !== 'string' || !p.add.includes('{title}')))
|
|
@@ -175,16 +168,7 @@ function parseFormat(p, bad) {
|
|
|
175
168
|
}
|
|
176
169
|
/** Clé `news:` de cadence.yaml. */
|
|
177
170
|
export function readNewsConfig(file) {
|
|
178
|
-
|
|
179
|
-
return {};
|
|
180
|
-
let raw;
|
|
181
|
-
try {
|
|
182
|
-
raw = parse(readFileSync(file, 'utf8'));
|
|
183
|
-
}
|
|
184
|
-
catch (e) {
|
|
185
|
-
throw new RafError(`${file} illisible : ${e.message.split('\n')[0]}`);
|
|
186
|
-
}
|
|
187
|
-
const n = raw?.news;
|
|
171
|
+
const n = readYamlKey(file, 'news');
|
|
188
172
|
if (n == null)
|
|
189
173
|
return {};
|
|
190
174
|
if (!isObject(n))
|
|
@@ -201,16 +185,7 @@ export function readNewsConfig(file) {
|
|
|
201
185
|
const AUTOSTART = ['warn', 'refuse', 'start'];
|
|
202
186
|
/** Clé `hook:` de cadence.yaml. */
|
|
203
187
|
export function readHookConfig(file) {
|
|
204
|
-
|
|
205
|
-
return {};
|
|
206
|
-
let raw;
|
|
207
|
-
try {
|
|
208
|
-
raw = parse(readFileSync(file, 'utf8'));
|
|
209
|
-
}
|
|
210
|
-
catch (e) {
|
|
211
|
-
throw new RafError(`${file} illisible : ${e.message.split('\n')[0]}`);
|
|
212
|
-
}
|
|
213
|
-
const h = raw?.hook;
|
|
188
|
+
const h = readYamlKey(file, 'hook');
|
|
214
189
|
if (h == null)
|
|
215
190
|
return {};
|
|
216
191
|
if (!isObject(h))
|
|
@@ -229,16 +204,7 @@ const PRICED_MODELS = ['sonnet', 'opus', 'haiku'];
|
|
|
229
204
|
const PRICE_KEYS = ['input', 'output', 'cacheRead'];
|
|
230
205
|
/** Clé `perf.prices` de cadence.yaml : la grille de prix du coût par lot livré ; null sans grille (clé absente ou vide). */
|
|
231
206
|
export function readPerfConfig(file) {
|
|
232
|
-
|
|
233
|
-
return null;
|
|
234
|
-
let raw;
|
|
235
|
-
try {
|
|
236
|
-
raw = parse(readFileSync(file, 'utf8'));
|
|
237
|
-
}
|
|
238
|
-
catch (e) {
|
|
239
|
-
throw new RafError(`${file} illisible : ${e.message.split('\n')[0]}`);
|
|
240
|
-
}
|
|
241
|
-
const perf = raw?.perf;
|
|
207
|
+
const perf = readYamlKey(file, 'perf');
|
|
242
208
|
if (perf == null)
|
|
243
209
|
return null;
|
|
244
210
|
if (!isObject(perf))
|
|
@@ -276,16 +242,7 @@ export function readPerfConfig(file) {
|
|
|
276
242
|
* joués par cadence au lieu d'être remplacés.
|
|
277
243
|
*/
|
|
278
244
|
export function readSessionConfig(file) {
|
|
279
|
-
|
|
280
|
-
return {};
|
|
281
|
-
let raw;
|
|
282
|
-
try {
|
|
283
|
-
raw = parse(readFileSync(file, 'utf8'));
|
|
284
|
-
}
|
|
285
|
-
catch (e) {
|
|
286
|
-
throw new RafError(`${file} illisible : ${e.message.split('\n')[0]}`);
|
|
287
|
-
}
|
|
288
|
-
const s = raw?.session;
|
|
245
|
+
const s = readYamlKey(file, 'session');
|
|
289
246
|
if (s == null)
|
|
290
247
|
return {};
|
|
291
248
|
if (!isObject(s))
|
|
@@ -316,16 +273,7 @@ const EFFORTS = ['default', 'low', 'medium', 'high', 'xhigh', 'max'];
|
|
|
316
273
|
const EFFORT_STEPS = ['precheck', 'spec', 'maquette', 'fiche', 'accept', 'implement', 'probe', 'fix', 'review', 'ux'];
|
|
317
274
|
/** Clé `verify:` de cadence.yaml. Absente : `{}`. */
|
|
318
275
|
export function readVerifyConfig(file) {
|
|
319
|
-
|
|
320
|
-
return {};
|
|
321
|
-
let raw;
|
|
322
|
-
try {
|
|
323
|
-
raw = parse(readFileSync(file, 'utf8'));
|
|
324
|
-
}
|
|
325
|
-
catch (e) {
|
|
326
|
-
throw new RafError(`${file} illisible : ${e.message.split('\n')[0]}`);
|
|
327
|
-
}
|
|
328
|
-
const v = raw?.verify;
|
|
276
|
+
const v = readYamlKey(file, 'verify');
|
|
329
277
|
if (v == null)
|
|
330
278
|
return {};
|
|
331
279
|
if (!isObject(v))
|
|
@@ -344,16 +292,7 @@ const ORCH_KEYS = ['start', 'verdict', 'test', 'testReplay', 'build', 'worktreeD
|
|
|
344
292
|
/** Clé `orchestrate:` de cadence.yaml. Absente : les défauts (auto, 45 min d'implémentation, 25 min de revue). */
|
|
345
293
|
export function readOrchestrateConfig(file) {
|
|
346
294
|
const config = { precheck: true, maquette: true, maquetteReviews: 3, fiche: true, precheckLocal: false, prodAccess: false, worktrees: false, spec: { dir: 'docs/specs' }, review: { threshold: 0.25, light: 'sonnet', full: 'opus' }, effort: { precheck: 'low', spec: 'medium', maquette: 'high', fiche: 'medium', accept: 'medium', implement: 'medium', probe: 'medium', fix: 'medium', review: 'high', ux: 'high' }, permissionMode: 'auto', addDirs: [], timeouts: { work: 45 * 60_000, review: 25 * 60_000 } };
|
|
347
|
-
|
|
348
|
-
return config;
|
|
349
|
-
let raw;
|
|
350
|
-
try {
|
|
351
|
-
raw = parse(readFileSync(file, 'utf8'));
|
|
352
|
-
}
|
|
353
|
-
catch (e) {
|
|
354
|
-
throw new RafError(`${file} illisible : ${e.message.split('\n')[0]}`);
|
|
355
|
-
}
|
|
356
|
-
const o = raw?.orchestrate;
|
|
295
|
+
const o = readYamlKey(file, 'orchestrate');
|
|
357
296
|
if (o == null)
|
|
358
297
|
return config;
|
|
359
298
|
const bad = (what) => new RafError(`${file} : orchestrate.${what}`);
|
|
@@ -583,15 +522,7 @@ const DOCS_KEYS = ['sync', 'since', 'articles'];
|
|
|
583
522
|
/** Clé `docs:` de cadence.yaml : `docs.sync`, la documentation qui doit suivre le code. */
|
|
584
523
|
export function readDocsConfig(file) {
|
|
585
524
|
const none = { sync: [] };
|
|
586
|
-
|
|
587
|
-
return none;
|
|
588
|
-
let raw;
|
|
589
|
-
try {
|
|
590
|
-
raw = parse(readFileSync(file, 'utf8'));
|
|
591
|
-
}
|
|
592
|
-
catch (e) {
|
|
593
|
-
throw new RafError(`${file} illisible : ${e.message.split('\n')[0]}`);
|
|
594
|
-
}
|
|
525
|
+
const raw = readYamlFile(file);
|
|
595
526
|
if (raw?.describedBy != null) {
|
|
596
527
|
throw new RafError(`${file} : describedBy n'existe plus (L308) : les documents hors dépôt se déclarent dans docs.articles, { repo, file } relatif au projet`);
|
|
597
528
|
}
|
|
@@ -650,16 +581,7 @@ export function readDocsConfig(file) {
|
|
|
650
581
|
}
|
|
651
582
|
/** Clé `references:` de cadence.yaml. */
|
|
652
583
|
export function readReferencesConfig(file) {
|
|
653
|
-
|
|
654
|
-
return {};
|
|
655
|
-
let raw;
|
|
656
|
-
try {
|
|
657
|
-
raw = parse(readFileSync(file, 'utf8'));
|
|
658
|
-
}
|
|
659
|
-
catch (e) {
|
|
660
|
-
throw new RafError(`${file} illisible : ${e.message.split('\n')[0]}`);
|
|
661
|
-
}
|
|
662
|
-
const r = raw?.references;
|
|
584
|
+
const r = readYamlKey(file, 'references');
|
|
663
585
|
if (r == null)
|
|
664
586
|
return {};
|
|
665
587
|
if (!isObject(r))
|
|
@@ -676,16 +598,7 @@ export function readReferencesConfig(file) {
|
|
|
676
598
|
/** Clé `migrate:` de cadence.yaml (L284) : `migrate.keep`. */
|
|
677
599
|
export function readMigrateConfig(file) {
|
|
678
600
|
const none = { keep: [] };
|
|
679
|
-
|
|
680
|
-
return none;
|
|
681
|
-
let raw;
|
|
682
|
-
try {
|
|
683
|
-
raw = parse(readFileSync(file, 'utf8'));
|
|
684
|
-
}
|
|
685
|
-
catch (e) {
|
|
686
|
-
throw new RafError(`${file} illisible : ${e.message.split('\n')[0]}`);
|
|
687
|
-
}
|
|
688
|
-
const m = raw?.migrate;
|
|
601
|
+
const m = readYamlKey(file, 'migrate');
|
|
689
602
|
if (m == null)
|
|
690
603
|
return none;
|
|
691
604
|
if (!isObject(m))
|
package/dist/corrections.js
CHANGED
|
@@ -198,32 +198,37 @@ Règle de départ : si le code livré ne fait pas ce que le lot ou le commit ann
|
|
|
198
198
|
- donnees-sql : requête, schéma, migration, format ou contenu de données faux ;
|
|
199
199
|
- test-manquant : le code est juste mais un comportement n'est pas couvert, ou un test ne vérifie pas ce que son titre promet ;
|
|
200
200
|
- faux-constat : la revue se trompe (le code était correct, constat infondé, hors lot, sans effet réel).`;
|
|
201
|
+
/** Appel `/api/chat` d'Ollama (température 0, sortie contrainte par `format`) : le texte du message, null sur réponse refusée ou échec réseau. Commun au classeur et au sous-classeur. */
|
|
202
|
+
async function ollamaChat(opts, format, system, user) {
|
|
203
|
+
const doFetch = opts.fetch ?? fetch;
|
|
204
|
+
try {
|
|
205
|
+
const res = await doFetch(`${opts.url.replace(/\/$/, '')}/api/chat`, {
|
|
206
|
+
method: 'POST',
|
|
207
|
+
headers: { 'content-type': 'application/json' },
|
|
208
|
+
body: JSON.stringify({
|
|
209
|
+
model: opts.model,
|
|
210
|
+
stream: false,
|
|
211
|
+
options: { temperature: 0 },
|
|
212
|
+
format,
|
|
213
|
+
messages: [{ role: 'system', content: system }, { role: 'user', content: user }],
|
|
214
|
+
}),
|
|
215
|
+
});
|
|
216
|
+
if (!res.ok)
|
|
217
|
+
return null;
|
|
218
|
+
const body = (await res.json());
|
|
219
|
+
return body.message?.content ?? '';
|
|
220
|
+
}
|
|
221
|
+
catch {
|
|
222
|
+
return null;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
201
225
|
/** Classeur sur un modèle local servi par Ollama (/api/chat, température 0, sortie contrainte par un schéma) : hors quota Anthropic. */
|
|
202
226
|
export function ollamaClassifier(opts) {
|
|
203
|
-
const doFetch = opts.fetch ?? fetch;
|
|
204
227
|
return async (pass) => {
|
|
205
228
|
const findings = pass.findings.length ? pass.findings.map((f, i) => `${i + 1}. ${f.slice(0, 1500)}`).join('\n') : '(aucun constat lisible)';
|
|
206
229
|
const user = `Lot ${pass.project} ${pass.lot}.\nConstats de la revue :\n${findings}\n\nCommits de la correction :\n${pass.commits.join('\n') || '(aucun)'}`;
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
method: 'POST',
|
|
210
|
-
headers: { 'content-type': 'application/json' },
|
|
211
|
-
body: JSON.stringify({
|
|
212
|
-
model: opts.model,
|
|
213
|
-
stream: false,
|
|
214
|
-
options: { temperature: 0 },
|
|
215
|
-
format: { type: 'object', properties: { cause: { type: 'string', enum: [...CAUSES] }, raison: { type: 'string' } }, required: ['cause', 'raison'] },
|
|
216
|
-
messages: [{ role: 'system', content: SYSTEM }, { role: 'user', content: user }],
|
|
217
|
-
}),
|
|
218
|
-
});
|
|
219
|
-
if (!res.ok)
|
|
220
|
-
return null;
|
|
221
|
-
const body = (await res.json());
|
|
222
|
-
return parseCause(body.message?.content ?? '');
|
|
223
|
-
}
|
|
224
|
-
catch {
|
|
225
|
-
return null;
|
|
226
|
-
}
|
|
230
|
+
const text = await ollamaChat(opts, { type: 'object', properties: { cause: { type: 'string', enum: [...CAUSES] }, raison: { type: 'string' } }, required: ['cause', 'raison'] }, SYSTEM, user);
|
|
231
|
+
return text === null ? null : parseCause(text);
|
|
227
232
|
};
|
|
228
233
|
}
|
|
229
234
|
/** Classe les passes absentes du cache, une à une ; un échec n'entre pas au cache (une relance le reprend). */
|
|
@@ -323,30 +328,11 @@ const SOUS_SYSTEM = `Tu sous-classes une passe de correction déjà classée «
|
|
|
323
328
|
${SOUS_CAUSES.map((s) => `- ${s} : ${SOUS_DEFINITIONS[s]} ;`).join('\n')}`;
|
|
324
329
|
/** Sous-classeur sur un modèle local servi par Ollama (comme `ollamaClassifier` : /api/chat, température 0, sortie contrainte par un schéma à sept valeurs). */
|
|
325
330
|
export function ollamaSousClassifier(opts) {
|
|
326
|
-
const doFetch = opts.fetch ?? fetch;
|
|
327
331
|
return async (pass) => {
|
|
328
332
|
const findings = pass.findings.map((f, i) => `${i + 1}. ${f.slice(0, 1500)}`).join('\n');
|
|
329
333
|
const user = `Lot ${pass.project} ${pass.lot}.\nConstats de la revue :\n${findings}\n\nCommits de la correction :\n${pass.commits.join('\n') || '(aucun)'}`;
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
method: 'POST',
|
|
333
|
-
headers: { 'content-type': 'application/json' },
|
|
334
|
-
body: JSON.stringify({
|
|
335
|
-
model: opts.model,
|
|
336
|
-
stream: false,
|
|
337
|
-
options: { temperature: 0 },
|
|
338
|
-
format: { type: 'object', properties: { sous: { type: 'string', enum: [...SOUS_CAUSES] }, raison: { type: 'string' } }, required: ['sous', 'raison'] },
|
|
339
|
-
messages: [{ role: 'system', content: SOUS_SYSTEM }, { role: 'user', content: user }],
|
|
340
|
-
}),
|
|
341
|
-
});
|
|
342
|
-
if (!res.ok)
|
|
343
|
-
return null;
|
|
344
|
-
const body = (await res.json());
|
|
345
|
-
return parseSousCause(body.message?.content ?? '');
|
|
346
|
-
}
|
|
347
|
-
catch {
|
|
348
|
-
return null;
|
|
349
|
-
}
|
|
334
|
+
const text = await ollamaChat(opts, { type: 'object', properties: { sous: { type: 'string', enum: [...SOUS_CAUSES] }, raison: { type: 'string' } }, required: ['sous', 'raison'] }, SOUS_SYSTEM, user);
|
|
335
|
+
return text === null ? null : parseSousCause(text);
|
|
350
336
|
};
|
|
351
337
|
}
|
|
352
338
|
/** La cause retenue d'une passe à constats : la relecture à la main, sinon le modèle ; absente sans classement. */
|
package/dist/docsync.js
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
import { resolve } from 'node:path';
|
|
2
2
|
import { changedFiles } from './git.js';
|
|
3
|
+
/**
|
|
4
|
+
* Commits dont les fichiers comptent pour `docs.sync` (L537) : ceux de type `refactor` (`refactor(L1): …`) sont un nettoyage ou
|
|
5
|
+
* un découpage déclaré sans changement de commande, de clé ni de sortie, donc sans document à écrire. `refactor!` et un refactor
|
|
6
|
+
* dont le corps porte le pied `BREAKING CHANGE:` restent un changement.
|
|
7
|
+
*/
|
|
8
|
+
function docSyncCommits(commits) {
|
|
9
|
+
return commits.filter((c) => !/^refactor(\([^)]*\))?:/.test(c.subject) || /^BREAKING[ -]CHANGE:/m.test(c.body));
|
|
10
|
+
}
|
|
11
|
+
/** Les écarts d'un lot : les fichiers de `paths` viennent des commits qui comptent, mais un document écrit dans un refactor reste écrit. */
|
|
12
|
+
export function lotDocSyncGaps(rules, root, commits) {
|
|
13
|
+
return docSyncGaps(rules, filesOf(root, docSyncCommits(commits)), filesOf(root, commits));
|
|
14
|
+
}
|
|
3
15
|
/** Motif de chemin : `*` reste dans un dossier, `**` le traverse, `?` un caractère, un `/` final prend tout le dossier ; sinon chemin exact. */
|
|
4
16
|
export function docSyncMatcher(pattern) {
|
|
5
17
|
const p = pattern.trim().replace(/\\/g, '/').replace(/^\.\//, '');
|
|
@@ -20,11 +32,11 @@ export function docSyncSet(patterns) {
|
|
|
20
32
|
export function filesOf(root, commits, opts = {}) {
|
|
21
33
|
return new Set(commits.flatMap((c) => changedFiles(root, c.sha, opts)));
|
|
22
34
|
}
|
|
23
|
-
/** Les règles que ces fichiers enfreignent : ils touchent `paths` sans qu'aucun `docs` ne soit parmi
|
|
24
|
-
|
|
35
|
+
/** Les règles que ces fichiers enfreignent : ils touchent `paths` sans qu'aucun `docs` ne soit parmi les fichiers `written`. */
|
|
36
|
+
function docSyncGaps(rules, files, written) {
|
|
25
37
|
const gaps = [];
|
|
26
38
|
for (const rule of rules) {
|
|
27
|
-
if ([...
|
|
39
|
+
if ([...written].some(docSyncSet(rule.docs)))
|
|
28
40
|
continue;
|
|
29
41
|
const inPaths = docSyncSet(rule.paths);
|
|
30
42
|
const touched = [...files].filter(inPaths).sort();
|
package/dist/lead.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync, readdirSync,
|
|
1
|
+
import { existsSync, readdirSync, statSync } from 'node:fs';
|
|
2
2
|
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';
|
|
@@ -7,7 +7,7 @@ import { localRepoNotes } from './local-task.js';
|
|
|
7
7
|
import { gitRoot, repoStatus } from './git.js';
|
|
8
8
|
import { linkCommits } from './link.js';
|
|
9
9
|
import { commitCount, lastActivity, lockStatus } from './session.js';
|
|
10
|
-
import {
|
|
10
|
+
import { isObject, readYamlFile, readYamlKey } from './yaml-keys.js';
|
|
11
11
|
import { isOpen, RafError } from './plan.js';
|
|
12
12
|
import { isRecurring } from './recurring.js';
|
|
13
13
|
import { priorityRank, readPriority } from './orchestrate/continue.js';
|
|
@@ -20,16 +20,7 @@ import { KEPT_TITLE, keptLine, keptTrees } from './orchestrate/worktree.js';
|
|
|
20
20
|
/** Le bloc `lead:` du cadence.yaml du dossier parent (absent = `undefined`) ; un `lead` qui n'est pas une table est refusé en nommant `key`. */
|
|
21
21
|
function readLeadBlock(parent, key, expected) {
|
|
22
22
|
const file = join(parent, 'cadence.yaml');
|
|
23
|
-
|
|
24
|
-
return undefined;
|
|
25
|
-
let raw;
|
|
26
|
-
try {
|
|
27
|
-
raw = parse(readFileSync(file, 'utf8'));
|
|
28
|
-
}
|
|
29
|
-
catch (e) {
|
|
30
|
-
throw new RafError(`${file} illisible : ${e.message.split('\n')[0]}`);
|
|
31
|
-
}
|
|
32
|
-
const l = raw?.lead;
|
|
23
|
+
const l = readYamlKey(file, 'lead');
|
|
33
24
|
if (l == null)
|
|
34
25
|
return undefined;
|
|
35
26
|
if (typeof l !== 'object' || Array.isArray(l))
|
|
@@ -52,8 +43,8 @@ export function findLeadDir(start) {
|
|
|
52
43
|
const file = join(dir, 'cadence.yaml');
|
|
53
44
|
if (existsSync(file)) {
|
|
54
45
|
try {
|
|
55
|
-
const raw =
|
|
56
|
-
if (raw &&
|
|
46
|
+
const raw = readYamlFile(file);
|
|
47
|
+
if (isObject(raw) && ('lead' in raw || 'priority' in raw))
|
|
57
48
|
return dir;
|
|
58
49
|
}
|
|
59
50
|
catch {
|
|
@@ -79,16 +70,7 @@ export function readLeadContextThreshold(parent) {
|
|
|
79
70
|
/** Clé `depth:` du cadence.yaml du dossier parent (L201, issue #8), à côté de `priority:` : combien de niveaux `lead tour` parcourt (1 : sous-dossiers directs, le défaut). */
|
|
80
71
|
export function readDepth(parent) {
|
|
81
72
|
const file = join(parent, 'cadence.yaml');
|
|
82
|
-
|
|
83
|
-
return 1;
|
|
84
|
-
let raw;
|
|
85
|
-
try {
|
|
86
|
-
raw = parse(readFileSync(file, 'utf8'));
|
|
87
|
-
}
|
|
88
|
-
catch (e) {
|
|
89
|
-
throw new RafError(`${file} illisible : ${e.message.split('\n')[0]}`);
|
|
90
|
-
}
|
|
91
|
-
const d = raw?.depth;
|
|
73
|
+
const d = readYamlKey(file, 'depth');
|
|
92
74
|
if (d == null)
|
|
93
75
|
return 1;
|
|
94
76
|
if (typeof d !== 'number' || !Number.isInteger(d) || d < 1)
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { processStart } from '../proc.js';
|
|
4
|
+
import { holderAlive } from './lock.js';
|
|
5
|
+
import { lotKey } from './state.js';
|
|
6
|
+
import { priorityRank, readPriority } from './continue.js';
|
|
7
|
+
const waitersDir = (home) => join(home, 'waiters');
|
|
8
|
+
let waiterSeq = 0;
|
|
9
|
+
/** Les attentes vivantes, toutes vagues confondues, de la première servie à la dernière : étape en cours avant nouveau tirage, puis rang de priorité, puis ancienneté. */
|
|
10
|
+
function liveWaiters(home) {
|
|
11
|
+
let names;
|
|
12
|
+
try {
|
|
13
|
+
names = readdirSync(waitersDir(home)).filter((n) => n.endsWith('.json'));
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return [];
|
|
17
|
+
}
|
|
18
|
+
const out = [];
|
|
19
|
+
for (const n of names) {
|
|
20
|
+
const file = join(waitersDir(home), n);
|
|
21
|
+
try {
|
|
22
|
+
const r = JSON.parse(readFileSync(file, 'utf8'));
|
|
23
|
+
const w = {
|
|
24
|
+
pid: Number(r.pid),
|
|
25
|
+
start: r.start === undefined ? undefined : String(r.start),
|
|
26
|
+
wave: String(r.wave),
|
|
27
|
+
since: Number(r.since),
|
|
28
|
+
seq: Number(r.seq),
|
|
29
|
+
fresh: !!r.fresh,
|
|
30
|
+
rank: Number.isFinite(Number(r.rank)) ? Number(r.rank) : Number.MAX_SAFE_INTEGER,
|
|
31
|
+
file,
|
|
32
|
+
};
|
|
33
|
+
if (holderAlive(w.pid, w.start))
|
|
34
|
+
out.push(w);
|
|
35
|
+
else
|
|
36
|
+
rmSync(file, { force: true }); // le processus est mort sans désinscrire son attente
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
// inscription illisible (en cours d'écriture) : ignorée à ce tour
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return out.sort((a, b) => Number(a.fresh) - Number(b.fresh) || a.rank - b.rank || a.since - b.since || a.pid - b.pid || a.seq - b.seq);
|
|
43
|
+
}
|
|
44
|
+
/** Inscrit une attente de créneau le temps de `run`, qui reçoit la question « est-ce mon tour ? » ; désinscrite à la fin, obtenue ou abandonnée (L531). */
|
|
45
|
+
export async function whileWaiting(home, o, run) {
|
|
46
|
+
const me = {
|
|
47
|
+
pid: process.pid,
|
|
48
|
+
start: processStart(process.pid) ?? undefined,
|
|
49
|
+
wave: o.wave,
|
|
50
|
+
since: Date.now(),
|
|
51
|
+
seq: waiterSeq++,
|
|
52
|
+
fresh: o.fresh ?? false,
|
|
53
|
+
rank: o.rank ?? Number.MAX_SAFE_INTEGER,
|
|
54
|
+
};
|
|
55
|
+
const file = join(waitersDir(home), `${me.pid}-${me.seq}.json`);
|
|
56
|
+
mkdirSync(waitersDir(home), { recursive: true });
|
|
57
|
+
writeFileSync(file, JSON.stringify(me));
|
|
58
|
+
try {
|
|
59
|
+
return await run(() => liveWaiters(home)[0]?.file === file);
|
|
60
|
+
}
|
|
61
|
+
finally {
|
|
62
|
+
rmSync(file, { force: true });
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
/** Un créneau est à prendre quand le total est sous le plafond et qu'aucune attente vivante ne passe avant la nôtre. */
|
|
66
|
+
export const hasRoom = (taken, limit, myTurn) => taken() < limit && myTurn();
|
|
67
|
+
/**
|
|
68
|
+
* La place dans la file des créneaux de la session que le lot `key` (`projet:lot`) lance : un lot qui n'a qu'un pas (celui qui part, déjà inscrit) n'a rien joué (`fresh`) ;
|
|
69
|
+
* le rang est celui de son projet dans `priority:` (`--priority`, sinon le cadence.yaml du dossier de lancement ; illisible : rang neutre). Lot inconnu : place neutre.
|
|
70
|
+
*/
|
|
71
|
+
export function slotPlace(all, launch, given, key) {
|
|
72
|
+
const l = key ? all.find((x) => lotKey(x.project, x.lot) === key) : undefined;
|
|
73
|
+
if (!l)
|
|
74
|
+
return {};
|
|
75
|
+
const fresh = l.steps.length <= 1;
|
|
76
|
+
try {
|
|
77
|
+
return { fresh, rank: priorityRank(l.project, given ?? readPriority(launch)) };
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
return { fresh }; // priority: illisible : rang neutre (après tous les déclarés), le lancement n'en dépend pas
|
|
81
|
+
}
|
|
82
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { gateProgress, gateText } from '../audit.js';
|
|
2
2
|
import { execFileSync } from 'node:child_process';
|
|
3
3
|
import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, statSync, writeFileSync } from 'node:fs';
|
|
4
|
-
import { availableParallelism,
|
|
4
|
+
import { availableParallelism, tmpdir } from 'node:os';
|
|
5
5
|
import { testWorkersFrom } from './workers.js';
|
|
6
6
|
import { basename, join, resolve } from 'node:path';
|
|
7
7
|
import { PLAN_DEFAULT, PLAN_LEGACY, planLoader, readOrchestrateConfig, readPlanConfig } from '../config.js';
|
|
@@ -29,15 +29,17 @@ import { fichesDuDossier } from '../spec.js';
|
|
|
29
29
|
import { candidates, fillTargets, handedBackStreak, parsePriority, parseUntil, drawSettingsReport, readPriority, stopReason } from './continue.js';
|
|
30
30
|
import { precheckNothingToFind, Budget, ESCALATION_RESERVE, DROPPED, STOP_REQUESTED, MAX_PASSES, REVIEW_RESERVE, budgetStep, heldForEscalation, lotSpent, MAX_PASSES_CONVERGING, briefName, countInterrupted, planNoteHandBack, leadReviewedHandBack, HYPOTHESES_HANDBACK, handBackDroppedQuestions, needsPrecheck, workDir } from './cycle.js';
|
|
31
31
|
import { installPrePush, keepGateGuard, longDuration, removePrePush } from './guard.js';
|
|
32
|
-
import { buildArgs, buildLocalArgs, killSessions, MODEL_ALIASES, mcpServersFor, readAgents, realClaude, enabledPlugins, settingsFiles } from './launch.js';
|
|
32
|
+
import { buildArgs, buildLocalArgs, claudeConfigDir, killSessions, MODEL_ALIASES, mcpServersFor, readAgents, realClaude, enabledPlugins, settingsFiles } from './launch.js';
|
|
33
33
|
import { activeLock, REPO_LOCK, releaseLock, takeLock } from './lock.js';
|
|
34
34
|
import { groupLots, runPool } from './pool.js';
|
|
35
|
+
import { slotPlace } from './attente.js';
|
|
35
36
|
import { AddFeed, addNotAlone, addToLive, openInRepo, projectName } from './ajout.js';
|
|
36
37
|
import { repoRefusals, trackedDirty } from './depot.js';
|
|
38
|
+
import { adoptLeadReview, depositIntegrations, depositsOnly, integrationText, runsElsewhere, takeIntegrations } from './integration-vivante.js';
|
|
37
39
|
import { removeWorktree } from './retrait.js';
|
|
38
40
|
import { repoBranch } from './integrate.js';
|
|
39
41
|
import { baseBranchOf, keptLines, lotBranch, lotTreeDir, planFileSet, recoverTree } from './worktree.js';
|
|
40
|
-
import {
|
|
42
|
+
import { effortKey, schemaFor, stepAgent } from './schemas.js';
|
|
41
43
|
import { resolveLotRepos } from '../repos.js';
|
|
42
44
|
import { excludeState, lotFinished, lotKey, lotRepoPaths, lotSlug, neighbourDirs, newLot, RunStore } from './state.js';
|
|
43
45
|
import { isExpress } from './express.js';
|
|
@@ -527,7 +529,7 @@ function dryRun(lots, io, deps, { budget, id, multiSlot }) {
|
|
|
527
529
|
writeFileSync(file, renderBrief(briefName(l, s.kind), s.kind === 'fiche' || vars.fiche === undefined ? vars : { ...vars, fiche: '' }, deps.templatesDir)); // la consigne de revue de fiche ne sert qu'au brief de la fiche : implement et review en sont la base d'une délégation à la main
|
|
528
530
|
const playwright = !!mcpServersFor(s.kind, l.visible, '', l.small, !!l.screen).playwright;
|
|
529
531
|
const local = s.model === 'local';
|
|
530
|
-
const args = (local ? buildLocalArgs : buildArgs)({ kind: s.kind, sessionId: '<uuid>', brief: '<brief>', model: s.model === 'local' ? 'sonnet' : s.model, local, effort: env.config.effort[s.kind
|
|
532
|
+
const args = (local ? buildLocalArgs : buildArgs)({ kind: s.kind, sessionId: '<uuid>', brief: '<brief>', model: s.model === 'local' ? 'sonnet' : s.model, local, effort: env.config.effort[effortKey(s.kind)], schema: schemaFor(s.kind), agent: stepAgent(s.kind), cwd: l.repo, wave: id, permissionMode: env.config.permissionMode, addDirs: [...env.config.addDirs, ...(playwright && (s.kind === 'implement' || s.kind === 'spec') ? [pwDir] : []), ...neighbourDirs(l)], timeoutMs: 0, mcpConfig: '<mcp>', playwright, disablePlugins: enabledPlugins(settingsFiles(deps.claudeHome, l.repo)) }, agents).map((a) => (a.startsWith('{') ? '<json>' : a));
|
|
531
533
|
io.out(` ${s.kind} : ${local ? 'claude-local' : 'claude'} ${args.join(' ')}`);
|
|
532
534
|
io.out(` brief : ${file}`);
|
|
533
535
|
const servers = Object.keys(mcpServersFor(s.kind, l.visible, '', l.small, !!l.screen));
|
|
@@ -1102,9 +1104,8 @@ async function execute(wave, lots, store, io, deps, today, args, all = lots) {
|
|
|
1102
1104
|
throw e;
|
|
1103
1105
|
}
|
|
1104
1106
|
// Plafond de sessions simultanées, toutes vagues confondues : chaque session attend un créneau libre avant de partir.
|
|
1105
|
-
const claude = async (
|
|
1106
|
-
const free = await acquireSlot(home, cap, {
|
|
1107
|
-
wave: wave.id,
|
|
1107
|
+
const claude = async (argv, o) => {
|
|
1108
|
+
const free = await acquireSlot(home, cap, { wave: wave.id, ...slotPlace(all, launch, args.priority, o.lot), // L531 : place dans la file des créneaux, commune aux vagues
|
|
1108
1109
|
pollMs: deps.slotPollMs,
|
|
1109
1110
|
// L332 : une demande écrite pendant l'attente (`--drop` de ce lot, `--stop-after-current`) l'interrompt ; la session ne part pas.
|
|
1110
1111
|
abandon: () => {
|
|
@@ -1120,7 +1121,7 @@ async function execute(wave, lots, store, io, deps, today, args, all = lots) {
|
|
|
1120
1121
|
onGot: (ms) => wctx.log(`${o.lot ? `${o.lot} · ` : ''}créneau de session obtenu après ${duration(ms)} d'attente`),
|
|
1121
1122
|
});
|
|
1122
1123
|
try {
|
|
1123
|
-
return await deps.claude(
|
|
1124
|
+
return await deps.claude(argv, o);
|
|
1124
1125
|
}
|
|
1125
1126
|
finally {
|
|
1126
1127
|
free();
|
|
@@ -1259,6 +1260,8 @@ async function execute(wave, lots, store, io, deps, today, args, all = lots) {
|
|
|
1259
1260
|
taken.push(c);
|
|
1260
1261
|
}
|
|
1261
1262
|
}
|
|
1263
|
+
// L532 : les intégrations déposées par `--resume --integrate` pendant la vague sont reprises de la même façon, au tour où le lot peut jouer.
|
|
1264
|
+
taken.push(...(await takeIntegrations({ store, ctxs, wctx, settled }, canRun)));
|
|
1262
1265
|
if (taken.length) {
|
|
1263
1266
|
try {
|
|
1264
1267
|
updateWaveRepos(home, process.pid, repos.filter((x) => !freed.has(x)), lanes());
|
|
@@ -1469,6 +1472,9 @@ async function resume(args, argv, io, deps, launch, today) {
|
|
|
1469
1472
|
const store = typeof args.resume === 'string' ? RunStore.find(launch, args.resume) : singleUnfinished(launch);
|
|
1470
1473
|
if (!store)
|
|
1471
1474
|
throw new RafError(`vague inconnue : ${args.resume}`);
|
|
1475
|
+
// L532 : `--integrate` seul sur une vague qui tourne encore dépose l'intégration dans sa file au lieu de refuser ; dit avant l'instantané (le dépôt ne demande rien à son code) ; une reprise qui attend le dépôt (L443) est inscrite vivante mais ne joue pas encore : la vague doit être « running ». Avec une autre option de reprise ou --replay, le refus ci-dessous reste.
|
|
1476
|
+
if (depositsOnly(args, runsElsewhere(store, launch)))
|
|
1477
|
+
return depositIntegrations(args.integrate, io, store);
|
|
1472
1478
|
if (deps.snapshot) {
|
|
1473
1479
|
// La reprise tourne sur l'instantané de la vague, jamais sur le dist/ courant ; une vague d'avant L61 n'en a pas.
|
|
1474
1480
|
if (LOT_LIKE.test(store.id) && snapshotExists(store.dir)) {
|
|
@@ -1743,19 +1749,14 @@ async function resume(args, argv, io, deps, launch, today) {
|
|
|
1743
1749
|
for (const l of integ) {
|
|
1744
1750
|
const k = lotKey(l.project, l.lot);
|
|
1745
1751
|
const lead = leadReviewed.get(l);
|
|
1746
|
-
if (lead)
|
|
1747
|
-
|
|
1748
|
-
l.next = 'integrate';
|
|
1749
|
-
l.code = { conforme: true, bloquants: 0, majeurs: 0, mineurs: 0, verdict: lead.verdict, sousTaches: [], nonVerifie: [], head: lead.head };
|
|
1750
|
-
l.verdict = lead.verdict;
|
|
1751
|
-
l.constats = [];
|
|
1752
|
-
}
|
|
1752
|
+
if (lead)
|
|
1753
|
+
adoptLeadReview(l, lead);
|
|
1753
1754
|
const planNote = l.next !== 'integrate'; // rendu sur une écriture de plan refusée (--replay) : le lot repart de l'étape gardée
|
|
1754
1755
|
if (planNote && l.outcome?.startsWith(HYPOTHESES_HANDBACK))
|
|
1755
1756
|
l.hypotheses = undefined; // les notes d'hypothèses se posent de nouveau (sans doublon) à l'entrée de l'étape
|
|
1756
1757
|
l.status = 'suspended';
|
|
1757
1758
|
l.outcome = null;
|
|
1758
|
-
const text =
|
|
1759
|
+
const text = planNote ? `écriture du plan reprise à l'étape ${l.next} (--resume --replay)` : integrationText(lead ?? null);
|
|
1759
1760
|
store.journal(`${k} → suspended — ${text}`);
|
|
1760
1761
|
io.out(`${k} → suspended — ${text}`);
|
|
1761
1762
|
}
|
|
@@ -1847,6 +1848,6 @@ export function realOrchestrateDeps(env) {
|
|
|
1847
1848
|
},
|
|
1848
1849
|
agentsDir: AGENTS_DIR,
|
|
1849
1850
|
snapshot: child ? undefined : { packageRoot: PACKAGE_ROOT, reexec: spawnReexec },
|
|
1850
|
-
claudeHome: env
|
|
1851
|
+
claudeHome: claudeConfigDir(env),
|
|
1851
1852
|
};
|
|
1852
1853
|
}
|
|
@@ -1,11 +1,10 @@
|
|
|
1
|
-
import { existsSync, readFileSync } from 'node:fs';
|
|
2
1
|
import { basename, join } from 'node:path';
|
|
3
|
-
import { parse } from 'yaml';
|
|
4
2
|
import { nextUp } from '../audit.js';
|
|
5
3
|
import { planLoader } from '../config.js';
|
|
6
4
|
import { gitRoot } from '../git.js';
|
|
7
5
|
import { findProjects, isProject } from '../lead.js';
|
|
8
6
|
import { RafError } from '../plan.js';
|
|
7
|
+
import { readYamlKey } from '../yaml-keys.js';
|
|
9
8
|
import { lotBudget, projectEnv } from './command.js';
|
|
10
9
|
import { readyFree } from '../ready-free.js';
|
|
11
10
|
import { lotFinished, lotKey } from './state.js';
|
|
@@ -32,16 +31,7 @@ export function parsePriority(text) {
|
|
|
32
31
|
/** Clé `priority:` du cadence.yaml du dossier de lancement (le dossier parent des projets) ; absente : aucune priorité déclarée. */
|
|
33
32
|
export function readPriority(launchDir) {
|
|
34
33
|
const file = join(launchDir, 'cadence.yaml');
|
|
35
|
-
|
|
36
|
-
return [];
|
|
37
|
-
let raw;
|
|
38
|
-
try {
|
|
39
|
-
raw = parse(readFileSync(file, 'utf8'));
|
|
40
|
-
}
|
|
41
|
-
catch (e) {
|
|
42
|
-
throw new RafError(`${file} illisible : ${e.message.split('\n')[0]}`);
|
|
43
|
-
}
|
|
44
|
-
const p = raw?.priority;
|
|
34
|
+
const p = readYamlKey(file, 'priority');
|
|
45
35
|
if (p == null)
|
|
46
36
|
return [];
|
|
47
37
|
if (!Array.isArray(p) || p.length === 0 || p.some((n) => typeof n !== 'string' || !n.trim()))
|