@noice-tech/pi-changelog 1.0.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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Noice Tech
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,58 @@
1
+ # @noice-tech/pi-changelog
2
+
3
+ A Pi package for release-aware commits, pull requests, unreleased previews, and public release notes.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ pi install npm:@noice-tech/pi-changelog
9
+ ```
10
+
11
+ Commit the resulting `.pi/settings.json` change so Pi installs the package for collaborators.
12
+
13
+ ## Prerequisites and trust
14
+
15
+ The workflows expect:
16
+
17
+ - Git
18
+ - [GitHub CLI](https://cli.github.com/) authenticated for the repository
19
+ - Bash and `jq`
20
+ - a shell that supports process substitution for the documented `gh api` PR-update flow
21
+
22
+ Pi packages run with the permissions granted to Pi. **Only use this package when you trust it with full system permissions and repository access.** In particular, `/commit` inspects changes, creates commits, pushes branches, and creates or updates GitHub pull requests.
23
+
24
+ ## Commands
25
+
26
+ ```text
27
+ /commit [auto|feat|fix|improve|internal] [optional context]
28
+ /unreleased
29
+ /release-notes <version | from..to>
30
+ /setup-release-notes-style [product/audience/channel notes]
31
+ ```
32
+
33
+ `auto` is accepted only as `/commit` inference input. Changelog and PR classifications are `feat`, `fix`, `improve`, or `internal`.
34
+
35
+ The canonical shared classification rules live in `extensions/changelog/rules.md`.
36
+
37
+ ## Repository-specific release-note style
38
+
39
+ Run `/setup-release-notes-style` to create or refine the only canonical repository-specific convention:
40
+
41
+ ```text
42
+ .pi/release-notes-style.md
43
+ ```
44
+
45
+ `/release-notes` reads that file when present. It otherwise emits a plain Markdown bullet list.
46
+
47
+ ## Release-note output
48
+
49
+ `/release-notes 1.2.3` uses a deterministic slug and writes two separate files:
50
+
51
+ - public copy: `release-notes/1.2.3.md`
52
+ - private review sources: `.pi/tmp/pi-changelog/release-notes-sources/1.2.3.md`
53
+
54
+ The public artifact never contains PR numbers, commit hashes, private URLs, or internal source notes. The `.pi/tmp/` source file is an ephemeral private-review aid. Keep `.pi/tmp/` ignored by Git and unpublished; the prompt does not edit a consumer repository's `.gitignore`.
55
+
56
+ ## Package contents
57
+
58
+ The npm package distributes the Pi manifest together with raw TypeScript and Markdown extension/prompt assets. No build step is required.
@@ -0,0 +1,498 @@
1
+ import type {
2
+ ExtensionAPI,
3
+ ExtensionCommandContext
4
+ } from '@earendil-works/pi-coding-agent'
5
+ import { getMarkdownTheme } from '@earendil-works/pi-coding-agent'
6
+ import { Container, Markdown, Spacer, Text } from '@earendil-works/pi-tui'
7
+ import { readFile } from 'node:fs/promises'
8
+ import { dirname, join } from 'node:path'
9
+ import { fileURLToPath } from 'node:url'
10
+
11
+ const CHANGE_TYPES = ['auto', 'feat', 'fix', 'improve', 'internal'] as const
12
+ type ChangeType = (typeof CHANGE_TYPES)[number]
13
+
14
+ const CHANGE_TYPE_OPTIONS: Array<{ type: ChangeType; label: string }> = [
15
+ {
16
+ type: 'auto',
17
+ label: 'auto - Let commit worker infer from session and diff'
18
+ },
19
+ { type: 'feat', label: 'feat - New user-facing capability' },
20
+ { type: 'fix', label: 'fix - User-facing bug fix' },
21
+ {
22
+ type: 'improve',
23
+ label: 'improve - User-facing refinement/performance/reliability'
24
+ },
25
+ {
26
+ type: 'internal',
27
+ label: 'internal - Infra/tooling/tests/refactor/deps/logging'
28
+ }
29
+ ]
30
+
31
+ const MESSAGE_TYPE = 'noice-changelog-commit-result'
32
+ const PROMPT_MESSAGE_TYPE = 'noice-changelog-commit-worker-prompt'
33
+ const STATUS_KEY = 'noice-changelog'
34
+
35
+ type CommitDisplayStatus = 'ok' | 'cancelled' | 'failed'
36
+
37
+ interface CommitResultDetails {
38
+ changeType?: ChangeType
39
+ userContext?: string
40
+ workerLeafId?: string | null
41
+ status?: CommitDisplayStatus
42
+ }
43
+
44
+ let commitWorkerRunning = false
45
+ let agentEndWaiter: ((messages: unknown[]) => void) | undefined
46
+ let latestCommitWorkerMessages: unknown[] | undefined
47
+
48
+ export default function noiceChangelogExtension(pi: ExtensionAPI) {
49
+ pi.on('agent_end', (event) => {
50
+ if (commitWorkerRunning) {
51
+ latestCommitWorkerMessages = event.messages
52
+ }
53
+ agentEndWaiter?.(event.messages)
54
+ agentEndWaiter = undefined
55
+ })
56
+
57
+ pi.on('context', (event) => {
58
+ return {
59
+ messages: event.messages.filter((message) => {
60
+ const customType = (message as { customType?: string }).customType
61
+ if (customType === MESSAGE_TYPE) return false
62
+ if (customType === PROMPT_MESSAGE_TYPE && !commitWorkerRunning)
63
+ return false
64
+ return true
65
+ })
66
+ }
67
+ })
68
+
69
+ async function sendResultAtSourceLeaf(
70
+ ctx: ExtensionCommandContext,
71
+ sourceLeafId: string | null | undefined,
72
+ message: {
73
+ customType: string
74
+ content: string
75
+ display: boolean
76
+ details?: CommitResultDetails
77
+ }
78
+ ) {
79
+ // `agent_end` fires before the session has fully left streaming mode. If we
80
+ // send while streaming, pi treats this as steering/follow-up input instead
81
+ // of appending a visible custom message, so it may only show on the next
82
+ // user turn. Wait until idle before writing the result entry.
83
+ if (!ctx.isIdle()) {
84
+ await ctx.waitForIdle()
85
+ }
86
+
87
+ pi.sendMessage(message)
88
+
89
+ // Keep the result attached to the source point, but leave the active leaf
90
+ // at the original source so the next user message branches from there.
91
+ const currentLeafId = ctx.sessionManager.getLeafId()
92
+ if (sourceLeafId && currentLeafId && currentLeafId !== sourceLeafId) {
93
+ await ctx.navigateTree(sourceLeafId, { summarize: false })
94
+ }
95
+ }
96
+
97
+ pi.registerMessageRenderer<CommitResultDetails>(
98
+ MESSAGE_TYPE,
99
+ (message, _options, theme) => {
100
+ const details = message.details
101
+ const c = new Container()
102
+ const displayStatus = getDisplayStatus(
103
+ typeof message.content === 'string' ? message.content : '',
104
+ details?.status
105
+ )
106
+ const statusLabel =
107
+ displayStatus === 'cancelled'
108
+ ? theme.fg('warning', 'cancelled')
109
+ : displayStatus === 'failed'
110
+ ? theme.fg('error', 'failed')
111
+ : theme.fg('success', 'ok')
112
+
113
+ c.addChild(
114
+ new Text(
115
+ `${statusLabel} ${theme.fg('toolTitle', theme.bold('commit'))}${details?.changeType ? ` ${theme.fg('accent', details.changeType)}` : ''}`,
116
+ 0,
117
+ 0
118
+ )
119
+ )
120
+
121
+ if (details?.userContext) {
122
+ c.addChild(
123
+ new Text(theme.fg('dim', `Context: ${details.userContext}`), 0, 0)
124
+ )
125
+ }
126
+
127
+ c.addChild(new Spacer(1))
128
+ c.addChild(
129
+ new Markdown(
130
+ typeof message.content === 'string' ? message.content : '',
131
+ 0,
132
+ 0,
133
+ getMarkdownTheme()
134
+ )
135
+ )
136
+
137
+ if (details?.workerLeafId) {
138
+ c.addChild(new Spacer(1))
139
+ c.addChild(
140
+ new Text(
141
+ theme.fg('dim', `Worker branch: ${details.workerLeafId}`),
142
+ 0,
143
+ 0
144
+ )
145
+ )
146
+ }
147
+
148
+ return c
149
+ }
150
+ )
151
+
152
+ pi.registerCommand('commit', {
153
+ description:
154
+ 'Commit changes and create/update PR. Usage: /commit <changeType> <what was done>',
155
+ getArgumentCompletions: getCommitArgumentCompletions,
156
+ handler: async (args, ctx) => {
157
+ if (commitWorkerRunning) {
158
+ ctx.ui.notify('Commit worker is already running', 'warning')
159
+ return
160
+ }
161
+
162
+ await ctx.waitForIdle()
163
+
164
+ const parsed = await resolveChangeTypeAndContext(args, ctx)
165
+ if (!parsed) {
166
+ return
167
+ }
168
+
169
+ const startLeafId = ctx.sessionManager.getLeafId()
170
+ const prompt = await buildWorkerPrompt(parsed.changeType, parsed.context)
171
+
172
+ commitWorkerRunning = true
173
+ ctx.ui.setStatus(STATUS_KEY, 'running in session branch...')
174
+ ctx.ui.notify(`Starting commit worker (${parsed.changeType})`, 'info')
175
+
176
+ try {
177
+ const agentEnd = waitForNextAgentEndAfterIdle(ctx)
178
+ latestCommitWorkerMessages = undefined
179
+ pi.sendMessage(
180
+ {
181
+ customType: PROMPT_MESSAGE_TYPE,
182
+ content: prompt,
183
+ display: false,
184
+ details: {
185
+ changeType: parsed.changeType,
186
+ userContext: parsed.context
187
+ }
188
+ },
189
+ { triggerTurn: true, deliverAs: 'followUp' }
190
+ )
191
+ const messages = await agentEnd
192
+
193
+ const workerLeafId = ctx.sessionManager.getLeafId()
194
+ const workerPromptIndex = findLastCustomMessageIndex(
195
+ messages,
196
+ PROMPT_MESSAGE_TYPE
197
+ )
198
+ const summary =
199
+ workerPromptIndex >= 0
200
+ ? extractLastAssistantText(messages, workerPromptIndex)
201
+ : ''
202
+ const assistantError =
203
+ workerPromptIndex >= 0
204
+ ? extractLastAssistantError(messages, workerPromptIndex)
205
+ : undefined
206
+
207
+ if (assistantError) {
208
+ if (startLeafId && workerLeafId && workerLeafId !== startLeafId) {
209
+ await ctx.navigateTree(startLeafId, { summarize: false })
210
+ }
211
+ await sendResultAtSourceLeaf(ctx, startLeafId, {
212
+ customType: MESSAGE_TYPE,
213
+ content: formatWorkerErrorResult(assistantError, summary),
214
+ display: true,
215
+ details: {
216
+ changeType: parsed.changeType,
217
+ userContext: parsed.context,
218
+ workerLeafId,
219
+ status: 'failed'
220
+ }
221
+ })
222
+ ctx.ui.notify(`Commit worker failed:\n${assistantError}`, 'error')
223
+ return
224
+ }
225
+
226
+ if (!summary) {
227
+ if (startLeafId && workerLeafId && workerLeafId !== startLeafId) {
228
+ await ctx.navigateTree(startLeafId, { summarize: false })
229
+ }
230
+ await sendResultAtSourceLeaf(ctx, startLeafId, {
231
+ customType: MESSAGE_TYPE,
232
+ content:
233
+ 'status: cancelled\nnotes: Commit command was cancelled before the worker produced a result.',
234
+ display: true,
235
+ details: {
236
+ changeType: parsed.changeType,
237
+ userContext: parsed.context,
238
+ workerLeafId,
239
+ status: 'cancelled'
240
+ }
241
+ })
242
+ ctx.ui.notify('Commit command cancelled', 'warning')
243
+ return
244
+ }
245
+
246
+ if (startLeafId && workerLeafId && workerLeafId !== startLeafId) {
247
+ const nav = await ctx.navigateTree(startLeafId, { summarize: false })
248
+ if (nav.cancelled) {
249
+ pi.sendMessage({
250
+ customType: MESSAGE_TYPE,
251
+ content:
252
+ 'status: cancelled\nnotes: Commit worker finished, but returning to the original branch was cancelled.',
253
+ display: true,
254
+ details: {
255
+ changeType: parsed.changeType,
256
+ userContext: parsed.context,
257
+ workerLeafId,
258
+ status: 'cancelled'
259
+ }
260
+ })
261
+ ctx.ui.notify(
262
+ 'Commit finished, but tree navigation was cancelled',
263
+ 'warning'
264
+ )
265
+ return
266
+ }
267
+ }
268
+
269
+ const displayStatus = getDisplayStatus(summary)
270
+ await sendResultAtSourceLeaf(ctx, startLeafId, {
271
+ customType: MESSAGE_TYPE,
272
+ content: summary,
273
+ display: true,
274
+ details: {
275
+ changeType: parsed.changeType,
276
+ userContext: parsed.context,
277
+ workerLeafId,
278
+ status: displayStatus
279
+ }
280
+ })
281
+ ctx.ui.notify(
282
+ formatCommitNotification(summary, displayStatus),
283
+ displayStatus === 'failed'
284
+ ? 'error'
285
+ : displayStatus === 'cancelled'
286
+ ? 'warning'
287
+ : 'info'
288
+ )
289
+ } catch (error) {
290
+ const message = error instanceof Error ? error.message : String(error)
291
+ if (startLeafId)
292
+ await ctx.navigateTree(startLeafId, { summarize: false })
293
+ await sendResultAtSourceLeaf(ctx, startLeafId, {
294
+ customType: MESSAGE_TYPE,
295
+ content: `Commit worker failed: ${message}`,
296
+ display: true,
297
+ details: {
298
+ changeType: parsed.changeType,
299
+ userContext: parsed.context,
300
+ status: 'failed'
301
+ }
302
+ })
303
+ ctx.ui.notify(`Commit worker failed:\n${message}`, 'error')
304
+ } finally {
305
+ ctx.ui.setStatus(STATUS_KEY, undefined)
306
+ commitWorkerRunning = false
307
+ latestCommitWorkerMessages = undefined
308
+ }
309
+ }
310
+ })
311
+ }
312
+
313
+ function getCommitArgumentCompletions(prefix: string) {
314
+ const trimmedStart = prefix.trimStart()
315
+ const leadingWhitespace = prefix.slice(0, prefix.length - trimmedStart.length)
316
+ const [firstWord = ''] = trimmedStart.split(/\s+/)
317
+ const isTypingDescription = /^\S+\s/.test(trimmedStart)
318
+
319
+ if (!isTypingDescription) {
320
+ const matches = CHANGE_TYPE_OPTIONS.filter((option) =>
321
+ option.type.startsWith(firstWord)
322
+ )
323
+ return matches.length > 0
324
+ ? matches.map((option) => ({
325
+ value: `${leadingWhitespace}${option.type} `,
326
+ label: option.label
327
+ }))
328
+ : null
329
+ }
330
+
331
+ if (!isChangeType(firstWord)) return null
332
+
333
+ const whatWasDone = trimmedStart.slice(firstWord.length).trim()
334
+ return [
335
+ {
336
+ value: prefix,
337
+ label: whatWasDone
338
+ ? `What was done: "${whatWasDone}"`
339
+ : 'Say what was done — rough wording is fine; leave blank to infer from session/diff'
340
+ }
341
+ ]
342
+ }
343
+
344
+ async function resolveChangeTypeAndContext(
345
+ args: string | undefined,
346
+ ctx: ExtensionCommandContext
347
+ ): Promise<{ changeType: ChangeType; context: string } | null> {
348
+ const trimmedArgs = args?.trim() ?? ''
349
+ const [firstWord = '', ...rest] = trimmedArgs.split(/\s+/)
350
+
351
+ if (isChangeType(firstWord)) {
352
+ return { changeType: firstWord, context: rest.join(' ').trim() }
353
+ }
354
+
355
+ const selected = await ctx.ui.select(
356
+ 'Change type',
357
+ CHANGE_TYPE_OPTIONS.map((option) => option.label)
358
+ )
359
+ if (!selected) return null
360
+
361
+ const option = CHANGE_TYPE_OPTIONS.find((item) =>
362
+ selected.startsWith(item.type)
363
+ )
364
+ if (!option) return null
365
+
366
+ return { changeType: option.type, context: trimmedArgs }
367
+ }
368
+
369
+ function isChangeType(value: string): value is ChangeType {
370
+ return CHANGE_TYPES.includes(value as ChangeType)
371
+ }
372
+
373
+ async function buildWorkerPrompt(changeType: ChangeType, userContext: string) {
374
+ const extensionDir = dirname(fileURLToPath(import.meta.url))
375
+ const [template, rules] = await Promise.all([
376
+ readFile(join(extensionDir, 'worker-prompt.md'), 'utf-8'),
377
+ readFile(join(extensionDir, 'rules.md'), 'utf-8')
378
+ ])
379
+
380
+ return template
381
+ .replaceAll('{{changeType}}', changeType)
382
+ .replaceAll('{{userContext}}', userContext || '(none)')
383
+ .replaceAll('{{rules}}', rules)
384
+ }
385
+
386
+ function waitForNextAgentEndAfterIdle(ctx: ExtensionCommandContext) {
387
+ return new Promise<unknown[]>((resolve) => {
388
+ agentEndWaiter = (messages) => {
389
+ void (async () => {
390
+ // `agent_end` also fires for transient provider failures that Pi may
391
+ // auto-retry. Wait until the whole agent run is idle, then use the
392
+ // latest worker messages captured by the global `agent_end` listener.
393
+ if (!ctx.isIdle()) {
394
+ await ctx.waitForIdle()
395
+ }
396
+ resolve(latestCommitWorkerMessages ?? messages)
397
+ })()
398
+ }
399
+ })
400
+ }
401
+
402
+ function getDisplayStatus(
403
+ content: string,
404
+ explicit?: CommitDisplayStatus
405
+ ): CommitDisplayStatus {
406
+ if (explicit) return explicit
407
+
408
+ const firstStatus = content.match(/^status:\s*(\S+)/im)?.[1]?.toLowerCase()
409
+ if (firstStatus === 'failed') return 'failed'
410
+ if (firstStatus === 'cancelled' || firstStatus === 'canceled') {
411
+ return 'cancelled'
412
+ }
413
+
414
+ return 'ok'
415
+ }
416
+
417
+ function formatWorkerErrorResult(error: string, partialSummary: string) {
418
+ const partial = partialSummary.trim()
419
+ return [
420
+ 'status: failed',
421
+ `notes: Commit worker errored${partial ? ' after a partial response' : ' before producing a result'}.`,
422
+ `error: ${error}`,
423
+ partial ? `\nPartial response:\n${partial}` : ''
424
+ ]
425
+ .filter(Boolean)
426
+ .join('\n')
427
+ }
428
+
429
+ function formatCommitNotification(
430
+ summary: string,
431
+ status: CommitDisplayStatus
432
+ ): string {
433
+ const title =
434
+ status === 'failed'
435
+ ? 'Commit worker failed'
436
+ : status === 'cancelled'
437
+ ? 'Commit command cancelled'
438
+ : 'Commit worker finished'
439
+ const trimmedSummary = summary.trim()
440
+ return trimmedSummary ? `${title}:\n${trimmedSummary}` : title
441
+ }
442
+
443
+ function findLastCustomMessageIndex(messages: unknown[], customType: string) {
444
+ for (let index = messages.length - 1; index >= 0; index--) {
445
+ const message = messages[index] as { customType?: string }
446
+ if (message.customType === customType) return index
447
+ }
448
+
449
+ return -1
450
+ }
451
+
452
+ function extractLastAssistantText(messages: unknown[], afterIndex = -1) {
453
+ const message = findLastAssistantMessage(messages, afterIndex)
454
+ return message ? extractTextFromContent(message.content).trim() : ''
455
+ }
456
+
457
+ function extractLastAssistantError(messages: unknown[], afterIndex = -1) {
458
+ const message = findLastAssistantMessage(messages, afterIndex)
459
+ if (message?.stopReason !== 'error') return undefined
460
+
461
+ return message.errorMessage?.trim() || 'Unknown provider error'
462
+ }
463
+
464
+ function findLastAssistantMessage(messages: unknown[], afterIndex = -1) {
465
+ for (let index = messages.length - 1; index > afterIndex; index--) {
466
+ const message = messages[index] as {
467
+ role?: string
468
+ content?: unknown
469
+ stopReason?: string
470
+ errorMessage?: string
471
+ }
472
+ if (message.role === 'assistant') return message
473
+ }
474
+
475
+ return undefined
476
+ }
477
+
478
+ function extractTextFromContent(content: unknown): string {
479
+ if (typeof content === 'string') return content
480
+ if (!Array.isArray(content)) return ''
481
+
482
+ return content
483
+ .map((part) => {
484
+ if (
485
+ part &&
486
+ typeof part === 'object' &&
487
+ 'type' in part &&
488
+ part.type === 'text' &&
489
+ 'text' in part &&
490
+ typeof part.text === 'string'
491
+ ) {
492
+ return part.text
493
+ }
494
+ return ''
495
+ })
496
+ .filter(Boolean)
497
+ .join('\n')
498
+ }
@@ -0,0 +1,107 @@
1
+ # Changelog rules
2
+
3
+ ## Goal
4
+
5
+ Changelogs should show that products are moving without turning every release into forced marketing.
6
+
7
+ GitHub Releases are internal shipping records. Public changelog/social text is derived from PR changelog summaries and must stand alone because repositories may be private.
8
+
9
+ ## PR title prefixes
10
+
11
+ Every PR title should start with exactly one prefix:
12
+
13
+ - `feat:` new user-facing capability
14
+ - `fix:` user-facing bug fix
15
+ - `improve:` user-facing refinement, UX, performance, or reliability improvement
16
+ - `internal:` infra, CI, tooling, refactor, tests, dependencies, logging, or other non-user-facing work
17
+
18
+ These prefixes are release intent, not conventional commits.
19
+
20
+ ## Prefix rules
21
+
22
+ - `feat:` means users can do something new.
23
+ - `fix:` means a user-visible bug or broken behavior was corrected.
24
+ - `improve:` means an existing user-facing workflow became clearer, faster, smoother, more reliable, or easier to use.
25
+ - `internal:` means the work may matter to development/release/reliability but is not a public product change.
26
+
27
+ Do not use `fix:` for technical-only fixes. Use `internal:` for TypeScript fixes, build fixes, CI fixes, test fixes, dependency fixes, refactor corrections, and internal error handling unless the corrected behavior is directly user-visible.
28
+
29
+ ## PR titles vs public summaries
30
+
31
+ PR titles are classification and review metadata. They are not the public changelog source.
32
+
33
+ Good PR titles are concrete:
34
+
35
+ - `feat: add branded end cards for free exports`
36
+ - `fix: prevent hidden tracks from rendering`
37
+ - `improve: speed up waveform rendering for long projects`
38
+ - `internal: centralize release deployment workflow`
39
+
40
+ Avoid vague value-prop titles:
41
+
42
+ - `feat: help users create better results`
43
+ - `improve: improve editor experience`
44
+ - `fix: make rendering better`
45
+
46
+ ## PR changelog section
47
+
48
+ Every PR body should include:
49
+
50
+ ```md
51
+ ## Changelog
52
+
53
+ Public summary:
54
+
55
+ - ...
56
+
57
+ Context:
58
+
59
+ - ...
60
+ ```
61
+
62
+ For `feat:`, `fix:`, and `improve:`, `Public summary` should contain one specific standalone user-facing sentence. Write it as if it may become one bullet in a public release post.
63
+
64
+ For `internal:`, write exactly:
65
+
66
+ ```md
67
+ Public summary:
68
+
69
+ - None.
70
+ ```
71
+
72
+ `Context` can include product/business details that help future release generation. It may be internal.
73
+
74
+ ## Public changelog source priority
75
+
76
+ Release changelog generation should use sources in this order:
77
+
78
+ 1. PR body `## Changelog` → `Public summary`
79
+ 2. PR body `## Changelog` → `Context`
80
+ 3. GitHub Release body
81
+ 4. PR title, only for classification or fallback
82
+ 5. Commit messages, only as a last fallback
83
+
84
+ If `Public summary` is `None`, skip that PR for public changelog output unless there is clear user-visible impact elsewhere in the release notes.
85
+
86
+ ## Public changelog output rules
87
+
88
+ Public changelog/social text:
89
+
90
+ - must not link to GitHub, PRs, commits, releases, or private repo artifacts
91
+ - must not include PR numbers, commit hashes, or private URLs
92
+ - should be standalone and understandable without release links
93
+ - should use only meaningful user-facing changes
94
+ - should make the product feel actively improved
95
+ - should not force hype or flexing
96
+ - should not include internal implementation details
97
+ - should avoid generic phrases like `improved experience`, `seamless`, `empower`, and `game-changing`
98
+
99
+ The same text should work for Discord and X.
100
+
101
+ ## GitHub Releases vs public changelog vs updates
102
+
103
+ - GitHub Release: internal factual shipping record.
104
+ - Public changelog/social post: short movement signal derived from public summaries.
105
+ - Authored product update: bigger product story with screenshots/video/narrative when a release has enough substance.
106
+
107
+ Not every release needs an authored product update.
@@ -0,0 +1,127 @@
1
+ You are the changelog commit worker.
2
+
3
+ You are running in a temporary branch of the user's active Pi session. Use the provided change type and short user description as the primary source for commit/PR wording. Use git diff only to verify that the description matches the actual changes and to catch important omissions; do not try to rediscover or guess the change from the diff when a description is provided.
4
+
5
+ Task:
6
+ Commit current changes and create or update the GitHub PR.
7
+
8
+ Command signature:
9
+ /commit ${changeType} ${whatWasDoneShort}
10
+
11
+ Selected change type:
12
+ {{changeType}}
13
+
14
+ What was done, in the user's words:
15
+ {{userContext}}
16
+
17
+ Before choosing commit messages, PR title, or PR changelog text, read and follow these rules exactly:
18
+
19
+ {{rules}}
20
+
21
+ Workflow:
22
+
23
+ 1. Inspect git status, current branch, diff, branch commits, candidate base branch, and existing PR.
24
+ 2. If a PR exists, read its current title, base branch, and full body before deciding what to change.
25
+ 3. If there are no changes to commit, update the PR body if useful, otherwise report no-op.
26
+ 4. If on main, create a branch.
27
+ 5. Commit current changes with a good prefixed commit message.
28
+ 6. Push the branch.
29
+ 7. If no PR exists for the branch, create one against the detected base branch.
30
+ 8. If PR exists, update title/body to reflect the full branch while preserving useful existing PR description content and existing base branch.
31
+ 9. When creating a PR body, always write the final markdown body to a file in a temporary directory and pass it to GitHub CLI with `--body-file`; do not pass markdown through `--body`.
32
+ 10. When updating an existing PR, avoid `gh pr edit`; it can fail on some repositories because GitHub CLI queries deprecated Projects Classic GraphQL fields. Use the REST API fallback described below instead.
33
+
34
+ Rules:
35
+
36
+ - Use the selected change type as user intent, unless it clearly contradicts the provided description and diff.
37
+ - If selected type is `auto`, infer the type from the provided description first, then session context and diff, using the changelog rules.
38
+ - Treat `whatWasDoneShort` as the user's rough wording. Convert awkward, terse, or informal language into clear PR title, commit message, and changelog wording according to the changelog rules.
39
+ - Prefer the user's description over diff-derived wording. Use the diff to verify accuracy and specificity, not to invent a different story.
40
+ - If the user's description is missing or too vague, use session context and diff as fallback.
41
+ - `fix:` is only for user-visible bug fixes. Technical-only fixes must use `internal:`.
42
+ - Commit message describes the current change using the user's description refined by the rules.
43
+ - PR title describes the full branch using the user's description refined by the rules.
44
+ - PR title must start with exactly one prefix from the changelog rules.
45
+ - PR title is classification/review metadata, not the public changelog summary.
46
+ - PR body `## Changelog` → `Public summary` is the canonical source for future public changelog generation.
47
+ - Do not use vague value-prop titles.
48
+ - Do not modify source files unless absolutely required to complete commit/PR metadata.
49
+ - Do not run broad validation unless it is obviously cheap and relevant.
50
+
51
+ PR base branch handling:
52
+
53
+ - Never rely on `gh pr create`'s implicit base branch; it usually defaults to the repository default branch, which may be wrong for branches based on release, staging, or feature branches.
54
+ - If an existing PR exists, preserve its current `baseRefName`. Do not change the PR base unless the user explicitly asked for that.
55
+ - Before creating a new PR, determine the intended base branch and pass it explicitly with `--base "$base_branch"`.
56
+ - Prefer a user-configured base if available, for example `git config --get branch.$current_branch.gh-merge-base` or `git config --get branch.$current_branch.noice-base`.
57
+ - Otherwise fetch remote branches and infer the likely base from the branch ancestry. Compare candidate remote branches such as the repository default branch, `develop`, `staging`, `release/*`, and other long-lived branches, excluding the current head branch. Prefer the candidate with the most recent merge-base with `HEAD`; this usually recovers the branch the work was created from.
58
+ - If the inferred base is not the repository default branch, mention that in the final notes. If the base is ambiguous, fail with a concise note asking the user to rerun with the intended base instead of opening a PR to the wrong branch.
59
+ - Use this shape when creating a PR:
60
+
61
+ ```sh
62
+ gh pr create --base "$base_branch" --title "$title" --body-file "$body_file"
63
+ ```
64
+
65
+ PR body handling:
66
+
67
+ - Always prepare the final PR body as a markdown file in a temporary directory, for example:
68
+ - `tmpdir=$(mktemp -d)`
69
+ - `body_file="$tmpdir/pr-body.md"`
70
+ - write the markdown body to `$body_file`
71
+ - Create PRs with `gh pr create --title "..." --body-file "$body_file" ...`.
72
+ - Update existing PRs with `gh api repos/OWNER/REPO/pulls/PR_NUMBER -X PATCH`; do not use `gh pr edit` for PR updates.
73
+ - Do not use `--body` for markdown PR bodies; it can cause shell quoting and formatting problems.
74
+ - For existing PR updates, use this safe REST flow and replace only the title/body values:
75
+
76
+ ```sh
77
+ repo=$(gh repo view --json nameWithOwner --jq .nameWithOwner)
78
+ pr_number=$(gh pr view --json number --jq .number)
79
+ title='improve: concise PR title here'
80
+ title_json=$(jq -Rn --arg value "$title" '$value')
81
+ body_json=$(jq -Rs . < "$body_file")
82
+ gh api "repos/$repo/pulls/$pr_number" \
83
+ -X PATCH \
84
+ --input <(printf '{"title":%s,"body":%s}' "$title_json" "$body_json") \
85
+ --jq '.html_url'
86
+ ```
87
+
88
+ - If `gh api` succeeds, do not print the full JSON response; use `--jq` to return a concise confirmation such as `.html_url`.
89
+ - If creating a new PR, use this body format:
90
+
91
+ ```md
92
+ ## Summary
93
+
94
+ - ...
95
+
96
+ ## Changelog
97
+
98
+ Public summary:
99
+
100
+ - For feat/fix/improve: one specific standalone user-facing sentence.
101
+ - For internal: None.
102
+
103
+ Context:
104
+
105
+ - Useful context for future release generation.
106
+
107
+ ## Verification
108
+
109
+ - Commands run, or `Not run`.
110
+ ```
111
+
112
+ - If updating an existing PR, enhance the existing body instead of blindly replacing it.
113
+ - Preserve useful existing sections, reviewer notes, checklists, screenshots/videos, testing notes, linked issues, and any manually written context unless they are now inaccurate.
114
+ - Ensure the final body still contains `## Summary`, `## Changelog`, and `## Verification`; add any missing sections in the standard format.
115
+ - Refresh only the parts that need to reflect the full branch, especially `## Changelog` → `Public summary`, `Context`, and verification.
116
+ - Remove or rewrite stale content only when the current diff/branch proves it is wrong.
117
+
118
+ Final output:
119
+ Return exactly five lines, with real values only:
120
+
121
+ - Line 1 starts with `status:` followed by exactly one of these words: `committed`, `updated_pr`, `no_changes`, or `failed`.
122
+ - Line 2 starts with `commit:` followed by the actual short SHA and commit message, or `none`.
123
+ - Line 3 starts with `pr:` followed by the actual PR number, title, and URL, or `none`.
124
+ - Line 4 starts with `verification:` followed by commands run, or `Not run`.
125
+ - Line 5 starts with `notes:` followed by important caveats, or `none`.
126
+
127
+ Do not include a fenced code block. Do not explain the format. Do not include the words `one of`, `actual`, `followed by`, or any placeholder text.
package/package.json ADDED
@@ -0,0 +1,53 @@
1
+ {
2
+ "name": "@noice-tech/pi-changelog",
3
+ "version": "1.0.0",
4
+ "description": "Pi changelog workflow for commits, pull requests, and public release notes.",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "keywords": [
8
+ "pi-package",
9
+ "pi",
10
+ "changelog",
11
+ "release-notes"
12
+ ],
13
+ "files": [
14
+ "extensions/**/*.ts",
15
+ "extensions/**/*.md",
16
+ "prompts/**/*.md",
17
+ "README.md",
18
+ "LICENSE"
19
+ ],
20
+ "publishConfig": {
21
+ "access": "public"
22
+ },
23
+ "repository": {
24
+ "type": "git",
25
+ "url": "git+https://github.com/noice-tech/noice-pi.git",
26
+ "directory": "packages/changelog"
27
+ },
28
+ "homepage": "https://github.com/noice-tech/noice-pi/tree/main/packages/changelog#readme",
29
+ "bugs": {
30
+ "url": "https://github.com/noice-tech/noice-pi/issues"
31
+ },
32
+ "peerDependencies": {
33
+ "@earendil-works/pi-coding-agent": "*",
34
+ "@earendil-works/pi-tui": "*"
35
+ },
36
+ "devDependencies": {
37
+ "@earendil-works/pi-coding-agent": "0.80.10",
38
+ "@earendil-works/pi-tui": "0.80.10",
39
+ "typescript": "6.0.3"
40
+ },
41
+ "pi": {
42
+ "extensions": [
43
+ "./extensions/changelog/index.ts"
44
+ ],
45
+ "prompts": [
46
+ "./prompts/*.md"
47
+ ]
48
+ },
49
+ "scripts": {
50
+ "typecheck": "tsc --noEmit --allowImportingTsExtensions --moduleResolution bundler --module ESNext --target ES2022 --skipLibCheck extensions/changelog/index.ts",
51
+ "test": "node ../../scripts/smoke-pack.mjs @noice-tech/pi-changelog packages/changelog"
52
+ }
53
+ }
@@ -0,0 +1,79 @@
1
+ ---
2
+ description: generate public release notes/social copy from a GitHub release or release range
3
+ argument-hint: '<version | from..to>'
4
+ ---
5
+
6
+ Generate public release notes/social copy for the requested GitHub release or release range, and write public copy and private review sources to separate Markdown files.
7
+
8
+ Input:
9
+ `$ARGUMENTS`
10
+
11
+ ## Output files
12
+
13
+ Derive `<slug>` from `$ARGUMENTS` by trimming surrounding whitespace, quotes, and backticks; keeping letters, numbers, dots, underscores, and hyphens; replacing every other character (including spaces and slashes) with `-`; collapsing repeated hyphens; and using `release-notes` if the result is empty.
14
+
15
+ Create or overwrite both files:
16
+
17
+ - public copy: `release-notes/<slug>.md`
18
+ - private review sources: `.pi/tmp/pi-changelog/release-notes-sources/<slug>.md`
19
+
20
+ Treat `.pi/tmp/` as ephemeral private workspace data that should remain ignored by Git and unpublished. Do not edit or create the consumer repository's `.gitignore`; only write the requested output files. Never put private source notes in the public artifact. After writing both files, reply only with both paths and a one-sentence summary of what was written.
21
+
22
+ ## Repository-specific style
23
+
24
+ If `.pi/release-notes-style.md` exists, read it before writing public copy. It is the only canonical repository-specific style convention. It may refine audience, voice, terminology, formatting, examples, emoji use, and bullet style, but it must not override the classification, source-priority, or privacy rules below.
25
+
26
+ ## Shared changelog rules
27
+
28
+ PR title prefixes are `feat:`, `fix:`, `improve:`, and `internal:`.
29
+
30
+ - `feat:` means users can do something new.
31
+ - `fix:` means a user-visible bug or broken behavior was corrected.
32
+ - `improve:` means an existing user-facing workflow became clearer, faster, smoother, more reliable, or easier to use.
33
+ - `internal:` means the work may matter to development, release, or reliability but is not a public product change.
34
+ - Technical-only TypeScript, build, CI, test, dependency, refactor, and internal error-handling fixes are `internal:` unless corrected behavior is directly user-visible.
35
+ - PR titles classify work; they are not the public changelog source.
36
+ - PR body `## Changelog` → `Public summary` is the canonical public changelog atom.
37
+
38
+ The input can be a single version/tag or a `from..to` range. Resolve bare and `v`-prefixed versions to Git tags. Use `gh` and `git` to inspect GitHub release bodies, tags, included merged PRs, and their titles and bodies.
39
+
40
+ Use sources in this order:
41
+
42
+ 1. PR body `## Changelog` → `Public summary`
43
+ 2. PR body `## Changelog` → `Context`
44
+ 3. GitHub Release body
45
+ 4. PR title, only for classification or fallback
46
+ 5. Commit messages, only as a last fallback
47
+
48
+ Include meaningful `feat:`, `fix:`, and `improve:` changes whose public summary is not `None`. Skip `internal:` changes and summaries of `None`. Be conservative with fixes.
49
+
50
+ ## Public copy
51
+
52
+ The public file must contain no GitHub or release links, PR numbers, commit hashes, private URLs, private artifact references, internal source notes, or implementation details. It must stand alone.
53
+
54
+ Follow `.pi/release-notes-style.md` when present. Otherwise write only a plain Markdown bullet list of specific public summaries, with no heading, intro, version line, emoji, CTA, links, or social framing.
55
+
56
+ If there are no meaningful public changes, write exactly:
57
+
58
+ ```text
59
+ No public changelog suggested for this release.
60
+ ```
61
+
62
+ ## Private review sources
63
+
64
+ Write source review details only to `.pi/tmp/pi-changelog/release-notes-sources/<slug>.md` in this format:
65
+
66
+ ```md
67
+ # Internal source notes
68
+
69
+ Used:
70
+
71
+ - <PR number/title or release source>
72
+
73
+ Skipped:
74
+
75
+ - <PR number/title and reason>
76
+
77
+ Potential authored product update: yes/no
78
+ Reason: <short reason>
79
+ ```
@@ -0,0 +1,77 @@
1
+ ---
2
+ description: create or refine repo-specific release notes voice and formatting guidance
3
+ argument-hint: '[product/audience/channel notes]'
4
+ ---
5
+
6
+ Create or refine the repository's only canonical release notes style file:
7
+
8
+ `.pi/release-notes-style.md`
9
+
10
+ Input notes from the user:
11
+ `$ARGUMENTS`
12
+
13
+ Write concise guidance for humans and `/release-notes` that defines this product's public release notes audience, voice, terminology, and formatting.
14
+
15
+ Do not duplicate package-owned rules about `feat`, `fix`, `improve`, or `internal` classification; inspecting GitHub releases, PRs, tags, commits, or ranges; source priority; internal source notes; privacy; or PR body structure.
16
+
17
+ Process:
18
+
19
+ 1. Inspect the repository README and product documentation for audience, language, and public tone.
20
+ 2. If `.pi/release-notes-style.md` exists, refine it without replacing useful guidance.
21
+ 3. Create `.pi/` if needed.
22
+ 4. Write `.pi/release-notes-style.md`.
23
+ 5. Reply with the path and a concise summary.
24
+
25
+ The style file should usually include:
26
+
27
+ ```md
28
+ # Release notes style
29
+
30
+ These instructions are for humans and for `/release-notes`.
31
+
32
+ ## Audience
33
+
34
+ <who the public changelog is for>
35
+
36
+ ## Voice
37
+
38
+ Use:
39
+
40
+ - ...
41
+
42
+ Avoid:
43
+
44
+ - ...
45
+
46
+ ## Product language
47
+
48
+ Use:
49
+
50
+ - ...
51
+
52
+ Avoid:
53
+
54
+ - ...
55
+
56
+ ## Public format
57
+
58
+ <exact public output format for this repo/channel>
59
+
60
+ ## Bullet style
61
+
62
+ Bullets should be:
63
+
64
+ - ...
65
+
66
+ ## Examples
67
+
68
+ Good:
69
+
70
+ - ...
71
+
72
+ Bad:
73
+
74
+ - ...
75
+ ```
76
+
77
+ Keep it short and prefer concrete examples. If product-specific details cannot be inferred, use clear placeholders such as `<Product>` or ask a brief follow-up before writing.
@@ -0,0 +1,101 @@
1
+ ---
2
+ description: preview unreleased changelog candidates since the latest git tag
3
+ argument-hint: ''
4
+ ---
5
+
6
+ Preview what would go into a new release from merged PRs since the latest git tag. Do not create tags, releases, commits, branches, PRs, or files. Reply directly in chat only.
7
+
8
+ Input:
9
+ `$ARGUMENTS`
10
+
11
+ Ignore arguments if provided. This command previews the current unreleased state only.
12
+
13
+ Follow these shared changelog rules exactly:
14
+
15
+ - PR title prefixes: `feat:`, `fix:`, `improve:`, and `internal:`.
16
+ - `feat:` means users can do something new.
17
+ - `fix:` means a user-visible bug or broken behavior was corrected.
18
+ - `improve:` means an existing user-facing workflow became clearer, faster, smoother, more reliable, or easier to use.
19
+ - `internal:` means the work may matter to development/release/reliability but is not a public product change.
20
+ - Do not use technical-only fixes as public fixes. TypeScript, build, CI, test, dependency, refactor, and internal error-handling fixes are `internal:` unless the corrected behavior is directly user-visible.
21
+ - PR titles are classification/review metadata. They are not the public changelog source.
22
+ - PR body `## Changelog` → `Public summary` is the canonical public changelog atom.
23
+
24
+ Workflow:
25
+
26
+ 1. Run `git fetch --tags`.
27
+ 2. Find the latest reachable tag with `git describe --tags --abbrev=0`.
28
+ 3. Get the latest tag commit date with `git log -1 --format=%cI <latest-tag>`.
29
+ 4. Inspect commits since the latest tag with `git log --oneline <latest-tag>..HEAD`.
30
+ 5. Use `gh` to find merged PRs after the latest tag commit date, normally with `gh pr list --state merged --search "merged:>=YYYY-MM-DD" --json number,title,url,mergedAt,body,mergeCommit,headRefName,baseRefName`.
31
+ 6. Cross-check the PRs against commits in `<latest-tag>..HEAD` when possible. If matching is uncertain, say so in the preview.
32
+ 7. For each relevant PR, read the title and body. Use `gh pr view` for full details if needed.
33
+ 8. Extract the PR body `## Changelog` section, especially:
34
+ - `Public summary:`
35
+ - `Context:`
36
+ 9. Classify each PR as:
37
+ - public candidate
38
+ - internal/skipped
39
+ - needs cleanup
40
+ 10. Reply directly with the preview. Do not write a file.
41
+
42
+ Public candidate rules:
43
+
44
+ - Include `feat:`, `fix:`, and `improve:` PRs when `Public summary` contains a meaningful user-facing summary.
45
+ - Skip `internal:` PRs by default.
46
+ - Skip PRs where `Public summary` is `None`.
47
+ - Be conservative with `fix:`. Include only user-visible fixes, not technical/build/CI/test/refactor/dependency fixes.
48
+ - Use `Public summary` text as the candidate release bullet when available.
49
+ - If the PR appears user-facing but lacks a usable public summary, put it under `Needs cleanup`, not `Public candidates`.
50
+
51
+ Needs cleanup rules:
52
+
53
+ Flag PRs that may affect release quality, especially:
54
+
55
+ - missing `## Changelog` section
56
+ - missing `Public summary`
57
+ - vague public summary such as “Improved experience” or “Bug fixes”
58
+ - `feat:`, `fix:`, or `improve:` PR with `Public summary: None`
59
+ - title prefix appears inconsistent with the changelog body
60
+ - possible user-facing change only discoverable from title/commits, not from `Public summary`
61
+
62
+ Output format:
63
+
64
+ ```md
65
+ Unreleased preview since <latest-tag>
66
+
67
+ Release recommendation: <yes|maybe|no>
68
+
69
+ Public candidates: <count>
70
+ Internal/skipped: <count>
71
+ Needs cleanup: <count>
72
+
73
+ ## Public candidates
74
+
75
+ - <public summary>
76
+ Source: #<number> <title>
77
+
78
+ ## Internal / skipped
79
+
80
+ - #<number> <title>
81
+ Reason: <reason>
82
+
83
+ ## Needs cleanup
84
+
85
+ - #<number> <title>
86
+ Issue: <issue>
87
+
88
+ ## Recommendation
89
+
90
+ <short recommendation explaining whether this looks worth releasing now and what, if anything, should be cleaned up first.>
91
+
92
+ ## Notes
93
+
94
+ - <include uncertainty about PR matching, no latest tag, no GitHub CLI access, or no unreleased commits only when relevant>
95
+ ```
96
+
97
+ If there is no latest tag, say that an unreleased preview cannot be anchored to a previous release and summarize recent merged PRs only if safe.
98
+
99
+ If there are no commits or PRs since the latest tag, recommend `no` and say there is nothing new to release.
100
+
101
+ Keep the preview raw and factual. Do not generate polished public/social release notes. Do not include a title, CTA, or marketing framing beyond the required format.