@sylad/cadence 0.16.0 → 0.18.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": "0.16.0",
11
+ "version": "0.18.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": "0.16.0",
4
+ "version": "0.18.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
@@ -24,7 +24,7 @@ Code prompt that shows the session's context, quota, cost and the orchestrate wa
24
24
 
25
25
  ## What's new
26
26
 
27
- **0.16.0**: a lot can declare **neighbouring repositories** (`repos:` key of the lot, `{ path, cite }` for a repository shared between projects): `cadence orchestrate` refuses, locks and guards them, gives them to every session as `--add-dir`, and `raf commits`, `raf show` and the review verdict read the commits that cite the lot there too. **0.15.0**: `hook.autostart` in `cadence.yaml` (`warn` by default, `refuse` or `start`): a pre-commit hook that refuses, or starts in the same commit, a lot still todo that the commit cites; `cadence session start|close --all [--depth n]`, one section per project under a folder. **0.14.0**: `cadence lead tour`, the lead's morning table of every project in a folder, printed by a program (one line per project: lots in progress, gaps, last notes, next ready lot, repository) in place of one subagent per project; the `lead` skill calls it. **0.13.1**: `cadence-hud` takes back the segments that fit once a big one has dropped (reset times, then the per-model consumption, now short: `opus 1.2M sonnet 800k`); `CADENCE_HUD_AMBIGUOUS=1` for a terminal that draws `▰ ▱ ⚙ │ ↻` in one cell. **0.13.0**: `cadence orchestrate --status --watch` (a wave's table redrawn in the terminal, which
27
+ **0.18.0**: `cadence session next` prints what it records (issue #6); `cadence session close` covers the period since the last `session start` (or the last close) and says so in its title, `--since` still wins (issue #7). **0.17.0**: `cadence-hud` shows an `N cmd` segment next to the agents, the background commands of the session (Bash run in the background and Monitor, subagents' included), tracked from the tool results and ended by the task's notification or `TaskStop`. **0.16.0**: a lot can declare **neighbouring repositories** (`repos:` key of the lot, `{ path, cite }` for a repository shared between projects): `cadence orchestrate` refuses, locks and guards them, gives them to every session as `--add-dir`, and `raf commits`, `raf show` and the review verdict read the commits that cite the lot there too. **0.15.0**: `hook.autostart` in `cadence.yaml` (`warn` by default, `refuse` or `start`): a pre-commit hook that refuses, or starts in the same commit, a lot still todo that the commit cites; `cadence session start|close --all [--depth n]`, one section per project under a folder. **0.14.0**: `cadence lead tour`, the lead's morning table of every project in a folder, printed by a program (one line per project: lots in progress, gaps, last notes, next ready lot, repository) in place of one subagent per project; the `lead` skill calls it. **0.13.1**: `cadence-hud` takes back the segments that fit once a big one has dropped (reset times, then the per-model consumption, now short: `opus 1.2M sonnet 800k`); `CADENCE_HUD_AMBIGUOUS=1` for a terminal that draws `▰ ▱ ⚙ │ ↻` in one cell. **0.13.0**: `cadence orchestrate --status --watch` (a wave's table redrawn in the terminal, which
28
28
  stops with the wave) and the lead skill that names the wave and follows its journal; lot ids with `/`
29
29
  (sub-tasks of a read-only plan) accepted by `orchestrate`; `cadence-hud`'s first line measured in
30
30
  terminal cells and kept to ASCII; `publish.yml` on the v5 actions. **0.12.0**: the `cadence-hud` plugin (a status band above the Claude Code prompt),
@@ -414,9 +414,10 @@ URL. The agent then returns a draft for you to correct — it never writes the f
414
414
  cadence session start # notes from the last close, lots in progress (silent ones flagged),
415
415
  # work done since yesterday by lot, drift, repo state, 3 proposals
416
416
  cadence session start --since "3 days ago" --idle 2
417
- cadence session close # today's commits by lot, commits without a lot, lots in progress
418
- # with no commit today, drift, uncommitted / unpushed work
419
- # exit 1 while something is still open
417
+ cadence session close # commits by lot since the last `session start` (or last successful close),
418
+ # commits without a lot, lots in progress with no commit in that window,
419
+ # drift, uncommitted / unpushed work; exit 1 while something is still open
420
+ cadence session close --since "2 days ago" # --since overrides the window
420
421
  cadence session start --all # from a folder that is not a repository: one "## project" section per
421
422
  cadence session close --all --depth 2 # sub-folder holding a plan (--depth n: n levels down, 1 by default);
422
423
  # exit = the worst code of the projects (2 error, 1 open, 0 closed)
@@ -424,6 +425,17 @@ cadence session next "finish L3" "review L4" # shown by the next session star
424
425
  cadence session next --clear # erase those notes, on purpose
425
426
  ```
426
427
 
428
+ `session start` records its time in the worktree's state (`session.json`, under the git dir), and a
429
+ `session close` that ends with `✓ prêt à fermer` records its own. The next close takes the later one
430
+ as its window and names it in the section title: `Commits de la période (depuis l'ouverture de 09:12)`
431
+ (`depuis la clôture de …`; with the date when it is not today). With no recorded time, or an unreadable
432
+ file, the window is the current day, said as such (`depuis 2026-09-28 00:00`); `--since` is quoted as given.
433
+ A refused close (exit 1) leaves the boundary alone.
434
+
435
+ On success `cadence session next` prints what it recorded (`2 lignes enregistrées pour la prochaine
436
+ ouverture`, then the lines as written, blank ones dropped), so `session-close` can report
437
+ them without running `session start` again.
438
+
427
439
  `--all` runs the same command inside each project found under the current folder —
428
440
  `docs/plan/raf.yaml`, or a `cadence.yaml` with `plan:` — and never enters a project
429
441
  to look for another. `--since` and `--idle` go to every project; an error in one
@@ -492,8 +504,8 @@ An entry is proposed only if it could be measured entirely. Never proposed:
492
504
  The command deletes nothing, never fails on the cleanup and does not change the exit code: the
493
505
  `session-close` skill shows the list and removes the entries only after the human agrees.
494
506
 
495
- The commands get `CADENCE_SINCE` (the `--since` in effect) and `CADENCE_TODAY`. They
496
- add facts and decide nothing: a failing command is reported and changes neither
507
+ The commands get `CADENCE_SINCE` and `CADENCE_TODAY`. `CADENCE_SINCE` is the `--since` as given; on `session close` without `--since` it is the instant of the last open or successful close as an ISO 8601 UTC string (`2026-09-28T07:12:00.000Z`), or `<today> 00:00` when no mark exists.
508
+ The commands add facts and decide nothing: a failing command is reported and changes neither
497
509
  the exit code nor the verdict.
498
510
 
499
511
  ## lead tour
@@ -1100,14 +1112,14 @@ repository with `cadence skills install` (to `.claude/skills/cadence-*` and
1100
1112
  A band above the Claude Code prompt (terminal and desktop app), refreshed every 5 s:
1101
1113
 
1102
1114
  ```
1103
- ctx ▰▰▰▰▱▱▱▱▱▱ 42 % 84k/200k │ 5h 23 % ↻ 2 h 10 │ 7j 61 % ↻ 6 j 15 h │ $1.23 │ fable 410k sonnet 85k │ ⚙ 2 agents
1115
+ ctx ▰▰▰▰▱▱▱▱▱▱ 42 % 84k/200k │ 5h 23 % ↻ 2 h 10 │ 7j 61 % ↻ 6 j 15 h │ $1.23 │ fable 410k sonnet 85k │ ⚙ 2 agents │ 2 cmd
1104
1116
  ⟳ cadence · 2026-10-07-2131 en cours │ budget ▱▱▱▱▱▱▱▱▱▱ 1 % 14k/2M │ 1 session/2
1105
1117
  L112 implémente implement@sonnet 34 s global-setup coupe le cache de compilation de Node...
1106
1118
  ```
1107
1119
 
1108
1120
  Context of the session (green / orange / red at 50 and 75 %), the 5-hour and 7-day quota
1109
1121
  windows with the time to their reset, the cost of the session and its split per model, the
1110
- subagents of this session, and every live `cadence orchestrate` wave: budget, sessions, one
1122
+ subagents of this session, the background commands in progress (`2 cmd`: Bash run in the background and Monitor, subagents' included, as the app's "background commands" card counts them), and every live `cadence orchestrate` wave: budget, sessions, one
1111
1123
  aligned line per active lot (status, step, model, elapsed, start of the title), the lots still
1112
1124
  waiting. When no wave is running, the last finished one stays on a grey line until the next
1113
1125
  starts. Waves are read from disk (`~/.cadence/orchestrate/waves/`, then `.cadence/runs/`); no
package/dist/cli.js CHANGED
@@ -407,7 +407,7 @@ function dispatch(argv, io) {
407
407
  const sconf = rest[0] === 'start' || rest[0] === 'close' ? readSessionConfig(configPath) : {};
408
408
  const facts = rest[0] === 'start' || rest[0] === 'close' ? sconf[rest[0]] : undefined;
409
409
  const clean = rest[0] === 'close' && sconf.clean ? { patterns: sconf.clean, days: sconf.cleanDays } : undefined;
410
- const sctx = { plan: loadPlan(), root, newsDir, state: stateDir(root), shared: sharedStateDir(root), today, out: io.out, facts, clean };
410
+ const sctx = { plan: loadPlan(), root, newsDir, state: stateDir(root), shared: sharedStateDir(root), today, now: io.now(), out: io.out, facts, clean };
411
411
  if (rest[0] !== 'start')
412
412
  return session(rest, sctx, values);
413
413
  // La reprise rejoue les vérifications d'effet (un essai, borné) : un effet rouge d'hier se lit avec les faits du matin.
@@ -773,7 +773,7 @@ function session([sub, ...args], ctx, values) {
773
773
  return sessionStart(ctx, { since: values.since ?? '24 hours ago', idle });
774
774
  }
775
775
  case 'close':
776
- return sessionClose(ctx, { since: values.since ?? `${ctx.today} 00:00` });
776
+ return sessionClose(ctx, { since: values.since });
777
777
  case 'next': {
778
778
  const lines = args.filter((l) => l.trim() !== '');
779
779
  if (values.clear) {
@@ -791,6 +791,10 @@ function session([sub, ...args], ctx, values) {
791
791
  'usage : cadence session next "ligne" … (pour effacer les notes exprès : cadence session next --clear)');
792
792
  }
793
793
  writeNext(ctx.state, ctx.today, lines);
794
+ // Rien sur stdout laissait croire à un échec : on redit les lignes telles qu'enregistrées, pour que
795
+ // session-close les rapporte sans relancer session start.
796
+ const n = lines.length;
797
+ ctx.out(`${n} ligne${n > 1 ? 's' : ''} enregistrée${n > 1 ? 's' : ''} pour la prochaine ouverture\n${lines.map((l) => ` - ${l}`).join('\n')}`);
794
798
  return 0;
795
799
  }
796
800
  default:
package/dist/session.js CHANGED
@@ -5,7 +5,7 @@ import { audit, exemptPlanOnly, nextUp, planCommits } from './audit.js';
5
5
  import { diffDays, maxDay } from './dates.js';
6
6
  import { repoStatus } from './git.js';
7
7
  import { linkCommits } from './link.js';
8
- import { lockAlive, readLock, readNext } from './state.js';
8
+ import { lockAlive, readLock, readNext, readSessionMark, writeSessionMark } from './state.js';
9
9
  const FACTS_TIMEOUT = 120_000;
10
10
  /**
11
11
  * Sortie de la commande du projet, ligne à ligne. Un échec se dit et ne bloque rien : ce sont des
@@ -81,6 +81,23 @@ export function lastActivity(lot, commits) {
81
81
  const days = [commits[0]?.day, lot.started, ...lot.notes.map((n) => n.date)].filter((d) => !!d);
82
82
  return days.length ? days.reduce(maxDay) : undefined;
83
83
  }
84
+ const hhmm = (d) => `${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}`;
85
+ /**
86
+ * Fenêtre de la clôture : --since s'il est donné ; sinon depuis la dernière ouverture (ou clôture réussie) du
87
+ * projet, dite avec son heure ; sinon le jour même.
88
+ */
89
+ function closeWindow(ctx, since) {
90
+ if (since !== undefined)
91
+ return { since, label: `depuis ${since}` };
92
+ const mark = readSessionMark(ctx.state);
93
+ if (!mark)
94
+ return { since: `${ctx.today} 00:00`, label: `depuis ${ctx.today} 00:00` };
95
+ const at = new Date(mark.at);
96
+ const what = mark.kind === 'start' ? "l'ouverture" : 'la clôture';
97
+ const sameDay = at.toDateString() === (ctx.now ?? new Date()).toDateString();
98
+ const when = sameDay ? `de ${hhmm(at)}` : `du ${at.getFullYear()}-${String(at.getMonth() + 1).padStart(2, '0')}-${String(at.getDate()).padStart(2, '0')} à ${hhmm(at)}`;
99
+ return { since: mark.at, label: `depuis ${what} ${when}` };
100
+ }
84
101
  export function sessionStart(ctx, opts) {
85
102
  const { plan, out, today } = ctx;
86
103
  const lots = plan.lots();
@@ -117,17 +134,20 @@ export function sessionStart(ctx, opts) {
117
134
  ...ready.map((l) => ({ l, why: l.quickwin ? 'gain rapide prêt' : 'prêt' })),
118
135
  ].slice(0, 3);
119
136
  section(out, 'Propositions', proposals.map(({ l, why }, i) => `${i + 1}. ${l.quickwin ? '⚡ ' : ''}${l.id} ${l.title} — ${why}`));
137
+ // La clôture du soir part de cette heure : elle ne relit pas ce que l'ouverture vient de rapporter.
138
+ writeSessionMark(ctx.state, { kind: 'start', at: (ctx.now ?? new Date()).toISOString() });
120
139
  return 0;
121
140
  }
122
141
  export function sessionClose(ctx, opts) {
123
142
  const { plan, out, today } = ctx;
124
143
  out(`${plan.project} — clôture du ${today}`);
125
- const recent = period(ctx, opts.since);
144
+ const window = closeWindow(ctx, opts.since);
145
+ const recent = period(ctx, window.since);
126
146
  const commits = byLotLines(recent);
127
147
  if (recent.orphans.length) {
128
148
  commits.push(`${recent.orphans.length} commit(s) sans lot :`, ...recent.orphans.map((c) => ` ${short(c)}`));
129
149
  }
130
- section(out, 'Commits de la période', commits.length ? commits : ['(aucun)']);
150
+ section(out, `Commits de la période (${window.label})`, commits.length ? commits : ['(aucun)']);
131
151
  const quiet = plan.lots().filter((l) => l.status === 'doing' && !isRecurring(l) && !recent.byLot.has(l.id));
132
152
  section(out, 'Lots en cours', quiet.map((l) => `${l.id} ${l.title} — aucun commit sur la période : ${plan.readonly ? "le fermer ou l'annoter avec l'outil du projet" : 'raf done ou raf note'}`));
133
153
  const issues = audit(plan, ctx.root, ctx.newsDir, today);
@@ -135,7 +155,7 @@ export function sessionClose(ctx, opts) {
135
155
  const repo = repoLine(ctx.root);
136
156
  const lock = lockStatus(ctx.shared);
137
157
  section(out, 'Dépôt', [repo.line, ...(lock ? [lock.line] : [])]);
138
- section(out, 'Faits propres au projet', projectFacts(ctx, opts.since));
158
+ section(out, 'Faits propres au projet', projectFacts(ctx, window.since));
139
159
  // Une proposition, pas une condition : rien n'est supprimé ici, le skill demande l'accord. Le
140
160
  // nettoyage ne change jamais le verdict et ne fait jamais tomber la clôture.
141
161
  if (ctx.clean?.patterns.length) {
@@ -154,5 +174,8 @@ export function sessionClose(ctx, opts) {
154
174
  }
155
175
  const open = issues.filter((i) => !i.warning).length + repo.open + (lock?.live ? 1 : 0);
156
176
  out(open === 0 ? '\n✓ prêt à fermer' : `\n✗ pas fermé : ${open} point(s)`);
177
+ // Seule une clôture réussie devient la borne : relancer la clôture après avoir réglé un point ne vide pas le rapport.
178
+ if (open === 0)
179
+ writeSessionMark(ctx.state, { kind: 'close', at: (ctx.now ?? new Date()).toISOString() });
157
180
  return open === 0 ? 0 : 1;
158
181
  }
package/dist/state.js CHANGED
@@ -33,6 +33,21 @@ export function clearNext(dir) {
33
33
  rmSync(join(dir, 'next.md'), { force: true });
34
34
  return previous;
35
35
  }
36
+ /** Dernière ouverture ou clôture réussie du projet ; null si inconnue ou illisible (la clôture retombe alors sur le jour même). */
37
+ export function readSessionMark(dir) {
38
+ try {
39
+ const raw = JSON.parse(readFileSync(join(dir, 'session.json'), 'utf8'));
40
+ if ((raw.kind === 'start' || raw.kind === 'close') && typeof raw.at === 'string' && !Number.isNaN(Date.parse(raw.at)))
41
+ return { kind: raw.kind, at: raw.at };
42
+ }
43
+ catch {
44
+ // absent ou illisible : pas de borne connue
45
+ }
46
+ return null;
47
+ }
48
+ export function writeSessionMark(dir, mark) {
49
+ writeFileSync(join(dir, 'session.json'), `${JSON.stringify(mark)}\n`);
50
+ }
36
51
  export function lockPath(dir) {
37
52
  return join(dir, 'deliver.lock');
38
53
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sylad/cadence",
3
- "version": "0.16.0",
3
+ "version": "0.18.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",
@@ -18,7 +18,7 @@ With no project named, run `cadence session close --all` from the parent folder,
18
18
 
19
19
  ## Steps
20
20
 
21
- 1. Run `cadence session close` (over several days: `--since "2 days ago"`). Exit code 1 means
21
+ 1. Run `cadence session close` (the window runs from the last `session start` or successful close and is named in the "Commits de la période" title; over several days: `--since "2 days ago"`). Exit code 1 means
22
22
  *not closed*: something below is still open.
23
23
  2. **Plan hygiene**, lot by lot:
24
24
  - a `doing` lot with no commit in the period → `raf done <id>` if its tests are green,
@@ -67,7 +67,7 @@ With no project named, run `cadence session close --all` from the parent folder,
67
67
  these steps say `raf`.
68
68
  6. **Clean state**: everything committed and pushed, no delivery running. If the command still exits 1,
69
69
  say what remains and do NOT say the session is closed.
70
- 7. **Three lines for next time**: `cadence session next "…" "…" "…"` — the next `session-start` shows them.
70
+ 7. **Three lines for next time**: `cadence session next "…" "…" "…"` — the next `session-start` shows them. The command prints the lines as recorded: report them from that output, do not run `session start` again.
71
71
  The lines replace the previous notes. Without a line the command refuses and keeps them; erase
72
72
  them on purpose with `cadence session next --clear`, only when nothing is left to say.
73
73