Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
1a58006
ci: back-merge main into next
aidd-bot[bot] Oct 16, 2026
45a48d6
chore(release): remove the 5.12 version pins (#1040)
blafourcade Oct 16, 2026
708eefa
feat(aidd-context): assess existing rules before writing (#1006)
blafourcade Oct 16, 2026
b5c2525
fix(telemetry): status after off says measurement was turned off (#1041)
blafourcade Oct 16, 2026
1c5ae72
test(cli): run the telemetry mutation 7x faster without weakening it …
blafourcade Oct 16, 2026
8f1b968
fix(telemetry): bind a branch only by the command's own declaration (…
blafourcade Oct 16, 2026
1202ff5
fix(telemetry): nothing measured is lost silently (#1046)
blafourcade Oct 17, 2026
616dbbe
fix(telemetry): read a transcript a block at a time (#1047)
blafourcade Oct 17, 2026
940e8e3
feat(telemetry): report envelope v2 keeps the figures apart from the …
blafourcade Oct 17, 2026
3167d69
fix(telemetry): one call one key, one repository one id (#1049)
blafourcade Oct 17, 2026
5721e2f
chore: no local path, username or session id in the repository (#1051)
blafourcade Oct 17, 2026
7876341
chore(telemetry): clean up what the R1 work left behind (#1053)
blafourcade Oct 17, 2026
856a8b3
fix(telemetry): the simplest true rule for a call with no request id …
blafourcade Oct 17, 2026
5a46c34
fix(telemetry): small defects the R1 audit reproduced (#1055)
blafourcade Oct 17, 2026
b1dc3c7
chore(deps): bump ink from 7.1.1 to 8.0.0 in /kanban (#1025)
dependabot[bot] Oct 19, 2026
e8aabe0
feat(telemetry): measure codex from its own session files (#1057)
blafourcade Oct 19, 2026
8419f25
chore(deps-dev): bump the npm-dev-dependencies group with 2 updates (…
dependabot[bot] Oct 19, 2026
2932494
chore(deps): bump react from 19.3.0 to 19.3.1 in /kanban (#1062)
dependabot[bot] Oct 19, 2026
61465f8
chore(deps-dev): bump vitest from 5.0.3 to 5.0.5 in /kanban in the np…
dependabot[bot] Oct 19, 2026
c039abc
ci(deps): bump the github-actions group with 5 updates (#1063)
dependabot[bot] Oct 19, 2026
c338b19
feat(telemetry): ask a codex tui session for its task (#1058)
blafourcade Oct 20, 2026
78938c9
feat(telemetry): split and filter the report by tool (#1064)
blafourcade Oct 20, 2026
3129aba
test(cli): keep the runner's own session out of the telemetry sandbox…
blafourcade Oct 20, 2026
da3a99c
chore(deps-dev): update cli build and test tooling
dependabot[bot] Oct 20, 2026
160caf3
docs(aidd-context): ask the project memory for its tickets and hooks …
alexsoyes Oct 21, 2026
c9d1c42
docs: let the aidd-dev skills alone decide a task folder's shape (#1073)
blafourcade Oct 21, 2026
f9a1ba2
refactor(aidd-refine): lean the brainstorm skill for small models (#1…
blafourcade Oct 22, 2026
196cd62
chore: plan and render the release video locally (#1056)
blafourcade Oct 22, 2026
b7763ea
refactor(aidd-dev): lean the plan skill for small models (#1080)
blafourcade Oct 23, 2026
ff2fa23
ci: run the plugin suite on macOS (#1083)
blafourcade Oct 23, 2026
3689983
chore: replan the release video from the full log and shorten its pau…
blafourcade Oct 26, 2026
11aa201
ci: render the release video on a pull request before the release (#1…
blafourcade Oct 26, 2026
560dbf0
test(cli): split each tool profile's tests by feature
alexsoyes Oct 26, 2026
dd8c352
Merge pull request #1090 from ai-driven-dev/test/split-profile-tests-…
alexsoyes Oct 26, 2026
bf23db6
ci: hold the release until its video is attached (#1093)
blafourcade Oct 26, 2026
4753f05
chore: carry the video programme in the release video text (#1094)
blafourcade Oct 26, 2026
c367cdc
chore: group the release video programme by theme (#1096)
blafourcade Oct 26, 2026
790c03b
chore: draw the release video intro at its first frame (#1097)
blafourcade Oct 26, 2026
84c34ad
ci: take a thumbnail with the release video and attach it (#1098)
blafourcade Oct 26, 2026
3421ebf
chore: take the thumbnail on the logo and the whole version (#1099)
blafourcade Oct 26, 2026
b2e5864
chore: draw the release thumbnail as its own composition (#1100)
blafourcade Oct 26, 2026
aa3d777
chore: open the release video on its thumbnail for one frame (#1101)
blafourcade Oct 26, 2026
00d4491
chore: say what the measurement opt-in does for Codex (#1102)
blafourcade Oct 26, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
1 change: 1 addition & 0 deletions .agents/skills/release-video
2 changes: 1 addition & 1 deletion .claude-plugin/marketplace.json
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@
{
"name": "aidd-telemetry",
"source": "./plugins/aidd-telemetry",
"description": "Measurement: counts the tokens of Claude Code sessions per task, ticket, model and day, locally. The hooks ask for a task on plain node; its two skills reach the `aidd` CLI to turn it on and answer.",
"description": "Measurement: counts the tokens of Claude Code and Codex sessions per task, ticket, model, tool and day, locally. The hooks ask for a task on plain node; its two skills reach the `aidd` CLI to turn it on and answer.",
"strict": true,
"metadata": {
"recommended": false
Expand Down
33 changes: 33 additions & 0 deletions .claude/skills/release-video/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
---
name: release-video
description: Plans and renders the release video, one scene per feature shipped since the last release, with its music and interface sounds. Use when the user wants to prepare the release announcement video, change its music, or the release video check reports missing scenes. Not for publishing the video or writing release notes.
argument-hint: release version | music
---

# Release video

```mermaid
flowchart LR
source([release]) --> plan --> render --> done([release-video.mp4])
render -->|"scene to fix"| plan
track([music]) --> compose-music --> render
```

## Actions

Run the flow above. Read only the next action file.

| Action | Does |
| --- | --- |
| plan | cover every shipped feature with a scene, or a reason to skip it |
| render | turn the plan into the MP4 and check it on screen |
| compose-music | write a new track when the user asks for other music |

## Transversal rules

- The plan lives in `aidd_docs/release-video/scenes.json`; the check and the renderer are in `assets/`, their tests in `scripts/__tests__/`.
- Show only what the product does; mark every invented figure as an example on screen; a demo of text alone, plainly a session, needs no mark.
- A viewer may watch with the sound off and never read the release notes: the picture carries the message, the sound only makes it lively.
- Never reuse the audio of a published video: it carries that video's own sounds, which then play at moments that mean nothing here.
- Keep product choices with the user, music included: it is judged by listening, which is the user's.
- Stop at the MP4: attaching it to a release and announcing it are the user's.
49 changes: 49 additions & 0 deletions .claude/skills/release-video/actions/01-plan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# 01 - Plan

Cover every feature shipped since the last release with one scene, or one written reason.

## Input

The repository at the release candidate, read again from the last release tag, never from the previous plan.

## Output

`aidd_docs/release-video/scenes.json` that `node .claude/skills/release-video/assets/check-release-video.js` accepts; the next step is [render](02-render.md).

## Process

1. **List.** Read every commit since the last release tag, whatever its type: `git log <last-tag>..HEAD --no-merges`. Keep what a user would notice, a `refactor:` or `docs:` that changes how a skill behaves included. Then run the check: it sees only `feat:` commits, so it proves nothing is missing from those, not that the plan is complete. Each surface under "Scènes manquantes" needs a scene or a skip.
2. **Read.** Open the skill or command each surface names; take its real behavior, never a guess.
3. **Draft.** Fill the [plan template](../assets/scenes-template.json), one scene per surface, with the templates, rows and fields of [scenes.md](../references/scenes.md).
- Write every title, tag and highlight in everyday words, outcome first: what the user gains, not what the feature is or how it works. `Count tokens in Codex too` becomes `See what your AI costs, in Codex too`. A title that names a feature without saying what it brings is rewritten.
- A measured gain goes in the title as one figure computed from the scene's own before and after (`38% leaner Brainstorm for small models`); the scene carries no row of numbers beside it.
- Leave `lead` out: the picture must say it, a subtitle under the title competes with it. Add one only when a scene cannot be understood without it.
- Only the demo window shows real commands.
4. **Skip.** Move a surface to `skipped` with a reason only when a user would not notice it.
5. **Fixes.** List the `fix:` commits since the last release tag. A fix is worth a scene only when its before and after can be shown on screen; a fix that only makes figures or states more reliable has none. Group the ones that qualify into one `bullet-grid` scene; the usual outcome is no scene.
6. **Contributors.** Name who shipped the release with `gh api repos/{owner}/{repo}/compare/<last-tag>...next --jq '[.commits[].author.login] | unique'`. Drop bots, keep every human author whatever the type of the commit (a docs commit counts). Add whoever opened an issue closed since the last tag: `gh api "repos/{owner}/{repo}/issues?state=closed&since=<last-tag date>" --paginate --jq '[.[] | select(.pull_request == null) | .user.login] | unique'`, because someone who reported what a developer then fixed shipped it too. Put the logins in one `contributors` scene, last.
7. **Intro.** Fill `intro.highlights`, the tags that name the release's big features, so that someone who never used AIDD sees at a glance what it brings: up to five, each a short clear sentence of five words at most, starting from what the user gets (`Telemetry now covers Codex`, not `Report by tool`). Leave `tagline` out: a phrase above the tags says less than they do. Take the facts from the release announcement, so the video and the message say the same thing.
8. **YouTube text.** Fill `youtube` in French, in the words of the published videos: `themes` name the release's themes, `programme` has one line per covered surface saying what it does, as in `compter ce que coûte votre travail, par tâche`. Each line names its `theme`, one of `themes`: the announcements on Discord and Skool show one section per theme, with its lines as they are and its commands in parentheses, so each line is read as a sentence a member will see. `assets/youtube-metadata.js` assembles the title and the description in their published layout.
9. **Judge.** Read the plan against [judge.md](../references/judge.md), which the check cannot do.
10. **Check.** Rerun the check until it passes, then show the plan and fold corrections.

## Test

| Case | Pass |
| --- | --- |
| Uncovered feature | the check names it; the plan gains a scene or a skip |
| Skill changed by a `refactor:` or `docs:` commit | read from the log, not the check; a scene or a written reason |
| Plan left from the previous release | scenes rebuilt from this release's log; none kept because they were there |
| Skip without reason | the check fails; a reason is added |
| Release with no `feat:` commit | no video; plan left empty |
| Invented figure | marked as an example in the scene |
| Demo of text alone | no mark: a session whose lines are plainly illustrative |
| `fix:` commits a user would notice | one grouped scene; internal ones left out |
| Contributor list | bots dropped; commit authors and openers of closed issues; one `contributors` scene, last |
| Scene with a measured gain | one title carrying the figure, computed from the scene's own numbers; no figures row |
| Scene that needs a lead to be understood | the picture is redrawn until it says it alone; a lead is the last resort |
| Title that names a feature, not what it brings | refused on review; rewritten from the user's gain |
| Internal word or text over the limits | the check fails naming the word or the field and where it is |
| Theme split by another one | the check fails naming the theme; its scenes are brought together |
| Surface with no `programme` line, or a title over 100 characters | the check fails naming it |
| Plan read against the judge list | every rule answered, a failing scene rewritten or removed |
33 changes: 33 additions & 0 deletions .claude/skills/release-video/actions/02-render.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# 02 - Render

Turn the accepted plan into the release video and look at it.

## Input

`aidd_docs/release-video/scenes.json` that the check accepts, and the version the release will carry, such as `5.13`: the next minor when a `feat:` shipped since the last tag, never a placeholder, because it is typed on screen.

## Output

`release-video.mp4` in the working directory, and `release-video.png` beside it: the thumbnail, a still composition of the logo, the version and the themes, not a frame of the video, and it is also the video's first frame, one frame long, so a chat shows it as the preview.

## Process

1. **Check.** Run `node .claude/skills/release-video/assets/check-release-video.js`. On failure, return to [plan](01-plan.md).
2. **Render.** Run `node .claude/skills/release-video/assets/render-release-video.cjs --version <version> --out release-video.mp4`. It needs `npx`, a network, ffmpeg and Chrome, and adds the music and the interface sounds of [sound.md](../references/sound.md).
3. **Look.** Extract one frame near the end of each scene with `ffmpeg -ss <seconds> -i release-video.mp4 -frames:v 1` and read them, and read `release-video.png`.
- Fix overflow, cut text and wrong values in the plan, then render again.
- Judge each frame as a viewer with the sound off, who never read the release notes: the picture alone must say what the release brings.
4. **Measure.** Cut a second of the intro, of a scene and of the outro with `ffmpeg -ss <s> -t 1 -i release-video.mp4 -af volumedetect -vn -f null -` and compare with the levels in [sound.md](../references/sound.md). You cannot listen: say it was measured, not heard.
5. **Report.** Give the path, the duration, and any avatar the script replaced by the logo.

## Test

| Case | Pass |
| --- | --- |
| Plan the check refuses | no render; back to plan |
| Account that no longer exists | video renders; the replaced avatar is reported |
| Text that does not fit a window | seen on a frame, fixed in the plan, rendered again |
| Version typed on screen | the real release version, not a placeholder |
| Frame read with the sound off | the picture alone says what the release brings |
| Levels measured | the hook is the loudest, the bed about 4 dB lower under the scenes and the outro, the ending no louder than the bed, the interface sounds stand out of the bed |
| Finished render | MP4 present, duration reported, nothing published |
28 changes: 28 additions & 0 deletions .claude/skills/release-video/actions/03-compose-music.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# 03 - Compose music

Write a new original track when the current one tires or does not fit.

## Input

What to change, in the user's words: slower, calmer, another mood.

## Output

`assets/music.mp3` in this skill, which every render then uses; the next step is [render](02-render.md).

## Process

1. **Shape.** Edit `assets/compose-music.py`: tempo, chords and levels, keeping the three parts and the bar lengths of [sound.md](../references/sound.md).
2. **Compose.** Run `python3 .claude/skills/release-video/assets/compose-music.py <out.wav>`; it needs numpy.
3. **Encode.** Run `ffmpeg -i <out.wav> -b:a 160k .claude/skills/release-video/assets/music.mp3`.
4. **Hear.** Render per [render](02-render.md) and give the user the MP4 and the track to listen to.
- Keep the track only on the user's word.

## Test

| Case | Pass |
| --- | --- |
| Script run | a stereo wav of about 27 seconds, with hook, bed and ending levels printed |
| Track used by a render | the measured levels keep the loud, calm, loud shape |
| Track from another video | refused: only an original composition is kept |
| User has not listened | the track is not declared good, only measured |
192 changes: 192 additions & 0 deletions .claude/skills/release-video/assets/check-release-video.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
#!/usr/bin/env node
/**
* Fails when a feature shipped since the last release has no scene in the release video, and
* no written reason for being left out. A release with no `feat:` commit needs no video.
*
* A surface is something a user can run: a plugin skill (`aidd-dev:10-batch`) or a CLI command
* (`aidd telemetry`). It is read from the files a `feat:` commit adds or modifies, so a
* rename shows the new name only.
*/

const { execFileSync } = require("node:child_process");
const fs = require("node:fs");
const path = require("node:path");
const { youtubeProblems } = require("./youtube-metadata.js");

const ROOT = path.resolve(__dirname, "../../../..");
const PLAN_PATH = path.join(ROOT, "aidd_docs/release-video/scenes.json");

/** Files in the commands folder that are plumbing, not a command a user types. */
const NOT_COMMANDS = new Set([
"global-options",
"menu",
"spawn-cli-command",
"sync-native-activation",
]);

const SKILL = /^plugins\/([^/]+)\/skills\/(\d{2}-[^/]+)\//;
const COMMAND = /^cli\/src\/presentation\/commands\/([^/]+)\.ts$/;

function surfaceOf(file) {
const skill = SKILL.exec(file);
if (skill) return `${skill[1]}:${skill[2]}`;
const command = COMMAND.exec(file);
if (command && !NOT_COMMANDS.has(command[1])) return `aidd ${command[1]}`;
return null;
}

function surfacesOf(files) {
const found = new Set(files.map(surfaceOf).filter(Boolean));
return [...found].sort();
}

/** Where a surface lives, so a surface added then removed or renamed in the same cycle is dropped. */
function pathOf(surface) {
if (surface.startsWith("aidd ")) {
return `cli/src/presentation/commands/${surface.slice(5)}.ts`;
}
const [plugin, skill] = surface.split(":");
return `plugins/${plugin}/skills/${skill}`;
}

function shown(plan) {
return new Set(plan.scenes.flatMap((scene) => scene.surfaces ?? []));
}

function missingScenes(surfaces, plan) {
const covered = shown(plan);
const skipped = new Set(plan.skipped.map((skip) => skip.surface));
return surfaces.filter((s) => !covered.has(s) && !skipped.has(s));
}

function invalidSkips(plan) {
return plan.skipped
.filter((skip) => !String(skip.reason ?? "").trim())
.map((skip) => skip.surface);
}

/** Without GIT_DIR and the like: a hook sets them, and they would point git at the repository
* being committed instead of `cwd`. */
const withoutGitEnv = () =>
Object.fromEntries(Object.entries(process.env).filter(([key]) => !key.startsWith("GIT_")));

const MAX_TAGLINE_WORDS = 6;
const MAX_HIGHLIGHTS = 5;
const MAX_HIGHLIGHT_WORDS = 5;
const MAX_LEAD_WORDS = 14;

const wordCount = (text) => String(text ?? "").trim().split(/\s+/).filter(Boolean).length;

/** The first slide and each lead are read in a glance: a long one is refused, not trimmed. */
function overlongText(plan) {
const found = [];
const { tagline, highlights = [] } = plan.intro ?? {};
if (wordCount(tagline) > MAX_TAGLINE_WORDS) {
found.push(`intro tagline: ${wordCount(tagline)} words, ${MAX_TAGLINE_WORDS} at most`);
}
if (highlights.length > MAX_HIGHLIGHTS) {
found.push(`intro highlights: ${highlights.length}, ${MAX_HIGHLIGHTS} at most`);
}
highlights.forEach((text, i) => {
if (wordCount(text) > MAX_HIGHLIGHT_WORDS) {
found.push(`intro highlight ${i + 1}: ${wordCount(text)} words, ${MAX_HIGHLIGHT_WORDS} at most`);
}
});
plan.scenes.forEach((scene, i) => {
if (wordCount(scene.lead) > MAX_LEAD_WORDS) {
found.push(`scene ${i + 1} lead: ${wordCount(scene.lead)} words, ${MAX_LEAD_WORDS} at most`);
}
});
return found;
}

/** Words only the people who build AIDD know. A viewer reads the outcome in plain words; the demo
* window may show the real commands. */
const JARGON = /\b(tokens?|envelopes?|axis|transcripts?|payloads?|hooks?)\b/gi;

function jargonIn(plan) {
const found = [];
const scan = (where, text) => {
for (const [word] of String(text ?? "").matchAll(JARGON)) found.push(`${where}: "${word.toLowerCase()}"`);
};
const { tagline, highlights = [] } = plan.intro ?? {};
scan("intro tagline", tagline);
highlights.forEach((text, i) => scan(`intro highlight ${i + 1}`, text));
plan.scenes.forEach((scene, i) => {
for (const field of ["title", "accent", "lead"]) scan(`scene ${i + 1} ${field}`, scene[field]);
});
return found;
}

function git(cwd, ...args) {
return execFileSync("git", args, { cwd, encoding: "utf8", env: withoutGitEnv() }).trim();
}

/** Surfaces touched by `feat:` commits since the last release tag and still present at HEAD. */
function featSurfaces(cwd = ROOT) {
const tag = git(cwd, "describe", "--tags", "--abbrev=0", "--match", "v[0-9]*");
const log = git(cwd, "log", `${tag}..HEAD`, "--no-merges", "--format=%H%x09%s");
const files = log
.split("\n")
.filter(Boolean)
.map((line) => line.split("\t"))
.filter(([, subject]) => /^feat(\([^)]*\))?!?:/.test(subject))
.flatMap(([sha]) =>
git(cwd, "show", "--name-only", "--diff-filter=AM", "--format=", sha).split("\n")
);
return surfacesOf(files).filter((surface) => fs.existsSync(path.join(cwd, pathOf(surface))));
}

/** A theme (the pill, top left) groups what touches the same domain: its scenes follow each other. */
function splitThemes(plan) {
const seen = new Set();
const split = new Set();
let current;
for (const { pill } of plan.scenes) {
if (pill !== current && seen.has(pill)) split.add(pill);
seen.add(pill);
current = pill;
}
return [...split];
}

function readPlan() {
if (!fs.existsSync(PLAN_PATH)) return { scenes: [], skipped: [] };
const plan = JSON.parse(fs.readFileSync(PLAN_PATH, "utf8"));
return { intro: plan.intro, youtube: plan.youtube, scenes: plan.scenes ?? [], skipped: plan.skipped ?? [] };
}

function run({
surfaces = featSurfaces(),
plan = readPlan(),
logger = console.error,
successLogger = console.log,
} = {}) {
const missing = missingScenes(surfaces, plan);
const unexplained = invalidSkips(plan);
const tooLong = overlongText(plan);
const technical = jargonIn(plan);
const split = splitThemes(plan);
const youtube = plan.scenes.length > 0 ? youtubeProblems(plan) : [];

if (missing.length === 0 && unexplained.length === 0 && tooLong.length === 0 && technical.length === 0 && split.length === 0 && youtube.length === 0) {
successLogger(`✅ Release video: ${surfaces.length} surface(s) covered`);
return 0;
}

if (missing.length > 0) logger(`❌ Scènes manquantes : ${missing.join(", ")}`);
if (unexplained.length > 0) {
logger(`❌ Surfaces ignorées sans raison : ${unexplained.join(", ")}`);
}
if (tooLong.length > 0) logger(`❌ Textes trop longs : ${tooLong.join(", ")}`);
if (technical.length > 0) logger(`❌ Mots techniques : ${technical.join(", ")}`);
if (split.length > 0) logger(`❌ Thèmes dispersés : ${split.join(", ")}`);
if (youtube.length > 0) logger(`❌ Texte YouTube : ${youtube.join(", ")}`);
return 1;
}

module.exports = { featSurfaces, invalidSkips, jargonIn, missingScenes, overlongText, readPlan, run, splitThemes, surfacesOf };

if (require.main === module) {
process.exit(run());
}
Loading
Loading