@gitkraken/compose-tools 0.7.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +15 -0
- package/README.md +3 -2
- package/dist/git/apply-target.d.ts +2 -1
- package/dist/git/apply-target.js +1 -1
- package/dist/git/commit-chain.d.ts +3 -1
- package/dist/git/commit-chain.js +7 -6
- package/dist/git/diff-collector.d.ts +21 -4
- package/dist/git/diff-collector.js +4 -4
- package/dist/git/port-dispatch.js +1 -1
- package/dist/git/safety.js +1 -1
- package/dist/git/types.d.ts +19 -1
- package/dist/index.d.ts +2 -2
- package/dist/ports/git.d.ts +8 -1
- package/dist/tasks/analyze/analyze.js +1 -1
- package/dist/tasks/analyze/prompt.js +1 -0
- package/dist/tasks/analyze/validator.d.ts +1 -1
- package/dist/tasks/analyze/validator.js +1 -1
- package/dist/tasks/compose-group/compose-group.js +1 -1
- package/dist/tasks/compose-group/prompt.js +6 -6
- package/dist/tasks/group/group.js +1 -1
- package/dist/tasks/group/prompt.js +4 -4
- package/dist/tasks/group/validator.d.ts +1 -1
- package/dist/tasks/group/validator.js +3 -3
- package/dist/tasks/quick-compose/prompt.js +1 -1
- package/dist/tasks/quick-compose/quick-compose.js +1 -1
- package/dist/tasks/quick-compose/validator.d.ts +1 -1
- package/dist/tasks/quick-compose/validator.js +2 -2
- package/dist/tasks/refine/prompt.d.ts +1 -1
- package/dist/tasks/refine/prompt.js +4 -4
- package/dist/tasks/refine/refine.js +1 -1
- package/dist/tasks/refine/types.d.ts +4 -3
- package/dist/tasks/refine/validator.d.ts +1 -1
- package/dist/tasks/refine/validator.js +1 -1
- package/dist/workflows/compose.d.ts +6 -2
- package/dist/workflows/compose.js +1 -1
- package/dist/workflows/refine.d.ts +2 -2
- package/dist/workflows/refine.js +1 -1
- package/dist/workflows/types.d.ts +6 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,21 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/) and this
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.8.0] - 2026-08-03
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- 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
|
+
- 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
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
|
|
18
|
+
- 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).
|
|
19
|
+
- `applyComposePlan` with a `hunkFilter` no longer loses filter-excluded content on destructive applies. The plan-apply path computed the leftover patch from the already-filtered hunk set, so hunks the filter dropped were never re-materialized as working-tree changes after the apply reset the source — the fresh `compose()` path already computed leftovers from the pre-filter collection. Both paths now agree: filter-dropped hunks always land in the leftover patch and are reported in `unappliedHunkIndices`.
|
|
20
|
+
- `commit-range` + `includeWorkdir` no longer silently destroys uncommitted changes in an excluded layer. Excluding `includeStaged` or `includeUnstaged` while that layer held changes previously dropped the layer from the composed input and then hard-reset the worktree over it — no error, no safety stash, success reported. Collection now rejects the combination with `ComposeWorkflowInputError` before any AI spend. A layer that becomes dirty after collection still aborts before the destructive apply — the two-phase plan/apply flow re-collects and throws `ComposeWorkflowInputError`, and the single-shot flow raises a `SAFETY_CHECK_FAILED` safety-check failure at pre-apply verification. **Breaking**: input that previously "succeeded" (destructively on `rewrite-range`, harmlessly on `branch`/`auto` targets) is now rejected — include the layer, or commit/stash it before composing. `includeUntracked: false` is unaffected; untracked files survive the rewrite.
|
|
21
|
+
- The apply path no longer deletes the safety stash — silently destroying its untracked files — when the post-rewrite working tree looks identical to the stash. The tree-equivalence shortcut compared the stash commit's own tree against the working tree, but a `--include-untracked` stash keeps its untracked payload in a third parent, invisible to that comparison — so the trees matched precisely when the stashed untracked files were missing from disk, and the stash holding their only copy was dropped. The shortcut is now skipped whenever the stash swept up untracked files (detected from status output and confirmed by checking whether the stash's third-parent tree is non-empty — `git stash -u` always adds a third parent but leaves its tree empty when nothing untracked was taken, so `status.showUntrackedFiles=no` can't hide a real payload), letting the normal pop restore them. When git instead aborts the pop because the stashed untracked files already exist on disk — the routine outcome when the compose re-materialized them as tracked content — the stash is dropped only after verifying every payload file is present in the working tree with identical content; otherwise it is kept and reported via `stashNotRestored`.
|
|
22
|
+
- The `auto`-target apply path with a single partitioned branch plan no longer swallows the safety-stash outcome: it now forwards the caller's `stashLabel` and propagates `stashConflict` / `stashNotRestored` like every other apply path.
|
|
23
|
+
|
|
9
24
|
## [0.7.0] - 2026-07-30
|
|
10
25
|
|
|
11
26
|
### Added
|
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. When set, `to` must equal the current branch tip, HEAD must be non-detached, and any layer excluded via `includeStaged`/`includeUnstaged` must be clean — a dirty excluded layer is rejected rather than silently discarded. |
|
|
187
187
|
|
|
188
188
|
## Targets
|
|
189
189
|
|
|
@@ -364,7 +364,7 @@ Custom message for the safety stash the apply step creates when the worktree is
|
|
|
364
364
|
stashLabel: `myapp-compose-${new Date().toISOString()}`
|
|
365
365
|
```
|
|
366
366
|
|
|
367
|
-
The apply step always stashes pending workdir state via `git stash push --include-untracked` before mutating, and pops it back after. If the post-apply pop hits conflicts, the result includes `stashConflict: { stashLabel, conflictedFiles }` — apply itself succeeded, the named stash is preserved, and the consumer surfaces recovery UX (typical pattern: scan `git stash list` for a stable prefix on next mode-entry to detect abandoned stashes after process kills).
|
|
367
|
+
The apply step always stashes pending workdir state via `git stash push --include-untracked` before mutating, and pops it back after. If the post-apply pop hits conflicts, the result includes `stashConflict: { stashLabel, conflictedFiles }` — apply itself succeeded, the named stash is preserved, and the consumer surfaces recovery UX (typical pattern: scan `git stash list` for a stable prefix on next mode-entry to detect abandoned stashes after process kills). If the pop is skipped or fails without conflicts, the result instead includes `stashNotRestored: { stashLabel, stashSha, reason }` — the stash entry is left in place (when it still exists) so the WIP it holds stays recoverable.
|
|
368
368
|
|
|
369
369
|
### `maxTokens`
|
|
370
370
|
|
|
@@ -462,6 +462,7 @@ Every top-level entry point returns an object with these common fields:
|
|
|
462
462
|
| `appliedCommitIds` | apply paths | Plan commit IDs actually written. Same as `plan.allOrderedCommits.map(c => c.id)` for full apply; a subset when `applyCommitIds` is set. |
|
|
463
463
|
| `unappliedHunkIndices` | apply paths | Hunk indices from the plan not covered by `appliedCommitIds`. Empty for full apply. |
|
|
464
464
|
| `stashConflict` | apply paths | `{ stashLabel, conflictedFiles }` set when the post-apply stash pop hit conflicts. Apply itself succeeded; the named stash is preserved for the consumer's recovery UX. |
|
|
465
|
+
| `stashNotRestored` | apply paths | `{ stashLabel, stashSha, reason }` set when the post-apply stash pop was skipped or failed without conflicts (`stash-ref-not-found`, `untracked-restore-failed`, or `pop-failed`). Apply itself succeeded; the stash entry is kept (when it still exists) so the WIP it holds stays recoverable. |
|
|
465
466
|
|
|
466
467
|
## Undo
|
|
467
468
|
|
|
@@ -2,7 +2,7 @@ import { ComposeGitPort, SigningConfig } from "../ports/git.js";
|
|
|
2
2
|
import { BranchGroup, BranchPartition, CommitSuggestion, ComposeHunk } from "../tasks/types.js";
|
|
3
3
|
import { PartitionResult } from "../tasks/partition/types.js";
|
|
4
4
|
import { ComposeSource, ComposeTarget } from "../workflows/types.js";
|
|
5
|
-
import { ApplyMutations, SafetySnapshot, StashConflict } from "./types.js";
|
|
5
|
+
import { ApplyMutations, SafetySnapshot, StashConflict, StashNotRestored } from "./types.js";
|
|
6
6
|
import { CollectionResult } from "./diff-collector.js";
|
|
7
7
|
|
|
8
8
|
//#region src/git/apply-target.d.ts
|
|
@@ -40,6 +40,7 @@ interface ApplyToTargetResult {
|
|
|
40
40
|
/** Git mutations performed during apply, for undo manifest construction. */
|
|
41
41
|
mutations: ApplyMutations;
|
|
42
42
|
stashConflict?: StashConflict;
|
|
43
|
+
stashNotRestored?: StashNotRestored;
|
|
43
44
|
}
|
|
44
45
|
declare function applyToTarget(params: ApplyToTargetParams): Promise<ApplyToTargetResult>;
|
|
45
46
|
//#endregion
|
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{reparentDescendants as f}from"./reparent.js";import{applyCommitChain as p,applyRangeRewrite as m,buildCommitChain as h,moveRefsAtSha as g}from"./commit-chain.js";import{verifyMultiBranchDiff as _,verifyResultingDiff as v}from"./safety.js";async function y(t){let{git:n,source:r,target:i,branchPlans:a,allOrderedCommits:o,hunks:s,snapshot:l,collection:u,signing:f,authorAttribution:g,stashLabel:_,leftoverPatch:y}=t;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:a,shaMap:d}=await h({git:n,baseSha:t,commits:o,hunks:s,signing:f,authorAttribution:g}),p=await v(n,t,a,l);if(!p.valid)throw new e(`SAFETY_CHECK_FAILED`,`Diff verification failed: ${p.error}`);let b=r.type===`commit-range`&&r.includeWorkdir!=null,x=await m({git:n,branch:i.branch,rangeFrom:i.rangeFrom,rangeTo:i.rangeTo,newHeadSha:a,originalBranchSha:u.branchSha,stashLabel:_,leftoverPatch:y,includeWorkdirActive:b});return{commitShas:Object.fromEntries(d),mutations:E(x.refMovements),stashConflict:x.stashConflict}}if(u.branchName&&!u.mergeBaseSha)throw new e(`OPERATION_FAILED`,`Branch source is missing mergeBaseSha — cannot determine base commit`);let S=u.mergeBaseSha??await c(n,`HEAD`),C=O(r,i);if(i.type===`auto`&&a.length>1)return b(n,S,a,s,l,u,C,f,g);if(i.type===`auto`&&a.length===1&&a[0].partitioning)return x(n,S,a[0],s,l,u,C,f,g);let{headSha:T,shaMap:A}=await h({git:n,baseSha:S,commits:o,hunks:s,signing:f,authorAttribution:g}),j=Object.fromEntries(A),M=await v(n,S,T,l);if(!M.valid)throw new e(`SAFETY_CHECK_FAILED`,`Diff verification failed: ${M.error}`);if(i.type===`branch`)return k({git:n,source:r,target:i,newHeadSha:T,collection:u,stashLabel:_,leftoverPatch:y,commitShas:j});if(i.type===`auto`){let e=a[0]?.branchGroup.name||u.branchName;if(e){let t=!C&&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:T}],...t});let i=D([r],j,t.originalBranchName,t.originalBranchSha);return{partitions:[r],commitShas:j,mutations:i}}let t=await p({git:n,newHeadSha:T,stashLabel:_});return{commitShas:j,mutations:w(),stashConflict:t.stashConflict}}let N=await p({git:n,newHeadSha:T,stashLabel:_,leftoverPatch:y});return{commitShas:j,mutations:w(),stashConflict:N.stashConflict}}async function b(t,n,i,a,s,c,l,u,f){let p=[],m=[],g=[],v=[],y={};for(let e of i){let r=S(e),{headSha:i,shaMap:o}=await h({git:t,baseSha:n,commits:r,hunks:a,signing:u,authorAttribution:f});g.push(i);for(let[e,t]of o)y[e]=t;if(e.partitioning&&e.partitioning.partitions.length>1){let t=C(e.partitioning.partitions,o);p.push(...t),m.push(t),v.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]),v.push({partitions:[t],tipSha:i})}}let b=await _(t,n,g,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:v,...x}),!c.branchName&&!l&&(await o(t),c.untrackedIncluded&&await r(t)),{partitions:p,partitionGroups:m,commitShas:y,mutations:D(p,y,x.originalBranchName,x.originalBranchSha)}}async function x(t,n,i,a,s,c,l,u,f){let{headSha:
|
|
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{reparentDescendants as f}from"./reparent.js";import{applyCommitChain as p,applyRangeRewrite as m,buildCommitChain as h,moveRefsAtSha as g}from"./commit-chain.js";import{verifyMultiBranchDiff as _,verifyResultingDiff as v}from"./safety.js";async function y(t){let{git:n,source:r,target:i,branchPlans:a,allOrderedCommits:o,hunks:s,snapshot:l,collection:u,signing:f,authorAttribution:g,stashLabel:_,leftoverPatch:y}=t;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:a,shaMap:d}=await h({git:n,baseSha:t,commits:o,hunks:s,signing:f,authorAttribution:g}),p=await v(n,t,a,l);if(!p.valid)throw new e(`SAFETY_CHECK_FAILED`,`Diff verification failed: ${p.error}`);let b=r.type===`commit-range`&&r.includeWorkdir!=null,x=await m({git:n,branch:i.branch,rangeFrom:i.rangeFrom,rangeTo:i.rangeTo,newHeadSha:a,originalBranchSha:u.branchSha,stashLabel:_,leftoverPatch:y,includeWorkdirActive:b});return{commitShas:Object.fromEntries(d),mutations:E(x.refMovements),stashConflict:x.stashConflict,stashNotRestored:x.stashNotRestored}}if(u.branchName&&!u.mergeBaseSha)throw new e(`OPERATION_FAILED`,`Branch source is missing mergeBaseSha — cannot determine base commit`);let S=u.mergeBaseSha??await c(n,`HEAD`),C=O(r,i);if(i.type===`auto`&&a.length>1)return b(n,S,a,s,l,u,C,f,g);if(i.type===`auto`&&a.length===1&&a[0].partitioning)return x(n,S,a[0],s,l,u,C,f,g,_);let{headSha:T,shaMap:A}=await h({git:n,baseSha:S,commits:o,hunks:s,signing:f,authorAttribution:g}),j=Object.fromEntries(A),M=await v(n,S,T,l);if(!M.valid)throw new e(`SAFETY_CHECK_FAILED`,`Diff verification failed: ${M.error}`);if(i.type===`branch`)return k({git:n,source:r,target:i,newHeadSha:T,collection:u,stashLabel:_,leftoverPatch:y,commitShas:j});if(i.type===`auto`){let e=a[0]?.branchGroup.name||u.branchName;if(e){let t=!C&&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:T}],...t});let i=D([r],j,t.originalBranchName,t.originalBranchSha);return{partitions:[r],commitShas:j,mutations:i}}let t=await p({git:n,newHeadSha:T,stashLabel:_});return{commitShas:j,mutations:w(),stashConflict:t.stashConflict,stashNotRestored:t.stashNotRestored}}let N=await p({git:n,newHeadSha:T,stashLabel:_,leftoverPatch:y});return{commitShas:j,mutations:w(),stashConflict:N.stashConflict,stashNotRestored:N.stashNotRestored}}async function b(t,n,i,a,s,c,l,u,f){let p=[],m=[],g=[],v=[],y={};for(let e of i){let r=S(e),{headSha:i,shaMap:o}=await h({git:t,baseSha:n,commits:r,hunks:a,signing:u,authorAttribution:f});g.push(i);for(let[e,t]of o)y[e]=t;if(e.partitioning&&e.partitioning.partitions.length>1){let t=C(e.partitioning.partitions,o);p.push(...t),m.push(t),v.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]),v.push({partitions:[t],tipSha:i})}}let b=await _(t,n,g,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:v,...x}),!c.branchName&&!l&&(await o(t),c.untrackedIncluded&&await r(t)),{partitions:p,partitionGroups:m,commitShas:y,mutations:D(p,y,x.originalBranchName,x.originalBranchSha)}}async function x(t,n,i,a,s,c,l,u,f,m){let{headSha:g,shaMap:_}=await h({git:t,baseSha:n,commits:S(i),hunks:a,signing:u,authorAttribution:f}),y=await v(t,n,g,s);if(!y.valid)throw new e(`SAFETY_CHECK_FAILED`,`Diff verification failed: ${y.error}`);let b=C(i.partitioning.partitions,_),x=Object.fromEntries(_);if(b.length>1){let e=!l&&c.branchName&&c.branchSha?{originalBranchName:c.branchName,originalBranchSha:c.branchSha}:{};return await d({git:t,branches:[{partitions:b,tipSha:g}],...e}),!c.branchName&&!l&&(await o(t),c.untrackedIncluded&&await r(t,{force:!0,directories:!0})),{partitions:b,commitShas:x,mutations:D(b,x,e.originalBranchName,e.originalBranchSha)}}let T=await p({git:t,newHeadSha:g,stashLabel:m});return{partitions:b,commitShas:x,mutations:w(),stashConflict:T.stashConflict,stashNotRestored:T.stashNotRestored}}function S(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 C(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 w(){return{createdRefs:[],deletedRefs:[],movedRefs:[],composedBranchCount:0}}function T(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 E(e){let t=[];for(let n of e){let{kind:e,name:r}=T(n.refname);t.push({kind:e,name:r,previousSha:n.previousSha,newSha:n.newSha})}return{createdRefs:[],deletedRefs:[],movedRefs:t,composedBranchCount:1}}function D(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 O(e,t){return t.type!==`branch`&&t.type!==`auto`||e.type===`commit-range`?!0:t.preserveOriginal===!0}async function k(r){let{git:o,source:d,target:p,newHeadSha:m,collection:h,leftoverPatch:_,commitShas:v}=r,y=O(d,p),b=d.type===`branch`&&!y&&h.branchName!=null&&h.branchSha!=null;if(b&&p.name===h.branchName)throw new e(`OPERATION_FAILED`,`target 'branch' with name '${p.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/${p.name}`),S=x?{createdRefs:[],deletedRefs:[],movedRefs:[{kind:`branch`,name:p.name,previousSha:x,newSha:m}],composedBranchCount:1}:{createdRefs:[{kind:`branch`,name:p.name,sha:m}],deletedRefs:[],movedRefs:[],composedBranchCount:1},C=await i(o),w=await c(o,`HEAD`);if(await u(o,`refs/heads/${p.name}`,m),b){let e=h.branchSha,r=h.branchName,i=await f({git:o,oldToSha:e,newTopSha:m,sourceBranchName:void 0});for(let e of i.reparentedRefs){let{kind:t,name:n}=T(e.refname);S.movedRefs.push({kind:t,name:n,previousSha:e.oldSha,newSha:e.newSha})}let c=await g(o,e,m,`refs/heads/${r}`);for(let e of c){let{kind:t,name:n}=T(e.refname);S.movedRefs.push({kind:t,name:n,previousSha:e.previousSha,newSha:e.newSha})}C===r&&(await n(o,p.name),await s(o,m,`hard`),_&&await t(o,_)),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`),_&&await t(o,_));return{commitShas:v,mutations:S}}export{y as applyToTarget};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { ComposeGitPort, SigningConfig } from "../ports/git.js";
|
|
2
2
|
import { CommitSuggestion, ComposeHunk } from "../tasks/types.js";
|
|
3
|
-
import { StashConflict } from "./types.js";
|
|
3
|
+
import { StashConflict, StashNotRestored } from "./types.js";
|
|
4
4
|
|
|
5
5
|
//#region src/git/commit-chain.d.ts
|
|
6
6
|
type AuthorAttributionStrategy = 'plurality' | 'first-hunk' | ((hunks: ComposeHunk[]) => {
|
|
@@ -35,6 +35,7 @@ interface ApplyCommitChainParams {
|
|
|
35
35
|
}
|
|
36
36
|
interface ApplyChainResult {
|
|
37
37
|
stashConflict?: StashConflict;
|
|
38
|
+
stashNotRestored?: StashNotRestored;
|
|
38
39
|
}
|
|
39
40
|
declare function applyCommitChain(params: ApplyCommitChainParams): Promise<ApplyChainResult>;
|
|
40
41
|
interface ApplyRangeRewriteParams {
|
|
@@ -72,6 +73,7 @@ interface ApplyRangeRewriteResult {
|
|
|
72
73
|
newSha: string;
|
|
73
74
|
}[];
|
|
74
75
|
stashConflict?: StashConflict;
|
|
76
|
+
stashNotRestored?: StashNotRestored;
|
|
75
77
|
}
|
|
76
78
|
/**
|
|
77
79
|
* Rewrite a contiguous range of commits on a branch in-place. Given a new commit chain
|
package/dist/git/commit-chain.js
CHANGED
|
@@ -1,7 +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,
|
|
2
|
-
`)[0]}': ${r}`,{cause:t})}for(let e of a)e.originalFileName&&e.diffHeader.includes(`rename from`)&&m.add(e.diffHeader);let
|
|
3
|
-
`)
|
|
4
|
-
`))
|
|
5
|
-
`);return
|
|
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{reparentDescendants as b}from"./reparent.js";import{buildPatchForCommit as x}from"./utils/patch-builder.js";import{randomUUID as S}from"node:crypto";import*as C from"node:fs";import*as w from"node:os";import*as T from"node:path";async function E(n){let{git:r,baseSha:a,commits:o,hunks:s,signing:l,authorAttribution:u=`plurality`}=n,d=T.join(w.tmpdir(),`compose-idx-${S()}`),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=x(a.map(e=>m.has(e.diffHeader)?{...e,diffHeader:G(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}),_=V(a,u),y=U(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{W(d)}}async function D(e){let{git:t,newHeadSha:i,branchName:a,stashLabel:o,leftoverPatch:s}=e,c=o??A,u=await j(t,c);try{a?await r(t,a,{createBranch:!0,startPoint:i}):await l(t,i,`hard`),s&&await n(t,s)}catch(e){throw u!=null&&await F(t,u),e}if(u!=null){let e=await F(t,u);if(e.kind===`conflicted`)return{stashConflict:{stashLabel:c,conflictedFiles:e.conflictedFiles}};if(e.kind===`not-restored`)return{stashNotRestored:{stashLabel:c,stashSha:u.sha,reason:e.reason}}}return{}}async function O(e){let{git:t,branch:i,rangeTo:o,newHeadSha:s,originalBranchSha:c,stashLabel:u,leftoverPatch:d,includeWorkdirActive:f}=e,p=await a(t)===i,m=u??A,h=p&&!f?await j(t,m):null,g,v,y=s,x=[];try{let e=await b({git:t,oldToSha:o,newTopSha:s,sourceBranchName:i});for(let t of e.reparentedRefs)x.push({refname:t.refname,previousSha:t.oldSha,newSha:t.newSha});e.sourceBranchNewSha==null?(await _(t,`refs/heads/${i}`,s),x.push({refname:`refs/heads/${i}`,previousSha:c,newSha:s}),y=s):y=e.sourceBranchNewSha;let a=await k(t,o,s,`refs/heads/${i}`);x.push(...a),p&&(await r(t,i),await l(t,y,`hard`),h==null&&d&&await n(t,d))}catch(e){throw h!=null&&await F(t,h),e}if(h!=null){let e=await F(t,h);e.kind===`conflicted`?g={stashLabel:m,conflictedFiles:e.conflictedFiles}:e.kind===`not-restored`&&(v={stashLabel:m,stashSha:h.sha,reason:e.reason})}return{finalTipSha:y,refMovements:x,stashConflict:g,stashNotRestored:v}}async function k(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 A=`compose backup`;async function j(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 N(e,t),{sha:t,hasUntracked:r})}catch{return null}}const M=new Set([`4b825dc642cb6eb9a060e54bf8d69288fbee4904`,`6ef19b41225c5369f1c104d45d8d85efa9b057b53b14b4b9b939dd74decc5321`]);async function N(e,t){try{let n=await d(e,`${t}^3^{tree}`);return n==null?!0:!M.has(n)}catch{return!0}}async function P(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 F(e,t){if(!t.hasUntracked)try{let n=(await u(e,`${t.sha}^{tree}`)).trim(),r=await L(e);if(n.length>0&&n===r){let n=await P(e,t.sha);if(n!=null)try{await p(e,n)}catch{}return{kind:`restored`}}}catch{}let n=await P(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 B(e);if(i.length>0)return{kind:`conflicted`,conflictedFiles:i};if(R(r)){if(await I(e,t.sha)){let n=await P(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 I(e,t){try{let n=await d(e,`${t}^3^{tree}`);return n==null?!1:(await o(e,n,await L(e),{nameOnly:!0,binary:!1,diffFilter:`DMT`,noRenames:!0})).trim().length===0}catch{return!1}}async function L(e){let t=T.join(w.tmpdir(),`compose-equiv-${S()}`);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{W(t)}}function R(e){let t=z(e);return t.includes(`already exists, no checkout`)||t.includes(`could not restore untracked files from stash`)}function z(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 B(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 V(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=H(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(H(t.author),t.author);for(let e of t.coAuthors??[])i.set(H(e),e)}let a=r?H(r):null,o=[];for(let[e,t]of i)e!==a&&o.push(t);return{author:r,coAuthors:o}}function H(e){return`${e.name.toLowerCase()}\0${e.email.toLowerCase()}`}function U(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 W(e){try{C.unlinkSync(e)}catch{}}function G(e,t){let n=e.split(`
|
|
6
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(`
|
|
7
|
-
`)}export{
|
|
8
|
+
`)}export{D as applyCommitChain,O as applyRangeRewrite,E as buildCommitChain,k as moveRefsAtSha};
|
|
@@ -87,9 +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,
|
|
91
|
-
* Mixing WIP with a range only makes sense
|
|
92
|
-
* being recomposed
|
|
90
|
+
* Requires `to` to equal both the branch tip and HEAD, HEAD to be non-detached, and any
|
|
91
|
+
* excluded staged/unstaged layer to be clean. Mixing WIP with a range only makes sense
|
|
92
|
+
* when the user is actively on the branch tip being recomposed, and excluding a dirty
|
|
93
|
+
* layer would destroy it when the rewrite resets the worktree to the new tip.
|
|
93
94
|
*/
|
|
94
95
|
declare function collectCommitRangeWithWorkdir(git: ComposeGitPort, branchName: string, from: string, to: string, options: {
|
|
95
96
|
includeStaged: boolean;
|
|
@@ -109,6 +110,22 @@ declare function collectWorkdirPortionForRange(git: ComposeGitPort, headSha: str
|
|
|
109
110
|
}): Promise<{
|
|
110
111
|
finalTreeSha: string;
|
|
111
112
|
}>;
|
|
113
|
+
/**
|
|
114
|
+
* Report which excluded workdir layers currently hold changes — `staged` (index vs `headSha`)
|
|
115
|
+
* or `unstaged` (tracked worktree vs index). Such a layer would be silently destroyed by a
|
|
116
|
+
* range rewrite: the worktree is hard-reset to the new tip, which omits the excluded layer,
|
|
117
|
+
* and no safety stash is taken when includeWorkdir is present. Untracked files are exempt —
|
|
118
|
+
* they survive the hard reset.
|
|
119
|
+
*/
|
|
120
|
+
declare function findDirtyExcludedLayers(git: ComposeGitPort, headSha: string, options: {
|
|
121
|
+
includeStaged: boolean;
|
|
122
|
+
includeUnstaged: boolean;
|
|
123
|
+
}): Promise<('staged' | 'unstaged')[]>;
|
|
124
|
+
/**
|
|
125
|
+
* Compute the tree SHA of the worktree's tracked content, working against a temp copy of the
|
|
126
|
+
* real index so the consumer's staged state is never modified.
|
|
127
|
+
*/
|
|
128
|
+
declare function computeTrackedWorktreeTreeSha(git: ComposeGitPort): Promise<string>;
|
|
112
129
|
declare function collectCommitsForSplit(git: ComposeGitPort, branchName: string, baseBranch: string): Promise<SplitCollectionResult>;
|
|
113
130
|
/**
|
|
114
131
|
* Compute per-source content hashes alongside the authoritative combined hash.
|
|
@@ -127,4 +144,4 @@ declare function collectSourceHashes(git: ComposeGitPort, options: {
|
|
|
127
144
|
includeUntracked: boolean;
|
|
128
145
|
}): Promise<SafetySnapshot['sourceHashes']>;
|
|
129
146
|
//#endregion
|
|
130
|
-
export { BranchCommitInfo, BranchDiffResult, CollectionResult, SplitCollectionResult, WorkingDiffOptions, WorkingDiffResult, collectBranchDiffs, collectCombinedDiffRange, collectCommitRangeDiffs, collectCommitRangeWithWorkdir, collectCommitsForSplit, collectFromSource, collectSourceHashes, collectWorkdirPortionForRange, collectWorkingDiffs };
|
|
147
|
+
export { BranchCommitInfo, BranchDiffResult, CollectionResult, SplitCollectionResult, WorkingDiffOptions, WorkingDiffResult, collectBranchDiffs, collectCombinedDiffRange, collectCommitRangeDiffs, collectCommitRangeWithWorkdir, collectCommitsForSplit, collectFromSource, collectSourceHashes, collectWorkdirPortionForRange, collectWorkingDiffs, computeTrackedWorktreeTreeSha, findDirtyExcludedLayers };
|
|
@@ -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:U(r,n,void 0,void 0,await W(e,{includeUnstaged:t,includeUntracked:!1})),combinedDiff:r}}async function O(e,t,n){return n?a(e,t,await B(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:U(o,i,void 0,void 0,await W(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 H(e,t);return P(e,await l(e,await H(e,n),r),await f(e,r))}async function M(e,t,n,r){await H(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=U(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 H(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.`);let h=await z(e,m,s);if(h.length>0)throw new t(`Cannot exclude ${h.join(` and `)} changes that currently exist: composing a range with includeWorkdir requires excluded layers to be clean — a range rewrite resets the worktree to the new tip and would silently discard them. Include them, or commit/stash them 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=U(b,m,p,v,await W(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 B(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{}}}async function z(e,t,n){let r=[];return n.includeStaged||(await a(e,t,await h(e))).trim()&&r.push(`staged`),n.includeUnstaged||(await a(e,await h(e),await B(e))).trim()&&r.push(`unstaged`),r}async function B(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 V(e,t,n){let r=await H(e,t),i=await l(e,await H(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 H(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 U(e,t,n,r,i){return{diffHash:v(e),headSha:t,branchSha:n,baseSha:r,sourceHashes:i}}async function W(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{j as collectBranchDiffs,P as collectCombinedDiffRange,M as collectCommitRangeDiffs,L as collectCommitRangeWithWorkdir,V as collectCommitsForSplit,T as collectFromSource,W as collectSourceHashes,R as collectWorkdirPortionForRange,E as collectWorkingDiffs,B as computeTrackedWorktreeTreeSha,z as findDirtyExcludedLayers};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{ComposeGitPortMissingOpError as e}from"../ports/git.js";function t(t,n){throw new e(t,n)}async function n(e,n,r){return e.revParse?e.revParse(n,r):e.exec?(await e.exec([`rev-parse`,n])).trim():t(`revParse`)}async function r(e,n,r){if(e.revParseVerify)return e.revParseVerify(n,r);if(e.exec)try{return(await e.exec([`rev-parse`,`--verify`,n])).trim()}catch{return null}return t(`revParseVerify`)}async function i(e,n){return e.gitDir?e.gitDir(n):e.exec?(await e.exec([`rev-parse`,`--absolute-git-dir`])).trim():t(`gitDir`)}async function a(e,n){if(e.currentBranch)return e.currentBranch(n);if(e.exec)try{return(await e.exec([`symbolic-ref`,`--short`,`HEAD`])).trim()||null}catch{return null}return t(`currentBranch`)}async function o(e,n,r,i){return e.mergeBase?e.mergeBase(n,r,i):e.exec?(await e.exec([`merge-base`,n,r])).trim():t(`mergeBase`)}async function s(e,n,r,i){if(e.mergeBaseIsAncestor)return e.mergeBaseIsAncestor(n,r,i);if(e.exec)try{return await e.exec([`merge-base`,`--is-ancestor`,n,r]),!0}catch{return!1}return t(`mergeBaseIsAncestor`)}async function c(e,n,r,i){if(e.diffTree)return e.diffTree(n,r,i);if(e.exec){let t=[`diff`];return i?.binary!==!1&&t.push(`--binary`),i?.nameOnly&&t.push(`--name-only`),t.push(n,r),e.exec(t)}return t(`diffTree`)}async function l(e,n){if(e.log)return e.log(n);if(e.exec){let t=[`log`,`--format=${n.format}`];if(n.noMerges&&t.push(`--no-merges`),n.reverse&&t.push(`--reverse`),n.topoOrder&&t.push(`--topo-order`),n.limit!=null&&t.push(`-n`,String(n.limit)),n.author&&t.push(`--author=${n.author}`),n.sha)t.push(`-1`,n.sha);else if(n.range)for(let e of n.range.split(/\s+/))e&&t.push(e);return e.exec(t)}return t(`log`)}async function u(e,n){if(e.forEachRef)return e.forEachRef(n);if(e.exec){let t=[`for-each-ref`,`--format=${n.format}`];return n.sort&&t.push(`--sort=${n.sort}`),n.pointsAt&&t.push(`--points-at=${n.pointsAt}`),n.prefix&&t.push(n.prefix),n.limit!=null&&t.push(`--count=${n.limit}`),e.exec(t)}return t(`forEachRef`)}async function d(e,n){if(e.revListChildren)return e.revListChildren(n);if(e.exec){let t=[`rev-list`,`--children`];return n.reverse&&t.push(`--reverse`),t.push(n.range),f(await e.exec(t))}return t(`revListChildren`)}function f(e){let t=[];for(let n of e.split(`
|
|
1
|
+
import{ComposeGitPortMissingOpError as e}from"../ports/git.js";function t(t,n){throw new e(t,n)}async function n(e,n,r){return e.revParse?e.revParse(n,r):e.exec?(await e.exec([`rev-parse`,n])).trim():t(`revParse`)}async function r(e,n,r){if(e.revParseVerify)return e.revParseVerify(n,r);if(e.exec)try{return(await e.exec([`rev-parse`,`--verify`,n])).trim()}catch{return null}return t(`revParseVerify`)}async function i(e,n){return e.gitDir?e.gitDir(n):e.exec?(await e.exec([`rev-parse`,`--absolute-git-dir`])).trim():t(`gitDir`)}async function a(e,n){if(e.currentBranch)return e.currentBranch(n);if(e.exec)try{return(await e.exec([`symbolic-ref`,`--short`,`HEAD`])).trim()||null}catch{return null}return t(`currentBranch`)}async function o(e,n,r,i){return e.mergeBase?e.mergeBase(n,r,i):e.exec?(await e.exec([`merge-base`,n,r])).trim():t(`mergeBase`)}async function s(e,n,r,i){if(e.mergeBaseIsAncestor)return e.mergeBaseIsAncestor(n,r,i);if(e.exec)try{return await e.exec([`merge-base`,`--is-ancestor`,n,r]),!0}catch{return!1}return t(`mergeBaseIsAncestor`)}async function c(e,n,r,i){if(e.diffTree)return e.diffTree(n,r,i);if(e.exec){let t=[`diff`];return i?.binary!==!1&&t.push(`--binary`),i?.nameOnly&&t.push(`--name-only`),i?.diffFilter&&t.push(`--diff-filter=${i.diffFilter}`),i?.noRenames&&t.push(`--no-renames`),t.push(n,r),e.exec(t)}return t(`diffTree`)}async function l(e,n){if(e.log)return e.log(n);if(e.exec){let t=[`log`,`--format=${n.format}`];if(n.noMerges&&t.push(`--no-merges`),n.reverse&&t.push(`--reverse`),n.topoOrder&&t.push(`--topo-order`),n.limit!=null&&t.push(`-n`,String(n.limit)),n.author&&t.push(`--author=${n.author}`),n.sha)t.push(`-1`,n.sha);else if(n.range)for(let e of n.range.split(/\s+/))e&&t.push(e);return e.exec(t)}return t(`log`)}async function u(e,n){if(e.forEachRef)return e.forEachRef(n);if(e.exec){let t=[`for-each-ref`,`--format=${n.format}`];return n.sort&&t.push(`--sort=${n.sort}`),n.pointsAt&&t.push(`--points-at=${n.pointsAt}`),n.prefix&&t.push(n.prefix),n.limit!=null&&t.push(`--count=${n.limit}`),e.exec(t)}return t(`forEachRef`)}async function d(e,n){if(e.revListChildren)return e.revListChildren(n);if(e.exec){let t=[`rev-list`,`--children`];return n.reverse&&t.push(`--reverse`),t.push(n.range),f(await e.exec(t))}return t(`revListChildren`)}function f(e){let t=[];for(let n of e.split(`
|
|
2
2
|
`)){let e=n.trim();if(!e)continue;let r=e.split(/\s+/);t.push({sha:r[0],children:r.slice(1)})}return t}async function p(e,n){return e.writeTree?e.writeTree(n):e.exec?(await e.exec([`write-tree`],{env:n?.indexEnv})).trim():t(`writeTree`)}async function m(e,n,r){if(e.readTree)return e.readTree(n,r);if(e.exec){await e.exec([`read-tree`,n],{env:r?.indexEnv});return}return t(`readTree`)}async function h(e,n){if(e.stageAll)return e.stageAll(n);if(e.exec){let t=[`add`];n?.intentToAdd?t.push(`-N`):n?.trackedOnly?t.push(`-u`):n?.paths||t.push(`-A`),n?.paths&&t.push(`--`,...n.paths),await e.exec(t,{env:n?.indexEnv});return}return t(`stageAll`)}async function g(e,n){return e.statusPorcelain?e.statusPorcelain(n):e.exec?e.exec([`status`,`--porcelain`]):t(`statusPorcelain`)}async function _(e,n){return e.listUntracked?e.listUntracked(n):e.exec?(await e.exec([`ls-files`,`--others`,`--exclude-standard`])).trim().split(`
|
|
3
3
|
`).filter(Boolean):t(`listUntracked`)}async function v(e,n,r,i){return e.showFile?e.showFile(n,r,i):e.exec?e.exec([`show`,`${n}:${r}`]):t(`showFile`)}async function y(e,n,r){if(e.getConfig)return e.getConfig(n,r);if(e.exec)try{return(await e.exec([`config`,n])).trim()||null}catch{return null}return t(`getConfig`)}async function b(e,n,r,i,a){if(e.commitTree)return e.commitTree(n,r,i,a);if(e.exec){let t=[`commit-tree`,n];for(let e of r)t.push(`-p`,e);t.push(`-m`,i),a?.signing?.enabled&&t.push(a.signing.signingKey?`-S${a.signing.signingKey}`:`-S`);let o={};return a?.author&&(o.GIT_AUTHOR_NAME=a.author.name,o.GIT_AUTHOR_EMAIL=a.author.email,o.GIT_COMMITTER_NAME=a.author.name,o.GIT_COMMITTER_EMAIL=a.author.email,a.author.date&&(o.GIT_AUTHOR_DATE=a.author.date.toISOString())),a?.signing?.gpgProgram&&(o.GIT_GPG_PROGRAM=a.signing.gpgProgram),(await e.exec(t,{env:Object.keys(o).length?o:void 0})).trim()}return t(`commitTree`)}async function x(e,n,r){if(e.applyPatchToIndex)return e.applyPatchToIndex(n,r);if(e.exec){let t=[`apply`,`--cached`];r?.allowBinaryReplacement!==!1&&t.push(`--allow-binary-replacement`),await e.exec(t,{env:r?.indexEnv,stdin:n});return}return t(`applyPatchToIndex`)}async function S(e,n,r){if(e.applyPatchToWorkdir)return e.applyPatchToWorkdir(n,r);if(e.exec){let t=[`apply`];r?.allowBinaryReplacement!==!1&&t.push(`--allow-binary-replacement`),await e.exec(t,{stdin:n});return}return t(`applyPatchToWorkdir`)}async function C(e,n,r,i){return e.mergeTree?e.mergeTree(n,r,i):e.exec?(await e.exec([`merge-tree`,`--write-tree`,n,r])).trim().split(`
|
|
4
4
|
`)[0]:t(`mergeTree`)}async function w(e,n,r,i){if(e.createBranch)return e.createBranch(n,r,i);if(e.exec){await e.exec([`branch`,n,r]);return}return t(`createBranch`)}async function T(e,n,r){if(e.deleteBranch)return e.deleteBranch(n,r);if(e.exec){await e.exec([`branch`,r?.force?`-D`:`-d`,n]);return}return t(`deleteBranch`)}async function E(e,n,r,i){if(e.updateRef)return e.updateRef(n,r,i);if(e.exec){if(i?.remove){await e.exec([`update-ref`,`-d`,n]);return}let t=[`update-ref`,n,r];i?.previousSha&&t.push(i.previousSha),await e.exec(t);return}return t(`updateRef`)}async function D(e,n,r){if(e.checkout)return e.checkout(n,r);if(e.exec){let t=[`checkout`];r?.createBranch&&t.push(`-b`),r?.detach&&t.push(`--detach`),t.push(n),r?.createBranch&&r?.startPoint&&t.push(r.startPoint),await e.exec(t);return}return t(`checkout`)}async function O(e,n,r,i){if(e.reset)return e.reset(n,r,i);if(e.exec){await e.exec([`reset`,`--${r}`,n]);return}return t(`reset`)}async function k(e,n){if(e.cleanWorkdir)return e.cleanWorkdir(n);if(e.exec){let t=`-`;n?.force!==!1&&(t+=`f`),n?.directories!==!1&&(t+=`d`),t===`-`&&(t=`-fd`),await e.exec([`clean`,t]);return}return t(`cleanWorkdir`)}async function A(e,n,r){if(e.discardWorkdirChanges)return e.discardWorkdirChanges(n,r);if(e.exec){let t=[`checkout`,`--`,...n?.length?n:[`.`]];await e.exec(t);return}return t(`discardWorkdirChanges`)}async function j(e,n){if(e.stashPush)return e.stashPush(n);if(e.exec){let t=[`stash`,`push`];n?.includeUntracked&&t.push(`--include-untracked`),n?.message&&t.push(`-m`,n.message),await e.exec(t);return}return t(`stashPush`)}const M={LANG:`C`,LC_ALL:`C`};async function N(e,n){if(e.stashPop)return e.stashPop(n);if(e.exec){let t=[`stash`,`pop`];n?.withIndex&&t.push(`--index`),n?.ref&&t.push(n.ref),await e.exec(t,{env:M});return}return t(`stashPop`)}async function P(e,n,r){if(e.stashDrop)return e.stashDrop(n,r);if(e.exec){await e.exec([`stash`,`drop`,n]);return}return t(`stashDrop`)}async function F(e,n,r){if(e.cherryPick)return e.cherryPick(n,r);if(e.exec){await e.exec([`cherry-pick`,n]);return}return t(`cherryPick`)}async function I(e,n){if(e.cherryPickAbort)return e.cherryPickAbort(n);if(e.exec){await e.exec([`cherry-pick`,`--abort`]);return}return t(`cherryPickAbort`)}export{x as applyPatchToIndex,S as applyPatchToWorkdir,D as checkout,F as cherryPick,I as cherryPickAbort,k as cleanWorkdir,b as commitTree,w as createBranch,a as currentBranch,T as deleteBranch,c as diffTree,A as discardWorkdirChanges,u as forEachRef,y as getConfig,i as gitDir,_ as listUntracked,M as localeNeutralEnv,l as log,o as mergeBase,s as mergeBaseIsAncestor,C as mergeTree,m as readTree,O as reset,d as revListChildren,n as revParse,r as revParseVerify,v as showFile,h as stageAll,P as stashDrop,N as stashPop,j as stashPush,g as statusPorcelain,E as updateRef,p as writeTree};
|
package/dist/git/safety.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{commitTree as e,diffTree as t,
|
|
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,findDirtyExcludedLayers as d}from"./diff-collector.js";async function f(e,t,n){return t.branchSha&&t.baseSha?n.includeStaged!=null||n.includeUnstaged||n.includeUntracked?m(e,t,n):g(e,t):p(e,t,n)}async function p(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 h(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 m(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 s={includeStaged:i.includeStaged??!0,includeUnstaged:i.includeUnstaged,includeUntracked:i.includeUntracked},c=await d(e,a,s);if(c.length>0)return{valid:!1,error:`Excluded ${c.join(` and `)} changes appeared since the AI analysis — applying would discard them`};let{finalTreeSha:u}=await l(e,a,s);if(o(await t(e,n.baseSha,u))!==n.diffHash){let t=await h(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 h(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 g(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 _(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 v(r,i,a,s){if(a.length===1)return _(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{v as verifyMultiBranchDiff,_ as verifyResultingDiff,f as verifySnapshot};
|
package/dist/git/types.d.ts
CHANGED
|
@@ -48,5 +48,23 @@ interface StashConflict {
|
|
|
48
48
|
stashLabel: string;
|
|
49
49
|
conflictedFiles: string[];
|
|
50
50
|
}
|
|
51
|
+
/**
|
|
52
|
+
* Reported when the apply path's safety stash could not be restored (and did not conflict).
|
|
53
|
+
* The apply itself succeeded. The stash entry is left in place when it still exists, so the
|
|
54
|
+
* pre-apply WIP it holds can be recovered manually.
|
|
55
|
+
*/
|
|
56
|
+
interface StashNotRestored {
|
|
57
|
+
stashLabel: string;
|
|
58
|
+
/** SHA of the stash commit, usable for recovery even if the stash ref is gone. */
|
|
59
|
+
stashSha: string;
|
|
60
|
+
/**
|
|
61
|
+
* - `'stash-ref-not-found'` — the stash entry could not be located (no longer in the stash
|
|
62
|
+
* list, or the git backend can't enumerate stashes), so no pop ran.
|
|
63
|
+
* - `'untracked-restore-failed'` — git aborted the pop while restoring the stash's untracked
|
|
64
|
+
* files (typically because a path it holds already exists in the working tree).
|
|
65
|
+
* - `'pop-failed'` — the pop failed for another reason without leaving conflicts.
|
|
66
|
+
*/
|
|
67
|
+
reason: 'stash-ref-not-found' | 'untracked-restore-failed' | 'pop-failed';
|
|
68
|
+
}
|
|
51
69
|
//#endregion
|
|
52
|
-
export { ApplyMutations, SafetySnapshot, StashConflict };
|
|
70
|
+
export { ApplyMutations, SafetySnapshot, StashConflict, StashNotRestored };
|
package/dist/index.d.ts
CHANGED
|
@@ -15,7 +15,7 @@ import { QuickComposeInput, QuickComposeOutput, QuickComposeResult } from "./tas
|
|
|
15
15
|
import { ComposeSession } from "./tasks/session.js";
|
|
16
16
|
import { PartitionInput, PartitionOutput, PartitionResult, PartitionStrategy } from "./tasks/partition/types.js";
|
|
17
17
|
import { ComposeContext, ComposeSource, ComposeTarget, OverrideSafetyFailures } from "./workflows/types.js";
|
|
18
|
-
import { StashConflict } from "./git/types.js";
|
|
18
|
+
import { StashConflict, StashNotRestored } from "./git/types.js";
|
|
19
19
|
import { ComposeBranchPlan } from "./git/apply-target.js";
|
|
20
20
|
import { InteriorRefViolation, detectInteriorRefs } from "./git/safety-range.js";
|
|
21
21
|
import { parseUnifiedDiff } from "./git/utils/hunk-parser.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 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 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 };
|
package/dist/ports/git.d.ts
CHANGED
|
@@ -30,7 +30,9 @@ interface ComposeGitOps {
|
|
|
30
30
|
mergeBase?: (a: string, b: string, opts?: OpOptions) => Promise<string>;
|
|
31
31
|
/** Check whether `ancestor` is an ancestor of `descendant`. Covers `merge-base --is-ancestor`. */
|
|
32
32
|
mergeBaseIsAncestor?: (ancestor: string, descendant: string, opts?: OpOptions) => Promise<boolean>;
|
|
33
|
-
/** Tree-to-tree unified diff
|
|
33
|
+
/** Tree-to-tree unified diff between two tree-ish. A custom implementation must honor every
|
|
34
|
+
* {@link DiffTreeOptions} field it is passed (`binary`, `nameOnly`, `diffFilter`, `noRenames`):
|
|
35
|
+
* callers rely on the filtering, and silently ignoring one can return a diff they misread. */
|
|
34
36
|
diffTree?: (from: string, to: string, opts?: DiffTreeOptions) => Promise<string>;
|
|
35
37
|
/** Commit log with custom format. Covers various `git log --format=... <range>` / `-1 <sha>` uses. */
|
|
36
38
|
log?: (opts: LogOptions) => Promise<string>;
|
|
@@ -110,6 +112,11 @@ interface DiffTreeOptions extends OpOptions {
|
|
|
110
112
|
binary?: boolean;
|
|
111
113
|
/** Return file list instead of patch content. */
|
|
112
114
|
nameOnly?: boolean;
|
|
115
|
+
/** Restrict to selected change kinds, passed verbatim to `--diff-filter` (e.g. `'DMT'`). */
|
|
116
|
+
diffFilter?: string;
|
|
117
|
+
/** Disable rename detection (`--no-renames`), so a moved or content-matching file reports as a
|
|
118
|
+
* delete plus an add instead of a rename. */
|
|
119
|
+
noRenames?: boolean;
|
|
113
120
|
}
|
|
114
121
|
interface LogOptions extends OpOptions {
|
|
115
122
|
format: string;
|
|
@@ -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{buildAnalyzeSystemPrompt as r,buildAnalyzeUserPrompt as i}from"./prompt.js";import{validateAnalyzeResponse as a}from"./validator.js";async function o(o){let s=o.hunks;if(!s||s.length===0)throw new e(`No hunks provided: supply 'hunks'`);if(o.session?.completedSteps.analyze)return{result:o.session.completedSteps.analyze,session:o.session,attempts:0,durationMs:0,callIds:[]};let c=o.session?void 0:o.promptOverride?.system??r(),l,u=n(i({hunks:s,commitMessages:o.commitMessages,commitOrder:o.commitOrder,branchName:o.branchName,baseBranch:o.baseBranch,issueUrl:o.issueUrl,pullRequestUrl:o.pullRequestUrl,instructions:o.instructions,redactHunkContent:o.redactHunkContent,inputTokenBudget:o.inputTokenBudget,systemPromptChars:c?.length,onBudgetReduction:e=>{l=e}}),o.promptOverride),d=s.
|
|
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{buildAnalyzeSystemPrompt as r,buildAnalyzeUserPrompt as i}from"./prompt.js";import{validateAnalyzeResponse as a}from"./validator.js";async function o(o){let s=o.hunks;if(!s||s.length===0)throw new e(`No hunks provided: supply 'hunks'`);if(o.session?.completedSteps.analyze)return{result:o.session.completedSteps.analyze,session:o.session,attempts:0,durationMs:0,callIds:[]};let c=o.session?void 0:o.promptOverride?.system??r(),l,u=n(i({hunks:s,commitMessages:o.commitMessages,commitOrder:o.commitOrder,branchName:o.branchName,baseBranch:o.baseBranch,issueUrl:o.issueUrl,pullRequestUrl:o.pullRequestUrl,instructions:o.instructions,redactHunkContent:o.redactHunkContent,inputTokenBudget:o.inputTokenBudget,systemPromptChars:c?.length,onBudgetReduction:e=>{l=e}}),o.promptOverride),d=new Set(s.map(e=>e.index)),f=await t({model:o.model,systemPrompt:c,userPrompt:u,session:o.session,maxRetries:o.maxRetries,maxTokens:o.maxTokens,onProgress:o.onProgress,cancellation:o.cancellation,onBeforeModelCall:o.onBeforeModelCall,onAfterModelCall:o.onAfterModelCall,onBeforePrompt:o.onBeforePrompt,validate:e=>a(e,d),taskName:`analyze`}),p=o.session?.completedSteps??{},m={...f.session,completedSteps:{...p,analyze:f.result}};return{result:f.result,session:m,usage:f.usage,attempts:f.attempts,durationMs:f.durationMs,callIds:f.callIds,hunksCondensedCount:l}}export{o as analyze};
|
|
@@ -15,6 +15,7 @@ Look for these patterns:
|
|
|
15
15
|
|
|
16
16
|
Rules:
|
|
17
17
|
- Every hunk must be annotated with exactly one concern
|
|
18
|
+
- Use each hunk's \`index\` value exactly as shown — the indices are not necessarily contiguous, so never renumber them or invent indices that were not provided
|
|
18
19
|
- Concerns should be named clearly and concisely
|
|
19
20
|
- Dependencies should capture real code-level relationships, not just thematic similarity
|
|
20
21
|
- Ordering constraints should capture concrete provider/consumer relationships between specific hunks, not vague thematic ordering
|
|
@@ -2,6 +2,6 @@ import { ValidationResult } from "../ai-loop.js";
|
|
|
2
2
|
import { AnalysisResult } from "./types.js";
|
|
3
3
|
|
|
4
4
|
//#region src/tasks/analyze/validator.d.ts
|
|
5
|
-
declare function validateAnalyzeResponse(responseText: string,
|
|
5
|
+
declare function validateAnalyzeResponse(responseText: string, expectedHunkIndices: ReadonlySet<number>): ValidationResult<AnalysisResult>;
|
|
6
6
|
//#endregion
|
|
7
7
|
export { validateAnalyzeResponse };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{extractOutputTag as e}from"../ai-loop.js";import{analysisResultJsonSchema as t}from"./schema.js";import{validateJsonSchema as n}from"../utils/json-schema.js";function r(r,o){let s=null;try{if(s=e(r),!s)return i([`No <output> tag found in response`],a());let c=n(t,JSON.parse(s));if(!c.success)return i(c.issues.map(e=>`${e.path.join(`.`)}: ${e.message}`),a());let l=c.data,u=new Set(l.hunkAnnotations.map(e=>e.hunkIndex)),d=[...
|
|
1
|
+
import{extractOutputTag as e}from"../ai-loop.js";import{analysisResultJsonSchema as t}from"./schema.js";import{validateJsonSchema as n}from"../utils/json-schema.js";function r(r,o){let s=null;try{if(s=e(r),!s)return i([`No <output> tag found in response`],a());let c=n(t,JSON.parse(s));if(!c.success)return i(c.issues.map(e=>`${e.path.join(`.`)}: ${e.message}`),a());let l=c.data,u=new Set(l.hunkAnnotations.map(e=>e.hunkIndex)),d=[...o].filter(e=>!u.has(e)).sort((e,t)=>e-t);if(d.length>0)return i([`Missing annotations for hunks: ${d.join(`, `)}`],`Your response is missing annotations for some hunks. Every hunk must be annotated.\n\nMissing: ${d.join(`, `)}\n\nPlease provide a corrected response.`);let f=[...u].filter(e=>!o.has(e)).sort((e,t)=>e-t);return f.length>0?i([`Annotations reference unknown hunk indices: ${f.join(`, `)}`],`Your response annotates hunk indices that don't exist in the input. Annotate only the hunk indices shown in the input, exactly as given.\n\nUnknown: ${f.join(`, `)}\n\nPlease provide a corrected response.`):{valid:!0,result:{title:l.title,summary:l.summary,concerns:l.concerns,dependencies:l.dependencies,hunkAnnotations:l.hunkAnnotations,orderingConstraints:l.orderingConstraints}}}catch(e){let t=e instanceof Error?e.message:String(e),n=(s??r).slice(-300);return i([`Failed to parse response as valid JSON: ${t}`,`Response tail: ${n}`],a())}}function i(e,t){return{valid:!1,errors:e,retryFeedback:t}}function a(){return`Your response has an invalid structure. Please return JSON inside <output> tags with this structure:
|
|
2
2
|
<output>
|
|
3
3
|
{
|
|
4
4
|
"title": "Short PR-style title (under 72 chars)",
|
|
@@ -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`);if(o.session?.completedSteps.composeGroup)return{result:o.session.completedSteps.composeGroup,session:o.session,attempts:0,durationMs:0,callIds:[]};let
|
|
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};
|
|
@@ -20,7 +20,7 @@ In addition to the \`branches\` field, include an \`analysis\` field at the top
|
|
|
20
20
|
## Critical constraints (output is rejected if violated)
|
|
21
21
|
|
|
22
22
|
1. **Each hunk index appears exactly once across the entire response.** Not zero. Not two. Exactly one occurrence — meaning it appears in exactly one commit's \`hunks\` array, and only once within that array. Listing the same index twice inside one commit's \`hunks\`, OR listing it in two different commits, both count as duplicates and both fail validation. This is the most common failure mode when two hunks look similar (same file, same kind of change).
|
|
23
|
-
2. **
|
|
23
|
+
2. **Every hunk index present in the input must appear** across the union of every commit's \`hunks\` array. Use the \`index\` values exactly as shown — they are not necessarily contiguous, so never renumber them or invent indices.
|
|
24
24
|
3. Each commit must contain at least one hunk. No empty commits.
|
|
25
25
|
4. Each commit's \`id\` must be unique (e.g. \`commit-1\`, \`commit-2\`).
|
|
26
26
|
|
|
@@ -58,7 +58,7 @@ Return only the <output> tag and no other text.`}function c(e,t,o){let s=`${e?.c
|
|
|
58
58
|
## Critical constraints (output is rejected if violated)
|
|
59
59
|
|
|
60
60
|
1. **Each hunk index appears exactly once across the entire response.** Not zero. Not two. Exactly one occurrence — meaning it appears in exactly one branch, in exactly one commit within that branch, and only once within that commit's \`hunks\` array. Listing the same index twice inside one commit's \`hunks\`, OR listing it in two different commits, OR listing it in two different branches, all count as duplicates and all fail validation. This is the most common failure mode when two hunks look similar (same file, same kind of change).
|
|
61
|
-
2. **
|
|
61
|
+
2. **Every hunk index present in the input must appear** across the union of every commit's \`hunks\` array across every branch. Use the \`index\` values exactly as shown — they are not necessarily contiguous, so never renumber them or invent indices.
|
|
62
62
|
3. Each commit must contain at least one hunk. No empty commits.
|
|
63
63
|
4. Each commit \`id\` must be unique across the whole response.
|
|
64
64
|
|
|
@@ -109,7 +109,7 @@ Prefer more, smaller commits. Each distinct change should be its own commit.`:``
|
|
|
109
109
|
## Rules
|
|
110
110
|
|
|
111
111
|
1. Group related hunks by purpose. All hunks of a renamed file must share a commit.
|
|
112
|
-
2. Every hunk index
|
|
112
|
+
2. Every hunk index from the input in exactly one commit — no omissions, no duplicates. Use the \`index\` values exactly as shown; they are not necessarily contiguous, so never renumber them.
|
|
113
113
|
3. Each commit needs a unique ID (e.g. "commit-1", "commit-2").
|
|
114
114
|
4. Every commit must have at least one hunk. No empty commits.
|
|
115
115
|
5. ${e?.promptFragment?`Write commit messages following the repository's conventions (see below). Write a one-sentence explanation per commit.
|
|
@@ -132,7 +132,7 @@ ${e.promptFragment}`:`Write commit messages: first line under 50 chars, third-pe
|
|
|
132
132
|
## Commit rules
|
|
133
133
|
|
|
134
134
|
1. Group related hunks by purpose. All hunks of a renamed file must share a commit.
|
|
135
|
-
2. Every hunk index
|
|
135
|
+
2. Every hunk index from the input in exactly one commit — no omissions, no duplicates. Use the \`index\` values exactly as shown; they are not necessarily contiguous, so never renumber them.
|
|
136
136
|
3. Each commit needs a unique ID (e.g., "commit-1", "commit-2").
|
|
137
137
|
4. Every commit must have at least one hunk. No empty commits.
|
|
138
138
|
5. ${n}
|
|
@@ -152,11 +152,11 @@ JSON inside <output> tags. Each branch needs a unique ID and non-empty name.
|
|
|
152
152
|
|
|
153
153
|
CRITICAL: Every hunk index must appear exactly once across all commits in all branches. Every commit must contain at least one hunk. Missing hunks, duplicate hunks, or empty commits will fail validation. Each branch must have a unique ID and non-empty name.
|
|
154
154
|
|
|
155
|
-
Return only the <output> tag and no other text.`}function p(t){let{hunks:n,commitMessages:r,commitOrder:i,branchName:a,baseBranch:o,issueUrl:s,pullRequestUrl:c,instructions:l,condensed:u,redactHunkContent:d,inputTokenBudget:f,systemPromptChars:p,onBudgetReduction:m}=t,{prompt:
|
|
155
|
+
Return only the <output> tag and no other text.`}function p(t){let{hunks:n,commitMessages:r,commitOrder:i,branchName:a,baseBranch:o,issueUrl:s,pullRequestUrl:c,instructions:l,condensed:u,redactHunkContent:d,inputTokenBudget:f,systemPromptChars:p,onBudgetReduction:m}=t,h=n.map(e=>e.index).join(`, `),{prompt:g,hunksCondensedForBudget:_}=e({hunks:n,assemble:e=>{let t=`Organize these ${n.length} code hunks (indices: ${h}) into commits. Every listed index must appear exactly once.
|
|
156
156
|
|
|
157
157
|
<hunks>
|
|
158
158
|
${e}
|
|
159
159
|
</hunks>`,u=[];return a&&u.push(`Branch: ${a}`),o&&u.push(`Base: ${o}`),s&&u.push(`Issue: ${s}`),c&&u.push(`PR: ${c}`),u.length>0&&(t+=`\n\nContext:\n${u.join(`
|
|
160
160
|
`)}`),r?.length&&(t+=`\n\nOriginal commit messages (match their style):\n<commit_messages>\n${JSON.stringify(r)}\n</commit_messages>`),i?.length&&(t+=`\n\nOriginal commit order:\n${JSON.stringify(i)}`),l&&(t+=`\n\nAdditional instructions:\n${l}`),t+=`
|
|
161
161
|
|
|
162
|
-
Return only the <output> tag and no other text.`,t},condensed:u,redactHunkContent:d,inputTokenBudget:f,systemPromptChars:p});return m?.(
|
|
162
|
+
Return only the <output> tag and no other text.`,t},condensed:u,redactHunkContent:d,inputTokenBudget:f,systemPromptChars:p});return m?.(_),g}export{o as buildComposeGroupSystemPrompt,p as buildComposeGroupUserPrompt};
|
|
@@ -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
|
|
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};
|
|
@@ -7,7 +7,7 @@ import{assembleHunkPrompt as e}from"../utils/budget.js";import{groupingResultJso
|
|
|
7
7
|
## Critical constraints (output is rejected if violated)
|
|
8
8
|
|
|
9
9
|
1. **Each hunk index appears exactly once across the entire response.** Not zero. Not two. Exactly one occurrence — meaning it appears in exactly one commit's \`hunks\` array, and only once within that array. Listing the same index twice inside one commit's \`hunks\`, OR listing it in two different commits, both count as duplicates and both fail validation. This is the most common failure mode when two hunks look similar (same file, same kind of change).
|
|
10
|
-
2. **
|
|
10
|
+
2. **Every hunk index present in the input must appear** across the union of every commit's \`hunks\` array. Use the \`index\` values exactly as shown — they are not necessarily contiguous, so never renumber them or invent indices.
|
|
11
11
|
3. Each commit must contain at least one hunk. No empty commits.
|
|
12
12
|
4. Each commit's \`id\` must be unique (e.g. \`commit-1\`, \`commit-2\`).
|
|
13
13
|
|
|
@@ -42,7 +42,7 @@ Your job has two levels:
|
|
|
42
42
|
## Critical constraints (output is rejected if violated)
|
|
43
43
|
|
|
44
44
|
1. **Each hunk index appears exactly once across the entire response.** Not zero. Not two. Exactly one occurrence — meaning it appears in exactly one branch, in exactly one commit within that branch, and only once within that commit's \`hunks\` array. Listing the same index twice inside one commit's \`hunks\`, OR listing it in two different commits, OR listing it in two different branches, all count as duplicates and all fail validation. This is the most common failure mode when two hunks look similar (same file, same kind of change).
|
|
45
|
-
2. **
|
|
45
|
+
2. **Every hunk index present in the input must appear** across the union of every commit's \`hunks\` array across every branch. Use the \`index\` values exactly as shown — they are not necessarily contiguous, so never renumber them or invent indices.
|
|
46
46
|
3. Each commit must contain at least one hunk. No empty commits.
|
|
47
47
|
4. Each commit \`id\` must be unique across the whole response.
|
|
48
48
|
|
|
@@ -81,7 +81,7 @@ ${r}
|
|
|
81
81
|
${r}
|
|
82
82
|
- When in doubt, consider: "Would a reviewer want to see these changes together, or would separating them make review easier?" If separating helps, split.`}function u(e){return e===`consolidated`?`
|
|
83
83
|
Prefer fewer, larger commits. Combine related changes into a single commit when they serve the same purpose. A commit can touch many files if they are all part of the same logical change.`:e===`granular`?`
|
|
84
|
-
Prefer more, smaller commits. Each distinct change should be its own commit, even if multiple changes serve a similar purpose. Aim for commits that touch as few files as possible.`:``}function d(t){let{hunks:n,commitMessages:r,instructions:i,mode:a=`branches`,redactHunkContent:o,inputTokenBudget:s,systemPromptChars:c,onBudgetReduction:l}=t,u=a===`commits`?`Examine the following ${n.length} code hunks (indices
|
|
84
|
+
Prefer more, smaller commits. Each distinct change should be its own commit, even if multiple changes serve a similar purpose. Aim for commits that touch as few files as possible.`:``}function d(t){let{hunks:n,commitMessages:r,instructions:i,mode:a=`branches`,redactHunkContent:o,inputTokenBudget:s,systemPromptChars:c,onBudgetReduction:l}=t,u=n.map(e=>e.index).join(`, `),d=a===`commits`?`Examine the following ${n.length} code hunks (indices: ${u}) and organize them into commits. Every listed index must appear exactly once.`:`Examine the following ${n.length} code hunks (indices: ${u}) and organize them into commits and branches. Every listed index must appear exactly once.`,{prompt:f,hunksCondensedForBudget:p}=e({hunks:n,assemble:e=>{let t=`${d}
|
|
85
85
|
|
|
86
86
|
<hunks>
|
|
87
87
|
${e}
|
|
@@ -94,7 +94,7 @@ ${JSON.stringify(r)}
|
|
|
94
94
|
</commit_messages>`),i&&(t+=`
|
|
95
95
|
|
|
96
96
|
Carefully follow these additional instructions, but do not deviate from the output structure:
|
|
97
|
-
${i}`),t+=`\n\n${a===`commits`?`Now organize the hunks into commits.`:`Now organize the hunks into commits and branches.`} Return only the <output> tag and no other text.`,t},redactHunkContent:o,inputTokenBudget:s,systemPromptChars:c});return l?.(
|
|
97
|
+
${i}`),t+=`\n\n${a===`commits`?`Now organize the hunks into commits.`:`Now organize the hunks into commits and branches.`} Return only the <output> tag and no other text.`,t},redactHunkContent:o,inputTokenBudget:s,systemPromptChars:c});return l?.(p),f}function f(e,t){let n;return n=t===`commits`?`Based on your analysis of the changes, now organize the hunks into commits. Each hunk must be assigned to exactly one commit.`:p(e.strategy),e.conventions?.promptFragment&&(n+=`\n\n${e.conventions.promptFragment}`),e.commitMessages?.length&&(n+=`\n\nMatch the style of these original commit messages:\n${JSON.stringify(e.commitMessages)}`),e.instructions&&(n+=`\n\nAdditional instructions:\n${e.instructions}`),t===`commits`?n+=`
|
|
98
98
|
|
|
99
99
|
Return your grouping as JSON inside <output> tags. Wrap all commits in a single branch with id "default", name "", title "", description "":
|
|
100
100
|
<output>
|
|
@@ -2,6 +2,6 @@ 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
|
-
declare function validateGroupResponse(responseText: string,
|
|
5
|
+
declare function validateGroupResponse(responseText: string, expectedHunkIndices: ReadonlySet<number>, mode?: 'commits' | 'branches', existingBranchNames?: Set<string>): ValidationResult<GroupingResult>;
|
|
6
6
|
//#endregion
|
|
7
7
|
export { validateGroupResponse };
|
|
@@ -1,9 +1,9 @@
|
|
|
1
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 l=null;try{if(l=e(r),!l)return i([`No <output> tag found in response`],a(s));let u=t(n,JSON.parse(l));if(!u.success)return i(u.issues.map(e=>`${e.path.join(`.`)}: ${e.message}`),a(s));let d=u.data,f=[],p=new Set,m=[],h=new Map,g=new Set,_=[],v=[],y=new Set,b=new Set;for(let e of d.branches){if(y.has(e.id))return i([`Duplicate branch ID: '${e.id}'`],a(s));if(y.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(b.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(b.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}'.`)}let t=[];for(let n of e.commits){g.has(n.id)&&_.push(n.id),g.add(n.id);let r=[],i=s===`branches`?`${e.id}/${n.id}`:n.id;for(let e of n.hunks){p.has(e)&&m.push(e),p.add(e),r.push(e);let t=h.get(e);t?t.push(i):h.set(e,[i])}r.length===0&&v.push(n.id),t.push({id:n.id,message:n.message,explanation:n.explanation,hunkIndices:r})}f.push({id:e.id,name:e.name,title:e.title,description:e.description,commits:t})}if(_.length>0)return i([`Duplicate commit IDs: ${_.join(`, `)}`],`Some commit IDs appear multiple times. Each must be unique.\n\nDuplicates: ${_.join(`, `)}`);if(v.length>0)return i([`Empty commits (no hunks assigned): ${v.join(`, `)}`],`Every commit must contain at least one hunk. These commits have an empty \`hunks\` array: ${v.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 x=
|
|
4
|
-
`);e.push(`Duplicate hunks (each must appear in exactly ONE commit):\n${r}`),t.push(`Duplicate hunks: ${n.join(`, `)}`)}
|
|
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 x=[...o].filter(e=>!p.has(e)).sort((e,t)=>e-t),S=[...p].filter(e=>!o.has(e)).sort((e,t)=>e-t);if(m.length>0||x.length>0||S.length>0){let e=[],t=[];if(m.length>0){let n=[...new Set(m)].sort((e,t)=>e-t),r=n.map(e=>` hunk ${e} → ${(h.get(e)??[]).join(`, `)}`).join(`
|
|
4
|
+
`);e.push(`Duplicate hunks (each must appear in exactly ONE commit):\n${r}`),t.push(`Duplicate hunks: ${n.join(`, `)}`)}x.length>0&&(e.push(`Missing hunks (not assigned to any commit): ${x.join(`, `)}`),t.push(`Missing hunks: ${x.join(`, `)}`)),S.length>0&&(e.push(`Extra hunks (not present in the input): ${S.join(`, `)}`),t.push(`Extra hunks: ${S.join(`, `)}`));let n=[...o].sort((e,t)=>e-t),r=[...h.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 ${
|
|
6
|
+
Your response listed ${r} hunk references across ${h.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(`
|
|
@@ -20,7 +20,7 @@ ${d}
|
|
|
20
20
|
## Critical constraints (output is rejected if violated)
|
|
21
21
|
|
|
22
22
|
1. **Each hunk index appears exactly once across the entire response.** Not zero. Not two. Exactly one occurrence — across all branches, across all commits, and only once within any commit's \`hunks\` array. Listing the same index twice inside one commit, OR in two different commits, OR in two different branches, all count as duplicates and all fail validation. This is the most common failure mode when two hunks look similar (same file, same kind of change).
|
|
23
|
-
2. **
|
|
23
|
+
2. **Every hunk index present in the input must appear** across the union of every commit's \`hunks\` array across every branch. Use the \`index\` values exactly as shown — they are not necessarily contiguous, so never renumber them or invent indices.
|
|
24
24
|
3. Each commit must contain at least one hunk. No empty commits.
|
|
25
25
|
4. Each commit \`id\` must be unique across the whole response.
|
|
26
26
|
5. \`orderedCommitIds\` must list every commit ID in the branch in the order they should be applied.
|
|
@@ -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{buildQuickComposeSystemPrompt as r,buildQuickComposeUserPrompt as i}from"./prompt.js";import{validateQuickComposeResponse as a}from"./validator.js";async function o(o){let s=o.hunks,c=o.mode??`branches`,l=o.includePartitions??!1;if(!s||s.length===0)throw new e(`No hunks provided for quick compose`);if(o.session?.completedSteps.quickCompose)return{result:o.session.completedSteps.quickCompose,session:o.session,attempts:0,durationMs:0,callIds:[]};let
|
|
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{buildQuickComposeSystemPrompt as r,buildQuickComposeUserPrompt as i}from"./prompt.js";import{validateQuickComposeResponse as a}from"./validator.js";async function o(o){let s=o.hunks,c=o.mode??`branches`,l=o.includePartitions??!1;if(!s||s.length===0)throw new e(`No hunks provided for quick compose`);let u=new Set(s.map(e=>e.index));if(o.session?.completedSteps.quickCompose)return{result:o.session.completedSteps.quickCompose,session:o.session,attempts:0,durationMs:0,callIds:[]};let d=o.promptOverride?.system??r(o.conventions,c,o.strategy,l),f=n(i({hunks:s,commitMessages:o.commitMessages,commitOrder:o.commitOrder,branchName:o.branchName,baseBranch:o.baseBranch,issueUrl:o.issueUrl,pullRequestUrl:o.pullRequestUrl,instructions:o.instructions,redactHunkContent:o.redactHunkContent}),o.promptOverride),p=await t({model:o.model,systemPrompt:d,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=>a(e,u,c,l,o.existingBranchNames),taskName:`quick-compose`}),m=o.session?.completedSteps??{},h={...p.session,completedSteps:{...m,quickCompose:p.result}};return{result:p.result,session:h,usage:p.usage,attempts:p.attempts,durationMs:p.durationMs,callIds:p.callIds}}export{o as quickCompose};
|
|
@@ -2,6 +2,6 @@ import { ValidationResult } from "../ai-loop.js";
|
|
|
2
2
|
import { QuickComposeResult } from "./types.js";
|
|
3
3
|
|
|
4
4
|
//#region src/tasks/quick-compose/validator.d.ts
|
|
5
|
-
declare function validateQuickComposeResponse(responseText: string,
|
|
5
|
+
declare function validateQuickComposeResponse(responseText: string, expectedHunkIndices: ReadonlySet<number>, mode?: 'commits' | 'branches', includePartitions?: boolean, existingBranchNames?: Set<string>): ValidationResult<QuickComposeResult>;
|
|
6
6
|
//#endregion
|
|
7
7
|
export { validateQuickComposeResponse };
|
|
@@ -3,12 +3,12 @@ import{extractOutputTag as e}from"../ai-loop.js";import{validateJsonSchema as t}
|
|
|
3
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.`);if(v.length>0){let e=[...new Set(v)].sort((e,t)=>e-t),t=e.map(e=>` hunk ${e} → ${(y.get(e)??[]).join(`, `)}`).join(`
|
|
4
4
|
`),n=[...y.values()].reduce((e,t)=>e+t.length,0);return o([`Duplicate hunks: ${e.join(`, `)}`],`Some hunks appear in multiple commits. Each hunk can only be used once.
|
|
5
5
|
|
|
6
|
-
Your response listed ${n} hunk references across ${y.size} unique indices. Expected: ${c} references across ${c} unique indices.
|
|
6
|
+
Your response listed ${n} hunk references across ${y.size} unique indices. Expected: ${c.size} references across ${c.size} unique indices.
|
|
7
7
|
|
|
8
8
|
Duplicates (each hunk index → the commits it landed in):
|
|
9
9
|
${t}
|
|
10
10
|
|
|
11
|
-
To fix: 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.`)}let w=
|
|
11
|
+
To fix: 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.`)}let w=[...c].filter(e=>!_.has(e)).sort((e,t)=>e-t),T=[..._].filter(e=>!c.has(e)).sort((e,t)=>e-t);return w.length>0?o([`Missing hunks: ${w.join(`, `)}`],`Some hunks are not assigned to any commit. All hunks must be included.\n\nMissing: ${w.join(`, `)}`):T.length>0?o([`Extra hunks: ${T.join(`, `)}`],`Some hunks in your response don't exist in the input.\n\nExtra: ${T.join(`, `)}`):{valid:!0,result:{branches:C,rationale:m.rationale}}}catch(e){let t=e instanceof Error?e.message:String(e),n=(f??r).slice(-300);return o([`Failed to parse response as valid JSON: ${t}`,`Response tail: ${n}`],s(l,u))}}function i(e,t){let n=new Map(t.map((e,t)=>[e,t])),r=0;for(let t of e)for(let e of t.commitIds){let t=n.get(e);if(t===void 0||t!==r)return!1;r++}return r===t.length}function a(e){return e.startsWith(`-`)||e.startsWith(`.`)||e.endsWith(`.lock`)||e.endsWith(`.`)||e.endsWith(`/`)||e.includes(`..`)||e.includes(`~`)||e.includes(`^`)||e.includes(`:`)||e.includes(`\\`)||e.includes(` `)||e.includes(`?`)||e.includes(`*`)||e.includes(`[`)||e.includes(`@{`)?!1:e.length>0}function o(e,t){return{valid:!1,errors:e,retryFeedback:t}}function s(e,t){return`Your response has an invalid structure. Please return JSON inside <output> tags:
|
|
12
12
|
<output>
|
|
13
13
|
{
|
|
14
14
|
"branches": [
|
|
@@ -6,7 +6,7 @@ ${n}
|
|
|
6
6
|
|
|
7
7
|
If any locked commit's id is missing from your output, the validator will reject your response. Locked commits' hunk indices must remain exclusively in those commits — do NOT move them into other commits. You may reposition locked entries in the ordering only if the user explicitly asks; otherwise keep them roughly where they were.
|
|
8
8
|
|
|
9
|
-
The other commits (and their hunks) may be freely restructured (split, merged, reordered, re-messaged) per the user's request below.`)}e.instructions?.trim()?t.push(`USER'S REFINEMENT REQUEST:\n${e.instructions.trim()}`):t.push(`The user did not include specific instructions on this turn. Review your prior plan critically and produce an improved version that addresses any obvious issues with grouping, ordering, or commit messages.`);let n=e.mode===`branches`?`across every commit, across every branch`:`across every commit`,r=e.lockedCommits.length>0?`
|
|
9
|
+
The other commits (and their hunks) may be freely restructured (split, merged, reordered, re-messaged) per the user's request below.`)}e.instructions?.trim()?t.push(`USER'S REFINEMENT REQUEST:\n${e.instructions.trim()}`):t.push(`The user did not include specific instructions on this turn. Review your prior plan critically and produce an improved version that addresses any obvious issues with grouping, ordering, or commit messages.`);let n=e.mode===`branches`?`across every commit, across every branch`:`across every commit`,r=[...e.hunkIndices].sort((e,t)=>e-t),i=e.lockedCommits.length>0?`
|
|
10
10
|
- Every locked commit listed above appears in your output as its own entry, with id/message/hunks unchanged.`:``;return t.push(`OUTPUT FORMAT — emit JSON inside a single <output> tag. Every commit object MUST have ALL FOUR of these keys: "id" (a unique string), "message" (a string), "explanation" (a string describing what/why), and "hunks" (an array of integer indices). Omitting any of these keys — especially "hunks" — is a validation error.
|
|
11
11
|
|
|
12
12
|
This is a concrete example of the structure. Your actual ids, messages, explanations, and hunk assignments will differ — but EVERY commit entry must follow this shape:
|
|
@@ -31,10 +31,10 @@ This is a concrete example of the structure. Your actual ids, messages, explanat
|
|
|
31
31
|
Constraints (the validator enforces all of these):
|
|
32
32
|
- Each commit object contains "id" (unique string), "message" (string), "explanation" (string), and "hunks" (array of integers). All four keys are required.
|
|
33
33
|
- Each hunk index appears EXACTLY ONCE ${n}.
|
|
34
|
-
- Every hunk
|
|
35
|
-
- Each commit contains at least one hunk (no empty commits).${
|
|
34
|
+
- Every one of these hunk indices appears in some commit's hunks array: ${r.join(`, `)}. Total = ${String(r.length)} indices. Use the indices exactly as given; do not renumber them.
|
|
35
|
+
- Each commit contains at least one hunk (no empty commits).${i}
|
|
36
36
|
|
|
37
|
-
Before emitting, recount: the union of every commit's "hunks" array must contain exactly
|
|
37
|
+
Before emitting, recount: the union of every commit's "hunks" array must contain exactly these ${String(r.length)} integers: ${r.join(`, `)}.
|
|
38
38
|
|
|
39
39
|
Return only the <output> tag and no other text.`),t.join(`
|
|
40
40
|
|
|
@@ -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.
|
|
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};
|
|
@@ -44,9 +44,10 @@ interface RefineInput {
|
|
|
44
44
|
/** Free-text consumer-supplied context (e.g. UI-ordering notes) injected verbatim into
|
|
45
45
|
* the prompt. Library-opaque; the consumer is responsible for content. */
|
|
46
46
|
clientContext?: string;
|
|
47
|
-
/**
|
|
48
|
-
*
|
|
49
|
-
|
|
47
|
+
/** The hunk indices the prior plan was produced from — the hunks' original `index` values,
|
|
48
|
+
* which are not necessarily contiguous (e.g. when a `hunkFilter` dropped entries). Used to
|
|
49
|
+
* validate that the refined plan still covers exactly these indices, each exactly once. */
|
|
50
|
+
hunkIndices: ReadonlySet<number>;
|
|
50
51
|
/** Output mode — must match the mode the prior plan was produced in (the task's locked-
|
|
51
52
|
* commits substitution and re-validation rely on this). Defaults to `'branches'`. */
|
|
52
53
|
mode?: 'commits' | 'branches';
|
|
@@ -4,7 +4,7 @@ import { GroupingResult } from "../group/types.js";
|
|
|
4
4
|
|
|
5
5
|
//#region src/tasks/refine/validator.d.ts
|
|
6
6
|
interface ValidateRefineParams {
|
|
7
|
-
|
|
7
|
+
expectedHunkIndices: ReadonlySet<number>;
|
|
8
8
|
mode: 'commits' | 'branches';
|
|
9
9
|
lockedCommits: Map<string, CommitSuggestion>;
|
|
10
10
|
/** Maximal runs of consecutive locked commits per prior-plan branch, in original order.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{validateGroupResponse as e}from"../group/validator.js";function t(t,r){let i=e(t,r.
|
|
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).`}}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}):{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.`}}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};
|
|
@@ -10,7 +10,7 @@ import { OrderingResult } from "../tasks/order/types.js";
|
|
|
10
10
|
import { ComposeSession } from "../tasks/session.js";
|
|
11
11
|
import { PartitionStrategy } from "../tasks/partition/types.js";
|
|
12
12
|
import { ComposeContext, ComposeSource, ComposeTarget, OverrideSafetyFailures } from "./types.js";
|
|
13
|
-
import { SafetySnapshot, StashConflict } from "../git/types.js";
|
|
13
|
+
import { SafetySnapshot, StashConflict, StashNotRestored } from "../git/types.js";
|
|
14
14
|
import { ComposeBranchPlan } from "../git/apply-target.js";
|
|
15
15
|
import { AiModelPort, UndoId } from "@gitkraken/shared-tools";
|
|
16
16
|
|
|
@@ -127,6 +127,8 @@ interface ApplyComposePlanResult {
|
|
|
127
127
|
unappliedHunkIndices?: number[];
|
|
128
128
|
/** Set when the post-apply stash pop hit conflicts. Apply itself succeeded; the named stash is preserved. */
|
|
129
129
|
stashConflict?: StashConflict;
|
|
130
|
+
/** Set when the post-apply stash pop was skipped or failed without conflicts. Apply itself succeeded. */
|
|
131
|
+
stashNotRestored?: StashNotRestored;
|
|
130
132
|
}
|
|
131
133
|
/** Controls how many AI pipeline steps run and how deeply each reasons. */
|
|
132
134
|
type ComposeDepth = 'quick' | 'balanced' | 'deep';
|
|
@@ -457,6 +459,8 @@ interface ComposeResult {
|
|
|
457
459
|
unappliedHunkIndices?: number[];
|
|
458
460
|
/** See {@link ApplyComposePlanResult.stashConflict}. */
|
|
459
461
|
stashConflict?: StashConflict;
|
|
462
|
+
/** See {@link ApplyComposePlanResult.stashNotRestored}. */
|
|
463
|
+
stashNotRestored?: StashNotRestored;
|
|
460
464
|
}
|
|
461
465
|
/**
|
|
462
466
|
* Compose code changes into a clean set of commits and branches.
|
|
@@ -505,4 +509,4 @@ declare function composePlan(input: ComposePlanInput): Promise<ComposePlanResult
|
|
|
505
509
|
*/
|
|
506
510
|
declare function applyComposePlan(input: ApplyComposePlanInput): Promise<ApplyComposePlanResult>;
|
|
507
511
|
//#endregion
|
|
508
|
-
export { ApplyComposePlanInput, ApplyComposePlanResult, ComposeApplyPlan, type ComposeBranchPlan, type ComposeContext, ComposeDepth, ComposeDiffStats, ComposeInput, ComposePlan, ComposePlanInput, ComposePlanResult, ComposeProgressEvent, ComposeResult, type ComposeSource, ComposeStrategy, type ComposeTarget, type OverrideSafetyFailures, type StashConflict, applyComposePlan, compose, composePlan, defaultPhaseMessages };
|
|
512
|
+
export { ApplyComposePlanInput, ApplyComposePlanResult, ComposeApplyPlan, type ComposeBranchPlan, type ComposeContext, ComposeDepth, ComposeDiffStats, ComposeInput, ComposePlan, ComposePlanInput, ComposePlanResult, ComposeProgressEvent, ComposeResult, type ComposeSource, ComposeStrategy, type ComposeTarget, type OverrideSafetyFailures, type StashConflict, type StashNotRestored, applyComposePlan, compose, composePlan, defaultPhaseMessages };
|
|
@@ -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,_.hunks)):S;s?.({phase:`applying`,...L.applying});let ue=Date.now(),$=await E({git:i,source:o,target:p,branchPlans:R.branchPlans,allOrderedCommits:H,hunks:_.hunks,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}}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}}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,b.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}}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(`
|
|
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
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 oe(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 se(e){let t=(await o(e,{format:`%(refname:short)`,prefix:`refs/heads/`})).trim().split(`
|
|
3
3
|
`).filter(Boolean);return new Set(t)}async function ce(e,t,n,r,i){let a=I(),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 P(D(e,`undo manifest persistence`)).save(l),a}export{U as applyComposePlan,R as compose,H as composePlan,L as defaultPhaseMessages};
|
|
@@ -19,7 +19,7 @@ type RefineProgressEvent = {
|
|
|
19
19
|
*
|
|
20
20
|
* The workflow takes a higher-level shape than the task does — it accepts a full
|
|
21
21
|
* {@link ComposePlan} and a list of `lockedCommits` *as ids*, materializes those ids into
|
|
22
|
-
* `CommitSuggestion` objects from the prior plan, derives the hunk
|
|
22
|
+
* `CommitSuggestion` objects from the prior plan, derives the hunk index set from the prior
|
|
23
23
|
* plan's commits, and lifts the task's `GroupingResult` back into a full `ComposePlan`. The
|
|
24
24
|
* task itself stays lower-level and reusable.
|
|
25
25
|
*/
|
|
@@ -31,7 +31,7 @@ interface RefinePlanInput {
|
|
|
31
31
|
*/
|
|
32
32
|
session: ComposeSession;
|
|
33
33
|
/** The plan to be refined. Used to resolve `lockedCommits` ids to their fixed content
|
|
34
|
-
* and to derive
|
|
34
|
+
* and to derive the hunk index set + output mode (commits vs branches) for validation. */
|
|
35
35
|
priorPlan: ComposePlan;
|
|
36
36
|
model: AiModelPort;
|
|
37
37
|
/** The user's refinement instructions. If absent, the prompt uses a generic "review your
|
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,
|
|
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};
|
|
@@ -26,6 +26,12 @@ type ComposeSource = {
|
|
|
26
26
|
branch: string; /** First commit in the range to recompose (inclusive). Must be an ancestor of `to`. */
|
|
27
27
|
from: string; /** Last commit in the range to recompose (inclusive). */
|
|
28
28
|
to: string;
|
|
29
|
+
/**
|
|
30
|
+
* Fold working-directory changes into the composed input. Each layer defaults to
|
|
31
|
+
* `true`. A layer excluded via `includeStaged`/`includeUnstaged` must be clean —
|
|
32
|
+
* collection rejects the compose when it holds changes, since a range rewrite
|
|
33
|
+
* would otherwise discard them when the worktree is reset to the new tip.
|
|
34
|
+
*/
|
|
29
35
|
includeWorkdir?: {
|
|
30
36
|
includeStaged?: boolean;
|
|
31
37
|
includeUnstaged?: boolean;
|
package/package.json
CHANGED