@gitkraken/compose-tools 0.1.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 (122) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/LICENSE +19 -0
  3. package/README.md +543 -0
  4. package/dist/ai/session.d.ts +12 -0
  5. package/dist/ai/session.js +1 -0
  6. package/dist/ai/types.d.ts +31 -0
  7. package/dist/ai/types.js +1 -0
  8. package/dist/cancellation.d.ts +20 -0
  9. package/dist/cancellation.js +1 -0
  10. package/dist/errors/ai.d.ts +10 -0
  11. package/dist/errors/ai.js +1 -0
  12. package/dist/errors/git.d.ts +12 -0
  13. package/dist/errors/git.js +1 -0
  14. package/dist/errors/task.d.ts +6 -0
  15. package/dist/errors/task.js +1 -0
  16. package/dist/errors/workflow.d.ts +13 -0
  17. package/dist/errors/workflow.js +1 -0
  18. package/dist/git/apply-branches.d.ts +28 -0
  19. package/dist/git/apply-branches.js +1 -0
  20. package/dist/git/apply-target.d.ts +39 -0
  21. package/dist/git/apply-target.js +1 -0
  22. package/dist/git/commit-chain.d.ts +87 -0
  23. package/dist/git/commit-chain.js +6 -0
  24. package/dist/git/diff-collector.d.ts +80 -0
  25. package/dist/git/diff-collector.js +8 -0
  26. package/dist/git/index.d.ts +7 -0
  27. package/dist/git/index.js +1 -0
  28. package/dist/git/port-dispatch.d.ts +34 -0
  29. package/dist/git/port-dispatch.js +3 -0
  30. package/dist/git/require-exec.d.ts +17 -0
  31. package/dist/git/require-exec.js +1 -0
  32. package/dist/git/safety.d.ts +22 -0
  33. package/dist/git/safety.js +1 -0
  34. package/dist/git/types.d.ts +43 -0
  35. package/dist/git/types.js +1 -0
  36. package/dist/git/utils/conventions.d.ts +33 -0
  37. package/dist/git/utils/conventions.js +10 -0
  38. package/dist/git/utils/crypto.d.ts +4 -0
  39. package/dist/git/utils/crypto.js +1 -0
  40. package/dist/git/utils/hunk-parser.d.ts +6 -0
  41. package/dist/git/utils/hunk-parser.js +7 -0
  42. package/dist/git/utils/patch-builder.d.ts +6 -0
  43. package/dist/git/utils/patch-builder.js +2 -0
  44. package/dist/git/utils/temp-index.d.ts +6 -0
  45. package/dist/git/utils/temp-index.js +1 -0
  46. package/dist/index.d.ts +28 -0
  47. package/dist/index.js +1 -0
  48. package/dist/ports/git.d.ts +199 -0
  49. package/dist/ports/git.js +1 -0
  50. package/dist/tasks/ai-loop.d.ts +99 -0
  51. package/dist/tasks/ai-loop.js +2 -0
  52. package/dist/tasks/analyze/analyze.d.ts +6 -0
  53. package/dist/tasks/analyze/analyze.js +1 -0
  54. package/dist/tasks/analyze/prompt.d.ts +16 -0
  55. package/dist/tasks/analyze/prompt.js +34 -0
  56. package/dist/tasks/analyze/schema.d.ts +38 -0
  57. package/dist/tasks/analyze/schema.js +1 -0
  58. package/dist/tasks/analyze/types.d.ts +69 -0
  59. package/dist/tasks/analyze/types.js +1 -0
  60. package/dist/tasks/analyze/validator.d.ts +7 -0
  61. package/dist/tasks/analyze/validator.js +13 -0
  62. package/dist/tasks/compose-group/compose-group.d.ts +15 -0
  63. package/dist/tasks/compose-group/compose-group.js +1 -0
  64. package/dist/tasks/compose-group/prompt.d.ts +26 -0
  65. package/dist/tasks/compose-group/prompt.js +162 -0
  66. package/dist/tasks/compose-group/types.d.ts +62 -0
  67. package/dist/tasks/compose-group/types.js +1 -0
  68. package/dist/tasks/group/group.d.ts +6 -0
  69. package/dist/tasks/group/group.js +1 -0
  70. package/dist/tasks/group/prompt.d.ts +15 -0
  71. package/dist/tasks/group/prompt.js +158 -0
  72. package/dist/tasks/group/schema.d.ts +19 -0
  73. package/dist/tasks/group/schema.js +1 -0
  74. package/dist/tasks/group/types.d.ts +61 -0
  75. package/dist/tasks/group/types.js +1 -0
  76. package/dist/tasks/group/validator.d.ts +7 -0
  77. package/dist/tasks/group/validator.js +53 -0
  78. package/dist/tasks/index.d.ts +15 -0
  79. package/dist/tasks/index.js +1 -0
  80. package/dist/tasks/order/order.d.ts +6 -0
  81. package/dist/tasks/order/order.js +1 -0
  82. package/dist/tasks/order/prompt.d.ts +13 -0
  83. package/dist/tasks/order/prompt.js +45 -0
  84. package/dist/tasks/order/schema.d.ts +12 -0
  85. package/dist/tasks/order/schema.js +1 -0
  86. package/dist/tasks/order/types.d.ts +36 -0
  87. package/dist/tasks/order/types.js +1 -0
  88. package/dist/tasks/order/validator.d.ts +11 -0
  89. package/dist/tasks/order/validator.js +12 -0
  90. package/dist/tasks/partition/partition.d.ts +6 -0
  91. package/dist/tasks/partition/partition.js +1 -0
  92. package/dist/tasks/partition/prompt.d.ts +16 -0
  93. package/dist/tasks/partition/prompt.js +61 -0
  94. package/dist/tasks/partition/schema.d.ts +14 -0
  95. package/dist/tasks/partition/schema.js +1 -0
  96. package/dist/tasks/partition/types.d.ts +53 -0
  97. package/dist/tasks/partition/types.js +1 -0
  98. package/dist/tasks/partition/validator.d.ts +7 -0
  99. package/dist/tasks/partition/validator.js +11 -0
  100. package/dist/tasks/quick-compose/prompt.d.ts +18 -0
  101. package/dist/tasks/quick-compose/prompt.js +47 -0
  102. package/dist/tasks/quick-compose/quick-compose.d.ts +6 -0
  103. package/dist/tasks/quick-compose/quick-compose.js +1 -0
  104. package/dist/tasks/quick-compose/schema.d.ts +27 -0
  105. package/dist/tasks/quick-compose/schema.js +1 -0
  106. package/dist/tasks/quick-compose/types.d.ts +63 -0
  107. package/dist/tasks/quick-compose/types.js +1 -0
  108. package/dist/tasks/quick-compose/validator.d.ts +7 -0
  109. package/dist/tasks/quick-compose/validator.js +25 -0
  110. package/dist/tasks/session.d.ts +22 -0
  111. package/dist/tasks/session.js +1 -0
  112. package/dist/tasks/types.d.ts +101 -0
  113. package/dist/tasks/types.js +3 -0
  114. package/dist/workflows/compose.d.ts +371 -0
  115. package/dist/workflows/compose.js +2 -0
  116. package/dist/workflows/split.d.ts +80 -0
  117. package/dist/workflows/split.js +3 -0
  118. package/dist/workflows/types.d.ts +63 -0
  119. package/dist/workflows/types.js +1 -0
  120. package/dist/workflows/undo.d.ts +56 -0
  121. package/dist/workflows/undo.js +1 -0
  122. package/package.json +37 -0
@@ -0,0 +1,45 @@
1
+ import{orderingResultSchema as e}from"./schema.js";import{z as t}from"zod";const n=JSON.stringify(t.toJSONSchema(e),null,2);function r(){return`You are an advanced AI programming assistant tasked with determining the optimal order for commits within each branch. Your goal is to order commits so that:
2
+
3
+ 1. **Dependencies are respected** — if commit A introduces something that commit B relies on, A must come before B
4
+ 2. **Refactoring before features** — structural improvements and refactors should precede new functionality that builds on them
5
+ 3. **Infrastructure before consumers** — types, interfaces, and utilities should come before the code that uses them
6
+ 4. **Tests near their implementation** — test commits should follow closely after the code they test
7
+ 5. **Clean build at each step** — ideally, the codebase should be in a valid state after each commit
8
+
9
+ You will receive one or more branches, each containing unordered commits. Order the commits within each branch independently.
10
+
11
+ ## Required Output Format
12
+
13
+ Output the ordered commit IDs per branch as JSON inside a <output> tag matching this schema:
14
+
15
+ \`\`\`json
16
+ ${n}
17
+ \`\`\`
18
+
19
+ Return only the <output> tag and no other text.`}function i(e){let{branches:t,hunks:n,instructions:r}=e,i=`Determine the optimal commit order within each branch:\n\n<branches>\n${JSON.stringify(t.map(e=>({id:e.id,name:e.name,commits:e.commits.map(e=>({id:e.id,message:e.message,explanation:e.explanation,hunkIndices:e.hunkIndices}))})))}\n</branches>`;if(n?.length){let e=JSON.stringify(n.map(e=>({index:e.index,fileName:e.fileName,hunkHeader:e.hunkHeader})));i+=`\n\nFor reference, here is the hunk index to file mapping:\n<hunk_map>\n${e}\n</hunk_map>`}return r&&(i+=`\n\nAdditional instructions:\n${r}`),i+=`
20
+
21
+ Return the ordered commit IDs per branch. Return only the <output> tag and no other text.`,i}function a(e){let t=`Based on the branches and commits you created, determine the optimal order for the commits within each branch.
22
+
23
+ Ordering principles:
24
+ 1. Dependencies first — if commit A introduces something commit B relies on, A must come before B
25
+ 2. Refactoring before features — structural improvements precede new functionality
26
+ 3. Infrastructure before consumers — types, interfaces, utilities before the code using them
27
+ 4. Tests near their implementation — test commits follow closely after the code they test
28
+ 5. Clean build at each step — the codebase should be valid after each commit
29
+
30
+ IMPORTANT: Respect the ordering constraints you identified in your analysis. If a hunk provides something (defines a file, function, type, etc.) that another hunk consumes (imports, calls, references), the provider's commit must come before or be the same as the consumer's commit.`;return e.instructions&&(t+=`\n\nAdditional instructions:\n${e.instructions}`),t+=`
31
+
32
+ Return the ordered commit IDs per branch as JSON inside <output> tags:
33
+ <output>
34
+ {
35
+ "branches": [
36
+ {
37
+ "branchId": "[branch id]",
38
+ "orderedCommitIds": ["[first]", "[second]"]
39
+ }
40
+ ],
41
+ "rationale": "[explanation]"
42
+ }
43
+ </output>
44
+
45
+ Return only the <output> tag and no other text.`,t}export{a as buildOrderContinuationPrompt,r as buildOrderSystemPrompt,i as buildOrderUserPrompt};
@@ -0,0 +1,12 @@
1
+ import { z } from "zod";
2
+
3
+ //#region src/tasks/order/schema.d.ts
4
+ declare const orderingResultSchema: z.ZodObject<{
5
+ branches: z.ZodArray<z.ZodObject<{
6
+ branchId: z.ZodString;
7
+ orderedCommitIds: z.ZodArray<z.ZodString>;
8
+ }, z.core.$strip>>;
9
+ rationale: z.ZodString;
10
+ }, z.core.$strip>;
11
+ //#endregion
12
+ export { orderingResultSchema };
@@ -0,0 +1 @@
1
+ import{z as e}from"zod";const t=e.object({branchId:e.string().describe(`ID of the branch being ordered`),orderedCommitIds:e.array(e.string()).describe(`Commit IDs in optimal order`)}),n=e.object({branches:e.array(t).describe(`Ordered commits per branch`),rationale:e.string().describe(`Explanation of ordering decisions`)});export{n as orderingResultSchema};
@@ -0,0 +1,36 @@
1
+ import { ProgressEvent, TokenUsage } from "../../ai/types.js";
2
+ import { BranchGroup, BranchOrdering, ComposeHunk, PromptOverride } from "../types.js";
3
+ import { OnAfterModelCall, OnBeforeModelCall, OnBeforePrompt } from "../ai-loop.js";
4
+ import { ComposeSession } from "../session.js";
5
+ import { AiModelPort } from "@gitkraken/shared-tools";
6
+
7
+ //#region src/tasks/order/types.d.ts
8
+ interface OrderInput {
9
+ branches?: BranchGroup[];
10
+ hunks?: ComposeHunk[];
11
+ instructions?: string;
12
+ model: AiModelPort;
13
+ session?: ComposeSession;
14
+ maxRetries?: number;
15
+ onProgress?: (event: ProgressEvent) => void;
16
+ signal?: AbortSignal;
17
+ onBeforeModelCall?: OnBeforeModelCall;
18
+ onAfterModelCall?: OnAfterModelCall;
19
+ onBeforePrompt?: OnBeforePrompt;
20
+ /** Optional prompt override. When set, replaces or decorates the built-in prompts. */
21
+ promptOverride?: PromptOverride;
22
+ }
23
+ interface OrderingResult {
24
+ branches: BranchOrdering[];
25
+ rationale: string;
26
+ }
27
+ interface OrderOutput {
28
+ result: OrderingResult;
29
+ session: ComposeSession;
30
+ usage?: TokenUsage;
31
+ attempts: number;
32
+ durationMs: number;
33
+ callIds: string[];
34
+ }
35
+ //#endregion
36
+ export { OrderInput, OrderOutput, OrderingResult };
@@ -0,0 +1 @@
1
+ export{};
@@ -0,0 +1,11 @@
1
+ import { ValidationResult } from "../ai-loop.js";
2
+ import { OrderingResult } from "./types.js";
3
+
4
+ //#region src/tasks/order/validator.d.ts
5
+ interface KnownBranch {
6
+ branchId: string;
7
+ commitIds: string[];
8
+ }
9
+ declare function validateOrderResponse(responseText: string, knownBranches: KnownBranch[]): ValidationResult<OrderingResult>;
10
+ //#endregion
11
+ export { validateOrderResponse };
@@ -0,0 +1,12 @@
1
+ import{extractOutputTag as e}from"../ai-loop.js";import{orderingResultSchema as t}from"./schema.js";function n(n,a){try{let o=e(n);if(!o)return r([`No <output> tag found in response`],i(a));let s=JSON.parse(o),c=t.safeParse(s);if(!c.success)return r(c.error.issues.map(e=>`${e.path.join(`.`)}: ${e.message}`),i(a));let l=c.data,u=[],d=new Set;for(let e of l.branches){d.add(e.branchId);let t=a.find(t=>t.branchId===e.branchId);if(!t)return r([`Unknown branch ID: '${e.branchId}'`],`Branch ID '${e.branchId}' doesn't match any known branch.\n\nKnown branches: ${a.map(e=>e.branchId).join(`, `)}`);let n=new Set(t.commitIds),i=new Set,o=[],s=[];for(let t of e.orderedCommitIds)i.has(t)&&o.push(t),n.has(t)||s.push(t),i.add(t);if(o.length>0)return r([`Branch '${e.branchId}': duplicate commit IDs: ${o.join(`, `)}`],`Branch '${e.branchId}' has duplicate commits. Each must appear exactly once.\n\nDuplicates: ${o.join(`, `)}`);if(s.length>0)return r([`Branch '${e.branchId}': unknown commit IDs: ${s.join(`, `)}`],`Branch '${e.branchId}' references unknown commits.\n\nUnknown: ${s.join(`, `)}\nValid: ${t.commitIds.join(`, `)}`);let c=t.commitIds.filter(e=>!i.has(e));if(c.length>0)return r([`Branch '${e.branchId}': missing commit IDs: ${c.join(`, `)}`],`Branch '${e.branchId}' is missing some commits.\n\nMissing: ${c.join(`, `)}`);u.push({branchId:e.branchId,orderedCommitIds:e.orderedCommitIds})}let f=a.filter(e=>!d.has(e.branchId));return f.length>0?r([`Missing branches: ${f.map(e=>e.branchId).join(`, `)}`],`Some branches are not included in the ordering.\n\nMissing: ${f.map(e=>e.branchId).join(`, `)}`):{valid:!0,result:{branches:u,rationale:l.rationale}}}catch{return r([`Failed to parse response as valid JSON`],i(a))}}function r(e,t){return{valid:!1,errors:e,retryFeedback:t}}function i(e){return`Your response has an invalid structure. Return JSON inside <output> tags:
2
+ <output>
3
+ {
4
+ "branches": [
5
+ ${e.map(e=>` { "branchId": "${e.branchId}", "orderedCommitIds": ${JSON.stringify(e.commitIds)} }`).join(`,
6
+ `)}
7
+ ],
8
+ "rationale": "[explanation]"
9
+ }
10
+ </output>
11
+
12
+ All branches must be present with all their commits ordered. Return only the <output> tag and no other text.`}export{n as validateOrderResponse};
@@ -0,0 +1,6 @@
1
+ import { PartitionInput, PartitionOutput } from "./types.js";
2
+
3
+ //#region src/tasks/partition/partition.d.ts
4
+ declare function partition(input: PartitionInput): Promise<PartitionOutput>;
5
+ //#endregion
6
+ export { partition };
@@ -0,0 +1 @@
1
+ import{TaskInputError as e}from"../../errors/task.js";import{runAiLoop as t}from"../ai-loop.js";import{applyUserPromptOverride as n}from"../types.js";import{buildPartitionContinuationPrompt as r,buildPartitionSystemPrompt as i,buildPartitionUserPrompt as a}from"./prompt.js";import{validatePartitionResponse as o}from"./validator.js";async function s(s){let c=s.commits,l=s.orderedCommitIds??c?.map(e=>e.id)??[];if(l.length===0)throw new e(`No commits provided: supply 'commits' or 'orderedCommitIds'`);if(s.session?.completedSteps.partition)return{result:s.session.completedSteps.partition,session:s.session,attempts:0,durationMs:0,callIds:[]};let u=n(s.session?r(s):a({commits:c,orderedCommitIds:l,sourceBranchName:s.sourceBranchName,baseBranch:s.baseBranch,instructions:s.instructions}),s.promptOverride),d=await t({model:s.model,systemPrompt:s.session?void 0:s.promptOverride?.system??i(s.conventions,s.strategy),userPrompt:u,session:s.session,maxRetries:s.maxRetries,onProgress:s.onProgress,signal:s.signal,onBeforeModelCall:s.onBeforeModelCall,onAfterModelCall:s.onAfterModelCall,onBeforePrompt:s.onBeforePrompt,validate:e=>o(e,l,s.existingBranchNames),taskName:`partition`}),f=s.session?.completedSteps??{},p={...d.session,completedSteps:{...f,partition:d.result}};return{result:d.result,session:p,usage:d.usage,attempts:d.attempts,durationMs:d.durationMs,callIds:d.callIds}}export{s as partition};
@@ -0,0 +1,16 @@
1
+ import { CommitSuggestion } from "../types.js";
2
+ import { ConventionDescriptor } from "../../git/utils/conventions.js";
3
+ import { PartitionInput, PartitionStrategy } from "./types.js";
4
+
5
+ //#region src/tasks/partition/prompt.d.ts
6
+ declare function buildPartitionSystemPrompt(conventions?: ConventionDescriptor, strategy?: PartitionStrategy): string;
7
+ declare function buildPartitionUserPrompt(params: {
8
+ commits: CommitSuggestion[];
9
+ orderedCommitIds: string[];
10
+ sourceBranchName?: string;
11
+ baseBranch?: string;
12
+ instructions?: string;
13
+ }): string;
14
+ declare function buildPartitionContinuationPrompt(input: PartitionInput): string;
15
+ //#endregion
16
+ export { buildPartitionContinuationPrompt, buildPartitionSystemPrompt, buildPartitionUserPrompt };
@@ -0,0 +1,61 @@
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.
2
+
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
+
5
+ Rules:
6
+ 1. Each sub-branch is a CONTIGUOUS range of commits from the ordered list
7
+ 2. Each sub-branch represents an independently reviewable and mergeable unit
8
+ 3. Earlier sub-branches can be merged first — later ones build on them
9
+ 4. A single sub-branch (no subdivision) is a perfectly valid and common outcome
10
+ 5. A single-commit sub-branch is valid if that commit is independently mergeable
11
+
12
+ ${i(t?.splitting)}
13
+
14
+ ${r}
15
+ - A short PR-style title (under 72 characters, imperative mood)
16
+ - A description of what the sub-branch accomplishes
17
+ - The commit IDs it contains (in order)
18
+
19
+ ## Required Output Format
20
+
21
+ Output your partition as JSON inside a <output> tag matching this schema:
22
+
23
+ \`\`\`json
24
+ ${n}
25
+ \`\`\`
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.
28
+ ${t}
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
+ ${t}
31
+ Prefer more partitions over fewer. Each stacked PR should tell one focused story.`:`Only partition when the sub-groups are independently reviewable AND independently mergeable, and a reviewer would genuinely benefit from seeing them as separate PRs.
32
+
33
+ Do NOT partition just because commits are distinct. A branch with a refactor commit followed by a feature commit that uses the refactor is a perfectly coherent single PR.
34
+
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
+
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(`
38
+ `)}\n</context>`),a&&(s+=`\n\nAdditional instructions:\n${a}`),s+=`
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+=`
41
+
42
+ Return the partition as JSON inside <output> tags:
43
+ <output>
44
+ {
45
+ "partitions": [
46
+ { "branchName": "[name]", "title": "[short PR title]", "description": "[what it does]", "commitIds": ["[id1]", "[id2]"] }
47
+ ],
48
+ "rationale": "[explanation]"
49
+ }
50
+ </output>
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}
53
+
54
+ Keep as a single branch unless sub-groups address completely different concerns with no merge-order preference. ${r}
55
+ When in doubt, do not partition.`:t===`aggressive`?`${n}
56
+
57
+ Subdivide whenever there is a clear concern boundary. Each stacked PR should tell one focused story. Prefer more partitions over fewer. ${r}`:`${n}
58
+
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
+
61
+ ${r}`}export{o as buildPartitionContinuationPrompt,r as buildPartitionSystemPrompt,a as buildPartitionUserPrompt};
@@ -0,0 +1,14 @@
1
+ import { z } from "zod";
2
+
3
+ //#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>;
13
+ //#endregion
14
+ export { partitionResultSchema };
@@ -0,0 +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};
@@ -0,0 +1,53 @@
1
+ import { ProgressEvent, TokenUsage } from "../../ai/types.js";
2
+ import { BranchPartition, CommitSuggestion, PromptOverride } from "../types.js";
3
+ import { ConventionDescriptor } from "../../git/utils/conventions.js";
4
+ import { OnAfterModelCall, OnBeforeModelCall, OnBeforePrompt } from "../ai-loop.js";
5
+ import { ComposeSession } from "../session.js";
6
+ import { AiModelPort } from "@gitkraken/shared-tools";
7
+
8
+ //#region src/tasks/partition/types.d.ts
9
+ interface PartitionStrategy {
10
+ /**
11
+ * How aggressively to subdivide a branch into stacked sub-branches.
12
+ * - "conservative": keep as single branch unless sub-groups are truly independent PRs
13
+ * - "balanced": subdivide when concerns are distinct and independently reviewable (default)
14
+ * - "aggressive": maximize subdivision; any concern boundary becomes a stack point
15
+ */
16
+ splitting?: 'conservative' | 'balanced' | 'aggressive';
17
+ }
18
+ interface PartitionInput {
19
+ commits?: CommitSuggestion[];
20
+ orderedCommitIds?: string[];
21
+ sourceBranchName?: string;
22
+ baseBranch?: string;
23
+ instructions?: string;
24
+ conventions?: ConventionDescriptor;
25
+ /** Controls how aggressively the AI subdivides into stacked branches. */
26
+ strategy?: PartitionStrategy;
27
+ /** Branch names that already exist (or are claimed by earlier phases) — used to detect collisions during validation. */
28
+ existingBranchNames?: Set<string>;
29
+ model: AiModelPort;
30
+ session?: ComposeSession;
31
+ maxRetries?: number;
32
+ onProgress?: (event: ProgressEvent) => void;
33
+ signal?: AbortSignal;
34
+ onBeforeModelCall?: OnBeforeModelCall;
35
+ onAfterModelCall?: OnAfterModelCall;
36
+ onBeforePrompt?: OnBeforePrompt;
37
+ /** Optional prompt override. When set, replaces or decorates the built-in prompts. */
38
+ promptOverride?: PromptOverride;
39
+ }
40
+ interface PartitionResult {
41
+ partitions: BranchPartition[];
42
+ rationale: string;
43
+ }
44
+ interface PartitionOutput {
45
+ result: PartitionResult;
46
+ session: ComposeSession;
47
+ usage?: TokenUsage;
48
+ attempts: number;
49
+ durationMs: number;
50
+ callIds: string[];
51
+ }
52
+ //#endregion
53
+ export { PartitionInput, PartitionOutput, PartitionResult, PartitionStrategy };
@@ -0,0 +1 @@
1
+ export{};
@@ -0,0 +1,7 @@
1
+ import { ValidationResult } from "../ai-loop.js";
2
+ import { PartitionResult } from "./types.js";
3
+
4
+ //#region src/tasks/partition/validator.d.ts
5
+ declare function validatePartitionResponse(responseText: string, knownIds: string[], existingBranchNames?: Set<string>): ValidationResult<PartitionResult>;
6
+ //#endregion
7
+ export { validatePartitionResponse };
@@ -0,0 +1,11 @@
1
+ import{extractOutputTag as e}from"../ai-loop.js";import{partitionResultSchema as t}from"./schema.js";function n(n,s,c){try{let l=e(n);if(!l)return a([`No <output> tag found in response`],o());let u=JSON.parse(l),d=t.safeParse(u);if(!d.success)return a(d.error.issues.map(e=>`${e.path.join(`.`)}: ${e.message}`),o());let f=d.data,p=[],m=[],h=new Set;for(let e=0;e<f.partitions.length;e++){let t=f.partitions[e];if(!i(t.branchName))return a([`Partition ${e}: '${t.branchName}' is not a valid git branch name`],`The branch name '${t.branchName}' is invalid. Git branch names cannot contain spaces, '..', '~', '^', ':', '\\', or end with '.lock'. Please provide a valid name.`);if(h.has(t.branchName))return a([`Duplicate partition branch name: '${t.branchName}'`],`Multiple partitions use the branch name '${t.branchName}'. Each partition must have a unique branch name. Please rename one of them.`);if(h.add(t.branchName),c?.has(t.branchName))return a([`Branch name '${t.branchName}' already exists in the repository`],`The branch name '${t.branchName}' already exists in the repository. Please choose a different name for partition ${e}.`);for(let e of t.commitIds)m.push(e);p.push({branchName:t.branchName,title:t.title,description:t.description,commitIds:t.commitIds})}let g=new Set,_=[];for(let e of m)g.has(e)&&_.push(e),g.add(e);if(_.length>0)return a([`Duplicate commit IDs: ${_.join(`, `)}`],`Some commits appear in multiple partitions. Each must be in exactly one.\n\nDuplicates: ${_.join(`, `)}`);let v=new Set(s),y=s.filter(e=>!g.has(e)),b=m.filter(e=>!v.has(e));return y.length>0?a([`Missing commits: ${y.join(`, `)}`],`Some commits are not assigned to any partition.\n\nMissing: ${y.join(`, `)}`):b.length>0?a([`Unknown commits: ${b.join(`, `)}`],`Some commit IDs don't match any known commit.\n\nUnknown: ${b.join(`, `)}`):r(p,s)?{valid:!0,result:{partitions:p,rationale:f.rationale}}:a([`Partitions are not contiguous`],`Each partition must contain a contiguous range of commits in the provided order. Please regroup into contiguous ranges.`)}catch{return a([`Failed to parse response as valid JSON`],o())}}function r(e,t){let n=new Map(t.map((e,t)=>[e,t])),r=0;for(let t of e)for(let e of t.commitIds){let t=n.get(e);if(t===void 0||t!==r)return!1;r++}return r===t.length}function i(e){return e.startsWith(`-`)||e.startsWith(`.`)||e.endsWith(`.lock`)||e.endsWith(`.`)||e.endsWith(`/`)||e.includes(`..`)||e.includes(`~`)||e.includes(`^`)||e.includes(`:`)||e.includes(`\\`)||e.includes(` `)||e.includes(`?`)||e.includes(`*`)||e.includes(`[`)||e.includes(`@{`)?!1:e.length>0}function a(e,t){return{valid:!1,errors:e,retryFeedback:t}}function o(){return`Your response has an invalid structure. Return JSON inside <output> tags:
2
+ <output>
3
+ {
4
+ "partitions": [
5
+ { "branchName": "[name]", "title": "[short PR title]", "description": "[what it does]", "commitIds": ["[id1]", "[id2]"] }
6
+ ],
7
+ "rationale": "[explanation]"
8
+ }
9
+ </output>
10
+
11
+ Return only the <output> tag and no other text.`}export{n as validatePartitionResponse};
@@ -0,0 +1,18 @@
1
+ import { ComposeHunk } from "../types.js";
2
+ import { ConventionDescriptor } from "../../git/utils/conventions.js";
3
+ import { GroupingStrategy } from "../group/types.js";
4
+
5
+ //#region src/tasks/quick-compose/prompt.d.ts
6
+ declare function buildQuickComposeSystemPrompt(conventions?: ConventionDescriptor, mode?: 'commits' | 'branches', strategy?: GroupingStrategy, includePartitions?: boolean): string;
7
+ declare function buildQuickComposeUserPrompt(params: {
8
+ hunks: ComposeHunk[];
9
+ commitMessages?: string[];
10
+ commitOrder?: string[];
11
+ branchName?: string;
12
+ baseBranch?: string;
13
+ issueUrl?: string;
14
+ pullRequestUrl?: string;
15
+ instructions?: string;
16
+ }): string;
17
+ //#endregion
18
+ export { buildQuickComposeSystemPrompt, buildQuickComposeUserPrompt };
@@ -0,0 +1,47 @@
1
+ import{quickComposeResultSchema as e}from"./schema.js";import{z as t}from"zod";const n=JSON.stringify(t.toJSONSchema(e),null,2);function r(e,t=`branches`,r,i=!1){let c=e?.promptFragment?`Write commit messages following the repository's conventions:\n\n${e.promptFragment}`:`Write commit messages:
2
+ - First line: concise summary, 50 chars max, third-person singular present tense ("Fixes", "Adds", "Improves")
3
+ - Then a blank line followed by details focusing on "why" not "what"`,l=a(r?.commitGranularity),u=t===`branches`?o(r?.branchSplitting,e):``,d=i?s():``;return`You are a fast AI code organizer. Given a set of code diff hunks, organize them into well-structured commits${t===`branches`?` and branches`:``} in a single pass.
4
+
5
+ ## Your task
6
+
7
+ 1. **Group** hunks into logical, atomic commits. Each hunk goes in exactly one commit.
8
+ 2. **Order** commits within each branch so dependencies flow correctly. If commit A defines something commit B uses, A comes first. Rename hunks (isRename=true) must come before any hunks referencing the new file path.
9
+ 3. Write a short commit message and explanation for each commit.
10
+ ${t===`branches`?`4. Assign commits to branches — each branch is a cohesive unit of work for one PR.`:`4. Place all commits in a single branch with id "default", name "", title "", description "".`}
11
+ ${i?`5. If a branch should be split into stacked sub-branches for review, provide partitions.`:``}
12
+
13
+ ## Commit style
14
+ ${c}
15
+ ${l}
16
+ ${u}
17
+ ${d}
18
+
19
+ ## Rules
20
+ - Every hunk must be assigned to exactly one commit
21
+ - Every commit must contain at least one hunk — no empty commits
22
+ - orderedCommitIds must list every commit ID in the branch in the order they should be applied
23
+ - Keep it practical — focus on clean grouping over deep dependency analysis
24
+
25
+ ## Required Output Format
26
+
27
+ Output as JSON inside a <output> tag matching this schema:
28
+
29
+ \`\`\`json
30
+ ${n}
31
+ \`\`\`
32
+
33
+ Return only the <output> tag and no other text.`}function i(e){let{hunks:t,commitMessages:n,commitOrder:r,branchName:i,baseBranch:a,issueUrl:o,pullRequestUrl:s,instructions:c}=e,l=`Organize these code hunks into commits:
34
+
35
+ <hunks>
36
+ ${JSON.stringify(t.map(e=>({index:e.index,fileName:e.fileName,hunkHeader:e.hunkHeader,content:e.content,...e.isBinary?{isBinary:!0}:{},...e.isRename?{isRename:!0}:{},...e.originalFileName?{originalFileName:e.originalFileName}:{}})))}
37
+ </hunks>`,u=[];return i&&u.push(`Branch: ${i}`),a&&u.push(`Base: ${a}`),o&&u.push(`Issue: ${o}`),s&&u.push(`PR: ${s}`),u.length>0&&(l+=`\n\nContext:\n${u.join(`
38
+ `)}`),n?.length&&(l+=`\n\nOriginal commit messages (match their style):\n<commit_messages>\n${JSON.stringify(n)}\n</commit_messages>`),r?.length&&(l+=`\n\nOriginal commit order:\n${JSON.stringify(r)}`),c&&(l+=`\n\nAdditional instructions:\n${c}`),l+=`
39
+
40
+ Return only the <output> tag and no other text.`,l}function a(e){return e===`consolidated`?`
41
+ Prefer fewer, larger commits. Combine related changes into a single commit.`:e===`granular`?`
42
+ 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`
43
+ ## Partition guidelines (stacked sub-branches)
44
+ - If a branch has multiple distinct concerns, split it into partitions for stacked review.
45
+ - Each partition is a contiguous range of commits from orderedCommitIds.
46
+ - Each partition gets a unique, valid git branch name.
47
+ - Single partition (no split) is a valid outcome.`}export{r as buildQuickComposeSystemPrompt,i as buildQuickComposeUserPrompt};
@@ -0,0 +1,6 @@
1
+ import { QuickComposeInput, QuickComposeOutput } from "./types.js";
2
+
3
+ //#region src/tasks/quick-compose/quick-compose.d.ts
4
+ declare function quickCompose(input: QuickComposeInput): Promise<QuickComposeOutput>;
5
+ //#endregion
6
+ export { quickCompose };
@@ -0,0 +1 @@
1
+ import{TaskInputError as e}from"../../errors/task.js";import{runAiLoop as t}from"../ai-loop.js";import{applyUserPromptOverride as n}from"../types.js";import{buildQuickComposeSystemPrompt as r,buildQuickComposeUserPrompt as i}from"./prompt.js";import{validateQuickComposeResponse as a}from"./validator.js";async function o(o){let s=o.hunks,c=o.mode??`branches`,l=o.includePartitions??!1;if(!s||s.length===0)throw new e(`No hunks provided for quick compose`);if(o.session?.completedSteps.quickCompose)return{result:o.session.completedSteps.quickCompose,session:o.session,attempts:0,durationMs:0,callIds:[]};let u=o.promptOverride?.system??r(o.conventions,c,o.strategy,l),d=n(i({hunks:s,commitMessages:o.commitMessages,commitOrder:o.commitOrder,branchName:o.branchName,baseBranch:o.baseBranch,issueUrl:o.issueUrl,pullRequestUrl:o.pullRequestUrl,instructions:o.instructions}),o.promptOverride),f=await t({model:o.model,systemPrompt:u,userPrompt:d,maxRetries:o.maxRetries,onProgress:o.onProgress,signal:o.signal,onBeforeModelCall:o.onBeforeModelCall,onAfterModelCall:o.onAfterModelCall,onBeforePrompt:o.onBeforePrompt,validate:e=>a(e,s.length,c,l,o.existingBranchNames),taskName:`quick-compose`}),p=o.session?.completedSteps??{},m={...f.session,completedSteps:{...p,quickCompose:f.result}};return{result:f.result,session:m,usage:f.usage,attempts:f.attempts,durationMs:f.durationMs,callIds:f.callIds}}export{o as quickCompose};
@@ -0,0 +1,27 @@
1
+ import { z } from "zod";
2
+
3
+ //#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>;
26
+ //#endregion
27
+ export { quickComposeResultSchema };
@@ -0,0 +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};
@@ -0,0 +1,63 @@
1
+ import { ProgressEvent, TokenUsage } from "../../ai/types.js";
2
+ import { BranchPartition, CommitSuggestion, ComposeHunk, PromptOverride } from "../types.js";
3
+ import { ConventionDescriptor } from "../../git/utils/conventions.js";
4
+ import { OnAfterModelCall, OnBeforeModelCall, OnBeforePrompt } from "../ai-loop.js";
5
+ import { GroupingStrategy } from "../group/types.js";
6
+ import { ComposeSession } from "../session.js";
7
+ import { AiModelPort } from "@gitkraken/shared-tools";
8
+
9
+ //#region src/tasks/quick-compose/types.d.ts
10
+ interface QuickComposeInput {
11
+ hunks: ComposeHunk[];
12
+ commitMessages?: string[];
13
+ commitOrder?: string[];
14
+ branchName?: string;
15
+ baseBranch?: string;
16
+ issueUrl?: string;
17
+ pullRequestUrl?: string;
18
+ instructions?: string;
19
+ conventions?: ConventionDescriptor;
20
+ /** "commits" = flat commit list (no branch naming). "branches" = AI decides branches. */
21
+ mode?: 'commits' | 'branches';
22
+ /** Whether to include partition info in the output (for auto target). */
23
+ includePartitions?: boolean;
24
+ /** Controls how aggressively the AI splits branches and commits. */
25
+ strategy?: GroupingStrategy;
26
+ /** Branch names that already exist — used to detect collisions during validation. */
27
+ existingBranchNames?: Set<string>;
28
+ model: AiModelPort;
29
+ /** Prior session to resume from. When `completedSteps.quickCompose` is present, skip the AI call. */
30
+ session?: ComposeSession;
31
+ maxRetries?: number;
32
+ onProgress?: (event: ProgressEvent) => void;
33
+ signal?: AbortSignal;
34
+ onBeforeModelCall?: OnBeforeModelCall;
35
+ onAfterModelCall?: OnAfterModelCall;
36
+ onBeforePrompt?: OnBeforePrompt;
37
+ /** Optional prompt override. When set, replaces or decorates the built-in prompts. */
38
+ promptOverride?: PromptOverride;
39
+ }
40
+ /** Raw combined result from the quick-compose AI output — one branch entry with embedded ordering+partitions. */
41
+ interface QuickComposeBranch {
42
+ id: string;
43
+ name: string;
44
+ title: string;
45
+ description: string;
46
+ commits: CommitSuggestion[];
47
+ orderedCommitIds: string[];
48
+ partitions?: BranchPartition[];
49
+ }
50
+ interface QuickComposeResult {
51
+ branches: QuickComposeBranch[];
52
+ rationale: string;
53
+ }
54
+ interface QuickComposeOutput {
55
+ result: QuickComposeResult;
56
+ session: ComposeSession;
57
+ usage?: TokenUsage;
58
+ attempts: number;
59
+ durationMs: number;
60
+ callIds: string[];
61
+ }
62
+ //#endregion
63
+ export { QuickComposeBranch, QuickComposeInput, QuickComposeOutput, QuickComposeResult };
@@ -0,0 +1 @@
1
+ export{};
@@ -0,0 +1,7 @@
1
+ import { ValidationResult } from "../ai-loop.js";
2
+ import { QuickComposeResult } from "./types.js";
3
+
4
+ //#region src/tasks/quick-compose/validator.d.ts
5
+ declare function validateQuickComposeResponse(responseText: string, totalHunks: number, mode?: 'commits' | 'branches', includePartitions?: boolean, existingBranchNames?: Set<string>): ValidationResult<QuickComposeResult>;
6
+ //#endregion
7
+ export { validateQuickComposeResponse };
@@ -0,0 +1,25 @@
1
+ import{extractOutputTag as e}from"../ai-loop.js";import{quickComposeResultSchema as t}from"./schema.js";function n(n,s,c=`branches`,l=!1,u){try{let d=e(n);if(!d)return a([`No <output> tag found in response`],o(c,l));let f=JSON.parse(d),p=t.safeParse(f);if(!p.success)return a(p.error.issues.map(e=>`${e.path.join(`.`)}: ${e.message}`),o(c,l));let m=p.data,h=new Set,g=new Set,_=new Set,v=[],y=new Set,b=[],x=[],S=[];for(let e of m.branches){if(h.has(e.id))return a([`Duplicate branch ID: '${e.id}'`],o(c,l));if(h.add(e.id),c===`branches`&&e.name.length===0)return a([`Branch '${e.id}': missing or empty 'name'`],o(c,l));if(c===`branches`&&e.name.length>0){if(g.has(e.name))return a([`Duplicate branch name: '${e.name}'`],`Multiple branches use the name '${e.name}'. Each branch must have a unique name. Please rename one of them.`);if(g.add(e.name),u?.has(e.name))return a([`Branch name '${e.name}' already exists in the repository`],`The branch name '${e.name}' already exists in the repository. Please choose a different name for branch '${e.id}'.`)}let t=[],n=new Set;for(let r of e.commits){y.has(r.id)&&b.push(r.id),y.add(r.id),n.add(r.id);let e=[];for(let t of r.hunks)_.has(t)&&v.push(t),_.add(t),e.push(t);e.length===0&&x.push(r.id),t.push({id:r.id,message:r.message,explanation:r.explanation,hunkIndices:e})}let s=new Set(e.orderedCommitIds);if(s.size!==e.orderedCommitIds.length)return a([`Branch '${e.id}': orderedCommitIds contains duplicates`],`The orderedCommitIds list for branch '${e.id}' contains duplicates. Each commit ID must appear exactly once.`);let d=[...n].filter(e=>!s.has(e)),f=e.orderedCommitIds.filter(e=>!n.has(e));if(d.length>0)return a([`Branch '${e.id}': orderedCommitIds is missing commits: ${d.join(`, `)}`],`The orderedCommitIds for branch '${e.id}' must include every commit. Missing: ${d.join(`, `)}`);if(f.length>0)return a([`Branch '${e.id}': orderedCommitIds references unknown commits: ${f.join(`, `)}`],`The orderedCommitIds for branch '${e.id}' references commits that don't exist. Unknown: ${f.join(`, `)}`);let p=e.partitions?[...e.partitions]:void 0;if(p){let t=new Set,n=[];for(let r=0;r<p.length;r++){let o=p[r];if(!i(o.branchName))return a([`Branch '${e.id}', partition ${r}: '${o.branchName}' is not a valid git branch name`],`The branch name '${o.branchName}' is invalid. Git branch names cannot contain spaces, '..', '~', '^', ':', '\\', or end with '.lock'. Please provide a valid name.`);if(t.has(o.branchName))return a([`Branch '${e.id}': duplicate partition name '${o.branchName}'`],`Multiple partitions use the branch name '${o.branchName}'. Each must be unique.`);if(t.add(o.branchName),u?.has(o.branchName))return a([`Partition branch name '${o.branchName}' already exists in the repository`],`The branch name '${o.branchName}' already exists in the repository. Please choose a different name.`);for(let e of o.commitIds)n.push(e)}let o=new Set(n),c=e.orderedCommitIds.filter(e=>!o.has(e)),l=n.filter(e=>!s.has(e));if(c.length>0)return a([`Branch '${e.id}': partition is missing commits: ${c.join(`, `)}`],`All commits must be assigned to a partition. Missing: ${c.join(`, `)}`);if(l.length>0)return a([`Branch '${e.id}': partition references unknown commits: ${l.join(`, `)}`],`Partition references unknown commits: ${l.join(`, `)}`);if(!r(p,e.orderedCommitIds))return a([`Branch '${e.id}': partitions are not contiguous`],`Each partition must contain a contiguous range of commits in the ordered list. Please regroup into contiguous ranges.`)}S.push({id:e.id,name:e.name,title:e.title,description:e.description,commits:t,orderedCommitIds:e.orderedCommitIds,partitions:p})}if(b.length>0)return a([`Duplicate commit IDs: ${b.join(`, `)}`],`Some commit IDs appear multiple times. Each must be unique.\n\nDuplicates: ${b.join(`, `)}`);if(x.length>0)return a([`Empty commits (no hunks assigned): ${x.join(`, `)}`],`Every commit must contain at least one hunk. These commits have an empty \`hunks\` array: ${x.join(`, `)}.
2
+
3
+ Either assign at least one hunk to each of these commits, or remove them entirely. If a commit's purpose is already covered by another commit, remove the duplicate. If a commit is supposed to cover specific hunks, assign those hunk indices to it. Every hunk index still needs to appear in exactly one commit.`);if(v.length>0)return a([`Duplicate hunks: ${v.join(`, `)}`],`Some hunks appear in multiple commits. Each hunk can only be used once.\n\nDuplicate hunks: ${v.join(`, `)}`);let C=new Set(Array.from({length:s},(e,t)=>t)),w=[...C].filter(e=>!_.has(e)),T=[..._].filter(e=>!C.has(e));return w.length>0?a([`Missing hunks: ${w.join(`, `)}`],`Some hunks are not assigned to any commit. All hunks must be included.\n\nMissing: ${w.join(`, `)}`):T.length>0?a([`Extra hunks: ${T.join(`, `)}`],`Some hunks in your response don't exist in the input.\n\nExtra: ${T.join(`, `)}`):{valid:!0,result:{branches:S,rationale:m.rationale}}}catch{return a([`Failed to parse response as valid JSON`],o(c,l))}}function r(e,t){let n=new Map(t.map((e,t)=>[e,t])),r=0;for(let t of e)for(let e of t.commitIds){let t=n.get(e);if(t===void 0||t!==r)return!1;r++}return r===t.length}function i(e){return e.startsWith(`-`)||e.startsWith(`.`)||e.endsWith(`.lock`)||e.endsWith(`.`)||e.endsWith(`/`)||e.includes(`..`)||e.includes(`~`)||e.includes(`^`)||e.includes(`:`)||e.includes(`\\`)||e.includes(` `)||e.includes(`?`)||e.includes(`*`)||e.includes(`[`)||e.includes(`@{`)?!1:e.length>0}function a(e,t){return{valid:!1,errors:e,retryFeedback:t}}function o(e,t){return`Your response has an invalid structure. Please return JSON inside <output> tags:
4
+ <output>
5
+ {
6
+ "branches": [
7
+ {
8
+ "id": ${e===`commits`?`"default"`:`"[branch id]"`},
9
+ "name": ${e===`commits`?`""`:`"[branch name]"`},
10
+ "title": ${e===`commits`?`""`:`"[short PR title]"`},
11
+ "description": ${e===`commits`?`""`:`"[branch description]"`},
12
+ "commits": [
13
+ { "id": "[commit id]", "message": "[message]", "explanation": "[explanation]", "hunks": [0, 1, 2] }
14
+ ],
15
+ "orderedCommitIds": ["[commit-id-1]", "[commit-id-2]"]${t?`,
16
+ "partitions": [
17
+ { "branchName": "[name]", "title": "[title]", "description": "[desc]", "commitIds": ["[id1]", "[id2]"] }
18
+ ]`:``}
19
+ }
20
+ ],
21
+ "rationale": "[explanation]"
22
+ }
23
+ </output>
24
+
25
+ Return only the <output> tag and no other text.`}export{n as validateQuickComposeResponse};
@@ -0,0 +1,22 @@
1
+ import { AiSession } from "../ai/session.js";
2
+ import { AnalysisResult } from "./analyze/types.js";
3
+ import { GroupingResult } from "./group/types.js";
4
+ import { OrderingResult } from "./order/types.js";
5
+ import { QuickComposeResult } from "./quick-compose/types.js";
6
+ import { PartitionResult } from "./partition/types.js";
7
+
8
+ //#region src/tasks/session.d.ts
9
+ interface ComposeSession extends AiSession {
10
+ completedSteps: {
11
+ analyze?: AnalysisResult;
12
+ group?: GroupingResult;
13
+ composeGroup?: GroupingResult;
14
+ quickCompose?: QuickComposeResult;
15
+ order?: OrderingResult;
16
+ partition?: PartitionResult;
17
+ };
18
+ }
19
+ declare function createComposeSession(systemPrompt: string): ComposeSession;
20
+ declare function withCompletedStep<K extends keyof ComposeSession['completedSteps']>(aiSession: AiSession, incomingSteps: ComposeSession['completedSteps'], key: K, value: NonNullable<ComposeSession['completedSteps'][K]>): ComposeSession;
21
+ //#endregion
22
+ export { ComposeSession, createComposeSession, withCompletedStep };
@@ -0,0 +1 @@
1
+ function e(e){return{messages:[],systemPrompt:e,completedSteps:{}}}function t(e,t,n,r){return{...e,completedSteps:{...t,[n]:r}}}export{e as createComposeSession,t as withCompletedStep};