@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.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +33 -7
- package/dist/audit.js +2 -2
- package/dist/cli.js +9 -9
- package/dist/config.js +31 -3
- package/dist/git.js +29 -0
- package/dist/orchestrate/cycle.js +5 -4
- package/dist/plan.js +8 -0
- package/dist/verify.js +30 -3
- package/package.json +1 -1
- package/templates/orchestrate/fix-minors.md +4 -0
- package/templates/orchestrate/fix.md +7 -2
- package/templates/orchestrate/implement.md +8 -3
|
@@ -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
|
+
"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.
|
|
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)
|
|
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,
|
|
278
|
-
`plan.files
|
|
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
|
-
|
|
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:
|
|
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
|
|
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
|
|
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.
|
|
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
|
|
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,
|
|
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,
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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
|
-
//
|
|
458
|
-
|
|
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.
|
|
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,
|
|
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
|
-
|
|
12
|
-
|
|
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
|
|