jev-affected 0.2.0 → 0.3.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 CHANGED
@@ -1,5 +1,15 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.3.0
4
+
5
+ - Add a repository-owned `jev-affected.yml` for dogfooding this project.
6
+ - Analyze committed, working-tree, or staged changes with explicit input modes.
7
+ - Detect pull-request base branches across GitHub Actions, GitLab CI, Buildkite,
8
+ and mapped CircleCI pipeline values.
9
+ - Inspect and safely clear the Git-local semantic decision cache.
10
+ - Avoid treating source text that mentions Git submodule markers as an actual
11
+ submodule diff.
12
+
3
13
  ## 0.2.0
4
14
 
5
15
  - Read API credentials from the process environment only.
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  <p align="center"><strong>Semantic task routing for software development.</strong></p>
4
4
  <p align="center">Run tasks based on what changed, not where it changed.</p>
5
- <p align="center">v0.2.0 Public Beta · Node.js 20+ · TypeScript · MIT · Jev-powered</p>
5
+ <p align="center">v0.3.0 Public Beta · Node.js 20+ · TypeScript · MIT · Jev-powered</p>
6
6
 
7
7
  ![Illustrative offline fixture demo, not live Jev output](assets/demo.gif)
8
8
 
@@ -50,7 +50,14 @@ npx jev-affected plan --base main
50
50
  npx jev-affected run --base main
51
51
  ```
52
52
 
53
- `plan` performs analysis but never executes task commands. `run` executes configured commands. **Only committed changes are included**: default head is `HEAD`, not the working tree. Commit agent edits before planning.
53
+ `plan` performs analysis but never executes task commands. `run` executes configured commands. The default mode compares committed changes. Use `--working-tree` to include branch commits, staged changes, unstaged changes and untracked files, or `--staged` to analyze only the Git index.
54
+
55
+ ```sh
56
+ npx jev-affected plan --working-tree --base main
57
+ npx jev-affected plan --staged
58
+ ```
59
+
60
+ `--working-tree` accepts `--base` but not `--head`. `--staged` compares the index with `HEAD` and cannot be combined with `--base` or `--head`.
54
61
 
55
62
  ## How it works
56
63
 
@@ -121,12 +128,13 @@ Low model probabilities are estimates, not a guarantee that a task is unnecessar
121
128
  | `inspect` | Preview sanitized analysis inputs locally; no request |
122
129
  | `doctor` | Check Node, config, Git base, API key and models endpoint |
123
130
  | `eval [--live]` | Evaluate fixture decisions and false-skip rate |
131
+ | `cache [status\|clear]` | Inspect or remove the Git-local decision cache |
124
132
 
125
- Common flags: `--base`, `--head`, `--config`, `--no-cache`, `--json`, `--help`, `--version`. JSON task output includes a stable `version: 1`. During `run --json`, child output goes to stderr, keeping stdout parseable.
133
+ Common flags: `--base`, `--head`, `--working-tree`, `--staged`, `--config`, `--no-cache`, `--json`, `--help`, `--version`. JSON task output includes a stable `version: 1`. During `run --json`, child output goes to stderr, keeping stdout parseable.
126
134
 
127
135
  Exit codes: `0` success; `1` failed executed task or failed evaluation gate; `2` config/argument/base or doctor check error; `3` internal/I/O failure. API failures do not fail a normal plan or run by themselves.
128
136
 
129
- Base priority: CLI → config → GitHub PR base branch → `origin/main` → `main` → `master`. The comparison starts at the merge-base with head. Explicit invalid bases fail instead of silently choosing another branch. Fetch full history in CI.
137
+ Base priority: CLI → config → detected CI target `origin/HEAD` → `origin/main` → `main` → `master`. The comparison starts at the merge-base with head. Explicit invalid bases fail instead of silently choosing another branch. Fetch full history in CI.
130
138
 
131
139
  ## CI
132
140
 
@@ -141,9 +149,18 @@ Base priority: CLI → config → GitHub PR base branch → `origin/main` → `m
141
149
 
142
150
  Install dependencies first. Do not expose credentials to untrusted PR code. Nx, Turbo and other runners can be invoked as configured commands; no plugins are required.
143
151
 
152
+ Target branches are detected from GitHub Actions, GitLab CI and Buildkite variables. CircleCI exposes the PR base as a pipeline value rather than a legacy job environment variable; map it to `CIRCLE_PR_BASE_BRANCH` in the job when using automatic detection. `--base` and `base` in configuration remain the explicit overrides.
153
+
154
+ | CI | Base input |
155
+ | --- | --- |
156
+ | GitHub Actions | `GITHUB_BASE_REF` |
157
+ | GitLab CI | merge-request diff base SHA, target branch, then default branch |
158
+ | Buildkite | `BUILDKITE_PULL_REQUEST_BASE_BRANCH` |
159
+ | CircleCI | `CIRCLE_PR_BASE_BRANCH` supplied from the PR base pipeline value |
160
+
144
161
  ## Agents
145
162
 
146
- After committing edits, run `jev-affected plan --json` and execute all tasks whose `decision` is `run`, or use `jev-affected run`. `inspect` lets you review what could leave the repository first.
163
+ Run `jev-affected plan --json --working-tree` while editing, or use the default committed mode after committing. Execute all tasks whose `decision` is `run`, or use `jev-affected run` with the same mode. `inspect` lets you review what could leave the repository first.
147
164
 
148
165
  The package includes a reusable agent skill at [`skills/jev-affected`](skills/jev-affected). Point a compatible coding agent at that directory, or copy it into the agent's skills directory, to give it the safe planning and execution workflow.
149
166
 
@@ -159,6 +176,8 @@ const plan = await createPlan({ config, base: 'main' });
159
176
 
160
177
  Automatic caches store probabilities and model metadata in `.git/jev-affected/cache` (including Git worktree support). Keys include state, questions, configuration and pinned actual model version. Corrupt cache entries fall back to RUN. **Moving aliases such as `jev-latest` bypass caching**, because an alias cannot safely establish its current actual version without another request. Pin an actual `jev-x.y.z` version to enable reuse. Neither patches nor API keys are stored in cache payloads. Cache writes are the only local side effect of `plan`; `--no-cache` disables them.
161
178
 
179
+ Use `jev-affected cache status` to inspect entry count and size, and `jev-affected cache clear` to remove only the cache directory resolved inside Git metadata. Both commands support `--json` and do not require a project configuration file.
180
+
162
181
  ## Evaluation
163
182
 
164
183
  ```sh
@@ -184,11 +203,11 @@ No backend and no telemetry. Data is read locally; semantic inputs go directly t
184
203
 
185
204
  **What if Jev is down?** Candidate tasks run. Deterministic rules and protected tasks still apply.
186
205
 
187
- **Are uncommitted files included?** No, v0.1 compares commits. Explicit refs make the input reproducible.
206
+ **Are uncommitted files included?** Only with `--working-tree` or `--staged`. Default mode compares commits so explicit refs remain reproducible.
188
207
 
189
208
  ## Roadmap
190
209
 
191
- v0.1 focuses on inspectable semantic dependencies and safe task plans. Later candidates: working-tree mode, historical evaluations, watch mode, task groups and Nx/Turbo adapters. No GUI, SaaS, MCP server or autonomous command generation is included.
210
+ The current release focuses on inspectable semantic dependencies and safe plans for committed, working-tree and staged changes. Later candidates include historical evaluations, watch mode, task groups and Nx/Turbo adapters. No GUI, SaaS, MCP server or autonomous command generation is included.
192
211
 
193
212
  ## Contributing and license
194
213
 
@@ -0,0 +1,8 @@
1
+ export interface CacheInfo {
2
+ path: string;
3
+ entries: number;
4
+ bytes: number;
5
+ }
6
+ export declare function cacheDirectory(cwd?: string): Promise<string>;
7
+ export declare function inspectCache(cwd?: string): Promise<CacheInfo>;
8
+ export declare function clearCache(cwd?: string): Promise<CacheInfo>;
package/dist/cache.js ADDED
@@ -0,0 +1,35 @@
1
+ import { readdir, rm, stat } from "node:fs/promises";
2
+ import { isAbsolute, relative, resolve } from "node:path";
3
+ import { ConfigError } from "./config.js";
4
+ import { git } from "./git.js";
5
+ export async function cacheDirectory(cwd = process.cwd()) {
6
+ const common = resolve(cwd, (await git(cwd, "rev-parse", "--git-common-dir")).trim());
7
+ const directory = resolve(cwd, (await git(cwd, "rev-parse", "--git-path", "jev-affected/cache")).trim());
8
+ const fromCommon = relative(common, directory);
9
+ if (!fromCommon || fromCommon.startsWith("..") || isAbsolute(fromCommon))
10
+ throw new ConfigError("Cannot resolve a safe Git cache directory.");
11
+ return directory;
12
+ }
13
+ export async function inspectCache(cwd = process.cwd()) {
14
+ const path = await cacheDirectory(cwd);
15
+ let names;
16
+ try {
17
+ names = (await readdir(path)).filter((name) => name.endsWith(".json"));
18
+ }
19
+ catch (error) {
20
+ if (error.code === "ENOENT")
21
+ return { path, entries: 0, bytes: 0 };
22
+ throw error;
23
+ }
24
+ const sizes = await Promise.all(names.map(async (name) => (await stat(resolve(path, name))).size));
25
+ return {
26
+ path,
27
+ entries: names.length,
28
+ bytes: sizes.reduce((total, size) => total + size, 0),
29
+ };
30
+ }
31
+ export async function clearCache(cwd = process.cwd()) {
32
+ const info = await inspectCache(cwd);
33
+ await rm(info.path, { recursive: true, force: true });
34
+ return info;
35
+ }
package/dist/cli.js CHANGED
@@ -4,6 +4,7 @@ import { resolve } from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
5
  import { parseArgs } from "node:util";
6
6
  import { TypeSafeClient } from "@typesafe-ai/sdk";
7
+ import { clearCache, inspectCache } from "./cache.js";
7
8
  import { ConfigError, loadConfig, resolveApiKey, template } from "./config.js";
8
9
  import { evaluate } from "./eval.js";
9
10
  import { executePlan } from "./executor.js";
@@ -34,6 +35,8 @@ async function main() {
34
35
  help: { type: "boolean", short: "h" },
35
36
  version: { type: "boolean", short: "v" },
36
37
  "no-cache": { type: "boolean" },
38
+ "working-tree": { type: "boolean" },
39
+ staged: { type: "boolean" },
37
40
  fixtures: { type: "string" },
38
41
  live: { type: "boolean" },
39
42
  },
@@ -52,13 +55,15 @@ async function main() {
52
55
  console.log(`jev-affected — Semantic task routing, powered by Jev.
53
56
 
54
57
  Usage: jev-affected <command> [options]
55
- Commands: init, plan, run, why <task>, inspect, doctor, eval
58
+ Commands: init, plan, run, why <task>, inspect, doctor, eval, cache [status|clear]
56
59
  Options:
57
60
  --base <ref> Compare from merge-base(ref, head)
58
61
  --head <ref> Head commit (default HEAD; committed changes only)
59
62
  --config <file> YAML configuration
60
63
  --json Machine-readable output
61
64
  --no-cache Bypass the local decision cache
65
+ --working-tree Include committed, staged, unstaged and untracked changes
66
+ --staged Analyze only changes staged against HEAD
62
67
  --parallel Execute selected commands concurrently
63
68
  --concurrency <n> Maximum parallel commands
64
69
  --fixtures <dir> Evaluation fixtures (default evals/fixtures)
@@ -68,11 +73,39 @@ Options:
68
73
  inspect never sends requests. plan never executes task commands.`);
69
74
  return;
70
75
  }
71
- if (!["init", "plan", "run", "why", "inspect", "doctor", "eval"].includes(command))
76
+ if (![
77
+ "init",
78
+ "plan",
79
+ "run",
80
+ "why",
81
+ "inspect",
82
+ "doctor",
83
+ "eval",
84
+ "cache",
85
+ ].includes(command))
72
86
  throw new ConfigError("Unknown command. See --help.");
73
- if (positionals.length > (command === "why" ? 2 : 1))
87
+ if (positionals.length > (["why", "cache"].includes(command) ? 2 : 1))
74
88
  throw new ConfigError("Unexpected positional arguments.");
89
+ if (v["working-tree"] && v.staged)
90
+ throw new ConfigError("Use --working-tree or --staged, not both.");
91
+ if (v["working-tree"] && v.head)
92
+ throw new ConfigError("--working-tree cannot be combined with --head.");
93
+ if (v.staged && (v.base || v.head))
94
+ throw new ConfigError("--staged cannot be combined with --base or --head.");
75
95
  const emit = (x) => console.log(JSON.stringify(x, null, 2));
96
+ if (command === "cache") {
97
+ const action = positionals[1] ?? "status";
98
+ if (!["status", "clear"].includes(action))
99
+ throw new ConfigError("Use cache status or cache clear.");
100
+ const info = action === "clear" ? await clearCache() : await inspectCache();
101
+ if (v.json)
102
+ emit({ action, ...info });
103
+ else if (action === "clear")
104
+ console.log(`Cleared ${info.entries} cache entries (${info.bytes} bytes).`);
105
+ else
106
+ console.log(`Cache: ${info.entries} entries, ${info.bytes} bytes\n${info.path}`);
107
+ return;
108
+ }
76
109
  if (command === "init") {
77
110
  try {
78
111
  await writeFile(String(v.config ?? "jev-affected.yml"), template, {
@@ -103,6 +136,8 @@ inspect never sends requests. plan never executes task commands.`);
103
136
  base: typeof v.base === "string" ? v.base : undefined,
104
137
  head: typeof v.head === "string" ? v.head : undefined,
105
138
  cache: !v["no-cache"],
139
+ workingTree: !!v["working-tree"],
140
+ staged: !!v.staged,
106
141
  };
107
142
  if (command === "why" &&
108
143
  (!positionals[1] || !Object.hasOwn(config.tasks, positionals[1])))
package/dist/git.d.ts CHANGED
@@ -14,8 +14,12 @@ export interface ChangeState {
14
14
  incomplete: boolean;
15
15
  warnings: string[];
16
16
  }
17
- export declare function collectChanges(config: Config, options?: {
17
+ export interface ChangeOptions {
18
18
  cwd?: string;
19
19
  base?: string;
20
20
  head?: string;
21
- }): Promise<ChangeState>;
21
+ workingTree?: boolean;
22
+ staged?: boolean;
23
+ }
24
+ export declare function detectCiBaseCandidates(env?: NodeJS.ProcessEnv): string[];
25
+ export declare function collectChanges(config: Config, options?: ChangeOptions): Promise<ChangeState>;
package/dist/git.js CHANGED
@@ -1,4 +1,6 @@
1
1
  import { execFile } from "node:child_process";
2
+ import { stat } from "node:fs/promises";
3
+ import { resolve as resolvePath } from "node:path";
2
4
  import { promisify } from "node:util";
3
5
  import picomatch from "picomatch";
4
6
  import { ConfigError, secretPatterns } from "./config.js";
@@ -12,38 +14,88 @@ export async function git(cwd, ...args) {
12
14
  })).stdout;
13
15
  }
14
16
  export const matches = (path, patterns) => patterns.length > 0 && picomatch(patterns, { dot: true })(path);
17
+ const branchRefs = (value) => {
18
+ const branch = value?.trim().replace(/^refs\/heads\//, "");
19
+ if (!branch || branch === "false")
20
+ return [];
21
+ if (branch.startsWith("origin/") || /^[0-9a-f]{7,40}$/.test(branch))
22
+ return [branch];
23
+ return [`origin/${branch}`, branch];
24
+ };
25
+ export function detectCiBaseCandidates(env = process.env) {
26
+ const refs = [
27
+ ...branchRefs(env.GITHUB_BASE_REF),
28
+ ...(env.GITLAB_CI === "true"
29
+ ? [
30
+ ...branchRefs(env.CI_MERGE_REQUEST_DIFF_BASE_SHA),
31
+ ...branchRefs(env.CI_MERGE_REQUEST_TARGET_BRANCH_NAME),
32
+ ...branchRefs(env.CI_DEFAULT_BRANCH),
33
+ ]
34
+ : []),
35
+ ...(env.BUILDKITE === "true"
36
+ ? branchRefs(env.BUILDKITE_PULL_REQUEST_BASE_BRANCH)
37
+ : []),
38
+ ...(env.CIRCLECI === "true" ? branchRefs(env.CIRCLE_PR_BASE_BRANCH) : []),
39
+ ];
40
+ return [...new Set(refs)];
41
+ }
42
+ const hasUnsupportedPatch = (patch) => /^Binary files .+ differ$/m.test(patch) ||
43
+ /^[+-]Subproject commit [0-9a-f]{7,}(?:-dirty)?$/m.test(patch);
44
+ async function untrackedPatch(cwd, path) {
45
+ try {
46
+ return await git(cwd, "diff", "--no-index", "--no-ext-diff", "--no-textconv", "--unified=3", "--", "/dev/null", path);
47
+ }
48
+ catch (error) {
49
+ const result = error;
50
+ if (Number(result.code) === 1 && typeof result.stdout === "string")
51
+ return result.stdout;
52
+ throw error;
53
+ }
54
+ }
15
55
  export async function collectChanges(config, options = {}) {
16
56
  const cwd = options.cwd ?? process.cwd();
57
+ if (options.workingTree && options.staged)
58
+ throw new ConfigError("Use --working-tree or --staged, not both.");
59
+ if (options.workingTree && options.head)
60
+ throw new ConfigError("--working-tree cannot be combined with --head.");
61
+ if (options.staged && (options.base || options.head))
62
+ throw new ConfigError("--staged cannot be combined with --base or --head.");
17
63
  const resolve = async (ref) => (await git(cwd, "rev-parse", "--verify", "--end-of-options", `${ref}^{commit}`)).trim();
18
- let head;
64
+ let headCommit;
19
65
  try {
20
- head = await resolve(options.head ?? "HEAD");
66
+ headCommit = await resolve(options.head ?? "HEAD");
21
67
  }
22
68
  catch {
23
69
  throw new ConfigError("Cannot resolve head commit.");
24
70
  }
25
- const explicit = options.base ?? config.base;
71
+ const explicit = options.staged ? "HEAD" : (options.base ?? config.base);
26
72
  const candidates = explicit
27
73
  ? [explicit]
28
74
  : [
29
- process.env.GITHUB_BASE_REF
30
- ? `origin/${process.env.GITHUB_BASE_REF}`
31
- : undefined,
75
+ ...detectCiBaseCandidates(),
76
+ "origin/HEAD",
32
77
  "origin/main",
33
78
  "main",
34
79
  "master",
35
- ].filter((x) => !!x);
80
+ ];
36
81
  let base;
37
82
  for (const ref of candidates) {
38
83
  try {
39
- base = (await git(cwd, "merge-base", await resolve(ref), head)).trim();
84
+ base = options.staged
85
+ ? await resolve("HEAD")
86
+ : (await git(cwd, "merge-base", await resolve(ref), headCommit)).trim();
40
87
  break;
41
88
  }
42
89
  catch { }
43
90
  }
44
91
  if (!base)
45
92
  throw new ConfigError("Cannot resolve Git base. Fetch the base branch or pass --base.");
46
- const parts = (await git(cwd, "diff", "--name-status", "-z", "--find-renames", base, head, "--")).split("\0");
93
+ const comparison = options.staged
94
+ ? ["--cached", base]
95
+ : options.workingTree
96
+ ? [base]
97
+ : [base, headCommit];
98
+ const parts = (await git(cwd, "diff", "--name-status", "-z", "--find-renames", ...comparison, "--")).split("\0");
47
99
  const files = [];
48
100
  for (let i = 0; i < parts.length && parts[i];) {
49
101
  const status = parts[i++];
@@ -59,9 +111,23 @@ export async function collectChanges(config, options = {}) {
59
111
  else
60
112
  files.push({ status, path });
61
113
  }
114
+ if (options.workingTree) {
115
+ const tracked = new Set(files.flatMap((file) => [
116
+ file.path,
117
+ ...(file.oldPath ? [file.oldPath] : []),
118
+ ]));
119
+ for (const path of (await git(cwd, "ls-files", "--others", "--exclude-standard", "-z", "--")).split("\0")) {
120
+ if (path && !tracked.has(path))
121
+ files.push({ status: "A", path });
122
+ }
123
+ }
62
124
  const state = {
63
125
  base,
64
- head,
126
+ head: options.staged
127
+ ? "INDEX"
128
+ : options.workingTree
129
+ ? "WORKTREE"
130
+ : headCommit,
65
131
  files: [],
66
132
  diff: "",
67
133
  incomplete: false,
@@ -78,9 +144,19 @@ export async function collectChanges(config, options = {}) {
78
144
  continue;
79
145
  state.files.push(file);
80
146
  try {
81
- const patch = await git(cwd, "diff", "--no-ext-diff", "--no-textconv", "--find-renames", "--unified=3", base, head, "--", ...paths);
82
- if (patch.includes("Binary files ") ||
83
- patch.includes("Subproject commit ")) {
147
+ if (options.workingTree && file.status === "A") {
148
+ const size = (await stat(resolvePath(cwd, file.path))).size;
149
+ if (Buffer.byteLength(state.diff) + size >
150
+ config.analysis.maxDiffBytes) {
151
+ state.incomplete = true;
152
+ state.warnings.push("Diff exceeds analysis limit; running all tasks.");
153
+ continue;
154
+ }
155
+ }
156
+ const patch = options.workingTree && file.status === "A"
157
+ ? await untrackedPatch(cwd, file.path)
158
+ : await git(cwd, "diff", "--no-ext-diff", "--no-textconv", "--find-renames", "--unified=3", ...(options.staged ? ["--cached", base] : comparison), "--", ...paths);
159
+ if (hasUnsupportedPatch(patch)) {
84
160
  state.incomplete = true;
85
161
  state.warnings.push("Binary or submodule change; running all tasks.");
86
162
  }
package/dist/index.d.ts CHANGED
@@ -1,5 +1,6 @@
1
+ export { type CacheInfo, cacheDirectory, clearCache, inspectCache, } from "./cache.js";
1
2
  export { API_KEY_VARS, type Config, ConfigError, loadConfig, parseConfig, resolveApiKey, } from "./config.js";
2
3
  export { executePlan } from "./executor.js";
3
- export { type ChangeState, collectChanges } from "./git.js";
4
+ export { type ChangedFile, type ChangeOptions, type ChangeState, collectChanges, detectCiBaseCandidates, } from "./git.js";
4
5
  export { createPlan, type Plan, type TaskDecision } from "./planner.js";
5
6
  export { type AnalysisInput, type AnalysisResult, type DecisionProvider, JevProvider, } from "./provider.js";
package/dist/index.js CHANGED
@@ -1,5 +1,6 @@
1
+ export { cacheDirectory, clearCache, inspectCache, } from "./cache.js";
1
2
  export { API_KEY_VARS, ConfigError, loadConfig, parseConfig, resolveApiKey, } from "./config.js";
2
3
  export { executePlan } from "./executor.js";
3
- export { collectChanges } from "./git.js";
4
+ export { collectChanges, detectCiBaseCandidates, } from "./git.js";
4
5
  export { createPlan } from "./planner.js";
5
6
  export { JevProvider, } from "./provider.js";
package/dist/planner.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { type Config } from "./config.js";
2
- import { type ChangeState } from "./git.js";
2
+ import { type ChangeOptions, type ChangeState } from "./git.js";
3
3
  import { type AnalysisResult, type DecisionProvider } from "./provider.js";
4
4
  export interface TaskDecision {
5
5
  id: string;
@@ -28,10 +28,7 @@ export interface Plan {
28
28
  }
29
29
  export declare function createPlan(input: {
30
30
  config: Config;
31
- cwd?: string;
32
- base?: string;
33
- head?: string;
34
31
  state?: ChangeState;
35
32
  provider?: DecisionProvider;
36
33
  cache?: boolean;
37
- }): Promise<Plan>;
34
+ } & ChangeOptions): Promise<Plan>;
package/dist/planner.js CHANGED
@@ -1,8 +1,9 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { mkdir, readFile, writeFile } from "node:fs/promises";
3
3
  import { resolve } from "node:path";
4
+ import { cacheDirectory } from "./cache.js";
4
5
  import { parseConfig, resolveApiKey, secretPatterns, } from "./config.js";
5
- import { collectChanges, git, matches } from "./git.js";
6
+ import { collectChanges, matches, } from "./git.js";
6
7
  import { JevProvider, } from "./provider.js";
7
8
  export async function createPlan(input) {
8
9
  const start = Date.now(), config = parseConfig(input.config), cwd = input.cwd ?? process.cwd();
@@ -60,7 +61,7 @@ export async function createPlan(input) {
60
61
  const pinned = /^jev-\d+\.\d+\.\d+$/.test(config.model);
61
62
  if (input.cache !== false && !input.provider && pinned) {
62
63
  try {
63
- const dir = resolve(cwd, (await git(cwd, "rev-parse", "--git-path", "jev-affected/cache")).trim());
64
+ const dir = await cacheDirectory(cwd);
64
65
  const key = createHash("sha256")
65
66
  .update(JSON.stringify({
66
67
  version: 1,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jev-affected",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Semantic task routing for software development, powered by Jev.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -13,12 +13,13 @@ projects and `npx jev-affected` elsewhere.
13
13
  1. Confirm the current directory is a Git repository and locate
14
14
  `jev-affected.yml`. Run `jev-affected init` only when setup is requested and no
15
15
  configuration exists.
16
- 2. Ensure the relevant edits are committed. Version 0.1 compares commits and does
17
- not include working-tree changes.
18
- 3. On first use, or when privacy matters, run
19
- `jev-affected inspect --base <ref>`. This previews the sanitized input locally
20
- without sending a request.
21
- 4. Run `jev-affected plan --json --base <ref>` and use its task decisions as the
16
+ 2. Choose the input mode. Use the default mode for committed branch changes,
17
+ `--working-tree --base <ref>` while editing, or `--staged` for the exact Git
18
+ index before a commit.
19
+ 3. On first use, or when privacy matters, run `jev-affected inspect` with the
20
+ same mode and base flags intended for planning. This previews the sanitized
21
+ input locally without sending a request.
22
+ 4. Run `jev-affected plan --json` with those same flags and use its task decisions as the
22
23
  record of what should run. Do not invent task reasons or commands.
23
24
  5. Run `jev-affected run --base <ref>` only when execution is requested. Commands
24
25
  come from the repository configuration and should be treated as repository
@@ -26,6 +27,10 @@ projects and `npx jev-affected` elsewhere.
26
27
  6. Report the selected and skipped task counts, actual model version, warnings,
27
28
  fallback decisions, and any failed task commands.
28
29
 
30
+ Use `jev-affected cache status` to inspect the Git-local decision cache. Use
31
+ `jev-affected cache clear` when a fresh semantic decision is needed; it removes
32
+ only the cache directory resolved inside Git metadata.
33
+
29
34
  Use an explicit base ref in CI and shallow clones. Fetch enough history for Git to
30
35
  compute the merge base.
31
36