git-jev-stage 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,11 +1,14 @@
1
1
  {
2
2
  "name": "git-jev-stage",
3
- "owner": { "name": "Ibrahem" },
3
+ "owner": {
4
+ "name": "Ibrahem"
5
+ },
6
+ "description": "Select Git changes for staging with a plain-language description.",
4
7
  "plugins": [
5
8
  {
6
9
  "name": "git-jev-stage",
7
10
  "source": "./",
8
- "description": "Stage the diff hunks that match a sentence."
11
+ "description": "Select Git changes for staging with a plain-language description."
9
12
  }
10
13
  ]
11
14
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "git-jev-stage",
3
- "description": "Stage the diff hunks that match a sentence.",
4
- "version": "0.1.0",
3
+ "description": "Select Git changes for staging with a plain-language description.",
4
+ "version": "0.1.1",
5
5
  "author": {
6
6
  "name": "Ibrahem"
7
7
  },
package/README.md CHANGED
@@ -1,14 +1,27 @@
1
1
  # git-jev-stage
2
2
 
3
- Stage the hunks that match a sentence.
3
+ Describe the change to stage in one sentence. git-jev-stage classifies each block of changed lines (a Git hunk), shows the plan, and stages the selected blocks after confirmation. It only stages. Working files stay as they are, and the commit and its message are yours to write.
4
4
 
5
5
  ```sh
6
6
  git jev-stage "only the auth fix and its tests"
7
7
  ```
8
8
 
9
- ![demo](docs/demo.gif)
9
+ ## Install
10
+
11
+ ```sh
12
+ npm install -g git-jev-stage
13
+ export TYPESAFE_API_KEY=...
14
+ ```
15
+
16
+ Git 2.30 or newer, Node 22 or newer. Run it inside a Git repository with at least one commit.
17
+
18
+ Automatic selection uses [Jev](https://typesafe.ai), TypeSafe's decision model, and needs an API key from the early-access waitlist at [typesafe.ai](https://typesafe.ai). The key is also read from the nearest `.env` file between the current directory and the repository root. Without a key, an interactive run asks about every hunk, `--yes` and `--json` exit with `missing-api-key`, and `--dry-run` marks every hunk as `mixed` and prints no patch.
10
19
 
11
- A working tree holds an auth fix, a CSS tweak and a `console.log` left over from debugging. One sentence stages the fix and its test; the rest stays unstaged.
20
+ ## The command
21
+
22
+ ![Auth changes staged; CSS and a debug log left unstaged.](docs/demo.gif)
23
+
24
+ A working tree contains an auth fix, a CSS tweak, and a leftover `console.log`.
12
25
 
13
26
  ```
14
27
  $ git jev-stage "only the auth fix and its tests"
@@ -28,32 +41,15 @@ MM src/auth/login.ts
28
41
  M test/auth.test.ts
29
42
  ```
30
43
 
31
- Then `git commit`, and `git jev-stage "the css change"` for the next one.
32
-
33
- ## Install
34
-
35
- ```sh
36
- npm install -g git-jev-stage
37
- export TYPESAFE_API_KEY=... # early-access key from typesafe.ai
38
- ```
39
-
40
- Git 2.30 or newer, Node 22 or newer. The key is also read from the nearest `.env` file between the current directory and the repository root.
44
+ In the plan, `+` means selected and `-` means left unstaged. The three scores are Jev's probabilities for `include`, `exclude` and `mixed`. A `mixed` or low-confidence decision needs review: that hunk is printed and asked about before the final confirmation. The plan lists the selection; the composed patch is printed by `--dry-run`.
41
45
 
42
- ## How it works
43
-
44
- 1. **Snapshot.** `git diff` from the index to the working tree, with fixed flags (`--binary --full-index --no-renames --unified=6`), parsed byte for byte into hunks. Each hunk gets an id from its path and bytes. HEAD, the index bytes and the diff are hashed.
45
- 2. **One question per hunk.** Every hunk goes to [Jev](https://typesafe.ai) as a `choice` question with three options: `include` (every changed line belongs to the sentence), `exclude` (none does), `mixed` (some do). The sentence and the neighboring hunks of the same file travel along as context. Large diffs are split into windows under a token budget and sent concurrently.
46
- 3. **Policy.** `include` or `exclude` with confidence at or above `--threshold` (default 0.6) is taken as is. Anything else, including a missing or malformed answer, is `mixed`.
47
- 4. **Plan, then confirm.** The plan prints before anything changes. Each `mixed` hunk is shown and asked about: the whole hunk goes in or stays out. Lines are never split.
48
- 5. **Atomic staging.** The selected hunks become one patch. It is applied to a private copy of the index with `git apply --cached --check` and then `git apply --cached`, the copy is verified, `index.lock` is taken, HEAD, the index bytes and the full diff are checked against the snapshot, and the copy is renamed into place. If anything moved in between, nothing is staged and the command exits 3.
49
-
50
- The working tree is never written. No commit is made, no message is generated.
46
+ Then `git commit`, and `git jev-stage "the css change"` for the next one.
51
47
 
52
48
  ## Flags
53
49
 
54
50
  | Flag | Effect |
55
51
  |---|---|
56
- | `--exclude "<sentence>"` | Lines matching this sentence never belong. |
52
+ | `--exclude "<sentence>"` | Adds a sentence describing changes Jev should classify as out of scope. |
57
53
  | `--dry-run` | Print the plan and the patch that would be staged. No prompts, no changes. |
58
54
  | `--yes` | Skip the confirmation. `mixed` hunks stay unstaged and are listed. |
59
55
  | `--json` | One JSON document on stdout, plan on stderr. Stages only with `--yes`. |
@@ -62,13 +58,35 @@ The working tree is never written. No commit is made, no message is generated.
62
58
 
63
59
  Exit codes: 0 done or nothing to stage, 1 error, 2 usage, 3 the snapshot went stale or the index is locked. Git routes `git jev-stage --help` to a man page; use `git-jev-stage --help`.
64
60
 
61
+ ## Limits
62
+
63
+ - Hunks are git's, cut with 6 lines of context. A hunk with wanted and unwanted lines is `mixed`, and is staged whole or not at all.
64
+ - Untracked files are not included. Run `git add -N <path>` to make a new text file available for hunk classification.
65
+ - Renames appear as a deletion and an addition. Either side can be staged separately.
66
+ - Binary, symlink and submodule changes stop the run with an error naming the paths. Stage or stash those first. A submodule that is only dirty is ignored.
67
+ - Empty new files and mode-only changes are listed as skipped. They cannot be staged by hunk; stage them with `git add`. Staging any hunk of a file also stages that file's mode change.
68
+ - Hunks in different windows of a large diff do not see each other.
69
+ - If HEAD, the index or the working tree changes between the plan and the confirmation, nothing is staged and the command exits 3. Run it again.
70
+
71
+ ## What leaves the machine
72
+
73
+ The command sends the staging sentence, the optional exclusion sentence, changed file paths, hunk headers and hunk text to api.typesafe.ai. During staging it writes a temporary private index and updates the repository index. It does not write working-tree files.
74
+
75
+ ## Implementation details
76
+
77
+ 1. **Snapshot.** `git diff` from the index to the working tree, with fixed flags (`--binary --full-index --no-renames --unified=6`), parsed byte for byte into hunks. Each hunk gets an id from its path and bytes. HEAD, the index bytes and the diff are hashed.
78
+ 2. **One question per hunk.** Every hunk goes to Jev as a `choice` question with three options: `include` (every changed line belongs to the sentence), `exclude` (none does), `mixed` (some do). The sentence and the neighboring hunks of the same file travel along as context. Large diffs are split into windows under a token budget and sent concurrently.
79
+ 3. **Policy.** `include` or `exclude` with confidence at or above `--threshold` (default 0.6) is taken as is. Anything else, including a missing or malformed answer, is `mixed`.
80
+ 4. **Plan, then confirm.** The plan prints before anything changes. Each `mixed` hunk is shown and asked about: the whole hunk goes in or stays out. Lines are never split.
81
+ 5. **Atomic staging.** The selected hunks become one patch. It is applied to a private copy of the index with `git apply --cached --check` and then `git apply --cached`, the copy is verified, `index.lock` is taken, HEAD, the index bytes and the full diff are checked against the snapshot, and the copy is renamed into place. If anything moved in between, nothing is staged and the command exits 3.
82
+
65
83
  ## For coding agents
66
84
 
67
85
  ```sh
68
86
  git jev-stage "the auth fix" --json --yes
69
87
  ```
70
88
 
71
- The document lists every hunk with its `id`, `header`, `text`, `decision`, `source`, `confidence` and `probabilities`, plus `applied`, `stagedHunkIds` and `mixedHunkIds`. An agent stages the mixed ones itself or leaves them.
89
+ The document lists every hunk with its `id`, `header`, `text`, `decision`, `source`, `confidence` and `probabilities`, plus `applied`, `stagedHunkIds` and `mixedHunkIds`. An agent stages the mixed ones itself or leaves them. `source` explains how the decision was produced: `model`, `manual`, `low-confidence`, `missing`, `invalid`, `too-large` or `no-provider`.
72
90
 
73
91
  Claude Code plugin:
74
92
 
@@ -81,31 +99,57 @@ Other agents: `npx skills add ibrahemid/git-jev-stage`. The skill is `skills/git
81
99
 
82
100
  ## Library
83
101
 
84
- ```ts
85
- import { planSelection, applySelection } from "git-jev-stage";
102
+ ```sh
103
+ npm install git-jev-stage
104
+ ```
86
105
 
87
- const plan = await planSelection({ cwd, intent: "the auth fix" });
106
+ ```ts
107
+ import { applySelection, planSelection, TypeSafeJevProvider } from "git-jev-stage";
108
+
109
+ const provider = new TypeSafeJevProvider({ apiKey: process.env.TYPESAFE_API_KEY });
110
+ const plan = await planSelection({
111
+ cwd: process.cwd(),
112
+ intent: "the auth fix",
113
+ provider,
114
+ });
88
115
  const includeIds = [...plan.decisions.values()]
89
116
  .filter((decision) => decision.decision === "include")
90
117
  .map((decision) => decision.hunkId);
91
118
  await applySelection(plan, { includeIds });
92
119
  ```
93
120
 
94
- `planSelection` never mutates. `applySelection` stages exactly the ids it is given. Pass `provider: new FakeProvider(script)` in tests; the same validator runs on scripted and real answers.
121
+ Read `plan.decisions` and the text of the selected hunks before applying.
95
122
 
96
- ## Limits
123
+ `planSelection({ cwd, intent, exclude?, threshold?, provider?, git? })` reads the unstaged diff and returns a `Plan`. It does not read `TYPESAFE_API_KEY` and does not mutate the index. Without a provider every decision is `mixed`, so the selected-id list above comes out empty.
97
124
 
98
- - Hunks are git's, cut with 6 lines of context. A hunk with wanted and unwanted lines is `mixed`, and is staged whole or not at all.
99
- - Binary, symlink and submodule changes stop the run with an error naming the paths. Stage or stash those first. A submodule that is only dirty is ignored.
100
- - Empty new files and mode-only changes are listed as skipped; stage them with `git add`. Staging any hunk of a file also stages that file's mode change.
101
- - Hunks in different windows of a large diff do not see each other.
102
- - If HEAD, the index or the working tree changes between the plan and your answer, nothing is staged and the command exits 3. Run it again.
103
- - The sentence and the hunk text are sent to api.typesafe.ai. Nothing is written to disk except the index.
104
- - Without a key the command asks about every hunk by hand, like `git add -p` with the sentence on screen.
125
+ `applySelection(plan, { includeIds, git? })` preserves existing staged changes and returns `{ stagedHunkIds, skippedMixedIds, patchBytes }`. It rejects unknown ids and refuses to apply if HEAD, the index or the working-tree diff changed. Selecting a hunk also stages that file's mode change.
126
+
127
+ Failures arrive as `UnknownHunkError`, `StaleSnapshotError`, `IndexLockedError` and `PatchApplyError`. The provider throws `ProviderConfigError` for an empty key and `ProviderError` for a failed request.
128
+
129
+ In tests, pass a `FakeProvider`:
130
+
131
+ ```ts
132
+ import { FakeProvider, type FakeScript, planSelection } from "git-jev-stage";
133
+
134
+ const script: FakeScript = {
135
+ defaultAnswer: {
136
+ choice: "include",
137
+ confidence: 1,
138
+ probabilities: { include: 1, exclude: 0, mixed: 0 },
139
+ },
140
+ };
141
+ const plan = await planSelection({
142
+ cwd: process.cwd(),
143
+ intent: "the auth fix",
144
+ provider: new FakeProvider(script),
145
+ });
146
+ ```
147
+
148
+ The same validator runs on scripted and real answers.
105
149
 
106
150
  ## Neighbors
107
151
 
108
- - `git add -p`: same granularity, one hunk at a time, no sentence.
152
+ - `git add -p`: interactive staging with options to split or edit hunks, without sentence-based classification.
109
153
  - [git-surgeon](https://github.com/raine/git-surgeon): stages explicit hunk ids and line ranges. Built for agents that already know which lines they want.
110
154
  - [VibeGit](https://github.com/kklemon/vibegit): groups a whole working tree into commits with an LLM. git-jev-stage answers one narrower question and never commits.
111
155
 
@@ -1224,7 +1224,7 @@ var IndexLockedError = class extends JevStageError {
1224
1224
  };
1225
1225
  var MissingApiKeyError = class extends JevStageError {
1226
1226
  code = "missing-api-key";
1227
- constructor(message = "TYPESAFE_API_KEY is not set", options) {
1227
+ constructor(message = "TYPESAFE_API_KEY is not set. Set an early-access key from typesafe.ai, or rerun in an interactive terminal without --json or --yes to select hunks by hand.", options) {
1228
1228
  super(message, options);
1229
1229
  this.name = "MissingApiKeyError";
1230
1230
  }
@@ -2387,7 +2387,7 @@ function withoutProvider(snapshot) {
2387
2387
  }
2388
2388
 
2389
2389
  // src/version.ts
2390
- var VERSION2 = "0.1.0";
2390
+ var VERSION2 = "0.1.1";
2391
2391
 
2392
2392
  // src/cli/args.ts
2393
2393
  import { parseArgs } from "node:util";
@@ -2409,10 +2409,11 @@ function usageText() {
2409
2409
  return [
2410
2410
  'usage: git jev-stage "<sentence>" [options]',
2411
2411
  "",
2412
- "Stages the unstaged hunks that match the sentence.",
2412
+ "Classifies every unstaged hunk against the sentence and stages the selected",
2413
+ "hunks after confirmation.",
2413
2414
  "",
2414
2415
  "options:",
2415
- ' --exclude "<sentence>" never stage hunks that match this sentence',
2416
+ ' --exclude "<sentence>" describes changes to classify as out of scope',
2416
2417
  " --threshold <n> confidence needed to stage or skip a hunk, 0-1 (default 0.6)",
2417
2418
  " --dry-run print the plan and the composed patch, stage nothing",
2418
2419
  " --yes skip prompts and leave mixed hunks unstaged",
@@ -2640,6 +2641,7 @@ var FILE_MARKS = Object.freeze({
2640
2641
  "mode-only": "M"
2641
2642
  });
2642
2643
  var PROBABILITY_ORDER = Object.freeze(["include", "exclude", "mixed"]);
2644
+ var SKIPPED_INSTRUCTION = "these changes cannot be staged by hunk; stage them with git add";
2643
2645
  function shouldColor({ requested, isTty, noColor }) {
2644
2646
  return requested && isTty && (noColor === void 0 || noColor.length === 0);
2645
2647
  }
@@ -2651,9 +2653,7 @@ function renderPlan({ plan, includeIds, color }) {
2651
2653
  lines.push(renderHunk(hunk.id, hunk.header, plan.decisions.get(hunk.id), color));
2652
2654
  }
2653
2655
  }
2654
- for (const skipped of plan.snapshot.skipped) {
2655
- lines.push(`skipped: ${skipped.path} (${skipped.reason})`);
2656
- }
2656
+ lines.push(...skippedLines(plan.snapshot.skipped));
2657
2657
  const summary = summarizeSelection(plan.snapshot.files, includeIds);
2658
2658
  lines.push(
2659
2659
  `will stage: ${count(summary.hunks, "hunk")}, ${count(summary.files, "file")} (+${summary.added} -${summary.removed})`
@@ -2661,6 +2661,20 @@ function renderPlan({ plan, includeIds, color }) {
2661
2661
  return `${lines.join("\n")}
2662
2662
  `;
2663
2663
  }
2664
+ function renderSkipped(skipped) {
2665
+ const lines = skippedLines(skipped);
2666
+ return lines.length === 0 ? "" : `${lines.join("\n")}
2667
+ `;
2668
+ }
2669
+ function skippedLines(skipped) {
2670
+ if (skipped.length === 0) {
2671
+ return [];
2672
+ }
2673
+ return [
2674
+ ...skipped.map((entry) => `skipped: ${entry.path} (${entry.reason})`),
2675
+ SKIPPED_INSTRUCTION
2676
+ ];
2677
+ }
2664
2678
  function renderPatch(patch, write) {
2665
2679
  if (patch.length === 0) {
2666
2680
  return;
@@ -2776,6 +2790,7 @@ async function stage(options, io) {
2776
2790
  ...provider === void 0 ? {} : { provider }
2777
2791
  });
2778
2792
  if (plan.snapshot.files.length === 0) {
2793
+ report(renderSkipped(plan.snapshot.skipped));
2779
2794
  if (options.json) {
2780
2795
  io.stdout(renderJsonDocument({ plan, applied: false, stagedHunkIds: [], mixedHunkIds: [] }));
2781
2796
  return;
package/dist/index.d.ts CHANGED
@@ -211,7 +211,7 @@ declare class GitCommandError extends JevStageError {
211
211
  }
212
212
 
213
213
  type Decision = "include" | "exclude" | "mixed";
214
- type DecisionSource = "model" | "low-confidence" | "missing" | "invalid" | "too-large" | "provider-error" | "manual" | "no-provider";
214
+ type DecisionSource = "model" | "low-confidence" | "missing" | "invalid" | "too-large" | "manual" | "no-provider";
215
215
  type FileKind = "modified" | "added" | "deleted" | "mode-only";
216
216
  interface Hunk {
217
217
  id: string;
package/dist/index.js CHANGED
@@ -634,7 +634,7 @@ var IndexLockedError = class extends JevStageError {
634
634
  };
635
635
  var MissingApiKeyError = class extends JevStageError {
636
636
  code = "missing-api-key";
637
- constructor(message = "TYPESAFE_API_KEY is not set", options) {
637
+ constructor(message = "TYPESAFE_API_KEY is not set. Set an early-access key from typesafe.ai, or rerun in an interactive terminal without --json or --yes to select hunks by hand.", options) {
638
638
  super(message, options);
639
639
  this.name = "MissingApiKeyError";
640
640
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "git-jev-stage",
3
- "version": "0.1.0",
4
- "description": "Stage the diff hunks that match a sentence.",
3
+ "version": "0.1.1",
4
+ "description": "Select Git changes for staging with a plain-language description.",
5
5
  "keywords": [
6
6
  "git",
7
7
  "staging",
@@ -1,10 +1,10 @@
1
1
  ---
2
2
  name: git-jev-stage
3
- description: Stage only the diff hunks that match a sentence, with git jev-stage. Use when asked to commit or stage part of the working tree ("commit just the auth fix", "stage only the tests", "leave the debug logging out").
3
+ description: Stage Git changes selected by a plain-language description, with git jev-stage. Use when asked to commit or stage part of the working tree ("commit just the auth fix", "stage only the tests", "leave the debug logging out").
4
4
  allowed-tools: Bash(git jev-stage *) Bash(git-jev-stage *) Bash(git status *) Bash(git diff *) Bash(git commit *) Bash(git add *)
5
5
  ---
6
6
 
7
- `git jev-stage "<sentence>"` classifies every unstaged hunk against the sentence and stages the ones that belong. It never commits, never edits files, and never stages a hunk it is unsure about.
7
+ `git jev-stage "<sentence>"` asks Jev to classify every unstaged hunk. In interactive mode, it stages selected hunks after confirmation, with separate questions for `mixed` or low-confidence decisions. With `--yes`, it stages accepted `include` decisions and leaves `mixed` hunks unstaged. It only stages: working files stay untouched, and the commit and its message are left to the caller.
8
8
 
9
9
  ## Steps
10
10
 
@@ -14,21 +14,22 @@ allowed-tools: Bash(git jev-stage *) Bash(git-jev-stage *) Bash(git status *) Ba
14
14
  git jev-stage "<what to stage>" --json --yes
15
15
  ```
16
16
 
17
- Add `--exclude "<what to leave out>"` when the request names something to keep out.
17
+ Add `--exclude "<what to leave out>"` to give Jev explicit out-of-scope context. Verify those changes remain unstaged.
18
18
 
19
19
  2. Read the JSON. `applied: true` means the `include` hunks are now in the index. `mixedHunkIds` lists hunks the tool did not stage because they hold both wanted and unwanted lines, or because it was not confident.
20
20
 
21
- 3. If `mixedHunkIds` is non-empty, decide each one yourself: read the hunk under `files[].hunks[]` (its `header` and `text`), then stage the whole hunk with `git add -p` if it belongs, or leave it. Never edit the working tree to split a hunk.
21
+ 3. If `mixedHunkIds` is non-empty, decide each one: read the hunk under `files[].hunks[]` (its `header` and `text`), then stage the whole hunk with `git add -p` if it belongs, or leave it. Never edit the working tree to split a hunk.
22
22
 
23
23
  4. Confirm with `git diff --cached --stat`, then commit.
24
24
 
25
25
  ## Failure modes
26
26
 
27
- - Exit 1 with `missing-api-key`: `TYPESAFE_API_KEY` is not set. Fall back to `git add -p`.
27
+ - Exit 1 with `missing-api-key`: `TYPESAFE_API_KEY` is not set. `--yes` and `--json` need a key. An interactive run without one asks about every hunk, and `--dry-run` marks every hunk as `mixed` and prints no patch. In an agent session, fall back to `git add -p`.
28
28
  - Exit 3 with `stale-snapshot` or `index-locked`: the working tree, index, or HEAD changed while planning, or another git process holds the index. Nothing was staged. Run the command again.
29
29
  - Exit 1 with `unsupported-entry`: binary, symlink, or submodule changes are present. Stage those by path with `git add` first, or stash them, then rerun.
30
+ - Paths printed as `skipped` are mode-only or empty-file changes. They cannot be staged by hunk; stage them with `git add`.
30
31
 
31
32
  ## Do not
32
33
 
33
34
  - Do not pass `--yes` without `--json` in an agent session; the interactive prompt cannot be answered.
34
- - Do not describe the whole diff. One sentence naming the change to keep is enough; everything else stays unstaged.
35
+ - Do not describe the whole diff. Describe the wanted change in one sentence, then verify `git diff --cached` before committing.