@gitkraken/compose-tools 0.9.0 → 0.10.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 +3 -3
- package/README.md +13 -8
- package/dist/ai/types.d.ts +25 -9
- package/dist/tasks/ai-loop.d.ts +29 -3
- package/dist/tasks/ai-loop.js +1 -1
- package/dist/tasks/compose-group/compose-group.js +1 -1
- package/dist/tasks/group/group.js +1 -1
- package/dist/tasks/group/repair.d.ts +39 -0
- package/dist/tasks/group/repair.js +16 -0
- package/dist/tasks/group/types.d.ts +1 -1
- package/dist/tasks/group/validator.d.ts +6 -0
- package/dist/tasks/group/validator.js +6 -6
- package/dist/tasks/utils/budget.d.ts +9 -1
- package/dist/tasks/utils/budget.js +1 -1
- package/dist/workflows/compose.d.ts +4 -2
- package/dist/workflows/compose.js +1 -1
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
## 0.10.0
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
### Minor Changes
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
- [#90](https://github.com/gitkraken/core-tools/pull/90) [`19c7d2f`](https://github.com/gitkraken/core-tools/commit/19c7d2f5aadd84159f5eaf6e7172cae6ff70134b) Thanks [@alejandroSuch](https://github.com/alejandroSuch)! - A `group` / `composeGroup` response that leaves hunks unassigned is now repaired with one small targeted call instead of a full retry. After the deterministic dedupe above, the remaining missing indices are sent on their own — those hunks' content plus the response's commits as id and message, no diff, no system prompt, no history — and the model answers with a `{hunkIndex, commitId}` assignment map. Asking for an assignment map rather than a fresh grouping keeps the repair from restructuring decisions the first response already made. Over four eval runs missing hunks accounted for 4 of 29 outright `VALIDATION_EXHAUSTED` failures, each previously costing a re-send of the whole diff. The repair is reported as a `missing-hunks` `ValidationRepair` (distinct from the free `duplicate-hunks` kind) and its call is accounted for like any other: its own entry in `callIds`, its own `onBeforeModelCall` / `onAfterModelCall` pair (flagged `isRepair`, sharing the `attempt` of the response it repairs), its tokens in `usage`, and its prompt through the `onBeforePrompt` gate. At most one repair per loop: a repair response that is itself invalid — malformed, incomplete, naming an unknown commit, or moving a hunk it was not asked about — falls through to the existing retry behaviour, as does a repair call that fails in transport. Conversation-mode `group` (hunks in the model's context, not in the task input) has no hunk bodies to ask about and retries as before, and a repair prompt that would exceed the input-token budget drops the hunk bodies and asks on file metadata alone. The repair honours `condenseHunks` / `condensed` and `redactHunkContent` like the main prompt; in `branches` mode each commit in the list carries its branch name and title. It is skipped, straight to the retry, when more than a quarter of the hunks are missing (a repair of two or fewer hunks is always tried), since past that the repair re-sends most of the diff. Declining the repair prompt at `onBeforePrompt` falls back to the retry instead of aborting. `feedbackTargetCallId` points at the response a repair completed, not at the repair call.
|
|
8
8
|
|
|
9
9
|
## [0.9.0] - 2026-10-05
|
|
10
10
|
|
package/README.md
CHANGED
|
@@ -256,15 +256,18 @@ Equivalent to calling `composePlan` — useful when you want the sugar `compose`
|
|
|
256
256
|
Per-call observability hooks. Informational only — the library doesn't interpret the return.
|
|
257
257
|
|
|
258
258
|
```typescript
|
|
259
|
-
onBeforeModelCall: ({ phase, attempt, maxAttempts, callId }) => {
|
|
259
|
+
onBeforeModelCall: ({ phase, attempt, maxAttempts, callId, isRepair }) => {
|
|
260
260
|
// phase: 'analyze' | 'group' | 'order' | 'partition' | 'compose-group' | 'quick-compose'
|
|
261
261
|
// callId: stable UUID — correlates with onAfterModelCall and with result.feedbackTargetCallId
|
|
262
|
+
// isRepair: true for a targeted missing-hunks repair call, which shares the `attempt` of the
|
|
263
|
+
// response it repairs
|
|
262
264
|
},
|
|
263
265
|
|
|
264
|
-
onAfterModelCall: ({ phase, attempt, callId, success, durationMs, usage, error, promptVersion, repairs }) => {
|
|
266
|
+
onAfterModelCall: ({ phase, attempt, callId, success, durationMs, usage, error, promptVersion, repairs, isRepair }) => {
|
|
265
267
|
// Fires on both success and error, after the response validator runs (durationMs still
|
|
266
|
-
// measures only the model call). `repairs` lists
|
|
267
|
-
//
|
|
268
|
+
// measures only the model call). `repairs` lists the fixes applied to an accepted response,
|
|
269
|
+
// see ValidationRepair: `duplicate-hunks` is deterministic and free, `missing-hunks` costs one
|
|
270
|
+
// extra model call. `isRepair` as above. An exception thrown here is swallowed.
|
|
268
271
|
},
|
|
269
272
|
```
|
|
270
273
|
|
|
@@ -272,10 +275,10 @@ Consumers build their own per-call telemetry from these. No built-in telemetry i
|
|
|
272
275
|
|
|
273
276
|
### `onBeforePrompt`
|
|
274
277
|
|
|
275
|
-
Gating hook — run immediately before each `model.generate`. Returning `false` aborts the workflow with `ComposeWorkflowError('CANCELLED')
|
|
278
|
+
Gating hook — run immediately before each `model.generate`. Returning `false` aborts the workflow with `ComposeWorkflowError('CANCELLED')`, except on a missing-hunks repair call (`isRepair: true`): declining that one skips the repair and falls back to a regular retry, which goes through this gate too. Use for large-prompt confirmations, cost warnings, org policy checks.
|
|
276
279
|
|
|
277
280
|
```typescript
|
|
278
|
-
onBeforePrompt: async ({ phase, attempt, tokenEstimate, charCount, messageCount }) => {
|
|
281
|
+
onBeforePrompt: async ({ phase, attempt, tokenEstimate, charCount, messageCount, isRepair }) => {
|
|
279
282
|
if (tokenEstimate > 20_000) {
|
|
280
283
|
return await showUserConfirmation(`Large prompt (${tokenEstimate} tokens). Continue?`);
|
|
281
284
|
}
|
|
@@ -398,6 +401,8 @@ const second = await composePlan({ git, model, depth: 'deep', session: first.ses
|
|
|
398
401
|
|
|
399
402
|
A resumed task contributes no usage and no callIds — telemetry for that phase reflects the original run.
|
|
400
403
|
|
|
404
|
+
A phase's `callIds` has one entry per attempt plus one per missing-hunks repair, so it can be longer than `attempts`.
|
|
405
|
+
|
|
401
406
|
### `promptOverrides`
|
|
402
407
|
|
|
403
408
|
Per-task prompt overrides for consumers with their own prompt template services (server-driven templates, provider-specific templates, org-tuned wording).
|
|
@@ -455,10 +460,10 @@ Every top-level entry point returns an object with these common fields:
|
|
|
455
460
|
| `snapshot` | all paths | Safety hash fingerprint of the collection — carry it to apply time |
|
|
456
461
|
| `source` | all paths | `{ type, hunks, commitMessages?, branchName?, baseBranch? }` — the hunks the AI operated on |
|
|
457
462
|
| `session` | all paths | Pass to a subsequent call to resume |
|
|
458
|
-
| `phases` | all paths | `PhaseDetail[]` with `{ name, durationMs, attempts, usage?, modelId?, promptVersion?, callIds?, repairs? }` per phase |
|
|
463
|
+
| `phases` | all paths | `PhaseDetail[]` with `{ name, durationMs, attempts, usage?, modelId?, promptVersion?, callIds?, repairs? }` per phase; `callIds` can outnumber `attempts` by the repair calls |
|
|
459
464
|
| `usage` | all paths | Aggregated `{ inputTokens, outputTokens, ... }` across all AI calls |
|
|
460
465
|
| `diffStats` | sugar / plan | `{ fileCount, hunkCount, addedLines, removedLines }` |
|
|
461
|
-
| `feedbackTargetCallId` | when AI ran | callId of the last validated model response — stamp this on thumbs up/down feedback events |
|
|
466
|
+
| `feedbackTargetCallId` | when AI ran | callId of the last validated model response (after a missing-hunks repair, the response it completed, not the repair call) — stamp this on thumbs up/down feedback events |
|
|
462
467
|
| `hunksFilteredCount` | when `hunkFilter` ran | Number of hunks the filter dropped |
|
|
463
468
|
| `commitShas` | apply paths | `{ commitId: sha }` mapping for every commit written |
|
|
464
469
|
| `undoId` | apply paths | Pass to `undoCompose()` to reverse |
|
package/dist/ai/types.d.ts
CHANGED
|
@@ -31,14 +31,27 @@ interface DuplicateHunksRepair {
|
|
|
31
31
|
detail: string;
|
|
32
32
|
}
|
|
33
33
|
/**
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
34
|
+
* Hunk indices assigned to no commit were placed by asking the model where they belong, sending
|
|
35
|
+
* only those hunks and the commit list. Costs one extra model call, which shows up in the usual
|
|
36
|
+
* call, token and cost accounting.
|
|
37
|
+
*/
|
|
38
|
+
interface MissingHunksRepair {
|
|
39
|
+
kind: 'missing-hunks';
|
|
40
|
+
/** The hunk indices the repair placed, ascending. */
|
|
41
|
+
hunkIndices: number[];
|
|
42
|
+
/** Unstable log string. */
|
|
43
|
+
detail: string;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* A fix a response validator applied to an AI response that would otherwise have been rejected,
|
|
47
|
+
* sparing a full retry. A repaired response is treated as valid — repairs are reported for
|
|
48
|
+
* observability, not as a failure signal. `duplicate-hunks` is deterministic and free;
|
|
49
|
+
* `missing-hunks` costs one small extra model call, visible in the usual call and cost accounting.
|
|
37
50
|
*
|
|
38
|
-
* This
|
|
39
|
-
*
|
|
51
|
+
* This is not an exhaustive record of every correction a validator makes: refine's locked-content
|
|
52
|
+
* substitution and `restoreLockedOrdering` stay silent.
|
|
40
53
|
*/
|
|
41
|
-
type ValidationRepair = DuplicateHunksRepair;
|
|
54
|
+
type ValidationRepair = DuplicateHunksRepair | MissingHunksRepair;
|
|
42
55
|
interface TokenUsage {
|
|
43
56
|
inputTokens: number;
|
|
44
57
|
outputTokens: number;
|
|
@@ -58,10 +71,13 @@ interface PhaseDetail {
|
|
|
58
71
|
modelId?: string;
|
|
59
72
|
/** Identifier of the prompt template used for this phase, when a consumer override supplied one. */
|
|
60
73
|
promptVersion?: string;
|
|
61
|
-
/**
|
|
74
|
+
/**
|
|
75
|
+
* Stable call IDs for each model.generate invocation in this phase: one per attempt, plus one
|
|
76
|
+
* for each targeted repair, so the count can exceed `attempts`.
|
|
77
|
+
*/
|
|
62
78
|
callIds?: string[];
|
|
63
79
|
/**
|
|
64
|
-
*
|
|
80
|
+
* Repairs applied to this phase's accepted response instead of spending a retry.
|
|
65
81
|
* Undefined when the response needed no repair.
|
|
66
82
|
*/
|
|
67
83
|
repairs?: ValidationRepair[];
|
|
@@ -73,4 +89,4 @@ interface ProgressEvent {
|
|
|
73
89
|
errors?: string[];
|
|
74
90
|
}
|
|
75
91
|
//#endregion
|
|
76
|
-
export { DuplicateHunksRepair, PhaseDetail, ProgressEvent, RepairScope, RepairedHunkClaim, TokenUsage, ValidationRepair, addUsage, emptyUsage };
|
|
92
|
+
export { DuplicateHunksRepair, MissingHunksRepair, PhaseDetail, ProgressEvent, RepairScope, RepairedHunkClaim, TokenUsage, ValidationRepair, addUsage, emptyUsage };
|
package/dist/tasks/ai-loop.d.ts
CHANGED
|
@@ -16,6 +16,12 @@ interface BeforeModelCallInfo {
|
|
|
16
16
|
maxAttempts: number;
|
|
17
17
|
/** Stable UUID identifying this model.generate call. Pairs with the matching After event. */
|
|
18
18
|
callId: string;
|
|
19
|
+
/**
|
|
20
|
+
* True when this is a targeted repair of the previous response rather than a fresh attempt:
|
|
21
|
+
* a much smaller prompt carrying only what the fix needs. Shares `attempt` with the call
|
|
22
|
+
* whose response it repairs.
|
|
23
|
+
*/
|
|
24
|
+
isRepair?: boolean;
|
|
19
25
|
}
|
|
20
26
|
interface AfterModelCallInfo {
|
|
21
27
|
phase: string;
|
|
@@ -31,17 +37,20 @@ interface AfterModelCallInfo {
|
|
|
31
37
|
/** Identifier of the prompt template used for this call, when a consumer override supplied one. */
|
|
32
38
|
promptVersion?: string;
|
|
33
39
|
/**
|
|
34
|
-
*
|
|
40
|
+
* Repairs applied to this response so it could be accepted instead of retried.
|
|
35
41
|
* Undefined when the response needed no repair (or when the call itself failed).
|
|
36
42
|
*/
|
|
37
43
|
repairs?: ValidationRepair[];
|
|
44
|
+
/** See {@link BeforeModelCallInfo.isRepair}. */
|
|
45
|
+
isRepair?: boolean;
|
|
38
46
|
}
|
|
39
47
|
type OnBeforeModelCall = (info: BeforeModelCallInfo) => void;
|
|
40
48
|
type OnAfterModelCall = (info: AfterModelCallInfo) => void;
|
|
41
49
|
/**
|
|
42
50
|
* Gating hook fired immediately before each `model.generate` call. Returning
|
|
43
51
|
* `false` aborts the loop with a native `AbortError` (DOMException with
|
|
44
|
-
* `name === 'AbortError'`)
|
|
52
|
+
* `name === 'AbortError'`), except on a repair call (`isRepair`), where it skips
|
|
53
|
+
* the repair and falls back to the regular retry. Intended for cost warnings, large-prompt
|
|
45
54
|
* confirmations, policy checks, and similar caller-side UX.
|
|
46
55
|
*
|
|
47
56
|
* `tokenEstimate` is a rough `charCount / 2.8` approximation — reasonable for
|
|
@@ -55,6 +64,8 @@ interface BeforePromptInfo {
|
|
|
55
64
|
charCount: number;
|
|
56
65
|
messageCount: number;
|
|
57
66
|
tokenEstimate: number;
|
|
67
|
+
/** See {@link BeforeModelCallInfo.isRepair}. */
|
|
68
|
+
isRepair?: boolean;
|
|
58
69
|
}
|
|
59
70
|
type OnBeforePrompt = (info: BeforePromptInfo) => Promise<boolean>;
|
|
60
71
|
/** Character-to-token approximation for English/code prompts. */
|
|
@@ -84,6 +95,21 @@ interface ValidationResult<T> {
|
|
|
84
95
|
retryFeedback?: string;
|
|
85
96
|
/** Set (non-empty) only when the validator repaired the response to make it valid. */
|
|
86
97
|
repairs?: ValidationRepair[];
|
|
98
|
+
/**
|
|
99
|
+
* Offered by a validator on a rejected response whose only defect one small model call can
|
|
100
|
+
* settle. The loop attempts it once, before falling back to a full retry.
|
|
101
|
+
*/
|
|
102
|
+
repair?: TargetedRepair<T>;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* A rejected response the validator can still salvage by asking the model one narrow question.
|
|
106
|
+
* The prompt must be self-contained — the loop sends it on its own, without the system prompt
|
|
107
|
+
* or the message history, which is the entire point of the escalation.
|
|
108
|
+
*/
|
|
109
|
+
interface TargetedRepair<T> {
|
|
110
|
+
prompt: string;
|
|
111
|
+
/** Validates the repair response and folds it into the response being repaired. Must be pure. */
|
|
112
|
+
apply: (responseText: string) => ValidationResult<T>;
|
|
87
113
|
}
|
|
88
114
|
interface AiLoopResult<T> {
|
|
89
115
|
result: T;
|
|
@@ -99,4 +125,4 @@ interface AiLoopResult<T> {
|
|
|
99
125
|
declare function runAiLoop<T>(params: AiLoopParams<T>): Promise<AiLoopResult<T>>;
|
|
100
126
|
declare function extractOutputTag(text: string): string | null;
|
|
101
127
|
//#endregion
|
|
102
|
-
export { AfterModelCallInfo, BeforeModelCallInfo, BeforePromptInfo, OnAfterModelCall, OnBeforeModelCall, OnBeforePrompt, ValidationResult, estimatedCharactersPerToken, extractOutputTag, runAiLoop };
|
|
128
|
+
export { AfterModelCallInfo, BeforeModelCallInfo, BeforePromptInfo, OnAfterModelCall, OnBeforeModelCall, OnBeforePrompt, TargetedRepair, ValidationResult, estimatedCharactersPerToken, extractOutputTag, runAiLoop };
|
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:
|
|
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:c,userPrompt:d,maxRetries:f=3,maxTokens:p=16384,onProgress:m,validate:h,taskName:g,cancellation:_,phase:v=g,onBeforeModelCall:y,onAfterModelCall:b,promptVersion:x}=a;if(!a.session&&!a.systemPrompt)throw new t(`${g}: either 'session' or 'systemPrompt' must be provided`);let S=Date.now(),C;a.session?C=r(a.session,d):(C=i(a.systemPrompt),C=r(C,d));let w,T=[],E=f+1,D=[],O=!1,k=async e=>a.onBeforePrompt?a.onBeforePrompt({phase:v,attempt:e.attempt,maxAttempts:E,charCount:e.charCount,messageCount:e.messageCount,tokenEstimate:Math.ceil(e.charCount/s),isRepair:e.isRepair}):!0,A=async(e,t)=>{if(!await k({attempt:t,charCount:e.prompt.length,messageCount:1,isRepair:!0}))return;let n=o();D.push(n);let r=Date.now();y?.({phase:v,attempt:t,maxAttempts:E,callId:n,isRepair:!0});let i;try{i=await c.generate({messages:[{role:`user`,content:e.prompt}],maxTokens:p,temperature:0,signal:_})}catch(e){let i=e instanceof Error?e.message:String(e);try{b?.({phase:v,attempt:t,callId:n,success:!1,durationMs:Date.now()-r,error:{code:e?.code??`UNKNOWN`,message:i},promptVersion:x,isRepair:!0})}catch{}if(l(e)||_?.aborted)throw new DOMException(i,`AbortError`);return}let a=Date.now()-r;w=u(w,i.usage);let s=e=>{try{b?.({phase:v,attempt:t,callId:n,success:!0,durationMs:a,usage:i.usage,promptVersion:x,repairs:e,isRepair:!0})}catch{}},d;try{d=e.apply(i.text)}catch(e){throw s(),e}return s(d.valid?d.repairs:void 0),d.valid&&d.result!==void 0?{result:d.result,repairs:d.repairs,responseText:i.text}:{errors:d.errors??[`Unknown repair validation error`]}};for(let e=0;e<=f;e++){_?.throwIfAborted();let t=e+1;if(m?.({type:`attempt_start`,attempt:t,maxAttempts:E}),a.onBeforePrompt&&!await k({attempt:t,charCount:(C.systemPrompt?.length??0)+C.messages.reduce((e,t)=>e+t.content.length,0),messageCount:C.messages.length}))throw new DOMException(`Prompt gate declined by caller (onBeforePrompt returned false)`,`AbortError`);let i=o();D.push(i);let s=Date.now();y?.({phase:v,attempt:t,maxAttempts:E,callId:i});let d;try{d=await c.generate({system:C.systemPrompt,messages:[...C.messages],maxTokens:p,temperature:0,signal:_})}catch(e){let n=e instanceof Error?e.message:String(e);try{b?.({phase:v,attempt:t,callId:i,success:!1,durationMs:Date.now()-s,error:{code:e?.code??`UNKNOWN`,message:n},promptVersion:x})}catch{}throw l(e)||_?.aborted?new DOMException(n,`AbortError`):e}let g=Date.now()-s,j=e=>{try{b?.({phase:v,attempt:t,callId:i,success:!0,durationMs:g,usage:d.usage,promptVersion:x,repairs:e})}catch{}};w=u(w,d.usage);let M;try{M=h(d.text)}catch(e){throw j(),e}if(j(M.valid?M.repairs:void 0),M.valid&&M.result!==void 0)return C=n(C,d.text),{result:M.result,session:C,usage:w,attempts:t,durationMs:Date.now()-S,callIds:D,repairs:M.repairs};if(T=M.errors??[`Unknown validation error`],M.repair&&!O){O=!0;let e=await A(M.repair,t);if(e&&`result`in e)return C=n(C,d.text),C=r(C,M.repair.prompt),C=n(C,e.responseText),{result:e.result,session:C,usage:w,attempts:t,durationMs:Date.now()-S,callIds:D,repairs:e.repairs};e&&(T=[...T,`Targeted repair rejected: ${e.errors.join(`; `)}`])}m?.({type:`validation_failed`,attempt:t,maxAttempts:E,errors:T}),e<f&&M.retryFeedback&&(C=n(C,d.text),C=r(C,M.retryFeedback))}throw new e(`VALIDATION_EXHAUSTED`,`Failed to get valid ${g} after ${E} attempts. Last errors:\n${T.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{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,authoring:o.authoring,analysis:o.analysis,hunkSummaries:o.hunkSummaries,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};
|
|
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,authoring:o.authoring,analysis:o.analysis,hunkSummaries:o.hunkSummaries,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,{repairContext:{hunks:s,condensed:o.condensed,redactHunkContent:o.redactHunkContent,inputTokenBudget:o.inputTokenBudget}}),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};
|
|
@@ -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,authoring:s.authoring,analysis:s.analysis,hunkSummaries:s.hunkSummaries,condensed:s.condensed,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};
|
|
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,authoring:s.authoring,analysis:s.analysis,hunkSummaries:s.hunkSummaries,condensed:s.condensed,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,c?.length?{repairContext:{hunks:c,condensed:s.condensed,redactHunkContent:s.redactHunkContent,inputTokenBudget:s.inputTokenBudget}}:void 0),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};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { ValidationRepair } from "../../ai/types.js";
|
|
2
|
+
import { BranchGroup, ComposeHunk } from "../types.js";
|
|
3
|
+
import { TargetedRepair } from "../ai-loop.js";
|
|
4
|
+
import { RedactHunkContent } from "../utils/redact.js";
|
|
5
|
+
import { GroupingResult } from "./types.js";
|
|
6
|
+
|
|
7
|
+
//#region src/tasks/group/repair.d.ts
|
|
8
|
+
/**
|
|
9
|
+
* What the missing-hunk repair needs from the task to build its prompt. Omitted in conversation
|
|
10
|
+
* mode, where the hunks live in the model's context rather than in the task input — without hunk
|
|
11
|
+
* bodies there is nothing to ask about, so a short response falls straight through to a retry.
|
|
12
|
+
*/
|
|
13
|
+
interface MissingHunksRepairContext {
|
|
14
|
+
hunks: readonly ComposeHunk[];
|
|
15
|
+
/** The caller asked for metadata only. Honoured here too: the repair never sends a hunk body. */
|
|
16
|
+
condensed?: boolean;
|
|
17
|
+
/** See {@link RedactHunkContent}. Honoured here too: a repair prompt must not leak excluded content. */
|
|
18
|
+
redactHunkContent?: RedactHunkContent;
|
|
19
|
+
/** Ceiling above which the repair prompt drops hunk bodies and asks on metadata alone. */
|
|
20
|
+
inputTokenBudget?: number;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Plans the repair for a grouping that left hunks unassigned: ask the model where the orphans
|
|
24
|
+
* belong instead of re-sending the whole diff. The question is deliberately narrow — an
|
|
25
|
+
* assignment map for the missing indices, never a fresh grouping — so a response cannot
|
|
26
|
+
* restructure decisions the first response already made.
|
|
27
|
+
*
|
|
28
|
+
* Returns undefined when the repair cannot be posed (no body for a missing index), or when so many
|
|
29
|
+
* hunks are missing that the question is most of the diff again.
|
|
30
|
+
*/
|
|
31
|
+
declare function planMissingHunksRepair(params: {
|
|
32
|
+
branches: BranchGroup[];
|
|
33
|
+
mode: 'commits' | 'branches';
|
|
34
|
+
missingIndices: number[];
|
|
35
|
+
priorRepairs?: ValidationRepair[];
|
|
36
|
+
context: MissingHunksRepairContext;
|
|
37
|
+
}): TargetedRepair<GroupingResult> | undefined;
|
|
38
|
+
//#endregion
|
|
39
|
+
export { MissingHunksRepairContext, planMissingHunksRepair };
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import{extractOutputTag as e}from"../ai-loop.js";import{DEFAULT_INPUT_TOKEN_BUDGET as t,estimateTokens as n,serializeHunk as r}from"../utils/budget.js";import{validateJsonSchema as i}from"../utils/json-schema.js";const a={$schema:`https://json-schema.org/draft/2020-12/schema`,type:`object`,properties:{assignments:{minItems:1,type:`array`,items:{type:`object`,properties:{hunkIndex:{type:`integer`,description:`Index of an unassigned hunk`},commitId:{type:`string`,minLength:1,description:`ID of the commit this hunk belongs to`}},required:[`hunkIndex`,`commitId`],additionalProperties:!1},description:`One entry per unassigned hunk`}},required:[`assignments`],additionalProperties:!1};function o(e){let{branches:t,mode:n,missingIndices:r,priorRepairs:i,context:a}=e;if(r.length>Math.max(2,a.hunks.length*.25))return;let o=new Map(a.hunks.map(e=>[e.index,e])),l=r.map(e=>o.get(e));if(l.some(e=>e===void 0))return;let u=t.flatMap(e=>e.commits.map(t=>n===`branches`?{id:t.id,message:t.message,branch:e.name,branchTitle:e.title}:{id:t.id,message:t.message}));if(u.length!==0)return{prompt:s({hunks:l.map(e=>e),commits:u,condensed:a.condensed,redactHunkContent:a.redactHunkContent,inputTokenBudget:a.inputTokenBudget}),apply:e=>c(e,{branches:t,missingIndices:r,priorRepairs:i})}}function s(e){let{hunks:i,commits:a,condensed:o,redactHunkContent:s,inputTokenBudget:c=t}=e,l=e=>{let t=JSON.stringify(i.map(t=>r(t,e?s?.(t)?`redacted`:`full`:`condensed`))),n=i.length===1?`hunk`:`hunks`,o=a.some(e=>e.branch!==void 0),c=JSON.stringify({assignments:[{hunkIndex:i[0].index,commitId:a[0].id}]},null,2);return`A grouping of code hunks into commits left ${i.length} ${n} unassigned. Decide which existing commit each one belongs to. Do not regroup anything else: the commits below are fixed, and every other hunk is already assigned.${o?` Each commit sits on the branch named next to it; choose the branch whose work the hunk belongs to, then the commit on it.`:``}
|
|
2
|
+
|
|
3
|
+
<unassigned_hunks>
|
|
4
|
+
${t}
|
|
5
|
+
</unassigned_hunks>
|
|
6
|
+
|
|
7
|
+
<commits>
|
|
8
|
+
${JSON.stringify(a)}
|
|
9
|
+
</commits>
|
|
10
|
+
|
|
11
|
+
Return JSON inside <output> tags with exactly one entry per unassigned hunk — the ${i.length} indices above, no others — each pointing at one of the commit IDs above:
|
|
12
|
+
<output>
|
|
13
|
+
${c}
|
|
14
|
+
</output>
|
|
15
|
+
|
|
16
|
+
Return only the <output> tag and no other text.`};if(o)return l(!1);let u=l(!0);return n(u.length)>c?l(!1):u}function c(t,n){let{branches:r,missingIndices:o,priorRepairs:s}=n,c=e(t);if(!c)return{valid:!1,errors:[`Repair response has no <output> tag`]};let l;try{l=JSON.parse(c)}catch(e){return{valid:!1,errors:[`Repair response is not valid JSON: ${e instanceof Error?e.message:String(e)}`]}}let u=i(a,l);if(!u.success)return{valid:!1,errors:u.issues.map(e=>`Repair response ${e.path.join(`.`)}: ${e.message}`)};let d=new Set(r.flatMap(e=>e.commits.map(e=>e.id))),f=new Set(o),p=new Map,m=[];for(let{hunkIndex:e,commitId:t}of u.data.assignments){if(!f.has(e)){m.push(`Repair assigned hunk ${e}, which was not one of the unassigned hunks`);continue}if(p.has(e)){m.push(`Repair assigned hunk ${e} more than once`);continue}if(!d.has(t)){m.push(`Repair assigned hunk ${e} to unknown commit '${t}'`);continue}p.set(e,t)}let h=o.filter(e=>!p.has(e));if(h.length>0&&m.push(`Repair left hunks unassigned: ${h.join(`, `)}`),m.length>0)return{valid:!1,errors:m};let g={branches:r.map(e=>({...e,commits:e.commits.map(e=>{let t=[...p].filter(([,t])=>t===e.id).map(([e])=>e);return t.length===0?e:{...e,hunkIndices:[...e.hunkIndices,...t].sort((e,t)=>e-t)}})}))},_=[...p].sort(([e],[t])=>e-t).map(([e,t])=>`hunk ${e} → ${t}`).join(`; `);return{valid:!0,result:g,repairs:[...s??[],{kind:`missing-hunks`,hunkIndices:[...o].sort((e,t)=>e-t),detail:`Placed ${o.length} unassigned hunk(s) via a targeted repair call: ${_}`}]}}export{o as planMissingHunksRepair};
|
|
@@ -86,7 +86,7 @@ interface GroupOutput {
|
|
|
86
86
|
/** Hunks condensed to metadata to fit the input-token budget. Undefined when the AI call was skipped. */
|
|
87
87
|
hunksCondensedCount?: number;
|
|
88
88
|
/**
|
|
89
|
-
*
|
|
89
|
+
* Repairs applied to the accepted AI response instead of spending a retry on it.
|
|
90
90
|
* Undefined when the response needed no repair.
|
|
91
91
|
*/
|
|
92
92
|
repairs?: ValidationRepair[];
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { ValidationResult } from "../ai-loop.js";
|
|
2
2
|
import { GroupingResult } from "./types.js";
|
|
3
|
+
import { MissingHunksRepairContext } from "./repair.js";
|
|
3
4
|
|
|
4
5
|
//#region src/tasks/group/validator.d.ts
|
|
5
6
|
interface GroupValidationOptions {
|
|
@@ -9,6 +10,11 @@ interface GroupValidationOptions {
|
|
|
9
10
|
* be told the hunk is missing or duplicated when it did neither.
|
|
10
11
|
*/
|
|
11
12
|
lockedCommitIds?: ReadonlySet<string>;
|
|
13
|
+
/**
|
|
14
|
+
* Enables the targeted repair for a response that left hunks unassigned. Omit it and such a
|
|
15
|
+
* response is rejected outright, as it was before the repair existed.
|
|
16
|
+
*/
|
|
17
|
+
repairContext?: MissingHunksRepairContext;
|
|
12
18
|
}
|
|
13
19
|
declare function validateGroupResponse(responseText: string, expectedHunkIndices: ReadonlySet<number>, mode?: 'commits' | 'branches', existingBranchNames?: Set<string>, options?: GroupValidationOptions): ValidationResult<GroupingResult>;
|
|
14
20
|
//#endregion
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import{extractOutputTag as e}from"../ai-loop.js";import{validateJsonSchema as t}from"../utils/json-schema.js";import{
|
|
1
|
+
import{extractOutputTag as e}from"../ai-loop.js";import{validateJsonSchema as t}from"../utils/json-schema.js";import{planMissingHunksRepair as n}from"./repair.js";import{groupingResultJsonSchema as r}from"./schema.js";function i(i,s,c=`branches`,l,u){let d=null;try{if(d=e(i),!d)return a([`No <output> tag found in response`],o(c));let f=t(r,JSON.parse(d));if(!f.success)return a(f.issues.map(e=>`${e.path.join(`.`)}: ${e.message}`),o(c));let p=f.data,m=[],h=new Set,g=[],_=new Map,v=new Map,y=new Set,b=[],x=[],S=[],C=new Set,w=new Set;for(let e of p.branches){if(C.has(e.id))return a([`Duplicate branch ID: '${e.id}'`],o(c));if(C.add(e.id),c===`branches`&&e.name.length===0)return a([`Branch '${e.id}': missing or empty 'name'`],o(c));if(c===`branches`&&e.name.length>0){if(w.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(w.add(e.name),l?.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}'.`)}for(let t of e.commits){y.has(t.id)&&b.push(t.id),y.add(t.id);let n=c===`branches`?`${e.id}/${t.id}`:t.id;for(let r of t.hunks){let i=_.get(r);i?i.push(n):_.set(r,[n]);let a=v.get(r);a?a.push({branchId:e.id,commitId:t.id,label:n}):v.set(r,[{branchId:e.id,commitId:t.id,label:n}])}}}let T=new Map,E=[],D=!1;for(let[e,t]of v){if(t.length<2)continue;if(g.push(e),c===`branches`&&new Set(t.map(e=>e.branchId)).size>1){D=!0;continue}let n=t.find(e=>u?.lockedCommitIds?.has(e.commitId))??t[0];T.set(e,n.label),E.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 O=g.length>0&&!D;for(let e of p.branches){let t=[];for(let n of e.commits){let r=c===`branches`?`${e.id}/${n.id}`:n.id,i=[],a=!1;for(let e of n.hunks){if(T.has(e)&&T.get(e)!==r){a=!0;continue}h.has(e)||(h.add(e),i.push(e))}n.hunks.length===0?x.push(n.id):i.length===0&&a&&S.push(n.id),t.push({id:n.id,message:n.message,explanation:n.explanation,hunkIndices:i})}m.push({id:e.id,name:e.name,title:e.title,description:e.description,commits:t})}if(b.length>0)return a([`Duplicate commit IDs: ${b.join(`, `)}`],`Some commit IDs appear multiple times. Each must be unique.\n\nDuplicates: ${b.join(`, `)}`);if(x.length>0)return a([`Empty commits (no hunks assigned): ${x.join(`, `)}`],`Every commit must contain at least one hunk. These commits have an empty \`hunks\` array: ${x.join(`, `)}.
|
|
2
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${n}`),t.push(`Duplicate hunks: ${
|
|
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 k=[...s].filter(e=>!h.has(e)).sort((e,t)=>e-t),A=[...h].filter(e=>!s.has(e)).sort((e,t)=>e-t),j=[...new Set(g)].sort((e,t)=>e-t),M=[...E].sort((e,t)=>e.hunkIndex-t.hunkIndex),N=M.length>0?{kind:`duplicate-hunks`,hunkIndices:M.map(e=>e.hunkIndex),claims:M,detail:`Dropped duplicate claims on ${M.length} hunk(s): ${M.map(e=>`hunk ${e.hunkIndex} kept in ${e.keptIn}`).join(`; `)}`}:void 0,P=j.length>0&&!O;if(k.length>0||A.length>0||S.length>0||P){let e=[],t=[];if(j.length>0){let n=j.map(e=>` hunk ${e} → ${(_.get(e)??[]).join(`, `)}`).join(`
|
|
4
|
+
`);e.push(`Duplicate hunks (each must appear in exactly ONE commit):\n${n}`),t.push(`Duplicate hunks: ${j.join(`, `)}`)}k.length>0&&(e.push(`Missing hunks (not assigned to any commit): ${k.join(`, `)}`),t.push(`Missing hunks: ${k.join(`, `)}`)),A.length>0&&(e.push(`Extra hunks (not present in the input): ${A.join(`, `)}`),t.push(`Extra hunks: ${A.join(`, `)}`)),S.length>0&&(e.push(`Commits whose only hunks were already claimed by an earlier commit: ${S.join(`, `)}`),t.push(`Empty commits after dropping duplicate hunks: ${S.join(`, `)}`));let r=[...s].sort((e,t)=>e-t),i=[..._.values()].reduce((e,t)=>e+t.length,0),o=`There are ${r.length} hunks, with indices: ${r.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 ${i} hunk references across ${_.size} unique indices. Expected: ${r.length} references across ${r.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
|
|
14
|
+
Return the corrected JSON inside <output> tags.`;if(u?.repairContext&&k.length>0&&A.length===0&&S.length===0&&!P){let e=n({branches:m,mode:c,missingIndices:k,priorRepairs:N?[N]:void 0,context:u.repairContext});if(e)return{...a(t,o),repair:e}}return a(t,o)}return N?{valid:!0,result:{branches:m},repairs:[N]}:{valid:!0,result:{branches:m}}}catch(e){let t=e instanceof Error?e.message:String(e),n=(d??i).slice(-300);return a([`Failed to parse response as valid JSON: ${t}`,`Response tail: ${n}`],o(c))}}function a(e,t){return{valid:!1,errors:e,retryFeedback:t}}function o(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": [
|
|
@@ -55,4 +55,4 @@ Return only the <output> tag and no other text.`:`Your response has an invalid s
|
|
|
55
55
|
}
|
|
56
56
|
</output>
|
|
57
57
|
|
|
58
|
-
Return only the <output> tag and no other text.`}export{
|
|
58
|
+
Return only the <output> tag and no other text.`}export{i as validateGroupResponse};
|
|
@@ -13,6 +13,14 @@ import { RedactHunkContent } from "./redact.js";
|
|
|
13
13
|
declare const DEFAULT_INPUT_TOKEN_BUDGET = 180000;
|
|
14
14
|
/** Rough char→token estimate shared with `runAiLoop`'s prompt-gate heuristic. */
|
|
15
15
|
declare function estimateTokens(chars: number): number;
|
|
16
|
+
/**
|
|
17
|
+
* How a hunk is serialized into a prompt:
|
|
18
|
+
* - `full`: verbatim patch body (default).
|
|
19
|
+
* - `redacted`: body replaced with a placeholder, metadata + line counts kept.
|
|
20
|
+
* - `condensed`: metadata + line counts only, no `content` field at all.
|
|
21
|
+
*/
|
|
22
|
+
type HunkRenderMode = 'full' | 'redacted' | 'condensed';
|
|
23
|
+
declare function serializeHunk(hunk: ComposeHunk, mode: HunkRenderMode): Record<string, unknown>;
|
|
16
24
|
interface AssembledHunkPrompt {
|
|
17
25
|
prompt: string;
|
|
18
26
|
/**
|
|
@@ -37,4 +45,4 @@ declare function assembleHunkPrompt(params: {
|
|
|
37
45
|
systemPromptChars?: number;
|
|
38
46
|
}): AssembledHunkPrompt;
|
|
39
47
|
//#endregion
|
|
40
|
-
export { AssembledHunkPrompt, DEFAULT_INPUT_TOKEN_BUDGET, assembleHunkPrompt, estimateTokens };
|
|
48
|
+
export { AssembledHunkPrompt, DEFAULT_INPUT_TOKEN_BUDGET, HunkRenderMode, assembleHunkPrompt, estimateTokens, serializeHunk };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{DiffTooLargeError as e}from"../../errors/task.js";import{estimatedCharactersPerToken as t}from"../ai-loop.js";import{REDACTED_HUNK_CONTENT as n}from"./redact.js";const r=18e4;function i(e){return Math.ceil(e/t)}function a(e,t){let r={...e.isBinary?{isBinary:!0}:{},...e.isRename?{isRename:!0}:{},...e.originalFileName?{originalFileName:e.originalFileName}:{}};return t===`condensed`?{index:e.index,fileName:e.fileName,hunkHeader:e.hunkHeader,additions:e.additions,deletions:e.deletions,...r}:t===`redacted`?{index:e.index,fileName:e.fileName,hunkHeader:e.hunkHeader,additions:e.additions,deletions:e.deletions,content:n,...r}:{index:e.index,fileName:e.fileName,hunkHeader:e.hunkHeader,content:e.content,...r}}function o(e,t){return JSON.stringify(a(e,t)).length}function s(e,t,n,r){let a=new Map(t),s=2+Math.max(0,e.length-1),c=0,l=e.map(e=>{let t=a.get(e.index),n=o(e,`condensed`),r=t===`condensed`?n:o(e,t);return c+=r,{hunk:e,currentLen:r,condensedLen:n}}),u=()=>i(r+s+c),d=()=>u()>n;if(d()){let e=l.map(e=>({hunk:e.hunk,savings:e.currentLen-e.condensedLen})).filter(e=>e.savings>0).sort((e,t)=>t.savings-e.savings);for(let{hunk:t,savings:n}of e){if(!d())break;c-=n,a.set(t.index,`condensed`)}}return{modes:a,estimatedTokens:u(),tooLarge:d()}}function c(t){let{hunks:n,assemble:i,condensed:o,redactHunkContent:c,inputTokenBudget:l=r,systemPromptChars:u=0}=t,d=new Map(n.map(e=>o?[e.index,`condensed`]:c?.(e)?[e.index,`redacted`]:[e.index,`full`])),{modes:f,estimatedTokens:p,tooLarge:m}=s(n,d,l,i(``).length+u);if(m)throw new e(p,l);let h=0;for(let e of n)d.get(e.index)===`full`&&f.get(e.index)===`condensed`&&h++;return{prompt:i(JSON.stringify(n.map(e=>a(e,f.get(e.index))))),hunksCondensedForBudget:h}}export{r as DEFAULT_INPUT_TOKEN_BUDGET,c as assembleHunkPrompt,i as estimateTokens};
|
|
1
|
+
import{DiffTooLargeError as e}from"../../errors/task.js";import{estimatedCharactersPerToken as t}from"../ai-loop.js";import{REDACTED_HUNK_CONTENT as n}from"./redact.js";const r=18e4;function i(e){return Math.ceil(e/t)}function a(e,t){let r={...e.isBinary?{isBinary:!0}:{},...e.isRename?{isRename:!0}:{},...e.originalFileName?{originalFileName:e.originalFileName}:{}};return t===`condensed`?{index:e.index,fileName:e.fileName,hunkHeader:e.hunkHeader,additions:e.additions,deletions:e.deletions,...r}:t===`redacted`?{index:e.index,fileName:e.fileName,hunkHeader:e.hunkHeader,additions:e.additions,deletions:e.deletions,content:n,...r}:{index:e.index,fileName:e.fileName,hunkHeader:e.hunkHeader,content:e.content,...r}}function o(e,t){return JSON.stringify(a(e,t)).length}function s(e,t,n,r){let a=new Map(t),s=2+Math.max(0,e.length-1),c=0,l=e.map(e=>{let t=a.get(e.index),n=o(e,`condensed`),r=t===`condensed`?n:o(e,t);return c+=r,{hunk:e,currentLen:r,condensedLen:n}}),u=()=>i(r+s+c),d=()=>u()>n;if(d()){let e=l.map(e=>({hunk:e.hunk,savings:e.currentLen-e.condensedLen})).filter(e=>e.savings>0).sort((e,t)=>t.savings-e.savings);for(let{hunk:t,savings:n}of e){if(!d())break;c-=n,a.set(t.index,`condensed`)}}return{modes:a,estimatedTokens:u(),tooLarge:d()}}function c(t){let{hunks:n,assemble:i,condensed:o,redactHunkContent:c,inputTokenBudget:l=r,systemPromptChars:u=0}=t,d=new Map(n.map(e=>o?[e.index,`condensed`]:c?.(e)?[e.index,`redacted`]:[e.index,`full`])),{modes:f,estimatedTokens:p,tooLarge:m}=s(n,d,l,i(``).length+u);if(m)throw new e(p,l);let h=0;for(let e of n)d.get(e.index)===`full`&&f.get(e.index)===`condensed`&&h++;return{prompt:i(JSON.stringify(n.map(e=>a(e,f.get(e.index))))),hunksCondensedForBudget:h}}export{r as DEFAULT_INPUT_TOKEN_BUDGET,c as assembleHunkPrompt,i as estimateTokens,a as serializeHunk};
|
|
@@ -251,7 +251,8 @@ interface ComposeInput {
|
|
|
251
251
|
onAfterModelCall?: OnAfterModelCall;
|
|
252
252
|
/**
|
|
253
253
|
* Gating hook called immediately before each model.generate call. Returning
|
|
254
|
-
* `false` aborts the workflow with `ComposeWorkflowError('CANCELLED')
|
|
254
|
+
* `false` aborts the workflow with `ComposeWorkflowError('CANCELLED')`, except on
|
|
255
|
+
* a missing-hunks repair call, where it falls back to a regular retry. Use for
|
|
255
256
|
* large-prompt confirmations, cost warnings, org policy checks, etc.
|
|
256
257
|
*/
|
|
257
258
|
onBeforePrompt?: OnBeforePrompt;
|
|
@@ -483,7 +484,8 @@ interface ComposeResult {
|
|
|
483
484
|
* Call ID of the "decision" model response for this workflow — the last
|
|
484
485
|
* validated response across all AI phases. Consumers that surface user
|
|
485
486
|
* feedback (thumbs up/down) stamp this ID on the feedback event so it maps
|
|
486
|
-
* back to a specific prompt/response.
|
|
487
|
+
* back to a specific prompt/response. When a missing-hunks repair completed
|
|
488
|
+
* that response, this is the response, not the repair call. Undefined when no AI phases ran
|
|
487
489
|
* (e.g. {@link applyComposePlan} without prior {@link composePlan}).
|
|
488
490
|
*/
|
|
489
491
|
feedbackTargetCallId?: string;
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import{GitError as e}from"../errors/git.js";import{ComposeWorkflowError as t,ComposeWorkflowInputError as n}from"../errors/workflow.js";import{applyPatchToIndex as r,currentBranch as i,diffTree as a,forEachRef as o,log as s,mergeBase as c,readTree as l,revParse as u,writeTree as d}from"../git/port-dispatch.js";import{validateRewriteRangeSafety as f}from"../git/safety-range.js";import{addUsage as p,emptyUsage as m}from"../ai/types.js";import{memoizeRedactHunkContent as h}from"../tasks/utils/redact.js";import{analyze as g}from"../tasks/analyze/analyze.js";import{composeGroup as _}from"../tasks/compose-group/compose-group.js";import{group as v}from"../tasks/group/group.js";import{order as y}from"../tasks/order/order.js";import{partition as b}from"../tasks/partition/partition.js";import{withDefaultNoiseRedaction as x}from"../tasks/utils/noise.js";import{sha256 as S}from"../git/utils/crypto.js";import{collectFromSource as C,resolveWorkdirLayers as w}from"../git/diff-collector.js";import{buildPatchForCommit as T}from"../git/utils/patch-builder.js";import{verifySnapshot as E}from"../git/safety.js";import{applyToTarget as D}from"../git/apply-target.js";import{requireExec as O}from"../git/require-exec.js";import{extractConventions as k}from"../git/utils/conventions.js";import{rethrowAsWorkflowError as A}from"../utils/cancellation.js";import{randomUUID as j}from"node:crypto";import{unlinkSync as M}from"node:fs";import{tmpdir as N}from"node:os";import{join as P}from"node:path";import{UndoStore as F,capturePreOperationState as I,generateUndoId as L}from"@gitkraken/shared-tools";const R={collecting:{message:`Collecting changes…`,weight:5},composing:{message:`Composing commits…`,weight:40},"compose-grouping":{message:`Grouping changes into commits…`,weight:40},analyzing:{message:`Analyzing changes…`,weight:20},grouping:{message:`Grouping changes into commits…`,weight:25},ordering:{message:`Ordering commits…`,weight:15},partitioning:{message:`Subdividing branches…`,weight:20},verifying:{message:`Verifying…`,weight:5},applying:{message:`Applying to repository…`,weight:10}};async function z(r){if(!r.applyPlan&&!r.model)throw new n(`Either 'model' or 'applyPlan' must be provided`);if(r.applyPlan)return te(r);if(r.analysis!=null&&r.session!=null)throw new n(`'analysis' cannot be combined with 'session'; provide one or the other`);let{git:i,model:a,source:o={type:`workdir`},onProgress:s,onBeforeApply:c,cancellation:l}=r,u=a,d=r.depth??`balanced`,p=r.target??await Y(i,o);Z(o,p),await ie(i,r.applyCommitIds,r.hunkFilter,o,p),await f({git:i,source:o,target:p,overrides:r.overrideSafetyFailures});let m=[];s?.({phase:`collecting`,...R.collecting});let g=Date.now(),_=await C(i,o);if(m.push({name:`collecting`,durationMs:Date.now()-g,attempts:1}),_.hunks.length===0)throw new e(`NO_CHANGES`,X(o));let{hunks:v,filteredCount:y}=await q(_.hunks,r.hunkFilter);if(v.length===0)throw new e(`NO_CHANGES`,X(o));let b=K(v);s?.({phase:`collecting`,...R.collecting,stats:b});let S=y!==void 0&&y>0?await J(i,_.snapshot,v):_.snapshot,w=r.conventions??await k({git:i,author:r.conventionAuthor}),T=await ce(i),j=p.type===`auto`?`branches`:`commits`,M=r.disableDefaultNoiseReduction?r.redactHunkContent:x(r.redactHunkContent),N=M?h(M):void 0,P=N?v.filter(e=>N(e)).length:void 0,F={hunks:v,collection:_,conventions:w,existingBranchNames:T,groupMode:j,target:p,model:u,instructions:r.instructions,context:r.context,analysis:r.analysis,condenseHunks:r.condenseHunks,hunkSummaries:r.hunkSummaries,strategy:r.strategy,maxRetries:r.maxRetries,maxTokens:r.maxTokens,onProgress:s,cancellation:l,onBeforeModelCall:r.onBeforeModelCall,onAfterModelCall:r.onAfterModelCall,onBeforePrompt:r.onBeforePrompt,session:r.session,promptOverrides:r.promptOverrides,redactHunkContent:N,inputTokenBudget:r.inputTokenBudget},L;try{L=await ee(d,F)}catch(e){A(e,l)}let z={analysis:L.analysis,grouping:L.grouping,ordering:L.ordering,branches:L.branchPlans,allOrderedCommits:L.allOrderedCommits};if(m.push(...L.phases),c&&!await c(z))return{plan:z,snapshot:S,session:L.session,usage:L.usage,phases:m,source:{type:o.type===`branch`?`branch`:`workdir`,hunks:v,commitMessages:_.commitMessages,branchName:_.branchName,baseBranch:_.baseBranch},diffStats:b,feedbackTargetCallId:ne(m),hunksFilteredCount:y,hunksRedactedCount:P,hunksCondensedCount:L.hunksCondensedCount};let B=await I(O(i,`capturing pre-operation state`));s?.({phase:`verifying`,...R.verifying});let V=Date.now(),H=await E(i,_.snapshot,re(o,_));if(!H.valid)throw new t(`SAFETY_CHECK_FAILED`,`Safety check failed: ${H.error}`);m.push({name:`verifying`,durationMs:Date.now()-V,attempts:1});let U=ae(L.allOrderedCommits,r.applyCommitIds),{leftoverHunks:W,leftoverPatch:G}=se(U,_.hunks),Q=W.length>0?await J(i,S,oe(U,v)):S;s?.({phase:`applying`,...R.applying});let ue=Date.now(),$=await D({git:i,source:o,target:p,branchPlans:L.branchPlans,allOrderedCommits:U,hunks:v,snapshot:Q,collection:_,signing:r.signing,authorAttribution:r.authorAttribution,stashLabel:r.stashLabel,leftoverPatch:G});m.push({name:`applying`,durationMs:Date.now()-ue,attempts:1});let de=await le(i,B,$.mutations,p,o);return{plan:z,snapshot:Q,partitions:$.partitions,partitionGroups:$.partitionGroups,session:L.session,usage:L.usage,phases:m,source:{type:o.type===`branch`?`branch`:`workdir`,hunks:v,commitMessages:_.commitMessages,branchName:_.branchName,baseBranch:_.baseBranch},diffStats:b,commitShas:$.commitShas,undoId:de,feedbackTargetCallId:ne(m),hunksFilteredCount:y,hunksRedactedCount:P,hunksCondensedCount:L.hunksCondensedCount,appliedCommitIds:U.map(e=>e.id),unappliedHunkIndices:W.length>0?W.map(e=>e.index):void 0,stashConflict:$.stashConflict,stashNotRestored:$.stashNotRestored}}function ee(e,t){switch(e){case`quick`:return B(t);case`balanced`:return V(t);case`deep`:return U(t)}}async function B(e){return H(e,{lightweight:!0,skipOrder:!0})}async function V(e){return H(e,{includeAnalysis:!0})}async function H(e,t){let{hunks:n,collection:r,conventions:i,existingBranchNames:a,groupMode:o,target:s,model:c,instructions:l,context:u,strategy:d,maxRetries:f,maxTokens:h,onProgress:g,cancellation:v,onBeforeModelCall:x,onAfterModelCall:S,onBeforePrompt:C}=e,w=m(),T=[],E=await _({hunks:n,commitMessages:r.commitMessages,commitOrder:r.commitOrder,branchName:r.branchName,baseBranch:r.baseBranch,issueUrl:u?.issueUrl,pullRequestUrl:u?.pullRequestUrl,authoring:u,analysis:e.analysis,hunkSummaries:e.hunkSummaries,instructions:l,conventions:i,mode:o,strategy:d?.grouping,existingBranchNames:o===`branches`?a:void 0,includeAnalysis:t.includeAnalysis&&e.analysis==null,lightweight:t.lightweight,condensed:t.condensed||e.condenseHunks,redactHunkContent:e.redactHunkContent,inputTokenBudget:e.inputTokenBudget,model:c,session:e.session,maxRetries:f,maxTokens:h,onProgress:e=>g?.({phase:`compose-grouping`,...R[`compose-grouping`],event:e}),cancellation:v,onBeforeModelCall:x,onAfterModelCall:S,onBeforePrompt:C,promptOverride:e.promptOverrides?.[`compose-group`]});E.usage&&p(w,E.usage),T.push({name:`compose-grouping`,durationMs:E.durationMs,attempts:E.attempts,usage:E.usage,callIds:E.callIds,repairs:E.repairs});let D=E.session,O,k;if(t.skipOrder)O=E.result.branches.map(e=>({branchGroup:e,orderedCommitIds:e.commits.map(e=>e.id)})),k={branches:O.map(e=>({branchId:e.branchGroup.id,orderedCommitIds:e.orderedCommitIds})),rationale:``};else{let t=await y({branches:E.result.branches,hunks:n,instructions:l,model:c,session:E.session,maxRetries:f,maxTokens:h,onProgress:e=>g?.({phase:`ordering`,...R.ordering,event:e}),cancellation:v,onBeforeModelCall:x,onAfterModelCall:S,onBeforePrompt:C,promptOverride:e.promptOverrides?.order});t.usage&&p(w,t.usage),T.push({name:`ordering`,durationMs:t.durationMs,attempts:t.attempts,usage:t.usage,callIds:t.callIds}),D=t.session,k=t.result,O=E.result.branches.map(e=>({branchGroup:e,orderedCommitIds:k.branches.find(t=>t.branchId===e.id)?.orderedCommitIds??e.commits.map(e=>e.id)}))}if(s.type===`auto`){let t=new Set(a);for(let e of E.result.branches)e.name&&t.add(e.name);for(let n of O){let r=await b({commits:Q(n.branchGroup,n.orderedCommitIds),orderedCommitIds:n.orderedCommitIds,sourceBranchName:n.branchGroup.name,instructions:l,conventions:i,strategy:d?.partition,existingBranchNames:t,model:c,maxRetries:f,maxTokens:h,onProgress:e=>g?.({phase:`partitioning`,...R.partitioning,branch:n.branchGroup.name,event:e}),cancellation:v,onBeforeModelCall:x,onAfterModelCall:S,onBeforePrompt:C,promptOverride:e.promptOverrides?.partition});r.usage&&p(w,r.usage),T.push({name:`partitioning:${n.branchGroup.name}`,durationMs:r.durationMs,attempts:r.attempts,usage:r.usage,callIds:r.callIds}),n.partitioning=r.result,D=r.session;for(let e of r.result.partitions)t.add(e.branchName)}}let A=O.flatMap(e=>Q(e.branchGroup,e.orderedCommitIds));return{analysis:e.analysis,grouping:E.result,ordering:k,branchPlans:O,allOrderedCommits:A,session:D,usage:w,phases:T,hunksCondensedCount:E.hunksCondensedCount}}async function U(e){let{hunks:t,collection:n,conventions:r,existingBranchNames:i,groupMode:a,target:o,model:s,instructions:c,context:l,strategy:u,maxRetries:d,maxTokens:f,onProgress:h,cancellation:_,onBeforeModelCall:x,onAfterModelCall:S,onBeforePrompt:C}=e,w=m(),T=[],E=e.analysis,D,O,k;if(E!=null)D=E,O=void 0,T.push({name:`analyzing`,durationMs:0,attempts:0,callIds:[]});else{let r=await g({hunks:t,commitMessages:n.commitMessages,commitOrder:n.commitOrder,branchName:n.branchName,baseBranch:n.baseBranch,issueUrl:l?.issueUrl,pullRequestUrl:l?.pullRequestUrl,authoring:l,hunkSummaries:e.hunkSummaries,instructions:c,condensed:e.condenseHunks,redactHunkContent:e.redactHunkContent,inputTokenBudget:e.inputTokenBudget,model:s,session:e.session,maxRetries:d,maxTokens:f,onProgress:e=>h?.({phase:`analyzing`,...R.analyzing,event:e}),cancellation:_,onBeforeModelCall:x,onAfterModelCall:S,onBeforePrompt:C,promptOverride:e.promptOverrides?.analyze});r.usage&&p(w,r.usage),T.push({name:`analyzing`,durationMs:r.durationMs,attempts:r.attempts,usage:r.usage,callIds:r.callIds}),D=r.result,O=r.session,k=r.hunksCondensedCount}let A=await v({hunks:t,commitMessages:n.commitMessages,instructions:c,authoring:l,analysis:E,condensed:e.condenseHunks,hunkSummaries:e.hunkSummaries,conventions:r,mode:a,strategy:u?.grouping,existingBranchNames:a===`branches`?i:void 0,redactHunkContent:e.redactHunkContent,inputTokenBudget:e.inputTokenBudget,model:s,session:O,maxRetries:d,maxTokens:f,onProgress:e=>h?.({phase:`grouping`,...R.grouping,event:e}),cancellation:_,onBeforeModelCall:x,onAfterModelCall:S,onBeforePrompt:C,promptOverride:e.promptOverrides?.group});A.usage&&p(w,A.usage),T.push({name:`grouping`,durationMs:A.durationMs,attempts:A.attempts,usage:A.usage,callIds:A.callIds,repairs:A.repairs});let j=await y({branches:A.result.branches,hunks:t,instructions:c,model:s,session:A.session,maxRetries:d,maxTokens:f,onProgress:e=>h?.({phase:`ordering`,...R.ordering,event:e}),cancellation:_,onBeforeModelCall:x,onAfterModelCall:S,onBeforePrompt:C,promptOverride:e.promptOverrides?.order});j.usage&&p(w,j.usage),T.push({name:`ordering`,durationMs:j.durationMs,attempts:j.attempts,usage:j.usage,callIds:j.callIds});let M=A.result.branches.map(e=>({branchGroup:e,orderedCommitIds:j.result.branches.find(t=>t.branchId===e.id)?.orderedCommitIds??e.commits.map(e=>e.id)})),N=j.session;if(o.type===`auto`){let t=new Set(i);for(let e of A.result.branches)e.name&&t.add(e.name);for(let n of M){let i=await b({commits:Q(n.branchGroup,n.orderedCommitIds),orderedCommitIds:n.orderedCommitIds,sourceBranchName:n.branchGroup.name,instructions:c,conventions:r,strategy:u?.partition,existingBranchNames:t,model:s,maxRetries:d,maxTokens:f,onProgress:e=>h?.({phase:`partitioning`,...R.partitioning,branch:n.branchGroup.name,event:e}),cancellation:_,onBeforeModelCall:x,onAfterModelCall:S,onBeforePrompt:C,promptOverride:e.promptOverrides?.partition});i.usage&&p(w,i.usage),T.push({name:`partitioning:${n.branchGroup.name}`,durationMs:i.durationMs,attempts:i.attempts,usage:i.usage,callIds:i.callIds}),n.partitioning=i.result,N=i.session;for(let e of i.result.partitions)t.add(e.branchName)}}let P=M.flatMap(e=>Q(e.branchGroup,e.orderedCommitIds));return{analysis:D,grouping:A.result,ordering:j.result,branchPlans:M,allOrderedCommits:P,session:N,usage:w,phases:T,hunksCondensedCount:k??A.hunksCondensedCount}}async function W(e){let t=await z({...e,onBeforeApply:async()=>!1});return{plan:t.plan,snapshot:t.snapshot,source:t.source,session:t.session,usage:t.usage,phases:t.phases,feedbackTargetCallId:t.feedbackTargetCallId,hunksFilteredCount:t.hunksFilteredCount,hunksRedactedCount:t.hunksRedactedCount,hunksCondensedCount:t.hunksCondensedCount,diffStats:t.diffStats}}async function G(e){let t=await z({git:e.git,applyPlan:e.applyPlan,source:e.source,target:e.target,onProgress:e.onProgress,signing:e.signing,authorAttribution:e.authorAttribution,applyCommitIds:e.applyCommitIds,stashLabel:e.stashLabel,hunkFilter:e.hunkFilter,overrideSafetyFailures:e.overrideSafetyFailures});return{commitShas:t.commitShas,partitions:t.partitions,partitionGroups:t.partitionGroups,phases:t.phases,undoId:t.undoId,appliedCommitIds:t.appliedCommitIds,unappliedHunkIndices:t.unappliedHunkIndices,stashConflict:t.stashConflict,stashNotRestored:t.stashNotRestored}}async function te(n){let{git:r,applyPlan:i,onProgress:a}=n,{plan:o,snapshot:s,source:c}=i,l=n.source??c,u=n.target??i?.target??await Y(r,l);Z(l,u),await ie(r,n.applyCommitIds,n.hunkFilter,l,u),await f({git:r,source:l,target:u,overrides:n.overrideSafetyFailures});let d=await I(O(r,`capturing pre-operation state`)),p=[];a?.({phase:`collecting`,...R.collecting});let h=Date.now(),g=await C(r,l);if(p.push({name:`collecting`,durationMs:Date.now()-h,attempts:1}),g.hunks.length===0)throw new e(`NO_CHANGES`,X(l));let{hunks:_}=await q(g.hunks,n.hunkFilter);if(_.length===0)throw new e(`NO_CHANGES`,X(l));let v=K(_);a?.({phase:`collecting`,...R.collecting,stats:v});let y=_.length<g.hunks.length?await J(r,g.snapshot,_):g.snapshot,b={...g,hunks:_,snapshot:y};if(b.snapshot.diffHash!==s.diffHash)throw new t(`SAFETY_CHECK_FAILED`,`Working directory has changed since the plan was created. Re-run compose to generate a new plan.`);a?.({phase:`verifying`,...R.verifying});let x=Date.now(),S=await E(r,b.snapshot,re(l,b));if(!S.valid)throw new t(`SAFETY_CHECK_FAILED`,`Safety check failed: ${S.error}`);p.push({name:`verifying`,durationMs:Date.now()-x,attempts:1});let w=ae(o.allOrderedCommits,n.applyCommitIds),{leftoverHunks:T,leftoverPatch:k}=se(w,g.hunks),A=T.length>0?await J(r,b.snapshot,oe(w,b.hunks)):b.snapshot;a?.({phase:`applying`,...R.applying});let j=Date.now(),M=await D({git:r,source:l,target:u,branchPlans:o.branches,allOrderedCommits:w,hunks:b.hunks,snapshot:A,collection:b,signing:n.signing,authorAttribution:n.authorAttribution,stashLabel:n.stashLabel,leftoverPatch:k});p.push({name:`applying`,durationMs:Date.now()-j,attempts:1});let N=await le(r,d,M.mutations,u,l);return{plan:o,snapshot:A,partitions:M.partitions,partitionGroups:M.partitionGroups,session:{messages:[],systemPrompt:``,completedSteps:{}},usage:m(),phases:p,source:{type:l.type===`branch`?`branch`:`workdir`,hunks:b.hunks,commitMessages:b.commitMessages,branchName:b.branchName,baseBranch:b.baseBranch},diffStats:v,commitShas:M.commitShas,undoId:N,appliedCommitIds:w.map(e=>e.id),unappliedHunkIndices:T.length>0?T.map(e=>e.index):void 0,stashConflict:M.stashConflict,stashNotRestored:M.stashNotRestored}}function K(e){let t=new Set,n=0,r=0;for(let i of e)t.add(i.fileName),n+=i.additions,r+=i.deletions;return{fileCount:t.size,hunkCount:e.length,addedLines:n,removedLines:r}}function ne(e){for(let t=e.length-1;t>=0;t--){let n=e[t].callIds;if(n&&n.length>0)return n[n.length-1]}}async function q(e,t){if(!t)return{hunks:e,filteredCount:void 0};let n=await t(e);return{hunks:n,filteredCount:e.length-n.length}}async function J(e,t,n){let i=t.baseSha??t.headSha,o=P(N(),`compose-filtered-idx-${j()}`),s={GIT_INDEX_FILE:o};try{await l(e,i,{indexEnv:s});let o=T(n);o.length>0&&await r(e,o,{indexEnv:s});let c=await a(e,i,await d(e,{indexEnv:s}));return{...t,diffHash:S(c)}}finally{try{M(o)}catch{}}}function re(e,t){return e.type===`workdir`?{includeUnstaged:!e.stagedOnly,includeUntracked:t.untrackedIncluded}:e.type===`commit-range`&&e.includeWorkdir?w(e.includeWorkdir):{includeUnstaged:!1,includeUntracked:!1}}async function ie(e,t,r,a,o){if(r==null&&t==null||a.type===`workdir`||!(o.type===`rewrite-range`||(o.type===`branch`||o.type===`auto`)&&o.preserveOriginal!==!0))return;let s=a.type===`branch`?await u(e,a.name):await u(e,a.to),c=await u(e,`HEAD`);if(await i(e)==null)throw new n(`Filtering and partial apply require HEAD to be on a branch (non-detached) when rewriting existing commits. Check out the source branch before composing.`);if(c!==s)throw new n(`Filtering and partial apply require HEAD to be at the source's tip (${s.slice(0,8)}); HEAD is at ${c.slice(0,8)}.`)}function ae(e,t){if(t==null)return e;let r=new Set(t),i=e.filter(e=>r.has(e.id)),a=new Set(i.map(e=>e.id)),o=t.filter(e=>!a.has(e));if(o.length>0)throw new n(`applyCommitIds contains commit ids not found in plan.allOrderedCommits: ${o.join(`, `)}`);return i}function oe(e,t){let n=new Set(e.flatMap(e=>e.hunkIndices));return t.filter(e=>n.has(e.index))}function se(e,t){let n=new Set(e.flatMap(e=>e.hunkIndices)),r=t.filter(e=>!n.has(e.index));if(r.length===0)return{leftoverHunks:[],leftoverPatch:void 0};let i=T(r);return{leftoverHunks:r,leftoverPatch:i.length>0?i:void 0}}async function Y(e,t){if(t.type===`branch`){let r=await u(e,t.name),i=(await s(e,{format:`%H`,range:`${await c(e,await u(e,t.mergeTarget??`main`),r)}..${r}`})).trim().split(`
|
|
1
|
+
import{GitError as e}from"../errors/git.js";import{ComposeWorkflowError as t,ComposeWorkflowInputError as n}from"../errors/workflow.js";import{applyPatchToIndex as r,currentBranch as i,diffTree as a,forEachRef as o,log as s,mergeBase as c,readTree as l,revParse as u,writeTree as d}from"../git/port-dispatch.js";import{validateRewriteRangeSafety as f}from"../git/safety-range.js";import{addUsage as p,emptyUsage as m}from"../ai/types.js";import{memoizeRedactHunkContent as h}from"../tasks/utils/redact.js";import{analyze as g}from"../tasks/analyze/analyze.js";import{composeGroup as _}from"../tasks/compose-group/compose-group.js";import{group as v}from"../tasks/group/group.js";import{order as y}from"../tasks/order/order.js";import{partition as b}from"../tasks/partition/partition.js";import{withDefaultNoiseRedaction as x}from"../tasks/utils/noise.js";import{sha256 as S}from"../git/utils/crypto.js";import{collectFromSource as C,resolveWorkdirLayers as w}from"../git/diff-collector.js";import{buildPatchForCommit as T}from"../git/utils/patch-builder.js";import{verifySnapshot as E}from"../git/safety.js";import{applyToTarget as D}from"../git/apply-target.js";import{requireExec as O}from"../git/require-exec.js";import{extractConventions as k}from"../git/utils/conventions.js";import{rethrowAsWorkflowError as A}from"../utils/cancellation.js";import{randomUUID as j}from"node:crypto";import{unlinkSync as M}from"node:fs";import{tmpdir as N}from"node:os";import{join as P}from"node:path";import{UndoStore as F,capturePreOperationState as I,generateUndoId as L}from"@gitkraken/shared-tools";const R={collecting:{message:`Collecting changes…`,weight:5},composing:{message:`Composing commits…`,weight:40},"compose-grouping":{message:`Grouping changes into commits…`,weight:40},analyzing:{message:`Analyzing changes…`,weight:20},grouping:{message:`Grouping changes into commits…`,weight:25},ordering:{message:`Ordering commits…`,weight:15},partitioning:{message:`Subdividing branches…`,weight:20},verifying:{message:`Verifying…`,weight:5},applying:{message:`Applying to repository…`,weight:10}};async function z(r){if(!r.applyPlan&&!r.model)throw new n(`Either 'model' or 'applyPlan' must be provided`);if(r.applyPlan)return te(r);if(r.analysis!=null&&r.session!=null)throw new n(`'analysis' cannot be combined with 'session'; provide one or the other`);let{git:i,model:a,source:o={type:`workdir`},onProgress:s,onBeforeApply:c,cancellation:l}=r,u=a,d=r.depth??`balanced`,p=r.target??await Y(i,o);Z(o,p),await ie(i,r.applyCommitIds,r.hunkFilter,o,p),await f({git:i,source:o,target:p,overrides:r.overrideSafetyFailures});let m=[];s?.({phase:`collecting`,...R.collecting});let g=Date.now(),_=await C(i,o);if(m.push({name:`collecting`,durationMs:Date.now()-g,attempts:1}),_.hunks.length===0)throw new e(`NO_CHANGES`,X(o));let{hunks:v,filteredCount:y}=await q(_.hunks,r.hunkFilter);if(v.length===0)throw new e(`NO_CHANGES`,X(o));let b=K(v);s?.({phase:`collecting`,...R.collecting,stats:b});let S=y!==void 0&&y>0?await J(i,_.snapshot,v):_.snapshot,w=r.conventions??await k({git:i,author:r.conventionAuthor}),T=await ce(i),j=p.type===`auto`?`branches`:`commits`,M=r.disableDefaultNoiseReduction?r.redactHunkContent:x(r.redactHunkContent),N=M?h(M):void 0,P=N?v.filter(e=>N(e)).length:void 0,F={hunks:v,collection:_,conventions:w,existingBranchNames:T,groupMode:j,target:p,model:u,instructions:r.instructions,context:r.context,analysis:r.analysis,condenseHunks:r.condenseHunks,hunkSummaries:r.hunkSummaries,strategy:r.strategy,maxRetries:r.maxRetries,maxTokens:r.maxTokens,onProgress:s,cancellation:l,onBeforeModelCall:r.onBeforeModelCall,onAfterModelCall:r.onAfterModelCall,onBeforePrompt:r.onBeforePrompt,session:r.session,promptOverrides:r.promptOverrides,redactHunkContent:N,inputTokenBudget:r.inputTokenBudget},L;try{L=await ee(d,F)}catch(e){A(e,l)}let z={analysis:L.analysis,grouping:L.grouping,ordering:L.ordering,branches:L.branchPlans,allOrderedCommits:L.allOrderedCommits};if(m.push(...L.phases),c&&!await c(z))return{plan:z,snapshot:S,session:L.session,usage:L.usage,phases:m,source:{type:o.type===`branch`?`branch`:`workdir`,hunks:v,commitMessages:_.commitMessages,branchName:_.branchName,baseBranch:_.baseBranch},diffStats:b,feedbackTargetCallId:ne(m),hunksFilteredCount:y,hunksRedactedCount:P,hunksCondensedCount:L.hunksCondensedCount};let B=await I(O(i,`capturing pre-operation state`));s?.({phase:`verifying`,...R.verifying});let V=Date.now(),H=await E(i,_.snapshot,re(o,_));if(!H.valid)throw new t(`SAFETY_CHECK_FAILED`,`Safety check failed: ${H.error}`);m.push({name:`verifying`,durationMs:Date.now()-V,attempts:1});let U=ae(L.allOrderedCommits,r.applyCommitIds),{leftoverHunks:W,leftoverPatch:G}=se(U,_.hunks),Q=W.length>0?await J(i,S,oe(U,v)):S;s?.({phase:`applying`,...R.applying});let ue=Date.now(),$=await D({git:i,source:o,target:p,branchPlans:L.branchPlans,allOrderedCommits:U,hunks:v,snapshot:Q,collection:_,signing:r.signing,authorAttribution:r.authorAttribution,stashLabel:r.stashLabel,leftoverPatch:G});m.push({name:`applying`,durationMs:Date.now()-ue,attempts:1});let de=await le(i,B,$.mutations,p,o);return{plan:z,snapshot:Q,partitions:$.partitions,partitionGroups:$.partitionGroups,session:L.session,usage:L.usage,phases:m,source:{type:o.type===`branch`?`branch`:`workdir`,hunks:v,commitMessages:_.commitMessages,branchName:_.branchName,baseBranch:_.baseBranch},diffStats:b,commitShas:$.commitShas,undoId:de,feedbackTargetCallId:ne(m),hunksFilteredCount:y,hunksRedactedCount:P,hunksCondensedCount:L.hunksCondensedCount,appliedCommitIds:U.map(e=>e.id),unappliedHunkIndices:W.length>0?W.map(e=>e.index):void 0,stashConflict:$.stashConflict,stashNotRestored:$.stashNotRestored}}function ee(e,t){switch(e){case`quick`:return B(t);case`balanced`:return V(t);case`deep`:return U(t)}}async function B(e){return H(e,{lightweight:!0,skipOrder:!0})}async function V(e){return H(e,{includeAnalysis:!0})}async function H(e,t){let{hunks:n,collection:r,conventions:i,existingBranchNames:a,groupMode:o,target:s,model:c,instructions:l,context:u,strategy:d,maxRetries:f,maxTokens:h,onProgress:g,cancellation:v,onBeforeModelCall:x,onAfterModelCall:S,onBeforePrompt:C}=e,w=m(),T=[],E=await _({hunks:n,commitMessages:r.commitMessages,commitOrder:r.commitOrder,branchName:r.branchName,baseBranch:r.baseBranch,issueUrl:u?.issueUrl,pullRequestUrl:u?.pullRequestUrl,authoring:u,analysis:e.analysis,hunkSummaries:e.hunkSummaries,instructions:l,conventions:i,mode:o,strategy:d?.grouping,existingBranchNames:o===`branches`?a:void 0,includeAnalysis:t.includeAnalysis&&e.analysis==null,lightweight:t.lightweight,condensed:t.condensed||e.condenseHunks,redactHunkContent:e.redactHunkContent,inputTokenBudget:e.inputTokenBudget,model:c,session:e.session,maxRetries:f,maxTokens:h,onProgress:e=>g?.({phase:`compose-grouping`,...R[`compose-grouping`],event:e}),cancellation:v,onBeforeModelCall:x,onAfterModelCall:S,onBeforePrompt:C,promptOverride:e.promptOverrides?.[`compose-group`]});E.usage&&p(w,E.usage),T.push({name:`compose-grouping`,durationMs:E.durationMs,attempts:E.attempts,usage:E.usage,callIds:E.callIds,repairs:E.repairs});let D=E.session,O,k;if(t.skipOrder)O=E.result.branches.map(e=>({branchGroup:e,orderedCommitIds:e.commits.map(e=>e.id)})),k={branches:O.map(e=>({branchId:e.branchGroup.id,orderedCommitIds:e.orderedCommitIds})),rationale:``};else{let t=await y({branches:E.result.branches,hunks:n,instructions:l,model:c,session:E.session,maxRetries:f,maxTokens:h,onProgress:e=>g?.({phase:`ordering`,...R.ordering,event:e}),cancellation:v,onBeforeModelCall:x,onAfterModelCall:S,onBeforePrompt:C,promptOverride:e.promptOverrides?.order});t.usage&&p(w,t.usage),T.push({name:`ordering`,durationMs:t.durationMs,attempts:t.attempts,usage:t.usage,callIds:t.callIds}),D=t.session,k=t.result,O=E.result.branches.map(e=>({branchGroup:e,orderedCommitIds:k.branches.find(t=>t.branchId===e.id)?.orderedCommitIds??e.commits.map(e=>e.id)}))}if(s.type===`auto`){let t=new Set(a);for(let e of E.result.branches)e.name&&t.add(e.name);for(let n of O){let r=await b({commits:Q(n.branchGroup,n.orderedCommitIds),orderedCommitIds:n.orderedCommitIds,sourceBranchName:n.branchGroup.name,instructions:l,conventions:i,strategy:d?.partition,existingBranchNames:t,model:c,maxRetries:f,maxTokens:h,onProgress:e=>g?.({phase:`partitioning`,...R.partitioning,branch:n.branchGroup.name,event:e}),cancellation:v,onBeforeModelCall:x,onAfterModelCall:S,onBeforePrompt:C,promptOverride:e.promptOverrides?.partition});r.usage&&p(w,r.usage),T.push({name:`partitioning:${n.branchGroup.name}`,durationMs:r.durationMs,attempts:r.attempts,usage:r.usage,callIds:r.callIds}),n.partitioning=r.result,D=r.session;for(let e of r.result.partitions)t.add(e.branchName)}}let A=O.flatMap(e=>Q(e.branchGroup,e.orderedCommitIds));return{analysis:e.analysis,grouping:E.result,ordering:k,branchPlans:O,allOrderedCommits:A,session:D,usage:w,phases:T,hunksCondensedCount:E.hunksCondensedCount}}async function U(e){let{hunks:t,collection:n,conventions:r,existingBranchNames:i,groupMode:a,target:o,model:s,instructions:c,context:l,strategy:u,maxRetries:d,maxTokens:f,onProgress:h,cancellation:_,onBeforeModelCall:x,onAfterModelCall:S,onBeforePrompt:C}=e,w=m(),T=[],E=e.analysis,D,O,k;if(E!=null)D=E,O=void 0,T.push({name:`analyzing`,durationMs:0,attempts:0,callIds:[]});else{let r=await g({hunks:t,commitMessages:n.commitMessages,commitOrder:n.commitOrder,branchName:n.branchName,baseBranch:n.baseBranch,issueUrl:l?.issueUrl,pullRequestUrl:l?.pullRequestUrl,authoring:l,hunkSummaries:e.hunkSummaries,instructions:c,condensed:e.condenseHunks,redactHunkContent:e.redactHunkContent,inputTokenBudget:e.inputTokenBudget,model:s,session:e.session,maxRetries:d,maxTokens:f,onProgress:e=>h?.({phase:`analyzing`,...R.analyzing,event:e}),cancellation:_,onBeforeModelCall:x,onAfterModelCall:S,onBeforePrompt:C,promptOverride:e.promptOverrides?.analyze});r.usage&&p(w,r.usage),T.push({name:`analyzing`,durationMs:r.durationMs,attempts:r.attempts,usage:r.usage,callIds:r.callIds}),D=r.result,O=r.session,k=r.hunksCondensedCount}let A=await v({hunks:t,commitMessages:n.commitMessages,instructions:c,authoring:l,analysis:E,condensed:e.condenseHunks,hunkSummaries:e.hunkSummaries,conventions:r,mode:a,strategy:u?.grouping,existingBranchNames:a===`branches`?i:void 0,redactHunkContent:e.redactHunkContent,inputTokenBudget:e.inputTokenBudget,model:s,session:O,maxRetries:d,maxTokens:f,onProgress:e=>h?.({phase:`grouping`,...R.grouping,event:e}),cancellation:_,onBeforeModelCall:x,onAfterModelCall:S,onBeforePrompt:C,promptOverride:e.promptOverrides?.group});A.usage&&p(w,A.usage),T.push({name:`grouping`,durationMs:A.durationMs,attempts:A.attempts,usage:A.usage,callIds:A.callIds,repairs:A.repairs});let j=await y({branches:A.result.branches,hunks:t,instructions:c,model:s,session:A.session,maxRetries:d,maxTokens:f,onProgress:e=>h?.({phase:`ordering`,...R.ordering,event:e}),cancellation:_,onBeforeModelCall:x,onAfterModelCall:S,onBeforePrompt:C,promptOverride:e.promptOverrides?.order});j.usage&&p(w,j.usage),T.push({name:`ordering`,durationMs:j.durationMs,attempts:j.attempts,usage:j.usage,callIds:j.callIds});let M=A.result.branches.map(e=>({branchGroup:e,orderedCommitIds:j.result.branches.find(t=>t.branchId===e.id)?.orderedCommitIds??e.commits.map(e=>e.id)})),N=j.session;if(o.type===`auto`){let t=new Set(i);for(let e of A.result.branches)e.name&&t.add(e.name);for(let n of M){let i=await b({commits:Q(n.branchGroup,n.orderedCommitIds),orderedCommitIds:n.orderedCommitIds,sourceBranchName:n.branchGroup.name,instructions:c,conventions:r,strategy:u?.partition,existingBranchNames:t,model:s,maxRetries:d,maxTokens:f,onProgress:e=>h?.({phase:`partitioning`,...R.partitioning,branch:n.branchGroup.name,event:e}),cancellation:_,onBeforeModelCall:x,onAfterModelCall:S,onBeforePrompt:C,promptOverride:e.promptOverrides?.partition});i.usage&&p(w,i.usage),T.push({name:`partitioning:${n.branchGroup.name}`,durationMs:i.durationMs,attempts:i.attempts,usage:i.usage,callIds:i.callIds}),n.partitioning=i.result,N=i.session;for(let e of i.result.partitions)t.add(e.branchName)}}let P=M.flatMap(e=>Q(e.branchGroup,e.orderedCommitIds));return{analysis:D,grouping:A.result,ordering:j.result,branchPlans:M,allOrderedCommits:P,session:N,usage:w,phases:T,hunksCondensedCount:k??A.hunksCondensedCount}}async function W(e){let t=await z({...e,onBeforeApply:async()=>!1});return{plan:t.plan,snapshot:t.snapshot,source:t.source,session:t.session,usage:t.usage,phases:t.phases,feedbackTargetCallId:t.feedbackTargetCallId,hunksFilteredCount:t.hunksFilteredCount,hunksRedactedCount:t.hunksRedactedCount,hunksCondensedCount:t.hunksCondensedCount,diffStats:t.diffStats}}async function G(e){let t=await z({git:e.git,applyPlan:e.applyPlan,source:e.source,target:e.target,onProgress:e.onProgress,signing:e.signing,authorAttribution:e.authorAttribution,applyCommitIds:e.applyCommitIds,stashLabel:e.stashLabel,hunkFilter:e.hunkFilter,overrideSafetyFailures:e.overrideSafetyFailures});return{commitShas:t.commitShas,partitions:t.partitions,partitionGroups:t.partitionGroups,phases:t.phases,undoId:t.undoId,appliedCommitIds:t.appliedCommitIds,unappliedHunkIndices:t.unappliedHunkIndices,stashConflict:t.stashConflict,stashNotRestored:t.stashNotRestored}}async function te(n){let{git:r,applyPlan:i,onProgress:a}=n,{plan:o,snapshot:s,source:c}=i,l=n.source??c,u=n.target??i?.target??await Y(r,l);Z(l,u),await ie(r,n.applyCommitIds,n.hunkFilter,l,u),await f({git:r,source:l,target:u,overrides:n.overrideSafetyFailures});let d=await I(O(r,`capturing pre-operation state`)),p=[];a?.({phase:`collecting`,...R.collecting});let h=Date.now(),g=await C(r,l);if(p.push({name:`collecting`,durationMs:Date.now()-h,attempts:1}),g.hunks.length===0)throw new e(`NO_CHANGES`,X(l));let{hunks:_}=await q(g.hunks,n.hunkFilter);if(_.length===0)throw new e(`NO_CHANGES`,X(l));let v=K(_);a?.({phase:`collecting`,...R.collecting,stats:v});let y=_.length<g.hunks.length?await J(r,g.snapshot,_):g.snapshot,b={...g,hunks:_,snapshot:y};if(b.snapshot.diffHash!==s.diffHash)throw new t(`SAFETY_CHECK_FAILED`,`Working directory has changed since the plan was created. Re-run compose to generate a new plan.`);a?.({phase:`verifying`,...R.verifying});let x=Date.now(),S=await E(r,b.snapshot,re(l,b));if(!S.valid)throw new t(`SAFETY_CHECK_FAILED`,`Safety check failed: ${S.error}`);p.push({name:`verifying`,durationMs:Date.now()-x,attempts:1});let w=ae(o.allOrderedCommits,n.applyCommitIds),{leftoverHunks:T,leftoverPatch:k}=se(w,g.hunks),A=T.length>0?await J(r,b.snapshot,oe(w,b.hunks)):b.snapshot;a?.({phase:`applying`,...R.applying});let j=Date.now(),M=await D({git:r,source:l,target:u,branchPlans:o.branches,allOrderedCommits:w,hunks:b.hunks,snapshot:A,collection:b,signing:n.signing,authorAttribution:n.authorAttribution,stashLabel:n.stashLabel,leftoverPatch:k});p.push({name:`applying`,durationMs:Date.now()-j,attempts:1});let N=await le(r,d,M.mutations,u,l);return{plan:o,snapshot:A,partitions:M.partitions,partitionGroups:M.partitionGroups,session:{messages:[],systemPrompt:``,completedSteps:{}},usage:m(),phases:p,source:{type:l.type===`branch`?`branch`:`workdir`,hunks:b.hunks,commitMessages:b.commitMessages,branchName:b.branchName,baseBranch:b.baseBranch},diffStats:v,commitShas:M.commitShas,undoId:N,appliedCommitIds:w.map(e=>e.id),unappliedHunkIndices:T.length>0?T.map(e=>e.index):void 0,stashConflict:M.stashConflict,stashNotRestored:M.stashNotRestored}}function K(e){let t=new Set,n=0,r=0;for(let i of e)t.add(i.fileName),n+=i.additions,r+=i.deletions;return{fileCount:t.size,hunkCount:e.length,addedLines:n,removedLines:r}}function ne(e){for(let t=e.length-1;t>=0;t--){let{callIds:n,repairs:r}=e[t];if(!n||n.length===0)continue;let i=r?.some(e=>e.kind===`missing-hunks`)&&n.length>1;return n[n.length-(i?2:1)]}}async function q(e,t){if(!t)return{hunks:e,filteredCount:void 0};let n=await t(e);return{hunks:n,filteredCount:e.length-n.length}}async function J(e,t,n){let i=t.baseSha??t.headSha,o=P(N(),`compose-filtered-idx-${j()}`),s={GIT_INDEX_FILE:o};try{await l(e,i,{indexEnv:s});let o=T(n);o.length>0&&await r(e,o,{indexEnv:s});let c=await a(e,i,await d(e,{indexEnv:s}));return{...t,diffHash:S(c)}}finally{try{M(o)}catch{}}}function re(e,t){return e.type===`workdir`?{includeUnstaged:!e.stagedOnly,includeUntracked:t.untrackedIncluded}:e.type===`commit-range`&&e.includeWorkdir?w(e.includeWorkdir):{includeUnstaged:!1,includeUntracked:!1}}async function ie(e,t,r,a,o){if(r==null&&t==null||a.type===`workdir`||!(o.type===`rewrite-range`||(o.type===`branch`||o.type===`auto`)&&o.preserveOriginal!==!0))return;let s=a.type===`branch`?await u(e,a.name):await u(e,a.to),c=await u(e,`HEAD`);if(await i(e)==null)throw new n(`Filtering and partial apply require HEAD to be on a branch (non-detached) when rewriting existing commits. Check out the source branch before composing.`);if(c!==s)throw new n(`Filtering and partial apply require HEAD to be at the source's tip (${s.slice(0,8)}); HEAD is at ${c.slice(0,8)}.`)}function ae(e,t){if(t==null)return e;let r=new Set(t),i=e.filter(e=>r.has(e.id)),a=new Set(i.map(e=>e.id)),o=t.filter(e=>!a.has(e));if(o.length>0)throw new n(`applyCommitIds contains commit ids not found in plan.allOrderedCommits: ${o.join(`, `)}`);return i}function oe(e,t){let n=new Set(e.flatMap(e=>e.hunkIndices));return t.filter(e=>n.has(e.index))}function se(e,t){let n=new Set(e.flatMap(e=>e.hunkIndices)),r=t.filter(e=>!n.has(e.index));if(r.length===0)return{leftoverHunks:[],leftoverPatch:void 0};let i=T(r);return{leftoverHunks:r,leftoverPatch:i.length>0?i:void 0}}async function Y(e,t){if(t.type===`branch`){let r=await u(e,t.name),i=(await s(e,{format:`%H`,range:`${await c(e,await u(e,t.mergeTarget??`main`),r)}..${r}`})).trim().split(`
|
|
2
2
|
`).at(-1);if(!i)throw new n(`Cannot recompose branch '${t.name}': no commits between merge-base and branch tip.`);return{type:`rewrite-range`,branch:t.name,rangeFrom:i,rangeTo:r}}return t.type===`commit-range`?{type:`rewrite-range`,branch:t.branch,rangeFrom:t.from,rangeTo:t.to}:{type:`head`}}function X(e){switch(e.type){case`workdir`:return`No changes found in working directory`;case`branch`:return`No changes found on branch '${e.name}'`;case`commit-range`:return`No changes found in range '${e.from}..${e.to}' on branch '${e.branch}'`}}function Z(e,t){if(t.type===`rewrite-range`&&e.type===`workdir`)throw new n(`target 'rewrite-range' is not valid with 'workdir' source`);if(t.type===`head`&&e.type!==`workdir`)throw new n(`target 'head' is not valid with '${e.type}' source — use 'branch', 'auto', or 'rewrite-range'`)}function Q(e,n){return n.map(n=>{let r=e.commits.find(e=>e.id===n);if(!r)throw new t(`INTERNAL`,`Commit ${n} not found in branch '${e.id}'`);return r})}async function ce(e){let t=(await o(e,{format:`%(refname:short)`,prefix:`refs/heads/`})).trim().split(`
|
|
3
3
|
`).filter(Boolean);return new Set(t)}async function le(e,t,n,r,i){let a=L(),o=n.composedBranchCount||1,s=`compose: ${o} branch${o===1?``:`es`} from ${i.type}`,c=[...n.movedRefs];if(r.type===`head`&&t.headBranch){let n=await u(e,`HEAD`);n!==t.headSha&&c.push({kind:`branch`,name:t.headBranch,previousSha:t.headSha,newSha:n})}let l={version:1,id:a,createdAt:new Date().toISOString(),description:s,previousHead:{sha:t.headSha,branch:t.headBranch},workdirClean:t.workdirClean,workdirBackupSha:t.workdirBackupSha,createdRefs:n.createdRefs,deletedRefs:n.deletedRefs,movedRefs:c};return await new F(O(e,`undo manifest persistence`)).save(l),a}export{G as applyComposePlan,q as applyHunkFilter,z as compose,W as composePlan,K as computeDiffStats,R as defaultPhaseMessages,Y as defaultTargetForSource,X as describeNoChangesForSource,J as rebuildFilteredSnapshot,Z as validateSourceTarget};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gitkraken/compose-tools",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"description": "AI-powered organization of git changes into clean, atomic commits and branches.",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE",
|
|
6
6
|
"author": "Axosoft, LLC dba GitKraken",
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
"CHANGELOG.md"
|
|
21
21
|
],
|
|
22
22
|
"dependencies": {
|
|
23
|
-
"@gitkraken/shared-tools": "^0.
|
|
23
|
+
"@gitkraken/shared-tools": "^0.3.0"
|
|
24
24
|
},
|
|
25
25
|
"devDependencies": {
|
|
26
26
|
"@types/node": "^22.0.0",
|