Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

## 0.7.2 - Unreleased

- Reclaimed dead same-host review locks automatically and added `clean-locks --stale-only` for safe scripted cleanup while preserving live and remote locks, thanks @goutamadwant.
- Fixed diff-scoped review and CI runs to include changed features regardless of their previous review status, preventing warm-state gates from silently skipping changed code, thanks @youhaowei.
- Updated pnpm, Node typings, formatter and linter tooling, and security workflow actions.
- Added Rust seed context for Cargo manifests, paired crate entrypoints, and directly declared modules across crate roots and binary layouts, thanks @Tanmay-008.
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ Supported provider names today:
- `clawpatch revalidate --finding <id>`: re-check one finding
- `clawpatch revalidate --all`: re-check open findings with report-style filters
- `clawpatch doctor`: check provider availability
- `clawpatch clean-locks`: clear feature locks
- `clawpatch clean-locks`: clear feature locks; add `--stale-only` to reclaim only dead local locks

Useful flags:

Expand Down
8 changes: 5 additions & 3 deletions docs/code-review.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,9 +81,11 @@ appends a compact summary to `GITHUB_STEP_SUMMARY` when that file is available.

Progress uses stderr so `--json` stdout remains machine-readable. The worker
pool is per-process, and lock files under `.clawpatch/locks/` prevent
overlapping review processes from claiming the same feature. Interrupted runs
can leave recoverable lock files; clear them with `clawpatch clean-locks` after
confirming no review process is still active. `clawpatch status` includes both
overlapping review processes from claiming the same feature. Interrupted local
runs with dead process IDs are reclaimed automatically on the next claim. Use
`clawpatch clean-locks --stale-only` for conservative cleanup that preserves live
local locks and locks from other hosts; the unfiltered `clean-locks` command still
requires confirming that no review process is active. `clawpatch status` includes both
feature-record locks and lock files in `activeLocks`, and reports the lock-file
count as `lockFiles`.

Expand Down
4 changes: 3 additions & 1 deletion docs/safety.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@ Current safety rules:
strictly sandboxed. See docs/providers.md.
- provider output must pass runtime schema validation.
- feature locks are stored in feature records and `.clawpatch/locks/`; `status`
surfaces both, and `clean-locks` clears both.
surfaces both. Claims automatically reclaim same-host locks whose process is
dead, and `clean-locks --stale-only` applies the same conservative rule without
clearing live local or remote locks.
- the mapper skips symlinked directories and common generated directories.

Not implemented today:
Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,12 @@
"crabbox:warmup": "crabbox warmup"
},
"dependencies": {
"proper-lockfile": "^4.1.2",
"zod": "^4.4.3"
},
"devDependencies": {
"@types/node": "^26.1.2",
"@types/proper-lockfile": "^4.1.4",
"oxfmt": "^0.61.0",
"oxlint": "^1.76.0",
"typescript": "^7.0.2",
Expand Down
43 changes: 43 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

24 changes: 19 additions & 5 deletions scripts/package-smoke.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import { tmpdir } from "node:os";
import { dirname, isAbsolute, join } from "node:path";
import { pathToFileURL } from "node:url";

const moduleRequire = createRequire(import.meta.url);
const root = process.cwd();

function createSmokeContext() {
Expand Down Expand Up @@ -251,10 +250,24 @@ function packPath(destination, output) {
}

function runtimeDependencyPaths(rootPath = root) {
const packageJson = JSON.parse(readFileSync(join(rootPath, "package.json"), "utf8"));
return runtimeDependencyNames(packageJson).map((name) =>
dirname(moduleRequire.resolve(`${name}/package.json`)),
);
const dependencyPaths = new Map();

function collect(packageJsonPath, packageRequire) {
const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf8"));
for (const name of runtimeDependencyNames(packageJson)) {
const dependencyPackageJson = packageRequire.resolve(`${name}/package.json`);
const dependencyPath = dirname(dependencyPackageJson);
if (dependencyPaths.has(dependencyPath)) {
continue;
}
dependencyPaths.set(dependencyPath, dependencyPath);
collect(dependencyPackageJson, createRequire(dependencyPackageJson));
}
}

const packageJsonPath = join(rootPath, "package.json");
collect(packageJsonPath, createRequire(packageJsonPath));
return [...dependencyPaths.values()];
}

function runtimeDependencyNames(packageJson) {
Expand Down Expand Up @@ -331,4 +344,5 @@ export const packageSmokeTestHooks = {
installArgs,
packDependencyArgs,
runtimeDependencyNames,
runtimeDependencyPaths,
};
10 changes: 9 additions & 1 deletion src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { emitProgress } from "./progress.js";
import { providerByName } from "./provider.js";
import {
clearFeatureLockFiles,
clearStaleFeatureLocks,
ensureStateDirs,
readFeatures,
readFeatureLockIds,
Expand Down Expand Up @@ -266,8 +267,15 @@ export async function doctorCommand(
};
}

export async function cleanLocksCommand(context: AppContext): Promise<unknown> {
export async function cleanLocksCommand(
context: AppContext,
flags: Record<string, string | boolean> = {},
): Promise<unknown> {
const loaded = await loadProjectState(context);
if (flags["staleOnly"] === true) {
const cleared = await clearStaleFeatureLocks(loaded.paths);
return { cleared: cleared.featuresCleared, lockFilesCleared: cleared.lockFilesCleared };
}
const features = await readFeatures(loaded.paths);
let cleared = 0;
for (const feature of features) {
Expand Down
8 changes: 7 additions & 1 deletion src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -249,7 +249,7 @@ const commandSpecs = {
run: doctorCommand,
},
"clean-locks": {
flags: [],
flags: ["staleOnly"],
usage: ["clawpatch clean-locks [flags]"],
run: cleanLocksCommand,
},
Expand Down Expand Up @@ -383,6 +383,12 @@ const optionSpecs: Record<string, OptionSpec> = {
force: { name: "force", kind: "boolean", target: "command", help: " --force" },
all: { name: "all", kind: "boolean", target: "command", help: " --all" },
draft: { name: "draft", kind: "boolean", target: "command", help: " --draft" },
"stale-only": {
name: "staleOnly",
kind: "boolean",
target: "command",
help: " --stale-only",
},
"include-dirty": {
name: "includeDirty",
kind: "boolean",
Expand Down
12 changes: 12 additions & 0 deletions src/package-smoke.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { readFileSync } from "node:fs";
import { join } from "node:path";
import { pathToFileURL } from "node:url";
import { describe, expect, it } from "vitest";
Expand All @@ -16,6 +17,7 @@ type PackageSmokeTesting = {
npmCache: string;
}): string[];
runtimeDependencyNames(packageJson: { dependencies?: Record<string, string> }): string[];
runtimeDependencyPaths(rootPath?: string): string[];
};
};

Expand All @@ -37,6 +39,16 @@ describe("package smoke harness", () => {
});
expect(dependencyNames).toEqual(["zod"]);

const packedDependencyNames = smoke.runtimeDependencyPaths().map((dependencyPath) => {
const packageJson = JSON.parse(
readFileSync(join(dependencyPath, "package.json"), "utf8"),
) as { name: string };
return packageJson.name;
});
expect(packedDependencyNames).toEqual(
expect.arrayContaining(["proper-lockfile", "graceful-fs", "retry", "signal-exit", "zod"]),
);

const packArgs = smoke.packDependencyArgs({
dependencyPath: dependencySource,
destination: "/tmp",
Expand Down
Loading
Loading