@gitkraken/compose-tools 0.7.0 → 0.8.1
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 +31 -0
- package/README.md +8 -5
- package/dist/ai/types.d.ts +46 -1
- package/dist/git/apply-target.d.ts +2 -1
- package/dist/git/apply-target.js +1 -1
- package/dist/git/commit-chain.d.ts +17 -6
- package/dist/git/commit-chain.js +7 -6
- package/dist/git/diff-collector.d.ts +35 -4
- package/dist/git/diff-collector.js +4 -4
- package/dist/git/index.js +1 -1
- package/dist/git/port-dispatch.js +1 -1
- package/dist/git/safety.js +1 -1
- package/dist/git/types.d.ts +19 -1
- package/dist/index.d.ts +3 -3
- package/dist/ports/git.d.ts +8 -1
- package/dist/tasks/ai-loop.d.ts +10 -1
- package/dist/tasks/ai-loop.js +1 -1
- package/dist/tasks/analyze/analyze.js +1 -1
- package/dist/tasks/analyze/prompt.js +1 -0
- package/dist/tasks/analyze/validator.d.ts +1 -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 +6 -6
- package/dist/tasks/compose-group/types.d.ts +3 -11
- package/dist/tasks/group/group.js +1 -1
- package/dist/tasks/group/prompt.js +4 -4
- package/dist/tasks/group/types.d.ts +6 -1
- package/dist/tasks/group/validator.d.ts +10 -2
- package/dist/tasks/group/validator.js +5 -5
- package/dist/tasks/quick-compose/prompt.js +1 -1
- package/dist/tasks/quick-compose/quick-compose.js +1 -1
- package/dist/tasks/quick-compose/validator.d.ts +1 -1
- package/dist/tasks/quick-compose/validator.js +2 -2
- package/dist/tasks/refine/prompt.d.ts +1 -1
- package/dist/tasks/refine/prompt.js +4 -4
- package/dist/tasks/refine/refine.js +1 -1
- package/dist/tasks/refine/types.d.ts +10 -4
- package/dist/tasks/refine/validator.d.ts +1 -1
- package/dist/tasks/refine/validator.js +1 -1
- package/dist/workflows/compose.d.ts +6 -2
- package/dist/workflows/compose.js +3 -3
- package/dist/workflows/refine.d.ts +2 -2
- package/dist/workflows/refine.js +1 -1
- package/dist/workflows/types.d.ts +17 -0
- package/package.json +1 -1
package/dist/ports/git.d.ts
CHANGED
|
@@ -30,7 +30,9 @@ interface ComposeGitOps {
|
|
|
30
30
|
mergeBase?: (a: string, b: string, opts?: OpOptions) => Promise<string>;
|
|
31
31
|
/** Check whether `ancestor` is an ancestor of `descendant`. Covers `merge-base --is-ancestor`. */
|
|
32
32
|
mergeBaseIsAncestor?: (ancestor: string, descendant: string, opts?: OpOptions) => Promise<boolean>;
|
|
33
|
-
/** Tree-to-tree unified diff
|
|
33
|
+
/** Tree-to-tree unified diff between two tree-ish. A custom implementation must honor every
|
|
34
|
+
* {@link DiffTreeOptions} field it is passed (`binary`, `nameOnly`, `diffFilter`, `noRenames`):
|
|
35
|
+
* callers rely on the filtering, and silently ignoring one can return a diff they misread. */
|
|
34
36
|
diffTree?: (from: string, to: string, opts?: DiffTreeOptions) => Promise<string>;
|
|
35
37
|
/** Commit log with custom format. Covers various `git log --format=... <range>` / `-1 <sha>` uses. */
|
|
36
38
|
log?: (opts: LogOptions) => Promise<string>;
|
|
@@ -110,6 +112,11 @@ interface DiffTreeOptions extends OpOptions {
|
|
|
110
112
|
binary?: boolean;
|
|
111
113
|
/** Return file list instead of patch content. */
|
|
112
114
|
nameOnly?: boolean;
|
|
115
|
+
/** Restrict to selected change kinds, passed verbatim to `--diff-filter` (e.g. `'DMT'`). */
|
|
116
|
+
diffFilter?: string;
|
|
117
|
+
/** Disable rename detection (`--no-renames`), so a moved or content-matching file reports as a
|
|
118
|
+
* delete plus an add instead of a rename. */
|
|
119
|
+
noRenames?: boolean;
|
|
113
120
|
}
|
|
114
121
|
interface LogOptions extends OpOptions {
|
|
115
122
|
format: string;
|
package/dist/tasks/ai-loop.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { AiSession } from "../ai/session.js";
|
|
2
|
-
import { ProgressEvent, TokenUsage } from "../ai/types.js";
|
|
2
|
+
import { ProgressEvent, TokenUsage, ValidationRepair } from "../ai/types.js";
|
|
3
3
|
import { AiModelPort } from "@gitkraken/shared-tools";
|
|
4
4
|
|
|
5
5
|
//#region src/tasks/ai-loop.d.ts
|
|
@@ -30,6 +30,11 @@ interface AfterModelCallInfo {
|
|
|
30
30
|
};
|
|
31
31
|
/** Identifier of the prompt template used for this call, when a consumer override supplied one. */
|
|
32
32
|
promptVersion?: string;
|
|
33
|
+
/**
|
|
34
|
+
* Deterministic repairs applied to this response so it could be accepted instead of retried.
|
|
35
|
+
* Undefined when the response needed no repair (or when the call itself failed).
|
|
36
|
+
*/
|
|
37
|
+
repairs?: ValidationRepair[];
|
|
33
38
|
}
|
|
34
39
|
type OnBeforeModelCall = (info: BeforeModelCallInfo) => void;
|
|
35
40
|
type OnAfterModelCall = (info: AfterModelCallInfo) => void;
|
|
@@ -77,6 +82,8 @@ interface ValidationResult<T> {
|
|
|
77
82
|
result?: T;
|
|
78
83
|
errors?: string[];
|
|
79
84
|
retryFeedback?: string;
|
|
85
|
+
/** Set (non-empty) only when the validator repaired the response to make it valid. */
|
|
86
|
+
repairs?: ValidationRepair[];
|
|
80
87
|
}
|
|
81
88
|
interface AiLoopResult<T> {
|
|
82
89
|
result: T;
|
|
@@ -86,6 +93,8 @@ interface AiLoopResult<T> {
|
|
|
86
93
|
durationMs: number;
|
|
87
94
|
/** Stable call IDs for each model.generate invocation this loop made, in order. */
|
|
88
95
|
callIds: string[];
|
|
96
|
+
/** Repairs applied to the accepted response. Undefined when it needed no repair. */
|
|
97
|
+
repairs?: ValidationRepair[];
|
|
89
98
|
}
|
|
90
99
|
declare function runAiLoop<T>(params: AiLoopParams<T>): Promise<AiLoopResult<T>>;
|
|
91
100
|
declare function extractOutputTag(text: string): string | null;
|
package/dist/tasks/ai-loop.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{AIError as e}from"../errors/ai.js";import{TaskInputError as t}from"../errors/task.js";import{appendAssistantMessage as n,appendUserMessage as r,createAiSession as i}from"../ai/session.js";import{addUsage as a}from"../ai/types.js";import{randomUUID as o}from"node:crypto";const s=2.8;async function c(a){let{model:s,userPrompt:c,maxRetries:d=3,maxTokens:f=16384,onProgress:p,validate:m,taskName:h,cancellation:g,phase:_=h,onBeforeModelCall:v,onAfterModelCall:y,promptVersion:b}=a;if(!a.session&&!a.systemPrompt)throw new t(`${h}: either 'session' or 'systemPrompt' must be provided`);let x=Date.now(),S;a.session?S=r(a.session,c):(S=i(a.systemPrompt),S=r(S,c));let C,w=[],T=d+1,E=[];for(let e=0;e<=d;e++){g?.throwIfAborted();let t=e+1;if(p?.({type:`attempt_start`,attempt:t,maxAttempts:T}),a.onBeforePrompt){let e=(S.systemPrompt?.length??0)+S.messages.reduce((e,t)=>e+t.content.length,0);if(!await a.onBeforePrompt({phase:_,attempt:t,maxAttempts:T,charCount:e,messageCount:S.messages.length,tokenEstimate:Math.ceil(e/2.8)}))throw new DOMException(`Prompt gate declined by caller (onBeforePrompt returned false)`,`AbortError`)}let i=o();E.push(i);let c=Date.now();v?.({phase:_,attempt:t,maxAttempts:T,callId:i});let h;try{h=await s.generate({system:S.systemPrompt,messages:[...S.messages],maxTokens:f,temperature:0,signal:g})}catch(e){let n=e instanceof Error?e.message:String(e);
|
|
1
|
+
import{AIError as e}from"../errors/ai.js";import{TaskInputError as t}from"../errors/task.js";import{appendAssistantMessage as n,appendUserMessage as r,createAiSession as i}from"../ai/session.js";import{addUsage as a}from"../ai/types.js";import{randomUUID as o}from"node:crypto";const s=2.8;async function c(a){let{model:s,userPrompt:c,maxRetries:d=3,maxTokens:f=16384,onProgress:p,validate:m,taskName:h,cancellation:g,phase:_=h,onBeforeModelCall:v,onAfterModelCall:y,promptVersion:b}=a;if(!a.session&&!a.systemPrompt)throw new t(`${h}: either 'session' or 'systemPrompt' must be provided`);let x=Date.now(),S;a.session?S=r(a.session,c):(S=i(a.systemPrompt),S=r(S,c));let C,w=[],T=d+1,E=[];for(let e=0;e<=d;e++){g?.throwIfAborted();let t=e+1;if(p?.({type:`attempt_start`,attempt:t,maxAttempts:T}),a.onBeforePrompt){let e=(S.systemPrompt?.length??0)+S.messages.reduce((e,t)=>e+t.content.length,0);if(!await a.onBeforePrompt({phase:_,attempt:t,maxAttempts:T,charCount:e,messageCount:S.messages.length,tokenEstimate:Math.ceil(e/2.8)}))throw new DOMException(`Prompt gate declined by caller (onBeforePrompt returned false)`,`AbortError`)}let i=o();E.push(i);let c=Date.now();v?.({phase:_,attempt:t,maxAttempts:T,callId:i});let h;try{h=await s.generate({system:S.systemPrompt,messages:[...S.messages],maxTokens:f,temperature:0,signal:g})}catch(e){let n=e instanceof Error?e.message:String(e);try{y?.({phase:_,attempt:t,callId:i,success:!1,durationMs:Date.now()-c,error:{code:e?.code??`UNKNOWN`,message:n},promptVersion:b})}catch{}throw l(e)||g?.aborted?new DOMException(n,`AbortError`):e}let D=Date.now()-c,O=e=>{try{y?.({phase:_,attempt:t,callId:i,success:!0,durationMs:D,usage:h.usage,promptVersion:b,repairs:e})}catch{}};C=u(C,h.usage);let k;try{k=m(h.text)}catch(e){throw O(),e}if(O(k.valid?k.repairs:void 0),k.valid&&k.result!==void 0)return S=n(S,h.text),{result:k.result,session:S,usage:C,attempts:t,durationMs:Date.now()-x,callIds:E,repairs:k.repairs};w=k.errors??[`Unknown validation error`],p?.({type:`validation_failed`,attempt:t,maxAttempts:T,errors:w}),e<d&&k.retryFeedback&&(S=n(S,h.text),S=r(S,k.retryFeedback))}throw new e(`VALIDATION_EXHAUSTED`,`Failed to get valid ${h} after ${T} attempts. Last errors:\n${w.join(`
|
|
2
2
|
`)}`)}function l(e){return e instanceof Error&&(e.name===`AbortError`||e.name===`DOMException`)}function u(e,t){if(!e)return t;if(!t)return e;let n={...e};return a(n,t),n}function d(e){let t=e.match(/<output>([\s\S]*?)(?:<\/output>|$)/)?.[1]?.trim()??null;return t?t.match(/^```(?:\w*)\n?([\s\S]*?)```$/)?.[1]?.trim()??t:null}export{s as estimatedCharactersPerToken,d as extractOutputTag,c as runAiLoop};
|
|
@@ -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{buildAnalyzeSystemPrompt as r,buildAnalyzeUserPrompt as i}from"./prompt.js";import{validateAnalyzeResponse as a}from"./validator.js";async function o(o){let s=o.hunks;if(!s||s.length===0)throw new e(`No hunks provided: supply 'hunks'`);if(o.session?.completedSteps.analyze)return{result:o.session.completedSteps.analyze,session:o.session,attempts:0,durationMs:0,callIds:[]};let c=o.session?void 0:o.promptOverride?.system??r(),l,u=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,inputTokenBudget:o.inputTokenBudget,systemPromptChars:c?.length,onBudgetReduction:e=>{l=e}}),o.promptOverride),d=s.
|
|
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{buildAnalyzeSystemPrompt as r,buildAnalyzeUserPrompt as i}from"./prompt.js";import{validateAnalyzeResponse as a}from"./validator.js";async function o(o){let s=o.hunks;if(!s||s.length===0)throw new e(`No hunks provided: supply 'hunks'`);if(o.session?.completedSteps.analyze)return{result:o.session.completedSteps.analyze,session:o.session,attempts:0,durationMs:0,callIds:[]};let c=o.session?void 0:o.promptOverride?.system??r(),l,u=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,inputTokenBudget:o.inputTokenBudget,systemPromptChars:c?.length,onBudgetReduction:e=>{l=e}}),o.promptOverride),d=new Set(s.map(e=>e.index)),f=await t({model:o.model,systemPrompt:c,userPrompt:u,session:o.session,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,d),taskName:`analyze`}),p=o.session?.completedSteps??{},m={...f.session,completedSteps:{...p,analyze:f.result}};return{result:f.result,session:m,usage:f.usage,attempts:f.attempts,durationMs:f.durationMs,callIds:f.callIds,hunksCondensedCount:l}}export{o as analyze};
|
|
@@ -15,6 +15,7 @@ Look for these patterns:
|
|
|
15
15
|
|
|
16
16
|
Rules:
|
|
17
17
|
- Every hunk must be annotated with exactly one concern
|
|
18
|
+
- Use each hunk's \`index\` value exactly as shown — the indices are not necessarily contiguous, so never renumber them or invent indices that were not provided
|
|
18
19
|
- Concerns should be named clearly and concisely
|
|
19
20
|
- Dependencies should capture real code-level relationships, not just thematic similarity
|
|
20
21
|
- Ordering constraints should capture concrete provider/consumer relationships between specific hunks, not vague thematic ordering
|
|
@@ -2,6 +2,6 @@ import { ValidationResult } from "../ai-loop.js";
|
|
|
2
2
|
import { AnalysisResult } from "./types.js";
|
|
3
3
|
|
|
4
4
|
//#region src/tasks/analyze/validator.d.ts
|
|
5
|
-
declare function validateAnalyzeResponse(responseText: string,
|
|
5
|
+
declare function validateAnalyzeResponse(responseText: string, expectedHunkIndices: ReadonlySet<number>): ValidationResult<AnalysisResult>;
|
|
6
6
|
//#endregion
|
|
7
7
|
export { validateAnalyzeResponse };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{extractOutputTag as e}from"../ai-loop.js";import{analysisResultJsonSchema as t}from"./schema.js";import{validateJsonSchema as n}from"../utils/json-schema.js";function r(r,o){let s=null;try{if(s=e(r),!s)return i([`No <output> tag found in response`],a());let c=n(t,JSON.parse(s));if(!c.success)return i(c.issues.map(e=>`${e.path.join(`.`)}: ${e.message}`),a());let l=c.data,u=new Set(l.hunkAnnotations.map(e=>e.hunkIndex)),d=[...
|
|
1
|
+
import{extractOutputTag as e}from"../ai-loop.js";import{analysisResultJsonSchema as t}from"./schema.js";import{validateJsonSchema as n}from"../utils/json-schema.js";function r(r,o){let s=null;try{if(s=e(r),!s)return i([`No <output> tag found in response`],a());let c=n(t,JSON.parse(s));if(!c.success)return i(c.issues.map(e=>`${e.path.join(`.`)}: ${e.message}`),a());let l=c.data,u=new Set(l.hunkAnnotations.map(e=>e.hunkIndex)),d=[...o].filter(e=>!u.has(e)).sort((e,t)=>e-t);if(d.length>0)return i([`Missing annotations for hunks: ${d.join(`, `)}`],`Your response is missing annotations for some hunks. Every hunk must be annotated.\n\nMissing: ${d.join(`, `)}\n\nPlease provide a corrected response.`);let f=[...u].filter(e=>!o.has(e)).sort((e,t)=>e-t);return f.length>0?i([`Annotations reference unknown hunk indices: ${f.join(`, `)}`],`Your response annotates hunk indices that don't exist in the input. Annotate only the hunk indices shown in the input, exactly as given.\n\nUnknown: ${f.join(`, `)}\n\nPlease provide a corrected response.`):{valid:!0,result:{title:l.title,summary:l.summary,concerns:l.concerns,dependencies:l.dependencies,hunkAnnotations:l.hunkAnnotations,orderingConstraints:l.orderingConstraints}}}catch(e){let t=e instanceof Error?e.message:String(e),n=(s??r).slice(-300);return i([`Failed to parse response as valid JSON: ${t}`,`Response tail: ${n}`],a())}}function i(e,t){return{valid:!1,errors:e,retryFeedback:t}}function a(){return`Your response has an invalid structure. Please return JSON inside <output> tags with this structure:
|
|
2
2
|
<output>
|
|
3
3
|
{
|
|
4
4
|
"title": "Short PR-style title (under 72 chars)",
|
|
@@ -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"../group/validator.js";import{buildComposeGroupSystemPrompt as i,buildComposeGroupUserPrompt as a}from"./prompt.js";async function o(o){let s=o.hunks,c=o.mode??`branches`;if(!s||s.length===0)throw new e(`No hunks provided for compose-group`);if(o.session?.completedSteps.composeGroup)return{result:o.session.completedSteps.composeGroup,session:o.session,attempts:0,durationMs:0,callIds:[]};let
|
|
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"../group/validator.js";import{buildComposeGroupSystemPrompt as i,buildComposeGroupUserPrompt as a}from"./prompt.js";async function o(o){let s=o.hunks,c=o.mode??`branches`;if(!s||s.length===0)throw new e(`No hunks provided for compose-group`);let l=new Set(s.map(e=>e.index));if(o.session?.completedSteps.composeGroup)return{result:o.session.completedSteps.composeGroup,session:o.session,attempts:0,durationMs:0,callIds:[]};let u=o.promptOverride?.system??i(o.conventions,c,o.strategy,o.includeAnalysis,o.lightweight),d,f=n(a({hunks:s,commitMessages:o.commitMessages,commitOrder:o.commitOrder,branchName:o.branchName,baseBranch:o.baseBranch,issueUrl:o.issueUrl,pullRequestUrl:o.pullRequestUrl,instructions:o.instructions,condensed:o.condensed,redactHunkContent:o.redactHunkContent,inputTokenBudget:o.inputTokenBudget,systemPromptChars:u.length,onBudgetReduction:e=>{d=e}}),o.promptOverride),p=await t({model:o.model,systemPrompt:u,userPrompt:f,maxRetries:o.maxRetries,maxTokens:o.maxTokens,onProgress:o.onProgress,cancellation:o.cancellation,onBeforeModelCall:o.onBeforeModelCall,onAfterModelCall:o.onAfterModelCall,onBeforePrompt:o.onBeforePrompt,validate:e=>r(e,l,c,o.existingBranchNames),taskName:`compose-group`}),m=o.session?.completedSteps??{},h={...p.session,completedSteps:{...m,composeGroup:p.result}};return{result:p.result,session:h,usage:p.usage,attempts:p.attempts,durationMs:p.durationMs,callIds:p.callIds,hunksCondensedCount:d,repairs:p.repairs}}export{o as composeGroup};
|
|
@@ -20,7 +20,7 @@ In addition to the \`branches\` field, include an \`analysis\` field at the top
|
|
|
20
20
|
## Critical constraints (output is rejected if violated)
|
|
21
21
|
|
|
22
22
|
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).
|
|
23
|
-
2. **
|
|
23
|
+
2. **Every hunk index present in the input must appear** across the union of every commit's \`hunks\` array. Use the \`index\` values exactly as shown — they are not necessarily contiguous, so never renumber them or invent indices.
|
|
24
24
|
3. Each commit must contain at least one hunk. No empty commits.
|
|
25
25
|
4. Each commit's \`id\` must be unique (e.g. \`commit-1\`, \`commit-2\`).
|
|
26
26
|
|
|
@@ -58,7 +58,7 @@ Return only the <output> tag and no other text.`}function c(e,t,o){let s=`${e?.c
|
|
|
58
58
|
## Critical constraints (output is rejected if violated)
|
|
59
59
|
|
|
60
60
|
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).
|
|
61
|
-
2. **
|
|
61
|
+
2. **Every hunk index present in the input must appear** across the union of every commit's \`hunks\` array across every branch. Use the \`index\` values exactly as shown — they are not necessarily contiguous, so never renumber them or invent indices.
|
|
62
62
|
3. Each commit must contain at least one hunk. No empty commits.
|
|
63
63
|
4. Each commit \`id\` must be unique across the whole response.
|
|
64
64
|
|
|
@@ -109,7 +109,7 @@ Prefer more, smaller commits. Each distinct change should be its own commit.`:``
|
|
|
109
109
|
## Rules
|
|
110
110
|
|
|
111
111
|
1. Group related hunks by purpose. All hunks of a renamed file must share a commit.
|
|
112
|
-
2. Every hunk index
|
|
112
|
+
2. Every hunk index from the input in exactly one commit — no omissions, no duplicates. Use the \`index\` values exactly as shown; they are not necessarily contiguous, so never renumber them.
|
|
113
113
|
3. Each commit needs a unique ID (e.g. "commit-1", "commit-2").
|
|
114
114
|
4. Every commit must have at least one hunk. No empty commits.
|
|
115
115
|
5. ${e?.promptFragment?`Write commit messages following the repository's conventions (see below). Write a one-sentence explanation per commit.
|
|
@@ -132,7 +132,7 @@ ${e.promptFragment}`:`Write commit messages: first line under 50 chars, third-pe
|
|
|
132
132
|
## Commit rules
|
|
133
133
|
|
|
134
134
|
1. Group related hunks by purpose. All hunks of a renamed file must share a commit.
|
|
135
|
-
2. Every hunk index
|
|
135
|
+
2. Every hunk index from the input in exactly one commit — no omissions, no duplicates. Use the \`index\` values exactly as shown; they are not necessarily contiguous, so never renumber them.
|
|
136
136
|
3. Each commit needs a unique ID (e.g., "commit-1", "commit-2").
|
|
137
137
|
4. Every commit must have at least one hunk. No empty commits.
|
|
138
138
|
5. ${n}
|
|
@@ -152,11 +152,11 @@ JSON inside <output> tags. Each branch needs a unique ID and non-empty name.
|
|
|
152
152
|
|
|
153
153
|
CRITICAL: Every hunk index must appear exactly once across all commits in all branches. Every commit must contain at least one hunk. Missing hunks, duplicate hunks, or empty commits will fail validation. Each branch must have a unique ID and non-empty name.
|
|
154
154
|
|
|
155
|
-
Return only the <output> tag and no other text.`}function p(t){let{hunks:n,commitMessages:r,commitOrder:i,branchName:a,baseBranch:o,issueUrl:s,pullRequestUrl:c,instructions:l,condensed:u,redactHunkContent:d,inputTokenBudget:f,systemPromptChars:p,onBudgetReduction:m}=t,{prompt:
|
|
155
|
+
Return only the <output> tag and no other text.`}function p(t){let{hunks:n,commitMessages:r,commitOrder:i,branchName:a,baseBranch:o,issueUrl:s,pullRequestUrl:c,instructions:l,condensed:u,redactHunkContent:d,inputTokenBudget:f,systemPromptChars:p,onBudgetReduction:m}=t,h=n.map(e=>e.index).join(`, `),{prompt:g,hunksCondensedForBudget:_}=e({hunks:n,assemble:e=>{let t=`Organize these ${n.length} code hunks (indices: ${h}) into commits. Every listed index must appear exactly once.
|
|
156
156
|
|
|
157
157
|
<hunks>
|
|
158
158
|
${e}
|
|
159
159
|
</hunks>`,u=[];return a&&u.push(`Branch: ${a}`),o&&u.push(`Base: ${o}`),s&&u.push(`Issue: ${s}`),c&&u.push(`PR: ${c}`),u.length>0&&(t+=`\n\nContext:\n${u.join(`
|
|
160
160
|
`)}`),r?.length&&(t+=`\n\nOriginal commit messages (match their style):\n<commit_messages>\n${JSON.stringify(r)}\n</commit_messages>`),i?.length&&(t+=`\n\nOriginal commit order:\n${JSON.stringify(i)}`),l&&(t+=`\n\nAdditional instructions:\n${l}`),t+=`
|
|
161
161
|
|
|
162
|
-
Return only the <output> tag and no other text.`,t},condensed:u,redactHunkContent:d,inputTokenBudget:f,systemPromptChars:p});return m?.(
|
|
162
|
+
Return only the <output> tag and no other text.`,t},condensed:u,redactHunkContent:d,inputTokenBudget:f,systemPromptChars:p});return m?.(_),g}export{o as buildComposeGroupSystemPrompt,p as buildComposeGroupUserPrompt};
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { ProgressEvent
|
|
1
|
+
import { ProgressEvent } 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
5
|
import { RedactHunkContent } from "../utils/redact.js";
|
|
6
|
-
import {
|
|
6
|
+
import { GroupOutput, GroupingStrategy } from "../group/types.js";
|
|
7
7
|
import { ComposeSession } from "../session.js";
|
|
8
8
|
import { AiModelPort } from "@gitkraken/shared-tools";
|
|
9
9
|
|
|
@@ -59,14 +59,6 @@ interface ComposeGroupInput {
|
|
|
59
59
|
/** Optional prompt override. When set, replaces or decorates the built-in prompts. */
|
|
60
60
|
promptOverride?: PromptOverride;
|
|
61
61
|
}
|
|
62
|
-
|
|
63
|
-
type ComposeGroupOutput = {
|
|
64
|
-
result: GroupingResult;
|
|
65
|
-
session: ComposeSession;
|
|
66
|
-
usage?: TokenUsage;
|
|
67
|
-
attempts: number;
|
|
68
|
-
durationMs: number;
|
|
69
|
-
callIds: string[];
|
|
70
|
-
};
|
|
62
|
+
type ComposeGroupOutput = GroupOutput;
|
|
71
63
|
//#endregion
|
|
72
64
|
export { ComposeGroupInput, ComposeGroupOutput };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{TaskInputError as e}from"../../errors/task.js";import{runAiLoop as t}from"../ai-loop.js";import{applyUserPromptOverride as n}from"../types.js";import{validateGroupResponse as r}from"./validator.js";import{buildGroupContinuationPrompt as i,buildGroupSystemPrompt as a,buildGroupUserPrompt as o}from"./prompt.js";async function s(s){let c=s.hunks,l=s.mode??`branches`;if((!c||c.length===0)&&!s.session)throw new e(`No hunks provided: supply 'hunks' or run analyze() first with a session`);if(s.session?.completedSteps.group)return{result:s.session.completedSteps.group,session:s.session,attempts:0,durationMs:0,callIds:[]};let u=s.session?void 0:s.promptOverride?.system??a(s.conventions,l,s.strategy),d,f=n(s.session?i(s,l):o({hunks:c,commitMessages:s.commitMessages,instructions:s.instructions,mode:l,redactHunkContent:s.redactHunkContent,inputTokenBudget:s.inputTokenBudget,systemPromptChars:u?.length,onBudgetReduction:e=>{d=e}}),s.promptOverride),p=c?.length
|
|
1
|
+
import{TaskInputError as e}from"../../errors/task.js";import{runAiLoop as t}from"../ai-loop.js";import{applyUserPromptOverride as n}from"../types.js";import{validateGroupResponse as r}from"./validator.js";import{buildGroupContinuationPrompt as i,buildGroupSystemPrompt as a,buildGroupUserPrompt as o}from"./prompt.js";async function s(s){let c=s.hunks,l=s.mode??`branches`;if((!c||c.length===0)&&!s.session)throw new e(`No hunks provided: supply 'hunks' or run analyze() first with a session`);if(s.session?.completedSteps.group)return{result:s.session.completedSteps.group,session:s.session,attempts:0,durationMs:0,callIds:[]};let u=s.session?void 0:s.promptOverride?.system??a(s.conventions,l,s.strategy),d,f=n(s.session?i(s,l):o({hunks:c,commitMessages:s.commitMessages,instructions:s.instructions,mode:l,redactHunkContent:s.redactHunkContent,inputTokenBudget:s.inputTokenBudget,systemPromptChars:u?.length,onBudgetReduction:e=>{d=e}}),s.promptOverride),p=c?.length?new Set(c.map(e=>e.index)):new Set(s.session?.completedSteps.analyze?.hunkAnnotations.map(e=>e.hunkIndex)??[]);if(p.size===0)throw new e(`Cannot validate grouping: no hunk indices available from input or session`);let m=await t({model:s.model,systemPrompt:u,userPrompt:f,session:s.session,maxRetries:s.maxRetries,maxTokens:s.maxTokens,onProgress:s.onProgress,cancellation:s.cancellation,onBeforeModelCall:s.onBeforeModelCall,onAfterModelCall:s.onAfterModelCall,onBeforePrompt:s.onBeforePrompt,validate:e=>r(e,p,l,s.existingBranchNames),taskName:`group`}),h=s.session?.completedSteps??{},g={...m.session,completedSteps:{...h,group:m.result}};return{result:m.result,session:g,usage:m.usage,attempts:m.attempts,durationMs:m.durationMs,callIds:m.callIds,hunksCondensedCount:d,repairs:m.repairs}}export{s as group};
|
|
@@ -7,7 +7,7 @@ import{assembleHunkPrompt as e}from"../utils/budget.js";import{groupingResultJso
|
|
|
7
7
|
## Critical constraints (output is rejected if violated)
|
|
8
8
|
|
|
9
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. **
|
|
10
|
+
2. **Every hunk index present in the input must appear** across the union of every commit's \`hunks\` array. Use the \`index\` values exactly as shown — they are not necessarily contiguous, so never renumber them or invent indices.
|
|
11
11
|
3. Each commit must contain at least one hunk. No empty commits.
|
|
12
12
|
4. Each commit's \`id\` must be unique (e.g. \`commit-1\`, \`commit-2\`).
|
|
13
13
|
|
|
@@ -42,7 +42,7 @@ Your job has two levels:
|
|
|
42
42
|
## Critical constraints (output is rejected if violated)
|
|
43
43
|
|
|
44
44
|
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).
|
|
45
|
-
2. **
|
|
45
|
+
2. **Every hunk index present in the input must appear** across the union of every commit's \`hunks\` array across every branch. Use the \`index\` values exactly as shown — they are not necessarily contiguous, so never renumber them or invent indices.
|
|
46
46
|
3. Each commit must contain at least one hunk. No empty commits.
|
|
47
47
|
4. Each commit \`id\` must be unique across the whole response.
|
|
48
48
|
|
|
@@ -81,7 +81,7 @@ ${r}
|
|
|
81
81
|
${r}
|
|
82
82
|
- When in doubt, consider: "Would a reviewer want to see these changes together, or would separating them make review easier?" If separating helps, split.`}function u(e){return e===`consolidated`?`
|
|
83
83
|
Prefer fewer, larger commits. Combine related changes into a single commit when they serve the same purpose. A commit can touch many files if they are all part of the same logical change.`:e===`granular`?`
|
|
84
|
-
Prefer more, smaller commits. Each distinct change should be its own commit, even if multiple changes serve a similar purpose. Aim for commits that touch as few files as possible.`:``}function d(t){let{hunks:n,commitMessages:r,instructions:i,mode:a=`branches`,redactHunkContent:o,inputTokenBudget:s,systemPromptChars:c,onBudgetReduction:l}=t,u=a===`commits`?`Examine the following ${n.length} code hunks (indices
|
|
84
|
+
Prefer more, smaller commits. Each distinct change should be its own commit, even if multiple changes serve a similar purpose. Aim for commits that touch as few files as possible.`:``}function d(t){let{hunks:n,commitMessages:r,instructions:i,mode:a=`branches`,redactHunkContent:o,inputTokenBudget:s,systemPromptChars:c,onBudgetReduction:l}=t,u=n.map(e=>e.index).join(`, `),d=a===`commits`?`Examine the following ${n.length} code hunks (indices: ${u}) and organize them into commits. Every listed index must appear exactly once.`:`Examine the following ${n.length} code hunks (indices: ${u}) and organize them into commits and branches. Every listed index must appear exactly once.`,{prompt:f,hunksCondensedForBudget:p}=e({hunks:n,assemble:e=>{let t=`${d}
|
|
85
85
|
|
|
86
86
|
<hunks>
|
|
87
87
|
${e}
|
|
@@ -94,7 +94,7 @@ ${JSON.stringify(r)}
|
|
|
94
94
|
</commit_messages>`),i&&(t+=`
|
|
95
95
|
|
|
96
96
|
Carefully follow these additional instructions, but do not deviate from the output structure:
|
|
97
|
-
${i}`),t+=`\n\n${a===`commits`?`Now organize the hunks into commits.`:`Now organize the hunks into commits and branches.`} Return only the <output> tag and no other text.`,t},redactHunkContent:o,inputTokenBudget:s,systemPromptChars:c});return l?.(
|
|
97
|
+
${i}`),t+=`\n\n${a===`commits`?`Now organize the hunks into commits.`:`Now organize the hunks into commits and branches.`} Return only the <output> tag and no other text.`,t},redactHunkContent:o,inputTokenBudget:s,systemPromptChars:c});return l?.(p),f}function f(e,t){let n;return n=t===`commits`?`Based on your analysis of the changes, now organize the hunks into commits. Each hunk must be assigned to exactly one commit.`:p(e.strategy),e.conventions?.promptFragment&&(n+=`\n\n${e.conventions.promptFragment}`),e.commitMessages?.length&&(n+=`\n\nMatch the style of these original commit messages:\n${JSON.stringify(e.commitMessages)}`),e.instructions&&(n+=`\n\nAdditional instructions:\n${e.instructions}`),t===`commits`?n+=`
|
|
98
98
|
|
|
99
99
|
Return your grouping as JSON inside <output> tags. Wrap all commits in a single branch with id "default", name "", title "", description "":
|
|
100
100
|
<output>
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ProgressEvent, TokenUsage } from "../../ai/types.js";
|
|
1
|
+
import { ProgressEvent, TokenUsage, ValidationRepair } 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";
|
|
@@ -69,6 +69,11 @@ interface GroupOutput {
|
|
|
69
69
|
callIds: string[];
|
|
70
70
|
/** Hunks condensed to metadata to fit the input-token budget. Undefined when the AI call was skipped. */
|
|
71
71
|
hunksCondensedCount?: number;
|
|
72
|
+
/**
|
|
73
|
+
* Deterministic repairs applied to the accepted AI response instead of spending a retry on it.
|
|
74
|
+
* Undefined when the response needed no repair.
|
|
75
|
+
*/
|
|
76
|
+
repairs?: ValidationRepair[];
|
|
72
77
|
}
|
|
73
78
|
//#endregion
|
|
74
79
|
export { GroupInput, GroupOutput, GroupingResult, GroupingStrategy };
|
|
@@ -2,6 +2,14 @@ import { ValidationResult } from "../ai-loop.js";
|
|
|
2
2
|
import { GroupingResult } from "./types.js";
|
|
3
3
|
|
|
4
4
|
//#region src/tasks/group/validator.d.ts
|
|
5
|
-
|
|
5
|
+
interface GroupValidationOptions {
|
|
6
|
+
/**
|
|
7
|
+
* Commit ids whose hunks a later phase restores verbatim. A contested hunk is kept in the
|
|
8
|
+
* locked claimant: dropping it there would be undone by substitution, and the model would then
|
|
9
|
+
* be told the hunk is missing or duplicated when it did neither.
|
|
10
|
+
*/
|
|
11
|
+
lockedCommitIds?: ReadonlySet<string>;
|
|
12
|
+
}
|
|
13
|
+
declare function validateGroupResponse(responseText: string, expectedHunkIndices: ReadonlySet<number>, mode?: 'commits' | 'branches', existingBranchNames?: Set<string>, options?: GroupValidationOptions): ValidationResult<GroupingResult>;
|
|
6
14
|
//#endregion
|
|
7
|
-
export { validateGroupResponse };
|
|
15
|
+
export { GroupValidationOptions, validateGroupResponse };
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import{extractOutputTag as e}from"../ai-loop.js";import{validateJsonSchema as t}from"../utils/json-schema.js";import{groupingResultJsonSchema as n}from"./schema.js";function r(r,o,s=`branches`,c){let
|
|
1
|
+
import{extractOutputTag as e}from"../ai-loop.js";import{validateJsonSchema as t}from"../utils/json-schema.js";import{groupingResultJsonSchema as n}from"./schema.js";function r(r,o,s=`branches`,c,l){let u=null;try{if(u=e(r),!u)return i([`No <output> tag found in response`],a(s));let d=t(n,JSON.parse(u));if(!d.success)return i(d.issues.map(e=>`${e.path.join(`.`)}: ${e.message}`),a(s));let f=d.data,p=[],m=new Set,h=[],g=new Map,_=new Map,v=new Set,y=[],b=[],x=[],S=new Set,C=new Set;for(let e of f.branches){if(S.has(e.id))return i([`Duplicate branch ID: '${e.id}'`],a(s));if(S.add(e.id),s===`branches`&&e.name.length===0)return i([`Branch '${e.id}': missing or empty 'name'`],a(s));if(s===`branches`&&e.name.length>0){if(C.has(e.name))return i([`Duplicate branch name: '${e.name}'`],`Multiple branches use the name '${e.name}'. Each branch must have a unique name. Please rename one of them.`);if(C.add(e.name),c?.has(e.name))return i([`Branch name '${e.name}' already exists in the repository`],`The branch name '${e.name}' already exists in the repository. Please choose a different name for branch '${e.id}'.`)}for(let t of e.commits){v.has(t.id)&&y.push(t.id),v.add(t.id);let n=s===`branches`?`${e.id}/${t.id}`:t.id;for(let r of t.hunks){let i=g.get(r);i?i.push(n):g.set(r,[n]);let a=_.get(r);a?a.push({branchId:e.id,commitId:t.id,label:n}):_.set(r,[{branchId:e.id,commitId:t.id,label:n}])}}}let w=new Map,T=[],E=!1;for(let[e,t]of _){if(t.length<2)continue;if(h.push(e),s===`branches`&&new Set(t.map(e=>e.branchId)).size>1){E=!0;continue}let n=t.find(e=>l?.lockedCommitIds?.has(e.commitId))??t[0];w.set(e,n.label),T.push({hunkIndex:e,keptIn:n.label,droppedFrom:t.filter(e=>e!==n).map(e=>e.label),scope:new Set(t.map(e=>e.commitId)).size===1?`within-commit`:`across-commits`})}let D=h.length>0&&!E;for(let e of f.branches){let t=[];for(let n of e.commits){let r=s===`branches`?`${e.id}/${n.id}`:n.id,i=[],a=!1;for(let e of n.hunks){if(w.has(e)&&w.get(e)!==r){a=!0;continue}m.has(e)||(m.add(e),i.push(e))}n.hunks.length===0?b.push(n.id):i.length===0&&a&&x.push(n.id),t.push({id:n.id,message:n.message,explanation:n.explanation,hunkIndices:i})}p.push({id:e.id,name:e.name,title:e.title,description:e.description,commits:t})}if(y.length>0)return i([`Duplicate commit IDs: ${y.join(`, `)}`],`Some commit IDs appear multiple times. Each must be unique.\n\nDuplicates: ${y.join(`, `)}`);if(b.length>0)return i([`Empty commits (no hunks assigned): ${b.join(`, `)}`],`Every commit must contain at least one hunk. These commits have an empty \`hunks\` array: ${b.join(`, `)}.
|
|
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
|
|
4
|
-
`);e.push(`Duplicate hunks (each must appear in exactly ONE commit):\n${
|
|
3
|
+
Either assign at least one hunk to each of these commits, or remove them entirely. If a commit's purpose is already covered by another commit, remove the duplicate. If a commit is supposed to cover specific hunks, assign those hunk indices to it. Every hunk index still needs to appear in exactly one commit.`);let O=[...o].filter(e=>!m.has(e)).sort((e,t)=>e-t),k=[...m].filter(e=>!o.has(e)).sort((e,t)=>e-t),A=[...new Set(h)].sort((e,t)=>e-t),j=A.length>0&&!D;if(O.length>0||k.length>0||x.length>0||j){let e=[],t=[];if(A.length>0){let n=A.map(e=>` hunk ${e} → ${(g.get(e)??[]).join(`, `)}`).join(`
|
|
4
|
+
`);e.push(`Duplicate hunks (each must appear in exactly ONE commit):\n${n}`),t.push(`Duplicate hunks: ${A.join(`, `)}`)}O.length>0&&(e.push(`Missing hunks (not assigned to any commit): ${O.join(`, `)}`),t.push(`Missing hunks: ${O.join(`, `)}`)),k.length>0&&(e.push(`Extra hunks (not present in the input): ${k.join(`, `)}`),t.push(`Extra hunks: ${k.join(`, `)}`)),x.length>0&&(e.push(`Commits whose only hunks were already claimed by an earlier commit: ${x.join(`, `)}`),t.push(`Empty commits after dropping duplicate hunks: ${x.join(`, `)}`));let n=[...o].sort((e,t)=>e-t),r=[...g.values()].reduce((e,t)=>e+t.length,0);return i(t,`There are ${n.length} hunks, with indices: ${n.join(`, `)}. Every one of these indices must appear in exactly one commit — no duplicates, no omissions, no other indices. Use the indices exactly as given; do not renumber them.
|
|
5
5
|
|
|
6
|
-
Your response listed ${
|
|
6
|
+
Your response listed ${r} hunk references across ${g.size} unique indices. Expected: ${n.length} references across ${n.length} unique indices.
|
|
7
7
|
|
|
8
8
|
Specific problems:
|
|
9
9
|
${e.map(e=>`- ${e}`).join(`
|
|
@@ -11,7 +11,7 @@ ${e.map(e=>`- ${e}`).join(`
|
|
|
11
11
|
|
|
12
12
|
To fix duplicates: for each duplicated hunk index, decide which one commit it truly belongs in and remove it from the others. Do not split a single hunk across two commits — git only knows how to apply a hunk once.
|
|
13
13
|
|
|
14
|
-
Return the corrected JSON inside <output> tags.`)}return{valid:!0,result:{branches:
|
|
14
|
+
Return the corrected JSON inside <output> tags.`)}if(T.length>0){let e=[...T].sort((e,t)=>e.hunkIndex-t.hunkIndex),t=e.map(e=>`hunk ${e.hunkIndex} kept in ${e.keptIn}`).join(`; `);return{valid:!0,result:{branches:p},repairs:[{kind:`duplicate-hunks`,hunkIndices:e.map(e=>e.hunkIndex),claims:e,detail:`Dropped duplicate claims on ${e.length} hunk(s): ${t}`}]}}return{valid:!0,result:{branches:p}}}catch(e){let t=e instanceof Error?e.message:String(e),n=(u??r).slice(-300);return i([`Failed to parse response as valid JSON: ${t}`,`Response tail: ${n}`],a(s))}}function i(e,t){return{valid:!1,errors:e,retryFeedback:t}}function a(e){return e===`commits`?`Your response has an invalid structure. Please return JSON inside <output> tags. Use a single branch with id "default", name "", description "":
|
|
15
15
|
<output>
|
|
16
16
|
{
|
|
17
17
|
"branches": [
|
|
@@ -20,7 +20,7 @@ ${d}
|
|
|
20
20
|
## Critical constraints (output is rejected if violated)
|
|
21
21
|
|
|
22
22
|
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).
|
|
23
|
-
2. **
|
|
23
|
+
2. **Every hunk index present in the input must appear** across the union of every commit's \`hunks\` array across every branch. Use the \`index\` values exactly as shown — they are not necessarily contiguous, so never renumber them or invent indices.
|
|
24
24
|
3. Each commit must contain at least one hunk. No empty commits.
|
|
25
25
|
4. Each commit \`id\` must be unique across the whole response.
|
|
26
26
|
5. \`orderedCommitIds\` must list every commit ID in the branch in the order they should be applied.
|
|
@@ -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
|
|
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`);let u=new Set(s.map(e=>e.index));if(o.session?.completedSteps.quickCompose)return{result:o.session.completedSteps.quickCompose,session:o.session,attempts:0,durationMs:0,callIds:[]};let d=o.promptOverride?.system??r(o.conventions,c,o.strategy,l),f=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),p=await t({model:o.model,systemPrompt:d,userPrompt:f,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,u,c,l,o.existingBranchNames),taskName:`quick-compose`}),m=o.session?.completedSteps??{},h={...p.session,completedSteps:{...m,quickCompose:p.result}};return{result:p.result,session:h,usage:p.usage,attempts:p.attempts,durationMs:p.durationMs,callIds:p.callIds}}export{o as quickCompose};
|
|
@@ -2,6 +2,6 @@ import { ValidationResult } from "../ai-loop.js";
|
|
|
2
2
|
import { QuickComposeResult } from "./types.js";
|
|
3
3
|
|
|
4
4
|
//#region src/tasks/quick-compose/validator.d.ts
|
|
5
|
-
declare function validateQuickComposeResponse(responseText: string,
|
|
5
|
+
declare function validateQuickComposeResponse(responseText: string, expectedHunkIndices: ReadonlySet<number>, mode?: 'commits' | 'branches', includePartitions?: boolean, existingBranchNames?: Set<string>): ValidationResult<QuickComposeResult>;
|
|
6
6
|
//#endregion
|
|
7
7
|
export { validateQuickComposeResponse };
|
|
@@ -3,12 +3,12 @@ import{extractOutputTag as e}from"../ai-loop.js";import{validateJsonSchema as t}
|
|
|
3
3
|
Either assign at least one hunk to each of these commits, or remove them entirely. If a commit's purpose is already covered by another commit, remove the duplicate. If a commit is supposed to cover specific hunks, assign those hunk indices to it. Every hunk index still needs to appear in exactly one commit.`);if(v.length>0){let e=[...new Set(v)].sort((e,t)=>e-t),t=e.map(e=>` hunk ${e} → ${(y.get(e)??[]).join(`, `)}`).join(`
|
|
4
4
|
`),n=[...y.values()].reduce((e,t)=>e+t.length,0);return o([`Duplicate hunks: ${e.join(`, `)}`],`Some hunks appear in multiple commits. Each hunk can only be used once.
|
|
5
5
|
|
|
6
|
-
Your response listed ${n} hunk references across ${y.size} unique indices. Expected: ${c} references across ${c} unique indices.
|
|
6
|
+
Your response listed ${n} hunk references across ${y.size} unique indices. Expected: ${c.size} references across ${c.size} unique indices.
|
|
7
7
|
|
|
8
8
|
Duplicates (each hunk index → the commits it landed in):
|
|
9
9
|
${t}
|
|
10
10
|
|
|
11
|
-
To fix: for each duplicated hunk index, decide which one commit it truly belongs in and remove it from the others. Do not split a single hunk across two commits — git only knows how to apply a hunk once.`)}let w=
|
|
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=[...c].filter(e=>!_.has(e)).sort((e,t)=>e-t),T=[..._].filter(e=>!c.has(e)).sort((e,t)=>e-t);return w.length>0?o([`Missing hunks: ${w.join(`, `)}`],`Some hunks are not assigned to any commit. All hunks must be included.\n\nMissing: ${w.join(`, `)}`):T.length>0?o([`Extra hunks: ${T.join(`, `)}`],`Some hunks in your response don't exist in the input.\n\nExtra: ${T.join(`, `)}`):{valid:!0,result:{branches:C,rationale:m.rationale}}}catch(e){let t=e instanceof Error?e.message:String(e),n=(f??r).slice(-300);return o([`Failed to parse response as valid JSON: ${t}`,`Response tail: ${n}`],s(l,u))}}function i(e,t){let n=new Map(t.map((e,t)=>[e,t])),r=0;for(let t of e)for(let e of t.commitIds){let t=n.get(e);if(t===void 0||t!==r)return!1;r++}return r===t.length}function a(e){return e.startsWith(`-`)||e.startsWith(`.`)||e.endsWith(`.lock`)||e.endsWith(`.`)||e.endsWith(`/`)||e.includes(`..`)||e.includes(`~`)||e.includes(`^`)||e.includes(`:`)||e.includes(`\\`)||e.includes(` `)||e.includes(`?`)||e.includes(`*`)||e.includes(`[`)||e.includes(`@{`)?!1:e.length>0}function o(e,t){return{valid:!1,errors:e,retryFeedback:t}}function s(e,t){return`Your response has an invalid structure. Please return JSON inside <output> tags:
|
|
12
12
|
<output>
|
|
13
13
|
{
|
|
14
14
|
"branches": [
|
|
@@ -6,7 +6,7 @@ ${n}
|
|
|
6
6
|
|
|
7
7
|
If any locked commit's id is missing from your output, the validator will reject your response. Locked commits' hunk indices must remain exclusively in those commits — do NOT move them into other commits. You may reposition locked entries in the ordering only if the user explicitly asks; otherwise keep them roughly where they were.
|
|
8
8
|
|
|
9
|
-
The other commits (and their hunks) may be freely restructured (split, merged, reordered, re-messaged) per the user's request below.`)}e.instructions?.trim()?t.push(`USER'S REFINEMENT REQUEST:\n${e.instructions.trim()}`):t.push(`The user did not include specific instructions on this turn. Review your prior plan critically and produce an improved version that addresses any obvious issues with grouping, ordering, or commit messages.`);let n=e.mode===`branches`?`across every commit, across every branch`:`across every commit`,r=e.lockedCommits.length>0?`
|
|
9
|
+
The other commits (and their hunks) may be freely restructured (split, merged, reordered, re-messaged) per the user's request below.`)}e.instructions?.trim()?t.push(`USER'S REFINEMENT REQUEST:\n${e.instructions.trim()}`):t.push(`The user did not include specific instructions on this turn. Review your prior plan critically and produce an improved version that addresses any obvious issues with grouping, ordering, or commit messages.`);let n=e.mode===`branches`?`across every commit, across every branch`:`across every commit`,r=[...e.hunkIndices].sort((e,t)=>e-t),i=e.lockedCommits.length>0?`
|
|
10
10
|
- Every locked commit listed above appears in your output as its own entry, with id/message/hunks unchanged.`:``;return t.push(`OUTPUT FORMAT — emit JSON inside a single <output> tag. Every commit object MUST have ALL FOUR of these keys: "id" (a unique string), "message" (a string), "explanation" (a string describing what/why), and "hunks" (an array of integer indices). Omitting any of these keys — especially "hunks" — is a validation error.
|
|
11
11
|
|
|
12
12
|
This is a concrete example of the structure. Your actual ids, messages, explanations, and hunk assignments will differ — but EVERY commit entry must follow this shape:
|
|
@@ -31,10 +31,10 @@ This is a concrete example of the structure. Your actual ids, messages, explanat
|
|
|
31
31
|
Constraints (the validator enforces all of these):
|
|
32
32
|
- Each commit object contains "id" (unique string), "message" (string), "explanation" (string), and "hunks" (array of integers). All four keys are required.
|
|
33
33
|
- Each hunk index appears EXACTLY ONCE ${n}.
|
|
34
|
-
- Every hunk
|
|
35
|
-
- Each commit contains at least one hunk (no empty commits).${
|
|
34
|
+
- Every one of these hunk indices appears in some commit's hunks array: ${r.join(`, `)}. Total = ${String(r.length)} indices. Use the indices exactly as given; do not renumber them.
|
|
35
|
+
- Each commit contains at least one hunk (no empty commits).${i}
|
|
36
36
|
|
|
37
|
-
Before emitting, recount: the union of every commit's "hunks" array must contain exactly
|
|
37
|
+
Before emitting, recount: the union of every commit's "hunks" array must contain exactly these ${String(r.length)} integers: ${r.join(`, `)}.
|
|
38
38
|
|
|
39
39
|
Return only the <output> tag and no other text.`),t.join(`
|
|
40
40
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{TaskInputError as e}from"../../errors/task.js";import{runAiLoop as t}from"../ai-loop.js";import{applyUserPromptOverride as n}from"../types.js";import{buildRefinementPrompt as r}from"./prompt.js";import{validateRefineResponse as i}from"./validator.js";async function a(a){if(a.session.messages.length===0)throw new e(`refine: session must contain prior messages from a previous compose call.`);if(a.
|
|
1
|
+
import{TaskInputError as e}from"../../errors/task.js";import{runAiLoop as t}from"../ai-loop.js";import{applyUserPromptOverride as n}from"../types.js";import{buildRefinementPrompt as r}from"./prompt.js";import{validateRefineResponse as i}from"./validator.js";async function a(a){if(a.session.messages.length===0)throw new e(`refine: session must contain prior messages from a previous compose call.`);if(a.hunkIndices.size===0)throw new e(`refine: hunkIndices must not be empty.`);let o=a.mode??`branches`,s=new Map;for(let e of a.lockedCommits??[])s.set(e.id,e);let c=n(r({instructions:a.instructions,lockedCommits:a.lockedCommits??[],clientContext:a.clientContext,hunkIndices:a.hunkIndices,mode:o}),a.promptOverride),l=await t({model:a.model,systemPrompt:void 0,userPrompt:c,session:a.session,maxRetries:a.maxRetries,maxTokens:a.maxTokens,onProgress:a.onProgress,cancellation:a.cancellation,onBeforeModelCall:a.onBeforeModelCall,onAfterModelCall:a.onAfterModelCall,onBeforePrompt:a.onBeforePrompt,validate:e=>i(e,{expectedHunkIndices:a.hunkIndices,mode:o,lockedCommits:s,lockedRuns:a.lockedRuns}),taskName:`refine`}),u={...l.session,completedSteps:a.session.completedSteps};return{result:l.result,session:u,usage:l.usage,attempts:l.attempts,durationMs:l.durationMs,callIds:l.callIds,repairs:l.repairs}}export{a as refine};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ProgressEvent, TokenUsage } from "../../ai/types.js";
|
|
1
|
+
import { ProgressEvent, TokenUsage, ValidationRepair } from "../../ai/types.js";
|
|
2
2
|
import { CommitSuggestion, PromptOverride } from "../types.js";
|
|
3
3
|
import { OnAfterModelCall, OnBeforeModelCall, OnBeforePrompt } from "../ai-loop.js";
|
|
4
4
|
import { GroupingResult } from "../group/types.js";
|
|
@@ -44,9 +44,10 @@ interface RefineInput {
|
|
|
44
44
|
/** Free-text consumer-supplied context (e.g. UI-ordering notes) injected verbatim into
|
|
45
45
|
* the prompt. Library-opaque; the consumer is responsible for content. */
|
|
46
46
|
clientContext?: string;
|
|
47
|
-
/**
|
|
48
|
-
*
|
|
49
|
-
|
|
47
|
+
/** The hunk indices the prior plan was produced from — the hunks' original `index` values,
|
|
48
|
+
* which are not necessarily contiguous (e.g. when a `hunkFilter` dropped entries). Used to
|
|
49
|
+
* validate that the refined plan still covers exactly these indices, each exactly once. */
|
|
50
|
+
hunkIndices: ReadonlySet<number>;
|
|
50
51
|
/** Output mode — must match the mode the prior plan was produced in (the task's locked-
|
|
51
52
|
* commits substitution and re-validation rely on this). Defaults to `'branches'`. */
|
|
52
53
|
mode?: 'commits' | 'branches';
|
|
@@ -71,6 +72,11 @@ interface RefineOutput {
|
|
|
71
72
|
attempts: number;
|
|
72
73
|
durationMs: number;
|
|
73
74
|
callIds: string[];
|
|
75
|
+
/**
|
|
76
|
+
* Deterministic repairs applied to the accepted AI response instead of spending a retry.
|
|
77
|
+
* Undefined when the response needed no repair.
|
|
78
|
+
*/
|
|
79
|
+
repairs?: ValidationRepair[];
|
|
74
80
|
}
|
|
75
81
|
//#endregion
|
|
76
82
|
export { RefineInput, RefineOutput };
|
|
@@ -4,7 +4,7 @@ import { GroupingResult } from "../group/types.js";
|
|
|
4
4
|
|
|
5
5
|
//#region src/tasks/refine/validator.d.ts
|
|
6
6
|
interface ValidateRefineParams {
|
|
7
|
-
|
|
7
|
+
expectedHunkIndices: ReadonlySet<number>;
|
|
8
8
|
mode: 'commits' | 'branches';
|
|
9
9
|
lockedCommits: Map<string, CommitSuggestion>;
|
|
10
10
|
/** Maximal runs of consecutive locked commits per prior-plan branch, in original order.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{validateGroupResponse as e}from"../group/validator.js";function t(t,r){let i=e(t,r.
|
|
1
|
+
import{validateGroupResponse as e}from"../group/validator.js";function t(t,r){let i=e(t,r.expectedHunkIndices,r.mode,void 0,{lockedCommitIds:new Set(r.lockedCommits.keys())});if(!i.valid||i.result===void 0)return i;let o=i.result;if(r.lockedCommits.size===0)return i;let s=new Map;for(let e of o.branches)for(let t=0;t<e.commits.length;t++)s.set(e.commits[t].id,{branch:e,commitIndex:t});let c=[];for(let e of r.lockedCommits.keys())s.has(e)||c.push(e);if(c.length>0){let e=c.map(e=>`'${e}'`).join(`, `);return{valid:!1,errors:[`Missing locked commits: ${e}`],retryFeedback:`Your response is missing the following LOCKED commits, which must always appear in the output: ${e}. Re-emit the plan with these commits present (preserving their id, message, explanation, and hunks exactly).`,repairs:i.repairs}}for(let[e,t]of r.lockedCommits){let n=s.get(e);if(n==null)continue;let r=n.branch.commits[n.commitIndex];n.branch.commits[n.commitIndex]={...r,id:t.id,message:t.message,explanation:t.explanation,hunkIndices:[...t.hunkIndices]}}let l=a(o,r.expectedHunkIndices);return l==null?(n(o,r.lockedRuns),{valid:!0,result:o,repairs:i.repairs}):{valid:!1,errors:[l],retryFeedback:`After restoring the locked commits' original hunk assignments, the resulting plan is structurally invalid: ${l}. This means you assigned hunks belonging to a locked commit to a different commit. Locked commits' hunks must remain exclusive to them — do not move those hunks elsewhere.`,repairs:i.repairs}}function n(e,t){if(t==null||t.length===0)return;let n=t.flat(),a=new Map(n.map((e,t)=>[e,t])),o=new Set(n);for(let t of e.branches)r(t,o,a);for(let n of t)n.length>=2&&i(e,n)}function r(e,t,n){let r=[],i=[];for(let n=0;n<e.commits.length;n++)t.has(e.commits[n].id)&&(r.push(n),i.push(e.commits[n]));if(!(i.length<2)){i.sort((e,t)=>(n.get(e.id)??0)-(n.get(t.id)??0));for(let t=0;t<r.length;t++)e.commits[r[t]]=i[t]}}function i(e,t){let n=new Set(t),r=e.branches.filter(e=>e.commits.some(e=>n.has(e.id)));if(r.length!==1)return;let i=r[0],a=[];for(let e of t){let t=i.commits.find(t=>t.id===e);t!=null&&a.push(t)}if(a.length<2)return;let o=Math.min(...a.map(e=>i.commits.indexOf(e))),s=i.commits.slice(0,o).filter(e=>!n.has(e.id)).length,c=i.commits.filter(e=>!n.has(e.id));c.splice(s,0,...a),i.commits=c}function a(e,t){let n=new Set,r=[],i=[],a=new Set,o=[];for(let t of e.branches)for(let e of t.commits){if(a.has(e.id)&&o.push(e.id),a.add(e.id),e.hunkIndices.length===0){i.push(e.id);continue}for(let t of e.hunkIndices)n.has(t)&&r.push(t),n.add(t)}if(o.length>0)return`duplicate commit ids after substitution: ${o.join(`, `)}`;if(i.length>0)return`empty commits after substitution (no hunks assigned): ${i.join(`, `)}`;if(r.length>0)return`hunk indices appear in more than one commit: ${[...new Set(r)].join(`, `)}`;let s=[...t].filter(e=>!n.has(e)).sort((e,t)=>e-t);if(s.length>0)return`hunk indices missing from the plan: ${s.join(`, `)}`;let c=[...n].filter(e=>!t.has(e)).sort((e,t)=>e-t);if(c.length>0)return`hunk indices not present in the input: ${c.join(`, `)}`}export{t as validateRefineResponse};
|
|
@@ -10,7 +10,7 @@ import { OrderingResult } from "../tasks/order/types.js";
|
|
|
10
10
|
import { ComposeSession } from "../tasks/session.js";
|
|
11
11
|
import { PartitionStrategy } from "../tasks/partition/types.js";
|
|
12
12
|
import { ComposeContext, ComposeSource, ComposeTarget, OverrideSafetyFailures } from "./types.js";
|
|
13
|
-
import { SafetySnapshot, StashConflict } from "../git/types.js";
|
|
13
|
+
import { SafetySnapshot, StashConflict, StashNotRestored } from "../git/types.js";
|
|
14
14
|
import { ComposeBranchPlan } from "../git/apply-target.js";
|
|
15
15
|
import { AiModelPort, UndoId } from "@gitkraken/shared-tools";
|
|
16
16
|
|
|
@@ -127,6 +127,8 @@ interface ApplyComposePlanResult {
|
|
|
127
127
|
unappliedHunkIndices?: number[];
|
|
128
128
|
/** Set when the post-apply stash pop hit conflicts. Apply itself succeeded; the named stash is preserved. */
|
|
129
129
|
stashConflict?: StashConflict;
|
|
130
|
+
/** Set when the post-apply stash pop was skipped or failed without conflicts. Apply itself succeeded. */
|
|
131
|
+
stashNotRestored?: StashNotRestored;
|
|
130
132
|
}
|
|
131
133
|
/** Controls how many AI pipeline steps run and how deeply each reasons. */
|
|
132
134
|
type ComposeDepth = 'quick' | 'balanced' | 'deep';
|
|
@@ -457,6 +459,8 @@ interface ComposeResult {
|
|
|
457
459
|
unappliedHunkIndices?: number[];
|
|
458
460
|
/** See {@link ApplyComposePlanResult.stashConflict}. */
|
|
459
461
|
stashConflict?: StashConflict;
|
|
462
|
+
/** See {@link ApplyComposePlanResult.stashNotRestored}. */
|
|
463
|
+
stashNotRestored?: StashNotRestored;
|
|
460
464
|
}
|
|
461
465
|
/**
|
|
462
466
|
* Compose code changes into a clean set of commits and branches.
|
|
@@ -505,4 +509,4 @@ declare function composePlan(input: ComposePlanInput): Promise<ComposePlanResult
|
|
|
505
509
|
*/
|
|
506
510
|
declare function applyComposePlan(input: ApplyComposePlanInput): Promise<ApplyComposePlanResult>;
|
|
507
511
|
//#endregion
|
|
508
|
-
export { ApplyComposePlanInput, ApplyComposePlanResult, ComposeApplyPlan, type ComposeBranchPlan, type ComposeContext, ComposeDepth, ComposeDiffStats, ComposeInput, ComposePlan, ComposePlanInput, ComposePlanResult, ComposeProgressEvent, ComposeResult, type ComposeSource, ComposeStrategy, type ComposeTarget, type OverrideSafetyFailures, type StashConflict, applyComposePlan, compose, composePlan, defaultPhaseMessages };
|
|
512
|
+
export { ApplyComposePlanInput, ApplyComposePlanResult, ComposeApplyPlan, type ComposeBranchPlan, type ComposeContext, ComposeDepth, ComposeDiffStats, ComposeInput, ComposePlan, ComposePlanInput, ComposePlanResult, ComposeProgressEvent, ComposeResult, type ComposeSource, ComposeStrategy, type ComposeTarget, type OverrideSafetyFailures, type StashConflict, type StashNotRestored, applyComposePlan, compose, composePlan, defaultPhaseMessages };
|