@gitkraken/compose-tools 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/README.md +38 -17
  3. package/dist/git/apply-target.d.ts +5 -9
  4. package/dist/git/apply-target.js +1 -1
  5. package/dist/git/commit-chain.d.ts +36 -42
  6. package/dist/git/commit-chain.js +7 -7
  7. package/dist/git/diff-collector.d.ts +40 -17
  8. package/dist/git/diff-collector.js +6 -7
  9. package/dist/git/index.d.ts +2 -2
  10. package/dist/git/index.js +1 -1
  11. package/dist/git/port-dispatch.d.ts +4 -2
  12. package/dist/git/port-dispatch.js +4 -3
  13. package/dist/git/reparent.d.ts +39 -0
  14. package/dist/git/reparent.js +5 -0
  15. package/dist/git/safety-range.d.ts +33 -0
  16. package/dist/git/safety-range.js +5 -0
  17. package/dist/git/types.d.ts +10 -14
  18. package/dist/git/utils/authorship.d.ts +22 -0
  19. package/dist/git/utils/authorship.js +1 -0
  20. package/dist/git/utils/hunk-parser.d.ts +1 -1
  21. package/dist/git/utils/hunk-parser.js +1 -1
  22. package/dist/index.d.ts +3 -2
  23. package/dist/index.js +1 -1
  24. package/dist/ports/git.d.ts +22 -1
  25. package/dist/tasks/analyze/analyze.js +1 -1
  26. package/dist/tasks/analyze/prompt.d.ts +2 -0
  27. package/dist/tasks/analyze/prompt.js +5 -5
  28. package/dist/tasks/analyze/types.d.ts +3 -0
  29. package/dist/tasks/compose-group/compose-group.js +1 -1
  30. package/dist/tasks/compose-group/prompt.d.ts +2 -0
  31. package/dist/tasks/compose-group/prompt.js +29 -29
  32. package/dist/tasks/compose-group/types.d.ts +3 -0
  33. package/dist/tasks/group/group.js +1 -1
  34. package/dist/tasks/group/prompt.d.ts +2 -0
  35. package/dist/tasks/group/prompt.js +22 -22
  36. package/dist/tasks/group/types.d.ts +3 -0
  37. package/dist/tasks/quick-compose/prompt.d.ts +2 -0
  38. package/dist/tasks/quick-compose/prompt.js +11 -11
  39. package/dist/tasks/quick-compose/quick-compose.js +1 -1
  40. package/dist/tasks/quick-compose/types.d.ts +3 -0
  41. package/dist/tasks/types.d.ts +10 -9
  42. package/dist/tasks/utils/redact.d.ts +14 -0
  43. package/dist/tasks/utils/redact.js +1 -0
  44. package/dist/workflows/compose.d.ts +25 -14
  45. package/dist/workflows/compose.js +3 -2
  46. package/dist/workflows/split.d.ts +3 -0
  47. package/dist/workflows/split.js +2 -2
  48. package/dist/workflows/types.d.ts +41 -18
  49. package/package.json +2 -2
@@ -1,4 +1,4 @@
1
- import{groupingResultSchema as e}from"../group/schema.js";import{z as t}from"zod";const n=JSON.stringify(t.toJSONSchema(e),null,2),r=`
1
+ import{hunkContentForPrompt as e}from"../utils/redact.js";import{groupingResultSchema as t}from"../group/schema.js";import{z as n}from"zod";const r=JSON.stringify(n.toJSONSchema(t),null,2),i=`
2
2
  In addition to the \`branches\` field, include an \`analysis\` field at the top level:
3
3
 
4
4
  \`\`\`json
@@ -10,15 +10,15 @@ In addition to the \`branches\` field, include an \`analysis\` field at the top
10
10
  "branches": [ ... ]
11
11
  }
12
12
  \`\`\`
13
- `,i=`5. Write a full commit message for each commit containing both a summary and a description:
13
+ `,a=`5. Write a full commit message for each commit containing both a summary and a description:
14
14
  - **Summary** (first line): concise, 50 chars max, third-person singular present tense ("Fixes", "Adds", "Improves"). This line stands alone as the commit subject.
15
15
  - **Blank line**: mandatory separator between summary and description.
16
16
  - **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.
17
- - Informal yet professional tone.`;function a(e,t=`branches`,n,r,i){return i?t===`commits`?u(e,n):d(e,n):t===`commits`?o(e,n,r):s(e,n,r)}function o(e,t,a){let o=e?.promptFragment?`5. Write a commit message for each commit following the repository's conventions (see below).
17
+ - Informal yet professional tone.`;function o(e,t=`branches`,n,r,i){return i?t===`commits`?d(e,n):f(e,n):t===`commits`?s(e,n,r):c(e,n,r)}function s(e,t,n){let o=e?.promptFragment?`5. Write a commit message for each commit following the repository's conventions (see below).
18
18
  6. Write a detailed explanation for each commit as if explaining to a reviewer.
19
19
 
20
- ${e.promptFragment}`:`${i}
21
- 6. Write a detailed explanation for each commit as if explaining to a reviewer`,s=l(t?.commitGranularity);return`You are an advanced AI programming assistant tasked with analyzing and organizing code changes into commits.
20
+ ${e.promptFragment}`:`${a}
21
+ 6. Write a detailed explanation for each commit as if explaining to a reviewer`,s=u(t?.commitGranularity);return`You are an advanced AI programming assistant tasked with analyzing and organizing code changes into commits.
22
22
 
23
23
  ## Critical constraints (output is rejected if violated)
24
24
 
@@ -29,14 +29,14 @@ ${e.promptFragment}`:`${i}
29
29
 
30
30
  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.
31
31
 
32
- ${a?`## Step 1: Analysis`:`## Step 1: Internal Analysis`}
32
+ ${n?`## Step 1: Analysis`:`## Step 1: Internal Analysis`}
33
33
 
34
34
  Before grouping, analyze the changes to understand:
35
35
  - **Concerns** — what distinct themes, features, or purposes exist across the hunks
36
36
  - **Dependencies** — which hunks provide things (define functions, types, files) that other hunks consume (import, call, reference)
37
37
  - **Ordering constraints** — if hunk A defines something hunk B uses, they should be in the same commit or A's commit should come before B's
38
38
 
39
- ${a?"Output this analysis as a JSON field named `analysis` alongside your `branches` field. The `analysis` field should contain:\n- `concerns`: array of strings, each describing a distinct theme or feature in the changes\n- `key_dependencies`: array of strings, each describing an ordering dependency between concerns":`Do NOT output this analysis. Use it internally to inform your grouping decisions.`}
39
+ ${n?"Output this analysis as a JSON field named `analysis` alongside your `branches` field. The `analysis` field should contain:\n- `concerns`: array of strings, each describing a distinct theme or feature in the changes\n- `key_dependencies`: array of strings, each describing an ordering dependency between concerns":`Do NOT output this analysis. Use it internally to inform your grouping decisions.`}
40
40
 
41
41
  ## Step 2: Group hunks into commits
42
42
 
@@ -50,16 +50,16 @@ ${s}
50
50
  Output your organization as JSON inside a <output> tag matching this schema:
51
51
 
52
52
  \`\`\`json
53
- ${n}
53
+ ${r}
54
54
  \`\`\`
55
- ${a?r:``}
55
+ ${n?i:``}
56
56
  Wrap all commits in a single branch object with id "default", name "", title "", and description "".
57
57
 
58
- Return only the <output> tag and no other text.`}function s(e,t,a){let o=e?.promptFragment?`5. Write a commit message for each commit following the repository's conventions (see below).
58
+ Return only the <output> tag and no other text.`}function c(e,t,n){let o=e?.promptFragment?`5. Write a commit message for each commit following the repository's conventions (see below).
59
59
  6. Write a detailed explanation for each commit as if explaining to a reviewer.
60
60
 
61
- ${e.promptFragment}`:`${i}
62
- 6. Write a detailed explanation for each commit as if explaining to a reviewer`,s=c(t?.branchSplitting,e),u=l(t?.commitGranularity);return`You are an advanced AI programming assistant tasked with analyzing and organizing code changes into commits and branches.
61
+ ${e.promptFragment}`:`${a}
62
+ 6. Write a detailed explanation for each commit as if explaining to a reviewer`,s=l(t?.branchSplitting,e),c=u(t?.commitGranularity);return`You are an advanced AI programming assistant tasked with analyzing and organizing code changes into commits and branches.
63
63
 
64
64
  ## Critical constraints (output is rejected if violated)
65
65
 
@@ -70,7 +70,7 @@ ${e.promptFragment}`:`${i}
70
70
 
71
71
  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.
72
72
 
73
- ${a?`## Step 1: Analysis`:`## Step 1: Internal Analysis`}
73
+ ${n?`## Step 1: Analysis`:`## Step 1: Internal Analysis`}
74
74
 
75
75
  Before grouping, analyze the changes to understand:
76
76
  - **Concerns** — what distinct themes, features, or purposes exist across the hunks
@@ -78,7 +78,7 @@ Before grouping, analyze the changes to understand:
78
78
  - **Ordering constraints** — if hunk A defines something hunk B uses, they should be in the same commit or A's commit should come before B's
79
79
  - **Merge-order preference** — even if two concerns compile independently, would merging one without the other be confusing or incomplete?
80
80
 
81
- ${a?"Output this analysis as a JSON field named `analysis` alongside your `branches` field. The `analysis` field should contain:\n- `concerns`: array of strings, each describing a distinct theme or feature in the changes\n- `key_dependencies`: array of strings, each describing an ordering dependency between concerns":`Do NOT output this analysis. Use it internally to inform your grouping decisions.`}
81
+ ${n?"Output this analysis as a JSON field named `analysis` alongside your `branches` field. The `analysis` field should contain:\n- `concerns`: array of strings, each describing a distinct theme or feature in the changes\n- `key_dependencies`: array of strings, each describing an ordering dependency between concerns":`Do NOT output this analysis. Use it internally to inform your grouping decisions.`}
82
82
 
83
83
  ## Step 2: Organize hunks into commits and branches
84
84
 
@@ -92,7 +92,7 @@ Your job has two levels:
92
92
  1. Group hunks into logical units that make sense together and can be applied atomically
93
93
  2. Ensure each commit is self-contained and atomic
94
94
  ${o}
95
- ${u}
95
+ ${c}
96
96
 
97
97
  ### Branch guidelines
98
98
 
@@ -103,14 +103,14 @@ ${s}
103
103
  Output your organization as JSON inside a <output> tag matching this schema:
104
104
 
105
105
  \`\`\`json
106
- ${n}
106
+ ${r}
107
107
  \`\`\`
108
- ${a?r:``}
109
- Return only the <output> tag and no other text.`}function c(e,t){let n=`Each branch must have a descriptive name, a short PR-style title (under 72 characters, imperative mood), and at least one commit.`,r=`- Each commit belongs to exactly one branch.
108
+ ${n?i:``}
109
+ Return only the <output> tag and no other text.`}function l(e,t){let n=`Each branch must have a descriptive name, a short PR-style title (under 72 characters, imperative mood), and at least one commit.`,r=`- Each commit belongs to exactly one branch.
110
110
  ${t?.promptFragment?`- ${n} Follow the repository's branch naming conventions (see above).`:`- ${n}`}
111
- - Changes addressing the same pattern across different files belong in ONE branch.`,i=`- Consider merge-order preference, not just code dependencies. Even if two concerns compile independently, they may need to be in the same branch if merging one without the other would be confusing or incomplete.`;return e===`conservative`?`${r}\n- Default to a single branch. Only create multiple branches when concerns are completely unrelated.\n${i}\n- When in doubt, use fewer branches.`:e===`aggressive`?`${r}\n- Create separate branches for each distinct concern.\n${i}\n- Prefer more branches over fewer.`:`${r}\n- Create separate branches when concerns are genuinely independent — meaning a reviewer would gain nothing from seeing them together.\n- Keep changes in the same branch when they form a coherent story.\n${i}\n- When in doubt: "Would a reviewer want to see these together?"`}function l(e){return e===`consolidated`?`
111
+ - Changes addressing the same pattern across different files belong in ONE branch.`,i=`- Consider merge-order preference, not just code dependencies. Even if two concerns compile independently, they may need to be in the same branch if merging one without the other would be confusing or incomplete.`;return e===`conservative`?`${r}\n- Default to a single branch. Only create multiple branches when concerns are completely unrelated.\n${i}\n- When in doubt, use fewer branches.`:e===`aggressive`?`${r}\n- Create separate branches for each distinct concern.\n${i}\n- Prefer more branches over fewer.`:`${r}\n- Create separate branches when concerns are genuinely independent — meaning a reviewer would gain nothing from seeing them together.\n- Keep changes in the same branch when they form a coherent story.\n${i}\n- When in doubt: "Would a reviewer want to see these together?"`}function u(e){return e===`consolidated`?`
112
112
  Prefer fewer, larger commits. Combine related changes into a single commit when they serve the same purpose.`:e===`granular`?`
113
- Prefer more, smaller commits. Each distinct change should be its own commit.`:``}function u(e,t){return`Organize code hunks into clean, atomic commits.
113
+ Prefer more, smaller commits. Each distinct change should be its own commit.`:``}function d(e,t){return`Organize code hunks into clean, atomic commits.
114
114
 
115
115
  ## Rules
116
116
 
@@ -121,7 +121,7 @@ Prefer more, smaller commits. Each distinct change should be its own commit.`:``
121
121
  5. ${e?.promptFragment?`Write commit messages following the repository's conventions (see below). Write a one-sentence explanation per commit.
122
122
 
123
123
  ${e.promptFragment}`:`Write commit messages: first line under 50 chars, imperative mood ("Add", "Fix", "Update"). Write a one-sentence explanation per commit.`}
124
- ${l(t?.commitGranularity)}
124
+ ${u(t?.commitGranularity)}
125
125
 
126
126
  ## Output
127
127
 
@@ -131,9 +131,9 @@ JSON inside <output> tags. Use one branch with id "default", name "", title "",
131
131
  {"branches":[{"id":"default","name":"","title":"","description":"","commits":[{"id":"commit-1","message":"Add feature X","explanation":"Brief reason.","hunks":[0,1,2]}]}]}
132
132
  \`\`\`
133
133
 
134
- CRITICAL: Every hunk index must appear exactly once across all commits. Every commit must contain at least one hunk. Missing hunks, duplicate hunks, or empty commits will fail validation. Return only the <output> tag.`}function d(e,t){let n=e?.promptFragment?`Write commit messages following the repository's conventions (see below). Write a one-sentence explanation per commit.
134
+ CRITICAL: Every hunk index must appear exactly once across all commits. Every commit must contain at least one hunk. Missing hunks, duplicate hunks, or empty commits will fail validation. Return only the <output> tag.`}function f(e,t){let n=e?.promptFragment?`Write commit messages following the repository's conventions (see below). Write a one-sentence explanation per commit.
135
135
 
136
- ${e.promptFragment}`:`Write commit messages: first line under 50 chars, third-person singular present tense ("Fixes", "Adds", "Improves"). Write a one-sentence explanation per commit.`,r=c(t?.branchSplitting,e);return`Organize code hunks into clean, atomic commits and branches.
136
+ ${e.promptFragment}`:`Write commit messages: first line under 50 chars, third-person singular present tense ("Fixes", "Adds", "Improves"). Write a one-sentence explanation per commit.`,r=l(t?.branchSplitting,e);return`Organize code hunks into clean, atomic commits and branches.
137
137
 
138
138
  ## Commit rules
139
139
 
@@ -142,7 +142,7 @@ ${e.promptFragment}`:`Write commit messages: first line under 50 chars, third-pe
142
142
  3. Each commit needs a unique ID (e.g., "commit-1", "commit-2").
143
143
  4. Every commit must have at least one hunk. No empty commits.
144
144
  5. ${n}
145
- ${l(t?.commitGranularity)}
145
+ ${u(t?.commitGranularity)}
146
146
 
147
147
  ## Branch guidelines
148
148
 
@@ -158,11 +158,11 @@ JSON inside <output> tags. Each branch needs a unique ID and non-empty name.
158
158
 
159
159
  CRITICAL: Every hunk index must appear exactly once across all commits in all branches. Every commit must contain at least one hunk. Missing hunks, duplicate hunks, or empty commits will fail validation. Each branch must have a unique ID and non-empty name.
160
160
 
161
- Return only the <output> tag and no other text.`}function f(e){let{hunks:t,commitMessages:n,commitOrder:r,branchName:i,baseBranch:a,issueUrl:o,pullRequestUrl:s,instructions:c,condensed:l}=e,u=JSON.stringify(l?t.map(e=>({index:e.index,fileName:e.fileName,hunkHeader:e.hunkHeader,additions:e.additions,deletions:e.deletions,...e.isBinary?{isBinary:!0}:{},...e.isRename?{isRename:!0}:{},...e.originalFileName?{originalFileName:e.originalFileName}:{}})):t.map(e=>({index:e.index,fileName:e.fileName,hunkHeader:e.hunkHeader,content:e.content,...e.isBinary?{isBinary:!0}:{},...e.isRename?{isRename:!0}:{},...e.originalFileName?{originalFileName:e.originalFileName}:{}}))),d=`Organize these ${t.length} code hunks (indices 0 to ${t.length-1}) into commits. Every index must appear exactly once.
161
+ Return only the <output> tag and no other text.`}function p(t){let{hunks:n,commitMessages:r,commitOrder:i,branchName:a,baseBranch:o,issueUrl:s,pullRequestUrl:c,instructions:l,condensed:u,redactHunkContent:d}=t,f=JSON.stringify(u?n.map(e=>({index:e.index,fileName:e.fileName,hunkHeader:e.hunkHeader,additions:e.additions,deletions:e.deletions,...e.isBinary?{isBinary:!0}:{},...e.isRename?{isRename:!0}:{},...e.originalFileName?{originalFileName:e.originalFileName}:{}})):n.map(t=>({index:t.index,fileName:t.fileName,hunkHeader:t.hunkHeader,content:e(t,d),...t.isBinary?{isBinary:!0}:{},...t.isRename?{isRename:!0}:{},...t.originalFileName?{originalFileName:t.originalFileName}:{}}))),p=`Organize these ${n.length} code hunks (indices 0 to ${n.length-1}) into commits. Every index must appear exactly once.
162
162
 
163
163
  <hunks>
164
- ${u}
165
- </hunks>`,f=[];return i&&f.push(`Branch: ${i}`),a&&f.push(`Base: ${a}`),o&&f.push(`Issue: ${o}`),s&&f.push(`PR: ${s}`),f.length>0&&(d+=`\n\nContext:\n${f.join(`
166
- `)}`),n?.length&&(d+=`\n\nOriginal commit messages (match their style):\n<commit_messages>\n${JSON.stringify(n)}\n</commit_messages>`),r?.length&&(d+=`\n\nOriginal commit order:\n${JSON.stringify(r)}`),c&&(d+=`\n\nAdditional instructions:\n${c}`),d+=`
164
+ ${f}
165
+ </hunks>`,m=[];return a&&m.push(`Branch: ${a}`),o&&m.push(`Base: ${o}`),s&&m.push(`Issue: ${s}`),c&&m.push(`PR: ${c}`),m.length>0&&(p+=`\n\nContext:\n${m.join(`
166
+ `)}`),r?.length&&(p+=`\n\nOriginal commit messages (match their style):\n<commit_messages>\n${JSON.stringify(r)}\n</commit_messages>`),i?.length&&(p+=`\n\nOriginal commit order:\n${JSON.stringify(i)}`),l&&(p+=`\n\nAdditional instructions:\n${l}`),p+=`
167
167
 
168
- Return only the <output> tag and no other text.`,d}export{a as buildComposeGroupSystemPrompt,f as buildComposeGroupUserPrompt};
168
+ Return only the <output> tag and no other text.`,p}export{o as buildComposeGroupSystemPrompt,p as buildComposeGroupUserPrompt};
@@ -2,6 +2,7 @@ import { ProgressEvent, TokenUsage } from "../../ai/types.js";
2
2
  import { ComposeHunk, PromptOverride } from "../types.js";
3
3
  import { ConventionDescriptor } from "../../git/utils/conventions.js";
4
4
  import { OnAfterModelCall, OnBeforeModelCall, OnBeforePrompt } from "../ai-loop.js";
5
+ import { RedactHunkContent } from "../utils/redact.js";
5
6
  import { GroupingResult, GroupingStrategy } from "../group/types.js";
6
7
  import { ComposeSession } from "../session.js";
7
8
  import { AiModelPort } from "@gitkraken/shared-tools";
@@ -37,6 +38,8 @@ interface ComposeGroupInput {
37
38
  lightweight?: boolean;
38
39
  /** When true, sends only hunk metadata (fileName, hunkHeader, counts) without diff content. */
39
40
  condensed?: boolean;
41
+ /** No-op when `condensed: true`. See {@link RedactHunkContent}. */
42
+ redactHunkContent?: RedactHunkContent;
40
43
  model: AiModelPort;
41
44
  /** Prior session to resume from. When `completedSteps.composeGroup` is present, skip the AI call. */
42
45
  session?: ComposeSession;
@@ -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,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
+ 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,redactHunkContent:s.redactHunkContent}),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,5 +1,6 @@
1
1
  import { ComposeHunk } from "../types.js";
2
2
  import { ConventionDescriptor } from "../../git/utils/conventions.js";
3
+ import { RedactHunkContent } from "../utils/redact.js";
3
4
  import { GroupInput, GroupingStrategy } from "./types.js";
4
5
 
5
6
  //#region src/tasks/group/prompt.d.ts
@@ -9,6 +10,7 @@ declare function buildGroupUserPrompt(params: {
9
10
  commitMessages?: string[];
10
11
  instructions?: string;
11
12
  mode?: 'commits' | 'branches';
13
+ redactHunkContent?: RedactHunkContent;
12
14
  }): string;
13
15
  declare function buildGroupContinuationPrompt(input: GroupInput, mode: 'commits' | 'branches'): string;
14
16
  //#endregion
@@ -1,8 +1,8 @@
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:
1
+ import{hunkContentForPrompt as e}from"../utils/redact.js";import{groupingResultSchema as t}from"./schema.js";import{z as n}from"zod";const r=JSON.stringify(n.toJSONSchema(t),null,2),i=`5. Write a full commit message for each commit containing both a summary and a description:
2
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
3
  - **Blank line**: mandatory separator between summary and description.
4
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.
5
+ - Informal yet professional tone.`,a=`Each branch must have a descriptive name, a short PR-style title (under 72 characters, imperative mood), and at least one commit.`;function o(e,t=`branches`,n){return t===`commits`?s(e,n):c(e,n)}function s(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
6
 
7
7
  ## Critical constraints (output is rejected if violated)
8
8
 
@@ -20,25 +20,25 @@ Before producing the \`<output>\` tag, do a self-check: count the total number o
20
20
  ${e?.promptFragment?`5. Write a commit message for each commit following the repository's conventions (see below).
21
21
  6. Write a detailed explanation for each commit as if explaining to a reviewer.
22
22
 
23
- ${e.promptFragment}`:`${r}
23
+ ${e.promptFragment}`:`${i}
24
24
  6. Write a detailed explanation for each commit as if explaining to a reviewer`}
25
- ${l(t?.commitGranularity)}
25
+ ${u(t?.commitGranularity)}
26
26
 
27
27
  ## Required Output Format
28
28
 
29
29
  Output your organization as JSON inside a <output> tag matching this schema:
30
30
 
31
31
  \`\`\`json
32
- ${n}
32
+ ${r}
33
33
  \`\`\`
34
34
 
35
35
  Wrap all commits in a single branch object with id "default", name "", title "", and description "".
36
36
 
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).
37
+ Return only the <output> tag and no other text.`}function c(e,t){let n=e?.promptFragment?`5. Write a commit message for each commit following the repository's conventions (see below).
38
38
  6. Write a detailed explanation for each commit as if explaining to a reviewer.
39
39
 
40
- ${e.promptFragment}`:`${r}
41
- 6. Write a detailed explanation for each commit as if explaining to a reviewer`,a=c(t?.branchSplitting,e);return`You are an advanced AI programming assistant tasked with organizing code changes into commits and branches. You will be working with individual code hunks.
40
+ ${e.promptFragment}`:`${i}
41
+ 6. Write a detailed explanation for each commit as if explaining to a reviewer`,a=l(t?.branchSplitting,e);return`You are an advanced AI programming assistant tasked with organizing code changes into commits and branches. You will be working with individual code hunks.
42
42
 
43
43
  Your job has two levels:
44
44
 
@@ -58,8 +58,8 @@ Before producing the \`<output>\` tag, do a self-check: count the total number o
58
58
 
59
59
  1. Group hunks into logical units that make sense together and can be applied atomically
60
60
  2. Ensure each commit is self-contained and atomic
61
- ${i}
62
- ${l(t?.commitGranularity)}
61
+ ${n}
62
+ ${u(t?.commitGranularity)}
63
63
 
64
64
  ## Branch guidelines
65
65
 
@@ -70,11 +70,11 @@ ${a}
70
70
  Output your organization as JSON inside a <output> tag matching this schema:
71
71
 
72
72
  \`\`\`json
73
- ${n}
73
+ ${r}
74
74
  \`\`\`
75
75
 
76
- Return only the <output> tag and no other text.`}function c(e,t){let n=`- Each commit belongs to exactly one branch.
77
- ${t?.promptFragment?`- ${i} Follow the repository's branch naming conventions (see above).`:`- ${i}`}
76
+ Return only the <output> tag and no other text.`}function l(e,t){let n=`- Each commit belongs to exactly one branch.
77
+ ${t?.promptFragment?`- ${a} Follow the repository's branch naming conventions (see above).`:`- ${a}`}
78
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}
79
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.
80
80
  ${r}
@@ -85,22 +85,22 @@ ${r}
85
85
  - Create separate branches when concerns are genuinely independent — meaning they address different problems or features, and a reviewer would gain nothing from seeing them together in one PR. Two commits that each add a separate utility function unrelated to each other are good candidates for independent branches.
86
86
  - Keep changes in the same branch when they form a coherent story that benefits from being reviewed together, even if they could technically stand alone.
87
87
  ${r}
88
- - When in doubt, consider: "Would a reviewer want to see these changes together, or would separating them make review easier?" If separating helps, split.`}function l(e){return e===`consolidated`?`
88
+ - When in doubt, consider: "Would a reviewer want to see these changes together, or would separating them make review easier?" If separating helps, split.`}function u(e){return e===`consolidated`?`
89
89
  Prefer fewer, larger commits. Combine related changes into a single commit when they serve the same purpose. A commit can touch many files if they are all part of the same logical change.`:e===`granular`?`
90
- Prefer more, smaller commits. Each distinct change should be its own commit, even if multiple changes serve a similar purpose. Aim for commits that touch as few files as possible.`:``}function u(e){let{hunks:t,commitMessages:n,instructions:r,mode:i=`branches`}=e,a=JSON.stringify(t.map(e=>({index:e.index,fileName:e.fileName,hunkHeader:e.hunkHeader,content:e.content,...e.isBinary?{isBinary:!0}:{},...e.isRename?{isRename:!0}:{},...e.originalFileName?{originalFileName:e.originalFileName}:{}}))),o=`${i===`commits`?`Examine the following ${t.length} code hunks (indices 0 to ${t.length-1}) and organize them into commits. Every index must appear exactly once.`:`Examine the following ${t.length} code hunks (indices 0 to ${t.length-1}) and organize them into commits and branches. Every index must appear exactly once.`}
90
+ Prefer more, smaller commits. Each distinct change should be its own commit, even if multiple changes serve a similar purpose. Aim for commits that touch as few files as possible.`:``}function d(t){let{hunks:n,commitMessages:r,instructions:i,mode:a=`branches`,redactHunkContent:o}=t,s=JSON.stringify(n.map(t=>({index:t.index,fileName:t.fileName,hunkHeader:t.hunkHeader,content:e(t,o),...t.isBinary?{isBinary:!0}:{},...t.isRename?{isRename:!0}:{},...t.originalFileName?{originalFileName:t.originalFileName}:{}}))),c=`${a===`commits`?`Examine the following ${n.length} code hunks (indices 0 to ${n.length-1}) and organize them into commits. Every index must appear exactly once.`:`Examine the following ${n.length} code hunks (indices 0 to ${n.length-1}) and organize them into commits and branches. Every index must appear exactly once.`}
91
91
 
92
92
  <hunks>
93
- ${a}
94
- </hunks>`;return n?.length&&(o+=`
93
+ ${s}
94
+ </hunks>`;return r?.length&&(c+=`
95
95
 
96
96
  The following commit messages are from the original commits. Use them to understand intent and match their style:
97
97
 
98
98
  <commit_messages>
99
- ${JSON.stringify(n)}
100
- </commit_messages>`),r&&(o+=`
99
+ ${JSON.stringify(r)}
100
+ </commit_messages>`),i&&(c+=`
101
101
 
102
102
  Carefully follow these additional instructions, but do not deviate from the output structure:
103
- ${r}`),o+=`\n\n${i===`commits`?`Now organize the hunks into commits.`:`Now organize the hunks into commits and branches.`} Return only the <output> tag and no other text.`,o}function d(e,t){let n;return n=t===`commits`?`Based on your analysis of the changes, now organize the hunks into commits. Each hunk must be assigned to exactly one commit.`:f(e.strategy),e.conventions?.promptFragment&&(n+=`\n\n${e.conventions.promptFragment}`),e.commitMessages?.length&&(n+=`\n\nMatch the style of these original commit messages:\n${JSON.stringify(e.commitMessages)}`),e.instructions&&(n+=`\n\nAdditional instructions:\n${e.instructions}`),t===`commits`?n+=`
103
+ ${i}`),c+=`\n\n${a===`commits`?`Now organize the hunks into commits.`:`Now organize the hunks into commits and branches.`} Return only the <output> tag and no other text.`,c}function f(e,t){let n;return n=t===`commits`?`Based on your analysis of the changes, now organize the hunks into commits. Each hunk must be assigned to exactly one commit.`:p(e.strategy),e.conventions?.promptFragment&&(n+=`\n\n${e.conventions.promptFragment}`),e.commitMessages?.length&&(n+=`\n\nMatch the style of these original commit messages:\n${JSON.stringify(e.commitMessages)}`),e.instructions&&(n+=`\n\nAdditional instructions:\n${e.instructions}`),t===`commits`?n+=`
104
104
 
105
105
  Return your grouping as JSON inside <output> tags. Wrap all commits in a single branch with id "default", name "", title "", description "":
106
106
  <output>
@@ -148,7 +148,7 @@ Return your grouping as JSON inside <output> tags:
148
148
  }
149
149
  </output>
150
150
 
151
- Return only the <output> tag and no other text.`,n}function f(e){let t=e?.branchSplitting??`balanced`,n=`Based on your analysis of the changes, now organize the hunks into commits and branches. Each hunk must be assigned to exactly one commit, and each commit belongs to exactly one branch.`,r=`Consider merge-order preference: even if two concerns compile independently, they should be in the same branch or stacked if merging one without the other would be incomplete or confusing (e.g., documentation for utilities should be with or after those utilities).`;return t===`conservative`?`${n}
151
+ Return only the <output> tag and no other text.`,n}function p(e){let t=e?.branchSplitting??`balanced`,n=`Based on your analysis of the changes, now organize the hunks into commits and branches. Each hunk must be assigned to exactly one commit, and each commit belongs to exactly one branch.`,r=`Consider merge-order preference: even if two concerns compile independently, they should be in the same branch or stacked if merging one without the other would be incomplete or confusing (e.g., documentation for utilities should be with or after those utilities).`;return t===`conservative`?`${n}
152
152
 
153
153
  Default to a single branch. Only create multiple branches when concerns are completely unrelated — different domains, different purposes, no merge-order preference.
154
154
  ${r}
@@ -161,4 +161,4 @@ Create separate branches when concerns are genuinely independent — different p
161
161
 
162
162
  Keep changes in the same branch when they form a coherent story that benefits from joint review, or when there is a merge-order preference between them.
163
163
  ${r}
164
- When in doubt, consider: "Would a reviewer want to see these together, or would separating them make review easier?"`}export{d as buildGroupContinuationPrompt,a as buildGroupSystemPrompt,u as buildGroupUserPrompt};
164
+ When in doubt, consider: "Would a reviewer want to see these together, or would separating them make review easier?"`}export{f as buildGroupContinuationPrompt,o as buildGroupSystemPrompt,d as buildGroupUserPrompt};
@@ -2,6 +2,7 @@ import { ProgressEvent, TokenUsage } from "../../ai/types.js";
2
2
  import { BranchGroup, ComposeHunk, PromptOverride } from "../types.js";
3
3
  import { ConventionDescriptor } from "../../git/utils/conventions.js";
4
4
  import { OnAfterModelCall, OnBeforeModelCall, OnBeforePrompt } from "../ai-loop.js";
5
+ import { RedactHunkContent } from "../utils/redact.js";
5
6
  import { AnalysisResult } from "../analyze/types.js";
6
7
  import { ComposeSession } from "../session.js";
7
8
  import { AiModelPort } from "@gitkraken/shared-tools";
@@ -35,6 +36,8 @@ interface GroupInput {
35
36
  strategy?: GroupingStrategy;
36
37
  /** Branch names that already exist in the repo — used to detect collisions during validation. */
37
38
  existingBranchNames?: Set<string>;
39
+ /** See {@link RedactHunkContent}. */
40
+ redactHunkContent?: RedactHunkContent;
38
41
  model: AiModelPort;
39
42
  session?: ComposeSession;
40
43
  maxRetries?: number;
@@ -1,5 +1,6 @@
1
1
  import { ComposeHunk } from "../types.js";
2
2
  import { ConventionDescriptor } from "../../git/utils/conventions.js";
3
+ import { RedactHunkContent } from "../utils/redact.js";
3
4
  import { GroupingStrategy } from "../group/types.js";
4
5
 
5
6
  //#region src/tasks/quick-compose/prompt.d.ts
@@ -13,6 +14,7 @@ declare function buildQuickComposeUserPrompt(params: {
13
14
  issueUrl?: string;
14
15
  pullRequestUrl?: string;
15
16
  instructions?: string;
17
+ redactHunkContent?: RedactHunkContent;
16
18
  }): string;
17
19
  //#endregion
18
20
  export { buildQuickComposeSystemPrompt, buildQuickComposeUserPrompt };
@@ -1,8 +1,8 @@
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:
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
2
  Each commit message must contain both a summary and a description, separated by a blank line:
3
3
  - **Summary** (first line): concise, 50 chars max, third-person singular present tense ("Fixes", "Adds", "Improves"). Stands alone as the commit subject.
4
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.
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.
6
6
 
7
7
  ## Your task
8
8
 
@@ -13,7 +13,7 @@ ${t===`branches`?`4. Assign commits to branches — each branch is a cohesive un
13
13
  ${i?`5. If a branch should be split into stacked sub-branches for review, provide partitions.`:``}
14
14
 
15
15
  ## Commit style
16
- ${c}
16
+ ${a}
17
17
  ${l}
18
18
  ${u}
19
19
  ${d}
@@ -36,21 +36,21 @@ Before producing the \`<output>\` tag, do a self-check: count the total number o
36
36
  Output as JSON inside a <output> tag matching this schema:
37
37
 
38
38
  \`\`\`json
39
- ${n}
39
+ ${r}
40
40
  \`\`\`
41
41
 
42
- Return only the <output> tag and no other text.`}function i(e){let{hunks:t,commitMessages:n,commitOrder:r,branchName:i,baseBranch:a,issueUrl:o,pullRequestUrl:s,instructions:c}=e,l=`Organize these code hunks into commits:
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:
43
43
 
44
44
  <hunks>
45
- ${JSON.stringify(t.map(e=>({index:e.index,fileName:e.fileName,hunkHeader:e.hunkHeader,content:e.content,...e.isBinary?{isBinary:!0}:{},...e.isRename?{isRename:!0}:{},...e.originalFileName?{originalFileName:e.originalFileName}:{}})))}
46
- </hunks>`,u=[];return i&&u.push(`Branch: ${i}`),a&&u.push(`Base: ${a}`),o&&u.push(`Issue: ${o}`),s&&u.push(`PR: ${s}`),u.length>0&&(l+=`\n\nContext:\n${u.join(`
47
- `)}`),n?.length&&(l+=`\n\nOriginal commit messages (match their style):\n<commit_messages>\n${JSON.stringify(n)}\n</commit_messages>`),r?.length&&(l+=`\n\nOriginal commit order:\n${JSON.stringify(r)}`),c&&(l+=`\n\nAdditional instructions:\n${c}`),l+=`
45
+ ${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
+ </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
+ `)}`),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
48
 
49
- Return only the <output> tag and no other text.`,l}function a(e){return e===`consolidated`?`
49
+ Return only the <output> tag and no other text.`,d}function o(e){return e===`consolidated`?`
50
50
  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 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`
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`
52
52
  ## Partition guidelines (stacked sub-branches)
53
53
  - If a branch has multiple distinct concerns, split it into partitions for stacked review.
54
54
  - Each partition is a contiguous range of commits from orderedCommitIds.
55
55
  - Each partition gets a unique, valid git branch name.
56
- - Single partition (no split) is a valid outcome.`}export{r as buildQuickComposeSystemPrompt,i as buildQuickComposeUserPrompt};
56
+ - Single partition (no split) is a valid outcome.`}export{i as buildQuickComposeSystemPrompt,a as buildQuickComposeUserPrompt};
@@ -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,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};
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,redactHunkContent:o.redactHunkContent}),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};
@@ -2,6 +2,7 @@ import { ProgressEvent, TokenUsage } from "../../ai/types.js";
2
2
  import { BranchPartition, CommitSuggestion, ComposeHunk, PromptOverride } from "../types.js";
3
3
  import { ConventionDescriptor } from "../../git/utils/conventions.js";
4
4
  import { OnAfterModelCall, OnBeforeModelCall, OnBeforePrompt } from "../ai-loop.js";
5
+ import { RedactHunkContent } from "../utils/redact.js";
5
6
  import { GroupingStrategy } from "../group/types.js";
6
7
  import { ComposeSession } from "../session.js";
7
8
  import { AiModelPort } from "@gitkraken/shared-tools";
@@ -25,6 +26,8 @@ interface QuickComposeInput {
25
26
  strategy?: GroupingStrategy;
26
27
  /** Branch names that already exist — used to detect collisions during validation. */
27
28
  existingBranchNames?: Set<string>;
29
+ /** See {@link RedactHunkContent}. */
30
+ redactHunkContent?: RedactHunkContent;
28
31
  model: AiModelPort;
29
32
  /** Prior session to resume from. When `completedSteps.quickCompose` is present, skip the AI call. */
30
33
  session?: ComposeSession;
@@ -7,18 +7,18 @@ interface ComposeHunk {
7
7
  content: string;
8
8
  additions: number;
9
9
  deletions: number;
10
- source: 'staged' | 'unstaged' | 'untracked' | 'branch';
11
- sourceCommitSha?: string;
12
10
  isRename?: boolean;
13
11
  originalFileName?: string;
14
12
  isBinary?: boolean;
15
13
  /** Full binary patch block (base85-encoded). Excluded from AI context, used only by patch builder. */
16
14
  binaryPatchData?: string;
17
15
  /**
18
- * Original author of this hunk (e.g. when the hunk came from a branch source
19
- * and the commit carrying it was authored by someone specific). The apply
20
- * phase aggregates per-commit hunks through an attribution strategy to choose
21
- * the commit's primary author and Co-authored-by trailers.
16
+ * Likely author of this hunk. For `commit-range` and `branch` sources the
17
+ * library populates this via a line-range-overlap heuristic against the
18
+ * per-commit hunks of the source range; the author with the highest
19
+ * cumulative overlap wins, and other authors land in `coAuthors`. The apply
20
+ * phase aggregates `author` across the hunks of each new commit to pick the
21
+ * commit's primary author and `Co-authored-by` trailers.
22
22
  *
23
23
  * `date` is the commit's author date when known — used by consumers for UI
24
24
  * display and, for the commit created by the apply path, as `GIT_AUTHOR_DATE`.
@@ -29,9 +29,10 @@ interface ComposeHunk {
29
29
  date?: Date;
30
30
  };
31
31
  /**
32
- * Additional authors attached to this hunk (for merge commits or the AI's
33
- * attribution hints). Combined with `author` from all hunks in a commit to
34
- * produce the final Co-authored-by trailer list.
32
+ * Additional likely authors of this hunk (those whose per-commit overlap
33
+ * was non-zero but didn't win the primary slot). Combined with `author`
34
+ * from all hunks in a new commit to produce the final `Co-authored-by`
35
+ * trailer list.
35
36
  */
36
37
  coAuthors?: {
37
38
  name: string;
@@ -0,0 +1,14 @@
1
+ import { ComposeHunk } from "../types.js";
2
+
3
+ //#region src/tasks/utils/redact.d.ts
4
+ /** Placeholder substituted for hunk content when {@link RedactHunkContent} returns true. */
5
+ declare const REDACTED_HUNK_CONTENT = "[content redacted because this file is excluded from AI processing \u2014 still assign this hunk to a commit based on its filename]";
6
+ /**
7
+ * Returns `true` for hunks whose content should be redacted from AI prompts. The library
8
+ * keeps the hunk's `fileName`, `hunkHeader`, `index`, line counts, and rename metadata in
9
+ * the prompt; only the patch body is replaced.
10
+ */
11
+ type RedactHunkContent = (hunk: ComposeHunk) => boolean;
12
+ declare function hunkContentForPrompt(hunk: ComposeHunk, redact: RedactHunkContent | undefined): string;
13
+ //#endregion
14
+ export { REDACTED_HUNK_CONTENT, RedactHunkContent, hunkContentForPrompt };
@@ -0,0 +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};
@@ -3,6 +3,7 @@ import { ComposeGitPort, SigningConfig } from "../ports/git.js";
3
3
  import { BranchPartition, CommitSuggestion, ComposeHunk, ComposeTaskName, PromptOverride } from "../tasks/types.js";
4
4
  import { ConventionDescriptor } from "../git/utils/conventions.js";
5
5
  import { OnAfterModelCall, OnBeforeModelCall, OnBeforePrompt } from "../tasks/ai-loop.js";
6
+ import { RedactHunkContent } from "../tasks/utils/redact.js";
6
7
  import { AnalysisResult } from "../tasks/analyze/types.js";
7
8
  import { GroupingResult, GroupingStrategy } from "../tasks/group/types.js";
8
9
  import { OrderingResult } from "../tasks/order/types.js";
@@ -40,6 +41,8 @@ interface ComposePlanInput {
40
41
  onBeforePrompt?: OnBeforePrompt;
41
42
  /** See {@link ComposeInput.hunkFilter}. */
42
43
  hunkFilter?: ComposeInput['hunkFilter'];
44
+ /** See {@link ComposeInput.redactHunkContent}. */
45
+ redactHunkContent?: ComposeInput['redactHunkContent'];
43
46
  /** See {@link ComposeInput.session}. */
44
47
  session?: ComposeSession;
45
48
  /** See {@link ComposeInput.promptOverrides}. */
@@ -73,8 +76,8 @@ interface ApplyComposePlanInput {
73
76
  signing?: SigningConfig;
74
77
  /** How to pick the primary author for each commit. See {@link ComposeInput.authorAttribution}. */
75
78
  authorAttribution?: ComposeInput['authorAttribution'];
76
- /** See {@link ComposeInput.applyUpTo}. */
77
- applyUpTo?: ApplyUpTo;
79
+ /** See {@link ComposeInput.applyCommitIds}. */
80
+ applyCommitIds?: string[];
78
81
  /** See {@link ComposeInput.stashLabel}. */
79
82
  stashLabel?: string;
80
83
  /**
@@ -189,6 +192,16 @@ interface ComposeInput {
189
192
  * values so the plan still references the authoritative collection.
190
193
  */
191
194
  hunkFilter?: (hunks: ComposeHunk[]) => Promise<ComposeHunk[]> | ComposeHunk[];
195
+ /**
196
+ * Predicate that returns true for hunks whose patch body should be replaced with a
197
+ * placeholder in AI prompts. Matching hunks keep their filename, hunk header, index,
198
+ * line counts, and rename metadata; only the body is masked. The apply path still uses
199
+ * the real content.
200
+ *
201
+ * Intended for `.aiexclude`/`.aiignore`/`.cursorignore`-style features: the AI assigns
202
+ * matching hunks to commits by filename alone, never seeing the diff body.
203
+ */
204
+ redactHunkContent?: RedactHunkContent;
192
205
  /**
193
206
  * Per-task prompt overrides, keyed by {@link ComposeTaskName}. Consumers with
194
207
  * existing prompt template services (server-driven templates, provider-specific
@@ -242,20 +255,18 @@ interface ComposeInput {
242
255
  name: string;
243
256
  email: string;
244
257
  } | undefined);
245
- /** Apply only a prefix of `plan.allOrderedCommits`. Hunks not in applied commits are restored to the working tree. */
246
- applyUpTo?: ApplyUpTo;
258
+ /**
259
+ * Apply only the listed commit ids from `plan.allOrderedCommits`. Hunks belonging to omitted
260
+ * commits (plus unassigned hunks, e.g. those dropped by `hunkFilter`) become unstaged WIP on
261
+ * top of the rewritten branch. Undefined applies every commit.
262
+ *
263
+ * On destructive targets (`rewrite-range`, or `branch`/`auto` with `preserveOriginal: false`)
264
+ * with `branch`/`commit-range` sources, HEAD must be non-detached and at the source's tip.
265
+ */
266
+ applyCommitIds?: string[];
247
267
  /** Stash message used by the apply path when stashing pending workdir state. Defaults to `'compose backup'`. */
248
268
  stashLabel?: string;
249
269
  }
250
- type ApplyUpTo = {
251
- kind: 'all';
252
- } | {
253
- kind: 'commitId';
254
- id: string;
255
- } | {
256
- kind: 'count';
257
- count: number;
258
- };
259
270
  interface ComposePlan {
260
271
  analysis?: AnalysisResult;
261
272
  grouping: GroupingResult;
@@ -436,4 +447,4 @@ declare function composePlan(input: ComposePlanInput): Promise<ComposePlanResult
436
447
  */
437
448
  declare function applyComposePlan(input: ApplyComposePlanInput): Promise<ApplyComposePlanResult>;
438
449
  //#endregion
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 };
450
+ export { ApplyComposePlanInput, ApplyComposePlanResult, ComposeApplyPlan, type ComposeBranchPlan, type ComposeContext, ComposeDepth, ComposeInput, ComposePlan, ComposePlanInput, ComposePlanResult, ComposeProgressEvent, ComposeResult, type ComposeSource, ComposeStrategy, type ComposeTarget, type StashConflict, applyComposePlan, compose, composePlan, defaultPhaseMessages };