Seclai changelog entries
Vendored file — do not edit in place. The canonical copy lives in the
seclai/sdk-toolsrepository atskills/seclai-changelog/, and is mirrored into each SDK repo withgit subtree. Edits made here are reported as drift bysdk-tools/sync.sh --checkand are overwritten on the next pull. To change it, change it upstream — or, if you can't reach that repo, open an issue on this one describing the fix and a maintainer will carry it across.
All six Seclai SDK repos keep a root CHANGELOG.md in Common Changelog format. This skill covers writing a new entry and backfilling from tags.
Write against the last release, not against the branch
An entry describes what changed between the last released tag and now. Anything introduced and then revised inside the same unreleased version is invisible to consumers and must not appear.
Three of these shipped into a draft in 2026-07:
- "Return
JsonElementfromListAlertsAsyncas before" — typed and then untyped again within an unreleased version. Net change: nothing. - "Omit unset properties on
SetEmailTriggerConfigRequest" — that class was introduced in the same version, so its serialization is not a change from anything. Fold such detail into theAddedentry instead. - "
Total/Page/Limitwere always 0 for callers who opted in" — opting in was itself new in that release, so no released version could have hit it. The real defect predated it and needed describing on its own terms.
Check it mechanically before publishing:
python3 .claude/skills/seclai-sdk-sync/sdksync.py surface <last-tag>
git diff <last-tag> -- <client sources>
If an entry does not correspond to something in that diff, delete it. Counts age badly for the same reason: "adding 22 paths" was written mid-sync and was 23 by the time the version shipped — derive them from the tag, do not carry them forward.
Format rules
- File starts with
# Changelog, then releases sorted latest-first. - Release heading:
## [1.4.0] - 2026-07-25— semver without av, ISO date. - No
Unreleasedsection. Entries are written in the PR that ships them, under the version that PR will become (see "Determine the version"). - Change groups are third-level headings, only these four, always in this order:
### Changed,### Added,### Removed,### Fixed. - A group heading is followed by an unordered list and nothing else.
- Entry form:
- Change ([ref](url)). Imperative present tense, self-describing — it must read correctly without its group heading. "Support CentOS", never "Support of CentOS" or "Added support". - Breaking changes take a
**Breaking:**prefix and sort first within their group. Otherwise sort by importance. - A release may open with a one-sentence italic notice instead of, or before, its groups — used for first releases (
_Initial release._) and no-op version bumps. - Version links are reference-style at the bottom:
[1.4.0]: https://github.com/seclai/<repo>/releases/tag/1.4.0. Check the tag format per repo — seclai-python and seclai-javascript tag without a prefix, seclai-go tagsv1.4.0because Go module resolution requires it. The heading stays bare (## [1.5.0]); only the link target carries thev. Confirm withgit tag --sort=-v:refname | head -1before writing the link block. - Authors are omitted — these are effectively single-contributor repos.
Determine the version
Do not guess the next version. Releases are cut by seclai/github-tag-action in .github/workflows/main-build.yaml, which reads the merge commit message:
- contains
#major→ major bump - contains
#minor→ minor bump - otherwise →
DEFAULT_BUMP: patch
So a PR that adds endpoints must say #minor in its title/merge commit, or the heading you write will not match the tag that gets cut. Confirm the intended bump with the user when it isn't stated, and flag the mismatch risk if the PR title lacks the keyword.
Check git tag --sort=-v:refname | head -1 for the current latest, then apply the bump.
Derive entries from diffs, not from release notes
gh release view bodies are --generate-notes output — just "PR #N by @author". They are useless as entry text. Commit subjects like "2026 05 22 api sync" are equally useless. Always read the actual diff.
Extract the public API delta for a range. Per repo:
| Repo | Path | Pattern |
|---|---|---|
| seclai-javascript | src/client.ts |
async ([a-zA-Z_][a-zA-Z0-9_]*)\( |
| seclai-python | seclai/seclai.py |
^ (async )?def ([a-z][a-z0-9_]*) |
| seclai-go | *.go |
^func \(c \*Client\) ([A-Z][A-Za-z0-9]*)\( |
| seclai-csharp | src/Seclai/SeclaiClient.cs |
public (async )?[A-Za-z<>,? ]+ ([A-Z][A-Za-z0-9]*)\( |
| seclai-cli | src/commands/ |
one file or subcommand per feature |
| seclai-mcp | src/ |
registered tool names |
# methods added between two tags
git diff PREV TAG -- src/ | grep "^+" | grep -oE '<pattern>' | sort -u
# and removed
git diff PREV TAG -- src/ | grep "^-" | grep -oE '<pattern>' | sort -u
A name in both lists was modified, not removed — check the signature diff before writing a Removed entry. Renames and reordering produce false positives constantly.
Also diff the type aliases (src/types.ts, seclai/models, etc.) and the bundled openapi/seclai.openapi.json — new schemas often mean new public types worth an entry even when no method changed.
Classify
- New method, option, type export, or capability → Added
- Changed signature, default, accepted type, or behavior of something that already worked → Changed
- Deleted public surface → Removed
- It was broken and now works → Fixed
Judgment calls that have come up:
- A wrong default host or a wrong request path is Fixed — requests were failing — not Changed.
- Making a required parameter optional (via overload) is Changed, not Added.
- Exporting a type that should already have been exported is Fixed.
- Adding an endpoint to the bundled spec without a client method is worth its own entry; say so plainly rather than implying the method exists.
- A version bump with no code change gets a notice, not a group:
_Stable release. No functional changes since 0.0.1._
Write
Reference the PR when there is one (([#9](https://github.com/seclai/<repo>/pull/9))), otherwise the short commit SHA (([36bff73](https://github.com/seclai/<repo>/commit/36bff73))). For a PR not yet opened, omit the reference — never invent a number.
Group related additions into one entry when they ship as a unit (e.g. eight email-domain methods), and give standalone capabilities their own line. Aim for entries a user scanning for "what changed for me" can act on.
Check the surface before you describe it
The changelog is the last gate before a release, so use the entries you are writing as a prompt to spot-check the public surface they describe. Fix these, or raise them, before they ship — each one is far cheaper now than after consumers depend on it.
Return the shape the spec declares. Look up the endpoint's 2xx response schema and mirror it:
| Spec response | Return type |
|---|---|
$ref to a schema |
that aliased type — never unknown |
type: object, additionalProperties: true |
Record<string, unknown> |
additionalProperties: {type: T} |
Record<string, T> |
type: array |
T[] / unknown[] — not Record |
A hand-written wrapper returning unknown where the generator had a real type
available is a bug, not a style choice.
Widening unknown is breaking. Changing a released method from unknown to
Record<string, unknown> breaks consumer code that does result as SomeInterface
— TypeScript rejects it with TS2352 and demands as unknown as SomeInterface.
That makes it a #major, never a patch or minor. New methods have no consumers
yet, so type them correctly from the start rather than inheriting a neighbour's
unknown.
Server-defaulted request fields must be optional. openapi-typescript emits any
property carrying a default as required, even when the spec leaves it out of
required. Callers then have to pass a value the server would have defaulted.
Wrap the generated request in an input type:
export type AddEmailDomainInput = Pick<AddEmailDomainRequest, "kind" | "value"> &
Partial<Omit<AddEmailDomainRequest, "kind" | "value">>;
See AddEmailDomainInput and CreateExperimentInput in src/types.ts.
Compile the doc examples. npm run typecheck covers src/ and tests/, not
README or changelog snippets. Paste any example you write into a scratch .ts
that imports from ../src/index and compile it — that is how the
delegated-is-required bug above was found, after the snippet had already shipped
in a PR.
Validate
Run the bundled checker from the repo root before declaring done:
python3 .claude/skills/seclai-changelog/validate.py CHANGELOG.md
That path holds in every SDK repo, since this skill is vendored into each one. When working inside sdk-tools itself, the canonical copy is skills/seclai-changelog/validate.py.
It verifies heading format, descending version order, group names and ordering, Breaking: sorting, absence of an Unreleased section, and that every release has exactly one matching link definition. It exits non-zero on error, so the same invocation works as a CI gate.
Also confirm CHANGELOG.md is in the published artifact list — files in package.json for the JS/CLI/MCP repos, the packaging config for the others.
Backfilling from tags
git tag --sort=-v:refnameandgh release listfor versions and dates. Use the GitHub release published date (UTC) as the entry date.- Walk consecutive tag pairs oldest-first, extracting the API delta for each as above.
- Map each range to its PR via
gh release view <tag> --json body, which does at least carry the PR number reliably. - The earliest release gets
_Initial release._.