@sylad/cadence 0.9.0 → 0.10.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.
@@ -6,7 +6,7 @@
6
6
  {
7
7
  "name": "cadence",
8
8
  "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).",
9
- "version": "0.9.0",
9
+ "version": "0.10.1",
10
10
  "source": "./",
11
11
  "author": { "name": "Sylvain Ladoire" }
12
12
  }
@@ -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": "0.9.0",
4
+ "version": "0.10.1",
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
@@ -36,7 +36,8 @@ reports a page left empty or in error.
36
36
  touches is a plan file**: the plan itself, its Gantt page (only at its default place, `gantt.html` next to the plan — written elsewhere with `raf gantt -o`, declare it under `plan.files`), the `plan:` key of the config file in use (`cadence.yaml`, or
37
37
  the `--config` file; a change to `deliver:`/`session:` is work), or a file the
38
38
  project lists under `plan.files` in `cadence.yaml` (a page it generates from
39
- the plan, a journal). Such a commit is never a "commit without a lot", and it
39
+ the plan, a journal), or the QA expectations file (`docs/qa/expectations.md`,
40
+ or the file named by `qa.expectations`; the lead commits the `qa-reviewer`'s draft of it). Such a commit is never a "commit without a lot", and it
40
41
  does not count as work on the lots it cites: it is absent from `raf commits`,
41
42
  does not start a `todo` lot, does not make a code review stale, and
42
43
  `cadence deliver` does not announce the lots it cites as delivered. The files
@@ -274,8 +275,8 @@ The counterpart of the UX review, off by default. With the rule on, `raf done`
274
275
  refuses a lot that has at least one commit citing it and no recorded verdict
275
276
  (`--force` to override), and `raf check` reports such lots finished after the
276
277
  `reviewSince` day. A lot with no commit has nothing to review; neither does a
277
- lot whose only commits touch plan files alone (the plan, or a file listed under
278
- `plan.files`), predate the plan's `since` or match an `ignore:` pattern — `raf commits <id>` prints exactly the counted set.
278
+ lot whose only commits touch plan files alone (the plan, a file listed under
279
+ `plan.files`, or the QA expectations file), predate the plan's `since` or match an `ignore:` pattern — `raf commits <id>` prints exactly the counted set.
279
280
 
280
281
  The verdict is tied to what was reviewed: `raf review` stores it on the lot with
281
282
  the sha of the lot's latest counted commit (`review: { date, verdict, commit }`,
@@ -346,7 +347,7 @@ qa:
346
347
  expectations: docs/quality/pages.md
347
348
  ```
348
349
 
349
- Only the agent reads that key; the CLI does not use it. Without an expectations file the agent walks
350
+ The agent reads that key, and so does `raf check`: a commit touching only that file is plan upkeep (no lot to cite, see above). Without an expectations file the agent walks
350
351
  the routes it discovers and still runs its universal checks: an error shown, a failed API call
351
352
  whose content is missing on screen, a broken or missing content image are defects with or without a
352
353
  file; an empty 2xx body, like whatever else would need an expectation to judge, is suspect at most
@@ -558,7 +559,7 @@ no journal entry, nothing written.
558
559
  ```sh
559
560
  cadence verify # one pass, one line per check, then a one-line summary
560
561
  cadence verify --retry 60 # retry each failing check for up to 60 s (deliver's 300 s is not applied)
561
- cadence verify --sha 6b0d9aa # the sha that replaces ${SHA} / ${SHORT} (default: last delivery, else HEAD)
562
+ cadence verify --sha 6b0d9aa # the sha that replaces ${SHA} / ${SHORT} (default: see below)
562
563
  ```
563
564
 
564
565
  ```
@@ -570,6 +571,22 @@ verify : 1 effet rouge sur 2 vérifications
570
571
  Exit code: **0** every check green · **1** at least one red effect · **2** nothing
571
572
  to verify or invalid configuration.
572
573
 
574
+ **Which sha is expected.** By default `${SHA}` / `${SHORT}` is the last recorded
575
+ delivery (else `HEAD`). One case differs: a project with `ci: none` (the default) and
576
+ **neither** deploy commands (`deliver.deploy`) nor a delivery script — a host that
577
+ builds every push, like Cloudflare Pages — delivers by pushing, and every push counts,
578
+ plan-maintenance commits included. The expected sha is then the head of the tracked
579
+ upstream branch (`origin/<branch>`, as last fetched — `verify` does no network git),
580
+ **provided that upstream is the remote's production branch**: read without network from
581
+ `refs/remotes/origin/HEAD` (set by `git clone` or `git remote set-head origin <branch>`);
582
+ when that reference does not exist locally (repository created with `git init` + push),
583
+ `main` then `master` are taken as the production branch. A work branch pushed with `-u`
584
+ is not published by such a host, so it keeps the default rule. A project with `ci: github`
585
+ or a `ci.command` and no deploy also keeps the last delivery. When local `HEAD` is ahead
586
+ of the upstream, the report says so first — `2 commit(s) non poussé(s) — l'effet vérifié
587
+ est celui de origin/main` — so a red there reads "waiting for a push", not "broken
588
+ effect". `session start` uses the same rule; `--sha` always wins.
589
+
573
590
  Time limits differ from deliver's. `verify` runs all the checks **in parallel**,
574
591
  each try with the whole 120 s limit (a `url` request gives up after 20 s; a slow
575
592
  check is killed at its limit, "délai dépassé", and takes nothing from the others); results are printed in the order
@@ -635,6 +652,15 @@ you with the findings. A small lot (`estimate` ≤ 0.5 or `quickwin`) gets one s
635
652
  usability (code only when the lot is not `visible`). Failing tests (reported red, or red when
636
653
  `orchestrate.test` is run) go straight to a correction.
637
654
 
655
+ **A lot that already has commits** (a spec commit, an interrupted wave, a lot committed by hand, a lot sent back after a
656
+ review) still starts with the implementation session: its brief tells it to read the lot, its notes and its open
657
+ sub-tasks, which carry the findings of any earlier review. If that session ends without a new commit on a lot that
658
+ already has commits, the wave goes on to the review (warning `implement sans nouveau commit`) instead of handing the
659
+ lot back; with no commit on the lot at all it is handed back as `implement sans commit`. The same goes for a
660
+ correction pass: a `fix` session that adds no commit to a lot that already has work commits goes on to a fresh review
661
+ (warning `fix sans nouveau commit : revue lancée sur les commits du lot`), the pass still counted — the cap of two
662
+ correction passes stays the guard against a loop; with no commit on the lot it is handed back as `fix sans commit`.
663
+
638
664
  **Compliant with minor findings**: the minors are not left for a follow-up lot. One **minors pass** runs
639
665
  before concluding: a new Sonnet session with its own brief (`fix-minors.md`) fixes the minors that are right
640
666
  and lists in `choix`, with the reason, the ones it rejects (it never stops to ask); it does not count among the
@@ -646,7 +672,7 @@ committed, a short re-review of that commit runs instead): `ready`, verdict reco
646
672
  the untreated minors returned as proposals. Same when the budget is exhausted right after a compliant
647
673
  review with minors: it concludes on that review instead of staying suspended.
648
674
 
649
- **Choices, not questions**: the author brief tells the session to decide minor interpretation questions itself and to list them under `choix` in its report; the reviewer receives that list to re-read, and the final table prints each one (`choix fait : …`). A session stops with a question only on a real blocker (scope, architecture, costly to undo, not settled by the plan or CLAUDE.md).
675
+ **Choices, not questions**: the author brief tells the session to decide minor interpretation questions itself and to list them under `choix` in its report; the reviewer receives that list to re-read, and the final table prints each one (`choix fait : …`). A session stops with a question only on a real blocker: a decision that changes the scope or the architecture or is costly to undo, AND that the plan, its notes and CLAUDE.md do not settle; everything else is a choice.
650
676
 
651
677
  **What stays with you**: choosing the lots, the questions raised (`--resume --answer`), re-verifying
652
678
  after the wave (`git log`, tests, `raf check`), `raf done`, **`raf ux`** (the orchestrator reports the UX
@@ -700,7 +726,7 @@ orchestrate:
700
726
 
701
727
  Without `start`, a read-only plan's `todo` lot is refused (start it with the project's tool); without
702
728
  `verdict`, the review verdict stays in the wave's state and you report it. Only the plan's files
703
- (`plan.path`, `plan.files`) are committed from those commands; anything else dirty stops the lot.
729
+ (`plan.path`, `plan.files`, the QA expectations file) are committed from those commands; anything else dirty stops the lot.
704
730
 
705
731
  ## Claude Code skills
706
732
 
package/dist/audit.js CHANGED
@@ -12,7 +12,7 @@ function configRel(plan, root) {
12
12
  }
13
13
  /** Le plan, sa page Gantt, la configuration lue (cadence.yaml) et les fichiers tenus avec lui (plan.files). */
14
14
  function ownFiles(plan, root) {
15
- return new Set([relative(root, plan.path), relative(root, join(dirname(plan.path), 'gantt.html')), configRel(plan, root), ...plan.files]);
15
+ return new Set([relative(root, plan.path), relative(root, join(dirname(plan.path), 'gantt.html')), configRel(plan, root), ...plan.maintained]);
16
16
  }
17
17
  function withoutPlanKey(text) {
18
18
  if (text === null)
@@ -39,7 +39,7 @@ export function planCommits(plan, root, opts = {}) {
39
39
  }
40
40
  /**
41
41
  * Commit d'entretien du plan : TOUS ses fichiers sont des fichiers du plan (le plan, sa page Gantt,
42
- * plan.files — son plan publié par exemple — et la configuration lue, mais celle-ci seulement quand la
42
+ * plan.files — son plan publié par exemple —, les attendus QA et la configuration lue, mais celle-ci seulement quand la
43
43
  * clé `plan:` est la seule à changer : deliver/session/… sont du travail). Les fichiers décident,
44
44
  * jamais le sujet : « chore(plan): … » qui touche un fichier source est un commit comme un autre.
45
45
  */
package/dist/cli.js CHANGED
@@ -6,9 +6,9 @@ import { audit, exemptPlanOnly, isPlanOnly, lotWork, nextUp, planCommits, unrevi
6
6
  import { short } from './check.js';
7
7
  import { isDay, toDay } from './dates.js';
8
8
  import { deliver, parseDeliverConfig, realDeps } from './deliver.js';
9
- import { effectLines, realCheckDeps, verifyCommand } from './verify.js';
9
+ import { defaultTarget, effectLines, realCheckDeps, verifyCommand } from './verify.js';
10
10
  import { ganttData, renderGantt } from './gantt.js';
11
- import { gitRoot, headSha, readCommits, resolveCommit } from './git.js';
11
+ import { gitRoot, readCommits, resolveCommit } from './git.js';
12
12
  import { installHook } from './hook.js';
13
13
  import { citedRefs, linkCommits } from './link.js';
14
14
  import { buildNews, loadEntries, newEntry, newsData, newsIssues, stampEntries } from './news.js';
@@ -19,7 +19,7 @@ import { AGENTS_DIR, installAgents, installSkills, SKILLS_DIR } from './skills.j
19
19
  import { orchestrate, realOrchestrateDeps } from './orchestrate/command.js';
20
20
  import { activeLock, REPO_LOCK } from './orchestrate/lock.js';
21
21
  import { sessionClose, sessionStart } from './session.js';
22
- import { clearNext, lastDelivery, readNext, sharedStateDir, stateDir, writeNext } from './state.js';
22
+ import { clearNext, readNext, sharedStateDir, stateDir, writeNext } from './state.js';
23
23
  const HELP = `raf — plan « reste à faire » versionné dans le dépôt, relié aux commits
24
24
 
25
25
  raf init [--project nom] [--prefix L] [--no-hook]
@@ -47,7 +47,7 @@ Quand la portée du sujet cite des lots — « feat(L3): … », « chore(L31,L3
47
47
  mention en passage (« page équipe (L27) »), une plage (« L28–L31 », « L45 à L48 ») ou le corps n'y comptent pas.
48
48
  Sans portée citant un lot, tout le message est lu : « fix: L3 corrigé », « L3/t1 ».
49
49
  Un commit qui ne touche que le plan, la clé plan: de cadence.yaml (deliver/session sont du travail) (et les fichiers déclarés sous plan.files, un plan
50
- publié par exemple) n'a pas à en citer, et ne compte pas pour les lots qu'il cite ; le sujet n'y change rien.
50
+ publié par exemple, ainsi que le fichier d'attendus QA : docs/qa/expectations.md ou qa.expectations) n'a pas à en citer, et ne compte pas pour les lots qu'il cite ; le sujet n'y change rien.
51
51
  Un lot --visible attend une entrée Nouveautés (docs/nouveautes/, --dir) avec capture ; raf check le vérifie.
52
52
  Un texte qui commence par « - » se passe après « -- » : raf note L1 -- "-5 %".
53
53
  Le plan est docs/plan/raf.yaml, ou celui que nomme « plan: » dans cadence.yaml ; un plan tenu par un
@@ -310,10 +310,10 @@ function dispatch(argv, io) {
310
310
  if (!Number.isFinite(retry) || retry < 0)
311
311
  throw new RafError(`--retry invalide : ${values.retry} (secondes, 0 ou plus)`);
312
312
  const config = parseDeliverConfig(readFileSync(configPath, 'utf8'), configPath);
313
- const sha = values.sha ? (resolveCommit(root, values.sha) ?? '') : (lastDelivery(sharedStateDir(root)) ?? headSha(root) ?? '');
314
- if (values.sha && !sha)
313
+ const target = values.sha ? { sha: resolveCommit(root, values.sha) ?? '', note: null } : defaultTarget(root, config);
314
+ if (values.sha && !target.sha)
315
315
  throw new RafError(`--sha ${values.sha} : commit introuvable`);
316
- return verifyCommand({ config, sha, retry, out: io.out }, realCheckDeps(root));
316
+ return verifyCommand({ config, sha: target.sha, note: target.note, retry, out: io.out }, realCheckDeps(root));
317
317
  }
318
318
  case 'skills': {
319
319
  if (rest[0] !== 'install')
@@ -516,8 +516,8 @@ function morningEffects(configPath, root) {
516
516
  const config = parseDeliverConfig(text, configPath);
517
517
  if (config.verify.length === 0)
518
518
  return null;
519
- const sha = lastDelivery(sharedStateDir(root)) ?? headSha(root) ?? '';
520
- return effectLines(config, sha, realCheckDeps(root, { quiet: true }));
519
+ const target = defaultTarget(root, config);
520
+ return effectLines(config, target.sha, realCheckDeps(root, { quiet: true }), target.note);
521
521
  }
522
522
  catch (e) {
523
523
  return [`✗ ${e.message}`];
package/dist/config.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { existsSync, readFileSync } from 'node:fs';
2
+ import { dirname, isAbsolute, posix, relative } from 'node:path';
2
3
  import { parse } from 'yaml';
3
4
  import { isDay } from './dates.js';
4
5
  import { FIELDS, RafError, STATUSES } from './plan.js';
@@ -6,6 +7,29 @@ const KEYS = ['path', 'project', 'since', 'ignore', 'files', 'lots', 'fields', '
6
7
  const FORMAT_KEYS = ['lots', 'fields', 'statuses', 'estimates'];
7
8
  const isObject = (v) => !!v && typeof v === 'object' && !Array.isArray(v);
8
9
  const list = (v) => (v == null ? [] : Array.isArray(v) ? v.map(String) : [String(v)]);
10
+ /**
11
+ * Clé `qa:` : le fichier d'attendus QA, ramené à la forme que git rapporte (relatif à la racine du dépôt,
12
+ * séparateurs posix, sans `./`). Une valeur vide vaut absente ; un type faux ou un chemin hors du dépôt est refusé.
13
+ */
14
+ function readQa(qa, file) {
15
+ if (qa == null)
16
+ return undefined;
17
+ if (!isObject(qa))
18
+ throw new RafError(`${file} : qa doit être un objet`);
19
+ const v = qa.expectations;
20
+ if (v == null)
21
+ return undefined;
22
+ if (typeof v !== 'string')
23
+ throw new RafError(`${file} : qa.expectations doit être un chemin`);
24
+ const raw = v.trim().replace(/\\/g, '/');
25
+ if (raw === '')
26
+ return undefined;
27
+ const root = dirname(file);
28
+ const rel = posix.normalize(isAbsolute(raw) ? relative(root, raw).replace(/\\/g, '/') : raw);
29
+ if (rel === '.' || rel === '..' || rel.startsWith('../') || isAbsolute(rel))
30
+ throw new RafError(`${file} : qa.expectations « ${v.trim()} » est hors du dépôt`);
31
+ return rel;
32
+ }
9
33
  /**
10
34
  * Clé `plan:` de cadence.yaml : où est le plan et, s'il est tenu par un autre outil, comment le lire.
11
35
  * Null quand le fichier ou la clé manque — le plan est alors docs/plan/raf.yaml au format de raf.
@@ -20,18 +44,22 @@ export function readPlanConfig(file) {
20
44
  catch (e) {
21
45
  throw new RafError(`${file} illisible : ${e.message.split('\n')[0]}`);
22
46
  }
23
- const p = raw?.plan;
47
+ const doc = raw;
48
+ const qa = readQa(doc?.qa, file);
49
+ const p = doc?.plan;
24
50
  if (p == null)
25
- return null;
51
+ return qa ? { settings: { qaExpectations: qa } } : null;
26
52
  const bad = (what) => new RafError(`${file} : plan.${what}`);
27
53
  if (typeof p === 'string' && p.trim() !== '')
28
- return { path: p, settings: {} };
54
+ return { path: p, settings: { ...(qa ? { qaExpectations: qa } : {}) } };
29
55
  if (!isObject(p))
30
56
  throw new RafError(`${file} : plan doit être un chemin ou un objet`);
31
57
  for (const k of Object.keys(p))
32
58
  if (!KEYS.includes(k))
33
59
  throw bad(`${k} inconnu (attendu : ${KEYS.join(', ')})`);
34
60
  const settings = {};
61
+ if (qa)
62
+ settings.qaExpectations = qa;
35
63
  if (p.project != null)
36
64
  settings.project = String(p.project);
37
65
  if (p.since != null) {
package/dist/git.js CHANGED
@@ -88,6 +88,35 @@ export function repoStatus(cwd) {
88
88
  ahead,
89
89
  };
90
90
  }
91
+ /** Branche amont suivie (« origin/main ») et son sha tel que connu localement (sans réseau), null sans amont. */
92
+ export function upstreamHead(cwd) {
93
+ const ref = tryGit(cwd, ['rev-parse', '--abbrev-ref', '--symbolic-full-name', '@{u}']);
94
+ const sha = ref ? tryGit(cwd, ['rev-parse', '--verify', '-q', '@{u}']) : null;
95
+ return ref && sha ? { ref, sha } : null;
96
+ }
97
+ /**
98
+ * L'amont suivi est-il la branche de production du dépôt distant ? Lu sans réseau : `refs/remotes/<remote>/HEAD`
99
+ * (origin/HEAD, posé par clone ou `git remote set-head`). Absent (dépôt créé par init + push) : repli sur les
100
+ * noms usuels, main puis master.
101
+ */
102
+ export function upstreamIsDefaultBranch(cwd, upstreamRef) {
103
+ // Le remote vient de la config de la branche, pas d'une coupe de `@{u}` au premier « / » : un remote peut contenir un
104
+ // « / » (gh/origin) et un amont local (branche locale suivie) a pour remote « . » — jamais la branche de production.
105
+ const branch = tryGit(cwd, ['symbolic-ref', '-q', '--short', 'HEAD']);
106
+ if (!branch)
107
+ return false;
108
+ const remote = tryGit(cwd, ['config', '--get', `branch.${branch}.remote`]);
109
+ const merge = tryGit(cwd, ['config', '--get', `branch.${branch}.merge`]);
110
+ if (!remote || remote === '.' || !merge)
111
+ return false;
112
+ const upstreamBranch = merge.replace(/^refs\/heads\//, '');
113
+ if (upstreamRef !== `${remote}/${upstreamBranch}`)
114
+ return false;
115
+ const head = tryGit(cwd, ['symbolic-ref', '-q', '--short', `refs/remotes/${remote}/HEAD`]);
116
+ if (head)
117
+ return head === upstreamRef;
118
+ return upstreamBranch === 'main' || upstreamBranch === 'master';
119
+ }
91
120
  /** Le commit est-il contenu dans une branche distante connue localement ? */
92
121
  export function onRemote(cwd, sha) {
93
122
  return (tryGit(cwd, ['branch', '-r', '--contains', sha]) ?? '').length > 0;
@@ -72,7 +72,7 @@ function trackedPaths(s) {
72
72
  async function commitPlan(c, message) {
73
73
  const repo = c.lot.repo;
74
74
  const plan = c.loadPlan();
75
- const own = new Set([relative(repo, plan.path), ...plan.files]);
75
+ const own = new Set([relative(repo, plan.path), ...plan.maintained]);
76
76
  const dirty = trackedPaths(await snapshot(repo, { remote: false }));
77
77
  if (dirty.length === 0)
78
78
  return null;
@@ -454,8 +454,9 @@ async function work(c, kind) {
454
454
  stop(c, 'handed-back', 'passe des mineurs sans commit, mais aucune revue conforme gardée');
455
455
  return;
456
456
  }
457
- // Après une réponse du lead, la session peut n'avoir plus rien à commiter : le travail du lot est déjà dans git.
458
- const alreadyDone = kind === 'implement' && l.answers.length > 0 && lotWork(plan, l.repo, l.lot).length > 0;
457
+ // La session peut n'avoir plus rien à commiter (réponse du lead, lot commité avant la vague) : le travail du lot est déjà dans git.
458
+ // Même règle côté fix (L56) : la passe n'avait rien à ajouter, le lot a déjà ses commits de travail → revue neuve ; la passe reste consommée, MAX_PASSES borne la boucle.
459
+ const alreadyDone = (kind === 'implement' || kind === 'fix') && lotWork(plan, l.repo, l.lot).length > 0;
459
460
  if (commits.length === 0 && !alreadyDone && !headMoved) {
460
461
  stop(c, 'handed-back', `${kind} sans commit`);
461
462
  return;
@@ -463,7 +464,7 @@ async function work(c, kind) {
463
464
  if (headMoved)
464
465
  l.warnings.push(`${minorsPass ? 'passe des mineurs' : 'correction'} reprise sans nouveau commit : un commit d'une session précédente est relu par la revue`);
465
466
  else if (commits.length === 0)
466
- l.warnings.push(`${kind} sans nouveau commit : revue lancée sur les commits déjà faits du lot`);
467
+ l.warnings.push(kind === 'fix' ? 'fix sans nouveau commit : revue lancée sur les commits du lot' : `${kind} sans nouveau commit : revue lancée sur les commits déjà faits du lot`);
467
468
  const red = [];
468
469
  if (!rep.tests.vert)
469
470
  red.push({ source: 'tests', gravite: 'bloquant', texte: `tests annoncés rouges par la session : ${rep.tests.commande} — ${rep.tests.resultat}` });
package/dist/plan.js CHANGED
@@ -130,6 +130,14 @@ export class Plan {
130
130
  get files() {
131
131
  return this.settings.files ?? [];
132
132
  }
133
+ /** Fichier d'attendus QA du `qa-reviewer`, relatif à la racine du dépôt. */
134
+ get qaExpectations() {
135
+ return this.settings.qaExpectations ?? 'docs/qa/expectations.md';
136
+ }
137
+ /** Tout ce qui est tenu avec le plan sans être du travail : plan.files et le fichier d'attendus QA. Source unique de l'audit et de l'orchestrateur. */
138
+ get maintained() {
139
+ return [...this.files, this.qaExpectations];
140
+ }
133
141
  /**
134
142
  * Références citées par un texte. Format de raf : préfixe et numéro (`L3`, `L3/t1`). Autre format :
135
143
  * les identifiants du plan eux-mêmes, quelle que soit leur forme (`E-A2`, `NC2.4`, `B33/t1-fusion`).
package/dist/verify.js CHANGED
@@ -1,5 +1,7 @@
1
1
  import { spawn } from 'node:child_process';
2
2
  import { constants } from 'node:os';
3
+ import { headSha, repoStatus, upstreamHead, upstreamIsDefaultBranch } from './git.js';
4
+ import { lastDelivery, sharedStateDir } from './state.js';
3
5
  import { onTermination } from './proc.js';
4
6
  import { describeCheck, realDeps, retryCheck, TIMED_OUT } from './deliver.js';
5
7
  function killGroup(pgid, sig) {
@@ -73,6 +75,28 @@ export function summaryLine(results) {
73
75
  export function verifyEnv(sha) {
74
76
  return { CADENCE_SHA: sha, CADENCE_SHORT: sha.slice(0, 7), CADENCE_BRANCH: '' };
75
77
  }
78
+ /**
79
+ * Sha attendu par les vérifications (${SHA} / ${SHORT}) et note éventuelle pour le compte rendu.
80
+ * - Par défaut : la dernière livraison, à défaut la tête (inchangé).
81
+ * - `ci: none` SANS commande ni script de livraison : la livraison EST le push (Cloudflare Pages construit chaque
82
+ * push, plan compris), donc la tête de la branche amont suivie (état local de la référence, aucun réseau) —
83
+ * mais seulement si cet amont est la branche de production du dépôt distant : une branche de travail poussée
84
+ * avec -u n'est pas publiée, elle garde la règle par défaut. Des commits locaux non poussés ne changent pas
85
+ * ce sha mais sont dits dans la note.
86
+ * - Sans amont : règle par défaut.
87
+ */
88
+ export function expectedTarget(root, config, last) {
89
+ const fallback = last ?? headSha(root) ?? '';
90
+ if (config.script !== undefined || config.deploy.length > 0 || config.ci !== 'none')
91
+ return { sha: fallback, note: null };
92
+ const up = upstreamHead(root);
93
+ if (!up || !upstreamIsDefaultBranch(root, up.ref))
94
+ return { sha: fallback, note: null };
95
+ const ahead = repoStatus(root).ahead;
96
+ return { sha: up.sha, note: ahead > 0 ? `${ahead} commit(s) non poussé(s) — l'effet vérifié est celui de ${up.ref}` : null };
97
+ }
98
+ /** Sha attendu de la reprise et de `cadence verify` sans --sha. */
99
+ export const defaultTarget = (root, config) => expectedTarget(root, config, lastDelivery(sharedStateDir(root)));
76
100
  /** Délai de chaque essai d'une vérification de `cadence verify`. */
77
101
  export const VERIFY_ATTEMPT_MS = 120_000;
78
102
  /** `cadence verify` : 0 tout vert, 1 un effet rouge, 2 rien à vérifier. */
@@ -85,6 +109,8 @@ export async function verifyCommand(ctx, deps) {
85
109
  : 'verify : aucune vérification déclarée (deliver.verify)');
86
110
  return 2;
87
111
  }
112
+ if (ctx.note)
113
+ out(ctx.note);
88
114
  const results = await replayChecks(config.verify, deps, ctx.sha, verifyEnv(ctx.sha), { retryMs: ctx.retry * 1000, attemptMs: VERIFY_ATTEMPT_MS });
89
115
  for (const r of results)
90
116
  out(resultLine(r));
@@ -100,15 +126,16 @@ export const MORNING_BUDGET_MS = 10_000;
100
126
  * Lignes « Effets en production » du rapport de reprise : un seul essai par vérification, borné. Rien à dire
101
127
  * (liste vide) pour un projet sans verify ; ne lève jamais — c'est un fait de plus, pas une condition.
102
128
  */
103
- export async function effectLines(config, sha, deps) {
129
+ export async function effectLines(config, sha, deps, note = null) {
104
130
  if (config.verify.length === 0)
105
131
  return [];
132
+ const head = note ? [note] : [];
106
133
  try {
107
134
  const results = await replayChecks(config.verify, deps, sha, verifyEnv(sha), { retryMs: 0, attemptMs: MORNING_BUDGET_MS });
108
135
  const shown = results.filter((r) => r.reason !== null);
109
- return shown.length === 0 ? [`✓ ${summaryLine(results)}`] : [...shown.map(resultLine), summaryLine(results)];
136
+ return shown.length === 0 ? [...head, `✓ ${summaryLine(results)}`] : [...head, ...shown.map(resultLine), summaryLine(results)];
110
137
  }
111
138
  catch (e) {
112
- return [`✗ verify : ${e.message}`];
139
+ return [...head, `✗ verify : ${e.message}`];
113
140
  }
114
141
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sylad/cadence",
3
- "version": "0.9.0",
3
+ "version": "0.10.1",
4
4
  "description": "A small, repo-native working method: a versioned plan linked to your commits, a changelog with screenshots, session rituals and deliveries proven by their effect.",
5
5
  "license": "MIT",
6
6
  "author": "Sylvain Ladoire",
@@ -13,6 +13,10 @@ deliver, run `raf done`, `raf ux` or `raf review`. Read the project's CLAUDE.md
13
13
  Do not stop to ask. A finding that is wrong, or not worth its change, is not fixed: list it under
14
14
  "choix" in your report with the reason — the reviewer re-reads it and the lead sees it. Leave
15
15
  "questions" empty. Making no commit at all is a valid outcome when every finding is rejected.
16
+ Minor choices to settle yourself, writing the alternative you did not take:
17
+ a spacing, colour or label value when a measurement or a rule justifies it; the URL of a link (the most general official page if a precise one is not certain);
18
+ a News entry: only if a finding asks for it; a written rule of the project's CLAUDE.md (apply it); who launches the review or UX pass (never the session: the program and the lead do).
19
+ "not my job to touch the plan" is not a question: say it in the report.
16
20
  You are one short session of an orchestrated wave: do not launch subagents (the Agent tool is disabled).
17
21
  Give your final report as the structured output (commits, tests, build, what you could not verify, choices).
18
22
  {{reponse}}
@@ -10,7 +10,12 @@ Rules: test first; commit each fix as soon as its tests pass, with explicit path
10
10
  `git add -A` or `commit -a`), and a message that cites the lot (`fix({{lot}}): …`); run the project's
11
11
  full test suite and build before reporting; do not push, deliver, run `raf done`, `raf ux` or
12
12
  `raf review`. Read the project's CLAUDE.md first.
13
- If a finding is wrong, or fixing it needs a decision, stop and report the question instead of guessing.
13
+ If a finding is wrong, say so under "choix" with the reason. Ask (under "questions") only when a fix needs a decision that passes the rule below.
14
+ A question is legitimate only if it names what it would change: the scope of the lot, the architecture, or a costly rollback (data, production, public API);
15
+ otherwise it is a choice: decide, write the alternative you did not take, go on. Minor choices to settle yourself:
16
+ a spacing, colour or label value when a measurement or a rule justifies it; the URL of a link (the most general official page if a precise one is not certain);
17
+ a News entry: only if a finding asks for it; a written rule of the project's CLAUDE.md (apply it); who launches the review or UX pass (never the session: the program and the lead do).
18
+ "not my job to touch the plan" is not a question: say it in the report.
14
19
  You are one short session of an orchestrated wave: do not launch subagents (the Agent tool is disabled).
15
- Give your final report as the structured output (commits, tests, build, what you could not verify, questions).
20
+ Give your final report as the structured output (commits, tests, build, what you could not verify, choices, questions).
16
21
  {{reponse}}
@@ -7,9 +7,14 @@ full test suite and build before reporting; do not push, deliver, run `raf done`
7
7
  `raf review`.
8
8
  Decide minor interpretation questions yourself (the wording of a message, a name, a default, the
9
9
  reading of an ambiguous line of the lot) and list each one under "choix" in your report, with the
10
- alternative you did not take — the reviewer re-reads them. Stop and ask (under "questions") only on a real blocker:
11
- a decision that changes the scope or the architecture, is costly to undo, or that the plan, its notes
12
- and the project's CLAUDE.md do not settle.
10
+ alternative you did not take — the reviewer re-reads them. Stop and ask (under "questions") only on a real blocker.
11
+ A question is legitimate only if it names what it would change: the scope of the lot, the architecture, or a costly rollback (data, production, public API), AND the plan, its notes and the project's CLAUDE.md do not settle it;
12
+ otherwise it is a choice: decide, write the alternative you did not take, go on. Minor choices to settle yourself:
13
+ a spacing, colour or label value when a measurement or a rule justifies it; the URL of a link (the most general official page if a precise one is not certain);
14
+ a News entry for a visible lot (yes, always); a written rule of the project's CLAUDE.md (apply it); who launches the review or UX pass (never the session: the program and the lead do).
15
+ "not my job to touch the plan" is not a question: say it in the report.
16
+ If the lot's commits already cover its open sub-tasks, do not ask whether to continue: make no commit and say so
17
+ in your report (the review that follows re-reads those commits). If they do not cover them all, implement the rest.
13
18
  Report: commits (sha + subject), tests and build results with their numbers, what you could not
14
19
  verify, choices made, open questions.
15
20