@gitkraken/compose-tools 0.8.0 → 0.8.1
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 +16 -0
- package/README.md +6 -4
- package/dist/ai/types.d.ts +46 -1
- package/dist/git/apply-target.js +1 -1
- package/dist/git/commit-chain.d.ts +14 -5
- package/dist/git/commit-chain.js +7 -7
- package/dist/git/diff-collector.d.ts +26 -12
- package/dist/git/diff-collector.js +4 -4
- package/dist/git/index.js +1 -1
- package/dist/git/safety.js +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/tasks/ai-loop.d.ts +10 -1
- package/dist/tasks/ai-loop.js +1 -1
- package/dist/tasks/compose-group/compose-group.js +1 -1
- package/dist/tasks/compose-group/types.d.ts +3 -11
- package/dist/tasks/group/group.js +1 -1
- package/dist/tasks/group/types.d.ts +6 -1
- package/dist/tasks/group/validator.d.ts +10 -2
- package/dist/tasks/group/validator.js +5 -5
- package/dist/tasks/refine/refine.js +1 -1
- package/dist/tasks/refine/types.d.ts +6 -1
- package/dist/tasks/refine/validator.js +1 -1
- package/dist/workflows/compose.js +3 -3
- package/dist/workflows/refine.js +1 -1
- package/dist/workflows/types.d.ts +15 -4
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,15 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/) and this
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.8.1] - 2026-08-05
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
|
|
13
|
+
- `commit-range` + `includeWorkdir` now **preserves** a workdir layer it was told not to compose, instead of rejecting the input. A layer excluded via `includeStaged`/`includeUnstaged` is captured as a patch immediately before the apply's worktree reset and re-applied to the worktree afterward — so `{ includeStaged: true, includeUnstaged: false }` composes the staged changes together with the range and leaves the unstaged work sitting in the worktree, which is what those flags read as meaning. This reverses the `ComposeWorkflowInputError` raised for that combination: the rejection was right about the danger (an excluded layer is absent from the collected input, therefore absent from `leftoverPatch`, and unprotected by a safety stash — none is taken when `includeWorkdir` is set — so the reset destroyed it) but resolved it by refusing the operation rather than by protecting the layer. Excluded content survives; its position does not — an excluded staged layer comes back as unstaged changes. Untracked files are unaffected, as before: the reset leaves them alone. Layer exclusion stays deliberately distinct from `hunkFilter` and partial apply, which drop specific changes the caller singled out of the composed input; an excluded layer was never part of that input at all.
|
|
14
|
+
- One `includeWorkdir` combination is still rejected, now narrowly: `includeStaged: false` together with `includeUnstaged: true` while staged changes exist. Unstaged changes are diffed against the index and therefore already presuppose the staged content, so once the two touch the same lines there is no coherent way to replay the excluded staged layer over a result composed from the unstaged one. `ComposeWorkflowInputError` names the fix (include the layer, or commit/stash it). Every other combination now composes.
|
|
15
|
+
- Drift in an excluded layer no longer fails the pre-apply safety check. `verifySnapshot` previously reported `Excluded ... changes appeared since the AI analysis` when a change appeared in an excluded layer between collection and apply; the capture now happens immediately before the reset, so whatever the layer holds at that moment is what gets restored and a late change no longer needs to block the apply.
|
|
16
|
+
- Documented that **every `includeWorkdir` layer defaults to `true`**, in both the `ComposeSource` type and the README. `{ includeStaged: true }` reads like an allowlist but composes the entire working directory, unstaged and untracked changes included; keeping a layer out requires naming it with `false`.
|
|
17
|
+
|
|
9
18
|
## [0.8.0] - 2026-08-03
|
|
10
19
|
|
|
11
20
|
### Added
|
|
@@ -13,6 +22,13 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/) and this
|
|
|
13
22
|
- Adds `stashNotRestored` on `ComposeResult`, `ApplyComposePlanResult`, and the apply-layer results — reported when the post-apply safety-stash restore was skipped or failed without conflicts (reasons: `stash-ref-not-found`, `untracked-restore-failed`, `pop-failed`). Previously a git-aborted pop and a missing stash ref were indistinguishable from a clean restore (or misreported as a conflict with no files). The stash entry is now left in place in these cases instead of being dropped, so the WIP it holds stays recoverable. Type export: `StashNotRestored`.
|
|
14
23
|
- Adds optional `diffFilter` and `noRenames` fields to `DiffTreeOptions` (mapping to `git diff`'s `--diff-filter` and `--no-renames`), so a consumer-supplied `diffTree` port can honor both. Used internally to compare a stash's untracked payload against the working tree without rename detection folding a missing file into a rename.
|
|
15
24
|
|
|
25
|
+
### Changed
|
|
26
|
+
|
|
27
|
+
- A hunk index claimed by more than one commit is now repaired deterministically instead of costing a retry. The response validator for `group` / `composeGroup` / `refinePlan` keeps the index in one commit and drops the other claims, then accepts the response. The winner is the locked claimant when one exists, otherwise the first claim in the model's **response** order — not necessarily first in the final chain, since the `order` phase and `restoreLockedOrdering` can both move it later. Dropping a later claim removes a wrong extra assignment rather than guessing intent — the hunk is still assigned somewhere. Previously the whole prompt was re-sent, which on a large diff meant re-transmitting every hunk to correct one repeated number; over four eval runs duplicate hunks accounted for 12 of 29 outright `VALIDATION_EXHAUSTED` failures. Repairs are reported, never silent: via `repairs` on the `onAfterModelCall` info object, on `GroupOutput`, on `RefineOutput`, on `RefinePlanResult.phases[]`, and on the matching `PhaseDetail` in `ComposeResult.phases`. Type exports: `ValidationRepair`, `DuplicateHunksRepair`, `RepairedHunkClaim`, `RepairScope`. "Reported, never silent" describes this repair, not validator corrections generally: refine's locked-content substitution and `restoreLockedOrdering` remain silent, and one of them rewrites a commit message the user will see. Two cases deliberately fall back to a retry instead of being repaired: claims spanning **two branches in branches mode** (branch chains are built in parallel from a common base there, so splitting adjacent hunks across them risks a merge conflict raised as a raw exec error; in commits mode the branches are never built as parallel chains, so the same contest is repaired like any other), and commits whose every hunk was a duplicate claim (repairing those would silently discard a commit the model intended, and a zero-hunk commit throws `Empty patch for commit` at apply time). Responses that are wrong in any other way still fail exactly as before. Repairing genuinely missing hunks is deliberately not covered, and neither is `quickCompose`, whose structurally identical duplicate check still rejects outright.
|
|
28
|
+
- **Commit messages are not reconciled with a repair.** A commit that loses a hunk keeps prose describing it, and the winner gains a hunk its message never mentions. The index math does not guess intent, but the resulting message can be wrong in the user's permanent history.
|
|
29
|
+
- `onAfterModelCall` now fires after the response validator runs rather than immediately after `model.generate` returns, so it can carry that call's `repairs`. `repairs` is populated only when the response was accepted. Its `durationMs` still measures only the model call, and the hook still fires when the validator throws. An exception thrown by the hook is now swallowed: an observability callback must not be able to abort a compose or mask the validator's own error.
|
|
30
|
+
- `ComposeGroupOutput` is now an alias of `GroupOutput` rather than a hand-written structural clone that had already drifted (it was missing `hunksCondensedCount`, and would have missed `repairs`).
|
|
31
|
+
|
|
16
32
|
### Fixed
|
|
17
33
|
|
|
18
34
|
- Composing with a `hunkFilter` that drops a non-trailing hunk no longer fails validation (or worse, tempts the model into renumbering hunks). The filter contract preserves the surviving hunks' original `index` values, but the prompts and all response validators assumed indices form a dense `0..N-1` range derived from the hunk count — so a correct response referencing the real indices was rejected, retried until `VALIDATION_EXHAUSTED`, and the retry feedback actively steered the model toward renumbering, which would have validated cleanly and resolved to the wrong hunks. Validation (analyze, group / compose-group, quick-compose, refine) now checks against the actual set of surviving hunk indices, prompts instruct the model to use the `index` values exactly as shown, and retry feedback lists the real indices. The fresh `compose()` apply path also now resolves plan indices against the filtered hunk set (matching `applyComposePlan`), so a plan referencing a filter-dropped index fails loudly instead of committing excluded content. Internal `refine` task input changed from `hunkCount: number` to `hunkIndices: ReadonlySet<number>` (not part of the public API).
|
package/README.md
CHANGED
|
@@ -183,7 +183,7 @@ See [Undo](#undo) below.
|
|
|
183
183
|
|---|---|
|
|
184
184
|
| `{ type: 'workdir', stagedOnly?, noUntracked? }` | Collects staged, unstaged, and untracked changes. `stagedOnly: true` limits to the index; `noUntracked: true` skips untracked files. |
|
|
185
185
|
| `{ type: 'branch', name, mergeTarget? }` | Collects `mergeBase(mergeTarget, name) → name` as one combined diff. `mergeTarget` defaults to `'main'`. |
|
|
186
|
-
| `{ type: 'commit-range', branch, from, to, includeWorkdir? }` | Collects a contiguous range on `branch` as a single combined diff. Both `from` and `to` are **inclusive** — they name the first and last commits being rewritten. For a single-commit recompose: `from = sha, to = sha`. Per-hunk authorship is attributed via a line-range-overlap heuristic against per-commit history. Pass `includeWorkdir: { includeStaged?, includeUnstaged?, includeUntracked? }` to combine the range with the user's working changes in one composed input. When set, `to` must equal the current branch tip
|
|
186
|
+
| `{ type: 'commit-range', branch, from, to, includeWorkdir? }` | Collects a contiguous range on `branch` as a single combined diff. Both `from` and `to` are **inclusive** — they name the first and last commits being rewritten. For a single-commit recompose: `from = sha, to = sha`. Per-hunk authorship is attributed via a line-range-overlap heuristic against per-commit history. Pass `includeWorkdir: { includeStaged?, includeUnstaged?, includeUntracked? }` to combine the range with the user's working changes in one composed input; **each layer defaults to `true`**, so `{ includeStaged: true }` composes the whole working directory — name a layer with `false` to keep it out. When set, `to` must equal the current branch tip and HEAD must be non-detached. A layer excluded via `includeStaged`/`includeUnstaged` is preserved rather than composed: its content is captured before the apply resets the worktree and re-applied afterward as unstaged changes. Excluding staged while composing unstaged is rejected when staged changes exist — unstaged changes are diffed against the index, so the excluded staged layer has nowhere coherent to be replayed. |
|
|
187
187
|
|
|
188
188
|
## Targets
|
|
189
189
|
|
|
@@ -259,8 +259,10 @@ onBeforeModelCall: ({ phase, attempt, maxAttempts, callId }) => {
|
|
|
259
259
|
// callId: stable UUID — correlates with onAfterModelCall and with result.feedbackTargetCallId
|
|
260
260
|
},
|
|
261
261
|
|
|
262
|
-
onAfterModelCall: ({ phase, attempt, callId, success, durationMs, usage, error, promptVersion }) => {
|
|
263
|
-
// Fires on both success and error
|
|
262
|
+
onAfterModelCall: ({ phase, attempt, callId, success, durationMs, usage, error, promptVersion, repairs }) => {
|
|
263
|
+
// Fires on both success and error, after the response validator runs (durationMs still
|
|
264
|
+
// measures only the model call). `repairs` lists deterministic fixes applied to an accepted
|
|
265
|
+
// response — see ValidationRepair. An exception thrown here is swallowed.
|
|
264
266
|
},
|
|
265
267
|
```
|
|
266
268
|
|
|
@@ -451,7 +453,7 @@ Every top-level entry point returns an object with these common fields:
|
|
|
451
453
|
| `snapshot` | all paths | Safety hash fingerprint of the collection — carry it to apply time |
|
|
452
454
|
| `source` | all paths | `{ type, hunks, commitMessages?, branchName?, baseBranch? }` — the hunks the AI operated on |
|
|
453
455
|
| `session` | all paths | Pass to a subsequent call to resume |
|
|
454
|
-
| `phases` | all paths | `PhaseDetail[]` with `{ name, durationMs, attempts, usage?, modelId?, promptVersion?, callIds? }` per phase |
|
|
456
|
+
| `phases` | all paths | `PhaseDetail[]` with `{ name, durationMs, attempts, usage?, modelId?, promptVersion?, callIds?, repairs? }` per phase |
|
|
455
457
|
| `usage` | all paths | Aggregated `{ inputTokens, outputTokens, ... }` across all AI calls |
|
|
456
458
|
| `diffStats` | sugar / plan | `{ fileCount, hunkCount, addedLines, removedLines }` |
|
|
457
459
|
| `feedbackTargetCallId` | when AI ran | callId of the last validated model response — stamp this on thumbs up/down feedback events |
|
package/dist/ai/types.d.ts
CHANGED
|
@@ -1,4 +1,44 @@
|
|
|
1
1
|
//#region src/ai/types.d.ts
|
|
2
|
+
/** Which commits contested a hunk index, and which one kept it. */
|
|
3
|
+
interface RepairedHunkClaim {
|
|
4
|
+
hunkIndex: number;
|
|
5
|
+
/** Commit that kept the hunk. `branchId/commitId` in branches mode, bare commit id in commits mode. */
|
|
6
|
+
keptIn: string;
|
|
7
|
+
/** Commits the claim was dropped from, in the order they appeared in the response. */
|
|
8
|
+
droppedFrom: string[];
|
|
9
|
+
/** Where THIS claim's contest lived; sibling claims in one repair can differ. */
|
|
10
|
+
scope: RepairScope;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Where a contested claim came from. Nothing emits `across-branches` today: in branches mode those
|
|
14
|
+
* contests are rejected rather than repaired (no claim to put a scope on), and in commits mode a
|
|
15
|
+
* multi-branch response's contests are `across-commits`. The member exists so a future repair
|
|
16
|
+
* covering the branches-mode case does not have to widen the union.
|
|
17
|
+
*/
|
|
18
|
+
type RepairScope = 'within-commit' | 'across-commits' | 'across-branches';
|
|
19
|
+
/**
|
|
20
|
+
* A hunk index claimed more than once was kept in a single commit and dropped from the others.
|
|
21
|
+
* The winner is the locked claimant when one exists, otherwise the first claim in the model's
|
|
22
|
+
* response order — which is not necessarily first in the final chain, since the `order` phase and
|
|
23
|
+
* `restoreLockedOrdering` can both place it later.
|
|
24
|
+
*/
|
|
25
|
+
interface DuplicateHunksRepair {
|
|
26
|
+
kind: 'duplicate-hunks';
|
|
27
|
+
/** The hunk indices whose extra claims were dropped, ascending. */
|
|
28
|
+
hunkIndices: number[];
|
|
29
|
+
claims: RepairedHunkClaim[];
|
|
30
|
+
/** Unstable log string. Read `claims` for anything programmatic; this shape is free to change. */
|
|
31
|
+
detail: string;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* A deterministic fix a response validator applied to an AI response that would otherwise have
|
|
35
|
+
* been rejected, sparing a retry. A repaired response is treated as valid — repairs are reported
|
|
36
|
+
* for observability, not as a failure signal.
|
|
37
|
+
*
|
|
38
|
+
* This covers duplicate-hunk repair only. It is not an exhaustive record of every correction a
|
|
39
|
+
* validator makes: refine's locked-content substitution and `restoreLockedOrdering` stay silent.
|
|
40
|
+
*/
|
|
41
|
+
type ValidationRepair = DuplicateHunksRepair;
|
|
2
42
|
interface TokenUsage {
|
|
3
43
|
inputTokens: number;
|
|
4
44
|
outputTokens: number;
|
|
@@ -20,6 +60,11 @@ interface PhaseDetail {
|
|
|
20
60
|
promptVersion?: string;
|
|
21
61
|
/** Stable call IDs for each model.generate invocation in this phase (one per attempt). */
|
|
22
62
|
callIds?: string[];
|
|
63
|
+
/**
|
|
64
|
+
* Deterministic repairs applied to this phase's accepted response instead of spending a retry.
|
|
65
|
+
* Undefined when the response needed no repair.
|
|
66
|
+
*/
|
|
67
|
+
repairs?: ValidationRepair[];
|
|
23
68
|
}
|
|
24
69
|
interface ProgressEvent {
|
|
25
70
|
type: 'attempt_start' | 'validation_failed';
|
|
@@ -28,4 +73,4 @@ interface ProgressEvent {
|
|
|
28
73
|
errors?: string[];
|
|
29
74
|
}
|
|
30
75
|
//#endregion
|
|
31
|
-
export { PhaseDetail, ProgressEvent, TokenUsage, addUsage, emptyUsage };
|
|
76
|
+
export { DuplicateHunksRepair, PhaseDetail, ProgressEvent, RepairScope, RepairedHunkClaim, TokenUsage, ValidationRepair, addUsage, emptyUsage };
|
package/dist/git/apply-target.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{GitError as e}from"../errors/git.js";import{applyPatchToWorkdir as t,checkout as n,cleanWorkdir as r,currentBranch as i,deleteBranch as a,discardWorkdirChanges as o,reset as s,revParse as c,revParseVerify as l,updateRef as u}from"./port-dispatch.js";import{applyBranchStructure as d}from"./apply-branches.js";import{
|
|
1
|
+
import{GitError as e}from"../errors/git.js";import{applyPatchToWorkdir as t,checkout as n,cleanWorkdir as r,currentBranch as i,deleteBranch as a,discardWorkdirChanges as o,reset as s,revParse as c,revParseVerify as l,updateRef as u}from"./port-dispatch.js";import{applyBranchStructure as d}from"./apply-branches.js";import{resolveWorkdirLayers as f}from"./diff-collector.js";import{reparentDescendants as p}from"./reparent.js";import{applyCommitChain as m,applyRangeRewrite as h,buildCommitChain as g,moveRefsAtSha as _}from"./commit-chain.js";import{verifyMultiBranchDiff as v,verifyResultingDiff as y}from"./safety.js";async function b(t){let{git:n,source:r,target:i,branchPlans:a,allOrderedCommits:o,hunks:s,snapshot:l,collection:u,signing:p,authorAttribution:_,stashLabel:v,leftoverPatch:b}=t,C=r.type===`commit-range`&&r.includeWorkdir!=null?f(r.includeWorkdir):void 0;if(i.type===`rewrite-range`){if(!u.branchName||!u.branchSha)throw new e(`OPERATION_FAILED`,`rewrite-range target requires a source that records branchName and branchSha (branch or commit-range).`);let t;try{t=await c(n,`${i.rangeFrom}^`)}catch{throw new e(`OPERATION_FAILED`,`rewrite-range target's rangeFrom (${i.rangeFrom.slice(0,8)}) has no parent (root commit) — rewriting the root commit is not currently supported.`)}let{headSha:r,shaMap:a}=await g({git:n,baseSha:t,commits:o,hunks:s,signing:p,authorAttribution:_}),d=await y(n,t,r,l);if(!d.valid)throw new e(`SAFETY_CHECK_FAILED`,`Diff verification failed: ${d.error}`);let f=await h({git:n,branch:i.branch,rangeFrom:i.rangeFrom,rangeTo:i.rangeTo,newHeadSha:r,originalBranchSha:u.branchSha,stashLabel:v,leftoverPatch:b,includeWorkdir:C});return{commitShas:Object.fromEntries(a),mutations:D(f.refMovements),stashConflict:f.stashConflict,stashNotRestored:f.stashNotRestored}}if(u.branchName&&!u.mergeBaseSha)throw new e(`OPERATION_FAILED`,`Branch source is missing mergeBaseSha — cannot determine base commit`);let w=u.mergeBaseSha??await c(n,`HEAD`),E=k(r,i);if(i.type===`auto`&&a.length>1)return x(n,w,a,s,l,u,E,p,_);if(i.type===`auto`&&a.length===1&&a[0].partitioning)return S(n,w,a[0],s,l,u,E,p,_,v,C);let{headSha:j,shaMap:M}=await g({git:n,baseSha:w,commits:o,hunks:s,signing:p,authorAttribution:_}),N=Object.fromEntries(M),P=await y(n,w,j,l);if(!P.valid)throw new e(`SAFETY_CHECK_FAILED`,`Diff verification failed: ${P.error}`);if(i.type===`branch`)return A({git:n,source:r,target:i,newHeadSha:j,collection:u,stashLabel:v,leftoverPatch:b,commitShas:N});if(i.type===`auto`){let e=a[0]?.branchGroup.name||u.branchName;if(e){let t=!E&&u.branchName&&u.branchSha?{originalBranchName:u.branchName,originalBranchSha:u.branchSha}:{},r={branchName:e,title:a[0]?.branchGroup.title??``,description:a[0]?.branchGroup.description??``,commitIds:o.map(e=>e.id)};await d({git:n,branches:[{partitions:[r],tipSha:j}],...t});let i=O([r],N,t.originalBranchName,t.originalBranchSha);return{partitions:[r],commitShas:N,mutations:i}}let t=await m({git:n,newHeadSha:j,stashLabel:v,includeWorkdir:C});return{commitShas:N,mutations:T(),stashConflict:t.stashConflict,stashNotRestored:t.stashNotRestored}}let F=await m({git:n,newHeadSha:j,stashLabel:v,leftoverPatch:b,includeWorkdir:C});return{commitShas:N,mutations:T(),stashConflict:F.stashConflict,stashNotRestored:F.stashNotRestored}}async function x(t,n,i,a,s,c,l,u,f){let p=[],m=[],h=[],_=[],y={};for(let e of i){let r=C(e),{headSha:i,shaMap:o}=await g({git:t,baseSha:n,commits:r,hunks:a,signing:u,authorAttribution:f});h.push(i);for(let[e,t]of o)y[e]=t;if(e.partitioning&&e.partitioning.partitions.length>1){let t=w(e.partitioning.partitions,o);p.push(...t),m.push(t),_.push({partitions:t,tipSha:i})}else{let t={branchName:e.branchGroup.name,title:e.branchGroup.title,description:e.branchGroup.description,commitIds:r.map(e=>o.get(e.id)??e.id)};p.push(t),m.push([t]),_.push({partitions:[t],tipSha:i})}}let b=await v(t,n,h,s);if(!b.valid)throw new e(`SAFETY_CHECK_FAILED`,`Diff verification failed: ${b.error}`);let x=!l&&c.branchName&&c.branchSha?{originalBranchName:c.branchName,originalBranchSha:c.branchSha}:{};return await d({git:t,branches:_,...x}),!c.branchName&&!l&&(await o(t),c.untrackedIncluded&&await r(t)),{partitions:p,partitionGroups:m,commitShas:y,mutations:O(p,y,x.originalBranchName,x.originalBranchSha)}}async function S(t,n,i,a,s,c,l,u,f,p,h){let{headSha:_,shaMap:v}=await g({git:t,baseSha:n,commits:C(i),hunks:a,signing:u,authorAttribution:f}),b=await y(t,n,_,s);if(!b.valid)throw new e(`SAFETY_CHECK_FAILED`,`Diff verification failed: ${b.error}`);let x=w(i.partitioning.partitions,v),S=Object.fromEntries(v);if(x.length>1){let e=!l&&c.branchName&&c.branchSha?{originalBranchName:c.branchName,originalBranchSha:c.branchSha}:{};return await d({git:t,branches:[{partitions:x,tipSha:_}],...e}),!c.branchName&&!l&&(await o(t),c.untrackedIncluded&&await r(t,{force:!0,directories:!0})),{partitions:x,commitShas:S,mutations:O(x,S,e.originalBranchName,e.originalBranchSha)}}let E=await m({git:t,newHeadSha:_,stashLabel:p,includeWorkdir:h});return{partitions:x,commitShas:S,mutations:T(),stashConflict:E.stashConflict,stashNotRestored:E.stashNotRestored}}function C(t){return t.orderedCommitIds.map(n=>{let r=t.branchGroup.commits.find(e=>e.id===n);if(!r)throw new e(`OPERATION_FAILED`,`Commit ${n} not found in branch '${t.branchGroup.id}'`);return r})}function w(t,n){return t.map(t=>({...t,commitIds:t.commitIds.map(t=>{let r=n.get(t);if(!r)throw new e(`OPERATION_FAILED`,`No SHA found for commit ID '${t}'`);return r})}))}function T(){return{createdRefs:[],deletedRefs:[],movedRefs:[],composedBranchCount:0}}function E(t){if(t.startsWith(`refs/heads/`))return{kind:`branch`,name:t.slice(11)};if(t.startsWith(`refs/tags/`))return{kind:`tag`,name:t.slice(10)};throw new e(`OPERATION_FAILED`,`Unexpected refname '${t}' in apply mutations — only refs/heads/* and refs/tags/* are tracked.`)}function D(e){let t=[];for(let n of e){let{kind:e,name:r}=E(n.refname);t.push({kind:e,name:r,previousSha:n.previousSha,newSha:n.newSha})}return{createdRefs:[],deletedRefs:[],movedRefs:t,composedBranchCount:1}}function O(e,t,n,r){let i=e.map(e=>{let n=e.commitIds[e.commitIds.length-1];return{kind:`branch`,name:e.branchName,sha:t[n]??n}}),a=[];return n&&r&&a.push({kind:`branch`,name:n,sha:r}),{createdRefs:i,deletedRefs:a,movedRefs:[],composedBranchCount:i.length}}function k(e,t){return t.type!==`branch`&&t.type!==`auto`||e.type===`commit-range`?!0:t.preserveOriginal===!0}async function A(r){let{git:o,source:d,target:f,newHeadSha:m,collection:h,leftoverPatch:g,commitShas:v}=r,y=k(d,f),b=d.type===`branch`&&!y&&h.branchName!=null&&h.branchSha!=null;if(b&&f.name===h.branchName)throw new e(`OPERATION_FAILED`,`target 'branch' with name '${f.name}' conflicts with the source branch (preserveOriginal=false). Use target 'rewrite-range' to rewrite the branch in place under its existing name.`);let x=await l(o,`refs/heads/${f.name}`),S=x?{createdRefs:[],deletedRefs:[],movedRefs:[{kind:`branch`,name:f.name,previousSha:x,newSha:m}],composedBranchCount:1}:{createdRefs:[{kind:`branch`,name:f.name,sha:m}],deletedRefs:[],movedRefs:[],composedBranchCount:1},C=await i(o),w=await c(o,`HEAD`);if(await u(o,`refs/heads/${f.name}`,m),b){let e=h.branchSha,r=h.branchName,i=await p({git:o,oldToSha:e,newTopSha:m,sourceBranchName:void 0});for(let e of i.reparentedRefs){let{kind:t,name:n}=E(e.refname);S.movedRefs.push({kind:t,name:n,previousSha:e.oldSha,newSha:e.newSha})}let c=await _(o,e,m,`refs/heads/${r}`);for(let e of c){let{kind:t,name:n}=E(e.refname);S.movedRefs.push({kind:t,name:n,previousSha:e.previousSha,newSha:e.newSha})}C===r&&(await n(o,f.name),await s(o,m,`hard`),g&&await t(o,g)),await a(o,r,{force:!0}),S.deletedRefs.push({kind:`branch`,name:r,sha:e})}else d.type===`workdir`&&!y&&(await s(o,w,`hard`),g&&await t(o,g));return{commitShas:v,mutations:S}}export{b as applyToTarget};
|
|
@@ -21,6 +21,12 @@ interface BuildCommitChainResult {
|
|
|
21
21
|
shaMap: Map<string, string>;
|
|
22
22
|
}
|
|
23
23
|
declare function buildCommitChain(params: BuildCommitChainParams): Promise<BuildCommitChainResult>;
|
|
24
|
+
/** Resolved `ComposeSource.includeWorkdir` layer flags — every layer decided, no defaulting left. */
|
|
25
|
+
type WorkdirLayers = {
|
|
26
|
+
includeStaged: boolean;
|
|
27
|
+
includeUnstaged: boolean;
|
|
28
|
+
includeUntracked: boolean;
|
|
29
|
+
};
|
|
24
30
|
interface ApplyCommitChainParams {
|
|
25
31
|
git: ComposeGitPort;
|
|
26
32
|
newHeadSha: string;
|
|
@@ -32,6 +38,8 @@ interface ApplyCommitChainParams {
|
|
|
32
38
|
* unstaged WIP, so the user's unselected content isn't lost when a destructive apply rewrites
|
|
33
39
|
* the source. */
|
|
34
40
|
leftoverPatch?: string;
|
|
41
|
+
/** See {@link ApplyRangeRewriteParams.includeWorkdir} — same contract on this apply path. */
|
|
42
|
+
includeWorkdir?: WorkdirLayers;
|
|
35
43
|
}
|
|
36
44
|
interface ApplyChainResult {
|
|
37
45
|
stashConflict?: StashConflict;
|
|
@@ -52,10 +60,11 @@ interface ApplyRangeRewriteParams {
|
|
|
52
60
|
originalBranchSha: string;
|
|
53
61
|
stashLabel?: string;
|
|
54
62
|
leftoverPatch?: string;
|
|
55
|
-
/**
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
|
|
63
|
+
/** Resolved `includeWorkdir` layer flags when the source folded WIP into the input. When set, no
|
|
64
|
+
* safety stash is taken — the WIP is part of the source, and leftover hunks are materialized via
|
|
65
|
+
* {@link leftoverPatch} — and any layer the caller excluded is captured before the worktree reset
|
|
66
|
+
* and re-applied after it. When undefined, a safety stash protects unrelated dirty WIP. */
|
|
67
|
+
includeWorkdir?: WorkdirLayers;
|
|
59
68
|
}
|
|
60
69
|
interface ApplyRangeRewriteResult {
|
|
61
70
|
/** Final source-branch tip SHA after descendant re-parenting and any ref-at-tip moves. */
|
|
@@ -94,4 +103,4 @@ declare function moveRefsAtSha(git: ComposeGitPort, oldSha: string, newSha: stri
|
|
|
94
103
|
newSha: string;
|
|
95
104
|
}[]>;
|
|
96
105
|
//#endregion
|
|
97
|
-
export { ApplyChainResult, ApplyCommitChainParams, ApplyRangeRewriteParams, ApplyRangeRewriteResult, AuthorAttributionStrategy, BuildCommitChainParams, BuildCommitChainResult, applyCommitChain, applyRangeRewrite, buildCommitChain, moveRefsAtSha };
|
|
106
|
+
export { ApplyChainResult, ApplyCommitChainParams, ApplyRangeRewriteParams, ApplyRangeRewriteResult, AuthorAttributionStrategy, BuildCommitChainParams, BuildCommitChainResult, WorkdirLayers, applyCommitChain, applyRangeRewrite, buildCommitChain, moveRefsAtSha };
|
package/dist/git/commit-chain.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import{GitError as e}from"../errors/git.js";import{applyPatchToIndex as t,applyPatchToWorkdir as n,checkout as r,commitTree as i,currentBranch as a,diffTree as o,localeNeutralEnv as s,readTree as c,reset as l,revParse as u,revParseVerify as d,stageAll as f,stashDrop as p,stashPop as m,stashPush as h,statusPorcelain as g,updateRef as _,writeTree as v}from"./port-dispatch.js";import{listRefsWithPeeledSha as y}from"./utils/refs.js";import{
|
|
2
|
-
`)[0]}': ${r}`,{cause:t})}for(let e of a)e.originalFileName&&e.diffHeader.includes(`rename from`)&&m.add(e.diffHeader);let g=await v(r,{indexEnv:h}),_=
|
|
3
|
-
`).some(e=>e.startsWith(`?? `));await h(e,{message:t,includeUntracked:!0});try{let t=(await u(e,`stash@{0}`)).trim();return t.length===0?null:(r||=await
|
|
4
|
-
`)){let n=e.indexOf(` `);if(!(n<0)&&e.slice(0,n)===t){let t=e.slice(n+1).trim();return t.length>0?t:null}}return null}async function
|
|
5
|
-
`))/^(UU|AA|DD|AU|UA|UD|DU) /.test(e)&&n.push(e.slice(3).trim());return n}catch{return[]}}function
|
|
6
|
-
`);return`${e.trimEnd()}\n\n${n}\n`}function
|
|
1
|
+
import{GitError as e}from"../errors/git.js";import{applyPatchToIndex as t,applyPatchToWorkdir as n,checkout as r,commitTree as i,currentBranch as a,diffTree as o,localeNeutralEnv as s,readTree as c,reset as l,revParse as u,revParseVerify as d,stageAll as f,stashDrop as p,stashPop as m,stashPush as h,statusPorcelain as g,updateRef as _,writeTree as v}from"./port-dispatch.js";import{listRefsWithPeeledSha as y}from"./utils/refs.js";import{captureExcludedLayersPatch as b}from"./diff-collector.js";import{reparentDescendants as x}from"./reparent.js";import{buildPatchForCommit as S}from"./utils/patch-builder.js";import{randomUUID as C}from"node:crypto";import*as w from"node:fs";import*as T from"node:os";import*as E from"node:path";async function D(n){let{git:r,baseSha:a,commits:o,hunks:s,signing:l,authorAttribution:u=`plurality`}=n,d=E.join(T.tmpdir(),`compose-idx-${C()}`),f=a,p=new Map,m=new Set;try{for(let n of o){let a=n.hunkIndices.map(t=>{let n=s.find(e=>e.index===t);if(!n)throw new e(`OPERATION_FAILED`,`Hunk index ${t} not found`);return n}),o=S(a.map(e=>m.has(e.diffHeader)?{...e,diffHeader:q(e.diffHeader,e.fileName)}:e));if(!o.trim())throw new e(`OPERATION_FAILED`,`Empty patch for commit: ${n.message}`);let h={GIT_INDEX_FILE:d};await c(r,f,{indexEnv:h});try{await t(r,o,{indexEnv:h})}catch(t){let r=t instanceof Error?t.message:String(t);throw new e(`OPERATION_FAILED`,`git apply failed for commit '${n.message.split(`
|
|
2
|
+
`)[0]}': ${r}`,{cause:t})}for(let e of a)e.originalFileName&&e.diffHeader.includes(`rename from`)&&m.add(e.diffHeader);let g=await v(r,{indexEnv:h}),_=U(a,u),y=G(n.message,_.coAuthors),b=await i(r,g,[f],y,{signing:l,author:_.author});p.set(n.id,b),f=b}return{headSha:f,shaMap:p}}finally{K(d)}}async function O(e,t,r){t&&await n(e,t),r&&await n(e,r)}async function k(e){let{git:t,newHeadSha:n,branchName:i,stashLabel:a,leftoverPatch:o,includeWorkdir:s}=e,c=a??M,d=s==null?await N(t,c):null,f=s==null?void 0:await b(t,await u(t,`HEAD`),s);try{i?await r(t,i,{createBranch:!0,startPoint:n}):await l(t,n,`hard`),await O(t,o,f)}catch(e){throw d!=null&&await L(t,d),e}if(d!=null){let e=await L(t,d);if(e.kind===`conflicted`)return{stashConflict:{stashLabel:c,conflictedFiles:e.conflictedFiles}};if(e.kind===`not-restored`)return{stashNotRestored:{stashLabel:c,stashSha:d.sha,reason:e.reason}}}return{}}async function A(e){let{git:t,branch:n,rangeTo:i,newHeadSha:o,originalBranchSha:s,stashLabel:c,leftoverPatch:d,includeWorkdir:f}=e,p=await a(t)===n,m=c??M,h=p&&f==null?await N(t,m):null,g=p&&f!=null?await b(t,await u(t,`HEAD`),f):void 0,v,y,S=o,C=[];try{let e=await x({git:t,oldToSha:i,newTopSha:o,sourceBranchName:n});for(let t of e.reparentedRefs)C.push({refname:t.refname,previousSha:t.oldSha,newSha:t.newSha});e.sourceBranchNewSha==null?(await _(t,`refs/heads/${n}`,o),C.push({refname:`refs/heads/${n}`,previousSha:s,newSha:o}),S=o):S=e.sourceBranchNewSha;let a=await j(t,i,o,`refs/heads/${n}`);C.push(...a),p&&(await r(t,n),await l(t,S,`hard`),await O(t,d,g))}catch(e){throw h!=null&&await L(t,h),e}if(h!=null){let e=await L(t,h);e.kind===`conflicted`?v={stashLabel:m,conflictedFiles:e.conflictedFiles}:e.kind===`not-restored`&&(y={stashLabel:m,stashSha:h.sha,reason:e.reason})}return{finalTipSha:S,refMovements:C,stashConflict:v,stashNotRestored:y}}async function j(e,t,n,r){let i=[];for(let{refname:a,sha:o}of await y(e,{pointsAt:t}))a!==r&&(!a.startsWith(`refs/heads/`)&&!a.startsWith(`refs/tags/`)||o===t&&(await _(e,a,n),i.push({refname:a,previousSha:t,newSha:n})));return i}const M=`compose backup`;async function N(e,t){let n=await g(e);if(!n.trim())return null;let r=n.split(`
|
|
3
|
+
`).some(e=>e.startsWith(`?? `));await h(e,{message:t,includeUntracked:!0});try{let t=(await u(e,`stash@{0}`)).trim();return t.length===0?null:(r||=await F(e,t),{sha:t,hasUntracked:r})}catch{return null}}const P=new Set([`4b825dc642cb6eb9a060e54bf8d69288fbee4904`,`6ef19b41225c5369f1c104d45d8d85efa9b057b53b14b4b9b939dd74decc5321`]);async function F(e,t){try{let n=await d(e,`${t}^3^{tree}`);return n==null?!0:!P.has(n)}catch{return!0}}async function I(e,t){if(!e.exec)return null;let n;try{n=await e.exec([`stash`,`list`,`--format=%H %gd`],{env:s})}catch{return null}for(let e of n.split(`
|
|
4
|
+
`)){let n=e.indexOf(` `);if(!(n<0)&&e.slice(0,n)===t){let t=e.slice(n+1).trim();return t.length>0?t:null}}return null}async function L(e,t){if(!t.hasUntracked)try{let n=(await u(e,`${t.sha}^{tree}`)).trim(),r=await z(e);if(n.length>0&&n===r){let n=await I(e,t.sha);if(n!=null)try{await p(e,n)}catch{}return{kind:`restored`}}}catch{}let n=await I(e,t.sha);if(n==null)return{kind:`not-restored`,reason:`stash-ref-not-found`};let r;try{return await m(e,{withIndex:!0,ref:n}),{kind:`restored`}}catch(e){r=e}let i=await H(e);if(i.length>0)return{kind:`conflicted`,conflictedFiles:i};if(B(r)){if(await R(e,t.sha)){let n=await I(e,t.sha);if(n!=null)try{await p(e,n)}catch{}return{kind:`restored`}}return{kind:`not-restored`,reason:`untracked-restore-failed`}}return{kind:`not-restored`,reason:`pop-failed`}}async function R(e,t){try{let n=await d(e,`${t}^3^{tree}`);return n==null?!1:(await o(e,n,await z(e),{nameOnly:!0,binary:!1,diffFilter:`DMT`,noRenames:!0})).trim().length===0}catch{return!1}}async function z(e){let t=E.join(T.tmpdir(),`compose-equiv-${C()}`);try{let n={GIT_INDEX_FILE:t};return await c(e,`HEAD`,{indexEnv:n}),await f(e,{indexEnv:n}),(await v(e,{indexEnv:n})).trim()}finally{K(t)}}function B(e){let t=V(e);return t.includes(`already exists, no checkout`)||t.includes(`could not restore untracked files from stash`)}function V(e){if(e==null)return``;if(e instanceof Error){let t=e.stderr;return t!=null&&t.length>0?`${e.message}\n${t}`:e.message}return String(e)}async function H(e){try{let t=await g(e),n=[];for(let e of t.split(`
|
|
5
|
+
`))/^(UU|AA|DD|AU|UA|UD|DU) /.test(e)&&n.push(e.slice(3).trim());return n}catch{return[]}}function U(e,t){let n=e.filter(e=>e.author!=null);if(n.length===0)return{author:void 0,coAuthors:[]};let r;if(typeof t==`function`)r=t(e);else if(t===`first-hunk`)r=n[0].author;else{let e=new Map;for(let t of n){let n=W(t.author),r=e.get(n);r?r.count++:e.set(n,{identity:t.author,count:1})}let t=0;for(let{identity:n,count:i}of e.values())i>t&&(r=n,t=i)}let i=new Map;for(let t of e){t.author&&i.set(W(t.author),t.author);for(let e of t.coAuthors??[])i.set(W(e),e)}let a=r?W(r):null,o=[];for(let[e,t]of i)e!==a&&o.push(t);return{author:r,coAuthors:o}}function W(e){return`${e.name.toLowerCase()}\0${e.email.toLowerCase()}`}function G(e,t){if(t.length===0)return e;let n=t.map(e=>`Co-authored-by: ${e.name} <${e.email}>`).join(`
|
|
6
|
+
`);return`${e.trimEnd()}\n\n${n}\n`}function K(e){try{w.unlinkSync(e)}catch{}}function q(e,t){let n=e.split(`
|
|
7
7
|
`),r=[];for(let e of n)e.startsWith(`diff --git `)?r.push(`diff --git a/${t} b/${t}`):e.startsWith(`similarity index`)||e.startsWith(`rename from`)||e.startsWith(`rename to`)||(e.startsWith(`--- a/`)?r.push(`--- a/${t}`):r.push(e));return r.join(`
|
|
8
|
-
`)}export{
|
|
8
|
+
`)}export{k as applyCommitChain,A as applyRangeRewrite,D as buildCommitChain,j as moveRefsAtSha};
|
|
@@ -87,10 +87,10 @@ declare function collectCombinedDiffRange(git: ComposeGitPort, parentOfFromSha:
|
|
|
87
87
|
* snapshot hashes that same combined diff — the bytes the AI sees match the bytes the
|
|
88
88
|
* apply phase verifies against.
|
|
89
89
|
*
|
|
90
|
-
* Requires `to` to equal both the branch tip and HEAD, HEAD to be non-detached
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
*
|
|
90
|
+
* Requires `to` to equal both the branch tip and HEAD, and HEAD to be non-detached — mixing WIP
|
|
91
|
+
* with a range only makes sense when the user is actively on the branch tip being recomposed.
|
|
92
|
+
* A layer the caller excluded stays out of the collected input entirely; preserving its content
|
|
93
|
+
* across the apply's worktree reset is the apply path's job, via {@link captureExcludedLayersPatch}.
|
|
94
94
|
*/
|
|
95
95
|
declare function collectCommitRangeWithWorkdir(git: ComposeGitPort, branchName: string, from: string, to: string, options: {
|
|
96
96
|
includeStaged: boolean;
|
|
@@ -110,17 +110,31 @@ declare function collectWorkdirPortionForRange(git: ComposeGitPort, headSha: str
|
|
|
110
110
|
}): Promise<{
|
|
111
111
|
finalTreeSha: string;
|
|
112
112
|
}>;
|
|
113
|
+
/** Resolve `ComposeSource.includeWorkdir`'s optional per-layer flags, each of which defaults to `true`. */
|
|
114
|
+
declare function resolveWorkdirLayers(includeWorkdir: {
|
|
115
|
+
includeStaged?: boolean;
|
|
116
|
+
includeUnstaged?: boolean;
|
|
117
|
+
includeUntracked?: boolean;
|
|
118
|
+
}): {
|
|
119
|
+
includeStaged: boolean;
|
|
120
|
+
includeUnstaged: boolean;
|
|
121
|
+
includeUntracked: boolean;
|
|
122
|
+
};
|
|
113
123
|
/**
|
|
114
|
-
*
|
|
115
|
-
*
|
|
116
|
-
*
|
|
117
|
-
*
|
|
118
|
-
*
|
|
124
|
+
* Capture the content of the workdir layers the caller excluded from a `commit-range +
|
|
125
|
+
* includeWorkdir` compose, as one patch. Returns `undefined` when both tracked layers are
|
|
126
|
+
* included, or when the excluded ones hold nothing.
|
|
127
|
+
*
|
|
128
|
+
* An excluded layer is deliberately absent from the collected input, so it is also absent from the
|
|
129
|
+
* plan and from the leftover patch — and a range rewrite hard-resets the worktree to the new tip.
|
|
130
|
+
* This patch is the only remaining record of that content, so the apply path takes it before
|
|
131
|
+
* mutating anything and re-applies it after the reset. Untracked files are not included: the reset
|
|
132
|
+
* leaves them alone.
|
|
119
133
|
*/
|
|
120
|
-
declare function
|
|
134
|
+
declare function captureExcludedLayersPatch(git: ComposeGitPort, headSha: string, options: {
|
|
121
135
|
includeStaged: boolean;
|
|
122
136
|
includeUnstaged: boolean;
|
|
123
|
-
}): Promise<
|
|
137
|
+
}): Promise<string | undefined>;
|
|
124
138
|
/**
|
|
125
139
|
* Compute the tree SHA of the worktree's tracked content, working against a temp copy of the
|
|
126
140
|
* real index so the consumer's staged state is never modified.
|
|
@@ -144,4 +158,4 @@ declare function collectSourceHashes(git: ComposeGitPort, options: {
|
|
|
144
158
|
includeUntracked: boolean;
|
|
145
159
|
}): Promise<SafetySnapshot['sourceHashes']>;
|
|
146
160
|
//#endregion
|
|
147
|
-
export { BranchCommitInfo, BranchDiffResult, CollectionResult, SplitCollectionResult, WorkingDiffOptions, WorkingDiffResult, collectBranchDiffs, collectCombinedDiffRange, collectCommitRangeDiffs, collectCommitRangeWithWorkdir, collectCommitsForSplit, collectFromSource, collectSourceHashes, collectWorkdirPortionForRange, collectWorkingDiffs, computeTrackedWorktreeTreeSha,
|
|
161
|
+
export { BranchCommitInfo, BranchDiffResult, CollectionResult, SplitCollectionResult, WorkingDiffOptions, WorkingDiffResult, captureExcludedLayersPatch, collectBranchDiffs, collectCombinedDiffRange, collectCommitRangeDiffs, collectCommitRangeWithWorkdir, collectCommitsForSplit, collectFromSource, collectSourceHashes, collectWorkdirPortionForRange, collectWorkingDiffs, computeTrackedWorktreeTreeSha, resolveWorkdirLayers };
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import{GitError as e}from"../errors/git.js";import{ComposeWorkflowInputError as t}from"../errors/workflow.js";import{applyPatchToIndex as n,createBranch as r,currentBranch as i,diffTree as a,gitDir as o,listUntracked as s,log as c,mergeBase as l,mergeBaseIsAncestor as u,readTree as d,revParse as f,revParseVerify as p,stageAll as m,writeTree as h}from"./port-dispatch.js";import{parseUnifiedDiff as g}from"./utils/hunk-parser.js";import{getAuthorAndCoAuthorsForCombinedDiffHunk as _}from"./utils/authorship.js";import{sha256 as v}from"./utils/crypto.js";import{withUntrackedInTempIndex as y}from"./utils/temp-index.js";import{randomUUID as b}from"node:crypto";import{copyFileSync as x,unlinkSync as S}from"node:fs";import{tmpdir as C}from"node:os";import{join as w}from"node:path";async function T(e,t){if(t.type===`workdir`){let n=!t.stagedOnly&&!t.noUntracked;return{...await E(e,{includeUnstaged:!t.stagedOnly,includeUntracked:n}),untrackedIncluded:n}}if(t.type===`commit-range`){if(t.includeWorkdir){let n=t.includeWorkdir.includeStaged??!0,r=t.includeWorkdir.includeUnstaged??!0,i=t.includeWorkdir.includeUntracked??!0,a=await L(e,t.branch,t.from,t.to,{includeStaged:n,includeUnstaged:r,includeUntracked:i});return{hunks:a.hunks,snapshot:a.snapshot,combinedDiff:a.combinedDiff,commitMessages:a.commits.map(e=>e.message),commitOrder:a.commits.map(e=>e.sha),branchName:t.branch,baseBranch:void 0,branchSha:a.snapshot.branchSha,mergeBaseSha:a.snapshot.baseSha,untrackedIncluded:i}}let n=await M(e,t.branch,t.from,t.to);return{hunks:n.hunks,snapshot:n.snapshot,combinedDiff:n.combinedDiff,commitMessages:n.commits.map(e=>e.message),commitOrder:n.commits.map(e=>e.sha),branchName:t.branch,baseBranch:void 0,branchSha:n.snapshot.branchSha,mergeBaseSha:n.snapshot.baseSha,untrackedIncluded:!1}}let n=t.mergeTarget??`main`,r=await j(e,t.name,n);return{hunks:r.hunks,snapshot:r.snapshot,combinedDiff:r.combinedDiff,commitMessages:r.commits.map(e=>e.message),commitOrder:r.commits.map(e=>e.sha),branchName:t.name,baseBranch:n,branchSha:r.snapshot.branchSha,mergeBaseSha:r.snapshot.baseSha,untrackedIncluded:!1}}async function E(e,t){let n=t.includeUnstaged||t.includeUntracked;return t.includeUntracked?k(e,n):D(e,n)}async function D(e,t){let n=await f(e,`HEAD`),r=await O(e,n,t);return{hunks:g(r),snapshot:
|
|
1
|
+
import{GitError as e}from"../errors/git.js";import{ComposeWorkflowInputError as t}from"../errors/workflow.js";import{applyPatchToIndex as n,createBranch as r,currentBranch as i,diffTree as a,gitDir as o,listUntracked as s,log as c,mergeBase as l,mergeBaseIsAncestor as u,readTree as d,revParse as f,revParseVerify as p,stageAll as m,writeTree as h}from"./port-dispatch.js";import{parseUnifiedDiff as g}from"./utils/hunk-parser.js";import{getAuthorAndCoAuthorsForCombinedDiffHunk as _}from"./utils/authorship.js";import{sha256 as v}from"./utils/crypto.js";import{withUntrackedInTempIndex as y}from"./utils/temp-index.js";import{randomUUID as b}from"node:crypto";import{copyFileSync as x,unlinkSync as S}from"node:fs";import{tmpdir as C}from"node:os";import{join as w}from"node:path";async function T(e,t){if(t.type===`workdir`){let n=!t.stagedOnly&&!t.noUntracked;return{...await E(e,{includeUnstaged:!t.stagedOnly,includeUntracked:n}),untrackedIncluded:n}}if(t.type===`commit-range`){if(t.includeWorkdir){let n=t.includeWorkdir.includeStaged??!0,r=t.includeWorkdir.includeUnstaged??!0,i=t.includeWorkdir.includeUntracked??!0,a=await L(e,t.branch,t.from,t.to,{includeStaged:n,includeUnstaged:r,includeUntracked:i});return{hunks:a.hunks,snapshot:a.snapshot,combinedDiff:a.combinedDiff,commitMessages:a.commits.map(e=>e.message),commitOrder:a.commits.map(e=>e.sha),branchName:t.branch,baseBranch:void 0,branchSha:a.snapshot.branchSha,mergeBaseSha:a.snapshot.baseSha,untrackedIncluded:i}}let n=await M(e,t.branch,t.from,t.to);return{hunks:n.hunks,snapshot:n.snapshot,combinedDiff:n.combinedDiff,commitMessages:n.commits.map(e=>e.message),commitOrder:n.commits.map(e=>e.sha),branchName:t.branch,baseBranch:void 0,branchSha:n.snapshot.branchSha,mergeBaseSha:n.snapshot.baseSha,untrackedIncluded:!1}}let n=t.mergeTarget??`main`,r=await j(e,t.name,n);return{hunks:r.hunks,snapshot:r.snapshot,combinedDiff:r.combinedDiff,commitMessages:r.commits.map(e=>e.message),commitOrder:r.commits.map(e=>e.sha),branchName:t.name,baseBranch:n,branchSha:r.snapshot.branchSha,mergeBaseSha:r.snapshot.baseSha,untrackedIncluded:!1}}async function E(e,t){let n=t.includeUnstaged||t.includeUntracked;return t.includeUntracked?k(e,n):D(e,n)}async function D(e,t){let n=await f(e,`HEAD`),r=await O(e,n,t);return{hunks:g(r),snapshot:W(r,n,void 0,void 0,await G(e,{includeUnstaged:t,includeUntracked:!1})),combinedDiff:r}}async function O(e,t,n){return n?a(e,t,await V(e)):a(e,t,await h(e))}async function k(e,t){return y(e,async n=>{await A(e,n,t);let r=await h(e,{indexEnv:n}),i=await f(e,`HEAD`),o=await a(e,i,r);return{hunks:g(o),snapshot:W(o,i,void 0,void 0,await G(e,{includeUnstaged:t,includeUntracked:!0})),combinedDiff:o}})}async function A(e,t,n){n&&await m(e,{trackedOnly:!0,indexEnv:t}),await m(e,{indexEnv:t})}async function j(e,t,n){let r=await U(e,t);return P(e,await l(e,await U(e,n),r),await f(e,r))}async function M(e,t,n,r){await U(e,t);let i=await f(e,n),a=await f(e,r);return P(e,await N(e,i),a)}async function N(e,n){try{return await f(e,`${n}^`)}catch{throw new t(`Cannot recompose commit ${n.slice(0,8)} — it has no parent (root commit). Recomposing the root commit is not currently supported.`)}}async function P(n,r,i){if(!await u(n,r,i))throw new t(`Cannot collect range ${r.slice(0,8)}..${i.slice(0,8)}: '${r.slice(0,8)}' is not an ancestor of '${i.slice(0,8)}'.`);let o=await a(n,r,i),s=g(o),l=I(await c(n,{format:`%H%n%an%n%ae%n%aI%n%B%n---COMMIT-END---`,range:`${r}..${i}`,reverse:!0}));if(l.length===0)throw new e(`NO_CHANGES`,`No commits in range '${r.slice(0,8)}..${i.slice(0,8)}'.`);let d=await F(n,l);for(let e of s){let{author:t,coAuthors:n}=_(d,e);t!=null&&(e.author=t),n.length>0&&(e.coAuthors=n)}let p=W(o,await f(n,`HEAD`),i,r);return{hunks:s,commits:l.map(e=>({sha:e.sha,message:e.message})),snapshot:p,combinedDiff:o}}async function F(e,t){let n=[];for(let r of t){let t;try{t=await a(e,`${r.sha}^`,r.sha)}catch{continue}let i=g(t,0);for(let e of i)e.author={name:r.authorName,email:r.authorEmail,date:r.authorDate};n.push(...i)}return n}function I(e){let t=[],n=e.split(`---COMMIT-END---`).filter(e=>e.trim());for(let e of n){let n=e.replace(/^\n+/,``).split(`
|
|
2
2
|
`);if(n.length<5)continue;let r=n[0],i=n[1],a=n[2],o=n[3],s=n.slice(4).join(`
|
|
3
|
-
`).trimEnd(),c=o?new Date(o):void 0,l=c&&!Number.isNaN(c.getTime())?c:void 0;t.push({sha:r,authorName:i,authorEmail:a,authorDate:l,message:s})}return t}async function L(e,n,r,o,s){let l=await f(e,await
|
|
3
|
+
`).trimEnd(),c=o?new Date(o):void 0,l=c&&!Number.isNaN(c.getTime())?c:void 0;t.push({sha:r,authorName:i,authorEmail:a,authorDate:l,message:s})}return t}async function L(e,n,r,o,s){let l=await f(e,await U(e,n)),d=await f(e,r),p=await f(e,o),m=await f(e,`HEAD`);if(p!==l)throw new t(`commit-range with includeWorkdir requires 'to' to equal branch tip. Branch '${n}' tip is ${l}; got 'to'=${p}.`);if(m!==p)throw new t(`commit-range with includeWorkdir requires HEAD to equal 'to'. HEAD=${m}, 'to'=${p}.`);if(await i(e)==null)throw new t(`commit-range with includeWorkdir requires HEAD to be on a branch (non-detached). Check out the branch holding the range before composing.`);if(!s.includeStaged&&s.includeUnstaged&&(await a(e,m,await h(e))).trim())throw new t(`Cannot exclude staged changes while composing unstaged changes: unstaged changes are diffed against the index, so the excluded staged layer cannot be replayed over the composed result. Include the staged layer, or commit/stash it before composing.`);let v=await N(e,d),{finalTreeSha:y}=await R(e,m,s),b=await a(e,v,y);if(!await u(e,v,p))throw new t(`Cannot collect range ${v.slice(0,8)}..${p.slice(0,8)}: '${v.slice(0,8)}' is not an ancestor of '${p.slice(0,8)}'.`);let x=g(b),S=I(await c(e,{format:`%H%n%an%n%ae%n%aI%n%B%n---COMMIT-END---`,range:`${v}..${p}`,reverse:!0})),C=await F(e,S);for(let e of x){let{author:t,coAuthors:n}=_(C,e);t!=null&&(e.author=t),n.length>0&&(e.coAuthors=n)}let w=W(b,m,p,v,await G(e,s));return{hunks:x,commits:S.map(e=>({sha:e.sha,message:e.message})),snapshot:w,combinedDiff:b}}async function R(e,t,r){let i=w(C(),`compose-workdir-${b()}`),o={GIT_INDEX_FILE:i};try{if(await d(e,t,{indexEnv:o}),r.includeStaged){let r=await a(e,t,await h(e));r.trim()&&await n(e,r,{indexEnv:o})}if(r.includeUnstaged){let t=await a(e,await h(e),await V(e));t.trim()&&await n(e,t,{indexEnv:o})}if(r.includeUntracked){let t=await s(e);t.length>0&&(await m(e,{intentToAdd:!0,paths:t,indexEnv:o}),await m(e,{paths:t,indexEnv:o}))}return{finalTreeSha:await h(e,{indexEnv:o})}}finally{try{S(i)}catch{}}}function z(e){return{includeStaged:e.includeStaged??!0,includeUnstaged:e.includeUnstaged??!0,includeUntracked:e.includeUntracked??!0}}async function B(e,t,n){if(n.includeStaged&&n.includeUnstaged)return;let r;return r=!n.includeStaged&&!n.includeUnstaged?await a(e,t,await V(e)):n.includeStaged?await a(e,await h(e),await V(e)):await a(e,t,await h(e)),r.trim()?r:void 0}async function V(e){let t=w(C(),`compose-wt-tree-${b()}`),n={GIT_INDEX_FILE:t};try{return x(w(await o(e),`index`),t),await m(e,{trackedOnly:!0,indexEnv:n}),await h(e,{indexEnv:n})}finally{try{S(t)}catch{}}}async function H(e,t,n){let r=await U(e,t),i=await l(e,await U(e,n),r),o=await f(e,r),s=(await c(e,{format:`%H`,range:`${i}..${r}`,reverse:!0})).trim().split(`
|
|
4
4
|
`).filter(Boolean),u=[];for(let t of s){let[n,r,i]=await Promise.all([c(e,{format:`%B`,sha:t}).then(e=>e.trim()),a(e,`${t}^`,t),a(e,`${t}^`,t,{binary:!1,nameOnly:!0})]),o=i.trim().split(`
|
|
5
|
-
`).filter(Boolean);u.push({sha:t,message:n,diff:r,filesChanged:o})}return{commits:u,mergeBaseSha:i,branchSha:o}}async function
|
|
5
|
+
`).filter(Boolean);u.push({sha:t,message:n,diff:r,filesChanged:o})}return{commits:u,mergeBaseSha:i,branchSha:o}}async function U(t,n){if(await p(t,`refs/heads/${n}`)||await p(t,n))return n;if(await p(t,`origin/${n}`))return await r(t,n,`origin/${n}`),n;throw new e(`BRANCH_NOT_FOUND`,`Branch '${n}' not found locally or on origin. Run 'git fetch' if the branch exists on the remote.`)}function W(e,t,n,r,i){return{diffHash:v(e),headSha:t,branchSha:n,baseSha:r,sourceHashes:i}}async function G(e,t){if(!e.exec)return;let n=t.includeStaged??!0,r={};if(n)try{r.staged=v(await e.exec([`diff`,`--cached`,`--binary`,`HEAD`]))}catch{}if(t.includeUnstaged)try{r.unstaged=v(await e.exec([`diff`,`--binary`]))}catch{}if(t.includeUntracked)try{let t=(await e.exec([`ls-files`,`--others`,`--exclude-standard`])).trim().split(`
|
|
6
6
|
`).filter(Boolean);if(t.length>0){let n=[];for(let r of t)try{let t=await e.exec([`hash-object`,`--`,r]);n.push(`${r}:${t.trim()}`)}catch{}r.untracked=v(n.join(`
|
|
7
|
-
`))}else r.untracked=v(``)}catch{}return Object.keys(r).length>0?r:void 0}export{j as collectBranchDiffs,P as collectCombinedDiffRange,M as collectCommitRangeDiffs,L as collectCommitRangeWithWorkdir,
|
|
7
|
+
`))}else r.untracked=v(``)}catch{}return Object.keys(r).length>0?r:void 0}export{B as captureExcludedLayersPatch,j as collectBranchDiffs,P as collectCombinedDiffRange,M as collectCommitRangeDiffs,L as collectCommitRangeWithWorkdir,H as collectCommitsForSplit,T as collectFromSource,G as collectSourceHashes,R as collectWorkdirPortionForRange,E as collectWorkingDiffs,V as computeTrackedWorktreeTreeSha,z as resolveWorkdirLayers};
|
package/dist/git/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{applyBranchStructure as e,createStackedBranches as t}from"./apply-branches.js";import{
|
|
1
|
+
import{applyBranchStructure as e,createStackedBranches as t}from"./apply-branches.js";import{collectBranchDiffs as n,collectCommitsForSplit as r,collectFromSource as i,collectWorkingDiffs as a}from"./diff-collector.js";import{applyCommitChain as o,buildCommitChain as s}from"./commit-chain.js";import{verifyMultiBranchDiff as c,verifyResultingDiff as l,verifySnapshot as u}from"./safety.js";import{applyToTarget as d}from"./apply-target.js";export{e as applyBranchStructure,o as applyCommitChain,d as applyToTarget,s as buildCommitChain,n as collectBranchDiffs,r as collectCommitsForSplit,i as collectFromSource,a as collectWorkingDiffs,t as createStackedBranches,c as verifyMultiBranchDiff,l as verifyResultingDiff,u as verifySnapshot};
|
package/dist/git/safety.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{commitTree as e,diffTree as t,mergeTree as n,revParse as r,stageAll as i,writeTree as a}from"./port-dispatch.js";import{sha256 as o}from"./utils/crypto.js";import{withUntrackedInTempIndex as s}from"./utils/temp-index.js";import{collectSourceHashes as c,collectWorkdirPortionForRange as l,computeTrackedWorktreeTreeSha as u
|
|
1
|
+
import{commitTree as e,diffTree as t,mergeTree as n,revParse as r,stageAll as i,writeTree as a}from"./port-dispatch.js";import{sha256 as o}from"./utils/crypto.js";import{withUntrackedInTempIndex as s}from"./utils/temp-index.js";import{collectSourceHashes as c,collectWorkdirPortionForRange as l,computeTrackedWorktreeTreeSha as u}from"./diff-collector.js";async function d(e,t,n){return t.branchSha&&t.baseSha?n.includeStaged!=null||n.includeUnstaged||n.includeUntracked?p(e,t,n):h(e,t):f(e,t,n)}async function f(e,n,c){let l=await r(e,`HEAD`);if(l!==n.headSha)return{valid:!1,error:`HEAD changed from ${n.headSha} to ${l}`};let d;if(d=c.includeUntracked?await s(e,async n=>(c.includeUnstaged&&await i(e,{trackedOnly:!0,indexEnv:n}),await i(e,{indexEnv:n}),t(e,l,await a(e,{indexEnv:n})))):c.includeUnstaged?await t(e,l,await u(e)):await t(e,l,await a(e)),o(d)!==n.diffHash){let t=await m(e,n,c);return t.length>0?{valid:!1,error:`Working directory ${t.join(` and `)} ${t.length===1?`has`:`have`} changed since the AI analysis`}:n.sourceHashes==null?{valid:!1,error:`Working directory changes have been modified since the AI analysis`}:{valid:!0}}return{valid:!0}}async function p(e,n,i){if(await r(e,n.branchSha)!==n.branchSha)return{valid:!1,error:`Branch has been modified since the AI analysis`};let a=await r(e,`HEAD`);if(a!==n.headSha)return{valid:!1,error:`HEAD changed from ${n.headSha} to ${a}`};let{finalTreeSha:s}=await l(e,a,{includeStaged:i.includeStaged??!0,includeUnstaged:i.includeUnstaged,includeUntracked:i.includeUntracked});if(o(await t(e,n.baseSha,s))!==n.diffHash){let t=await m(e,n,i);return t.length>0?{valid:!1,error:`Working directory ${t.join(` and `)} ${t.length===1?`has`:`have`} changed since the AI analysis`}:n.sourceHashes==null?{valid:!1,error:`Working directory or branch state has drifted since the AI analysis`}:{valid:!0}}return{valid:!0}}async function m(e,t,n){if(!t.sourceHashes)return[];let r=await c(e,{includeStaged:n.includeStaged,includeUnstaged:n.includeUnstaged,includeUntracked:n.includeUntracked});if(!r)return[];let i=[];return t.sourceHashes.staged!=null&&r.staged!==t.sourceHashes.staged&&i.push(`staged changes`),t.sourceHashes.unstaged!=null&&r.unstaged!==t.sourceHashes.unstaged&&i.push(`unstaged changes`),t.sourceHashes.untracked!=null&&r.untracked!==t.sourceHashes.untracked&&i.push(`untracked files`),i}async function h(e,n){return await r(e,n.branchSha)===n.branchSha?o(await t(e,n.baseSha,n.branchSha))===n.diffHash?{valid:!0}:{valid:!1,error:`Branch diff has changed since the AI analysis`}:{valid:!1,error:`Branch has been modified since the AI analysis`}}async function g(e,n,r,i){return o(await t(e,n,r))===i.diffHash?{valid:!0}:{valid:!1,error:`Output diff does not match input — the commit chain does not reproduce the original changes`}}async function _(r,i,a,s){if(a.length===1)return g(r,i,a[0],s);let c=a[0];for(let t=1;t<a.length;t++)c=await e(r,await n(r,c,a[t]),[c,a[t]],`verify merge`);return o(await t(r,i,c))===s.diffHash?{valid:!0}:{valid:!1,error:`Combined diff of all branches does not match the original changes`}}export{_ as verifyMultiBranchDiff,g as verifyResultingDiff,d as verifySnapshot};
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { PhaseDetail, ProgressEvent, TokenUsage } from "./ai/types.js";
|
|
1
|
+
import { DuplicateHunksRepair, PhaseDetail, ProgressEvent, RepairScope, RepairedHunkClaim, TokenUsage, ValidationRepair } from "./ai/types.js";
|
|
2
2
|
import { AIError, AIErrorCode } from "./errors/ai.js";
|
|
3
3
|
import { GitError, GitErrorCode } from "./errors/git.js";
|
|
4
4
|
import { DiffTooLargeError, TaskInputError } from "./errors/task.js";
|
|
@@ -33,4 +33,4 @@ import { RefinePlanInput, RefinePlanResult, RefineProgressEvent, refinePlan } fr
|
|
|
33
33
|
import { SplitInput, SplitPlan, SplitProgressEvent, SplitResult, split } from "./workflows/split.js";
|
|
34
34
|
import { UndoComposeInput, ValidateUndoComposeInput, listUndoIds, undoCompose, validateUndoCompose } from "./workflows/undo.js";
|
|
35
35
|
import { UndoForceOptions } from "@gitkraken/shared-tools";
|
|
36
|
-
export { AIError, type AIErrorCode, type AfterModelCallInfo, type AnalysisResult, type AnalyzeInput, type AnalyzeOutput, type ApplyComposePlanInput, type ApplyComposePlanResult, type ApplyPatchOptions, type BeforeModelCallInfo, type BeforePromptInfo, type BranchGroup, type BranchOrdering, type BranchPartition, type CheckoutOptions, type CleanOptions, type CommitDetail, type CommitSuggestion, type CommitTreeOptions, type ComposeApplyPlan, type ComposeBranchPlan, type ComposeContext, type ComposeDepth, type ComposeDiffStats, type ComposeGitOps, type ComposeGitPort, ComposeGitPortMissingOpError, type ComposeGroupInput, type ComposeGroupOutput, type ComposeHunk, type ComposeInput, type ComposePlan, type ComposePlanInput, type ComposePlanResult, type ComposeProgressEvent, type ComposeResult, type ComposeSession, type ComposeSource, type ComposeStrategy, type ComposeTarget, type ComposeTaskName, ComposeWorkflowError, type ComposeWorkflowErrorCode, ComposeWorkflowInputError, type Concern, type ConcernDependency, type ConventionDescriptor, DEFAULT_INPUT_TOKEN_BUDGET, type DeleteBranchOptions, DiffTooLargeError, type DiffTreeOptions, type ForEachRefOptions, type GitCommitIdentity, GitError, type GitErrorCode, type GroupInput, type GroupOutput, type GroupingResult, type GroupingStrategy, type HunkAnnotation, type HunkOrderingConstraint, type IndexScopedOptions, type InteriorRefViolation, type LogOptions, type OnAfterModelCall, type OnBeforeModelCall, type OnBeforePrompt, type OpOptions, type OrderInput, type OrderOutput, type OrderingResult, type OverrideSafetyFailures, type PartitionInput, type PartitionOutput, type PartitionResult, type PartitionStrategy, type PhaseDetail, type ProgressEvent, type PromptOverride, type QuickComposeInput, type QuickComposeOutput, type QuickComposeResult, REDACTED_HUNK_CONTENT, type RedactHunkContent, type RefinePlanInput, type RefinePlanResult, type RefineProgressEvent, type SigningConfig, type SplitInput, type SplitPlan, type SplitProgressEvent, type SplitResult, type StageAllOptions, type StashConflict, type StashNotRestored, type StashPushOptions, TaskInputError, type TokenUsage, type UndoComposeInput, type UndoForceOptions, type UpdateRefOptions, type ValidateUndoComposeInput, analyze, applyComposePlan, compose, composeGroup, composePlan, defaultPhaseMessages, detectInteriorRefs, estimatedCharactersPerToken, group, isNoiseHunk, listUndoIds, order, parseUnifiedDiff, partition, quickCompose, refinePlan, split, undoCompose, validateUndoCompose, withDefaultNoiseRedaction };
|
|
36
|
+
export { AIError, type AIErrorCode, type AfterModelCallInfo, type AnalysisResult, type AnalyzeInput, type AnalyzeOutput, type ApplyComposePlanInput, type ApplyComposePlanResult, type ApplyPatchOptions, type BeforeModelCallInfo, type BeforePromptInfo, type BranchGroup, type BranchOrdering, type BranchPartition, type CheckoutOptions, type CleanOptions, type CommitDetail, type CommitSuggestion, type CommitTreeOptions, type ComposeApplyPlan, type ComposeBranchPlan, type ComposeContext, type ComposeDepth, type ComposeDiffStats, type ComposeGitOps, type ComposeGitPort, ComposeGitPortMissingOpError, type ComposeGroupInput, type ComposeGroupOutput, type ComposeHunk, type ComposeInput, type ComposePlan, type ComposePlanInput, type ComposePlanResult, type ComposeProgressEvent, type ComposeResult, type ComposeSession, type ComposeSource, type ComposeStrategy, type ComposeTarget, type ComposeTaskName, ComposeWorkflowError, type ComposeWorkflowErrorCode, ComposeWorkflowInputError, type Concern, type ConcernDependency, type ConventionDescriptor, DEFAULT_INPUT_TOKEN_BUDGET, type DeleteBranchOptions, DiffTooLargeError, type DiffTreeOptions, type DuplicateHunksRepair, type ForEachRefOptions, type GitCommitIdentity, GitError, type GitErrorCode, type GroupInput, type GroupOutput, type GroupingResult, type GroupingStrategy, type HunkAnnotation, type HunkOrderingConstraint, type IndexScopedOptions, type InteriorRefViolation, type LogOptions, type OnAfterModelCall, type OnBeforeModelCall, type OnBeforePrompt, type OpOptions, type OrderInput, type OrderOutput, type OrderingResult, type OverrideSafetyFailures, type PartitionInput, type PartitionOutput, type PartitionResult, type PartitionStrategy, type PhaseDetail, type ProgressEvent, type PromptOverride, type QuickComposeInput, type QuickComposeOutput, type QuickComposeResult, REDACTED_HUNK_CONTENT, type RedactHunkContent, type RefinePlanInput, type RefinePlanResult, type RefineProgressEvent, type RepairScope, type RepairedHunkClaim, type SigningConfig, type SplitInput, type SplitPlan, type SplitProgressEvent, type SplitResult, type StageAllOptions, type StashConflict, type StashNotRestored, type StashPushOptions, TaskInputError, type TokenUsage, type UndoComposeInput, type UndoForceOptions, type UpdateRefOptions, type ValidateUndoComposeInput, type ValidationRepair, analyze, applyComposePlan, compose, composeGroup, composePlan, defaultPhaseMessages, detectInteriorRefs, estimatedCharactersPerToken, group, isNoiseHunk, listUndoIds, order, parseUnifiedDiff, partition, quickCompose, refinePlan, split, undoCompose, validateUndoCompose, withDefaultNoiseRedaction };
|
package/dist/tasks/ai-loop.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { AiSession } from "../ai/session.js";
|
|
2
|
-
import { ProgressEvent, TokenUsage } from "../ai/types.js";
|
|
2
|
+
import { ProgressEvent, TokenUsage, ValidationRepair } from "../ai/types.js";
|
|
3
3
|
import { AiModelPort } from "@gitkraken/shared-tools";
|
|
4
4
|
|
|
5
5
|
//#region src/tasks/ai-loop.d.ts
|
|
@@ -30,6 +30,11 @@ interface AfterModelCallInfo {
|
|
|
30
30
|
};
|
|
31
31
|
/** Identifier of the prompt template used for this call, when a consumer override supplied one. */
|
|
32
32
|
promptVersion?: string;
|
|
33
|
+
/**
|
|
34
|
+
* Deterministic repairs applied to this response so it could be accepted instead of retried.
|
|
35
|
+
* Undefined when the response needed no repair (or when the call itself failed).
|
|
36
|
+
*/
|
|
37
|
+
repairs?: ValidationRepair[];
|
|
33
38
|
}
|
|
34
39
|
type OnBeforeModelCall = (info: BeforeModelCallInfo) => void;
|
|
35
40
|
type OnAfterModelCall = (info: AfterModelCallInfo) => void;
|
|
@@ -77,6 +82,8 @@ interface ValidationResult<T> {
|
|
|
77
82
|
result?: T;
|
|
78
83
|
errors?: string[];
|
|
79
84
|
retryFeedback?: string;
|
|
85
|
+
/** Set (non-empty) only when the validator repaired the response to make it valid. */
|
|
86
|
+
repairs?: ValidationRepair[];
|
|
80
87
|
}
|
|
81
88
|
interface AiLoopResult<T> {
|
|
82
89
|
result: T;
|
|
@@ -86,6 +93,8 @@ interface AiLoopResult<T> {
|
|
|
86
93
|
durationMs: number;
|
|
87
94
|
/** Stable call IDs for each model.generate invocation this loop made, in order. */
|
|
88
95
|
callIds: string[];
|
|
96
|
+
/** Repairs applied to the accepted response. Undefined when it needed no repair. */
|
|
97
|
+
repairs?: ValidationRepair[];
|
|
89
98
|
}
|
|
90
99
|
declare function runAiLoop<T>(params: AiLoopParams<T>): Promise<AiLoopResult<T>>;
|
|
91
100
|
declare function extractOutputTag(text: string): string | null;
|
package/dist/tasks/ai-loop.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{AIError as e}from"../errors/ai.js";import{TaskInputError as t}from"../errors/task.js";import{appendAssistantMessage as n,appendUserMessage as r,createAiSession as i}from"../ai/session.js";import{addUsage as a}from"../ai/types.js";import{randomUUID as o}from"node:crypto";const s=2.8;async function c(a){let{model:s,userPrompt:c,maxRetries:d=3,maxTokens:f=16384,onProgress:p,validate:m,taskName:h,cancellation:g,phase:_=h,onBeforeModelCall:v,onAfterModelCall:y,promptVersion:b}=a;if(!a.session&&!a.systemPrompt)throw new t(`${h}: either 'session' or 'systemPrompt' must be provided`);let x=Date.now(),S;a.session?S=r(a.session,c):(S=i(a.systemPrompt),S=r(S,c));let C,w=[],T=d+1,E=[];for(let e=0;e<=d;e++){g?.throwIfAborted();let t=e+1;if(p?.({type:`attempt_start`,attempt:t,maxAttempts:T}),a.onBeforePrompt){let e=(S.systemPrompt?.length??0)+S.messages.reduce((e,t)=>e+t.content.length,0);if(!await a.onBeforePrompt({phase:_,attempt:t,maxAttempts:T,charCount:e,messageCount:S.messages.length,tokenEstimate:Math.ceil(e/2.8)}))throw new DOMException(`Prompt gate declined by caller (onBeforePrompt returned false)`,`AbortError`)}let i=o();E.push(i);let c=Date.now();v?.({phase:_,attempt:t,maxAttempts:T,callId:i});let h;try{h=await s.generate({system:S.systemPrompt,messages:[...S.messages],maxTokens:f,temperature:0,signal:g})}catch(e){let n=e instanceof Error?e.message:String(e);
|
|
1
|
+
import{AIError as e}from"../errors/ai.js";import{TaskInputError as t}from"../errors/task.js";import{appendAssistantMessage as n,appendUserMessage as r,createAiSession as i}from"../ai/session.js";import{addUsage as a}from"../ai/types.js";import{randomUUID as o}from"node:crypto";const s=2.8;async function c(a){let{model:s,userPrompt:c,maxRetries:d=3,maxTokens:f=16384,onProgress:p,validate:m,taskName:h,cancellation:g,phase:_=h,onBeforeModelCall:v,onAfterModelCall:y,promptVersion:b}=a;if(!a.session&&!a.systemPrompt)throw new t(`${h}: either 'session' or 'systemPrompt' must be provided`);let x=Date.now(),S;a.session?S=r(a.session,c):(S=i(a.systemPrompt),S=r(S,c));let C,w=[],T=d+1,E=[];for(let e=0;e<=d;e++){g?.throwIfAborted();let t=e+1;if(p?.({type:`attempt_start`,attempt:t,maxAttempts:T}),a.onBeforePrompt){let e=(S.systemPrompt?.length??0)+S.messages.reduce((e,t)=>e+t.content.length,0);if(!await a.onBeforePrompt({phase:_,attempt:t,maxAttempts:T,charCount:e,messageCount:S.messages.length,tokenEstimate:Math.ceil(e/2.8)}))throw new DOMException(`Prompt gate declined by caller (onBeforePrompt returned false)`,`AbortError`)}let i=o();E.push(i);let c=Date.now();v?.({phase:_,attempt:t,maxAttempts:T,callId:i});let h;try{h=await s.generate({system:S.systemPrompt,messages:[...S.messages],maxTokens:f,temperature:0,signal:g})}catch(e){let n=e instanceof Error?e.message:String(e);try{y?.({phase:_,attempt:t,callId:i,success:!1,durationMs:Date.now()-c,error:{code:e?.code??`UNKNOWN`,message:n},promptVersion:b})}catch{}throw l(e)||g?.aborted?new DOMException(n,`AbortError`):e}let D=Date.now()-c,O=e=>{try{y?.({phase:_,attempt:t,callId:i,success:!0,durationMs:D,usage:h.usage,promptVersion:b,repairs:e})}catch{}};C=u(C,h.usage);let k;try{k=m(h.text)}catch(e){throw O(),e}if(O(k.valid?k.repairs:void 0),k.valid&&k.result!==void 0)return S=n(S,h.text),{result:k.result,session:S,usage:C,attempts:t,durationMs:Date.now()-x,callIds:E,repairs:k.repairs};w=k.errors??[`Unknown validation error`],p?.({type:`validation_failed`,attempt:t,maxAttempts:T,errors:w}),e<d&&k.retryFeedback&&(S=n(S,h.text),S=r(S,k.retryFeedback))}throw new e(`VALIDATION_EXHAUSTED`,`Failed to get valid ${h} after ${T} attempts. Last errors:\n${w.join(`
|
|
2
2
|
`)}`)}function l(e){return e instanceof Error&&(e.name===`AbortError`||e.name===`DOMException`)}function u(e,t){if(!e)return t;if(!t)return e;let n={...e};return a(n,t),n}function d(e){let t=e.match(/<output>([\s\S]*?)(?:<\/output>|$)/)?.[1]?.trim()??null;return t?t.match(/^```(?:\w*)\n?([\s\S]*?)```$/)?.[1]?.trim()??t:null}export{s as estimatedCharactersPerToken,d as extractOutputTag,c as runAiLoop};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{TaskInputError as e}from"../../errors/task.js";import{runAiLoop as t}from"../ai-loop.js";import{applyUserPromptOverride as n}from"../types.js";import{validateGroupResponse as r}from"../group/validator.js";import{buildComposeGroupSystemPrompt as i,buildComposeGroupUserPrompt as a}from"./prompt.js";async function o(o){let s=o.hunks,c=o.mode??`branches`;if(!s||s.length===0)throw new e(`No hunks provided for compose-group`);let l=new Set(s.map(e=>e.index));if(o.session?.completedSteps.composeGroup)return{result:o.session.completedSteps.composeGroup,session:o.session,attempts:0,durationMs:0,callIds:[]};let u=o.promptOverride?.system??i(o.conventions,c,o.strategy,o.includeAnalysis,o.lightweight),d,f=n(a({hunks:s,commitMessages:o.commitMessages,commitOrder:o.commitOrder,branchName:o.branchName,baseBranch:o.baseBranch,issueUrl:o.issueUrl,pullRequestUrl:o.pullRequestUrl,instructions:o.instructions,condensed:o.condensed,redactHunkContent:o.redactHunkContent,inputTokenBudget:o.inputTokenBudget,systemPromptChars:u.length,onBudgetReduction:e=>{d=e}}),o.promptOverride),p=await t({model:o.model,systemPrompt:u,userPrompt:f,maxRetries:o.maxRetries,maxTokens:o.maxTokens,onProgress:o.onProgress,cancellation:o.cancellation,onBeforeModelCall:o.onBeforeModelCall,onAfterModelCall:o.onAfterModelCall,onBeforePrompt:o.onBeforePrompt,validate:e=>r(e,l,c,o.existingBranchNames),taskName:`compose-group`}),m=o.session?.completedSteps??{},h={...p.session,completedSteps:{...m,composeGroup:p.result}};return{result:p.result,session:h,usage:p.usage,attempts:p.attempts,durationMs:p.durationMs,callIds:p.callIds,hunksCondensedCount:d}}export{o as composeGroup};
|
|
1
|
+
import{TaskInputError as e}from"../../errors/task.js";import{runAiLoop as t}from"../ai-loop.js";import{applyUserPromptOverride as n}from"../types.js";import{validateGroupResponse as r}from"../group/validator.js";import{buildComposeGroupSystemPrompt as i,buildComposeGroupUserPrompt as a}from"./prompt.js";async function o(o){let s=o.hunks,c=o.mode??`branches`;if(!s||s.length===0)throw new e(`No hunks provided for compose-group`);let l=new Set(s.map(e=>e.index));if(o.session?.completedSteps.composeGroup)return{result:o.session.completedSteps.composeGroup,session:o.session,attempts:0,durationMs:0,callIds:[]};let u=o.promptOverride?.system??i(o.conventions,c,o.strategy,o.includeAnalysis,o.lightweight),d,f=n(a({hunks:s,commitMessages:o.commitMessages,commitOrder:o.commitOrder,branchName:o.branchName,baseBranch:o.baseBranch,issueUrl:o.issueUrl,pullRequestUrl:o.pullRequestUrl,instructions:o.instructions,condensed:o.condensed,redactHunkContent:o.redactHunkContent,inputTokenBudget:o.inputTokenBudget,systemPromptChars:u.length,onBudgetReduction:e=>{d=e}}),o.promptOverride),p=await t({model:o.model,systemPrompt:u,userPrompt:f,maxRetries:o.maxRetries,maxTokens:o.maxTokens,onProgress:o.onProgress,cancellation:o.cancellation,onBeforeModelCall:o.onBeforeModelCall,onAfterModelCall:o.onAfterModelCall,onBeforePrompt:o.onBeforePrompt,validate:e=>r(e,l,c,o.existingBranchNames),taskName:`compose-group`}),m=o.session?.completedSteps??{},h={...p.session,completedSteps:{...m,composeGroup:p.result}};return{result:p.result,session:h,usage:p.usage,attempts:p.attempts,durationMs:p.durationMs,callIds:p.callIds,hunksCondensedCount:d,repairs:p.repairs}}export{o as composeGroup};
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { ProgressEvent
|
|
1
|
+
import { ProgressEvent } from "../../ai/types.js";
|
|
2
2
|
import { ComposeHunk, PromptOverride } from "../types.js";
|
|
3
3
|
import { ConventionDescriptor } from "../../git/utils/conventions.js";
|
|
4
4
|
import { OnAfterModelCall, OnBeforeModelCall, OnBeforePrompt } from "../ai-loop.js";
|
|
5
5
|
import { RedactHunkContent } from "../utils/redact.js";
|
|
6
|
-
import {
|
|
6
|
+
import { GroupOutput, GroupingStrategy } from "../group/types.js";
|
|
7
7
|
import { ComposeSession } from "../session.js";
|
|
8
8
|
import { AiModelPort } from "@gitkraken/shared-tools";
|
|
9
9
|
|
|
@@ -59,14 +59,6 @@ interface ComposeGroupInput {
|
|
|
59
59
|
/** Optional prompt override. When set, replaces or decorates the built-in prompts. */
|
|
60
60
|
promptOverride?: PromptOverride;
|
|
61
61
|
}
|
|
62
|
-
|
|
63
|
-
type ComposeGroupOutput = {
|
|
64
|
-
result: GroupingResult;
|
|
65
|
-
session: ComposeSession;
|
|
66
|
-
usage?: TokenUsage;
|
|
67
|
-
attempts: number;
|
|
68
|
-
durationMs: number;
|
|
69
|
-
callIds: string[];
|
|
70
|
-
};
|
|
62
|
+
type ComposeGroupOutput = GroupOutput;
|
|
71
63
|
//#endregion
|
|
72
64
|
export { ComposeGroupInput, ComposeGroupOutput };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{TaskInputError as e}from"../../errors/task.js";import{runAiLoop as t}from"../ai-loop.js";import{applyUserPromptOverride as n}from"../types.js";import{validateGroupResponse as r}from"./validator.js";import{buildGroupContinuationPrompt as i,buildGroupSystemPrompt as a,buildGroupUserPrompt as o}from"./prompt.js";async function s(s){let c=s.hunks,l=s.mode??`branches`;if((!c||c.length===0)&&!s.session)throw new e(`No hunks provided: supply 'hunks' or run analyze() first with a session`);if(s.session?.completedSteps.group)return{result:s.session.completedSteps.group,session:s.session,attempts:0,durationMs:0,callIds:[]};let u=s.session?void 0:s.promptOverride?.system??a(s.conventions,l,s.strategy),d,f=n(s.session?i(s,l):o({hunks:c,commitMessages:s.commitMessages,instructions:s.instructions,mode:l,redactHunkContent:s.redactHunkContent,inputTokenBudget:s.inputTokenBudget,systemPromptChars:u?.length,onBudgetReduction:e=>{d=e}}),s.promptOverride),p=c?.length?new Set(c.map(e=>e.index)):new Set(s.session?.completedSteps.analyze?.hunkAnnotations.map(e=>e.hunkIndex)??[]);if(p.size===0)throw new e(`Cannot validate grouping: no hunk indices available from input or session`);let m=await t({model:s.model,systemPrompt:u,userPrompt:f,session:s.session,maxRetries:s.maxRetries,maxTokens:s.maxTokens,onProgress:s.onProgress,cancellation:s.cancellation,onBeforeModelCall:s.onBeforeModelCall,onAfterModelCall:s.onAfterModelCall,onBeforePrompt:s.onBeforePrompt,validate:e=>r(e,p,l,s.existingBranchNames),taskName:`group`}),h=s.session?.completedSteps??{},g={...m.session,completedSteps:{...h,group:m.result}};return{result:m.result,session:g,usage:m.usage,attempts:m.attempts,durationMs:m.durationMs,callIds:m.callIds,hunksCondensedCount:d}}export{s as group};
|
|
1
|
+
import{TaskInputError as e}from"../../errors/task.js";import{runAiLoop as t}from"../ai-loop.js";import{applyUserPromptOverride as n}from"../types.js";import{validateGroupResponse as r}from"./validator.js";import{buildGroupContinuationPrompt as i,buildGroupSystemPrompt as a,buildGroupUserPrompt as o}from"./prompt.js";async function s(s){let c=s.hunks,l=s.mode??`branches`;if((!c||c.length===0)&&!s.session)throw new e(`No hunks provided: supply 'hunks' or run analyze() first with a session`);if(s.session?.completedSteps.group)return{result:s.session.completedSteps.group,session:s.session,attempts:0,durationMs:0,callIds:[]};let u=s.session?void 0:s.promptOverride?.system??a(s.conventions,l,s.strategy),d,f=n(s.session?i(s,l):o({hunks:c,commitMessages:s.commitMessages,instructions:s.instructions,mode:l,redactHunkContent:s.redactHunkContent,inputTokenBudget:s.inputTokenBudget,systemPromptChars:u?.length,onBudgetReduction:e=>{d=e}}),s.promptOverride),p=c?.length?new Set(c.map(e=>e.index)):new Set(s.session?.completedSteps.analyze?.hunkAnnotations.map(e=>e.hunkIndex)??[]);if(p.size===0)throw new e(`Cannot validate grouping: no hunk indices available from input or session`);let m=await t({model:s.model,systemPrompt:u,userPrompt:f,session:s.session,maxRetries:s.maxRetries,maxTokens:s.maxTokens,onProgress:s.onProgress,cancellation:s.cancellation,onBeforeModelCall:s.onBeforeModelCall,onAfterModelCall:s.onAfterModelCall,onBeforePrompt:s.onBeforePrompt,validate:e=>r(e,p,l,s.existingBranchNames),taskName:`group`}),h=s.session?.completedSteps??{},g={...m.session,completedSteps:{...h,group:m.result}};return{result:m.result,session:g,usage:m.usage,attempts:m.attempts,durationMs:m.durationMs,callIds:m.callIds,hunksCondensedCount:d,repairs:m.repairs}}export{s as group};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ProgressEvent, TokenUsage } from "../../ai/types.js";
|
|
1
|
+
import { ProgressEvent, TokenUsage, ValidationRepair } from "../../ai/types.js";
|
|
2
2
|
import { BranchGroup, ComposeHunk, PromptOverride } from "../types.js";
|
|
3
3
|
import { ConventionDescriptor } from "../../git/utils/conventions.js";
|
|
4
4
|
import { OnAfterModelCall, OnBeforeModelCall, OnBeforePrompt } from "../ai-loop.js";
|
|
@@ -69,6 +69,11 @@ interface GroupOutput {
|
|
|
69
69
|
callIds: string[];
|
|
70
70
|
/** Hunks condensed to metadata to fit the input-token budget. Undefined when the AI call was skipped. */
|
|
71
71
|
hunksCondensedCount?: number;
|
|
72
|
+
/**
|
|
73
|
+
* Deterministic repairs applied to the accepted AI response instead of spending a retry on it.
|
|
74
|
+
* Undefined when the response needed no repair.
|
|
75
|
+
*/
|
|
76
|
+
repairs?: ValidationRepair[];
|
|
72
77
|
}
|
|
73
78
|
//#endregion
|
|
74
79
|
export { GroupInput, GroupOutput, GroupingResult, GroupingStrategy };
|
|
@@ -2,6 +2,14 @@ import { ValidationResult } from "../ai-loop.js";
|
|
|
2
2
|
import { GroupingResult } from "./types.js";
|
|
3
3
|
|
|
4
4
|
//#region src/tasks/group/validator.d.ts
|
|
5
|
-
|
|
5
|
+
interface GroupValidationOptions {
|
|
6
|
+
/**
|
|
7
|
+
* Commit ids whose hunks a later phase restores verbatim. A contested hunk is kept in the
|
|
8
|
+
* locked claimant: dropping it there would be undone by substitution, and the model would then
|
|
9
|
+
* be told the hunk is missing or duplicated when it did neither.
|
|
10
|
+
*/
|
|
11
|
+
lockedCommitIds?: ReadonlySet<string>;
|
|
12
|
+
}
|
|
13
|
+
declare function validateGroupResponse(responseText: string, expectedHunkIndices: ReadonlySet<number>, mode?: 'commits' | 'branches', existingBranchNames?: Set<string>, options?: GroupValidationOptions): ValidationResult<GroupingResult>;
|
|
6
14
|
//#endregion
|
|
7
|
-
export { validateGroupResponse };
|
|
15
|
+
export { GroupValidationOptions, validateGroupResponse };
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import{extractOutputTag as e}from"../ai-loop.js";import{validateJsonSchema as t}from"../utils/json-schema.js";import{groupingResultJsonSchema as n}from"./schema.js";function r(r,o,s=`branches`,c){let
|
|
1
|
+
import{extractOutputTag as e}from"../ai-loop.js";import{validateJsonSchema as t}from"../utils/json-schema.js";import{groupingResultJsonSchema as n}from"./schema.js";function r(r,o,s=`branches`,c,l){let u=null;try{if(u=e(r),!u)return i([`No <output> tag found in response`],a(s));let d=t(n,JSON.parse(u));if(!d.success)return i(d.issues.map(e=>`${e.path.join(`.`)}: ${e.message}`),a(s));let f=d.data,p=[],m=new Set,h=[],g=new Map,_=new Map,v=new Set,y=[],b=[],x=[],S=new Set,C=new Set;for(let e of f.branches){if(S.has(e.id))return i([`Duplicate branch ID: '${e.id}'`],a(s));if(S.add(e.id),s===`branches`&&e.name.length===0)return i([`Branch '${e.id}': missing or empty 'name'`],a(s));if(s===`branches`&&e.name.length>0){if(C.has(e.name))return i([`Duplicate branch name: '${e.name}'`],`Multiple branches use the name '${e.name}'. Each branch must have a unique name. Please rename one of them.`);if(C.add(e.name),c?.has(e.name))return i([`Branch name '${e.name}' already exists in the repository`],`The branch name '${e.name}' already exists in the repository. Please choose a different name for branch '${e.id}'.`)}for(let t of e.commits){v.has(t.id)&&y.push(t.id),v.add(t.id);let n=s===`branches`?`${e.id}/${t.id}`:t.id;for(let r of t.hunks){let i=g.get(r);i?i.push(n):g.set(r,[n]);let a=_.get(r);a?a.push({branchId:e.id,commitId:t.id,label:n}):_.set(r,[{branchId:e.id,commitId:t.id,label:n}])}}}let w=new Map,T=[],E=!1;for(let[e,t]of _){if(t.length<2)continue;if(h.push(e),s===`branches`&&new Set(t.map(e=>e.branchId)).size>1){E=!0;continue}let n=t.find(e=>l?.lockedCommitIds?.has(e.commitId))??t[0];w.set(e,n.label),T.push({hunkIndex:e,keptIn:n.label,droppedFrom:t.filter(e=>e!==n).map(e=>e.label),scope:new Set(t.map(e=>e.commitId)).size===1?`within-commit`:`across-commits`})}let D=h.length>0&&!E;for(let e of f.branches){let t=[];for(let n of e.commits){let r=s===`branches`?`${e.id}/${n.id}`:n.id,i=[],a=!1;for(let e of n.hunks){if(w.has(e)&&w.get(e)!==r){a=!0;continue}m.has(e)||(m.add(e),i.push(e))}n.hunks.length===0?b.push(n.id):i.length===0&&a&&x.push(n.id),t.push({id:n.id,message:n.message,explanation:n.explanation,hunkIndices:i})}p.push({id:e.id,name:e.name,title:e.title,description:e.description,commits:t})}if(y.length>0)return i([`Duplicate commit IDs: ${y.join(`, `)}`],`Some commit IDs appear multiple times. Each must be unique.\n\nDuplicates: ${y.join(`, `)}`);if(b.length>0)return i([`Empty commits (no hunks assigned): ${b.join(`, `)}`],`Every commit must contain at least one hunk. These commits have an empty \`hunks\` array: ${b.join(`, `)}.
|
|
2
2
|
|
|
3
|
-
Either assign at least one hunk to each of these commits, or remove them entirely. If a commit's purpose is already covered by another commit, remove the duplicate. If a commit is supposed to cover specific hunks, assign those hunk indices to it. Every hunk index still needs to appear in exactly one commit.`);let
|
|
4
|
-
`);e.push(`Duplicate hunks (each must appear in exactly ONE commit):\n${
|
|
3
|
+
Either assign at least one hunk to each of these commits, or remove them entirely. If a commit's purpose is already covered by another commit, remove the duplicate. If a commit is supposed to cover specific hunks, assign those hunk indices to it. Every hunk index still needs to appear in exactly one commit.`);let O=[...o].filter(e=>!m.has(e)).sort((e,t)=>e-t),k=[...m].filter(e=>!o.has(e)).sort((e,t)=>e-t),A=[...new Set(h)].sort((e,t)=>e-t),j=A.length>0&&!D;if(O.length>0||k.length>0||x.length>0||j){let e=[],t=[];if(A.length>0){let n=A.map(e=>` hunk ${e} → ${(g.get(e)??[]).join(`, `)}`).join(`
|
|
4
|
+
`);e.push(`Duplicate hunks (each must appear in exactly ONE commit):\n${n}`),t.push(`Duplicate hunks: ${A.join(`, `)}`)}O.length>0&&(e.push(`Missing hunks (not assigned to any commit): ${O.join(`, `)}`),t.push(`Missing hunks: ${O.join(`, `)}`)),k.length>0&&(e.push(`Extra hunks (not present in the input): ${k.join(`, `)}`),t.push(`Extra hunks: ${k.join(`, `)}`)),x.length>0&&(e.push(`Commits whose only hunks were already claimed by an earlier commit: ${x.join(`, `)}`),t.push(`Empty commits after dropping duplicate hunks: ${x.join(`, `)}`));let n=[...o].sort((e,t)=>e-t),r=[...g.values()].reduce((e,t)=>e+t.length,0);return i(t,`There are ${n.length} hunks, with indices: ${n.join(`, `)}. Every one of these indices must appear in exactly one commit — no duplicates, no omissions, no other indices. Use the indices exactly as given; do not renumber them.
|
|
5
5
|
|
|
6
|
-
Your response listed ${r} hunk references across ${
|
|
6
|
+
Your response listed ${r} hunk references across ${g.size} unique indices. Expected: ${n.length} references across ${n.length} unique indices.
|
|
7
7
|
|
|
8
8
|
Specific problems:
|
|
9
9
|
${e.map(e=>`- ${e}`).join(`
|
|
@@ -11,7 +11,7 @@ ${e.map(e=>`- ${e}`).join(`
|
|
|
11
11
|
|
|
12
12
|
To fix duplicates: for each duplicated hunk index, decide which one commit it truly belongs in and remove it from the others. Do not split a single hunk across two commits — git only knows how to apply a hunk once.
|
|
13
13
|
|
|
14
|
-
Return the corrected JSON inside <output> tags.`)}return{valid:!0,result:{branches:
|
|
14
|
+
Return the corrected JSON inside <output> tags.`)}if(T.length>0){let e=[...T].sort((e,t)=>e.hunkIndex-t.hunkIndex),t=e.map(e=>`hunk ${e.hunkIndex} kept in ${e.keptIn}`).join(`; `);return{valid:!0,result:{branches:p},repairs:[{kind:`duplicate-hunks`,hunkIndices:e.map(e=>e.hunkIndex),claims:e,detail:`Dropped duplicate claims on ${e.length} hunk(s): ${t}`}]}}return{valid:!0,result:{branches:p}}}catch(e){let t=e instanceof Error?e.message:String(e),n=(u??r).slice(-300);return i([`Failed to parse response as valid JSON: ${t}`,`Response tail: ${n}`],a(s))}}function i(e,t){return{valid:!1,errors:e,retryFeedback:t}}function a(e){return e===`commits`?`Your response has an invalid structure. Please return JSON inside <output> tags. Use a single branch with id "default", name "", description "":
|
|
15
15
|
<output>
|
|
16
16
|
{
|
|
17
17
|
"branches": [
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{TaskInputError as e}from"../../errors/task.js";import{runAiLoop as t}from"../ai-loop.js";import{applyUserPromptOverride as n}from"../types.js";import{buildRefinementPrompt as r}from"./prompt.js";import{validateRefineResponse as i}from"./validator.js";async function a(a){if(a.session.messages.length===0)throw new e(`refine: session must contain prior messages from a previous compose call.`);if(a.hunkIndices.size===0)throw new e(`refine: hunkIndices must not be empty.`);let o=a.mode??`branches`,s=new Map;for(let e of a.lockedCommits??[])s.set(e.id,e);let c=n(r({instructions:a.instructions,lockedCommits:a.lockedCommits??[],clientContext:a.clientContext,hunkIndices:a.hunkIndices,mode:o}),a.promptOverride),l=await t({model:a.model,systemPrompt:void 0,userPrompt:c,session:a.session,maxRetries:a.maxRetries,maxTokens:a.maxTokens,onProgress:a.onProgress,cancellation:a.cancellation,onBeforeModelCall:a.onBeforeModelCall,onAfterModelCall:a.onAfterModelCall,onBeforePrompt:a.onBeforePrompt,validate:e=>i(e,{expectedHunkIndices:a.hunkIndices,mode:o,lockedCommits:s,lockedRuns:a.lockedRuns}),taskName:`refine`}),u={...l.session,completedSteps:a.session.completedSteps};return{result:l.result,session:u,usage:l.usage,attempts:l.attempts,durationMs:l.durationMs,callIds:l.callIds}}export{a as refine};
|
|
1
|
+
import{TaskInputError as e}from"../../errors/task.js";import{runAiLoop as t}from"../ai-loop.js";import{applyUserPromptOverride as n}from"../types.js";import{buildRefinementPrompt as r}from"./prompt.js";import{validateRefineResponse as i}from"./validator.js";async function a(a){if(a.session.messages.length===0)throw new e(`refine: session must contain prior messages from a previous compose call.`);if(a.hunkIndices.size===0)throw new e(`refine: hunkIndices must not be empty.`);let o=a.mode??`branches`,s=new Map;for(let e of a.lockedCommits??[])s.set(e.id,e);let c=n(r({instructions:a.instructions,lockedCommits:a.lockedCommits??[],clientContext:a.clientContext,hunkIndices:a.hunkIndices,mode:o}),a.promptOverride),l=await t({model:a.model,systemPrompt:void 0,userPrompt:c,session:a.session,maxRetries:a.maxRetries,maxTokens:a.maxTokens,onProgress:a.onProgress,cancellation:a.cancellation,onBeforeModelCall:a.onBeforeModelCall,onAfterModelCall:a.onAfterModelCall,onBeforePrompt:a.onBeforePrompt,validate:e=>i(e,{expectedHunkIndices:a.hunkIndices,mode:o,lockedCommits:s,lockedRuns:a.lockedRuns}),taskName:`refine`}),u={...l.session,completedSteps:a.session.completedSteps};return{result:l.result,session:u,usage:l.usage,attempts:l.attempts,durationMs:l.durationMs,callIds:l.callIds,repairs:l.repairs}}export{a as refine};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ProgressEvent, TokenUsage } from "../../ai/types.js";
|
|
1
|
+
import { ProgressEvent, TokenUsage, ValidationRepair } from "../../ai/types.js";
|
|
2
2
|
import { CommitSuggestion, PromptOverride } from "../types.js";
|
|
3
3
|
import { OnAfterModelCall, OnBeforeModelCall, OnBeforePrompt } from "../ai-loop.js";
|
|
4
4
|
import { GroupingResult } from "../group/types.js";
|
|
@@ -72,6 +72,11 @@ interface RefineOutput {
|
|
|
72
72
|
attempts: number;
|
|
73
73
|
durationMs: number;
|
|
74
74
|
callIds: string[];
|
|
75
|
+
/**
|
|
76
|
+
* Deterministic repairs applied to the accepted AI response instead of spending a retry.
|
|
77
|
+
* Undefined when the response needed no repair.
|
|
78
|
+
*/
|
|
79
|
+
repairs?: ValidationRepair[];
|
|
75
80
|
}
|
|
76
81
|
//#endregion
|
|
77
82
|
export { RefineInput, RefineOutput };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{validateGroupResponse as e}from"../group/validator.js";function t(t,r){let i=e(t,r.expectedHunkIndices,r.mode);if(!i.valid||i.result===void 0)return i;let o=i.result;if(r.lockedCommits.size===0)return i;let s=new Map;for(let e of o.branches)for(let t=0;t<e.commits.length;t++)s.set(e.commits[t].id,{branch:e,commitIndex:t});let c=[];for(let e of r.lockedCommits.keys())s.has(e)||c.push(e);if(c.length>0){let e=c.map(e=>`'${e}'`).join(`, `);return{valid:!1,errors:[`Missing locked commits: ${e}`],retryFeedback:`Your response is missing the following LOCKED commits, which must always appear in the output: ${e}. Re-emit the plan with these commits present (preserving their id, message, explanation, and hunks exactly)
|
|
1
|
+
import{validateGroupResponse as e}from"../group/validator.js";function t(t,r){let i=e(t,r.expectedHunkIndices,r.mode,void 0,{lockedCommitIds:new Set(r.lockedCommits.keys())});if(!i.valid||i.result===void 0)return i;let o=i.result;if(r.lockedCommits.size===0)return i;let s=new Map;for(let e of o.branches)for(let t=0;t<e.commits.length;t++)s.set(e.commits[t].id,{branch:e,commitIndex:t});let c=[];for(let e of r.lockedCommits.keys())s.has(e)||c.push(e);if(c.length>0){let e=c.map(e=>`'${e}'`).join(`, `);return{valid:!1,errors:[`Missing locked commits: ${e}`],retryFeedback:`Your response is missing the following LOCKED commits, which must always appear in the output: ${e}. Re-emit the plan with these commits present (preserving their id, message, explanation, and hunks exactly).`,repairs:i.repairs}}for(let[e,t]of r.lockedCommits){let n=s.get(e);if(n==null)continue;let r=n.branch.commits[n.commitIndex];n.branch.commits[n.commitIndex]={...r,id:t.id,message:t.message,explanation:t.explanation,hunkIndices:[...t.hunkIndices]}}let l=a(o,r.expectedHunkIndices);return l==null?(n(o,r.lockedRuns),{valid:!0,result:o,repairs:i.repairs}):{valid:!1,errors:[l],retryFeedback:`After restoring the locked commits' original hunk assignments, the resulting plan is structurally invalid: ${l}. This means you assigned hunks belonging to a locked commit to a different commit. Locked commits' hunks must remain exclusive to them — do not move those hunks elsewhere.`,repairs:i.repairs}}function n(e,t){if(t==null||t.length===0)return;let n=t.flat(),a=new Map(n.map((e,t)=>[e,t])),o=new Set(n);for(let t of e.branches)r(t,o,a);for(let n of t)n.length>=2&&i(e,n)}function r(e,t,n){let r=[],i=[];for(let n=0;n<e.commits.length;n++)t.has(e.commits[n].id)&&(r.push(n),i.push(e.commits[n]));if(!(i.length<2)){i.sort((e,t)=>(n.get(e.id)??0)-(n.get(t.id)??0));for(let t=0;t<r.length;t++)e.commits[r[t]]=i[t]}}function i(e,t){let n=new Set(t),r=e.branches.filter(e=>e.commits.some(e=>n.has(e.id)));if(r.length!==1)return;let i=r[0],a=[];for(let e of t){let t=i.commits.find(t=>t.id===e);t!=null&&a.push(t)}if(a.length<2)return;let o=Math.min(...a.map(e=>i.commits.indexOf(e))),s=i.commits.slice(0,o).filter(e=>!n.has(e.id)).length,c=i.commits.filter(e=>!n.has(e.id));c.splice(s,0,...a),i.commits=c}function a(e,t){let n=new Set,r=[],i=[],a=new Set,o=[];for(let t of e.branches)for(let e of t.commits){if(a.has(e.id)&&o.push(e.id),a.add(e.id),e.hunkIndices.length===0){i.push(e.id);continue}for(let t of e.hunkIndices)n.has(t)&&r.push(t),n.add(t)}if(o.length>0)return`duplicate commit ids after substitution: ${o.join(`, `)}`;if(i.length>0)return`empty commits after substitution (no hunks assigned): ${i.join(`, `)}`;if(r.length>0)return`hunk indices appear in more than one commit: ${[...new Set(r)].join(`, `)}`;let s=[...t].filter(e=>!n.has(e)).sort((e,t)=>e-t);if(s.length>0)return`hunk indices missing from the plan: ${s.join(`, `)}`;let c=[...n].filter(e=>!t.has(e)).sort((e,t)=>e-t);if(c.length>0)return`hunk indices not present in the input: ${c.join(`, `)}`}export{t as validateRefineResponse};
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import{GitError as e}from"../errors/git.js";import{ComposeWorkflowError as t,ComposeWorkflowInputError as n}from"../errors/workflow.js";import{applyPatchToIndex as r,currentBranch as i,diffTree as a,forEachRef as o,log as s,mergeBase as c,readTree as l,revParse as u,writeTree as d}from"../git/port-dispatch.js";import{validateRewriteRangeSafety as f}from"../git/safety-range.js";import{addUsage as p,emptyUsage as m}from"../ai/types.js";import{memoizeRedactHunkContent as h}from"../tasks/utils/redact.js";import{analyze as g}from"../tasks/analyze/analyze.js";import{composeGroup as _}from"../tasks/compose-group/compose-group.js";import{group as v}from"../tasks/group/group.js";import{order as y}from"../tasks/order/order.js";import{partition as b}from"../tasks/partition/partition.js";import{withDefaultNoiseRedaction as x}from"../tasks/utils/noise.js";import{buildPatchForCommit as S}from"../git/utils/patch-builder.js";import{sha256 as C}from"../git/utils/crypto.js";import{collectFromSource as w}from"../git/diff-collector.js";import{verifySnapshot as T}from"../git/safety.js";import{applyToTarget as E}from"../git/apply-target.js";import{requireExec as D}from"../git/require-exec.js";import{extractConventions as O}from"../git/utils/conventions.js";import{rethrowAsWorkflowError as k}from"../utils/cancellation.js";import{randomUUID as A}from"node:crypto";import{unlinkSync as j}from"node:fs";import{tmpdir as M}from"node:os";import{join as N}from"node:path";import{UndoStore as P,capturePreOperationState as F,generateUndoId as I}from"@gitkraken/shared-tools";const L={collecting:{message:`Collecting changes…`,weight:5},composing:{message:`Composing commits…`,weight:40},"compose-grouping":{message:`Grouping changes into commits…`,weight:40},analyzing:{message:`Analyzing changes…`,weight:20},grouping:{message:`Grouping changes into commits…`,weight:25},ordering:{message:`Ordering commits…`,weight:15},partitioning:{message:`Subdividing branches…`,weight:20},verifying:{message:`Verifying…`,weight:5},applying:{message:`Applying to repository…`,weight:10}};async function R(r){if(!r.applyPlan&&!r.model)throw new n(`Either 'model' or 'applyPlan' must be provided`);if(r.applyPlan)return ne(r);let{git:i,model:a,source:o={type:`workdir`},onProgress:s,onBeforeApply:c,cancellation:l}=r,u=a,d=r.depth??`balanced`,p=r.target??await ae(i,o);oe(o,p),await Y(i,r.applyCommitIds,r.hunkFilter,o,p),await f({git:i,source:o,target:p,overrides:r.overrideSafetyFailures});let m=[];s?.({phase:`collecting`,...L.collecting});let g=Date.now(),_=await w(i,o);if(m.push({name:`collecting`,durationMs:Date.now()-g,attempts:1}),_.hunks.length===0)throw new e(`NO_CHANGES`,Z(o));let{hunks:v,filteredCount:y}=await K(_.hunks,r.hunkFilter);if(v.length===0)throw new e(`NO_CHANGES`,Z(o));let b=W(v);s?.({phase:`collecting`,...L.collecting,stats:b});let S=y!==void 0&&y>0?await q(i,_.snapshot,v):_.snapshot,C=r.conventions??await O({git:i,author:r.conventionAuthor}),A=await se(i),j=p.type===`auto`?`branches`:`commits`,M=r.disableDefaultNoiseReduction?r.redactHunkContent:x(r.redactHunkContent),N=M?h(M):void 0,P=N?v.filter(e=>N(e)).length:void 0,I={hunks:v,collection:_,conventions:C,existingBranchNames:A,groupMode:j,target:p,model:u,instructions:r.instructions,context:r.context,strategy:r.strategy,maxRetries:r.maxRetries,maxTokens:r.maxTokens,onProgress:s,cancellation:l,onBeforeModelCall:r.onBeforeModelCall,onAfterModelCall:r.onAfterModelCall,onBeforePrompt:r.onBeforePrompt,session:r.session,promptOverrides:r.promptOverrides,redactHunkContent:N,inputTokenBudget:r.inputTokenBudget},R;try{R=await ee(d,I)}catch(e){k(e,l)}let z={analysis:R.analysis,grouping:R.grouping,ordering:R.ordering,branches:R.branchPlans,allOrderedCommits:R.allOrderedCommits};if(m.push(...R.phases),c&&!await c(z))return{plan:z,snapshot:S,session:R.session,usage:R.usage,phases:m,source:{type:o.type===`branch`?`branch`:`workdir`,hunks:v,commitMessages:_.commitMessages,branchName:_.branchName,baseBranch:_.baseBranch},diffStats:b,feedbackTargetCallId:G(m),hunksFilteredCount:y,hunksRedactedCount:P,hunksCondensedCount:R.hunksCondensedCount};let te=await F(D(i,`capturing pre-operation state`));s?.({phase:`verifying`,...L.verifying});let B=Date.now(),V=await T(i,_.snapshot,J(o,_));if(!V.valid)throw new t(`SAFETY_CHECK_FAILED`,`Safety check failed: ${V.error}`);m.push({name:`verifying`,durationMs:Date.now()-B,attempts:1});let H=X(R.allOrderedCommits,r.applyCommitIds),{leftoverHunks:U,leftoverPatch:Q}=ie(H,_.hunks),le=U.length>0?await q(i,S,re(H,v)):S;s?.({phase:`applying`,...L.applying});let ue=Date.now(),$=await E({git:i,source:o,target:p,branchPlans:R.branchPlans,allOrderedCommits:H,hunks:v,snapshot:le,collection:_,signing:r.signing,authorAttribution:r.authorAttribution,stashLabel:r.stashLabel,leftoverPatch:Q});m.push({name:`applying`,durationMs:Date.now()-ue,attempts:1});let de=await ce(i,te,$.mutations,p,o);return{plan:z,snapshot:le,partitions:$.partitions,partitionGroups:$.partitionGroups,session:R.session,usage:R.usage,phases:m,source:{type:o.type===`branch`?`branch`:`workdir`,hunks:v,commitMessages:_.commitMessages,branchName:_.branchName,baseBranch:_.baseBranch},diffStats:b,commitShas:$.commitShas,undoId:de,feedbackTargetCallId:G(m),hunksFilteredCount:y,hunksRedactedCount:P,hunksCondensedCount:R.hunksCondensedCount,appliedCommitIds:H.map(e=>e.id),unappliedHunkIndices:U.length>0?U.map(e=>e.index):void 0,stashConflict:$.stashConflict,stashNotRestored:$.stashNotRestored}}function ee(e,t){switch(e){case`quick`:return z(t);case`balanced`:return te(t);case`deep`:return V(t)}}async function z(e){return B(e,{lightweight:!0,skipOrder:!0})}async function te(e){return B(e,{includeAnalysis:!0})}async function B(e,t){let{hunks:n,collection:r,conventions:i,existingBranchNames:a,groupMode:o,target:s,model:c,instructions:l,context:u,strategy:d,maxRetries:f,maxTokens:h,onProgress:g,cancellation:v,onBeforeModelCall:x,onAfterModelCall:S,onBeforePrompt:C}=e,w=m(),T=[],E=await _({hunks:n,commitMessages:r.commitMessages,commitOrder:r.commitOrder,branchName:r.branchName,baseBranch:r.baseBranch,issueUrl:u?.issueUrl,pullRequestUrl:u?.pullRequestUrl,instructions:l,conventions:i,mode:o,strategy:d?.grouping,existingBranchNames:o===`branches`?a:void 0,includeAnalysis:t.includeAnalysis,lightweight:t.lightweight,condensed:t.condensed,redactHunkContent:e.redactHunkContent,inputTokenBudget:e.inputTokenBudget,model:c,session:e.session,maxRetries:f,maxTokens:h,onProgress:e=>g?.({phase:`compose-grouping`,...L[`compose-grouping`],event:e}),cancellation:v,onBeforeModelCall:x,onAfterModelCall:S,onBeforePrompt:C,promptOverride:e.promptOverrides?.[`compose-group`]});E.usage&&p(w,E.usage),T.push({name:`compose-grouping`,durationMs:E.durationMs,attempts:E.attempts,usage:E.usage,callIds:E.callIds});let D=E.session,O,k;if(t.skipOrder)O=E.result.branches.map(e=>({branchGroup:e,orderedCommitIds:e.commits.map(e=>e.id)})),k={branches:O.map(e=>({branchId:e.branchGroup.id,orderedCommitIds:e.orderedCommitIds})),rationale:``};else{let t=await y({branches:E.result.branches,hunks:n,instructions:l,model:c,session:E.session,maxRetries:f,maxTokens:h,onProgress:e=>g?.({phase:`ordering`,...L.ordering,event:e}),cancellation:v,onBeforeModelCall:x,onAfterModelCall:S,onBeforePrompt:C,promptOverride:e.promptOverrides?.order});t.usage&&p(w,t.usage),T.push({name:`ordering`,durationMs:t.durationMs,attempts:t.attempts,usage:t.usage,callIds:t.callIds}),D=t.session,k=t.result,O=E.result.branches.map(e=>({branchGroup:e,orderedCommitIds:k.branches.find(t=>t.branchId===e.id)?.orderedCommitIds??e.commits.map(e=>e.id)}))}if(s.type===`auto`){let t=new Set(a);for(let e of E.result.branches)e.name&&t.add(e.name);for(let n of O){let r=await b({commits:Q(n.branchGroup,n.orderedCommitIds),orderedCommitIds:n.orderedCommitIds,sourceBranchName:n.branchGroup.name,instructions:l,conventions:i,strategy:d?.partition,existingBranchNames:t,model:c,maxRetries:f,maxTokens:h,onProgress:e=>g?.({phase:`partitioning`,...L.partitioning,branch:n.branchGroup.name,event:e}),cancellation:v,onBeforeModelCall:x,onAfterModelCall:S,onBeforePrompt:C,promptOverride:e.promptOverrides?.partition});r.usage&&p(w,r.usage),T.push({name:`partitioning:${n.branchGroup.name}`,durationMs:r.durationMs,attempts:r.attempts,usage:r.usage,callIds:r.callIds}),n.partitioning=r.result,D=r.session;for(let e of r.result.partitions)t.add(e.branchName)}}let A=O.flatMap(e=>Q(e.branchGroup,e.orderedCommitIds));return{analysis:void 0,grouping:E.result,ordering:k,branchPlans:O,allOrderedCommits:A,session:D,usage:w,phases:T,hunksCondensedCount:E.hunksCondensedCount}}async function V(e){let{hunks:t,collection:n,conventions:r,existingBranchNames:i,groupMode:a,target:o,model:s,instructions:c,context:l,strategy:u,maxRetries:d,maxTokens:f,onProgress:h,cancellation:_,onBeforeModelCall:x,onAfterModelCall:S,onBeforePrompt:C}=e,w=m(),T=[],E=await g({hunks:t,commitMessages:n.commitMessages,commitOrder:n.commitOrder,branchName:n.branchName,baseBranch:n.baseBranch,issueUrl:l?.issueUrl,pullRequestUrl:l?.pullRequestUrl,instructions:c,redactHunkContent:e.redactHunkContent,inputTokenBudget:e.inputTokenBudget,model:s,session:e.session,maxRetries:d,maxTokens:f,onProgress:e=>h?.({phase:`analyzing`,...L.analyzing,event:e}),cancellation:_,onBeforeModelCall:x,onAfterModelCall:S,onBeforePrompt:C,promptOverride:e.promptOverrides?.analyze});E.usage&&p(w,E.usage),T.push({name:`analyzing`,durationMs:E.durationMs,attempts:E.attempts,usage:E.usage,callIds:E.callIds});let D=await v({hunks:t,commitMessages:n.commitMessages,instructions:c,conventions:r,mode:a,strategy:u?.grouping,existingBranchNames:a===`branches`?i:void 0,redactHunkContent:e.redactHunkContent,inputTokenBudget:e.inputTokenBudget,model:s,session:E.session,maxRetries:d,maxTokens:f,onProgress:e=>h?.({phase:`grouping`,...L.grouping,event:e}),cancellation:_,onBeforeModelCall:x,onAfterModelCall:S,onBeforePrompt:C,promptOverride:e.promptOverrides?.group});D.usage&&p(w,D.usage),T.push({name:`grouping`,durationMs:D.durationMs,attempts:D.attempts,usage:D.usage,callIds:D.callIds});let O=await y({branches:D.result.branches,hunks:t,instructions:c,model:s,session:D.session,maxRetries:d,maxTokens:f,onProgress:e=>h?.({phase:`ordering`,...L.ordering,event:e}),cancellation:_,onBeforeModelCall:x,onAfterModelCall:S,onBeforePrompt:C,promptOverride:e.promptOverrides?.order});O.usage&&p(w,O.usage),T.push({name:`ordering`,durationMs:O.durationMs,attempts:O.attempts,usage:O.usage,callIds:O.callIds});let k=D.result.branches.map(e=>({branchGroup:e,orderedCommitIds:O.result.branches.find(t=>t.branchId===e.id)?.orderedCommitIds??e.commits.map(e=>e.id)})),A=O.session;if(o.type===`auto`){let t=new Set(i);for(let e of D.result.branches)e.name&&t.add(e.name);for(let n of k){let i=await b({commits:Q(n.branchGroup,n.orderedCommitIds),orderedCommitIds:n.orderedCommitIds,sourceBranchName:n.branchGroup.name,instructions:c,conventions:r,strategy:u?.partition,existingBranchNames:t,model:s,maxRetries:d,maxTokens:f,onProgress:e=>h?.({phase:`partitioning`,...L.partitioning,branch:n.branchGroup.name,event:e}),cancellation:_,onBeforeModelCall:x,onAfterModelCall:S,onBeforePrompt:C,promptOverride:e.promptOverrides?.partition});i.usage&&p(w,i.usage),T.push({name:`partitioning:${n.branchGroup.name}`,durationMs:i.durationMs,attempts:i.attempts,usage:i.usage,callIds:i.callIds}),n.partitioning=i.result,A=i.session;for(let e of i.result.partitions)t.add(e.branchName)}}let j=k.flatMap(e=>Q(e.branchGroup,e.orderedCommitIds));return{analysis:E.result,grouping:D.result,ordering:O.result,branchPlans:k,allOrderedCommits:j,session:A,usage:w,phases:T,hunksCondensedCount:E.hunksCondensedCount}}async function H(e){let t=await R({...e,onBeforeApply:async()=>!1});return{plan:t.plan,snapshot:t.snapshot,source:t.source,session:t.session,usage:t.usage,phases:t.phases,feedbackTargetCallId:t.feedbackTargetCallId,hunksFilteredCount:t.hunksFilteredCount,hunksRedactedCount:t.hunksRedactedCount,hunksCondensedCount:t.hunksCondensedCount,diffStats:t.diffStats}}async function U(e){let t=await R({git:e.git,applyPlan:e.applyPlan,source:e.source,target:e.target,onProgress:e.onProgress,signing:e.signing,authorAttribution:e.authorAttribution,applyCommitIds:e.applyCommitIds,stashLabel:e.stashLabel,hunkFilter:e.hunkFilter,overrideSafetyFailures:e.overrideSafetyFailures});return{commitShas:t.commitShas,partitions:t.partitions,partitionGroups:t.partitionGroups,phases:t.phases,undoId:t.undoId,appliedCommitIds:t.appliedCommitIds,unappliedHunkIndices:t.unappliedHunkIndices,stashConflict:t.stashConflict,stashNotRestored:t.stashNotRestored}}async function ne(n){let{git:r,applyPlan:i,onProgress:a}=n,{plan:o,snapshot:s,source:c}=i,l=n.source??c,u=n.target??i?.target??await ae(r,l);oe(l,u),await Y(r,n.applyCommitIds,n.hunkFilter,l,u),await f({git:r,source:l,target:u,overrides:n.overrideSafetyFailures});let d=await F(D(r,`capturing pre-operation state`)),p=[];a?.({phase:`collecting`,...L.collecting});let h=Date.now(),g=await w(r,l);if(p.push({name:`collecting`,durationMs:Date.now()-h,attempts:1}),g.hunks.length===0)throw new e(`NO_CHANGES`,Z(l));let{hunks:_}=await K(g.hunks,n.hunkFilter);if(_.length===0)throw new e(`NO_CHANGES`,Z(l));let v=W(_);a?.({phase:`collecting`,...L.collecting,stats:v});let y=_.length<g.hunks.length?await q(r,g.snapshot,_):g.snapshot,b={...g,hunks:_,snapshot:y};if(b.snapshot.diffHash!==s.diffHash)throw new t(`SAFETY_CHECK_FAILED`,`Working directory has changed since the plan was created. Re-run compose to generate a new plan.`);a?.({phase:`verifying`,...L.verifying});let x=Date.now(),S=await T(r,b.snapshot,J(l,b));if(!S.valid)throw new t(`SAFETY_CHECK_FAILED`,`Safety check failed: ${S.error}`);p.push({name:`verifying`,durationMs:Date.now()-x,attempts:1});let C=X(o.allOrderedCommits,n.applyCommitIds),{leftoverHunks:O,leftoverPatch:k}=ie(C,g.hunks),A=O.length>0?await q(r,b.snapshot,re(C,b.hunks)):b.snapshot;a?.({phase:`applying`,...L.applying});let j=Date.now(),M=await E({git:r,source:l,target:u,branchPlans:o.branches,allOrderedCommits:C,hunks:b.hunks,snapshot:A,collection:b,signing:n.signing,authorAttribution:n.authorAttribution,stashLabel:n.stashLabel,leftoverPatch:k});p.push({name:`applying`,durationMs:Date.now()-j,attempts:1});let N=await ce(r,d,M.mutations,u,l);return{plan:o,snapshot:A,partitions:M.partitions,partitionGroups:M.partitionGroups,session:{messages:[],systemPrompt:``,completedSteps:{}},usage:m(),phases:p,source:{type:l.type===`branch`?`branch`:`workdir`,hunks:b.hunks,commitMessages:b.commitMessages,branchName:b.branchName,baseBranch:b.baseBranch},diffStats:v,commitShas:M.commitShas,undoId:N,appliedCommitIds:C.map(e=>e.id),unappliedHunkIndices:O.length>0?O.map(e=>e.index):void 0,stashConflict:M.stashConflict,stashNotRestored:M.stashNotRestored}}function W(e){let t=new Set,n=0,r=0;for(let i of e)t.add(i.fileName),n+=i.additions,r+=i.deletions;return{fileCount:t.size,hunkCount:e.length,addedLines:n,removedLines:r}}function G(e){for(let t=e.length-1;t>=0;t--){let n=e[t].callIds;if(n&&n.length>0)return n[n.length-1]}}async function K(e,t){if(!t)return{hunks:e,filteredCount:void 0};let n=await t(e);return{hunks:n,filteredCount:e.length-n.length}}async function q(e,t,n){let i=t.baseSha??t.headSha,o=N(M(),`compose-filtered-idx-${A()}`),s={GIT_INDEX_FILE:o};try{await l(e,i,{indexEnv:s});let o=S(n);o.length>0&&await r(e,o,{indexEnv:s});let c=await a(e,i,await d(e,{indexEnv:s}));return{...t,diffHash:C(c)}}finally{try{j(o)}catch{}}}function J(e,t){return e.type===`workdir`?{includeUnstaged:!e.stagedOnly,includeUntracked:t.untrackedIncluded}:e.type===`commit-range`&&e.includeWorkdir?{includeStaged:e.includeWorkdir.includeStaged??!0,includeUnstaged:e.includeWorkdir.includeUnstaged??!0,includeUntracked:e.includeWorkdir.includeUntracked??!0}:{includeUnstaged:!1,includeUntracked:!1}}async function Y(e,t,r,a,o){if(r==null&&t==null||a.type===`workdir`||!(o.type===`rewrite-range`||(o.type===`branch`||o.type===`auto`)&&o.preserveOriginal!==!0))return;let s=a.type===`branch`?await u(e,a.name):await u(e,a.to),c=await u(e,`HEAD`);if(await i(e)==null)throw new n(`Filtering and partial apply require HEAD to be on a branch (non-detached) when rewriting existing commits. Check out the source branch before composing.`);if(c!==s)throw new n(`Filtering and partial apply require HEAD to be at the source's tip (${s.slice(0,8)}); HEAD is at ${c.slice(0,8)}.`)}function X(e,t){if(t==null)return e;let r=new Set(t),i=e.filter(e=>r.has(e.id)),a=new Set(i.map(e=>e.id)),o=t.filter(e=>!a.has(e));if(o.length>0)throw new n(`applyCommitIds contains commit ids not found in plan.allOrderedCommits: ${o.join(`, `)}`);return i}function re(e,t){let n=new Set(e.flatMap(e=>e.hunkIndices));return t.filter(e=>n.has(e.index))}function ie(e,t){let n=new Set(e.flatMap(e=>e.hunkIndices)),r=t.filter(e=>!n.has(e.index));if(r.length===0)return{leftoverHunks:[],leftoverPatch:void 0};let i=S(r);return{leftoverHunks:r,leftoverPatch:i.length>0?i:void 0}}async function ae(e,t){if(t.type===`branch`){let r=await u(e,t.name),i=(await s(e,{format:`%H`,range:`${await c(e,await u(e,t.mergeTarget??`main`),r)}..${r}`})).trim().split(`
|
|
2
|
-
`).at(-1);if(!i)throw new n(`Cannot recompose branch '${t.name}': no commits between merge-base and branch tip.`);return{type:`rewrite-range`,branch:t.name,rangeFrom:i,rangeTo:r}}return t.type===`commit-range`?{type:`rewrite-range`,branch:t.branch,rangeFrom:t.from,rangeTo:t.to}:{type:`head`}}function Z(e){switch(e.type){case`workdir`:return`No changes found in working directory`;case`branch`:return`No changes found on branch '${e.name}'`;case`commit-range`:return`No changes found in range '${e.from}..${e.to}' on branch '${e.branch}'`}}function
|
|
3
|
-
`).filter(Boolean);return new Set(t)}async function
|
|
1
|
+
import{GitError as e}from"../errors/git.js";import{ComposeWorkflowError as t,ComposeWorkflowInputError as n}from"../errors/workflow.js";import{applyPatchToIndex as r,currentBranch as i,diffTree as a,forEachRef as o,log as s,mergeBase as c,readTree as l,revParse as u,writeTree as d}from"../git/port-dispatch.js";import{validateRewriteRangeSafety as f}from"../git/safety-range.js";import{addUsage as p,emptyUsage as m}from"../ai/types.js";import{memoizeRedactHunkContent as h}from"../tasks/utils/redact.js";import{analyze as g}from"../tasks/analyze/analyze.js";import{composeGroup as _}from"../tasks/compose-group/compose-group.js";import{group as v}from"../tasks/group/group.js";import{order as y}from"../tasks/order/order.js";import{partition as b}from"../tasks/partition/partition.js";import{withDefaultNoiseRedaction as x}from"../tasks/utils/noise.js";import{sha256 as S}from"../git/utils/crypto.js";import{collectFromSource as C,resolveWorkdirLayers as w}from"../git/diff-collector.js";import{buildPatchForCommit as T}from"../git/utils/patch-builder.js";import{verifySnapshot as E}from"../git/safety.js";import{applyToTarget as D}from"../git/apply-target.js";import{requireExec as O}from"../git/require-exec.js";import{extractConventions as k}from"../git/utils/conventions.js";import{rethrowAsWorkflowError as A}from"../utils/cancellation.js";import{randomUUID as j}from"node:crypto";import{unlinkSync as M}from"node:fs";import{tmpdir as N}from"node:os";import{join as P}from"node:path";import{UndoStore as F,capturePreOperationState as I,generateUndoId as L}from"@gitkraken/shared-tools";const R={collecting:{message:`Collecting changes…`,weight:5},composing:{message:`Composing commits…`,weight:40},"compose-grouping":{message:`Grouping changes into commits…`,weight:40},analyzing:{message:`Analyzing changes…`,weight:20},grouping:{message:`Grouping changes into commits…`,weight:25},ordering:{message:`Ordering commits…`,weight:15},partitioning:{message:`Subdividing branches…`,weight:20},verifying:{message:`Verifying…`,weight:5},applying:{message:`Applying to repository…`,weight:10}};async function z(r){if(!r.applyPlan&&!r.model)throw new n(`Either 'model' or 'applyPlan' must be provided`);if(r.applyPlan)return te(r);let{git:i,model:a,source:o={type:`workdir`},onProgress:s,onBeforeApply:c,cancellation:l}=r,u=a,d=r.depth??`balanced`,p=r.target??await oe(i,o);se(o,p),await ne(i,r.applyCommitIds,r.hunkFilter,o,p),await f({git:i,source:o,target:p,overrides:r.overrideSafetyFailures});let m=[];s?.({phase:`collecting`,...R.collecting});let g=Date.now(),_=await C(i,o);if(m.push({name:`collecting`,durationMs:Date.now()-g,attempts:1}),_.hunks.length===0)throw new e(`NO_CHANGES`,Z(o));let{hunks:v,filteredCount:y}=await J(_.hunks,r.hunkFilter);if(v.length===0)throw new e(`NO_CHANGES`,Z(o));let b=K(v);s?.({phase:`collecting`,...R.collecting,stats:b});let S=y!==void 0&&y>0?await Y(i,_.snapshot,v):_.snapshot,w=r.conventions??await k({git:i,author:r.conventionAuthor}),T=await ce(i),j=p.type===`auto`?`branches`:`commits`,M=r.disableDefaultNoiseReduction?r.redactHunkContent:x(r.redactHunkContent),N=M?h(M):void 0,P=N?v.filter(e=>N(e)).length:void 0,F={hunks:v,collection:_,conventions:w,existingBranchNames:T,groupMode:j,target:p,model:u,instructions:r.instructions,context:r.context,strategy:r.strategy,maxRetries:r.maxRetries,maxTokens:r.maxTokens,onProgress:s,cancellation:l,onBeforeModelCall:r.onBeforeModelCall,onAfterModelCall:r.onAfterModelCall,onBeforePrompt:r.onBeforePrompt,session:r.session,promptOverrides:r.promptOverrides,redactHunkContent:N,inputTokenBudget:r.inputTokenBudget},L;try{L=await ee(d,F)}catch(e){A(e,l)}let z={analysis:L.analysis,grouping:L.grouping,ordering:L.ordering,branches:L.branchPlans,allOrderedCommits:L.allOrderedCommits};if(m.push(...L.phases),c&&!await c(z))return{plan:z,snapshot:S,session:L.session,usage:L.usage,phases:m,source:{type:o.type===`branch`?`branch`:`workdir`,hunks:v,commitMessages:_.commitMessages,branchName:_.branchName,baseBranch:_.baseBranch},diffStats:b,feedbackTargetCallId:q(m),hunksFilteredCount:y,hunksRedactedCount:P,hunksCondensedCount:L.hunksCondensedCount};let B=await I(O(i,`capturing pre-operation state`));s?.({phase:`verifying`,...R.verifying});let V=Date.now(),H=await E(i,_.snapshot,X(o,_));if(!H.valid)throw new t(`SAFETY_CHECK_FAILED`,`Safety check failed: ${H.error}`);m.push({name:`verifying`,durationMs:Date.now()-V,attempts:1});let U=re(L.allOrderedCommits,r.applyCommitIds),{leftoverHunks:W,leftoverPatch:G}=ae(U,_.hunks),Q=W.length>0?await Y(i,S,ie(U,v)):S;s?.({phase:`applying`,...R.applying});let ue=Date.now(),$=await D({git:i,source:o,target:p,branchPlans:L.branchPlans,allOrderedCommits:U,hunks:v,snapshot:Q,collection:_,signing:r.signing,authorAttribution:r.authorAttribution,stashLabel:r.stashLabel,leftoverPatch:G});m.push({name:`applying`,durationMs:Date.now()-ue,attempts:1});let de=await le(i,B,$.mutations,p,o);return{plan:z,snapshot:Q,partitions:$.partitions,partitionGroups:$.partitionGroups,session:L.session,usage:L.usage,phases:m,source:{type:o.type===`branch`?`branch`:`workdir`,hunks:v,commitMessages:_.commitMessages,branchName:_.branchName,baseBranch:_.baseBranch},diffStats:b,commitShas:$.commitShas,undoId:de,feedbackTargetCallId:q(m),hunksFilteredCount:y,hunksRedactedCount:P,hunksCondensedCount:L.hunksCondensedCount,appliedCommitIds:U.map(e=>e.id),unappliedHunkIndices:W.length>0?W.map(e=>e.index):void 0,stashConflict:$.stashConflict,stashNotRestored:$.stashNotRestored}}function ee(e,t){switch(e){case`quick`:return B(t);case`balanced`:return V(t);case`deep`:return U(t)}}async function B(e){return H(e,{lightweight:!0,skipOrder:!0})}async function V(e){return H(e,{includeAnalysis:!0})}async function H(e,t){let{hunks:n,collection:r,conventions:i,existingBranchNames:a,groupMode:o,target:s,model:c,instructions:l,context:u,strategy:d,maxRetries:f,maxTokens:h,onProgress:g,cancellation:v,onBeforeModelCall:x,onAfterModelCall:S,onBeforePrompt:C}=e,w=m(),T=[],E=await _({hunks:n,commitMessages:r.commitMessages,commitOrder:r.commitOrder,branchName:r.branchName,baseBranch:r.baseBranch,issueUrl:u?.issueUrl,pullRequestUrl:u?.pullRequestUrl,instructions:l,conventions:i,mode:o,strategy:d?.grouping,existingBranchNames:o===`branches`?a:void 0,includeAnalysis:t.includeAnalysis,lightweight:t.lightweight,condensed:t.condensed,redactHunkContent:e.redactHunkContent,inputTokenBudget:e.inputTokenBudget,model:c,session:e.session,maxRetries:f,maxTokens:h,onProgress:e=>g?.({phase:`compose-grouping`,...R[`compose-grouping`],event:e}),cancellation:v,onBeforeModelCall:x,onAfterModelCall:S,onBeforePrompt:C,promptOverride:e.promptOverrides?.[`compose-group`]});E.usage&&p(w,E.usage),T.push({name:`compose-grouping`,durationMs:E.durationMs,attempts:E.attempts,usage:E.usage,callIds:E.callIds,repairs:E.repairs});let D=E.session,O,k;if(t.skipOrder)O=E.result.branches.map(e=>({branchGroup:e,orderedCommitIds:e.commits.map(e=>e.id)})),k={branches:O.map(e=>({branchId:e.branchGroup.id,orderedCommitIds:e.orderedCommitIds})),rationale:``};else{let t=await y({branches:E.result.branches,hunks:n,instructions:l,model:c,session:E.session,maxRetries:f,maxTokens:h,onProgress:e=>g?.({phase:`ordering`,...R.ordering,event:e}),cancellation:v,onBeforeModelCall:x,onAfterModelCall:S,onBeforePrompt:C,promptOverride:e.promptOverrides?.order});t.usage&&p(w,t.usage),T.push({name:`ordering`,durationMs:t.durationMs,attempts:t.attempts,usage:t.usage,callIds:t.callIds}),D=t.session,k=t.result,O=E.result.branches.map(e=>({branchGroup:e,orderedCommitIds:k.branches.find(t=>t.branchId===e.id)?.orderedCommitIds??e.commits.map(e=>e.id)}))}if(s.type===`auto`){let t=new Set(a);for(let e of E.result.branches)e.name&&t.add(e.name);for(let n of O){let r=await b({commits:Q(n.branchGroup,n.orderedCommitIds),orderedCommitIds:n.orderedCommitIds,sourceBranchName:n.branchGroup.name,instructions:l,conventions:i,strategy:d?.partition,existingBranchNames:t,model:c,maxRetries:f,maxTokens:h,onProgress:e=>g?.({phase:`partitioning`,...R.partitioning,branch:n.branchGroup.name,event:e}),cancellation:v,onBeforeModelCall:x,onAfterModelCall:S,onBeforePrompt:C,promptOverride:e.promptOverrides?.partition});r.usage&&p(w,r.usage),T.push({name:`partitioning:${n.branchGroup.name}`,durationMs:r.durationMs,attempts:r.attempts,usage:r.usage,callIds:r.callIds}),n.partitioning=r.result,D=r.session;for(let e of r.result.partitions)t.add(e.branchName)}}let A=O.flatMap(e=>Q(e.branchGroup,e.orderedCommitIds));return{analysis:void 0,grouping:E.result,ordering:k,branchPlans:O,allOrderedCommits:A,session:D,usage:w,phases:T,hunksCondensedCount:E.hunksCondensedCount}}async function U(e){let{hunks:t,collection:n,conventions:r,existingBranchNames:i,groupMode:a,target:o,model:s,instructions:c,context:l,strategy:u,maxRetries:d,maxTokens:f,onProgress:h,cancellation:_,onBeforeModelCall:x,onAfterModelCall:S,onBeforePrompt:C}=e,w=m(),T=[],E=await g({hunks:t,commitMessages:n.commitMessages,commitOrder:n.commitOrder,branchName:n.branchName,baseBranch:n.baseBranch,issueUrl:l?.issueUrl,pullRequestUrl:l?.pullRequestUrl,instructions:c,redactHunkContent:e.redactHunkContent,inputTokenBudget:e.inputTokenBudget,model:s,session:e.session,maxRetries:d,maxTokens:f,onProgress:e=>h?.({phase:`analyzing`,...R.analyzing,event:e}),cancellation:_,onBeforeModelCall:x,onAfterModelCall:S,onBeforePrompt:C,promptOverride:e.promptOverrides?.analyze});E.usage&&p(w,E.usage),T.push({name:`analyzing`,durationMs:E.durationMs,attempts:E.attempts,usage:E.usage,callIds:E.callIds});let D=await v({hunks:t,commitMessages:n.commitMessages,instructions:c,conventions:r,mode:a,strategy:u?.grouping,existingBranchNames:a===`branches`?i:void 0,redactHunkContent:e.redactHunkContent,inputTokenBudget:e.inputTokenBudget,model:s,session:E.session,maxRetries:d,maxTokens:f,onProgress:e=>h?.({phase:`grouping`,...R.grouping,event:e}),cancellation:_,onBeforeModelCall:x,onAfterModelCall:S,onBeforePrompt:C,promptOverride:e.promptOverrides?.group});D.usage&&p(w,D.usage),T.push({name:`grouping`,durationMs:D.durationMs,attempts:D.attempts,usage:D.usage,callIds:D.callIds,repairs:D.repairs});let O=await y({branches:D.result.branches,hunks:t,instructions:c,model:s,session:D.session,maxRetries:d,maxTokens:f,onProgress:e=>h?.({phase:`ordering`,...R.ordering,event:e}),cancellation:_,onBeforeModelCall:x,onAfterModelCall:S,onBeforePrompt:C,promptOverride:e.promptOverrides?.order});O.usage&&p(w,O.usage),T.push({name:`ordering`,durationMs:O.durationMs,attempts:O.attempts,usage:O.usage,callIds:O.callIds});let k=D.result.branches.map(e=>({branchGroup:e,orderedCommitIds:O.result.branches.find(t=>t.branchId===e.id)?.orderedCommitIds??e.commits.map(e=>e.id)})),A=O.session;if(o.type===`auto`){let t=new Set(i);for(let e of D.result.branches)e.name&&t.add(e.name);for(let n of k){let i=await b({commits:Q(n.branchGroup,n.orderedCommitIds),orderedCommitIds:n.orderedCommitIds,sourceBranchName:n.branchGroup.name,instructions:c,conventions:r,strategy:u?.partition,existingBranchNames:t,model:s,maxRetries:d,maxTokens:f,onProgress:e=>h?.({phase:`partitioning`,...R.partitioning,branch:n.branchGroup.name,event:e}),cancellation:_,onBeforeModelCall:x,onAfterModelCall:S,onBeforePrompt:C,promptOverride:e.promptOverrides?.partition});i.usage&&p(w,i.usage),T.push({name:`partitioning:${n.branchGroup.name}`,durationMs:i.durationMs,attempts:i.attempts,usage:i.usage,callIds:i.callIds}),n.partitioning=i.result,A=i.session;for(let e of i.result.partitions)t.add(e.branchName)}}let j=k.flatMap(e=>Q(e.branchGroup,e.orderedCommitIds));return{analysis:E.result,grouping:D.result,ordering:O.result,branchPlans:k,allOrderedCommits:j,session:A,usage:w,phases:T,hunksCondensedCount:E.hunksCondensedCount}}async function W(e){let t=await z({...e,onBeforeApply:async()=>!1});return{plan:t.plan,snapshot:t.snapshot,source:t.source,session:t.session,usage:t.usage,phases:t.phases,feedbackTargetCallId:t.feedbackTargetCallId,hunksFilteredCount:t.hunksFilteredCount,hunksRedactedCount:t.hunksRedactedCount,hunksCondensedCount:t.hunksCondensedCount,diffStats:t.diffStats}}async function G(e){let t=await z({git:e.git,applyPlan:e.applyPlan,source:e.source,target:e.target,onProgress:e.onProgress,signing:e.signing,authorAttribution:e.authorAttribution,applyCommitIds:e.applyCommitIds,stashLabel:e.stashLabel,hunkFilter:e.hunkFilter,overrideSafetyFailures:e.overrideSafetyFailures});return{commitShas:t.commitShas,partitions:t.partitions,partitionGroups:t.partitionGroups,phases:t.phases,undoId:t.undoId,appliedCommitIds:t.appliedCommitIds,unappliedHunkIndices:t.unappliedHunkIndices,stashConflict:t.stashConflict,stashNotRestored:t.stashNotRestored}}async function te(n){let{git:r,applyPlan:i,onProgress:a}=n,{plan:o,snapshot:s,source:c}=i,l=n.source??c,u=n.target??i?.target??await oe(r,l);se(l,u),await ne(r,n.applyCommitIds,n.hunkFilter,l,u),await f({git:r,source:l,target:u,overrides:n.overrideSafetyFailures});let d=await I(O(r,`capturing pre-operation state`)),p=[];a?.({phase:`collecting`,...R.collecting});let h=Date.now(),g=await C(r,l);if(p.push({name:`collecting`,durationMs:Date.now()-h,attempts:1}),g.hunks.length===0)throw new e(`NO_CHANGES`,Z(l));let{hunks:_}=await J(g.hunks,n.hunkFilter);if(_.length===0)throw new e(`NO_CHANGES`,Z(l));let v=K(_);a?.({phase:`collecting`,...R.collecting,stats:v});let y=_.length<g.hunks.length?await Y(r,g.snapshot,_):g.snapshot,b={...g,hunks:_,snapshot:y};if(b.snapshot.diffHash!==s.diffHash)throw new t(`SAFETY_CHECK_FAILED`,`Working directory has changed since the plan was created. Re-run compose to generate a new plan.`);a?.({phase:`verifying`,...R.verifying});let x=Date.now(),S=await E(r,b.snapshot,X(l,b));if(!S.valid)throw new t(`SAFETY_CHECK_FAILED`,`Safety check failed: ${S.error}`);p.push({name:`verifying`,durationMs:Date.now()-x,attempts:1});let w=re(o.allOrderedCommits,n.applyCommitIds),{leftoverHunks:T,leftoverPatch:k}=ae(w,g.hunks),A=T.length>0?await Y(r,b.snapshot,ie(w,b.hunks)):b.snapshot;a?.({phase:`applying`,...R.applying});let j=Date.now(),M=await D({git:r,source:l,target:u,branchPlans:o.branches,allOrderedCommits:w,hunks:b.hunks,snapshot:A,collection:b,signing:n.signing,authorAttribution:n.authorAttribution,stashLabel:n.stashLabel,leftoverPatch:k});p.push({name:`applying`,durationMs:Date.now()-j,attempts:1});let N=await le(r,d,M.mutations,u,l);return{plan:o,snapshot:A,partitions:M.partitions,partitionGroups:M.partitionGroups,session:{messages:[],systemPrompt:``,completedSteps:{}},usage:m(),phases:p,source:{type:l.type===`branch`?`branch`:`workdir`,hunks:b.hunks,commitMessages:b.commitMessages,branchName:b.branchName,baseBranch:b.baseBranch},diffStats:v,commitShas:M.commitShas,undoId:N,appliedCommitIds:w.map(e=>e.id),unappliedHunkIndices:T.length>0?T.map(e=>e.index):void 0,stashConflict:M.stashConflict,stashNotRestored:M.stashNotRestored}}function K(e){let t=new Set,n=0,r=0;for(let i of e)t.add(i.fileName),n+=i.additions,r+=i.deletions;return{fileCount:t.size,hunkCount:e.length,addedLines:n,removedLines:r}}function q(e){for(let t=e.length-1;t>=0;t--){let n=e[t].callIds;if(n&&n.length>0)return n[n.length-1]}}async function J(e,t){if(!t)return{hunks:e,filteredCount:void 0};let n=await t(e);return{hunks:n,filteredCount:e.length-n.length}}async function Y(e,t,n){let i=t.baseSha??t.headSha,o=P(N(),`compose-filtered-idx-${j()}`),s={GIT_INDEX_FILE:o};try{await l(e,i,{indexEnv:s});let o=T(n);o.length>0&&await r(e,o,{indexEnv:s});let c=await a(e,i,await d(e,{indexEnv:s}));return{...t,diffHash:S(c)}}finally{try{M(o)}catch{}}}function X(e,t){return e.type===`workdir`?{includeUnstaged:!e.stagedOnly,includeUntracked:t.untrackedIncluded}:e.type===`commit-range`&&e.includeWorkdir?w(e.includeWorkdir):{includeUnstaged:!1,includeUntracked:!1}}async function ne(e,t,r,a,o){if(r==null&&t==null||a.type===`workdir`||!(o.type===`rewrite-range`||(o.type===`branch`||o.type===`auto`)&&o.preserveOriginal!==!0))return;let s=a.type===`branch`?await u(e,a.name):await u(e,a.to),c=await u(e,`HEAD`);if(await i(e)==null)throw new n(`Filtering and partial apply require HEAD to be on a branch (non-detached) when rewriting existing commits. Check out the source branch before composing.`);if(c!==s)throw new n(`Filtering and partial apply require HEAD to be at the source's tip (${s.slice(0,8)}); HEAD is at ${c.slice(0,8)}.`)}function re(e,t){if(t==null)return e;let r=new Set(t),i=e.filter(e=>r.has(e.id)),a=new Set(i.map(e=>e.id)),o=t.filter(e=>!a.has(e));if(o.length>0)throw new n(`applyCommitIds contains commit ids not found in plan.allOrderedCommits: ${o.join(`, `)}`);return i}function ie(e,t){let n=new Set(e.flatMap(e=>e.hunkIndices));return t.filter(e=>n.has(e.index))}function ae(e,t){let n=new Set(e.flatMap(e=>e.hunkIndices)),r=t.filter(e=>!n.has(e.index));if(r.length===0)return{leftoverHunks:[],leftoverPatch:void 0};let i=T(r);return{leftoverHunks:r,leftoverPatch:i.length>0?i:void 0}}async function oe(e,t){if(t.type===`branch`){let r=await u(e,t.name),i=(await s(e,{format:`%H`,range:`${await c(e,await u(e,t.mergeTarget??`main`),r)}..${r}`})).trim().split(`
|
|
2
|
+
`).at(-1);if(!i)throw new n(`Cannot recompose branch '${t.name}': no commits between merge-base and branch tip.`);return{type:`rewrite-range`,branch:t.name,rangeFrom:i,rangeTo:r}}return t.type===`commit-range`?{type:`rewrite-range`,branch:t.branch,rangeFrom:t.from,rangeTo:t.to}:{type:`head`}}function Z(e){switch(e.type){case`workdir`:return`No changes found in working directory`;case`branch`:return`No changes found on branch '${e.name}'`;case`commit-range`:return`No changes found in range '${e.from}..${e.to}' on branch '${e.branch}'`}}function se(e,t){if(t.type===`rewrite-range`&&e.type===`workdir`)throw new n(`target 'rewrite-range' is not valid with 'workdir' source`);if(t.type===`head`&&e.type!==`workdir`)throw new n(`target 'head' is not valid with '${e.type}' source — use 'branch', 'auto', or 'rewrite-range'`)}function Q(e,n){return n.map(n=>{let r=e.commits.find(e=>e.id===n);if(!r)throw new t(`INTERNAL`,`Commit ${n} not found in branch '${e.id}'`);return r})}async function ce(e){let t=(await o(e,{format:`%(refname:short)`,prefix:`refs/heads/`})).trim().split(`
|
|
3
|
+
`).filter(Boolean);return new Set(t)}async function le(e,t,n,r,i){let a=L(),o=n.composedBranchCount||1,s=`compose: ${o} branch${o===1?``:`es`} from ${i.type}`,c=[...n.movedRefs];if(r.type===`head`&&t.headBranch){let n=await u(e,`HEAD`);n!==t.headSha&&c.push({kind:`branch`,name:t.headBranch,previousSha:t.headSha,newSha:n})}let l={version:1,id:a,createdAt:new Date().toISOString(),description:s,previousHead:{sha:t.headSha,branch:t.headBranch},workdirClean:t.workdirClean,workdirBackupSha:t.workdirBackupSha,createdRefs:n.createdRefs,deletedRefs:n.deletedRefs,movedRefs:c};return await new F(O(e,`undo manifest persistence`)).save(l),a}export{G as applyComposePlan,z as compose,W as composePlan,R as defaultPhaseMessages};
|
package/dist/workflows/refine.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{ComposeWorkflowInputError as e}from"../errors/workflow.js";import{emptyUsage as t}from"../ai/types.js";import{rethrowAsWorkflowError as n}from"../utils/cancellation.js";import{refine as r}from"../tasks/refine/refine.js";async function i(i){if(i.session.messages.length===0)throw new e(`refinePlan: session must contain prior messages from a previous compose call.`);if(i.priorPlan.allOrderedCommits.length===0)throw new e(`refinePlan: priorPlan must contain at least one commit.`);let c=[];if(i.lockedCommits?.length)for(let t of i.lockedCommits){let n=i.priorPlan.allOrderedCommits.find(e=>e.id===t);if(n==null)throw new e(`refinePlan: lockedCommits id '${t}' not found in priorPlan.allOrderedCommits.`);c.push(n)}let l=o(i.priorPlan),u=s(i.priorPlan),d=i.lockedCommits?.length?a(i.priorPlan,i.lockedCommits):void 0,f=Date.now(),p;try{p=await r({session:i.session,lockedCommits:c,lockedRuns:d,instructions:i.instructions,clientContext:i.clientContext,hunkIndices:l,mode:u,model:i.model,maxRetries:i.maxRetries,maxTokens:i.maxTokens,cancellation:i.cancellation,onBeforeModelCall:i.onBeforeModelCall,onAfterModelCall:i.onAfterModelCall,onBeforePrompt:i.onBeforePrompt,promptOverride:i.promptOverride,onProgress:i.onProgress?e=>i.onProgress?.({phase:`refining`,message:`Refining commits…`,weight:100,event:e}):void 0})}catch(e){n(e,i.cancellation)}let m=p.result,h=m.branches.map(e=>({branchGroup:e,orderedCommitIds:e.commits.map(e=>e.id)})),g={branches:h.map(e=>({branchId:e.branchGroup.id,orderedCommitIds:e.orderedCommitIds})),rationale:``},_=h.flatMap(e=>e.branchGroup.commits);return{plan:{analysis:i.session.completedSteps.analyze,grouping:m,ordering:g,branches:h,allOrderedCommits:_},session:p.session,usage:p.usage??t(),phases:[{name:`refine`,durationMs:Date.now()-f,attempts:p.attempts,usage:p.usage,callIds:p.callIds}],feedbackTargetCallId:p.callIds.length>0?p.callIds[p.callIds.length-1]:void 0}}function a(e,t){let n=new Set(t);if(n.size===0)return[];let r=[];for(let t of e.branches){let e=[];for(let i of t.branchGroup.commits)n.has(i.id)?e.push(i.id):e.length>0&&(r.push(e),e=[]);e.length>0&&r.push(e)}return r}function o(e){let t=new Set;for(let n of e.allOrderedCommits)for(let e of n.hunkIndices)t.add(e);return t}function s(e){return e.branches.length===1&&e.branches[0].branchGroup.id===`default`&&e.branches[0].branchGroup.name===``?`commits`:`branches`}export{a as computeLockedRuns,i as refinePlan};
|
|
1
|
+
import{ComposeWorkflowInputError as e}from"../errors/workflow.js";import{emptyUsage as t}from"../ai/types.js";import{rethrowAsWorkflowError as n}from"../utils/cancellation.js";import{refine as r}from"../tasks/refine/refine.js";async function i(i){if(i.session.messages.length===0)throw new e(`refinePlan: session must contain prior messages from a previous compose call.`);if(i.priorPlan.allOrderedCommits.length===0)throw new e(`refinePlan: priorPlan must contain at least one commit.`);let c=[];if(i.lockedCommits?.length)for(let t of i.lockedCommits){let n=i.priorPlan.allOrderedCommits.find(e=>e.id===t);if(n==null)throw new e(`refinePlan: lockedCommits id '${t}' not found in priorPlan.allOrderedCommits.`);c.push(n)}let l=o(i.priorPlan),u=s(i.priorPlan),d=i.lockedCommits?.length?a(i.priorPlan,i.lockedCommits):void 0,f=Date.now(),p;try{p=await r({session:i.session,lockedCommits:c,lockedRuns:d,instructions:i.instructions,clientContext:i.clientContext,hunkIndices:l,mode:u,model:i.model,maxRetries:i.maxRetries,maxTokens:i.maxTokens,cancellation:i.cancellation,onBeforeModelCall:i.onBeforeModelCall,onAfterModelCall:i.onAfterModelCall,onBeforePrompt:i.onBeforePrompt,promptOverride:i.promptOverride,onProgress:i.onProgress?e=>i.onProgress?.({phase:`refining`,message:`Refining commits…`,weight:100,event:e}):void 0})}catch(e){n(e,i.cancellation)}let m=p.result,h=m.branches.map(e=>({branchGroup:e,orderedCommitIds:e.commits.map(e=>e.id)})),g={branches:h.map(e=>({branchId:e.branchGroup.id,orderedCommitIds:e.orderedCommitIds})),rationale:``},_=h.flatMap(e=>e.branchGroup.commits);return{plan:{analysis:i.session.completedSteps.analyze,grouping:m,ordering:g,branches:h,allOrderedCommits:_},session:p.session,usage:p.usage??t(),phases:[{name:`refine`,durationMs:Date.now()-f,attempts:p.attempts,usage:p.usage,callIds:p.callIds,repairs:p.repairs}],feedbackTargetCallId:p.callIds.length>0?p.callIds[p.callIds.length-1]:void 0}}function a(e,t){let n=new Set(t);if(n.size===0)return[];let r=[];for(let t of e.branches){let e=[];for(let i of t.branchGroup.commits)n.has(i.id)?e.push(i.id):e.length>0&&(r.push(e),e=[]);e.length>0&&r.push(e)}return r}function o(e){let t=new Set;for(let n of e.allOrderedCommits)for(let e of n.hunkIndices)t.add(e);return t}function s(e){return e.branches.length===1&&e.branches[0].branchGroup.id===`default`&&e.branches[0].branchGroup.name===``?`commits`:`branches`}export{a as computeLockedRuns,i as refinePlan};
|
|
@@ -27,10 +27,21 @@ type ComposeSource = {
|
|
|
27
27
|
from: string; /** Last commit in the range to recompose (inclusive). */
|
|
28
28
|
to: string;
|
|
29
29
|
/**
|
|
30
|
-
* Fold working-directory changes into the composed input. Each layer defaults to
|
|
31
|
-
* `true
|
|
32
|
-
*
|
|
33
|
-
*
|
|
30
|
+
* Fold working-directory changes into the composed input. **Each layer defaults to
|
|
31
|
+
* `true`**, so `{ includeStaged: true }` composes the whole working directory — name a
|
|
32
|
+
* layer with `false` to keep it out.
|
|
33
|
+
*
|
|
34
|
+
* A layer excluded this way is neither composed nor discarded: its content is captured
|
|
35
|
+
* immediately before the apply resets the worktree and re-applied afterward as unstaged
|
|
36
|
+
* changes. This is a different thing from `hunkFilter` or a partial apply, which exclude
|
|
37
|
+
* specific changes the caller picked out of the composed input; an excluded layer was
|
|
38
|
+
* never part of that input. The layer's content survives, but its staged/unstaged
|
|
39
|
+
* position does not — excluded staged changes come back unstaged.
|
|
40
|
+
*
|
|
41
|
+
* One combination is rejected: `includeStaged: false` with `includeUnstaged: true` while
|
|
42
|
+
* staged changes exist. Unstaged changes are diffed against the index, so they already
|
|
43
|
+
* presuppose the staged content — there is no coherent way to replay the excluded staged
|
|
44
|
+
* layer over a result composed from unstaged changes.
|
|
34
45
|
*/
|
|
35
46
|
includeWorkdir?: {
|
|
36
47
|
includeStaged?: boolean;
|
package/package.json
CHANGED