tau-coding-agent 0.1.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 (75) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +42 -0
  3. package/extensions/answer.ts +601 -0
  4. package/extensions/branch-term.ts +405 -0
  5. package/extensions/btw.ts +444 -0
  6. package/extensions/ghostty.ts +301 -0
  7. package/extensions/git-diff-stats.ts +277 -0
  8. package/extensions/git-pr-status.ts +286 -0
  9. package/extensions/insights.ts +2367 -0
  10. package/extensions/interlude.ts +144 -0
  11. package/extensions/loop.ts +529 -0
  12. package/extensions/memory.ts +1889 -0
  13. package/extensions/notify.ts +161 -0
  14. package/extensions/openai-fast.ts +227 -0
  15. package/extensions/openai-verbosity.ts +223 -0
  16. package/extensions/review.ts +4347 -0
  17. package/extensions/sandbox/index.ts +2578 -0
  18. package/extensions/usage/anthropic.ts +198 -0
  19. package/extensions/usage/github-copilot.ts +204 -0
  20. package/extensions/usage/google-gemini-cli.ts +232 -0
  21. package/extensions/usage/index.ts +2310 -0
  22. package/extensions/usage/minimax.ts +208 -0
  23. package/extensions/usage/openai-codex.ts +180 -0
  24. package/extensions/usage/openrouter.ts +168 -0
  25. package/extensions/usage/providers.ts +26 -0
  26. package/extensions/usage/shared.ts +156 -0
  27. package/extensions/usage/types.ts +53 -0
  28. package/extensions/usage/zai.ts +186 -0
  29. package/extensions/websearch/README.md +68 -0
  30. package/extensions/websearch/browser/chromium.ts +330 -0
  31. package/extensions/websearch/browser/discovery.ts +75 -0
  32. package/extensions/websearch/browser/firefox.ts +150 -0
  33. package/extensions/websearch/browser/sqlite.ts +38 -0
  34. package/extensions/websearch/config.ts +76 -0
  35. package/extensions/websearch/index.ts +312 -0
  36. package/extensions/websearch/normalize.ts +155 -0
  37. package/extensions/websearch/providers/anthropic.pi.ts +139 -0
  38. package/extensions/websearch/providers/gemini.browser.ts +185 -0
  39. package/extensions/websearch/providers/gemini.pi.ts +108 -0
  40. package/extensions/websearch/providers/openai-codex.browser.ts +77 -0
  41. package/extensions/websearch/providers/openai-codex.pi.ts +49 -0
  42. package/extensions/websearch/providers/openai-codex.shared.ts +123 -0
  43. package/extensions/websearch/providers/pi-model.shared.ts +86 -0
  44. package/extensions/websearch/providers/search-prompt.shared.ts +17 -0
  45. package/extensions/websearch/providers/shared.ts +76 -0
  46. package/extensions/websearch/types.ts +52 -0
  47. package/extensions/worktree.ts +2223 -0
  48. package/package.json +72 -0
  49. package/skills/browser-tools/SKILL.md +253 -0
  50. package/skills/browser-tools/scripts/browser-content.js +100 -0
  51. package/skills/browser-tools/scripts/browser-cookies.js +33 -0
  52. package/skills/browser-tools/scripts/browser-dismiss-cookies.js +455 -0
  53. package/skills/browser-tools/scripts/browser-eval.js +75 -0
  54. package/skills/browser-tools/scripts/browser-logs-tail.js +89 -0
  55. package/skills/browser-tools/scripts/browser-nav.js +28 -0
  56. package/skills/browser-tools/scripts/browser-net-summary.js +115 -0
  57. package/skills/browser-tools/scripts/browser-pick.js +133 -0
  58. package/skills/browser-tools/scripts/browser-screenshot.js +18 -0
  59. package/skills/browser-tools/scripts/browser-start.js +281 -0
  60. package/skills/browser-tools/scripts/browser-watch.js +321 -0
  61. package/skills/browser-tools/scripts/utils.js +60 -0
  62. package/skills/git-clean-history/SKILL.md +45 -0
  63. package/skills/git-commit/SKILL.md +55 -0
  64. package/skills/oracle/SKILL.md +64 -0
  65. package/skills/oracle/scripts/oracle +249 -0
  66. package/skills/oracle/scripts/oracle-bundle +227 -0
  67. package/skills/sentry/SKILL.md +198 -0
  68. package/skills/sentry/lib/auth.js +99 -0
  69. package/skills/sentry/scripts/fetch-event.js +325 -0
  70. package/skills/sentry/scripts/fetch-issue.js +368 -0
  71. package/skills/sentry/scripts/list-issues.js +245 -0
  72. package/skills/sentry/scripts/search-events.js +291 -0
  73. package/skills/sentry/scripts/search-logs.js +234 -0
  74. package/skills/update-changelog/SKILL.md +135 -0
  75. package/skills/web-design/SKILL.md +117 -0
@@ -0,0 +1,291 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { SENTRY_API_BASE, getAuthToken, fetchJson, resolveProjectId } from "../lib/auth.js";
4
+
5
+ const HELP = `Usage: search-events.js [options]
6
+
7
+ Search for events (transactions, errors) in Sentry Discover.
8
+
9
+ Options:
10
+ --org, -o <org> Organization slug (required)
11
+ --project, -p <project> Project slug or ID
12
+ --query, -q <query> Search query (Discover syntax)
13
+ --period, -t <period> Time period (default: 24h, e.g., 1h, 7d, 14d)
14
+ --start <datetime> Start time (ISO 8601, e.g., 2025-12-23T15:00:00)
15
+ --end <datetime> End time (ISO 8601)
16
+ --transaction <name> Filter by transaction name
17
+ --tag <key:value> Filter by tag (can be repeated)
18
+ --level <level> Filter by level (error, warning, info)
19
+ --limit, -n <n> Max results (default: 25, max: 100)
20
+ --fields <fields> Comma-separated fields to include
21
+ --json Output raw JSON
22
+ -h, --help Show this help
23
+
24
+ Common Fields:
25
+ id, title, timestamp, transaction, message, level, environment,
26
+ user.email, user.id, tags[key], http.method, http.url
27
+
28
+ Query Syntax (Discover):
29
+ transaction:process-* Match transaction names with wildcards
30
+ level:error Filter by log level
31
+ user.email:foo@bar.com Filter by user email
32
+ environment:production Filter by environment
33
+ has:stack.filename Events with stack traces
34
+ !has:user Events without user
35
+
36
+ Date Range Examples:
37
+ --period 7d Last 7 days
38
+ --start 2025-12-23T15:00:00 From specific time to now
39
+ --start 2025-12-23T15:00:00 --end 2025-12-23T18:00:00 Specific range
40
+
41
+ Examples:
42
+ # Find all transactions for a transaction name
43
+ search-events.js --org myorg --project backend --transaction process-incoming-email
44
+
45
+ # Find errors in the last 7 days
46
+ search-events.js --org myorg --query "level:error" --period 7d
47
+
48
+ # Find events around a specific time
49
+ search-events.js --org myorg --start 2025-12-23T15:00:00 --end 2025-12-23T17:00:00
50
+
51
+ # Search with a specific tag
52
+ search-events.js --org myorg --tag thread_id:th_abc123
53
+
54
+ # Get more fields
55
+ search-events.js --org myorg --fields "id,title,timestamp,user.email"
56
+ `;
57
+
58
+ function parseArgs(args) {
59
+ const options = {
60
+ org: null,
61
+ project: null,
62
+ query: null,
63
+ period: null,
64
+ start: null,
65
+ end: null,
66
+ transaction: null,
67
+ tags: [],
68
+ level: null,
69
+ limit: 25,
70
+ fields: ["id", "title", "timestamp", "transaction", "message"],
71
+ json: false,
72
+ help: false,
73
+ };
74
+
75
+ for (let i = 0; i < args.length; i++) {
76
+ const arg = args[i];
77
+
78
+ switch (arg) {
79
+ case "--help":
80
+ case "-h":
81
+ options.help = true;
82
+ break;
83
+ case "--json":
84
+ options.json = true;
85
+ break;
86
+ case "--org":
87
+ case "-o":
88
+ options.org = args[++i];
89
+ break;
90
+ case "--project":
91
+ case "-p":
92
+ options.project = args[++i];
93
+ break;
94
+ case "--query":
95
+ case "-q":
96
+ options.query = args[++i];
97
+ break;
98
+ case "--period":
99
+ case "-t":
100
+ options.period = args[++i];
101
+ break;
102
+ case "--start":
103
+ options.start = args[++i];
104
+ break;
105
+ case "--end":
106
+ options.end = args[++i];
107
+ break;
108
+ case "--transaction":
109
+ options.transaction = args[++i];
110
+ break;
111
+ case "--tag":
112
+ options.tags.push(args[++i]);
113
+ break;
114
+ case "--level":
115
+ options.level = args[++i];
116
+ break;
117
+ case "--limit":
118
+ case "-n":
119
+ options.limit = parseInt(args[++i], 10);
120
+ break;
121
+ case "--fields":
122
+ options.fields = args[++i].split(",").map((f) => f.trim());
123
+ break;
124
+ }
125
+ }
126
+
127
+ // Default to 24h if no time range specified
128
+ if (!options.period && !options.start) {
129
+ options.period = "24h";
130
+ }
131
+
132
+ return options;
133
+ }
134
+
135
+ function formatEvent(event, fields) {
136
+ const lines = [];
137
+
138
+ const id = event.id || event["event.type"] || "?";
139
+ const ts = event.timestamp || "N/A";
140
+ const title = event.title || event.transaction || event.message || "(no title)";
141
+ const transaction = event.transaction || "";
142
+
143
+ // Format timestamp
144
+ let displayTs = ts;
145
+ try {
146
+ const date = new Date(ts);
147
+ if (!isNaN(date.getTime())) {
148
+ displayTs = date.toISOString().replace("T", " ").slice(0, 19);
149
+ }
150
+ } catch {}
151
+
152
+ lines.push(`[${displayTs}] ${title}`);
153
+
154
+ if (transaction && transaction !== title) {
155
+ lines.push(` transaction: ${transaction}`);
156
+ }
157
+
158
+ if (event.message && event.message !== title) {
159
+ lines.push(` message: ${event.message}`);
160
+ }
161
+
162
+ // Show any extra fields the user requested
163
+ for (const field of fields) {
164
+ if (["id", "title", "timestamp", "transaction", "message"].includes(field)) continue;
165
+ const value = event[field];
166
+ if (value !== undefined && value !== null && value !== "") {
167
+ lines.push(` ${field}: ${value}`);
168
+ }
169
+ }
170
+
171
+ lines.push(` id: ${id}`);
172
+
173
+ return lines.join("\n");
174
+ }
175
+
176
+ function formatOutput(data, fields) {
177
+ if (!data.data || data.data.length === 0) {
178
+ return "No events found matching your query.";
179
+ }
180
+
181
+ const lines = [];
182
+ lines.push(`Found ${data.data.length} events:\n`);
183
+
184
+ for (const event of data.data) {
185
+ lines.push(formatEvent(event, fields));
186
+ lines.push("");
187
+ }
188
+
189
+ return lines.join("\n").trimEnd();
190
+ }
191
+
192
+ async function main() {
193
+ const args = process.argv.slice(2);
194
+ const options = parseArgs(args);
195
+
196
+ if (options.help) {
197
+ console.log(HELP);
198
+ process.exit(0);
199
+ }
200
+
201
+ if (!options.org) {
202
+ console.error("Error: --org is required");
203
+ console.error("Run with --help for usage information");
204
+ process.exit(1);
205
+ }
206
+
207
+ const token = getAuthToken();
208
+
209
+ // Build query parameters
210
+ const params = new URLSearchParams();
211
+ params.set("dataset", "discover");
212
+
213
+ // Time range
214
+ if (options.start) {
215
+ params.set("start", options.start);
216
+ if (options.end) {
217
+ params.set("end", options.end);
218
+ } else {
219
+ // If only start, use current time as end
220
+ params.set("end", new Date().toISOString());
221
+ }
222
+ } else if (options.period) {
223
+ params.set("statsPeriod", options.period);
224
+ }
225
+
226
+ params.set("per_page", Math.min(options.limit, 100).toString());
227
+ params.set("sort", "-timestamp");
228
+
229
+ // Add fields
230
+ for (const field of options.fields) {
231
+ params.append("field", field);
232
+ }
233
+
234
+ // Always include project.name for context
235
+ if (!options.fields.includes("project.name")) {
236
+ params.append("field", "project.name");
237
+ }
238
+
239
+ // Build search query
240
+ const queryParts = [];
241
+
242
+ if (options.project) {
243
+ const projectId = await resolveProjectId(options.org, options.project, token);
244
+ params.set("project", projectId);
245
+ }
246
+
247
+ if (options.query) {
248
+ queryParts.push(options.query);
249
+ }
250
+
251
+ if (options.transaction) {
252
+ queryParts.push(`transaction:${options.transaction}`);
253
+ }
254
+
255
+ if (options.level) {
256
+ queryParts.push(`level:${options.level}`);
257
+ }
258
+
259
+ for (const tag of options.tags) {
260
+ // Handle tags[key]:value format
261
+ if (tag.includes(":")) {
262
+ const [key, value] = tag.split(":", 2);
263
+ if (key.startsWith("tags[")) {
264
+ queryParts.push(`${key}:${value}`);
265
+ } else {
266
+ queryParts.push(`tags[${key}]:${value}`);
267
+ }
268
+ }
269
+ }
270
+
271
+ if (queryParts.length > 0) {
272
+ params.set("query", queryParts.join(" "));
273
+ }
274
+
275
+ const url = `${SENTRY_API_BASE}/organizations/${encodeURIComponent(options.org)}/events/?${params.toString()}`;
276
+
277
+ try {
278
+ const data = await fetchJson(url, token);
279
+
280
+ if (options.json) {
281
+ console.log(JSON.stringify(data, null, 2));
282
+ } else {
283
+ console.log(formatOutput(data, options.fields));
284
+ }
285
+ } catch (err) {
286
+ console.error("Error:", err.message);
287
+ process.exit(1);
288
+ }
289
+ }
290
+
291
+ main();
@@ -0,0 +1,234 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { SENTRY_API_BASE, getAuthToken, fetchJson } from "../lib/auth.js";
4
+
5
+ const LOG_FIELDS = ["sentry.item_id", "trace", "sentry.severity", "timestamp", "message"];
6
+
7
+ /**
8
+ * Parse a Sentry logs explorer URL
9
+ * Examples:
10
+ * https://earendil.sentry.io/explore/logs/?project=123&statsPeriod=14d
11
+ * https://sentry.io/organizations/myorg/explore/logs/?project=123
12
+ */
13
+ function parseLogsUrl(urlStr) {
14
+ try {
15
+ const url = new URL(urlStr);
16
+ const params = url.searchParams;
17
+ const result = {};
18
+
19
+ // Extract org from subdomain (earendil.sentry.io) or path (/organizations/myorg/)
20
+ const subdomainMatch = url.hostname.match(/^([^.]+)\.sentry\.io$/);
21
+ if (subdomainMatch && subdomainMatch[1] !== "www") {
22
+ result.org = subdomainMatch[1];
23
+ } else {
24
+ const pathMatch = url.pathname.match(/\/organizations\/([^/]+)\//);
25
+ if (pathMatch) {
26
+ result.org = pathMatch[1];
27
+ }
28
+ }
29
+
30
+ // Extract project ID
31
+ if (params.has("project")) {
32
+ result.project = params.get("project");
33
+ }
34
+
35
+ // Extract time period
36
+ if (params.has("statsPeriod")) {
37
+ result.period = params.get("statsPeriod");
38
+ }
39
+
40
+ // Extract query
41
+ if (params.has("logsQuery")) {
42
+ result.query = params.get("logsQuery");
43
+ }
44
+
45
+ return result;
46
+ } catch {
47
+ return null;
48
+ }
49
+ }
50
+
51
+ function parseArgs(args) {
52
+ const options = {
53
+ org: null,
54
+ project: null,
55
+ query: null,
56
+ period: "24h",
57
+ limit: 100,
58
+ json: false,
59
+ help: false,
60
+ };
61
+
62
+ for (let i = 0; i < args.length; i++) {
63
+ const arg = args[i];
64
+
65
+ if (arg === "--help" || arg === "-h") {
66
+ options.help = true;
67
+ } else if (arg === "--json") {
68
+ options.json = true;
69
+ } else if (arg === "--org" || arg === "-o") {
70
+ options.org = args[++i];
71
+ } else if (arg === "--project" || arg === "-p") {
72
+ options.project = args[++i];
73
+ } else if (arg === "--period" || arg === "-t") {
74
+ options.period = args[++i];
75
+ } else if (arg === "--limit" || arg === "-n") {
76
+ options.limit = parseInt(args[++i], 10);
77
+ } else if (!arg.startsWith("-")) {
78
+ // Check if it's a Sentry URL
79
+ if (arg.includes("sentry.io/") && arg.includes("/logs")) {
80
+ const urlOptions = parseLogsUrl(arg);
81
+ if (urlOptions) {
82
+ if (urlOptions.org) options.org = urlOptions.org;
83
+ if (urlOptions.project) options.project = urlOptions.project;
84
+ if (urlOptions.period) options.period = urlOptions.period;
85
+ if (urlOptions.query) options.query = urlOptions.query;
86
+ }
87
+ } else if (!options.query) {
88
+ options.query = arg;
89
+ }
90
+ }
91
+ }
92
+
93
+ return options;
94
+ }
95
+
96
+ function showHelp() {
97
+ console.log(`Usage: search-logs.js [query|url] [options]
98
+
99
+ Search for logs in Sentry.
100
+
101
+ Arguments:
102
+ query Search query (e.g., "level:error", "user.id:123")
103
+ url Sentry logs explorer URL (extracts org, project, period)
104
+
105
+ Options:
106
+ --org, -o <org> Organization slug (required unless URL provided)
107
+ --project, -p <p> Project slug or ID to filter by
108
+ --period, -t <p> Time period (default: 24h, e.g., 1h, 7d, 90d)
109
+ --limit, -n <n> Max results (default: 100, max: 1000)
110
+ --json Output raw JSON
111
+ -h, --help Show this help
112
+
113
+ Search Query Syntax:
114
+ level:error Filter by log level (trace, debug, info, warn, error, fatal)
115
+ message:*timeout* Search message text
116
+ trace:abc123 Filter by trace ID
117
+ project:my-project Filter by project slug
118
+
119
+ Combine filters: level:error message:*failed*
120
+
121
+ Examples:
122
+ search-logs.js --org myorg
123
+ search-logs.js "level:error" --org myorg --project backend
124
+ search-logs.js "message:*timeout*" --org myorg --period 7d
125
+ search-logs.js --org myorg --limit 50 --json
126
+
127
+ # Use a Sentry URL directly:
128
+ search-logs.js "https://myorg.sentry.io/explore/logs/?project=123&statsPeriod=7d"
129
+ `);
130
+ }
131
+
132
+ function formatLogEntry(entry) {
133
+ const lines = [];
134
+
135
+ const ts = entry.timestamp || "N/A";
136
+ const severity = entry["sentry.severity"] || "info";
137
+ const message = entry.message || "(no message)";
138
+ const trace = entry.trace || null;
139
+
140
+ // Format timestamp for display
141
+ let displayTs = ts;
142
+ try {
143
+ const date = new Date(ts);
144
+ if (!isNaN(date.getTime())) {
145
+ displayTs = date.toISOString().replace("T", " ").slice(0, 19);
146
+ }
147
+ } catch {}
148
+
149
+ // Color-code severity in output
150
+ const severityDisplay = `[${severity.toUpperCase().padEnd(5)}]`;
151
+
152
+ lines.push(`${displayTs} ${severityDisplay} ${message}`);
153
+
154
+ if (trace) {
155
+ lines.push(` trace: ${trace}`);
156
+ }
157
+
158
+ return lines.join("\n");
159
+ }
160
+
161
+ function formatOutput(data) {
162
+ if (!data.data || data.data.length === 0) {
163
+ return "No logs found matching your query.";
164
+ }
165
+
166
+ const lines = [];
167
+ lines.push(`Found ${data.data.length} log entries:\n`);
168
+
169
+ for (const entry of data.data) {
170
+ lines.push(formatLogEntry(entry));
171
+ lines.push("");
172
+ }
173
+
174
+ return lines.join("\n").trimEnd();
175
+ }
176
+
177
+ async function main() {
178
+ const args = process.argv.slice(2);
179
+ const options = parseArgs(args);
180
+
181
+ if (options.help) {
182
+ showHelp();
183
+ process.exit(0);
184
+ }
185
+
186
+ if (!options.org) {
187
+ console.error("Error: --org is required");
188
+ console.error("Run with --help for usage information");
189
+ process.exit(1);
190
+ }
191
+
192
+ const token = getAuthToken();
193
+
194
+ // Build query parameters
195
+ const params = new URLSearchParams();
196
+ params.set("dataset", "logs");
197
+ params.set("statsPeriod", options.period);
198
+ params.set("per_page", Math.min(options.limit, 1000).toString());
199
+ params.set("sort", "-timestamp");
200
+
201
+ // Add fields
202
+ for (const field of LOG_FIELDS) {
203
+ params.append("field", field);
204
+ }
205
+
206
+ // Build search query
207
+ const queryParts = [];
208
+ if (options.project) {
209
+ queryParts.push(`project:${options.project}`);
210
+ }
211
+ if (options.query) {
212
+ queryParts.push(options.query);
213
+ }
214
+ if (queryParts.length > 0) {
215
+ params.set("query", queryParts.join(" "));
216
+ }
217
+
218
+ const url = `${SENTRY_API_BASE}/organizations/${encodeURIComponent(options.org)}/events/?${params.toString()}`;
219
+
220
+ try {
221
+ const data = await fetchJson(url, token);
222
+
223
+ if (options.json) {
224
+ console.log(JSON.stringify(data, null, 2));
225
+ } else {
226
+ console.log(formatOutput(data));
227
+ }
228
+ } catch (err) {
229
+ console.error("Error:", err.message);
230
+ process.exit(1);
231
+ }
232
+ }
233
+
234
+ main();
@@ -0,0 +1,135 @@
1
+ ---
2
+ name: update-changelog
3
+ description: "Update CHANGELOG.md following Keep a Changelog (https://keepachangelog.com/en/1.1.0/)"
4
+ ---
5
+
6
+ # Update Changelog (Keep a Changelog)
7
+
8
+ Update the repository changelog with user-facing changes that landed since the last release.
9
+
10
+ This skill is **explicitly based on** Keep a Changelog v1.1.0:
11
+ https://keepachangelog.com/en/1.1.0/
12
+
13
+ ## Rules (non-negotiable)
14
+
15
+ - **Do not add installation instructions** to the changelog.
16
+ - Only include **notable, user-visible** changes.
17
+ - **Never add raw commit SHAs**. Prefer PR numbers (e.g. `#123`) and/or issue IDs.
18
+ - Add entries **only under `Unreleased`** (unless you are also cutting a release and moving items into a versioned section).
19
+ - Preserve the project’s existing formatting where possible, but align new content to Keep a Changelog.
20
+
21
+ ## File to edit
22
+
23
+ - Prefer `CHANGELOG.md`.
24
+ - If missing, use `CHANGELOG`.
25
+
26
+ ## Step-by-step
27
+
28
+ ### 1) Identify the baseline (last released version)
29
+
30
+ Pick a baseline tag/version to compare against.
31
+
32
+ - If the project uses git tags:
33
+
34
+ ```bash
35
+ git describe --tags --abbrev=0
36
+ ```
37
+
38
+ - If tags are missing/inconsistent, use the newest release section in the changelog as the baseline.
39
+
40
+ ### 2) Collect candidate changes
41
+
42
+ Gather commits/PRs since the baseline and identify user-facing changes.
43
+
44
+ ```bash
45
+ git log <baseline>..HEAD --oneline
46
+ ```
47
+
48
+ If you have PR metadata available (e.g., via GitHub), use it to improve wording and include PR numbers.
49
+
50
+ ### 3) Ensure the changelog structure matches Keep a Changelog
51
+
52
+ At minimum, Keep a Changelog expects:
53
+
54
+ - A top `Unreleased` section
55
+ - Optional subsections under `Unreleased` (and under each release):
56
+ - `Added`, `Changed`, `Deprecated`, `Removed`, `Fixed`, `Security`
57
+
58
+ If `Unreleased` exists but is missing subsections, create only the ones you need for the new entries.
59
+
60
+ ### 4) Add entries under `Unreleased`
61
+
62
+ Classify each change into one of the standard headings:
63
+
64
+ - **Added**: new features
65
+ - **Changed**: changes in existing functionality (including behavior changes)
66
+ - **Deprecated**: soon-to-be removed features
67
+ - **Removed**: removed features
68
+ - **Fixed**: bug fixes
69
+ - **Security**: vulnerability fixes
70
+
71
+ Write entries as **consistent bullet points**:
72
+
73
+ - Start with the category verb (Added/Changed/Deprecated/Removed/Fixed/Security) _only via the section heading_, not inside each bullet.
74
+ - Each bullet should be a single, past-tense sentence fragment.
75
+ - Prefer: `- Added … (#123)` / `- Fixed … (#456)` style (consistent grammar).
76
+
77
+ ### 5) Keep it user-facing
78
+
79
+ Include:
80
+
81
+ - visible behavior changes
82
+ - new CLI flags/API additions
83
+ - bug fixes with clear impact
84
+ - security fixes (without leaking sensitive details)
85
+
86
+ Exclude (unless they change user-visible behavior):
87
+
88
+ - pure refactors
89
+ - internal cleanup
90
+ - dependency bumps with no user impact
91
+ - typo-only doc edits
92
+
93
+ ### 6) Links (only if the file already uses them)
94
+
95
+ Keep a Changelog commonly includes link references at the bottom, e.g.:
96
+
97
+ - `[Unreleased]: <compare link>`
98
+ - `[1.2.3]: <compare link>`
99
+
100
+ If the project already uses these, update them accordingly (don’t introduce link refs if the changelog doesn’t use them).
101
+
102
+ ## Example (consistent Keep a Changelog format)
103
+
104
+ ```markdown
105
+ ## [Unreleased]
106
+
107
+ ### Added
108
+
109
+ - Added widget-level caching for faster dashboard loads. (#123)
110
+
111
+ ### Changed
112
+
113
+ - Changed default retry policy to exponential backoff. (#140)
114
+
115
+ ### Fixed
116
+
117
+ - Fixed crash when importing a config with empty sections. (#155)
118
+
119
+ ## [1.4.0] - 2026-02-01
120
+
121
+ ### Added
122
+
123
+ - Added support for exporting reports as CSV. (#110)
124
+
125
+ ### Fixed
126
+
127
+ - Fixed incorrect timezone handling in scheduled jobs. (#117)
128
+ ```
129
+
130
+ ## Quality checklist
131
+
132
+ - Entries are under **`Unreleased`** and categorized correctly.
133
+ - Wording is consistent (same tense/style across bullets).
134
+ - No installation instructions, no commit SHAs.
135
+ - The changelog remains easy to scan and matches the repo’s established conventions.