@kitschpatrol/chagent 0.2.0 → 0.4.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/dist/bin/cli.js CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
- import { C as version, S as maxTimeoutMinutes, b as defaultModels, i as generateChangelog, l as agentNames, n as previewChangelog, r as reviewChangelog, s as writeChangelog, t as assertReviewTerminal, u as efforts, x as defaults, y as setLogger } from "../review-BwgeC2dB.js";
3
- import { n as assertAmendable, t as amendRelease } from "../lib-DTp3sA-Q.js";
2
+ import { C as maxTimeoutMinutes, S as defaults, b as setLogger, d as rootChangelogs, i as generateChangelog, l as agentNames, n as previewChangelog, r as reviewChangelog, s as writeChangelog, t as assertReviewTerminal, u as efforts, w as version, x as defaultModels } from "../review-DftIAoEl.js";
3
+ import { n as assertAmendable, t as amendRelease } from "../lib-Byxdn2st.js";
4
4
  import { t as createProgressLine } from "../progress-DQtRnH1N.js";
5
5
  import { createLogger } from "lognow";
6
6
  import path from "node:path";
@@ -109,6 +109,16 @@ try {
109
109
  default: false,
110
110
  description: "Amend a verified unpublished release commit and matching local tags",
111
111
  type: "boolean"
112
+ }).option("root-changelog", {
113
+ choices: rootChangelogs,
114
+ defaultDescription: defaults.rootChangelog,
115
+ description: "Monorepo root changelog: a summary of package releases, the root package alone, or none",
116
+ type: "string"
117
+ }).option("skip-package", {
118
+ array: true,
119
+ defaultDescription: "none",
120
+ description: "Workspace package directory glob to leave without a changelog; repeatable",
121
+ type: "string"
112
122
  }).option("github", {
113
123
  defaultDescription: String(defaults.github),
114
124
  description: "Enrich Git evidence using gh when available; disable with --no-github",
@@ -148,7 +158,9 @@ try {
148
158
  reporter.report(message);
149
159
  },
150
160
  rebuild: args.rebuild,
161
+ rootChangelog: args.rootChangelog,
151
162
  signal: controller.signal,
163
+ skipPackage: args.skipPackage,
152
164
  timeout: args.timeout
153
165
  }));
154
166
  try {
@@ -1,4 +1,4 @@
1
- import { u as GenerateOptions } from "../types-COQP1I5w.js";
1
+ import { u as GenerateOptions } from "../types-B6FOOkTQ.js";
2
2
  import { VersionBumpOptions } from "bumpp";
3
3
  type ExecuteHook = Exclude<NonNullable<VersionBumpOptions['execute']>, string>;
4
4
  export type BumppHookOptions = Omit<GenerateOptions, 'cwd' | 'release'> & {
package/dist/lib/bumpp.js CHANGED
@@ -1,4 +1,4 @@
1
- import { g as git, i as generateChangelog, m as localDate, r as reviewChangelog, s as writeChangelog, t as assertReviewTerminal } from "../review-BwgeC2dB.js";
1
+ import { _ as git, h as localDate, i as generateChangelog, r as reviewChangelog, s as writeChangelog, t as assertReviewTerminal } from "../review-DftIAoEl.js";
2
2
  import { t as createProgressLine } from "../progress-DQtRnH1N.js";
3
3
 
4
4
  // src/lib/bumpp.ts
@@ -1,4 +1,4 @@
1
- import { _ as Config, a as ChangeSet, c as Evidence, d as GenerationRequest, f as GenerationResponse, g as RunSettings, h as ReleaseSummary, i as Change, l as FileEdit, m as ReleaseRequest, n as AgentName, o as ChangelogPlan, p as ReleaseContext, r as Category, s as Effort, t as AgentAdapter, u as GenerateOptions, v as defineConfig, y as loadConfig } from "../types-COQP1I5w.js";
1
+ import { _ as RunSettings, a as ChangeSet, b as loadConfig, c as Evidence, d as GenerationRequest, f as GenerationResponse, g as RootChangelog, h as ReleaseSummary, i as Change, l as FileEdit, m as ReleaseRequest, n as AgentName, o as ChangelogPlan, p as ReleaseContext, r as Category, s as Effort, t as AgentAdapter, u as GenerateOptions, v as Config, y as defineConfig } from "../types-B6FOOkTQ.js";
2
2
  import { ILogBasic, ILogLayer } from "lognow";
3
3
  /** Create an adapter for an installed CLI using its existing authentication. */
4
4
  export declare function createAgent(name: AgentName): AgentAdapter;
@@ -28,4 +28,4 @@ export declare function reviewChangelog(plan: ChangelogPlan, options?: {
28
28
  * writes.
29
29
  */
30
30
  export declare function writeChangelog(plan: ChangelogPlan): Promise<string[]>;
31
- export { type AgentAdapter, type AgentName, type Category, type Change, type ChangeSet, type ChangelogPlan, type Config, type Effort, type Evidence, type FileEdit, type GenerateOptions, type GenerationRequest, type GenerationResponse, type ReleaseContext, type ReleaseRequest, type ReleaseSummary, type RunSettings, defineConfig, loadConfig };
31
+ export { type AgentAdapter, type AgentName, type Category, type Change, type ChangeSet, type ChangelogPlan, type Config, type Effort, type Evidence, type FileEdit, type GenerateOptions, type GenerationRequest, type GenerationResponse, type ReleaseContext, type ReleaseRequest, type ReleaseSummary, type RootChangelog, type RunSettings, defineConfig, loadConfig };
package/dist/lib/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { a as defineConfig, c as createAgent, i as generateChangelog, n as previewChangelog, o as loadConfig, r as reviewChangelog, s as writeChangelog, y as setLogger } from "../review-BwgeC2dB.js";
2
- import { t as amendRelease } from "../lib-DTp3sA-Q.js";
1
+ import { a as defineConfig, b as setLogger, c as createAgent, i as generateChangelog, n as previewChangelog, o as loadConfig, r as reviewChangelog, s as writeChangelog } from "../review-DftIAoEl.js";
2
+ import { t as amendRelease } from "../lib-Byxdn2st.js";
3
3
 
4
4
  export { amendRelease, createAgent, defineConfig, generateChangelog, loadConfig, previewChangelog, reviewChangelog, setLogger, writeChangelog };
@@ -1,4 +1,4 @@
1
- import { _ as isAncestor, d as LOCKFILE_PATH, f as inspectRepository, g as git, h as manifestAt, p as isVersionOnlyChange, s as writeChangelog, v as runCommand } from "./review-BwgeC2dB.js";
1
+ import { _ as git, f as LOCKFILE_PATH, g as manifestAt, m as isVersionOnlyChange, p as inspectRepository, s as writeChangelog, v as isAncestor, y as runCommand } from "./review-DftIAoEl.js";
2
2
  import path from "node:path";
3
3
  import { realpath } from "node:fs/promises";
4
4
 
@@ -15,7 +15,7 @@ import { isCancel, select } from "@clack/prompts";
15
15
 
16
16
  // package.json
17
17
  var name = "@kitschpatrol/chagent";
18
- var version = "0.2.0";
18
+ var version = "0.4.0";
19
19
  var engines = { "node": "^24.16.0 || >=26.3.0" };
20
20
 
21
21
  // src/lib/defaults.ts
@@ -25,6 +25,7 @@ const defaults = {
25
25
  effort: "xhigh",
26
26
  github: true,
27
27
  maxInputChars: 8e4,
28
+ rootChangelog: "summary",
28
29
  timeout: 20
29
30
  };
30
31
  const defaultModels = {
@@ -229,8 +230,11 @@ async function readBlobs(root, objects) {
229
230
  }
230
231
 
231
232
  // src/lib/repository.ts
232
- /** Matches package manager lockfile paths. */
233
- const LOCKFILE_PATH = /(?:^|\/)(?:pnpm-lock\.yaml|package-lock\.json|yarn\.lock|bun\.lockb?)$/v;
233
+ /**
234
+ * Matches package manager lockfile paths. Excludes npm-shrinkwrap.json, which
235
+ * is published and constrains consumers.
236
+ */
237
+ const LOCKFILE_PATH = /(?:^|\/)(?:pnpm-lock\.yaml|package-lock\.json|yarn\.lock|bun\.lockb?|deno\.lock|uv\.lock|poetry\.lock|pdm\.lock|Pipfile\.lock|Cargo\.lock|Gemfile\.lock|composer\.lock|go\.sum)$/v;
234
238
  const TAG_DECORATION = "tag: refs/tags/";
235
239
  const GITHUB_REMOTE = /github\.com[:\/]([^\/]+\/[^\/]+?)(?:\.git)?$/v;
236
240
  const manifestSchema$1 = z.object({
@@ -483,21 +487,15 @@ function pnpmWorkspaceGlobs(text) {
483
487
  if (!workspace.success) throw new Error(`Invalid pnpm-workspace.yaml:\n${z.prettifyError(workspace.error)}`, { cause: workspace.error });
484
488
  return workspace.data?.packages;
485
489
  }
486
- async function workspacePackages(root, manifest, config) {
487
- let patterns = Array.isArray(manifest?.workspaces) ? manifest.workspaces : manifest?.workspaces?.packages ?? [];
488
- const workspaceText = await readOptional(path.join(root, "pnpm-workspace.yaml"));
489
- if (workspaceText !== void 0) patterns = pnpmWorkspaceGlobs(workspaceText) ?? patterns;
490
- patterns = config.packages ?? patterns;
490
+ /** Find directories matching globs, as POSIX paths relative to the root. */
491
+ async function matchDirectories(root, patterns, exclude = []) {
491
492
  const directories = [];
492
- const positive = patterns.filter((pattern) => !pattern.startsWith("!"));
493
- const negative = patterns.filter((pattern) => pattern.startsWith("!")).map((pattern) => pattern.slice(1));
494
- const matches = glob(positive, {
493
+ const matches = glob(patterns, {
495
494
  cwd: root,
496
495
  exclude: [
497
496
  "**/node_modules/**",
498
497
  "**/.git/**",
499
- ...negative,
500
- ...config.exclude ?? []
498
+ ...exclude
501
499
  ],
502
500
  withFileTypes: true
503
501
  });
@@ -505,6 +503,27 @@ async function workspacePackages(root, manifest, config) {
505
503
  if (!entry.isDirectory()) continue;
506
504
  directories.push(path.relative(root, path.join(entry.parentPath, entry.name)).split(path.sep).join("/"));
507
505
  }
506
+ return directories;
507
+ }
508
+ /** Mark the packages that remain boundaries but get no changelog. */
509
+ async function markSkipped(root, packages, patterns) {
510
+ const skipped = /* @__PURE__ */ new Set();
511
+ for (const pattern of patterns) {
512
+ const matches = (await matchDirectories(root, [pattern])).filter((directory) => packages.some((pkg) => pkg.directory === directory));
513
+ if (matches.length === 0) throw new Error(`skipPackage pattern "${pattern}" matches no workspace package directory. Use a directory glob relative to the repository root, such as "packages/docs"; rootChangelog controls the root changelog.`);
514
+ for (const directory of matches) skipped.add(directory);
515
+ }
516
+ return packages.map((pkg) => skipped.has(pkg.directory) ? {
517
+ ...pkg,
518
+ skipped: true
519
+ } : pkg);
520
+ }
521
+ async function workspacePackages(root, manifest, config) {
522
+ let patterns = Array.isArray(manifest?.workspaces) ? manifest.workspaces : manifest?.workspaces?.packages ?? [];
523
+ const workspaceText = await readOptional(path.join(root, "pnpm-workspace.yaml"));
524
+ if (workspaceText !== void 0) patterns = pnpmWorkspaceGlobs(workspaceText) ?? patterns;
525
+ patterns = config.packages ?? patterns;
526
+ const directories = await matchDirectories(root, patterns.filter((pattern) => !pattern.startsWith("!")), [...patterns.filter((pattern) => pattern.startsWith("!")).map((pattern) => pattern.slice(1)), ...config.exclude ?? []]);
508
527
  const packages = [];
509
528
  for (const directory of directories.toSorted()) {
510
529
  if (!(await realpath(path.join(root, directory))).startsWith(root + path.sep)) throw new Error(`Workspace package escapes the repository: ${directory}`);
@@ -512,7 +531,11 @@ async function workspacePackages(root, manifest, config) {
512
531
  if (text === void 0 && config.packages === void 0) continue;
513
532
  packages.push(packageInfo(text === void 0 ? void 0 : parseManifest(text, `${directory}/package.json`), directory, path.posix.basename(directory)));
514
533
  }
515
- return packages;
534
+ return markSkipped(root, packages, config.skipPackage ?? []);
535
+ }
536
+ /** Select the workspace packages that get changelogs of their own. */
537
+ function describedPackages(repository) {
538
+ return repository.packages.filter((pkg) => pkg.skipped !== true);
516
539
  }
517
540
  async function taggedReleases(repository, pkg, config) {
518
541
  const candidates = [];
@@ -628,7 +651,11 @@ runtime compatibility, installation and distribution, documentation and examples
628
651
  dependencies, builds, tests, CI, and release tooling. Include meaningful maintenance
629
652
  even when it is the only change in a release. Group routine runtime dependency
630
653
  updates into a useful entry, and group development dependency/tooling refreshes
631
- instead of listing every version. Identify important packages or migrations when
654
+ instead of listing every version. A dependency change is a change to a declared
655
+ requirement, such as a version range in a manifest, workspace catalog, or
656
+ override. Lockfiles only record resolved versions: omit refreshed resolutions and
657
+ transitive updates that appear only in a lockfile, even when the same commit
658
+ changes other files. Identify important packages or migrations when
632
659
  the evidence supports them. Classify dependency and tooling changes by their
633
660
  actual impact: fixing runtime compatibility for consumers is Fixed, while fixing
634
661
  compatibility of contributor tooling alone is Maintenance. Documentation
@@ -703,8 +730,9 @@ Before returning, check coverage across behavior/API, compatibility, dependencie
703
730
  documentation, build/test/CI, and distribution. Do not drop a change just because
704
731
  its commit is called chore, docs, build, or dependency updates. Return an empty
705
732
  changes array only when the evidence is entirely release-version bookkeeping,
706
- generated artifacts, formatting noise, or changes already covered by existing
707
- entries. Do not pad the result with unsupported changes.`;
733
+ lockfile-only resolution changes, generated artifacts, formatting noise, or
734
+ changes already covered by existing entries. Do not pad the result with
735
+ unsupported changes.`;
708
736
  /** Invalidates cached drafts whenever the instructions change. */
709
737
  const instructionVersion = hash(instructions);
710
738
 
@@ -727,6 +755,11 @@ const efforts = [
727
755
  "xhigh",
728
756
  "max"
729
757
  ];
758
+ const rootChangelogs = [
759
+ "summary",
760
+ "package",
761
+ "none"
762
+ ];
730
763
 
731
764
  // src/lib/agents.ts
732
765
  const CONTROL_CHARACTERS = /\p{Cc}/v;
@@ -1054,6 +1087,13 @@ const configSchema = z.strictObject({
1054
1087
  model: z.string().min(1).optional(),
1055
1088
  /** Workspace directory globs, overriding manifest workspace discovery. */
1056
1089
  packages: z.array(z.string()).optional(),
1090
+ /**
1091
+ * What a monorepo's root changelog records: a summary of package releases,
1092
+ * the root package's own changes, or nothing.
1093
+ */
1094
+ rootChangelog: z.enum(rootChangelogs).optional(),
1095
+ /** Workspace package directory globs that get no changelog of their own. */
1096
+ skipPackage: z.array(z.string()).optional(),
1057
1097
  /** Templates containing {version} and optionally {name}. */
1058
1098
  tagPatterns: z.array(z.string().includes("{version}")).optional(),
1059
1099
  /** Minutes each agent session may run. */
@@ -1065,6 +1105,7 @@ const resolvedConfigSchema = configSchema.required({
1065
1105
  effort: true,
1066
1106
  github: true,
1067
1107
  maxInputChars: true,
1108
+ rootChangelog: true,
1068
1109
  timeout: true
1069
1110
  }).transform(({ timeout, ...config }) => ({
1070
1111
  ...config,
@@ -1315,15 +1356,19 @@ const GENERATED_DIRECTORY = /^(?:build|changelog|coverage|dist)(?:\/|$)/iv;
1315
1356
  const GENERATED_ANYWHERE = /(?:^|\/)(?:changelog(?:\.md)?$|node_modules(?:\/|$))/iv;
1316
1357
  /**
1317
1358
  * Generated output lives at the top of a package, so `packages/build` and
1318
- * `src/dist` remain source.
1359
+ * `src/dist` remain source. Lockfiles record resolved versions, not declared
1360
+ * dependency ranges.
1319
1361
  */
1320
1362
  function ignored(file, packages) {
1321
1363
  const owner = packages.findLast(({ directory }) => file.startsWith(`${directory}/`));
1322
1364
  const relative = owner === void 0 ? file : file.slice(owner.directory.length + 1);
1323
- return GENERATED_ANYWHERE.test(file) || GENERATED_DIRECTORY.test(relative);
1365
+ return LOCKFILE_PATH.test(file) || GENERATED_ANYWHERE.test(file) || GENERATED_DIRECTORY.test(relative);
1324
1366
  }
1325
- async function scopes(repository, pkg, ref) {
1326
- if (pkg.directory === ".") return ["."];
1367
+ /**
1368
+ * A package's directory and, transitively, those of the workspace packages it
1369
+ * depends on at a ref.
1370
+ */
1371
+ async function packageScope(repository, pkg, ref) {
1327
1372
  const directories = /* @__PURE__ */ new Set([pkg.directory]);
1328
1373
  const visit = async (current) => {
1329
1374
  const manifest = await manifestAt(repository.root, current.directory, ref);
@@ -1349,11 +1394,7 @@ function manifestObjects(files, before, after) {
1349
1394
  return files.flatMap((file) => isManifest(file) ? [`${before}:${file}`, `${after}:${file}`] : []);
1350
1395
  }
1351
1396
  function hasSourceChanges(files, commit, blobs) {
1352
- for (const file of files) {
1353
- if (LOCKFILE_PATH.test(file)) continue;
1354
- if (!isManifest(file) || !isVersionOnlyEdit(blobs.get(`${commit}^:${file}`), blobs.get(`${commit}:${file}`))) return true;
1355
- }
1356
- return files.length > 0 && files.every((file) => LOCKFILE_PATH.test(file));
1397
+ return files.some((file) => !isManifest(file) || !isVersionOnlyEdit(blobs.get(`${commit}^:${file}`), blobs.get(`${commit}:${file}`)));
1357
1398
  }
1358
1399
  /** Read commits and their changed files from one process. */
1359
1400
  async function commitsInRange(root, range, paths) {
@@ -1397,7 +1438,7 @@ async function commitsInRange(root, range, paths) {
1397
1438
  /** Collect source changes, excluding generated notes and release-only commits. */
1398
1439
  async function collectEvidence(repository, pkg, release, excludeDirectories = []) {
1399
1440
  const { packages, root } = repository;
1400
- const paths = [...(await scopes(repository, pkg, release.ref)).map((directory) => `:(literal)${directory}`), ...excludeDirectories.map((directory) => `:(exclude,literal)${directory}`)];
1441
+ const paths = [...(pkg.directory === "." ? ["."] : await packageScope(repository, pkg, release.ref)).map((directory) => `:(literal)${directory}`), ...excludeDirectories.map((directory) => `:(exclude,literal)${directory}`)];
1401
1442
  const range = release.previousRef === void 0 ? release.ref : `${release.previousRef}..${release.ref}`;
1402
1443
  const from = release.previousRef ?? await git(root, [
1403
1444
  "hash-object",
@@ -1637,7 +1678,7 @@ function researchEvidence(evidence, limit) {
1637
1678
  async function prepareRelease(context, pkg, release, existingEntries, mode, from, onlyRepositoryFiles = false) {
1638
1679
  const { options, repository } = context;
1639
1680
  options.signal?.throwIfAborted();
1640
- const excludeDirectories = onlyRepositoryFiles ? repository.packages.map((item) => item.directory) : [];
1681
+ const excludeDirectories = await excludedPackages(context, pkg, release.ref, onlyRepositoryFiles);
1641
1682
  return {
1642
1683
  evidence: await collectEvidence(repository, pkg, from === void 0 ? release : {
1643
1684
  ...release,
@@ -1654,10 +1695,24 @@ async function prepareRelease(context, pkg, release, existingEntries, mode, from
1654
1695
  from: from ?? release.previousRef,
1655
1696
  to: release.ref
1656
1697
  },
1657
- scope: pkg.directory === "." && repository.packages.length > 0 ? "repository" : "package",
1698
+ scope: pkg.directory === "." && repository.packages.length > 0 && options.rootChangelog === "summary" ? "repository" : "package",
1658
1699
  version: release.version
1659
1700
  };
1660
1701
  }
1702
+ /**
1703
+ * Select the package directories a release leaves out: every package when
1704
+ * describing repository files alone, skipped packages in a whole-repository
1705
+ * summary, or, when the root is a package itself, those it does not depend on.
1706
+ */
1707
+ async function excludedPackages(context, pkg, ref, onlyRepositoryFiles) {
1708
+ const { options, repository } = context;
1709
+ const directories = repository.packages.map((item) => item.directory);
1710
+ if (onlyRepositoryFiles || directories.length === 0) return directories;
1711
+ if (pkg.directory !== ".") return [];
1712
+ if (options.rootChangelog === "summary") return repository.packages.filter((item) => item.skipped === true).map((item) => item.directory);
1713
+ const scope = new Set(await packageScope(repository, pkg, ref));
1714
+ return directories.filter((directory) => !scope.has(directory));
1715
+ }
1661
1716
  function cacheKey(context, release) {
1662
1717
  const { effort, maxInputChars, model } = context.options;
1663
1718
  return JSON.stringify({
@@ -2071,7 +2126,7 @@ function aggregateShared(context, prepared, results, exception, quiet) {
2071
2126
  const rootEdit = plan.files.find((file) => path.dirname(file.path) === repository.root);
2072
2127
  if (rootEdit === void 0) return;
2073
2128
  const additions = prepared.summaries.map((summary) => ({
2074
- changes: aggregateChanges(repository, plan.releases.filter((item) => item.version === summary.version && !quiet.has(`${item.changelogPath}@${item.version}`))),
2129
+ changes: aggregateChanges(repository, plan.releases.filter((item) => item.version === summary.version && item.ref === summary.ref && !quiet.has(`${item.changelogPath}@${item.version}`))),
2075
2130
  existingBody: parseChangelog(rootEdit.after).sections.find((section) => section.version === summary.version)?.body,
2076
2131
  release: prepared.releases.find((item) => item.version === summary.version)
2077
2132
  }));
@@ -2084,11 +2139,11 @@ async function generateWorkspace(context) {
2084
2139
  const shared = rootReleases.some((release) => release.version !== "Unreleased");
2085
2140
  const rootFile = await findChangelog(repository.root);
2086
2141
  const rootContent = options.rebuild ? void 0 : rootFile.content;
2087
- const rootMissing = shared ? new Set(missingReleases(rootReleases, rootContent).map((release) => release.version)) : void 0;
2088
- const needsPackageHistory = rootMissing === void 0 || rootMissing.values().some((version) => version !== "Unreleased");
2089
- const recordedAtRoot = (summary) => rootMissing === void 0 ? rootContent !== void 0 && missingPackageSummaries([summary], rootContent).length === 0 : !rootMissing.has(summary.version);
2142
+ const rootMissing = shared ? missingReleases(rootReleases, rootContent) : void 0;
2143
+ const needsPackageHistory = rootMissing === void 0 || rootMissing.some((release) => release.version !== "Unreleased");
2144
+ const recordedAtRoot = (summary) => rootMissing === void 0 ? rootContent !== void 0 && missingPackageSummaries([summary], rootContent).length === 0 : rootMissing.every((release) => release.version !== summary.version || release.ref !== summary.ref);
2090
2145
  const packages = [];
2091
- for (const pkg of repository.packages) packages.push(await preparePackage(context, pkg, needsPackageHistory, recordedAtRoot));
2146
+ for (const pkg of describedPackages(repository)) packages.push(await preparePackage(context, pkg, needsPackageHistory, recordedAtRoot));
2092
2147
  const root = shared ? await preparePackage(context, repository.rootPackage) : void 0;
2093
2148
  const independent = shared ? void 0 : await prepareIndependent(context, packages.flatMap((pkg) => pkg.summaries));
2094
2149
  const requests = [...packages, ...root === void 0 ? [] : [root]].flatMap((pkg) => pkg.work.map((item) => item.request));
@@ -2122,6 +2177,7 @@ async function generateChangelog(options = {}) {
2122
2177
  log.debug(`Resolved options: ${JSON.stringify(config)}`);
2123
2178
  const repository = await inspectRepository(root, config);
2124
2179
  log.debug(`Repository ${root} at ${repository.head}: ${repository.tags.length} reachable tags, ${repository.packages.length} workspace packages.`);
2180
+ if (config.rootChangelog === "none" && describedPackages(repository).length === 0) throw new Error("The root changelog is set to \"none\", but no workspace packages remain to describe, so there is no changelog to generate. Choose another root changelog mode, or configure packages that are not skipped.");
2125
2181
  const agent = typeof selected === "object" ? selected : createAgent(config.agent);
2126
2182
  const plan = {
2127
2183
  checkpoints: [],
@@ -2149,14 +2205,20 @@ async function generateChangelog(options = {}) {
2149
2205
  plan,
2150
2206
  repository
2151
2207
  };
2152
- if (repository.packages.length === 0) {
2153
- const prepared = await preparePackage(context, repository.rootPackage);
2154
- const results = await generateReleases(context, prepared.work.map((item) => item.request));
2155
- plan.releases.push(...renderPackage(context, prepared, results));
2156
- } else await generateWorkspace(context);
2208
+ if (repository.packages.length > 0 && config.rootChangelog === "summary") await generateWorkspace(context);
2209
+ else await generatePackages(context);
2157
2210
  if (rebuild) keepEmptyRebuilds(plan);
2158
2211
  return plan;
2159
2212
  }
2213
+ /** Generate each changelog on its own, with no root summary. */
2214
+ async function generatePackages(context) {
2215
+ const { options, plan, repository } = context;
2216
+ const packages = options.rootChangelog === "none" ? describedPackages(repository) : [repository.rootPackage, ...describedPackages(repository)];
2217
+ const prepared = [];
2218
+ for (const pkg of packages) prepared.push(await preparePackage(context, pkg));
2219
+ const results = await generateReleases(context, prepared.flatMap((item) => item.work.map((work) => work.request)));
2220
+ for (const item of prepared) plan.releases.push(...renderPackage(context, item, results));
2221
+ }
2160
2222
  /** A rebuild that finds nothing to record must not erase an existing changelog. */
2161
2223
  function keepEmptyRebuilds(plan) {
2162
2224
  plan.files = plan.files.filter((edit) => {
@@ -2317,4 +2379,4 @@ async function reviewChangelog(plan, options = {}) {
2317
2379
  }
2318
2380
  }
2319
2381
 
2320
- export { version as C, maxTimeoutMinutes as S, isAncestor as _, defineConfig as a, defaultModels as b, createAgent as c, LOCKFILE_PATH as d, inspectRepository as f, git as g, manifestAt as h, generateChangelog as i, agentNames as l, localDate as m, previewChangelog as n, loadConfig as o, isVersionOnlyChange as p, reviewChangelog as r, writeChangelog as s, assertReviewTerminal as t, efforts as u, runCommand as v, defaults as x, setLogger as y };
2382
+ export { maxTimeoutMinutes as C, defaults as S, git as _, defineConfig as a, setLogger as b, createAgent as c, rootChangelogs as d, LOCKFILE_PATH as f, manifestAt as g, localDate as h, generateChangelog as i, agentNames as l, isVersionOnlyChange as m, previewChangelog as n, loadConfig as o, inspectRepository as p, reviewChangelog as r, writeChangelog as s, assertReviewTerminal as t, efforts as u, isAncestor as v, version as w, defaultModels as x, runCommand as y };
@@ -17,6 +17,12 @@ declare const configSchema: z.ZodObject<{
17
17
  maxInputChars: z.ZodOptional<z.ZodNumber>;
18
18
  model: z.ZodOptional<z.ZodString>;
19
19
  packages: z.ZodOptional<z.ZodArray<z.ZodString>>;
20
+ rootChangelog: z.ZodOptional<z.ZodEnum<{
21
+ summary: "summary";
22
+ package: "package";
23
+ none: "none";
24
+ }>>;
25
+ skipPackage: z.ZodOptional<z.ZodArray<z.ZodString>>;
20
26
  tagPatterns: z.ZodOptional<z.ZodArray<z.ZodString>>;
21
27
  timeout: z.ZodOptional<z.ZodNumber>;
22
28
  }, z.core.$strict>;
@@ -29,8 +35,10 @@ declare const categories: readonly ["Added", "Changed", "Deprecated", "Removed",
29
35
  type Category = (typeof categories)[number];
30
36
  declare const agentNames: readonly ["codex", "claude"];
31
37
  declare const efforts: readonly ["low", "medium", "high", "xhigh", "max"];
38
+ declare const rootChangelogs: readonly ["summary", "package", "none"];
32
39
  type AgentName = (typeof agentNames)[number];
33
40
  type Effort = (typeof efforts)[number];
41
+ type RootChangelog = (typeof rootChangelogs)[number];
34
42
  type Change = {
35
43
  breaking: boolean;
36
44
  category: Category;
@@ -159,4 +167,4 @@ type ChangelogPlan = {
159
167
  releases: ReleaseSummary[];
160
168
  root: string;
161
169
  };
162
- export { Config as _, ChangeSet as a, Evidence as c, GenerationRequest as d, GenerationResponse as f, RunSettings as g, ReleaseSummary as h, Change as i, FileEdit as l, ReleaseRequest as m, AgentName as n, ChangelogPlan as o, ReleaseContext as p, Category as r, Effort as s, AgentAdapter as t, GenerateOptions as u, defineConfig as v, loadConfig as y };
170
+ export { RunSettings as _, ChangeSet as a, loadConfig as b, Evidence as c, GenerationRequest as d, GenerationResponse as f, RootChangelog as g, ReleaseSummary as h, Change as i, FileEdit as l, ReleaseRequest as m, AgentName as n, ChangelogPlan as o, ReleaseContext as p, Category as r, Effort as s, AgentAdapter as t, GenerateOptions as u, Config as v, defineConfig as y };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kitschpatrol/chagent",
3
- "version": "0.2.0",
3
+ "version": "0.4.0",
4
4
  "description": "Changelog agent. CLI tool and TypeScript library to generate and update changelogs with a little help from your LLMs.",
5
5
  "keywords": [
6
6
  "changelog",
package/readme.md CHANGED
@@ -132,24 +132,26 @@ Usage:
132
132
  chagent
133
133
  ```
134
134
 
135
- | Option | Description | Type | Default |
136
- | ----------------------- | --------------------------------------------------------------------- | --------------------------------------------- | --------------------------------------------- |
137
- | `--cwd` | Repository directory | `string` | current directory |
138
- | `--agent` | Installed agent CLI | `"codex"` `"claude"` | `codex` |
139
- | `--model` | Agent model | `string` | gpt-6.1-sol (Codex); claude-opus-5-5 (Claude) |
140
- | `--effort` | Reasoning effort | `"low"` `"medium"` `"high"` `"xhigh"` `"max"` | `xhigh` |
141
- | `--interactive`<br>`-i` | Review a diff and optionally edit before writing | `boolean` | `false` |
142
- | `--editor` | Editor command for interactive review; configure GUI editors to wait | `string` | git var GIT_EDITOR |
143
- | `--dry-run` | Print a diff without applying changes | `boolean` | `false` |
144
- | `--rebuild` | Reconstruct all history, replacing existing release text | `boolean` | `false` |
145
- | `--amend` | Amend a verified unpublished release commit and matching local tags | `boolean` | `false` |
146
- | `--github` | Enrich Git evidence using gh when available; disable with --no-github | `boolean` | `true` |
147
- | `--cache` | Cache drafts and incremental state; disable with --no-cache | `boolean` | `true` |
148
- | `--config` | Configuration file path | `string` | auto-detected from repository root |
149
- | `--timeout` | Agent timeout in minutes | `number` | `20` |
150
- | `--verbose` | Log commands, cache decisions, and session details to stderr | `boolean` | `false` |
151
- | `--help`<br>`-h` | Show help | `boolean` | |
152
- | `--version`<br>`-v` | Show version number | `boolean` | |
135
+ | Option | Description | Type | Default |
136
+ | ----------------------- | --------------------------------------------------------------------------------------- | --------------------------------------------- | --------------------------------------------- |
137
+ | `--cwd` | Repository directory | `string` | current directory |
138
+ | `--agent` | Installed agent CLI | `"codex"` `"claude"` | `codex` |
139
+ | `--model` | Agent model | `string` | gpt-6.1-sol (Codex); claude-opus-5-5 (Claude) |
140
+ | `--effort` | Reasoning effort | `"low"` `"medium"` `"high"` `"xhigh"` `"max"` | `xhigh` |
141
+ | `--interactive`<br>`-i` | Review a diff and optionally edit before writing | `boolean` | `false` |
142
+ | `--editor` | Editor command for interactive review; configure GUI editors to wait | `string` | git var GIT_EDITOR |
143
+ | `--dry-run` | Print a diff without applying changes | `boolean` | `false` |
144
+ | `--rebuild` | Reconstruct all history, replacing existing release text | `boolean` | `false` |
145
+ | `--amend` | Amend a verified unpublished release commit and matching local tags | `boolean` | `false` |
146
+ | `--root-changelog` | Monorepo root changelog: a summary of package releases, the root package alone, or none | `"summary"` `"package"` `"none"` | `summary` |
147
+ | `--skip-package` | Workspace package directory glob to leave without a changelog; repeatable | `array` | `none` |
148
+ | `--github` | Enrich Git evidence using gh when available; disable with --no-github | `boolean` | `true` |
149
+ | `--cache` | Cache drafts and incremental state; disable with --no-cache | `boolean` | `true` |
150
+ | `--config` | Configuration file path | `string` | auto-detected from repository root |
151
+ | `--timeout` | Agent timeout in minutes | `number` | `20` |
152
+ | `--verbose` | Log commands, cache decisions, and session details to stderr | `boolean` | `false` |
153
+ | `--help`<br>`-h` | Show help | `boolean` | |
154
+ | `--version`<br>`-v` | Show version number | `boolean` | |
153
155
 
154
156
  <!-- /cli-help -->
155
157
 
@@ -246,6 +248,10 @@ For an existing post-bump workflow, `chagent --amend` explicitly amends an unpub
246
248
 
247
249
  Workspace declarations in `pnpm-workspace.yaml` or `package.json` determine the packages to process. Each current workspace package gets its own changelog. The root contains repository highlights and links to package releases.
248
250
 
251
+ Use the `rootChangelog` option or `--root-changelog` flag to choose what the root changelog records. The default, `summary`, works as described above. `package` treats the root as a package of its own, for repositories whose main package lives at the root: its changelog describes the root's files and the workspace packages it depends on in full detail, without changes from other packages or links to their releases. `none` writes only the package changelogs. After changing the mode of an existing root changelog, run with `--rebuild` to reconstruct its history in the new form.
252
+
253
+ Use `skipPackage` directory globs, or repeat `--skip-package`, to leave workspace packages such as documentation sites or examples without changelogs. A skipped package is left out of the root changelog, and its changes are not counted among the root's own, but packages that depend on it still include its relevant changes. To count a directory's changes as the root's instead, `exclude` it from workspace discovery. A pattern that matches no workspace package is an error.
254
+
249
255
  For other monorepo layouts, configure `packages` directory globs. These directories do not need npm manifests; their Git tags identify releases, using the directory basename as the package name.
250
256
 
251
257
  Shared-version repositories use repository versions at the root. Independently versioned repositories use package-qualified headings such as `@example/package@1.2.0`. Repository infrastructure changes appear in the root's `Unreleased` section. Explicit workspace dependencies contribute relevant source context to dependent packages. Dependency and tooling maintenance belongs under `Maintenance`. `build`, `coverage`, and `dist` directories at the top of a package count as generated output and are excluded, so a package named `packages/build` is still treated as source.
@@ -266,6 +272,7 @@ export default defineConfig({
266
272
  github: true,
267
273
  maxInputChars: 80_000,
268
274
  model: 'gpt-6.1-sol',
275
+ rootChangelog: 'summary',
269
276
  timeout: 20,
270
277
  })
271
278
  ```
@@ -274,7 +281,7 @@ Importing `defineConfig` requires chagent in the repository's own dependencies.
274
281
 
275
282
  Also supported: `chagent.config.mjs`, `chagent.config.js`, `chagent.config.json`, or a `chagent` key in `package.json`. CLI flags override configured values. `--config` selects a specific file.
276
283
 
277
- Use `packages` and `exclude` directory globs to customize workspace discovery. Use `tagPatterns` to select unusual release tag conventions. Templates support `{name}` and `{version}`; defaults recognize `v1.2.3`, `1.2.3`, `@scope/package@1.2.3`, and `@scope/package/v1.2.3`.
284
+ Use `packages` and `exclude` directory globs to customize workspace discovery, and `skipPackage` to leave discovered packages without changelogs. Use `tagPatterns` to select unusual release tag conventions. Templates support `{name}` and `{version}`; defaults recognize `v1.2.3`, `1.2.3`, `@scope/package@1.2.3`, and `@scope/package/v1.2.3`.
278
285
 
279
286
  ## Background
280
287
 
@@ -284,7 +291,7 @@ Chagent reads reachable version tags and commit history regardless of the projec
284
291
 
285
292
  GitHub enrichment uses release dates and commit-associated PR information. PR lookups batch unique commits across all requested releases and packages; each release receives only its associated PRs. **GitHub release-note bodies are never generation inputs.** Changelog files and generated build output are excluded from source evidence. Existing changelog prose is supplied only to preserve edits and avoid duplicate entries.
286
293
 
287
- The agent chooses which historical source files, public interfaces, documentation, and build configuration to inspect. It reads Git snapshots at the supplied release refs instead of treating current or uncommitted files as historical evidence. `maxInputChars` bounds the starting context supplied to agents; it does not restrict which repository files they can investigate. Requests prioritize release boundaries and existing human entries, then fit optional starting evidence into the remaining budget. An individual release's existing entries are never truncated. `timeout` sets the minutes each agent session may run. Lockfile maintenance retains its commit intent, while pure npm version-bump bookkeeping is excluded.
294
+ The agent chooses which historical source files, public interfaces, documentation, and build configuration to inspect. It reads Git snapshots at the supplied release refs instead of treating current or uncommitted files as historical evidence. `maxInputChars` bounds the starting context supplied to agents; it does not restrict which repository files they can investigate. Requests prioritize release boundaries and existing human entries, then fit optional starting evidence into the remaining budget. An individual release's existing entries are never truncated. `timeout` sets the minutes each agent session may run. Dependency entries reflect declared version ranges; lockfile-only resolution changes and pure npm version-bump bookkeeping are excluded.
288
295
 
289
296
  Agents return structured changes with evidence references. Stray whitespace and list markers in descriptions are normalized. A response that still fails validation gets one correction request; a second failure stops the run before anything is written or cached. Application code owns the header, release boundaries, Markdown insertion, and Git writes. Agents run in the repository: Codex uses a read-only sandbox with approval requests disabled; Claude exposes read/search and Bash tools in [`dontAsk` mode](https://code.claude.com/docs/en/permissions), which allows read-only commands and denies actions requiring permission. Customizations are disabled for these invocations. Only chagent writes the reviewed changelog. Repository content is sent to the selected agent service using your existing account.
290
297