@gitkraken/compose-tools 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +72 -1
- package/README.md +109 -22
- package/dist/errors/workflow.d.ts +16 -6
- package/dist/errors/workflow.js +1 -1
- package/dist/git/apply-branches.d.ts +0 -2
- package/dist/git/apply-branches.js +1 -1
- package/dist/git/apply-target.d.ts +10 -3
- package/dist/git/apply-target.js +1 -1
- package/dist/git/commit-chain.d.ts +47 -33
- package/dist/git/commit-chain.js +7 -5
- package/dist/git/diff-collector.d.ts +69 -19
- package/dist/git/diff-collector.js +6 -7
- package/dist/git/index.d.ts +2 -2
- package/dist/git/index.js +1 -1
- package/dist/git/port-dispatch.d.ts +11 -3
- package/dist/git/port-dispatch.js +4 -3
- package/dist/git/reparent.d.ts +39 -0
- package/dist/git/reparent.js +5 -0
- package/dist/git/require-exec.js +1 -1
- package/dist/git/safety-range.d.ts +33 -0
- package/dist/git/safety-range.js +5 -0
- package/dist/git/safety.d.ts +3 -3
- package/dist/git/safety.js +1 -1
- package/dist/git/types.d.ts +16 -15
- package/dist/git/utils/authorship.d.ts +22 -0
- package/dist/git/utils/authorship.js +1 -0
- package/dist/git/utils/conventions.d.ts +0 -1
- package/dist/git/utils/conventions.js +4 -4
- package/dist/git/utils/hunk-parser.d.ts +1 -1
- package/dist/git/utils/hunk-parser.js +1 -1
- package/dist/git/utils/temp-index.d.ts +1 -1
- package/dist/git/utils/temp-index.js +1 -1
- package/dist/index.d.ts +5 -2
- package/dist/index.js +1 -1
- package/dist/ports/git.d.ts +48 -7
- package/dist/tasks/ai-loop.d.ts +9 -15
- package/dist/tasks/ai-loop.js +2 -2
- package/dist/tasks/analyze/analyze.js +1 -1
- package/dist/tasks/analyze/prompt.d.ts +2 -0
- package/dist/tasks/analyze/prompt.js +5 -5
- package/dist/tasks/analyze/types.d.ts +6 -1
- package/dist/tasks/analyze/validator.js +1 -1
- package/dist/tasks/compose-group/compose-group.js +1 -1
- package/dist/tasks/compose-group/prompt.d.ts +2 -0
- package/dist/tasks/compose-group/prompt.js +52 -46
- package/dist/tasks/compose-group/types.d.ts +6 -1
- package/dist/tasks/group/group.js +1 -1
- package/dist/tasks/group/prompt.d.ts +2 -0
- package/dist/tasks/group/prompt.js +45 -39
- package/dist/tasks/group/types.d.ts +6 -1
- package/dist/tasks/group/validator.js +9 -4
- package/dist/tasks/order/order.js +1 -1
- package/dist/tasks/order/types.d.ts +3 -1
- package/dist/tasks/order/validator.js +1 -1
- package/dist/tasks/partition/partition.js +1 -1
- package/dist/tasks/partition/types.d.ts +3 -1
- package/dist/tasks/partition/validator.js +1 -1
- package/dist/tasks/quick-compose/prompt.d.ts +2 -0
- package/dist/tasks/quick-compose/prompt.js +24 -15
- package/dist/tasks/quick-compose/quick-compose.js +1 -1
- package/dist/tasks/quick-compose/types.d.ts +6 -1
- package/dist/tasks/quick-compose/validator.js +10 -2
- package/dist/tasks/types.d.ts +10 -9
- package/dist/tasks/utils/redact.d.ts +14 -0
- package/dist/tasks/utils/redact.js +1 -0
- package/dist/utils/cancellation.d.ts +26 -0
- package/dist/utils/cancellation.js +1 -0
- package/dist/workflows/compose.d.ts +89 -10
- package/dist/workflows/compose.js +3 -2
- package/dist/workflows/split.d.ts +14 -3
- package/dist/workflows/split.js +3 -3
- package/dist/workflows/types.d.ts +46 -18
- package/dist/workflows/undo.d.ts +18 -11
- package/dist/workflows/undo.js +1 -1
- package/package.json +2 -2
- package/dist/cancellation.d.ts +0 -20
- package/dist/cancellation.js +0 -1
|
@@ -2,6 +2,7 @@ import { ProgressEvent, TokenUsage } from "../../ai/types.js";
|
|
|
2
2
|
import { ComposeHunk, PromptOverride } from "../types.js";
|
|
3
3
|
import { ConventionDescriptor } from "../../git/utils/conventions.js";
|
|
4
4
|
import { OnAfterModelCall, OnBeforeModelCall, OnBeforePrompt } from "../ai-loop.js";
|
|
5
|
+
import { RedactHunkContent } from "../utils/redact.js";
|
|
5
6
|
import { GroupingResult, GroupingStrategy } from "../group/types.js";
|
|
6
7
|
import { ComposeSession } from "../session.js";
|
|
7
8
|
import { AiModelPort } from "@gitkraken/shared-tools";
|
|
@@ -37,12 +38,16 @@ interface ComposeGroupInput {
|
|
|
37
38
|
lightweight?: boolean;
|
|
38
39
|
/** When true, sends only hunk metadata (fileName, hunkHeader, counts) without diff content. */
|
|
39
40
|
condensed?: boolean;
|
|
41
|
+
/** No-op when `condensed: true`. See {@link RedactHunkContent}. */
|
|
42
|
+
redactHunkContent?: RedactHunkContent;
|
|
40
43
|
model: AiModelPort;
|
|
41
44
|
/** Prior session to resume from. When `completedSteps.composeGroup` is present, skip the AI call. */
|
|
42
45
|
session?: ComposeSession;
|
|
43
46
|
maxRetries?: number;
|
|
47
|
+
/** Output-token cap forwarded to `runAiLoop`. Default 16384. */
|
|
48
|
+
maxTokens?: number;
|
|
44
49
|
onProgress?: (event: ProgressEvent) => void;
|
|
45
|
-
|
|
50
|
+
cancellation?: AbortSignal;
|
|
46
51
|
onBeforeModelCall?: OnBeforeModelCall;
|
|
47
52
|
onAfterModelCall?: OnAfterModelCall;
|
|
48
53
|
onBeforePrompt?: OnBeforePrompt;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{TaskInputError as e}from"../../errors/task.js";import{runAiLoop as t}from"../ai-loop.js";import{applyUserPromptOverride as n}from"../types.js";import{validateGroupResponse as r}from"./validator.js";import{buildGroupContinuationPrompt as i,buildGroupSystemPrompt as a,buildGroupUserPrompt as o}from"./prompt.js";async function s(s){let c=s.hunks,l=s.mode??`branches`;if((!c||c.length===0)&&!s.session)throw new e(`No hunks provided: supply 'hunks' or run analyze() first with a session`);if(s.session?.completedSteps.group)return{result:s.session.completedSteps.group,session:s.session,attempts:0,durationMs:0,callIds:[]};let u=n(s.session?i(s,l):o({hunks:c,commitMessages:s.commitMessages,instructions:s.instructions,mode:l}),s.promptOverride),d=c?.length??s.session?.completedSteps.analyze?.hunkAnnotations.length??0;if(d===0)throw new e(`Cannot validate grouping: no hunk count available from input or session`);let f=await t({model:s.model,systemPrompt:s.session?void 0:s.promptOverride?.system??a(s.conventions,l,s.strategy),userPrompt:u,session:s.session,maxRetries:s.maxRetries,onProgress:s.onProgress,
|
|
1
|
+
import{TaskInputError as e}from"../../errors/task.js";import{runAiLoop as t}from"../ai-loop.js";import{applyUserPromptOverride as n}from"../types.js";import{validateGroupResponse as r}from"./validator.js";import{buildGroupContinuationPrompt as i,buildGroupSystemPrompt as a,buildGroupUserPrompt as o}from"./prompt.js";async function s(s){let c=s.hunks,l=s.mode??`branches`;if((!c||c.length===0)&&!s.session)throw new e(`No hunks provided: supply 'hunks' or run analyze() first with a session`);if(s.session?.completedSteps.group)return{result:s.session.completedSteps.group,session:s.session,attempts:0,durationMs:0,callIds:[]};let u=n(s.session?i(s,l):o({hunks:c,commitMessages:s.commitMessages,instructions:s.instructions,mode:l,redactHunkContent:s.redactHunkContent}),s.promptOverride),d=c?.length??s.session?.completedSteps.analyze?.hunkAnnotations.length??0;if(d===0)throw new e(`Cannot validate grouping: no hunk count available from input or session`);let f=await t({model:s.model,systemPrompt:s.session?void 0:s.promptOverride?.system??a(s.conventions,l,s.strategy),userPrompt:u,session:s.session,maxRetries:s.maxRetries,maxTokens:s.maxTokens,onProgress:s.onProgress,cancellation:s.cancellation,onBeforeModelCall:s.onBeforeModelCall,onAfterModelCall:s.onAfterModelCall,onBeforePrompt:s.onBeforePrompt,validate:e=>r(e,d,l,s.existingBranchNames),taskName:`group`}),p=s.session?.completedSteps??{},m={...f.session,completedSteps:{...p,group:f.result}};return{result:f.result,session:m,usage:f.usage,attempts:f.attempts,durationMs:f.durationMs,callIds:f.callIds}}export{s as group};
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { ComposeHunk } from "../types.js";
|
|
2
2
|
import { ConventionDescriptor } from "../../git/utils/conventions.js";
|
|
3
|
+
import { RedactHunkContent } from "../utils/redact.js";
|
|
3
4
|
import { GroupInput, GroupingStrategy } from "./types.js";
|
|
4
5
|
|
|
5
6
|
//#region src/tasks/group/prompt.d.ts
|
|
@@ -9,6 +10,7 @@ declare function buildGroupUserPrompt(params: {
|
|
|
9
10
|
commitMessages?: string[];
|
|
10
11
|
instructions?: string;
|
|
11
12
|
mode?: 'commits' | 'branches';
|
|
13
|
+
redactHunkContent?: RedactHunkContent;
|
|
12
14
|
}): string;
|
|
13
15
|
declare function buildGroupContinuationPrompt(input: GroupInput, mode: 'commits' | 'branches'): string;
|
|
14
16
|
//#endregion
|
|
@@ -1,56 +1,65 @@
|
|
|
1
|
-
import{
|
|
2
|
-
-
|
|
3
|
-
-
|
|
4
|
-
-
|
|
1
|
+
import{hunkContentForPrompt as e}from"../utils/redact.js";import{groupingResultSchema as t}from"./schema.js";import{z as n}from"zod";const r=JSON.stringify(n.toJSONSchema(t),null,2),i=`5. Write a full commit message for each commit containing both a summary and a description:
|
|
2
|
+
- **Summary** (first line): concise, 50 chars max, third-person singular present tense ("Fixes", "Adds", "Improves"). This line stands alone as the commit subject.
|
|
3
|
+
- **Blank line**: mandatory separator between summary and description.
|
|
4
|
+
- **Description** (body): one or more lines explaining *why* this change was made, not *what* it does. Focus on motivation, trade-offs, or context a reviewer needs. For non-trivial changes, always include a description — don't leave it at just the summary.
|
|
5
|
+
- Informal yet professional tone.`,a=`Each branch must have a descriptive name, a short PR-style title (under 72 characters, imperative mood), and at least one commit.`;function o(e,t=`branches`,n){return t===`commits`?s(e,n):c(e,n)}function s(e,t){return`You are an advanced AI programming assistant tasked with organizing code changes into commits. You will be working with individual code hunks.
|
|
6
|
+
|
|
7
|
+
## Critical constraints (output is rejected if violated)
|
|
8
|
+
|
|
9
|
+
1. **Each hunk index appears exactly once across the entire response.** Not zero. Not two. Exactly one occurrence — meaning it appears in exactly one commit's \`hunks\` array, and only once within that array. Listing the same index twice inside one commit's \`hunks\`, OR listing it in two different commits, both count as duplicates and both fail validation. This is the most common failure mode when two hunks look similar (same file, same kind of change).
|
|
10
|
+
2. **All hunk indices 0..N-1 must appear** across the union of every commit's \`hunks\` array.
|
|
11
|
+
3. Each commit must contain at least one hunk. No empty commits.
|
|
12
|
+
4. Each commit's \`id\` must be unique (e.g. \`commit-1\`, \`commit-2\`).
|
|
13
|
+
|
|
14
|
+
Before producing the \`<output>\` tag, do a self-check: count the total number of hunk indices across every commit's \`hunks\` array. That count MUST equal the input hunk count. If it does not match, you have either duplicated or skipped a hunk — find and fix it before outputting.
|
|
5
15
|
|
|
6
16
|
## Commit guidelines
|
|
7
17
|
|
|
8
18
|
1. Group hunks into logical units that make sense together and can be applied atomically
|
|
9
|
-
2.
|
|
10
|
-
3. Ensure each commit is self-contained and atomic
|
|
11
|
-
4. Each commit must have a unique ID (e.g., "commit-1", "commit-2")
|
|
12
|
-
5. Every commit must contain at least one hunk. Do not produce empty commits
|
|
19
|
+
2. Ensure each commit is self-contained and atomic
|
|
13
20
|
${e?.promptFragment?`5. Write a commit message for each commit following the repository's conventions (see below).
|
|
14
21
|
6. Write a detailed explanation for each commit as if explaining to a reviewer.
|
|
15
22
|
|
|
16
|
-
${e.promptFragment}`:`${
|
|
23
|
+
${e.promptFragment}`:`${i}
|
|
17
24
|
6. Write a detailed explanation for each commit as if explaining to a reviewer`}
|
|
18
|
-
${
|
|
25
|
+
${u(t?.commitGranularity)}
|
|
19
26
|
|
|
20
27
|
## Required Output Format
|
|
21
28
|
|
|
22
29
|
Output your organization as JSON inside a <output> tag matching this schema:
|
|
23
30
|
|
|
24
31
|
\`\`\`json
|
|
25
|
-
${
|
|
32
|
+
${r}
|
|
26
33
|
\`\`\`
|
|
27
34
|
|
|
28
35
|
Wrap all commits in a single branch object with id "default", name "", title "", and description "".
|
|
29
36
|
|
|
30
|
-
|
|
31
|
-
- Every hunk must be assigned to exactly one commit
|
|
32
|
-
- Every commit must contain at least one hunk (no empty commits)
|
|
33
|
-
- Use exactly one branch with id "default", name "", title "", and description ""
|
|
34
|
-
- Return only the <output> tag and no other text.`}function s(e,t){let i=e?.promptFragment?`5. Write a commit message for each commit following the repository's conventions (see below).
|
|
37
|
+
Return only the <output> tag and no other text.`}function c(e,t){let n=e?.promptFragment?`5. Write a commit message for each commit following the repository's conventions (see below).
|
|
35
38
|
6. Write a detailed explanation for each commit as if explaining to a reviewer.
|
|
36
39
|
|
|
37
|
-
${e.promptFragment}`:`${
|
|
38
|
-
6. Write a detailed explanation for each commit as if explaining to a reviewer`,a=
|
|
40
|
+
${e.promptFragment}`:`${i}
|
|
41
|
+
6. Write a detailed explanation for each commit as if explaining to a reviewer`,a=l(t?.branchSplitting,e);return`You are an advanced AI programming assistant tasked with organizing code changes into commits and branches. You will be working with individual code hunks.
|
|
39
42
|
|
|
40
43
|
Your job has two levels:
|
|
41
44
|
|
|
42
45
|
**Level 1: Commits** — group hunks into logical, atomic commits.
|
|
43
46
|
**Level 2: Branches** — group commits into branches, where each branch is a cohesive unit of work suitable for one pull request.
|
|
44
47
|
|
|
48
|
+
## Critical constraints (output is rejected if violated)
|
|
49
|
+
|
|
50
|
+
1. **Each hunk index appears exactly once across the entire response.** Not zero. Not two. Exactly one occurrence — meaning it appears in exactly one branch, in exactly one commit within that branch, and only once within that commit's \`hunks\` array. Listing the same index twice inside one commit's \`hunks\`, OR listing it in two different commits, OR listing it in two different branches, all count as duplicates and all fail validation. This is the most common failure mode when two hunks look similar (same file, same kind of change).
|
|
51
|
+
2. **All hunk indices 0..N-1 must appear** across the union of every commit's \`hunks\` array across every branch.
|
|
52
|
+
3. Each commit must contain at least one hunk. No empty commits.
|
|
53
|
+
4. Each commit \`id\` must be unique across the whole response.
|
|
54
|
+
|
|
55
|
+
Before producing the \`<output>\` tag, do a self-check: count the total number of hunk indices across every commit's \`hunks\` array (across every branch). That count MUST equal the input hunk count. If it does not match, you have either duplicated or skipped a hunk — find and fix it before outputting.
|
|
56
|
+
|
|
45
57
|
## Commit guidelines
|
|
46
58
|
|
|
47
59
|
1. Group hunks into logical units that make sense together and can be applied atomically
|
|
48
|
-
2.
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
5. Every commit must contain at least one hunk. Do not produce empty commits
|
|
52
|
-
${i}
|
|
53
|
-
${l(t?.commitGranularity)}
|
|
60
|
+
2. Ensure each commit is self-contained and atomic
|
|
61
|
+
${n}
|
|
62
|
+
${u(t?.commitGranularity)}
|
|
54
63
|
|
|
55
64
|
## Branch guidelines
|
|
56
65
|
|
|
@@ -61,14 +70,11 @@ ${a}
|
|
|
61
70
|
Output your organization as JSON inside a <output> tag matching this schema:
|
|
62
71
|
|
|
63
72
|
\`\`\`json
|
|
64
|
-
${
|
|
73
|
+
${r}
|
|
65
74
|
\`\`\`
|
|
66
75
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
- Every commit must contain at least one hunk (no empty commits)
|
|
70
|
-
- Return only the <output> tag and no other text.`}function c(e,t){let n=`- Each commit belongs to exactly one branch.
|
|
71
|
-
${t?.promptFragment?`- ${i} Follow the repository's branch naming conventions (see above).`:`- ${i}`}
|
|
76
|
+
Return only the <output> tag and no other text.`}function l(e,t){let n=`- Each commit belongs to exactly one branch.
|
|
77
|
+
${t?.promptFragment?`- ${a} Follow the repository's branch naming conventions (see above).`:`- ${a}`}
|
|
72
78
|
- Changes addressing the same pattern across different files belong in ONE branch. For example, applying the same bug fix to multiple functions, or updating similar config across modules — these are the same concern even if the files are different.`,r=`- Consider merge-order preference, not just code dependencies. Even if two concerns compile independently, they may need to be in the same branch or stacked branches if merging one without the other would be confusing or incomplete (e.g., documentation that describes utilities introduced in other commits should be in the same branch or stacked after those utilities).`;return e===`conservative`?`${n}
|
|
73
79
|
- Default to a single branch. Only create multiple branches when concerns are completely unrelated — different domains, different purposes, and no merge-order preference between them.
|
|
74
80
|
${r}
|
|
@@ -79,22 +85,22 @@ ${r}
|
|
|
79
85
|
- Create separate branches when concerns are genuinely independent — meaning they address different problems or features, and a reviewer would gain nothing from seeing them together in one PR. Two commits that each add a separate utility function unrelated to each other are good candidates for independent branches.
|
|
80
86
|
- Keep changes in the same branch when they form a coherent story that benefits from being reviewed together, even if they could technically stand alone.
|
|
81
87
|
${r}
|
|
82
|
-
- When in doubt, consider: "Would a reviewer want to see these changes together, or would separating them make review easier?" If separating helps, split.`}function
|
|
88
|
+
- When in doubt, consider: "Would a reviewer want to see these changes together, or would separating them make review easier?" If separating helps, split.`}function u(e){return e===`consolidated`?`
|
|
83
89
|
Prefer fewer, larger commits. Combine related changes into a single commit when they serve the same purpose. A commit can touch many files if they are all part of the same logical change.`:e===`granular`?`
|
|
84
|
-
Prefer more, smaller commits. Each distinct change should be its own commit, even if multiple changes serve a similar purpose. Aim for commits that touch as few files as possible.`:``}function
|
|
90
|
+
Prefer more, smaller commits. Each distinct change should be its own commit, even if multiple changes serve a similar purpose. Aim for commits that touch as few files as possible.`:``}function d(t){let{hunks:n,commitMessages:r,instructions:i,mode:a=`branches`,redactHunkContent:o}=t,s=JSON.stringify(n.map(t=>({index:t.index,fileName:t.fileName,hunkHeader:t.hunkHeader,content:e(t,o),...t.isBinary?{isBinary:!0}:{},...t.isRename?{isRename:!0}:{},...t.originalFileName?{originalFileName:t.originalFileName}:{}}))),c=`${a===`commits`?`Examine the following ${n.length} code hunks (indices 0 to ${n.length-1}) and organize them into commits. Every index must appear exactly once.`:`Examine the following ${n.length} code hunks (indices 0 to ${n.length-1}) and organize them into commits and branches. Every index must appear exactly once.`}
|
|
85
91
|
|
|
86
92
|
<hunks>
|
|
87
|
-
${
|
|
88
|
-
</hunks>`;return
|
|
93
|
+
${s}
|
|
94
|
+
</hunks>`;return r?.length&&(c+=`
|
|
89
95
|
|
|
90
96
|
The following commit messages are from the original commits. Use them to understand intent and match their style:
|
|
91
97
|
|
|
92
98
|
<commit_messages>
|
|
93
|
-
${JSON.stringify(
|
|
94
|
-
</commit_messages>`),
|
|
99
|
+
${JSON.stringify(r)}
|
|
100
|
+
</commit_messages>`),i&&(c+=`
|
|
95
101
|
|
|
96
102
|
Carefully follow these additional instructions, but do not deviate from the output structure:
|
|
97
|
-
${
|
|
103
|
+
${i}`),c+=`\n\n${a===`commits`?`Now organize the hunks into commits.`:`Now organize the hunks into commits and branches.`} Return only the <output> tag and no other text.`,c}function f(e,t){let n;return n=t===`commits`?`Based on your analysis of the changes, now organize the hunks into commits. Each hunk must be assigned to exactly one commit.`:p(e.strategy),e.conventions?.promptFragment&&(n+=`\n\n${e.conventions.promptFragment}`),e.commitMessages?.length&&(n+=`\n\nMatch the style of these original commit messages:\n${JSON.stringify(e.commitMessages)}`),e.instructions&&(n+=`\n\nAdditional instructions:\n${e.instructions}`),t===`commits`?n+=`
|
|
98
104
|
|
|
99
105
|
Return your grouping as JSON inside <output> tags. Wrap all commits in a single branch with id "default", name "", title "", description "":
|
|
100
106
|
<output>
|
|
@@ -142,7 +148,7 @@ Return your grouping as JSON inside <output> tags:
|
|
|
142
148
|
}
|
|
143
149
|
</output>
|
|
144
150
|
|
|
145
|
-
Return only the <output> tag and no other text.`,n}function
|
|
151
|
+
Return only the <output> tag and no other text.`,n}function p(e){let t=e?.branchSplitting??`balanced`,n=`Based on your analysis of the changes, now organize the hunks into commits and branches. Each hunk must be assigned to exactly one commit, and each commit belongs to exactly one branch.`,r=`Consider merge-order preference: even if two concerns compile independently, they should be in the same branch or stacked if merging one without the other would be incomplete or confusing (e.g., documentation for utilities should be with or after those utilities).`;return t===`conservative`?`${n}
|
|
146
152
|
|
|
147
153
|
Default to a single branch. Only create multiple branches when concerns are completely unrelated — different domains, different purposes, no merge-order preference.
|
|
148
154
|
${r}
|
|
@@ -155,4 +161,4 @@ Create separate branches when concerns are genuinely independent — different p
|
|
|
155
161
|
|
|
156
162
|
Keep changes in the same branch when they form a coherent story that benefits from joint review, or when there is a merge-order preference between them.
|
|
157
163
|
${r}
|
|
158
|
-
When in doubt, consider: "Would a reviewer want to see these together, or would separating them make review easier?"`}export{
|
|
164
|
+
When in doubt, consider: "Would a reviewer want to see these together, or would separating them make review easier?"`}export{f as buildGroupContinuationPrompt,o as buildGroupSystemPrompt,d as buildGroupUserPrompt};
|
|
@@ -2,6 +2,7 @@ import { ProgressEvent, TokenUsage } from "../../ai/types.js";
|
|
|
2
2
|
import { BranchGroup, ComposeHunk, PromptOverride } from "../types.js";
|
|
3
3
|
import { ConventionDescriptor } from "../../git/utils/conventions.js";
|
|
4
4
|
import { OnAfterModelCall, OnBeforeModelCall, OnBeforePrompt } from "../ai-loop.js";
|
|
5
|
+
import { RedactHunkContent } from "../utils/redact.js";
|
|
5
6
|
import { AnalysisResult } from "../analyze/types.js";
|
|
6
7
|
import { ComposeSession } from "../session.js";
|
|
7
8
|
import { AiModelPort } from "@gitkraken/shared-tools";
|
|
@@ -35,11 +36,15 @@ interface GroupInput {
|
|
|
35
36
|
strategy?: GroupingStrategy;
|
|
36
37
|
/** Branch names that already exist in the repo — used to detect collisions during validation. */
|
|
37
38
|
existingBranchNames?: Set<string>;
|
|
39
|
+
/** See {@link RedactHunkContent}. */
|
|
40
|
+
redactHunkContent?: RedactHunkContent;
|
|
38
41
|
model: AiModelPort;
|
|
39
42
|
session?: ComposeSession;
|
|
40
43
|
maxRetries?: number;
|
|
44
|
+
/** Output-token cap forwarded to `runAiLoop`. Default 16384. Bump on very large diffs whose structured output overflows the cap mid-JSON. */
|
|
45
|
+
maxTokens?: number;
|
|
41
46
|
onProgress?: (event: ProgressEvent) => void;
|
|
42
|
-
|
|
47
|
+
cancellation?: AbortSignal;
|
|
43
48
|
onBeforeModelCall?: OnBeforeModelCall;
|
|
44
49
|
onAfterModelCall?: OnAfterModelCall;
|
|
45
50
|
onBeforePrompt?: OnBeforePrompt;
|
|
@@ -1,12 +1,17 @@
|
|
|
1
|
-
import{extractOutputTag as e}from"../ai-loop.js";import{groupingResultSchema as t}from"./schema.js";function n(n,a,o=`branches`,s){let c=null;try{if(c=e(n),!c)return r([`No <output> tag found in response`],i(o));let l=JSON.parse(c),u=t.safeParse(l);if(!u.success)return r(u.error.issues.map(e=>`${e.path.join(`.`)}: ${e.message}`),i(o));let d=u.data,f=[],p=new Set,m=[],h=new
|
|
1
|
+
import{extractOutputTag as e}from"../ai-loop.js";import{groupingResultSchema as t}from"./schema.js";function n(n,a,o=`branches`,s){let c=null;try{if(c=e(n),!c)return r([`No <output> tag found in response`],i(o));let l=JSON.parse(c),u=t.safeParse(l);if(!u.success)return r(u.error.issues.map(e=>`${e.path.join(`.`)}: ${e.message}`),i(o));let d=u.data,f=[],p=new Set,m=[],h=new Map,g=new Set,_=[],v=[],y=new Set,b=new Set;for(let e of d.branches){if(y.has(e.id))return r([`Duplicate branch ID: '${e.id}'`],i(o));if(y.add(e.id),o===`branches`&&e.name.length===0)return r([`Branch '${e.id}': missing or empty 'name'`],i(o));if(o===`branches`&&e.name.length>0){if(b.has(e.name))return r([`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(b.add(e.name),s?.has(e.name))return r([`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=[];for(let n of e.commits){g.has(n.id)&&_.push(n.id),g.add(n.id);let r=[],i=o===`branches`?`${e.id}/${n.id}`:n.id;for(let e of n.hunks){p.has(e)&&m.push(e),p.add(e),r.push(e);let t=h.get(e);t?t.push(i):h.set(e,[i])}r.length===0&&v.push(n.id),t.push({id:n.id,message:n.message,explanation:n.explanation,hunkIndices:r})}f.push({id:e.id,name:e.name,title:e.title,description:e.description,commits:t})}if(_.length>0)return r([`Duplicate commit IDs: ${_.join(`, `)}`],`Some commit IDs appear multiple times. Each must be unique.\n\nDuplicates: ${_.join(`, `)}`);if(v.length>0)return r([`Empty commits (no hunks assigned): ${v.join(`, `)}`],`Every commit must contain at least one hunk. These commits have an empty \`hunks\` array: ${v.join(`, `)}.
|
|
2
2
|
|
|
3
|
-
Either assign at least one hunk to each of these commits, or remove them entirely. If a commit's purpose is already covered by another commit, remove the duplicate. If a commit is supposed to cover specific hunks, assign those hunk indices to it. Every hunk index still needs to appear in exactly one commit.`);let
|
|
3
|
+
Either assign at least one hunk to each of these commits, or remove them entirely. If a commit's purpose is already covered by another commit, remove the duplicate. If a commit is supposed to cover specific hunks, assign those hunk indices to it. Every hunk index still needs to appear in exactly one commit.`);let x=new Set(Array.from({length:a},(e,t)=>t)),S=[...x].filter(e=>!p.has(e)),C=[...p].filter(e=>!x.has(e));if(m.length>0||S.length>0||C.length>0){let e=[],t=[];if(m.length>0){let n=[...new Set(m)].sort((e,t)=>e-t),r=n.map(e=>` hunk ${e} → ${(h.get(e)??[]).join(`, `)}`).join(`
|
|
4
|
+
`);e.push(`Duplicate hunks (each must appear in exactly ONE commit):\n${r}`),t.push(`Duplicate hunks: ${n.join(`, `)}`)}S.length>0&&(e.push(`Missing hunks (not assigned to any commit): ${S.join(`, `)}`),t.push(`Missing hunks: ${S.join(`, `)}`)),C.length>0&&(e.push(`Extra hunks (index out of range): ${C.join(`, `)}`),t.push(`Extra hunks: ${C.join(`, `)}`));let n=[...h.values()].reduce((e,t)=>e+t.length,0);return r(t,`There are ${a} hunks (indices 0 to ${a-1}). Every index must appear in exactly one commit — no duplicates, no gaps, no out-of-range indices.
|
|
4
5
|
|
|
5
|
-
Your response
|
|
6
|
+
Your response listed ${n} hunk references across ${h.size} unique indices. Expected: ${a} references across ${a} unique indices.
|
|
7
|
+
|
|
8
|
+
Specific problems:
|
|
6
9
|
${e.map(e=>`- ${e}`).join(`
|
|
7
10
|
`)}
|
|
8
11
|
|
|
9
|
-
|
|
12
|
+
To fix duplicates: for each duplicated hunk index, decide which one commit it truly belongs in and remove it from the others. Do not split a single hunk across two commits — git only knows how to apply a hunk once.
|
|
13
|
+
|
|
14
|
+
Return the corrected JSON inside <output> tags.`)}return{valid:!0,result:{branches:f}}}catch(e){let t=e instanceof Error?e.message:String(e),a=(c??n).slice(-300);return r([`Failed to parse response as valid JSON: ${t}`,`Response tail: ${a}`],i(o))}}function r(e,t){return{valid:!1,errors:e,retryFeedback:t}}function i(e){return e===`commits`?`Your response has an invalid structure. Please return JSON inside <output> tags. Use a single branch with id "default", name "", description "":
|
|
10
15
|
<output>
|
|
11
16
|
{
|
|
12
17
|
"branches": [
|
|
@@ -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{buildOrderContinuationPrompt as r,buildOrderSystemPrompt as i,buildOrderUserPrompt as a}from"./prompt.js";import{validateOrderResponse as o}from"./validator.js";async function s(s){let c=s.branches??s.session?.completedSteps.group?.branches;if((!c||c.length===0)&&!s.session)throw new e(`No branches provided: supply 'branches' or run group() first with a session`);if(s.session?.completedSteps.order)return{result:s.session.completedSteps.order,session:s.session,attempts:0,durationMs:0,callIds:[]};let l=(c??[]).map(e=>({branchId:e.id,commitIds:e.commits.map(e=>e.id)})),u=n(s.session?r(s):a({branches:c,hunks:s.hunks,instructions:s.instructions}),s.promptOverride),d=await t({model:s.model,systemPrompt:s.session?void 0:s.promptOverride?.system??i(),userPrompt:u,session:s.session,maxRetries:s.maxRetries,onProgress:s.onProgress,
|
|
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{buildOrderContinuationPrompt as r,buildOrderSystemPrompt as i,buildOrderUserPrompt as a}from"./prompt.js";import{validateOrderResponse as o}from"./validator.js";async function s(s){let c=s.branches??s.session?.completedSteps.group?.branches;if((!c||c.length===0)&&!s.session)throw new e(`No branches provided: supply 'branches' or run group() first with a session`);if(s.session?.completedSteps.order)return{result:s.session.completedSteps.order,session:s.session,attempts:0,durationMs:0,callIds:[]};let l=(c??[]).map(e=>({branchId:e.id,commitIds:e.commits.map(e=>e.id)})),u=n(s.session?r(s):a({branches:c,hunks:s.hunks,instructions:s.instructions}),s.promptOverride),d=await t({model:s.model,systemPrompt:s.session?void 0:s.promptOverride?.system??i(),userPrompt:u,session:s.session,maxRetries:s.maxRetries,maxTokens:s.maxTokens,onProgress:s.onProgress,cancellation:s.cancellation,onBeforeModelCall:s.onBeforeModelCall,onAfterModelCall:s.onAfterModelCall,onBeforePrompt:s.onBeforePrompt,validate:e=>o(e,l),taskName:`order`}),f=s.session?.completedSteps??{},p={...d.session,completedSteps:{...f,order:d.result}};return{result:d.result,session:p,usage:d.usage,attempts:d.attempts,durationMs:d.durationMs,callIds:d.callIds}}export{s as order};
|
|
@@ -12,8 +12,10 @@ interface OrderInput {
|
|
|
12
12
|
model: AiModelPort;
|
|
13
13
|
session?: ComposeSession;
|
|
14
14
|
maxRetries?: number;
|
|
15
|
+
/** Output-token cap forwarded to `runAiLoop`. Default 16384. */
|
|
16
|
+
maxTokens?: number;
|
|
15
17
|
onProgress?: (event: ProgressEvent) => void;
|
|
16
|
-
|
|
18
|
+
cancellation?: AbortSignal;
|
|
17
19
|
onBeforeModelCall?: OnBeforeModelCall;
|
|
18
20
|
onAfterModelCall?: OnAfterModelCall;
|
|
19
21
|
onBeforePrompt?: OnBeforePrompt;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{extractOutputTag as e}from"../ai-loop.js";import{orderingResultSchema as t}from"./schema.js";function n(n,a){
|
|
1
|
+
import{extractOutputTag as e}from"../ai-loop.js";import{orderingResultSchema as t}from"./schema.js";function n(n,a){let o=null;try{if(o=e(n),!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(e){let t=e instanceof Error?e.message:String(e),s=(o??n).slice(-300);return r([`Failed to parse response as valid JSON: ${t}`,`Response tail: ${s}`],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
2
|
<output>
|
|
3
3
|
{
|
|
4
4
|
"branches": [
|
|
@@ -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{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,
|
|
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,maxTokens:s.maxTokens,onProgress:s.onProgress,cancellation:s.cancellation,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};
|
|
@@ -29,8 +29,10 @@ interface PartitionInput {
|
|
|
29
29
|
model: AiModelPort;
|
|
30
30
|
session?: ComposeSession;
|
|
31
31
|
maxRetries?: number;
|
|
32
|
+
/** Output-token cap forwarded to `runAiLoop`. Default 16384. */
|
|
33
|
+
maxTokens?: number;
|
|
32
34
|
onProgress?: (event: ProgressEvent) => void;
|
|
33
|
-
|
|
35
|
+
cancellation?: AbortSignal;
|
|
34
36
|
onBeforeModelCall?: OnBeforeModelCall;
|
|
35
37
|
onAfterModelCall?: OnAfterModelCall;
|
|
36
38
|
onBeforePrompt?: OnBeforePrompt;
|
|
@@ -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){
|
|
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:
|
|
2
2
|
<output>
|
|
3
3
|
{
|
|
4
4
|
"partitions": [
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { ComposeHunk } from "../types.js";
|
|
2
2
|
import { ConventionDescriptor } from "../../git/utils/conventions.js";
|
|
3
|
+
import { RedactHunkContent } from "../utils/redact.js";
|
|
3
4
|
import { GroupingStrategy } from "../group/types.js";
|
|
4
5
|
|
|
5
6
|
//#region src/tasks/quick-compose/prompt.d.ts
|
|
@@ -13,6 +14,7 @@ declare function buildQuickComposeUserPrompt(params: {
|
|
|
13
14
|
issueUrl?: string;
|
|
14
15
|
pullRequestUrl?: string;
|
|
15
16
|
instructions?: string;
|
|
17
|
+
redactHunkContent?: RedactHunkContent;
|
|
16
18
|
}): string;
|
|
17
19
|
//#endregion
|
|
18
20
|
export { buildQuickComposeSystemPrompt, buildQuickComposeUserPrompt };
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
import{
|
|
2
|
-
|
|
3
|
-
|
|
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:
|
|
3
|
+
- **Summary** (first line): concise, 50 chars max, third-person singular present tense ("Fixes", "Adds", "Improves"). Stands alone as the commit subject.
|
|
4
|
+
- **Blank line**: mandatory separator.
|
|
5
|
+
- **Description** (body): one or more lines explaining *why* the change was made, not *what* it does. Focus on motivation, trade-offs, or context a reviewer needs. For non-trivial changes, always include a description.`,l=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
6
|
|
|
5
7
|
## Your task
|
|
6
8
|
|
|
@@ -11,15 +13,22 @@ ${t===`branches`?`4. Assign commits to branches — each branch is a cohesive un
|
|
|
11
13
|
${i?`5. If a branch should be split into stacked sub-branches for review, provide partitions.`:``}
|
|
12
14
|
|
|
13
15
|
## Commit style
|
|
14
|
-
${
|
|
16
|
+
${a}
|
|
15
17
|
${l}
|
|
16
18
|
${u}
|
|
17
19
|
${d}
|
|
18
20
|
|
|
21
|
+
## Critical constraints (output is rejected if violated)
|
|
22
|
+
|
|
23
|
+
1. **Each hunk index appears exactly once across the entire response.** Not zero. Not two. Exactly one occurrence — across all branches, across all commits, and only once within any commit's \`hunks\` array. Listing the same index twice inside one commit, OR in two different commits, OR in two different branches, all count as duplicates and all fail validation. This is the most common failure mode when two hunks look similar (same file, same kind of change).
|
|
24
|
+
2. **All hunk indices 0..N-1 must appear** across the union of every commit's \`hunks\` array across every branch.
|
|
25
|
+
3. Each commit must contain at least one hunk. No empty commits.
|
|
26
|
+
4. Each commit \`id\` must be unique across the whole response.
|
|
27
|
+
5. \`orderedCommitIds\` must list every commit ID in the branch in the order they should be applied.
|
|
28
|
+
|
|
29
|
+
Before producing the \`<output>\` tag, do a self-check: count the total number of hunk indices across every commit's \`hunks\` array (across every branch). That count MUST equal the input hunk count. If it does not match, you have either duplicated or skipped a hunk — find and fix it before outputting.
|
|
30
|
+
|
|
19
31
|
## 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
32
|
- Keep it practical — focus on clean grouping over deep dependency analysis
|
|
24
33
|
|
|
25
34
|
## Required Output Format
|
|
@@ -27,21 +36,21 @@ ${d}
|
|
|
27
36
|
Output as JSON inside a <output> tag matching this schema:
|
|
28
37
|
|
|
29
38
|
\`\`\`json
|
|
30
|
-
${
|
|
39
|
+
${r}
|
|
31
40
|
\`\`\`
|
|
32
41
|
|
|
33
|
-
Return only the <output> tag and no other text.`}function
|
|
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:
|
|
34
43
|
|
|
35
44
|
<hunks>
|
|
36
|
-
${JSON.stringify(
|
|
37
|
-
</hunks>`,
|
|
38
|
-
`)}`),
|
|
45
|
+
${JSON.stringify(n.map(t=>({index:t.index,fileName:t.fileName,hunkHeader:t.hunkHeader,content:e(t,u),...t.isBinary?{isBinary:!0}:{},...t.isRename?{isRename:!0}:{},...t.originalFileName?{originalFileName:t.originalFileName}:{}})))}
|
|
46
|
+
</hunks>`,f=[];return a&&f.push(`Branch: ${a}`),o&&f.push(`Base: ${o}`),s&&f.push(`Issue: ${s}`),c&&f.push(`PR: ${c}`),f.length>0&&(d+=`\n\nContext:\n${f.join(`
|
|
47
|
+
`)}`),r?.length&&(d+=`\n\nOriginal commit messages (match their style):\n<commit_messages>\n${JSON.stringify(r)}\n</commit_messages>`),i?.length&&(d+=`\n\nOriginal commit order:\n${JSON.stringify(i)}`),l&&(d+=`\n\nAdditional instructions:\n${l}`),d+=`
|
|
39
48
|
|
|
40
|
-
Return only the <output> tag and no other text.`,
|
|
49
|
+
Return only the <output> tag and no other text.`,d}function o(e){return e===`consolidated`?`
|
|
41
50
|
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
|
|
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`
|
|
43
52
|
## Partition guidelines (stacked sub-branches)
|
|
44
53
|
- If a branch has multiple distinct concerns, split it into partitions for stacked review.
|
|
45
54
|
- Each partition is a contiguous range of commits from orderedCommitIds.
|
|
46
55
|
- Each partition gets a unique, valid git branch name.
|
|
47
|
-
- Single partition (no split) is a valid outcome.`}export{
|
|
56
|
+
- Single partition (no split) is a valid outcome.`}export{i as buildQuickComposeSystemPrompt,a as buildQuickComposeUserPrompt};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{TaskInputError as e}from"../../errors/task.js";import{runAiLoop as t}from"../ai-loop.js";import{applyUserPromptOverride as n}from"../types.js";import{buildQuickComposeSystemPrompt as r,buildQuickComposeUserPrompt as i}from"./prompt.js";import{validateQuickComposeResponse as a}from"./validator.js";async function o(o){let s=o.hunks,c=o.mode??`branches`,l=o.includePartitions??!1;if(!s||s.length===0)throw new e(`No hunks provided for quick compose`);if(o.session?.completedSteps.quickCompose)return{result:o.session.completedSteps.quickCompose,session:o.session,attempts:0,durationMs:0,callIds:[]};let u=o.promptOverride?.system??r(o.conventions,c,o.strategy,l),d=n(i({hunks:s,commitMessages:o.commitMessages,commitOrder:o.commitOrder,branchName:o.branchName,baseBranch:o.baseBranch,issueUrl:o.issueUrl,pullRequestUrl:o.pullRequestUrl,instructions:o.instructions}),o.promptOverride),f=await t({model:o.model,systemPrompt:u,userPrompt:d,maxRetries:o.maxRetries,onProgress:o.onProgress,
|
|
1
|
+
import{TaskInputError as e}from"../../errors/task.js";import{runAiLoop as t}from"../ai-loop.js";import{applyUserPromptOverride as n}from"../types.js";import{buildQuickComposeSystemPrompt as r,buildQuickComposeUserPrompt as i}from"./prompt.js";import{validateQuickComposeResponse as a}from"./validator.js";async function o(o){let s=o.hunks,c=o.mode??`branches`,l=o.includePartitions??!1;if(!s||s.length===0)throw new e(`No hunks provided for quick compose`);if(o.session?.completedSteps.quickCompose)return{result:o.session.completedSteps.quickCompose,session:o.session,attempts:0,durationMs:0,callIds:[]};let u=o.promptOverride?.system??r(o.conventions,c,o.strategy,l),d=n(i({hunks:s,commitMessages:o.commitMessages,commitOrder:o.commitOrder,branchName:o.branchName,baseBranch:o.baseBranch,issueUrl:o.issueUrl,pullRequestUrl:o.pullRequestUrl,instructions:o.instructions,redactHunkContent:o.redactHunkContent}),o.promptOverride),f=await t({model:o.model,systemPrompt:u,userPrompt:d,maxRetries:o.maxRetries,maxTokens:o.maxTokens,onProgress:o.onProgress,cancellation:o.cancellation,onBeforeModelCall:o.onBeforeModelCall,onAfterModelCall:o.onAfterModelCall,onBeforePrompt:o.onBeforePrompt,validate:e=>a(e,s.length,c,l,o.existingBranchNames),taskName:`quick-compose`}),p=o.session?.completedSteps??{},m={...f.session,completedSteps:{...p,quickCompose:f.result}};return{result:f.result,session:m,usage:f.usage,attempts:f.attempts,durationMs:f.durationMs,callIds:f.callIds}}export{o as quickCompose};
|
|
@@ -2,6 +2,7 @@ import { ProgressEvent, TokenUsage } from "../../ai/types.js";
|
|
|
2
2
|
import { BranchPartition, CommitSuggestion, ComposeHunk, PromptOverride } from "../types.js";
|
|
3
3
|
import { ConventionDescriptor } from "../../git/utils/conventions.js";
|
|
4
4
|
import { OnAfterModelCall, OnBeforeModelCall, OnBeforePrompt } from "../ai-loop.js";
|
|
5
|
+
import { RedactHunkContent } from "../utils/redact.js";
|
|
5
6
|
import { GroupingStrategy } from "../group/types.js";
|
|
6
7
|
import { ComposeSession } from "../session.js";
|
|
7
8
|
import { AiModelPort } from "@gitkraken/shared-tools";
|
|
@@ -25,12 +26,16 @@ interface QuickComposeInput {
|
|
|
25
26
|
strategy?: GroupingStrategy;
|
|
26
27
|
/** Branch names that already exist — used to detect collisions during validation. */
|
|
27
28
|
existingBranchNames?: Set<string>;
|
|
29
|
+
/** See {@link RedactHunkContent}. */
|
|
30
|
+
redactHunkContent?: RedactHunkContent;
|
|
28
31
|
model: AiModelPort;
|
|
29
32
|
/** Prior session to resume from. When `completedSteps.quickCompose` is present, skip the AI call. */
|
|
30
33
|
session?: ComposeSession;
|
|
31
34
|
maxRetries?: number;
|
|
35
|
+
/** Output-token cap forwarded to `runAiLoop`. Default 16384. */
|
|
36
|
+
maxTokens?: number;
|
|
32
37
|
onProgress?: (event: ProgressEvent) => void;
|
|
33
|
-
|
|
38
|
+
cancellation?: AbortSignal;
|
|
34
39
|
onBeforeModelCall?: OnBeforeModelCall;
|
|
35
40
|
onAfterModelCall?: OnAfterModelCall;
|
|
36
41
|
onBeforePrompt?: OnBeforePrompt;
|
|
@@ -1,6 +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){
|
|
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(`, `)}.
|
|
2
2
|
|
|
3
|
-
Either assign at least one hunk to each of these commits, or remove them entirely. If a commit's purpose is already covered by another commit, remove the duplicate. If a commit is supposed to cover specific hunks, assign those hunk indices to it. Every hunk index still needs to appear in exactly one commit.`);if(v.length>0)
|
|
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.
|
|
5
|
+
|
|
6
|
+
Your response listed ${n} hunk references across ${y.size} unique indices. Expected: ${s} references across ${s} unique indices.
|
|
7
|
+
|
|
8
|
+
Duplicates (each hunk index → the commits it landed in):
|
|
9
|
+
${t}
|
|
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:
|
|
4
12
|
<output>
|
|
5
13
|
{
|
|
6
14
|
"branches": [
|
package/dist/tasks/types.d.ts
CHANGED
|
@@ -7,18 +7,18 @@ interface ComposeHunk {
|
|
|
7
7
|
content: string;
|
|
8
8
|
additions: number;
|
|
9
9
|
deletions: number;
|
|
10
|
-
source: 'staged' | 'unstaged' | 'untracked' | 'branch';
|
|
11
|
-
sourceCommitSha?: string;
|
|
12
10
|
isRename?: boolean;
|
|
13
11
|
originalFileName?: string;
|
|
14
12
|
isBinary?: boolean;
|
|
15
13
|
/** Full binary patch block (base85-encoded). Excluded from AI context, used only by patch builder. */
|
|
16
14
|
binaryPatchData?: string;
|
|
17
15
|
/**
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
16
|
+
* Likely author of this hunk. For `commit-range` and `branch` sources the
|
|
17
|
+
* library populates this via a line-range-overlap heuristic against the
|
|
18
|
+
* per-commit hunks of the source range; the author with the highest
|
|
19
|
+
* cumulative overlap wins, and other authors land in `coAuthors`. The apply
|
|
20
|
+
* phase aggregates `author` across the hunks of each new commit to pick the
|
|
21
|
+
* commit's primary author and `Co-authored-by` trailers.
|
|
22
22
|
*
|
|
23
23
|
* `date` is the commit's author date when known — used by consumers for UI
|
|
24
24
|
* display and, for the commit created by the apply path, as `GIT_AUTHOR_DATE`.
|
|
@@ -29,9 +29,10 @@ interface ComposeHunk {
|
|
|
29
29
|
date?: Date;
|
|
30
30
|
};
|
|
31
31
|
/**
|
|
32
|
-
* Additional authors
|
|
33
|
-
*
|
|
34
|
-
* produce the final Co-authored-by
|
|
32
|
+
* Additional likely authors of this hunk (those whose per-commit overlap
|
|
33
|
+
* was non-zero but didn't win the primary slot). Combined with `author`
|
|
34
|
+
* from all hunks in a new commit to produce the final `Co-authored-by`
|
|
35
|
+
* trailer list.
|
|
35
36
|
*/
|
|
36
37
|
coAuthors?: {
|
|
37
38
|
name: string;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { ComposeHunk } from "../types.js";
|
|
2
|
+
|
|
3
|
+
//#region src/tasks/utils/redact.d.ts
|
|
4
|
+
/** Placeholder substituted for hunk content when {@link RedactHunkContent} returns true. */
|
|
5
|
+
declare const REDACTED_HUNK_CONTENT = "[content redacted because this file is excluded from AI processing \u2014 still assign this hunk to a commit based on its filename]";
|
|
6
|
+
/**
|
|
7
|
+
* Returns `true` for hunks whose content should be redacted from AI prompts. The library
|
|
8
|
+
* keeps the hunk's `fileName`, `hunkHeader`, `index`, line counts, and rename metadata in
|
|
9
|
+
* the prompt; only the patch body is replaced.
|
|
10
|
+
*/
|
|
11
|
+
type RedactHunkContent = (hunk: ComposeHunk) => boolean;
|
|
12
|
+
declare function hunkContentForPrompt(hunk: ComposeHunk, redact: RedactHunkContent | undefined): string;
|
|
13
|
+
//#endregion
|
|
14
|
+
export { REDACTED_HUNK_CONTENT, RedactHunkContent, hunkContentForPrompt };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
const e=`[content redacted because this file is excluded from AI processing — still assign this hunk to a commit based on its filename]`;function t(t,n){return n?.(t)?e:t.content}export{e as REDACTED_HUNK_CONTENT,t as hunkContentForPrompt};
|