@gitkraken/compose-tools 0.1.0 → 0.2.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 +32 -1
- package/README.md +80 -14
- 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 +13 -2
- package/dist/git/apply-target.js +1 -1
- package/dist/git/commit-chain.d.ts +27 -7
- package/dist/git/commit-chain.js +7 -5
- package/dist/git/diff-collector.d.ts +37 -10
- package/dist/git/diff-collector.js +7 -7
- package/dist/git/port-dispatch.d.ts +9 -3
- package/dist/git/port-dispatch.js +3 -3
- package/dist/git/require-exec.js +1 -1
- package/dist/git/safety.d.ts +3 -3
- package/dist/git/safety.js +1 -1
- package/dist/git/types.d.ts +6 -1
- package/dist/git/utils/conventions.d.ts +0 -1
- package/dist/git/utils/conventions.js +4 -4
- 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 -3
- package/dist/index.js +1 -1
- package/dist/ports/git.d.ts +27 -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/types.d.ts +3 -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.js +27 -21
- package/dist/tasks/compose-group/types.d.ts +3 -1
- package/dist/tasks/group/group.js +1 -1
- package/dist/tasks/group/prompt.js +27 -21
- package/dist/tasks/group/types.d.ts +3 -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.js +14 -5
- package/dist/tasks/quick-compose/quick-compose.js +1 -1
- package/dist/tasks/quick-compose/types.d.ts +3 -1
- package/dist/tasks/quick-compose/validator.js +10 -2
- package/dist/utils/cancellation.d.ts +26 -0
- package/dist/utils/cancellation.js +1 -0
- package/dist/workflows/compose.d.ts +78 -10
- package/dist/workflows/compose.js +2 -2
- package/dist/workflows/split.d.ts +11 -3
- package/dist/workflows/split.js +3 -3
- package/dist/workflows/types.d.ts +5 -0
- 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
|
@@ -41,8 +41,10 @@ interface ComposeGroupInput {
|
|
|
41
41
|
/** Prior session to resume from. When `completedSteps.composeGroup` is present, skip the AI call. */
|
|
42
42
|
session?: ComposeSession;
|
|
43
43
|
maxRetries?: number;
|
|
44
|
+
/** Output-token cap forwarded to `runAiLoop`. Default 16384. */
|
|
45
|
+
maxTokens?: number;
|
|
44
46
|
onProgress?: (event: ProgressEvent) => void;
|
|
45
|
-
|
|
47
|
+
cancellation?: AbortSignal;
|
|
46
48
|
onBeforeModelCall?: OnBeforeModelCall;
|
|
47
49
|
onAfterModelCall?: OnAfterModelCall;
|
|
48
50
|
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}),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,15 +1,22 @@
|
|
|
1
|
-
import{groupingResultSchema as e}from"./schema.js";import{z as t}from"zod";const n=JSON.stringify(t.toJSONSchema(e),null,2),r=`5. Write a commit message for each commit:
|
|
2
|
-
-
|
|
3
|
-
-
|
|
4
|
-
-
|
|
1
|
+
import{groupingResultSchema as e}from"./schema.js";import{z as t}from"zod";const n=JSON.stringify(t.toJSONSchema(e),null,2),r=`5. Write a full commit message for each commit containing both a summary and a description:
|
|
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.`,i=`Each branch must have a descriptive name, a short PR-style title (under 72 characters, imperative mood), and at least one commit.`;function a(e,t=`branches`,n){return t===`commits`?o(e,n):s(e,n)}function o(e,t){return`You are an advanced AI programming assistant tasked with organizing code changes into commits. You will be working with individual code hunks.
|
|
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
|
|
|
@@ -27,11 +34,7 @@ ${n}
|
|
|
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 s(e,t){let i=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
40
|
${e.promptFragment}`:`${r}
|
|
@@ -42,13 +45,19 @@ Your job has two levels:
|
|
|
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
|
-
3. Ensure each commit is self-contained and atomic
|
|
50
|
-
4. Each commit must have a unique ID (e.g., "commit-1", "commit-2")
|
|
51
|
-
5. Every commit must contain at least one hunk. Do not produce empty commits
|
|
60
|
+
2. Ensure each commit is self-contained and atomic
|
|
52
61
|
${i}
|
|
53
62
|
${l(t?.commitGranularity)}
|
|
54
63
|
|
|
@@ -64,10 +73,7 @@ Output your organization as JSON inside a <output> tag matching this schema:
|
|
|
64
73
|
${n}
|
|
65
74
|
\`\`\`
|
|
66
75
|
|
|
67
|
-
|
|
68
|
-
- Every hunk must be assigned to exactly one commit in exactly one branch
|
|
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.
|
|
76
|
+
Return only the <output> tag and no other text.`}function c(e,t){let n=`- Each commit belongs to exactly one branch.
|
|
71
77
|
${t?.promptFragment?`- ${i} Follow the repository's branch naming conventions (see above).`:`- ${i}`}
|
|
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.
|
|
@@ -38,8 +38,10 @@ interface GroupInput {
|
|
|
38
38
|
model: AiModelPort;
|
|
39
39
|
session?: ComposeSession;
|
|
40
40
|
maxRetries?: number;
|
|
41
|
+
/** Output-token cap forwarded to `runAiLoop`. Default 16384. Bump on very large diffs whose structured output overflows the cap mid-JSON. */
|
|
42
|
+
maxTokens?: number;
|
|
41
43
|
onProgress?: (event: ProgressEvent) => void;
|
|
42
|
-
|
|
44
|
+
cancellation?: AbortSignal;
|
|
43
45
|
onBeforeModelCall?: OnBeforeModelCall;
|
|
44
46
|
onAfterModelCall?: OnAfterModelCall;
|
|
45
47
|
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,6 +1,8 @@
|
|
|
1
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
|
-
|
|
3
|
-
|
|
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=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
6
|
|
|
5
7
|
## Your task
|
|
6
8
|
|
|
@@ -16,10 +18,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
|
|
@@ -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}),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};
|
|
@@ -29,8 +29,10 @@ interface QuickComposeInput {
|
|
|
29
29
|
/** Prior session to resume from. When `completedSteps.quickCompose` is present, skip the AI call. */
|
|
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,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": [
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
//#region src/utils/cancellation.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* True for native `AbortError` thrown by signal-aware adapters
|
|
4
|
+
* (e.g. shared-tools' AI loop, `child_process.spawn` with a signal,
|
|
5
|
+
* `fetch` with a signal). Workflow files use this in their try/catch
|
|
6
|
+
* to translate native abort errors into `ComposeWorkflowError('CANCELLED')`.
|
|
7
|
+
*/
|
|
8
|
+
declare function isAbortError(err: unknown): boolean;
|
|
9
|
+
/**
|
|
10
|
+
* Translate a caught error into the workflow-domain shape, then throw.
|
|
11
|
+
*
|
|
12
|
+
* - `ComposeWorkflowError` is rethrown unchanged (already in the right shape).
|
|
13
|
+
* - Native `AbortError` (or any error caught while the cancellation token is
|
|
14
|
+
* already aborted) becomes `ComposeWorkflowError('CANCELLED')`.
|
|
15
|
+
* - Anything else is rethrown unchanged.
|
|
16
|
+
*
|
|
17
|
+
* Workflows that delegate AI work to a task wrap that single task call in
|
|
18
|
+
* `try { ... } catch (err) { rethrowAsWorkflowError(err, input.cancellation); }`.
|
|
19
|
+
* Git operations elsewhere in the workflow do not pass `cancellation` into
|
|
20
|
+
* underlying ops — they run to completion regardless — and so cannot
|
|
21
|
+
* surface `AbortError`. Returns `never` so callers don't have to add an
|
|
22
|
+
* unreachable trailing throw.
|
|
23
|
+
*/
|
|
24
|
+
declare function rethrowAsWorkflowError(err: unknown, cancellation?: AbortSignal): never;
|
|
25
|
+
//#endregion
|
|
26
|
+
export { isAbortError, rethrowAsWorkflowError };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{ComposeWorkflowError as e}from"../errors/workflow.js";function t(e){return e instanceof Error&&(e.name===`AbortError`||e.name===`DOMException`)}function n(n,r){throw n instanceof e?n:t(n)||r?.aborted?new e(`CANCELLED`,n instanceof Error?n.message:`Operation cancelled`,{cause:n}):n}export{t as isAbortError,n as rethrowAsWorkflowError};
|
|
@@ -9,7 +9,7 @@ import { OrderingResult } from "../tasks/order/types.js";
|
|
|
9
9
|
import { ComposeSession } from "../tasks/session.js";
|
|
10
10
|
import { PartitionStrategy } from "../tasks/partition/types.js";
|
|
11
11
|
import { ComposeContext, ComposeSource, ComposeTarget } from "./types.js";
|
|
12
|
-
import { SafetySnapshot } from "../git/types.js";
|
|
12
|
+
import { SafetySnapshot, StashConflict } from "../git/types.js";
|
|
13
13
|
import { ComposeBranchPlan } from "../git/apply-target.js";
|
|
14
14
|
import { AiModelPort, UndoId } from "@gitkraken/shared-tools";
|
|
15
15
|
|
|
@@ -31,8 +31,10 @@ interface ComposePlanInput {
|
|
|
31
31
|
strategy?: ComposeStrategy;
|
|
32
32
|
depth?: ComposeDepth;
|
|
33
33
|
maxRetries?: number;
|
|
34
|
+
/** See {@link ComposeInput.maxTokens}. */
|
|
35
|
+
maxTokens?: number;
|
|
34
36
|
onProgress?: (event: ComposeProgressEvent) => void;
|
|
35
|
-
|
|
37
|
+
cancellation?: AbortSignal;
|
|
36
38
|
onBeforeModelCall?: OnBeforeModelCall;
|
|
37
39
|
onAfterModelCall?: OnAfterModelCall;
|
|
38
40
|
onBeforePrompt?: OnBeforePrompt;
|
|
@@ -56,18 +58,33 @@ interface ComposePlanResult {
|
|
|
56
58
|
/** See {@link ComposeResult.hunksFilteredCount}. */
|
|
57
59
|
hunksFilteredCount?: number;
|
|
58
60
|
}
|
|
59
|
-
/**
|
|
61
|
+
/**
|
|
62
|
+
* Pure-apply input — takes a previously computed plan and writes it to the repository.
|
|
63
|
+
*
|
|
64
|
+
* No `cancellation` — `applyComposePlan` is git-only and runs to completion.
|
|
65
|
+
*/
|
|
60
66
|
interface ApplyComposePlanInput {
|
|
61
67
|
git: ComposeGitPort;
|
|
62
68
|
applyPlan: ComposeApplyPlan;
|
|
63
69
|
source?: ComposeSource;
|
|
64
70
|
target?: ComposeTarget;
|
|
65
71
|
onProgress?: (event: ComposeProgressEvent) => void;
|
|
66
|
-
signal?: AbortSignal;
|
|
67
72
|
/** GPG signing config for produced commits. See {@link ComposeInput.signing}. */
|
|
68
73
|
signing?: SigningConfig;
|
|
69
74
|
/** How to pick the primary author for each commit. See {@link ComposeInput.authorAttribution}. */
|
|
70
75
|
authorAttribution?: ComposeInput['authorAttribution'];
|
|
76
|
+
/** See {@link ComposeInput.applyUpTo}. */
|
|
77
|
+
applyUpTo?: ApplyUpTo;
|
|
78
|
+
/** See {@link ComposeInput.stashLabel}. */
|
|
79
|
+
stashLabel?: string;
|
|
80
|
+
/**
|
|
81
|
+
* Same filter that was passed to {@link composePlan} when the cached plan was created.
|
|
82
|
+
* The plan's snapshot was rebuilt from the filtered hunks; the apply step re-collects
|
|
83
|
+
* from source and must re-apply this filter before comparing the diff hash, otherwise
|
|
84
|
+
* the comparison fails on every consumer that uses a filter (the fresh unfiltered
|
|
85
|
+
* diff hash never equals the cached filtered one). See {@link ComposeInput.hunkFilter}.
|
|
86
|
+
*/
|
|
87
|
+
hunkFilter?: ComposeInput['hunkFilter'];
|
|
71
88
|
}
|
|
72
89
|
/** Result of {@link applyComposePlan}. */
|
|
73
90
|
interface ApplyComposePlanResult {
|
|
@@ -76,6 +93,12 @@ interface ApplyComposePlanResult {
|
|
|
76
93
|
partitionGroups?: BranchPartition[][];
|
|
77
94
|
phases: PhaseDetail[];
|
|
78
95
|
undoId?: UndoId;
|
|
96
|
+
/** Plan commit IDs actually written. A prefix of `plan.allOrderedCommits.map(c => c.id)` for partial apply. */
|
|
97
|
+
appliedCommitIds?: string[];
|
|
98
|
+
/** Hunk indices not covered by the applied commits. Empty for full apply. */
|
|
99
|
+
unappliedHunkIndices?: number[];
|
|
100
|
+
/** Set when the post-apply stash pop hit conflicts. Apply itself succeeded; the named stash is preserved. */
|
|
101
|
+
stashConflict?: StashConflict;
|
|
79
102
|
}
|
|
80
103
|
/** Controls how many AI pipeline steps run and how deeply each reasons. */
|
|
81
104
|
type ComposeDepth = 'quick' | 'balanced' | 'deep';
|
|
@@ -115,13 +138,28 @@ interface ComposeInput {
|
|
|
115
138
|
/** Controls AI pipeline depth. 'quick' = single AI call. 'balanced' = 2-3 calls (default). 'deep' = 4+ calls with full analysis. */
|
|
116
139
|
depth?: ComposeDepth;
|
|
117
140
|
maxRetries?: number;
|
|
141
|
+
/**
|
|
142
|
+
* Output-token cap forwarded to every AI task in the pipeline. Defaults to
|
|
143
|
+
* `runAiLoop`'s 16384. Bump on very large diffs whose structured output
|
|
144
|
+
* (e.g. analyze's per-hunk annotations) overflows the cap mid-JSON; the
|
|
145
|
+
* validator will surface that as a JSON-parse failure with a "Response
|
|
146
|
+
* tail" preview if it happens.
|
|
147
|
+
*/
|
|
148
|
+
maxTokens?: number;
|
|
118
149
|
onProgress?: (event: ComposeProgressEvent) => void;
|
|
119
150
|
onBeforeApply?: (plan: ComposePlan) => Promise<boolean>;
|
|
120
151
|
/**
|
|
121
|
-
* Optional cancellation
|
|
122
|
-
*
|
|
152
|
+
* Optional cancellation token. Forwarded only to the AI loop (via the
|
|
153
|
+
* tasks the workflow runs). Git operations — collection, snapshot
|
|
154
|
+
* verification, applying, undo manifest persistence — run to
|
|
155
|
+
* completion regardless of whether `cancellation` was aborted; mid-flight
|
|
156
|
+
* cancellation of a git op risks a half-finished mutation, so by design
|
|
157
|
+
* only the AI loop is cancellable.
|
|
158
|
+
*
|
|
159
|
+
* On AI-loop abort, the workflow translates the native `AbortError` into
|
|
160
|
+
* `ComposeWorkflowError('CANCELLED')`.
|
|
123
161
|
*/
|
|
124
|
-
|
|
162
|
+
cancellation?: AbortSignal;
|
|
125
163
|
/**
|
|
126
164
|
* Called immediately before each model.generate invocation. Use for progress UI,
|
|
127
165
|
* telemetry, or correlation. The library doesn't interpret the result.
|
|
@@ -131,7 +169,7 @@ interface ComposeInput {
|
|
|
131
169
|
onAfterModelCall?: OnAfterModelCall;
|
|
132
170
|
/**
|
|
133
171
|
* Gating hook called immediately before each model.generate call. Returning
|
|
134
|
-
* `false` aborts the workflow with `
|
|
172
|
+
* `false` aborts the workflow with `ComposeWorkflowError('CANCELLED')`. Use for
|
|
135
173
|
* large-prompt confirmations, cost warnings, org policy checks, etc.
|
|
136
174
|
*/
|
|
137
175
|
onBeforePrompt?: OnBeforePrompt;
|
|
@@ -204,7 +242,20 @@ interface ComposeInput {
|
|
|
204
242
|
name: string;
|
|
205
243
|
email: string;
|
|
206
244
|
} | undefined);
|
|
245
|
+
/** Apply only a prefix of `plan.allOrderedCommits`. Hunks not in applied commits are restored to the working tree. */
|
|
246
|
+
applyUpTo?: ApplyUpTo;
|
|
247
|
+
/** Stash message used by the apply path when stashing pending workdir state. Defaults to `'compose backup'`. */
|
|
248
|
+
stashLabel?: string;
|
|
207
249
|
}
|
|
250
|
+
type ApplyUpTo = {
|
|
251
|
+
kind: 'all';
|
|
252
|
+
} | {
|
|
253
|
+
kind: 'commitId';
|
|
254
|
+
id: string;
|
|
255
|
+
} | {
|
|
256
|
+
kind: 'count';
|
|
257
|
+
count: number;
|
|
258
|
+
};
|
|
208
259
|
interface ComposePlan {
|
|
209
260
|
analysis?: AnalysisResult;
|
|
210
261
|
grouping: GroupingResult;
|
|
@@ -331,6 +382,12 @@ interface ComposeResult {
|
|
|
331
382
|
* Undefined when no filter ran.
|
|
332
383
|
*/
|
|
333
384
|
hunksFilteredCount?: number;
|
|
385
|
+
/** See {@link ApplyComposePlanResult.appliedCommitIds}. */
|
|
386
|
+
appliedCommitIds?: string[];
|
|
387
|
+
/** See {@link ApplyComposePlanResult.unappliedHunkIndices}. */
|
|
388
|
+
unappliedHunkIndices?: number[];
|
|
389
|
+
/** See {@link ApplyComposePlanResult.stashConflict}. */
|
|
390
|
+
stashConflict?: StashConflict;
|
|
334
391
|
}
|
|
335
392
|
/**
|
|
336
393
|
* Compose code changes into a clean set of commits and branches.
|
|
@@ -347,6 +404,11 @@ interface ComposeResult {
|
|
|
347
404
|
* Caller is responsible for the `model` lifecycle. If `applyPlan` is provided, the AI
|
|
348
405
|
* phases are skipped and the previously computed plan is re-applied; the workdir
|
|
349
406
|
* snapshot in the plan is verified before applying.
|
|
407
|
+
*
|
|
408
|
+
* Cancellation only flows into the AI loop (via the tasks the workflow runs).
|
|
409
|
+
* Git operations are not cancellable mid-flight by design — they always run to
|
|
410
|
+
* completion. When the AI loop aborts, the workflow translates the native
|
|
411
|
+
* `AbortError` into `ComposeWorkflowError('CANCELLED')`.
|
|
350
412
|
*/
|
|
351
413
|
declare function compose(input: ComposeInput): Promise<ComposeResult>;
|
|
352
414
|
/**
|
|
@@ -357,15 +419,21 @@ declare function compose(input: ComposeInput): Promise<ComposeResult>;
|
|
|
357
419
|
* consumers whose UX lets the user edit the plan before committing.
|
|
358
420
|
*
|
|
359
421
|
* For single-shot "plan and apply in one go", use {@link compose} directly.
|
|
422
|
+
*
|
|
423
|
+
* Cancellation handling matches {@link compose} — only the AI loop is
|
|
424
|
+
* cancellable; git operations run to completion.
|
|
360
425
|
*/
|
|
361
426
|
declare function composePlan(input: ComposePlanInput): Promise<ComposePlanResult>;
|
|
362
427
|
/**
|
|
363
428
|
* Apply a previously computed plan (from {@link composePlan}) to the repository.
|
|
364
429
|
*
|
|
365
430
|
* Verifies the snapshot against the current workdir before applying — if the
|
|
366
|
-
* subsets the plan touches have drifted, throws `
|
|
431
|
+
* subsets the plan touches have drifted, throws `ComposeWorkflowError('SAFETY_CHECK_FAILED')`.
|
|
367
432
|
* On success, returns an `undoId` that {@link undoCompose} can reverse.
|
|
433
|
+
*
|
|
434
|
+
* No AI runs in this path, so cancellation has no effect — the apply phase
|
|
435
|
+
* runs to completion regardless of whether `cancellation` was aborted.
|
|
368
436
|
*/
|
|
369
437
|
declare function applyComposePlan(input: ApplyComposePlanInput): Promise<ApplyComposePlanResult>;
|
|
370
438
|
//#endregion
|
|
371
|
-
export { ApplyComposePlanInput, ApplyComposePlanResult, ComposeApplyPlan, type ComposeBranchPlan, type ComposeContext, ComposeDepth, ComposeInput, ComposePlan, ComposePlanInput, ComposePlanResult, ComposeProgressEvent, ComposeResult, type ComposeSource, ComposeStrategy, type ComposeTarget, applyComposePlan, compose, composePlan, defaultPhaseMessages };
|
|
439
|
+
export { ApplyComposePlanInput, ApplyComposePlanResult, ApplyUpTo, ComposeApplyPlan, type ComposeBranchPlan, type ComposeContext, ComposeDepth, ComposeInput, ComposePlan, ComposePlanInput, ComposePlanResult, ComposeProgressEvent, ComposeResult, type ComposeSource, ComposeStrategy, type ComposeTarget, type StashConflict, applyComposePlan, compose, composePlan, defaultPhaseMessages };
|