@gitkraken/compose-tools 0.8.1 → 0.10.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 (46) hide show
  1. package/CHANGELOG.md +15 -3
  2. package/README.md +15 -8
  3. package/dist/ai/types.d.ts +25 -9
  4. package/dist/context.d.ts +66 -0
  5. package/dist/context.js +1 -0
  6. package/dist/git/apply-target.js +1 -1
  7. package/dist/git/index.js +1 -1
  8. package/dist/index.d.ts +3 -1
  9. package/dist/index.js +1 -1
  10. package/dist/tasks/ai-loop.d.ts +29 -3
  11. package/dist/tasks/ai-loop.js +1 -1
  12. package/dist/tasks/analyze/analyze.js +1 -1
  13. package/dist/tasks/analyze/prompt.d.ts +6 -2
  14. package/dist/tasks/analyze/prompt.js +5 -5
  15. package/dist/tasks/analyze/types.d.ts +11 -0
  16. package/dist/tasks/compose-group/compose-group.js +1 -1
  17. package/dist/tasks/compose-group/prompt.d.ts +6 -1
  18. package/dist/tasks/compose-group/prompt.js +31 -31
  19. package/dist/tasks/compose-group/types.d.ts +16 -0
  20. package/dist/tasks/group/group.js +1 -1
  21. package/dist/tasks/group/prompt.d.ts +7 -1
  22. package/dist/tasks/group/prompt.js +21 -21
  23. package/dist/tasks/group/repair.d.ts +39 -0
  24. package/dist/tasks/group/repair.js +16 -0
  25. package/dist/tasks/group/types.d.ts +17 -1
  26. package/dist/tasks/group/validator.d.ts +6 -0
  27. package/dist/tasks/group/validator.js +6 -6
  28. package/dist/tasks/quick-compose/prompt.d.ts +3 -1
  29. package/dist/tasks/quick-compose/prompt.js +11 -11
  30. package/dist/tasks/quick-compose/quick-compose.js +1 -1
  31. package/dist/tasks/quick-compose/types.d.ts +7 -0
  32. package/dist/tasks/utils/analysis-prompt.d.ts +14 -0
  33. package/dist/tasks/utils/analysis-prompt.js +5 -0
  34. package/dist/tasks/utils/authoring-context.d.ts +47 -0
  35. package/dist/tasks/utils/authoring-context.js +9 -0
  36. package/dist/tasks/utils/budget.d.ts +9 -1
  37. package/dist/tasks/utils/budget.js +1 -1
  38. package/dist/tasks/utils/hunk-summaries.d.ts +19 -0
  39. package/dist/tasks/utils/hunk-summaries.js +5 -0
  40. package/dist/workflows/author.d.ts +101 -0
  41. package/dist/workflows/author.js +16 -0
  42. package/dist/workflows/compose.d.ts +93 -3
  43. package/dist/workflows/compose.js +3 -3
  44. package/dist/workflows/split.js +2 -2
  45. package/dist/workflows/types.d.ts +16 -2
  46. package/package.json +2 -2
@@ -1,4 +1,4 @@
1
- import{assembleHunkPrompt as e}from"../utils/budget.js";import{groupingResultJsonSchema as t}from"../group/schema.js";const n=JSON.stringify(t,null,2),r=`
1
+ import{renderAuthoringContext as e}from"../utils/authoring-context.js";import{assembleHunkPrompt as t}from"../utils/budget.js";import{renderHunkSummaries as n}from"../utils/hunk-summaries.js";import{groupingResultJsonSchema as r}from"../group/schema.js";import{renderSuppliedAnalysis as i}from"../utils/analysis-prompt.js";const a=JSON.stringify(r,null,2),o=`
2
2
  In addition to the \`branches\` field, include an \`analysis\` field at the top level:
3
3
 
4
4
  \`\`\`json
@@ -10,12 +10,12 @@ 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
+ `,s=`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.`,a=`5. Write a concise single-line commit message for each commit — a subject only, no body: 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.`;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,o){let s=`${e?.commits.bodyStyle===`minimal`?a:i}
18
- 6. Write a detailed explanation for each commit as if explaining to a reviewer.${e?.promptFragment?`\n\n${e.promptFragment}`:``}`,c=u(t?.commitGranularity);return`You are an advanced AI programming assistant tasked with analyzing and organizing code changes into commits.
17
+ - Informal yet professional tone.`,c=`5. Write a concise single-line commit message for each commit — a subject only, no body: 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.`;function l(e,t=`branches`,n,r,i){return i?t===`commits`?m(e,n):h(e,n):t===`commits`?u(e,n,r):d(e,n,r)}function u(e,t,n){let r=`${e?.commits.bodyStyle===`minimal`?c:s}
18
+ 6. Write a detailed explanation for each commit as if explaining to a reviewer.${e?.promptFragment?`\n\n${e.promptFragment}`:``}`,i=p(t?.commitGranularity);return`You are an advanced AI programming assistant tasked with analyzing and organizing code changes into commits.
19
19
 
20
20
  ## Critical constraints (output is rejected if violated)
21
21
 
@@ -26,34 +26,34 @@ In addition to the \`branches\` field, include an \`analysis\` field at the top
26
26
 
27
27
  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.
28
28
 
29
- ${o?`## Step 1: Analysis`:`## Step 1: Internal Analysis`}
29
+ ${n?`## Step 1: Analysis`:`## Step 1: Internal Analysis`}
30
30
 
31
31
  Before grouping, analyze the changes to understand:
32
32
  - **Concerns** — what distinct themes, features, or purposes exist across the hunks
33
33
  - **Dependencies** — which hunks provide things (define functions, types, files) that other hunks consume (import, call, reference)
34
34
  - **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
35
35
 
36
- ${o?"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.`}
36
+ ${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.`}
37
37
 
38
38
  ## Step 2: Group hunks into commits
39
39
 
40
40
  1. Group hunks into logical units that make sense together and can be applied atomically
41
41
  2. Ensure each commit is self-contained and atomic
42
- ${s}
43
- ${c}
42
+ ${r}
43
+ ${i}
44
44
 
45
45
  ## Required Output Format
46
46
 
47
47
  Output your organization as JSON inside a <output> tag matching this schema:
48
48
 
49
49
  \`\`\`json
50
- ${n}
50
+ ${a}
51
51
  \`\`\`
52
- ${o?r:``}
52
+ ${n?o:``}
53
53
  Wrap all commits in a single branch object with id "default", name "", title "", and description "".
54
54
 
55
- Return only the <output> tag and no other text.`}function c(e,t,o){let s=`${e?.commits.bodyStyle===`minimal`?a:i}
56
- 6. Write a detailed explanation for each commit as if explaining to a reviewer.${e?.promptFragment?`\n\n${e.promptFragment}`:``}`,c=l(t?.branchSplitting,e),d=u(t?.commitGranularity);return`You are an advanced AI programming assistant tasked with analyzing and organizing code changes into commits and branches.
55
+ Return only the <output> tag and no other text.`}function d(e,t,n){let r=`${e?.commits.bodyStyle===`minimal`?c:s}
56
+ 6. Write a detailed explanation for each commit as if explaining to a reviewer.${e?.promptFragment?`\n\n${e.promptFragment}`:``}`,i=f(t?.branchSplitting,e),l=p(t?.commitGranularity);return`You are an advanced AI programming assistant tasked with analyzing and organizing code changes into commits and branches.
57
57
 
58
58
  ## Critical constraints (output is rejected if violated)
59
59
 
@@ -64,7 +64,7 @@ Return only the <output> tag and no other text.`}function c(e,t,o){let s=`${e?.c
64
64
 
65
65
  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.
66
66
 
67
- ${o?`## Step 1: Analysis`:`## Step 1: Internal Analysis`}
67
+ ${n?`## Step 1: Analysis`:`## Step 1: Internal Analysis`}
68
68
 
69
69
  Before grouping, analyze the changes to understand:
70
70
  - **Concerns** — what distinct themes, features, or purposes exist across the hunks
@@ -72,7 +72,7 @@ Before grouping, analyze the changes to understand:
72
72
  - **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
73
73
  - **Merge-order preference** — even if two concerns compile independently, would merging one without the other be confusing or incomplete?
74
74
 
75
- ${o?"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.`}
75
+ ${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.`}
76
76
 
77
77
  ## Step 2: Organize hunks into commits and branches
78
78
 
@@ -85,26 +85,26 @@ Your job has two levels:
85
85
 
86
86
  1. Group hunks into logical units that make sense together and can be applied atomically
87
87
  2. Ensure each commit is self-contained and atomic
88
- ${s}
89
- ${d}
88
+ ${r}
89
+ ${l}
90
90
 
91
91
  ### Branch guidelines
92
92
 
93
- ${c}
93
+ ${i}
94
94
 
95
95
  ## Required Output Format
96
96
 
97
97
  Output your organization as JSON inside a <output> tag matching this schema:
98
98
 
99
99
  \`\`\`json
100
- ${n}
100
+ ${a}
101
101
  \`\`\`
102
- ${o?r:``}
103
- 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.
102
+ ${n?o:``}
103
+ Return only the <output> tag and no other text.`}function f(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.
104
104
  ${t?.promptFragment?`- ${n} Follow the repository's branch naming conventions (see above).`:`- ${n}`}
105
- - 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`?`
105
+ - 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 p(e){return e===`consolidated`?`
106
106
  Prefer fewer, larger commits. Combine related changes into a single commit when they serve the same purpose.`:e===`granular`?`
107
- Prefer more, smaller commits. Each distinct change should be its own commit.`:``}function d(e,t){return`Organize code hunks into clean, atomic commits.
107
+ Prefer more, smaller commits. Each distinct change should be its own commit.`:``}function m(e,t){return`Organize code hunks into clean, atomic commits.
108
108
 
109
109
  ## Rules
110
110
 
@@ -115,7 +115,7 @@ Prefer more, smaller commits. Each distinct change should be its own commit.`:``
115
115
  5. ${e?.promptFragment?`Write commit messages following the repository's conventions (see below). Write a one-sentence explanation per commit.
116
116
 
117
117
  ${e.promptFragment}`:`Write commit messages: first line under 50 chars, imperative mood ("Add", "Fix", "Update"). Write a one-sentence explanation per commit.`}
118
- ${u(t?.commitGranularity)}
118
+ ${p(t?.commitGranularity)}
119
119
 
120
120
  ## Output
121
121
 
@@ -125,9 +125,9 @@ JSON inside <output> tags. Use one branch with id "default", name "", title "",
125
125
  {"branches":[{"id":"default","name":"","title":"","description":"","commits":[{"id":"commit-1","message":"Add feature X","explanation":"Brief reason.","hunks":[0,1,2]}]}]}
126
126
  \`\`\`
127
127
 
128
- 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.
128
+ 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 h(e,t){let n=e?.promptFragment?`Write commit messages following the repository's conventions (see below). Write a one-sentence explanation per commit.
129
129
 
130
- ${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.
130
+ ${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=f(t?.branchSplitting,e);return`Organize code hunks into clean, atomic commits and branches.
131
131
 
132
132
  ## Commit rules
133
133
 
@@ -136,7 +136,7 @@ ${e.promptFragment}`:`Write commit messages: first line under 50 chars, third-pe
136
136
  3. Each commit needs a unique ID (e.g., "commit-1", "commit-2").
137
137
  4. Every commit must have at least one hunk. No empty commits.
138
138
  5. ${n}
139
- ${u(t?.commitGranularity)}
139
+ ${p(t?.commitGranularity)}
140
140
 
141
141
  ## Branch guidelines
142
142
 
@@ -152,11 +152,11 @@ JSON inside <output> tags. Each branch needs a unique ID and non-empty name.
152
152
 
153
153
  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.
154
154
 
155
- 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,inputTokenBudget:f,systemPromptChars:p,onBudgetReduction:m}=t,h=n.map(e=>e.index).join(`, `),{prompt:g,hunksCondensedForBudget:_}=e({hunks:n,assemble:e=>{let t=`Organize these ${n.length} code hunks (indices: ${h}) into commits. Every listed index must appear exactly once.
155
+ Return only the <output> tag and no other text.`}function g(r){let{hunks:a,commitMessages:o,commitOrder:s,branchName:c,baseBranch:l,issueUrl:u,pullRequestUrl:d,authoring:f,analysis:p,hunkSummaries:m,instructions:h,condensed:g,redactHunkContent:_,inputTokenBudget:v,systemPromptChars:y,onBudgetReduction:b}=r,x=i(p),S=n(m,a),C=a.map(e=>e.index).join(`, `),{prompt:w,hunksCondensedForBudget:T}=t({hunks:a,assemble:t=>{let n=`Organize these ${a.length} code hunks (indices: ${C}) into commits. Every listed index must appear exactly once.
156
156
 
157
157
  <hunks>
158
- ${e}
159
- </hunks>`,u=[];return a&&u.push(`Branch: ${a}`),o&&u.push(`Base: ${o}`),s&&u.push(`Issue: ${s}`),c&&u.push(`PR: ${c}`),u.length>0&&(t+=`\n\nContext:\n${u.join(`
160
- `)}`),r?.length&&(t+=`\n\nOriginal commit messages (match their style):\n<commit_messages>\n${JSON.stringify(r)}\n</commit_messages>`),i?.length&&(t+=`\n\nOriginal commit order:\n${JSON.stringify(i)}`),l&&(t+=`\n\nAdditional instructions:\n${l}`),t+=`
158
+ ${t}
159
+ </hunks>`,r=e(f,{issueUrl:u,pullRequestUrl:d}),i=[];return c&&i.push(`Branch: ${c}`),l&&i.push(`Base: ${l}`),i.push(...r.lines),i.length>0&&(n+=`\n\nContext:\n${i.join(`
160
+ `)}`),S&&(n+=`\n\n${S}`),x&&(n+=`\n\n${x}`),r.block&&(n+=`\n\n${r.block}`),o?.length&&(n+=`\n\nOriginal commit messages (match their style):\n<commit_messages>\n${JSON.stringify(o)}\n</commit_messages>`),s?.length&&(n+=`\n\nOriginal commit order:\n${JSON.stringify(s)}`),h&&(n+=`\n\nAdditional instructions:\n${h}`),n+=`
161
161
 
162
- Return only the <output> tag and no other text.`,t},condensed:u,redactHunkContent:d,inputTokenBudget:f,systemPromptChars:p});return m?.(_),g}export{o as buildComposeGroupSystemPrompt,p as buildComposeGroupUserPrompt};
162
+ Return only the <output> tag and no other text.`,n},condensed:g,redactHunkContent:_,inputTokenBudget:v,systemPromptChars:y});return b?.(T),w}export{l as buildComposeGroupSystemPrompt,g as buildComposeGroupUserPrompt};
@@ -1,8 +1,10 @@
1
1
  import { ProgressEvent } from "../../ai/types.js";
2
+ import { AuthoringContext } from "../../context.js";
2
3
  import { ComposeHunk, PromptOverride } from "../types.js";
3
4
  import { ConventionDescriptor } from "../../git/utils/conventions.js";
4
5
  import { OnAfterModelCall, OnBeforeModelCall, OnBeforePrompt } from "../ai-loop.js";
5
6
  import { RedactHunkContent } from "../utils/redact.js";
7
+ import { AnalysisResult } from "../analyze/types.js";
6
8
  import { GroupOutput, GroupingStrategy } from "../group/types.js";
7
9
  import { ComposeSession } from "../session.js";
8
10
  import { AiModelPort } from "@gitkraken/shared-tools";
@@ -24,6 +26,20 @@ interface ComposeGroupInput {
24
26
  baseBranch?: string;
25
27
  issueUrl?: string;
26
28
  pullRequestUrl?: string;
29
+ /**
30
+ * What the author knows that the hunks cannot show — intent, issue text, per-file rationale,
31
+ * ordering constraints. Rendered into the prompt as escaped, tagged data, never as instructions;
32
+ * for guidance the model should act on, use {@link instructions}.
33
+ */
34
+ authoring?: AuthoringContext;
35
+ /**
36
+ * Analysis supplied by the caller. Rendered into the prompt as the authoritative breakdown of the
37
+ * change, so the model reuses it instead of deriving one. Makes {@link includeAnalysis}
38
+ * unnecessary — there is nothing left for the model to produce.
39
+ */
40
+ analysis?: AnalysisResult;
41
+ /** One-line summaries of what individual hunks do, keyed by index. See `ComposeInput.hunkSummaries`. */
42
+ hunkSummaries?: Record<number, string>;
27
43
  instructions?: string;
28
44
  conventions?: ConventionDescriptor;
29
45
  /** "commits" = flat commit list. "branches" = AI decides branches (default). */
@@ -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=s.session?void 0:s.promptOverride?.system??a(s.conventions,l,s.strategy),d,f=n(s.session?i(s,l):o({hunks:c,commitMessages:s.commitMessages,instructions:s.instructions,mode:l,redactHunkContent:s.redactHunkContent,inputTokenBudget:s.inputTokenBudget,systemPromptChars:u?.length,onBudgetReduction:e=>{d=e}}),s.promptOverride),p=c?.length?new Set(c.map(e=>e.index)):new Set(s.session?.completedSteps.analyze?.hunkAnnotations.map(e=>e.hunkIndex)??[]);if(p.size===0)throw new e(`Cannot validate grouping: no hunk indices available from input or session`);let m=await t({model:s.model,systemPrompt:u,userPrompt:f,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,p,l,s.existingBranchNames),taskName:`group`}),h=s.session?.completedSteps??{},g={...m.session,completedSteps:{...h,group:m.result}};return{result:m.result,session:g,usage:m.usage,attempts:m.attempts,durationMs:m.durationMs,callIds:m.callIds,hunksCondensedCount:d,repairs:m.repairs}}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=s.session?void 0:s.promptOverride?.system??a(s.conventions,l,s.strategy),d,f=n(s.session?i(s,l):o({hunks:c,commitMessages:s.commitMessages,instructions:s.instructions,authoring:s.authoring,analysis:s.analysis,hunkSummaries:s.hunkSummaries,condensed:s.condensed,mode:l,redactHunkContent:s.redactHunkContent,inputTokenBudget:s.inputTokenBudget,systemPromptChars:u?.length,onBudgetReduction:e=>{d=e}}),s.promptOverride),p=c?.length?new Set(c.map(e=>e.index)):new Set(s.session?.completedSteps.analyze?.hunkAnnotations.map(e=>e.hunkIndex)??[]);if(p.size===0)throw new e(`Cannot validate grouping: no hunk indices available from input or session`);let m=await t({model:s.model,systemPrompt:u,userPrompt:f,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,p,l,s.existingBranchNames,c?.length?{repairContext:{hunks:c,condensed:s.condensed,redactHunkContent:s.redactHunkContent,inputTokenBudget:s.inputTokenBudget}}:void 0),taskName:`group`}),h=s.session?.completedSteps??{},g={...m.session,completedSteps:{...h,group:m.result}};return{result:m.result,session:g,usage:m.usage,attempts:m.attempts,durationMs:m.durationMs,callIds:m.callIds,hunksCondensedCount:d,repairs:m.repairs}}export{s as group};
@@ -1,6 +1,8 @@
1
+ import { AuthoringContext } from "../../context.js";
1
2
  import { ComposeHunk } from "../types.js";
2
3
  import { ConventionDescriptor } from "../../git/utils/conventions.js";
3
4
  import { RedactHunkContent } from "../utils/redact.js";
5
+ import { AnalysisResult } from "../analyze/types.js";
4
6
  import { GroupInput, GroupingStrategy } from "./types.js";
5
7
 
6
8
  //#region src/tasks/group/prompt.d.ts
@@ -8,7 +10,11 @@ declare function buildGroupSystemPrompt(conventions?: ConventionDescriptor, mode
8
10
  declare function buildGroupUserPrompt(params: {
9
11
  hunks: ComposeHunk[];
10
12
  commitMessages?: string[];
11
- instructions?: string;
13
+ instructions?: string; /** What the author knows that the hunks cannot show. Rendered as escaped, tagged data. */
14
+ authoring?: AuthoringContext; /** Caller-supplied analysis, used in place of one derived by a prior `analyze` call. */
15
+ analysis?: AnalysisResult; /** One-line summaries of what individual hunks do, keyed by index. See `ComposeInput.hunkSummaries`. */
16
+ hunkSummaries?: Record<number, string>; /** Send hunk metadata only, omitting every patch body. See `ComposeInput.condenseHunks`. */
17
+ condensed?: boolean;
12
18
  mode?: 'commits' | 'branches';
13
19
  redactHunkContent?: RedactHunkContent; /** Input-token ceiling that triggers progressive hunk reduction. Defaults to the shared budget. */
14
20
  inputTokenBudget?: number; /** Char count of the system prompt, folded into the budget estimate. Defaults to 0. */
@@ -1,8 +1,8 @@
1
- import{assembleHunkPrompt as e}from"../utils/budget.js";import{groupingResultJsonSchema as t}from"./schema.js";const n=JSON.stringify(t,null,2),r=`5. Write a full commit message for each commit containing both a summary and a description:
1
+ import{authoringContextBlock as e}from"../utils/authoring-context.js";import{assembleHunkPrompt as t}from"../utils/budget.js";import{renderHunkSummaries as n}from"../utils/hunk-summaries.js";import{groupingResultJsonSchema as r}from"./schema.js";import{renderSuppliedAnalysis as i}from"../utils/analysis-prompt.js";const a=JSON.stringify(r,null,2),o=`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=`5. Write a concise single-line commit message for each commit — a subject only, no body: 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.`,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.
5
+ - Informal yet professional tone.`,s=`5. Write a concise single-line commit message for each commit — a subject only, no body: 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.`,c=`Each branch must have a descriptive name, a short PR-style title (under 72 characters, imperative mood), and at least one commit.`;function l(e,t=`branches`,n){return t===`commits`?u(e,n):d(e,n)}function u(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
 
@@ -17,22 +17,22 @@ Before producing the \`<output>\` tag, do a self-check: count the total number o
17
17
 
18
18
  1. Group hunks into logical units that make sense together and can be applied atomically
19
19
  2. Ensure each commit is self-contained and atomic
20
- ${`${e?.commits.bodyStyle===`minimal`?i:r}
20
+ ${`${e?.commits.bodyStyle===`minimal`?s:o}
21
21
  6. Write a detailed explanation for each commit as if explaining to a reviewer.${e?.promptFragment?`\n\n${e.promptFragment}`:``}`}
22
- ${u(t?.commitGranularity)}
22
+ ${p(t?.commitGranularity)}
23
23
 
24
24
  ## Required Output Format
25
25
 
26
26
  Output your organization as JSON inside a <output> tag matching this schema:
27
27
 
28
28
  \`\`\`json
29
- ${n}
29
+ ${a}
30
30
  \`\`\`
31
31
 
32
32
  Wrap all commits in a single branch object with id "default", name "", title "", and description "".
33
33
 
34
- Return only the <output> tag and no other text.`}function c(e,t){let a=`${e?.commits.bodyStyle===`minimal`?i:r}
35
- 6. Write a detailed explanation for each commit as if explaining to a reviewer.${e?.promptFragment?`\n\n${e.promptFragment}`:``}`,o=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.
34
+ Return only the <output> tag and no other text.`}function d(e,t){let n=`${e?.commits.bodyStyle===`minimal`?s:o}
35
+ 6. Write a detailed explanation for each commit as if explaining to a reviewer.${e?.promptFragment?`\n\n${e.promptFragment}`:``}`,r=f(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.
36
36
 
37
37
  Your job has two levels:
38
38
 
@@ -52,23 +52,23 @@ Before producing the \`<output>\` tag, do a self-check: count the total number o
52
52
 
53
53
  1. Group hunks into logical units that make sense together and can be applied atomically
54
54
  2. Ensure each commit is self-contained and atomic
55
- ${a}
56
- ${u(t?.commitGranularity)}
55
+ ${n}
56
+ ${p(t?.commitGranularity)}
57
57
 
58
58
  ## Branch guidelines
59
59
 
60
- ${o}
60
+ ${r}
61
61
 
62
62
  ## Required Output Format
63
63
 
64
64
  Output your organization as JSON inside a <output> tag matching this schema:
65
65
 
66
66
  \`\`\`json
67
- ${n}
67
+ ${a}
68
68
  \`\`\`
69
69
 
70
- Return only the <output> tag and no other text.`}function l(e,t){let n=`- Each commit belongs to exactly one branch.
71
- ${t?.promptFragment?`- ${a} Follow the repository's branch naming conventions (see above).`:`- ${a}`}
70
+ Return only the <output> tag and no other text.`}function f(e,t){let n=`- Each commit belongs to exactly one branch.
71
+ ${t?.promptFragment?`- ${c} Follow the repository's branch naming conventions (see above).`:`- ${c}`}
72
72
  - Changes addressing the same pattern across different files belong in ONE branch. For example, applying the same bug fix to multiple functions, or updating similar config across modules — these are the same concern even if the files are different.`,r=`- Consider merge-order preference, not just code dependencies. Even if two concerns compile independently, they may need to be in the same branch or stacked branches if merging one without the other would be confusing or incomplete (e.g., documentation that describes utilities introduced in other commits should be in the same branch or stacked after those utilities).`;return e===`conservative`?`${n}
73
73
  - 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.
74
74
  ${r}
@@ -79,22 +79,22 @@ ${r}
79
79
  - 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.
80
80
  - 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.
81
81
  ${r}
82
- - 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`?`
82
+ - 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 p(e){return e===`consolidated`?`
83
83
  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`?`
84
- 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,inputTokenBudget:s,systemPromptChars:c,onBudgetReduction:l}=t,u=n.map(e=>e.index).join(`, `),d=a===`commits`?`Examine the following ${n.length} code hunks (indices: ${u}) and organize them into commits. Every listed index must appear exactly once.`:`Examine the following ${n.length} code hunks (indices: ${u}) and organize them into commits and branches. Every listed index must appear exactly once.`,{prompt:f,hunksCondensedForBudget:p}=e({hunks:n,assemble:e=>{let t=`${d}
84
+ 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 m(r){let{hunks:a,commitMessages:o,instructions:s,authoring:c,analysis:l,hunkSummaries:u,condensed:d,mode:f=`branches`,redactHunkContent:p,inputTokenBudget:m,systemPromptChars:h,onBudgetReduction:g}=r,_=e(c),v=i(l),y=n(u,a),b=a.map(e=>e.index).join(`, `),x=f===`commits`?`Examine the following ${a.length} code hunks (indices: ${b}) and organize them into commits. Every listed index must appear exactly once.`:`Examine the following ${a.length} code hunks (indices: ${b}) and organize them into commits and branches. Every listed index must appear exactly once.`,{prompt:S,hunksCondensedForBudget:C}=t({hunks:a,assemble:e=>{let t=`${x}
85
85
 
86
86
  <hunks>
87
87
  ${e}
88
- </hunks>`;return r?.length&&(t+=`
88
+ </hunks>`;return o?.length&&(t+=`
89
89
 
90
90
  The following commit messages are from the original commits. Use them to understand intent and match their style:
91
91
 
92
92
  <commit_messages>
93
- ${JSON.stringify(r)}
94
- </commit_messages>`),i&&(t+=`
93
+ ${JSON.stringify(o)}
94
+ </commit_messages>`),y&&(t+=`\n\n${y}`),v&&(t+=`\n\n${v}`),_&&(t+=`\n\n${_}`),s&&(t+=`
95
95
 
96
96
  Carefully follow these additional instructions, but do not deviate from the output structure:
97
- ${i}`),t+=`\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.`,t},redactHunkContent:o,inputTokenBudget:s,systemPromptChars:c});return l?.(p),f}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+=`
97
+ ${s}`),t+=`\n\n${f===`commits`?`Now organize the hunks into commits.`:`Now organize the hunks into commits and branches.`} Return only the <output> tag and no other text.`,t},condensed:d,redactHunkContent:p,inputTokenBudget:m,systemPromptChars:h});return g?.(C),S}function h(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.`:g(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+=`
98
98
 
99
99
  Return your grouping as JSON inside <output> tags. Wrap all commits in a single branch with id "default", name "", title "", description "":
100
100
  <output>
@@ -142,7 +142,7 @@ Return your grouping as JSON inside <output> tags:
142
142
  }
143
143
  </output>
144
144
 
145
- 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}
145
+ Return only the <output> tag and no other text.`,n}function g(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}
146
146
 
147
147
  Default to a single branch. Only create multiple branches when concerns are completely unrelated — different domains, different purposes, no merge-order preference.
148
148
  ${r}
@@ -155,4 +155,4 @@ Create separate branches when concerns are genuinely independent — different p
155
155
 
156
156
  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.
157
157
  ${r}
158
- 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};
158
+ When in doubt, consider: "Would a reviewer want to see these together, or would separating them make review easier?"`}export{h as buildGroupContinuationPrompt,l as buildGroupSystemPrompt,m as buildGroupUserPrompt};
@@ -0,0 +1,39 @@
1
+ import { ValidationRepair } from "../../ai/types.js";
2
+ import { BranchGroup, ComposeHunk } from "../types.js";
3
+ import { TargetedRepair } from "../ai-loop.js";
4
+ import { RedactHunkContent } from "../utils/redact.js";
5
+ import { GroupingResult } from "./types.js";
6
+
7
+ //#region src/tasks/group/repair.d.ts
8
+ /**
9
+ * What the missing-hunk repair needs from the task to build its prompt. Omitted in conversation
10
+ * mode, where the hunks live in the model's context rather than in the task input — without hunk
11
+ * bodies there is nothing to ask about, so a short response falls straight through to a retry.
12
+ */
13
+ interface MissingHunksRepairContext {
14
+ hunks: readonly ComposeHunk[];
15
+ /** The caller asked for metadata only. Honoured here too: the repair never sends a hunk body. */
16
+ condensed?: boolean;
17
+ /** See {@link RedactHunkContent}. Honoured here too: a repair prompt must not leak excluded content. */
18
+ redactHunkContent?: RedactHunkContent;
19
+ /** Ceiling above which the repair prompt drops hunk bodies and asks on metadata alone. */
20
+ inputTokenBudget?: number;
21
+ }
22
+ /**
23
+ * Plans the repair for a grouping that left hunks unassigned: ask the model where the orphans
24
+ * belong instead of re-sending the whole diff. The question is deliberately narrow — an
25
+ * assignment map for the missing indices, never a fresh grouping — so a response cannot
26
+ * restructure decisions the first response already made.
27
+ *
28
+ * Returns undefined when the repair cannot be posed (no body for a missing index), or when so many
29
+ * hunks are missing that the question is most of the diff again.
30
+ */
31
+ declare function planMissingHunksRepair(params: {
32
+ branches: BranchGroup[];
33
+ mode: 'commits' | 'branches';
34
+ missingIndices: number[];
35
+ priorRepairs?: ValidationRepair[];
36
+ context: MissingHunksRepairContext;
37
+ }): TargetedRepair<GroupingResult> | undefined;
38
+ //#endregion
39
+ export { MissingHunksRepairContext, planMissingHunksRepair };
@@ -0,0 +1,16 @@
1
+ import{extractOutputTag as e}from"../ai-loop.js";import{DEFAULT_INPUT_TOKEN_BUDGET as t,estimateTokens as n,serializeHunk as r}from"../utils/budget.js";import{validateJsonSchema as i}from"../utils/json-schema.js";const a={$schema:`https://json-schema.org/draft/2020-12/schema`,type:`object`,properties:{assignments:{minItems:1,type:`array`,items:{type:`object`,properties:{hunkIndex:{type:`integer`,description:`Index of an unassigned hunk`},commitId:{type:`string`,minLength:1,description:`ID of the commit this hunk belongs to`}},required:[`hunkIndex`,`commitId`],additionalProperties:!1},description:`One entry per unassigned hunk`}},required:[`assignments`],additionalProperties:!1};function o(e){let{branches:t,mode:n,missingIndices:r,priorRepairs:i,context:a}=e;if(r.length>Math.max(2,a.hunks.length*.25))return;let o=new Map(a.hunks.map(e=>[e.index,e])),l=r.map(e=>o.get(e));if(l.some(e=>e===void 0))return;let u=t.flatMap(e=>e.commits.map(t=>n===`branches`?{id:t.id,message:t.message,branch:e.name,branchTitle:e.title}:{id:t.id,message:t.message}));if(u.length!==0)return{prompt:s({hunks:l.map(e=>e),commits:u,condensed:a.condensed,redactHunkContent:a.redactHunkContent,inputTokenBudget:a.inputTokenBudget}),apply:e=>c(e,{branches:t,missingIndices:r,priorRepairs:i})}}function s(e){let{hunks:i,commits:a,condensed:o,redactHunkContent:s,inputTokenBudget:c=t}=e,l=e=>{let t=JSON.stringify(i.map(t=>r(t,e?s?.(t)?`redacted`:`full`:`condensed`))),n=i.length===1?`hunk`:`hunks`,o=a.some(e=>e.branch!==void 0),c=JSON.stringify({assignments:[{hunkIndex:i[0].index,commitId:a[0].id}]},null,2);return`A grouping of code hunks into commits left ${i.length} ${n} unassigned. Decide which existing commit each one belongs to. Do not regroup anything else: the commits below are fixed, and every other hunk is already assigned.${o?` Each commit sits on the branch named next to it; choose the branch whose work the hunk belongs to, then the commit on it.`:``}
2
+
3
+ <unassigned_hunks>
4
+ ${t}
5
+ </unassigned_hunks>
6
+
7
+ <commits>
8
+ ${JSON.stringify(a)}
9
+ </commits>
10
+
11
+ Return JSON inside <output> tags with exactly one entry per unassigned hunk — the ${i.length} indices above, no others — each pointing at one of the commit IDs above:
12
+ <output>
13
+ ${c}
14
+ </output>
15
+
16
+ Return only the <output> tag and no other text.`};if(o)return l(!1);let u=l(!0);return n(u.length)>c?l(!1):u}function c(t,n){let{branches:r,missingIndices:o,priorRepairs:s}=n,c=e(t);if(!c)return{valid:!1,errors:[`Repair response has no <output> tag`]};let l;try{l=JSON.parse(c)}catch(e){return{valid:!1,errors:[`Repair response is not valid JSON: ${e instanceof Error?e.message:String(e)}`]}}let u=i(a,l);if(!u.success)return{valid:!1,errors:u.issues.map(e=>`Repair response ${e.path.join(`.`)}: ${e.message}`)};let d=new Set(r.flatMap(e=>e.commits.map(e=>e.id))),f=new Set(o),p=new Map,m=[];for(let{hunkIndex:e,commitId:t}of u.data.assignments){if(!f.has(e)){m.push(`Repair assigned hunk ${e}, which was not one of the unassigned hunks`);continue}if(p.has(e)){m.push(`Repair assigned hunk ${e} more than once`);continue}if(!d.has(t)){m.push(`Repair assigned hunk ${e} to unknown commit '${t}'`);continue}p.set(e,t)}let h=o.filter(e=>!p.has(e));if(h.length>0&&m.push(`Repair left hunks unassigned: ${h.join(`, `)}`),m.length>0)return{valid:!1,errors:m};let g={branches:r.map(e=>({...e,commits:e.commits.map(e=>{let t=[...p].filter(([,t])=>t===e.id).map(([e])=>e);return t.length===0?e:{...e,hunkIndices:[...e.hunkIndices,...t].sort((e,t)=>e-t)}})}))},_=[...p].sort(([e],[t])=>e-t).map(([e,t])=>`hunk ${e} → ${t}`).join(`; `);return{valid:!0,result:g,repairs:[...s??[],{kind:`missing-hunks`,hunkIndices:[...o].sort((e,t)=>e-t),detail:`Placed ${o.length} unassigned hunk(s) via a targeted repair call: ${_}`}]}}export{o as planMissingHunksRepair};
@@ -1,4 +1,5 @@
1
1
  import { ProgressEvent, TokenUsage, ValidationRepair } from "../../ai/types.js";
2
+ import { AuthoringContext } from "../../context.js";
2
3
  import { BranchGroup, ComposeHunk, PromptOverride } from "../types.js";
3
4
  import { ConventionDescriptor } from "../../git/utils/conventions.js";
4
5
  import { OnAfterModelCall, OnBeforeModelCall, OnBeforePrompt } from "../ai-loop.js";
@@ -26,9 +27,24 @@ interface GroupingStrategy {
26
27
  }
27
28
  interface GroupInput {
28
29
  hunks?: ComposeHunk[];
30
+ /**
31
+ * Analysis to group against, in place of running `analyze` first. Rendered into the prompt as the
32
+ * authoritative breakdown of the change. Ignored in session mode, where the analysis is already
33
+ * in the conversation.
34
+ */
29
35
  analysis?: AnalysisResult;
36
+ /** One-line summaries of what individual hunks do, keyed by index. See `ComposeInput.hunkSummaries`. */
37
+ hunkSummaries?: Record<number, string>;
30
38
  commitMessages?: string[];
31
39
  instructions?: string;
40
+ /**
41
+ * What the author knows that the hunks cannot show — intent, issue text, per-file rationale,
42
+ * ordering constraints. Rendered into the prompt as escaped, tagged data, never as instructions;
43
+ * for guidance the model should act on, use {@link instructions}.
44
+ */
45
+ authoring?: AuthoringContext;
46
+ /** Send hunk metadata only, omitting every patch body. See `ComposeInput.condenseHunks`. */
47
+ condensed?: boolean;
32
48
  conventions?: ConventionDescriptor;
33
49
  /** "commits" = flat commit list (no branch naming). "branches" = AI decides branches (default). */
34
50
  mode?: 'commits' | 'branches';
@@ -70,7 +86,7 @@ interface GroupOutput {
70
86
  /** Hunks condensed to metadata to fit the input-token budget. Undefined when the AI call was skipped. */
71
87
  hunksCondensedCount?: number;
72
88
  /**
73
- * Deterministic repairs applied to the accepted AI response instead of spending a retry on it.
89
+ * Repairs applied to the accepted AI response instead of spending a retry on it.
74
90
  * Undefined when the response needed no repair.
75
91
  */
76
92
  repairs?: ValidationRepair[];
@@ -1,5 +1,6 @@
1
1
  import { ValidationResult } from "../ai-loop.js";
2
2
  import { GroupingResult } from "./types.js";
3
+ import { MissingHunksRepairContext } from "./repair.js";
3
4
 
4
5
  //#region src/tasks/group/validator.d.ts
5
6
  interface GroupValidationOptions {
@@ -9,6 +10,11 @@ interface GroupValidationOptions {
9
10
  * be told the hunk is missing or duplicated when it did neither.
10
11
  */
11
12
  lockedCommitIds?: ReadonlySet<string>;
13
+ /**
14
+ * Enables the targeted repair for a response that left hunks unassigned. Omit it and such a
15
+ * response is rejected outright, as it was before the repair existed.
16
+ */
17
+ repairContext?: MissingHunksRepairContext;
12
18
  }
13
19
  declare function validateGroupResponse(responseText: string, expectedHunkIndices: ReadonlySet<number>, mode?: 'commits' | 'branches', existingBranchNames?: Set<string>, options?: GroupValidationOptions): ValidationResult<GroupingResult>;
14
20
  //#endregion
@@ -1,9 +1,9 @@
1
- import{extractOutputTag as e}from"../ai-loop.js";import{validateJsonSchema as t}from"../utils/json-schema.js";import{groupingResultJsonSchema as n}from"./schema.js";function r(r,o,s=`branches`,c,l){let u=null;try{if(u=e(r),!u)return i([`No <output> tag found in response`],a(s));let d=t(n,JSON.parse(u));if(!d.success)return i(d.issues.map(e=>`${e.path.join(`.`)}: ${e.message}`),a(s));let f=d.data,p=[],m=new Set,h=[],g=new Map,_=new Map,v=new Set,y=[],b=[],x=[],S=new Set,C=new Set;for(let e of f.branches){if(S.has(e.id))return i([`Duplicate branch ID: '${e.id}'`],a(s));if(S.add(e.id),s===`branches`&&e.name.length===0)return i([`Branch '${e.id}': missing or empty 'name'`],a(s));if(s===`branches`&&e.name.length>0){if(C.has(e.name))return i([`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(C.add(e.name),c?.has(e.name))return i([`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}'.`)}for(let t of e.commits){v.has(t.id)&&y.push(t.id),v.add(t.id);let n=s===`branches`?`${e.id}/${t.id}`:t.id;for(let r of t.hunks){let i=g.get(r);i?i.push(n):g.set(r,[n]);let a=_.get(r);a?a.push({branchId:e.id,commitId:t.id,label:n}):_.set(r,[{branchId:e.id,commitId:t.id,label:n}])}}}let w=new Map,T=[],E=!1;for(let[e,t]of _){if(t.length<2)continue;if(h.push(e),s===`branches`&&new Set(t.map(e=>e.branchId)).size>1){E=!0;continue}let n=t.find(e=>l?.lockedCommitIds?.has(e.commitId))??t[0];w.set(e,n.label),T.push({hunkIndex:e,keptIn:n.label,droppedFrom:t.filter(e=>e!==n).map(e=>e.label),scope:new Set(t.map(e=>e.commitId)).size===1?`within-commit`:`across-commits`})}let D=h.length>0&&!E;for(let e of f.branches){let t=[];for(let n of e.commits){let r=s===`branches`?`${e.id}/${n.id}`:n.id,i=[],a=!1;for(let e of n.hunks){if(w.has(e)&&w.get(e)!==r){a=!0;continue}m.has(e)||(m.add(e),i.push(e))}n.hunks.length===0?b.push(n.id):i.length===0&&a&&x.push(n.id),t.push({id:n.id,message:n.message,explanation:n.explanation,hunkIndices:i})}p.push({id:e.id,name:e.name,title:e.title,description:e.description,commits:t})}if(y.length>0)return i([`Duplicate commit IDs: ${y.join(`, `)}`],`Some commit IDs appear multiple times. Each must be unique.\n\nDuplicates: ${y.join(`, `)}`);if(b.length>0)return i([`Empty commits (no hunks assigned): ${b.join(`, `)}`],`Every commit must contain at least one hunk. These commits have an empty \`hunks\` array: ${b.join(`, `)}.
1
+ import{extractOutputTag as e}from"../ai-loop.js";import{validateJsonSchema as t}from"../utils/json-schema.js";import{planMissingHunksRepair as n}from"./repair.js";import{groupingResultJsonSchema as r}from"./schema.js";function i(i,s,c=`branches`,l,u){let d=null;try{if(d=e(i),!d)return a([`No <output> tag found in response`],o(c));let f=t(r,JSON.parse(d));if(!f.success)return a(f.issues.map(e=>`${e.path.join(`.`)}: ${e.message}`),o(c));let p=f.data,m=[],h=new Set,g=[],_=new Map,v=new Map,y=new Set,b=[],x=[],S=[],C=new Set,w=new Set;for(let e of p.branches){if(C.has(e.id))return a([`Duplicate branch ID: '${e.id}'`],o(c));if(C.add(e.id),c===`branches`&&e.name.length===0)return a([`Branch '${e.id}': missing or empty 'name'`],o(c));if(c===`branches`&&e.name.length>0){if(w.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(w.add(e.name),l?.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}'.`)}for(let t of e.commits){y.has(t.id)&&b.push(t.id),y.add(t.id);let n=c===`branches`?`${e.id}/${t.id}`:t.id;for(let r of t.hunks){let i=_.get(r);i?i.push(n):_.set(r,[n]);let a=v.get(r);a?a.push({branchId:e.id,commitId:t.id,label:n}):v.set(r,[{branchId:e.id,commitId:t.id,label:n}])}}}let T=new Map,E=[],D=!1;for(let[e,t]of v){if(t.length<2)continue;if(g.push(e),c===`branches`&&new Set(t.map(e=>e.branchId)).size>1){D=!0;continue}let n=t.find(e=>u?.lockedCommitIds?.has(e.commitId))??t[0];T.set(e,n.label),E.push({hunkIndex:e,keptIn:n.label,droppedFrom:t.filter(e=>e!==n).map(e=>e.label),scope:new Set(t.map(e=>e.commitId)).size===1?`within-commit`:`across-commits`})}let O=g.length>0&&!D;for(let e of p.branches){let t=[];for(let n of e.commits){let r=c===`branches`?`${e.id}/${n.id}`:n.id,i=[],a=!1;for(let e of n.hunks){if(T.has(e)&&T.get(e)!==r){a=!0;continue}h.has(e)||(h.add(e),i.push(e))}n.hunks.length===0?x.push(n.id):i.length===0&&a&&S.push(n.id),t.push({id:n.id,message:n.message,explanation:n.explanation,hunkIndices:i})}m.push({id:e.id,name:e.name,title:e.title,description:e.description,commits:t})}if(b.length>0)return a([`Duplicate commit IDs: ${b.join(`, `)}`],`Some commit IDs appear multiple times. Each must be unique.\n\nDuplicates: ${b.join(`, `)}`);if(x.length>0)return a([`Empty commits (no hunks assigned): ${x.join(`, `)}`],`Every commit must contain at least one hunk. These commits have an empty \`hunks\` array: ${x.join(`, `)}.
2
2
 
3
- Either assign at least one hunk to each of these commits, or remove them entirely. If a commit's purpose is already covered by another commit, remove the duplicate. If a commit is supposed to cover specific hunks, assign those hunk indices to it. Every hunk index still needs to appear in exactly one commit.`);let O=[...o].filter(e=>!m.has(e)).sort((e,t)=>e-t),k=[...m].filter(e=>!o.has(e)).sort((e,t)=>e-t),A=[...new Set(h)].sort((e,t)=>e-t),j=A.length>0&&!D;if(O.length>0||k.length>0||x.length>0||j){let e=[],t=[];if(A.length>0){let n=A.map(e=>` hunk ${e} → ${(g.get(e)??[]).join(`, `)}`).join(`
4
- `);e.push(`Duplicate hunks (each must appear in exactly ONE commit):\n${n}`),t.push(`Duplicate hunks: ${A.join(`, `)}`)}O.length>0&&(e.push(`Missing hunks (not assigned to any commit): ${O.join(`, `)}`),t.push(`Missing hunks: ${O.join(`, `)}`)),k.length>0&&(e.push(`Extra hunks (not present in the input): ${k.join(`, `)}`),t.push(`Extra hunks: ${k.join(`, `)}`)),x.length>0&&(e.push(`Commits whose only hunks were already claimed by an earlier commit: ${x.join(`, `)}`),t.push(`Empty commits after dropping duplicate hunks: ${x.join(`, `)}`));let n=[...o].sort((e,t)=>e-t),r=[...g.values()].reduce((e,t)=>e+t.length,0);return i(t,`There are ${n.length} hunks, with indices: ${n.join(`, `)}. Every one of these indices must appear in exactly one commit — no duplicates, no omissions, no other indices. Use the indices exactly as given; do not renumber them.
3
+ Either assign at least one hunk to each of these commits, or remove them entirely. If a commit's purpose is already covered by another commit, remove the duplicate. If a commit is supposed to cover specific hunks, assign those hunk indices to it. Every hunk index still needs to appear in exactly one commit.`);let k=[...s].filter(e=>!h.has(e)).sort((e,t)=>e-t),A=[...h].filter(e=>!s.has(e)).sort((e,t)=>e-t),j=[...new Set(g)].sort((e,t)=>e-t),M=[...E].sort((e,t)=>e.hunkIndex-t.hunkIndex),N=M.length>0?{kind:`duplicate-hunks`,hunkIndices:M.map(e=>e.hunkIndex),claims:M,detail:`Dropped duplicate claims on ${M.length} hunk(s): ${M.map(e=>`hunk ${e.hunkIndex} kept in ${e.keptIn}`).join(`; `)}`}:void 0,P=j.length>0&&!O;if(k.length>0||A.length>0||S.length>0||P){let e=[],t=[];if(j.length>0){let n=j.map(e=>` hunk ${e} → ${(_.get(e)??[]).join(`, `)}`).join(`
4
+ `);e.push(`Duplicate hunks (each must appear in exactly ONE commit):\n${n}`),t.push(`Duplicate hunks: ${j.join(`, `)}`)}k.length>0&&(e.push(`Missing hunks (not assigned to any commit): ${k.join(`, `)}`),t.push(`Missing hunks: ${k.join(`, `)}`)),A.length>0&&(e.push(`Extra hunks (not present in the input): ${A.join(`, `)}`),t.push(`Extra hunks: ${A.join(`, `)}`)),S.length>0&&(e.push(`Commits whose only hunks were already claimed by an earlier commit: ${S.join(`, `)}`),t.push(`Empty commits after dropping duplicate hunks: ${S.join(`, `)}`));let r=[...s].sort((e,t)=>e-t),i=[..._.values()].reduce((e,t)=>e+t.length,0),o=`There are ${r.length} hunks, with indices: ${r.join(`, `)}. Every one of these indices must appear in exactly one commit — no duplicates, no omissions, no other indices. Use the indices exactly as given; do not renumber them.
5
5
 
6
- Your response listed ${r} hunk references across ${g.size} unique indices. Expected: ${n.length} references across ${n.length} unique indices.
6
+ Your response listed ${i} hunk references across ${_.size} unique indices. Expected: ${r.length} references across ${r.length} unique indices.
7
7
 
8
8
  Specific problems:
9
9
  ${e.map(e=>`- ${e}`).join(`
@@ -11,7 +11,7 @@ ${e.map(e=>`- ${e}`).join(`
11
11
 
12
12
  To fix duplicates: for each duplicated hunk index, decide which one commit it truly belongs in and remove it from the others. Do not split a single hunk across two commits — git only knows how to apply a hunk once.
13
13
 
14
- Return the corrected JSON inside <output> tags.`)}if(T.length>0){let e=[...T].sort((e,t)=>e.hunkIndex-t.hunkIndex),t=e.map(e=>`hunk ${e.hunkIndex} kept in ${e.keptIn}`).join(`; `);return{valid:!0,result:{branches:p},repairs:[{kind:`duplicate-hunks`,hunkIndices:e.map(e=>e.hunkIndex),claims:e,detail:`Dropped duplicate claims on ${e.length} hunk(s): ${t}`}]}}return{valid:!0,result:{branches:p}}}catch(e){let t=e instanceof Error?e.message:String(e),n=(u??r).slice(-300);return i([`Failed to parse response as valid JSON: ${t}`,`Response tail: ${n}`],a(s))}}function i(e,t){return{valid:!1,errors:e,retryFeedback:t}}function a(e){return e===`commits`?`Your response has an invalid structure. Please return JSON inside <output> tags. Use a single branch with id "default", name "", description "":
14
+ Return the corrected JSON inside <output> tags.`;if(u?.repairContext&&k.length>0&&A.length===0&&S.length===0&&!P){let e=n({branches:m,mode:c,missingIndices:k,priorRepairs:N?[N]:void 0,context:u.repairContext});if(e)return{...a(t,o),repair:e}}return a(t,o)}return N?{valid:!0,result:{branches:m},repairs:[N]}:{valid:!0,result:{branches:m}}}catch(e){let t=e instanceof Error?e.message:String(e),n=(d??i).slice(-300);return a([`Failed to parse response as valid JSON: ${t}`,`Response tail: ${n}`],o(c))}}function a(e,t){return{valid:!1,errors:e,retryFeedback:t}}function o(e){return e===`commits`?`Your response has an invalid structure. Please return JSON inside <output> tags. Use a single branch with id "default", name "", description "":
15
15
  <output>
16
16
  {
17
17
  "branches": [
@@ -55,4 +55,4 @@ Return only the <output> tag and no other text.`:`Your response has an invalid s
55
55
  }
56
56
  </output>
57
57
 
58
- Return only the <output> tag and no other text.`}export{r as validateGroupResponse};
58
+ Return only the <output> tag and no other text.`}export{i as validateGroupResponse};
@@ -1,3 +1,4 @@
1
+ import { AuthoringContext } from "../../context.js";
1
2
  import { ComposeHunk } from "../types.js";
2
3
  import { ConventionDescriptor } from "../../git/utils/conventions.js";
3
4
  import { RedactHunkContent } from "../utils/redact.js";
@@ -12,7 +13,8 @@ declare function buildQuickComposeUserPrompt(params: {
12
13
  branchName?: string;
13
14
  baseBranch?: string;
14
15
  issueUrl?: string;
15
- pullRequestUrl?: string;
16
+ pullRequestUrl?: string; /** What the author knows that the hunks cannot show. Rendered as escaped, tagged data. */
17
+ authoring?: AuthoringContext;
16
18
  instructions?: string;
17
19
  redactHunkContent?: RedactHunkContent;
18
20
  }): string;