dw-sdlc 0.13.0 → 0.15.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/dist/agentic-config.js +3 -3
- package/dist/checks/file-size-ratchet.js +2 -2
- package/dist/checks/tree.js +2 -2
- package/dist/cli.js +8 -1
- package/dist/commands/doctor.js +27 -8
- package/dist/commands/references-setup.js +2 -2
- package/dist/commands/references-sync.js +4 -5
- package/dist/commands/setup.js +2 -2
- package/dist/commands/sweep.js +89 -84
- package/dist/doctor-plugins.d.ts +2 -0
- package/dist/doctor-plugins.js +7 -1
- package/dist/exec.d.ts +14 -0
- package/dist/exec.js +39 -0
- package/dist/git-env.js +2 -2
- package/dist/landed.js +10 -12
- package/dist/pr/decide.d.ts +1 -1
- package/dist/pr/decide.js +37 -1
- package/dist/pr/read.js +14 -2
- package/dist/pr/state.d.ts +10 -1
- package/dist/pr/state.js +36 -2
- package/dist/setup-rules.js +2 -2
- package/dist/sweep-terminals.d.ts +89 -0
- package/dist/sweep-terminals.js +262 -0
- package/dist/sweep-trees.d.ts +17 -0
- package/dist/sweep-trees.js +72 -0
- package/dist/worker/herdr.js +14 -2
- package/dist/working-tree.js +3 -6
- package/dist/worktree-lock.d.ts +6 -3
- package/dist/worktree-lock.js +4 -3
- package/package.json +1 -1
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// What a Sweep reads of a repository's linked worktrees: where each lies, what it has checked out, its lock,
|
|
2
|
+
// the files that hold it and the operation paused in it.
|
|
3
|
+
import { existsSync } from "node:fs";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { exec } from "./exec.js";
|
|
6
|
+
import { repositoryFreeEnv } from "./git-env.js";
|
|
7
|
+
import { CannotRun } from "./pr/read.js";
|
|
8
|
+
import { asked, git } from "./working-tree.js";
|
|
9
|
+
/** `git <args>` at `root`, or a stop naming its problem. */
|
|
10
|
+
export function must(root, args) {
|
|
11
|
+
const result = git(root, args);
|
|
12
|
+
if ("error" in result) {
|
|
13
|
+
throw new CannotRun(result.error, "check the repository with git status");
|
|
14
|
+
}
|
|
15
|
+
return result.out;
|
|
16
|
+
}
|
|
17
|
+
/** Every worktree of the repository, the main checkout first. */
|
|
18
|
+
export function worktreesOf(root) {
|
|
19
|
+
const trees = [];
|
|
20
|
+
let tree;
|
|
21
|
+
for (const line of must(root, ["worktree", "list", "--porcelain"]).split("\n")) {
|
|
22
|
+
if (line.startsWith("worktree ")) {
|
|
23
|
+
tree = { path: line.slice("worktree ".length), head: "", branch: undefined, lock: undefined, prunable: false };
|
|
24
|
+
trees.push(tree);
|
|
25
|
+
}
|
|
26
|
+
else if (tree !== undefined && line.startsWith("HEAD ")) {
|
|
27
|
+
tree.head = line.slice("HEAD ".length);
|
|
28
|
+
}
|
|
29
|
+
else if (tree !== undefined && line.startsWith("branch refs/heads/")) {
|
|
30
|
+
tree.branch = line.slice("branch refs/heads/".length);
|
|
31
|
+
}
|
|
32
|
+
else if (tree !== undefined && (line === "locked" || line.startsWith("locked "))) {
|
|
33
|
+
tree.lock = line.slice("locked".length).trim();
|
|
34
|
+
}
|
|
35
|
+
else if (tree !== undefined && (line === "prunable" || line.startsWith("prunable "))) {
|
|
36
|
+
tree.prunable = true;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return trees;
|
|
40
|
+
}
|
|
41
|
+
/** The files `git status` lists in the worktree at `path`, ignored ones aside, or why it cannot be read. */
|
|
42
|
+
export function dirtyFiles(path) {
|
|
43
|
+
const args = ["-C", path, "status", "--porcelain=v1", "-z", "--untracked-files=all"];
|
|
44
|
+
const result = exec("git", args, { env: repositoryFreeEnv() });
|
|
45
|
+
if (result.status !== 0) {
|
|
46
|
+
const said = result.stderr.split("\n").find((line) => line.trim() !== "");
|
|
47
|
+
return { error: `git status: ${said?.trim() ?? `exited ${result.status ?? "on a signal"}`}` };
|
|
48
|
+
}
|
|
49
|
+
const files = [];
|
|
50
|
+
const entries = result.stdout.split("\0").filter((entry) => entry !== "");
|
|
51
|
+
for (let index = 0; index < entries.length; index++) {
|
|
52
|
+
const entry = entries[index] ?? "";
|
|
53
|
+
files.push(entry.slice(3));
|
|
54
|
+
if (entry.startsWith("R") || entry.startsWith("C")) {
|
|
55
|
+
index++;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return files;
|
|
59
|
+
}
|
|
60
|
+
const OPERATIONS = [
|
|
61
|
+
["rebase-merge", "rebase"],
|
|
62
|
+
["rebase-apply", "rebase or am"],
|
|
63
|
+
["MERGE_HEAD", "merge"],
|
|
64
|
+
["CHERRY_PICK_HEAD", "cherry-pick"],
|
|
65
|
+
["REVERT_HEAD", "revert"],
|
|
66
|
+
["BISECT_LOG", "bisect"]
|
|
67
|
+
];
|
|
68
|
+
/** The operation paused in the worktree at `path`: its git dir holds the state git resumes it from. */
|
|
69
|
+
export function pausedOperation(path) {
|
|
70
|
+
const dir = asked(path, ["rev-parse", "--absolute-git-dir"]);
|
|
71
|
+
return dir === undefined ? undefined : OPERATIONS.find(([file]) => existsSync(join(dir, file)))?.[1];
|
|
72
|
+
}
|
package/dist/worker/herdr.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// What the `worker` commands share: `hc` and `herdr-app` as child processes, the Lead a command acts for,
|
|
2
2
|
// and the Workers herdr lists for it.
|
|
3
|
-
import {
|
|
3
|
+
import { exec, TimedOut } from "../exec.js";
|
|
4
4
|
import { isObject, parseObject } from "../json-text.js";
|
|
5
5
|
/** What stops a `worker` command before it acts: exit 2, the problem and, when there is one, its remedy. */
|
|
6
6
|
export class Refusal extends Error {
|
|
@@ -15,9 +15,21 @@ export function said(called) {
|
|
|
15
15
|
const line = `${called.stderr}\n${called.stdout}`.split("\n").find((text) => text.trim() !== "");
|
|
16
16
|
return line?.trim() ?? `exited ${called.status}`;
|
|
17
17
|
}
|
|
18
|
+
/** `tool args` as a child process; past its limit it stops the command as any failed call does. */
|
|
19
|
+
function reach(tool, args) {
|
|
20
|
+
try {
|
|
21
|
+
return exec(tool, args);
|
|
22
|
+
}
|
|
23
|
+
catch (error) {
|
|
24
|
+
if (error instanceof TimedOut) {
|
|
25
|
+
throw new Refusal(error.message, "check herdr is responding, then call again");
|
|
26
|
+
}
|
|
27
|
+
throw error;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
18
30
|
/** `tool args` as a child process; a tool that is not on PATH stops the command. */
|
|
19
31
|
export function call(tool, args) {
|
|
20
|
-
const result =
|
|
32
|
+
const result = reach(tool, args);
|
|
21
33
|
if (result.error !== undefined) {
|
|
22
34
|
if ("code" in result.error && result.error.code === "ENOENT") {
|
|
23
35
|
throw new Refusal(`${tool} is not on PATH`, "install herdr, or run the command in a terminal herdr started");
|
package/dist/working-tree.js
CHANGED
|
@@ -1,18 +1,15 @@
|
|
|
1
1
|
// A checkout's working tree as git would commit it: the tree of every file git tracks or would track,
|
|
2
2
|
// hashed on a copy of the index. The Gate's cache fingerprints it and the watch for Drift records it.
|
|
3
|
-
import { spawnSync } from "node:child_process";
|
|
4
3
|
import { copyFileSync, existsSync, mkdtempSync, rmSync, statSync, utimesSync } from "node:fs";
|
|
5
4
|
import { tmpdir } from "node:os";
|
|
6
5
|
import { join } from "node:path";
|
|
6
|
+
import { exec } from "./exec.js";
|
|
7
7
|
import { repositoryFreeEnv } from "./git-env.js";
|
|
8
8
|
/** `git <args>` in the repository at `root`, on `index` when given. */
|
|
9
9
|
export function git(root, args, index) {
|
|
10
10
|
const env = repositoryFreeEnv();
|
|
11
|
-
const result =
|
|
12
|
-
|
|
13
|
-
env: index === undefined ? env : { ...env, GIT_INDEX_FILE: index },
|
|
14
|
-
// A Step that writes thousands of files makes a diff-tree past the default 1 MiB.
|
|
15
|
-
maxBuffer: 64 * 1024 * 1024
|
|
11
|
+
const result = exec("git", ["-C", root, ...args], {
|
|
12
|
+
env: index === undefined ? env : { ...env, GIT_INDEX_FILE: index }
|
|
16
13
|
});
|
|
17
14
|
if (result.status === 0) {
|
|
18
15
|
return { out: result.stdout.trim() };
|
package/dist/worktree-lock.d.ts
CHANGED
|
@@ -4,12 +4,15 @@ export type StaleLock = {
|
|
|
4
4
|
held: string;
|
|
5
5
|
why: string;
|
|
6
6
|
};
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
} | StaleLock | {
|
|
7
|
+
/** A lock that keeps the worktree; `pid` is the live process a Claude Code lock names. */
|
|
8
|
+
export type LockStays = {
|
|
10
9
|
state: "stays";
|
|
11
10
|
reason: string;
|
|
12
11
|
command: string | null;
|
|
12
|
+
pid?: number;
|
|
13
13
|
};
|
|
14
|
+
export type LockVerdict = {
|
|
15
|
+
state: "none";
|
|
16
|
+
} | StaleLock | LockStays;
|
|
14
17
|
/** What the lock of the worktree at `path`, held for `reason`, says about whether the Sweep may lift it. */
|
|
15
18
|
export declare function lockVerdict(path: string, reason: string | undefined): LockVerdict;
|
package/dist/worktree-lock.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// What a worktree's lock means to a Sweep. Claude Code locks the worktree of a session with the reason
|
|
2
2
|
// `claude session <name> (pid <N> start <time>)`, <time> as `ps -o lstart` writes it in UTC. Such a lock is
|
|
3
3
|
// stale once no process N runs or N started at another time. Any other reason is the dev's to lift.
|
|
4
|
-
import {
|
|
4
|
+
import { exec } from "./exec.js";
|
|
5
5
|
import { shellLine } from "./pr/decide.js";
|
|
6
6
|
const CLAUDE_LOCK = /^claude session .* \(pid (?<pid>\d{1,10}) start (?<start>.+)\)$/;
|
|
7
7
|
const MAX_PID = 2 ** 31 - 1;
|
|
@@ -22,7 +22,7 @@ function runningOf(pid) {
|
|
|
22
22
|
const starts = [{ TZ: "UTC" }, {}]
|
|
23
23
|
.map((zone) => {
|
|
24
24
|
const env = { ...process.env, LC_ALL: "C", ...zone };
|
|
25
|
-
const result =
|
|
25
|
+
const result = exec("ps", ["-o", "lstart=", "-p", String(pid)], { env });
|
|
26
26
|
return result.status === 0 ? squeezed(result.stdout) : "";
|
|
27
27
|
})
|
|
28
28
|
.filter((start) => start !== "");
|
|
@@ -58,6 +58,7 @@ export function lockVerdict(path, reason) {
|
|
|
58
58
|
return {
|
|
59
59
|
state: "stays",
|
|
60
60
|
reason: `locked by a live Claude Code session: pid ${pid} is running (${reason})`,
|
|
61
|
-
command: null
|
|
61
|
+
command: null,
|
|
62
|
+
pid
|
|
62
63
|
};
|
|
63
64
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dw-sdlc",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.15.0",
|
|
4
4
|
"description": "The Gate and the agentic config of a repository: the dw-sdlc bin with the Gate, the setup, the doctor, the config check, the Sync and setup of References, and the Ship script that drives a pull request to merged; the resolver of the config's layers, its JSON Schema and the validator that reads it.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|