@hifullmoon/aicommit 2.6.4 → 2.6.5

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/CHANGELOG.md CHANGED
@@ -4,6 +4,12 @@ This file lists notable user-facing changes. Internal refactors, test-only chang
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [2.6.5] - 2026-09-21
8
+
9
+ ### Fixed
10
+
11
+ - Large split plans now validate commit messages during planning and automatically retry with specific policy errors, including missing or empty subjects. Planning failures are labeled accurately instead of being reported as API failures.
12
+
7
13
  ## [2.6.4] - 2026-09-19
8
14
 
9
15
  ### Fixed
@@ -261,7 +267,8 @@ This file lists notable user-facing changes. Internal refactors, test-only chang
261
267
  - Added file-level split planning and execution with Git-state concurrency checks.
262
268
  - Added provider presets and user/project configuration boundaries.
263
269
 
264
- [Unreleased]: https://github.com/hi-fullmoon/AICommit/compare/v2.6.4...HEAD
270
+ [Unreleased]: https://github.com/hi-fullmoon/AICommit/compare/v2.6.5...HEAD
271
+ [2.6.5]: https://github.com/hi-fullmoon/AICommit/releases/tag/v2.6.5
265
272
  [2.6.4]: https://github.com/hi-fullmoon/AICommit/releases/tag/v2.6.4
266
273
  [2.6.3]: https://github.com/hi-fullmoon/AICommit/releases/tag/v2.6.3
267
274
  [2.6.2]: https://github.com/hi-fullmoon/AICommit/releases/tag/v2.6.2
@@ -33,7 +33,7 @@ The release workflow uses npm Trusted Publishing without a long-lived `NPM_TOKEN
33
33
  ```bash
34
34
  workdir=$(mktemp -d)
35
35
  cd "$workdir"
36
- npm install --package-lock-only @hifullmoon/aicommit@2.6.4
36
+ npm install --package-lock-only @hifullmoon/aicommit@2.6.5
37
37
  npm audit signatures
38
38
  ```
39
39
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hifullmoon/aicommit",
3
- "version": "2.6.4",
3
+ "version": "2.6.5",
4
4
  "description": "Safe, local-first AI commit message generator for Git workflows",
5
5
  "type": "module",
6
6
  "bin": {
package/src/api.js CHANGED
@@ -207,6 +207,7 @@ export async function getResponseText(
207
207
  : reasoning || '';
208
208
 
209
209
  const partial = text.trim();
210
+ const followUp = typeof followUpPrompt === 'function' ? followUpPrompt() : followUpPrompt;
210
211
  const recoveryPrompt =
211
212
  truncatedByLimit || invalidResponse
212
213
  ? `The previous response was ${
@@ -214,8 +215,8 @@ export async function getResponseText(
214
215
  }. ` +
215
216
  'Reproduce the COMPLETE answer from the beginning; do not continue from the cut-off point. ' +
216
217
  'Keep the answer concise.\n\n' +
217
- followUpPrompt
218
- : followUpPrompt;
218
+ followUp
219
+ : followUp;
219
220
 
220
221
  // With reasoning, its conclusion plus the partial answer is enough to
221
222
  // reconstruct the output without paying to send the original diff again.
@@ -4,7 +4,7 @@ import { createAnalysisBudget, DEFAULT_LARGE_CHANGE, estimateTokens } from './an
4
4
  import { getResponseText } from './api.js';
5
5
  import { encodeUntrustedData } from './trust.js';
6
6
  import { ERROR_CATEGORIES, fail } from './errors.js';
7
- import { normalizeCommitPolicy } from './policy.js';
7
+ import { normalizeCommitPolicy, validateCommitCandidate } from './policy.js';
8
8
  import { getProviderAdapter } from './providers.js';
9
9
  import { createHash } from 'node:crypto';
10
10
  import {
@@ -315,6 +315,7 @@ function parseAnalysisJson(raw) {
315
315
  }
316
316
 
317
317
  async function jsonCall(config, instruction, items, validate = null, stream = null) {
318
+ let validationError = '';
318
319
  const parseAndValidate = (raw) => {
319
320
  const parsed = parseAnalysisJson(raw);
320
321
  validate?.(parsed);
@@ -328,14 +329,17 @@ async function jsonCall(config, instruction, items, validate = null, stream = nu
328
329
  ],
329
330
  0,
330
331
  Math.min(2048, config.maxTokens || 1024),
331
- 'Return the complete requested JSON array, preserving every required input ID exactly once. ' +
332
- `Required IDs: ${JSON.stringify(items.map((item) => item.id))}`,
332
+ () =>
333
+ 'Return the complete requested JSON array, preserving every required input ID exactly once. ' +
334
+ `Required IDs: ${JSON.stringify(items.map((item) => item.id))}. ` +
335
+ (validationError ? `Correct these validation errors: ${validationError}` : ''),
333
336
  stream,
334
337
  (response) => {
335
338
  try {
336
339
  parseAndValidate(response);
337
340
  return true;
338
- } catch {
341
+ } catch (error) {
342
+ validationError = error.message;
339
343
  return false;
340
344
  }
341
345
  },
@@ -351,6 +355,28 @@ async function jsonCall(config, instruction, items, validate = null, stream = nu
351
355
  }
352
356
  }
353
357
 
358
+ export function validatePlanMessages(groups, policy) {
359
+ const errors = [];
360
+ for (const [index, group] of groups.entries()) {
361
+ const subject = typeof group.subject === 'string' ? group.subject.trim() : '';
362
+ if (!subject) {
363
+ errors.push(`Group ${index + 1}: The subject field must be a non-empty commit header.`);
364
+ continue;
365
+ }
366
+ const body = typeof group.body === 'string' ? group.body.trim() : '';
367
+ const validation = validateCommitCandidate(body ? `${subject}\n\n${body}` : subject, {
368
+ policy,
369
+ });
370
+ if (!validation.valid)
371
+ errors.push(`Group ${index + 1}: ${validation.errors.map((item) => item.message).join(' ')}`);
372
+ }
373
+ if (errors.length)
374
+ throw fail(
375
+ ERROR_CATEGORIES.RESPONSE_FORMAT,
376
+ `Split commit messages violate commitPolicy: ${errors.join(' ')}`,
377
+ );
378
+ }
379
+
354
380
  export function validatePartition(groups, ids, requireSummary = true) {
355
381
  const remaining = new Set(ids);
356
382
  if (!Array.isArray(groups) || !groups.length)
@@ -777,15 +803,11 @@ export async function planAnalyzedChanges(config, facts, coverage = null, stream
777
803
  };
778
804
  const groups = await jsonCall(
779
805
  config,
780
- `Each commit message must follow this policy: ${JSON.stringify(policy)}.\nGroup related changes into logical commits, including related implementation and tests across directories. Return [{"ids":[input IDs],"summary":"factual combined change summary","subject":"commit subject","body":"optional commit body"}]. Assign every input ID exactly once. Do not merge unrelated changes just to reduce group count.${finalPlan ? ' The summary field is optional in this final plan.' : ''}${!deep && level === 0 ? ' Inputs are a compact local inventory, not full semantic summaries. One ID may represent multiple files and must remain atomic; use kind, module, status, fileCount, examples, and any representativeExcerpt conservatively.' : ''}`,
806
+ `Each commit message must follow this policy: ${JSON.stringify(policy)}.\nThe subject field must contain the complete Conventional Commit header: <type>[optional scope][optional !]: <description>. Respect the policy's language, scope, length, body, and breaking-change rules; include a body when required and omit it when forbidden.\nGroup related changes into logical commits, including related implementation and tests across directories. Return [{"ids":[input IDs],"summary":"factual combined change summary","subject":"complete commit header","body":"commit body if permitted or required"}]. Assign every input ID exactly once. Do not merge unrelated changes just to reduce group count.${finalPlan ? ' The summary field is optional in this final plan.' : ''}${!deep && level === 0 ? ' Inputs are a compact local inventory, not full semantic summaries. One ID may represent multiple files and must remain atomic; use kind, module, status, fileCount, examples, and any representativeExcerpt conservatively.' : ''}`,
781
807
  batch,
782
808
  (candidate) => {
783
809
  validatePartition(candidate, ids, false);
784
- if (candidate.some((group) => typeof group.subject !== 'string' || !group.subject.trim()))
785
- throw fail(
786
- ERROR_CATEGORIES.RESPONSE_FORMAT,
787
- 'Analysis plan is missing a commit subject.',
788
- );
810
+ validatePlanMessages(candidate, policy);
789
811
  },
790
812
  batchStream,
791
813
  );
package/src/split.js CHANGED
@@ -1836,7 +1836,7 @@ export async function splitFlow(
1836
1836
  reasoning: config.reasoning,
1837
1837
  machineOutput,
1838
1838
  cancelMessage: 'Split cancelled.',
1839
- failureMessage: 'API call failed',
1839
+ failureMessage: 'Split planning failed',
1840
1840
  task: async (stream) => {
1841
1841
  if (large) {
1842
1842
  let plan;