@gitkraken/compose-tools 0.5.0 → 0.6.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 (48) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/dist/errors/task.d.ts +11 -1
  3. package/dist/errors/task.js +1 -1
  4. package/dist/git/utils/conventions.d.ts +7 -0
  5. package/dist/git/utils/conventions.js +8 -9
  6. package/dist/index.d.ts +4 -2
  7. package/dist/index.js +1 -1
  8. package/dist/tasks/analyze/analyze.js +1 -1
  9. package/dist/tasks/analyze/prompt.d.ts +4 -1
  10. package/dist/tasks/analyze/prompt.js +5 -5
  11. package/dist/tasks/analyze/schema.d.ts +127 -36
  12. package/dist/tasks/analyze/schema.js +1 -1
  13. package/dist/tasks/analyze/types.d.ts +8 -0
  14. package/dist/tasks/analyze/validator.js +2 -2
  15. package/dist/tasks/compose-group/compose-group.js +1 -1
  16. package/dist/tasks/compose-group/prompt.d.ts +4 -1
  17. package/dist/tasks/compose-group/prompt.js +23 -29
  18. package/dist/tasks/compose-group/types.d.ts +5 -0
  19. package/dist/tasks/group/group.js +1 -1
  20. package/dist/tasks/group/prompt.d.ts +4 -1
  21. package/dist/tasks/group/prompt.js +15 -21
  22. package/dist/tasks/group/schema.d.ts +89 -17
  23. package/dist/tasks/group/schema.js +1 -1
  24. package/dist/tasks/group/types.d.ts +8 -0
  25. package/dist/tasks/group/validator.js +6 -6
  26. package/dist/tasks/order/prompt.js +5 -5
  27. package/dist/tasks/order/schema.d.ts +35 -10
  28. package/dist/tasks/order/schema.js +1 -1
  29. package/dist/tasks/order/validator.js +2 -2
  30. package/dist/tasks/partition/prompt.js +9 -9
  31. package/dist/tasks/partition/schema.d.ts +46 -12
  32. package/dist/tasks/partition/schema.js +1 -1
  33. package/dist/tasks/partition/validator.js +2 -2
  34. package/dist/tasks/quick-compose/prompt.js +8 -9
  35. package/dist/tasks/quick-compose/schema.d.ts +142 -25
  36. package/dist/tasks/quick-compose/schema.js +1 -1
  37. package/dist/tasks/quick-compose/validator.js +5 -5
  38. package/dist/tasks/utils/budget.d.ts +40 -0
  39. package/dist/tasks/utils/budget.js +1 -0
  40. package/dist/tasks/utils/json-schema.d.ts +61 -0
  41. package/dist/tasks/utils/json-schema.js +1 -0
  42. package/dist/tasks/utils/noise.d.ts +21 -0
  43. package/dist/tasks/utils/noise.js +2 -0
  44. package/dist/tasks/utils/redact.d.ts +7 -1
  45. package/dist/tasks/utils/redact.js +1 -1
  46. package/dist/workflows/compose.d.ts +36 -0
  47. package/dist/workflows/compose.js +3 -3
  48. package/package.json +2 -2
@@ -1,8 +1,7 @@
1
- import{hunkContentForPrompt as e}from"../utils/redact.js";import{quickComposeResultSchema as t}from"./schema.js";import{z as n}from"zod";const r=JSON.stringify(n.toJSONSchema(t),null,2);function i(e,t=`branches`,n,i=!1){let a=e?.promptFragment?`Write commit messages following the repository's conventions:\n\n${e.promptFragment}`:`Write commit messages:
2
- Each commit message must contain both a summary and a description, separated by a blank line:
1
+ import{hunkContentForPrompt as e}from"../utils/redact.js";import{quickComposeResultJsonSchema as t}from"./schema.js";const n=JSON.stringify(t,null,2);function r(e,t=`branches`,r,i=!1){let c=`Write commit messages:\n${e?.commits.bodyStyle===`minimal`?`Each commit message is a single subject line with no body: concise, 50 chars max, third-person singular present tense ("Fixes", "Adds", "Improves"). This repository's history overwhelmingly uses one-line commit messages, so do not add a description.`:`Each commit message must contain both a summary and a description, separated by a blank line:
3
2
  - **Summary** (first line): concise, 50 chars max, third-person singular present tense ("Fixes", "Adds", "Improves"). Stands alone as the commit subject.
4
3
  - **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=o(n?.commitGranularity),u=t===`branches`?s(n?.branchSplitting,e):``,d=i?c():``;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
+ - **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.`}${e?.promptFragment?`\n\n${e.promptFragment}`:``}`,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.
6
5
 
7
6
  ## Your task
8
7
 
@@ -13,7 +12,7 @@ ${t===`branches`?`4. Assign commits to branches — each branch is a cohesive un
13
12
  ${i?`5. If a branch should be split into stacked sub-branches for review, provide partitions.`:``}
14
13
 
15
14
  ## Commit style
16
- ${a}
15
+ ${c}
17
16
  ${l}
18
17
  ${u}
19
18
  ${d}
@@ -36,21 +35,21 @@ Before producing the \`<output>\` tag, do a self-check: count the total number o
36
35
  Output as JSON inside a <output> tag matching this schema:
37
36
 
38
37
  \`\`\`json
39
- ${r}
38
+ ${n}
40
39
  \`\`\`
41
40
 
42
- Return only the <output> tag and no other text.`}function a(t){let{hunks:n,commitMessages:r,commitOrder:i,branchName:a,baseBranch:o,issueUrl:s,pullRequestUrl:c,instructions:l,redactHunkContent:u}=t,d=`Organize these code hunks into commits:
41
+ Return only the <output> tag and no other text.`}function i(t){let{hunks:n,commitMessages:r,commitOrder:i,branchName:a,baseBranch:o,issueUrl:s,pullRequestUrl:c,instructions:l,redactHunkContent:u}=t,d=`Organize these code hunks into commits:
43
42
 
44
43
  <hunks>
45
44
  ${JSON.stringify(n.map(t=>({index:t.index,fileName:t.fileName,hunkHeader:t.hunkHeader,content:e(t,u),...t.isBinary?{isBinary:!0}:{},...t.isRename?{isRename:!0}:{},...t.originalFileName?{originalFileName:t.originalFileName}:{}})))}
46
45
  </hunks>`,f=[];return a&&f.push(`Branch: ${a}`),o&&f.push(`Base: ${o}`),s&&f.push(`Issue: ${s}`),c&&f.push(`PR: ${c}`),f.length>0&&(d+=`\n\nContext:\n${f.join(`
47
46
  `)}`),r?.length&&(d+=`\n\nOriginal commit messages (match their style):\n<commit_messages>\n${JSON.stringify(r)}\n</commit_messages>`),i?.length&&(d+=`\n\nOriginal commit order:\n${JSON.stringify(i)}`),l&&(d+=`\n\nAdditional instructions:\n${l}`),d+=`
48
47
 
49
- Return only the <output> tag and no other text.`,d}function o(e){return e===`consolidated`?`
48
+ Return only the <output> tag and no other text.`,d}function a(e){return e===`consolidated`?`
50
49
  Prefer fewer, larger commits. Combine related changes into a single commit.`:e===`granular`?`
51
- Prefer more, smaller commits. Each distinct change gets its own commit.`:``}function s(e,t){let n=`\n## Branch guidelines\n- Each branch has a descriptive name, a short PR title (under 72 chars), and at least one commit.\n- ${t?.promptFragment?`Follow the repository's branch naming conventions.`:`Give each branch a descriptive name.`}`;return e===`conservative`?`${n}\n- Default to a single branch. Only split when concerns are completely unrelated.`:e===`aggressive`?`${n}\n- Create separate branches for each distinct concern. Maximize focus per PR.`:`${n}\n- Split when concerns are genuinely independent. Keep together when they form a coherent story.`}function c(){return`
50
+ Prefer more, smaller commits. Each distinct change gets its own commit.`:``}function o(e,t){let n=`\n## Branch guidelines\n- Each branch has a descriptive name, a short PR title (under 72 chars), and at least one commit.\n- ${t?.promptFragment?`Follow the repository's branch naming conventions.`:`Give each branch a descriptive name.`}`;return e===`conservative`?`${n}\n- Default to a single branch. Only split when concerns are completely unrelated.`:e===`aggressive`?`${n}\n- Create separate branches for each distinct concern. Maximize focus per PR.`:`${n}\n- Split when concerns are genuinely independent. Keep together when they form a coherent story.`}function s(){return`
52
51
  ## Partition guidelines (stacked sub-branches)
53
52
  - If a branch has multiple distinct concerns, split it into partitions for stacked review.
54
53
  - Each partition is a contiguous range of commits from orderedCommitIds.
55
54
  - Each partition gets a unique, valid git branch name.
56
- - Single partition (no split) is a valid outcome.`}export{i as buildQuickComposeSystemPrompt,a as buildQuickComposeUserPrompt};
55
+ - Single partition (no split) is a valid outcome.`}export{r as buildQuickComposeSystemPrompt,i as buildQuickComposeUserPrompt};
@@ -1,27 +1,144 @@
1
- import { z } from "zod";
2
-
3
1
  //#region src/tasks/quick-compose/schema.d.ts
4
- declare const quickComposeResultSchema: z.ZodObject<{
5
- branches: z.ZodArray<z.ZodObject<{
6
- id: z.ZodString;
7
- name: z.ZodString;
8
- title: z.ZodString;
9
- description: z.ZodString;
10
- commits: z.ZodArray<z.ZodObject<{
11
- id: z.ZodString;
12
- message: z.ZodString;
13
- explanation: z.ZodString;
14
- hunks: z.ZodArray<z.ZodNumber>;
15
- }, z.core.$strip>>;
16
- orderedCommitIds: z.ZodArray<z.ZodString>;
17
- partitions: z.ZodOptional<z.ZodArray<z.ZodObject<{
18
- branchName: z.ZodString;
19
- title: z.ZodString;
20
- description: z.ZodString;
21
- commitIds: z.ZodArray<z.ZodString>;
22
- }, z.core.$strip>>>;
23
- }, z.core.$strip>>;
24
- rationale: z.ZodString;
25
- }, z.core.$strip>;
2
+ /** Shape of the LLM JSON output before the validator maps it to public types (`hunks` → `hunkIndices`). */
3
+ interface QuickComposeWire {
4
+ branches: QuickComposeWireBranch[];
5
+ rationale: string;
6
+ }
7
+ interface QuickComposeWireBranch {
8
+ id: string;
9
+ name: string;
10
+ title: string;
11
+ description: string;
12
+ commits: QuickComposeWireCommit[];
13
+ orderedCommitIds: string[];
14
+ partitions?: QuickComposeWirePartition[];
15
+ }
16
+ interface QuickComposeWireCommit {
17
+ id: string;
18
+ message: string;
19
+ explanation: string;
20
+ hunks: number[];
21
+ }
22
+ interface QuickComposeWirePartition {
23
+ branchName: string;
24
+ title: string;
25
+ description: string;
26
+ commitIds: string[];
27
+ }
28
+ declare const quickComposeResultJsonSchema: {
29
+ readonly $schema: "https://json-schema.org/draft/2020-12/schema";
30
+ readonly type: "object";
31
+ readonly properties: {
32
+ readonly branches: {
33
+ readonly minItems: 1;
34
+ readonly type: "array";
35
+ readonly items: {
36
+ readonly type: "object";
37
+ readonly properties: {
38
+ readonly id: {
39
+ readonly type: "string";
40
+ readonly minLength: 1;
41
+ readonly description: "Unique branch ID";
42
+ };
43
+ readonly name: {
44
+ readonly type: "string";
45
+ readonly description: "Branch name (empty string for commits-only mode)";
46
+ };
47
+ readonly title: {
48
+ readonly type: "string";
49
+ readonly description: "Short PR-style title (under 72 chars, imperative mood)";
50
+ };
51
+ readonly description: {
52
+ readonly type: "string";
53
+ readonly description: "What this branch accomplishes";
54
+ };
55
+ readonly commits: {
56
+ readonly minItems: 1;
57
+ readonly type: "array";
58
+ readonly items: {
59
+ readonly type: "object";
60
+ readonly properties: {
61
+ readonly id: {
62
+ readonly type: "string";
63
+ readonly description: "Unique commit ID (e.g. commit-1)";
64
+ };
65
+ readonly message: {
66
+ readonly type: "string";
67
+ readonly description: "Commit message";
68
+ };
69
+ readonly explanation: {
70
+ readonly type: "string";
71
+ readonly description: "Detailed explanation of the commit";
72
+ };
73
+ readonly hunks: {
74
+ readonly type: "array";
75
+ readonly items: {
76
+ readonly type: "integer";
77
+ readonly minimum: -9007199254740991;
78
+ readonly maximum: 9007199254740991;
79
+ readonly description: "Hunk index";
80
+ };
81
+ readonly description: "Hunk indices included in this commit";
82
+ };
83
+ };
84
+ readonly required: readonly ["id", "message", "explanation", "hunks"];
85
+ readonly additionalProperties: false;
86
+ };
87
+ readonly description: "Commits in this branch";
88
+ };
89
+ readonly orderedCommitIds: {
90
+ readonly minItems: 1;
91
+ readonly type: "array";
92
+ readonly items: {
93
+ readonly type: "string";
94
+ };
95
+ readonly description: "Commit IDs in optimal order";
96
+ };
97
+ readonly partitions: {
98
+ readonly description: "Stacked sub-branches (only for multi-branch mode)";
99
+ readonly type: "array";
100
+ readonly items: {
101
+ readonly type: "object";
102
+ readonly properties: {
103
+ readonly branchName: {
104
+ readonly type: "string";
105
+ readonly minLength: 1;
106
+ readonly description: "Git branch name for this partition";
107
+ };
108
+ readonly title: {
109
+ readonly type: "string";
110
+ readonly description: "Short PR-style title";
111
+ };
112
+ readonly description: {
113
+ readonly type: "string";
114
+ readonly description: "What this partition accomplishes";
115
+ };
116
+ readonly commitIds: {
117
+ readonly minItems: 1;
118
+ readonly type: "array";
119
+ readonly items: {
120
+ readonly type: "string";
121
+ };
122
+ readonly description: "Ordered commit IDs in this partition";
123
+ };
124
+ };
125
+ readonly required: readonly ["branchName", "title", "description", "commitIds"];
126
+ readonly additionalProperties: false;
127
+ };
128
+ };
129
+ };
130
+ readonly required: readonly ["id", "name", "title", "description", "commits", "orderedCommitIds"];
131
+ readonly additionalProperties: false;
132
+ };
133
+ readonly description: "Branches with grouped, ordered commits";
134
+ };
135
+ readonly rationale: {
136
+ readonly type: "string";
137
+ readonly description: "Brief explanation of the grouping and ordering rationale";
138
+ };
139
+ };
140
+ readonly required: readonly ["branches", "rationale"];
141
+ readonly additionalProperties: false;
142
+ };
26
143
  //#endregion
27
- export { quickComposeResultSchema };
144
+ export { QuickComposeWire, QuickComposeWireBranch, QuickComposeWireCommit, QuickComposeWirePartition, quickComposeResultJsonSchema };
@@ -1 +1 @@
1
- import{z as e}from"zod";const t=e.object({id:e.string().describe(`Unique commit ID (e.g. commit-1)`),message:e.string().describe(`Commit message`),explanation:e.string().describe(`Detailed explanation of the commit`),hunks:e.array(e.number().int().describe(`Hunk index`)).describe(`Hunk indices included in this commit`)}),n=e.object({branchName:e.string().min(1).describe(`Git branch name for this partition`),title:e.string().describe(`Short PR-style title`),description:e.string().describe(`What this partition accomplishes`),commitIds:e.array(e.string()).min(1).describe(`Ordered commit IDs in this partition`)}),r=e.object({id:e.string().min(1).describe(`Unique branch ID`),name:e.string().describe(`Branch name (empty string for commits-only mode)`),title:e.string().describe(`Short PR-style title (under 72 chars, imperative mood)`),description:e.string().describe(`What this branch accomplishes`),commits:e.array(t).min(1).describe(`Commits in this branch`),orderedCommitIds:e.array(e.string()).min(1).describe(`Commit IDs in optimal order`),partitions:e.array(n).optional().describe(`Stacked sub-branches (only for multi-branch mode)`)}),i=e.object({branches:e.array(r).min(1).describe(`Branches with grouped, ordered commits`),rationale:e.string().describe(`Brief explanation of the grouping and ordering rationale`)});export{i as quickComposeResultSchema};
1
+ const e={$schema:`https://json-schema.org/draft/2020-12/schema`,type:`object`,properties:{branches:{minItems:1,type:`array`,items:{type:`object`,properties:{id:{type:`string`,minLength:1,description:`Unique branch ID`},name:{type:`string`,description:`Branch name (empty string for commits-only mode)`},title:{type:`string`,description:`Short PR-style title (under 72 chars, imperative mood)`},description:{type:`string`,description:`What this branch accomplishes`},commits:{minItems:1,type:`array`,items:{type:`object`,properties:{id:{type:`string`,description:`Unique commit ID (e.g. commit-1)`},message:{type:`string`,description:`Commit message`},explanation:{type:`string`,description:`Detailed explanation of the commit`},hunks:{type:`array`,items:{type:`integer`,minimum:-9007199254740991,maximum:9007199254740991,description:`Hunk index`},description:`Hunk indices included in this commit`}},required:[`id`,`message`,`explanation`,`hunks`],additionalProperties:!1},description:`Commits in this branch`},orderedCommitIds:{minItems:1,type:`array`,items:{type:`string`},description:`Commit IDs in optimal order`},partitions:{description:`Stacked sub-branches (only for multi-branch mode)`,type:`array`,items:{type:`object`,properties:{branchName:{type:`string`,minLength:1,description:`Git branch name for this partition`},title:{type:`string`,description:`Short PR-style title`},description:{type:`string`,description:`What this partition accomplishes`},commitIds:{minItems:1,type:`array`,items:{type:`string`},description:`Ordered commit IDs in this partition`}},required:[`branchName`,`title`,`description`,`commitIds`],additionalProperties:!1}}},required:[`id`,`name`,`title`,`description`,`commits`,`orderedCommitIds`],additionalProperties:!1},description:`Branches with grouped, ordered commits`},rationale:{type:`string`,description:`Brief explanation of the grouping and ordering rationale`}},required:[`branches`,`rationale`],additionalProperties:!1};export{e as quickComposeResultJsonSchema};
@@ -1,14 +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){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(`, `)}.
1
+ import{extractOutputTag as e}from"../ai-loop.js";import{validateJsonSchema as t}from"../utils/json-schema.js";import{quickComposeResultJsonSchema as n}from"./schema.js";function r(r,c,l=`branches`,u=!1,d){let f=null;try{if(f=e(r),!f)return o([`No <output> tag found in response`],s(l,u));let p=t(n,JSON.parse(f));if(!p.success)return o(p.issues.map(e=>`${e.path.join(`.`)}: ${e.message}`),s(l,u));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 o([`Duplicate branch ID: '${e.id}'`],s(l,u));if(h.add(e.id),l===`branches`&&e.name.length===0)return o([`Branch '${e.id}': missing or empty 'name'`],s(l,u));if(l===`branches`&&e.name.length>0){if(g.has(e.name))return o([`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),d?.has(e.name))return o([`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=l===`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 r=new Set(e.orderedCommitIds);if(r.size!==e.orderedCommitIds.length)return o([`Branch '${e.id}': orderedCommitIds contains duplicates`],`The orderedCommitIds list for branch '${e.id}' contains duplicates. Each commit ID must appear exactly once.`);let c=[...n].filter(e=>!r.has(e)),f=e.orderedCommitIds.filter(e=>!n.has(e));if(c.length>0)return o([`Branch '${e.id}': orderedCommitIds is missing commits: ${c.join(`, `)}`],`The orderedCommitIds for branch '${e.id}' must include every commit. Missing: ${c.join(`, `)}`);if(f.length>0)return o([`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 i=p[r];if(!a(i.branchName))return o([`Branch '${e.id}', partition ${r}: '${i.branchName}' is not a valid git branch name`],`The branch name '${i.branchName}' is invalid. Git branch names cannot contain spaces, '..', '~', '^', ':', '\\', or end with '.lock'. Please provide a valid name.`);if(t.has(i.branchName))return o([`Branch '${e.id}': duplicate partition name '${i.branchName}'`],`Multiple partitions use the branch name '${i.branchName}'. Each must be unique.`);if(t.add(i.branchName),d?.has(i.branchName))return o([`Partition branch name '${i.branchName}' already exists in the repository`],`The branch name '${i.branchName}' already exists in the repository. Please choose a different name.`);for(let e of i.commitIds)n.push(e)}let s=new Set(n),c=e.orderedCommitIds.filter(e=>!s.has(e)),l=n.filter(e=>!r.has(e));if(c.length>0)return o([`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 o([`Branch '${e.id}': partition references unknown commits: ${l.join(`, `)}`],`Partition references unknown commits: ${l.join(`, `)}`);if(!i(p,e.orderedCommitIds))return o([`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 o([`Duplicate commit IDs: ${x.join(`, `)}`],`Some commit IDs appear multiple times. Each must be unique.\n\nDuplicates: ${x.join(`, `)}`);if(S.length>0)return o([`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
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.
4
+ `),n=[...y.values()].reduce((e,t)=>e+t.length,0);return o([`Duplicate hunks: ${e.join(`, `)}`],`Some hunks appear in multiple commits. Each hunk can only be used once.
5
5
 
6
- Your response listed ${n} hunk references across ${y.size} unique indices. Expected: ${s} references across ${s} unique indices.
6
+ Your response listed ${n} hunk references across ${y.size} unique indices. Expected: ${c} references across ${c} unique indices.
7
7
 
8
8
  Duplicates (each hunk index → the commits it landed in):
9
9
  ${t}
10
10
 
11
- To fix: for each duplicated hunk index, decide which one commit it truly belongs in and remove it from the others. Do not split a single hunk across two commits — git only knows how to apply a hunk once.`)}let w=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:
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:c},(e,t)=>t)),T=[...w].filter(e=>!_.has(e)),E=[..._].filter(e=>!w.has(e));return T.length>0?o([`Missing hunks: ${T.join(`, `)}`],`Some hunks are not assigned to any commit. All hunks must be included.\n\nMissing: ${T.join(`, `)}`):E.length>0?o([`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),n=(f??r).slice(-300);return o([`Failed to parse response as valid JSON: ${t}`,`Response tail: ${n}`],s(l,u))}}function i(e,t){let n=new Map(t.map((e,t)=>[e,t])),r=0;for(let t of e)for(let e of t.commitIds){let t=n.get(e);if(t===void 0||t!==r)return!1;r++}return r===t.length}function a(e){return e.startsWith(`-`)||e.startsWith(`.`)||e.endsWith(`.lock`)||e.endsWith(`.`)||e.endsWith(`/`)||e.includes(`..`)||e.includes(`~`)||e.includes(`^`)||e.includes(`:`)||e.includes(`\\`)||e.includes(` `)||e.includes(`?`)||e.includes(`*`)||e.includes(`[`)||e.includes(`@{`)?!1:e.length>0}function o(e,t){return{valid:!1,errors:e,retryFeedback:t}}function s(e,t){return`Your response has an invalid structure. Please return JSON inside <output> tags:
12
12
  <output>
13
13
  {
14
14
  "branches": [
@@ -30,4 +30,4 @@ To fix: for each duplicated hunk index, decide which one commit it truly belongs
30
30
  }
31
31
  </output>
32
32
 
33
- Return only the <output> tag and no other text.`}export{n as validateQuickComposeResponse};
33
+ Return only the <output> tag and no other text.`}export{r as validateQuickComposeResponse};
@@ -0,0 +1,40 @@
1
+ import { ComposeHunk } from "../types.js";
2
+ import { RedactHunkContent } from "./redact.js";
3
+
4
+ //#region src/tasks/utils/budget.d.ts
5
+ /**
6
+ * Ceiling on estimated input tokens for a single hunk-bearing prompt.
7
+ *
8
+ * Production p99 for the composer sits around 150k input tokens; pathological diffs
9
+ * (lockfile-dominated) reach 700k-1.7M. 180k leaves headroom above normal usage while
10
+ * still forcing degradation on the pathological tail, and stays comfortably inside the
11
+ * 200k context window of the models the composer targets.
12
+ */
13
+ declare const DEFAULT_INPUT_TOKEN_BUDGET = 180000;
14
+ /** Rough char→token estimate shared with `runAiLoop`'s prompt-gate heuristic. */
15
+ declare function estimateTokens(chars: number): number;
16
+ interface AssembledHunkPrompt {
17
+ prompt: string;
18
+ /**
19
+ * Hunks that lost their real body to budget pressure (full to condensed). Non-zero means
20
+ * content-dependent reasoning (analyze/group dependency detection) saw less than the full diff.
21
+ */
22
+ hunksCondensedForBudget: number;
23
+ }
24
+ /**
25
+ * Serializes `hunks` into the `<hunks>` JSON block and assembles the full prompt via
26
+ * `assemble`, keeping estimated input tokens under `inputTokenBudget`. Noise hunks (per
27
+ * `redactHunkContent`) start redacted, `condensed` forces metadata-only, else full; over
28
+ * budget, hunks are condensed biggest-first, throwing {@link DiffTooLargeError} if they still
29
+ * cannot fit. `assemble` is called with `''` to measure fixed overhead, so it must be pure.
30
+ */
31
+ declare function assembleHunkPrompt(params: {
32
+ hunks: ComposeHunk[];
33
+ assemble: (hunksJson: string) => string;
34
+ condensed?: boolean;
35
+ redactHunkContent?: RedactHunkContent;
36
+ inputTokenBudget?: number;
37
+ systemPromptChars?: number;
38
+ }): AssembledHunkPrompt;
39
+ //#endregion
40
+ export { AssembledHunkPrompt, DEFAULT_INPUT_TOKEN_BUDGET, assembleHunkPrompt, estimateTokens };
@@ -0,0 +1 @@
1
+ import{DiffTooLargeError as e}from"../../errors/task.js";import{estimatedCharactersPerToken as t}from"../ai-loop.js";import{REDACTED_HUNK_CONTENT as n}from"./redact.js";const r=18e4;function i(e){return Math.ceil(e/t)}function a(e,t){let r={...e.isBinary?{isBinary:!0}:{},...e.isRename?{isRename:!0}:{},...e.originalFileName?{originalFileName:e.originalFileName}:{}};return t===`condensed`?{index:e.index,fileName:e.fileName,hunkHeader:e.hunkHeader,additions:e.additions,deletions:e.deletions,...r}:t===`redacted`?{index:e.index,fileName:e.fileName,hunkHeader:e.hunkHeader,additions:e.additions,deletions:e.deletions,content:n,...r}:{index:e.index,fileName:e.fileName,hunkHeader:e.hunkHeader,content:e.content,...r}}function o(e,t){return JSON.stringify(a(e,t)).length}function s(e,t,n,r){let a=new Map(t),s=2+Math.max(0,e.length-1),c=0,l=e.map(e=>{let t=a.get(e.index),n=o(e,`condensed`),r=t===`condensed`?n:o(e,t);return c+=r,{hunk:e,currentLen:r,condensedLen:n}}),u=()=>i(r+s+c),d=()=>u()>n;if(d()){let e=l.map(e=>({hunk:e.hunk,savings:e.currentLen-e.condensedLen})).filter(e=>e.savings>0).sort((e,t)=>t.savings-e.savings);for(let{hunk:t,savings:n}of e){if(!d())break;c-=n,a.set(t.index,`condensed`)}}return{modes:a,estimatedTokens:u(),tooLarge:d()}}function c(t){let{hunks:n,assemble:i,condensed:o,redactHunkContent:c,inputTokenBudget:l=r,systemPromptChars:u=0}=t,d=new Map(n.map(e=>o?[e.index,`condensed`]:c?.(e)?[e.index,`redacted`]:[e.index,`full`])),{modes:f,estimatedTokens:p,tooLarge:m}=s(n,d,l,i(``).length+u);if(m)throw new e(p,l);let h=0;for(let e of n)d.get(e.index)===`full`&&f.get(e.index)===`condensed`&&h++;return{prompt:i(JSON.stringify(n.map(e=>a(e,f.get(e.index))))),hunksCondensedForBudget:h}}export{r as DEFAULT_INPUT_TOKEN_BUDGET,c as assembleHunkPrompt,i as estimateTokens};
@@ -0,0 +1,61 @@
1
+ //#region src/tasks/utils/json-schema.d.ts
2
+ interface SchemaIssue {
3
+ path: (string | number)[];
4
+ message: string;
5
+ }
6
+ type SchemaResult<T> = {
7
+ success: true;
8
+ data: T;
9
+ } | {
10
+ success: false;
11
+ issues: SchemaIssue[];
12
+ };
13
+ interface SchemaAnnotations {
14
+ $schema?: string;
15
+ description?: string;
16
+ }
17
+ interface ObjectSchemaDef extends SchemaAnnotations {
18
+ type: 'object';
19
+ properties: Readonly<Record<string, JsonSchemaDef>>;
20
+ required: readonly string[];
21
+ additionalProperties: false;
22
+ }
23
+ interface ArraySchemaDef extends SchemaAnnotations {
24
+ type: 'array';
25
+ items: JsonSchemaDef;
26
+ minItems?: number;
27
+ default?: readonly unknown[];
28
+ }
29
+ interface StringSchemaDef extends SchemaAnnotations {
30
+ type: 'string';
31
+ minLength?: number;
32
+ maxLength?: number;
33
+ enum?: readonly string[];
34
+ }
35
+ interface NumberSchemaDef extends SchemaAnnotations {
36
+ type: 'number' | 'integer';
37
+ minimum?: number;
38
+ maximum?: number;
39
+ }
40
+ interface BooleanSchemaDef extends SchemaAnnotations {
41
+ type: 'boolean';
42
+ }
43
+ /**
44
+ * The JSON-Schema subset used by the task output schemas. Deliberately closed: no `anyOf`,
45
+ * `const`, `$ref`, `pattern`, `format`, record types, or type arrays — widen only when a task
46
+ * schema actually needs more, and keep prompt snapshots byte-stable when doing so.
47
+ */
48
+ type JsonSchemaDef = ObjectSchemaDef | ArraySchemaDef | StringSchemaDef | NumberSchemaDef | BooleanSchemaDef;
49
+ /**
50
+ * Validates `value` against `schema` with the same observable semantics the previous zod
51
+ * schemas had: every issue is collected in one pass, unknown object keys are silently
52
+ * stripped, a `required` property whose subschema carries `default` is filled with a fresh
53
+ * clone of that default when absent, and issue messages mirror zod v4 wording so telemetry
54
+ * and error logs stay comparable across the migration. One deliberate divergence: on a type
55
+ * mismatch zod could add a second size-constraint issue keyed off the wrong-typed value's
56
+ * `.length`; this validator reports only the type issue (same failure outcome, one issue
57
+ * fewer). On success `data` is a rebuilt tree, never the input by reference.
58
+ */
59
+ declare function validateJsonSchema<T>(schema: JsonSchemaDef, value: unknown): SchemaResult<T>;
60
+ //#endregion
61
+ export { ArraySchemaDef, BooleanSchemaDef, JsonSchemaDef, NumberSchemaDef, ObjectSchemaDef, SchemaIssue, SchemaResult, StringSchemaDef, validateJsonSchema };
@@ -0,0 +1 @@
1
+ function e(e,n){let r=[],i=t(e,n,[],r);return r.length>0?{success:!1,issues:r}:{success:!0,data:i}}function t(e,t,s,c){switch(e.type){case`object`:return n(e,t,s,c);case`array`:return r(e,t,s,c);case`string`:return i(e,t,s,c);case`number`:case`integer`:return a(e,t,s,c);case`boolean`:if(typeof t!=`boolean`){c.push({path:s,message:o(`boolean`,t)});return}return t}}function n(e,n,r,i){if(typeof n!=`object`||!n||Array.isArray(n)){i.push({path:r,message:o(`object`,n)});return}let a=n,s=new Set(e.required),c={};for(let[n,o]of Object.entries(e.properties)){let e=a[n];if(e===void 0){o.type===`array`&&o.default!==void 0?c[n]=structuredClone(o.default):s.has(n)&&t(o,void 0,[...r,n],i);continue}c[n]=t(o,e,[...r,n],i)}return c}function r(e,n,r,i){if(!Array.isArray(n)){i.push({path:r,message:o(`array`,n)});return}return e.minItems!==void 0&&n.length<e.minItems&&i.push({path:r,message:`Too small: expected array to have >=${e.minItems} items`}),n.map((n,a)=>t(e.items,n,[...r,a],i))}function i(e,t,n,r){if(e.enum!==void 0){if(typeof t!=`string`||!e.enum.includes(t)){let t=e.enum.map(e=>JSON.stringify(e)).join(`|`);r.push({path:n,message:`Invalid option: expected one of ${t}`});return}return t}if(typeof t!=`string`){r.push({path:n,message:o(`string`,t)});return}return e.minLength!==void 0&&t.length<e.minLength&&r.push({path:n,message:`Too small: expected string to have >=${e.minLength} characters`}),e.maxLength!==void 0&&t.length>e.maxLength&&r.push({path:n,message:`Too big: expected string to have <=${e.maxLength} characters`}),t}function a(e,t,n,r){if(typeof t!=`number`||!Number.isFinite(t)){r.push({path:n,message:o(`number`,t)});return}if(e.type===`integer`&&!Number.isInteger(t))return r.push({path:n,message:`Invalid input: expected int, received number`}),t;let i=e.type===`integer`?`int`:`number`;return e.minimum!==void 0&&t<e.minimum&&r.push({path:n,message:`Too small: expected ${i} to be >=${e.minimum}`}),e.maximum!==void 0&&t>e.maximum&&r.push({path:n,message:`Too big: expected ${i} to be <=${e.maximum}`}),t}function o(e,t){return`Invalid input: expected ${e}, received ${s(t)}`}function s(e){return e===void 0?`undefined`:e===null?`null`:Array.isArray(e)?`array`:typeof e==`number`&&Number.isNaN(e)?`NaN`:typeof e}export{e as validateJsonSchema};
@@ -0,0 +1,21 @@
1
+ import { ComposeHunk } from "../types.js";
2
+ import { RedactHunkContent } from "./redact.js";
3
+
4
+ //#region src/tasks/utils/noise.d.ts
5
+ /**
6
+ * Default {@link RedactHunkContent} predicate for hunks that carry near-zero grouping signal
7
+ * but can dominate token count: lockfiles, vendored/build-output trees, minified or generated
8
+ * files, and binaries. Matched hunks keep their metadata; only the body is masked. Detects by
9
+ * path, by content markers, by minified line length, and by `isBinary`; renames are matched on
10
+ * `originalFileName` too.
11
+ */
12
+ declare function isNoiseHunk(hunk: ComposeHunk): boolean;
13
+ /**
14
+ * Combines the built-in {@link isNoiseHunk} detector with an optional caller predicate.
15
+ * The result redacts a hunk when either predicate matches, so consumers extend the
16
+ * default without losing noise reduction. Returns {@link isNoiseHunk} unchanged when no
17
+ * caller predicate is supplied.
18
+ */
19
+ declare function withDefaultNoiseRedaction(caller: RedactHunkContent | undefined): RedactHunkContent;
20
+ //#endregion
21
+ export { isNoiseHunk, withDefaultNoiseRedaction };
@@ -0,0 +1,2 @@
1
+ const e=new Set([`package-lock.json`,`npm-shrinkwrap.json`,`yarn.lock`,`pnpm-lock.yaml`,`bun.lock`,`bun.lockb`,`composer.lock`,`gemfile.lock`,`podfile.lock`,`cargo.lock`,`poetry.lock`,`pipfile.lock`,`go.sum`,`flake.lock`,`gradle.lockfile`,`packages.lock.json`,`mix.lock`,`pubspec.lock`]),t=new Set([`node_modules`,`vendor`,`bower_components`,`dist`,`build`,`out`,`.next`,`.nuxt`,`coverage`,`__generated__`]),n=[/\.min\.(js|css|mjs|cjs)$/,/\.bundle\.js$/,/\.map$/,/\.generated\./,/\.pb\.(go|cc|h)$/,/_pb2(_grpc)?\.py$/,/\.(g|freezed)\.dart$/,/\.designer\.cs$/],r=[/@generated\b/,/Code generated .* DO NOT EDIT/,/linguist-generated/,/This file (was|is) (auto(matically)?[- ]?)?generated/i];function i(e){let t=e.replace(/\\/g,`/`),n=t.lastIndexOf(`/`);return n===-1?t:t.slice(n+1)}function a(r){let a=r.replace(/\\/g,`/`),o=i(a).toLowerCase();return e.has(o)||n.some(e=>e.test(o))?!0:a.split(`/`).some(e=>t.has(e.toLowerCase()))}function o(e){if(!e)return!1;let t=e.slice(0,4e3);if(r.some(e=>e.test(t)))return!0;let n=0;for(let t=0;t<e.length;t++){if(e[t]===`
2
+ `){n=0;continue}if(n++,n>=5e3)return!0}return!1}function s(e){return e.isBinary||a(e.fileName)||e.originalFileName&&a(e.originalFileName)?!0:o(e.content)}function c(e){return e?t=>s(t)||e(t):s}export{s as isNoiseHunk,c as withDefaultNoiseRedaction};
@@ -10,5 +10,11 @@ declare const REDACTED_HUNK_CONTENT = "[content redacted because this file is ex
10
10
  */
11
11
  type RedactHunkContent = (hunk: ComposeHunk) => boolean;
12
12
  declare function hunkContentForPrompt(hunk: ComposeHunk, redact: RedactHunkContent | undefined): string;
13
+ /**
14
+ * Wraps a predicate so each `hunk.index` is evaluated once. The noise detector scans hunk
15
+ * content, and the same predicate feeds both the redacted-count tally and every prompt build,
16
+ * so caching avoids repeated scans and keeps the two in agreement for non-deterministic inputs.
17
+ */
18
+ declare function memoizeRedactHunkContent(redact: RedactHunkContent): RedactHunkContent;
13
19
  //#endregion
14
- export { REDACTED_HUNK_CONTENT, RedactHunkContent, hunkContentForPrompt };
20
+ export { REDACTED_HUNK_CONTENT, RedactHunkContent, hunkContentForPrompt, memoizeRedactHunkContent };
@@ -1 +1 @@
1
- const e=`[content redacted because this file is excluded from AI processing — still assign this hunk to a commit based on its filename]`;function t(t,n){return n?.(t)?e:t.content}export{e as REDACTED_HUNK_CONTENT,t as hunkContentForPrompt};
1
+ const e=`[content redacted because this file is excluded from AI processing — still assign this hunk to a commit based on its filename]`;function t(t,n){return n?.(t)?e:t.content}function n(e){let t=new Map;return n=>{let r=t.get(n.index);if(r!==void 0)return r;let i=e(n);return t.set(n.index,i),i}}export{e as REDACTED_HUNK_CONTENT,t as hunkContentForPrompt,n as memoizeRedactHunkContent};
@@ -43,6 +43,10 @@ interface ComposePlanInput {
43
43
  hunkFilter?: ComposeInput['hunkFilter'];
44
44
  /** See {@link ComposeInput.redactHunkContent}. */
45
45
  redactHunkContent?: ComposeInput['redactHunkContent'];
46
+ /** See {@link ComposeInput.disableDefaultNoiseReduction}. */
47
+ disableDefaultNoiseReduction?: ComposeInput['disableDefaultNoiseReduction'];
48
+ /** See {@link ComposeInput.inputTokenBudget}. */
49
+ inputTokenBudget?: ComposeInput['inputTokenBudget'];
46
50
  /** See {@link ComposeInput.session}. */
47
51
  session?: ComposeSession;
48
52
  /** See {@link ComposeInput.promptOverrides}. */
@@ -73,6 +77,10 @@ interface ComposePlanResult {
73
77
  feedbackTargetCallId?: string;
74
78
  /** See {@link ComposeResult.hunksFilteredCount}. */
75
79
  hunksFilteredCount?: number;
80
+ /** See {@link ComposeResult.hunksRedactedCount}. */
81
+ hunksRedactedCount?: number;
82
+ /** See {@link ComposeResult.hunksCondensedCount}. */
83
+ hunksCondensedCount?: number;
76
84
  /** See {@link ComposeResult.diffStats}. */
77
85
  diffStats?: ComposeDiffStats;
78
86
  }
@@ -217,8 +225,23 @@ interface ComposeInput {
217
225
  *
218
226
  * Intended for `.aiexclude`/`.aiignore`/`.cursorignore`-style features: the AI assigns
219
227
  * matching hunks to commits by filename alone, never seeing the diff body.
228
+ *
229
+ * Combined (OR) with the built-in noise detector unless `disableDefaultNoiseReduction`
230
+ * is set: a caller predicate extends the default, it does not replace it.
220
231
  */
221
232
  redactHunkContent?: RedactHunkContent;
233
+ /**
234
+ * Opt out of the built-in noise detector (lockfiles, vendored/build trees, minified or
235
+ * generated files, binaries). On by default so pathological diffs don't send megabytes
236
+ * of generated content verbatim. When true, only `redactHunkContent` (if any) applies.
237
+ */
238
+ disableDefaultNoiseReduction?: boolean;
239
+ /**
240
+ * Input-token ceiling for the hunk-bearing prompts (compose-group, analyze, group). When
241
+ * exceeded, hunks are condensed to metadata biggest-first; a diff that cannot fit even then
242
+ * throws `DiffTooLargeError`. Defaults to `DEFAULT_INPUT_TOKEN_BUDGET` (180k).
243
+ */
244
+ inputTokenBudget?: number;
222
245
  /**
223
246
  * Per-task prompt overrides, keyed by {@link ComposeTaskName}. Consumers with
224
247
  * existing prompt template services (server-driven templates, provider-specific
@@ -415,6 +438,19 @@ interface ComposeResult {
415
438
  * Undefined when no filter ran.
416
439
  */
417
440
  hunksFilteredCount?: number;
441
+ /**
442
+ * Count of hunks whose patch body was masked before the AI saw it (built-in noise detector
443
+ * plus any consumer `redactHunkContent`), so consumers can audit the on-by-default noise
444
+ * reduction. Undefined when no redaction predicate ran (e.g. `disableDefaultNoiseReduction`
445
+ * with no consumer predicate, or the apply-only path).
446
+ */
447
+ hunksRedactedCount?: number;
448
+ /**
449
+ * Count of hunks condensed to metadata to fit the input-token budget (full to condensed).
450
+ * Non-zero signals the AI reasoned over less than the full diff, so grouping quality may
451
+ * degrade. Undefined when no AI phase ran (apply-only path).
452
+ */
453
+ hunksCondensedCount?: number;
418
454
  /** See {@link ApplyComposePlanResult.appliedCommitIds}. */
419
455
  appliedCommitIds?: string[];
420
456
  /** See {@link ApplyComposePlanResult.unappliedHunkIndices}. */
@@ -1,3 +1,3 @@
1
- import{GitError as e}from"../errors/git.js";import{ComposeWorkflowError as t,ComposeWorkflowInputError as n}from"../errors/workflow.js";import{applyPatchToIndex as r,currentBranch as i,diffTree as a,forEachRef as o,log as s,mergeBase as c,readTree as l,revParse as u,writeTree as d}from"../git/port-dispatch.js";import{validateRewriteRangeSafety as f}from"../git/safety-range.js";import{addUsage as p,emptyUsage as m}from"../ai/types.js";import{analyze as h}from"../tasks/analyze/analyze.js";import{composeGroup as g}from"../tasks/compose-group/compose-group.js";import{group as _}from"../tasks/group/group.js";import{order as v}from"../tasks/order/order.js";import{partition as y}from"../tasks/partition/partition.js";import{buildPatchForCommit as b}from"../git/utils/patch-builder.js";import{sha256 as x}from"../git/utils/crypto.js";import{collectFromSource as S}from"../git/diff-collector.js";import{verifySnapshot as C}from"../git/safety.js";import{applyToTarget as w}from"../git/apply-target.js";import{requireExec as T}from"../git/require-exec.js";import{extractConventions as E}from"../git/utils/conventions.js";import{rethrowAsWorkflowError as D}from"../utils/cancellation.js";import{randomUUID as O}from"node:crypto";import{unlinkSync as k}from"node:fs";import{tmpdir as A}from"node:os";import{join as j}from"node:path";import{UndoStore as M,capturePreOperationState as N,generateUndoId as P}from"@gitkraken/shared-tools";const F={collecting:{message:`Collecting changes…`,weight:5},composing:{message:`Composing commits…`,weight:40},"compose-grouping":{message:`Grouping changes into commits…`,weight:40},analyzing:{message:`Analyzing changes…`,weight:20},grouping:{message:`Grouping changes into commits…`,weight:25},ordering:{message:`Ordering commits…`,weight:15},partitioning:{message:`Subdividing branches…`,weight:20},verifying:{message:`Verifying…`,weight:5},applying:{message:`Applying to repository…`,weight:10}};async function I(r){if(!r.applyPlan&&!r.model)throw new n(`Either 'model' or 'applyPlan' must be provided`);if(r.applyPlan)return ne(r);let{git:i,model:a,source:o={type:`workdir`},onProgress:s,onBeforeApply:c,cancellation:l}=r,u=a,d=r.depth??`balanced`,p=r.target??await Z(i,o);re(o,p),await q(i,r.applyCommitIds,r.hunkFilter,o,p),await f({git:i,source:o,target:p,overrides:r.overrideSafetyFailures});let m=[];s?.({phase:`collecting`,...F.collecting});let h=Date.now(),g=await S(i,o);if(m.push({name:`collecting`,durationMs:Date.now()-h,attempts:1}),g.hunks.length===0)throw new e(`NO_CHANGES`,Q(o));let{hunks:_,filteredCount:v}=await W(g.hunks,r.hunkFilter);if(_.length===0)throw new e(`NO_CHANGES`,Q(o));let y=H(_);s?.({phase:`collecting`,...F.collecting,stats:y});let b=v!==void 0&&v>0?await G(i,g.snapshot,_):g.snapshot,x={hunks:_,collection:g,conventions:r.conventions??await E({git:i,author:r.conventionAuthor}),existingBranchNames:await ie(i),groupMode:p.type===`auto`?`branches`:`commits`,target:p,model:u,instructions:r.instructions,context:r.context,strategy:r.strategy,maxRetries:r.maxRetries,maxTokens:r.maxTokens,onProgress:s,cancellation:l,onBeforeModelCall:r.onBeforeModelCall,onAfterModelCall:r.onAfterModelCall,onBeforePrompt:r.onBeforePrompt,session:r.session,promptOverrides:r.promptOverrides,redactHunkContent:r.redactHunkContent},O;try{O=await ee(d,x)}catch(e){D(e,l)}let k={analysis:O.analysis,grouping:O.grouping,ordering:O.ordering,branches:O.branchPlans,allOrderedCommits:O.allOrderedCommits};if(m.push(...O.phases),c&&!await c(k))return{plan:k,snapshot:b,session:O.session,usage:O.usage,phases:m,source:{type:o.type===`branch`?`branch`:`workdir`,hunks:_,commitMessages:g.commitMessages,branchName:g.branchName,baseBranch:g.baseBranch},diffStats:y,feedbackTargetCallId:U(m),hunksFilteredCount:v};let A=await N(T(i,`capturing pre-operation state`));s?.({phase:`verifying`,...F.verifying});let j=Date.now(),M=await C(i,g.snapshot,K(o,g));if(!M.valid)throw new t(`SAFETY_CHECK_FAILED`,`Safety check failed: ${M.error}`);m.push({name:`verifying`,durationMs:Date.now()-j,attempts:1});let P=J(O.allOrderedCommits,r.applyCommitIds),{leftoverHunks:I,leftoverPatch:L}=X(P,g.hunks),R=I.length>0?await G(i,b,Y(P,g.hunks)):b;s?.({phase:`applying`,...F.applying});let z=Date.now(),B=await w({git:i,source:o,target:p,branchPlans:O.branchPlans,allOrderedCommits:P,hunks:g.hunks,snapshot:R,collection:g,signing:r.signing,authorAttribution:r.authorAttribution,stashLabel:r.stashLabel,leftoverPatch:L});m.push({name:`applying`,durationMs:Date.now()-z,attempts:1});let V=await ae(i,A,B.mutations,p,o);return{plan:k,snapshot:R,partitions:B.partitions,partitionGroups:B.partitionGroups,session:O.session,usage:O.usage,phases:m,source:{type:o.type===`branch`?`branch`:`workdir`,hunks:_,commitMessages:g.commitMessages,branchName:g.branchName,baseBranch:g.baseBranch},diffStats:y,commitShas:B.commitShas,undoId:V,feedbackTargetCallId:U(m),hunksFilteredCount:v,appliedCommitIds:P.map(e=>e.id),unappliedHunkIndices:I.length>0?I.map(e=>e.index):void 0,stashConflict:B.stashConflict}}function ee(e,t){switch(e){case`quick`:return L(t);case`balanced`:return R(t);case`deep`:return B(t)}}async function L(e){return z(e,{lightweight:!0,skipOrder:!0})}async function R(e){return z(e,{includeAnalysis:!0})}async function z(e,t){let{hunks:n,collection:r,conventions:i,existingBranchNames:a,groupMode:o,target:s,model:c,instructions:l,context:u,strategy:d,maxRetries:f,maxTokens:h,onProgress:_,cancellation:b,onBeforeModelCall:x,onAfterModelCall:S,onBeforePrompt:C}=e,w=m(),T=[],E=await g({hunks:n,commitMessages:r.commitMessages,commitOrder:r.commitOrder,branchName:r.branchName,baseBranch:r.baseBranch,issueUrl:u?.issueUrl,pullRequestUrl:u?.pullRequestUrl,instructions:l,conventions:i,mode:o,strategy:d?.grouping,existingBranchNames:o===`branches`?a:void 0,includeAnalysis:t.includeAnalysis,lightweight:t.lightweight,condensed:t.condensed,redactHunkContent:e.redactHunkContent,model:c,session:e.session,maxRetries:f,maxTokens:h,onProgress:e=>_?.({phase:`compose-grouping`,...F[`compose-grouping`],event:e}),cancellation:b,onBeforeModelCall:x,onAfterModelCall:S,onBeforePrompt:C,promptOverride:e.promptOverrides?.[`compose-group`]});E.usage&&p(w,E.usage),T.push({name:`compose-grouping`,durationMs:E.durationMs,attempts:E.attempts,usage:E.usage,callIds:E.callIds});let D=E.session,O,k;if(t.skipOrder)O=E.result.branches.map(e=>({branchGroup:e,orderedCommitIds:e.commits.map(e=>e.id)})),k={branches:O.map(e=>({branchId:e.branchGroup.id,orderedCommitIds:e.orderedCommitIds})),rationale:``};else{let t=await v({branches:E.result.branches,hunks:n,instructions:l,model:c,session:E.session,maxRetries:f,maxTokens:h,onProgress:e=>_?.({phase:`ordering`,...F.ordering,event:e}),cancellation:b,onBeforeModelCall:x,onAfterModelCall:S,onBeforePrompt:C,promptOverride:e.promptOverrides?.order});t.usage&&p(w,t.usage),T.push({name:`ordering`,durationMs:t.durationMs,attempts:t.attempts,usage:t.usage,callIds:t.callIds}),D=t.session,k=t.result,O=E.result.branches.map(e=>({branchGroup:e,orderedCommitIds:k.branches.find(t=>t.branchId===e.id)?.orderedCommitIds??e.commits.map(e=>e.id)}))}if(s.type===`auto`){let t=new Set(a);for(let e of E.result.branches)e.name&&t.add(e.name);for(let n of O){let r=await y({commits:$(n.branchGroup,n.orderedCommitIds),orderedCommitIds:n.orderedCommitIds,sourceBranchName:n.branchGroup.name,instructions:l,conventions:i,strategy:d?.partition,existingBranchNames:t,model:c,maxRetries:f,maxTokens:h,onProgress:e=>_?.({phase:`partitioning`,...F.partitioning,branch:n.branchGroup.name,event:e}),cancellation:b,onBeforeModelCall:x,onAfterModelCall:S,onBeforePrompt:C,promptOverride:e.promptOverrides?.partition});r.usage&&p(w,r.usage),T.push({name:`partitioning:${n.branchGroup.name}`,durationMs:r.durationMs,attempts:r.attempts,usage:r.usage,callIds:r.callIds}),n.partitioning=r.result,D=r.session;for(let e of r.result.partitions)t.add(e.branchName)}}let A=O.flatMap(e=>$(e.branchGroup,e.orderedCommitIds));return{analysis:void 0,grouping:E.result,ordering:k,branchPlans:O,allOrderedCommits:A,session:D,usage:w,phases:T}}async function B(e){let{hunks:t,collection:n,conventions:r,existingBranchNames:i,groupMode:a,target:o,model:s,instructions:c,context:l,strategy:u,maxRetries:d,maxTokens:f,onProgress:g,cancellation:b,onBeforeModelCall:x,onAfterModelCall:S,onBeforePrompt:C}=e,w=m(),T=[],E=await h({hunks:t,commitMessages:n.commitMessages,commitOrder:n.commitOrder,branchName:n.branchName,baseBranch:n.baseBranch,issueUrl:l?.issueUrl,pullRequestUrl:l?.pullRequestUrl,instructions:c,redactHunkContent:e.redactHunkContent,model:s,session:e.session,maxRetries:d,maxTokens:f,onProgress:e=>g?.({phase:`analyzing`,...F.analyzing,event:e}),cancellation:b,onBeforeModelCall:x,onAfterModelCall:S,onBeforePrompt:C,promptOverride:e.promptOverrides?.analyze});E.usage&&p(w,E.usage),T.push({name:`analyzing`,durationMs:E.durationMs,attempts:E.attempts,usage:E.usage,callIds:E.callIds});let D=await _({hunks:t,commitMessages:n.commitMessages,instructions:c,conventions:r,mode:a,strategy:u?.grouping,existingBranchNames:a===`branches`?i:void 0,redactHunkContent:e.redactHunkContent,model:s,session:E.session,maxRetries:d,maxTokens:f,onProgress:e=>g?.({phase:`grouping`,...F.grouping,event:e}),cancellation:b,onBeforeModelCall:x,onAfterModelCall:S,onBeforePrompt:C,promptOverride:e.promptOverrides?.group});D.usage&&p(w,D.usage),T.push({name:`grouping`,durationMs:D.durationMs,attempts:D.attempts,usage:D.usage,callIds:D.callIds});let O=await v({branches:D.result.branches,hunks:t,instructions:c,model:s,session:D.session,maxRetries:d,maxTokens:f,onProgress:e=>g?.({phase:`ordering`,...F.ordering,event:e}),cancellation:b,onBeforeModelCall:x,onAfterModelCall:S,onBeforePrompt:C,promptOverride:e.promptOverrides?.order});O.usage&&p(w,O.usage),T.push({name:`ordering`,durationMs:O.durationMs,attempts:O.attempts,usage:O.usage,callIds:O.callIds});let k=D.result.branches.map(e=>({branchGroup:e,orderedCommitIds:O.result.branches.find(t=>t.branchId===e.id)?.orderedCommitIds??e.commits.map(e=>e.id)})),A=O.session;if(o.type===`auto`){let t=new Set(i);for(let e of D.result.branches)e.name&&t.add(e.name);for(let n of k){let i=await y({commits:$(n.branchGroup,n.orderedCommitIds),orderedCommitIds:n.orderedCommitIds,sourceBranchName:n.branchGroup.name,instructions:c,conventions:r,strategy:u?.partition,existingBranchNames:t,model:s,maxRetries:d,maxTokens:f,onProgress:e=>g?.({phase:`partitioning`,...F.partitioning,branch:n.branchGroup.name,event:e}),cancellation:b,onBeforeModelCall:x,onAfterModelCall:S,onBeforePrompt:C,promptOverride:e.promptOverrides?.partition});i.usage&&p(w,i.usage),T.push({name:`partitioning:${n.branchGroup.name}`,durationMs:i.durationMs,attempts:i.attempts,usage:i.usage,callIds:i.callIds}),n.partitioning=i.result,A=i.session;for(let e of i.result.partitions)t.add(e.branchName)}}let j=k.flatMap(e=>$(e.branchGroup,e.orderedCommitIds));return{analysis:E.result,grouping:D.result,ordering:O.result,branchPlans:k,allOrderedCommits:j,session:A,usage:w,phases:T}}async function V(e){let t=await I({...e,onBeforeApply:async()=>!1});return{plan:t.plan,snapshot:t.snapshot,source:t.source,session:t.session,usage:t.usage,phases:t.phases,feedbackTargetCallId:t.feedbackTargetCallId,hunksFilteredCount:t.hunksFilteredCount,diffStats:t.diffStats}}async function te(e){let t=await I({git:e.git,applyPlan:e.applyPlan,source:e.source,target:e.target,onProgress:e.onProgress,signing:e.signing,authorAttribution:e.authorAttribution,applyCommitIds:e.applyCommitIds,stashLabel:e.stashLabel,hunkFilter:e.hunkFilter,overrideSafetyFailures:e.overrideSafetyFailures});return{commitShas:t.commitShas,partitions:t.partitions,partitionGroups:t.partitionGroups,phases:t.phases,undoId:t.undoId,appliedCommitIds:t.appliedCommitIds,unappliedHunkIndices:t.unappliedHunkIndices,stashConflict:t.stashConflict}}async function ne(n){let{git:r,applyPlan:i,onProgress:a}=n,{plan:o,snapshot:s,source:c}=i,l=n.source??c,u=n.target??i?.target??await Z(r,l);re(l,u),await q(r,n.applyCommitIds,n.hunkFilter,l,u),await f({git:r,source:l,target:u,overrides:n.overrideSafetyFailures});let d=await N(T(r,`capturing pre-operation state`)),p=[];a?.({phase:`collecting`,...F.collecting});let h=Date.now(),g=await S(r,l);if(p.push({name:`collecting`,durationMs:Date.now()-h,attempts:1}),g.hunks.length===0)throw new e(`NO_CHANGES`,Q(l));let{hunks:_}=await W(g.hunks,n.hunkFilter);if(_.length===0)throw new e(`NO_CHANGES`,Q(l));let v=H(_);a?.({phase:`collecting`,...F.collecting,stats:v});let y=_.length<g.hunks.length?await G(r,g.snapshot,_):g.snapshot,b={...g,hunks:_,snapshot:y};if(b.snapshot.diffHash!==s.diffHash)throw new t(`SAFETY_CHECK_FAILED`,`Working directory has changed since the plan was created. Re-run compose to generate a new plan.`);a?.({phase:`verifying`,...F.verifying});let x=Date.now(),E=await C(r,b.snapshot,K(l,b));if(!E.valid)throw new t(`SAFETY_CHECK_FAILED`,`Safety check failed: ${E.error}`);p.push({name:`verifying`,durationMs:Date.now()-x,attempts:1});let D=J(o.allOrderedCommits,n.applyCommitIds),{leftoverHunks:O,leftoverPatch:k}=X(D,b.hunks),A=O.length>0?await G(r,b.snapshot,Y(D,b.hunks)):b.snapshot;a?.({phase:`applying`,...F.applying});let j=Date.now(),M=await w({git:r,source:l,target:u,branchPlans:o.branches,allOrderedCommits:D,hunks:b.hunks,snapshot:A,collection:b,signing:n.signing,authorAttribution:n.authorAttribution,stashLabel:n.stashLabel,leftoverPatch:k});p.push({name:`applying`,durationMs:Date.now()-j,attempts:1});let P=await ae(r,d,M.mutations,u,l);return{plan:o,snapshot:A,partitions:M.partitions,partitionGroups:M.partitionGroups,session:{messages:[],systemPrompt:``,completedSteps:{}},usage:m(),phases:p,source:{type:l.type===`branch`?`branch`:`workdir`,hunks:b.hunks,commitMessages:b.commitMessages,branchName:b.branchName,baseBranch:b.baseBranch},diffStats:v,commitShas:M.commitShas,undoId:P,appliedCommitIds:D.map(e=>e.id),unappliedHunkIndices:O.length>0?O.map(e=>e.index):void 0,stashConflict:M.stashConflict}}function H(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 U(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 W(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 G(e,t,n){let i=t.baseSha??t.headSha,o=j(A(),`compose-filtered-idx-${O()}`),s={GIT_INDEX_FILE:o};try{await l(e,i,{indexEnv:s});let o=b(n);o.length>0&&await r(e,o,{indexEnv:s});let c=await a(e,i,await d(e,{indexEnv:s}));return{...t,diffHash:x(c)}}finally{try{k(o)}catch{}}}function K(e,t){return e.type===`workdir`?{includeUnstaged:!e.stagedOnly,includeUntracked:t.untrackedIncluded}:e.type===`commit-range`&&e.includeWorkdir?{includeStaged:e.includeWorkdir.includeStaged??!0,includeUnstaged:e.includeWorkdir.includeUnstaged??!0,includeUntracked:e.includeWorkdir.includeUntracked??!0}:{includeUnstaged:!1,includeUntracked:!1}}async function q(e,t,r,a,o){if(r==null&&t==null||a.type===`workdir`||!(o.type===`rewrite-range`||(o.type===`branch`||o.type===`auto`)&&o.preserveOriginal!==!0))return;let s=a.type===`branch`?await u(e,a.name):await u(e,a.to),c=await u(e,`HEAD`);if(await i(e)==null)throw new n(`Filtering and partial apply require HEAD to be on a branch (non-detached) when rewriting existing commits. Check out the source branch before composing.`);if(c!==s)throw new n(`Filtering and partial apply require HEAD to be at the source's tip (${s.slice(0,8)}); HEAD is at ${c.slice(0,8)}.`)}function J(e,t){if(t==null)return e;let r=new Set(t),i=e.filter(e=>r.has(e.id)),a=new Set(i.map(e=>e.id)),o=t.filter(e=>!a.has(e));if(o.length>0)throw new n(`applyCommitIds contains commit ids not found in plan.allOrderedCommits: ${o.join(`, `)}`);return i}function Y(e,t){let n=new Set(e.flatMap(e=>e.hunkIndices));return t.filter(e=>n.has(e.index))}function X(e,t){let n=new Set(e.flatMap(e=>e.hunkIndices)),r=t.filter(e=>!n.has(e.index));if(r.length===0)return{leftoverHunks:[],leftoverPatch:void 0};let i=b(r);return{leftoverHunks:r,leftoverPatch:i.length>0?i:void 0}}async function Z(e,t){if(t.type===`branch`){let r=await u(e,t.name),i=(await s(e,{format:`%H`,range:`${await c(e,await u(e,t.mergeTarget??`main`),r)}..${r}`})).trim().split(`
2
- `).at(-1);if(!i)throw new n(`Cannot recompose branch '${t.name}': no commits between merge-base and branch tip.`);return{type:`rewrite-range`,branch:t.name,rangeFrom:i,rangeTo:r}}return t.type===`commit-range`?{type:`rewrite-range`,branch:t.branch,rangeFrom:t.from,rangeTo:t.to}:{type:`head`}}function Q(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 re(e,t){if(t.type===`rewrite-range`&&e.type===`workdir`)throw new n(`target 'rewrite-range' is not valid with 'workdir' source`);if(t.type===`head`&&e.type!==`workdir`)throw new n(`target 'head' is not valid with '${e.type}' source — use 'branch', 'auto', or 'rewrite-range'`)}function $(e,n){return n.map(n=>{let r=e.commits.find(e=>e.id===n);if(!r)throw new t(`INTERNAL`,`Commit ${n} not found in branch '${e.id}'`);return r})}async function ie(e){let t=(await o(e,{format:`%(refname:short)`,prefix:`refs/heads/`})).trim().split(`
3
- `).filter(Boolean);return new Set(t)}async function ae(e,t,n,r,i){let a=P(),o=n.composedBranchCount||1,s=`compose: ${o} branch${o===1?``:`es`} from ${i.type}`,c=[...n.movedRefs];if(r.type===`head`&&t.headBranch){let n=await u(e,`HEAD`);n!==t.headSha&&c.push({kind:`branch`,name:t.headBranch,previousSha:t.headSha,newSha:n})}let l={version:1,id:a,createdAt:new Date().toISOString(),description:s,previousHead:{sha:t.headSha,branch:t.headBranch},workdirClean:t.workdirClean,workdirBackupSha:t.workdirBackupSha,createdRefs:n.createdRefs,deletedRefs:n.deletedRefs,movedRefs:c};return await new M(T(e,`undo manifest persistence`)).save(l),a}export{te as applyComposePlan,I as compose,V as composePlan,F as defaultPhaseMessages};
1
+ import{GitError as e}from"../errors/git.js";import{ComposeWorkflowError as t,ComposeWorkflowInputError as n}from"../errors/workflow.js";import{applyPatchToIndex as r,currentBranch as i,diffTree as a,forEachRef as o,log as s,mergeBase as c,readTree as l,revParse as u,writeTree as d}from"../git/port-dispatch.js";import{validateRewriteRangeSafety as f}from"../git/safety-range.js";import{addUsage as p,emptyUsage as m}from"../ai/types.js";import{memoizeRedactHunkContent as h}from"../tasks/utils/redact.js";import{analyze as g}from"../tasks/analyze/analyze.js";import{composeGroup as _}from"../tasks/compose-group/compose-group.js";import{group as v}from"../tasks/group/group.js";import{order as y}from"../tasks/order/order.js";import{partition as b}from"../tasks/partition/partition.js";import{withDefaultNoiseRedaction as x}from"../tasks/utils/noise.js";import{buildPatchForCommit as S}from"../git/utils/patch-builder.js";import{sha256 as C}from"../git/utils/crypto.js";import{collectFromSource as w}from"../git/diff-collector.js";import{verifySnapshot as T}from"../git/safety.js";import{applyToTarget as E}from"../git/apply-target.js";import{requireExec as D}from"../git/require-exec.js";import{extractConventions as O}from"../git/utils/conventions.js";import{rethrowAsWorkflowError as k}from"../utils/cancellation.js";import{randomUUID as A}from"node:crypto";import{unlinkSync as j}from"node:fs";import{tmpdir as M}from"node:os";import{join as N}from"node:path";import{UndoStore as P,capturePreOperationState as F,generateUndoId as I}from"@gitkraken/shared-tools";const L={collecting:{message:`Collecting changes…`,weight:5},composing:{message:`Composing commits…`,weight:40},"compose-grouping":{message:`Grouping changes into commits…`,weight:40},analyzing:{message:`Analyzing changes…`,weight:20},grouping:{message:`Grouping changes into commits…`,weight:25},ordering:{message:`Ordering commits…`,weight:15},partitioning:{message:`Subdividing branches…`,weight:20},verifying:{message:`Verifying…`,weight:5},applying:{message:`Applying to repository…`,weight:10}};async function R(r){if(!r.applyPlan&&!r.model)throw new n(`Either 'model' or 'applyPlan' must be provided`);if(r.applyPlan)return ne(r);let{git:i,model:a,source:o={type:`workdir`},onProgress:s,onBeforeApply:c,cancellation:l}=r,u=a,d=r.depth??`balanced`,p=r.target??await ae(i,o);oe(o,p),await Y(i,r.applyCommitIds,r.hunkFilter,o,p),await f({git:i,source:o,target:p,overrides:r.overrideSafetyFailures});let m=[];s?.({phase:`collecting`,...L.collecting});let g=Date.now(),_=await w(i,o);if(m.push({name:`collecting`,durationMs:Date.now()-g,attempts:1}),_.hunks.length===0)throw new e(`NO_CHANGES`,Z(o));let{hunks:v,filteredCount:y}=await K(_.hunks,r.hunkFilter);if(v.length===0)throw new e(`NO_CHANGES`,Z(o));let b=W(v);s?.({phase:`collecting`,...L.collecting,stats:b});let S=y!==void 0&&y>0?await q(i,_.snapshot,v):_.snapshot,C=r.conventions??await O({git:i,author:r.conventionAuthor}),A=await se(i),j=p.type===`auto`?`branches`:`commits`,M=r.disableDefaultNoiseReduction?r.redactHunkContent:x(r.redactHunkContent),N=M?h(M):void 0,P=N?v.filter(e=>N(e)).length:void 0,I={hunks:v,collection:_,conventions:C,existingBranchNames:A,groupMode:j,target:p,model:u,instructions:r.instructions,context:r.context,strategy:r.strategy,maxRetries:r.maxRetries,maxTokens:r.maxTokens,onProgress:s,cancellation:l,onBeforeModelCall:r.onBeforeModelCall,onAfterModelCall:r.onAfterModelCall,onBeforePrompt:r.onBeforePrompt,session:r.session,promptOverrides:r.promptOverrides,redactHunkContent:N,inputTokenBudget:r.inputTokenBudget},R;try{R=await ee(d,I)}catch(e){k(e,l)}let z={analysis:R.analysis,grouping:R.grouping,ordering:R.ordering,branches:R.branchPlans,allOrderedCommits:R.allOrderedCommits};if(m.push(...R.phases),c&&!await c(z))return{plan:z,snapshot:S,session:R.session,usage:R.usage,phases:m,source:{type:o.type===`branch`?`branch`:`workdir`,hunks:v,commitMessages:_.commitMessages,branchName:_.branchName,baseBranch:_.baseBranch},diffStats:b,feedbackTargetCallId:G(m),hunksFilteredCount:y,hunksRedactedCount:P,hunksCondensedCount:R.hunksCondensedCount};let te=await F(D(i,`capturing pre-operation state`));s?.({phase:`verifying`,...L.verifying});let B=Date.now(),V=await T(i,_.snapshot,J(o,_));if(!V.valid)throw new t(`SAFETY_CHECK_FAILED`,`Safety check failed: ${V.error}`);m.push({name:`verifying`,durationMs:Date.now()-B,attempts:1});let H=X(R.allOrderedCommits,r.applyCommitIds),{leftoverHunks:U,leftoverPatch:Q}=ie(H,_.hunks),le=U.length>0?await q(i,S,re(H,_.hunks)):S;s?.({phase:`applying`,...L.applying});let ue=Date.now(),$=await E({git:i,source:o,target:p,branchPlans:R.branchPlans,allOrderedCommits:H,hunks:_.hunks,snapshot:le,collection:_,signing:r.signing,authorAttribution:r.authorAttribution,stashLabel:r.stashLabel,leftoverPatch:Q});m.push({name:`applying`,durationMs:Date.now()-ue,attempts:1});let de=await ce(i,te,$.mutations,p,o);return{plan:z,snapshot:le,partitions:$.partitions,partitionGroups:$.partitionGroups,session:R.session,usage:R.usage,phases:m,source:{type:o.type===`branch`?`branch`:`workdir`,hunks:v,commitMessages:_.commitMessages,branchName:_.branchName,baseBranch:_.baseBranch},diffStats:b,commitShas:$.commitShas,undoId:de,feedbackTargetCallId:G(m),hunksFilteredCount:y,hunksRedactedCount:P,hunksCondensedCount:R.hunksCondensedCount,appliedCommitIds:H.map(e=>e.id),unappliedHunkIndices:U.length>0?U.map(e=>e.index):void 0,stashConflict:$.stashConflict}}function ee(e,t){switch(e){case`quick`:return z(t);case`balanced`:return te(t);case`deep`:return V(t)}}async function z(e){return B(e,{lightweight:!0,skipOrder:!0})}async function te(e){return B(e,{includeAnalysis:!0})}async function B(e,t){let{hunks:n,collection:r,conventions:i,existingBranchNames:a,groupMode:o,target:s,model:c,instructions:l,context:u,strategy:d,maxRetries:f,maxTokens:h,onProgress:g,cancellation:v,onBeforeModelCall:x,onAfterModelCall:S,onBeforePrompt:C}=e,w=m(),T=[],E=await _({hunks:n,commitMessages:r.commitMessages,commitOrder:r.commitOrder,branchName:r.branchName,baseBranch:r.baseBranch,issueUrl:u?.issueUrl,pullRequestUrl:u?.pullRequestUrl,instructions:l,conventions:i,mode:o,strategy:d?.grouping,existingBranchNames:o===`branches`?a:void 0,includeAnalysis:t.includeAnalysis,lightweight:t.lightweight,condensed:t.condensed,redactHunkContent:e.redactHunkContent,inputTokenBudget:e.inputTokenBudget,model:c,session:e.session,maxRetries:f,maxTokens:h,onProgress:e=>g?.({phase:`compose-grouping`,...L[`compose-grouping`],event:e}),cancellation:v,onBeforeModelCall:x,onAfterModelCall:S,onBeforePrompt:C,promptOverride:e.promptOverrides?.[`compose-group`]});E.usage&&p(w,E.usage),T.push({name:`compose-grouping`,durationMs:E.durationMs,attempts:E.attempts,usage:E.usage,callIds:E.callIds});let D=E.session,O,k;if(t.skipOrder)O=E.result.branches.map(e=>({branchGroup:e,orderedCommitIds:e.commits.map(e=>e.id)})),k={branches:O.map(e=>({branchId:e.branchGroup.id,orderedCommitIds:e.orderedCommitIds})),rationale:``};else{let t=await y({branches:E.result.branches,hunks:n,instructions:l,model:c,session:E.session,maxRetries:f,maxTokens:h,onProgress:e=>g?.({phase:`ordering`,...L.ordering,event:e}),cancellation:v,onBeforeModelCall:x,onAfterModelCall:S,onBeforePrompt:C,promptOverride:e.promptOverrides?.order});t.usage&&p(w,t.usage),T.push({name:`ordering`,durationMs:t.durationMs,attempts:t.attempts,usage:t.usage,callIds:t.callIds}),D=t.session,k=t.result,O=E.result.branches.map(e=>({branchGroup:e,orderedCommitIds:k.branches.find(t=>t.branchId===e.id)?.orderedCommitIds??e.commits.map(e=>e.id)}))}if(s.type===`auto`){let t=new Set(a);for(let e of E.result.branches)e.name&&t.add(e.name);for(let n of O){let r=await b({commits:Q(n.branchGroup,n.orderedCommitIds),orderedCommitIds:n.orderedCommitIds,sourceBranchName:n.branchGroup.name,instructions:l,conventions:i,strategy:d?.partition,existingBranchNames:t,model:c,maxRetries:f,maxTokens:h,onProgress:e=>g?.({phase:`partitioning`,...L.partitioning,branch:n.branchGroup.name,event:e}),cancellation:v,onBeforeModelCall:x,onAfterModelCall:S,onBeforePrompt:C,promptOverride:e.promptOverrides?.partition});r.usage&&p(w,r.usage),T.push({name:`partitioning:${n.branchGroup.name}`,durationMs:r.durationMs,attempts:r.attempts,usage:r.usage,callIds:r.callIds}),n.partitioning=r.result,D=r.session;for(let e of r.result.partitions)t.add(e.branchName)}}let A=O.flatMap(e=>Q(e.branchGroup,e.orderedCommitIds));return{analysis:void 0,grouping:E.result,ordering:k,branchPlans:O,allOrderedCommits:A,session:D,usage:w,phases:T,hunksCondensedCount:E.hunksCondensedCount}}async function V(e){let{hunks:t,collection:n,conventions:r,existingBranchNames:i,groupMode:a,target:o,model:s,instructions:c,context:l,strategy:u,maxRetries:d,maxTokens:f,onProgress:h,cancellation:_,onBeforeModelCall:x,onAfterModelCall:S,onBeforePrompt:C}=e,w=m(),T=[],E=await g({hunks:t,commitMessages:n.commitMessages,commitOrder:n.commitOrder,branchName:n.branchName,baseBranch:n.baseBranch,issueUrl:l?.issueUrl,pullRequestUrl:l?.pullRequestUrl,instructions:c,redactHunkContent:e.redactHunkContent,inputTokenBudget:e.inputTokenBudget,model:s,session:e.session,maxRetries:d,maxTokens:f,onProgress:e=>h?.({phase:`analyzing`,...L.analyzing,event:e}),cancellation:_,onBeforeModelCall:x,onAfterModelCall:S,onBeforePrompt:C,promptOverride:e.promptOverrides?.analyze});E.usage&&p(w,E.usage),T.push({name:`analyzing`,durationMs:E.durationMs,attempts:E.attempts,usage:E.usage,callIds:E.callIds});let D=await v({hunks:t,commitMessages:n.commitMessages,instructions:c,conventions:r,mode:a,strategy:u?.grouping,existingBranchNames:a===`branches`?i:void 0,redactHunkContent:e.redactHunkContent,inputTokenBudget:e.inputTokenBudget,model:s,session:E.session,maxRetries:d,maxTokens:f,onProgress:e=>h?.({phase:`grouping`,...L.grouping,event:e}),cancellation:_,onBeforeModelCall:x,onAfterModelCall:S,onBeforePrompt:C,promptOverride:e.promptOverrides?.group});D.usage&&p(w,D.usage),T.push({name:`grouping`,durationMs:D.durationMs,attempts:D.attempts,usage:D.usage,callIds:D.callIds});let O=await y({branches:D.result.branches,hunks:t,instructions:c,model:s,session:D.session,maxRetries:d,maxTokens:f,onProgress:e=>h?.({phase:`ordering`,...L.ordering,event:e}),cancellation:_,onBeforeModelCall:x,onAfterModelCall:S,onBeforePrompt:C,promptOverride:e.promptOverrides?.order});O.usage&&p(w,O.usage),T.push({name:`ordering`,durationMs:O.durationMs,attempts:O.attempts,usage:O.usage,callIds:O.callIds});let k=D.result.branches.map(e=>({branchGroup:e,orderedCommitIds:O.result.branches.find(t=>t.branchId===e.id)?.orderedCommitIds??e.commits.map(e=>e.id)})),A=O.session;if(o.type===`auto`){let t=new Set(i);for(let e of D.result.branches)e.name&&t.add(e.name);for(let n of k){let i=await b({commits:Q(n.branchGroup,n.orderedCommitIds),orderedCommitIds:n.orderedCommitIds,sourceBranchName:n.branchGroup.name,instructions:c,conventions:r,strategy:u?.partition,existingBranchNames:t,model:s,maxRetries:d,maxTokens:f,onProgress:e=>h?.({phase:`partitioning`,...L.partitioning,branch:n.branchGroup.name,event:e}),cancellation:_,onBeforeModelCall:x,onAfterModelCall:S,onBeforePrompt:C,promptOverride:e.promptOverrides?.partition});i.usage&&p(w,i.usage),T.push({name:`partitioning:${n.branchGroup.name}`,durationMs:i.durationMs,attempts:i.attempts,usage:i.usage,callIds:i.callIds}),n.partitioning=i.result,A=i.session;for(let e of i.result.partitions)t.add(e.branchName)}}let j=k.flatMap(e=>Q(e.branchGroup,e.orderedCommitIds));return{analysis:E.result,grouping:D.result,ordering:O.result,branchPlans:k,allOrderedCommits:j,session:A,usage:w,phases:T,hunksCondensedCount:E.hunksCondensedCount}}async function H(e){let t=await R({...e,onBeforeApply:async()=>!1});return{plan:t.plan,snapshot:t.snapshot,source:t.source,session:t.session,usage:t.usage,phases:t.phases,feedbackTargetCallId:t.feedbackTargetCallId,hunksFilteredCount:t.hunksFilteredCount,hunksRedactedCount:t.hunksRedactedCount,hunksCondensedCount:t.hunksCondensedCount,diffStats:t.diffStats}}async function U(e){let t=await R({git:e.git,applyPlan:e.applyPlan,source:e.source,target:e.target,onProgress:e.onProgress,signing:e.signing,authorAttribution:e.authorAttribution,applyCommitIds:e.applyCommitIds,stashLabel:e.stashLabel,hunkFilter:e.hunkFilter,overrideSafetyFailures:e.overrideSafetyFailures});return{commitShas:t.commitShas,partitions:t.partitions,partitionGroups:t.partitionGroups,phases:t.phases,undoId:t.undoId,appliedCommitIds:t.appliedCommitIds,unappliedHunkIndices:t.unappliedHunkIndices,stashConflict:t.stashConflict}}async function ne(n){let{git:r,applyPlan:i,onProgress:a}=n,{plan:o,snapshot:s,source:c}=i,l=n.source??c,u=n.target??i?.target??await ae(r,l);oe(l,u),await Y(r,n.applyCommitIds,n.hunkFilter,l,u),await f({git:r,source:l,target:u,overrides:n.overrideSafetyFailures});let d=await F(D(r,`capturing pre-operation state`)),p=[];a?.({phase:`collecting`,...L.collecting});let h=Date.now(),g=await w(r,l);if(p.push({name:`collecting`,durationMs:Date.now()-h,attempts:1}),g.hunks.length===0)throw new e(`NO_CHANGES`,Z(l));let{hunks:_}=await K(g.hunks,n.hunkFilter);if(_.length===0)throw new e(`NO_CHANGES`,Z(l));let v=W(_);a?.({phase:`collecting`,...L.collecting,stats:v});let y=_.length<g.hunks.length?await q(r,g.snapshot,_):g.snapshot,b={...g,hunks:_,snapshot:y};if(b.snapshot.diffHash!==s.diffHash)throw new t(`SAFETY_CHECK_FAILED`,`Working directory has changed since the plan was created. Re-run compose to generate a new plan.`);a?.({phase:`verifying`,...L.verifying});let x=Date.now(),S=await T(r,b.snapshot,J(l,b));if(!S.valid)throw new t(`SAFETY_CHECK_FAILED`,`Safety check failed: ${S.error}`);p.push({name:`verifying`,durationMs:Date.now()-x,attempts:1});let C=X(o.allOrderedCommits,n.applyCommitIds),{leftoverHunks:O,leftoverPatch:k}=ie(C,b.hunks),A=O.length>0?await q(r,b.snapshot,re(C,b.hunks)):b.snapshot;a?.({phase:`applying`,...L.applying});let j=Date.now(),M=await E({git:r,source:l,target:u,branchPlans:o.branches,allOrderedCommits:C,hunks:b.hunks,snapshot:A,collection:b,signing:n.signing,authorAttribution:n.authorAttribution,stashLabel:n.stashLabel,leftoverPatch:k});p.push({name:`applying`,durationMs:Date.now()-j,attempts:1});let N=await ce(r,d,M.mutations,u,l);return{plan:o,snapshot:A,partitions:M.partitions,partitionGroups:M.partitionGroups,session:{messages:[],systemPrompt:``,completedSteps:{}},usage:m(),phases:p,source:{type:l.type===`branch`?`branch`:`workdir`,hunks:b.hunks,commitMessages:b.commitMessages,branchName:b.branchName,baseBranch:b.baseBranch},diffStats:v,commitShas:M.commitShas,undoId:N,appliedCommitIds:C.map(e=>e.id),unappliedHunkIndices:O.length>0?O.map(e=>e.index):void 0,stashConflict:M.stashConflict}}function W(e){let t=new Set,n=0,r=0;for(let i of e)t.add(i.fileName),n+=i.additions,r+=i.deletions;return{fileCount:t.size,hunkCount:e.length,addedLines:n,removedLines:r}}function G(e){for(let t=e.length-1;t>=0;t--){let n=e[t].callIds;if(n&&n.length>0)return n[n.length-1]}}async function K(e,t){if(!t)return{hunks:e,filteredCount:void 0};let n=await t(e);return{hunks:n,filteredCount:e.length-n.length}}async function q(e,t,n){let i=t.baseSha??t.headSha,o=N(M(),`compose-filtered-idx-${A()}`),s={GIT_INDEX_FILE:o};try{await l(e,i,{indexEnv:s});let o=S(n);o.length>0&&await r(e,o,{indexEnv:s});let c=await a(e,i,await d(e,{indexEnv:s}));return{...t,diffHash:C(c)}}finally{try{j(o)}catch{}}}function J(e,t){return e.type===`workdir`?{includeUnstaged:!e.stagedOnly,includeUntracked:t.untrackedIncluded}:e.type===`commit-range`&&e.includeWorkdir?{includeStaged:e.includeWorkdir.includeStaged??!0,includeUnstaged:e.includeWorkdir.includeUnstaged??!0,includeUntracked:e.includeWorkdir.includeUntracked??!0}:{includeUnstaged:!1,includeUntracked:!1}}async function Y(e,t,r,a,o){if(r==null&&t==null||a.type===`workdir`||!(o.type===`rewrite-range`||(o.type===`branch`||o.type===`auto`)&&o.preserveOriginal!==!0))return;let s=a.type===`branch`?await u(e,a.name):await u(e,a.to),c=await u(e,`HEAD`);if(await i(e)==null)throw new n(`Filtering and partial apply require HEAD to be on a branch (non-detached) when rewriting existing commits. Check out the source branch before composing.`);if(c!==s)throw new n(`Filtering and partial apply require HEAD to be at the source's tip (${s.slice(0,8)}); HEAD is at ${c.slice(0,8)}.`)}function X(e,t){if(t==null)return e;let r=new Set(t),i=e.filter(e=>r.has(e.id)),a=new Set(i.map(e=>e.id)),o=t.filter(e=>!a.has(e));if(o.length>0)throw new n(`applyCommitIds contains commit ids not found in plan.allOrderedCommits: ${o.join(`, `)}`);return i}function re(e,t){let n=new Set(e.flatMap(e=>e.hunkIndices));return t.filter(e=>n.has(e.index))}function ie(e,t){let n=new Set(e.flatMap(e=>e.hunkIndices)),r=t.filter(e=>!n.has(e.index));if(r.length===0)return{leftoverHunks:[],leftoverPatch:void 0};let i=S(r);return{leftoverHunks:r,leftoverPatch:i.length>0?i:void 0}}async function ae(e,t){if(t.type===`branch`){let r=await u(e,t.name),i=(await s(e,{format:`%H`,range:`${await c(e,await u(e,t.mergeTarget??`main`),r)}..${r}`})).trim().split(`
2
+ `).at(-1);if(!i)throw new n(`Cannot recompose branch '${t.name}': no commits between merge-base and branch tip.`);return{type:`rewrite-range`,branch:t.name,rangeFrom:i,rangeTo:r}}return t.type===`commit-range`?{type:`rewrite-range`,branch:t.branch,rangeFrom:t.from,rangeTo:t.to}:{type:`head`}}function Z(e){switch(e.type){case`workdir`:return`No changes found in working directory`;case`branch`:return`No changes found on branch '${e.name}'`;case`commit-range`:return`No changes found in range '${e.from}..${e.to}' on branch '${e.branch}'`}}function oe(e,t){if(t.type===`rewrite-range`&&e.type===`workdir`)throw new n(`target 'rewrite-range' is not valid with 'workdir' source`);if(t.type===`head`&&e.type!==`workdir`)throw new n(`target 'head' is not valid with '${e.type}' source — use 'branch', 'auto', or 'rewrite-range'`)}function Q(e,n){return n.map(n=>{let r=e.commits.find(e=>e.id===n);if(!r)throw new t(`INTERNAL`,`Commit ${n} not found in branch '${e.id}'`);return r})}async function se(e){let t=(await o(e,{format:`%(refname:short)`,prefix:`refs/heads/`})).trim().split(`
3
+ `).filter(Boolean);return new Set(t)}async function ce(e,t,n,r,i){let a=I(),o=n.composedBranchCount||1,s=`compose: ${o} branch${o===1?``:`es`} from ${i.type}`,c=[...n.movedRefs];if(r.type===`head`&&t.headBranch){let n=await u(e,`HEAD`);n!==t.headSha&&c.push({kind:`branch`,name:t.headBranch,previousSha:t.headSha,newSha:n})}let l={version:1,id:a,createdAt:new Date().toISOString(),description:s,previousHead:{sha:t.headSha,branch:t.headBranch},workdirClean:t.workdirClean,workdirBackupSha:t.workdirBackupSha,createdRefs:n.createdRefs,deletedRefs:n.deletedRefs,movedRefs:c};return await new P(D(e,`undo manifest persistence`)).save(l),a}export{U as applyComposePlan,R as compose,H as composePlan,L as defaultPhaseMessages};