@hifullmoon/aicommit 2.5.0 → 2.6.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/CHANGELOG.md +18 -1
- package/README.md +17 -6
- package/README.zh-CN.md +17 -6
- package/docs/distribution.md +1 -1
- package/docs/troubleshooting.md +14 -14
- package/package.json +1 -1
- package/schemas/aicommit-output.schema.json +61 -3
- package/src/change-analysis.js +7 -3
- package/src/cli.js +59 -15
- package/src/completion.js +14 -8
- package/src/errors.js +6 -2
- package/src/generation-ui.js +1 -1
- package/src/git.js +17 -5
- package/src/main.js +121 -4
- package/src/output.js +19 -2
- package/src/split.js +118 -13
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,22 @@ This file lists notable user-facing changes. Internal refactors, test-only chang
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [2.6.0] - 2026-09-14
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- Added `generate --scope=staged|all` and `apply` for a reviewable single-commit plan that rechecks the repository snapshot before committing.
|
|
12
|
+
- Added read-only `split status --output=json` so interrupted split transactions expose completed commits and the next recovery action.
|
|
13
|
+
|
|
14
|
+
### Changed
|
|
15
|
+
|
|
16
|
+
- Machine output schema 1.1 now reports commit IDs, plan paths, explicit change scope, and structured recovery errors. Consumers pinned to schema 1.0 should update their parsers.
|
|
17
|
+
- Staged snapshot checks use Git object IDs instead of regenerating full patches, and normal generation skips `git diff --stat` unless its diff is truncated.
|
|
18
|
+
|
|
19
|
+
### Fixed
|
|
20
|
+
|
|
21
|
+
- Split planning now displays the final model request's thinking for large changes and preserves it in interactive review; `auto` mode displays thinking when a provider emits it. JSON output remains free of model reasoning.
|
|
22
|
+
|
|
7
23
|
## [2.5.0] - 2026-09-10
|
|
8
24
|
|
|
9
25
|
### Added
|
|
@@ -217,7 +233,8 @@ This file lists notable user-facing changes. Internal refactors, test-only chang
|
|
|
217
233
|
- Added file-level split planning and execution with Git-state concurrency checks.
|
|
218
234
|
- Added provider presets and user/project configuration boundaries.
|
|
219
235
|
|
|
220
|
-
[Unreleased]: https://github.com/hi-fullmoon/AICommit/compare/v2.
|
|
236
|
+
[Unreleased]: https://github.com/hi-fullmoon/AICommit/compare/v2.6.0...HEAD
|
|
237
|
+
[2.6.0]: https://github.com/hi-fullmoon/AICommit/releases/tag/v2.6.0
|
|
221
238
|
[2.5.0]: https://github.com/hi-fullmoon/AICommit/releases/tag/v2.5.0
|
|
222
239
|
[2.4.1]: https://github.com/hi-fullmoon/AICommit/releases/tag/v2.4.1
|
|
223
240
|
[2.4.0]: https://github.com/hi-fullmoon/AICommit/releases/tag/v2.4.0
|
package/README.md
CHANGED
|
@@ -311,11 +311,15 @@ aicommit --dry-run # generate and review without creating a commit
|
|
|
311
311
|
aicommit split --dry-run # review a split plan without creating commits
|
|
312
312
|
aicommit --yes # non-interactively commit already staged changes
|
|
313
313
|
aicommit --yes --dry-run # non-interactively preview all changes; restores staging
|
|
314
|
+
aicommit --yes --dry-run --scope=staged --output=json # preview only the index
|
|
315
|
+
aicommit generate --scope=staged --file=/tmp/commit-plan.json --yes --output=json
|
|
316
|
+
aicommit apply --file=/tmp/commit-plan.json --yes --output=json
|
|
314
317
|
aicommit split --scope=all --yes # non-interactively plan and commit all working-tree changes
|
|
315
318
|
aicommit split --scope=all --yes --allow-single-fallback # explicitly permit a conservative fallback commit
|
|
316
319
|
aicommit split plan --scope=staged --file=/tmp/split-plan.json --yes
|
|
317
320
|
aicommit split apply --file=/tmp/split-plan.json --yes
|
|
318
321
|
aicommit split resume --yes # resume an interrupted split transaction
|
|
322
|
+
aicommit split status --output=json # inspect recovery state without changing Git
|
|
319
323
|
aicommit split abort --yes # discard a stale checkpoint; keep commits and changes
|
|
320
324
|
aicommit --reasoning=low # stream low-effort reasoning; Ctrl+O expands/collapses it
|
|
321
325
|
aicommit --no-reasoning # explicitly disable reasoning when supported
|
|
@@ -331,8 +335,8 @@ aicommit -h # help
|
|
|
331
335
|
| `-l`, `--lang` | Commit message language (`zh` or `en`) |
|
|
332
336
|
| `-p`, `--provider` | Use the named provider from `providers` |
|
|
333
337
|
| `-m`, `--model` | Use a named model profile from the selected provider |
|
|
334
|
-
| `--scope` | `staged` or `all` scope for
|
|
335
|
-
| `--file` | JSON plan path for
|
|
338
|
+
| `--scope` | `staged` or `all` scope for dry-run, generate, and split planning |
|
|
339
|
+
| `--file` | JSON plan path for generate/apply and split plan/apply |
|
|
336
340
|
| `--dry-run` | Generate and review a message or split plan without creating commits |
|
|
337
341
|
| `-y`, `--yes` | Accept without prompts; normal mode requires explicitly staged changes |
|
|
338
342
|
| `--allow-single-fallback` | Explicitly permit non-interactive split to create one complete fallback commit |
|
|
@@ -388,11 +392,13 @@ Verify the registration with `whence -w _aicommit`; it should print `_aicommit:
|
|
|
388
392
|
|
|
389
393
|
### Machine-readable output
|
|
390
394
|
|
|
391
|
-
Use `--output=json` for scripts and CI. Commit and split flows also require `--yes`, preventing a machine consumer from hanging on an interactive prompt. stdout contains exactly one JSON object; progress, debug details, and diagnostics go to stderr. `
|
|
395
|
+
Use `--output=json` for scripts and CI. Commit, generate, apply, and split execution flows also require `--yes`, preventing a machine consumer from hanging on an interactive prompt. stdout contains exactly one JSON object; progress, debug details, and diagnostics go to stderr. `split status`, `doctor`, and `update` in JSON mode do not require `--yes`.
|
|
396
|
+
|
|
397
|
+
For an agent-reviewed single commit, run `generate --scope=staged|all --file=<path> --yes --output=json`, inspect the returned message and plan, then run `apply --file=<path> --yes --output=json`. The plan is a validated one-group artifact; apply checks its base HEAD, change list, and content fingerprint before committing. Keep the file outside the worktree or in `.git/aicommit/`. `generate --scope=all` includes staged, unstaged, and untracked changes, restores its temporary staging, and refuses detected sensitive content in non-interactive mode. For a quick preview without an artifact, use `--dry-run --scope=staged|all --yes --output=json`; an omitted scope retains the older automatic staging behavior when the index is empty.
|
|
392
398
|
|
|
393
399
|
```json
|
|
394
400
|
{
|
|
395
|
-
"schemaVersion": "1.
|
|
401
|
+
"schemaVersion": "1.1",
|
|
396
402
|
"ok": true,
|
|
397
403
|
"message": "fix: handle provider retry limits",
|
|
398
404
|
"plan": null,
|
|
@@ -407,11 +413,16 @@ Use `--output=json` for scripts and CI. Commit and split flows also require `--y
|
|
|
407
413
|
"warnings": [],
|
|
408
414
|
"exitReason": "dry_run",
|
|
409
415
|
"committed": false,
|
|
416
|
+
"commitState": "none",
|
|
417
|
+
"commitSha": null,
|
|
418
|
+
"planFile": null,
|
|
419
|
+
"scope": "staged",
|
|
420
|
+
"changeCount": 1,
|
|
410
421
|
"error": null
|
|
411
422
|
}
|
|
412
423
|
```
|
|
413
424
|
|
|
414
|
-
The published [JSON schema](schemas/aicommit-output.schema.json) covers success,
|
|
425
|
+
The published [JSON schema](schemas/aicommit-output.schema.json) covers success, plans, status, doctor/check, and error results. Machine output never includes the diff or model reasoning. Plans expose only each group message and its assigned paths. `committed` reports whether this invocation created a commit; `commitState` describes `none`, `partial`, `complete`, or `unknown` transaction state. A split failure can return `committed: true` with `error.code: "split_partial_failure"`, completed commit IDs in `data.split`, and `error.nextAction`. After a crash that emits no JSON, use `split status --output=json` before retrying.
|
|
415
426
|
|
|
416
427
|
Stable process exits are shared by text and JSON modes:
|
|
417
428
|
|
|
@@ -444,7 +455,7 @@ The staged index (or complete split-mode working tree, including untracked file
|
|
|
444
455
|
|
|
445
456
|
Reasoning defaults to `on` with `medium` effort. It is mapped natively for OpenAI reasoning models, DeepSeek, OpenRouter, and MiniMax; models that do not expose reasoning continue normally and show an unavailable notice instead of failing. Official OpenAI endpoints validate the selected effort against the model generation before sending the request, so unsupported combinations such as `o3 --no-reasoning` or `gpt-5.1 --reasoning=max` fail locally with a clear list of supported levels. DeepSeek's current `deepseek-v4-flash` and `deepseek-v4-pro` models receive `thinking: { "type": "enabled" }` plus `reasoning_effort`; `medium`/`xhigh` are normalized to DeepSeek's `high` level.
|
|
446
457
|
|
|
447
|
-
When reasoning mode is `on` (including via `--reasoning=<level>`), aicommit requests a streaming response and displays reasoning as it arrives. The live view follows the newest two terminal lines by default; press `Ctrl+O` to expand or collapse the accumulated text during generation or review. Long expanded output is kept inside the terminal viewport—use `PageUp`/`PageDown` to read every page. Holding `Ctrl+O` counts as one toggle, so key repeat cannot leave duplicate panels behind. Output is sanitized and capped by `reasoning.maxDisplayChars`; providers that do not expose reasoning show a short unavailable notice.
|
|
458
|
+
When reasoning mode is `on` (including via `--reasoning=<level>`), aicommit requests a streaming response and displays reasoning as it arrives. In `auto` mode it also displays thinking if the provider emits it, without forcing reasoning on. Split mode streams the final planning request's thinking, including when large changes use batched analysis; the review prompt keeps that reasoning available. Intermediate analysis batches show progress rather than mixing several reasoning streams. The live panel requires an interactive terminal, and JSON output never includes reasoning. The live view follows the newest two terminal lines by default; press `Ctrl+O` to expand or collapse the accumulated text during generation or review. Long expanded output is kept inside the terminal viewport—use `PageUp`/`PageDown` to read every page. Holding `Ctrl+O` counts as one toggle, so key repeat cannot leave duplicate panels behind. Output is sanitized and capped by `reasoning.maxDisplayChars`; providers that do not expose reasoning show a short unavailable notice.
|
|
448
459
|
|
|
449
460
|
```json
|
|
450
461
|
{
|
package/README.zh-CN.md
CHANGED
|
@@ -313,11 +313,15 @@ aicommit --dry-run # 生成并审阅,但不创建提交
|
|
|
313
313
|
aicommit split --dry-run # 审阅拆分计划,但不创建提交
|
|
314
314
|
aicommit --yes # 非交互提交已明确暂存的变更
|
|
315
315
|
aicommit --yes --dry-run # 非交互预览所有变更;退出时恢复暂存状态
|
|
316
|
+
aicommit --yes --dry-run --scope=staged --output=json # 只预览暂存区
|
|
317
|
+
aicommit generate --scope=staged --file=/tmp/commit-plan.json --yes --output=json
|
|
318
|
+
aicommit apply --file=/tmp/commit-plan.json --yes --output=json
|
|
316
319
|
aicommit split --scope=all --yes # 非交互规划并提交所有工作区变更
|
|
317
320
|
aicommit split --scope=all --yes --allow-single-fallback # 明确允许规划预算耗尽后的保守提交
|
|
318
321
|
aicommit split plan --scope=staged --file=/tmp/split-plan.json --yes
|
|
319
322
|
aicommit split apply --file=/tmp/split-plan.json --yes
|
|
320
323
|
aicommit split resume --yes # 恢复中断的拆分事务
|
|
324
|
+
aicommit split status --output=json # 只读查询恢复状态
|
|
321
325
|
aicommit split abort --yes # 丢弃过期 checkpoint;保留提交和变更
|
|
322
326
|
aicommit --reasoning=low # 流式显示低强度推理;Ctrl+O 展开或收起
|
|
323
327
|
aicommit --no-reasoning # Provider / 模型支持时显式关闭推理
|
|
@@ -333,8 +337,8 @@ aicommit -h # 帮助
|
|
|
333
337
|
| `-l`, `--lang` | 提交信息语言:`zh` 或 `en` |
|
|
334
338
|
| `-p`, `--provider` | 使用 `providers` 中的命名 Provider |
|
|
335
339
|
| `-m`, `--model` | 使用所选 Provider 下的命名模型配置 |
|
|
336
|
-
| `--scope` |
|
|
337
|
-
| `--file` |
|
|
340
|
+
| `--scope` | dry-run、generate 和 split 规划的范围:`staged` 或 `all` |
|
|
341
|
+
| `--file` | generate/apply 和 split plan/apply 的 JSON 计划路径 |
|
|
338
342
|
| `--dry-run` | 生成并审阅消息或拆分计划,但不创建提交 |
|
|
339
343
|
| `-y`, `--yes` | 不提示直接接受;普通模式要求变更已明确暂存 |
|
|
340
344
|
| `--allow-single-fallback` | 明确允许非交互拆分创建一个覆盖完整变更的保守提交 |
|
|
@@ -390,11 +394,13 @@ exec zsh
|
|
|
390
394
|
|
|
391
395
|
### 机器可读输出
|
|
392
396
|
|
|
393
|
-
脚本和 CI 请使用 `--output=json
|
|
397
|
+
脚本和 CI 请使用 `--output=json`。提交、generate、apply 和 split 执行流程还必须使用 `--yes`,避免机器消费者卡在交互提示上。stdout 只包含一个 JSON 对象;进度、调试信息和诊断输出会写入 stderr。`split status`、`doctor` 和 `update` 的 JSON 模式不要求 `--yes`。
|
|
398
|
+
|
|
399
|
+
让 AI 审阅单次提交时,先执行 `generate --scope=staged|all --file=<路径> --yes --output=json`,检查返回的消息和计划,再执行 `apply --file=<路径> --yes --output=json`。计划复用经过校验的单组工件;apply 会在提交前核对基础 HEAD、变更清单和内容指纹。计划文件应放在工作区外或 `.git/aicommit/`。`generate --scope=all` 包含已暂存、未暂存和未跟踪变更,结束时恢复临时暂存;非交互模式遇到检测出的敏感内容会停止。只想快速预览时,可用 `--dry-run --scope=staged|all --yes --output=json`;不指定范围时,保留旧版在暂存区为空时自动暂存的行为。
|
|
394
400
|
|
|
395
401
|
```json
|
|
396
402
|
{
|
|
397
|
-
"schemaVersion": "1.
|
|
403
|
+
"schemaVersion": "1.1",
|
|
398
404
|
"ok": true,
|
|
399
405
|
"message": "fix: handle provider retry limits",
|
|
400
406
|
"plan": null,
|
|
@@ -409,11 +415,16 @@ exec zsh
|
|
|
409
415
|
"warnings": [],
|
|
410
416
|
"exitReason": "dry_run",
|
|
411
417
|
"committed": false,
|
|
418
|
+
"commitState": "none",
|
|
419
|
+
"commitSha": null,
|
|
420
|
+
"planFile": null,
|
|
421
|
+
"scope": "staged",
|
|
422
|
+
"changeCount": 1,
|
|
412
423
|
"error": null
|
|
413
424
|
}
|
|
414
425
|
```
|
|
415
426
|
|
|
416
|
-
已发布的 [JSON schema](schemas/aicommit-output.schema.json)
|
|
427
|
+
已发布的 [JSON schema](schemas/aicommit-output.schema.json) 覆盖成功、计划、状态、doctor / check 和错误结果。机器输出绝不包含 diff 或模型推理;计划只暴露每组消息及其分配路径。`committed` 表示本次调用是否创建了提交;`commitState` 表示事务处于 `none`、`partial`、`complete` 或 `unknown`。拆分失败时可能返回 `committed: true`、`error.code: "split_partial_failure"`、`data.split` 中已完成的提交 ID,以及 `error.nextAction`。若进程崩溃而没有输出 JSON,应先执行 `split status --output=json` 再决定下一步。
|
|
417
428
|
|
|
418
429
|
文本与 JSON 模式共享稳定的进程退出码:
|
|
419
430
|
|
|
@@ -446,7 +457,7 @@ exec zsh
|
|
|
446
457
|
|
|
447
458
|
推理默认为 `on`,强度为 `medium`。OpenAI 推理模型、DeepSeek、OpenRouter 和 MiniMax 使用各自原生映射;不暴露推理能力的模型会正常继续,并显示“不可用”提示,而不是失败。官方 OpenAI 端点会在发送请求前根据模型代际校验所选强度,因此 `o3 --no-reasoning` 或 `gpt-5.1 --reasoning=max` 等不支持的组合会在本地失败,并清楚列出支持的级别。DeepSeek 当前的 `deepseek-v4-flash` 和 `deepseek-v4-pro` 会收到 `thinking: { "type": "enabled" }` 与 `reasoning_effort`;`medium` / `xhigh` 会归一化为 DeepSeek 的 `high` 级别。
|
|
448
459
|
|
|
449
|
-
推理模式为 `on` 时(包括通过 `--reasoning=<level>` 启用),AICommit
|
|
460
|
+
推理模式为 `on` 时(包括通过 `--reasoning=<level>` 启用),AICommit 会请求流式响应并实时显示推理;`auto` 模式下若 Provider 主动返回 think,也会显示,但不会强制开启推理。split 模式会显示最终规划请求的 think,大变更走分批分析时也一样,并在审阅界面保留这段内容;前面的分析批次只显示进度,避免多个推理流混在一起。实时面板需要交互式终端,JSON 输出始终不包含推理。默认实时视图跟随终端最新两行;生成或审阅期间按 `Ctrl+O` 可展开或收起累计文本。较长的展开输出会限制在终端视口内,可用 `PageUp` / `PageDown` 阅读所有页面。长按 `Ctrl+O` 只计为一次切换,避免按键重复留下多个面板。输出会经过清理,并受 `reasoning.maxDisplayChars` 限制;不暴露推理的 Provider 会显示简短的不可用提示。
|
|
450
461
|
|
|
451
462
|
```json
|
|
452
463
|
{
|
package/docs/distribution.md
CHANGED
|
@@ -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.
|
|
36
|
+
npm install --package-lock-only @hifullmoon/aicommit@2.6.0
|
|
37
37
|
npm audit signatures
|
|
38
38
|
```
|
|
39
39
|
|
package/docs/troubleshooting.md
CHANGED
|
@@ -15,20 +15,20 @@ JSON mode keeps one machine object on stdout and diagnostics on stderr. The `err
|
|
|
15
15
|
|
|
16
16
|
JSON 模式保证 stdout 只有一个机器对象,诊断进入 stderr。`error.category` 与进程退出码可稳定用于自动化。
|
|
17
17
|
|
|
18
|
-
| Symptom / 症状 | Category / exit | Likely cause / 常见原因 | Check and recovery / 检查与恢复
|
|
19
|
-
| ------------------------------------------------------------------------------- | ------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- |
|
|
20
|
-
| Config fails before Git/provider access / 在 Git/provider 前配置失败 | `config` / `2` | Malformed JSON, invalid URL, unsupported option, missing credential / JSON 损坏、URL 或选项无效、凭据缺失 | `aicommit config path`; `aicommit config validate`; fix only the user-owned file shown
|
|
21
|
-
| Project settings are ignored / 项目设置被忽略 | warning | Repository config tried to set endpoint, credential, reasoning, or raise a ceiling / 仓库配置尝试设置连接、凭据、推理或提高预算 | Move trusted connection settings to `~/.aicommit/config.json`; keep team rules in `.aicommit.policy.json`
|
|
22
|
-
| Not a Git repository, conflict, empty index / 非仓库、冲突或 index 为空 | `git_state` / `3` | Wrong directory or unsafe Git state / 目录错误或 Git 状态不安全 | `git status`; `aicommit /absolute/repo/path`; resolve conflicts before retrying
|
|
23
|
-
| DNS, connection, or timeout / DNS、连接或超时 | `network` / `4` | Endpoint unavailable, proxy/TLS issue, budget too short / endpoint 不可达、代理/TLS、超时过短 | `aicommit doctor -p NAME`; verify HTTPS URL; raise user-owned `timeoutMs` only if expected
|
|
24
|
-
| HTTP authentication/rate/parameter failure / 鉴权、限流或参数失败 | `provider` / `5` | Wrong key/model/body; non-retryable 4xx / key、model、body 错误 | Check `apiKeyEnv`, model and compatibility table; authentication is never retried automatically
|
|
25
|
-
| Empty or malformed model reply / 空或畸形回复 | `response_format` / `6` | Unsupported response dialect, token limit, or policy validation failure / 响应方言、token 限制或 policy 校验失败 | Raise `maxTokens`, choose the matching adapter, inspect validator issue code; one correction is already attempted
|
|
26
|
-
| `split run --scope=all --yes` stops before API call / split 非交互在 API 前停止 | `sensitive_data` / `7` | Complete untracked scan found sensitive-looking data / 完整未跟踪扫描发现疑似敏感数据 | Review/stage intended files explicitly; do not bypass without checking the actual content
|
|
27
|
-
| Commit aborts after generation / 生成后提交中止 | `concurrent_modification` / `8` | Index/worktree changed during the protected window / 受保护窗口中 index/worktree 被修改 | Review `git status`, restore the intended snapshot, and generate again
|
|
28
|
-
| Split stopped after one or more commits / split 部分提交后停止 |
|
|
29
|
-
| `aicommit update` refuses the installation / update 拒绝当前安装 | `config` / `2` | Source checkout, npm link, or another active Node/npm environment / 源码、npm link 或当前是另一套 Node/npm 环境 | Activate the Node environment that installed AICommit, or run `npm install --global @hifullmoon/aicommit@latest` manually
|
|
30
|
-
| `aicommit update` cannot reach npm / update 无法访问 npm | `network` / `4` | Registry authentication, proxy, DNS, or network failure / registry 鉴权、代理、DNS 或网络失败 | Check `npm config get registry` and npm authentication/proxy settings, then retry
|
|
31
|
-
| npm provenance is absent or invalid / npm provenance 缺失或失败 | npm audit failure | Old npm CLI, non-trusted release, or wrong version / npm 过旧、非可信发布或版本错误 | Upgrade npm; run `npm audit signatures`; install only a version linked to the official workflow
|
|
18
|
+
| Symptom / 症状 | Category / exit | Likely cause / 常见原因 | Check and recovery / 检查与恢复 |
|
|
19
|
+
| ------------------------------------------------------------------------------- | ------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
20
|
+
| Config fails before Git/provider access / 在 Git/provider 前配置失败 | `config` / `2` | Malformed JSON, invalid URL, unsupported option, missing credential / JSON 损坏、URL 或选项无效、凭据缺失 | `aicommit config path`; `aicommit config validate`; fix only the user-owned file shown |
|
|
21
|
+
| Project settings are ignored / 项目设置被忽略 | warning | Repository config tried to set endpoint, credential, reasoning, or raise a ceiling / 仓库配置尝试设置连接、凭据、推理或提高预算 | Move trusted connection settings to `~/.aicommit/config.json`; keep team rules in `.aicommit.policy.json` |
|
|
22
|
+
| Not a Git repository, conflict, empty index / 非仓库、冲突或 index 为空 | `git_state` / `3` | Wrong directory or unsafe Git state / 目录错误或 Git 状态不安全 | `git status`; `aicommit /absolute/repo/path`; resolve conflicts before retrying |
|
|
23
|
+
| DNS, connection, or timeout / DNS、连接或超时 | `network` / `4` | Endpoint unavailable, proxy/TLS issue, budget too short / endpoint 不可达、代理/TLS、超时过短 | `aicommit doctor -p NAME`; verify HTTPS URL; raise user-owned `timeoutMs` only if expected |
|
|
24
|
+
| HTTP authentication/rate/parameter failure / 鉴权、限流或参数失败 | `provider` / `5` | Wrong key/model/body; non-retryable 4xx / key、model、body 错误 | Check `apiKeyEnv`, model and compatibility table; authentication is never retried automatically |
|
|
25
|
+
| Empty or malformed model reply / 空或畸形回复 | `response_format` / `6` | Unsupported response dialect, token limit, or policy validation failure / 响应方言、token 限制或 policy 校验失败 | Raise `maxTokens`, choose the matching adapter, inspect validator issue code; one correction is already attempted |
|
|
26
|
+
| `split run --scope=all --yes` stops before API call / split 非交互在 API 前停止 | `sensitive_data` / `7` | Complete untracked scan found sensitive-looking data / 完整未跟踪扫描发现疑似敏感数据 | Review/stage intended files explicitly; do not bypass without checking the actual content |
|
|
27
|
+
| Commit aborts after generation / 生成后提交中止 | `concurrent_modification` / `8` | Index/worktree changed during the protected window / 受保护窗口中 index/worktree 被修改 | Review `git status`, restore the intended snapshot, and generate again |
|
|
28
|
+
| Split stopped after one or more commits / split 部分提交后停止 | `git_state` / `3` | Hook, crash, SIGINT, or concurrent pending edit / hook、崩溃、中断或待处理文件变化 | Inspect `aicommit split status --output=json`, then run `aicommit split resume --yes`; only use `split abort` to discard recovery metadata |
|
|
29
|
+
| `aicommit update` refuses the installation / update 拒绝当前安装 | `config` / `2` | Source checkout, npm link, or another active Node/npm environment / 源码、npm link 或当前是另一套 Node/npm 环境 | Activate the Node environment that installed AICommit, or run `npm install --global @hifullmoon/aicommit@latest` manually |
|
|
30
|
+
| `aicommit update` cannot reach npm / update 无法访问 npm | `network` / `4` | Registry authentication, proxy, DNS, or network failure / registry 鉴权、代理、DNS 或网络失败 | Check `npm config get registry` and npm authentication/proxy settings, then retry |
|
|
31
|
+
| npm provenance is absent or invalid / npm provenance 缺失或失败 | npm audit failure | Old npm CLI, non-trusted release, or wrong version / npm 过旧、非可信发布或版本错误 | Upgrade npm; run `npm audit signatures`; install only a version linked to the official workflow |
|
|
32
32
|
|
|
33
33
|
If a failure remains, capture `aicommit doctor --output=json`, Node/Git versions, the error category, and redacted config sources. Never attach a diff, commit message, config file, API key, reasoning trace, or credential-helper output to a public issue.
|
|
34
34
|
|
package/package.json
CHANGED
|
@@ -16,11 +16,16 @@
|
|
|
16
16
|
"warnings",
|
|
17
17
|
"exitReason",
|
|
18
18
|
"committed",
|
|
19
|
+
"commitState",
|
|
20
|
+
"commitSha",
|
|
21
|
+
"planFile",
|
|
22
|
+
"scope",
|
|
23
|
+
"changeCount",
|
|
19
24
|
"error"
|
|
20
25
|
],
|
|
21
26
|
"properties": {
|
|
22
27
|
"schemaVersion": {
|
|
23
|
-
"const": "1.
|
|
28
|
+
"const": "1.1"
|
|
24
29
|
},
|
|
25
30
|
"ok": {
|
|
26
31
|
"type": "boolean"
|
|
@@ -107,10 +112,26 @@
|
|
|
107
112
|
"committed": {
|
|
108
113
|
"type": "boolean"
|
|
109
114
|
},
|
|
115
|
+
"commitState": {
|
|
116
|
+
"enum": ["none", "partial", "complete", "unknown"]
|
|
117
|
+
},
|
|
118
|
+
"commitSha": {
|
|
119
|
+
"type": ["string", "null"]
|
|
120
|
+
},
|
|
121
|
+
"planFile": {
|
|
122
|
+
"type": ["string", "null"]
|
|
123
|
+
},
|
|
124
|
+
"scope": {
|
|
125
|
+
"type": ["string", "null"]
|
|
126
|
+
},
|
|
127
|
+
"changeCount": {
|
|
128
|
+
"type": ["integer", "null"],
|
|
129
|
+
"minimum": 0
|
|
130
|
+
},
|
|
110
131
|
"error": {
|
|
111
132
|
"type": ["object", "null"],
|
|
112
133
|
"additionalProperties": false,
|
|
113
|
-
"required": ["category", "message"],
|
|
134
|
+
"required": ["category", "code", "message", "retryable", "nextAction"],
|
|
114
135
|
"properties": {
|
|
115
136
|
"category": {
|
|
116
137
|
"enum": [
|
|
@@ -126,6 +147,16 @@
|
|
|
126
147
|
},
|
|
127
148
|
"message": {
|
|
128
149
|
"type": "string"
|
|
150
|
+
},
|
|
151
|
+
"code": {
|
|
152
|
+
"type": "string",
|
|
153
|
+
"minLength": 1
|
|
154
|
+
},
|
|
155
|
+
"retryable": {
|
|
156
|
+
"type": "boolean"
|
|
157
|
+
},
|
|
158
|
+
"nextAction": {
|
|
159
|
+
"type": ["string", "null"]
|
|
129
160
|
}
|
|
130
161
|
}
|
|
131
162
|
},
|
|
@@ -150,7 +181,7 @@
|
|
|
150
181
|
"description": "Files represented by partial excerpts; not fully analyzed files."
|
|
151
182
|
},
|
|
152
183
|
"strategy": {
|
|
153
|
-
"enum": ["auto", "deep"]
|
|
184
|
+
"enum": ["auto", "deep", "fallback"]
|
|
154
185
|
},
|
|
155
186
|
"metadataOnlyFiles": {
|
|
156
187
|
"type": "number",
|
|
@@ -181,6 +212,33 @@
|
|
|
181
212
|
"minimum": 0
|
|
182
213
|
}
|
|
183
214
|
}
|
|
215
|
+
},
|
|
216
|
+
"split": {
|
|
217
|
+
"type": "object",
|
|
218
|
+
"additionalProperties": false,
|
|
219
|
+
"required": ["state"],
|
|
220
|
+
"properties": {
|
|
221
|
+
"state": { "enum": ["idle", "pending"] },
|
|
222
|
+
"transactionId": { "type": "string" },
|
|
223
|
+
"checkpointPath": { "type": "string" },
|
|
224
|
+
"scope": { "enum": ["staged", "all"] },
|
|
225
|
+
"totalGroups": { "type": "integer", "minimum": 0 },
|
|
226
|
+
"completedGroups": { "type": "integer", "minimum": 0 },
|
|
227
|
+
"pendingGroups": { "type": "integer", "minimum": 0 },
|
|
228
|
+
"completedCommits": {
|
|
229
|
+
"type": "array",
|
|
230
|
+
"items": { "type": "string", "pattern": "^[0-9a-f]{40}([0-9a-f]{24})?$" }
|
|
231
|
+
},
|
|
232
|
+
"inFlight": {
|
|
233
|
+
"type": ["object", "null"],
|
|
234
|
+
"additionalProperties": false,
|
|
235
|
+
"properties": {
|
|
236
|
+
"group": { "type": "integer", "minimum": 1 },
|
|
237
|
+
"state": { "enum": ["pending", "committed", "unknown"] }
|
|
238
|
+
}
|
|
239
|
+
},
|
|
240
|
+
"nextAction": { "type": "string" }
|
|
241
|
+
}
|
|
184
242
|
}
|
|
185
243
|
}
|
|
186
244
|
}
|
package/src/change-analysis.js
CHANGED
|
@@ -313,7 +313,7 @@ function parseAnalysisJson(raw) {
|
|
|
313
313
|
throw new SyntaxError('Response contains no complete JSON array.');
|
|
314
314
|
}
|
|
315
315
|
|
|
316
|
-
async function jsonCall(config, instruction, items, validate = null) {
|
|
316
|
+
async function jsonCall(config, instruction, items, validate = null, stream = null) {
|
|
317
317
|
const parseAndValidate = (raw) => {
|
|
318
318
|
const parsed = parseAnalysisJson(raw);
|
|
319
319
|
validate?.(parsed);
|
|
@@ -329,7 +329,7 @@ async function jsonCall(config, instruction, items, validate = null) {
|
|
|
329
329
|
Math.min(2048, config.maxTokens || 1024),
|
|
330
330
|
'Return the complete requested JSON array, preserving every required input ID exactly once. ' +
|
|
331
331
|
`Required IDs: ${JSON.stringify(items.map((item) => item.id))}`,
|
|
332
|
-
|
|
332
|
+
stream,
|
|
333
333
|
(response) => {
|
|
334
334
|
try {
|
|
335
335
|
parseAndValidate(response);
|
|
@@ -339,6 +339,7 @@ async function jsonCall(config, instruction, items, validate = null) {
|
|
|
339
339
|
}
|
|
340
340
|
},
|
|
341
341
|
);
|
|
342
|
+
stream?.onReasoningComplete?.(result.reasoning);
|
|
342
343
|
try {
|
|
343
344
|
return parseAndValidate(result.text);
|
|
344
345
|
} catch (cause) {
|
|
@@ -712,7 +713,7 @@ export async function summarizeChanges(config, facts) {
|
|
|
712
713
|
return `Structured change summaries (not raw diff):\n${JSON.stringify(items)}`;
|
|
713
714
|
}
|
|
714
715
|
|
|
715
|
-
export async function planAnalyzedChanges(config, facts, coverage = null) {
|
|
716
|
+
export async function planAnalyzedChanges(config, facts, coverage = null, stream = null) {
|
|
716
717
|
let candidates = facts;
|
|
717
718
|
const deep = isDeepAnalysis(config) && coverage?.strategy !== 'auto';
|
|
718
719
|
const policy = normalizeCommitPolicy(config.commitPolicy, config.language);
|
|
@@ -749,6 +750,9 @@ export async function planAnalyzedChanges(config, facts, coverage = null) {
|
|
|
749
750
|
'Analysis plan is missing a commit subject.',
|
|
750
751
|
);
|
|
751
752
|
},
|
|
753
|
+
// Earlier batches may run many requests. Surface thinking from the
|
|
754
|
+
// final plan, which is the reasoning relevant to the review screen.
|
|
755
|
+
batches.length === 1 ? stream : null,
|
|
752
756
|
);
|
|
753
757
|
for (const group of groups) {
|
|
754
758
|
next.push({
|
package/src/cli.js
CHANGED
|
@@ -16,13 +16,17 @@ function showHelp() {
|
|
|
16
16
|
${chalk.dim('$')} aicommit [path] [options]
|
|
17
17
|
${chalk.dim('$')} aicommit setup
|
|
18
18
|
${chalk.dim('$')} aicommit update
|
|
19
|
-
${chalk.dim('$')} aicommit
|
|
19
|
+
${chalk.dim('$')} aicommit generate --scope=staged|all --file=<path> [options]
|
|
20
|
+
${chalk.dim('$')} aicommit apply --file=<path> [options]
|
|
21
|
+
${chalk.dim('$')} aicommit split [run|plan|apply|resume|abort|status] [options]
|
|
20
22
|
|
|
21
23
|
${chalk.bold('Everyday commands:')}
|
|
22
24
|
setup Interactive configuration wizard
|
|
23
25
|
update Update the global npm installation to latest
|
|
24
26
|
doctor Diagnose runtime, config, credentials, and connectivity
|
|
25
27
|
split Plan and create file-level logical commits
|
|
28
|
+
generate Export one validated commit plan without committing
|
|
29
|
+
apply Apply a validated commit plan
|
|
26
30
|
|
|
27
31
|
${chalk.bold('Advanced commands:')}
|
|
28
32
|
config show Print the effective configuration with secrets redacted
|
|
@@ -35,6 +39,7 @@ function showHelp() {
|
|
|
35
39
|
split apply Validate and apply an exported JSON plan
|
|
36
40
|
split resume Resume the repository's unfinished transaction
|
|
37
41
|
split abort Discard recovery metadata; keep commits and changes
|
|
42
|
+
split status Inspect recovery metadata without changing Git state
|
|
38
43
|
|
|
39
44
|
${chalk.bold('Arguments:')}
|
|
40
45
|
path Target directory (default: current directory)
|
|
@@ -45,8 +50,8 @@ function showHelp() {
|
|
|
45
50
|
-l, --lang=<zh|en> Commit message language (default: zh)
|
|
46
51
|
-p, --provider=<name> Use the named provider from config "providers"
|
|
47
52
|
-m, --model=<name> Use a named model from the selected provider
|
|
48
|
-
--scope=<scope> Scope for
|
|
49
|
-
--file=<path>
|
|
53
|
+
--scope=<scope> Scope for dry-run, generate, or split planning: staged, all
|
|
54
|
+
--file=<path> Commit-plan artifact or commit-message file
|
|
50
55
|
--range=<revision> Git revision/range for "policy check" (default: HEAD)
|
|
51
56
|
--reasoning=<level> Set reasoning effort (enabled by default: medium)
|
|
52
57
|
--no-reasoning Explicitly disable reasoning when supported
|
|
@@ -91,6 +96,9 @@ function parsedDefaults(overrides = {}) {
|
|
|
91
96
|
debug: false,
|
|
92
97
|
split: null,
|
|
93
98
|
splitCommand: null,
|
|
99
|
+
generate: false,
|
|
100
|
+
generateScope: null,
|
|
101
|
+
previewScope: null,
|
|
94
102
|
splitPlanFile: null,
|
|
95
103
|
dryRun: false,
|
|
96
104
|
yes: false,
|
|
@@ -165,10 +173,17 @@ export function parseArgs(args = process.argv.slice(2)) {
|
|
|
165
173
|
args = args.slice(2);
|
|
166
174
|
}
|
|
167
175
|
|
|
176
|
+
const generate = !update && args[0] === 'generate';
|
|
177
|
+
if (generate) args = args.slice(1);
|
|
178
|
+
|
|
168
179
|
let splitCommand = null;
|
|
180
|
+
if (!update && args[0] === 'apply') {
|
|
181
|
+
splitCommand = 'apply';
|
|
182
|
+
args = args.slice(1);
|
|
183
|
+
}
|
|
169
184
|
if (!update && args[0] === 'split') {
|
|
170
185
|
const requestedAction = args[1];
|
|
171
|
-
const actions = ['run', 'plan', 'apply', 'resume', 'abort'];
|
|
186
|
+
const actions = ['run', 'plan', 'apply', 'resume', 'abort', 'status'];
|
|
172
187
|
if (!requestedAction || requestedAction.startsWith('-')) {
|
|
173
188
|
// Keep the common path short: `aicommit split` is the interactive
|
|
174
189
|
// split flow, while explicit actions remain available for automation
|
|
@@ -181,7 +196,7 @@ export function parseArgs(args = process.argv.slice(2)) {
|
|
|
181
196
|
} else {
|
|
182
197
|
throw fail(
|
|
183
198
|
ERROR_CATEGORIES.CONFIG,
|
|
184
|
-
'split requires one action: run, plan, apply, resume, or
|
|
199
|
+
'split requires one action: run, plan, apply, resume, abort, or status.',
|
|
185
200
|
);
|
|
186
201
|
}
|
|
187
202
|
}
|
|
@@ -197,6 +212,8 @@ export function parseArgs(args = process.argv.slice(2)) {
|
|
|
197
212
|
let output = 'text';
|
|
198
213
|
let debug = false;
|
|
199
214
|
let split = null;
|
|
215
|
+
let generateScope = null;
|
|
216
|
+
let previewScope = null;
|
|
200
217
|
let splitPlanFile = null;
|
|
201
218
|
let policyMessageFile = null;
|
|
202
219
|
let policyRange = null;
|
|
@@ -238,20 +255,26 @@ export function parseArgs(args = process.argv.slice(2)) {
|
|
|
238
255
|
|
|
239
256
|
if (arg === '--scope') {
|
|
240
257
|
splitScopeOption = true;
|
|
241
|
-
|
|
258
|
+
const scope = takeValue(args, i, arg, 'staged|all');
|
|
242
259
|
i++;
|
|
243
|
-
if (!['staged', 'all'].includes(
|
|
244
|
-
throw fail(ERROR_CATEGORIES.CONFIG, `Invalid split scope: "${
|
|
260
|
+
if (!['staged', 'all'].includes(scope)) {
|
|
261
|
+
throw fail(ERROR_CATEGORIES.CONFIG, `Invalid split scope: "${scope}". Use staged or all.`);
|
|
245
262
|
}
|
|
263
|
+
if (generate) generateScope = scope;
|
|
264
|
+
else if (splitCommand) split = scope;
|
|
265
|
+
else previewScope = scope;
|
|
246
266
|
continue;
|
|
247
267
|
}
|
|
248
268
|
|
|
249
269
|
if (arg.startsWith('--scope=')) {
|
|
250
270
|
splitScopeOption = true;
|
|
251
|
-
|
|
252
|
-
if (!['staged', 'all'].includes(
|
|
253
|
-
throw fail(ERROR_CATEGORIES.CONFIG, `Invalid split scope: "${
|
|
271
|
+
const scope = arg.slice('--scope='.length);
|
|
272
|
+
if (!['staged', 'all'].includes(scope)) {
|
|
273
|
+
throw fail(ERROR_CATEGORIES.CONFIG, `Invalid split scope: "${scope}". Use staged or all.`);
|
|
254
274
|
}
|
|
275
|
+
if (generate) generateScope = scope;
|
|
276
|
+
else if (splitCommand) split = scope;
|
|
277
|
+
else previewScope = scope;
|
|
255
278
|
continue;
|
|
256
279
|
}
|
|
257
280
|
|
|
@@ -427,14 +450,32 @@ export function parseArgs(args = process.argv.slice(2)) {
|
|
|
427
450
|
}
|
|
428
451
|
}
|
|
429
452
|
}
|
|
430
|
-
if (
|
|
453
|
+
if (generate) {
|
|
454
|
+
if (!generateScope || !splitPlanFile) {
|
|
455
|
+
throw fail(
|
|
456
|
+
ERROR_CATEGORIES.CONFIG,
|
|
457
|
+
'generate requires --scope=staged|all and --file=<path>.',
|
|
458
|
+
);
|
|
459
|
+
}
|
|
460
|
+
if (splitCommand || allowSingleFallback) {
|
|
461
|
+
throw fail(ERROR_CATEGORIES.CONFIG, 'generate does not accept split commit options.');
|
|
462
|
+
}
|
|
463
|
+
dryRun = true;
|
|
464
|
+
}
|
|
465
|
+
if (splitScopeOption && !generate && splitCommand && !['run', 'plan'].includes(splitCommand)) {
|
|
431
466
|
throw fail(
|
|
432
467
|
ERROR_CATEGORIES.CONFIG,
|
|
433
|
-
'--scope is only valid with
|
|
468
|
+
'--scope is only valid with dry-run, generate, split run, or split plan.',
|
|
434
469
|
);
|
|
435
470
|
}
|
|
436
|
-
if (
|
|
437
|
-
throw fail(ERROR_CATEGORIES.CONFIG, '--
|
|
471
|
+
if (previewScope && !dryRun) {
|
|
472
|
+
throw fail(ERROR_CATEGORIES.CONFIG, '--scope without a subcommand requires --dry-run.');
|
|
473
|
+
}
|
|
474
|
+
if (splitPlanFile && !generate && !['plan', 'apply'].includes(splitCommand)) {
|
|
475
|
+
throw fail(
|
|
476
|
+
ERROR_CATEGORIES.CONFIG,
|
|
477
|
+
'--file is only valid with generate, apply, or split plan/apply.',
|
|
478
|
+
);
|
|
438
479
|
}
|
|
439
480
|
if (allowSingleFallback && (splitCommand !== 'run' || !yes || dryRun)) {
|
|
440
481
|
throw fail(
|
|
@@ -508,6 +549,9 @@ export function parseArgs(args = process.argv.slice(2)) {
|
|
|
508
549
|
debug,
|
|
509
550
|
split,
|
|
510
551
|
splitCommand,
|
|
552
|
+
generate,
|
|
553
|
+
generateScope,
|
|
554
|
+
previewScope,
|
|
511
555
|
splitPlanFile,
|
|
512
556
|
dryRun,
|
|
513
557
|
yes,
|
package/src/completion.js
CHANGED
|
@@ -5,6 +5,8 @@ const TOP_LEVEL = [
|
|
|
5
5
|
'config',
|
|
6
6
|
'policy',
|
|
7
7
|
'completion',
|
|
8
|
+
'generate',
|
|
9
|
+
'apply',
|
|
8
10
|
'split',
|
|
9
11
|
'--help',
|
|
10
12
|
'--version',
|
|
@@ -33,7 +35,7 @@ _aicommit() {
|
|
|
33
35
|
config) COMPREPLY=( $(compgen -W "show validate path --provider --model --output --debug" -- "$cur") ); return ;;
|
|
34
36
|
policy) COMPREPLY=( $(compgen -W "template check --file --range --output --debug" -- "$cur") ); return ;;
|
|
35
37
|
completion) COMPREPLY=( $(compgen -W "bash zsh fish" -- "$cur") ); return ;;
|
|
36
|
-
split) COMPREPLY=( $(compgen -W "run plan apply resume abort --scope --file --yes --allow-single-fallback --output --debug" -- "$cur") ); return ;;
|
|
38
|
+
split) COMPREPLY=( $(compgen -W "run plan apply resume abort status --scope --file --yes --allow-single-fallback --output --debug" -- "$cur") ); return ;;
|
|
37
39
|
esac
|
|
38
40
|
case "$prev" in
|
|
39
41
|
--lang|-l) COMPREPLY=( $(compgen -W "zh en" -- "$cur") ); return ;;
|
|
@@ -58,7 +60,9 @@ _aicommit() {
|
|
|
58
60
|
'config:inspect or validate configuration'
|
|
59
61
|
'policy:print or enforce a repository team policy'
|
|
60
62
|
'completion:generate shell completion'
|
|
61
|
-
'
|
|
63
|
+
'generate:export one commit plan'
|
|
64
|
+
'apply:apply a validated commit plan'
|
|
65
|
+
'split:run, plan, apply, resume, abort, or inspect split commits'
|
|
62
66
|
)
|
|
63
67
|
options=(
|
|
64
68
|
'--help[show help]'
|
|
@@ -66,8 +70,8 @@ _aicommit() {
|
|
|
66
70
|
'--lang=[commit language]:language:(zh en)'
|
|
67
71
|
'--provider=[provider name]:provider:'
|
|
68
72
|
'--model=[model name]:model:'
|
|
69
|
-
'--scope=[
|
|
70
|
-
'--file=[
|
|
73
|
+
'--scope=[change scope]:scope:(staged all)'
|
|
74
|
+
'--file=[commit plan or commit-message file]:path:_files'
|
|
71
75
|
'--range=[Git range for policy check]:revision:'
|
|
72
76
|
'--reasoning=[reasoning effort]:effort:(low medium high xhigh max)'
|
|
73
77
|
'--no-reasoning[disable reasoning]'
|
|
@@ -87,7 +91,7 @@ _aicommit() {
|
|
|
87
91
|
config) _values 'config action' show validate path ;;
|
|
88
92
|
policy) _values 'policy action' template check ;;
|
|
89
93
|
completion) _values 'shell' bash zsh fish ;;
|
|
90
|
-
split) _values 'split action' run plan apply resume abort ;;
|
|
94
|
+
split) _values 'split action' run plan apply resume abort status ;;
|
|
91
95
|
esac
|
|
92
96
|
;;
|
|
93
97
|
esac
|
|
@@ -103,18 +107,20 @@ complete -c aicommit -n '__fish_use_subcommand' -a doctor -d 'Diagnose configura
|
|
|
103
107
|
complete -c aicommit -n '__fish_use_subcommand' -a config -d 'Inspect or validate configuration'
|
|
104
108
|
complete -c aicommit -n '__fish_use_subcommand' -a policy -d 'Print or enforce a repository team policy'
|
|
105
109
|
complete -c aicommit -n '__fish_use_subcommand' -a completion -d 'Generate shell completion'
|
|
110
|
+
complete -c aicommit -n '__fish_use_subcommand' -a generate -d 'Export one commit plan'
|
|
111
|
+
complete -c aicommit -n '__fish_use_subcommand' -a apply -d 'Apply a validated commit plan'
|
|
106
112
|
complete -c aicommit -n '__fish_use_subcommand' -a split -d 'Run, plan, apply, resume, or abort split commits'
|
|
107
113
|
complete -c aicommit -n '__fish_seen_subcommand_from config' -a 'show validate path'
|
|
108
114
|
complete -c aicommit -n '__fish_seen_subcommand_from policy' -a 'template check'
|
|
109
115
|
complete -c aicommit -n '__fish_seen_subcommand_from completion' -a 'bash zsh fish'
|
|
110
|
-
complete -c aicommit -n '__fish_seen_subcommand_from split' -a 'run plan apply resume abort'
|
|
116
|
+
complete -c aicommit -n '__fish_seen_subcommand_from split' -a 'run plan apply resume abort status'
|
|
111
117
|
complete -c aicommit -s h -l help -d 'Show help'
|
|
112
118
|
complete -c aicommit -s v -l version -d 'Show version'
|
|
113
119
|
complete -c aicommit -s l -l lang -x -a 'zh en' -d 'Commit language'
|
|
114
120
|
complete -c aicommit -s p -l provider -x -d 'Provider name'
|
|
115
121
|
complete -c aicommit -s m -l model -x -d 'Model name'
|
|
116
|
-
complete -c aicommit -l scope -x -a 'staged all' -d '
|
|
117
|
-
complete -c aicommit -l file -r -d '
|
|
122
|
+
complete -c aicommit -l scope -x -a 'staged all' -d 'Change scope'
|
|
123
|
+
complete -c aicommit -l file -r -d 'Commit plan file'
|
|
118
124
|
complete -c aicommit -l range -r -d 'Git range for policy check'
|
|
119
125
|
complete -c aicommit -l reasoning -x -a 'low medium high xhigh max' -d 'Reasoning effort'
|
|
120
126
|
complete -c aicommit -l no-reasoning -d 'Disable reasoning'
|
package/src/errors.js
CHANGED
|
@@ -38,8 +38,13 @@ export class AicommitError extends Error {
|
|
|
38
38
|
super(message);
|
|
39
39
|
this.name = 'AicommitError';
|
|
40
40
|
this.category = category;
|
|
41
|
+
this.code = options.code || category;
|
|
41
42
|
this.exitCode = EXIT_CODES[category] ?? EXIT_CODES.internal;
|
|
42
43
|
this.reported = Boolean(options.reported);
|
|
44
|
+
this.retryable = Boolean(options.retryable);
|
|
45
|
+
this.nextAction = options.nextAction || null;
|
|
46
|
+
this.committed = Boolean(options.committed);
|
|
47
|
+
this.commitState = options.commitState || 'none';
|
|
43
48
|
if (options.data && typeof options.data === 'object' && !Array.isArray(options.data)) {
|
|
44
49
|
this.data = options.data;
|
|
45
50
|
}
|
|
@@ -62,11 +67,10 @@ export function classifyError(err) {
|
|
|
62
67
|
const lower = message.toLowerCase();
|
|
63
68
|
const code = errorCode(err);
|
|
64
69
|
if (
|
|
65
|
-
err instanceof TypeError ||
|
|
66
70
|
NETWORK_CODES.has(code) ||
|
|
67
71
|
/timed out|fetch failed|socket|network|dns|econn|enotfound/.test(lower)
|
|
68
72
|
) {
|
|
69
|
-
return fail(ERROR_CATEGORIES.NETWORK, message, { cause: err });
|
|
73
|
+
return fail(ERROR_CATEGORIES.NETWORK, message, { cause: err, retryable: true });
|
|
70
74
|
}
|
|
71
75
|
if (/^http \d{3}:|streaming api error|rate limit|provider request/.test(lower)) {
|
|
72
76
|
return fail(ERROR_CATEGORIES.PROVIDER, message, { cause: err });
|
package/src/generation-ui.js
CHANGED
|
@@ -18,7 +18,7 @@ export async function runModelTask({
|
|
|
18
18
|
const spinner = ora({ text: chalk.dim(spinnerText), color: 'cyan' }).start();
|
|
19
19
|
let liveReasoning;
|
|
20
20
|
const stream =
|
|
21
|
-
reasoning
|
|
21
|
+
reasoning && reasoning.mode !== 'off' && !machineOutput
|
|
22
22
|
? {
|
|
23
23
|
onReasoningDelta(chunk) {
|
|
24
24
|
if (!liveReasoning) {
|
package/src/git.js
CHANGED
|
@@ -70,13 +70,24 @@ export function getStagedDiff(cwd, contextLines) {
|
|
|
70
70
|
return readGit(['diff', unifiedArg(contextLines), '--staged'], cwd).trim();
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
-
//
|
|
74
|
-
//
|
|
75
|
-
//
|
|
73
|
+
// Git's raw staged diff contains the full old/new blob IDs, modes, status,
|
|
74
|
+
// and NUL-delimited paths for every changed entry. Hashing that metadata is
|
|
75
|
+
// equivalent to hashing the staged patch for snapshot identity, including
|
|
76
|
+
// binary changes, without regenerating and spooling the entire patch during
|
|
77
|
+
// every pre/post-model safety check.
|
|
76
78
|
export function getIndexFingerprint(cwd) {
|
|
77
79
|
return updateGitHash(
|
|
78
80
|
createHash('sha256'),
|
|
79
|
-
[
|
|
81
|
+
[
|
|
82
|
+
'diff',
|
|
83
|
+
'--raw',
|
|
84
|
+
'--staged',
|
|
85
|
+
'--no-renames',
|
|
86
|
+
'--no-ext-diff',
|
|
87
|
+
'--no-textconv',
|
|
88
|
+
'--abbrev=64',
|
|
89
|
+
'-z',
|
|
90
|
+
],
|
|
80
91
|
cwd,
|
|
81
92
|
).digest('hex');
|
|
82
93
|
}
|
|
@@ -263,7 +274,8 @@ export function condenseDiff(diff, maxChars, stat, maxSectionChars = Infinity) {
|
|
|
263
274
|
}
|
|
264
275
|
const marker = `... (diff truncated — ${diff.length} chars total)`;
|
|
265
276
|
const body = kept ? `${kept}\n${marker}` : marker;
|
|
266
|
-
|
|
277
|
+
const resolvedStat = typeof stat === 'function' ? stat() : stat;
|
|
278
|
+
return { diff: resolvedStat ? `${resolvedStat}\n\n${body}` : body, truncated: true };
|
|
267
279
|
}
|
|
268
280
|
|
|
269
281
|
export function getChangedFiles(cwd) {
|
package/src/main.js
CHANGED
|
@@ -17,6 +17,8 @@ import {
|
|
|
17
17
|
runGit,
|
|
18
18
|
isGitRepo,
|
|
19
19
|
getIndexFingerprint,
|
|
20
|
+
hasHead,
|
|
21
|
+
readGit,
|
|
20
22
|
createIndexTransaction,
|
|
21
23
|
protectSensitiveDiff,
|
|
22
24
|
unifiedArg,
|
|
@@ -54,8 +56,13 @@ import {
|
|
|
54
56
|
applySplitPlan,
|
|
55
57
|
resumeSplit,
|
|
56
58
|
splitFlow,
|
|
59
|
+
splitStatus,
|
|
57
60
|
getStagedChangedFiles,
|
|
61
|
+
getAllChangedFiles,
|
|
62
|
+
getSplitStateFingerprint,
|
|
63
|
+
safeExportPlanPath,
|
|
58
64
|
} from './split.js';
|
|
65
|
+
import { createSplitPlanArtifact, writeSplitPlanArtifact } from './split-plan.js';
|
|
59
66
|
import { runModelTask } from './generation-ui.js';
|
|
60
67
|
import { runSetup } from './setup.js';
|
|
61
68
|
import { detectProviderType } from './providers.js';
|
|
@@ -108,6 +115,9 @@ async function runMain() {
|
|
|
108
115
|
debug,
|
|
109
116
|
split,
|
|
110
117
|
splitCommand,
|
|
118
|
+
generate,
|
|
119
|
+
generateScope,
|
|
120
|
+
previewScope,
|
|
111
121
|
splitPlanFile,
|
|
112
122
|
dryRun,
|
|
113
123
|
yes,
|
|
@@ -130,7 +140,15 @@ async function runMain() {
|
|
|
130
140
|
return { exitReason: 'completion' };
|
|
131
141
|
}
|
|
132
142
|
const machineOutput = output === 'json';
|
|
133
|
-
if (
|
|
143
|
+
if (
|
|
144
|
+
machineOutput &&
|
|
145
|
+
!yes &&
|
|
146
|
+
!doctor &&
|
|
147
|
+
!configAction &&
|
|
148
|
+
!policyAction &&
|
|
149
|
+
!update &&
|
|
150
|
+
splitCommand !== 'status'
|
|
151
|
+
) {
|
|
134
152
|
throw fail(ERROR_CATEGORIES.CONFIG, '--output=json requires --yes for commit and split flows.');
|
|
135
153
|
}
|
|
136
154
|
|
|
@@ -178,13 +196,14 @@ async function runMain() {
|
|
|
178
196
|
console.log(' ' + chalk.dim('─'.repeat(45)));
|
|
179
197
|
console.log(' ' + chalk.dim(`Working directory: ${sanitizeTerminalText(process.cwd())}`));
|
|
180
198
|
|
|
181
|
-
if (['apply', 'resume', 'abort'].includes(splitCommand)) {
|
|
199
|
+
if (['apply', 'resume', 'abort', 'status'].includes(splitCommand)) {
|
|
182
200
|
const projectRoot = getProjectRoot();
|
|
183
201
|
if (!isGitRepo(projectRoot)) {
|
|
184
202
|
throw fail(ERROR_CATEGORIES.GIT_STATE, `Not a git repository: ${process.cwd()}`);
|
|
185
203
|
}
|
|
186
204
|
if (splitCommand === 'resume') return resumeSplit(projectRoot, { yes, machineOutput });
|
|
187
205
|
if (splitCommand === 'abort') return abortSplit(projectRoot, { yes, machineOutput });
|
|
206
|
+
if (splitCommand === 'status') return splitStatus(projectRoot);
|
|
188
207
|
return applySplitPlan(projectRoot, splitPlanFile, { yes, machineOutput });
|
|
189
208
|
}
|
|
190
209
|
|
|
@@ -340,6 +359,35 @@ async function runMain() {
|
|
|
340
359
|
// All git commands run at the repo root (projectRoot), even when aicommit
|
|
341
360
|
// is invoked from a subdirectory.
|
|
342
361
|
let indexTransaction = null;
|
|
362
|
+
const effectiveScope = generate ? generateScope : previewScope;
|
|
363
|
+
let stagedAllForPreview = false;
|
|
364
|
+
const generateSnapshot = generate
|
|
365
|
+
? (() => {
|
|
366
|
+
const changes =
|
|
367
|
+
generateScope === 'staged'
|
|
368
|
+
? getStagedChangedFiles(projectRoot)
|
|
369
|
+
: getAllChangedFiles(projectRoot);
|
|
370
|
+
if (!changes.length) {
|
|
371
|
+
throw fail(
|
|
372
|
+
ERROR_CATEGORIES.GIT_STATE,
|
|
373
|
+
`No ${generateScope} changes to generate a plan for.`,
|
|
374
|
+
);
|
|
375
|
+
}
|
|
376
|
+
return {
|
|
377
|
+
changes,
|
|
378
|
+
baseHead: hasHead(projectRoot)
|
|
379
|
+
? readGit(['rev-parse', 'HEAD'], projectRoot).trim()
|
|
380
|
+
: null,
|
|
381
|
+
fingerprint: getSplitStateFingerprint(
|
|
382
|
+
projectRoot,
|
|
383
|
+
hasHead(projectRoot),
|
|
384
|
+
changes,
|
|
385
|
+
generateScope,
|
|
386
|
+
),
|
|
387
|
+
file: safeExportPlanPath(projectRoot, splitPlanFile),
|
|
388
|
+
};
|
|
389
|
+
})()
|
|
390
|
+
: null;
|
|
343
391
|
const finishCancelled = ({
|
|
344
392
|
notice = 'Commit cancelled.',
|
|
345
393
|
message = null,
|
|
@@ -374,6 +422,20 @@ async function runMain() {
|
|
|
374
422
|
indexTransaction ||= createIndexTransaction(projectRoot);
|
|
375
423
|
return indexTransaction;
|
|
376
424
|
};
|
|
425
|
+
if (effectiveScope === 'all') {
|
|
426
|
+
try {
|
|
427
|
+
beginIndexTransaction();
|
|
428
|
+
runGit(['add', '-A'], projectRoot);
|
|
429
|
+
indexTransaction.markOwned();
|
|
430
|
+
stagedAllForPreview = true;
|
|
431
|
+
} catch (err) {
|
|
432
|
+
indexTransaction?.restore({ force: true });
|
|
433
|
+
indexTransaction = null;
|
|
434
|
+
throw fail(ERROR_CATEGORIES.GIT_STATE, `Failed to stage plan snapshot: ${err.message}`, {
|
|
435
|
+
cause: err,
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
}
|
|
377
439
|
if (!getChangedFiles(projectRoot).length) {
|
|
378
440
|
// Nothing staged. But git diff --staged is also empty for unstaged work
|
|
379
441
|
// and untracked files — surface what git status actually shows instead of
|
|
@@ -390,6 +452,10 @@ async function runMain() {
|
|
|
390
452
|
throw fail(ERROR_CATEGORIES.GIT_STATE, 'No changes to commit.', { reported: true });
|
|
391
453
|
}
|
|
392
454
|
|
|
455
|
+
if (effectiveScope === 'staged' || generate) {
|
|
456
|
+
throw fail(ERROR_CATEGORIES.GIT_STATE, `No ${effectiveScope} changes to preview.`);
|
|
457
|
+
}
|
|
458
|
+
|
|
393
459
|
console.log('\n ' + chalk.yellow(`✗ No staged changes — ${tips.join(', ')}.`));
|
|
394
460
|
printChangeList(withUntrackedFiles(unstaged, untracked));
|
|
395
461
|
console.log('');
|
|
@@ -531,6 +597,13 @@ async function runMain() {
|
|
|
531
597
|
let diffForModel = diff;
|
|
532
598
|
let protectAnalysis = true;
|
|
533
599
|
if (protectedInput.findings.length) {
|
|
600
|
+
if (generate && generateScope === 'all' && yes) {
|
|
601
|
+
throw fail(
|
|
602
|
+
ERROR_CATEGORIES.SENSITIVE_DATA,
|
|
603
|
+
'Non-interactive generate --scope=all refuses sensitive content; review and stage intended files explicitly.',
|
|
604
|
+
{ code: 'sensitive_all_scope' },
|
|
605
|
+
);
|
|
606
|
+
}
|
|
534
607
|
warnings.push('Sensitive data was detected and protected before the provider request.');
|
|
535
608
|
console.log('\n ' + chalk.yellow.bold('⚠ Potential sensitive data detected:'));
|
|
536
609
|
for (const finding of protectedInput.findings) {
|
|
@@ -571,7 +644,7 @@ async function runMain() {
|
|
|
571
644
|
: condenseDiff(
|
|
572
645
|
strippedDiff,
|
|
573
646
|
config.maxDiffChars,
|
|
574
|
-
getDiffStat(projectRoot),
|
|
647
|
+
() => getDiffStat(projectRoot),
|
|
575
648
|
config.maxFileDiffChars,
|
|
576
649
|
);
|
|
577
650
|
let analysis;
|
|
@@ -723,7 +796,7 @@ async function runMain() {
|
|
|
723
796
|
? 'use'
|
|
724
797
|
: await confirmAction(
|
|
725
798
|
message,
|
|
726
|
-
reasoningEnabled
|
|
799
|
+
reasoningEnabled || reasoningText
|
|
727
800
|
? {
|
|
728
801
|
text: reasoningText,
|
|
729
802
|
maxChars: config.reasoning.maxDisplayChars,
|
|
@@ -773,14 +846,51 @@ async function runMain() {
|
|
|
773
846
|
console.log('');
|
|
774
847
|
|
|
775
848
|
if (dryRun) {
|
|
849
|
+
stagedAllForPreview ||= Boolean(indexTransaction);
|
|
776
850
|
const restored = indexTransaction ? indexTransaction.restore() : true;
|
|
777
851
|
indexTransaction = null;
|
|
778
852
|
if (!restored) {
|
|
853
|
+
if (generate) {
|
|
854
|
+
throw fail(
|
|
855
|
+
ERROR_CATEGORIES.CONCURRENT_MODIFICATION,
|
|
856
|
+
'The Git index changed while generating a commit plan; no plan was written.',
|
|
857
|
+
);
|
|
858
|
+
}
|
|
779
859
|
warnings.push('The Git index changed during the run and was left untouched.');
|
|
780
860
|
console.log(
|
|
781
861
|
' ' + chalk.yellow('⚠ The Git index changed during the run and was left untouched.'),
|
|
782
862
|
);
|
|
783
863
|
}
|
|
864
|
+
let plan = null;
|
|
865
|
+
let planFile = null;
|
|
866
|
+
if (generate) {
|
|
867
|
+
const fingerprint = getSplitStateFingerprint(
|
|
868
|
+
projectRoot,
|
|
869
|
+
hasHead(projectRoot),
|
|
870
|
+
undefined,
|
|
871
|
+
generateScope,
|
|
872
|
+
);
|
|
873
|
+
const baseHead = hasHead(projectRoot)
|
|
874
|
+
? readGit(['rev-parse', 'HEAD'], projectRoot).trim()
|
|
875
|
+
: null;
|
|
876
|
+
if (fingerprint !== generateSnapshot.fingerprint || baseHead !== generateSnapshot.baseHead) {
|
|
877
|
+
throw fail(
|
|
878
|
+
ERROR_CATEGORIES.CONCURRENT_MODIFICATION,
|
|
879
|
+
'Repository changes changed while generating a commit plan; no plan was written.',
|
|
880
|
+
);
|
|
881
|
+
}
|
|
882
|
+
plan = [{ message, files: generateSnapshot.changes.map((change) => change.path) }];
|
|
883
|
+
const artifact = createSplitPlanArtifact({
|
|
884
|
+
scope: generateScope,
|
|
885
|
+
baseHead,
|
|
886
|
+
fingerprint,
|
|
887
|
+
language: config.language,
|
|
888
|
+
commitPolicy: config.commitPolicy,
|
|
889
|
+
changes: generateSnapshot.changes,
|
|
890
|
+
groups: plan,
|
|
891
|
+
});
|
|
892
|
+
planFile = await writeSplitPlanArtifact(generateSnapshot.file, artifact);
|
|
893
|
+
}
|
|
784
894
|
console.log(
|
|
785
895
|
' ' +
|
|
786
896
|
chalk.green.bold(
|
|
@@ -789,6 +899,10 @@ async function runMain() {
|
|
|
789
899
|
);
|
|
790
900
|
return {
|
|
791
901
|
message,
|
|
902
|
+
plan,
|
|
903
|
+
planFile,
|
|
904
|
+
scope: effectiveScope || (stagedAllForPreview || indexTransaction ? 'all' : 'staged'),
|
|
905
|
+
changeCount: generate ? generateSnapshot.changes.length : changedFiles.length,
|
|
792
906
|
provider: selectedProvider,
|
|
793
907
|
model: config.modelId,
|
|
794
908
|
latencyMs: elapsed,
|
|
@@ -835,6 +949,9 @@ async function runMain() {
|
|
|
835
949
|
console.log('\n ' + chalk.green.bold('✓ Done!\n'));
|
|
836
950
|
return {
|
|
837
951
|
message,
|
|
952
|
+
commitSha: readGit(['rev-parse', 'HEAD'], projectRoot).trim(),
|
|
953
|
+
scope: 'staged',
|
|
954
|
+
changeCount: changedFiles.length,
|
|
838
955
|
provider: selectedProvider,
|
|
839
956
|
model: config.modelId,
|
|
840
957
|
latencyMs: elapsed,
|
package/src/output.js
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { classifyError, EXIT_CODES } from './errors.js';
|
|
2
2
|
|
|
3
|
-
export const OUTPUT_SCHEMA_VERSION = '1.
|
|
3
|
+
export const OUTPUT_SCHEMA_VERSION = '1.1';
|
|
4
|
+
|
|
5
|
+
function safeString(value) {
|
|
6
|
+
return typeof value === 'string' && value ? value : null;
|
|
7
|
+
}
|
|
4
8
|
|
|
5
9
|
function normalizedUsage(usage) {
|
|
6
10
|
if (!usage) return null;
|
|
@@ -43,6 +47,11 @@ export function successOutput(result = {}) {
|
|
|
43
47
|
warnings: Array.isArray(result.warnings) ? result.warnings.map(String) : [],
|
|
44
48
|
exitReason: result.exitReason || 'success',
|
|
45
49
|
committed: Boolean(result.committed),
|
|
50
|
+
commitState: result.commitState || (result.committed ? 'complete' : 'none'),
|
|
51
|
+
commitSha: safeString(result.commitSha),
|
|
52
|
+
planFile: safeString(result.planFile),
|
|
53
|
+
scope: safeString(result.scope),
|
|
54
|
+
changeCount: Number.isInteger(result.changeCount) ? result.changeCount : null,
|
|
46
55
|
error: null,
|
|
47
56
|
};
|
|
48
57
|
if (result.data && typeof result.data === 'object' && !Array.isArray(result.data)) {
|
|
@@ -64,10 +73,18 @@ export function errorOutput(err) {
|
|
|
64
73
|
usage: null,
|
|
65
74
|
warnings: [],
|
|
66
75
|
exitReason: classified.category,
|
|
67
|
-
committed:
|
|
76
|
+
committed: classified.committed,
|
|
77
|
+
commitState: classified.commitState,
|
|
78
|
+
commitSha: null,
|
|
79
|
+
planFile: null,
|
|
80
|
+
scope: null,
|
|
81
|
+
changeCount: null,
|
|
68
82
|
error: {
|
|
69
83
|
category: classified.category,
|
|
84
|
+
code: classified.code,
|
|
70
85
|
message: classified.message,
|
|
86
|
+
retryable: classified.retryable,
|
|
87
|
+
nextAction: classified.nextAction,
|
|
71
88
|
},
|
|
72
89
|
};
|
|
73
90
|
if (classified.data && typeof classified.data === 'object' && !Array.isArray(classified.data)) {
|
package/src/split.js
CHANGED
|
@@ -1163,6 +1163,83 @@ function currentHeadParent(projectRoot) {
|
|
|
1163
1163
|
return fields[1] || null;
|
|
1164
1164
|
}
|
|
1165
1165
|
|
|
1166
|
+
function splitCheckpointSummary(projectRoot, loaded) {
|
|
1167
|
+
const { path, checkpoint } = loaded;
|
|
1168
|
+
const knownCommits = checkpoint.completed.map((record) => record.commit);
|
|
1169
|
+
let inFlight = null;
|
|
1170
|
+
if (checkpoint.inFlight) {
|
|
1171
|
+
const record = checkpoint.inFlight;
|
|
1172
|
+
const head = currentHead(projectRoot);
|
|
1173
|
+
const state =
|
|
1174
|
+
head === record.parent
|
|
1175
|
+
? 'pending'
|
|
1176
|
+
: currentHeadParent(projectRoot) === record.parent &&
|
|
1177
|
+
currentHeadTree(projectRoot) === record.tree
|
|
1178
|
+
? 'committed'
|
|
1179
|
+
: 'unknown';
|
|
1180
|
+
if (state === 'committed') knownCommits.push(head);
|
|
1181
|
+
inFlight = { group: record.index + 1, state };
|
|
1182
|
+
}
|
|
1183
|
+
const totalGroups = checkpoint.plan.groups.length;
|
|
1184
|
+
return {
|
|
1185
|
+
state: 'pending',
|
|
1186
|
+
transactionId: checkpoint.transactionId,
|
|
1187
|
+
checkpointPath: path,
|
|
1188
|
+
scope: checkpoint.plan.scope,
|
|
1189
|
+
totalGroups,
|
|
1190
|
+
completedGroups: knownCommits.length,
|
|
1191
|
+
pendingGroups: totalGroups - knownCommits.length,
|
|
1192
|
+
completedCommits: knownCommits,
|
|
1193
|
+
inFlight,
|
|
1194
|
+
nextAction: 'aicommit split resume --yes',
|
|
1195
|
+
};
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
export function splitStatus(projectRoot) {
|
|
1199
|
+
let summary;
|
|
1200
|
+
try {
|
|
1201
|
+
summary = splitCheckpointSummary(projectRoot, readSplitCheckpoint(projectRoot));
|
|
1202
|
+
} catch (err) {
|
|
1203
|
+
if (!/No split checkpoint found:/.test(err.message)) {
|
|
1204
|
+
throw fail(ERROR_CATEGORIES.GIT_STATE, `Cannot inspect split checkpoint: ${err.message}`, {
|
|
1205
|
+
code: 'split_checkpoint_invalid',
|
|
1206
|
+
cause: err,
|
|
1207
|
+
});
|
|
1208
|
+
}
|
|
1209
|
+
summary = { state: 'idle' };
|
|
1210
|
+
}
|
|
1211
|
+
const known = summary.completedCommits || [];
|
|
1212
|
+
const commitState =
|
|
1213
|
+
summary.inFlight?.state === 'unknown'
|
|
1214
|
+
? 'unknown'
|
|
1215
|
+
: !known.length
|
|
1216
|
+
? 'none'
|
|
1217
|
+
: summary.pendingGroups
|
|
1218
|
+
? 'partial'
|
|
1219
|
+
: 'complete';
|
|
1220
|
+
return {
|
|
1221
|
+
exitReason: 'split_status',
|
|
1222
|
+
committed: false,
|
|
1223
|
+
commitState,
|
|
1224
|
+
commitSha: known.at(-1) || null,
|
|
1225
|
+
scope: summary.scope || null,
|
|
1226
|
+
data: { split: summary },
|
|
1227
|
+
};
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
function splitCommitFailure(projectRoot, message) {
|
|
1231
|
+
const status = splitStatus(projectRoot);
|
|
1232
|
+
const knownCommits = status.data.split.completedCommits || [];
|
|
1233
|
+
throw fail(ERROR_CATEGORIES.GIT_STATE, message, {
|
|
1234
|
+
code: knownCommits.length ? 'split_partial_failure' : 'split_commit_failed',
|
|
1235
|
+
reported: true,
|
|
1236
|
+
committed: knownCommits.length > 0,
|
|
1237
|
+
commitState: status.commitState,
|
|
1238
|
+
nextAction: 'aicommit split resume --yes',
|
|
1239
|
+
data: status.data,
|
|
1240
|
+
});
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1166
1243
|
function readHeadEntries(projectRoot, paths) {
|
|
1167
1244
|
const entries = new Map();
|
|
1168
1245
|
if (!hasHead(projectRoot) || !paths.length) return entries;
|
|
@@ -1717,6 +1794,18 @@ export async function splitFlow(
|
|
|
1717
1794
|
task: async (stream) => {
|
|
1718
1795
|
if (large) {
|
|
1719
1796
|
let plan;
|
|
1797
|
+
let planReasoning = null;
|
|
1798
|
+
const planningStream = stream
|
|
1799
|
+
? {
|
|
1800
|
+
onReasoningDelta(chunk) {
|
|
1801
|
+
planReasoning = (planReasoning || '') + chunk;
|
|
1802
|
+
stream.onReasoningDelta(chunk);
|
|
1803
|
+
},
|
|
1804
|
+
onReasoningComplete(text) {
|
|
1805
|
+
if (text) planReasoning = text;
|
|
1806
|
+
},
|
|
1807
|
+
}
|
|
1808
|
+
: null;
|
|
1720
1809
|
try {
|
|
1721
1810
|
analysis = await analyzeChanges(
|
|
1722
1811
|
planningConfig,
|
|
@@ -1729,7 +1818,12 @@ export async function splitFlow(
|
|
|
1729
1818
|
),
|
|
1730
1819
|
persistentAnalysisCache,
|
|
1731
1820
|
);
|
|
1732
|
-
plan = await planAnalyzedChanges(
|
|
1821
|
+
plan = await planAnalyzedChanges(
|
|
1822
|
+
planningConfig,
|
|
1823
|
+
analysis.facts,
|
|
1824
|
+
analysis.coverage,
|
|
1825
|
+
planningStream,
|
|
1826
|
+
);
|
|
1733
1827
|
} catch (err) {
|
|
1734
1828
|
const exhausted =
|
|
1735
1829
|
err.data?.analysis?.exhausted ||
|
|
@@ -1759,6 +1853,7 @@ export async function splitFlow(
|
|
|
1759
1853
|
fallbackReason: exhausted || 'planning_capacity',
|
|
1760
1854
|
};
|
|
1761
1855
|
plan = normalizePlan([], allFiles, config.language, config.commitPolicy);
|
|
1856
|
+
planReasoning = null;
|
|
1762
1857
|
const warning = `Large-change planning used one conservative all-files commit after ${exhausted || 'planning capacity'} exhaustion; review the fallback message and grouping.`;
|
|
1763
1858
|
warnings.push(warning);
|
|
1764
1859
|
console.error(` Fallback: ${warning}`);
|
|
@@ -1781,7 +1876,7 @@ export async function splitFlow(
|
|
|
1781
1876
|
raw: JSON.stringify(plan),
|
|
1782
1877
|
elapsed: planningConfig.analysisBudget.snapshot().elapsedMs,
|
|
1783
1878
|
usage: planningConfig.analysisBudget.snapshot().usage,
|
|
1784
|
-
reasoning:
|
|
1879
|
+
reasoning: planReasoning,
|
|
1785
1880
|
};
|
|
1786
1881
|
}
|
|
1787
1882
|
return generateSplitPlan(
|
|
@@ -1876,7 +1971,7 @@ export async function splitFlow(
|
|
|
1876
1971
|
{ name: 'Cancel', value: 'cancel', description: 'Abort without committing' },
|
|
1877
1972
|
],
|
|
1878
1973
|
},
|
|
1879
|
-
reasoningEnabled
|
|
1974
|
+
reasoningEnabled || reasoningText
|
|
1880
1975
|
? {
|
|
1881
1976
|
text: reasoningText,
|
|
1882
1977
|
maxChars: config.reasoning.maxDisplayChars,
|
|
@@ -2013,6 +2108,8 @@ export async function splitFlow(
|
|
|
2013
2108
|
return {
|
|
2014
2109
|
plan: groups,
|
|
2015
2110
|
planFile: writtenPlanPath,
|
|
2111
|
+
scope,
|
|
2112
|
+
changeCount: allFiles.length,
|
|
2016
2113
|
provider,
|
|
2017
2114
|
model: config.modelId,
|
|
2018
2115
|
latencyMs: elapsed,
|
|
@@ -2054,12 +2151,13 @@ export async function splitFlow(
|
|
|
2054
2151
|
if (ok) {
|
|
2055
2152
|
console.log('\n ' + chalk.green.bold(`✓ Done! Created ${groups.length} commits.\n`));
|
|
2056
2153
|
} else {
|
|
2057
|
-
|
|
2058
|
-
reported: true,
|
|
2059
|
-
});
|
|
2154
|
+
splitCommitFailure(projectRoot, 'One or more split commits failed.');
|
|
2060
2155
|
}
|
|
2061
2156
|
return {
|
|
2062
2157
|
plan: groups,
|
|
2158
|
+
commitSha: currentHead(projectRoot),
|
|
2159
|
+
scope,
|
|
2160
|
+
changeCount: allFiles.length,
|
|
2063
2161
|
provider,
|
|
2064
2162
|
model: config.modelId,
|
|
2065
2163
|
latencyMs: elapsed,
|
|
@@ -2168,9 +2266,13 @@ export async function resumeSplit(projectRoot, { yes = false, machineOutput = fa
|
|
|
2168
2266
|
);
|
|
2169
2267
|
return {
|
|
2170
2268
|
plan: checkpoint.plan.groups,
|
|
2269
|
+
commitSha: currentHead(projectRoot),
|
|
2270
|
+
scope: checkpoint.plan.scope,
|
|
2271
|
+
changeCount: checkpoint.plan.changes.length,
|
|
2171
2272
|
warnings: [],
|
|
2172
2273
|
exitReason: 'success',
|
|
2173
|
-
committed:
|
|
2274
|
+
committed: false,
|
|
2275
|
+
commitState: 'complete',
|
|
2174
2276
|
edited: false,
|
|
2175
2277
|
rewrites: 0,
|
|
2176
2278
|
};
|
|
@@ -2225,9 +2327,7 @@ export async function resumeSplit(projectRoot, { yes = false, machineOutput = fa
|
|
|
2225
2327
|
{ transaction: { path: transaction.path, checkpoint } },
|
|
2226
2328
|
);
|
|
2227
2329
|
if (!ok) {
|
|
2228
|
-
|
|
2229
|
-
reported: true,
|
|
2230
|
-
});
|
|
2330
|
+
splitCommitFailure(projectRoot, 'One or more resumed split commits failed.');
|
|
2231
2331
|
}
|
|
2232
2332
|
console.log(
|
|
2233
2333
|
'\n ' +
|
|
@@ -2237,6 +2337,9 @@ export async function resumeSplit(projectRoot, { yes = false, machineOutput = fa
|
|
|
2237
2337
|
);
|
|
2238
2338
|
return {
|
|
2239
2339
|
plan: checkpoint.plan.groups,
|
|
2340
|
+
commitSha: currentHead(projectRoot),
|
|
2341
|
+
scope: checkpoint.plan.scope,
|
|
2342
|
+
changeCount: checkpoint.plan.changes.length,
|
|
2240
2343
|
provider: null,
|
|
2241
2344
|
model: null,
|
|
2242
2345
|
latencyMs: null,
|
|
@@ -2443,13 +2546,15 @@ export async function applySplitPlan(
|
|
|
2443
2546
|
{ planArtifact: artifact },
|
|
2444
2547
|
);
|
|
2445
2548
|
if (!ok) {
|
|
2446
|
-
|
|
2447
|
-
reported: true,
|
|
2448
|
-
});
|
|
2549
|
+
splitCommitFailure(projectRoot, 'One or more split commits failed.');
|
|
2449
2550
|
}
|
|
2450
2551
|
console.log('\n ' + chalk.green.bold(`✓ Done! Created ${artifact.groups.length} commits.\n`));
|
|
2451
2552
|
return {
|
|
2452
2553
|
plan: artifact.groups,
|
|
2554
|
+
planFile: loaded.path,
|
|
2555
|
+
commitSha: readGit(['rev-parse', 'HEAD'], projectRoot).trim(),
|
|
2556
|
+
scope: artifact.scope,
|
|
2557
|
+
changeCount: artifact.changes.length,
|
|
2453
2558
|
provider: null,
|
|
2454
2559
|
model: null,
|
|
2455
2560
|
latencyMs: null,
|