@gitkraken/compose-tools 0.2.0 → 0.3.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.
Files changed (49) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/README.md +38 -17
  3. package/dist/git/apply-target.d.ts +5 -9
  4. package/dist/git/apply-target.js +1 -1
  5. package/dist/git/commit-chain.d.ts +36 -42
  6. package/dist/git/commit-chain.js +7 -7
  7. package/dist/git/diff-collector.d.ts +40 -17
  8. package/dist/git/diff-collector.js +6 -7
  9. package/dist/git/index.d.ts +2 -2
  10. package/dist/git/index.js +1 -1
  11. package/dist/git/port-dispatch.d.ts +4 -2
  12. package/dist/git/port-dispatch.js +4 -3
  13. package/dist/git/reparent.d.ts +39 -0
  14. package/dist/git/reparent.js +5 -0
  15. package/dist/git/safety-range.d.ts +33 -0
  16. package/dist/git/safety-range.js +5 -0
  17. package/dist/git/types.d.ts +10 -14
  18. package/dist/git/utils/authorship.d.ts +22 -0
  19. package/dist/git/utils/authorship.js +1 -0
  20. package/dist/git/utils/hunk-parser.d.ts +1 -1
  21. package/dist/git/utils/hunk-parser.js +1 -1
  22. package/dist/index.d.ts +3 -2
  23. package/dist/index.js +1 -1
  24. package/dist/ports/git.d.ts +22 -1
  25. package/dist/tasks/analyze/analyze.js +1 -1
  26. package/dist/tasks/analyze/prompt.d.ts +2 -0
  27. package/dist/tasks/analyze/prompt.js +5 -5
  28. package/dist/tasks/analyze/types.d.ts +3 -0
  29. package/dist/tasks/compose-group/compose-group.js +1 -1
  30. package/dist/tasks/compose-group/prompt.d.ts +2 -0
  31. package/dist/tasks/compose-group/prompt.js +29 -29
  32. package/dist/tasks/compose-group/types.d.ts +3 -0
  33. package/dist/tasks/group/group.js +1 -1
  34. package/dist/tasks/group/prompt.d.ts +2 -0
  35. package/dist/tasks/group/prompt.js +22 -22
  36. package/dist/tasks/group/types.d.ts +3 -0
  37. package/dist/tasks/quick-compose/prompt.d.ts +2 -0
  38. package/dist/tasks/quick-compose/prompt.js +11 -11
  39. package/dist/tasks/quick-compose/quick-compose.js +1 -1
  40. package/dist/tasks/quick-compose/types.d.ts +3 -0
  41. package/dist/tasks/types.d.ts +10 -9
  42. package/dist/tasks/utils/redact.d.ts +14 -0
  43. package/dist/tasks/utils/redact.js +1 -0
  44. package/dist/workflows/compose.d.ts +25 -14
  45. package/dist/workflows/compose.js +3 -2
  46. package/dist/workflows/split.d.ts +3 -0
  47. package/dist/workflows/split.js +2 -2
  48. package/dist/workflows/types.d.ts +41 -18
  49. package/package.json +2 -2
@@ -1,2 +1,3 @@
1
- import{GitError as e}from"../errors/git.js";import{ComposeWorkflowError as t,ComposeWorkflowInputError as n}from"../errors/workflow.js";import{addUsage as r,emptyUsage as i}from"../ai/types.js";import{analyze as a}from"../tasks/analyze/analyze.js";import{composeGroup as o}from"../tasks/compose-group/compose-group.js";import{group as s}from"../tasks/group/group.js";import{order as c}from"../tasks/order/order.js";import{partition as l}from"../tasks/partition/partition.js";import{applyPatchToIndex as u,diffTree as d,forEachRef as f,readTree as p,revParse as m,writeTree as h}from"../git/port-dispatch.js";import{buildPatchForCommit as g}from"../git/utils/patch-builder.js";import{sha256 as _}from"../git/utils/crypto.js";import{collectFromSource as v}from"../git/diff-collector.js";import{verifySnapshot as y}from"../git/safety.js";import{applyToTarget as b}from"../git/apply-target.js";import{requireExec as x}from"../git/require-exec.js";import{extractConventions as S}from"../git/utils/conventions.js";import{rethrowAsWorkflowError as C}from"../utils/cancellation.js";import{randomUUID as w}from"node:crypto";import{unlinkSync as T}from"node:fs";import{tmpdir as E}from"node:os";import{join as D}from"node:path";import{UndoStore as O,capturePreOperationState as k,generateUndoId as A}from"@gitkraken/shared-tools";const j={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 M(r){if(!r.applyPlan&&!r.model)throw new n(`Either 'model' or 'applyPlan' must be provided`);if(r.applyPlan)return B(r);let{git:i,model:a,source:o={type:`workdir`},onProgress:s,onBeforeApply:c,cancellation:l}=r,u=a,d=r.depth??`balanced`,f=r.target??Y(o);Z(o,f);let p=[];s?.({phase:`collecting`,...j.collecting});let m=Date.now(),h=await v(i,o);if(p.push({name:`collecting`,durationMs:Date.now()-m,attempts:1}),h.hunks.length===0)throw new e(`NO_CHANGES`,X(o));let{hunks:g,filteredCount:_}=await U(h.hunks,r.hunkFilter);if(g.length===0)throw new e(`NO_CHANGES`,X(o));let w=_!==void 0&&_>0?await W(i,h.snapshot,g):h.snapshot,T={hunks:g,collection:h,conventions:r.conventions??await S({git:i,author:r.conventionAuthor}),existingBranchNames:await ee(i),groupMode:f.type===`auto`?`branches`:`commits`,target:f,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},E;try{E=await N(d,T)}catch(e){C(e,l)}let D={analysis:E.analysis,grouping:E.grouping,ordering:E.ordering,branches:E.branchPlans,allOrderedCommits:E.allOrderedCommits};if(p.push(...E.phases),c&&!await c(D))return{plan:D,snapshot:w,session:E.session,usage:E.usage,phases:p,source:{type:o.type===`branch`?`branch`:`workdir`,hunks:g,commitMessages:h.commitMessages,branchName:h.branchName,baseBranch:h.baseBranch},diffStats:V(g),feedbackTargetCallId:H(p),hunksFilteredCount:_};let O=await k(x(i,`capturing pre-operation state`));s?.({phase:`verifying`,...j.verifying});let A=Date.now(),M=await y(i,h.snapshot,G(o,h));if(!M.valid)throw new t(`SAFETY_CHECK_FAILED`,`Safety check failed: ${M.error}`);p.push({name:`verifying`,durationMs:Date.now()-A,attempts:1});let P=K(E.allOrderedCommits,r.applyUpTo,f),{leftoverHunks:F,leftoverPatch:I,leftoverPatchBranchOnly:L}=J(P,h.hunks),R=F.length>0?await W(i,w,q(P,h.hunks)):w;s?.({phase:`applying`,...j.applying});let z=Date.now(),Q=await b({git:i,target:f,branchPlans:E.branchPlans,allOrderedCommits:P,hunks:h.hunks,snapshot:R,collection:h,signing:r.signing,authorAttribution:r.authorAttribution,stashLabel:r.stashLabel,leftoverPatch:I,leftoverPatchBranchOnly:L});p.push({name:`applying`,durationMs:Date.now()-z,attempts:1});let te=await $(i,O,Q.mutations,f,o);return{plan:D,snapshot:R,partitions:Q.partitions,partitionGroups:Q.partitionGroups,session:E.session,usage:E.usage,phases:p,source:{type:o.type===`branch`?`branch`:`workdir`,hunks:g,commitMessages:h.commitMessages,branchName:h.branchName,baseBranch:h.baseBranch},diffStats:V(g),commitShas:Q.commitShas,undoId:te,feedbackTargetCallId:H(p),hunksFilteredCount:_,appliedCommitIds:P.map(e=>e.id),unappliedHunkIndices:F.length>0?F.map(e=>e.index):void 0,stashConflict:Q.stashConflict}}function N(e,t){switch(e){case`quick`:return P(t);case`balanced`:return F(t);case`deep`:return L(t)}}async function P(e){return I(e,{lightweight:!0,skipOrder:!0})}async function F(e){return I(e,{includeAnalysis:!0})}async function I(e,t){let{hunks:n,collection:a,conventions:s,existingBranchNames:u,groupMode:d,target:f,model:p,instructions:m,context:h,strategy:g,maxRetries:_,maxTokens:v,onProgress:y,cancellation:b,onBeforeModelCall:x,onAfterModelCall:S,onBeforePrompt:C}=e,w=i(),T=[],E=await o({hunks:n,commitMessages:a.commitMessages,commitOrder:a.commitOrder,branchName:a.branchName,baseBranch:a.baseBranch,issueUrl:h?.issueUrl,pullRequestUrl:h?.pullRequestUrl,instructions:m,conventions:s,mode:d,strategy:g?.grouping,existingBranchNames:d===`branches`?u:void 0,includeAnalysis:t.includeAnalysis,lightweight:t.lightweight,condensed:t.condensed,model:p,session:e.session,maxRetries:_,maxTokens:v,onProgress:e=>y?.({phase:`compose-grouping`,...j[`compose-grouping`],event:e}),cancellation:b,onBeforeModelCall:x,onAfterModelCall:S,onBeforePrompt:C,promptOverride:e.promptOverrides?.[`compose-group`]});E.usage&&r(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 c({branches:E.result.branches,hunks:n,instructions:m,model:p,session:E.session,maxRetries:_,maxTokens:v,onProgress:e=>y?.({phase:`ordering`,...j.ordering,event:e}),cancellation:b,onBeforeModelCall:x,onAfterModelCall:S,onBeforePrompt:C,promptOverride:e.promptOverrides?.order});t.usage&&r(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(f.type===`auto`){let t=new Set(u);for(let e of E.result.branches)e.name&&t.add(e.name);for(let n of O){let i=await l({commits:Q(n.branchGroup,n.orderedCommitIds),orderedCommitIds:n.orderedCommitIds,sourceBranchName:n.branchGroup.name,instructions:m,conventions:s,strategy:g?.partition,existingBranchNames:t,model:p,maxRetries:_,maxTokens:v,onProgress:e=>y?.({phase:`partitioning`,...j.partitioning,branch:n.branchGroup.name,event:e}),cancellation:b,onBeforeModelCall:x,onAfterModelCall:S,onBeforePrompt:C,promptOverride:e.promptOverrides?.partition});i.usage&&r(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,D=i.session;for(let e of i.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}}async function L(e){let{hunks:t,collection:n,conventions:o,existingBranchNames:u,groupMode:d,target:f,model:p,instructions:m,context:h,strategy:g,maxRetries:_,maxTokens:v,onProgress:y,cancellation:b,onBeforeModelCall:x,onAfterModelCall:S,onBeforePrompt:C}=e,w=i(),T=[],E=await a({hunks:t,commitMessages:n.commitMessages,commitOrder:n.commitOrder,branchName:n.branchName,baseBranch:n.baseBranch,issueUrl:h?.issueUrl,pullRequestUrl:h?.pullRequestUrl,instructions:m,model:p,session:e.session,maxRetries:_,maxTokens:v,onProgress:e=>y?.({phase:`analyzing`,...j.analyzing,event:e}),cancellation:b,onBeforeModelCall:x,onAfterModelCall:S,onBeforePrompt:C,promptOverride:e.promptOverrides?.analyze});E.usage&&r(w,E.usage),T.push({name:`analyzing`,durationMs:E.durationMs,attempts:E.attempts,usage:E.usage,callIds:E.callIds});let D=await s({hunks:t,commitMessages:n.commitMessages,instructions:m,conventions:o,mode:d,strategy:g?.grouping,existingBranchNames:d===`branches`?u:void 0,model:p,session:E.session,maxRetries:_,maxTokens:v,onProgress:e=>y?.({phase:`grouping`,...j.grouping,event:e}),cancellation:b,onBeforeModelCall:x,onAfterModelCall:S,onBeforePrompt:C,promptOverride:e.promptOverrides?.group});D.usage&&r(w,D.usage),T.push({name:`grouping`,durationMs:D.durationMs,attempts:D.attempts,usage:D.usage,callIds:D.callIds});let O=await c({branches:D.result.branches,hunks:t,instructions:m,model:p,session:D.session,maxRetries:_,maxTokens:v,onProgress:e=>y?.({phase:`ordering`,...j.ordering,event:e}),cancellation:b,onBeforeModelCall:x,onAfterModelCall:S,onBeforePrompt:C,promptOverride:e.promptOverrides?.order});O.usage&&r(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(f.type===`auto`){let t=new Set(u);for(let e of D.result.branches)e.name&&t.add(e.name);for(let n of k){let i=await l({commits:Q(n.branchGroup,n.orderedCommitIds),orderedCommitIds:n.orderedCommitIds,sourceBranchName:n.branchGroup.name,instructions:m,conventions:o,strategy:g?.partition,existingBranchNames:t,model:p,maxRetries:_,maxTokens:v,onProgress:e=>y?.({phase:`partitioning`,...j.partitioning,branch:n.branchGroup.name,event:e}),cancellation:b,onBeforeModelCall:x,onAfterModelCall:S,onBeforePrompt:C,promptOverride:e.promptOverrides?.partition});i.usage&&r(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 M=k.flatMap(e=>Q(e.branchGroup,e.orderedCommitIds));return{analysis:E.result,grouping:D.result,ordering:O.result,branchPlans:k,allOrderedCommits:M,session:A,usage:w,phases:T}}async function R(e){let t=await M({...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}}async function z(e){let t=await M({git:e.git,applyPlan:e.applyPlan,source:e.source,target:e.target,onProgress:e.onProgress,signing:e.signing,authorAttribution:e.authorAttribution,applyUpTo:e.applyUpTo,stashLabel:e.stashLabel,hunkFilter:e.hunkFilter});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 B(n){let{git:r,applyPlan:a,onProgress:o}=n,{plan:s,snapshot:c,source:l}=a,u=n.source??l,d=n.target??a?.target??Y(u);Z(u,d);let f=await k(x(r,`capturing pre-operation state`)),p=[];o?.({phase:`collecting`,...j.collecting});let m=Date.now(),h=await v(r,u);if(p.push({name:`collecting`,durationMs:Date.now()-m,attempts:1}),h.hunks.length===0)throw new e(`NO_CHANGES`,X(u));let{hunks:g}=await U(h.hunks,n.hunkFilter);if(g.length===0)throw new e(`NO_CHANGES`,X(u));let _=g.length<h.hunks.length?await W(r,h.snapshot,g):h.snapshot,S={...h,hunks:g,snapshot:_};if(S.snapshot.diffHash!==c.diffHash)throw new t(`SAFETY_CHECK_FAILED`,`Working directory has changed since the plan was created. Re-run compose to generate a new plan.`);o?.({phase:`verifying`,...j.verifying});let C=Date.now(),w=await y(r,S.snapshot,G(u,S));if(!w.valid)throw new t(`SAFETY_CHECK_FAILED`,`Safety check failed: ${w.error}`);p.push({name:`verifying`,durationMs:Date.now()-C,attempts:1});let T=K(s.allOrderedCommits,n.applyUpTo,d),{leftoverHunks:E,leftoverPatch:D,leftoverPatchBranchOnly:O}=J(T,S.hunks),A=E.length>0?await W(r,S.snapshot,q(T,S.hunks)):S.snapshot;o?.({phase:`applying`,...j.applying});let M=Date.now(),N=await b({git:r,target:d,branchPlans:s.branches,allOrderedCommits:T,hunks:S.hunks,snapshot:A,collection:S,signing:n.signing,authorAttribution:n.authorAttribution,stashLabel:n.stashLabel,leftoverPatch:D,leftoverPatchBranchOnly:O});p.push({name:`applying`,durationMs:Date.now()-M,attempts:1});let P=await $(r,f,N.mutations,d,u);return{plan:s,snapshot:A,partitions:N.partitions,partitionGroups:N.partitionGroups,session:{messages:[],systemPrompt:``,completedSteps:{}},usage:i(),phases:p,source:{type:u.type===`branch`?`branch`:`workdir`,hunks:S.hunks,commitMessages:S.commitMessages,branchName:S.branchName,baseBranch:S.baseBranch},diffStats:V(S.hunks),commitShas:N.commitShas,undoId:P,appliedCommitIds:T.map(e=>e.id),unappliedHunkIndices:E.length>0?E.map(e=>e.index):void 0,stashConflict:N.stashConflict}}function V(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 H(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 U(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 W(e,t,n){let r=t.baseSha??t.headSha,i=D(E(),`compose-filtered-idx-${w()}`),a={GIT_INDEX_FILE:i};try{await p(e,r,{indexEnv:a});let i=g(n);i.length>0&&await u(e,i,{indexEnv:a});let o=await d(e,r,await h(e,{indexEnv:a}));return{...t,diffHash:_(o)}}finally{try{T(i)}catch{}}}function G(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}}function K(e,t,r){if(!t||t.kind===`all`)return e;if(r.type===`auto`)throw new n(`applyUpTo is not supported with target type 'auto'. Use 'head', 'branch', 'branch-replace', or 'rewrite-range'.`);if(t.kind===`count`){let r=t.count;if(!Number.isInteger(r)||r<0||r>e.length)throw new n(`applyUpTo.count ${String(r)} is out of range (0 to ${e.length})`);return e.slice(0,r)}let i=e.findIndex(e=>e.id===t.id);if(i===-1)throw new n(`applyUpTo.id '${t.id}' not found in plan.allOrderedCommits`);return e.slice(0,i+1)}function q(e,t){let n=new Set(e.flatMap(e=>e.hunkIndices));return t.filter(e=>n.has(e.index))}function J(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,leftoverPatchBranchOnly:void 0};let i=g(r),a=r.filter(e=>e.source===`branch`),o=a.length>0?g(a):``;return{leftoverHunks:r,leftoverPatch:i.length>0?i:void 0,leftoverPatchBranchOnly:o.length>0?o:void 0}}function Y(e){return e.type===`branch`?{type:`branch-replace`}:e.type===`commit-range`?{type:`rewrite-range`,branch:e.branch,rangeFrom:e.from,rangeTo:e.to}:{type:`head`}}function X(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 Z(e,t){if(t.type===`branch-replace`&&e.type!==`branch`)throw new n(`target 'branch-replace' requires source type 'branch'`);if(t.type===`rewrite-range`&&e.type!==`commit-range`&&e.type!==`branch`)throw new n(`target 'rewrite-range' requires source type 'commit-range' or 'branch'`)}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 ee(e){let t=(await f(e,{format:`%(refname:short)`,prefix:`refs/heads/`})).trim().split(`
2
- `).filter(Boolean);return new Set(t)}async function $(e,t,n,r,i){let a=A(),o=n.createdBranches.length||1,s=`compose: ${o} branch${o===1?``:`es`} from ${i.type}`,c=[...n.movedBranches];if(r.type===`head`&&t.headBranch){let n=await m(e,`HEAD`);n!==t.headSha&&c.push({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,createdBranches:n.createdBranches,deletedBranches:n.deletedBranches,movedBranches:c};return await new O(x(e,`undo manifest persistence`)).save(l),a}export{z as applyComposePlan,M as compose,R as composePlan,j as defaultPhaseMessages};
1
+ import{GitError as e}from"../errors/git.js";import{ComposeWorkflowError as t,ComposeWorkflowInputError as n}from"../errors/workflow.js";import{addUsage as r,emptyUsage as i}from"../ai/types.js";import{analyze as a}from"../tasks/analyze/analyze.js";import{composeGroup as o}from"../tasks/compose-group/compose-group.js";import{group as s}from"../tasks/group/group.js";import{order as c}from"../tasks/order/order.js";import{partition as l}from"../tasks/partition/partition.js";import{applyPatchToIndex as u,currentBranch as d,diffTree as f,forEachRef as p,log as m,mergeBase as h,readTree as g,revParse as _,writeTree as v}from"../git/port-dispatch.js";import{buildPatchForCommit as y}from"../git/utils/patch-builder.js";import{sha256 as b}from"../git/utils/crypto.js";import{collectFromSource as x}from"../git/diff-collector.js";import{verifySnapshot as S}from"../git/safety.js";import{applyToTarget as C}from"../git/apply-target.js";import{requireExec as w}from"../git/require-exec.js";import{validateRewriteRangeSafety as T}from"../git/safety-range.js";import{extractConventions as E}from"../git/utils/conventions.js";import{rethrowAsWorkflowError as D}from"../utils/cancellation.js";import{randomUUID as O}from"node:crypto";import{unlinkSync as k}from"node:fs";import{tmpdir as A}from"node:os";import{join as j}from"node:path";import{UndoStore as M,capturePreOperationState as N,generateUndoId as P}from"@gitkraken/shared-tools";const F={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 I(r){if(!r.applyPlan&&!r.model)throw new n(`Either 'model' or 'applyPlan' must be provided`);if(r.applyPlan)return re(r);let{git:i,model:a,source:o={type:`workdir`},onProgress:s,onBeforeApply:c,cancellation:l}=r,u=a,d=r.depth??`balanced`,f=r.target??await X(i,o);Q(o,f),await K(i,r.applyCommitIds,r.hunkFilter,o,f),await T({git:i,source:o,target:f});let p=[];s?.({phase:`collecting`,...F.collecting});let m=Date.now(),h=await x(i,o);if(p.push({name:`collecting`,durationMs:Date.now()-m,attempts:1}),h.hunks.length===0)throw new e(`NO_CHANGES`,Z(o));let{hunks:g,filteredCount:_}=await U(h.hunks,r.hunkFilter);if(g.length===0)throw new e(`NO_CHANGES`,Z(o));let v=_!==void 0&&_>0?await W(i,h.snapshot,g):h.snapshot,y={hunks:g,collection:h,conventions:r.conventions??await E({git:i,author:r.conventionAuthor}),existingBranchNames:await ie(i),groupMode:f.type===`auto`?`branches`:`commits`,target:f,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:r.redactHunkContent},b;try{b=await ee(d,y)}catch(e){D(e,l)}let O={analysis:b.analysis,grouping:b.grouping,ordering:b.ordering,branches:b.branchPlans,allOrderedCommits:b.allOrderedCommits};if(p.push(...b.phases),c&&!await c(O))return{plan:O,snapshot:v,session:b.session,usage:b.usage,phases:p,source:{type:o.type===`branch`?`branch`:`workdir`,hunks:g,commitMessages:h.commitMessages,branchName:h.branchName,baseBranch:h.baseBranch},diffStats:V(g),feedbackTargetCallId:H(p),hunksFilteredCount:_};let k=await N(w(i,`capturing pre-operation state`));s?.({phase:`verifying`,...F.verifying});let A=Date.now(),j=await S(i,h.snapshot,G(o,h));if(!j.valid)throw new t(`SAFETY_CHECK_FAILED`,`Safety check failed: ${j.error}`);p.push({name:`verifying`,durationMs:Date.now()-A,attempts:1});let M=q(b.allOrderedCommits,r.applyCommitIds),{leftoverHunks:P,leftoverPatch:I}=Y(M,h.hunks),L=P.length>0?await W(i,v,J(M,h.hunks)):v;s?.({phase:`applying`,...F.applying});let R=Date.now(),z=await C({git:i,source:o,target:f,branchPlans:b.branchPlans,allOrderedCommits:M,hunks:h.hunks,snapshot:L,collection:h,signing:r.signing,authorAttribution:r.authorAttribution,stashLabel:r.stashLabel,leftoverPatch:I});p.push({name:`applying`,durationMs:Date.now()-R,attempts:1});let B=await ae(i,k,z.mutations,f,o);return{plan:O,snapshot:L,partitions:z.partitions,partitionGroups:z.partitionGroups,session:b.session,usage:b.usage,phases:p,source:{type:o.type===`branch`?`branch`:`workdir`,hunks:g,commitMessages:h.commitMessages,branchName:h.branchName,baseBranch:h.baseBranch},diffStats:V(g),commitShas:z.commitShas,undoId:B,feedbackTargetCallId:H(p),hunksFilteredCount:_,appliedCommitIds:M.map(e=>e.id),unappliedHunkIndices:P.length>0?P.map(e=>e.index):void 0,stashConflict:z.stashConflict}}function ee(e,t){switch(e){case`quick`:return L(t);case`balanced`:return R(t);case`deep`:return B(t)}}async function L(e){return z(e,{lightweight:!0,skipOrder:!0})}async function R(e){return z(e,{includeAnalysis:!0})}async function z(e,t){let{hunks:n,collection:a,conventions:s,existingBranchNames:u,groupMode:d,target:f,model:p,instructions:m,context:h,strategy:g,maxRetries:_,maxTokens:v,onProgress:y,cancellation:b,onBeforeModelCall:x,onAfterModelCall:S,onBeforePrompt:C}=e,w=i(),T=[],E=await o({hunks:n,commitMessages:a.commitMessages,commitOrder:a.commitOrder,branchName:a.branchName,baseBranch:a.baseBranch,issueUrl:h?.issueUrl,pullRequestUrl:h?.pullRequestUrl,instructions:m,conventions:s,mode:d,strategy:g?.grouping,existingBranchNames:d===`branches`?u:void 0,includeAnalysis:t.includeAnalysis,lightweight:t.lightweight,condensed:t.condensed,redactHunkContent:e.redactHunkContent,model:p,session:e.session,maxRetries:_,maxTokens:v,onProgress:e=>y?.({phase:`compose-grouping`,...F[`compose-grouping`],event:e}),cancellation:b,onBeforeModelCall:x,onAfterModelCall:S,onBeforePrompt:C,promptOverride:e.promptOverrides?.[`compose-group`]});E.usage&&r(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 c({branches:E.result.branches,hunks:n,instructions:m,model:p,session:E.session,maxRetries:_,maxTokens:v,onProgress:e=>y?.({phase:`ordering`,...F.ordering,event:e}),cancellation:b,onBeforeModelCall:x,onAfterModelCall:S,onBeforePrompt:C,promptOverride:e.promptOverrides?.order});t.usage&&r(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(f.type===`auto`){let t=new Set(u);for(let e of E.result.branches)e.name&&t.add(e.name);for(let n of O){let i=await l({commits:$(n.branchGroup,n.orderedCommitIds),orderedCommitIds:n.orderedCommitIds,sourceBranchName:n.branchGroup.name,instructions:m,conventions:s,strategy:g?.partition,existingBranchNames:t,model:p,maxRetries:_,maxTokens:v,onProgress:e=>y?.({phase:`partitioning`,...F.partitioning,branch:n.branchGroup.name,event:e}),cancellation:b,onBeforeModelCall:x,onAfterModelCall:S,onBeforePrompt:C,promptOverride:e.promptOverrides?.partition});i.usage&&r(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,D=i.session;for(let e of i.result.partitions)t.add(e.branchName)}}let A=O.flatMap(e=>$(e.branchGroup,e.orderedCommitIds));return{analysis:void 0,grouping:E.result,ordering:k,branchPlans:O,allOrderedCommits:A,session:D,usage:w,phases:T}}async function B(e){let{hunks:t,collection:n,conventions:o,existingBranchNames:u,groupMode:d,target:f,model:p,instructions:m,context:h,strategy:g,maxRetries:_,maxTokens:v,onProgress:y,cancellation:b,onBeforeModelCall:x,onAfterModelCall:S,onBeforePrompt:C}=e,w=i(),T=[],E=await a({hunks:t,commitMessages:n.commitMessages,commitOrder:n.commitOrder,branchName:n.branchName,baseBranch:n.baseBranch,issueUrl:h?.issueUrl,pullRequestUrl:h?.pullRequestUrl,instructions:m,redactHunkContent:e.redactHunkContent,model:p,session:e.session,maxRetries:_,maxTokens:v,onProgress:e=>y?.({phase:`analyzing`,...F.analyzing,event:e}),cancellation:b,onBeforeModelCall:x,onAfterModelCall:S,onBeforePrompt:C,promptOverride:e.promptOverrides?.analyze});E.usage&&r(w,E.usage),T.push({name:`analyzing`,durationMs:E.durationMs,attempts:E.attempts,usage:E.usage,callIds:E.callIds});let D=await s({hunks:t,commitMessages:n.commitMessages,instructions:m,conventions:o,mode:d,strategy:g?.grouping,existingBranchNames:d===`branches`?u:void 0,redactHunkContent:e.redactHunkContent,model:p,session:E.session,maxRetries:_,maxTokens:v,onProgress:e=>y?.({phase:`grouping`,...F.grouping,event:e}),cancellation:b,onBeforeModelCall:x,onAfterModelCall:S,onBeforePrompt:C,promptOverride:e.promptOverrides?.group});D.usage&&r(w,D.usage),T.push({name:`grouping`,durationMs:D.durationMs,attempts:D.attempts,usage:D.usage,callIds:D.callIds});let O=await c({branches:D.result.branches,hunks:t,instructions:m,model:p,session:D.session,maxRetries:_,maxTokens:v,onProgress:e=>y?.({phase:`ordering`,...F.ordering,event:e}),cancellation:b,onBeforeModelCall:x,onAfterModelCall:S,onBeforePrompt:C,promptOverride:e.promptOverrides?.order});O.usage&&r(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(f.type===`auto`){let t=new Set(u);for(let e of D.result.branches)e.name&&t.add(e.name);for(let n of k){let i=await l({commits:$(n.branchGroup,n.orderedCommitIds),orderedCommitIds:n.orderedCommitIds,sourceBranchName:n.branchGroup.name,instructions:m,conventions:o,strategy:g?.partition,existingBranchNames:t,model:p,maxRetries:_,maxTokens:v,onProgress:e=>y?.({phase:`partitioning`,...F.partitioning,branch:n.branchGroup.name,event:e}),cancellation:b,onBeforeModelCall:x,onAfterModelCall:S,onBeforePrompt:C,promptOverride:e.promptOverrides?.partition});i.usage&&r(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=>$(e.branchGroup,e.orderedCommitIds));return{analysis:E.result,grouping:D.result,ordering:O.result,branchPlans:k,allOrderedCommits:j,session:A,usage:w,phases:T}}async function te(e){let t=await I({...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}}async function ne(e){let t=await I({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});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 re(n){let{git:r,applyPlan:a,onProgress:o}=n,{plan:s,snapshot:c,source:l}=a,u=n.source??l,d=n.target??a?.target??await X(r,u);Q(u,d),await K(r,n.applyCommitIds,n.hunkFilter,u,d),await T({git:r,source:u,target:d});let f=await N(w(r,`capturing pre-operation state`)),p=[];o?.({phase:`collecting`,...F.collecting});let m=Date.now(),h=await x(r,u);if(p.push({name:`collecting`,durationMs:Date.now()-m,attempts:1}),h.hunks.length===0)throw new e(`NO_CHANGES`,Z(u));let{hunks:g}=await U(h.hunks,n.hunkFilter);if(g.length===0)throw new e(`NO_CHANGES`,Z(u));let _=g.length<h.hunks.length?await W(r,h.snapshot,g):h.snapshot,v={...h,hunks:g,snapshot:_};if(v.snapshot.diffHash!==c.diffHash)throw new t(`SAFETY_CHECK_FAILED`,`Working directory has changed since the plan was created. Re-run compose to generate a new plan.`);o?.({phase:`verifying`,...F.verifying});let y=Date.now(),b=await S(r,v.snapshot,G(u,v));if(!b.valid)throw new t(`SAFETY_CHECK_FAILED`,`Safety check failed: ${b.error}`);p.push({name:`verifying`,durationMs:Date.now()-y,attempts:1});let E=q(s.allOrderedCommits,n.applyCommitIds),{leftoverHunks:D,leftoverPatch:O}=Y(E,v.hunks),k=D.length>0?await W(r,v.snapshot,J(E,v.hunks)):v.snapshot;o?.({phase:`applying`,...F.applying});let A=Date.now(),j=await C({git:r,source:u,target:d,branchPlans:s.branches,allOrderedCommits:E,hunks:v.hunks,snapshot:k,collection:v,signing:n.signing,authorAttribution:n.authorAttribution,stashLabel:n.stashLabel,leftoverPatch:O});p.push({name:`applying`,durationMs:Date.now()-A,attempts:1});let M=await ae(r,f,j.mutations,d,u);return{plan:s,snapshot:k,partitions:j.partitions,partitionGroups:j.partitionGroups,session:{messages:[],systemPrompt:``,completedSteps:{}},usage:i(),phases:p,source:{type:u.type===`branch`?`branch`:`workdir`,hunks:v.hunks,commitMessages:v.commitMessages,branchName:v.branchName,baseBranch:v.baseBranch},diffStats:V(v.hunks),commitShas:j.commitShas,undoId:M,appliedCommitIds:E.map(e=>e.id),unappliedHunkIndices:D.length>0?D.map(e=>e.index):void 0,stashConflict:j.stashConflict}}function V(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 H(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 U(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 W(e,t,n){let r=t.baseSha??t.headSha,i=j(A(),`compose-filtered-idx-${O()}`),a={GIT_INDEX_FILE:i};try{await g(e,r,{indexEnv:a});let i=y(n);i.length>0&&await u(e,i,{indexEnv:a});let o=await f(e,r,await v(e,{indexEnv:a}));return{...t,diffHash:b(o)}}finally{try{k(i)}catch{}}}function G(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 K(e,t,r,i,a){if(r==null&&t==null||i.type===`workdir`||!(a.type===`rewrite-range`||(a.type===`branch`||a.type===`auto`)&&a.preserveOriginal!==!0))return;let o=i.type===`branch`?await _(e,i.name):await _(e,i.to),s=await _(e,`HEAD`);if(await d(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(s!==o)throw new n(`Filtering and partial apply require HEAD to be at the source's tip (${o.slice(0,8)}); HEAD is at ${s.slice(0,8)}.`)}function q(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 J(e,t){let n=new Set(e.flatMap(e=>e.hunkIndices));return t.filter(e=>n.has(e.index))}function Y(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=y(r);return{leftoverHunks:r,leftoverPatch:i.length>0?i:void 0}}async function X(e,t){if(t.type===`branch`){let r=await _(e,t.name),i=(await m(e,{format:`%H`,range:`${await h(e,await _(e,t.mergeTarget??`main`),r)}..${r}`,reverse:!0,limit:1})).trim().split(`
2
+ `)[0];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 Q(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 $(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 ie(e){let t=(await p(e,{format:`%(refname:short)`,prefix:`refs/heads/`})).trim().split(`
3
+ `).filter(Boolean);return new Set(t)}async function ae(e,t,n,r,i){let a=P(),o=n.createdRefs.filter(e=>e.kind===`branch`).length||1,s=`compose: ${o} branch${o===1?``:`es`} from ${i.type}`,c=[...n.movedRefs];if(r.type===`head`&&t.headBranch){let n=await _(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 M(w(e,`undo manifest persistence`)).save(l),a}export{ne as applyComposePlan,I as compose,te as composePlan,F as defaultPhaseMessages};
@@ -2,6 +2,7 @@ import { PhaseDetail, ProgressEvent, TokenUsage } from "../ai/types.js";
2
2
  import { ComposeGitPort } from "../ports/git.js";
3
3
  import { BranchPartition, CommitDetail, ComposeHunk } from "../tasks/types.js";
4
4
  import { OnAfterModelCall, OnBeforeModelCall, OnBeforePrompt } from "../tasks/ai-loop.js";
5
+ import { RedactHunkContent } from "../tasks/utils/redact.js";
5
6
  import { AnalysisResult } from "../tasks/analyze/types.js";
6
7
  import { ComposeSession } from "../tasks/session.js";
7
8
  import { PartitionResult } from "../tasks/partition/types.js";
@@ -33,6 +34,8 @@ interface SplitInput {
33
34
  onBeforeModelCall?: OnBeforeModelCall;
34
35
  onAfterModelCall?: OnAfterModelCall;
35
36
  onBeforePrompt?: OnBeforePrompt;
37
+ /** See {@link RedactHunkContent}. */
38
+ redactHunkContent?: RedactHunkContent;
36
39
  }
37
40
  interface SplitPlan {
38
41
  analysis: AnalysisResult;
@@ -1,3 +1,3 @@
1
1
  import{GitError as e}from"../errors/git.js";import{addUsage as t,emptyUsage as n}from"../ai/types.js";import{analyze as r}from"../tasks/analyze/analyze.js";import{partition as i}from"../tasks/partition/partition.js";import{forEachRef as a}from"../git/port-dispatch.js";import{createStackedBranches as o}from"../git/apply-branches.js";import{parseUnifiedDiff as s}from"../git/utils/hunk-parser.js";import{collectCommitsForSplit as c}from"../git/diff-collector.js";import{requireExec as l}from"../git/require-exec.js";import{rethrowAsWorkflowError as u}from"../utils/cancellation.js";import{defaultPhaseMessages as d}from"./compose.js";import{UndoStore as f,capturePreOperationState as p,generateUndoId as m}from"@gitkraken/shared-tools";async function h(a){let{git:f,model:m,branchName:h,baseBranch:v=`main`,instructions:y,maxRetries:b,maxTokens:x,onProgress:S,onBeforeApply:C,cancellation:w,onBeforeModelCall:T,onAfterModelCall:E,onBeforePrompt:D}=a,O=n(),k=[];S?.({phase:`collecting`,...d.collecting});let A=Date.now(),{commits:j,branchSha:M}=await c(f,h,v);if(k.push({name:`collecting`,durationMs:Date.now()-A,attempts:1}),j.length===0)throw new e(`NO_CHANGES`,`No commits found between '${v}' and '${h}'`);if(j.length<2)throw new e(`NO_CHANGES`,`Too few commits to split. A branch needs at least 2 commits.`);let N=s(j.map(e=>e.diff).join(`
2
- `),`branch`),P=j.map(e=>({id:e.sha,message:e.message,explanation:``,hunkIndices:[]})),F;try{F=await r({hunks:N,commitMessages:j.map(e=>e.message),commitOrder:j.map(e=>e.sha),branchName:h,baseBranch:v,instructions:y,model:m,maxRetries:b,maxTokens:x,onProgress:e=>S?.({phase:`analyzing`,...d.analyzing,event:e}),cancellation:w,onBeforeModelCall:T,onAfterModelCall:E,onBeforePrompt:D})}catch(e){u(e,w)}F.usage&&t(O,F.usage),k.push({name:`analyzing`,durationMs:F.durationMs,attempts:F.attempts,usage:F.usage,callIds:F.callIds});let I=j.map(e=>e.sha),L=await g(f),R;try{R=await i({commits:P,orderedCommitIds:I,sourceBranchName:h,baseBranch:v,instructions:y,existingBranchNames:L,model:m,session:F.session,maxRetries:b,maxTokens:x,onProgress:e=>S?.({phase:`partitioning`,...d.partitioning,event:e}),cancellation:w,onBeforeModelCall:T,onAfterModelCall:E,onBeforePrompt:D})}catch(e){u(e,w)}R.usage&&t(O,R.usage),k.push({name:`partitioning`,durationMs:R.durationMs,attempts:R.attempts,usage:R.usage,callIds:R.callIds});let z={type:`branch`,hunks:N,commitMessages:j.map(e=>e.message),branchName:h,baseBranch:v},B={analysis:F.result,partitioning:R.result,commits:j};if(R.result.partitions.length<=1||C&&!await C(B))return{plan:B,partitions:R.result.partitions,session:R.session,usage:O,phases:k,source:z};let V=await p(l(f,`capturing pre-operation state`));S?.({phase:`applying`,...d.applying});let H=Date.now();await o({git:f,partitions:R.result.partitions,originalBranchName:h,originalBranchSha:M}),k.push({name:`applying`,durationMs:Date.now()-H,attempts:1});let U=await _(f,V,R.result.partitions,h,M);return{plan:B,partitions:R.result.partitions,session:R.session,usage:O,phases:k,source:z,undoId:U}}async function g(e){let t=(await a(e,{format:`%(refname:short)`,prefix:`refs/heads/`})).trim().split(`
3
- `).filter(Boolean);return new Set(t)}async function _(e,t,n,r,i){let a=m(),o=n.map(e=>{let t=e.commitIds[e.commitIds.length-1];return{name:e.branchName,sha:t}}),s={version:1,id:a,createdAt:new Date().toISOString(),description:`split: ${n.length} branches from '${r}'`,previousHead:{sha:t.headSha,branch:t.headBranch},workdirClean:t.workdirClean,workdirBackupSha:t.workdirBackupSha,createdBranches:o,deletedBranches:[{name:r,sha:i}],movedBranches:[]};return await new f(l(e,`undo manifest persistence`)).save(s),a}export{h as split};
2
+ `)),P=j.map(e=>({id:e.sha,message:e.message,explanation:``,hunkIndices:[]})),F;try{F=await r({hunks:N,commitMessages:j.map(e=>e.message),commitOrder:j.map(e=>e.sha),branchName:h,baseBranch:v,instructions:y,redactHunkContent:a.redactHunkContent,model:m,maxRetries:b,maxTokens:x,onProgress:e=>S?.({phase:`analyzing`,...d.analyzing,event:e}),cancellation:w,onBeforeModelCall:T,onAfterModelCall:E,onBeforePrompt:D})}catch(e){u(e,w)}F.usage&&t(O,F.usage),k.push({name:`analyzing`,durationMs:F.durationMs,attempts:F.attempts,usage:F.usage,callIds:F.callIds});let I=j.map(e=>e.sha),L=await g(f),R;try{R=await i({commits:P,orderedCommitIds:I,sourceBranchName:h,baseBranch:v,instructions:y,existingBranchNames:L,model:m,session:F.session,maxRetries:b,maxTokens:x,onProgress:e=>S?.({phase:`partitioning`,...d.partitioning,event:e}),cancellation:w,onBeforeModelCall:T,onAfterModelCall:E,onBeforePrompt:D})}catch(e){u(e,w)}R.usage&&t(O,R.usage),k.push({name:`partitioning`,durationMs:R.durationMs,attempts:R.attempts,usage:R.usage,callIds:R.callIds});let z={type:`branch`,hunks:N,commitMessages:j.map(e=>e.message),branchName:h,baseBranch:v},B={analysis:F.result,partitioning:R.result,commits:j};if(R.result.partitions.length<=1||C&&!await C(B))return{plan:B,partitions:R.result.partitions,session:R.session,usage:O,phases:k,source:z};let V=await p(l(f,`capturing pre-operation state`));S?.({phase:`applying`,...d.applying});let H=Date.now();await o({git:f,partitions:R.result.partitions,originalBranchName:h,originalBranchSha:M}),k.push({name:`applying`,durationMs:Date.now()-H,attempts:1});let U=await _(f,V,R.result.partitions,h,M);return{plan:B,partitions:R.result.partitions,session:R.session,usage:O,phases:k,source:z,undoId:U}}async function g(e){let t=(await a(e,{format:`%(refname:short)`,prefix:`refs/heads/`})).trim().split(`
3
+ `).filter(Boolean);return new Set(t)}async function _(e,t,n,r,i){let a=m(),o=n.map(e=>({kind:`branch`,name:e.branchName,sha:e.commitIds[e.commitIds.length-1]})),s={version:1,id:a,createdAt:new Date().toISOString(),description:`split: ${n.length} branches from '${r}'`,previousHead:{sha:t.headSha,branch:t.headBranch},workdirClean:t.workdirClean,workdirBackupSha:t.workdirBackupSha,createdRefs:o,deletedRefs:[{kind:`branch`,name:r,sha:i}],movedRefs:[]};return await new f(l(e,`undo manifest persistence`)).save(s),a}export{h as split};
@@ -5,10 +5,13 @@
5
5
  * - `workdir`: collect from the working directory. Set `stagedOnly` to ignore unstaged changes,
6
6
  * `noUntracked` to ignore untracked files.
7
7
  * - `branch`: collect the diff between `name` and `mergeTarget` (defaults to the upstream/base branch).
8
- * - `commit-range`: collect from a contiguous range of commits on a branch, with per-hunk author
9
- * info derived from the owning commits. Used by "recompose this commit" / "recompose selected
10
- * commits" flows. `from` is exclusive (parent of first commit), `to` is inclusive. For a
11
- * single-commit recompose: `from = parent(commitSha)`, `to = commitSha`.
8
+ * Internally translates to a commit range from `parent(first-commit-on-branch)` to `name`'s tip,
9
+ * so the same rules apply as for `commit-range`.
10
+ * - `commit-range`: collect from a contiguous range of commits on a branch as a single combined
11
+ * diff. Used by "recompose this commit" / "recompose selected commits" flows. Both `from` and
12
+ * `to` are **inclusive** — they name the first and last commits being rewritten. For a
13
+ * single-commit recompose: `from = commitSha`, `to = commitSha`. The library internally
14
+ * computes `parent(from)` for git operations. `from` must be an ancestor of `to`.
12
15
  */
13
16
  type ComposeSource = {
14
17
  type: 'workdir';
@@ -20,8 +23,8 @@ type ComposeSource = {
20
23
  mergeTarget?: string;
21
24
  } | {
22
25
  type: 'commit-range';
23
- branch: string;
24
- from: string;
26
+ branch: string; /** First commit in the range to recompose (inclusive). Must be an ancestor of `to`. */
27
+ from: string; /** Last commit in the range to recompose (inclusive). */
25
28
  to: string;
26
29
  includeWorkdir?: {
27
30
  includeStaged?: boolean;
@@ -32,13 +35,34 @@ type ComposeSource = {
32
35
  /**
33
36
  * Where compose writes the result.
34
37
  *
35
- * - `head`: commit onto the current branch.
36
- * - `branch`: create or replace a single named branch.
37
- * - `branch-replace`: replace the source branch (only valid when source is `branch`).
38
- * - `auto`: create one branch per grouped concern, optionally subdividing into stacks.
39
- * - `rewrite-range`: rewrite a contiguous commit range on `branch`, preserving commits
40
- * before `rangeFrom` (always) and commits after `rangeTo` (by cherry-pick when
41
- * `preserveSuffix`). Used with `source: 'commit-range'` for mid-branch recompose.
38
+ * Legal source / target pairings:
39
+ *
40
+ * | source | head | branch | auto | rewrite-range |
41
+ * | -------------- | :--: | :----: | :--: | :-----------: |
42
+ * | `workdir` | ✓ | ✓ | ✓ | ✗ |
43
+ * | `branch` | ✗ | ✓ | ✓ | ✓ |
44
+ * | `commit-range` | ✗ | ✓ | ✓ | ✓ |
45
+ *
46
+ * - `head`: write the output commits onto the current branch (workdir source only).
47
+ * - `branch`: create a new branch from the base commit and write the chain to it. The base is
48
+ * HEAD for `workdir` source, or `parent(rangeFrom)` (equivalently `mergeBase` for `branch` source)
49
+ * for `branch`/`commit-range` sources. `preserveOriginal` behavior:
50
+ * - `workdir` source: `true` → WIP stays in the worktree; `false` → WIP is cleared from the
51
+ * worktree (except hunks excluded by `hunkFilter` or workdir flags, and any partial-apply
52
+ * leftover — those stay in their original location).
53
+ * - `branch` source: `true` → source branch is preserved; `false` → source branch ref is deleted
54
+ * once the full diff has been applied to the new branch.
55
+ * - `commit-range` source: forced to `true` (the source commits always stay reachable from the
56
+ * source branch). Passing `false` is a no-op.
57
+ * - `auto`: same as `branch` but the AI splits the work across one or more new branches or
58
+ * stacks; the same `preserveOriginal` rules apply per source type.
59
+ * - `rewrite-range`: replace the source's commits in place. The new chain stacks on
60
+ * `parent(rangeFrom)`. Every ref whose tip is a strict descendant of `rangeTo` gets its
61
+ * downstream chain re-parented onto the new tip; refs at `rangeTo` itself move with the
62
+ * source branch. Used with `source: 'commit-range'` for mid-branch recompose, or with
63
+ * `source: 'branch'` to rewrite the entire branch in place. Partial apply (`applyCommitIds`)
64
+ * and `hunkFilter` are supported on this target only when HEAD is on the source branch and
65
+ * pointing at `rangeTo`.
42
66
  */
43
67
  type ComposeTarget = {
44
68
  type: 'head';
@@ -46,18 +70,17 @@ type ComposeTarget = {
46
70
  type: 'branch';
47
71
  name: string;
48
72
  preserveOriginal?: boolean;
49
- } | {
50
- type: 'branch-replace';
51
73
  } | {
52
74
  type: 'auto';
53
75
  preserveOriginal?: boolean;
54
76
  } | {
55
77
  type: 'rewrite-range';
56
78
  branch: string;
57
- rangeFrom: string;
79
+ /** First commit in the rewritten range (inclusive). The new chain stacks on
80
+ * `parent(rangeFrom)`. */
81
+ rangeFrom: string; /** Last commit in the rewritten range (inclusive). */
58
82
  rangeTo: string; /** Commits before `rangeFrom` always stay; this flag is reserved for future semantics. Default true. */
59
- preservePrefix?: boolean; /** When true (default), cherry-pick `rangeTo..branch-tip` onto the new range tip. */
60
- preserveSuffix?: boolean;
83
+ preservePrefix?: boolean;
61
84
  };
62
85
  /** Optional context passed through to AI prompts (issue/PR URLs surface in commit messages). */
63
86
  interface ComposeContext {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gitkraken/compose-tools",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "AI-powered organization of git changes into clean, atomic commits and branches.",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "author": "Axosoft, LLC dba GitKraken",
@@ -21,7 +21,7 @@
21
21
  ],
22
22
  "dependencies": {
23
23
  "zod": "^4.3.6",
24
- "@gitkraken/shared-tools": "^0.1.1"
24
+ "@gitkraken/shared-tools": "^0.2.0"
25
25
  },
26
26
  "devDependencies": {
27
27
  "@types/node": "^22.0.0",