copilot-session-usage 0.8.1__tar.gz → 0.8.4__tar.gz

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 (128) hide show
  1. {copilot_session_usage-0.8.1 → copilot_session_usage-0.8.4}/.github/skills/gh-release-notes/SKILL.md +129 -61
  2. {copilot_session_usage-0.8.1 → copilot_session_usage-0.8.4}/.github/skills/gh-release-notes/scripts/generate_release_notes.py +138 -32
  3. {copilot_session_usage-0.8.1 → copilot_session_usage-0.8.4}/.github/workflows/release.yml +3 -12
  4. {copilot_session_usage-0.8.1 → copilot_session_usage-0.8.4}/CONTRIBUTING.md +11 -1
  5. {copilot_session_usage-0.8.1 → copilot_session_usage-0.8.4}/PKG-INFO +1 -1
  6. {copilot_session_usage-0.8.1 → copilot_session_usage-0.8.4}/src/copilot_session_usage/data/models-and-pricing.lock +3 -3
  7. {copilot_session_usage-0.8.1 → copilot_session_usage-0.8.4}/src/copilot_session_usage/data/models-and-pricing.yml +104 -0
  8. copilot_session_usage-0.8.4/tests/test_generate_release_notes.py +631 -0
  9. {copilot_session_usage-0.8.1 → copilot_session_usage-0.8.4}/tests/test_release_notes.py +36 -0
  10. copilot_session_usage-0.8.1/tests/test_generate_release_notes.py +0 -333
  11. {copilot_session_usage-0.8.1 → copilot_session_usage-0.8.4}/.editorconfig +0 -0
  12. {copilot_session_usage-0.8.1 → copilot_session_usage-0.8.4}/.gitattributes +0 -0
  13. {copilot_session_usage-0.8.1 → copilot_session_usage-0.8.4}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  14. {copilot_session_usage-0.8.1 → copilot_session_usage-0.8.4}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  15. {copilot_session_usage-0.8.1 → copilot_session_usage-0.8.4}/.github/changes/requests/skill-breakdown/01-request.md +0 -0
  16. {copilot_session_usage-0.8.1 → copilot_session_usage-0.8.4}/.github/guidelines/git-commit-message.guideline.md +0 -0
  17. {copilot_session_usage-0.8.1 → copilot_session_usage-0.8.4}/.github/guidelines/knowledge-base.guidelines.md +0 -0
  18. {copilot_session_usage-0.8.1 → copilot_session_usage-0.8.4}/.github/pull_request_template.md +0 -0
  19. {copilot_session_usage-0.8.1 → copilot_session_usage-0.8.4}/.github/skills/consolidate-knowledge-base/SKILL.md +0 -0
  20. {copilot_session_usage-0.8.1 → copilot_session_usage-0.8.4}/.github/skills/gh-commit-changes/SKILL.md +0 -0
  21. {copilot_session_usage-0.8.1 → copilot_session_usage-0.8.4}/.github/skills/gh-create-pull-request/SKILL.md +0 -0
  22. {copilot_session_usage-0.8.1 → copilot_session_usage-0.8.4}/.github/skills/record-finding/SKILL.md +0 -0
  23. {copilot_session_usage-0.8.1 → copilot_session_usage-0.8.4}/.github/workflows/ci.yml +0 -0
  24. {copilot_session_usage-0.8.1 → copilot_session_usage-0.8.4}/.github/workflows/publish.yml +0 -0
  25. {copilot_session_usage-0.8.1 → copilot_session_usage-0.8.4}/.github/workflows/refresh-pricing.yml +0 -0
  26. {copilot_session_usage-0.8.1 → copilot_session_usage-0.8.4}/.github/workflows/release-notes.yml +0 -0
  27. {copilot_session_usage-0.8.1 → copilot_session_usage-0.8.4}/.gitignore +0 -0
  28. {copilot_session_usage-0.8.1 → copilot_session_usage-0.8.4}/.readthedocs.yaml +0 -0
  29. {copilot_session_usage-0.8.1 → copilot_session_usage-0.8.4}/AGENTS.md +0 -0
  30. {copilot_session_usage-0.8.1 → copilot_session_usage-0.8.4}/CHANGELOG.md +0 -0
  31. {copilot_session_usage-0.8.1 → copilot_session_usage-0.8.4}/CONSTITUTION.md +0 -0
  32. {copilot_session_usage-0.8.1 → copilot_session_usage-0.8.4}/LICENSE +0 -0
  33. {copilot_session_usage-0.8.1 → copilot_session_usage-0.8.4}/README.md +0 -0
  34. {copilot_session_usage-0.8.1 → copilot_session_usage-0.8.4}/docs/articles/presentation.md +0 -0
  35. {copilot_session_usage-0.8.1 → copilot_session_usage-0.8.4}/docs/internal/automated_release_proces.md +0 -0
  36. {copilot_session_usage-0.8.1 → copilot_session_usage-0.8.4}/docs/source/_static/changelog.js +0 -0
  37. {copilot_session_usage-0.8.1 → copilot_session_usage-0.8.4}/docs/source/_static/custom.css +0 -0
  38. {copilot_session_usage-0.8.1 → copilot_session_usage-0.8.4}/docs/source/changelog.md +0 -0
  39. {copilot_session_usage-0.8.1 → copilot_session_usage-0.8.4}/docs/source/conf.py +0 -0
  40. {copilot_session_usage-0.8.1 → copilot_session_usage-0.8.4}/docs/source/explanation/how-cost-estimation-works.md +0 -0
  41. {copilot_session_usage-0.8.1 → copilot_session_usage-0.8.4}/docs/source/explanation/index.md +0 -0
  42. {copilot_session_usage-0.8.1 → copilot_session_usage-0.8.4}/docs/source/how-to/add-commit-trailer.md +0 -0
  43. {copilot_session_usage-0.8.1 → copilot_session_usage-0.8.4}/docs/source/how-to/analyze-specific-session.md +0 -0
  44. {copilot_session_usage-0.8.1 → copilot_session_usage-0.8.4}/docs/source/how-to/batch-and-spending.md +0 -0
  45. {copilot_session_usage-0.8.1 → copilot_session_usage-0.8.4}/docs/source/how-to/copilot-cli-provider.md +0 -0
  46. {copilot_session_usage-0.8.1 → copilot_session_usage-0.8.4}/docs/source/how-to/export-json.md +0 -0
  47. {copilot_session_usage-0.8.1 → copilot_session_usage-0.8.4}/docs/source/how-to/index.md +0 -0
  48. {copilot_session_usage-0.8.1 → copilot_session_usage-0.8.4}/docs/source/how-to/wsl2.md +0 -0
  49. {copilot_session_usage-0.8.1 → copilot_session_usage-0.8.4}/docs/source/index.md +0 -0
  50. {copilot_session_usage-0.8.1 → copilot_session_usage-0.8.4}/docs/source/installation.md +0 -0
  51. {copilot_session_usage-0.8.1 → copilot_session_usage-0.8.4}/docs/source/reference/api.md +0 -0
  52. {copilot_session_usage-0.8.1 → copilot_session_usage-0.8.4}/docs/source/reference/cli.md +0 -0
  53. {copilot_session_usage-0.8.1 → copilot_session_usage-0.8.4}/docs/source/reference/index.md +0 -0
  54. {copilot_session_usage-0.8.1 → copilot_session_usage-0.8.4}/docs/source/reference/pricing.md +0 -0
  55. {copilot_session_usage-0.8.1 → copilot_session_usage-0.8.4}/docs/source/tutorials/getting-started.md +0 -0
  56. {copilot_session_usage-0.8.1 → copilot_session_usage-0.8.4}/docs/source/tutorials/index.md +0 -0
  57. {copilot_session_usage-0.8.1 → copilot_session_usage-0.8.4}/guidelines.yml +0 -0
  58. {copilot_session_usage-0.8.1 → copilot_session_usage-0.8.4}/justfile +0 -0
  59. {copilot_session_usage-0.8.1 → copilot_session_usage-0.8.4}/knowledge/_schema/Base.schema.yaml +0 -0
  60. {copilot_session_usage-0.8.1 → copilot_session_usage-0.8.4}/knowledge/_schema/Concept.schema.yaml +0 -0
  61. {copilot_session_usage-0.8.1 → copilot_session_usage-0.8.4}/knowledge/_schema/Experiment.schema.yaml +0 -0
  62. {copilot_session_usage-0.8.1 → copilot_session_usage-0.8.4}/knowledge/_schema/Finding.schema.yaml +0 -0
  63. {copilot_session_usage-0.8.1 → copilot_session_usage-0.8.4}/knowledge/_schema/Hypothesis.schema.yaml +0 -0
  64. {copilot_session_usage-0.8.1 → copilot_session_usage-0.8.4}/knowledge/_schema/Outcome.schema.yaml +0 -0
  65. {copilot_session_usage-0.8.1 → copilot_session_usage-0.8.4}/knowledge/_schema/Playbook.schema.yaml +0 -0
  66. {copilot_session_usage-0.8.1 → copilot_session_usage-0.8.4}/knowledge/_schema/Principle.schema.yaml +0 -0
  67. {copilot_session_usage-0.8.1 → copilot_session_usage-0.8.4}/knowledge/_schema/Reference.schema.yaml +0 -0
  68. {copilot_session_usage-0.8.1 → copilot_session_usage-0.8.4}/knowledge/_schema/Structure.schema.yaml +0 -0
  69. {copilot_session_usage-0.8.1 → copilot_session_usage-0.8.4}/knowledge/concepts/copilot-cli.md +0 -0
  70. {copilot_session_usage-0.8.1 → copilot_session_usage-0.8.4}/knowledge/concepts/index.md +0 -0
  71. {copilot_session_usage-0.8.1 → copilot_session_usage-0.8.4}/knowledge/concepts/overview.md +0 -0
  72. {copilot_session_usage-0.8.1 → copilot_session_usage-0.8.4}/knowledge/concepts/session-cost-analysis.md +0 -0
  73. {copilot_session_usage-0.8.1 → copilot_session_usage-0.8.4}/knowledge/concepts/threshold-based-pricing.md +0 -0
  74. {copilot_session_usage-0.8.1 → copilot_session_usage-0.8.4}/knowledge/experiments/index.md +0 -0
  75. {copilot_session_usage-0.8.1 → copilot_session_usage-0.8.4}/knowledge/experiments/verify-subagent-cost-attribution.md +0 -0
  76. {copilot_session_usage-0.8.1 → copilot_session_usage-0.8.4}/knowledge/findings/2026.07.02-00.00-subagent-logs-runsubagent-prefix.md +0 -0
  77. {copilot_session_usage-0.8.1 → copilot_session_usage-0.8.4}/knowledge/findings/2026.07.02-22.00-title-generation-not-counted-as-model-turn.md +0 -0
  78. {copilot_session_usage-0.8.1 → copilot_session_usage-0.8.4}/knowledge/findings/2026.07.02-23.00-cache-write-approximation.md +0 -0
  79. {copilot_session_usage-0.8.1 → copilot_session_usage-0.8.4}/knowledge/findings/2026.07.13-14.27-release-notes-overreported-maintainer-changes.md +0 -0
  80. {copilot_session_usage-0.8.1 → copilot_session_usage-0.8.4}/knowledge/findings/2026.07.24-15.01-actions-token-cannot-create-pr.md +0 -0
  81. {copilot_session_usage-0.8.1 → copilot_session_usage-0.8.4}/knowledge/findings/2026.08.05-16.24-utility-models-appear-in-logs-but-are-not-billed.md +0 -0
  82. {copilot_session_usage-0.8.1 → copilot_session_usage-0.8.4}/knowledge/findings/index.md +0 -0
  83. {copilot_session_usage-0.8.1 → copilot_session_usage-0.8.4}/knowledge/guides/automation-scripts.md +0 -0
  84. {copilot_session_usage-0.8.1 → copilot_session_usage-0.8.4}/knowledge/guides/cost-optimization.md +0 -0
  85. {copilot_session_usage-0.8.1 → copilot_session_usage-0.8.4}/knowledge/guides/index.md +0 -0
  86. {copilot_session_usage-0.8.1 → copilot_session_usage-0.8.4}/knowledge/guides/wsl2-setup.md +0 -0
  87. {copilot_session_usage-0.8.1 → copilot_session_usage-0.8.4}/knowledge/ideas/index.md +0 -0
  88. {copilot_session_usage-0.8.1 → copilot_session_usage-0.8.4}/knowledge/ideas/multi-session-efficiency-analytics.md +0 -0
  89. {copilot_session_usage-0.8.1 → copilot_session_usage-0.8.4}/knowledge/index.md +0 -0
  90. {copilot_session_usage-0.8.1 → copilot_session_usage-0.8.4}/knowledge/log.md +0 -0
  91. {copilot_session_usage-0.8.1 → copilot_session_usage-0.8.4}/knowledge/principles/findings-are-immutable.md +0 -0
  92. {copilot_session_usage-0.8.1 → copilot_session_usage-0.8.4}/knowledge/principles/index.md +0 -0
  93. {copilot_session_usage-0.8.1 → copilot_session_usage-0.8.4}/knowledge/reference/debug-log-format.md +0 -0
  94. {copilot_session_usage-0.8.1 → copilot_session_usage-0.8.4}/knowledge/reference/index.md +0 -0
  95. {copilot_session_usage-0.8.1 → copilot_session_usage-0.8.4}/knowledge/reference/pricing-formats.md +0 -0
  96. {copilot_session_usage-0.8.1 → copilot_session_usage-0.8.4}/knowledge/structures/cache-cost-approximation.md +0 -0
  97. {copilot_session_usage-0.8.1 → copilot_session_usage-0.8.4}/knowledge/structures/index.md +0 -0
  98. {copilot_session_usage-0.8.1 → copilot_session_usage-0.8.4}/knowledge/structures/knowledge-base-information-types.md +0 -0
  99. {copilot_session_usage-0.8.1 → copilot_session_usage-0.8.4}/knowledge/structures/session-discovery-algorithm.md +0 -0
  100. {copilot_session_usage-0.8.1 → copilot_session_usage-0.8.4}/knowledge/structures/subagent-cost-tracking.md +0 -0
  101. {copilot_session_usage-0.8.1 → copilot_session_usage-0.8.4}/knowledge/structures/vscode-copilot-extension.md +0 -0
  102. {copilot_session_usage-0.8.1 → copilot_session_usage-0.8.4}/pyproject.toml +0 -0
  103. {copilot_session_usage-0.8.1 → copilot_session_usage-0.8.4}/scripts/refresh_pricing.py +0 -0
  104. {copilot_session_usage-0.8.1 → copilot_session_usage-0.8.4}/skills/copilot-session-usage/SKILL.md +0 -0
  105. {copilot_session_usage-0.8.1 → copilot_session_usage-0.8.4}/skills/copilot-session-usage/references/span-analysis-template.md +0 -0
  106. {copilot_session_usage-0.8.1 → copilot_session_usage-0.8.4}/src/copilot_session_usage/__init__.py +0 -0
  107. {copilot_session_usage-0.8.1 → copilot_session_usage-0.8.4}/src/copilot_session_usage/_internal/__init__.py +0 -0
  108. {copilot_session_usage-0.8.1 → copilot_session_usage-0.8.4}/src/copilot_session_usage/_internal/copilot_cli.py +0 -0
  109. {copilot_session_usage-0.8.1 → copilot_session_usage-0.8.4}/src/copilot_session_usage/_internal/core.py +0 -0
  110. {copilot_session_usage-0.8.1 → copilot_session_usage-0.8.4}/src/copilot_session_usage/_internal/git.py +0 -0
  111. {copilot_session_usage-0.8.1 → copilot_session_usage-0.8.4}/src/copilot_session_usage/_internal/vscode.py +0 -0
  112. {copilot_session_usage-0.8.1 → copilot_session_usage-0.8.4}/src/copilot_session_usage/api.py +0 -0
  113. {copilot_session_usage-0.8.1 → copilot_session_usage-0.8.4}/src/copilot_session_usage/cli.py +0 -0
  114. {copilot_session_usage-0.8.1 → copilot_session_usage-0.8.4}/src/copilot_session_usage/data/__init__.py +0 -0
  115. {copilot_session_usage-0.8.1 → copilot_session_usage-0.8.4}/src/copilot_session_usage/data/custom-models-pricing.yml +0 -0
  116. {copilot_session_usage-0.8.1 → copilot_session_usage-0.8.4}/tests/conftest.py +0 -0
  117. {copilot_session_usage-0.8.1 → copilot_session_usage-0.8.4}/tests/test_api.py +0 -0
  118. {copilot_session_usage-0.8.1 → copilot_session_usage-0.8.4}/tests/test_cli.py +0 -0
  119. {copilot_session_usage-0.8.1 → copilot_session_usage-0.8.4}/tests/test_copilot_cli.py +0 -0
  120. {copilot_session_usage-0.8.1 → copilot_session_usage-0.8.4}/tests/test_core.py +0 -0
  121. {copilot_session_usage-0.8.1 → copilot_session_usage-0.8.4}/tests/test_coverage_gaps.py +0 -0
  122. {copilot_session_usage-0.8.1 → copilot_session_usage-0.8.4}/tests/test_git.py +0 -0
  123. {copilot_session_usage-0.8.1 → copilot_session_usage-0.8.4}/tests/test_pricing_runtime.py +0 -0
  124. {copilot_session_usage-0.8.1 → copilot_session_usage-0.8.4}/tests/test_rendering.py +0 -0
  125. {copilot_session_usage-0.8.1 → copilot_session_usage-0.8.4}/tests/test_vscode.py +0 -0
  126. {copilot_session_usage-0.8.1 → copilot_session_usage-0.8.4}/tests/test_vscode_platform.py +0 -0
  127. {copilot_session_usage-0.8.1 → copilot_session_usage-0.8.4}/uv.lock +0 -0
  128. {copilot_session_usage-0.8.1 → copilot_session_usage-0.8.4}/uv.toml +0 -0
@@ -10,6 +10,20 @@ user-invocable: true
10
10
  Generate **end-user-friendly, user-impact-only** release notes by analyzing the actual changes between releases.
11
11
  Interactive use requires no script; automated generation and validation use the bundled script described below.
12
12
 
13
+ ## Portability
14
+
15
+ This skill works with any Git repository, regardless of language, framework,
16
+ directory layout, documentation host, or release workflow. Discover the target
17
+ product and its public documentation from the requested repository; do not assume
18
+ the skill's own repository is the product being released.
19
+
20
+ The bundled script uses only the Python standard library (Python 3.10+), Git,
21
+ and `gh copilot`. Install this skill where Copilot can discover it in the target
22
+ repository or user-level skill directory. The script may live outside the target
23
+ repository; select that repository with `--repo`. No project package, task runner,
24
+ configuration file, or particular CI workflow is required. Documentation links
25
+ come from repository evidence, not a built-in domain allowlist.
26
+
13
27
  The output is meant to be **copy-pasted into a GitHub Release**. It must contain
14
28
  release-note sections only: never add a document title, version heading, preamble,
15
29
  file summary, commit summary, or closing separator. GitHub already displays the
@@ -18,13 +32,22 @@ Markdown.
18
32
 
19
33
  ## Non-negotiable output contract
20
34
 
35
+ - Describe each user outcome once, in one concise bullet. End that same bullet
36
+ with the closest verified documentation link when available. Do not collect
37
+ those links at the bottom or repeat the change in Documentation or Examples.
38
+ - Scale notes to the actual change: one pricing refresh or small fix normally
39
+ needs one bullet; a large feature release needs distinct outcomes, not a
40
+ bullet for every commit, field, edge case, or implementation detail.
21
41
  - Render every public documentation URL as an inline Markdown link with concise
22
42
  descriptive text: `See the [pricing reference for details](https://example.com/pricing)`.
23
43
  - Never expose a documentation URL as plain text, after a colon, in parentheses,
24
44
  or on its own line. A bare `https://...` URL is invalid release-note output.
25
45
  - Never include CI, release automation, Git evidence, generator internals,
26
- governance, contributor or agent guidance, tests, repository housekeeping, or
46
+ internal governance, contributor or agent guidance, tests, repository housekeeping, or
27
47
  maintainer process.
48
+ - Apply internal-material exclusions by audience, not filename. If the target
49
+ repository publishes skills, documentation, or test utilities as its product,
50
+ describe evidenced changes for that product's users.
28
51
  - Never include `## Maintenance` together with any user-facing section. When a
29
52
  user-facing change qualifies, omit Maintenance and discard all internal-only
30
53
  candidates.
@@ -44,20 +67,31 @@ The skill includes `scripts/generate_release_notes.py`, a standalone Python scri
44
67
  - writes the deterministic `## Maintenance` output without invoking Copilot when the
45
68
  requested Git range contains no commits or the caller explicitly requests
46
69
  `--maintenance-only`;
47
- - precomputes the commit log and complete diff locally so generation also works
70
+ - precomputes the commit log, complete diff summary, and full diff locally so generation also works
48
71
  when the Copilot CLI cannot inspect Git history inside its tool environment;
49
72
  - invokes the Copilot CLI with `/gh-release-notes`;
50
- - gives the skill scoped file-writing tools so it writes the final Markdown to the
51
- requested output file while the response stream is discarded; and
73
+ - exposes Copilot's `view`, `glob`, and `grep` tools for reading and finding
74
+ skill instructions and exact documentation pages (`read` is a permission
75
+ category, not a builtin tool name);
76
+ - invokes Copilot with `--output-format json`, reads JSONL events, and selects
77
+ only the completed `assistant.message` with `phase: final_answer`; commentary,
78
+ streaming deltas, tool results, and telemetry never become release notes;
79
+ - validates the final Markdown and writes the requested file itself, without
80
+ depending on the model's file-writing tools;
81
+ - retries generation up to three times with validation feedback, with a five-minute
82
+ timeout per request, clearing rejected output before each retry;
83
+ - refuses publication after exhausted attempts, never substituting Maintenance
84
+ for a failed request; and
52
85
  - verifies that the requested output file follows the release-note output contract. The
53
86
  script does not normalize Markdown or decide user impact, categorize changes,
54
87
  discover documentation, infer breaking changes, or require examples; those
55
88
  decisions belong to this skill.
56
89
 
57
- Generate notes for an exact range from the repository root:
90
+ Generate notes for an exact range using the script's installed path:
58
91
 
59
92
  ```bash
60
- python .github/skills/gh-release-notes/scripts/generate_release_notes.py \
93
+ python /path/to/gh-release-notes/scripts/generate_release_notes.py \
94
+ --repo /path/to/repo \
61
95
  --from-ref v1.0.0 \
62
96
  --to-ref v1.1.0 \
63
97
  --output release-notes.md
@@ -67,21 +101,28 @@ For an intentional maintenance-only release whose range contains internal commit
67
101
  pass `--maintenance-only` to write the deterministic Maintenance section without
68
102
  invoking Copilot.
69
103
 
70
- `release-notes.md` is the canonical shared artifact filename. The generator, both
71
- release workflows, and `gh release --notes-file` use this same file so the release
72
- body never depends on Copilot's response stream. An explicitly supplied `--output`
73
- path is honored exactly. For a non-empty range, the generator creates the empty
74
- handoff file before invoking Copilot so the skill can edit the known
75
- repository-relative target; the file is accepted only after the skill has replaced
76
- it with valid Markdown. An empty range produces the valid maintenance output
104
+ `release-notes.md` is the default output filename, relative to the target repository.
105
+ An explicitly supplied `--output` path is honored exactly; absolute output paths
106
+ are also supported. A release workflow can consume the validated file through
107
+ `gh release --notes-file` or another publishing tool. This skill does not require
108
+ or create that workflow and does not publish releases itself.
109
+ For a non-empty range, the generator clears stale output
110
+ before invoking Copilot and persists only a validated structured final answer.
111
+ Copilot has reading and Git tools, but no file-writing tools. The CLI must support
112
+ JSONL output with final-answer phases and a successful terminal `result` event.
113
+ Malformed or incomplete streams fail validation; no Markdown is scraped from
114
+ surrounding chatter or tool traces.
115
+ An empty range produces the valid maintenance output
77
116
  directly and does not require Copilot authentication or skill discovery.
78
117
 
79
118
  The range is Git's two-dot range, `from_ref..to_ref`: `from_ref` itself is excluded
80
119
  and `to_ref` is included. Both refs may be tags, branches, or commit IDs.
81
120
 
82
121
  Set `COPILOT_MODEL` or pass `--model` to make model selection explicit. The script also
83
- supports `--validate FILE` when an existing release-note file should be checked for
84
- readability and non-empty content without invoking Copilot.
122
+ supports `--validate FILE` to enforce section structure, flat non-empty bullets,
123
+ HTTPS Markdown links, exact duplicate rejection, and Maintenance exclusivity
124
+ without invoking Copilot. Semantic accuracy and paraphrased repetition still
125
+ require the skill's audience review.
85
126
 
86
127
  ---
87
128
 
@@ -107,7 +148,7 @@ What changed since the last release tag?
107
148
  2. **Applies a strict user-impact gate** — includes a change only when an end user can do, observe, configure, rely on, or learn something different
108
149
  3. **Interprets for end-users** — no technical jargon, functions, variable names, file paths, or implementation summaries
109
150
  4. **Categorizes intelligently** — Features, Enhancements, Bug Fixes, Breaking Changes, Examples, Documentation, and Maintenance
110
- 5. **Adds concrete examples** — shows what users see or can do after a qualifying change
151
+ 5. **Adds examples when useful** — includes short inline syntax only when it helps users act
111
152
  6. **Links to relevant public docs** — links each user-facing change to the closest published documentation page with concise inline Markdown link text when one exists, even if that page was not changed in the release
112
153
  7. **Consolidates related changes** — groups related diffs and eliminates back-and-forth noise
113
154
  8. **Outputs clean markdown** — ready to paste into a GitHub Release note
@@ -169,9 +210,12 @@ Gather all commits in the specified range with their messages.
169
210
 
170
211
  ### Step 2: Examine relevant diffs
171
212
  ```bash
172
- git diff v1.0.0..v1.1.0 -- . ':(exclude).github' ':(exclude)skills' ':(exclude)guidelines'
213
+ git diff --no-ext-diff v1.0.0..v1.1.0
173
214
  ```
174
- Read actual code changes line-by-line to understand behavior. Inspect excluded paths only when needed to verify whether they caused a direct user-visible consequence; never report the paths themselves.
215
+ Read actual changes to understand behavior. Determine which files form the
216
+ published product before narrowing the analysis. Do not exclude directories by
217
+ name: a skill catalog, documentation site, or testing library may publish skills,
218
+ guidelines, knowledge, or test utilities as its actual product.
175
219
 
176
220
  Treat these as non-release content, and exclude them entirely unless the diff
177
221
  also proves a direct change to the published user experience:
@@ -186,7 +230,9 @@ also proves a direct change to the published user experience:
186
230
 
187
231
  These exclusions can be overridden only when the diff proves a direct user
188
232
  impact, such as a packaging change that changes the installable artifact or a
189
- security fix that changes behavior for users. In that case, describe the user
233
+ security fix that changes behavior for users. Published skills, guidelines, or
234
+ test utilities also qualify when they are the requested repository's product,
235
+ rather than internal support material. In that case, describe the user
190
236
  outcome, not the internal mechanism or workflow that enabled it.
191
237
 
192
238
  After identifying a qualifying change, inspect the repository's user-facing
@@ -223,6 +269,21 @@ Do not wait for the documentation page itself to be modified. For example, a
223
269
  pricing or model-data change should link to the project's pricing/model reference
224
270
  page if that page explains the affected behavior.
225
271
 
272
+ Keep the link at the end of the bullet that describes the change, before starting
273
+ the next item. For a fictional reporting application, for example:
274
+
275
+ ```markdown
276
+ ## New Features
277
+ - **PDF report exports** with `report export --format pdf`. [Export guide](https://docs.example.com/reports/export).
278
+ - **Scheduled reports** with configurable delivery times. [Scheduling guide](https://docs.example.com/reports/scheduling).
279
+ ```
280
+
281
+ These are layout examples, not claims about the requested range. Verify syntax
282
+ and URLs from the checked-out product documentation. Use a section fragment only
283
+ when its anchor is verified; do not guess one from a heading. Do not add a
284
+ Documentation section linking these guides again. A guide modified alongside its
285
+ feature belongs with the feature, not in a second bullet saying the guide changed.
286
+
226
287
  If no trustworthy public documentation URL can be established, omit the
227
288
  Documentation section and documentation bullets rather than writing a text-only
228
289
  documentation entry. Never emit `## Documentation` unless its body contains at
@@ -247,6 +308,14 @@ Breaking changes come from:
247
308
  - If a feature was added then removed → don't mention it
248
309
  - If something changed multiple times → only note the final state
249
310
  - If multiple commits fix the same issue → merge into one bullet
311
+ - Assign each outcome to exactly one section. A breaking change belongs in
312
+ Breaking Changes, not again in New Features or Enhancements.
313
+ - Put useful command syntax inline in the outcome's bullet. Add Examples only
314
+ for a distinct workflow that cannot be explained by that short syntax; do not
315
+ restate the same capability under a second heading.
316
+ - Delete unchanged defaults, generic benefits, "also updated documentation",
317
+ "improved reliability", and "no breaking changes" unless they convey a specific
318
+ evidenced difference users need to know.
250
319
 
251
320
  ### Step 6: Categorize & Format
252
321
 
@@ -257,8 +326,9 @@ Fixes`, `## Breaking Changes`, `## Examples`, `## Documentation`, and
257
326
  `## Maintenance`. Do not add headings such as `Internal`, `User impact`, `Upgrade
258
327
  notes`, or `Summary`; fold useful user impact into the permitted sections instead.
259
328
 
260
- Treat Documentation as a link-only section: include it only when at least one
261
- trustworthy public documentation URL was found, and make every Documentation
329
+ Reserve Documentation for documentation-only changes that help users operate the
330
+ product and are not already covered by another bullet. Never use it as a link
331
+ collection for features or fixes. Make every Documentation
262
332
  bullet an inline Markdown link with descriptive link text and an absolute HTTPS
263
333
  target. If no such URL was found, omit the entire Documentation section. Do not
264
334
  replace the missing URL with a repository path, an unlinked description, or a
@@ -279,8 +349,8 @@ Documentation bullet remains, remove `## Maintenance` and all of its bullets.
279
349
 
280
350
  ```markdown
281
351
  ## New Features
282
- - Added dark mode toggle in settings
283
- - New PDF export option
352
+ - Added a dark mode toggle in settings. [Appearance guide](https://docs.example.com/settings#dark-mode).
353
+ - Export reports as PDF from the File menu. [Export guide](https://docs.example.com/export).
284
354
 
285
355
  ## Enhancements
286
356
  - Improved search performance (now supports partial matches)
@@ -291,20 +361,19 @@ Documentation bullet remains, remove `## Maintenance` and all of its bullets.
291
361
  - Resolved crash when uploading 10MB+ files
292
362
 
293
363
  ## Breaking Changes
294
- - Database schema updated — run migration before upgrading
295
- - CSV export removed; use Excel or PDF instead
296
-
297
- ## Examples
298
- - Dark mode can be enabled in Settings → Appearance → Theme
299
- - CSV export is no longer available; choose Excel or PDF from Export menu
364
+ - Run the database migration before upgrading. [Migration guide](https://docs.example.com/upgrade#database).
365
+ - CSV export removed; use Excel or PDF instead. [Export guide](https://docs.example.com/export).
300
366
 
301
367
  ## Documentation
302
- - [Dark mode guide](https://docs.example.com/settings#dark-mode)
303
- - [Migration notes](https://docs.example.com/upgrade#database)
368
+ - New [account recovery guide](https://docs.example.com/accounts/recovery).
369
+ ```
304
370
 
371
+ The example above illustrates a user-facing release. A maintenance-only release
372
+ instead contains only:
373
+
374
+ ```markdown
305
375
  ## Maintenance
306
- This release contains maintenance and internal improvements. No user-facing behavior
307
- changed.
376
+ This release contains maintenance and internal improvements. No user-facing behavior changed.
308
377
  ```
309
378
 
310
379
  Omit `## Breaking Changes` completely when the actual range contains no breaking
@@ -321,7 +390,7 @@ directly observe a supported behavior that the mechanism enables.
321
390
  1. **Parse input** — extract `from_ref`, `to_ref`, `repo_path`, and optional filters
322
391
  2. **Discover public documentation** — inspect the repository's available user-facing documentation surfaces and any published links they expose. Do not assume a particular language, documentation generator, directory layout, metadata file, or URL naming scheme. Prefer the closest trustworthy public page for each qualifying change.
323
392
  For hosting services like readthedocs that propose latest/stable URLs, prefer the stable version.
324
- Infere fragment identifiers to point to specific sections when available.
393
+ Use fragment identifiers only when their anchors can be verified.
325
394
  Use absolute HTTPS URLs, and omit a documentation link when no public page can be established rather than inventing one.
326
395
  3. **Fetch commits** — run `git log` with range, collect hashes and messages
327
396
  4. **Read diffs per file** — use `git show <hash>` for each relevant commit and examine changed behavior, not just filenames
@@ -329,13 +398,13 @@ Use absolute HTTPS URLs, and omit a documentation link when no public page can b
329
398
  6. **Interpret impact** — state what users can do, observe, configure, rely on, or learn differently
330
399
  7. **Detect breaking changes** — scan for BREAKING markers, public API removals, format changes, migrations, and changed defaults
331
400
  8. **Group by category** — assign each qualifying change to **New Features**, **Enhancements**, **Bug Fixes**, **Breaking Changes**, **Examples**, **Documentation**, or **Maintenance**
332
- 9. **Build examples** — for each user-facing change, use README, public docs, tests, or CLI help as evidence; add a concrete example for every added or changed CLI command, public API call, configuration option, or before/after workflow
401
+ 9. **Add useful syntax** — use public docs or CLI help to verify short inline command/API examples; add an Examples section only when a distinct workflow needs it
333
402
  10. **Consolidate** — merge related items, remove duplicates and flip-flops
334
403
  11. **Handle an empty range** — if no product change qualifies, generate exactly one concise `## Maintenance` section
335
404
  12. **Format markdown** — generate clean section headings and bullets with no title, preamble, footer, file summary, or commit summary
336
- 13. **Use concise relevant public docs links** — every Documentation bullet must contain an inline Markdown link with descriptive link text and an absolute HTTPS target to the closest relevant published page. Every qualifying feature, enhancement, bug-fix, breaking-change, or example bullet should use the same concise Markdown-link style when a page exists, using a fragment identifier when the page has a matching section. Do not expose bare URLs in prose or use bare URLs as the only link form. Do not require the page to have changed in the range. If no trustworthy public URL exists, omit Documentation entirely.
405
+ 13. **Attach documentation locally** — end each outcome's bullet with its closest verified public HTTPS Markdown link. Do not repeat it in Documentation; that section is only for independent documentation-only changes. Do not invent URLs or fragments.
337
406
  14. **Run the final audience review** — inspect every bullet and remove anything about CI, workflows, release tooling, Git evidence, validation, governance, contributor or agent guidance, repository housekeeping, changed files, or maintainer process. Remove the entire section if that leaves it empty. Also remove Documentation bullets whose links target contributor, governance, engineering, or maintainer material.
338
- 15. **Run the final format review** — ensure every `https://` occurrence is inside `[visible text](https://target)`, with no bare URL in prose, and remove `## Maintenance` whenever another qualifying section exists.
407
+ 15. **Run the final format review** — ensure every `https://` occurrence is inside `[visible text](https://target)`, with no bare URL in prose, remove duplicate outcomes and empty sections, and remove `## Maintenance` whenever another qualifying section exists.
339
408
 
340
409
  ---
341
410
 
@@ -357,8 +426,8 @@ Start directly with the release-note section instead:
357
426
 
358
427
  ```markdown
359
428
  ## New Features
360
- - Added dark mode toggle in settings
361
- - New PDF export option
429
+ - Added a dark mode toggle in settings. [Appearance guide](https://docs.example.com/settings#dark-mode).
430
+ - Export reports as PDF from the File menu. [Export guide](https://docs.example.com/export).
362
431
 
363
432
  ## Enhancements
364
433
  - Improved search performance (supports partial matches)
@@ -369,15 +438,10 @@ Start directly with the release-note section instead:
369
438
  - Resolved crash when uploading 10MB+ files
370
439
 
371
440
  ## Breaking Changes
372
- - Database schema updated — run migration before upgrading
373
-
374
- ## Examples
375
- - Enable dark mode from Settings → Appearance → Theme
376
- - Export a report as PDF from the File → Export menu
441
+ - Run the database migration before upgrading. [Migration guide](https://docs.example.com/upgrade#database).
377
442
 
378
443
  ## Documentation
379
- - [Dark mode guide](https://docs.example.com/settings#dark-mode)
380
- - [Upgrade instructions](https://docs.example.com/upgrade#database)
444
+ - New [account recovery guide](https://docs.example.com/accounts/recovery).
381
445
  ```
382
446
 
383
447
  **Key Rules:**
@@ -391,9 +455,9 @@ Start directly with the release-note section instead:
391
455
  for maintenance-only releases
392
456
  - Use public documentation URLs; never use repo-relative paths like `docs/...` or `README.md`
393
457
  - For each user-facing bullet, search existing documentation for the closest page about the impacted behavior and link it inline with descriptive Markdown text when available, even when the documentation file was unchanged
394
- - Use fragment identifiers (`#section-name`) to point to specific docs sections
395
- - Add a concrete **Examples** section for every added or changed CLI command, public API call, configuration option, or user-visible before/after behavior. Derive syntax from the project's public help, API docs, README, or supported usage examples; do not invent it
396
- - Add a **Documentation** section only when at least one trustworthy public documentation URL exists; every bullet in that section must contain an absolute HTTPS Markdown link with descriptive link text. If no public URL can be established, omit the section and do not emit a text-only documentation bullet
458
+ - Use fragment identifiers (`#section-name`) only for verified section anchors
459
+ - Prefer short inline examples in the relevant bullet; use **Examples** only for a distinct useful workflow, never to repeat a feature
460
+ - Use **Documentation** only for independent documentation-only changes with a verified HTTPS link; never repeat links or outcomes already covered elsewhere
397
461
  - Prefer concise inline Markdown links in every section, such as `See the [pricing reference for details](https://example.com/pricing)`, rather than exposing a full URL after a colon or in parentheses
398
462
  - Multiple links OK if they point to different topics
399
463
  - Omit any section that has no bullets
@@ -409,18 +473,22 @@ Start directly with the release-note section instead:
409
473
 
410
474
  ## Non-interactive automation mode
411
475
 
412
- When this skill is invoked by a CI job with an explicit output-file request:
476
+ When invoked by the bundled generator or a CI job using structured output:
413
477
 
414
478
  - Honor the requested tag range and repository path exactly.
415
- - Treat the requested output file as mandatory. Writing it is the only successful completion condition.
416
- - Replace the pre-created handoff file with the final Markdown using the `edit` file tool.
417
- - After writing, use the `read` file tool to verify that the requested file exists and contains the final release-note Markdown.
418
- - Do not modify, commit, or push any other repository files.
419
- - The output file must contain only the final release-note Markdown, without an explanation, title heading, or code fence.
479
+ - Return only the complete release-note Markdown in the final answer. The Python
480
+ caller selects its structured event, validates it, and writes the requested file.
481
+ - Do not create, edit, commit, or push any repository files. There is no model-side
482
+ output-file handoff or file verification step.
483
+ - Use `view`, `glob`, `grep`, and read-only Git commands to inspect the evidence,
484
+ skill instructions, public syntax, and documentation destinations as needed.
485
+ - The final answer must contain only release-note Markdown, without an
486
+ explanation, title heading, or code fence.
420
487
  - The first line must be exactly one of: `## New Features`, `## Enhancements`, `## Bug Fixes`, `## Breaking Changes`, `## Examples`, `## Documentation`, or `## Maintenance`.
421
488
  - Do not write a preamble, tool-call transcript, title, code fence, or explanatory text before the first release-note section.
422
- - Never use the Copilot response stream as output. The caller may discard it after the file is written.
423
- - Do not report the release notes only in the response. If the file cannot be written or verified, the task has failed.
489
+ - Do not add progress prose, an explanation, or a tool trace to the final answer.
490
+ - Never infer "no user-facing changes" because writing, authentication, or
491
+ generation failed. Maintenance requires evidence about the requested range.
424
492
  - Preserve the user-impact categories, concrete examples, evidence-based breaking-change detection, and repository-derived public documentation links described above.
425
493
 
426
494
  ## How to Use This Skill in a Session
@@ -451,9 +519,9 @@ Generate release notes from v2.1.0 to v2.2.0 for /path/to/my-app
451
519
  - Include breaking changes prominently
452
520
  - Omit the entire Breaking Changes section when no breaking change is evidenced
453
521
  - Group related changes
454
- - Add a concrete example for every changed CLI command, public API, configuration option, or before/after workflow, drawing syntax from README, public docs, or CLI/API help
522
+ - Add verified inline syntax only when useful; do not require an Examples section or repeat the same outcome
455
523
  - Discover and link to the project's public documentation when a trustworthy page is available
456
- - Link each qualifying user-facing change to the closest relevant public documentation page when one exists, whether or not that page changed in the release
524
+ - End each qualifying change's bullet with its closest verified public documentation link when available; never move these links into a bottom-of-notes collection
457
525
  - Omit empty sections
458
526
  - If a candidate cannot pass the normal-user audience test, omit it rather than placing it under Maintenance or Documentation
459
527
 
@@ -494,8 +562,8 @@ Generate release notes from v2.1.0 to v2.2.0 for /path/to/my-app
494
562
 
495
563
  ## Limitations
496
564
 
497
- - Requires a git repository with proper tags
565
+ - Requires a Git repository with available ancestor refs; tags are optional
498
566
  - Complex changes may need human interpretation
499
567
  - Very large diffs should be reduced to their evidenced user impact; never summarize them by file count
500
- - Works best with semantic versioning (v1.0.0 format)
568
+ - Accepts tags, branches, or commit IDs; no version naming convention is required
501
569
  - Needs meaningful commit messages for best results, but commit messages alone are never evidence of user impact
@@ -4,7 +4,9 @@
4
4
  from __future__ import annotations
5
5
 
6
6
  import argparse
7
+ import json
7
8
  import os
9
+ import re
8
10
  import subprocess
9
11
  import sys
10
12
  from collections.abc import Sequence
@@ -13,6 +15,9 @@ from pathlib import Path
13
15
  SKILL_NAME = "gh-release-notes"
14
16
  DEFAULT_OUTPUT = Path("release-notes.md")
15
17
  MAX_GIT_CONTEXT_LENGTH = 60_000
18
+ MAX_GENERATION_ATTEMPTS = 3
19
+ COPILOT_TIMEOUT_SECONDS = 300
20
+ DOCUMENTATION_LINK = re.compile(r"\[[^\]\n]+\]\(https://[^\s)]+\)")
16
21
  PERMITTED_HEADINGS = frozenset(
17
22
  {
18
23
  "## New Features",
@@ -95,9 +100,10 @@ def build_git_context(repo: Path, from_ref: str, to_ref: str) -> str:
95
100
  diff = git_output(repo, "diff", "diff", "--no-ext-diff", "--unified=3", f"{from_ref}..{to_ref}")
96
101
  context = (
97
102
  f"Precomputed Git evidence for {from_ref}..{to_ref}:\n\n"
103
+ f"Complete diff (the skill determines user impact, not directory names):\n"
104
+ f"{diff or '(empty)'}\n\n"
98
105
  f"Commit log:\n{log or '(no commits)'}\n\n"
99
- f"Diff summary:\n{diff_stat or '(empty)'}\n\n"
100
- f"Diff:\n{diff or '(empty)'}"
106
+ f"Complete diff summary:\n{diff_stat or '(empty)'}"
101
107
  )
102
108
  if len(context) <= MAX_GIT_CONTEXT_LENGTH:
103
109
  return context
@@ -105,7 +111,9 @@ def build_git_context(repo: Path, from_ref: str, to_ref: str) -> str:
105
111
  truncated = context[:MAX_GIT_CONTEXT_LENGTH]
106
112
  return (
107
113
  f"{truncated}\n\n[Git evidence truncated at {MAX_GIT_CONTEXT_LENGTH} characters; "
108
- "use the included summary and inspect the checked-out files when needed.]"
114
+ "this excerpt is incomplete. Inspect the remaining changes with read/git tools "
115
+ "before deciding release coverage.]\n\n"
116
+ f"Complete diff summary:\n{diff_stat}"
109
117
  )
110
118
 
111
119
 
@@ -125,18 +133,19 @@ def build_prompt(
125
133
  prompt = (
126
134
  f"Use the /{SKILL_NAME} skill. Generate release notes for the exact Git range "
127
135
  f"{from_ref}..{to_ref} in {repo}. The skill is authoritative for analysis, "
128
- f"classification, wording, documentation, and Markdown format. Write the final "
129
- f"release-note Markdown directly to the shared repository file "
130
- f"{output_reference} using the file-writing tools. Its resolved path is {output}. "
131
- "The release workflow reads this exact file as its notes input; the Copilot "
132
- "response stream is discarded. Do not put release notes or a summary in your "
133
- "response, and do not modify any other files. "
136
+ f"classification, wording, documentation, and Markdown format. Return ONLY the "
137
+ f"complete release-note Markdown in your final answer, without commentary, "
138
+ f"title, or fences. The Python caller extracts the structured final_answer "
139
+ f"message, validates it, and writes file {output_reference}; its resolved path "
140
+ f"is {output}. Do not create or edit any files. "
134
141
  "Before writing, enforce the skill's final output contract: render every "
135
142
  "documentation URL as concise inline Markdown such as "
136
143
  "See the [pricing reference for details](https://example.com/pricing), never as a "
137
144
  "bare URL; exclude all internal CI, release automation, governance, "
138
145
  "contributor, agent, generator, Git-evidence, and maintainer content; and "
139
- "omit Maintenance whenever any user-facing section remains."
146
+ "omit Maintenance whenever any user-facing section remains. "
147
+ "End each change's bullet with its closest verified documentation link. "
148
+ "State each outcome once; do not repeat it in Documentation or Examples."
140
149
  )
141
150
  if git_context:
142
151
  prompt += (
@@ -163,14 +172,11 @@ def build_copilot_command(
163
172
  "--no-auto-update",
164
173
  "--no-color",
165
174
  "--output-format",
166
- "text",
175
+ "json",
167
176
  "--disable-builtin-mcps",
168
- "--available-tools=read,create,edit,bash",
177
+ "--available-tools=view,glob,grep,bash,skill",
169
178
  "--allow-tool=read",
170
- "--allow-tool=write",
171
179
  "--allow-tool=shell(git:*)",
172
- "--allow-url=https://github.com",
173
- "--allow-url=https://copilot-session-usage.readthedocs.io",
174
180
  ]
175
181
  if model:
176
182
  command.extend(["--model", model])
@@ -255,18 +261,60 @@ def run_copilot(
255
261
  repo: Path,
256
262
  prompt: str,
257
263
  model: str | None,
258
- ) -> None:
259
- """Run Copilot CLI and leave the generated Markdown in the shared output file."""
260
- result = subprocess.run(
261
- build_copilot_command(prompt, model),
262
- cwd=repo,
263
- check=False,
264
- capture_output=True,
265
- text=True,
266
- )
264
+ ) -> str:
265
+ """Run Copilot CLI and return only its structured final-answer message."""
266
+ try:
267
+ result = subprocess.run(
268
+ build_copilot_command(prompt, model),
269
+ cwd=repo,
270
+ check=False,
271
+ capture_output=True,
272
+ text=True,
273
+ timeout=COPILOT_TIMEOUT_SECONDS,
274
+ )
275
+ except subprocess.TimeoutExpired as error:
276
+ raise RuntimeError(
277
+ f"Copilot CLI timed out after {COPILOT_TIMEOUT_SECONDS} seconds."
278
+ ) from error
267
279
  if result.returncode != 0:
268
280
  output = f"{result.stdout}\n{result.stderr}".strip()
269
281
  raise RuntimeError(f"Copilot CLI failed with exit code {result.returncode}:\n{output}")
282
+ return parse_copilot_response(result.stdout)
283
+
284
+
285
+ def parse_copilot_response(response: str) -> str:
286
+ """Select final Markdown from JSONL events, never from progress or tool output."""
287
+ final_answer: str | None = None
288
+ completed = False
289
+ for line in response.splitlines():
290
+ if not line.strip():
291
+ continue
292
+ try:
293
+ event = json.loads(line)
294
+ except json.JSONDecodeError as error:
295
+ raise RuntimeError("Copilot returned invalid JSONL output.") from error
296
+ if not isinstance(event, dict):
297
+ raise RuntimeError("Copilot returned a non-object JSONL event.")
298
+ data = event.get("data", {})
299
+ if event.get("type") == "result":
300
+ if event.get("exitCode") != 0:
301
+ raise RuntimeError("Copilot reported an unsuccessful result event.")
302
+ completed = True
303
+ elif (
304
+ event.get("type") == "assistant.message"
305
+ and isinstance(data, dict)
306
+ and data.get("phase") == "final_answer"
307
+ and not data.get("toolRequests")
308
+ ):
309
+ content = data.get("content")
310
+ if not isinstance(content, str):
311
+ raise RuntimeError("Copilot final-answer content must be a string.")
312
+ final_answer = content
313
+ if not completed:
314
+ raise RuntimeError("Copilot JSONL output has no successful result event.")
315
+ if final_answer is None:
316
+ raise RuntimeError("Copilot JSONL output has no final-answer message.")
317
+ return final_answer
270
318
 
271
319
 
272
320
  def validate_output(output: Path) -> None:
@@ -275,6 +323,12 @@ def validate_output(output: Path) -> None:
275
323
  content = output.read_text(encoding="utf-8")
276
324
  except OSError as error:
277
325
  raise RuntimeError(f"Unable to read release-note output {output}: {error}") from error
326
+ validate_content(content, output)
327
+ print("Release-note output file verified.")
328
+
329
+
330
+ def validate_content(content: str, output: Path) -> None:
331
+ """Validate complete Markdown without extracting notes from an agent transcript."""
278
332
  if not content.strip():
279
333
  raise RuntimeError(f"Copilot created an empty release-note file: {output}")
280
334
 
@@ -302,7 +356,41 @@ def validate_output(output: Path) -> None:
302
356
  f"Release-note output contains an invalid section heading "
303
357
  f"{invalid_headings[0]!r}: {output}"
304
358
  )
305
- print("Release-note output file verified.")
359
+ if len(headings) != len(set(headings)):
360
+ raise RuntimeError(f"Release-note output contains repeated section headings: {output}")
361
+ if "## Maintenance" in headings and len(headings) != 1:
362
+ raise RuntimeError(f"Maintenance must not accompany user-facing sections: {output}")
363
+ without_links = DOCUMENTATION_LINK.sub("", content)
364
+ if re.search(r"https?://", without_links):
365
+ raise RuntimeError(f"Release-note output contains a bare or invalid URL: {output}")
366
+
367
+ section = ""
368
+ section_has_content = False
369
+ seen_bullets: set[str] = set()
370
+ for line in content.splitlines():
371
+ if not line.strip():
372
+ continue
373
+ if line in PERMITTED_HEADINGS:
374
+ if section and not section_has_content:
375
+ raise RuntimeError(f"Release-note output contains an empty section: {output}")
376
+ section = line
377
+ section_has_content = False
378
+ continue
379
+ section_has_content = True
380
+ if section == "## Maintenance":
381
+ continue
382
+ if not line.startswith("- ") or not line[2:].strip():
383
+ raise RuntimeError(f"Release-note sections must contain concise flat bullets: {output}")
384
+ bullet = " ".join(line[2:].casefold().split()).rstrip(".")
385
+ if bullet in {"none", "n/a", "no breaking changes"}:
386
+ raise RuntimeError(f"Release-note output contains a placeholder bullet: {output}")
387
+ if bullet in seen_bullets:
388
+ raise RuntimeError(f"Release-note output contains a duplicate bullet: {output}")
389
+ seen_bullets.add(bullet)
390
+ if section == "## Documentation" and not DOCUMENTATION_LINK.search(line):
391
+ raise RuntimeError(f"Documentation bullets require an inline HTTPS link: {output}")
392
+ if not section_has_content:
393
+ raise RuntimeError(f"Release-note output contains an empty section: {output}")
306
394
 
307
395
 
308
396
  def generate_release_notes(
@@ -331,13 +419,31 @@ def generate_release_notes(
331
419
  require_copilot_token()
332
420
  run_skill_check(repo)
333
421
  git_context = build_git_context(repo, from_ref, to_ref)
334
- run_copilot(
335
- repo,
336
- build_prompt(from_ref, to_ref, repo, output, git_context),
337
- model,
338
- )
339
-
340
- validate_output(output)
422
+ prompt = build_prompt(from_ref, to_ref, repo, output, git_context)
423
+ feedback = ""
424
+ for attempt in range(1, MAX_GENERATION_ATTEMPTS + 1):
425
+ output.write_text("", encoding="utf-8")
426
+ try:
427
+ response = run_copilot(repo, prompt + feedback, model)
428
+ validate_content(response, output)
429
+ output.write_text(response, encoding="utf-8")
430
+ validate_output(output)
431
+ break
432
+ except RuntimeError as error:
433
+ print(f"Release-note attempt {attempt}/{MAX_GENERATION_ATTEMPTS} failed: {error}")
434
+ if attempt == MAX_GENERATION_ATTEMPTS:
435
+ output.write_text("", encoding="utf-8")
436
+ raise RuntimeError(
437
+ f"Release-note generation failed after {attempt} attempts; "
438
+ f"no notes are safe to publish. Last error: {error}"
439
+ ) from error
440
+ feedback = (
441
+ f"\nThe previous attempt failed validation: {error}\n"
442
+ "Return ONLY the complete release-note Markdown in your final answer, "
443
+ "starting with a permitted ## heading, with no commentary or tool traces. "
444
+ "The Python caller writes the file; do not attempt file-writing tools. "
445
+ "Do not infer Maintenance from a generation failure."
446
+ )
341
447
  print(f"Release notes written to {output}")
342
448
 
343
449