@gitkraken/compose-tools 0.4.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/dist/errors/task.d.ts +11 -1
  3. package/dist/errors/task.js +1 -1
  4. package/dist/git/commit-chain.js +3 -4
  5. package/dist/git/reparent.js +3 -4
  6. package/dist/git/safety-range.d.ts +14 -2
  7. package/dist/git/safety-range.js +4 -5
  8. package/dist/git/utils/conventions.d.ts +7 -0
  9. package/dist/git/utils/conventions.js +8 -9
  10. package/dist/git/utils/refs.d.ts +18 -0
  11. package/dist/git/utils/refs.js +2 -0
  12. package/dist/index.d.ts +6 -3
  13. package/dist/index.js +1 -1
  14. package/dist/tasks/analyze/analyze.js +1 -1
  15. package/dist/tasks/analyze/prompt.d.ts +4 -1
  16. package/dist/tasks/analyze/prompt.js +5 -5
  17. package/dist/tasks/analyze/schema.d.ts +127 -36
  18. package/dist/tasks/analyze/schema.js +1 -1
  19. package/dist/tasks/analyze/types.d.ts +8 -0
  20. package/dist/tasks/analyze/validator.js +2 -2
  21. package/dist/tasks/compose-group/compose-group.js +1 -1
  22. package/dist/tasks/compose-group/prompt.d.ts +4 -1
  23. package/dist/tasks/compose-group/prompt.js +23 -29
  24. package/dist/tasks/compose-group/types.d.ts +5 -0
  25. package/dist/tasks/group/group.js +1 -1
  26. package/dist/tasks/group/prompt.d.ts +4 -1
  27. package/dist/tasks/group/prompt.js +15 -21
  28. package/dist/tasks/group/schema.d.ts +89 -17
  29. package/dist/tasks/group/schema.js +1 -1
  30. package/dist/tasks/group/types.d.ts +8 -0
  31. package/dist/tasks/group/validator.js +6 -6
  32. package/dist/tasks/order/prompt.js +5 -5
  33. package/dist/tasks/order/schema.d.ts +35 -10
  34. package/dist/tasks/order/schema.js +1 -1
  35. package/dist/tasks/order/validator.js +2 -2
  36. package/dist/tasks/partition/prompt.js +9 -9
  37. package/dist/tasks/partition/schema.d.ts +46 -12
  38. package/dist/tasks/partition/schema.js +1 -1
  39. package/dist/tasks/partition/validator.js +2 -2
  40. package/dist/tasks/quick-compose/prompt.js +8 -9
  41. package/dist/tasks/quick-compose/schema.d.ts +142 -25
  42. package/dist/tasks/quick-compose/schema.js +1 -1
  43. package/dist/tasks/quick-compose/validator.js +5 -5
  44. package/dist/tasks/refine/refine.js +1 -1
  45. package/dist/tasks/refine/types.d.ts +14 -0
  46. package/dist/tasks/refine/validator.d.ts +5 -1
  47. package/dist/tasks/refine/validator.js +1 -1
  48. package/dist/tasks/utils/budget.d.ts +40 -0
  49. package/dist/tasks/utils/budget.js +1 -0
  50. package/dist/tasks/utils/json-schema.d.ts +61 -0
  51. package/dist/tasks/utils/json-schema.js +1 -0
  52. package/dist/tasks/utils/noise.d.ts +21 -0
  53. package/dist/tasks/utils/noise.js +2 -0
  54. package/dist/tasks/utils/redact.d.ts +7 -1
  55. package/dist/tasks/utils/redact.js +1 -1
  56. package/dist/workflows/compose.d.ts +47 -2
  57. package/dist/workflows/compose.js +3 -3
  58. package/dist/workflows/refine.d.ts +9 -1
  59. package/dist/workflows/refine.js +1 -1
  60. package/dist/workflows/split.js +3 -3
  61. package/dist/workflows/types.d.ts +14 -1
  62. package/package.json +2 -2
@@ -1,4 +1,4 @@
1
- import{partitionResultSchema as e}from"./schema.js";import{z as t}from"zod";const n=JSON.stringify(t.toJSONSchema(e),null,2);function r(e,t){let r=e?.branches&&e.branches.format!==`unknown`?`For each sub-branch, name it following the repository's branch naming conventions.\n\n${e.promptFragment}`:`For each sub-branch, provide a branch name derived from the source branch name (e.g., "feature/part-1-description").`;return`You are an advanced AI programming assistant tasked with determining whether an ordered list of commits on a single branch should be subdivided into stacked branches for a pull request workflow.
1
+ import{partitionResultJsonSchema as e}from"./schema.js";const t=JSON.stringify(e,null,2);function n(e,n){let i=e?.branches&&e.branches.format!==`unknown`?`For each sub-branch, name it following the repository's branch naming conventions.\n\n${e.promptFragment}`:`For each sub-branch, provide a branch name derived from the source branch name (e.g., "feature/part-1-description").`;return`You are an advanced AI programming assistant tasked with determining whether an ordered list of commits on a single branch should be subdivided into stacked branches for a pull request workflow.
2
2
 
3
3
  Your goal is to examine the commits and decide whether they form one cohesive unit or whether there are natural subdivision points that would benefit from stacked PRs.
4
4
 
@@ -9,9 +9,9 @@ Rules:
9
9
  4. A single sub-branch (no subdivision) is a perfectly valid and common outcome
10
10
  5. A single-commit sub-branch is valid if that commit is independently mergeable
11
11
 
12
- ${i(t?.splitting)}
12
+ ${r(n?.splitting)}
13
13
 
14
- ${r}
14
+ ${i}
15
15
  - A short PR-style title (under 72 characters, imperative mood)
16
16
  - A description of what the sub-branch accomplishes
17
17
  - The commit IDs it contains (in order)
@@ -21,10 +21,10 @@ ${r}
21
21
  Output your partition as JSON inside a <output> tag matching this schema:
22
22
 
23
23
  \`\`\`json
24
- ${n}
24
+ ${t}
25
25
  \`\`\`
26
26
 
27
- Return only the <output> tag and no other text.`}function i(e){let t=`Consider merge-order preference: even if sub-groups compile independently, keep them together if merging one without the other would be incomplete or confusing to reviewers.`;return e===`conservative`?`Keep commits as a single branch unless the sub-groups address completely different concerns with no merge-order preference between them.
27
+ Return only the <output> tag and no other text.`}function r(e){let t=`Consider merge-order preference: even if sub-groups compile independently, keep them together if merging one without the other would be incomplete or confusing to reviewers.`;return e===`conservative`?`Keep commits as a single branch unless the sub-groups address completely different concerns with no merge-order preference between them.
28
28
  ${t}
29
29
  Changes addressing the same pattern across different files should stay together. When in doubt, do not partition.`:e===`aggressive`?`Subdivide whenever there is a clear concern boundary between contiguous groups of commits. Each sub-group should be as focused as possible.
30
30
  ${t}
@@ -34,10 +34,10 @@ Do NOT partition just because commits are distinct. A branch with a refactor com
34
34
 
35
35
  Changes addressing the same pattern across different files should stay together. For example, applying a similar fix to multiple functions belongs in one branch, not split across multiple stacked PRs.
36
36
 
37
- ${t}`}function a(e){let{commits:t,orderedCommitIds:n,sourceBranchName:r,baseBranch:i,instructions:a}=e,o=n.map(e=>{let n=t.find(t=>t.id===e);return n?{id:n.id,message:n.message,explanation:n.explanation}:{id:e,message:`unknown`,explanation:``}}),s=`Determine whether these ordered commits should be subdivided into stacked branches:\n\n<commits>\n${JSON.stringify(o)}\n</commits>`,c=[];return r&&c.push(`Source branch: ${r}`),i&&c.push(`Base branch: ${i}`),c.length>0&&(s+=`\n\n<context>\n${c.join(`
37
+ ${t}`}function i(e){let{commits:t,orderedCommitIds:n,sourceBranchName:r,baseBranch:i,instructions:a}=e,o=n.map(e=>{let n=t.find(t=>t.id===e);return n?{id:n.id,message:n.message,explanation:n.explanation}:{id:e,message:`unknown`,explanation:``}}),s=`Determine whether these ordered commits should be subdivided into stacked branches:\n\n<commits>\n${JSON.stringify(o)}\n</commits>`,c=[];return r&&c.push(`Source branch: ${r}`),i&&c.push(`Base branch: ${i}`),c.length>0&&(s+=`\n\n<context>\n${c.join(`
38
38
  `)}\n</context>`),a&&(s+=`\n\nAdditional instructions:\n${a}`),s+=`
39
39
 
40
- Return the partition. Return only the <output> tag and no other text.`,s}function o(e){let t=s(e.strategy);return e.sourceBranchName&&(t+=`\n\nSource branch: ${e.sourceBranchName}`),e.baseBranch&&(t+=`\nBase branch: ${e.baseBranch}`),e.conventions?.promptFragment&&(t+=`\n\n${e.conventions.promptFragment}`),e.instructions&&(t+=`\n\nAdditional instructions:\n${e.instructions}`),t+=`
40
+ Return the partition. Return only the <output> tag and no other text.`,s}function a(e){let t=o(e.strategy);return e.sourceBranchName&&(t+=`\n\nSource branch: ${e.sourceBranchName}`),e.baseBranch&&(t+=`\nBase branch: ${e.baseBranch}`),e.conventions?.promptFragment&&(t+=`\n\n${e.conventions.promptFragment}`),e.instructions&&(t+=`\n\nAdditional instructions:\n${e.instructions}`),t+=`
41
41
 
42
42
  Return the partition as JSON inside <output> tags:
43
43
  <output>
@@ -49,7 +49,7 @@ Return the partition as JSON inside <output> tags:
49
49
  }
50
50
  </output>
51
51
 
52
- Return only the <output> tag and no other text.`,t}function s(e){let t=e?.splitting??`balanced`,n=`Based on the ordered commits for this branch, determine whether they should be subdivided into stacked sub-branches. Each sub-branch must be a contiguous range of commits.`,r=`Consider merge-order preference: even if sub-groups compile independently, keep them together if merging one without the other would be incomplete or confusing.`;return t===`conservative`?`${n}
52
+ Return only the <output> tag and no other text.`,t}function o(e){let t=e?.splitting??`balanced`,n=`Based on the ordered commits for this branch, determine whether they should be subdivided into stacked sub-branches. Each sub-branch must be a contiguous range of commits.`,r=`Consider merge-order preference: even if sub-groups compile independently, keep them together if merging one without the other would be incomplete or confusing.`;return t===`conservative`?`${n}
53
53
 
54
54
  Keep as a single branch unless sub-groups address completely different concerns with no merge-order preference. ${r}
55
55
  When in doubt, do not partition.`:t===`aggressive`?`${n}
@@ -58,4 +58,4 @@ Subdivide whenever there is a clear concern boundary. Each stacked PR should tel
58
58
 
59
59
  Only subdivide when the sub-groups are independently reviewable AND independently mergeable, and a reviewer would genuinely benefit from separate PRs. Do NOT partition just because commits are distinct — a refactor followed by a feature using it is a coherent single PR.
60
60
 
61
- ${r}`}export{o as buildPartitionContinuationPrompt,r as buildPartitionSystemPrompt,a as buildPartitionUserPrompt};
61
+ ${r}`}export{a as buildPartitionContinuationPrompt,n as buildPartitionSystemPrompt,i as buildPartitionUserPrompt};
@@ -1,14 +1,48 @@
1
- import { z } from "zod";
2
-
3
1
  //#region src/tasks/partition/schema.d.ts
4
- declare const partitionResultSchema: z.ZodObject<{
5
- partitions: z.ZodArray<z.ZodObject<{
6
- branchName: z.ZodString;
7
- title: z.ZodString;
8
- description: z.ZodString;
9
- commitIds: z.ZodArray<z.ZodString>;
10
- }, z.core.$strip>>;
11
- rationale: z.ZodString;
12
- }, z.core.$strip>;
2
+ declare const partitionResultJsonSchema: {
3
+ readonly $schema: "https://json-schema.org/draft/2020-12/schema";
4
+ readonly type: "object";
5
+ readonly properties: {
6
+ readonly partitions: {
7
+ readonly minItems: 1;
8
+ readonly type: "array";
9
+ readonly items: {
10
+ readonly type: "object";
11
+ readonly properties: {
12
+ readonly branchName: {
13
+ readonly type: "string";
14
+ readonly minLength: 1;
15
+ readonly description: "Valid git branch name";
16
+ };
17
+ readonly title: {
18
+ readonly type: "string";
19
+ readonly description: "Short PR-style title for this sub-branch (under 72 chars, imperative mood, e.g. 'Refactor authentication middleware')";
20
+ };
21
+ readonly description: {
22
+ readonly type: "string";
23
+ readonly description: "What this sub-branch accomplishes";
24
+ };
25
+ readonly commitIds: {
26
+ readonly minItems: 1;
27
+ readonly type: "array";
28
+ readonly items: {
29
+ readonly type: "string";
30
+ };
31
+ readonly description: "Commit IDs in this partition (contiguous range)";
32
+ };
33
+ };
34
+ readonly required: readonly ["branchName", "title", "description", "commitIds"];
35
+ readonly additionalProperties: false;
36
+ };
37
+ readonly description: "Contiguous partitions of commits into stacked branches";
38
+ };
39
+ readonly rationale: {
40
+ readonly type: "string";
41
+ readonly description: "Explanation of why this partition was chosen";
42
+ };
43
+ };
44
+ readonly required: readonly ["partitions", "rationale"];
45
+ readonly additionalProperties: false;
46
+ };
13
47
  //#endregion
14
- export { partitionResultSchema };
48
+ export { partitionResultJsonSchema };
@@ -1 +1 @@
1
- import{z as e}from"zod";const t=e.object({branchName:e.string().min(1).describe(`Valid git branch name`),title:e.string().describe(`Short PR-style title for this sub-branch (under 72 chars, imperative mood, e.g. 'Refactor authentication middleware')`),description:e.string().describe(`What this sub-branch accomplishes`),commitIds:e.array(e.string()).min(1).describe(`Commit IDs in this partition (contiguous range)`)}),n=e.object({partitions:e.array(t).min(1).describe(`Contiguous partitions of commits into stacked branches`),rationale:e.string().describe(`Explanation of why this partition was chosen`)});export{n as partitionResultSchema};
1
+ const e={$schema:`https://json-schema.org/draft/2020-12/schema`,type:`object`,properties:{partitions:{minItems:1,type:`array`,items:{type:`object`,properties:{branchName:{type:`string`,minLength:1,description:`Valid git branch name`},title:{type:`string`,description:`Short PR-style title for this sub-branch (under 72 chars, imperative mood, e.g. 'Refactor authentication middleware')`},description:{type:`string`,description:`What this sub-branch accomplishes`},commitIds:{minItems:1,type:`array`,items:{type:`string`},description:`Commit IDs in this partition (contiguous range)`}},required:[`branchName`,`title`,`description`,`commitIds`],additionalProperties:!1},description:`Contiguous partitions of commits into stacked branches`},rationale:{type:`string`,description:`Explanation of why this partition was chosen`}},required:[`partitions`,`rationale`],additionalProperties:!1};export{e as partitionResultJsonSchema};
@@ -1,4 +1,4 @@
1
- import{extractOutputTag as e}from"../ai-loop.js";import{partitionResultSchema as t}from"./schema.js";function n(n,s,c){let l=null;try{if(l=e(n),!l)return a([`No <output> tag found in response`],o());let u=JSON.parse(l),d=t.safeParse(u);if(!d.success)return a(d.error.issues.map(e=>`${e.path.join(`.`)}: ${e.message}`),o());let f=d.data,p=[],m=[],h=new Set;for(let e=0;e<f.partitions.length;e++){let t=f.partitions[e];if(!i(t.branchName))return a([`Partition ${e}: '${t.branchName}' is not a valid git branch name`],`The branch name '${t.branchName}' is invalid. Git branch names cannot contain spaces, '..', '~', '^', ':', '\\', or end with '.lock'. Please provide a valid name.`);if(h.has(t.branchName))return a([`Duplicate partition branch name: '${t.branchName}'`],`Multiple partitions use the branch name '${t.branchName}'. Each partition must have a unique branch name. Please rename one of them.`);if(h.add(t.branchName),c?.has(t.branchName))return a([`Branch name '${t.branchName}' already exists in the repository`],`The branch name '${t.branchName}' already exists in the repository. Please choose a different name for partition ${e}.`);for(let e of t.commitIds)m.push(e);p.push({branchName:t.branchName,title:t.title,description:t.description,commitIds:t.commitIds})}let g=new Set,_=[];for(let e of m)g.has(e)&&_.push(e),g.add(e);if(_.length>0)return a([`Duplicate commit IDs: ${_.join(`, `)}`],`Some commits appear in multiple partitions. Each must be in exactly one.\n\nDuplicates: ${_.join(`, `)}`);let v=new Set(s),y=s.filter(e=>!g.has(e)),b=m.filter(e=>!v.has(e));return y.length>0?a([`Missing commits: ${y.join(`, `)}`],`Some commits are not assigned to any partition.\n\nMissing: ${y.join(`, `)}`):b.length>0?a([`Unknown commits: ${b.join(`, `)}`],`Some commit IDs don't match any known commit.\n\nUnknown: ${b.join(`, `)}`):r(p,s)?{valid:!0,result:{partitions:p,rationale:f.rationale}}:a([`Partitions are not contiguous`],`Each partition must contain a contiguous range of commits in the provided order. Please regroup into contiguous ranges.`)}catch(e){let t=e instanceof Error?e.message:String(e),r=(l??n).slice(-300);return a([`Failed to parse response as valid JSON: ${t}`,`Response tail: ${r}`],o())}}function r(e,t){let n=new Map(t.map((e,t)=>[e,t])),r=0;for(let t of e)for(let e of t.commitIds){let t=n.get(e);if(t===void 0||t!==r)return!1;r++}return r===t.length}function i(e){return e.startsWith(`-`)||e.startsWith(`.`)||e.endsWith(`.lock`)||e.endsWith(`.`)||e.endsWith(`/`)||e.includes(`..`)||e.includes(`~`)||e.includes(`^`)||e.includes(`:`)||e.includes(`\\`)||e.includes(` `)||e.includes(`?`)||e.includes(`*`)||e.includes(`[`)||e.includes(`@{`)?!1:e.length>0}function a(e,t){return{valid:!1,errors:e,retryFeedback:t}}function o(){return`Your response has an invalid structure. Return JSON inside <output> tags:
1
+ import{extractOutputTag as e}from"../ai-loop.js";import{validateJsonSchema as t}from"../utils/json-schema.js";import{partitionResultJsonSchema as n}from"./schema.js";function r(r,c,l){let u=null;try{if(u=e(r),!u)return o([`No <output> tag found in response`],s());let d=t(n,JSON.parse(u));if(!d.success)return o(d.issues.map(e=>`${e.path.join(`.`)}: ${e.message}`),s());let f=d.data,p=[],m=[],h=new Set;for(let e=0;e<f.partitions.length;e++){let t=f.partitions[e];if(!a(t.branchName))return o([`Partition ${e}: '${t.branchName}' is not a valid git branch name`],`The branch name '${t.branchName}' is invalid. Git branch names cannot contain spaces, '..', '~', '^', ':', '\\', or end with '.lock'. Please provide a valid name.`);if(h.has(t.branchName))return o([`Duplicate partition branch name: '${t.branchName}'`],`Multiple partitions use the branch name '${t.branchName}'. Each partition must have a unique branch name. Please rename one of them.`);if(h.add(t.branchName),l?.has(t.branchName))return o([`Branch name '${t.branchName}' already exists in the repository`],`The branch name '${t.branchName}' already exists in the repository. Please choose a different name for partition ${e}.`);for(let e of t.commitIds)m.push(e);p.push({branchName:t.branchName,title:t.title,description:t.description,commitIds:t.commitIds})}let g=new Set,_=[];for(let e of m)g.has(e)&&_.push(e),g.add(e);if(_.length>0)return o([`Duplicate commit IDs: ${_.join(`, `)}`],`Some commits appear in multiple partitions. Each must be in exactly one.\n\nDuplicates: ${_.join(`, `)}`);let v=new Set(c),y=c.filter(e=>!g.has(e)),b=m.filter(e=>!v.has(e));return y.length>0?o([`Missing commits: ${y.join(`, `)}`],`Some commits are not assigned to any partition.\n\nMissing: ${y.join(`, `)}`):b.length>0?o([`Unknown commits: ${b.join(`, `)}`],`Some commit IDs don't match any known commit.\n\nUnknown: ${b.join(`, `)}`):i(p,c)?{valid:!0,result:{partitions:p,rationale:f.rationale}}:o([`Partitions are not contiguous`],`Each partition must contain a contiguous range of commits in the provided order. Please regroup into contiguous ranges.`)}catch(e){let t=e instanceof Error?e.message:String(e),n=(u??r).slice(-300);return o([`Failed to parse response as valid JSON: ${t}`,`Response tail: ${n}`],s())}}function i(e,t){let n=new Map(t.map((e,t)=>[e,t])),r=0;for(let t of e)for(let e of t.commitIds){let t=n.get(e);if(t===void 0||t!==r)return!1;r++}return r===t.length}function a(e){return e.startsWith(`-`)||e.startsWith(`.`)||e.endsWith(`.lock`)||e.endsWith(`.`)||e.endsWith(`/`)||e.includes(`..`)||e.includes(`~`)||e.includes(`^`)||e.includes(`:`)||e.includes(`\\`)||e.includes(` `)||e.includes(`?`)||e.includes(`*`)||e.includes(`[`)||e.includes(`@{`)?!1:e.length>0}function o(e,t){return{valid:!1,errors:e,retryFeedback:t}}function s(){return`Your response has an invalid structure. Return JSON inside <output> tags:
2
2
  <output>
3
3
  {
4
4
  "partitions": [
@@ -8,4 +8,4 @@ import{extractOutputTag as e}from"../ai-loop.js";import{partitionResultSchema as
8
8
  }
9
9
  </output>
10
10
 
11
- Return only the <output> tag and no other text.`}export{n as validatePartitionResponse};
11
+ Return only the <output> tag and no other text.`}export{r as validatePartitionResponse};
@@ -1,8 +1,7 @@
1
- import{hunkContentForPrompt as e}from"../utils/redact.js";import{quickComposeResultSchema as t}from"./schema.js";import{z as n}from"zod";const r=JSON.stringify(n.toJSONSchema(t),null,2);function i(e,t=`branches`,n,i=!1){let a=e?.promptFragment?`Write commit messages following the repository's conventions:\n\n${e.promptFragment}`:`Write commit messages:
2
- Each commit message must contain both a summary and a description, separated by a blank line:
1
+ import{hunkContentForPrompt as e}from"../utils/redact.js";import{quickComposeResultJsonSchema as t}from"./schema.js";const n=JSON.stringify(t,null,2);function r(e,t=`branches`,r,i=!1){let c=`Write commit messages:\n${e?.commits.bodyStyle===`minimal`?`Each commit message is a single subject line with no body: concise, 50 chars max, third-person singular present tense ("Fixes", "Adds", "Improves"). This repository's history overwhelmingly uses one-line commit messages, so do not add a description.`:`Each commit message must contain both a summary and a description, separated by a blank line:
3
2
  - **Summary** (first line): concise, 50 chars max, third-person singular present tense ("Fixes", "Adds", "Improves"). Stands alone as the commit subject.
4
3
  - **Blank line**: mandatory separator.
5
- - **Description** (body): one or more lines explaining *why* the change was made, not *what* it does. Focus on motivation, trade-offs, or context a reviewer needs. For non-trivial changes, always include a description.`,l=o(n?.commitGranularity),u=t===`branches`?s(n?.branchSplitting,e):``,d=i?c():``;return`You are a fast AI code organizer. Given a set of code diff hunks, organize them into well-structured commits${t===`branches`?` and branches`:``} in a single pass.
4
+ - **Description** (body): one or more lines explaining *why* the change was made, not *what* it does. Focus on motivation, trade-offs, or context a reviewer needs. For non-trivial changes, always include a description.`}${e?.promptFragment?`\n\n${e.promptFragment}`:``}`,l=a(r?.commitGranularity),u=t===`branches`?o(r?.branchSplitting,e):``,d=i?s():``;return`You are a fast AI code organizer. Given a set of code diff hunks, organize them into well-structured commits${t===`branches`?` and branches`:``} in a single pass.
6
5
 
7
6
  ## Your task
8
7
 
@@ -13,7 +12,7 @@ ${t===`branches`?`4. Assign commits to branches — each branch is a cohesive un
13
12
  ${i?`5. If a branch should be split into stacked sub-branches for review, provide partitions.`:``}
14
13
 
15
14
  ## Commit style
16
- ${a}
15
+ ${c}
17
16
  ${l}
18
17
  ${u}
19
18
  ${d}
@@ -36,21 +35,21 @@ Before producing the \`<output>\` tag, do a self-check: count the total number o
36
35
  Output as JSON inside a <output> tag matching this schema:
37
36
 
38
37
  \`\`\`json
39
- ${r}
38
+ ${n}
40
39
  \`\`\`
41
40
 
42
- Return only the <output> tag and no other text.`}function a(t){let{hunks:n,commitMessages:r,commitOrder:i,branchName:a,baseBranch:o,issueUrl:s,pullRequestUrl:c,instructions:l,redactHunkContent:u}=t,d=`Organize these code hunks into commits:
41
+ Return only the <output> tag and no other text.`}function i(t){let{hunks:n,commitMessages:r,commitOrder:i,branchName:a,baseBranch:o,issueUrl:s,pullRequestUrl:c,instructions:l,redactHunkContent:u}=t,d=`Organize these code hunks into commits:
43
42
 
44
43
  <hunks>
45
44
  ${JSON.stringify(n.map(t=>({index:t.index,fileName:t.fileName,hunkHeader:t.hunkHeader,content:e(t,u),...t.isBinary?{isBinary:!0}:{},...t.isRename?{isRename:!0}:{},...t.originalFileName?{originalFileName:t.originalFileName}:{}})))}
46
45
  </hunks>`,f=[];return a&&f.push(`Branch: ${a}`),o&&f.push(`Base: ${o}`),s&&f.push(`Issue: ${s}`),c&&f.push(`PR: ${c}`),f.length>0&&(d+=`\n\nContext:\n${f.join(`
47
46
  `)}`),r?.length&&(d+=`\n\nOriginal commit messages (match their style):\n<commit_messages>\n${JSON.stringify(r)}\n</commit_messages>`),i?.length&&(d+=`\n\nOriginal commit order:\n${JSON.stringify(i)}`),l&&(d+=`\n\nAdditional instructions:\n${l}`),d+=`
48
47
 
49
- Return only the <output> tag and no other text.`,d}function o(e){return e===`consolidated`?`
48
+ Return only the <output> tag and no other text.`,d}function a(e){return e===`consolidated`?`
50
49
  Prefer fewer, larger commits. Combine related changes into a single commit.`:e===`granular`?`
51
- Prefer more, smaller commits. Each distinct change gets its own commit.`:``}function s(e,t){let n=`\n## Branch guidelines\n- Each branch has a descriptive name, a short PR title (under 72 chars), and at least one commit.\n- ${t?.promptFragment?`Follow the repository's branch naming conventions.`:`Give each branch a descriptive name.`}`;return e===`conservative`?`${n}\n- Default to a single branch. Only split when concerns are completely unrelated.`:e===`aggressive`?`${n}\n- Create separate branches for each distinct concern. Maximize focus per PR.`:`${n}\n- Split when concerns are genuinely independent. Keep together when they form a coherent story.`}function c(){return`
50
+ Prefer more, smaller commits. Each distinct change gets its own commit.`:``}function o(e,t){let n=`\n## Branch guidelines\n- Each branch has a descriptive name, a short PR title (under 72 chars), and at least one commit.\n- ${t?.promptFragment?`Follow the repository's branch naming conventions.`:`Give each branch a descriptive name.`}`;return e===`conservative`?`${n}\n- Default to a single branch. Only split when concerns are completely unrelated.`:e===`aggressive`?`${n}\n- Create separate branches for each distinct concern. Maximize focus per PR.`:`${n}\n- Split when concerns are genuinely independent. Keep together when they form a coherent story.`}function s(){return`
52
51
  ## Partition guidelines (stacked sub-branches)
53
52
  - If a branch has multiple distinct concerns, split it into partitions for stacked review.
54
53
  - Each partition is a contiguous range of commits from orderedCommitIds.
55
54
  - Each partition gets a unique, valid git branch name.
56
- - Single partition (no split) is a valid outcome.`}export{i as buildQuickComposeSystemPrompt,a as buildQuickComposeUserPrompt};
55
+ - Single partition (no split) is a valid outcome.`}export{r as buildQuickComposeSystemPrompt,i as buildQuickComposeUserPrompt};
@@ -1,27 +1,144 @@
1
- import { z } from "zod";
2
-
3
1
  //#region src/tasks/quick-compose/schema.d.ts
4
- declare const quickComposeResultSchema: z.ZodObject<{
5
- branches: z.ZodArray<z.ZodObject<{
6
- id: z.ZodString;
7
- name: z.ZodString;
8
- title: z.ZodString;
9
- description: z.ZodString;
10
- commits: z.ZodArray<z.ZodObject<{
11
- id: z.ZodString;
12
- message: z.ZodString;
13
- explanation: z.ZodString;
14
- hunks: z.ZodArray<z.ZodNumber>;
15
- }, z.core.$strip>>;
16
- orderedCommitIds: z.ZodArray<z.ZodString>;
17
- partitions: z.ZodOptional<z.ZodArray<z.ZodObject<{
18
- branchName: z.ZodString;
19
- title: z.ZodString;
20
- description: z.ZodString;
21
- commitIds: z.ZodArray<z.ZodString>;
22
- }, z.core.$strip>>>;
23
- }, z.core.$strip>>;
24
- rationale: z.ZodString;
25
- }, z.core.$strip>;
2
+ /** Shape of the LLM JSON output before the validator maps it to public types (`hunks` → `hunkIndices`). */
3
+ interface QuickComposeWire {
4
+ branches: QuickComposeWireBranch[];
5
+ rationale: string;
6
+ }
7
+ interface QuickComposeWireBranch {
8
+ id: string;
9
+ name: string;
10
+ title: string;
11
+ description: string;
12
+ commits: QuickComposeWireCommit[];
13
+ orderedCommitIds: string[];
14
+ partitions?: QuickComposeWirePartition[];
15
+ }
16
+ interface QuickComposeWireCommit {
17
+ id: string;
18
+ message: string;
19
+ explanation: string;
20
+ hunks: number[];
21
+ }
22
+ interface QuickComposeWirePartition {
23
+ branchName: string;
24
+ title: string;
25
+ description: string;
26
+ commitIds: string[];
27
+ }
28
+ declare const quickComposeResultJsonSchema: {
29
+ readonly $schema: "https://json-schema.org/draft/2020-12/schema";
30
+ readonly type: "object";
31
+ readonly properties: {
32
+ readonly branches: {
33
+ readonly minItems: 1;
34
+ readonly type: "array";
35
+ readonly items: {
36
+ readonly type: "object";
37
+ readonly properties: {
38
+ readonly id: {
39
+ readonly type: "string";
40
+ readonly minLength: 1;
41
+ readonly description: "Unique branch ID";
42
+ };
43
+ readonly name: {
44
+ readonly type: "string";
45
+ readonly description: "Branch name (empty string for commits-only mode)";
46
+ };
47
+ readonly title: {
48
+ readonly type: "string";
49
+ readonly description: "Short PR-style title (under 72 chars, imperative mood)";
50
+ };
51
+ readonly description: {
52
+ readonly type: "string";
53
+ readonly description: "What this branch accomplishes";
54
+ };
55
+ readonly commits: {
56
+ readonly minItems: 1;
57
+ readonly type: "array";
58
+ readonly items: {
59
+ readonly type: "object";
60
+ readonly properties: {
61
+ readonly id: {
62
+ readonly type: "string";
63
+ readonly description: "Unique commit ID (e.g. commit-1)";
64
+ };
65
+ readonly message: {
66
+ readonly type: "string";
67
+ readonly description: "Commit message";
68
+ };
69
+ readonly explanation: {
70
+ readonly type: "string";
71
+ readonly description: "Detailed explanation of the commit";
72
+ };
73
+ readonly hunks: {
74
+ readonly type: "array";
75
+ readonly items: {
76
+ readonly type: "integer";
77
+ readonly minimum: -9007199254740991;
78
+ readonly maximum: 9007199254740991;
79
+ readonly description: "Hunk index";
80
+ };
81
+ readonly description: "Hunk indices included in this commit";
82
+ };
83
+ };
84
+ readonly required: readonly ["id", "message", "explanation", "hunks"];
85
+ readonly additionalProperties: false;
86
+ };
87
+ readonly description: "Commits in this branch";
88
+ };
89
+ readonly orderedCommitIds: {
90
+ readonly minItems: 1;
91
+ readonly type: "array";
92
+ readonly items: {
93
+ readonly type: "string";
94
+ };
95
+ readonly description: "Commit IDs in optimal order";
96
+ };
97
+ readonly partitions: {
98
+ readonly description: "Stacked sub-branches (only for multi-branch mode)";
99
+ readonly type: "array";
100
+ readonly items: {
101
+ readonly type: "object";
102
+ readonly properties: {
103
+ readonly branchName: {
104
+ readonly type: "string";
105
+ readonly minLength: 1;
106
+ readonly description: "Git branch name for this partition";
107
+ };
108
+ readonly title: {
109
+ readonly type: "string";
110
+ readonly description: "Short PR-style title";
111
+ };
112
+ readonly description: {
113
+ readonly type: "string";
114
+ readonly description: "What this partition accomplishes";
115
+ };
116
+ readonly commitIds: {
117
+ readonly minItems: 1;
118
+ readonly type: "array";
119
+ readonly items: {
120
+ readonly type: "string";
121
+ };
122
+ readonly description: "Ordered commit IDs in this partition";
123
+ };
124
+ };
125
+ readonly required: readonly ["branchName", "title", "description", "commitIds"];
126
+ readonly additionalProperties: false;
127
+ };
128
+ };
129
+ };
130
+ readonly required: readonly ["id", "name", "title", "description", "commits", "orderedCommitIds"];
131
+ readonly additionalProperties: false;
132
+ };
133
+ readonly description: "Branches with grouped, ordered commits";
134
+ };
135
+ readonly rationale: {
136
+ readonly type: "string";
137
+ readonly description: "Brief explanation of the grouping and ordering rationale";
138
+ };
139
+ };
140
+ readonly required: readonly ["branches", "rationale"];
141
+ readonly additionalProperties: false;
142
+ };
26
143
  //#endregion
27
- export { quickComposeResultSchema };
144
+ export { QuickComposeWire, QuickComposeWireBranch, QuickComposeWireCommit, QuickComposeWirePartition, quickComposeResultJsonSchema };
@@ -1 +1 @@
1
- import{z as e}from"zod";const t=e.object({id:e.string().describe(`Unique commit ID (e.g. commit-1)`),message:e.string().describe(`Commit message`),explanation:e.string().describe(`Detailed explanation of the commit`),hunks:e.array(e.number().int().describe(`Hunk index`)).describe(`Hunk indices included in this commit`)}),n=e.object({branchName:e.string().min(1).describe(`Git branch name for this partition`),title:e.string().describe(`Short PR-style title`),description:e.string().describe(`What this partition accomplishes`),commitIds:e.array(e.string()).min(1).describe(`Ordered commit IDs in this partition`)}),r=e.object({id:e.string().min(1).describe(`Unique branch ID`),name:e.string().describe(`Branch name (empty string for commits-only mode)`),title:e.string().describe(`Short PR-style title (under 72 chars, imperative mood)`),description:e.string().describe(`What this branch accomplishes`),commits:e.array(t).min(1).describe(`Commits in this branch`),orderedCommitIds:e.array(e.string()).min(1).describe(`Commit IDs in optimal order`),partitions:e.array(n).optional().describe(`Stacked sub-branches (only for multi-branch mode)`)}),i=e.object({branches:e.array(r).min(1).describe(`Branches with grouped, ordered commits`),rationale:e.string().describe(`Brief explanation of the grouping and ordering rationale`)});export{i as quickComposeResultSchema};
1
+ const e={$schema:`https://json-schema.org/draft/2020-12/schema`,type:`object`,properties:{branches:{minItems:1,type:`array`,items:{type:`object`,properties:{id:{type:`string`,minLength:1,description:`Unique branch ID`},name:{type:`string`,description:`Branch name (empty string for commits-only mode)`},title:{type:`string`,description:`Short PR-style title (under 72 chars, imperative mood)`},description:{type:`string`,description:`What this branch accomplishes`},commits:{minItems:1,type:`array`,items:{type:`object`,properties:{id:{type:`string`,description:`Unique commit ID (e.g. commit-1)`},message:{type:`string`,description:`Commit message`},explanation:{type:`string`,description:`Detailed explanation of the commit`},hunks:{type:`array`,items:{type:`integer`,minimum:-9007199254740991,maximum:9007199254740991,description:`Hunk index`},description:`Hunk indices included in this commit`}},required:[`id`,`message`,`explanation`,`hunks`],additionalProperties:!1},description:`Commits in this branch`},orderedCommitIds:{minItems:1,type:`array`,items:{type:`string`},description:`Commit IDs in optimal order`},partitions:{description:`Stacked sub-branches (only for multi-branch mode)`,type:`array`,items:{type:`object`,properties:{branchName:{type:`string`,minLength:1,description:`Git branch name for this partition`},title:{type:`string`,description:`Short PR-style title`},description:{type:`string`,description:`What this partition accomplishes`},commitIds:{minItems:1,type:`array`,items:{type:`string`},description:`Ordered commit IDs in this partition`}},required:[`branchName`,`title`,`description`,`commitIds`],additionalProperties:!1}}},required:[`id`,`name`,`title`,`description`,`commits`,`orderedCommitIds`],additionalProperties:!1},description:`Branches with grouped, ordered commits`},rationale:{type:`string`,description:`Brief explanation of the grouping and ordering rationale`}},required:[`branches`,`rationale`],additionalProperties:!1};export{e as quickComposeResultJsonSchema};
@@ -1,14 +1,14 @@
1
- import{extractOutputTag as e}from"../ai-loop.js";import{quickComposeResultSchema as t}from"./schema.js";function n(n,s,c=`branches`,l=!1,u){let d=null;try{if(d=e(n),!d)return a([`No <output> tag found in response`],o(c,l));let f=JSON.parse(d),p=t.safeParse(f);if(!p.success)return a(p.error.issues.map(e=>`${e.path.join(`.`)}: ${e.message}`),o(c,l));let m=p.data,h=new Set,g=new Set,_=new Set,v=[],y=new Map,b=new Set,x=[],S=[],C=[];for(let e of m.branches){if(h.has(e.id))return a([`Duplicate branch ID: '${e.id}'`],o(c,l));if(h.add(e.id),c===`branches`&&e.name.length===0)return a([`Branch '${e.id}': missing or empty 'name'`],o(c,l));if(c===`branches`&&e.name.length>0){if(g.has(e.name))return a([`Duplicate branch name: '${e.name}'`],`Multiple branches use the name '${e.name}'. Each branch must have a unique name. Please rename one of them.`);if(g.add(e.name),u?.has(e.name))return a([`Branch name '${e.name}' already exists in the repository`],`The branch name '${e.name}' already exists in the repository. Please choose a different name for branch '${e.id}'.`)}let t=[],n=new Set;for(let r of e.commits){b.has(r.id)&&x.push(r.id),b.add(r.id),n.add(r.id);let i=[],a=c===`branches`?`${e.id}/${r.id}`:r.id;for(let e of r.hunks){_.has(e)&&v.push(e),_.add(e),i.push(e);let t=y.get(e);t?t.push(a):y.set(e,[a])}i.length===0&&S.push(r.id),t.push({id:r.id,message:r.message,explanation:r.explanation,hunkIndices:i})}let s=new Set(e.orderedCommitIds);if(s.size!==e.orderedCommitIds.length)return a([`Branch '${e.id}': orderedCommitIds contains duplicates`],`The orderedCommitIds list for branch '${e.id}' contains duplicates. Each commit ID must appear exactly once.`);let d=[...n].filter(e=>!s.has(e)),f=e.orderedCommitIds.filter(e=>!n.has(e));if(d.length>0)return a([`Branch '${e.id}': orderedCommitIds is missing commits: ${d.join(`, `)}`],`The orderedCommitIds for branch '${e.id}' must include every commit. Missing: ${d.join(`, `)}`);if(f.length>0)return a([`Branch '${e.id}': orderedCommitIds references unknown commits: ${f.join(`, `)}`],`The orderedCommitIds for branch '${e.id}' references commits that don't exist. Unknown: ${f.join(`, `)}`);let p=e.partitions?[...e.partitions]:void 0;if(p){let t=new Set,n=[];for(let r=0;r<p.length;r++){let o=p[r];if(!i(o.branchName))return a([`Branch '${e.id}', partition ${r}: '${o.branchName}' is not a valid git branch name`],`The branch name '${o.branchName}' is invalid. Git branch names cannot contain spaces, '..', '~', '^', ':', '\\', or end with '.lock'. Please provide a valid name.`);if(t.has(o.branchName))return a([`Branch '${e.id}': duplicate partition name '${o.branchName}'`],`Multiple partitions use the branch name '${o.branchName}'. Each must be unique.`);if(t.add(o.branchName),u?.has(o.branchName))return a([`Partition branch name '${o.branchName}' already exists in the repository`],`The branch name '${o.branchName}' already exists in the repository. Please choose a different name.`);for(let e of o.commitIds)n.push(e)}let o=new Set(n),c=e.orderedCommitIds.filter(e=>!o.has(e)),l=n.filter(e=>!s.has(e));if(c.length>0)return a([`Branch '${e.id}': partition is missing commits: ${c.join(`, `)}`],`All commits must be assigned to a partition. Missing: ${c.join(`, `)}`);if(l.length>0)return a([`Branch '${e.id}': partition references unknown commits: ${l.join(`, `)}`],`Partition references unknown commits: ${l.join(`, `)}`);if(!r(p,e.orderedCommitIds))return a([`Branch '${e.id}': partitions are not contiguous`],`Each partition must contain a contiguous range of commits in the ordered list. Please regroup into contiguous ranges.`)}C.push({id:e.id,name:e.name,title:e.title,description:e.description,commits:t,orderedCommitIds:e.orderedCommitIds,partitions:p})}if(x.length>0)return a([`Duplicate commit IDs: ${x.join(`, `)}`],`Some commit IDs appear multiple times. Each must be unique.\n\nDuplicates: ${x.join(`, `)}`);if(S.length>0)return a([`Empty commits (no hunks assigned): ${S.join(`, `)}`],`Every commit must contain at least one hunk. These commits have an empty \`hunks\` array: ${S.join(`, `)}.
1
+ import{extractOutputTag as e}from"../ai-loop.js";import{validateJsonSchema as t}from"../utils/json-schema.js";import{quickComposeResultJsonSchema as n}from"./schema.js";function r(r,c,l=`branches`,u=!1,d){let f=null;try{if(f=e(r),!f)return o([`No <output> tag found in response`],s(l,u));let p=t(n,JSON.parse(f));if(!p.success)return o(p.issues.map(e=>`${e.path.join(`.`)}: ${e.message}`),s(l,u));let m=p.data,h=new Set,g=new Set,_=new Set,v=[],y=new Map,b=new Set,x=[],S=[],C=[];for(let e of m.branches){if(h.has(e.id))return o([`Duplicate branch ID: '${e.id}'`],s(l,u));if(h.add(e.id),l===`branches`&&e.name.length===0)return o([`Branch '${e.id}': missing or empty 'name'`],s(l,u));if(l===`branches`&&e.name.length>0){if(g.has(e.name))return o([`Duplicate branch name: '${e.name}'`],`Multiple branches use the name '${e.name}'. Each branch must have a unique name. Please rename one of them.`);if(g.add(e.name),d?.has(e.name))return o([`Branch name '${e.name}' already exists in the repository`],`The branch name '${e.name}' already exists in the repository. Please choose a different name for branch '${e.id}'.`)}let t=[],n=new Set;for(let r of e.commits){b.has(r.id)&&x.push(r.id),b.add(r.id),n.add(r.id);let i=[],a=l===`branches`?`${e.id}/${r.id}`:r.id;for(let e of r.hunks){_.has(e)&&v.push(e),_.add(e),i.push(e);let t=y.get(e);t?t.push(a):y.set(e,[a])}i.length===0&&S.push(r.id),t.push({id:r.id,message:r.message,explanation:r.explanation,hunkIndices:i})}let r=new Set(e.orderedCommitIds);if(r.size!==e.orderedCommitIds.length)return o([`Branch '${e.id}': orderedCommitIds contains duplicates`],`The orderedCommitIds list for branch '${e.id}' contains duplicates. Each commit ID must appear exactly once.`);let c=[...n].filter(e=>!r.has(e)),f=e.orderedCommitIds.filter(e=>!n.has(e));if(c.length>0)return o([`Branch '${e.id}': orderedCommitIds is missing commits: ${c.join(`, `)}`],`The orderedCommitIds for branch '${e.id}' must include every commit. Missing: ${c.join(`, `)}`);if(f.length>0)return o([`Branch '${e.id}': orderedCommitIds references unknown commits: ${f.join(`, `)}`],`The orderedCommitIds for branch '${e.id}' references commits that don't exist. Unknown: ${f.join(`, `)}`);let p=e.partitions?[...e.partitions]:void 0;if(p){let t=new Set,n=[];for(let r=0;r<p.length;r++){let i=p[r];if(!a(i.branchName))return o([`Branch '${e.id}', partition ${r}: '${i.branchName}' is not a valid git branch name`],`The branch name '${i.branchName}' is invalid. Git branch names cannot contain spaces, '..', '~', '^', ':', '\\', or end with '.lock'. Please provide a valid name.`);if(t.has(i.branchName))return o([`Branch '${e.id}': duplicate partition name '${i.branchName}'`],`Multiple partitions use the branch name '${i.branchName}'. Each must be unique.`);if(t.add(i.branchName),d?.has(i.branchName))return o([`Partition branch name '${i.branchName}' already exists in the repository`],`The branch name '${i.branchName}' already exists in the repository. Please choose a different name.`);for(let e of i.commitIds)n.push(e)}let s=new Set(n),c=e.orderedCommitIds.filter(e=>!s.has(e)),l=n.filter(e=>!r.has(e));if(c.length>0)return o([`Branch '${e.id}': partition is missing commits: ${c.join(`, `)}`],`All commits must be assigned to a partition. Missing: ${c.join(`, `)}`);if(l.length>0)return o([`Branch '${e.id}': partition references unknown commits: ${l.join(`, `)}`],`Partition references unknown commits: ${l.join(`, `)}`);if(!i(p,e.orderedCommitIds))return o([`Branch '${e.id}': partitions are not contiguous`],`Each partition must contain a contiguous range of commits in the ordered list. Please regroup into contiguous ranges.`)}C.push({id:e.id,name:e.name,title:e.title,description:e.description,commits:t,orderedCommitIds:e.orderedCommitIds,partitions:p})}if(x.length>0)return o([`Duplicate commit IDs: ${x.join(`, `)}`],`Some commit IDs appear multiple times. Each must be unique.\n\nDuplicates: ${x.join(`, `)}`);if(S.length>0)return o([`Empty commits (no hunks assigned): ${S.join(`, `)}`],`Every commit must contain at least one hunk. These commits have an empty \`hunks\` array: ${S.join(`, `)}.
2
2
 
3
3
  Either assign at least one hunk to each of these commits, or remove them entirely. If a commit's purpose is already covered by another commit, remove the duplicate. If a commit is supposed to cover specific hunks, assign those hunk indices to it. Every hunk index still needs to appear in exactly one commit.`);if(v.length>0){let e=[...new Set(v)].sort((e,t)=>e-t),t=e.map(e=>` hunk ${e} → ${(y.get(e)??[]).join(`, `)}`).join(`
4
- `),n=[...y.values()].reduce((e,t)=>e+t.length,0);return a([`Duplicate hunks: ${e.join(`, `)}`],`Some hunks appear in multiple commits. Each hunk can only be used once.
4
+ `),n=[...y.values()].reduce((e,t)=>e+t.length,0);return o([`Duplicate hunks: ${e.join(`, `)}`],`Some hunks appear in multiple commits. Each hunk can only be used once.
5
5
 
6
- Your response listed ${n} hunk references across ${y.size} unique indices. Expected: ${s} references across ${s} unique indices.
6
+ Your response listed ${n} hunk references across ${y.size} unique indices. Expected: ${c} references across ${c} unique indices.
7
7
 
8
8
  Duplicates (each hunk index → the commits it landed in):
9
9
  ${t}
10
10
 
11
- To fix: for each duplicated hunk index, decide which one commit it truly belongs in and remove it from the others. Do not split a single hunk across two commits — git only knows how to apply a hunk once.`)}let w=new Set(Array.from({length:s},(e,t)=>t)),T=[...w].filter(e=>!_.has(e)),E=[..._].filter(e=>!w.has(e));return T.length>0?a([`Missing hunks: ${T.join(`, `)}`],`Some hunks are not assigned to any commit. All hunks must be included.\n\nMissing: ${T.join(`, `)}`):E.length>0?a([`Extra hunks: ${E.join(`, `)}`],`Some hunks in your response don't exist in the input.\n\nExtra: ${E.join(`, `)}`):{valid:!0,result:{branches:C,rationale:m.rationale}}}catch(e){let t=e instanceof Error?e.message:String(e),r=(d??n).slice(-300);return a([`Failed to parse response as valid JSON: ${t}`,`Response tail: ${r}`],o(c,l))}}function r(e,t){let n=new Map(t.map((e,t)=>[e,t])),r=0;for(let t of e)for(let e of t.commitIds){let t=n.get(e);if(t===void 0||t!==r)return!1;r++}return r===t.length}function i(e){return e.startsWith(`-`)||e.startsWith(`.`)||e.endsWith(`.lock`)||e.endsWith(`.`)||e.endsWith(`/`)||e.includes(`..`)||e.includes(`~`)||e.includes(`^`)||e.includes(`:`)||e.includes(`\\`)||e.includes(` `)||e.includes(`?`)||e.includes(`*`)||e.includes(`[`)||e.includes(`@{`)?!1:e.length>0}function a(e,t){return{valid:!1,errors:e,retryFeedback:t}}function o(e,t){return`Your response has an invalid structure. Please return JSON inside <output> tags:
11
+ To fix: for each duplicated hunk index, decide which one commit it truly belongs in and remove it from the others. Do not split a single hunk across two commits — git only knows how to apply a hunk once.`)}let w=new Set(Array.from({length:c},(e,t)=>t)),T=[...w].filter(e=>!_.has(e)),E=[..._].filter(e=>!w.has(e));return T.length>0?o([`Missing hunks: ${T.join(`, `)}`],`Some hunks are not assigned to any commit. All hunks must be included.\n\nMissing: ${T.join(`, `)}`):E.length>0?o([`Extra hunks: ${E.join(`, `)}`],`Some hunks in your response don't exist in the input.\n\nExtra: ${E.join(`, `)}`):{valid:!0,result:{branches:C,rationale:m.rationale}}}catch(e){let t=e instanceof Error?e.message:String(e),n=(f??r).slice(-300);return o([`Failed to parse response as valid JSON: ${t}`,`Response tail: ${n}`],s(l,u))}}function i(e,t){let n=new Map(t.map((e,t)=>[e,t])),r=0;for(let t of e)for(let e of t.commitIds){let t=n.get(e);if(t===void 0||t!==r)return!1;r++}return r===t.length}function a(e){return e.startsWith(`-`)||e.startsWith(`.`)||e.endsWith(`.lock`)||e.endsWith(`.`)||e.endsWith(`/`)||e.includes(`..`)||e.includes(`~`)||e.includes(`^`)||e.includes(`:`)||e.includes(`\\`)||e.includes(` `)||e.includes(`?`)||e.includes(`*`)||e.includes(`[`)||e.includes(`@{`)?!1:e.length>0}function o(e,t){return{valid:!1,errors:e,retryFeedback:t}}function s(e,t){return`Your response has an invalid structure. Please return JSON inside <output> tags:
12
12
  <output>
13
13
  {
14
14
  "branches": [
@@ -30,4 +30,4 @@ To fix: for each duplicated hunk index, decide which one commit it truly belongs
30
30
  }
31
31
  </output>
32
32
 
33
- Return only the <output> tag and no other text.`}export{n as validateQuickComposeResponse};
33
+ Return only the <output> tag and no other text.`}export{r as validateQuickComposeResponse};
@@ -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{buildRefinementPrompt as r}from"./prompt.js";import{validateRefineResponse as i}from"./validator.js";async function a(a){if(a.session.messages.length===0)throw new e(`refine: session must contain prior messages from a previous compose call.`);if(a.hunkCount<=0)throw new e(`refine: hunkCount must be a positive integer.`);let o=a.mode??`branches`,s=new Map;for(let e of a.lockedCommits??[])s.set(e.id,e);let c=n(r({instructions:a.instructions,lockedCommits:a.lockedCommits??[],clientContext:a.clientContext,hunkCount:a.hunkCount,mode:o}),a.promptOverride),l=await t({model:a.model,systemPrompt:void 0,userPrompt:c,session:a.session,maxRetries:a.maxRetries,maxTokens:a.maxTokens,onProgress:a.onProgress,cancellation:a.cancellation,onBeforeModelCall:a.onBeforeModelCall,onAfterModelCall:a.onAfterModelCall,onBeforePrompt:a.onBeforePrompt,validate:e=>i(e,{hunkCount:a.hunkCount,mode:o,lockedCommits:s}),taskName:`refine`}),u={...l.session,completedSteps:a.session.completedSteps};return{result:l.result,session:u,usage:l.usage,attempts:l.attempts,durationMs:l.durationMs,callIds:l.callIds}}export{a as refine};
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{buildRefinementPrompt as r}from"./prompt.js";import{validateRefineResponse as i}from"./validator.js";async function a(a){if(a.session.messages.length===0)throw new e(`refine: session must contain prior messages from a previous compose call.`);if(a.hunkCount<=0)throw new e(`refine: hunkCount must be a positive integer.`);let o=a.mode??`branches`,s=new Map;for(let e of a.lockedCommits??[])s.set(e.id,e);let c=n(r({instructions:a.instructions,lockedCommits:a.lockedCommits??[],clientContext:a.clientContext,hunkCount:a.hunkCount,mode:o}),a.promptOverride),l=await t({model:a.model,systemPrompt:void 0,userPrompt:c,session:a.session,maxRetries:a.maxRetries,maxTokens:a.maxTokens,onProgress:a.onProgress,cancellation:a.cancellation,onBeforeModelCall:a.onBeforeModelCall,onAfterModelCall:a.onAfterModelCall,onBeforePrompt:a.onBeforePrompt,validate:e=>i(e,{hunkCount:a.hunkCount,mode:o,lockedCommits:s,lockedRuns:a.lockedRuns}),taskName:`refine`}),u={...l.session,completedSteps:a.session.completedSteps};return{result:l.result,session:u,usage:l.usage,attempts:l.attempts,durationMs:l.durationMs,callIds:l.callIds}}export{a as refine};
@@ -25,6 +25,20 @@ interface RefineInput {
25
25
  * prompt and enforces their presence + substitutes their content (id, message,
26
26
  * explanation, hunkIndices) into the AI's response after validation. */
27
27
  lockedCommits?: readonly CommitSuggestion[];
28
+ /**
29
+ * The locked commits partitioned into the maximal runs of *consecutive* locked commits
30
+ * (no commit between them) within each branch of the prior plan, in that plan's original
31
+ * order — one entry per run, singletons included. Flattening yields the locked commits'
32
+ * original relative order.
33
+ *
34
+ * The validator uses this to undo AI reshuffling in the post-substitution cleanup: it
35
+ * restores the locked commits' relative order within each output branch and pulls each
36
+ * multi-commit run back together contiguously. Adjacency can't be derived from
37
+ * `lockedCommits` alone (which carries no positional context), so callers driving the task
38
+ * directly must supply it; the workflow computes it from the prior plan. Omit for no
39
+ * reordering.
40
+ */
41
+ lockedRuns?: readonly (readonly string[])[];
28
42
  /** Per-call user instructions appended to the refinement prompt. */
29
43
  instructions?: string;
30
44
  /** Free-text consumer-supplied context (e.g. UI-ordering notes) injected verbatim into
@@ -7,13 +7,17 @@ interface ValidateRefineParams {
7
7
  hunkCount: number;
8
8
  mode: 'commits' | 'branches';
9
9
  lockedCommits: Map<string, CommitSuggestion>;
10
+ /** Maximal runs of consecutive locked commits per prior-plan branch, in original order.
11
+ * See {@link import('./types.js').RefineInput.lockedRuns}. */
12
+ lockedRuns?: readonly (readonly string[])[];
10
13
  }
11
14
  /**
12
15
  * Two-stage validation for the refine task:
13
16
  *
14
17
  * 1. Base structural validation via `validateGroupResponse` — same invariants the rest of
15
18
  * the library enforces (hunk index uniqueness + coverage, non-empty commits, unique ids).
16
- * 2. Locked-commits check + content substitution + post-substitution invariant check.
19
+ * 2. Locked-commits check + content substitution + post-substitution invariant check +
20
+ * locked-order restoration (see {@link restoreLockedOrdering}).
17
21
  *
18
22
  * The substitution step is what makes refine resilient to minor AI drift: when the AI emits
19
23
  * a commit with a locked id but slightly different message text, we silently restore the
@@ -1 +1 @@
1
- import{validateGroupResponse as e}from"../group/validator.js";function t(t,r){let i=e(t,r.hunkCount,r.mode);if(!i.valid||i.result===void 0)return i;let a=i.result;if(r.lockedCommits.size===0)return i;let o=new Map;for(let e of a.branches)for(let t=0;t<e.commits.length;t++)o.set(e.commits[t].id,{branch:e,commitIndex:t});let s=[];for(let e of r.lockedCommits.keys())o.has(e)||s.push(e);if(s.length>0){let e=s.map(e=>`'${e}'`).join(`, `);return{valid:!1,errors:[`Missing locked commits: ${e}`],retryFeedback:`Your response is missing the following LOCKED commits, which must always appear in the output: ${e}. Re-emit the plan with these commits present (preserving their id, message, explanation, and hunks exactly).`}}for(let[e,t]of r.lockedCommits){let n=o.get(e);if(n==null)continue;let r=n.branch.commits[n.commitIndex];n.branch.commits[n.commitIndex]={...r,id:t.id,message:t.message,explanation:t.explanation,hunkIndices:[...t.hunkIndices]}}let c=n(a,r.hunkCount);return c==null?{valid:!0,result:a}:{valid:!1,errors:[c],retryFeedback:`After restoring the locked commits' original hunk assignments, the resulting plan is structurally invalid: ${c}. This means you assigned hunks belonging to a locked commit to a different commit. Locked commits' hunks must remain exclusive to them — do not move those hunks elsewhere.`}}function n(e,t){let n=new Set,r=[],i=[],a=new Set,o=[];for(let t of e.branches)for(let e of t.commits){if(a.has(e.id)&&o.push(e.id),a.add(e.id),e.hunkIndices.length===0){i.push(e.id);continue}for(let t of e.hunkIndices)n.has(t)&&r.push(t),n.add(t)}if(o.length>0)return`duplicate commit ids after substitution: ${o.join(`, `)}`;if(i.length>0)return`empty commits after substitution (no hunks assigned): ${i.join(`, `)}`;if(r.length>0)return`hunk indices appear in more than one commit: ${[...new Set(r)].join(`, `)}`;let s=[];for(let e=0;e<t;e++)n.has(e)||s.push(e);if(s.length>0)return`hunk indices missing from the plan: ${s.join(`, `)}`;let c=[...n].filter(e=>e>=t||e<0);if(c.length>0)return`hunk indices outside the valid range 0..${String(t-1)}: ${c.join(`, `)}`}export{t as validateRefineResponse};
1
+ import{validateGroupResponse as e}from"../group/validator.js";function t(t,r){let i=e(t,r.hunkCount,r.mode);if(!i.valid||i.result===void 0)return i;let o=i.result;if(r.lockedCommits.size===0)return i;let s=new Map;for(let e of o.branches)for(let t=0;t<e.commits.length;t++)s.set(e.commits[t].id,{branch:e,commitIndex:t});let c=[];for(let e of r.lockedCommits.keys())s.has(e)||c.push(e);if(c.length>0){let e=c.map(e=>`'${e}'`).join(`, `);return{valid:!1,errors:[`Missing locked commits: ${e}`],retryFeedback:`Your response is missing the following LOCKED commits, which must always appear in the output: ${e}. Re-emit the plan with these commits present (preserving their id, message, explanation, and hunks exactly).`}}for(let[e,t]of r.lockedCommits){let n=s.get(e);if(n==null)continue;let r=n.branch.commits[n.commitIndex];n.branch.commits[n.commitIndex]={...r,id:t.id,message:t.message,explanation:t.explanation,hunkIndices:[...t.hunkIndices]}}let l=a(o,r.hunkCount);return l==null?(n(o,r.lockedRuns),{valid:!0,result:o}):{valid:!1,errors:[l],retryFeedback:`After restoring the locked commits' original hunk assignments, the resulting plan is structurally invalid: ${l}. This means you assigned hunks belonging to a locked commit to a different commit. Locked commits' hunks must remain exclusive to them — do not move those hunks elsewhere.`}}function n(e,t){if(t==null||t.length===0)return;let n=t.flat(),a=new Map(n.map((e,t)=>[e,t])),o=new Set(n);for(let t of e.branches)r(t,o,a);for(let n of t)n.length>=2&&i(e,n)}function r(e,t,n){let r=[],i=[];for(let n=0;n<e.commits.length;n++)t.has(e.commits[n].id)&&(r.push(n),i.push(e.commits[n]));if(!(i.length<2)){i.sort((e,t)=>(n.get(e.id)??0)-(n.get(t.id)??0));for(let t=0;t<r.length;t++)e.commits[r[t]]=i[t]}}function i(e,t){let n=new Set(t),r=e.branches.filter(e=>e.commits.some(e=>n.has(e.id)));if(r.length!==1)return;let i=r[0],a=[];for(let e of t){let t=i.commits.find(t=>t.id===e);t!=null&&a.push(t)}if(a.length<2)return;let o=Math.min(...a.map(e=>i.commits.indexOf(e))),s=i.commits.slice(0,o).filter(e=>!n.has(e.id)).length,c=i.commits.filter(e=>!n.has(e.id));c.splice(s,0,...a),i.commits=c}function a(e,t){let n=new Set,r=[],i=[],a=new Set,o=[];for(let t of e.branches)for(let e of t.commits){if(a.has(e.id)&&o.push(e.id),a.add(e.id),e.hunkIndices.length===0){i.push(e.id);continue}for(let t of e.hunkIndices)n.has(t)&&r.push(t),n.add(t)}if(o.length>0)return`duplicate commit ids after substitution: ${o.join(`, `)}`;if(i.length>0)return`empty commits after substitution (no hunks assigned): ${i.join(`, `)}`;if(r.length>0)return`hunk indices appear in more than one commit: ${[...new Set(r)].join(`, `)}`;let s=[];for(let e=0;e<t;e++)n.has(e)||s.push(e);if(s.length>0)return`hunk indices missing from the plan: ${s.join(`, `)}`;let c=[...n].filter(e=>e>=t||e<0);if(c.length>0)return`hunk indices outside the valid range 0..${String(t-1)}: ${c.join(`, `)}`}export{t as validateRefineResponse};
@@ -0,0 +1,40 @@
1
+ import { ComposeHunk } from "../types.js";
2
+ import { RedactHunkContent } from "./redact.js";
3
+
4
+ //#region src/tasks/utils/budget.d.ts
5
+ /**
6
+ * Ceiling on estimated input tokens for a single hunk-bearing prompt.
7
+ *
8
+ * Production p99 for the composer sits around 150k input tokens; pathological diffs
9
+ * (lockfile-dominated) reach 700k-1.7M. 180k leaves headroom above normal usage while
10
+ * still forcing degradation on the pathological tail, and stays comfortably inside the
11
+ * 200k context window of the models the composer targets.
12
+ */
13
+ declare const DEFAULT_INPUT_TOKEN_BUDGET = 180000;
14
+ /** Rough char→token estimate shared with `runAiLoop`'s prompt-gate heuristic. */
15
+ declare function estimateTokens(chars: number): number;
16
+ interface AssembledHunkPrompt {
17
+ prompt: string;
18
+ /**
19
+ * Hunks that lost their real body to budget pressure (full to condensed). Non-zero means
20
+ * content-dependent reasoning (analyze/group dependency detection) saw less than the full diff.
21
+ */
22
+ hunksCondensedForBudget: number;
23
+ }
24
+ /**
25
+ * Serializes `hunks` into the `<hunks>` JSON block and assembles the full prompt via
26
+ * `assemble`, keeping estimated input tokens under `inputTokenBudget`. Noise hunks (per
27
+ * `redactHunkContent`) start redacted, `condensed` forces metadata-only, else full; over
28
+ * budget, hunks are condensed biggest-first, throwing {@link DiffTooLargeError} if they still
29
+ * cannot fit. `assemble` is called with `''` to measure fixed overhead, so it must be pure.
30
+ */
31
+ declare function assembleHunkPrompt(params: {
32
+ hunks: ComposeHunk[];
33
+ assemble: (hunksJson: string) => string;
34
+ condensed?: boolean;
35
+ redactHunkContent?: RedactHunkContent;
36
+ inputTokenBudget?: number;
37
+ systemPromptChars?: number;
38
+ }): AssembledHunkPrompt;
39
+ //#endregion
40
+ export { AssembledHunkPrompt, DEFAULT_INPUT_TOKEN_BUDGET, assembleHunkPrompt, estimateTokens };
@@ -0,0 +1 @@
1
+ import{DiffTooLargeError as e}from"../../errors/task.js";import{estimatedCharactersPerToken as t}from"../ai-loop.js";import{REDACTED_HUNK_CONTENT as n}from"./redact.js";const r=18e4;function i(e){return Math.ceil(e/t)}function a(e,t){let r={...e.isBinary?{isBinary:!0}:{},...e.isRename?{isRename:!0}:{},...e.originalFileName?{originalFileName:e.originalFileName}:{}};return t===`condensed`?{index:e.index,fileName:e.fileName,hunkHeader:e.hunkHeader,additions:e.additions,deletions:e.deletions,...r}:t===`redacted`?{index:e.index,fileName:e.fileName,hunkHeader:e.hunkHeader,additions:e.additions,deletions:e.deletions,content:n,...r}:{index:e.index,fileName:e.fileName,hunkHeader:e.hunkHeader,content:e.content,...r}}function o(e,t){return JSON.stringify(a(e,t)).length}function s(e,t,n,r){let a=new Map(t),s=2+Math.max(0,e.length-1),c=0,l=e.map(e=>{let t=a.get(e.index),n=o(e,`condensed`),r=t===`condensed`?n:o(e,t);return c+=r,{hunk:e,currentLen:r,condensedLen:n}}),u=()=>i(r+s+c),d=()=>u()>n;if(d()){let e=l.map(e=>({hunk:e.hunk,savings:e.currentLen-e.condensedLen})).filter(e=>e.savings>0).sort((e,t)=>t.savings-e.savings);for(let{hunk:t,savings:n}of e){if(!d())break;c-=n,a.set(t.index,`condensed`)}}return{modes:a,estimatedTokens:u(),tooLarge:d()}}function c(t){let{hunks:n,assemble:i,condensed:o,redactHunkContent:c,inputTokenBudget:l=r,systemPromptChars:u=0}=t,d=new Map(n.map(e=>o?[e.index,`condensed`]:c?.(e)?[e.index,`redacted`]:[e.index,`full`])),{modes:f,estimatedTokens:p,tooLarge:m}=s(n,d,l,i(``).length+u);if(m)throw new e(p,l);let h=0;for(let e of n)d.get(e.index)===`full`&&f.get(e.index)===`condensed`&&h++;return{prompt:i(JSON.stringify(n.map(e=>a(e,f.get(e.index))))),hunksCondensedForBudget:h}}export{r as DEFAULT_INPUT_TOKEN_BUDGET,c as assembleHunkPrompt,i as estimateTokens};
@@ -0,0 +1,61 @@
1
+ //#region src/tasks/utils/json-schema.d.ts
2
+ interface SchemaIssue {
3
+ path: (string | number)[];
4
+ message: string;
5
+ }
6
+ type SchemaResult<T> = {
7
+ success: true;
8
+ data: T;
9
+ } | {
10
+ success: false;
11
+ issues: SchemaIssue[];
12
+ };
13
+ interface SchemaAnnotations {
14
+ $schema?: string;
15
+ description?: string;
16
+ }
17
+ interface ObjectSchemaDef extends SchemaAnnotations {
18
+ type: 'object';
19
+ properties: Readonly<Record<string, JsonSchemaDef>>;
20
+ required: readonly string[];
21
+ additionalProperties: false;
22
+ }
23
+ interface ArraySchemaDef extends SchemaAnnotations {
24
+ type: 'array';
25
+ items: JsonSchemaDef;
26
+ minItems?: number;
27
+ default?: readonly unknown[];
28
+ }
29
+ interface StringSchemaDef extends SchemaAnnotations {
30
+ type: 'string';
31
+ minLength?: number;
32
+ maxLength?: number;
33
+ enum?: readonly string[];
34
+ }
35
+ interface NumberSchemaDef extends SchemaAnnotations {
36
+ type: 'number' | 'integer';
37
+ minimum?: number;
38
+ maximum?: number;
39
+ }
40
+ interface BooleanSchemaDef extends SchemaAnnotations {
41
+ type: 'boolean';
42
+ }
43
+ /**
44
+ * The JSON-Schema subset used by the task output schemas. Deliberately closed: no `anyOf`,
45
+ * `const`, `$ref`, `pattern`, `format`, record types, or type arrays — widen only when a task
46
+ * schema actually needs more, and keep prompt snapshots byte-stable when doing so.
47
+ */
48
+ type JsonSchemaDef = ObjectSchemaDef | ArraySchemaDef | StringSchemaDef | NumberSchemaDef | BooleanSchemaDef;
49
+ /**
50
+ * Validates `value` against `schema` with the same observable semantics the previous zod
51
+ * schemas had: every issue is collected in one pass, unknown object keys are silently
52
+ * stripped, a `required` property whose subschema carries `default` is filled with a fresh
53
+ * clone of that default when absent, and issue messages mirror zod v4 wording so telemetry
54
+ * and error logs stay comparable across the migration. One deliberate divergence: on a type
55
+ * mismatch zod could add a second size-constraint issue keyed off the wrong-typed value's
56
+ * `.length`; this validator reports only the type issue (same failure outcome, one issue
57
+ * fewer). On success `data` is a rebuilt tree, never the input by reference.
58
+ */
59
+ declare function validateJsonSchema<T>(schema: JsonSchemaDef, value: unknown): SchemaResult<T>;
60
+ //#endregion
61
+ export { ArraySchemaDef, BooleanSchemaDef, JsonSchemaDef, NumberSchemaDef, ObjectSchemaDef, SchemaIssue, SchemaResult, StringSchemaDef, validateJsonSchema };