npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@gitkraken/compose-tools

v0.4.0

Published

AI-powered organization of git changes into clean, atomic commits and branches.

Readme

@gitkraken/compose-tools

AI-powered git change organization. Takes a messy working directory or a branch full of interleaved commits and restructures them into clean, atomic commits on well-named branches — ready for code review.

What it does

compose() collects your changes, sends the diffs through an AI pipeline that understands the logical structure of code changes, and produces clean commits grouped by concern. Depending on configuration, it can:

  • Rewrite a working directory's staged / unstaged / untracked changes into atomic commits on the current branch
  • Reorganize a branch's commit history into logically grouped commits
  • Rewrite a contiguous range of commits in the middle of a branch, preserving the commits before and after the range
  • Split changes across multiple new branches (one per concern)
  • Subdivide branches into stacked sub-branches for incremental review

split() is a focused variant — it takes an existing multi-commit branch and partitions it into a stack of smaller branches.

Every operation that mutates the repository returns an undoId that can be passed to undoCompose() to surgically reverse only the git mutations that were recorded.

Install

pnpm add @gitkraken/compose-tools @gitkraken/shared-tools

The library is runtime-agnostic — it never executes git or AI calls directly. Consumers provide adapters for their own git and AI backends.

Implement the ports

ComposeGitPort

The git surface the library needs. Two ways to provide it:

Minimal — raw exec only. Fine for a CLI-git consumer.

import type { ComposeGitPort } from '@gitkraken/compose-tools';

const git: ComposeGitPort = {
    exec: async (args, options) => {
        const result = await yourGitBackend.run(args, {
            cwd: '/path/to/repo',
            env: options?.env,
            stdin: options?.stdin,
            signal: options?.signal,
        });
        if (result.exitCode !== 0) throw new Error(`git ${args[0]} failed: ${result.stderr}`);
        return result.stdout;
    },
};

The library builds every git operation it needs (diff, write-tree, commit-tree, update-ref, stash, for-each-ref, etc.) from exec.

Hybrid — exec plus targeted high-level ops. Provide a high-level op when your backend has a better implementation. The library prefers the high-level op over routing through exec.

const git: ComposeGitPort = {
    exec: async (args, options) => { /* ... */ },

    // Consumer has a signing-aware commit creator; library should prefer it.
    commitTree: async (tree, parents, message, opts) => {
        return yourSigningCommitCreator({ tree, parents, message, signing: opts?.signing });
    },
};

Libgit2-style — high-level ops only, no exec. A libgit2-backed consumer implements the specific ops used by its flow. If a flow needs an op the consumer didn't provide and exec isn't available either, the library throws ComposeGitPortMissingOpError naming the missing op.

The full list of high-level ops is in the ComposeGitOps type; the most commonly overridden are commitTree, readTree, writeTree, applyPatchToIndex, applyPatchToWorkdir, diffTree, stageAll. OpOptions is currently empty — git operations are not cancellable in this library; they always run to completion to avoid leaving the repo half-mutated. Cancellation flows only into the AI loop (see cancellation below).

stashPop accepts a StashPopOptions argument with withIndex?: boolean (maps to git stash pop --index); ops-only consumers should honor withIndex to preserve the user's pre-stash staging after the apply step's safety stash is popped.

AiModelPort

Stateless text generation — the caller passes the full message history each call.

import type { AiModelPort } from '@gitkraken/shared-tools';

const model: AiModelPort = {
    generate: async (params) => {
        const response = await yourLLM.chat({
            system: params.system,
            messages: params.messages,
            maxTokens: params.maxTokens,
            temperature: params.temperature,
            signal: params.signal,
        });
        return {
            text: response.text,
            usage: { inputTokens: response.inputTokens, outputTokens: response.outputTokens },
        };
    },
};

The adapter doesn't track sessions — session state is managed inside the library.

Entry points

The library exposes three flavors of the main workflow plus split and undo.

compose(input) — one-call sugar

Runs planning and apply together. Ideal when the consumer doesn't need to surface the plan for review.

import { compose } from '@gitkraken/compose-tools';

const result = await compose({
    git,
    model,
    source: { type: 'workdir' },
    onProgress: (event) => console.log(event.phase, event.message),
});

// result.plan                  — AI's analysis, grouping, ordering
// result.commitShas            — { 'commit-1': 'abc123…', … }
// result.undoId                — pass to undoCompose() to reverse
// result.phases                — timing/usage/callIds per phase
// result.usage                 — aggregated token counts
// result.feedbackTargetCallId  — stable ID of the "decision" model call (for thumbs up/down events)

composePlan(input) + applyComposePlan(input) — two-phase

Returns the plan without mutating the repo, lets the user review/edit it, then applies. This is the shape interactive review UIs use (plan → review surface → user edits → apply).

import { composePlan, applyComposePlan } from '@gitkraken/compose-tools';

const planResult = await composePlan({
    git,
    model,
    source: { type: 'workdir' },
});

// Show planResult.plan + planResult.source.hunks to the user. They may
// reassign hunks between commits, reorder, edit messages, etc.

const userEditedPlan = /* user-edited version of planResult.plan */;

const applied = await applyComposePlan({
    git,
    applyPlan: {
        plan: userEditedPlan,
        source: planResult.source,     // the library's collected hunks
        snapshot: planResult.snapshot, // safety hash — verified at apply time
    },
});

applyComposePlan reverifies the workdir matches snapshot before mutating. If it drifted between plan and apply, it throws ComposeWorkflowError('SAFETY_CHECK_FAILED').

split(input)

Partitions an existing branch into a stack of smaller branches.

import { split } from '@gitkraken/compose-tools';

const result = await split({
    git,
    model,
    branchName: 'feature/large-pr',
    baseBranch: 'main',
    onBeforeApply: async (plan) => {
        for (const p of plan.partitioning.partitions) console.log(`${p.branchName}: ${p.title}`);
        return true;
    },
});

Requires at least 2 commits on the branch. The original branch is replaced by the partition branches.

undoCompose(input)

See Undo below.

Sources

| Source | Description | |---|---| | { type: 'workdir', stagedOnly?, noUntracked? } | Collects staged, unstaged, and untracked changes. stagedOnly: true limits to the index; noUntracked: true skips untracked files. | | { type: 'branch', name, mergeTarget? } | Collects mergeBase(mergeTarget, name) → name as one combined diff. mergeTarget defaults to 'main'. | | { type: 'commit-range', branch, from, to, includeWorkdir? } | Collects a contiguous range on branch as a single combined diff. Both from and to are inclusive — they name the first and last commits being rewritten. For a single-commit recompose: from = sha, to = sha. Per-hunk authorship is attributed via a line-range-overlap heuristic against per-commit history. Pass includeWorkdir: { includeStaged?, includeUnstaged?, includeUntracked? } to combine the range with the user's working changes in one composed input. When set, to must equal the current branch tip AND HEAD must be non-detached. |

Targets

| Target | Description | |---|---| | { type: 'head' } | Commit onto the current branch. Default for workdir source. | | { type: 'branch', name, preserveOriginal? } | Create a single named branch. preserveOriginal: true keeps the source branch / WIP; false cleans it up (deletes the source branch ref, or clears the committed WIP from the worktree). | | { type: 'auto', preserveOriginal? } | Create one branch per concern, optionally subdividing into stacked sub-branches. Same preserveOriginal semantics as branch. Default for branch/commit-range sources. | | { type: 'rewrite-range', branch, rangeFrom, rangeTo } | Replace a contiguous range on branch in-place. The new chain stacks on parent(rangeFrom). Refs whose tip is a strict descendant of rangeTo get their downstream chains re-parented onto the new tip via commit-tree; refs at rangeTo itself move with the source branch. |

Source/target pairing matrix

| source | head | branch | auto | rewrite-range | | -------------- | :--: | :----: | :--: | :-----------: | | workdir | ✓ | ✓ | ✓ | ✗ | | branch | ✗ | ✓ | ✓ | ✓ | | commit-range | ✗ | ✓ | ✓ | ✓ |

Invalid pairings throw ComposeWorkflowInputError at composePlan time.

Options

depth

Controls how many AI calls run and how deeply each reasons.

| Depth | Phases | Best for | |---|---|---| | 'quick' | Single compose-group call (lightweight prompt) | Small changes, fast feedback | | 'balanced' (default) | compose-group + optional order | Most use cases | | 'deep' | analyze + group + order + optional partition per branch | Large, complex changesets |

strategy

Controls AI aggressiveness at grouping and partitioning time. All settings default to 'balanced'.

strategy: {
    grouping: {
        branchSplitting: 'conservative' | 'balanced' | 'aggressive',
        commitGranularity: 'consolidated' | 'balanced' | 'granular',
    },
    partition: {
        splitting: 'conservative' | 'balanced' | 'aggressive',
    },
}
  • branchSplitting — how aggressively to separate changes into distinct branches
  • commitGranularity — how fine-grained individual commits should be
  • partition.splitting — how aggressively to subdivide branches into stacked sub-branches

onBeforeApply

Inspect the plan before any git mutations. Return false to abort — the workflow returns the plan without applying it.

onBeforeApply: async (plan) => {
    console.log(`${plan.branches.length} branches, ${plan.allOrderedCommits.length} commits`);
    return true;
}

Equivalent to calling composePlan — useful when you want the sugar compose path but need a chance to intervene.

onBeforeModelCall / onAfterModelCall

Per-call observability hooks. Informational only — the library doesn't interpret the return.

onBeforeModelCall: ({ phase, attempt, maxAttempts, callId }) => {
    // phase: 'analyze' | 'group' | 'order' | 'partition' | 'compose-group' | 'quick-compose'
    // callId: stable UUID — correlates with onAfterModelCall and with result.feedbackTargetCallId
},

onAfterModelCall: ({ phase, attempt, callId, success, durationMs, usage, error, promptVersion }) => {
    // Fires on both success and error
},

Consumers build their own per-call telemetry from these. No built-in telemetry is emitted by the library.

onBeforePrompt

Gating hook — run immediately before each model.generate. Returning false aborts the workflow with ComposeWorkflowError('CANCELLED'). Use for large-prompt confirmations, cost warnings, org policy checks.

onBeforePrompt: async ({ phase, attempt, tokenEstimate, charCount, messageCount }) => {
    if (tokenEstimate > 20_000) {
        return await showUserConfirmation(`Large prompt (${tokenEstimate} tokens). Continue?`);
    }
    return true;
}

hunkFilter

Runs after source collection and before the AI pipeline. Drops hunks the consumer doesn't want the AI to see. Used for AIIgnore globs, org policy filters, etc.

hunkFilter: async (hunks) => hunks.filter(h => !isAiIgnored(h.fileName)),

The filter must preserve the original index values on remaining hunks (only drop entries, never renumber). The library rebuilds the safety snapshot to reflect just the filtered subset, so verifyResultingDiff still has a correct expectation. result.hunksFilteredCount tells the consumer how many were dropped.

hunkFilter and the two-phase composePlan / applyComposePlan flow: applyComposePlan accepts the same hunkFilter field. You must pass an equivalent filter at apply time — the library re-applies it to the freshly collected hunks before the drift check, otherwise the cached (filtered) snapshot's diff hash won't match the (unfiltered) fresh diff. For consumers that serialize the plan across processes (e.g. CLI save-to-JSON workflows), record the filter spec — the list of excluded file names, the AIIgnore glob set, etc. — alongside the plan and reconstruct the function on apply. The filter function itself isn't serializable; its inputs typically are.

redactHunkContent

Predicate that returns true for hunks whose patch body should be masked from AI prompts. Matching hunks keep their fileName, hunkHeader, index, line counts, and rename metadata — only the body is replaced with a placeholder. The apply path still uses the real content from the collection.

const aiExcluded = new Set(['secrets.env', 'config/keys.json']);
redactHunkContent: (h) => aiExcluded.has(h.fileName) || aiExcluded.has(h.originalFileName ?? h.fileName),

Intended for .aiexclude / .aiignore / .cursorignore-style features where the AI should group a hunk into a commit by filename alone without ever seeing its content. Distinct from hunkFilter: filtered hunks are dropped from the plan entirely (and never end up in any commit), while redacted hunks stay in the plan and get applied normally — only the AI prompts hide their content.

Apply paths don't observe redactHunkContent (no AI runs at apply time), so it isn't required on applyComposePlan inputs. Per-task inputs (analyze, group, compose-group, quick-compose) accept it directly when running tasks à la carte. The order and partition tasks never embed hunk content, so the predicate is a no-op there.

signing

GPG signing for produced commits. Typically derived from git config commit.gpgsign + user.signingkey.

signing: {
    enabled: true,
    signingKey: 'ABCD1234',     // optional — passed to commit-tree as -S<key>
    gpgProgram: '/usr/bin/gpg', // optional — sets GIT_GPG_PROGRAM env
}

authorAttribution

How the primary author is picked when a commit's hunks come from multiple original authors (recompose / commit-range sources). Ignored when no hunks carry author info (e.g. workdir source).

| Value | Behavior | |---|---| | 'plurality' (default) | The author who owns the most hunks wins; all other distinct authors become Co-authored-by: trailers | | 'first-hunk' | Use the first hunk's author; other distinct authors become Co-authored-by: trailers | | function | Fully custom — inspect the hunks and return the primary identity (or undefined to use the commit creator's identity). Co-authors are still derived from the union of remaining authors |

cancellation

Standard AbortSignal, but with a deliberately narrow scope: it is forwarded only into the AI loop — every model.generate call and the prompt-gate hook. Git operations (collection, snapshot verification, applying, undo manifest persistence) run to completion regardless of whether cancellation was aborted; mid-flight cancellation of a git op risks a half-mutated repo, so by design only the AI loop is cancellable.

On AI-loop abort, the workflow translates the native AbortError into ComposeWorkflowError('CANCELLED') so consumers only have to check for the workflow-domain shape.

const controller = new AbortController();
const result = await compose({ git, model, cancellation: controller.signal });
// later: controller.abort() — interrupts the next AI call; any git op already in
// flight finishes normally, then the workflow throws ComposeWorkflowError('CANCELLED').

applyComposePlan, undoCompose, validateUndoCompose, and listUndoIds do not accept cancellation — they are git-only and run to completion.

applyCommitIds

Apply only the listed plan commit ids. Hunks belonging to commits not in the list — plus any hunks dropped by hunkFilter — become uncommitted leftover in the worktree (modified for tracked files, untracked for new files; index state is not preserved when the source included WIP).

applyCommitIds: undefined                     // default — apply the entire plan
applyCommitIds: ['commit-1', 'commit-3']      // apply only these two commits, in plan order

The list is interpreted as a set: order doesn't matter (the plan's own order is preserved), but every id must appear in plan.allOrderedCommits — unknown ids throw ComposeWorkflowInputError.

When applyCommitIds (or hunkFilter) is set and the target rewrites existing commits (rewrite-range, or branch/auto with preserveOriginal: false) on a branch/commit-range source, the library additionally requires HEAD to be on a branch (non-detached) and pointing at the source's tip (branch tip or range to). The leftover patch is materialized on top of the rewritten branch as unstaged WIP. Other source/target combinations impose no HEAD constraint.

The result includes appliedCommitIds (the SHAs actually written) and unappliedHunkIndices (the hunk indices the partial apply left behind).

stashLabel

Custom message for the safety stash the apply step creates when the worktree is dirty. Defaults to compose backup.

stashLabel: `myapp-compose-${new Date().toISOString()}`

The apply step always stashes pending workdir state via git stash push --include-untracked before mutating, and pops it back after. If the post-apply pop hits conflicts, the result includes stashConflict: { stashLabel, conflictedFiles } — apply itself succeeded, the named stash is preserved, and the consumer surfaces recovery UX (typical pattern: scan git stash list for a stable prefix on next mode-entry to detect abandoned stashes after process kills).

maxTokens

Output-token cap forwarded to every model.generate call in the pipeline. Defaults to runAiLoop's 16384.

Bump it on very large diffs whose structured output (e.g. analyze's per-hunk annotations) overflows the cap mid-JSON. When that happens, the validator surfaces a JSON-parse failure with a Response tail: … preview of the last 300 characters of the model's output, so it's obvious the response was truncated rather than malformed.

const result = await compose({ git, model, maxTokens: 32_000 });

maxTokens is also accepted on every individual task (analyze, group, compose-group, order, partition, quick-compose) for the same reason.

session (resumable workflows)

Pass a ComposeSession from a prior run to skip already-completed AI phases. Each task checks its own entry in completedSteps; if present, the task returns the cached result instead of invoking the model.

Useful when a user ToS-rejected between phases, when a CLI wants to build plans incrementally, or when recovering from a transient failure without redoing the whole pipeline.

// First attempt — aborted between analyze and group
const first = await composePlan({ git, model, depth: 'deep', ... });

// Retry with the session — analyze is skipped, group/order/partition run fresh
const second = await composePlan({ git, model, depth: 'deep', session: first.session, ... });

A resumed task contributes no usage and no callIds — telemetry for that phase reflects the original run.

promptOverrides

Per-task prompt overrides for consumers with their own prompt template services (server-driven templates, provider-specific templates, org-tuned wording).

import type { ComposeTaskName, PromptOverride } from '@gitkraken/compose-tools';

promptOverrides: {
    group: {
        system: myCustomGroupSystemPrompt,      // replaces the built-in system prompt entirely
        userPrefix: 'Additional context:\n...', // prepended to the user prompt
        userSuffix: '\nReminder: prefer short subjects.', // appended
    },
    order: {
        userSuffix: myOrderingHints,
    },
}

Task names: 'analyze' | 'group' | 'compose-group' | 'order' | 'partition' | 'quick-compose'.

The library still runs the same validators against the model response, so a replacement system prompt must preserve the output contract the task expects (JSON shape inside <output> tags, hunk index constraints, etc.).

context

Optional issue / PR URLs surfaced in commit messages.

context: { issueUrl: 'https://github.com/org/repo/issues/42', pullRequestUrl: '...' }

instructions

Free-form natural-language guidance passed to the AI.

instructions: 'Separate the refactoring from the feature work. Put all test changes in their own commit.'

maxRetries

Per-task retry count when the AI output fails validation. Default 3.

conventions / conventionAuthor

Override auto-detected commit message conventions. By default the library samples recent commits from the repo. Set conventionAuthor to sample commits from a specific author only (handy when the current user's style should drive it).

Result metadata

Every top-level entry point returns an object with these common fields:

| Field | Set on | Meaning | |---|---|---| | plan | all paths | The AI's full analysis / grouping / ordering / branches | | snapshot | all paths | Safety hash fingerprint of the collection — carry it to apply time | | source | all paths | { type, hunks, commitMessages?, branchName?, baseBranch? } — the hunks the AI operated on | | session | all paths | Pass to a subsequent call to resume | | phases | all paths | PhaseDetail[] with { name, durationMs, attempts, usage?, modelId?, promptVersion?, callIds? } per phase | | usage | all paths | Aggregated { inputTokens, outputTokens, ... } across all AI calls | | diffStats | sugar / plan | { fileCount, hunkCount, addedLines, removedLines } | | feedbackTargetCallId | when AI ran | callId of the last validated model response — stamp this on thumbs up/down feedback events | | hunksFilteredCount | when hunkFilter ran | Number of hunks the filter dropped | | commitShas | apply paths | { commitId: sha } mapping for every commit written | | undoId | apply paths | Pass to undoCompose() to reverse | | partitions / partitionGroups | apply paths with partitioning | Applied partition info | | appliedCommitIds | apply paths | Plan commit IDs actually written. Same as plan.allOrderedCommits.map(c => c.id) for full apply; a subset when applyCommitIds is set. | | unappliedHunkIndices | apply paths | Hunk indices from the plan not covered by appliedCommitIds. Empty for full apply. | | stashConflict | apply paths | { stashLabel, conflictedFiles } set when the post-apply stash pop hit conflicts. Apply itself succeeded; the named stash is preserved for the consumer's recovery UX. |

Undo

Every operation that mutates the repository returns an undoId. The undo system is surgical — it reverses only the git mutations that were recorded, rather than restoring a full repository snapshot.

Undo an operation

import { undoCompose } from '@gitkraken/compose-tools';

const undoResult = await undoCompose({
    git,
    undoId: result.undoId,
    onProgress: (step) => console.log(step),
});

// undoResult.deletedBranches   — branches removed (reversed a creation)
// undoResult.recreatedBranches — branches restored (reversed a deletion)
// undoResult.restoredBranches  — branches moved back to their pre-operation SHA
// undoResult.restoredHead      — whether HEAD was restored
// undoResult.restoredWorkdir   — whether the working directory was restored
// undoResult.warnings          — non-fatal issues encountered

Safety checks

The undo system validates safety before executing. It refuses to proceed when:

  • Dirty working directory — uncommitted changes would be at risk. Never force-able.
  • Branch has new commits — a branch created by the operation has been modified since. Force-able with force: true.
  • HEAD SHA unreachable — the pre-operation HEAD commit is no longer reachable (e.g. after garbage collection).
  • Manifest not found — the undo ID doesn't correspond to a stored manifest.

Dry-run validation

import { validateUndoCompose } from '@gitkraken/compose-tools';

const validation = await validateUndoCompose({ git, undoId: result.undoId });

if (!validation.safe) {
    for (const blocker of validation.blockers) console.error(`Blocked: ${blocker.type} — ${blocker.message}`);
}
for (const warning of validation.warnings) console.warn(`Warning: ${warning.type} — ${warning.message}`);

Force undo

await undoCompose({ git, undoId, force: true });

Downgrades the "branch has new commits" blocker to a warning. Equivalent to force: { branchHasNewCommits: true }.

For finer control, pass an object form:

await undoCompose({
    git,
    undoId,
    force: { branchHasNewCommits: true, dirtyWorkdir: true },
});

dirtyWorkdir: true allows undo to proceed despite uncommitted changes — the HEAD restore step does a reset --hard, so anything in the worktree is clobbered. Use only when the dirty state is a known artifact of the operation being undone (e.g. a failed stash pop after a successful apply where the consumer chose to roll back) and the caller has confirmed work loss with the user. head_sha_unreachable and manifest_not_found are never force-able regardless of flags.

List all undo IDs

import { listUndoIds } from '@gitkraken/compose-tools';

const ids = await listUndoIds(git);

Manifests are persisted as git objects under refs/undo/<id> — they survive branch operations and are cleaned up automatically when an undo is executed.

Individual task functions

The AI pipeline is built from composable task functions. Call them individually for custom workflows.

| Function | Purpose | |---|---| | analyze(input) | Identify logical concerns, dependencies, and ordering constraints in a set of hunks | | group(input) | Group hunks into branches and commits | | order(input) | Determine optimal commit ordering within each branch | | partition(input) | Subdivide a branch into stacked sub-branches | | composeGroup(input) | Combined analyze + group in a single AI call (used by balanced / quick depths) | | quickCompose(input) | Full pipeline (analyze + group + order + optional partition) in a single AI call |

Each task accepts an optional session to maintain AI conversation context (or to resume from cached completedSteps), and returns an updated session in its output. Each task also accepts promptOverride, maxTokens, onBeforeModelCall, onAfterModelCall, onBeforePrompt, and cancellation for the same behaviors as the top-level entry points.

Error handling

| Error | Codes | |---|---| | GitError | NO_CHANGES, BRANCH_NOT_FOUND, OPERATION_FAILED, SAFETY_CHECK_FAILED | | ComposeWorkflowError | SAFETY_CHECK_FAILED (workdir changed between plan and apply), INTERNAL, CANCELLED | | ComposeWorkflowInputError | Invalid workflow input (missing model, incompatible source/target combination, etc.) | | TaskInputError | Invalid task input | | AIError | AI output failed schema validation after all retries. Code: VALIDATION_EXHAUSTED | | ComposeGitPortMissingOpError | A flow needs a ComposeGitOps method the consumer didn't provide and exec isn't available either. Has .op (missing op name) and .hint. |

import { compose, GitError, ComposeWorkflowError, AIError } from '@gitkraken/compose-tools';

try {
    await compose({ git, model });
} catch (error) {
    if (error instanceof GitError && error.code === 'NO_CHANGES') {
        console.log('Nothing to compose');
    } else if (error instanceof AIError) {
        console.error('AI could not produce valid output after retries');
    } else if (error instanceof ComposeWorkflowError && error.code === 'CANCELLED') {
        console.log('Operation cancelled');
    } else if (error instanceof ComposeWorkflowError) {
        console.error(`Workflow failed: ${error.code} — ${error.message}`);
    }
}

Progress tracking

Every top-level entry point accepts an onProgress callback that receives a discriminated ComposeProgressEvent. Each event carries a default message, a relative weight (for progress bar math), and a phase-specific payload.

import { compose, defaultPhaseMessages } from '@gitkraken/compose-tools';

const result = await compose({
    git,
    model,
    onProgress: (event) => {
        // event.phase:   'collecting' | 'compose-grouping' | 'analyzing' | 'grouping' |
        //                'ordering' | 'partitioning' | 'verifying' | 'applying'
        // event.message: default English label — use directly or map to localized copy
        // event.weight:  approximate relative duration for progress math
        // event.event:   ProgressEvent with { type, attempt, maxAttempts, errors? } (AI phases)
        // event.detail:  phase-specific string (non-AI phases)
        // event.branch:  only for 'partitioning', names the branch being subdivided
        // event.stats:   only for 'collecting'; set on the post-collection emit
        //                with { fileCount, hunkCount, addedLines, removedLines }
    },
});

// defaultPhaseMessages is exported for consumers who want to remap phase labels
// for localization without losing the weights.

After completion, result.phases has timing and usage per phase:

for (const phase of result.phases) {
    console.log(`${phase.name}: ${phase.durationMs}ms, ${phase.attempts} attempts`);
    if (phase.usage) console.log(`  ${phase.usage.inputTokens} in, ${phase.usage.outputTokens} out`);
    if (phase.callIds) console.log(`  call IDs: ${phase.callIds.join(', ')}`);
}

console.log(`Total tokens: ${result.usage.inputTokens} in, ${result.usage.outputTokens} out`);

Contributing

This package follows the monorepo's standard tooling — see the root CONTRIBUTING.md. Local commands:

pnpm --filter @gitkraken/compose-tools build
pnpm --filter @gitkraken/compose-tools typecheck
pnpm --filter @gitkraken/compose-tools test