@hifullmoon/aicommit 2.6.5 → 2.6.7
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 +19 -1
- package/README.md +2 -0
- package/README.zh-CN.md +2 -0
- package/docs/distribution.md +1 -1
- package/package.json +1 -1
- package/src/change-analysis.js +81 -4
- package/src/local-analysis.js +50 -6
- package/src/split.js +87 -3
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.7] - 2026-10-05
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
|
|
11
|
+
- Large-change local analysis now selects bounded excerpts across change hunks, prioritizing behavior changes over imports and comments.
|
|
12
|
+
|
|
13
|
+
### Fixed
|
|
14
|
+
|
|
15
|
+
- Local change summaries now fit the complete commit request input budget, including system policy, repository context, and JSON envelopes.
|
|
16
|
+
|
|
17
|
+
## [2.6.6] - 2026-09-21
|
|
18
|
+
|
|
19
|
+
### Fixed
|
|
20
|
+
|
|
21
|
+
- Large split plans now preserve complete file groupings while correcting only invalid commit messages, with bounded retries and character counts. Interactive runs can edit messages after automatic correction fails and continue to plan review; non-interactive failures preserve a private diagnostic draft for protected input.
|
|
22
|
+
|
|
7
23
|
## [2.6.5] - 2026-09-21
|
|
8
24
|
|
|
9
25
|
### Fixed
|
|
@@ -267,7 +283,9 @@ This file lists notable user-facing changes. Internal refactors, test-only chang
|
|
|
267
283
|
- Added file-level split planning and execution with Git-state concurrency checks.
|
|
268
284
|
- Added provider presets and user/project configuration boundaries.
|
|
269
285
|
|
|
270
|
-
[Unreleased]: https://github.com/hi-fullmoon/AICommit/compare/v2.6.
|
|
286
|
+
[Unreleased]: https://github.com/hi-fullmoon/AICommit/compare/v2.6.7...HEAD
|
|
287
|
+
[2.6.7]: https://github.com/hi-fullmoon/AICommit/releases/tag/v2.6.7
|
|
288
|
+
[2.6.6]: https://github.com/hi-fullmoon/AICommit/releases/tag/v2.6.6
|
|
271
289
|
[2.6.5]: https://github.com/hi-fullmoon/AICommit/releases/tag/v2.6.5
|
|
272
290
|
[2.6.4]: https://github.com/hi-fullmoon/AICommit/releases/tag/v2.6.4
|
|
273
291
|
[2.6.3]: https://github.com/hi-fullmoon/AICommit/releases/tag/v2.6.3
|
package/README.md
CHANGED
|
@@ -516,4 +516,6 @@ For exhaustive chunk-by-chunk model analysis, opt in through personal configurat
|
|
|
516
516
|
|
|
517
517
|
`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`.
|
|
518
518
|
|
|
519
|
+
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`.
|
|
520
|
+
|
|
519
521
|
Complete patches and larger untracked text are captured in local temporary files, with descriptors opened only during reads and writes. Files are cleaned on normal exit or cancellation; crashes may leave them behind. Content reads are bounded. Lines exceeding 1 MiB and experimental hunk planning for large changes fail explicitly; file-level planning capacity uses the complete conservative fallback.
|
package/README.zh-CN.md
CHANGED
|
@@ -518,4 +518,6 @@ exec zsh
|
|
|
518
518
|
|
|
519
519
|
`deep` 会增加请求和 token 消耗,最多 256 次请求。请求前会预估初始分块、必要归并和最小分层规划树的成本;确定无法装入总预算时直接切换到本地清单路径,已验证的缓存命中不计入这次预估。通过完整校验的初始事实分块会短期写入 Git 元数据目录;同一快照失败或中断后重试时直接复用,完整生成成功后清理。缓存不直接保存捕获的 diff、推理、凭据或完整 Provider 响应,只保存可能包含代码派生细节的模型摘要;选择发送未保护的原始内容时默认不落盘,只有个人配置显式设置 `allowUnprotected: true` 才允许。仓库配置不能切换策略、启用未保护缓存或提高费用与缓存上限。两种策略均使用保守 token 估算,未知 usage 按预留额度计入;缓存命中不计请求或 token。模型的不完整输出永远不会被提交:预算或容量降级会重新生成一个包含全部已审核文件的完整计划;交互模式先展示确认,非交互提交则要求 `--allow-single-fallback`。
|
|
520
520
|
|
|
521
|
+
大变更拆分先完成全部文件分组,再修正不合规的提交信息。自动修正最多三轮,只发送出错的消息、实际长度和校验原因,不重发 diff,也不改动文件归属或已合规的消息。修正与原规划共用请求、时间和 token 预算;每轮 JSON 恢复可能额外请求一次。自动修正失败后,交互模式允许编辑出错的提交信息,校验通过后返回正常计划确认流程。非交互模式停止提交,不会将已有分组合并成单次提交;对于已保护的输入,会将完整分组和消息保存到私有临时诊断文件,该文件不能直接用于 `split apply`。
|
|
522
|
+
|
|
521
523
|
完整补丁及较大未跟踪文本暂存到本地临时文件,只在读写时打开文件句柄,正常结束或取消时清理;异常崩溃可能遗留临时文件。正文读取有界。单行超过 1 MiB 和大变更的实验性 hunk 规划会明确报错;文件级规划容量不足时使用覆盖完整变更的保守回退。
|
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.6.
|
|
36
|
+
npm install --package-lock-only @hifullmoon/aicommit@2.6.7
|
|
37
37
|
npm audit signatures
|
|
38
38
|
```
|
|
39
39
|
|
package/package.json
CHANGED
package/src/change-analysis.js
CHANGED
|
@@ -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, validateCommitCandidate } from './policy.js';
|
|
7
|
+
import { normalizeCommitPolicy, parseCommitMessage, validateCommitCandidate } from './policy.js';
|
|
8
8
|
import { getProviderAdapter } from './providers.js';
|
|
9
9
|
import { createHash } from 'node:crypto';
|
|
10
10
|
import {
|
|
@@ -377,6 +377,77 @@ export function validatePlanMessages(groups, policy) {
|
|
|
377
377
|
);
|
|
378
378
|
}
|
|
379
379
|
|
|
380
|
+
export function invalidPlanMessages(groups, policy) {
|
|
381
|
+
return groups.flatMap((group, index) => {
|
|
382
|
+
try {
|
|
383
|
+
validatePlanMessages([group], policy);
|
|
384
|
+
return [];
|
|
385
|
+
} catch (error) {
|
|
386
|
+
const message = parseCommitMessage(typeof group.subject === 'string' ? group.subject : '');
|
|
387
|
+
return [
|
|
388
|
+
{
|
|
389
|
+
id: String(index),
|
|
390
|
+
subject: group.subject,
|
|
391
|
+
body: group.body,
|
|
392
|
+
subjectLength: [...(message.parsed?.subject || '')].length,
|
|
393
|
+
subjectMaxLength: policy.subject.maxLength,
|
|
394
|
+
errors: error.message,
|
|
395
|
+
},
|
|
396
|
+
];
|
|
397
|
+
}
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
export function applyMessageCorrections(groups, corrections, policy) {
|
|
402
|
+
const invalid = invalidPlanMessages(groups, policy);
|
|
403
|
+
const remaining = new Set(invalid.map((item) => item.id));
|
|
404
|
+
if (!Array.isArray(corrections)) throw new Error('Expected a message correction array.');
|
|
405
|
+
const next = groups.map((group) => ({ ...group }));
|
|
406
|
+
for (const correction of corrections) {
|
|
407
|
+
if (!correction || !remaining.delete(correction.id))
|
|
408
|
+
throw new Error('Corrections must contain each invalid group ID exactly once.');
|
|
409
|
+
validatePlanMessages([correction], policy);
|
|
410
|
+
// Never accept model-provided file assignments, summaries, or group ordering.
|
|
411
|
+
next[Number(correction.id)] = {
|
|
412
|
+
...next[Number(correction.id)],
|
|
413
|
+
subject: correction.subject,
|
|
414
|
+
body: correction.body,
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
if (remaining.size) throw new Error('Corrections omitted invalid group IDs.');
|
|
418
|
+
return next;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
async function repairPlanMessages(config, groups, policy, stream) {
|
|
422
|
+
for (let attempt = 1; attempt <= 3; attempt++) {
|
|
423
|
+
const invalid = invalidPlanMessages(groups, policy);
|
|
424
|
+
if (!invalid.length) return groups;
|
|
425
|
+
stream?.onProgress?.(`Correcting ${invalid.length} commit messages (${attempt}/3) ...`);
|
|
426
|
+
try {
|
|
427
|
+
const corrections = await jsonCall(
|
|
428
|
+
config,
|
|
429
|
+
`Repair only the supplied commit messages. Policy: ${JSON.stringify(policy)}. ` +
|
|
430
|
+
`The description after the type/scope prefix must fit within ${policy.subject.maxLength} Unicode characters. ` +
|
|
431
|
+
(policy.subject.headerMaxLength
|
|
432
|
+
? `The complete header must fit within ${policy.subject.headerMaxLength} characters. `
|
|
433
|
+
: '') +
|
|
434
|
+
'Rewrite concisely; do not truncate. Move detail into the body only when permitted. ' +
|
|
435
|
+
'Return [{"id":"original ID","subject":"complete commit header","body":"optional body"}]. ' +
|
|
436
|
+
'Preserve every supplied ID exactly once. Do not plan or regroup files.',
|
|
437
|
+
invalid,
|
|
438
|
+
(candidate) => applyMessageCorrections(groups, candidate, policy),
|
|
439
|
+
stream,
|
|
440
|
+
);
|
|
441
|
+
return applyMessageCorrections(groups, corrections, policy);
|
|
442
|
+
} catch (error) {
|
|
443
|
+
if (error.category !== ERROR_CATEGORIES.RESPONSE_FORMAT || attempt === 3) {
|
|
444
|
+
error.data = { ...error.data, messageRepairDraft: groups };
|
|
445
|
+
throw error;
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
380
451
|
export function validatePartition(groups, ids, requireSummary = true) {
|
|
381
452
|
const remaining = new Set(ids);
|
|
382
453
|
if (!Array.isArray(groups) || !groups.length)
|
|
@@ -807,7 +878,6 @@ export async function planAnalyzedChanges(config, facts, coverage = null, stream
|
|
|
807
878
|
batch,
|
|
808
879
|
(candidate) => {
|
|
809
880
|
validatePartition(candidate, ids, false);
|
|
810
|
-
validatePlanMessages(candidate, policy);
|
|
811
881
|
},
|
|
812
882
|
batchStream,
|
|
813
883
|
);
|
|
@@ -834,8 +904,15 @@ export async function planAnalyzedChanges(config, facts, coverage = null, stream
|
|
|
834
904
|
});
|
|
835
905
|
}
|
|
836
906
|
}
|
|
837
|
-
if (batches.length === 1)
|
|
838
|
-
|
|
907
|
+
if (batches.length === 1) {
|
|
908
|
+
const plan = next.map(({ subject, body, files }) => ({ subject, body, files }));
|
|
909
|
+
try {
|
|
910
|
+
return await repairPlanMessages(config, plan, policy, stream);
|
|
911
|
+
} catch (error) {
|
|
912
|
+
error.data = { ...error.data, messageRepairDraft: plan, messageRepairComplete: true };
|
|
913
|
+
throw error;
|
|
914
|
+
}
|
|
915
|
+
}
|
|
839
916
|
candidates = next;
|
|
840
917
|
}
|
|
841
918
|
throw fail(ERROR_CATEGORIES.PROVIDER, 'Split planning exceeded the maximum summary depth.', {
|
package/src/local-analysis.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
2
|
import { isLockFile, matchStripPattern } from './git.js';
|
|
3
3
|
import { ERROR_CATEGORIES, fail } from './errors.js';
|
|
4
|
+
import { buildCommitMessages } from './api.js';
|
|
5
|
+
import { estimateTokens } from './analysis-budget.js';
|
|
4
6
|
|
|
5
7
|
// These are evidence-selection rules, not exclusions from Git or secret scans.
|
|
6
8
|
export function isGeneratedFile(path) {
|
|
@@ -117,6 +119,16 @@ export function localOverview(config, facts) {
|
|
|
117
119
|
samples: [],
|
|
118
120
|
};
|
|
119
121
|
const cap = localInputBytes(config);
|
|
122
|
+
const fits = () => {
|
|
123
|
+
const text = JSON.stringify(data);
|
|
124
|
+
return (
|
|
125
|
+
Buffer.byteLength(text) <= cap &&
|
|
126
|
+
estimateTokens(JSON.stringify(buildCommitMessages(config, text).messages)) <=
|
|
127
|
+
(config.analysisBudget?.limits.chunkInputTokens ||
|
|
128
|
+
config.largeChange?.chunkInputTokens ||
|
|
129
|
+
12000)
|
|
130
|
+
);
|
|
131
|
+
};
|
|
120
132
|
let sampledFiles = 0;
|
|
121
133
|
const seen = new Set();
|
|
122
134
|
const ordered = ranked(facts);
|
|
@@ -145,17 +157,18 @@ export function localOverview(config, facts) {
|
|
|
145
157
|
if (data.samples.length >= 16) break;
|
|
146
158
|
const sample = summaryOf(fact);
|
|
147
159
|
data.samples.push(sample);
|
|
148
|
-
|
|
160
|
+
data.omittedGroups--;
|
|
161
|
+
if (!fits()) {
|
|
149
162
|
data.samples.pop();
|
|
163
|
+
data.omittedGroups++;
|
|
150
164
|
continue;
|
|
151
165
|
}
|
|
152
166
|
if (fact.evidence) sampledFiles++;
|
|
153
|
-
data.omittedGroups--;
|
|
154
167
|
}
|
|
155
168
|
// A tiny configured budget may not even fit aggregate metadata. Fail before
|
|
156
169
|
// sending rather than silently slicing JSON or dropping the uncertainty notice.
|
|
157
170
|
const text = JSON.stringify(data);
|
|
158
|
-
if (
|
|
171
|
+
if (!fits())
|
|
159
172
|
throw fail(
|
|
160
173
|
ERROR_CATEGORIES.CONFIG,
|
|
161
174
|
'Local change inventory does not fit the input budget. Increase largeChange.chunkInputTokens or maxDiffChars.',
|
|
@@ -172,30 +185,61 @@ export function analyzeLocally(config, capture, protect, previews) {
|
|
|
172
185
|
hash: createHash('sha256'),
|
|
173
186
|
evidence: '',
|
|
174
187
|
textual: false,
|
|
188
|
+
excerpts: [],
|
|
189
|
+
excerpt: '',
|
|
190
|
+
score: 0,
|
|
175
191
|
},
|
|
176
192
|
]),
|
|
177
193
|
);
|
|
194
|
+
const finishExcerpt = (record) => {
|
|
195
|
+
if (!record.excerpt) return;
|
|
196
|
+
record.excerpts.push({ text: record.excerpt, score: record.score });
|
|
197
|
+
record.excerpts.sort((a, b) => b.score - a.score);
|
|
198
|
+
record.excerpts.length = Math.min(record.excerpts.length, 8);
|
|
199
|
+
record.excerpt = '';
|
|
200
|
+
record.score = 0;
|
|
201
|
+
};
|
|
178
202
|
for (const unit of capture.units(protect, previews)) {
|
|
179
203
|
if (unit.metadataOnly) continue;
|
|
180
204
|
const record = records.get(unit.fileId);
|
|
181
205
|
// Tracked fragments carry Git headers; untracked fragments are plain text.
|
|
182
206
|
const section = unit.id.match(/S\d+(?=P\d+$)/)?.[0];
|
|
183
207
|
if (section && section !== record.section) {
|
|
208
|
+
finishExcerpt(record);
|
|
184
209
|
record.section = section;
|
|
185
210
|
record.inHunk = false;
|
|
186
211
|
}
|
|
187
212
|
for (const line of unit.text.split(/(?<=\n)/)) {
|
|
188
213
|
if (section) {
|
|
189
|
-
if (line.startsWith('@@'))
|
|
214
|
+
if (line.startsWith('@@')) {
|
|
215
|
+
finishExcerpt(record);
|
|
216
|
+
record.inHunk = true;
|
|
217
|
+
}
|
|
190
218
|
if (!record.inHunk || !/^[+-]/.test(line)) continue;
|
|
191
219
|
}
|
|
192
220
|
record.hash.update(line);
|
|
193
221
|
record.textual = true;
|
|
194
|
-
|
|
195
|
-
|
|
222
|
+
// Keep bounded candidates across hunks instead of spending the entire
|
|
223
|
+
// evidence allowance on imports at the beginning of a file.
|
|
224
|
+
const remaining = 450 - Buffer.byteLength(record.excerpt);
|
|
225
|
+
if (remaining > 3) {
|
|
226
|
+
record.excerpt += clip(line, remaining);
|
|
227
|
+
record.score = Math.max(
|
|
228
|
+
record.score,
|
|
229
|
+
/^[+-]\s*(?:import\b|export\s+\{|\/\/|\*|$)/.test(line) ? 1 : 4,
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
if (Buffer.byteLength(record.excerpt) >= 447) finishExcerpt(record);
|
|
196
233
|
}
|
|
197
234
|
}
|
|
198
235
|
const grouped = new Map();
|
|
236
|
+
for (const record of records.values()) {
|
|
237
|
+
finishExcerpt(record);
|
|
238
|
+
record.evidence = record.excerpts
|
|
239
|
+
.slice(0, 3)
|
|
240
|
+
.map((excerpt) => clip(excerpt.text, 300))
|
|
241
|
+
.join('');
|
|
242
|
+
}
|
|
199
243
|
for (const { file, hash, evidence, textual } of records.values()) {
|
|
200
244
|
const kind = category(file, config);
|
|
201
245
|
const module = moduleOf(file.addPaths?.at(-1) || file.path);
|
package/src/split.js
CHANGED
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
readlinkSync,
|
|
9
9
|
realpathSync,
|
|
10
10
|
rmSync,
|
|
11
|
+
writeFileSync,
|
|
11
12
|
constants as fsConstants,
|
|
12
13
|
} from 'node:fs';
|
|
13
14
|
import { createHash } from 'node:crypto';
|
|
@@ -71,6 +72,8 @@ import {
|
|
|
71
72
|
analysisConfig,
|
|
72
73
|
analyzeChanges,
|
|
73
74
|
planAnalyzedChanges,
|
|
75
|
+
invalidPlanMessages,
|
|
76
|
+
applyMessageCorrections,
|
|
74
77
|
summarizeChanges,
|
|
75
78
|
} from './change-analysis.js';
|
|
76
79
|
import { updateGitHash, spoolGit } from './git-spool.js';
|
|
@@ -675,6 +678,35 @@ async function editPlan(groups, allFiles, language, commitPolicy) {
|
|
|
675
678
|
}
|
|
676
679
|
}
|
|
677
680
|
|
|
681
|
+
async function editInvalidPlanMessages(groups, policy) {
|
|
682
|
+
let draft = JSON.stringify(invalidPlanMessages(groups, policy), null, 2);
|
|
683
|
+
while (true) {
|
|
684
|
+
const action = await vimSelect({
|
|
685
|
+
message: 'File grouping is ready. Edit the invalid commit messages to continue?',
|
|
686
|
+
choices: [
|
|
687
|
+
{ name: 'Edit commit messages', value: 'edit' },
|
|
688
|
+
{ name: 'Cancel', value: 'cancel' },
|
|
689
|
+
],
|
|
690
|
+
});
|
|
691
|
+
if (action === 'cancel') return null;
|
|
692
|
+
const edited = await editor({
|
|
693
|
+
message: 'Fix subject/body fields; keep each id unchanged. Save and close to validate.',
|
|
694
|
+
default: draft,
|
|
695
|
+
postfix: '.json',
|
|
696
|
+
waitForUseInput: false,
|
|
697
|
+
});
|
|
698
|
+
try {
|
|
699
|
+
const candidate = JSON.parse(edited);
|
|
700
|
+
const corrected = applyMessageCorrections(groups, candidate, policy);
|
|
701
|
+
return corrected;
|
|
702
|
+
} catch (error) {
|
|
703
|
+
// Keep the user's edits available for the next attempt, including invalid JSON.
|
|
704
|
+
console.error(` ${sanitizeTerminalText(error.message)}`);
|
|
705
|
+
draft = edited;
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
|
|
678
710
|
// Common binary formats — sending their bytes to the model as utf-8 garbage
|
|
679
711
|
// helps no one, so previews are skipped for these extensions.
|
|
680
712
|
const BINARY_FILE_RE =
|
|
@@ -1880,6 +1912,9 @@ export async function splitFlow(
|
|
|
1880
1912
|
);
|
|
1881
1913
|
}
|
|
1882
1914
|
} catch (err) {
|
|
1915
|
+
// A validated partition must not become an all-files fallback just
|
|
1916
|
+
// because repairing its messages exhausted the remaining budget.
|
|
1917
|
+
if (err.data?.messageRepairDraft) throw err;
|
|
1883
1918
|
const exhausted =
|
|
1884
1919
|
err.data?.analysis?.exhausted ||
|
|
1885
1920
|
(!planningConfig.analysisBudget.remainingMs() ? 'time' : null);
|
|
@@ -1950,9 +1985,58 @@ export async function splitFlow(
|
|
|
1950
1985
|
},
|
|
1951
1986
|
}));
|
|
1952
1987
|
} catch (err) {
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1988
|
+
if (
|
|
1989
|
+
err.data?.messageRepairComplete &&
|
|
1990
|
+
!yes &&
|
|
1991
|
+
!machineOutput &&
|
|
1992
|
+
process.stdin.isTTY &&
|
|
1993
|
+
process.stdout.isTTY
|
|
1994
|
+
) {
|
|
1995
|
+
console.error(` ${indentError(err)}`);
|
|
1996
|
+
const corrected = await editInvalidPlanMessages(
|
|
1997
|
+
err.data.messageRepairDraft,
|
|
1998
|
+
normalizeCommitPolicy(config.commitPolicy, config.language),
|
|
1999
|
+
);
|
|
2000
|
+
if (!corrected) return finishCancelled();
|
|
2001
|
+
raw = JSON.stringify(corrected);
|
|
2002
|
+
elapsed = planningConfig.analysisBudget.snapshot().elapsedMs;
|
|
2003
|
+
usage = planningConfig.analysisBudget.snapshot().usage;
|
|
2004
|
+
reasoningText = null;
|
|
2005
|
+
warnings.push('Commit messages were corrected manually; the file grouping was preserved.');
|
|
2006
|
+
}
|
|
2007
|
+
if (!raw && err.data?.messageRepairDraft && protectModelInput) {
|
|
2008
|
+
try {
|
|
2009
|
+
const directory = mkdtempSync(join(tmpdir(), 'aicommit-message-repair-'));
|
|
2010
|
+
const path = join(directory, 'draft.json');
|
|
2011
|
+
writeFileSync(
|
|
2012
|
+
path,
|
|
2013
|
+
JSON.stringify(
|
|
2014
|
+
{
|
|
2015
|
+
kind: 'aicommit-message-repair-draft',
|
|
2016
|
+
notice:
|
|
2017
|
+
'Complete file grouping with invalid messages; not an apply-ready split plan.',
|
|
2018
|
+
groups: err.data.messageRepairDraft,
|
|
2019
|
+
},
|
|
2020
|
+
null,
|
|
2021
|
+
2,
|
|
2022
|
+
) + '\n',
|
|
2023
|
+
{ mode: 0o600, flag: 'wx' },
|
|
2024
|
+
);
|
|
2025
|
+
err.data.messageRepairDraftPath = path;
|
|
2026
|
+
console.error(
|
|
2027
|
+
` Message repair draft saved to ${path} (complete grouping, not apply-ready).`,
|
|
2028
|
+
);
|
|
2029
|
+
} catch (writeError) {
|
|
2030
|
+
console.error(
|
|
2031
|
+
` Could not save message repair draft: ${sanitizeTerminalText(writeError.message)}`,
|
|
2032
|
+
);
|
|
2033
|
+
}
|
|
2034
|
+
}
|
|
2035
|
+
if (!raw) {
|
|
2036
|
+
console.log(`\n ${indentError(err)}\n`);
|
|
2037
|
+
err.reported = true;
|
|
2038
|
+
throw err;
|
|
2039
|
+
}
|
|
1956
2040
|
}
|
|
1957
2041
|
|
|
1958
2042
|
let groups;
|