@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
@@ -41,8 +41,10 @@ interface ComposeGroupInput {
41
41
  /** Prior session to resume from. When `completedSteps.composeGroup` is present, skip the AI call. */
42
42
  session?: ComposeSession;
43
43
  maxRetries?: number;
44
+ /** Output-token cap forwarded to `runAiLoop`. Default 16384. */
45
+ maxTokens?: number;
44
46
  onProgress?: (event: ProgressEvent) => void;
45
- signal?: AbortSignal;
47
+ cancellation?: AbortSignal;
46
48
  onBeforeModelCall?: OnBeforeModelCall;
47
49
  onAfterModelCall?: OnAfterModelCall;
48
50
  onBeforePrompt?: OnBeforePrompt;
@@ -1 +1 @@
1
- import{TaskInputError as e}from"../../errors/task.js";import{runAiLoop as t}from"../ai-loop.js";import{applyUserPromptOverride as n}from"../types.js";import{validateGroupResponse as r}from"./validator.js";import{buildGroupContinuationPrompt as i,buildGroupSystemPrompt as a,buildGroupUserPrompt as o}from"./prompt.js";async function s(s){let c=s.hunks,l=s.mode??`branches`;if((!c||c.length===0)&&!s.session)throw new e(`No hunks provided: supply 'hunks' or run analyze() first with a session`);if(s.session?.completedSteps.group)return{result:s.session.completedSteps.group,session:s.session,attempts:0,durationMs:0,callIds:[]};let u=n(s.session?i(s,l):o({hunks:c,commitMessages:s.commitMessages,instructions:s.instructions,mode:l}),s.promptOverride),d=c?.length??s.session?.completedSteps.analyze?.hunkAnnotations.length??0;if(d===0)throw new e(`Cannot validate grouping: no hunk count available from input or session`);let f=await t({model:s.model,systemPrompt:s.session?void 0:s.promptOverride?.system??a(s.conventions,l,s.strategy),userPrompt:u,session:s.session,maxRetries:s.maxRetries,onProgress:s.onProgress,signal:s.signal,onBeforeModelCall:s.onBeforeModelCall,onAfterModelCall:s.onAfterModelCall,onBeforePrompt:s.onBeforePrompt,validate:e=>r(e,d,l,s.existingBranchNames),taskName:`group`}),p=s.session?.completedSteps??{},m={...f.session,completedSteps:{...p,group:f.result}};return{result:f.result,session:m,usage:f.usage,attempts:f.attempts,durationMs:f.durationMs,callIds:f.callIds}}export{s as group};
1
+ import{TaskInputError as e}from"../../errors/task.js";import{runAiLoop as t}from"../ai-loop.js";import{applyUserPromptOverride as n}from"../types.js";import{validateGroupResponse as r}from"./validator.js";import{buildGroupContinuationPrompt as i,buildGroupSystemPrompt as a,buildGroupUserPrompt as o}from"./prompt.js";async function s(s){let c=s.hunks,l=s.mode??`branches`;if((!c||c.length===0)&&!s.session)throw new e(`No hunks provided: supply 'hunks' or run analyze() first with a session`);if(s.session?.completedSteps.group)return{result:s.session.completedSteps.group,session:s.session,attempts:0,durationMs:0,callIds:[]};let u=n(s.session?i(s,l):o({hunks:c,commitMessages:s.commitMessages,instructions:s.instructions,mode:l}),s.promptOverride),d=c?.length??s.session?.completedSteps.analyze?.hunkAnnotations.length??0;if(d===0)throw new e(`Cannot validate grouping: no hunk count available from input or session`);let f=await t({model:s.model,systemPrompt:s.session?void 0:s.promptOverride?.system??a(s.conventions,l,s.strategy),userPrompt:u,session:s.session,maxRetries:s.maxRetries,maxTokens:s.maxTokens,onProgress:s.onProgress,cancellation:s.cancellation,onBeforeModelCall:s.onBeforeModelCall,onAfterModelCall:s.onAfterModelCall,onBeforePrompt:s.onBeforePrompt,validate:e=>r(e,d,l,s.existingBranchNames),taskName:`group`}),p=s.session?.completedSteps??{},m={...f.session,completedSteps:{...p,group:f.result}};return{result:f.result,session:m,usage:f.usage,attempts:f.attempts,durationMs:f.durationMs,callIds:f.callIds}}export{s as group};
@@ -1,15 +1,22 @@
1
- import{groupingResultSchema as e}from"./schema.js";import{z as t}from"zod";const n=JSON.stringify(t.toJSONSchema(e),null,2),r=`5. Write a commit message for each commit:
2
- - First line: concise summary, 50 chars max, third-person singular present tense ("Fixes", "Adds", "Improves")
3
- - Then a blank line followed by details focusing on "why" not "what"
4
- - Informal yet professional tone`,i=`Each branch must have a descriptive name, a short PR-style title (under 72 characters, imperative mood), and at least one commit.`;function a(e,t=`branches`,n){return t===`commits`?o(e,n):s(e,n)}function o(e,t){return`You are an advanced AI programming assistant tasked with organizing code changes into commits. You will be working with individual code hunks.
1
+ import{groupingResultSchema as e}from"./schema.js";import{z as t}from"zod";const n=JSON.stringify(t.toJSONSchema(e),null,2),r=`5. Write a full commit message for each commit containing both a summary and a description:
2
+ - **Summary** (first line): concise, 50 chars max, third-person singular present tense ("Fixes", "Adds", "Improves"). This line stands alone as the commit subject.
3
+ - **Blank line**: mandatory separator between summary and description.
4
+ - **Description** (body): one or more lines explaining *why* this change was made, not *what* it does. Focus on motivation, trade-offs, or context a reviewer needs. For non-trivial changes, always include a description — don't leave it at just the summary.
5
+ - Informal yet professional tone.`,i=`Each branch must have a descriptive name, a short PR-style title (under 72 characters, imperative mood), and at least one commit.`;function a(e,t=`branches`,n){return t===`commits`?o(e,n):s(e,n)}function o(e,t){return`You are an advanced AI programming assistant tasked with organizing code changes into commits. You will be working with individual code hunks.
6
+
7
+ ## Critical constraints (output is rejected if violated)
8
+
9
+ 1. **Each hunk index appears exactly once across the entire response.** Not zero. Not two. Exactly one occurrence — meaning it appears in exactly one commit's \`hunks\` array, and only once within that array. Listing the same index twice inside one commit's \`hunks\`, OR listing it in two different commits, both count as duplicates and both fail validation. This is the most common failure mode when two hunks look similar (same file, same kind of change).
10
+ 2. **All hunk indices 0..N-1 must appear** across the union of every commit's \`hunks\` array.
11
+ 3. Each commit must contain at least one hunk. No empty commits.
12
+ 4. Each commit's \`id\` must be unique (e.g. \`commit-1\`, \`commit-2\`).
13
+
14
+ Before producing the \`<output>\` tag, do a self-check: count the total number of hunk indices across every commit's \`hunks\` array. That count MUST equal the input hunk count. If it does not match, you have either duplicated or skipped a hunk — find and fix it before outputting.
5
15
 
6
16
  ## Commit guidelines
7
17
 
8
18
  1. Group hunks into logical units that make sense together and can be applied atomically
9
- 2. Use each hunk only once. Ensure all hunks are assigned to exactly one commit
10
- 3. Ensure each commit is self-contained and atomic
11
- 4. Each commit must have a unique ID (e.g., "commit-1", "commit-2")
12
- 5. Every commit must contain at least one hunk. Do not produce empty commits
19
+ 2. Ensure each commit is self-contained and atomic
13
20
  ${e?.promptFragment?`5. Write a commit message for each commit following the repository's conventions (see below).
14
21
  6. Write a detailed explanation for each commit as if explaining to a reviewer.
15
22
 
@@ -27,11 +34,7 @@ ${n}
27
34
 
28
35
  Wrap all commits in a single branch object with id "default", name "", title "", and description "".
29
36
 
30
- Remember:
31
- - Every hunk must be assigned to exactly one commit
32
- - Every commit must contain at least one hunk (no empty commits)
33
- - Use exactly one branch with id "default", name "", title "", and description ""
34
- - Return only the <output> tag and no other text.`}function s(e,t){let i=e?.promptFragment?`5. Write a commit message for each commit following the repository's conventions (see below).
37
+ Return only the <output> tag and no other text.`}function s(e,t){let i=e?.promptFragment?`5. Write a commit message for each commit following the repository's conventions (see below).
35
38
  6. Write a detailed explanation for each commit as if explaining to a reviewer.
36
39
 
37
40
  ${e.promptFragment}`:`${r}
@@ -42,13 +45,19 @@ Your job has two levels:
42
45
  **Level 1: Commits** — group hunks into logical, atomic commits.
43
46
  **Level 2: Branches** — group commits into branches, where each branch is a cohesive unit of work suitable for one pull request.
44
47
 
48
+ ## Critical constraints (output is rejected if violated)
49
+
50
+ 1. **Each hunk index appears exactly once across the entire response.** Not zero. Not two. Exactly one occurrence — meaning it appears in exactly one branch, in exactly one commit within that branch, and only once within that commit's \`hunks\` array. Listing the same index twice inside one commit's \`hunks\`, OR listing it in two different commits, OR listing it in two different branches, all count as duplicates and all fail validation. This is the most common failure mode when two hunks look similar (same file, same kind of change).
51
+ 2. **All hunk indices 0..N-1 must appear** across the union of every commit's \`hunks\` array across every branch.
52
+ 3. Each commit must contain at least one hunk. No empty commits.
53
+ 4. Each commit \`id\` must be unique across the whole response.
54
+
55
+ Before producing the \`<output>\` tag, do a self-check: count the total number of hunk indices across every commit's \`hunks\` array (across every branch). That count MUST equal the input hunk count. If it does not match, you have either duplicated or skipped a hunk — find and fix it before outputting.
56
+
45
57
  ## Commit guidelines
46
58
 
47
59
  1. Group hunks into logical units that make sense together and can be applied atomically
48
- 2. Use each hunk only once. Ensure all hunks are assigned to exactly one commit
49
- 3. Ensure each commit is self-contained and atomic
50
- 4. Each commit must have a unique ID (e.g., "commit-1", "commit-2")
51
- 5. Every commit must contain at least one hunk. Do not produce empty commits
60
+ 2. Ensure each commit is self-contained and atomic
52
61
  ${i}
53
62
  ${l(t?.commitGranularity)}
54
63
 
@@ -64,10 +73,7 @@ Output your organization as JSON inside a <output> tag matching this schema:
64
73
  ${n}
65
74
  \`\`\`
66
75
 
67
- Remember:
68
- - Every hunk must be assigned to exactly one commit in exactly one branch
69
- - Every commit must contain at least one hunk (no empty commits)
70
- - Return only the <output> tag and no other text.`}function c(e,t){let n=`- Each commit belongs to exactly one branch.
76
+ Return only the <output> tag and no other text.`}function c(e,t){let n=`- Each commit belongs to exactly one branch.
71
77
  ${t?.promptFragment?`- ${i} Follow the repository's branch naming conventions (see above).`:`- ${i}`}
72
78
  - Changes addressing the same pattern across different files belong in ONE branch. For example, applying the same bug fix to multiple functions, or updating similar config across modules — these are the same concern even if the files are different.`,r=`- Consider merge-order preference, not just code dependencies. Even if two concerns compile independently, they may need to be in the same branch or stacked branches if merging one without the other would be confusing or incomplete (e.g., documentation that describes utilities introduced in other commits should be in the same branch or stacked after those utilities).`;return e===`conservative`?`${n}
73
79
  - Default to a single branch. Only create multiple branches when concerns are completely unrelated — different domains, different purposes, and no merge-order preference between them.
@@ -38,8 +38,10 @@ interface GroupInput {
38
38
  model: AiModelPort;
39
39
  session?: ComposeSession;
40
40
  maxRetries?: number;
41
+ /** Output-token cap forwarded to `runAiLoop`. Default 16384. Bump on very large diffs whose structured output overflows the cap mid-JSON. */
42
+ maxTokens?: number;
41
43
  onProgress?: (event: ProgressEvent) => void;
42
- signal?: AbortSignal;
44
+ cancellation?: AbortSignal;
43
45
  onBeforeModelCall?: OnBeforeModelCall;
44
46
  onAfterModelCall?: OnAfterModelCall;
45
47
  onBeforePrompt?: OnBeforePrompt;
@@ -1,12 +1,17 @@
1
- import{extractOutputTag as e}from"../ai-loop.js";import{groupingResultSchema as t}from"./schema.js";function n(n,a,o=`branches`,s){let c=null;try{if(c=e(n),!c)return r([`No <output> tag found in response`],i(o));let l=JSON.parse(c),u=t.safeParse(l);if(!u.success)return r(u.error.issues.map(e=>`${e.path.join(`.`)}: ${e.message}`),i(o));let d=u.data,f=[],p=new Set,m=[],h=new Set,g=[],_=[],v=new Set,y=new Set;for(let e of d.branches){if(v.has(e.id))return r([`Duplicate branch ID: '${e.id}'`],i(o));if(v.add(e.id),o===`branches`&&e.name.length===0)return r([`Branch '${e.id}': missing or empty 'name'`],i(o));if(o===`branches`&&e.name.length>0){if(y.has(e.name))return r([`Duplicate branch name: '${e.name}'`],`Multiple branches use the name '${e.name}'. Each branch must have a unique name. Please rename one of them.`);if(y.add(e.name),s?.has(e.name))return r([`Branch name '${e.name}' already exists in the repository`],`The branch name '${e.name}' already exists in the repository. Please choose a different name for branch '${e.id}'.`)}let t=[];for(let n of e.commits){h.has(n.id)&&g.push(n.id),h.add(n.id);let e=[];for(let t of n.hunks)p.has(t)&&m.push(t),p.add(t),e.push(t);e.length===0&&_.push(n.id),t.push({id:n.id,message:n.message,explanation:n.explanation,hunkIndices:e})}f.push({id:e.id,name:e.name,title:e.title,description:e.description,commits:t})}if(g.length>0)return r([`Duplicate commit IDs: ${g.join(`, `)}`],`Some commit IDs appear multiple times. Each must be unique.\n\nDuplicates: ${g.join(`, `)}`);if(_.length>0)return r([`Empty commits (no hunks assigned): ${_.join(`, `)}`],`Every commit must contain at least one hunk. These commits have an empty \`hunks\` array: ${_.join(`, `)}.
1
+ import{extractOutputTag as e}from"../ai-loop.js";import{groupingResultSchema as t}from"./schema.js";function n(n,a,o=`branches`,s){let c=null;try{if(c=e(n),!c)return r([`No <output> tag found in response`],i(o));let l=JSON.parse(c),u=t.safeParse(l);if(!u.success)return r(u.error.issues.map(e=>`${e.path.join(`.`)}: ${e.message}`),i(o));let d=u.data,f=[],p=new Set,m=[],h=new Map,g=new Set,_=[],v=[],y=new Set,b=new Set;for(let e of d.branches){if(y.has(e.id))return r([`Duplicate branch ID: '${e.id}'`],i(o));if(y.add(e.id),o===`branches`&&e.name.length===0)return r([`Branch '${e.id}': missing or empty 'name'`],i(o));if(o===`branches`&&e.name.length>0){if(b.has(e.name))return r([`Duplicate branch name: '${e.name}'`],`Multiple branches use the name '${e.name}'. Each branch must have a unique name. Please rename one of them.`);if(b.add(e.name),s?.has(e.name))return r([`Branch name '${e.name}' already exists in the repository`],`The branch name '${e.name}' already exists in the repository. Please choose a different name for branch '${e.id}'.`)}let t=[];for(let n of e.commits){g.has(n.id)&&_.push(n.id),g.add(n.id);let r=[],i=o===`branches`?`${e.id}/${n.id}`:n.id;for(let e of n.hunks){p.has(e)&&m.push(e),p.add(e),r.push(e);let t=h.get(e);t?t.push(i):h.set(e,[i])}r.length===0&&v.push(n.id),t.push({id:n.id,message:n.message,explanation:n.explanation,hunkIndices:r})}f.push({id:e.id,name:e.name,title:e.title,description:e.description,commits:t})}if(_.length>0)return r([`Duplicate commit IDs: ${_.join(`, `)}`],`Some commit IDs appear multiple times. Each must be unique.\n\nDuplicates: ${_.join(`, `)}`);if(v.length>0)return r([`Empty commits (no hunks assigned): ${v.join(`, `)}`],`Every commit must contain at least one hunk. These commits have an empty \`hunks\` array: ${v.join(`, `)}.
2
2
 
3
- Either assign at least one hunk to each of these commits, or remove them entirely. If a commit's purpose is already covered by another commit, remove the duplicate. If a commit is supposed to cover specific hunks, assign those hunk indices to it. Every hunk index still needs to appear in exactly one commit.`);let b=new Set(Array.from({length:a},(e,t)=>t)),x=[...b].filter(e=>!p.has(e)),S=[...p].filter(e=>!b.has(e));if(m.length>0||x.length>0||S.length>0){let e=[],t=[];return m.length>0&&(e.push(`Duplicate hunks (assigned to multiple commits): ${m.join(`, `)}`),t.push(`Duplicate hunks: ${m.join(`, `)}`)),x.length>0&&(e.push(`Missing hunks (not assigned to any commit): ${x.join(`, `)}`),t.push(`Missing hunks: ${x.join(`, `)}`)),S.length>0&&(e.push(`Extra hunks (index out of range): ${S.join(`, `)}`),t.push(`Extra hunks: ${S.join(`, `)}`)),r(t,`There are ${a} hunks (indices 0 to ${a-1}). Every index must appear in exactly one commit — no duplicates, no gaps, no out-of-range indices.
3
+ Either assign at least one hunk to each of these commits, or remove them entirely. If a commit's purpose is already covered by another commit, remove the duplicate. If a commit is supposed to cover specific hunks, assign those hunk indices to it. Every hunk index still needs to appear in exactly one commit.`);let x=new Set(Array.from({length:a},(e,t)=>t)),S=[...x].filter(e=>!p.has(e)),C=[...p].filter(e=>!x.has(e));if(m.length>0||S.length>0||C.length>0){let e=[],t=[];if(m.length>0){let n=[...new Set(m)].sort((e,t)=>e-t),r=n.map(e=>` hunk ${e} → ${(h.get(e)??[]).join(`, `)}`).join(`
4
+ `);e.push(`Duplicate hunks (each must appear in exactly ONE commit):\n${r}`),t.push(`Duplicate hunks: ${n.join(`, `)}`)}S.length>0&&(e.push(`Missing hunks (not assigned to any commit): ${S.join(`, `)}`),t.push(`Missing hunks: ${S.join(`, `)}`)),C.length>0&&(e.push(`Extra hunks (index out of range): ${C.join(`, `)}`),t.push(`Extra hunks: ${C.join(`, `)}`));let n=[...h.values()].reduce((e,t)=>e+t.length,0);return r(t,`There are ${a} hunks (indices 0 to ${a-1}). Every index must appear in exactly one commit — no duplicates, no gaps, no out-of-range indices.
4
5
 
5
- Your response has these problems:
6
+ Your response listed ${n} hunk references across ${h.size} unique indices. Expected: ${a} references across ${a} unique indices.
7
+
8
+ Specific problems:
6
9
  ${e.map(e=>`- ${e}`).join(`
7
10
  `)}
8
11
 
9
- Fix these specific issues and return the corrected JSON inside <output> tags.`)}return{valid:!0,result:{branches:f}}}catch(e){let t=e instanceof Error?e.message:String(e),a=(c??n).substring(0,200);return r([`Failed to parse response as valid JSON: ${t}`,`Response preview: ${a}`],i(o))}}function r(e,t){return{valid:!1,errors:e,retryFeedback:t}}function i(e){return e===`commits`?`Your response has an invalid structure. Please return JSON inside <output> tags. Use a single branch with id "default", name "", description "":
12
+ To fix duplicates: for each duplicated hunk index, decide which one commit it truly belongs in and remove it from the others. Do not split a single hunk across two commits — git only knows how to apply a hunk once.
13
+
14
+ Return the corrected JSON inside <output> tags.`)}return{valid:!0,result:{branches:f}}}catch(e){let t=e instanceof Error?e.message:String(e),a=(c??n).slice(-300);return r([`Failed to parse response as valid JSON: ${t}`,`Response tail: ${a}`],i(o))}}function r(e,t){return{valid:!1,errors:e,retryFeedback:t}}function i(e){return e===`commits`?`Your response has an invalid structure. Please return JSON inside <output> tags. Use a single branch with id "default", name "", description "":
10
15
  <output>
11
16
  {
12
17
  "branches": [
@@ -1 +1 @@
1
- import{TaskInputError as e}from"../../errors/task.js";import{runAiLoop as t}from"../ai-loop.js";import{applyUserPromptOverride as n}from"../types.js";import{buildOrderContinuationPrompt as r,buildOrderSystemPrompt as i,buildOrderUserPrompt as a}from"./prompt.js";import{validateOrderResponse as o}from"./validator.js";async function s(s){let c=s.branches??s.session?.completedSteps.group?.branches;if((!c||c.length===0)&&!s.session)throw new e(`No branches provided: supply 'branches' or run group() first with a session`);if(s.session?.completedSteps.order)return{result:s.session.completedSteps.order,session:s.session,attempts:0,durationMs:0,callIds:[]};let l=(c??[]).map(e=>({branchId:e.id,commitIds:e.commits.map(e=>e.id)})),u=n(s.session?r(s):a({branches:c,hunks:s.hunks,instructions:s.instructions}),s.promptOverride),d=await t({model:s.model,systemPrompt:s.session?void 0:s.promptOverride?.system??i(),userPrompt:u,session:s.session,maxRetries:s.maxRetries,onProgress:s.onProgress,signal:s.signal,onBeforeModelCall:s.onBeforeModelCall,onAfterModelCall:s.onAfterModelCall,onBeforePrompt:s.onBeforePrompt,validate:e=>o(e,l),taskName:`order`}),f=s.session?.completedSteps??{},p={...d.session,completedSteps:{...f,order:d.result}};return{result:d.result,session:p,usage:d.usage,attempts:d.attempts,durationMs:d.durationMs,callIds:d.callIds}}export{s as order};
1
+ import{TaskInputError as e}from"../../errors/task.js";import{runAiLoop as t}from"../ai-loop.js";import{applyUserPromptOverride as n}from"../types.js";import{buildOrderContinuationPrompt as r,buildOrderSystemPrompt as i,buildOrderUserPrompt as a}from"./prompt.js";import{validateOrderResponse as o}from"./validator.js";async function s(s){let c=s.branches??s.session?.completedSteps.group?.branches;if((!c||c.length===0)&&!s.session)throw new e(`No branches provided: supply 'branches' or run group() first with a session`);if(s.session?.completedSteps.order)return{result:s.session.completedSteps.order,session:s.session,attempts:0,durationMs:0,callIds:[]};let l=(c??[]).map(e=>({branchId:e.id,commitIds:e.commits.map(e=>e.id)})),u=n(s.session?r(s):a({branches:c,hunks:s.hunks,instructions:s.instructions}),s.promptOverride),d=await t({model:s.model,systemPrompt:s.session?void 0:s.promptOverride?.system??i(),userPrompt:u,session:s.session,maxRetries:s.maxRetries,maxTokens:s.maxTokens,onProgress:s.onProgress,cancellation:s.cancellation,onBeforeModelCall:s.onBeforeModelCall,onAfterModelCall:s.onAfterModelCall,onBeforePrompt:s.onBeforePrompt,validate:e=>o(e,l),taskName:`order`}),f=s.session?.completedSteps??{},p={...d.session,completedSteps:{...f,order:d.result}};return{result:d.result,session:p,usage:d.usage,attempts:d.attempts,durationMs:d.durationMs,callIds:d.callIds}}export{s as order};
@@ -12,8 +12,10 @@ interface OrderInput {
12
12
  model: AiModelPort;
13
13
  session?: ComposeSession;
14
14
  maxRetries?: number;
15
+ /** Output-token cap forwarded to `runAiLoop`. Default 16384. */
16
+ maxTokens?: number;
15
17
  onProgress?: (event: ProgressEvent) => void;
16
- signal?: AbortSignal;
18
+ cancellation?: AbortSignal;
17
19
  onBeforeModelCall?: OnBeforeModelCall;
18
20
  onAfterModelCall?: OnAfterModelCall;
19
21
  onBeforePrompt?: OnBeforePrompt;
@@ -1,4 +1,4 @@
1
- import{extractOutputTag as e}from"../ai-loop.js";import{orderingResultSchema as t}from"./schema.js";function n(n,a){try{let o=e(n);if(!o)return r([`No <output> tag found in response`],i(a));let s=JSON.parse(o),c=t.safeParse(s);if(!c.success)return r(c.error.issues.map(e=>`${e.path.join(`.`)}: ${e.message}`),i(a));let l=c.data,u=[],d=new Set;for(let e of l.branches){d.add(e.branchId);let t=a.find(t=>t.branchId===e.branchId);if(!t)return r([`Unknown branch ID: '${e.branchId}'`],`Branch ID '${e.branchId}' doesn't match any known branch.\n\nKnown branches: ${a.map(e=>e.branchId).join(`, `)}`);let n=new Set(t.commitIds),i=new Set,o=[],s=[];for(let t of e.orderedCommitIds)i.has(t)&&o.push(t),n.has(t)||s.push(t),i.add(t);if(o.length>0)return r([`Branch '${e.branchId}': duplicate commit IDs: ${o.join(`, `)}`],`Branch '${e.branchId}' has duplicate commits. Each must appear exactly once.\n\nDuplicates: ${o.join(`, `)}`);if(s.length>0)return r([`Branch '${e.branchId}': unknown commit IDs: ${s.join(`, `)}`],`Branch '${e.branchId}' references unknown commits.\n\nUnknown: ${s.join(`, `)}\nValid: ${t.commitIds.join(`, `)}`);let c=t.commitIds.filter(e=>!i.has(e));if(c.length>0)return r([`Branch '${e.branchId}': missing commit IDs: ${c.join(`, `)}`],`Branch '${e.branchId}' is missing some commits.\n\nMissing: ${c.join(`, `)}`);u.push({branchId:e.branchId,orderedCommitIds:e.orderedCommitIds})}let f=a.filter(e=>!d.has(e.branchId));return f.length>0?r([`Missing branches: ${f.map(e=>e.branchId).join(`, `)}`],`Some branches are not included in the ordering.\n\nMissing: ${f.map(e=>e.branchId).join(`, `)}`):{valid:!0,result:{branches:u,rationale:l.rationale}}}catch{return r([`Failed to parse response as valid JSON`],i(a))}}function r(e,t){return{valid:!1,errors:e,retryFeedback:t}}function i(e){return`Your response has an invalid structure. Return JSON inside <output> tags:
1
+ import{extractOutputTag as e}from"../ai-loop.js";import{orderingResultSchema as t}from"./schema.js";function n(n,a){let o=null;try{if(o=e(n),!o)return r([`No <output> tag found in response`],i(a));let s=JSON.parse(o),c=t.safeParse(s);if(!c.success)return r(c.error.issues.map(e=>`${e.path.join(`.`)}: ${e.message}`),i(a));let l=c.data,u=[],d=new Set;for(let e of l.branches){d.add(e.branchId);let t=a.find(t=>t.branchId===e.branchId);if(!t)return r([`Unknown branch ID: '${e.branchId}'`],`Branch ID '${e.branchId}' doesn't match any known branch.\n\nKnown branches: ${a.map(e=>e.branchId).join(`, `)}`);let n=new Set(t.commitIds),i=new Set,o=[],s=[];for(let t of e.orderedCommitIds)i.has(t)&&o.push(t),n.has(t)||s.push(t),i.add(t);if(o.length>0)return r([`Branch '${e.branchId}': duplicate commit IDs: ${o.join(`, `)}`],`Branch '${e.branchId}' has duplicate commits. Each must appear exactly once.\n\nDuplicates: ${o.join(`, `)}`);if(s.length>0)return r([`Branch '${e.branchId}': unknown commit IDs: ${s.join(`, `)}`],`Branch '${e.branchId}' references unknown commits.\n\nUnknown: ${s.join(`, `)}\nValid: ${t.commitIds.join(`, `)}`);let c=t.commitIds.filter(e=>!i.has(e));if(c.length>0)return r([`Branch '${e.branchId}': missing commit IDs: ${c.join(`, `)}`],`Branch '${e.branchId}' is missing some commits.\n\nMissing: ${c.join(`, `)}`);u.push({branchId:e.branchId,orderedCommitIds:e.orderedCommitIds})}let f=a.filter(e=>!d.has(e.branchId));return f.length>0?r([`Missing branches: ${f.map(e=>e.branchId).join(`, `)}`],`Some branches are not included in the ordering.\n\nMissing: ${f.map(e=>e.branchId).join(`, `)}`):{valid:!0,result:{branches:u,rationale:l.rationale}}}catch(e){let t=e instanceof Error?e.message:String(e),s=(o??n).slice(-300);return r([`Failed to parse response as valid JSON: ${t}`,`Response tail: ${s}`],i(a))}}function r(e,t){return{valid:!1,errors:e,retryFeedback:t}}function i(e){return`Your response has an invalid structure. Return JSON inside <output> tags:
2
2
  <output>
3
3
  {
4
4
  "branches": [
@@ -1 +1 @@
1
- import{TaskInputError as e}from"../../errors/task.js";import{runAiLoop as t}from"../ai-loop.js";import{applyUserPromptOverride as n}from"../types.js";import{buildPartitionContinuationPrompt as r,buildPartitionSystemPrompt as i,buildPartitionUserPrompt as a}from"./prompt.js";import{validatePartitionResponse as o}from"./validator.js";async function s(s){let c=s.commits,l=s.orderedCommitIds??c?.map(e=>e.id)??[];if(l.length===0)throw new e(`No commits provided: supply 'commits' or 'orderedCommitIds'`);if(s.session?.completedSteps.partition)return{result:s.session.completedSteps.partition,session:s.session,attempts:0,durationMs:0,callIds:[]};let u=n(s.session?r(s):a({commits:c,orderedCommitIds:l,sourceBranchName:s.sourceBranchName,baseBranch:s.baseBranch,instructions:s.instructions}),s.promptOverride),d=await t({model:s.model,systemPrompt:s.session?void 0:s.promptOverride?.system??i(s.conventions,s.strategy),userPrompt:u,session:s.session,maxRetries:s.maxRetries,onProgress:s.onProgress,signal:s.signal,onBeforeModelCall:s.onBeforeModelCall,onAfterModelCall:s.onAfterModelCall,onBeforePrompt:s.onBeforePrompt,validate:e=>o(e,l,s.existingBranchNames),taskName:`partition`}),f=s.session?.completedSteps??{},p={...d.session,completedSteps:{...f,partition:d.result}};return{result:d.result,session:p,usage:d.usage,attempts:d.attempts,durationMs:d.durationMs,callIds:d.callIds}}export{s as partition};
1
+ import{TaskInputError as e}from"../../errors/task.js";import{runAiLoop as t}from"../ai-loop.js";import{applyUserPromptOverride as n}from"../types.js";import{buildPartitionContinuationPrompt as r,buildPartitionSystemPrompt as i,buildPartitionUserPrompt as a}from"./prompt.js";import{validatePartitionResponse as o}from"./validator.js";async function s(s){let c=s.commits,l=s.orderedCommitIds??c?.map(e=>e.id)??[];if(l.length===0)throw new e(`No commits provided: supply 'commits' or 'orderedCommitIds'`);if(s.session?.completedSteps.partition)return{result:s.session.completedSteps.partition,session:s.session,attempts:0,durationMs:0,callIds:[]};let u=n(s.session?r(s):a({commits:c,orderedCommitIds:l,sourceBranchName:s.sourceBranchName,baseBranch:s.baseBranch,instructions:s.instructions}),s.promptOverride),d=await t({model:s.model,systemPrompt:s.session?void 0:s.promptOverride?.system??i(s.conventions,s.strategy),userPrompt:u,session:s.session,maxRetries:s.maxRetries,maxTokens:s.maxTokens,onProgress:s.onProgress,cancellation:s.cancellation,onBeforeModelCall:s.onBeforeModelCall,onAfterModelCall:s.onAfterModelCall,onBeforePrompt:s.onBeforePrompt,validate:e=>o(e,l,s.existingBranchNames),taskName:`partition`}),f=s.session?.completedSteps??{},p={...d.session,completedSteps:{...f,partition:d.result}};return{result:d.result,session:p,usage:d.usage,attempts:d.attempts,durationMs:d.durationMs,callIds:d.callIds}}export{s as partition};
@@ -29,8 +29,10 @@ interface PartitionInput {
29
29
  model: AiModelPort;
30
30
  session?: ComposeSession;
31
31
  maxRetries?: number;
32
+ /** Output-token cap forwarded to `runAiLoop`. Default 16384. */
33
+ maxTokens?: number;
32
34
  onProgress?: (event: ProgressEvent) => void;
33
- signal?: AbortSignal;
35
+ cancellation?: AbortSignal;
34
36
  onBeforeModelCall?: OnBeforeModelCall;
35
37
  onAfterModelCall?: OnAfterModelCall;
36
38
  onBeforePrompt?: OnBeforePrompt;
@@ -1,4 +1,4 @@
1
- import{extractOutputTag as e}from"../ai-loop.js";import{partitionResultSchema as t}from"./schema.js";function n(n,s,c){try{let l=e(n);if(!l)return a([`No <output> tag found in response`],o());let u=JSON.parse(l),d=t.safeParse(u);if(!d.success)return a(d.error.issues.map(e=>`${e.path.join(`.`)}: ${e.message}`),o());let f=d.data,p=[],m=[],h=new Set;for(let e=0;e<f.partitions.length;e++){let t=f.partitions[e];if(!i(t.branchName))return a([`Partition ${e}: '${t.branchName}' is not a valid git branch name`],`The branch name '${t.branchName}' is invalid. Git branch names cannot contain spaces, '..', '~', '^', ':', '\\', or end with '.lock'. Please provide a valid name.`);if(h.has(t.branchName))return a([`Duplicate partition branch name: '${t.branchName}'`],`Multiple partitions use the branch name '${t.branchName}'. Each partition must have a unique branch name. Please rename one of them.`);if(h.add(t.branchName),c?.has(t.branchName))return a([`Branch name '${t.branchName}' already exists in the repository`],`The branch name '${t.branchName}' already exists in the repository. Please choose a different name for partition ${e}.`);for(let e of t.commitIds)m.push(e);p.push({branchName:t.branchName,title:t.title,description:t.description,commitIds:t.commitIds})}let g=new Set,_=[];for(let e of m)g.has(e)&&_.push(e),g.add(e);if(_.length>0)return a([`Duplicate commit IDs: ${_.join(`, `)}`],`Some commits appear in multiple partitions. Each must be in exactly one.\n\nDuplicates: ${_.join(`, `)}`);let v=new Set(s),y=s.filter(e=>!g.has(e)),b=m.filter(e=>!v.has(e));return y.length>0?a([`Missing commits: ${y.join(`, `)}`],`Some commits are not assigned to any partition.\n\nMissing: ${y.join(`, `)}`):b.length>0?a([`Unknown commits: ${b.join(`, `)}`],`Some commit IDs don't match any known commit.\n\nUnknown: ${b.join(`, `)}`):r(p,s)?{valid:!0,result:{partitions:p,rationale:f.rationale}}:a([`Partitions are not contiguous`],`Each partition must contain a contiguous range of commits in the provided order. Please regroup into contiguous ranges.`)}catch{return a([`Failed to parse response as valid JSON`],o())}}function r(e,t){let n=new Map(t.map((e,t)=>[e,t])),r=0;for(let t of e)for(let e of t.commitIds){let t=n.get(e);if(t===void 0||t!==r)return!1;r++}return r===t.length}function i(e){return e.startsWith(`-`)||e.startsWith(`.`)||e.endsWith(`.lock`)||e.endsWith(`.`)||e.endsWith(`/`)||e.includes(`..`)||e.includes(`~`)||e.includes(`^`)||e.includes(`:`)||e.includes(`\\`)||e.includes(` `)||e.includes(`?`)||e.includes(`*`)||e.includes(`[`)||e.includes(`@{`)?!1:e.length>0}function a(e,t){return{valid:!1,errors:e,retryFeedback:t}}function o(){return`Your response has an invalid structure. Return JSON inside <output> tags:
1
+ import{extractOutputTag as e}from"../ai-loop.js";import{partitionResultSchema as t}from"./schema.js";function n(n,s,c){let l=null;try{if(l=e(n),!l)return a([`No <output> tag found in response`],o());let u=JSON.parse(l),d=t.safeParse(u);if(!d.success)return a(d.error.issues.map(e=>`${e.path.join(`.`)}: ${e.message}`),o());let f=d.data,p=[],m=[],h=new Set;for(let e=0;e<f.partitions.length;e++){let t=f.partitions[e];if(!i(t.branchName))return a([`Partition ${e}: '${t.branchName}' is not a valid git branch name`],`The branch name '${t.branchName}' is invalid. Git branch names cannot contain spaces, '..', '~', '^', ':', '\\', or end with '.lock'. Please provide a valid name.`);if(h.has(t.branchName))return a([`Duplicate partition branch name: '${t.branchName}'`],`Multiple partitions use the branch name '${t.branchName}'. Each partition must have a unique branch name. Please rename one of them.`);if(h.add(t.branchName),c?.has(t.branchName))return a([`Branch name '${t.branchName}' already exists in the repository`],`The branch name '${t.branchName}' already exists in the repository. Please choose a different name for partition ${e}.`);for(let e of t.commitIds)m.push(e);p.push({branchName:t.branchName,title:t.title,description:t.description,commitIds:t.commitIds})}let g=new Set,_=[];for(let e of m)g.has(e)&&_.push(e),g.add(e);if(_.length>0)return a([`Duplicate commit IDs: ${_.join(`, `)}`],`Some commits appear in multiple partitions. Each must be in exactly one.\n\nDuplicates: ${_.join(`, `)}`);let v=new Set(s),y=s.filter(e=>!g.has(e)),b=m.filter(e=>!v.has(e));return y.length>0?a([`Missing commits: ${y.join(`, `)}`],`Some commits are not assigned to any partition.\n\nMissing: ${y.join(`, `)}`):b.length>0?a([`Unknown commits: ${b.join(`, `)}`],`Some commit IDs don't match any known commit.\n\nUnknown: ${b.join(`, `)}`):r(p,s)?{valid:!0,result:{partitions:p,rationale:f.rationale}}:a([`Partitions are not contiguous`],`Each partition must contain a contiguous range of commits in the provided order. Please regroup into contiguous ranges.`)}catch(e){let t=e instanceof Error?e.message:String(e),r=(l??n).slice(-300);return a([`Failed to parse response as valid JSON: ${t}`,`Response tail: ${r}`],o())}}function r(e,t){let n=new Map(t.map((e,t)=>[e,t])),r=0;for(let t of e)for(let e of t.commitIds){let t=n.get(e);if(t===void 0||t!==r)return!1;r++}return r===t.length}function i(e){return e.startsWith(`-`)||e.startsWith(`.`)||e.endsWith(`.lock`)||e.endsWith(`.`)||e.endsWith(`/`)||e.includes(`..`)||e.includes(`~`)||e.includes(`^`)||e.includes(`:`)||e.includes(`\\`)||e.includes(` `)||e.includes(`?`)||e.includes(`*`)||e.includes(`[`)||e.includes(`@{`)?!1:e.length>0}function a(e,t){return{valid:!1,errors:e,retryFeedback:t}}function o(){return`Your response has an invalid structure. Return JSON inside <output> tags:
2
2
  <output>
3
3
  {
4
4
  "partitions": [
@@ -1,6 +1,8 @@
1
1
  import{quickComposeResultSchema as e}from"./schema.js";import{z as t}from"zod";const n=JSON.stringify(t.toJSONSchema(e),null,2);function r(e,t=`branches`,r,i=!1){let c=e?.promptFragment?`Write commit messages following the repository's conventions:\n\n${e.promptFragment}`:`Write commit messages:
2
- - First line: concise summary, 50 chars max, third-person singular present tense ("Fixes", "Adds", "Improves")
3
- - Then a blank line followed by details focusing on "why" not "what"`,l=a(r?.commitGranularity),u=t===`branches`?o(r?.branchSplitting,e):``,d=i?s():``;return`You are a fast AI code organizer. Given a set of code diff hunks, organize them into well-structured commits${t===`branches`?` and branches`:``} in a single pass.
2
+ Each commit message must contain both a summary and a description, separated by a blank line:
3
+ - **Summary** (first line): concise, 50 chars max, third-person singular present tense ("Fixes", "Adds", "Improves"). Stands alone as the commit subject.
4
+ - **Blank line**: mandatory separator.
5
+ - **Description** (body): one or more lines explaining *why* the change was made, not *what* it does. Focus on motivation, trade-offs, or context a reviewer needs. For non-trivial changes, always include a description.`,l=a(r?.commitGranularity),u=t===`branches`?o(r?.branchSplitting,e):``,d=i?s():``;return`You are a fast AI code organizer. Given a set of code diff hunks, organize them into well-structured commits${t===`branches`?` and branches`:``} in a single pass.
4
6
 
5
7
  ## Your task
6
8
 
@@ -16,10 +18,17 @@ ${l}
16
18
  ${u}
17
19
  ${d}
18
20
 
21
+ ## Critical constraints (output is rejected if violated)
22
+
23
+ 1. **Each hunk index appears exactly once across the entire response.** Not zero. Not two. Exactly one occurrence — across all branches, across all commits, and only once within any commit's \`hunks\` array. Listing the same index twice inside one commit, OR in two different commits, OR in two different branches, all count as duplicates and all fail validation. This is the most common failure mode when two hunks look similar (same file, same kind of change).
24
+ 2. **All hunk indices 0..N-1 must appear** across the union of every commit's \`hunks\` array across every branch.
25
+ 3. Each commit must contain at least one hunk. No empty commits.
26
+ 4. Each commit \`id\` must be unique across the whole response.
27
+ 5. \`orderedCommitIds\` must list every commit ID in the branch in the order they should be applied.
28
+
29
+ Before producing the \`<output>\` tag, do a self-check: count the total number of hunk indices across every commit's \`hunks\` array (across every branch). That count MUST equal the input hunk count. If it does not match, you have either duplicated or skipped a hunk — find and fix it before outputting.
30
+
19
31
  ## Rules
20
- - Every hunk must be assigned to exactly one commit
21
- - Every commit must contain at least one hunk — no empty commits
22
- - orderedCommitIds must list every commit ID in the branch in the order they should be applied
23
32
  - Keep it practical — focus on clean grouping over deep dependency analysis
24
33
 
25
34
  ## Required Output Format
@@ -1 +1 @@
1
- import{TaskInputError as e}from"../../errors/task.js";import{runAiLoop as t}from"../ai-loop.js";import{applyUserPromptOverride as n}from"../types.js";import{buildQuickComposeSystemPrompt as r,buildQuickComposeUserPrompt as i}from"./prompt.js";import{validateQuickComposeResponse as a}from"./validator.js";async function o(o){let s=o.hunks,c=o.mode??`branches`,l=o.includePartitions??!1;if(!s||s.length===0)throw new e(`No hunks provided for quick compose`);if(o.session?.completedSteps.quickCompose)return{result:o.session.completedSteps.quickCompose,session:o.session,attempts:0,durationMs:0,callIds:[]};let u=o.promptOverride?.system??r(o.conventions,c,o.strategy,l),d=n(i({hunks:s,commitMessages:o.commitMessages,commitOrder:o.commitOrder,branchName:o.branchName,baseBranch:o.baseBranch,issueUrl:o.issueUrl,pullRequestUrl:o.pullRequestUrl,instructions:o.instructions}),o.promptOverride),f=await t({model:o.model,systemPrompt:u,userPrompt:d,maxRetries:o.maxRetries,onProgress:o.onProgress,signal:o.signal,onBeforeModelCall:o.onBeforeModelCall,onAfterModelCall:o.onAfterModelCall,onBeforePrompt:o.onBeforePrompt,validate:e=>a(e,s.length,c,l,o.existingBranchNames),taskName:`quick-compose`}),p=o.session?.completedSteps??{},m={...f.session,completedSteps:{...p,quickCompose:f.result}};return{result:f.result,session:m,usage:f.usage,attempts:f.attempts,durationMs:f.durationMs,callIds:f.callIds}}export{o as quickCompose};
1
+ import{TaskInputError as e}from"../../errors/task.js";import{runAiLoop as t}from"../ai-loop.js";import{applyUserPromptOverride as n}from"../types.js";import{buildQuickComposeSystemPrompt as r,buildQuickComposeUserPrompt as i}from"./prompt.js";import{validateQuickComposeResponse as a}from"./validator.js";async function o(o){let s=o.hunks,c=o.mode??`branches`,l=o.includePartitions??!1;if(!s||s.length===0)throw new e(`No hunks provided for quick compose`);if(o.session?.completedSteps.quickCompose)return{result:o.session.completedSteps.quickCompose,session:o.session,attempts:0,durationMs:0,callIds:[]};let u=o.promptOverride?.system??r(o.conventions,c,o.strategy,l),d=n(i({hunks:s,commitMessages:o.commitMessages,commitOrder:o.commitOrder,branchName:o.branchName,baseBranch:o.baseBranch,issueUrl:o.issueUrl,pullRequestUrl:o.pullRequestUrl,instructions:o.instructions}),o.promptOverride),f=await t({model:o.model,systemPrompt:u,userPrompt:d,maxRetries:o.maxRetries,maxTokens:o.maxTokens,onProgress:o.onProgress,cancellation:o.cancellation,onBeforeModelCall:o.onBeforeModelCall,onAfterModelCall:o.onAfterModelCall,onBeforePrompt:o.onBeforePrompt,validate:e=>a(e,s.length,c,l,o.existingBranchNames),taskName:`quick-compose`}),p=o.session?.completedSteps??{},m={...f.session,completedSteps:{...p,quickCompose:f.result}};return{result:f.result,session:m,usage:f.usage,attempts:f.attempts,durationMs:f.durationMs,callIds:f.callIds}}export{o as quickCompose};
@@ -29,8 +29,10 @@ interface QuickComposeInput {
29
29
  /** Prior session to resume from. When `completedSteps.quickCompose` is present, skip the AI call. */
30
30
  session?: ComposeSession;
31
31
  maxRetries?: number;
32
+ /** Output-token cap forwarded to `runAiLoop`. Default 16384. */
33
+ maxTokens?: number;
32
34
  onProgress?: (event: ProgressEvent) => void;
33
- signal?: AbortSignal;
35
+ cancellation?: AbortSignal;
34
36
  onBeforeModelCall?: OnBeforeModelCall;
35
37
  onAfterModelCall?: OnAfterModelCall;
36
38
  onBeforePrompt?: OnBeforePrompt;
@@ -1,6 +1,14 @@
1
- import{extractOutputTag as e}from"../ai-loop.js";import{quickComposeResultSchema as t}from"./schema.js";function n(n,s,c=`branches`,l=!1,u){try{let d=e(n);if(!d)return a([`No <output> tag found in response`],o(c,l));let f=JSON.parse(d),p=t.safeParse(f);if(!p.success)return a(p.error.issues.map(e=>`${e.path.join(`.`)}: ${e.message}`),o(c,l));let m=p.data,h=new Set,g=new Set,_=new Set,v=[],y=new Set,b=[],x=[],S=[];for(let e of m.branches){if(h.has(e.id))return a([`Duplicate branch ID: '${e.id}'`],o(c,l));if(h.add(e.id),c===`branches`&&e.name.length===0)return a([`Branch '${e.id}': missing or empty 'name'`],o(c,l));if(c===`branches`&&e.name.length>0){if(g.has(e.name))return a([`Duplicate branch name: '${e.name}'`],`Multiple branches use the name '${e.name}'. Each branch must have a unique name. Please rename one of them.`);if(g.add(e.name),u?.has(e.name))return a([`Branch name '${e.name}' already exists in the repository`],`The branch name '${e.name}' already exists in the repository. Please choose a different name for branch '${e.id}'.`)}let t=[],n=new Set;for(let r of e.commits){y.has(r.id)&&b.push(r.id),y.add(r.id),n.add(r.id);let e=[];for(let t of r.hunks)_.has(t)&&v.push(t),_.add(t),e.push(t);e.length===0&&x.push(r.id),t.push({id:r.id,message:r.message,explanation:r.explanation,hunkIndices:e})}let s=new Set(e.orderedCommitIds);if(s.size!==e.orderedCommitIds.length)return a([`Branch '${e.id}': orderedCommitIds contains duplicates`],`The orderedCommitIds list for branch '${e.id}' contains duplicates. Each commit ID must appear exactly once.`);let d=[...n].filter(e=>!s.has(e)),f=e.orderedCommitIds.filter(e=>!n.has(e));if(d.length>0)return a([`Branch '${e.id}': orderedCommitIds is missing commits: ${d.join(`, `)}`],`The orderedCommitIds for branch '${e.id}' must include every commit. Missing: ${d.join(`, `)}`);if(f.length>0)return a([`Branch '${e.id}': orderedCommitIds references unknown commits: ${f.join(`, `)}`],`The orderedCommitIds for branch '${e.id}' references commits that don't exist. Unknown: ${f.join(`, `)}`);let p=e.partitions?[...e.partitions]:void 0;if(p){let t=new Set,n=[];for(let r=0;r<p.length;r++){let o=p[r];if(!i(o.branchName))return a([`Branch '${e.id}', partition ${r}: '${o.branchName}' is not a valid git branch name`],`The branch name '${o.branchName}' is invalid. Git branch names cannot contain spaces, '..', '~', '^', ':', '\\', or end with '.lock'. Please provide a valid name.`);if(t.has(o.branchName))return a([`Branch '${e.id}': duplicate partition name '${o.branchName}'`],`Multiple partitions use the branch name '${o.branchName}'. Each must be unique.`);if(t.add(o.branchName),u?.has(o.branchName))return a([`Partition branch name '${o.branchName}' already exists in the repository`],`The branch name '${o.branchName}' already exists in the repository. Please choose a different name.`);for(let e of o.commitIds)n.push(e)}let o=new Set(n),c=e.orderedCommitIds.filter(e=>!o.has(e)),l=n.filter(e=>!s.has(e));if(c.length>0)return a([`Branch '${e.id}': partition is missing commits: ${c.join(`, `)}`],`All commits must be assigned to a partition. Missing: ${c.join(`, `)}`);if(l.length>0)return a([`Branch '${e.id}': partition references unknown commits: ${l.join(`, `)}`],`Partition references unknown commits: ${l.join(`, `)}`);if(!r(p,e.orderedCommitIds))return a([`Branch '${e.id}': partitions are not contiguous`],`Each partition must contain a contiguous range of commits in the ordered list. Please regroup into contiguous ranges.`)}S.push({id:e.id,name:e.name,title:e.title,description:e.description,commits:t,orderedCommitIds:e.orderedCommitIds,partitions:p})}if(b.length>0)return a([`Duplicate commit IDs: ${b.join(`, `)}`],`Some commit IDs appear multiple times. Each must be unique.\n\nDuplicates: ${b.join(`, `)}`);if(x.length>0)return a([`Empty commits (no hunks assigned): ${x.join(`, `)}`],`Every commit must contain at least one hunk. These commits have an empty \`hunks\` array: ${x.join(`, `)}.
1
+ import{extractOutputTag as e}from"../ai-loop.js";import{quickComposeResultSchema as t}from"./schema.js";function n(n,s,c=`branches`,l=!1,u){let d=null;try{if(d=e(n),!d)return a([`No <output> tag found in response`],o(c,l));let f=JSON.parse(d),p=t.safeParse(f);if(!p.success)return a(p.error.issues.map(e=>`${e.path.join(`.`)}: ${e.message}`),o(c,l));let m=p.data,h=new Set,g=new Set,_=new Set,v=[],y=new Map,b=new Set,x=[],S=[],C=[];for(let e of m.branches){if(h.has(e.id))return a([`Duplicate branch ID: '${e.id}'`],o(c,l));if(h.add(e.id),c===`branches`&&e.name.length===0)return a([`Branch '${e.id}': missing or empty 'name'`],o(c,l));if(c===`branches`&&e.name.length>0){if(g.has(e.name))return a([`Duplicate branch name: '${e.name}'`],`Multiple branches use the name '${e.name}'. Each branch must have a unique name. Please rename one of them.`);if(g.add(e.name),u?.has(e.name))return a([`Branch name '${e.name}' already exists in the repository`],`The branch name '${e.name}' already exists in the repository. Please choose a different name for branch '${e.id}'.`)}let t=[],n=new Set;for(let r of e.commits){b.has(r.id)&&x.push(r.id),b.add(r.id),n.add(r.id);let i=[],a=c===`branches`?`${e.id}/${r.id}`:r.id;for(let e of r.hunks){_.has(e)&&v.push(e),_.add(e),i.push(e);let t=y.get(e);t?t.push(a):y.set(e,[a])}i.length===0&&S.push(r.id),t.push({id:r.id,message:r.message,explanation:r.explanation,hunkIndices:i})}let s=new Set(e.orderedCommitIds);if(s.size!==e.orderedCommitIds.length)return a([`Branch '${e.id}': orderedCommitIds contains duplicates`],`The orderedCommitIds list for branch '${e.id}' contains duplicates. Each commit ID must appear exactly once.`);let d=[...n].filter(e=>!s.has(e)),f=e.orderedCommitIds.filter(e=>!n.has(e));if(d.length>0)return a([`Branch '${e.id}': orderedCommitIds is missing commits: ${d.join(`, `)}`],`The orderedCommitIds for branch '${e.id}' must include every commit. Missing: ${d.join(`, `)}`);if(f.length>0)return a([`Branch '${e.id}': orderedCommitIds references unknown commits: ${f.join(`, `)}`],`The orderedCommitIds for branch '${e.id}' references commits that don't exist. Unknown: ${f.join(`, `)}`);let p=e.partitions?[...e.partitions]:void 0;if(p){let t=new Set,n=[];for(let r=0;r<p.length;r++){let o=p[r];if(!i(o.branchName))return a([`Branch '${e.id}', partition ${r}: '${o.branchName}' is not a valid git branch name`],`The branch name '${o.branchName}' is invalid. Git branch names cannot contain spaces, '..', '~', '^', ':', '\\', or end with '.lock'. Please provide a valid name.`);if(t.has(o.branchName))return a([`Branch '${e.id}': duplicate partition name '${o.branchName}'`],`Multiple partitions use the branch name '${o.branchName}'. Each must be unique.`);if(t.add(o.branchName),u?.has(o.branchName))return a([`Partition branch name '${o.branchName}' already exists in the repository`],`The branch name '${o.branchName}' already exists in the repository. Please choose a different name.`);for(let e of o.commitIds)n.push(e)}let o=new Set(n),c=e.orderedCommitIds.filter(e=>!o.has(e)),l=n.filter(e=>!s.has(e));if(c.length>0)return a([`Branch '${e.id}': partition is missing commits: ${c.join(`, `)}`],`All commits must be assigned to a partition. Missing: ${c.join(`, `)}`);if(l.length>0)return a([`Branch '${e.id}': partition references unknown commits: ${l.join(`, `)}`],`Partition references unknown commits: ${l.join(`, `)}`);if(!r(p,e.orderedCommitIds))return a([`Branch '${e.id}': partitions are not contiguous`],`Each partition must contain a contiguous range of commits in the ordered list. Please regroup into contiguous ranges.`)}C.push({id:e.id,name:e.name,title:e.title,description:e.description,commits:t,orderedCommitIds:e.orderedCommitIds,partitions:p})}if(x.length>0)return a([`Duplicate commit IDs: ${x.join(`, `)}`],`Some commit IDs appear multiple times. Each must be unique.\n\nDuplicates: ${x.join(`, `)}`);if(S.length>0)return a([`Empty commits (no hunks assigned): ${S.join(`, `)}`],`Every commit must contain at least one hunk. These commits have an empty \`hunks\` array: ${S.join(`, `)}.
2
2
 
3
- Either assign at least one hunk to each of these commits, or remove them entirely. If a commit's purpose is already covered by another commit, remove the duplicate. If a commit is supposed to cover specific hunks, assign those hunk indices to it. Every hunk index still needs to appear in exactly one commit.`);if(v.length>0)return a([`Duplicate hunks: ${v.join(`, `)}`],`Some hunks appear in multiple commits. Each hunk can only be used once.\n\nDuplicate hunks: ${v.join(`, `)}`);let C=new Set(Array.from({length:s},(e,t)=>t)),w=[...C].filter(e=>!_.has(e)),T=[..._].filter(e=>!C.has(e));return w.length>0?a([`Missing hunks: ${w.join(`, `)}`],`Some hunks are not assigned to any commit. All hunks must be included.\n\nMissing: ${w.join(`, `)}`):T.length>0?a([`Extra hunks: ${T.join(`, `)}`],`Some hunks in your response don't exist in the input.\n\nExtra: ${T.join(`, `)}`):{valid:!0,result:{branches:S,rationale:m.rationale}}}catch{return a([`Failed to parse response as valid JSON`],o(c,l))}}function r(e,t){let n=new Map(t.map((e,t)=>[e,t])),r=0;for(let t of e)for(let e of t.commitIds){let t=n.get(e);if(t===void 0||t!==r)return!1;r++}return r===t.length}function i(e){return e.startsWith(`-`)||e.startsWith(`.`)||e.endsWith(`.lock`)||e.endsWith(`.`)||e.endsWith(`/`)||e.includes(`..`)||e.includes(`~`)||e.includes(`^`)||e.includes(`:`)||e.includes(`\\`)||e.includes(` `)||e.includes(`?`)||e.includes(`*`)||e.includes(`[`)||e.includes(`@{`)?!1:e.length>0}function a(e,t){return{valid:!1,errors:e,retryFeedback:t}}function o(e,t){return`Your response has an invalid structure. Please return JSON inside <output> tags:
3
+ Either assign at least one hunk to each of these commits, or remove them entirely. If a commit's purpose is already covered by another commit, remove the duplicate. If a commit is supposed to cover specific hunks, assign those hunk indices to it. Every hunk index still needs to appear in exactly one commit.`);if(v.length>0){let e=[...new Set(v)].sort((e,t)=>e-t),t=e.map(e=>` hunk ${e} → ${(y.get(e)??[]).join(`, `)}`).join(`
4
+ `),n=[...y.values()].reduce((e,t)=>e+t.length,0);return a([`Duplicate hunks: ${e.join(`, `)}`],`Some hunks appear in multiple commits. Each hunk can only be used once.
5
+
6
+ Your response listed ${n} hunk references across ${y.size} unique indices. Expected: ${s} references across ${s} unique indices.
7
+
8
+ Duplicates (each hunk index → the commits it landed in):
9
+ ${t}
10
+
11
+ To fix: for each duplicated hunk index, decide which one commit it truly belongs in and remove it from the others. Do not split a single hunk across two commits — git only knows how to apply a hunk once.`)}let w=new Set(Array.from({length:s},(e,t)=>t)),T=[...w].filter(e=>!_.has(e)),E=[..._].filter(e=>!w.has(e));return T.length>0?a([`Missing hunks: ${T.join(`, `)}`],`Some hunks are not assigned to any commit. All hunks must be included.\n\nMissing: ${T.join(`, `)}`):E.length>0?a([`Extra hunks: ${E.join(`, `)}`],`Some hunks in your response don't exist in the input.\n\nExtra: ${E.join(`, `)}`):{valid:!0,result:{branches:C,rationale:m.rationale}}}catch(e){let t=e instanceof Error?e.message:String(e),r=(d??n).slice(-300);return a([`Failed to parse response as valid JSON: ${t}`,`Response tail: ${r}`],o(c,l))}}function r(e,t){let n=new Map(t.map((e,t)=>[e,t])),r=0;for(let t of e)for(let e of t.commitIds){let t=n.get(e);if(t===void 0||t!==r)return!1;r++}return r===t.length}function i(e){return e.startsWith(`-`)||e.startsWith(`.`)||e.endsWith(`.lock`)||e.endsWith(`.`)||e.endsWith(`/`)||e.includes(`..`)||e.includes(`~`)||e.includes(`^`)||e.includes(`:`)||e.includes(`\\`)||e.includes(` `)||e.includes(`?`)||e.includes(`*`)||e.includes(`[`)||e.includes(`@{`)?!1:e.length>0}function a(e,t){return{valid:!1,errors:e,retryFeedback:t}}function o(e,t){return`Your response has an invalid structure. Please return JSON inside <output> tags:
4
12
  <output>
5
13
  {
6
14
  "branches": [
@@ -0,0 +1,26 @@
1
+ //#region src/utils/cancellation.d.ts
2
+ /**
3
+ * True for native `AbortError` thrown by signal-aware adapters
4
+ * (e.g. shared-tools' AI loop, `child_process.spawn` with a signal,
5
+ * `fetch` with a signal). Workflow files use this in their try/catch
6
+ * to translate native abort errors into `ComposeWorkflowError('CANCELLED')`.
7
+ */
8
+ declare function isAbortError(err: unknown): boolean;
9
+ /**
10
+ * Translate a caught error into the workflow-domain shape, then throw.
11
+ *
12
+ * - `ComposeWorkflowError` is rethrown unchanged (already in the right shape).
13
+ * - Native `AbortError` (or any error caught while the cancellation token is
14
+ * already aborted) becomes `ComposeWorkflowError('CANCELLED')`.
15
+ * - Anything else is rethrown unchanged.
16
+ *
17
+ * Workflows that delegate AI work to a task wrap that single task call in
18
+ * `try { ... } catch (err) { rethrowAsWorkflowError(err, input.cancellation); }`.
19
+ * Git operations elsewhere in the workflow do not pass `cancellation` into
20
+ * underlying ops &mdash; they run to completion regardless &mdash; and so cannot
21
+ * surface `AbortError`. Returns `never` so callers don't have to add an
22
+ * unreachable trailing throw.
23
+ */
24
+ declare function rethrowAsWorkflowError(err: unknown, cancellation?: AbortSignal): never;
25
+ //#endregion
26
+ export { isAbortError, rethrowAsWorkflowError };
@@ -0,0 +1 @@
1
+ import{ComposeWorkflowError as e}from"../errors/workflow.js";function t(e){return e instanceof Error&&(e.name===`AbortError`||e.name===`DOMException`)}function n(n,r){throw n instanceof e?n:t(n)||r?.aborted?new e(`CANCELLED`,n instanceof Error?n.message:`Operation cancelled`,{cause:n}):n}export{t as isAbortError,n as rethrowAsWorkflowError};
@@ -9,7 +9,7 @@ import { OrderingResult } from "../tasks/order/types.js";
9
9
  import { ComposeSession } from "../tasks/session.js";
10
10
  import { PartitionStrategy } from "../tasks/partition/types.js";
11
11
  import { ComposeContext, ComposeSource, ComposeTarget } from "./types.js";
12
- import { SafetySnapshot } from "../git/types.js";
12
+ import { SafetySnapshot, StashConflict } from "../git/types.js";
13
13
  import { ComposeBranchPlan } from "../git/apply-target.js";
14
14
  import { AiModelPort, UndoId } from "@gitkraken/shared-tools";
15
15
 
@@ -31,8 +31,10 @@ interface ComposePlanInput {
31
31
  strategy?: ComposeStrategy;
32
32
  depth?: ComposeDepth;
33
33
  maxRetries?: number;
34
+ /** See {@link ComposeInput.maxTokens}. */
35
+ maxTokens?: number;
34
36
  onProgress?: (event: ComposeProgressEvent) => void;
35
- signal?: AbortSignal;
37
+ cancellation?: AbortSignal;
36
38
  onBeforeModelCall?: OnBeforeModelCall;
37
39
  onAfterModelCall?: OnAfterModelCall;
38
40
  onBeforePrompt?: OnBeforePrompt;
@@ -56,18 +58,33 @@ interface ComposePlanResult {
56
58
  /** See {@link ComposeResult.hunksFilteredCount}. */
57
59
  hunksFilteredCount?: number;
58
60
  }
59
- /** Pure-apply input — takes a previously computed plan and writes it to the repository. */
61
+ /**
62
+ * Pure-apply input — takes a previously computed plan and writes it to the repository.
63
+ *
64
+ * No `cancellation` — `applyComposePlan` is git-only and runs to completion.
65
+ */
60
66
  interface ApplyComposePlanInput {
61
67
  git: ComposeGitPort;
62
68
  applyPlan: ComposeApplyPlan;
63
69
  source?: ComposeSource;
64
70
  target?: ComposeTarget;
65
71
  onProgress?: (event: ComposeProgressEvent) => void;
66
- signal?: AbortSignal;
67
72
  /** GPG signing config for produced commits. See {@link ComposeInput.signing}. */
68
73
  signing?: SigningConfig;
69
74
  /** How to pick the primary author for each commit. See {@link ComposeInput.authorAttribution}. */
70
75
  authorAttribution?: ComposeInput['authorAttribution'];
76
+ /** See {@link ComposeInput.applyUpTo}. */
77
+ applyUpTo?: ApplyUpTo;
78
+ /** See {@link ComposeInput.stashLabel}. */
79
+ stashLabel?: string;
80
+ /**
81
+ * Same filter that was passed to {@link composePlan} when the cached plan was created.
82
+ * The plan's snapshot was rebuilt from the filtered hunks; the apply step re-collects
83
+ * from source and must re-apply this filter before comparing the diff hash, otherwise
84
+ * the comparison fails on every consumer that uses a filter (the fresh unfiltered
85
+ * diff hash never equals the cached filtered one). See {@link ComposeInput.hunkFilter}.
86
+ */
87
+ hunkFilter?: ComposeInput['hunkFilter'];
71
88
  }
72
89
  /** Result of {@link applyComposePlan}. */
73
90
  interface ApplyComposePlanResult {
@@ -76,6 +93,12 @@ interface ApplyComposePlanResult {
76
93
  partitionGroups?: BranchPartition[][];
77
94
  phases: PhaseDetail[];
78
95
  undoId?: UndoId;
96
+ /** Plan commit IDs actually written. A prefix of `plan.allOrderedCommits.map(c => c.id)` for partial apply. */
97
+ appliedCommitIds?: string[];
98
+ /** Hunk indices not covered by the applied commits. Empty for full apply. */
99
+ unappliedHunkIndices?: number[];
100
+ /** Set when the post-apply stash pop hit conflicts. Apply itself succeeded; the named stash is preserved. */
101
+ stashConflict?: StashConflict;
79
102
  }
80
103
  /** Controls how many AI pipeline steps run and how deeply each reasons. */
81
104
  type ComposeDepth = 'quick' | 'balanced' | 'deep';
@@ -115,13 +138,28 @@ interface ComposeInput {
115
138
  /** Controls AI pipeline depth. 'quick' = single AI call. 'balanced' = 2-3 calls (default). 'deep' = 4+ calls with full analysis. */
116
139
  depth?: ComposeDepth;
117
140
  maxRetries?: number;
141
+ /**
142
+ * Output-token cap forwarded to every AI task in the pipeline. Defaults to
143
+ * `runAiLoop`'s 16384. Bump on very large diffs whose structured output
144
+ * (e.g. analyze's per-hunk annotations) overflows the cap mid-JSON; the
145
+ * validator will surface that as a JSON-parse failure with a "Response
146
+ * tail" preview if it happens.
147
+ */
148
+ maxTokens?: number;
118
149
  onProgress?: (event: ComposeProgressEvent) => void;
119
150
  onBeforeApply?: (plan: ComposePlan) => Promise<boolean>;
120
151
  /**
121
- * Optional cancellation signal. Forwarded to every AI call and every git operation.
122
- * On abort, the workflow throws `WorkflowError('CANCELLED')`.
152
+ * Optional cancellation token. Forwarded only to the AI loop (via the
153
+ * tasks the workflow runs). Git operations &mdash; collection, snapshot
154
+ * verification, applying, undo manifest persistence &mdash; run to
155
+ * completion regardless of whether `cancellation` was aborted; mid-flight
156
+ * cancellation of a git op risks a half-finished mutation, so by design
157
+ * only the AI loop is cancellable.
158
+ *
159
+ * On AI-loop abort, the workflow translates the native `AbortError` into
160
+ * `ComposeWorkflowError('CANCELLED')`.
123
161
  */
124
- signal?: AbortSignal;
162
+ cancellation?: AbortSignal;
125
163
  /**
126
164
  * Called immediately before each model.generate invocation. Use for progress UI,
127
165
  * telemetry, or correlation. The library doesn't interpret the result.
@@ -131,7 +169,7 @@ interface ComposeInput {
131
169
  onAfterModelCall?: OnAfterModelCall;
132
170
  /**
133
171
  * Gating hook called immediately before each model.generate call. Returning
134
- * `false` aborts the workflow with `WorkflowError('CANCELLED')`. Use for
172
+ * `false` aborts the workflow with `ComposeWorkflowError('CANCELLED')`. Use for
135
173
  * large-prompt confirmations, cost warnings, org policy checks, etc.
136
174
  */
137
175
  onBeforePrompt?: OnBeforePrompt;
@@ -204,7 +242,20 @@ interface ComposeInput {
204
242
  name: string;
205
243
  email: string;
206
244
  } | undefined);
245
+ /** Apply only a prefix of `plan.allOrderedCommits`. Hunks not in applied commits are restored to the working tree. */
246
+ applyUpTo?: ApplyUpTo;
247
+ /** Stash message used by the apply path when stashing pending workdir state. Defaults to `'compose backup'`. */
248
+ stashLabel?: string;
207
249
  }
250
+ type ApplyUpTo = {
251
+ kind: 'all';
252
+ } | {
253
+ kind: 'commitId';
254
+ id: string;
255
+ } | {
256
+ kind: 'count';
257
+ count: number;
258
+ };
208
259
  interface ComposePlan {
209
260
  analysis?: AnalysisResult;
210
261
  grouping: GroupingResult;
@@ -331,6 +382,12 @@ interface ComposeResult {
331
382
  * Undefined when no filter ran.
332
383
  */
333
384
  hunksFilteredCount?: number;
385
+ /** See {@link ApplyComposePlanResult.appliedCommitIds}. */
386
+ appliedCommitIds?: string[];
387
+ /** See {@link ApplyComposePlanResult.unappliedHunkIndices}. */
388
+ unappliedHunkIndices?: number[];
389
+ /** See {@link ApplyComposePlanResult.stashConflict}. */
390
+ stashConflict?: StashConflict;
334
391
  }
335
392
  /**
336
393
  * Compose code changes into a clean set of commits and branches.
@@ -347,6 +404,11 @@ interface ComposeResult {
347
404
  * Caller is responsible for the `model` lifecycle. If `applyPlan` is provided, the AI
348
405
  * phases are skipped and the previously computed plan is re-applied; the workdir
349
406
  * snapshot in the plan is verified before applying.
407
+ *
408
+ * Cancellation only flows into the AI loop (via the tasks the workflow runs).
409
+ * Git operations are not cancellable mid-flight by design — they always run to
410
+ * completion. When the AI loop aborts, the workflow translates the native
411
+ * `AbortError` into `ComposeWorkflowError('CANCELLED')`.
350
412
  */
351
413
  declare function compose(input: ComposeInput): Promise<ComposeResult>;
352
414
  /**
@@ -357,15 +419,21 @@ declare function compose(input: ComposeInput): Promise<ComposeResult>;
357
419
  * consumers whose UX lets the user edit the plan before committing.
358
420
  *
359
421
  * For single-shot "plan and apply in one go", use {@link compose} directly.
422
+ *
423
+ * Cancellation handling matches {@link compose} — only the AI loop is
424
+ * cancellable; git operations run to completion.
360
425
  */
361
426
  declare function composePlan(input: ComposePlanInput): Promise<ComposePlanResult>;
362
427
  /**
363
428
  * Apply a previously computed plan (from {@link composePlan}) to the repository.
364
429
  *
365
430
  * Verifies the snapshot against the current workdir before applying — if the
366
- * subsets the plan touches have drifted, throws `WorkflowError('SAFETY_CHECK_FAILED')`.
431
+ * subsets the plan touches have drifted, throws `ComposeWorkflowError('SAFETY_CHECK_FAILED')`.
367
432
  * On success, returns an `undoId` that {@link undoCompose} can reverse.
433
+ *
434
+ * No AI runs in this path, so cancellation has no effect — the apply phase
435
+ * runs to completion regardless of whether `cancellation` was aborted.
368
436
  */
369
437
  declare function applyComposePlan(input: ApplyComposePlanInput): Promise<ApplyComposePlanResult>;
370
438
  //#endregion
371
- export { ApplyComposePlanInput, ApplyComposePlanResult, ComposeApplyPlan, type ComposeBranchPlan, type ComposeContext, ComposeDepth, ComposeInput, ComposePlan, ComposePlanInput, ComposePlanResult, ComposeProgressEvent, ComposeResult, type ComposeSource, ComposeStrategy, type ComposeTarget, applyComposePlan, compose, composePlan, defaultPhaseMessages };
439
+ export { ApplyComposePlanInput, ApplyComposePlanResult, ApplyUpTo, ComposeApplyPlan, type ComposeBranchPlan, type ComposeContext, ComposeDepth, ComposeInput, ComposePlan, ComposePlanInput, ComposePlanResult, ComposeProgressEvent, ComposeResult, type ComposeSource, ComposeStrategy, type ComposeTarget, type StashConflict, applyComposePlan, compose, composePlan, defaultPhaseMessages };