@sylad/cadence 0.9.0 → 0.10.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.
@@ -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.0",
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.0",
5
5
  "author": { "name": "Sylvain Ladoire" },
6
6
  "homepage": "https://github.com/Sylad/cadence",
7
7
  "repository": "https://github.com/Sylad/cadence",
package/README.md CHANGED
@@ -558,7 +558,7 @@ no journal entry, nothing written.
558
558
  ```sh
559
559
  cadence verify # one pass, one line per check, then a one-line summary
560
560
  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)
561
+ cadence verify --sha 6b0d9aa # the sha that replaces ${SHA} / ${SHORT} (default: see below)
562
562
  ```
563
563
 
564
564
  ```
@@ -570,6 +570,22 @@ verify : 1 effet rouge sur 2 vérifications
570
570
  Exit code: **0** every check green · **1** at least one red effect · **2** nothing
571
571
  to verify or invalid configuration.
572
572
 
573
+ **Which sha is expected.** By default `${SHA}` / `${SHORT}` is the last recorded
574
+ delivery (else `HEAD`). One case differs: a project with `ci: none` (the default) and
575
+ **neither** deploy commands (`deliver.deploy`) nor a delivery script — a host that
576
+ builds every push, like Cloudflare Pages — delivers by pushing, and every push counts,
577
+ plan-maintenance commits included. The expected sha is then the head of the tracked
578
+ upstream branch (`origin/<branch>`, as last fetched — `verify` does no network git),
579
+ **provided that upstream is the remote's production branch**: read without network from
580
+ `refs/remotes/origin/HEAD` (set by `git clone` or `git remote set-head origin <branch>`);
581
+ when that reference does not exist locally (repository created with `git init` + push),
582
+ `main` then `master` are taken as the production branch. A work branch pushed with `-u`
583
+ is not published by such a host, so it keeps the default rule. A project with `ci: github`
584
+ or a `ci.command` and no deploy also keeps the last delivery. When local `HEAD` is ahead
585
+ of the upstream, the report says so first — `2 commit(s) non poussé(s) — l'effet vérifié
586
+ est celui de origin/main` — so a red there reads "waiting for a push", not "broken
587
+ effect". `session start` uses the same rule; `--sha` always wins.
588
+
573
589
  Time limits differ from deliver's. `verify` runs all the checks **in parallel**,
574
590
  each try with the whole 120 s limit (a `url` request gives up after 20 s; a slow
575
591
  check is killed at its limit, "délai dépassé", and takes nothing from the others); results are printed in the order
@@ -635,6 +651,15 @@ you with the findings. A small lot (`estimate` ≤ 0.5 or `quickwin`) gets one s
635
651
  usability (code only when the lot is not `visible`). Failing tests (reported red, or red when
636
652
  `orchestrate.test` is run) go straight to a correction.
637
653
 
654
+ **A lot that already has commits** (a spec commit, an interrupted wave, a lot committed by hand, a lot sent back after a
655
+ review) still starts with the implementation session: its brief tells it to read the lot, its notes and its open
656
+ sub-tasks, which carry the findings of any earlier review. If that session ends without a new commit on a lot that
657
+ already has commits, the wave goes on to the review (warning `implement sans nouveau commit`) instead of handing the
658
+ lot back; with no commit on the lot at all it is handed back as `implement sans commit`. The same goes for a
659
+ correction pass: a `fix` session that adds no commit to a lot that already has work commits goes on to a fresh review
660
+ (warning `fix sans nouveau commit : revue lancée sur les commits du lot`), the pass still counted — the cap of two
661
+ correction passes stays the guard against a loop; with no commit on the lot it is handed back as `fix sans commit`.
662
+
638
663
  **Compliant with minor findings**: the minors are not left for a follow-up lot. One **minors pass** runs
639
664
  before concluding: a new Sonnet session with its own brief (`fix-minors.md`) fixes the minors that are right
640
665
  and lists in `choix`, with the reason, the ones it rejects (it never stops to ask); it does not count among the
@@ -646,7 +671,7 @@ committed, a short re-review of that commit runs instead): `ready`, verdict reco
646
671
  the untreated minors returned as proposals. Same when the budget is exhausted right after a compliant
647
672
  review with minors: it concludes on that review instead of staying suspended.
648
673
 
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).
674
+ **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
675
 
651
676
  **What stays with you**: choosing the lots, the questions raised (`--resume --answer`), re-verifying
652
677
  after the wave (`git log`, tests, `raf check`), `raf done`, **`raf ux`** (the orchestrator reports the UX
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]
@@ -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/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;
@@ -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/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.0",
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