@gitkraken/compose-tools 0.1.0 → 0.2.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 (59) hide show
  1. package/CHANGELOG.md +32 -1
  2. package/README.md +80 -14
  3. package/dist/errors/workflow.d.ts +16 -6
  4. package/dist/errors/workflow.js +1 -1
  5. package/dist/git/apply-branches.d.ts +0 -2
  6. package/dist/git/apply-branches.js +1 -1
  7. package/dist/git/apply-target.d.ts +13 -2
  8. package/dist/git/apply-target.js +1 -1
  9. package/dist/git/commit-chain.d.ts +27 -7
  10. package/dist/git/commit-chain.js +7 -5
  11. package/dist/git/diff-collector.d.ts +37 -10
  12. package/dist/git/diff-collector.js +7 -7
  13. package/dist/git/port-dispatch.d.ts +9 -3
  14. package/dist/git/port-dispatch.js +3 -3
  15. package/dist/git/require-exec.js +1 -1
  16. package/dist/git/safety.d.ts +3 -3
  17. package/dist/git/safety.js +1 -1
  18. package/dist/git/types.d.ts +6 -1
  19. package/dist/git/utils/conventions.d.ts +0 -1
  20. package/dist/git/utils/conventions.js +4 -4
  21. package/dist/git/utils/temp-index.d.ts +1 -1
  22. package/dist/git/utils/temp-index.js +1 -1
  23. package/dist/index.d.ts +5 -3
  24. package/dist/index.js +1 -1
  25. package/dist/ports/git.d.ts +27 -7
  26. package/dist/tasks/ai-loop.d.ts +9 -15
  27. package/dist/tasks/ai-loop.js +2 -2
  28. package/dist/tasks/analyze/analyze.js +1 -1
  29. package/dist/tasks/analyze/types.d.ts +3 -1
  30. package/dist/tasks/analyze/validator.js +1 -1
  31. package/dist/tasks/compose-group/compose-group.js +1 -1
  32. package/dist/tasks/compose-group/prompt.js +27 -21
  33. package/dist/tasks/compose-group/types.d.ts +3 -1
  34. package/dist/tasks/group/group.js +1 -1
  35. package/dist/tasks/group/prompt.js +27 -21
  36. package/dist/tasks/group/types.d.ts +3 -1
  37. package/dist/tasks/group/validator.js +9 -4
  38. package/dist/tasks/order/order.js +1 -1
  39. package/dist/tasks/order/types.d.ts +3 -1
  40. package/dist/tasks/order/validator.js +1 -1
  41. package/dist/tasks/partition/partition.js +1 -1
  42. package/dist/tasks/partition/types.d.ts +3 -1
  43. package/dist/tasks/partition/validator.js +1 -1
  44. package/dist/tasks/quick-compose/prompt.js +14 -5
  45. package/dist/tasks/quick-compose/quick-compose.js +1 -1
  46. package/dist/tasks/quick-compose/types.d.ts +3 -1
  47. package/dist/tasks/quick-compose/validator.js +10 -2
  48. package/dist/utils/cancellation.d.ts +26 -0
  49. package/dist/utils/cancellation.js +1 -0
  50. package/dist/workflows/compose.d.ts +78 -10
  51. package/dist/workflows/compose.js +2 -2
  52. package/dist/workflows/split.d.ts +11 -3
  53. package/dist/workflows/split.js +3 -3
  54. package/dist/workflows/types.d.ts +5 -0
  55. package/dist/workflows/undo.d.ts +18 -11
  56. package/dist/workflows/undo.js +1 -1
  57. package/package.json +2 -2
  58. package/dist/cancellation.d.ts +0 -20
  59. package/dist/cancellation.js +0 -1
@@ -1,2 +1,2 @@
1
- import{WorkflowError as e,WorkflowInputError as t}from"../errors/workflow.js";import{throwIfCancelled as n}from"../cancellation.js";import{GitError as r}from"../errors/git.js";import{addUsage as i,emptyUsage as a}from"../ai/types.js";import{analyze as o}from"../tasks/analyze/analyze.js";import{composeGroup as s}from"../tasks/compose-group/compose-group.js";import{group as c}from"../tasks/group/group.js";import{order as l}from"../tasks/order/order.js";import{partition as u}from"../tasks/partition/partition.js";import{applyPatchToIndex as d,diffTree as f,forEachRef as p,readTree as m,revParse as h,writeTree as g}from"../git/port-dispatch.js";import{buildPatchForCommit as _}from"../git/utils/patch-builder.js";import{sha256 as v}from"../git/utils/crypto.js";import{collectFromSource as y}from"../git/diff-collector.js";import{verifySnapshot as b}from"../git/safety.js";import{applyToTarget as x}from"../git/apply-target.js";import{requireExec as S}from"../git/require-exec.js";import{extractConventions as C}from"../git/utils/conventions.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(i){if(!i.applyPlan&&!i.model)throw new t(`Either 'model' or 'applyPlan' must be provided`);if(i.applyPlan)return B(i);let{git:a,model:o,source:s={type:`workdir`},onProgress:c,onBeforeApply:l,signal:u}=i;n(u);let d=o,f=i.depth??`balanced`,p=i.target??G(s);q(s,p);let m=[];c?.({phase:`collecting`,...j.collecting});let h=Date.now(),g=await y(a,s,{signal:u});if(m.push({name:`collecting`,durationMs:Date.now()-h,attempts:1}),g.hunks.length===0)throw new r(`NO_CHANGES`,K(s));let{hunks:_,filteredCount:v}=await U(g.hunks,i.hunkFilter);if(_.length===0)throw new r(`NO_CHANGES`,K(s));let w=v!==void 0&&v>0?await W(a,g.snapshot,_,u):g.snapshot,T=await N(f,{hunks:_,collection:g,conventions:i.conventions??await C({git:a,author:i.conventionAuthor,signal:u}),existingBranchNames:await Y(a,u),groupMode:p.type===`auto`?`branches`:`commits`,target:p,model:d,instructions:i.instructions,context:i.context,strategy:i.strategy,maxRetries:i.maxRetries,onProgress:c,signal:u,onBeforeModelCall:i.onBeforeModelCall,onAfterModelCall:i.onAfterModelCall,onBeforePrompt:i.onBeforePrompt,session:i.session,promptOverrides:i.promptOverrides}),E={analysis:T.analysis,grouping:T.grouping,ordering:T.ordering,branches:T.branchPlans,allOrderedCommits:T.allOrderedCommits};if(m.push(...T.phases),l&&!await l(E))return{plan:E,snapshot:w,session:T.session,usage:T.usage,phases:m,source:{type:s.type===`branch`?`branch`:`workdir`,hunks:_,commitMessages:g.commitMessages,branchName:g.branchName,baseBranch:g.baseBranch},diffStats:V(_),feedbackTargetCallId:H(m),hunksFilteredCount:v};let D=await k(S(a,`capturing pre-operation state`),{signal:u});c?.({phase:`verifying`,...j.verifying});let O=Date.now(),A=await b(a,g.snapshot,{includeUnstaged:s.type===`workdir`&&!s.stagedOnly,includeUntracked:g.untrackedIncluded,signal:u});if(!A.valid)throw new e(`SAFETY_CHECK_FAILED`,`Safety check failed: ${A.error}`);m.push({name:`verifying`,durationMs:Date.now()-O,attempts:1}),c?.({phase:`applying`,...j.applying});let M=Date.now(),P=await x({git:a,target:p,branchPlans:T.branchPlans,allOrderedCommits:T.allOrderedCommits,hunks:g.hunks,snapshot:w,collection:g,signal:u,signing:i.signing,authorAttribution:i.authorAttribution});m.push({name:`applying`,durationMs:Date.now()-M,attempts:1});let F=await X(a,D,P.mutations,p,s,u);return{plan:E,snapshot:w,partitions:P.partitions,partitionGroups:P.partitionGroups,session:T.session,usage:T.usage,phases:m,source:{type:s.type===`branch`?`branch`:`workdir`,hunks:_,commitMessages:g.commitMessages,branchName:g.branchName,baseBranch:g.baseBranch},diffStats:V(_),commitShas:P.commitShas,undoId:F,feedbackTargetCallId:H(m),hunksFilteredCount:v}}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:r,collection:o,conventions:c,existingBranchNames:d,groupMode:f,target:p,model:m,instructions:h,context:g,strategy:_,maxRetries:v,onProgress:y,signal:b,onBeforeModelCall:x,onAfterModelCall:S,onBeforePrompt:C}=e;n(b);let w=a(),T=[],E=await s({hunks:r,commitMessages:o.commitMessages,commitOrder:o.commitOrder,branchName:o.branchName,baseBranch:o.baseBranch,issueUrl:g?.issueUrl,pullRequestUrl:g?.pullRequestUrl,instructions:h,conventions:c,mode:f,strategy:_?.grouping,existingBranchNames:f===`branches`?d:void 0,includeAnalysis:t.includeAnalysis,lightweight:t.lightweight,condensed:t.condensed,model:m,session:e.session,maxRetries:v,onProgress:e=>y?.({phase:`compose-grouping`,...j[`compose-grouping`],event:e}),signal:b,onBeforeModelCall:x,onAfterModelCall:S,onBeforePrompt:C,promptOverride:e.promptOverrides?.[`compose-group`]});E.usage&&i(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 l({branches:E.result.branches,hunks:r,instructions:h,model:m,session:E.session,maxRetries:v,onProgress:e=>y?.({phase:`ordering`,...j.ordering,event:e}),signal:b,onBeforeModelCall:x,onAfterModelCall:S,onBeforePrompt:C,promptOverride:e.promptOverrides?.order});t.usage&&i(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(p.type===`auto`){let t=new Set(d);for(let e of E.result.branches)e.name&&t.add(e.name);for(let n of O){let r=await u({commits:J(n.branchGroup,n.orderedCommitIds),orderedCommitIds:n.orderedCommitIds,sourceBranchName:n.branchGroup.name,instructions:h,conventions:c,strategy:_?.partition,existingBranchNames:t,model:m,maxRetries:v,onProgress:e=>y?.({phase:`partitioning`,...j.partitioning,branch:n.branchGroup.name,event:e}),signal:b,onBeforeModelCall:x,onAfterModelCall:S,onBeforePrompt:C,promptOverride:e.promptOverrides?.partition});r.usage&&i(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=>J(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:r,conventions:s,existingBranchNames:d,groupMode:f,target:p,model:m,instructions:h,context:g,strategy:_,maxRetries:v,onProgress:y,signal:b,onBeforeModelCall:x,onAfterModelCall:S,onBeforePrompt:C}=e;n(b);let w=a(),T=[],E=await o({hunks:t,commitMessages:r.commitMessages,commitOrder:r.commitOrder,branchName:r.branchName,baseBranch:r.baseBranch,issueUrl:g?.issueUrl,pullRequestUrl:g?.pullRequestUrl,instructions:h,model:m,session:e.session,maxRetries:v,onProgress:e=>y?.({phase:`analyzing`,...j.analyzing,event:e}),signal:b,onBeforeModelCall:x,onAfterModelCall:S,onBeforePrompt:C,promptOverride:e.promptOverrides?.analyze});E.usage&&i(w,E.usage),T.push({name:`analyzing`,durationMs:E.durationMs,attempts:E.attempts,usage:E.usage,callIds:E.callIds});let D=await c({hunks:t,commitMessages:r.commitMessages,instructions:h,conventions:s,mode:f,strategy:_?.grouping,existingBranchNames:f===`branches`?d:void 0,model:m,session:E.session,maxRetries:v,onProgress:e=>y?.({phase:`grouping`,...j.grouping,event:e}),signal:b,onBeforeModelCall:x,onAfterModelCall:S,onBeforePrompt:C,promptOverride:e.promptOverrides?.group});D.usage&&i(w,D.usage),T.push({name:`grouping`,durationMs:D.durationMs,attempts:D.attempts,usage:D.usage,callIds:D.callIds});let O=await l({branches:D.result.branches,hunks:t,instructions:h,model:m,session:D.session,maxRetries:v,onProgress:e=>y?.({phase:`ordering`,...j.ordering,event:e}),signal:b,onBeforeModelCall:x,onAfterModelCall:S,onBeforePrompt:C,promptOverride:e.promptOverrides?.order});O.usage&&i(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(p.type===`auto`){let t=new Set(d);for(let e of D.result.branches)e.name&&t.add(e.name);for(let n of k){let r=await u({commits:J(n.branchGroup,n.orderedCommitIds),orderedCommitIds:n.orderedCommitIds,sourceBranchName:n.branchGroup.name,instructions:h,conventions:s,strategy:_?.partition,existingBranchNames:t,model:m,maxRetries:v,onProgress:e=>y?.({phase:`partitioning`,...j.partitioning,branch:n.branchGroup.name,event:e}),signal:b,onBeforeModelCall:x,onAfterModelCall:S,onBeforePrompt:C,promptOverride:e.promptOverrides?.partition});r.usage&&i(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,A=r.session;for(let e of r.result.partitions)t.add(e.branchName)}}let M=k.flatMap(e=>J(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,signal:e.signal,signing:e.signing,authorAttribution:e.authorAttribution});return{commitShas:t.commitShas,partitions:t.partitions,partitionGroups:t.partitionGroups,phases:t.phases,undoId:t.undoId}}async function B(t){let{git:i,applyPlan:o,onProgress:s,signal:c}=t;n(c);let{plan:l,snapshot:u,source:d}=o,f=t.source??d,p=t.target??o?.target??G(f);q(f,p);let m=await k(S(i,`capturing pre-operation state`),{signal:c}),h=[];s?.({phase:`collecting`,...j.collecting});let g=Date.now(),_=await y(i,f,{signal:c});if(h.push({name:`collecting`,durationMs:Date.now()-g,attempts:1}),_.hunks.length===0)throw new r(`NO_CHANGES`,K(f));if(_.snapshot.diffHash!==u.diffHash)throw new e(`SAFETY_CHECK_FAILED`,`Working directory has changed since the plan was created. Re-run compose to generate a new plan.`);s?.({phase:`verifying`,...j.verifying});let v=Date.now(),C=await b(i,_.snapshot,{includeUnstaged:f.type===`workdir`&&!f.stagedOnly,includeUntracked:_.untrackedIncluded,signal:c});if(!C.valid)throw new e(`SAFETY_CHECK_FAILED`,`Safety check failed: ${C.error}`);h.push({name:`verifying`,durationMs:Date.now()-v,attempts:1}),s?.({phase:`applying`,...j.applying});let w=Date.now(),T=await x({git:i,target:p,branchPlans:l.branches,allOrderedCommits:l.allOrderedCommits,hunks:_.hunks,snapshot:_.snapshot,collection:_,signal:c,signing:t.signing,authorAttribution:t.authorAttribution});h.push({name:`applying`,durationMs:Date.now()-w,attempts:1});let E=await X(i,m,T.mutations,p,f,c);return{plan:l,snapshot:_.snapshot,partitions:T.partitions,partitionGroups:T.partitionGroups,session:{messages:[],systemPrompt:``,completedSteps:{}},usage:a(),phases:h,source:{type:f.type===`branch`?`branch`:`workdir`,hunks:_.hunks,commitMessages:_.commitMessages,branchName:_.branchName,baseBranch:_.baseBranch},diffStats:V(_.hunks),commitShas:T.commitShas,undoId:E}}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,r){let i=D(E(),`compose-filtered-idx-${w()}`),a={GIT_INDEX_FILE:i};try{await m(e,t.headSha,{indexEnv:a,signal:r});let i=_(n);i.length>0&&await d(e,i,{indexEnv:a,signal:r});let o=await g(e,{indexEnv:a,signal:r}),s=await f(e,t.headSha,o,{signal:r});return{...t,diffHash:v(s)}}finally{try{T(i)}catch{}}}function G(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 K(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,n){if(n.type===`branch-replace`&&e.type!==`branch`)throw new t(`target 'branch-replace' requires source type 'branch'`);if(n.type===`rewrite-range`&&e.type!==`commit-range`&&e.type!==`branch`)throw new t(`target 'rewrite-range' requires source type 'commit-range' or 'branch'`)}function J(t,n){return n.map(n=>{let r=t.commits.find(e=>e.id===n);if(!r)throw new e(`INTERNAL`,`Commit ${n} not found in branch '${t.id}'`);return r})}async function Y(e,t){let n=(await p(e,{format:`%(refname:short)`,prefix:`refs/heads/`,signal:t})).trim().split(`
2
- `).filter(Boolean);return new Set(n)}async function X(e,t,n,r,i,a){let o=A(),s=n.createdBranches.length||1,c=`compose: ${s} branch${s===1?``:`es`} from ${i.type}`,l=[...n.movedBranches];if(r.type===`head`&&t.headBranch){let n=await h(e,`HEAD`,{signal:a});n!==t.headSha&&l.push({name:t.headBranch,previousSha:t.headSha,newSha:n})}let u={version:1,id:o,createdAt:new Date().toISOString(),description:c,previousHead:{sha:t.headSha,branch:t.headBranch},workdirClean:t.workdirClean,workdirBackupSha:t.workdirBackupSha,createdBranches:n.createdBranches,deletedBranches:n.deletedBranches,movedBranches:l};return await new O(S(e,`undo manifest persistence`)).save(u,{signal:a}),o}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,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};
@@ -15,13 +15,21 @@ interface SplitInput {
15
15
  baseBranch?: string;
16
16
  instructions?: string;
17
17
  maxRetries?: number;
18
+ /** Output-token cap forwarded to every AI task in the split pipeline. Defaults to `runAiLoop`'s 16384. */
19
+ maxTokens?: number;
18
20
  onProgress?: (event: SplitProgressEvent) => void;
19
21
  onBeforeApply?: (plan: SplitPlan) => Promise<boolean>;
20
22
  /**
21
- * Optional cancellation signal. Forwarded to every AI call and every git operation.
22
- * On abort, the workflow throws `WorkflowError('CANCELLED')`.
23
+ * Optional cancellation token. Forwarded only to the AI loop (via the
24
+ * `analyze` and `partition` task calls). Git operations &mdash; collection,
25
+ * pre-state capture, branch creation, undo manifest persistence &mdash; run
26
+ * to completion regardless of whether `cancellation` was aborted.
27
+ *
28
+ * On AI-loop abort, the workflow translates the native `AbortError` into
29
+ * `ComposeWorkflowError('CANCELLED')` so consumers only have to check for
30
+ * the workflow-domain shape.
23
31
  */
24
- signal?: AbortSignal;
32
+ cancellation?: AbortSignal;
25
33
  onBeforeModelCall?: OnBeforeModelCall;
26
34
  onAfterModelCall?: OnAfterModelCall;
27
35
  onBeforePrompt?: OnBeforePrompt;
@@ -1,3 +1,3 @@
1
- import{throwIfCancelled as e}from"../cancellation.js";import{GitError as t}from"../errors/git.js";import{addUsage as n,emptyUsage as r}from"../ai/types.js";import{analyze as i}from"../tasks/analyze/analyze.js";import{partition as a}from"../tasks/partition/partition.js";import{forEachRef as o}from"../git/port-dispatch.js";import{createStackedBranches as s}from"../git/apply-branches.js";import{parseUnifiedDiff as c}from"../git/utils/hunk-parser.js";import{collectCommitsForSplit as l}from"../git/diff-collector.js";import{requireExec as u}from"../git/require-exec.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(o){let{git:f,model:m,branchName:h,baseBranch:v=`main`,instructions:y,maxRetries:b,onProgress:x,onBeforeApply:S,signal:C,onBeforeModelCall:w,onAfterModelCall:T,onBeforePrompt:E}=o;e(C);let D=r(),O=[];x?.({phase:`collecting`,...d.collecting});let k=Date.now(),{commits:A,branchSha:j}=await l(f,h,v,C);if(O.push({name:`collecting`,durationMs:Date.now()-k,attempts:1}),A.length===0)throw new t(`NO_CHANGES`,`No commits found between '${v}' and '${h}'`);if(A.length<2)throw new t(`NO_CHANGES`,`Too few commits to split. A branch needs at least 2 commits.`);let M=c(A.map(e=>e.diff).join(`
2
- `),`branch`),N=A.map(e=>({id:e.sha,message:e.message,explanation:``,hunkIndices:[]})),P=await i({hunks:M,commitMessages:A.map(e=>e.message),commitOrder:A.map(e=>e.sha),branchName:h,baseBranch:v,instructions:y,model:m,maxRetries:b,onProgress:e=>x?.({phase:`analyzing`,...d.analyzing,event:e}),signal:C,onBeforeModelCall:w,onAfterModelCall:T,onBeforePrompt:E});P.usage&&n(D,P.usage),O.push({name:`analyzing`,durationMs:P.durationMs,attempts:P.attempts,usage:P.usage,callIds:P.callIds});let F=await a({commits:N,orderedCommitIds:A.map(e=>e.sha),sourceBranchName:h,baseBranch:v,instructions:y,existingBranchNames:await g(f,C),model:m,session:P.session,maxRetries:b,onProgress:e=>x?.({phase:`partitioning`,...d.partitioning,event:e}),signal:C,onBeforeModelCall:w,onAfterModelCall:T,onBeforePrompt:E});F.usage&&n(D,F.usage),O.push({name:`partitioning`,durationMs:F.durationMs,attempts:F.attempts,usage:F.usage,callIds:F.callIds});let I={type:`branch`,hunks:M,commitMessages:A.map(e=>e.message),branchName:h,baseBranch:v},L={analysis:P.result,partitioning:F.result,commits:A};if(F.result.partitions.length<=1||S&&!await S(L))return{plan:L,partitions:F.result.partitions,session:F.session,usage:D,phases:O,source:I};let R=await p(u(f,`capturing pre-operation state`),{signal:C});x?.({phase:`applying`,...d.applying});let z=Date.now();await s({git:f,partitions:F.result.partitions,originalBranchName:h,originalBranchSha:j,signal:C}),O.push({name:`applying`,durationMs:Date.now()-z,attempts:1});let B=await _(f,R,F.result.partitions,h,j,C);return{plan:L,partitions:F.result.partitions,session:F.session,usage:D,phases:O,source:I,undoId:B}}async function g(e,t){let n=(await o(e,{format:`%(refname:short)`,prefix:`refs/heads/`,signal:t})).trim().split(`
3
- `).filter(Boolean);return new Set(n)}async function _(e,t,n,r,i,a){let o=m(),s=n.map(e=>{let t=e.commitIds[e.commitIds.length-1];return{name:e.branchName,sha:t}}),c={version:1,id:o,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:s,deletedBranches:[{name:r,sha:i}],movedBranches:[]};return await new f(u(e,`undo manifest persistence`)).save(c,{signal:a}),o}export{h as split};
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};
@@ -23,6 +23,11 @@ type ComposeSource = {
23
23
  branch: string;
24
24
  from: string;
25
25
  to: string;
26
+ includeWorkdir?: {
27
+ includeStaged?: boolean;
28
+ includeUnstaged?: boolean;
29
+ includeUntracked?: boolean;
30
+ };
26
31
  };
27
32
  /**
28
33
  * Where compose writes the result.
@@ -1,5 +1,5 @@
1
1
  import { ComposeGitPort } from "../ports/git.js";
2
- import { UndoId, UndoResult, UndoValidation } from "@gitkraken/shared-tools";
2
+ import { UndoForceOptions, UndoId, UndoResult, UndoValidation } from "@gitkraken/shared-tools";
3
3
 
4
4
  //#region src/workflows/undo.d.ts
5
5
  interface UndoComposeInput {
@@ -7,27 +7,28 @@ interface UndoComposeInput {
7
7
  /** The undo ID returned by `compose()` or `split()`. */
8
8
  undoId: UndoId;
9
9
  /**
10
- * When true, downgrades `branch_has_new_commits` blockers to warnings and
11
- * proceeds. The `dirty_workdir` blocker is never force-able.
10
+ * When `true`, downgrades `branch_has_new_commits` blockers to warnings —
11
+ * shorthand for `{ branchHasNewCommits: true }`. Pass an object form to
12
+ * additionally bypass `dirty_workdir` (e.g., when rolling back from a
13
+ * stash-pop conflict where the dirty state is the operation's own
14
+ * leftover, not user work).
12
15
  */
13
- force?: boolean;
16
+ force?: boolean | UndoForceOptions;
14
17
  /** Called before each undo step with a human-readable description. */
15
18
  onProgress?: (step: string) => void;
16
- /** Optional cancellation signal. Forwarded to every git operation. */
17
- signal?: AbortSignal;
18
19
  }
19
20
  interface ValidateUndoComposeInput {
20
21
  git: ComposeGitPort;
21
22
  /** The undo ID to validate. */
22
23
  undoId: UndoId;
23
- /** Optional cancellation signal. Forwarded to every git operation. */
24
- signal?: AbortSignal;
25
24
  }
26
25
  /**
27
26
  * Validate whether undoing a compose/split operation can safely proceed.
28
27
  *
29
28
  * Returns blockers (hard stops) and warnings (notable conditions). Does not
30
29
  * mutate the repository. Use this for dry-run checks before {@link undoCompose}.
30
+ *
31
+ * Not cancellable — undo work is git-only and runs to completion.
31
32
  */
32
33
  declare function validateUndoCompose(input: ValidateUndoComposeInput): Promise<UndoValidation>;
33
34
  /**
@@ -38,10 +39,14 @@ declare function validateUndoCompose(input: ValidateUndoComposeInput): Promise<U
38
39
  * mutations that were recorded. Returns a structured {@link UndoResult}.
39
40
  *
40
41
  * Safety:
41
- * - Refuses to proceed when the working directory has uncommitted changes.
42
+ * - Refuses to proceed when the working directory has uncommitted changes
43
+ * unless `force.dirtyWorkdir` is set.
42
44
  * - Refuses when a branch created by the operation has been modified since
43
- * (unless `force` is set).
45
+ * unless `force` is set (or `force.branchHasNewCommits`).
44
46
  * - Best-effort on working directory restoration from stash backup.
47
+ *
48
+ * Not cancellable — undo runs to completion to avoid leaving the repo in
49
+ * a half-reversed state.
45
50
  */
46
51
  declare function undoCompose(input: UndoComposeInput): Promise<UndoResult>;
47
52
  /**
@@ -50,7 +55,9 @@ declare function undoCompose(input: UndoComposeInput): Promise<UndoResult>;
50
55
  * Returns IDs for all workflows that used the shared undo infrastructure
51
56
  * (compose, split, and any future packages). Each ID can be passed to
52
57
  * {@link validateUndoCompose} or {@link undoCompose}.
58
+ *
59
+ * Not cancellable — git-only operation that runs to completion.
53
60
  */
54
- declare function listUndoIds(git: ComposeGitPort, signal?: AbortSignal): Promise<UndoId[]>;
61
+ declare function listUndoIds(git: ComposeGitPort): Promise<UndoId[]>;
55
62
  //#endregion
56
63
  export { UndoComposeInput, ValidateUndoComposeInput, listUndoIds, undoCompose, validateUndoCompose };
@@ -1 +1 @@
1
- import{requireExec as e}from"../git/require-exec.js";import{UndoStore as t,undoOperation as n,validateUndo as r}from"@gitkraken/shared-tools";async function i(t){return r({git:e(t.git,`undo validation`),manifestId:t.undoId,signal:t.signal})}async function a(t){return n({git:e(t.git,`undo execution`),manifestId:t.undoId,force:t.force,onProgress:t.onProgress,signal:t.signal})}async function o(n,r){return new t(e(n,`undo listing`)).list({signal:r})}export{o as listUndoIds,a as undoCompose,i as validateUndoCompose};
1
+ import{requireExec as e}from"../git/require-exec.js";import{UndoStore as t,undoOperation as n,validateUndo as r}from"@gitkraken/shared-tools";async function i(t){return r({git:e(t.git,`undo validation`),manifestId:t.undoId})}async function a(t){return n({git:e(t.git,`undo execution`),manifestId:t.undoId,force:t.force,onProgress:t.onProgress})}async function o(n){return new t(e(n,`undo listing`)).list()}export{o as listUndoIds,a as undoCompose,i as validateUndoCompose};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gitkraken/compose-tools",
3
- "version": "0.1.0",
3
+ "version": "0.2.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.0"
24
+ "@gitkraken/shared-tools": "^0.1.1"
25
25
  },
26
26
  "devDependencies": {
27
27
  "@types/node": "^22.0.0",
@@ -1,20 +0,0 @@
1
- //#region src/cancellation.d.ts
2
- /**
3
- * Throws a `WorkflowError('CANCELLED')` if the signal has been aborted.
4
- *
5
- * Call at workflow phase boundaries so cancellation is observable even when
6
- * a consumer's adapter doesn't fully respect the signal on every operation.
7
- * Individual `git.exec` and `model.generate` calls should also receive the
8
- * signal via their options; this helper is the coarse-grained backstop.
9
- */
10
- declare function throwIfCancelled(signal?: AbortSignal): void;
11
- /**
12
- * Wraps a native `AbortError` (thrown by a signal-aware adapter) in a
13
- * `WorkflowError('CANCELLED')`. Rethrows non-cancellation errors unchanged.
14
- *
15
- * Use to normalize exceptions from `git.exec` or `model.generate` so consumers
16
- * only have to check for `WorkflowError` with code `'CANCELLED'`.
17
- */
18
- declare function wrapCancellation<T>(promise: Promise<T>, signal?: AbortSignal): Promise<T>;
19
- //#endregion
20
- export { throwIfCancelled, wrapCancellation };
@@ -1 +0,0 @@
1
- import{WorkflowError as e}from"./errors/workflow.js";function t(t){if(t?.aborted)throw new e(`CANCELLED`,t.reason instanceof Error?t.reason.message:`Operation cancelled`)}function n(t,n){return t.catch(t=>{throw r(t)||n?.aborted?new e(`CANCELLED`,t instanceof Error?t.message:`Operation cancelled`,{cause:t}):t})}function r(e){return e instanceof Error&&(e.name===`AbortError`||e.name===`DOMException`)}export{t as throwIfCancelled,n as wrapCancellation};