tau-all-agent 0.2.0 → 0.3.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.
Files changed (42) hide show
  1. package/extensions/answer.ts +112 -85
  2. package/extensions/branch-term/index.ts +9 -5
  3. package/extensions/btw.ts +67 -36
  4. package/extensions/insights.ts +67 -56
  5. package/extensions/loop.ts +14 -52
  6. package/extensions/memory.ts +65 -33
  7. package/extensions/notify.ts +5 -3
  8. package/extensions/review/fix.ts +10 -16
  9. package/extensions/review/git.ts +7 -1
  10. package/extensions/review/index.ts +2 -2
  11. package/extensions/review/models.ts +12 -2
  12. package/extensions/review/review.ts +3 -5
  13. package/extensions/review/runtime.ts +9 -8
  14. package/extensions/review/triage.ts +2 -4
  15. package/extensions/sandbox/README.md +5 -0
  16. package/extensions/sandbox/bash.ts +83 -85
  17. package/extensions/sandbox/index.ts +21 -11
  18. package/extensions/sandbox/permissions/network.ts +8 -3
  19. package/extensions/sandbox/permissions/unsandboxed.ts +1 -240
  20. package/extensions/sandbox/runtime.ts +53 -19
  21. package/extensions/subagent/README.md +58 -1
  22. package/extensions/subagent/config.ts +118 -0
  23. package/extensions/subagent/index.ts +54 -22
  24. package/extensions/telegram/README.md +2 -0
  25. package/extensions/telegram/assistant-result.d.mts +4 -1
  26. package/extensions/telegram/assistant-result.mjs +31 -16
  27. package/extensions/telegram/daemon.mjs +146 -194
  28. package/extensions/telegram/index.ts +11 -7
  29. package/extensions/tool-display-mode.ts +29 -50
  30. package/extensions/usage/README.md +5 -0
  31. package/extensions/usage/index.ts +139 -80
  32. package/extensions/websearch/README.md +8 -1
  33. package/extensions/websearch/config.ts +2 -1
  34. package/extensions/websearch/index.ts +18 -9
  35. package/extensions/websearch/providers/{openai-codex.pi.ts → openai.pi.ts} +51 -16
  36. package/extensions/websearch/providers/shared.ts +10 -3
  37. package/extensions/websearch/types.ts +2 -1
  38. package/extensions/worktree.ts +33 -89
  39. package/package.json +1 -1
  40. package/skills/oracle/SKILL.md +14 -8
  41. package/skills/oracle/scripts/oracle +43 -19
  42. package/skills/update-changelog/SKILL.md +8 -4
@@ -19,25 +19,14 @@
19
19
  */
20
20
 
21
21
  import type { ExtensionAPI, ExtensionCommandContext, Theme } from "@earendil-works/pi-coding-agent";
22
- import {
23
- DynamicBorder,
24
- SessionManager,
25
- highlightCode,
26
- type SessionHeader,
27
- } from "@earendil-works/pi-coding-agent";
28
- import {
29
- Box,
30
- Container,
31
- type SelectItem,
32
- SelectList,
33
- Text,
34
- matchesKey,
35
- } from "@earendil-works/pi-tui";
22
+ import { SessionManager, highlightCode, type SessionHeader } from "@earendil-works/pi-coding-agent";
23
+ import { Box, Text } from "@earendil-works/pi-tui";
36
24
  import { spawnSync } from "node:child_process";
37
25
  import * as fs from "node:fs";
38
26
  import * as path from "node:path";
39
27
 
40
28
  const STATUS_KEY = "0-worktree";
29
+ const LIST_ENTRY_TYPE = "worktree-list";
41
30
  const MANUAL_OPEN_ENTRY_TYPE = "worktree-open-command";
42
31
  const RESTORE_STASH_ENTRY_TYPE = "worktree-restore-command";
43
32
  const SCRIPT_RERUN_ENTRY_TYPE = "worktree-script-rerun-command";
@@ -200,7 +189,7 @@ function showCommandMessage(
200
189
  command: string,
201
190
  ): void {
202
191
  const copiedToClipboard = copyToClipboard(command);
203
- if (!ctx.hasUI) {
192
+ if (ctx.mode === "print") {
204
193
  console.log(formatCommandMessageIntro(intro, copiedToClipboard));
205
194
  console.log(command);
206
195
  return;
@@ -1262,8 +1251,8 @@ function hasValidSessionFile(sessionFile: string): boolean {
1262
1251
  function createFreshSessionFile(targetCwd: string, sessionDir?: string): string {
1263
1252
  const session = SessionManager.create(targetCwd, sessionDir);
1264
1253
  const sessionFile = session.getSessionFile()!;
1265
- // Pi defers its first write until an assistant reply. Persist the native header
1266
- // now so switchSession can open an otherwise empty conversation.
1254
+ // Pi persists the session on its first user or assistant message. Persist the
1255
+ // native header so switchSession can open an otherwise empty conversation.
1267
1256
  fs.writeFileSync(sessionFile, `${JSON.stringify(session.getHeader())}\n`, { flag: "wx" });
1268
1257
  return sessionFile;
1269
1258
  }
@@ -1308,9 +1297,13 @@ async function switchToWorktree(
1308
1297
  // including when the source has not been persisted yet.
1309
1298
  const storage = SessionManager.create(ctx.sessionManager.getCwd(), currentSessionDir);
1310
1299
  const sessionDir = storage.usesDefaultSessionDir() ? undefined : currentSessionDir;
1311
- const hasAssistantReply = ctx.sessionManager
1300
+ const hasPersistedMessage = ctx.sessionManager
1312
1301
  .getEntries()
1313
- .some((entry) => entry.type === "message" && entry.message.role === "assistant");
1302
+ .some(
1303
+ (entry) =>
1304
+ entry.type === "message" &&
1305
+ (entry.message.role === "user" || entry.message.role === "assistant"),
1306
+ );
1314
1307
 
1315
1308
  let sessionFile: string;
1316
1309
  if (hasValidSessionFile(currentSessionFile)) {
@@ -1326,13 +1319,13 @@ async function switchToWorktree(
1326
1319
  }
1327
1320
  sessionFile = forkedSessionFile;
1328
1321
  } else {
1329
- if (hasAssistantReply) {
1322
+ if (hasPersistedMessage) {
1330
1323
  throw new Error(`Current session file is missing or invalid: ${currentSessionFile}`);
1331
1324
  }
1332
1325
 
1333
1326
  const message = "Current session has no persisted history yet. Switching with a fresh session.";
1334
1327
  if (ctx.hasUI) ctx.ui.notify(message, "warning");
1335
- else console.log(message);
1328
+ else console.error(message);
1336
1329
 
1337
1330
  sessionFile = createFreshSessionFile(targetPath, sessionDir);
1338
1331
  }
@@ -1965,6 +1958,11 @@ async function handleList(pi: ExtensionAPI, ctx: ExtensionCommandContext): Promi
1965
1958
  return { repo, items };
1966
1959
  });
1967
1960
 
1961
+ if (ctx.mode === "json") {
1962
+ pi.appendEntry(LIST_ENTRY_TYPE, { items });
1963
+ return;
1964
+ }
1965
+
1968
1966
  if (ctx.mode !== "tui") {
1969
1967
  const output = items
1970
1968
  .map((item) => {
@@ -1985,80 +1983,26 @@ async function handleList(pi: ExtensionAPI, ctx: ExtensionCommandContext): Promi
1985
1983
  return;
1986
1984
  }
1987
1985
 
1988
- const theme = ctx.ui.theme;
1989
-
1990
- const selectItems: SelectItem[] = items.map((item) => ({
1991
- value: item.wt.path,
1992
- label: formatWorktreeLabel(item, theme),
1993
- }));
1994
-
1995
- const itemByValue = new Map(items.map((item) => [item.wt.path, item]));
1986
+ const options = items.map((item) => formatWorktreeLabel(item, ctx.ui.theme));
1987
+ const choice = await ctx.ui.select("Choose worktree", options);
1988
+ if (choice === undefined) return;
1996
1989
 
1997
- type ListResult = { action: "switch" | "archive"; item: WorktreeDisplayItem } | null;
1990
+ const item = items[options.indexOf(choice)];
1991
+ if (!item) return;
1998
1992
 
1999
- const result = await ctx.ui.custom<ListResult>((tui, theme, _kb, done) => {
2000
- const container = new Container();
2001
- container.addChild(new DynamicBorder((s: string) => theme.fg("borderMuted", s)));
2002
-
2003
- const selectList = new SelectList(selectItems, Math.min(selectItems.length, 15), {
2004
- selectedPrefix: (t) => theme.fg("accent", t),
2005
- selectedText: (t) => theme.fg("accent", t),
2006
- description: (t) => t,
2007
- scrollInfo: (t) => theme.fg("dim", t),
2008
- noMatch: (t) => theme.fg("warning", t),
2009
- });
2010
- selectList.onSelect = (si) => {
2011
- const item = itemByValue.get(si.value);
2012
- if (item) done({ action: "switch", item });
2013
- else done(null);
2014
- };
2015
- selectList.onCancel = () => done(null);
2016
- container.addChild(selectList);
2017
-
2018
- container.addChild(
2019
- new Text(theme.fg("dim", " ↑↓ navigate enter switch a archive esc close"), 0, 0),
2020
- );
2021
- container.addChild(new DynamicBorder((s: string) => theme.fg("borderMuted", s)));
2022
-
2023
- return {
2024
- render: (w) => container.render(w),
2025
- invalidate: () => container.invalidate(),
2026
- handleInput: (data) => {
2027
- if (matchesKey(data, "a")) {
2028
- const si = selectList.getSelectedItem();
2029
- if (si) {
2030
- const item = itemByValue.get(si.value);
2031
- if (item) {
2032
- done({ action: "archive", item });
2033
- return;
2034
- }
2035
- }
2036
- }
2037
- selectList.handleInput(data);
2038
- tui.requestRender();
2039
- },
2040
- };
2041
- });
2042
-
2043
- if (!result) return;
2044
-
2045
- if (result.action === "switch") {
2046
- await switchToWorktree(pi, ctx, result.item.wt.path, repo.currentRoot);
1993
+ const action = await ctx.ui.select(`Worktree: ${collapsePath(item.wt.path)}`, [
1994
+ "Switch",
1995
+ "Archive",
1996
+ ]);
1997
+ if (action === "Switch") {
1998
+ await switchToWorktree(pi, ctx, item.wt.path, repo.currentRoot);
2047
1999
  return;
2048
2000
  }
2049
2001
 
2050
- if (result.action === "archive") {
2051
- await withSpinnerStatus(ctx, `archiving ${result.item.branch}`, async () => {
2002
+ if (action === "Archive") {
2003
+ await withSpinnerStatus(ctx, `archiving ${item.branch}`, async () => {
2052
2004
  const defaultMain = await getDefaultMainBranch(pi, repo.mainRoot);
2053
- await archiveWorktree(
2054
- pi,
2055
- ctx,
2056
- repo,
2057
- result.item.branch,
2058
- "prompt",
2059
- defaultMain,
2060
- result.item.wt,
2061
- );
2005
+ await archiveWorktree(pi, ctx, repo, item.branch, "prompt", defaultMain, item.wt);
2062
2006
  });
2063
2007
  }
2064
2008
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tau-all-agent",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Full Tau package for Pi with coding workflows, web search, sandboxing, memory, Telegram, Home Assistant, OpenSCAD, skills, and a theme",
5
5
  "keywords": [
6
6
  "agent-tools",
@@ -7,11 +7,13 @@ description: "Get independent reviews, second opinions, or help getting unstuck
7
7
 
8
8
  Use this skill when you want a second opinion from a capable model in a different family than the current session.
9
9
 
10
- The oracle workflow bundles selected files into a standalone prompt, selects an alternate model with `pi --list-models`, and sends the bundle to `pi -p --no-tools`. It is read-only by default because the oracle receives the selected context directly and does not get tools.
10
+ The oracle workflow bundles selected files into a standalone prompt, selects an alternate model with `pi --list-models`, and sends the bundle to `pi -p --no-tools`.
11
+
12
+ Discovery and generation disable extensions, skills, prompt templates, themes, and context files (`AGENTS.md` / `CLAUDE.md`). A fixed system prompt and empty append prompt replace discovered `SYSTEM.md` / `APPEND_SYSTEM.md`. The bundle supplies the task and project context. Pi retains normal authentication and configuration, but has no tools or persisted session. This isolates instructions, not the process. It is not a filesystem sandbox or a read-only guarantee.
11
13
 
12
14
  ## Model selection
13
15
 
14
- The `./scripts/oracle` wrapper chooses the strongest known model it can find in the first usable family from this order:
16
+ The `scripts/oracle` wrapper chooses the strongest known model it can find in the first usable family from this order:
15
17
 
16
18
  | Current family | Oracle preference |
17
19
  | ------------------ | ----------------------------------------------------- |
@@ -33,24 +35,28 @@ If the current model is not obvious to the script, pass it explicitly with `--cu
33
35
 
34
36
  ## Commands
35
37
 
36
- Run commands from this skill directory:
38
+ Run from the repository being reviewed, not this skill directory. Use the installed scripts' absolute paths. In the examples, replace `/absolute/path/to/oracle` with the directory containing this `SKILL.md`.
39
+
40
+ The wrapper resolves the invocation's Git root once (or uses the invocation directory outside Git) and uses it for model discovery, bundling, and generation. Relative file selectors are rooted there, even when invoked from a repository subdirectory.
37
41
 
38
42
  ```bash
39
43
  # Preview selected files
40
- ./scripts/oracle-bundle --dry-run -p "<task>" --file "src/**" --file "!**/*.test.*"
44
+ /absolute/path/to/oracle/scripts/oracle-bundle --dry-run \
45
+ -p "<task>" --file "src/**" --file "!**/*.test.*"
41
46
 
42
47
  # Preview the full bundle
43
- ./scripts/oracle-bundle -p "<task>" --file "src/**" --file "!**/*.test.*"
48
+ /absolute/path/to/oracle/scripts/oracle-bundle \
49
+ -p "<task>" --file "src/**" --file "!**/*.test.*"
44
50
 
45
51
  # Show which oracle model would be selected
46
- ./scripts/oracle --list-models --current openai-codex/gpt-6-astra
52
+ /absolute/path/to/oracle/scripts/oracle --list-models --current openai-codex/gpt-6-astra
47
53
 
48
54
  # Ask the automatically selected oracle model
49
- ./scripts/oracle --current openai-codex/gpt-6-astra \
55
+ /absolute/path/to/oracle/scripts/oracle --current openai-codex/gpt-6-astra \
50
56
  -p "<task>" --file "src/**" --file "!**/*.test.*"
51
57
 
52
58
  # Override the oracle model when the automatic choice is wrong
53
- ./scripts/oracle --model anthropic/claude-fable-5 \
59
+ /absolute/path/to/oracle/scripts/oracle --model anthropic/claude-fable-5-1 \
54
60
  -p "<task>" --file "src/**" --file "!**/*.test.*"
55
61
  ```
56
62
 
@@ -15,6 +15,11 @@ const BARE_PI_ARGS = [
15
15
  "--no-skills",
16
16
  "--no-prompt-templates",
17
17
  "--no-themes",
18
+ "--no-context-files",
19
+ "--system-prompt",
20
+ "You are an independent advisor. Use the supplied task and files as your context.",
21
+ "--append-system-prompt",
22
+ "/dev/null",
18
23
  ];
19
24
 
20
25
  const FAMILY_ORDER = {
@@ -25,8 +30,10 @@ const FAMILY_ORDER = {
25
30
  };
26
31
 
27
32
  const args = parseArgs(process.argv.slice(2));
33
+ const cwd = repoRoot();
28
34
  const settings = readSettings();
29
- const currentModel = args.currentModel ?? process.env.ORACLE_CURRENT_MODEL ?? defaultModel(settings);
35
+ const currentModel =
36
+ args.currentModel ?? process.env.ORACLE_CURRENT_MODEL ?? defaultModel(settings);
30
37
  const currentFamily = modelFamily(currentModel ?? "") ?? "unknown";
31
38
  const availableModels = listPiModels();
32
39
  const enabledModels = new Set((settings.enabledModels ?? []).map(normalizeModelSpec));
@@ -46,7 +53,7 @@ console.error(
46
53
  );
47
54
 
48
55
  const bundle = run(path.join(SCRIPT_DIR, "oracle-bundle"), args.bundleArgs, {
49
- cwd: repoRoot(),
56
+ cwd,
50
57
  encoding: "utf8",
51
58
  maxBuffer: 64 * 1024 * 1024,
52
59
  });
@@ -60,7 +67,7 @@ const pi = spawnSync(
60
67
  "pi",
61
68
  [...BARE_PI_ARGS, "--model", oracleModel, "--thinking", THINKING_LEVEL],
62
69
  {
63
- cwd: repoRoot(),
70
+ cwd,
64
71
  input: bundle.stdout,
65
72
  stdio: ["pipe", "inherit", "inherit"],
66
73
  },
@@ -119,7 +126,7 @@ function getAgentDir() {
119
126
  if (!configured) return path.join(homedir(), ".pi", "agent");
120
127
  if (configured === "~") return homedir();
121
128
  if (configured.startsWith("~/")) return path.join(homedir(), configured.slice(2));
122
- return path.resolve(configured);
129
+ return path.resolve(cwd, configured);
123
130
  }
124
131
 
125
132
  function defaultModel(settings) {
@@ -128,7 +135,11 @@ function defaultModel(settings) {
128
135
  }
129
136
 
130
137
  function listPiModels() {
131
- const result = run("pi", ["--list-models"], { encoding: "utf8", maxBuffer: 16 * 1024 * 1024 });
138
+ const result = run("pi", [...BARE_PI_ARGS, "--list-models"], {
139
+ cwd,
140
+ encoding: "utf8",
141
+ maxBuffer: 16 * 1024 * 1024,
142
+ });
132
143
  if (result.error) die(result.error.message);
133
144
  if (result.status !== 0) die(result.stderr || "pi --list-models failed");
134
145
 
@@ -200,8 +211,11 @@ function modelRank(model, family) {
200
211
  const normalized = model.toLowerCase();
201
212
 
202
213
  if (family === "openai") {
203
- if (/gpt-6[-_.]astra/.test(normalized)) return 11;
204
- if (/gpt-5\.6[-_.]sol/.test(normalized)) return 10;
214
+ if (/gpt-6[-_.]astra/.test(normalized)) return 14;
215
+ if (/gpt-6\.1[-_.]sol/.test(normalized)) return 13;
216
+ if (/gpt-6[-_.]sol/.test(normalized)) return 12;
217
+ if (/gpt-5\.6[-_.]sol/.test(normalized)) return 11;
218
+ if (/gpt-6[-_.]luna/.test(normalized)) return 10;
205
219
  if (/gpt-5\.6/.test(normalized)) return 9;
206
220
  if (/gpt-5\.5/.test(normalized)) return 8;
207
221
  if (/gpt-5\.4/.test(normalized)) return 7;
@@ -215,16 +229,22 @@ function modelRank(model, family) {
215
229
  }
216
230
 
217
231
  if (family === "anthropic") {
218
- if (/claude[-_.]fable[-_.]5/.test(normalized)) return 10;
219
- if (/claude[-_.]opus[-_.]4[-_.]8/.test(normalized)) return 9;
220
- if (/claude[-_.]opus[-_.]4[-_.]7/.test(normalized)) return 8;
221
- if (/claude[-_.]opus[-_.]4[-_.]6/.test(normalized)) return 7;
222
- if (/claude[-_.]opus[-_.]4[-_.]5/.test(normalized)) return 6;
223
- if (/claude[-_.]opus[-_.]4[-_.]1/.test(normalized)) return 5;
224
- if (/claude[-_.]opus[-_.]4/.test(normalized)) return 4;
225
- if (/claude[-_.]sonnet[-_.]4[-_.]6/.test(normalized)) return 3;
226
- if (/claude[-_.]sonnet[-_.]4[-_.]5/.test(normalized)) return 2;
227
- if (/claude[-_.]sonnet[-_.]4/.test(normalized)) return 1;
232
+ if (/claude[-_.]fable[-_.]5[-_.]1/.test(normalized)) return 16;
233
+ if (/claude[-_.]fable[-_.]5/.test(normalized)) return 15;
234
+ if (/claude[-_.]opus[-_.]5[-_.]5/.test(normalized)) return 14;
235
+ if (/claude[-_.]opus[-_.]5/.test(normalized)) return 13;
236
+ if (/claude[-_.]opus[-_.]4[-_.]8/.test(normalized)) return 12;
237
+ if (/claude[-_.]opus[-_.]4[-_.]7/.test(normalized)) return 11;
238
+ if (/claude[-_.]opus[-_.]4[-_.]6/.test(normalized)) return 10;
239
+ if (/claude[-_.]opus[-_.]4[-_.]5/.test(normalized)) return 9;
240
+ if (/claude[-_.]opus[-_.]4[-_.]1/.test(normalized)) return 8;
241
+ if (/claude[-_.]opus[-_.]4/.test(normalized)) return 7;
242
+ if (/claude[-_.]sonnet[-_.]5[-_.]5/.test(normalized)) return 6;
243
+ if (/claude[-_.]sonnet[-_.]5/.test(normalized)) return 5;
244
+ if (/claude[-_.]sonnet[-_.]4[-_.]6/.test(normalized)) return 4;
245
+ if (/claude[-_.]sonnet[-_.]4[-_.]5/.test(normalized)) return 3;
246
+ if (/claude[-_.]sonnet[-_.]4/.test(normalized)) return 2;
247
+ if (/claude[-_.]haiku[-_.]5[-_.]5/.test(normalized)) return 1;
228
248
  return 0;
229
249
  }
230
250
 
@@ -248,13 +268,17 @@ function modelFamily(spec) {
248
268
  }
249
269
 
250
270
  function normalizeModelSpec(spec) {
251
- return String(spec).trim().replace(/:(off|minimal|low|medium|high|xhigh|max)$/i, "");
271
+ return String(spec)
272
+ .trim()
273
+ .replace(/:(off|minimal|low|medium|high|xhigh|max)$/i, "");
252
274
  }
253
275
 
254
276
  function printModelPlan(models, enabledModels, currentModel, currentFamily) {
255
277
  console.log(`Current model: ${currentModel ?? "unknown"}`);
256
278
  console.log(`Current family: ${currentFamily}`);
257
- console.log(`Family order: ${(FAMILY_ORDER[currentFamily] ?? FAMILY_ORDER.unknown).join(" -> ")}`);
279
+ console.log(
280
+ `Family order: ${(FAMILY_ORDER[currentFamily] ?? FAMILY_ORDER.unknown).join(" -> ")}`,
281
+ );
258
282
  console.log("");
259
283
 
260
284
  for (const family of ["anthropic", "openai", "google"]) {
@@ -9,20 +9,24 @@ Use [Keep a Changelog 2.0.0](https://keepachangelog.com/en/2.0.0/) and preserve
9
9
 
10
10
  ## Choose the scope
11
11
 
12
- - **Document a specific change:** inspect the relevant diff, issue, or PR and the existing `Unreleased` entries. Update only that change; a complete release-history audit is unnecessary.
13
- - **Audit changes since a release:** identify the requested baseline, or the latest release tag (`git describe --tags --abbrev=0`). If tags are missing or inconsistent, use the newest release section as evidence for the baseline. Inspect `git log <baseline>..HEAD --oneline` and relevant diffs or PRs to find notable omissions.
12
+ - **Document a specific change:** inspect the relevant diff, issue, or PR and the existing `Unreleased` entries. Update only that change, checking its same-release feature history when needed to classify or merge it correctly; a complete release-history audit is unnecessary.
13
+ - **Audit changes since a release:** compare the requested baseline, or the latest release tag (`git describe --tags --abbrev=0`), with `HEAD`. For a specified released section, compare its preceding release with that target release instead. If tags are missing or inconsistent, use release sections and history to establish the boundaries. Inspect `git log <baseline>..<target> --oneline` and relevant diffs or PRs.
14
14
  - **Prepare a release:** move `Unreleased` entries into a versioned section only when the release operation is requested.
15
15
 
16
+ During audits and release preparation, reconcile existing entries for duplicates, superseded notes, and misclassification as well as notable omissions.
17
+
16
18
  A changelog request does not authorize committing, publishing, or creating a release.
17
19
 
18
20
  ## Entry rules
19
21
 
20
22
  - Include notable user-visible behavior, APIs, flags, bug fixes, and security changes. Exclude internal cleanup, tests, typo-only documentation edits, dependency bumps, and other changes without visible user impact.
21
- - Write concise, concrete entries matching the repository's bullet grammar. Explain user impact rather than copying commit subjects or listing implementation details.
23
+ - Describe net changes since the previous released baseline, not development chronology. Omit regressions introduced and resolved within the same release, including in existing features. Genuine changes to previously released features or integrations still warrant `Changed` or `Fixed` entries.
24
+ - Give each feature first shipped in this release one coherent `Added` description in its final form. Fold notable pre-release refinements and fixes into that description or omit them, rather than listing them separately under `Changed` or `Fixed`.
25
+ - Write concise, concrete entries matching the repository's bullet grammar. Explain user impact rather than copying commit subjects or listing implementation details. Avoid mechanically repeating section labels such as “Added”, “Fixed”, or “Removed” in bullets; write grammatical descriptions rather than merely deleting those prefixes.
22
26
  - Link issues or pull requests when useful, but prefer plain prose over lists of references.
23
27
  - Mark breaking changes with `**Breaking:**` within their change category, and say what breaks.
24
28
  - Keep upgrade notes brief. Link substantial procedures to a migration guide or release notes.
25
- - Add entries under `Unreleased` unless an explicitly requested release requires moving them. Preserve released content during ordinary updates.
29
+ - Add entries under `Unreleased` unless an explicitly requested release requires moving them or an audit/correction targets a released section. Preserve released content otherwise.
26
30
  - Merge with an existing entry when it describes the same change rather than adding a duplicate.
27
31
 
28
32
  ## Categories