@sylad/cadence 1.42.1 → 1.43.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.
@@ -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.42.1",
11
+ "version": "1.43.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.42.1",
4
+ "version": "1.43.0",
5
5
  "author": { "name": "Sylvain Ladoire" },
6
6
  "homepage": "https://github.com/Sylad/cadence",
7
7
  "repository": "https://github.com/Sylad/cadence",
@@ -0,0 +1,138 @@
1
+ import { isDay } from '../dates.js';
2
+ import { RafError } from '../plan.js';
3
+ import { parsePriority } from './continue.js';
4
+ import { resumeCommand } from './table.js';
5
+ /** Sessions simultanées, toutes vagues confondues (`--max-sessions`, ou CADENCE_MAX_SESSIONS). */
6
+ export const DEFAULT_MAX_SESSIONS = 2;
7
+ /** `1500000`, `1.5M`, `800k`. */
8
+ export function parseBudget(text) {
9
+ const m = /^(\d+(?:[.,]\d+)?)\s*([kKmM]?)$/.exec(text.trim());
10
+ if (!m)
11
+ throw new RafError(`--budget invalide : ${text} (1500000, 1.5M, 800k)`);
12
+ const n = Number(m[1].replace(',', '.')) * ({ '': 1, k: 1e3, m: 1e6 }[m[2].toLowerCase()]);
13
+ if (!(n > 0))
14
+ throw new RafError(`--budget invalide : ${text}`);
15
+ return Math.round(n);
16
+ }
17
+ export function parseMaxSessions(text, what = '--max-sessions') {
18
+ if (!/^\d+$/.test(text.trim()) || Number(text) < 1)
19
+ throw new RafError(`${what} invalide : ${text} (entier ≥ 1)`);
20
+ return Number(text);
21
+ }
22
+ // une vague (2026-10-04-1412) n'a ni « : » ni « / » : leur présence désigne un lot, quel que soit son motif (R-M7/ux-1)
23
+ export const LOT_LIKE = /^(?:[\w.-]+(?:\/[\w.-]+)*:[\w.-][\w./-]*|[\w.-]+\/[\w./-]*|[A-Za-z]+\d+)(?:@[\w.-]+)?$/;
24
+ function lotArg(text) {
25
+ // le premier « : » sépare le projet du lot, un « @ » final le modèle ; le lot garde ses « / » (L120), le projet peut être un chemin depuis le dossier (`groupe/gamma`, projet imbriqué de lead tour --depth, L201)
26
+ const m = /^(?:([\w.-]+(?:\/[\w.-]+)*):)?([\w.-][\w./-]*?)(?:@([\w.-]+))?$/.exec(text);
27
+ if (!m)
28
+ throw new RafError(`lot invalide : ${text} (projet:lot, lot, projet:lot@haiku ; @ suivi d'un alias ou d'un nom de modèle)`);
29
+ return { project: m[1], lot: m[2], model: m[3] };
30
+ }
31
+ /** `projet:lot=+200k`, `lot=+0.3M` (L223) : le « = » distingue le relèvement d'un lot du budget de la vague. */
32
+ function lotBudgetArg(text) {
33
+ const bad = () => new RafError(`--budget invalide : ${text} (projet:lot=+500k)`);
34
+ const m = /^([^=]+)=\+(.+)$/.exec(text);
35
+ if (!m)
36
+ throw bad();
37
+ let target;
38
+ let raise;
39
+ try {
40
+ target = lotArg(m[1]);
41
+ raise = parseBudget(m[2]);
42
+ }
43
+ catch {
44
+ throw bad();
45
+ }
46
+ if (target.model)
47
+ throw bad();
48
+ return { project: target.project, lot: target.lot, raise, text: m[2] };
49
+ }
50
+ export function parseOrchestrateArgs(argv) {
51
+ const a = { lots: [], lotBudgets: [], dryRun: false, waits: false, watch: false, continue: false, answers: [], drop: [], stopAfterCurrent: false, add: [], integrate: [], abandonCommits: false };
52
+ const optional = (i) => (argv[i + 1] !== undefined && !argv[i + 1].startsWith('-') && !LOT_LIKE.test(argv[i + 1]) ? argv[i + 1] : undefined);
53
+ for (let i = 0; i < argv.length; i++) {
54
+ const t = argv[i];
55
+ const value = (flag) => {
56
+ if (argv[i + 1] === undefined)
57
+ throw new RafError(`${flag} attend une valeur`);
58
+ return argv[++i];
59
+ };
60
+ if (t === '--dry-run')
61
+ a.dryRun = true;
62
+ else if (t === '--status' || t === '--resume') {
63
+ const v = optional(i);
64
+ if (v !== undefined)
65
+ i++;
66
+ a[t === '--status' ? 'status' : 'resume'] = v ?? true;
67
+ }
68
+ else if (t === '--waits')
69
+ a.waits = true;
70
+ else if (t === '--since') {
71
+ a.since = value(t);
72
+ if (!isDay(a.since))
73
+ throw new RafError(`--since invalide : ${a.since} (AAAA-MM-JJ)`);
74
+ }
75
+ else if (t === '--detail')
76
+ a.detail = value(t);
77
+ else if (t === '--watch')
78
+ a.watch = true;
79
+ else if (t === '--interval')
80
+ a.interval = parseMaxSessions(value(t), '--interval');
81
+ else if (t === '--budget') {
82
+ const v = value(t);
83
+ if (v.includes('='))
84
+ a.lotBudgets.push(lotBudgetArg(v));
85
+ else
86
+ a.budget = parseBudget(v);
87
+ }
88
+ else if (t === '--max-sessions')
89
+ a.maxSessions = parseMaxSessions(value(t));
90
+ else if (t === '--goal') {
91
+ // Un texte oublié ne doit pas avaler l'option suivante (`--goal --dry-run` lancerait une vraie vague payante).
92
+ const next = argv[i + 1];
93
+ if (next?.startsWith('--'))
94
+ throw new RafError(`--goal attend un texte, pas l'option ${next}`);
95
+ a.goal = value(t).trim();
96
+ if (a.goal === '')
97
+ throw new RafError('--goal attend un texte non vide');
98
+ }
99
+ else if (t === '--wave')
100
+ a.wave = value(t);
101
+ else if (t === '--drop')
102
+ a.drop.push(lotArg(value(t)));
103
+ else if (t === '--stop-after-current')
104
+ a.stopAfterCurrent = true;
105
+ else if (t === '--add')
106
+ a.add.push(lotArg(value(t)));
107
+ else if (t === '--integrate')
108
+ a.integrate.push(lotArg(value(t)));
109
+ else if (t === '--replay')
110
+ a.integrate.push({ ...lotArg(value(t)), replay: true });
111
+ else if (t === '--remove-worktree')
112
+ a.removeWorktree = lotArg(value(t));
113
+ else if (t === '--abandon-commits')
114
+ a.abandonCommits = true;
115
+ else if (t === '--continue')
116
+ a.continue = true;
117
+ else if (t === '--until')
118
+ a.until = value(t);
119
+ else if (t === '--priority')
120
+ a.priority = parsePriority(value(t));
121
+ else if (t === '--answer') {
122
+ const target = lotArg(value(t));
123
+ a.answers.push({ project: target.project, lot: target.lot, text: value(t) });
124
+ }
125
+ else if (t.startsWith('-'))
126
+ throw new RafError(`option inconnue : ${t}`);
127
+ else
128
+ a.lots.push(lotArg(t));
129
+ }
130
+ // L309 : --wave <id> après un --resume sans valeur désigne la vague à reprendre ; sans cela le --resume reprenait en silence une AUTRE vague.
131
+ if (a.resume !== undefined && a.wave !== undefined) {
132
+ if (a.resume === true)
133
+ a.resume = a.wave;
134
+ else if (a.resume !== a.wave)
135
+ throw new RafError(`--resume ${a.resume} et --wave ${a.wave} désignent deux vagues : une seule (${resumeCommand('<vague>')})`);
136
+ }
137
+ return a;
138
+ }