@hifullmoon/aicommit 2.6.8 → 2.6.9

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,13 @@ This file lists notable user-facing changes. Internal refactors, test-only chang
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [2.6.9] - 2026-10-08
8
+
9
+ ### Fixed
10
+
11
+ - Split planning no longer creates generic "update remaining files" commits for omitted changes. Incomplete plans are rejected, and exhausted analysis budgets stop without committing.
12
+ - Single-commit fallback after planning capacity exhaustion now generates its message from change summaries within the remaining analysis budget.
13
+
7
14
  ## [2.6.8] - 2026-10-07
8
15
 
9
16
  ### Fixed
@@ -289,7 +296,8 @@ This file lists notable user-facing changes. Internal refactors, test-only chang
289
296
  - Added file-level split planning and execution with Git-state concurrency checks.
290
297
  - Added provider presets and user/project configuration boundaries.
291
298
 
292
- [Unreleased]: https://github.com/hi-fullmoon/AICommit/compare/v2.6.8...HEAD
299
+ [Unreleased]: https://github.com/hi-fullmoon/AICommit/compare/v2.6.9...HEAD
300
+ [2.6.9]: https://github.com/hi-fullmoon/AICommit/tree/v2.6.9
293
301
  [2.6.8]: https://github.com/hi-fullmoon/AICommit/tree/v2.6.8
294
302
  [2.6.7]: https://github.com/hi-fullmoon/AICommit/tree/v2.6.7
295
303
  [2.6.6]: https://github.com/hi-fullmoon/AICommit/tree/v2.6.6
package/README.md CHANGED
@@ -513,7 +513,7 @@ The default `largeChange.strategy: "auto"` inventories every file locally, group
513
513
 
514
514
  A normal commit typically needs one model request, with no per-file AI calls or recursive model reduction. The inventory contains at most 16 representative groups under a UTF-8 byte budget, prioritizing coverage across code, configuration, tests, and other categories. It explicitly describes sampling limits. Both terminal and JSON output distinguish fully analyzed files, representative excerpts, and metadata-only files. Provider retries, response recovery, policy correction, and user-requested regeneration can still add requests.
515
515
 
516
- Split mode builds local candidates and sends them in bounded batches of at most `splitMaxPlanFiles`, then merges the batch plans hierarchically. When an `auto` inventory exceeds four times that candidate limit, adjacent candidates are first bundled locally by top-level module, file kind, and Git status; the model receives compact counts, examples, and selected excerpts while the complete file mapping stays local. Every file remains represented even when the complete candidate inventory cannot fit one request. If `deep` analysis exhausts its aggregate budget or the hierarchy cannot converge, interactive and dry-run flows produce one conservative all-files plan with an explicit warning instead of using incomplete model output. Non-interactive committing stops unless `--allow-single-fallback` explicitly authorizes that degradation. Small changes keep the existing request path.
516
+ Split mode builds local candidates and sends them in bounded batches of at most `splitMaxPlanFiles`, then merges the batch plans hierarchically. When an `auto` inventory exceeds four times that candidate limit, adjacent candidates are first bundled locally by top-level module, file kind, and Git status; the model receives compact counts, examples, and selected excerpts while the complete file mapping stays local. Every file remains represented even when the complete candidate inventory cannot fit one request. Exhausted analysis budgets stop without committing; increase the budget or use smaller batches. If the hierarchy cannot converge while budget remains, an all-files commit message can be generated from local change summaries; non-interactive committing requires `--allow-single-fallback`. Plans omitting files or hunks are rejected instead of creating an automatic "update remaining files" commit. Small changes keep the existing request path.
517
517
 
518
518
  For exhaustive chunk-by-chunk model analysis, opt in through personal configuration:
519
519
 
@@ -535,7 +535,7 @@ For exhaustive chunk-by-chunk model analysis, opt in through personal configurat
535
535
  }
536
536
  ```
537
537
 
538
- `deep` spends more requests and tokens, with a maximum of 256 requests. Before dispatch, a preflight estimate covers initial chunks, required reductions, and the minimum hierarchical planning tree; an impossible deep run switches to the local inventory path, and validated cache hits are excluded from that estimate. Validated initial fact chunks are stored briefly under Git metadata, reused when the same snapshot is retried after failure or interruption, and removed after complete generation succeeds. The cache does not directly store captured diffs, reasoning, credentials, or complete provider responses; it stores model summaries that may contain code-derived details. Unprotected original input is not persisted unless personal configuration explicitly enables `allowUnprotected`. Repository configuration cannot change this personal strategy, enable unprotected caching, or raise spending/cache ceilings. Both strategies use conservative token estimates; cache hits consume no request or token budget. Incomplete model output is never committed: budget/capacity fallback is a new complete plan containing every reviewed file; interactive runs show it for review, while non-interactive committing requires `--allow-single-fallback`.
538
+ `deep` spends more requests and tokens, with a maximum of 256 requests. Before dispatch, a preflight estimate covers initial chunks, required reductions, and the minimum hierarchical planning tree; an impossible deep run switches to the local inventory path, and validated cache hits are excluded from that estimate. Validated initial fact chunks are stored briefly under Git metadata, reused when the same snapshot is retried after failure or interruption, and removed after complete generation succeeds. The cache does not directly store captured diffs, reasoning, credentials, or complete provider responses; it stores model summaries that may contain code-derived details. Unprotected original input is not persisted unless personal configuration explicitly enables `allowUnprotected`. Repository configuration cannot change this personal strategy, enable unprotected caching, or raise spending/cache ceilings. Both strategies use conservative token estimates; cache hits consume no request or token budget. Incomplete model output is never committed. Budget exhaustion stops generation; capacity fallback requires enough budget to generate a message from change summaries. Interactive runs show it for review, while non-interactive committing requires `--allow-single-fallback`.
539
539
 
540
540
  Large-change split planning completes file grouping before repairing commit messages. If a message violates the policy (for example, its description exceeds 72 characters), up to three message-only correction rounds preserve file assignments and valid messages without resending diffs. These rounds share the existing request, time, and token budgets; JSON recovery may require an additional request within a round. If automatic repair fails, interactive runs offer an editor for the invalid messages and then return to normal plan review. Non-interactive runs stop without committing or collapsing the groups into a single commit. For protected input, a private temporary diagnostic JSON file preserves the complete grouping and messages; it is not an artifact accepted by `split apply`.
541
541
 
package/README.zh-CN.md CHANGED
@@ -338,7 +338,7 @@ aicommit --yes --dry-run --scope=staged --output=json # 只预览暂存区
338
338
  aicommit generate --scope=staged --file=/tmp/commit-plan.json --yes --output=json
339
339
  aicommit apply --file=/tmp/commit-plan.json --yes --output=json
340
340
  aicommit split --scope=all --yes # 非交互规划并提交所有工作区变更
341
- aicommit split --scope=all --yes --allow-single-fallback # 明确允许规划预算耗尽后的保守提交
341
+ aicommit split --scope=all --yes --allow-single-fallback # 明确允许预算内生成的保守单次提交
342
342
  aicommit split plan --scope=staged --file=/tmp/split-plan.json --yes
343
343
  aicommit split apply --file=/tmp/split-plan.json --yes
344
344
  aicommit split resume --yes # 恢复中断的拆分事务
@@ -515,7 +515,7 @@ exec zsh
515
515
 
516
516
  普通提交通常只需要一次模型请求,不会为每个文件调用 AI,也不会递归调用模型汇总。摘要最多包含 16 个代表组,并受 UTF-8 字节预算约束;优先覆盖代码、配置和测试等不同类别。摘要明确说明抽样范围,界面和 JSON 分别报告全文分析、代表片段和仅元数据的文件数,不把抽样视为完整理解。提供方重试、响应恢复、格式修正和用户重新生成仍可能增加请求。
517
517
 
518
- 批次提交先在本地建立候选组,再按每批最多 `splitMaxPlanFiles` 个候选发送,并分层合并各批计划。当 `auto` 清单规模超过该候选上限的四倍时,会先按顶层模块、文件类型和 Git 状态在本地归并相邻候选;模型只接收紧凑计数、少量路径示例和代表片段,完整文件映射始终留在本地。完整候选清单放不进一次请求时,仍会保留每个文件。如果 `deep` 分析耗尽总预算,或分层规划无法收敛,交互和 dry-run 流程会明确警告并生成一个覆盖全部文件的保守计划,而不会采用不完整的模型结果;非交互提交默认停止,只有显式传入 `--allow-single-fallback` 才允许该降级。小变更保持原有请求路径。
518
+ 批次提交先在本地建立候选组,再按每批最多 `splitMaxPlanFiles` 个候选发送,并分层合并各批计划。当 `auto` 清单规模超过该候选上限的四倍时,会先按顶层模块、文件类型和 Git 状态在本地归并相邻候选;模型只接收紧凑计数、少量路径示例和代表片段,完整文件映射始终留在本地。完整候选清单放不进一次请求时,仍会保留每个文件。分析预算耗尽时停止提交,保留变更并提示提高预算或减少单批文件。如果分层规划无法收敛且预算仍充足,可根据本地变更摘要生成覆盖全部文件的单次提交;非交互提交需要 `--allow-single-fallback`。遗漏文件或 hunk 的计划会被拒绝,不再自动生成“更新其余文件”。小变更保持原有请求路径。
519
519
 
520
520
  确实需要逐块 AI 分析时,在个人配置中设置:
521
521
 
@@ -537,7 +537,7 @@ exec zsh
537
537
  }
538
538
  ```
539
539
 
540
- `deep` 会增加请求和 token 消耗,最多 256 次请求。请求前会预估初始分块、必要归并和最小分层规划树的成本;确定无法装入总预算时直接切换到本地清单路径,已验证的缓存命中不计入这次预估。通过完整校验的初始事实分块会短期写入 Git 元数据目录;同一快照失败或中断后重试时直接复用,完整生成成功后清理。缓存不直接保存捕获的 diff、推理、凭据或完整 Provider 响应,只保存可能包含代码派生细节的模型摘要;选择发送未保护的原始内容时默认不落盘,只有个人配置显式设置 `allowUnprotected: true` 才允许。仓库配置不能切换策略、启用未保护缓存或提高费用与缓存上限。两种策略均使用保守 token 估算,未知 usage 按预留额度计入;缓存命中不计请求或 token。模型的不完整输出永远不会被提交:预算或容量降级会重新生成一个包含全部已审核文件的完整计划;交互模式先展示确认,非交互提交则要求 `--allow-single-fallback`。
540
+ `deep` 会增加请求和 token 消耗,最多 256 次请求。请求前会预估初始分块、必要归并和最小分层规划树的成本;确定无法装入总预算时直接切换到本地清单路径,已验证的缓存命中不计入这次预估。通过完整校验的初始事实分块会短期写入 Git 元数据目录;同一快照失败或中断后重试时直接复用,完整生成成功后清理。缓存不直接保存捕获的 diff、推理、凭据或完整 Provider 响应,只保存可能包含代码派生细节的模型摘要;选择发送未保护的原始内容时默认不落盘,只有个人配置显式设置 `allowUnprotected: true` 才允许。仓库配置不能切换策略、启用未保护缓存或提高费用与缓存上限。两种策略均使用保守 token 估算,未知 usage 按预留额度计入;缓存命中不计请求或 token。模型的不完整输出永远不会被提交。预算耗尽会停止生成;容量降级只有在剩余预算足够根据变更摘要生成提交信息时才可继续,交互模式先展示确认,非交互提交则要求 `--allow-single-fallback`。
541
541
 
542
542
  大变更拆分先完成全部文件分组,再修正不合规的提交信息。自动修正最多三轮,只发送出错的消息、实际长度和校验原因,不重发 diff,也不改动文件归属或已合规的消息。修正与原规划共用请求、时间和 token 预算;每轮 JSON 恢复可能额外请求一次。自动修正失败后,交互模式允许编辑出错的提交信息,校验通过后返回正常计划确认流程。非交互模式停止提交,不会将已有分组合并成单次提交;对于已保护的输入,会将完整分组和消息保存到私有临时诊断文件,该文件不能直接用于 `split apply`。
543
543
 
@@ -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.8
36
+ npm install --package-lock-only @hifullmoon/aicommit@2.6.9
37
37
  npm audit signatures
38
38
  ```
39
39
 
@@ -36,9 +36,9 @@ If a failure remains, capture `aicommit doctor --output=json`, Node/Git versions
36
36
 
37
37
  ## Large-change limits / 大变更限制
38
38
 
39
- Default `auto` analysis builds a local inventory and selects bounded excerpts, then batches oversized split inventories and merges their plans hierarchically. Deep analysis still has fixed request (256) and depth (8) limits. If its aggregate token/time budget is exhausted or hierarchical planning cannot converge, split mode emits an explicit warning and can fall back to one complete all-files plan; it never uses a partial model plan. Interactive and dry-run flows show that plan, while non-interactive committing requires the explicit `--allow-single-fallback` option. Token/time limits remain configurable in personal settings. Unknown model token counts use conservative estimates; an oversized request is not dispatched, and provider-context errors are not blindly replayed.
39
+ Default `auto` analysis builds a local inventory and selects bounded excerpts, then batches oversized split inventories and merges their plans hierarchically. Deep analysis still has fixed request (256) and depth (8) limits. Exhausted token/time budgets stop without committing, even with `--allow-single-fallback`; increase the budget or use smaller batches. If hierarchical planning cannot converge while budget remains, a complete all-files message can be generated from local summaries. Non-interactive committing requires `--allow-single-fallback`. Missing file or hunk coverage is rejected instead of receiving a generic catch-all message. Unknown model token counts use conservative estimates; an oversized request is not dispatched, and provider-context errors are not blindly replayed.
40
40
 
41
- 默认 `auto` 在本地建立清单并选择受限片段;拆分候选过多时会分批请求,再分层合并计划。深度分析仍有固定的请求数(256)和汇总层级(8)上限。如果总 token/时间预算耗尽,或分层规划无法收敛,拆分模式会明确警告,并可降级为一个覆盖全部文件的计划,绝不会采用模型返回的半份计划。交互和 dry-run 流程会展示该计划;非交互提交必须显式传入 `--allow-single-fallback`。token 和时间预算仍可在个人配置中调整。未知模型采用保守 token 估算,单次输入超限时不会发送该请求;Provider 上下文超限不会盲目重试。
41
+ 默认 `auto` 在本地建立清单并选择受限片段;拆分候选过多时会分批请求,再分层合并计划。深度分析仍有固定的请求数(256)和汇总层级(8)上限。总 token/时间预算耗尽时停止提交,即使传入 `--allow-single-fallback` 也不会生成固定文案;请提高预算或减少单批文件。如果分层规划无法收敛且预算仍充足,可根据本地摘要生成覆盖全部文件的提交信息;非交互提交必须显式传入 `--allow-single-fallback`。遗漏文件或 hunk 的计划直接拒绝。未知模型采用保守 token 估算,单次输入超限时不会发送该请求;Provider 上下文超限不会盲目重试。
42
42
 
43
43
  Validated initial `deep` chunks are cached under private Git metadata after a failed or interrupted run. A retry of the identical snapshot and model settings reports cached chunks and requests only the remainder. Any input/model/protection change causes a miss. Successful generation clears the active cache; stale entries expire after 24 hours. Unprotected input is not cached unless personal `largeChange.cache.allowUnprotected` is explicitly enabled.
44
44
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hifullmoon/aicommit",
3
- "version": "2.6.8",
3
+ "version": "2.6.9",
4
4
  "description": "AI Git commit message CLI for Conventional Commits with OpenAI, DeepSeek, OpenRouter and local Ollama models",
5
5
  "type": "module",
6
6
  "bin": {
package/src/split.js CHANGED
@@ -378,7 +378,8 @@ export async function generateSplitPlan(
378
378
  `- Body mode: ${policy.body.mode}; at most ${policy.body.maxLines} non-empty lines.`,
379
379
  `- Breaking changes: ${policy.breakingChange}.`,
380
380
  '- Give every message a short subject line; when the subject alone does not say it all, add a body of bullet lines (what changed and why), each starting with "- " — the same format the single-commit flow produces.',
381
- '- Assign EVERY file shown in the "Changed files:" list to exactly one group — do not leave any out. Only files marked "(not shown)" may be omitted; they are collected into a final catch-all commit automatically.',
381
+ '- Assign EVERY file shown in the "Changed files:" list to exactly one group — do not leave any out. Incomplete coverage is rejected; no catch-all commit is created automatically.',
382
+ '- Describe the actual supported changes in every subject. Do not use generic subjects such as "update remaining files", "commit other files", or "提交剩余其他文件".',
382
383
  ...(hunkMode
383
384
  ? [
384
385
  '- Experimental hunk mode is enabled. A file annotated with hunk IDs may either stay whole in "files", or its IDs may be assigned across groups with "hunks":[{"path":"app.js","ids":["H1"]}].',
@@ -485,53 +486,8 @@ function groupMessage(g, policy) {
485
486
  return validateCommitCandidate(message, { policy }).valid ? message : '';
486
487
  }
487
488
 
488
- function fallbackCommitMessage(policy) {
489
- const type = policy.types.includes('chore') ? 'chore' : policy.types[0];
490
- let scope = '';
491
- if (policy.scope.mode === 'required') {
492
- if (policy.scope.values.length) scope = policy.scope.values[0];
493
- else {
494
- const disallowed = new Set(policy.scope.disallowedValues || []);
495
- const preferred = ['changes', 'repository', 'all'];
496
- scope = preferred.find((candidate) => !disallowed.has(candidate)) || '';
497
- for (let index = 1; !scope; index++) {
498
- const candidate = `fallback-${index}`;
499
- if (!disallowed.has(candidate)) scope = candidate;
500
- }
501
- }
502
- }
503
- const breaking = policy.breakingChange === 'require' ? '!' : '';
504
- const prefix = `${type}${scope ? `(${scope})` : ''}${breaking}: `;
505
- const preferred = policy.effectiveLanguage === 'zh' ? '更新其余文件' : 'update remaining files';
506
- const available = Math.max(
507
- 1,
508
- Math.min(
509
- policy.subject.maxLength,
510
- policy.subject.headerMaxLength
511
- ? policy.subject.headerMaxLength - [...prefix].length
512
- : policy.subject.maxLength,
513
- ),
514
- );
515
- const subject = [...preferred].slice(0, available).join('');
516
- const body =
517
- policy.body.mode === 'required'
518
- ? policy.effectiveLanguage === 'zh'
519
- ? '- 包含本次已审核的全部文件变更'
520
- : '- Include all reviewed file changes'
521
- : '';
522
- const message = cleanCommitMessage(`${prefix}${subject}${body ? `\n\n${body}` : ''}`);
523
- const validation = validateCommitCandidate(message, { policy });
524
- if (!validation.valid) {
525
- throw fail(
526
- ERROR_CATEGORIES.CONFIG,
527
- `Cannot construct a conservative fallback message under commitPolicy: ${validation.errors.map((item) => item.message).join(' ')}`,
528
- );
529
- }
530
- return message;
531
- }
532
-
533
489
  // Clean up the model's plan: drop unknown/duplicate files, drop empty
534
- // groups, and sweep any file the model forgot into a final catch-all group.
490
+ // groups, and reject incomplete coverage instead of inventing a commit message.
535
491
  export function normalizePlan(groups, allFiles, language, commitPolicy = null) {
536
492
  const policy = normalizeCommitPolicy(commitPolicy, language);
537
493
  const known = new Map(allFiles.map((f) => [f.path, f]));
@@ -593,11 +549,11 @@ export function normalizePlan(groups, allFiles, language, commitPolicy = null) {
593
549
  if (ids.length) leftoverHunks.push({ path: change.path, ids });
594
550
  }
595
551
  if (leftoverFiles.length || leftoverHunks.length) {
596
- result.push({
597
- message: fallbackCommitMessage(policy),
598
- files: leftoverFiles,
599
- ...(leftoverHunks.length ? { hunks: leftoverHunks } : {}),
600
- });
552
+ throw fail(
553
+ ERROR_CATEGORIES.RESPONSE_FORMAT,
554
+ `Split plan omitted ${leftoverFiles.length} files and ${leftoverHunks.length} hunk assignments with valid commit messages. No commit was created; regenerate the plan or supply messages describing those changes.`,
555
+ { data: { omittedFiles: leftoverFiles, omittedHunks: leftoverHunks } },
556
+ );
601
557
  }
602
558
 
603
559
  return result;
@@ -1919,6 +1875,13 @@ export async function splitFlow(
1919
1875
  err.data?.analysis?.exhausted ||
1920
1876
  (!planningConfig.analysisBudget.remainingMs() ? 'time' : null);
1921
1877
  if (!exhausted && !err.data?.fallbackPlan) throw err;
1878
+ if (exhausted) {
1879
+ throw fail(
1880
+ err.category || ERROR_CATEGORIES.PROVIDER,
1881
+ `Large-change planning exhausted its ${exhausted} budget before producing a complete plan. No commit was created; increase the analysis budget or split the changes into smaller batches.`,
1882
+ { cause: err, data: err.data },
1883
+ );
1884
+ }
1922
1885
  if (yes && !dryRun && !allowSingleFallback) {
1923
1886
  throw fail(
1924
1887
  err.category || ERROR_CATEGORIES.PROVIDER,
@@ -1942,7 +1905,9 @@ export async function splitFlow(
1942
1905
  degradedFrom: planningConfig.largeChange.strategy,
1943
1906
  fallbackReason: exhausted || 'planning_capacity',
1944
1907
  };
1945
- plan = normalizePlan([], allFiles, config.language, config.commitPolicy);
1908
+ const summary = await summarizeChanges(fallbackConfig, analysis.facts);
1909
+ const generated = await generateCommitMessage(fallbackConfig, summary, 0, '', stream);
1910
+ plan = [{ message: generated.message, files: allFiles.map((file) => file.path) }];
1946
1911
  planReasoning = null;
1947
1912
  const warning = `Large-change planning used one conservative all-files commit after ${exhausted || 'planning capacity'} exhaustion; review the fallback message and grouping.`;
1948
1913
  warnings.push(warning);