@gitkraken/compose-tools 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +27 -0
- package/LICENSE +19 -0
- package/README.md +543 -0
- package/dist/ai/session.d.ts +12 -0
- package/dist/ai/session.js +1 -0
- package/dist/ai/types.d.ts +31 -0
- package/dist/ai/types.js +1 -0
- package/dist/cancellation.d.ts +20 -0
- package/dist/cancellation.js +1 -0
- package/dist/errors/ai.d.ts +10 -0
- package/dist/errors/ai.js +1 -0
- package/dist/errors/git.d.ts +12 -0
- package/dist/errors/git.js +1 -0
- package/dist/errors/task.d.ts +6 -0
- package/dist/errors/task.js +1 -0
- package/dist/errors/workflow.d.ts +13 -0
- package/dist/errors/workflow.js +1 -0
- package/dist/git/apply-branches.d.ts +28 -0
- package/dist/git/apply-branches.js +1 -0
- package/dist/git/apply-target.d.ts +39 -0
- package/dist/git/apply-target.js +1 -0
- package/dist/git/commit-chain.d.ts +87 -0
- package/dist/git/commit-chain.js +6 -0
- package/dist/git/diff-collector.d.ts +80 -0
- package/dist/git/diff-collector.js +8 -0
- package/dist/git/index.d.ts +7 -0
- package/dist/git/index.js +1 -0
- package/dist/git/port-dispatch.d.ts +34 -0
- package/dist/git/port-dispatch.js +3 -0
- package/dist/git/require-exec.d.ts +17 -0
- package/dist/git/require-exec.js +1 -0
- package/dist/git/safety.d.ts +22 -0
- package/dist/git/safety.js +1 -0
- package/dist/git/types.d.ts +43 -0
- package/dist/git/types.js +1 -0
- package/dist/git/utils/conventions.d.ts +33 -0
- package/dist/git/utils/conventions.js +10 -0
- package/dist/git/utils/crypto.d.ts +4 -0
- package/dist/git/utils/crypto.js +1 -0
- package/dist/git/utils/hunk-parser.d.ts +6 -0
- package/dist/git/utils/hunk-parser.js +7 -0
- package/dist/git/utils/patch-builder.d.ts +6 -0
- package/dist/git/utils/patch-builder.js +2 -0
- package/dist/git/utils/temp-index.d.ts +6 -0
- package/dist/git/utils/temp-index.js +1 -0
- package/dist/index.d.ts +28 -0
- package/dist/index.js +1 -0
- package/dist/ports/git.d.ts +199 -0
- package/dist/ports/git.js +1 -0
- package/dist/tasks/ai-loop.d.ts +99 -0
- package/dist/tasks/ai-loop.js +2 -0
- package/dist/tasks/analyze/analyze.d.ts +6 -0
- package/dist/tasks/analyze/analyze.js +1 -0
- package/dist/tasks/analyze/prompt.d.ts +16 -0
- package/dist/tasks/analyze/prompt.js +34 -0
- package/dist/tasks/analyze/schema.d.ts +38 -0
- package/dist/tasks/analyze/schema.js +1 -0
- package/dist/tasks/analyze/types.d.ts +69 -0
- package/dist/tasks/analyze/types.js +1 -0
- package/dist/tasks/analyze/validator.d.ts +7 -0
- package/dist/tasks/analyze/validator.js +13 -0
- package/dist/tasks/compose-group/compose-group.d.ts +15 -0
- package/dist/tasks/compose-group/compose-group.js +1 -0
- package/dist/tasks/compose-group/prompt.d.ts +26 -0
- package/dist/tasks/compose-group/prompt.js +162 -0
- package/dist/tasks/compose-group/types.d.ts +62 -0
- package/dist/tasks/compose-group/types.js +1 -0
- package/dist/tasks/group/group.d.ts +6 -0
- package/dist/tasks/group/group.js +1 -0
- package/dist/tasks/group/prompt.d.ts +15 -0
- package/dist/tasks/group/prompt.js +158 -0
- package/dist/tasks/group/schema.d.ts +19 -0
- package/dist/tasks/group/schema.js +1 -0
- package/dist/tasks/group/types.d.ts +61 -0
- package/dist/tasks/group/types.js +1 -0
- package/dist/tasks/group/validator.d.ts +7 -0
- package/dist/tasks/group/validator.js +53 -0
- package/dist/tasks/index.d.ts +15 -0
- package/dist/tasks/index.js +1 -0
- package/dist/tasks/order/order.d.ts +6 -0
- package/dist/tasks/order/order.js +1 -0
- package/dist/tasks/order/prompt.d.ts +13 -0
- package/dist/tasks/order/prompt.js +45 -0
- package/dist/tasks/order/schema.d.ts +12 -0
- package/dist/tasks/order/schema.js +1 -0
- package/dist/tasks/order/types.d.ts +36 -0
- package/dist/tasks/order/types.js +1 -0
- package/dist/tasks/order/validator.d.ts +11 -0
- package/dist/tasks/order/validator.js +12 -0
- package/dist/tasks/partition/partition.d.ts +6 -0
- package/dist/tasks/partition/partition.js +1 -0
- package/dist/tasks/partition/prompt.d.ts +16 -0
- package/dist/tasks/partition/prompt.js +61 -0
- package/dist/tasks/partition/schema.d.ts +14 -0
- package/dist/tasks/partition/schema.js +1 -0
- package/dist/tasks/partition/types.d.ts +53 -0
- package/dist/tasks/partition/types.js +1 -0
- package/dist/tasks/partition/validator.d.ts +7 -0
- package/dist/tasks/partition/validator.js +11 -0
- package/dist/tasks/quick-compose/prompt.d.ts +18 -0
- package/dist/tasks/quick-compose/prompt.js +47 -0
- package/dist/tasks/quick-compose/quick-compose.d.ts +6 -0
- package/dist/tasks/quick-compose/quick-compose.js +1 -0
- package/dist/tasks/quick-compose/schema.d.ts +27 -0
- package/dist/tasks/quick-compose/schema.js +1 -0
- package/dist/tasks/quick-compose/types.d.ts +63 -0
- package/dist/tasks/quick-compose/types.js +1 -0
- package/dist/tasks/quick-compose/validator.d.ts +7 -0
- package/dist/tasks/quick-compose/validator.js +25 -0
- package/dist/tasks/session.d.ts +22 -0
- package/dist/tasks/session.js +1 -0
- package/dist/tasks/types.d.ts +101 -0
- package/dist/tasks/types.js +3 -0
- package/dist/workflows/compose.d.ts +371 -0
- package/dist/workflows/compose.js +2 -0
- package/dist/workflows/split.d.ts +80 -0
- package/dist/workflows/split.js +3 -0
- package/dist/workflows/types.d.ts +63 -0
- package/dist/workflows/types.js +1 -0
- package/dist/workflows/undo.d.ts +56 -0
- package/dist/workflows/undo.js +1 -0
- package/package.json +37 -0
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
//#region src/tasks/types.d.ts
|
|
2
|
+
interface ComposeHunk {
|
|
3
|
+
index: number;
|
|
4
|
+
fileName: string;
|
|
5
|
+
diffHeader: string;
|
|
6
|
+
hunkHeader: string;
|
|
7
|
+
content: string;
|
|
8
|
+
additions: number;
|
|
9
|
+
deletions: number;
|
|
10
|
+
source: 'staged' | 'unstaged' | 'untracked' | 'branch';
|
|
11
|
+
sourceCommitSha?: string;
|
|
12
|
+
isRename?: boolean;
|
|
13
|
+
originalFileName?: string;
|
|
14
|
+
isBinary?: boolean;
|
|
15
|
+
/** Full binary patch block (base85-encoded). Excluded from AI context, used only by patch builder. */
|
|
16
|
+
binaryPatchData?: string;
|
|
17
|
+
/**
|
|
18
|
+
* Original author of this hunk (e.g. when the hunk came from a branch source
|
|
19
|
+
* and the commit carrying it was authored by someone specific). The apply
|
|
20
|
+
* phase aggregates per-commit hunks through an attribution strategy to choose
|
|
21
|
+
* the commit's primary author and Co-authored-by trailers.
|
|
22
|
+
*
|
|
23
|
+
* `date` is the commit's author date when known — used by consumers for UI
|
|
24
|
+
* display and, for the commit created by the apply path, as `GIT_AUTHOR_DATE`.
|
|
25
|
+
*/
|
|
26
|
+
author?: {
|
|
27
|
+
name: string;
|
|
28
|
+
email: string;
|
|
29
|
+
date?: Date;
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* Additional authors attached to this hunk (for merge commits or the AI's
|
|
33
|
+
* attribution hints). Combined with `author` from all hunks in a commit to
|
|
34
|
+
* produce the final Co-authored-by trailer list.
|
|
35
|
+
*/
|
|
36
|
+
coAuthors?: {
|
|
37
|
+
name: string;
|
|
38
|
+
email: string;
|
|
39
|
+
date?: Date;
|
|
40
|
+
}[];
|
|
41
|
+
}
|
|
42
|
+
interface CommitSuggestion {
|
|
43
|
+
id: string;
|
|
44
|
+
message: string;
|
|
45
|
+
explanation: string;
|
|
46
|
+
hunkIndices: number[];
|
|
47
|
+
}
|
|
48
|
+
interface CommitDetail {
|
|
49
|
+
sha: string;
|
|
50
|
+
message: string;
|
|
51
|
+
diff: string;
|
|
52
|
+
filesChanged: string[];
|
|
53
|
+
}
|
|
54
|
+
interface BranchGroup {
|
|
55
|
+
id: string;
|
|
56
|
+
name: string;
|
|
57
|
+
title: string;
|
|
58
|
+
description: string;
|
|
59
|
+
commits: CommitSuggestion[];
|
|
60
|
+
}
|
|
61
|
+
interface BranchOrdering {
|
|
62
|
+
branchId: string;
|
|
63
|
+
orderedCommitIds: string[];
|
|
64
|
+
}
|
|
65
|
+
interface BranchPartition {
|
|
66
|
+
branchName: string;
|
|
67
|
+
title: string;
|
|
68
|
+
description: string;
|
|
69
|
+
commitIds: string[];
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Identifier for each AI task the library can run. Matches the `taskName`
|
|
73
|
+
* value passed to `runAiLoop` and the phase label surfaced on {@link
|
|
74
|
+
* import('../ai/types.js').PhaseDetail}.
|
|
75
|
+
*/
|
|
76
|
+
type ComposeTaskName = 'analyze' | 'group' | 'compose-group' | 'order' | 'partition' | 'quick-compose';
|
|
77
|
+
/**
|
|
78
|
+
* Per-task prompt override. Every field is optional; undefined fields fall
|
|
79
|
+
* back to the library's built-in prompt.
|
|
80
|
+
*
|
|
81
|
+
* - `system`: replaces the task's system prompt entirely. The caller is
|
|
82
|
+
* responsible for preserving any library-required constraints (output
|
|
83
|
+
* format, validation schemas, etc.) the built-in prompt encodes — the
|
|
84
|
+
* library's validators still run against whatever the model produces.
|
|
85
|
+
* - `userPrefix`: prepended to the task's user prompt, separated by a
|
|
86
|
+
* blank line. Use for context the AI should see before the task body.
|
|
87
|
+
* - `userSuffix`: appended to the task's user prompt, separated by a
|
|
88
|
+
* blank line. Use for late-binding instructions or reminders.
|
|
89
|
+
*/
|
|
90
|
+
interface PromptOverride {
|
|
91
|
+
system?: string;
|
|
92
|
+
userPrefix?: string;
|
|
93
|
+
userSuffix?: string;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Helper that applies a {@link PromptOverride} to a built user prompt.
|
|
97
|
+
* Returns the original prompt when no override exists.
|
|
98
|
+
*/
|
|
99
|
+
declare function applyUserPromptOverride(prompt: string, override?: PromptOverride): string;
|
|
100
|
+
//#endregion
|
|
101
|
+
export { BranchGroup, BranchOrdering, BranchPartition, CommitDetail, CommitSuggestion, ComposeHunk, ComposeTaskName, PromptOverride, applyUserPromptOverride };
|
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
import { PhaseDetail, ProgressEvent, TokenUsage } from "../ai/types.js";
|
|
2
|
+
import { ComposeGitPort, SigningConfig } from "../ports/git.js";
|
|
3
|
+
import { BranchPartition, CommitSuggestion, ComposeHunk, ComposeTaskName, PromptOverride } from "../tasks/types.js";
|
|
4
|
+
import { ConventionDescriptor } from "../git/utils/conventions.js";
|
|
5
|
+
import { OnAfterModelCall, OnBeforeModelCall, OnBeforePrompt } from "../tasks/ai-loop.js";
|
|
6
|
+
import { AnalysisResult } from "../tasks/analyze/types.js";
|
|
7
|
+
import { GroupingResult, GroupingStrategy } from "../tasks/group/types.js";
|
|
8
|
+
import { OrderingResult } from "../tasks/order/types.js";
|
|
9
|
+
import { ComposeSession } from "../tasks/session.js";
|
|
10
|
+
import { PartitionStrategy } from "../tasks/partition/types.js";
|
|
11
|
+
import { ComposeContext, ComposeSource, ComposeTarget } from "./types.js";
|
|
12
|
+
import { SafetySnapshot } from "../git/types.js";
|
|
13
|
+
import { ComposeBranchPlan } from "../git/apply-target.js";
|
|
14
|
+
import { AiModelPort, UndoId } from "@gitkraken/shared-tools";
|
|
15
|
+
|
|
16
|
+
//#region src/workflows/compose.d.ts
|
|
17
|
+
/**
|
|
18
|
+
* Pure-planning input — runs the AI pipeline and returns a {@link ComposePlan}
|
|
19
|
+
* without mutating the repository. Same shape as {@link ComposeInput} minus
|
|
20
|
+
* `applyPlan` and `onBeforeApply`.
|
|
21
|
+
*/
|
|
22
|
+
interface ComposePlanInput {
|
|
23
|
+
git: ComposeGitPort;
|
|
24
|
+
model: AiModelPort;
|
|
25
|
+
source?: ComposeSource;
|
|
26
|
+
target?: ComposeTarget;
|
|
27
|
+
context?: ComposeContext;
|
|
28
|
+
instructions?: string;
|
|
29
|
+
conventions?: ConventionDescriptor;
|
|
30
|
+
conventionAuthor?: string;
|
|
31
|
+
strategy?: ComposeStrategy;
|
|
32
|
+
depth?: ComposeDepth;
|
|
33
|
+
maxRetries?: number;
|
|
34
|
+
onProgress?: (event: ComposeProgressEvent) => void;
|
|
35
|
+
signal?: AbortSignal;
|
|
36
|
+
onBeforeModelCall?: OnBeforeModelCall;
|
|
37
|
+
onAfterModelCall?: OnAfterModelCall;
|
|
38
|
+
onBeforePrompt?: OnBeforePrompt;
|
|
39
|
+
/** See {@link ComposeInput.hunkFilter}. */
|
|
40
|
+
hunkFilter?: ComposeInput['hunkFilter'];
|
|
41
|
+
/** See {@link ComposeInput.session}. */
|
|
42
|
+
session?: ComposeSession;
|
|
43
|
+
/** See {@link ComposeInput.promptOverrides}. */
|
|
44
|
+
promptOverrides?: ComposeInput['promptOverrides'];
|
|
45
|
+
}
|
|
46
|
+
/** Result of {@link composePlan}. Contains everything needed to later call {@link applyComposePlan}. */
|
|
47
|
+
interface ComposePlanResult {
|
|
48
|
+
plan: ComposePlan;
|
|
49
|
+
snapshot: SafetySnapshot;
|
|
50
|
+
source: ComposeResult['source'];
|
|
51
|
+
session: ComposeSession;
|
|
52
|
+
usage: TokenUsage;
|
|
53
|
+
phases: PhaseDetail[];
|
|
54
|
+
/** See {@link ComposeResult.feedbackTargetCallId}. */
|
|
55
|
+
feedbackTargetCallId?: string;
|
|
56
|
+
/** See {@link ComposeResult.hunksFilteredCount}. */
|
|
57
|
+
hunksFilteredCount?: number;
|
|
58
|
+
}
|
|
59
|
+
/** Pure-apply input — takes a previously computed plan and writes it to the repository. */
|
|
60
|
+
interface ApplyComposePlanInput {
|
|
61
|
+
git: ComposeGitPort;
|
|
62
|
+
applyPlan: ComposeApplyPlan;
|
|
63
|
+
source?: ComposeSource;
|
|
64
|
+
target?: ComposeTarget;
|
|
65
|
+
onProgress?: (event: ComposeProgressEvent) => void;
|
|
66
|
+
signal?: AbortSignal;
|
|
67
|
+
/** GPG signing config for produced commits. See {@link ComposeInput.signing}. */
|
|
68
|
+
signing?: SigningConfig;
|
|
69
|
+
/** How to pick the primary author for each commit. See {@link ComposeInput.authorAttribution}. */
|
|
70
|
+
authorAttribution?: ComposeInput['authorAttribution'];
|
|
71
|
+
}
|
|
72
|
+
/** Result of {@link applyComposePlan}. */
|
|
73
|
+
interface ApplyComposePlanResult {
|
|
74
|
+
commitShas?: Record<string, string>;
|
|
75
|
+
partitions?: BranchPartition[];
|
|
76
|
+
partitionGroups?: BranchPartition[][];
|
|
77
|
+
phases: PhaseDetail[];
|
|
78
|
+
undoId?: UndoId;
|
|
79
|
+
}
|
|
80
|
+
/** Controls how many AI pipeline steps run and how deeply each reasons. */
|
|
81
|
+
type ComposeDepth = 'quick' | 'balanced' | 'deep';
|
|
82
|
+
interface ComposeStrategy {
|
|
83
|
+
/** Controls branch splitting and commit granularity in the group step. */
|
|
84
|
+
grouping?: GroupingStrategy;
|
|
85
|
+
/** Controls how aggressively branches are subdivided into stacks in the partition step. */
|
|
86
|
+
partition?: PartitionStrategy;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* A serializable bundle of everything needed to apply a previously computed plan.
|
|
90
|
+
*
|
|
91
|
+
* The `snapshot` field is a content-hash safety check — it ensures the working directory
|
|
92
|
+
* still matches what the plan was computed against. Distinct from the `preState` returned
|
|
93
|
+
* by {@link compose}, which is a state snapshot used to roll back the operation.
|
|
94
|
+
*/
|
|
95
|
+
interface ComposeApplyPlan {
|
|
96
|
+
plan: ComposePlan;
|
|
97
|
+
source: ComposeSource;
|
|
98
|
+
snapshot: SafetySnapshot;
|
|
99
|
+
target?: ComposeTarget;
|
|
100
|
+
}
|
|
101
|
+
interface ComposeInput {
|
|
102
|
+
git: ComposeGitPort;
|
|
103
|
+
/** AI model — required unless `applyPlan` is provided. */
|
|
104
|
+
model?: AiModelPort;
|
|
105
|
+
/** Apply a previously computed plan without re-running AI phases. When set, `model` is not needed. */
|
|
106
|
+
applyPlan?: ComposeApplyPlan;
|
|
107
|
+
source?: ComposeSource;
|
|
108
|
+
target?: ComposeTarget;
|
|
109
|
+
context?: ComposeContext;
|
|
110
|
+
instructions?: string;
|
|
111
|
+
conventions?: ConventionDescriptor;
|
|
112
|
+
conventionAuthor?: string;
|
|
113
|
+
/** Controls AI aggressiveness for splitting branches, commits, and partitions. */
|
|
114
|
+
strategy?: ComposeStrategy;
|
|
115
|
+
/** Controls AI pipeline depth. 'quick' = single AI call. 'balanced' = 2-3 calls (default). 'deep' = 4+ calls with full analysis. */
|
|
116
|
+
depth?: ComposeDepth;
|
|
117
|
+
maxRetries?: number;
|
|
118
|
+
onProgress?: (event: ComposeProgressEvent) => void;
|
|
119
|
+
onBeforeApply?: (plan: ComposePlan) => Promise<boolean>;
|
|
120
|
+
/**
|
|
121
|
+
* Optional cancellation signal. Forwarded to every AI call and every git operation.
|
|
122
|
+
* On abort, the workflow throws `WorkflowError('CANCELLED')`.
|
|
123
|
+
*/
|
|
124
|
+
signal?: AbortSignal;
|
|
125
|
+
/**
|
|
126
|
+
* Called immediately before each model.generate invocation. Use for progress UI,
|
|
127
|
+
* telemetry, or correlation. The library doesn't interpret the result.
|
|
128
|
+
*/
|
|
129
|
+
onBeforeModelCall?: OnBeforeModelCall;
|
|
130
|
+
/** Called after each model.generate invocation, on both success and error. */
|
|
131
|
+
onAfterModelCall?: OnAfterModelCall;
|
|
132
|
+
/**
|
|
133
|
+
* Gating hook called immediately before each model.generate call. Returning
|
|
134
|
+
* `false` aborts the workflow with `WorkflowError('CANCELLED')`. Use for
|
|
135
|
+
* large-prompt confirmations, cost warnings, org policy checks, etc.
|
|
136
|
+
*/
|
|
137
|
+
onBeforePrompt?: OnBeforePrompt;
|
|
138
|
+
/**
|
|
139
|
+
* Called after source collection and before the AI pipeline runs. The
|
|
140
|
+
* returned hunks become the set the AI sees (and the set whose indices
|
|
141
|
+
* are referenced by the resulting plan). Use for AIIgnore globs, org
|
|
142
|
+
* policy filters, or any consumer-side hunk-level policy.
|
|
143
|
+
*
|
|
144
|
+
* The difference between the returned array's length and the input's is
|
|
145
|
+
* surfaced as {@link ComposeResult.hunksFilteredCount}. If the filter
|
|
146
|
+
* removes every hunk, the workflow throws `GitError('NO_CHANGES')` just
|
|
147
|
+
* like an empty collection.
|
|
148
|
+
*
|
|
149
|
+
* The filter MUST NOT mutate hunks in place or invent new indices — it
|
|
150
|
+
* can only drop entries. Remaining hunks keep their original `index`
|
|
151
|
+
* values so the plan still references the authoritative collection.
|
|
152
|
+
*/
|
|
153
|
+
hunkFilter?: (hunks: ComposeHunk[]) => Promise<ComposeHunk[]> | ComposeHunk[];
|
|
154
|
+
/**
|
|
155
|
+
* Per-task prompt overrides, keyed by {@link ComposeTaskName}. Consumers with
|
|
156
|
+
* existing prompt template services (server-driven templates, provider-specific
|
|
157
|
+
* templates, org-tuned wording) use this to replace the library's default
|
|
158
|
+
* system prompt or decorate the user prompt without forking the task code.
|
|
159
|
+
*
|
|
160
|
+
* The library still runs the same validators against the model response, so a
|
|
161
|
+
* replacement `system` prompt has to preserve the output contract the task
|
|
162
|
+
* expects (JSON shape inside `<output>` tags, hunk index constraints, etc.).
|
|
163
|
+
*/
|
|
164
|
+
promptOverrides?: Partial<Record<ComposeTaskName, PromptOverride>>;
|
|
165
|
+
/**
|
|
166
|
+
* Resume a prior workflow instead of running every AI phase from scratch.
|
|
167
|
+
*
|
|
168
|
+
* When provided, each AI task checks its own entry in `completedSteps`. If
|
|
169
|
+
* present, the task returns that cached result instead of invoking the
|
|
170
|
+
* model. Unblocks recovery paths (e.g. user declined a ToS confirmation
|
|
171
|
+
* between `analyze` and `group` — retry with the same session, analyze is
|
|
172
|
+
* skipped, group runs fresh) and supports CLI / MCP consumers that want to
|
|
173
|
+
* build plans incrementally.
|
|
174
|
+
*
|
|
175
|
+
* Each task checks only its own entry in `completedSteps`; entries belonging
|
|
176
|
+
* to steps the current depth doesn't run are ignored. A resumed task
|
|
177
|
+
* contributes no usage and no callIds — telemetry for that phase reflects
|
|
178
|
+
* the original run, not this one.
|
|
179
|
+
*/
|
|
180
|
+
session?: ComposeSession;
|
|
181
|
+
/**
|
|
182
|
+
* GPG signing config for commits produced by the apply phase. Typically
|
|
183
|
+
* derived from `git config commit.gpgsign` + `user.signingkey`. When
|
|
184
|
+
* `enabled` is true, the library passes `-S` to `commit-tree` and sets
|
|
185
|
+
* `GIT_GPG_PROGRAM` env when `gpgProgram` is provided.
|
|
186
|
+
*/
|
|
187
|
+
signing?: SigningConfig;
|
|
188
|
+
/**
|
|
189
|
+
* How to pick the primary author for each commit when its hunks come from
|
|
190
|
+
* multiple original authors (recompose / commit-range sources).
|
|
191
|
+
*
|
|
192
|
+
* - `'plurality'` (default): the author who owns the most hunks in the
|
|
193
|
+
* commit wins; all other distinct authors become `Co-authored-by:` trailers.
|
|
194
|
+
* - `'first-hunk'`: use the first hunk's author; all other distinct authors
|
|
195
|
+
* become `Co-authored-by:` trailers.
|
|
196
|
+
* - function: fully custom — consumer inspects the hunks and returns the
|
|
197
|
+
* primary identity (or `undefined` to use the commit creator's identity).
|
|
198
|
+
* Co-authors are still derived from the union of hunk authors minus the
|
|
199
|
+
* returned primary.
|
|
200
|
+
*
|
|
201
|
+
* Ignored when no hunks have `author` set (e.g. workdir source).
|
|
202
|
+
*/
|
|
203
|
+
authorAttribution?: 'plurality' | 'first-hunk' | ((hunks: ComposeHunk[]) => {
|
|
204
|
+
name: string;
|
|
205
|
+
email: string;
|
|
206
|
+
} | undefined);
|
|
207
|
+
}
|
|
208
|
+
interface ComposePlan {
|
|
209
|
+
analysis?: AnalysisResult;
|
|
210
|
+
grouping: GroupingResult;
|
|
211
|
+
ordering: OrderingResult;
|
|
212
|
+
branches: ComposeBranchPlan[];
|
|
213
|
+
allOrderedCommits: CommitSuggestion[];
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* Discriminated union of phase events emitted during compose/split/apply.
|
|
217
|
+
*
|
|
218
|
+
* Every event carries:
|
|
219
|
+
* - `message`: a default user-friendly label (e.g. "Analyzing changes…").
|
|
220
|
+
* Consumers can use it directly or map to localized copy.
|
|
221
|
+
* - `weight`: a relative duration estimate. Useful for building a progress
|
|
222
|
+
* bar where phases have different costs. Weights sum to roughly 100 across
|
|
223
|
+
* a typical run; individual numbers are approximate and may be tuned over
|
|
224
|
+
* library releases. Treat as a hint, not a promise.
|
|
225
|
+
* - `detail`/`event`: phase-specific extra info (git state, AI attempt, etc.).
|
|
226
|
+
*/
|
|
227
|
+
type ComposeProgressEvent = {
|
|
228
|
+
phase: 'collecting';
|
|
229
|
+
message: string;
|
|
230
|
+
weight: number;
|
|
231
|
+
detail?: string;
|
|
232
|
+
} | {
|
|
233
|
+
phase: 'composing';
|
|
234
|
+
message: string;
|
|
235
|
+
weight: number;
|
|
236
|
+
event: ProgressEvent;
|
|
237
|
+
} | {
|
|
238
|
+
phase: 'compose-grouping';
|
|
239
|
+
message: string;
|
|
240
|
+
weight: number;
|
|
241
|
+
event: ProgressEvent;
|
|
242
|
+
} | {
|
|
243
|
+
phase: 'analyzing';
|
|
244
|
+
message: string;
|
|
245
|
+
weight: number;
|
|
246
|
+
event: ProgressEvent;
|
|
247
|
+
} | {
|
|
248
|
+
phase: 'grouping';
|
|
249
|
+
message: string;
|
|
250
|
+
weight: number;
|
|
251
|
+
event: ProgressEvent;
|
|
252
|
+
} | {
|
|
253
|
+
phase: 'ordering';
|
|
254
|
+
message: string;
|
|
255
|
+
weight: number;
|
|
256
|
+
event: ProgressEvent;
|
|
257
|
+
} | {
|
|
258
|
+
phase: 'partitioning';
|
|
259
|
+
message: string;
|
|
260
|
+
weight: number;
|
|
261
|
+
branch: string;
|
|
262
|
+
event: ProgressEvent;
|
|
263
|
+
} | {
|
|
264
|
+
phase: 'verifying';
|
|
265
|
+
message: string;
|
|
266
|
+
weight: number;
|
|
267
|
+
detail?: string;
|
|
268
|
+
} | {
|
|
269
|
+
phase: 'applying';
|
|
270
|
+
message: string;
|
|
271
|
+
weight: number;
|
|
272
|
+
detail?: string;
|
|
273
|
+
};
|
|
274
|
+
/** Default messages and weights per phase. Exposed so consumers can override for localization. */
|
|
275
|
+
declare const defaultPhaseMessages: Record<ComposeProgressEvent['phase'], {
|
|
276
|
+
message: string;
|
|
277
|
+
weight: number;
|
|
278
|
+
}>;
|
|
279
|
+
interface ComposeResult {
|
|
280
|
+
plan: ComposePlan;
|
|
281
|
+
/**
|
|
282
|
+
* Content-hash snapshot from the collection phase.
|
|
283
|
+
*
|
|
284
|
+
* Pass this back via {@link ComposeApplyPlan.snapshot} to apply the plan later — the
|
|
285
|
+
* apply path verifies the working directory still matches before re-running. Distinct
|
|
286
|
+
* from {@link undoId}, which is for rolling back.
|
|
287
|
+
*/
|
|
288
|
+
snapshot: SafetySnapshot;
|
|
289
|
+
partitions?: BranchPartition[];
|
|
290
|
+
/** Partitions grouped by independent stack. Each inner array is one stack (root first, tip last). */
|
|
291
|
+
partitionGroups?: BranchPartition[][];
|
|
292
|
+
session: ComposeSession;
|
|
293
|
+
usage: TokenUsage;
|
|
294
|
+
phases: PhaseDetail[];
|
|
295
|
+
source: {
|
|
296
|
+
type: 'workdir' | 'branch';
|
|
297
|
+
hunks: ComposeHunk[];
|
|
298
|
+
commitMessages?: string[];
|
|
299
|
+
branchName?: string;
|
|
300
|
+
baseBranch?: string;
|
|
301
|
+
};
|
|
302
|
+
/**
|
|
303
|
+
* Size of the input the AI operated on. Useful for consumers building
|
|
304
|
+
* telemetry events — no need to recount hunks and lines from the source.
|
|
305
|
+
*/
|
|
306
|
+
diffStats: {
|
|
307
|
+
fileCount: number;
|
|
308
|
+
hunkCount: number;
|
|
309
|
+
addedLines: number;
|
|
310
|
+
removedLines: number;
|
|
311
|
+
};
|
|
312
|
+
commitShas?: Record<string, string>;
|
|
313
|
+
/**
|
|
314
|
+
* Identifier for undoing this operation.
|
|
315
|
+
*
|
|
316
|
+
* Pass to {@link undoCompose} to reverse all git mutations this compose performed.
|
|
317
|
+
* Only set when changes were actually applied (not set when the user rejected
|
|
318
|
+
* the plan via `onBeforeApply`).
|
|
319
|
+
*/
|
|
320
|
+
undoId?: UndoId;
|
|
321
|
+
/**
|
|
322
|
+
* Call ID of the "decision" model response for this workflow — the last
|
|
323
|
+
* validated response across all AI phases. Consumers that surface user
|
|
324
|
+
* feedback (thumbs up/down) stamp this ID on the feedback event so it maps
|
|
325
|
+
* back to a specific prompt/response. Undefined when no AI phases ran
|
|
326
|
+
* (e.g. {@link applyComposePlan} without prior {@link composePlan}).
|
|
327
|
+
*/
|
|
328
|
+
feedbackTargetCallId?: string;
|
|
329
|
+
/**
|
|
330
|
+
* Number of hunks the consumer's `hunkFilter` removed before AI saw them.
|
|
331
|
+
* Undefined when no filter ran.
|
|
332
|
+
*/
|
|
333
|
+
hunksFilteredCount?: number;
|
|
334
|
+
}
|
|
335
|
+
/**
|
|
336
|
+
* Compose code changes into a clean set of commits and branches.
|
|
337
|
+
*
|
|
338
|
+
* Three pipelines depending on `depth`:
|
|
339
|
+
* - `quick` — single AI call, output order trusted (good for squashed diffs)
|
|
340
|
+
* - `balanced` (default) — compose-group + order, optional analysis included in the prompt
|
|
341
|
+
* - `deep` — separate analyze, group, order, partition steps with rich reasoning
|
|
342
|
+
*
|
|
343
|
+
* Returns a {@link ComposeResult} with an `undoId` — pass it to {@link undoCompose}
|
|
344
|
+
* to reverse all git mutations. The undo manifest is persisted in the git object
|
|
345
|
+
* database and survives process restarts.
|
|
346
|
+
*
|
|
347
|
+
* Caller is responsible for the `model` lifecycle. If `applyPlan` is provided, the AI
|
|
348
|
+
* phases are skipped and the previously computed plan is re-applied; the workdir
|
|
349
|
+
* snapshot in the plan is verified before applying.
|
|
350
|
+
*/
|
|
351
|
+
declare function compose(input: ComposeInput): Promise<ComposeResult>;
|
|
352
|
+
/**
|
|
353
|
+
* Run the AI pipeline without mutating the repository.
|
|
354
|
+
*
|
|
355
|
+
* Returns a plan + snapshot that can be reviewed, edited, and later handed to
|
|
356
|
+
* {@link applyComposePlan}. This is the two-phase entry point — ideal for
|
|
357
|
+
* consumers whose UX lets the user edit the plan before committing.
|
|
358
|
+
*
|
|
359
|
+
* For single-shot "plan and apply in one go", use {@link compose} directly.
|
|
360
|
+
*/
|
|
361
|
+
declare function composePlan(input: ComposePlanInput): Promise<ComposePlanResult>;
|
|
362
|
+
/**
|
|
363
|
+
* Apply a previously computed plan (from {@link composePlan}) to the repository.
|
|
364
|
+
*
|
|
365
|
+
* Verifies the snapshot against the current workdir before applying — if the
|
|
366
|
+
* subsets the plan touches have drifted, throws `WorkflowError('SAFETY_CHECK_FAILED')`.
|
|
367
|
+
* On success, returns an `undoId` that {@link undoCompose} can reverse.
|
|
368
|
+
*/
|
|
369
|
+
declare function applyComposePlan(input: ApplyComposePlanInput): Promise<ApplyComposePlanResult>;
|
|
370
|
+
//#endregion
|
|
371
|
+
export { ApplyComposePlanInput, ApplyComposePlanResult, ComposeApplyPlan, type ComposeBranchPlan, type ComposeContext, ComposeDepth, ComposeInput, ComposePlan, ComposePlanInput, ComposePlanResult, ComposeProgressEvent, ComposeResult, type ComposeSource, ComposeStrategy, type ComposeTarget, applyComposePlan, compose, composePlan, defaultPhaseMessages };
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{WorkflowError as e,WorkflowInputError as t}from"../errors/workflow.js";import{throwIfCancelled as n}from"../cancellation.js";import{GitError as r}from"../errors/git.js";import{addUsage as i,emptyUsage as a}from"../ai/types.js";import{analyze as o}from"../tasks/analyze/analyze.js";import{composeGroup as s}from"../tasks/compose-group/compose-group.js";import{group as c}from"../tasks/group/group.js";import{order as l}from"../tasks/order/order.js";import{partition as u}from"../tasks/partition/partition.js";import{applyPatchToIndex as d,diffTree as f,forEachRef as p,readTree as m,revParse as h,writeTree as g}from"../git/port-dispatch.js";import{buildPatchForCommit as _}from"../git/utils/patch-builder.js";import{sha256 as v}from"../git/utils/crypto.js";import{collectFromSource as y}from"../git/diff-collector.js";import{verifySnapshot as b}from"../git/safety.js";import{applyToTarget as x}from"../git/apply-target.js";import{requireExec as S}from"../git/require-exec.js";import{extractConventions as C}from"../git/utils/conventions.js";import{randomUUID as w}from"node:crypto";import{unlinkSync as T}from"node:fs";import{tmpdir as E}from"node:os";import{join as D}from"node:path";import{UndoStore as O,capturePreOperationState as k,generateUndoId as A}from"@gitkraken/shared-tools";const j={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 M(i){if(!i.applyPlan&&!i.model)throw new t(`Either 'model' or 'applyPlan' must be provided`);if(i.applyPlan)return B(i);let{git:a,model:o,source:s={type:`workdir`},onProgress:c,onBeforeApply:l,signal:u}=i;n(u);let d=o,f=i.depth??`balanced`,p=i.target??G(s);q(s,p);let m=[];c?.({phase:`collecting`,...j.collecting});let h=Date.now(),g=await y(a,s,{signal:u});if(m.push({name:`collecting`,durationMs:Date.now()-h,attempts:1}),g.hunks.length===0)throw new r(`NO_CHANGES`,K(s));let{hunks:_,filteredCount:v}=await U(g.hunks,i.hunkFilter);if(_.length===0)throw new r(`NO_CHANGES`,K(s));let w=v!==void 0&&v>0?await W(a,g.snapshot,_,u):g.snapshot,T=await N(f,{hunks:_,collection:g,conventions:i.conventions??await C({git:a,author:i.conventionAuthor,signal:u}),existingBranchNames:await Y(a,u),groupMode:p.type===`auto`?`branches`:`commits`,target:p,model:d,instructions:i.instructions,context:i.context,strategy:i.strategy,maxRetries:i.maxRetries,onProgress:c,signal:u,onBeforeModelCall:i.onBeforeModelCall,onAfterModelCall:i.onAfterModelCall,onBeforePrompt:i.onBeforePrompt,session:i.session,promptOverrides:i.promptOverrides}),E={analysis:T.analysis,grouping:T.grouping,ordering:T.ordering,branches:T.branchPlans,allOrderedCommits:T.allOrderedCommits};if(m.push(...T.phases),l&&!await l(E))return{plan:E,snapshot:w,session:T.session,usage:T.usage,phases:m,source:{type:s.type===`branch`?`branch`:`workdir`,hunks:_,commitMessages:g.commitMessages,branchName:g.branchName,baseBranch:g.baseBranch},diffStats:V(_),feedbackTargetCallId:H(m),hunksFilteredCount:v};let D=await k(S(a,`capturing pre-operation state`),{signal:u});c?.({phase:`verifying`,...j.verifying});let O=Date.now(),A=await b(a,g.snapshot,{includeUnstaged:s.type===`workdir`&&!s.stagedOnly,includeUntracked:g.untrackedIncluded,signal:u});if(!A.valid)throw new e(`SAFETY_CHECK_FAILED`,`Safety check failed: ${A.error}`);m.push({name:`verifying`,durationMs:Date.now()-O,attempts:1}),c?.({phase:`applying`,...j.applying});let M=Date.now(),P=await x({git:a,target:p,branchPlans:T.branchPlans,allOrderedCommits:T.allOrderedCommits,hunks:g.hunks,snapshot:w,collection:g,signal:u,signing:i.signing,authorAttribution:i.authorAttribution});m.push({name:`applying`,durationMs:Date.now()-M,attempts:1});let F=await X(a,D,P.mutations,p,s,u);return{plan:E,snapshot:w,partitions:P.partitions,partitionGroups:P.partitionGroups,session:T.session,usage:T.usage,phases:m,source:{type:s.type===`branch`?`branch`:`workdir`,hunks:_,commitMessages:g.commitMessages,branchName:g.branchName,baseBranch:g.baseBranch},diffStats:V(_),commitShas:P.commitShas,undoId:F,feedbackTargetCallId:H(m),hunksFilteredCount:v}}function N(e,t){switch(e){case`quick`:return P(t);case`balanced`:return F(t);case`deep`:return L(t)}}async function P(e){return I(e,{lightweight:!0,skipOrder:!0})}async function F(e){return I(e,{includeAnalysis:!0})}async function I(e,t){let{hunks:r,collection:o,conventions:c,existingBranchNames:d,groupMode:f,target:p,model:m,instructions:h,context:g,strategy:_,maxRetries:v,onProgress:y,signal:b,onBeforeModelCall:x,onAfterModelCall:S,onBeforePrompt:C}=e;n(b);let w=a(),T=[],E=await s({hunks:r,commitMessages:o.commitMessages,commitOrder:o.commitOrder,branchName:o.branchName,baseBranch:o.baseBranch,issueUrl:g?.issueUrl,pullRequestUrl:g?.pullRequestUrl,instructions:h,conventions:c,mode:f,strategy:_?.grouping,existingBranchNames:f===`branches`?d:void 0,includeAnalysis:t.includeAnalysis,lightweight:t.lightweight,condensed:t.condensed,model:m,session:e.session,maxRetries:v,onProgress:e=>y?.({phase:`compose-grouping`,...j[`compose-grouping`],event:e}),signal:b,onBeforeModelCall:x,onAfterModelCall:S,onBeforePrompt:C,promptOverride:e.promptOverrides?.[`compose-group`]});E.usage&&i(w,E.usage),T.push({name:`compose-grouping`,durationMs:E.durationMs,attempts:E.attempts,usage:E.usage,callIds:E.callIds});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 l({branches:E.result.branches,hunks:r,instructions:h,model:m,session:E.session,maxRetries:v,onProgress:e=>y?.({phase:`ordering`,...j.ordering,event:e}),signal:b,onBeforeModelCall:x,onAfterModelCall:S,onBeforePrompt:C,promptOverride:e.promptOverrides?.order});t.usage&&i(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(p.type===`auto`){let t=new Set(d);for(let e of E.result.branches)e.name&&t.add(e.name);for(let n of O){let r=await u({commits:J(n.branchGroup,n.orderedCommitIds),orderedCommitIds:n.orderedCommitIds,sourceBranchName:n.branchGroup.name,instructions:h,conventions:c,strategy:_?.partition,existingBranchNames:t,model:m,maxRetries:v,onProgress:e=>y?.({phase:`partitioning`,...j.partitioning,branch:n.branchGroup.name,event:e}),signal:b,onBeforeModelCall:x,onAfterModelCall:S,onBeforePrompt:C,promptOverride:e.promptOverrides?.partition});r.usage&&i(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=>J(e.branchGroup,e.orderedCommitIds));return{analysis:void 0,grouping:E.result,ordering:k,branchPlans:O,allOrderedCommits:A,session:D,usage:w,phases:T}}async function L(e){let{hunks:t,collection:r,conventions:s,existingBranchNames:d,groupMode:f,target:p,model:m,instructions:h,context:g,strategy:_,maxRetries:v,onProgress:y,signal:b,onBeforeModelCall:x,onAfterModelCall:S,onBeforePrompt:C}=e;n(b);let w=a(),T=[],E=await o({hunks:t,commitMessages:r.commitMessages,commitOrder:r.commitOrder,branchName:r.branchName,baseBranch:r.baseBranch,issueUrl:g?.issueUrl,pullRequestUrl:g?.pullRequestUrl,instructions:h,model:m,session:e.session,maxRetries:v,onProgress:e=>y?.({phase:`analyzing`,...j.analyzing,event:e}),signal:b,onBeforeModelCall:x,onAfterModelCall:S,onBeforePrompt:C,promptOverride:e.promptOverrides?.analyze});E.usage&&i(w,E.usage),T.push({name:`analyzing`,durationMs:E.durationMs,attempts:E.attempts,usage:E.usage,callIds:E.callIds});let D=await c({hunks:t,commitMessages:r.commitMessages,instructions:h,conventions:s,mode:f,strategy:_?.grouping,existingBranchNames:f===`branches`?d:void 0,model:m,session:E.session,maxRetries:v,onProgress:e=>y?.({phase:`grouping`,...j.grouping,event:e}),signal:b,onBeforeModelCall:x,onAfterModelCall:S,onBeforePrompt:C,promptOverride:e.promptOverrides?.group});D.usage&&i(w,D.usage),T.push({name:`grouping`,durationMs:D.durationMs,attempts:D.attempts,usage:D.usage,callIds:D.callIds});let O=await l({branches:D.result.branches,hunks:t,instructions:h,model:m,session:D.session,maxRetries:v,onProgress:e=>y?.({phase:`ordering`,...j.ordering,event:e}),signal:b,onBeforeModelCall:x,onAfterModelCall:S,onBeforePrompt:C,promptOverride:e.promptOverrides?.order});O.usage&&i(w,O.usage),T.push({name:`ordering`,durationMs:O.durationMs,attempts:O.attempts,usage:O.usage,callIds:O.callIds});let k=D.result.branches.map(e=>({branchGroup:e,orderedCommitIds:O.result.branches.find(t=>t.branchId===e.id)?.orderedCommitIds??e.commits.map(e=>e.id)})),A=O.session;if(p.type===`auto`){let t=new Set(d);for(let e of D.result.branches)e.name&&t.add(e.name);for(let n of k){let r=await u({commits:J(n.branchGroup,n.orderedCommitIds),orderedCommitIds:n.orderedCommitIds,sourceBranchName:n.branchGroup.name,instructions:h,conventions:s,strategy:_?.partition,existingBranchNames:t,model:m,maxRetries:v,onProgress:e=>y?.({phase:`partitioning`,...j.partitioning,branch:n.branchGroup.name,event:e}),signal:b,onBeforeModelCall:x,onAfterModelCall:S,onBeforePrompt:C,promptOverride:e.promptOverrides?.partition});r.usage&&i(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,A=r.session;for(let e of r.result.partitions)t.add(e.branchName)}}let M=k.flatMap(e=>J(e.branchGroup,e.orderedCommitIds));return{analysis:E.result,grouping:D.result,ordering:O.result,branchPlans:k,allOrderedCommits:M,session:A,usage:w,phases:T}}async function R(e){let t=await M({...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}}async function z(e){let t=await M({git:e.git,applyPlan:e.applyPlan,source:e.source,target:e.target,onProgress:e.onProgress,signal:e.signal,signing:e.signing,authorAttribution:e.authorAttribution});return{commitShas:t.commitShas,partitions:t.partitions,partitionGroups:t.partitionGroups,phases:t.phases,undoId:t.undoId}}async function B(t){let{git:i,applyPlan:o,onProgress:s,signal:c}=t;n(c);let{plan:l,snapshot:u,source:d}=o,f=t.source??d,p=t.target??o?.target??G(f);q(f,p);let m=await k(S(i,`capturing pre-operation state`),{signal:c}),h=[];s?.({phase:`collecting`,...j.collecting});let g=Date.now(),_=await y(i,f,{signal:c});if(h.push({name:`collecting`,durationMs:Date.now()-g,attempts:1}),_.hunks.length===0)throw new r(`NO_CHANGES`,K(f));if(_.snapshot.diffHash!==u.diffHash)throw new e(`SAFETY_CHECK_FAILED`,`Working directory has changed since the plan was created. Re-run compose to generate a new plan.`);s?.({phase:`verifying`,...j.verifying});let v=Date.now(),C=await b(i,_.snapshot,{includeUnstaged:f.type===`workdir`&&!f.stagedOnly,includeUntracked:_.untrackedIncluded,signal:c});if(!C.valid)throw new e(`SAFETY_CHECK_FAILED`,`Safety check failed: ${C.error}`);h.push({name:`verifying`,durationMs:Date.now()-v,attempts:1}),s?.({phase:`applying`,...j.applying});let w=Date.now(),T=await x({git:i,target:p,branchPlans:l.branches,allOrderedCommits:l.allOrderedCommits,hunks:_.hunks,snapshot:_.snapshot,collection:_,signal:c,signing:t.signing,authorAttribution:t.authorAttribution});h.push({name:`applying`,durationMs:Date.now()-w,attempts:1});let E=await X(i,m,T.mutations,p,f,c);return{plan:l,snapshot:_.snapshot,partitions:T.partitions,partitionGroups:T.partitionGroups,session:{messages:[],systemPrompt:``,completedSteps:{}},usage:a(),phases:h,source:{type:f.type===`branch`?`branch`:`workdir`,hunks:_.hunks,commitMessages:_.commitMessages,branchName:_.branchName,baseBranch:_.baseBranch},diffStats:V(_.hunks),commitShas:T.commitShas,undoId:E}}function V(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 H(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 U(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 W(e,t,n,r){let i=D(E(),`compose-filtered-idx-${w()}`),a={GIT_INDEX_FILE:i};try{await m(e,t.headSha,{indexEnv:a,signal:r});let i=_(n);i.length>0&&await d(e,i,{indexEnv:a,signal:r});let o=await g(e,{indexEnv:a,signal:r}),s=await f(e,t.headSha,o,{signal:r});return{...t,diffHash:v(s)}}finally{try{T(i)}catch{}}}function G(e){return e.type===`branch`?{type:`branch-replace`}:e.type===`commit-range`?{type:`rewrite-range`,branch:e.branch,rangeFrom:e.from,rangeTo:e.to}:{type:`head`}}function K(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 q(e,n){if(n.type===`branch-replace`&&e.type!==`branch`)throw new t(`target 'branch-replace' requires source type 'branch'`);if(n.type===`rewrite-range`&&e.type!==`commit-range`&&e.type!==`branch`)throw new t(`target 'rewrite-range' requires source type 'commit-range' or 'branch'`)}function J(t,n){return n.map(n=>{let r=t.commits.find(e=>e.id===n);if(!r)throw new e(`INTERNAL`,`Commit ${n} not found in branch '${t.id}'`);return r})}async function Y(e,t){let n=(await p(e,{format:`%(refname:short)`,prefix:`refs/heads/`,signal:t})).trim().split(`
|
|
2
|
+
`).filter(Boolean);return new Set(n)}async function X(e,t,n,r,i,a){let o=A(),s=n.createdBranches.length||1,c=`compose: ${s} branch${s===1?``:`es`} from ${i.type}`,l=[...n.movedBranches];if(r.type===`head`&&t.headBranch){let n=await h(e,`HEAD`,{signal:a});n!==t.headSha&&l.push({name:t.headBranch,previousSha:t.headSha,newSha:n})}let u={version:1,id:o,createdAt:new Date().toISOString(),description:c,previousHead:{sha:t.headSha,branch:t.headBranch},workdirClean:t.workdirClean,workdirBackupSha:t.workdirBackupSha,createdBranches:n.createdBranches,deletedBranches:n.deletedBranches,movedBranches:l};return await new O(S(e,`undo manifest persistence`)).save(u,{signal:a}),o}export{z as applyComposePlan,M as compose,R as composePlan,j as defaultPhaseMessages};
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { PhaseDetail, ProgressEvent, TokenUsage } from "../ai/types.js";
|
|
2
|
+
import { ComposeGitPort } from "../ports/git.js";
|
|
3
|
+
import { BranchPartition, CommitDetail, ComposeHunk } from "../tasks/types.js";
|
|
4
|
+
import { OnAfterModelCall, OnBeforeModelCall, OnBeforePrompt } from "../tasks/ai-loop.js";
|
|
5
|
+
import { AnalysisResult } from "../tasks/analyze/types.js";
|
|
6
|
+
import { ComposeSession } from "../tasks/session.js";
|
|
7
|
+
import { PartitionResult } from "../tasks/partition/types.js";
|
|
8
|
+
import { AiModelPort, UndoId } from "@gitkraken/shared-tools";
|
|
9
|
+
|
|
10
|
+
//#region src/workflows/split.d.ts
|
|
11
|
+
interface SplitInput {
|
|
12
|
+
git: ComposeGitPort;
|
|
13
|
+
model: AiModelPort;
|
|
14
|
+
branchName: string;
|
|
15
|
+
baseBranch?: string;
|
|
16
|
+
instructions?: string;
|
|
17
|
+
maxRetries?: number;
|
|
18
|
+
onProgress?: (event: SplitProgressEvent) => void;
|
|
19
|
+
onBeforeApply?: (plan: SplitPlan) => Promise<boolean>;
|
|
20
|
+
/**
|
|
21
|
+
* Optional cancellation signal. Forwarded to every AI call and every git operation.
|
|
22
|
+
* On abort, the workflow throws `WorkflowError('CANCELLED')`.
|
|
23
|
+
*/
|
|
24
|
+
signal?: AbortSignal;
|
|
25
|
+
onBeforeModelCall?: OnBeforeModelCall;
|
|
26
|
+
onAfterModelCall?: OnAfterModelCall;
|
|
27
|
+
onBeforePrompt?: OnBeforePrompt;
|
|
28
|
+
}
|
|
29
|
+
interface SplitPlan {
|
|
30
|
+
analysis: AnalysisResult;
|
|
31
|
+
partitioning: PartitionResult;
|
|
32
|
+
commits: CommitDetail[];
|
|
33
|
+
}
|
|
34
|
+
type SplitProgressEvent = {
|
|
35
|
+
phase: 'collecting';
|
|
36
|
+
detail?: string;
|
|
37
|
+
} | {
|
|
38
|
+
phase: 'analyzing';
|
|
39
|
+
event: ProgressEvent;
|
|
40
|
+
} | {
|
|
41
|
+
phase: 'partitioning';
|
|
42
|
+
event: ProgressEvent;
|
|
43
|
+
} | {
|
|
44
|
+
phase: 'applying';
|
|
45
|
+
detail?: string;
|
|
46
|
+
};
|
|
47
|
+
interface SplitResult {
|
|
48
|
+
plan: SplitPlan;
|
|
49
|
+
partitions: BranchPartition[];
|
|
50
|
+
session: ComposeSession;
|
|
51
|
+
usage: TokenUsage;
|
|
52
|
+
phases: PhaseDetail[];
|
|
53
|
+
source: {
|
|
54
|
+
type: 'branch';
|
|
55
|
+
hunks: ComposeHunk[];
|
|
56
|
+
commitMessages: string[];
|
|
57
|
+
branchName: string;
|
|
58
|
+
baseBranch: string;
|
|
59
|
+
};
|
|
60
|
+
/**
|
|
61
|
+
* Identifier for undoing this operation.
|
|
62
|
+
*
|
|
63
|
+
* Pass to {@link undoCompose} to reverse all git mutations this split performed.
|
|
64
|
+
* Only set when branches were actually created (not set when the user rejected
|
|
65
|
+
* the plan via `onBeforeApply` or there were too few partitions).
|
|
66
|
+
*/
|
|
67
|
+
undoId?: UndoId;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Split a multi-commit branch into a stack of smaller branches.
|
|
71
|
+
*
|
|
72
|
+
* Analyzes the branch's diff, partitions commits into independent branches, and
|
|
73
|
+
* (when there's more than one partition) creates the new branches as a stack rooted
|
|
74
|
+
* at `baseBranch`. The original branch is deleted and replaced by the partition branches.
|
|
75
|
+
*
|
|
76
|
+
* Returns an `undoId` — pass to `undoCompose` from this package to reverse the split.
|
|
77
|
+
*/
|
|
78
|
+
declare function split(input: SplitInput): Promise<SplitResult>;
|
|
79
|
+
//#endregion
|
|
80
|
+
export { SplitInput, SplitPlan, SplitProgressEvent, SplitResult, split };
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import{throwIfCancelled as e}from"../cancellation.js";import{GitError as t}from"../errors/git.js";import{addUsage as n,emptyUsage as r}from"../ai/types.js";import{analyze as i}from"../tasks/analyze/analyze.js";import{partition as a}from"../tasks/partition/partition.js";import{forEachRef as o}from"../git/port-dispatch.js";import{createStackedBranches as s}from"../git/apply-branches.js";import{parseUnifiedDiff as c}from"../git/utils/hunk-parser.js";import{collectCommitsForSplit as l}from"../git/diff-collector.js";import{requireExec as u}from"../git/require-exec.js";import{defaultPhaseMessages as d}from"./compose.js";import{UndoStore as f,capturePreOperationState as p,generateUndoId as m}from"@gitkraken/shared-tools";async function h(o){let{git:f,model:m,branchName:h,baseBranch:v=`main`,instructions:y,maxRetries:b,onProgress:x,onBeforeApply:S,signal:C,onBeforeModelCall:w,onAfterModelCall:T,onBeforePrompt:E}=o;e(C);let D=r(),O=[];x?.({phase:`collecting`,...d.collecting});let k=Date.now(),{commits:A,branchSha:j}=await l(f,h,v,C);if(O.push({name:`collecting`,durationMs:Date.now()-k,attempts:1}),A.length===0)throw new t(`NO_CHANGES`,`No commits found between '${v}' and '${h}'`);if(A.length<2)throw new t(`NO_CHANGES`,`Too few commits to split. A branch needs at least 2 commits.`);let M=c(A.map(e=>e.diff).join(`
|
|
2
|
+
`),`branch`),N=A.map(e=>({id:e.sha,message:e.message,explanation:``,hunkIndices:[]})),P=await i({hunks:M,commitMessages:A.map(e=>e.message),commitOrder:A.map(e=>e.sha),branchName:h,baseBranch:v,instructions:y,model:m,maxRetries:b,onProgress:e=>x?.({phase:`analyzing`,...d.analyzing,event:e}),signal:C,onBeforeModelCall:w,onAfterModelCall:T,onBeforePrompt:E});P.usage&&n(D,P.usage),O.push({name:`analyzing`,durationMs:P.durationMs,attempts:P.attempts,usage:P.usage,callIds:P.callIds});let F=await a({commits:N,orderedCommitIds:A.map(e=>e.sha),sourceBranchName:h,baseBranch:v,instructions:y,existingBranchNames:await g(f,C),model:m,session:P.session,maxRetries:b,onProgress:e=>x?.({phase:`partitioning`,...d.partitioning,event:e}),signal:C,onBeforeModelCall:w,onAfterModelCall:T,onBeforePrompt:E});F.usage&&n(D,F.usage),O.push({name:`partitioning`,durationMs:F.durationMs,attempts:F.attempts,usage:F.usage,callIds:F.callIds});let I={type:`branch`,hunks:M,commitMessages:A.map(e=>e.message),branchName:h,baseBranch:v},L={analysis:P.result,partitioning:F.result,commits:A};if(F.result.partitions.length<=1||S&&!await S(L))return{plan:L,partitions:F.result.partitions,session:F.session,usage:D,phases:O,source:I};let R=await p(u(f,`capturing pre-operation state`),{signal:C});x?.({phase:`applying`,...d.applying});let z=Date.now();await s({git:f,partitions:F.result.partitions,originalBranchName:h,originalBranchSha:j,signal:C}),O.push({name:`applying`,durationMs:Date.now()-z,attempts:1});let B=await _(f,R,F.result.partitions,h,j,C);return{plan:L,partitions:F.result.partitions,session:F.session,usage:D,phases:O,source:I,undoId:B}}async function g(e,t){let n=(await o(e,{format:`%(refname:short)`,prefix:`refs/heads/`,signal:t})).trim().split(`
|
|
3
|
+
`).filter(Boolean);return new Set(n)}async function _(e,t,n,r,i,a){let o=m(),s=n.map(e=>{let t=e.commitIds[e.commitIds.length-1];return{name:e.branchName,sha:t}}),c={version:1,id:o,createdAt:new Date().toISOString(),description:`split: ${n.length} branches from '${r}'`,previousHead:{sha:t.headSha,branch:t.headBranch},workdirClean:t.workdirClean,workdirBackupSha:t.workdirBackupSha,createdBranches:s,deletedBranches:[{name:r,sha:i}],movedBranches:[]};return await new f(u(e,`undo manifest persistence`)).save(c,{signal:a}),o}export{h as split};
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
//#region src/workflows/types.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Where compose pulls changes from.
|
|
4
|
+
*
|
|
5
|
+
* - `workdir`: collect from the working directory. Set `stagedOnly` to ignore unstaged changes,
|
|
6
|
+
* `noUntracked` to ignore untracked files.
|
|
7
|
+
* - `branch`: collect the diff between `name` and `mergeTarget` (defaults to the upstream/base branch).
|
|
8
|
+
* - `commit-range`: collect from a contiguous range of commits on a branch, with per-hunk author
|
|
9
|
+
* info derived from the owning commits. Used by "recompose this commit" / "recompose selected
|
|
10
|
+
* commits" flows. `from` is exclusive (parent of first commit), `to` is inclusive. For a
|
|
11
|
+
* single-commit recompose: `from = parent(commitSha)`, `to = commitSha`.
|
|
12
|
+
*/
|
|
13
|
+
type ComposeSource = {
|
|
14
|
+
type: 'workdir';
|
|
15
|
+
stagedOnly?: boolean;
|
|
16
|
+
noUntracked?: boolean;
|
|
17
|
+
} | {
|
|
18
|
+
type: 'branch';
|
|
19
|
+
name: string;
|
|
20
|
+
mergeTarget?: string;
|
|
21
|
+
} | {
|
|
22
|
+
type: 'commit-range';
|
|
23
|
+
branch: string;
|
|
24
|
+
from: string;
|
|
25
|
+
to: string;
|
|
26
|
+
};
|
|
27
|
+
/**
|
|
28
|
+
* Where compose writes the result.
|
|
29
|
+
*
|
|
30
|
+
* - `head`: commit onto the current branch.
|
|
31
|
+
* - `branch`: create or replace a single named branch.
|
|
32
|
+
* - `branch-replace`: replace the source branch (only valid when source is `branch`).
|
|
33
|
+
* - `auto`: create one branch per grouped concern, optionally subdividing into stacks.
|
|
34
|
+
* - `rewrite-range`: rewrite a contiguous commit range on `branch`, preserving commits
|
|
35
|
+
* before `rangeFrom` (always) and commits after `rangeTo` (by cherry-pick when
|
|
36
|
+
* `preserveSuffix`). Used with `source: 'commit-range'` for mid-branch recompose.
|
|
37
|
+
*/
|
|
38
|
+
type ComposeTarget = {
|
|
39
|
+
type: 'head';
|
|
40
|
+
} | {
|
|
41
|
+
type: 'branch';
|
|
42
|
+
name: string;
|
|
43
|
+
preserveOriginal?: boolean;
|
|
44
|
+
} | {
|
|
45
|
+
type: 'branch-replace';
|
|
46
|
+
} | {
|
|
47
|
+
type: 'auto';
|
|
48
|
+
preserveOriginal?: boolean;
|
|
49
|
+
} | {
|
|
50
|
+
type: 'rewrite-range';
|
|
51
|
+
branch: string;
|
|
52
|
+
rangeFrom: string;
|
|
53
|
+
rangeTo: string; /** Commits before `rangeFrom` always stay; this flag is reserved for future semantics. Default true. */
|
|
54
|
+
preservePrefix?: boolean; /** When true (default), cherry-pick `rangeTo..branch-tip` onto the new range tip. */
|
|
55
|
+
preserveSuffix?: boolean;
|
|
56
|
+
};
|
|
57
|
+
/** Optional context passed through to AI prompts (issue/PR URLs surface in commit messages). */
|
|
58
|
+
interface ComposeContext {
|
|
59
|
+
issueUrl?: string;
|
|
60
|
+
pullRequestUrl?: string;
|
|
61
|
+
}
|
|
62
|
+
//#endregion
|
|
63
|
+
export { ComposeContext, ComposeSource, ComposeTarget };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export{};
|