@webpieces/rules-config 0.4.721 → 0.4.723
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@webpieces/rules-config",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.723",
|
|
4
4
|
"description": "Shared webpieces.config.json loader. Single source of truth for validation rule configuration consumed by @webpieces/ai-hook-rules, @webpieces/code-rules, and @webpieces/nx-webpieces-rules.",
|
|
5
5
|
"type": "commonjs",
|
|
6
6
|
"main": "./src/index.js",
|
package/src/worktrees.d.ts
CHANGED
|
@@ -13,6 +13,18 @@ export declare class WorktreeWorkInFlight {
|
|
|
13
13
|
export declare class Worktree {
|
|
14
14
|
path: string;
|
|
15
15
|
branch: string;
|
|
16
|
+
/**
|
|
17
|
+
* The commit this worktree's HEAD is at, verbatim from the `HEAD <sha>` line every porcelain record
|
|
18
|
+
* already carries. '' only for a bare worktree, which has no HEAD at all.
|
|
19
|
+
*
|
|
20
|
+
* Load-bearing, and the reason it stopped being dropped by the parser: a BRANCH NAME does not
|
|
21
|
+
* identify a tree. A second clone, or a stale worktree, can hold the same name at a different
|
|
22
|
+
* commit — which is the wrong-objects-under-the-right-name failure `wp-land-pr` already refused to
|
|
23
|
+
* make from the tree it happened to be standing in. Pairing the name with this sha turns that veto
|
|
24
|
+
* into a SELECTION: the tree to archive from and reap is the one whose HEAD is the exact commit
|
|
25
|
+
* GitHub squashed, whichever directory the operator is standing in. See LandedTreeResolver.
|
|
26
|
+
*/
|
|
27
|
+
head: string;
|
|
16
28
|
isMain: boolean;
|
|
17
29
|
prunable: boolean;
|
|
18
30
|
locked: boolean;
|
|
@@ -25,7 +37,7 @@ export declare class Worktree {
|
|
|
25
37
|
* reported all three as "locked by a human". See agent-worktree-lock.ts.
|
|
26
38
|
*/
|
|
27
39
|
lockReason: string;
|
|
28
|
-
constructor(path: string, branch: string, isMain: boolean, prunable: boolean, locked: boolean, lockReason: string);
|
|
40
|
+
constructor(path: string, branch: string, head: string, isMain: boolean, prunable: boolean, locked: boolean, lockReason: string);
|
|
29
41
|
}
|
|
30
42
|
export declare class WorktreeService {
|
|
31
43
|
/**
|
|
@@ -67,6 +79,12 @@ export declare class WorktreeService {
|
|
|
67
79
|
* Returns FALSE on anything uncertain (no `.git` at all, unreadable, a submodule's `.git` file
|
|
68
80
|
* in a non-worktree checkout). False is the fail-open direction here: callers then print both
|
|
69
81
|
* forms rather than confidently printing the wrong one.
|
|
82
|
+
*
|
|
83
|
+
* This answers "am I in a linked worktree", which is a question about MESSAGE WORDING and nothing
|
|
84
|
+
* else. It is deliberately NOT a way to ask "which worktree am I in" so as to then act on that tree:
|
|
85
|
+
* a `currentWorktree(root)` used to exist for exactly that and it is deleted, because pnpm hoists a
|
|
86
|
+
* bin's cwd out of a nested `.claude/worktrees/**` tree and made the answer confidently wrong. Work
|
|
87
|
+
* that has to NAME a tree resolves it from a commit — see pr-gate's LandedTreeResolver.
|
|
70
88
|
*/
|
|
71
89
|
isLinkedWorktree(root: string): boolean;
|
|
72
90
|
/**
|
|
@@ -85,12 +103,6 @@ export declare class WorktreeService {
|
|
|
85
103
|
* second as the first tells an operator to go look for edits that are not there.
|
|
86
104
|
*/
|
|
87
105
|
workInFlight(worktreePath: string): WorktreeWorkInFlight;
|
|
88
|
-
/**
|
|
89
|
-
* The worktree record for the tree rooted at `root`, or null when it is not one of git's
|
|
90
|
-
* worktrees (or git could not answer). Callers use it to name the exact directory a
|
|
91
|
-
* `git worktree remove` has to take — a reap instruction with the wrong path is worse than none.
|
|
92
|
-
*/
|
|
93
|
-
currentWorktree(root: string): Worktree | null;
|
|
94
106
|
/**
|
|
95
107
|
* Parse the porcelain records. A record starts at a `worktree <path>` line and runs to the blank
|
|
96
108
|
* line; the FIRST record is the main worktree (git guarantees the ordering). A `prunable` worktree
|
package/src/worktrees.js
CHANGED
|
@@ -23,6 +23,7 @@ const to_error_1 = require("./to-error");
|
|
|
23
23
|
// main worktree. Keys are space-separated; `detached`, `bare` and `locked` may appear bare (no value).
|
|
24
24
|
const WORKTREE_KEY = 'worktree ';
|
|
25
25
|
const BRANCH_KEY = 'branch ';
|
|
26
|
+
const HEAD_KEY = 'HEAD ';
|
|
26
27
|
const LOCKED_KEY = 'locked ';
|
|
27
28
|
const REFS_HEADS = 'refs/heads/';
|
|
28
29
|
/**
|
|
@@ -46,6 +47,18 @@ class Worktree {
|
|
|
46
47
|
path;
|
|
47
48
|
// Short branch name (refs/heads/ stripped). Empty when the worktree is detached or bare.
|
|
48
49
|
branch;
|
|
50
|
+
/**
|
|
51
|
+
* The commit this worktree's HEAD is at, verbatim from the `HEAD <sha>` line every porcelain record
|
|
52
|
+
* already carries. '' only for a bare worktree, which has no HEAD at all.
|
|
53
|
+
*
|
|
54
|
+
* Load-bearing, and the reason it stopped being dropped by the parser: a BRANCH NAME does not
|
|
55
|
+
* identify a tree. A second clone, or a stale worktree, can hold the same name at a different
|
|
56
|
+
* commit — which is the wrong-objects-under-the-right-name failure `wp-land-pr` already refused to
|
|
57
|
+
* make from the tree it happened to be standing in. Pairing the name with this sha turns that veto
|
|
58
|
+
* into a SELECTION: the tree to archive from and reap is the one whose HEAD is the exact commit
|
|
59
|
+
* GitHub squashed, whichever directory the operator is standing in. See LandedTreeResolver.
|
|
60
|
+
*/
|
|
61
|
+
head;
|
|
49
62
|
// The primary clone — the one that owns .git. Never counted against the cap, never removable.
|
|
50
63
|
isMain;
|
|
51
64
|
// git already knows this worktree's directory is gone; `git worktree prune` will clear it.
|
|
@@ -62,13 +75,17 @@ class Worktree {
|
|
|
62
75
|
*/
|
|
63
76
|
lockReason;
|
|
64
77
|
// eslint-disable-next-line @typescript-eslint/max-params
|
|
65
|
-
constructor(path, branch,
|
|
78
|
+
constructor(path, branch,
|
|
79
|
+
// REQUIRED, with no default, for the same reason lockReason is: a defaulted '' would read as
|
|
80
|
+
// "this tree is not at any commit", and every sha comparison built on it would silently decline.
|
|
81
|
+
head, isMain, prunable, locked,
|
|
66
82
|
// REQUIRED, with no default. A 5-arg construction would silently mean "no reason recorded",
|
|
67
83
|
// which routes straight back to the bug this exists to fix — every agent lock unreadable and
|
|
68
84
|
// its worktree spared forever. A missing argument must be a compile error, not a quiet ''.
|
|
69
85
|
lockReason) {
|
|
70
86
|
this.path = path;
|
|
71
87
|
this.branch = branch;
|
|
88
|
+
this.head = head;
|
|
72
89
|
this.isMain = isMain;
|
|
73
90
|
this.prunable = prunable;
|
|
74
91
|
this.locked = locked;
|
|
@@ -131,6 +148,12 @@ let WorktreeService = class WorktreeService {
|
|
|
131
148
|
* Returns FALSE on anything uncertain (no `.git` at all, unreadable, a submodule's `.git` file
|
|
132
149
|
* in a non-worktree checkout). False is the fail-open direction here: callers then print both
|
|
133
150
|
* forms rather than confidently printing the wrong one.
|
|
151
|
+
*
|
|
152
|
+
* This answers "am I in a linked worktree", which is a question about MESSAGE WORDING and nothing
|
|
153
|
+
* else. It is deliberately NOT a way to ask "which worktree am I in" so as to then act on that tree:
|
|
154
|
+
* a `currentWorktree(root)` used to exist for exactly that and it is deleted, because pnpm hoists a
|
|
155
|
+
* bin's cwd out of a nested `.claude/worktrees/**` tree and made the answer confidently wrong. Work
|
|
156
|
+
* that has to NAME a tree resolves it from a commit — see pr-gate's LandedTreeResolver.
|
|
134
157
|
*/
|
|
135
158
|
isLinkedWorktree(root) {
|
|
136
159
|
// eslint-disable-next-line @webpieces/no-unmanaged-exceptions
|
|
@@ -168,19 +191,6 @@ let WorktreeService = class WorktreeService {
|
|
|
168
191
|
}
|
|
169
192
|
return new WorktreeWorkInFlight(false, '');
|
|
170
193
|
}
|
|
171
|
-
/**
|
|
172
|
-
* The worktree record for the tree rooted at `root`, or null when it is not one of git's
|
|
173
|
-
* worktrees (or git could not answer). Callers use it to name the exact directory a
|
|
174
|
-
* `git worktree remove` has to take — a reap instruction with the wrong path is worse than none.
|
|
175
|
-
*/
|
|
176
|
-
currentWorktree(root) {
|
|
177
|
-
const resolved = path.resolve(root);
|
|
178
|
-
for (const tree of this.listWorktrees(root)) {
|
|
179
|
-
if (path.resolve(tree.path) === resolved)
|
|
180
|
-
return tree;
|
|
181
|
-
}
|
|
182
|
-
return null;
|
|
183
|
-
}
|
|
184
194
|
/**
|
|
185
195
|
* Parse the porcelain records. A record starts at a `worktree <path>` line and runs to the blank
|
|
186
196
|
* line; the FIRST record is the main worktree (git guarantees the ordering). A `prunable` worktree
|
|
@@ -190,15 +200,17 @@ let WorktreeService = class WorktreeService {
|
|
|
190
200
|
const trees = [];
|
|
191
201
|
let path = '';
|
|
192
202
|
let branch = '';
|
|
203
|
+
let head = '';
|
|
193
204
|
let prunable = false;
|
|
194
205
|
let locked = false;
|
|
195
206
|
let lockReason = '';
|
|
196
207
|
const flush = () => {
|
|
197
208
|
if (path === '')
|
|
198
209
|
return;
|
|
199
|
-
trees.push(new Worktree(path, branch, trees.length === 0, prunable, locked, lockReason));
|
|
210
|
+
trees.push(new Worktree(path, branch, head, trees.length === 0, prunable, locked, lockReason));
|
|
200
211
|
path = '';
|
|
201
212
|
branch = '';
|
|
213
|
+
head = '';
|
|
202
214
|
prunable = false;
|
|
203
215
|
locked = false;
|
|
204
216
|
lockReason = '';
|
|
@@ -213,6 +225,10 @@ let WorktreeService = class WorktreeService {
|
|
|
213
225
|
flush();
|
|
214
226
|
path = line.slice(WORKTREE_KEY.length).trim();
|
|
215
227
|
}
|
|
228
|
+
else if (line.startsWith(HEAD_KEY)) {
|
|
229
|
+
// Already on the wire — git emits it for every non-bare record, detached ones included.
|
|
230
|
+
head = line.slice(HEAD_KEY.length).trim();
|
|
231
|
+
}
|
|
216
232
|
else if (line.startsWith(BRANCH_KEY)) {
|
|
217
233
|
const ref = line.slice(BRANCH_KEY.length).trim();
|
|
218
234
|
branch = ref.startsWith(REFS_HEADS) ? ref.slice(REFS_HEADS.length) : ref;
|
package/src/worktrees.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"worktrees.js","sourceRoot":"","sources":["../../../../../packages/tooling/rules-config/src/worktrees.ts"],"names":[],"mappings":"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","sourcesContent":["import { spawnSync } from 'child_process';\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport { injectable, bindingScopeValues } from 'inversify';\n\nimport { toError } from './to-error';\n\n/**\n * Reading the git worktree list.\n *\n * WHY this exists separately from merged-branches: a worktree is a SECOND budget. Every worktree holds\n * a branch, so if worktree-held branches also counted against the local-branch cap, five worktrees\n * would consume the entire branch budget and no branch could ever be created again. The two are\n * therefore counted apart — held branches against the worktree cap, parked branches against the branch\n * cap — and this service is what tells the two apart.\n *\n * Unlike the merged-PR lookup, everything here is LOCAL and cheap (one `git worktree list`), so it is\n * safe to call on the guard's blocking path.\n */\n\n// `git worktree list --porcelain` emits blank-line-separated records. The first record is always the\n// main worktree. Keys are space-separated; `detached`, `bare` and `locked` may appear bare (no value).\nconst WORKTREE_KEY = 'worktree ';\nconst BRANCH_KEY = 'branch ';\nconst LOCKED_KEY = 'locked ';\nconst REFS_HEADS = 'refs/heads/';\n\n/**\n * Whether a worktree is being worked in, and the reason in the words an operator gets shown.\n *\n * Data-only (per CLAUDE.md, classes for data). A bare boolean was the first cut and it collapsed\n * \"there are uncommitted files here\" into \"git would not answer me\" — two different things to tell\n * somebody who is deciding whether a directory is safe to remove.\n */\nexport class WorktreeWorkInFlight {\n held: boolean;\n reason: string;\n\n constructor(held: boolean, reason: string) {\n this.held = held;\n this.reason = reason;\n }\n}\n\n// Data-only (per CLAUDE.md, classes for data).\nexport class Worktree {\n path: string;\n // Short branch name (refs/heads/ stripped). Empty when the worktree is detached or bare.\n branch: string;\n // The primary clone — the one that owns .git. Never counted against the cap, never removable.\n isMain: boolean;\n // git already knows this worktree's directory is gone; `git worktree prune` will clear it.\n prunable: boolean;\n // Somebody ran `git worktree lock`. WHO is a question only `lockReason` can answer.\n locked: boolean;\n /**\n * The `--reason` text verbatim, '' when the lock carried none.\n *\n * Load-bearing, not decoration: the Claude Code agent harness locks every worktree it opens for a\n * subagent and writes a machine-readable reason naming the agent and its pid. Without this field\n * `locked` collapsed a live agent, a dead agent and a human into one verdict, and wp-cleanup\n * reported all three as \"locked by a human\". See agent-worktree-lock.ts.\n */\n lockReason: string;\n\n // eslint-disable-next-line @typescript-eslint/max-params\n constructor(\n path: string,\n branch: string,\n isMain: boolean,\n prunable: boolean,\n locked: boolean,\n // REQUIRED, with no default. A 5-arg construction would silently mean \"no reason recorded\",\n // which routes straight back to the bug this exists to fix — every agent lock unreadable and\n // its worktree spared forever. A missing argument must be a compile error, not a quiet ''.\n lockReason: string,\n ) {\n this.path = path;\n this.branch = branch;\n this.isMain = isMain;\n this.prunable = prunable;\n this.locked = locked;\n this.lockReason = lockReason;\n }\n}\n\n// Result of a captured git invocation: ok=false on spawn failure or non-zero exit.\ninterface CmdCapture {\n ok: boolean;\n out: string;\n}\n\n@injectable(bindingScopeValues.Singleton)\nexport class WorktreeService {\n /**\n * Every worktree, main one first. Fails SOFT to [] — a repo with no worktree support, or a git\n * that errors, must read as \"no worktrees\" so the cap fails OPEN rather than blocking on data we\n * do not have.\n */\n listWorktrees(repoRoot: string): Worktree[] {\n const result = this.capture(repoRoot, ['worktree', 'list', '--porcelain']);\n if (!result.ok || result.out === '') return [];\n return this.parsePorcelain(result.out);\n }\n\n // The linked worktrees — everything except the primary clone. This is what the cap counts.\n linkedWorktrees(repoRoot: string): Worktree[] {\n return this.listWorktrees(repoRoot).filter((tree: Worktree): boolean => !tree.isMain);\n }\n\n /**\n * Branch names checked out in ANY worktree, including the primary clone's own HEAD.\n *\n * Two callers, one reason: git flatly refuses to delete a branch that is checked out somewhere.\n * A held branch must never be proposed for `git branch -D` (the delete would fail and take the\n * whole reap command down with it), and it must not be counted as a parked branch either.\n */\n heldBranches(repoRoot: string): Set<string> {\n const held = new Set<string>();\n for (const tree of this.listWorktrees(repoRoot)) {\n if (tree.branch !== '') held.add(tree.branch);\n }\n return held;\n }\n\n /**\n * Am I standing in a LINKED worktree (as opposed to the primary clone)?\n *\n * This is the question every recovery message needs, because the two trees take different\n * commands: `git checkout main` fatals inside a linked worktree (\"main is already checked out\n * at <primary>\"), and a dead linked worktree is reaped with `git worktree remove`, not\n * `git branch -d`. A guard that cannot tell them apart must print BOTH forms and let the AI\n * guess — which is exactly how an AI ends up running the fatal one.\n *\n * The test is a single `statSync`, no process spawn: git gives a linked worktree a `.git` FILE\n * (a `gitdir:` pointer) where the primary clone has a `.git` DIRECTORY. This runs on the read\n * path, where reads vastly outnumber every other tool call, so the cost matters.\n *\n * It is a CHEAP FAST PATH over the authoritative answer, not a second authority. The authority is\n * `DotWebpieces.gitDirs` — `gitDir !== commonDir`, git's own canonical test, which also handles the\n * layouts a `.git` stat cannot see (`--separate-git-dir`, submodules). This is NOT calling it,\n * deliberately: that costs a `rev-parse` spawn per read. `worktree-identity.spec.ts` asserts the two\n * agree for the primary clone, an in-repo `.claude/worktrees/**` worktree and a sibling worktree\n * outside the repo — so if git ever changes the `.git` layout this fails there, not in the field.\n * Anything that needs to be RIGHT rather than cheap (tree identity, state paths) asks `gitDirs`.\n *\n * Returns FALSE on anything uncertain (no `.git` at all, unreadable, a submodule's `.git` file\n * in a non-worktree checkout). False is the fail-open direction here: callers then print both\n * forms rather than confidently printing the wrong one.\n */\n isLinkedWorktree(root: string): boolean {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n return !fs.statSync(path.join(root, '.git')).isDirectory();\n } catch (err: unknown) {\n const error = toError(err);\n void error;\n return false;\n }\n }\n\n /**\n * Is somebody working in this worktree — and if so, WHY do we say that?\n *\n * THE ONE CHECK THAT MAKES A ZERO-COMMIT WORKTREE UNREAPABLE. A ref with no commits of its own\n * loses nothing when it is deleted — that is a property of the REF. A DIRECTORY is different:\n * every worktree looks exactly like that husk from `git worktree add -b` until its first commit,\n * which is precisely the window an agent is working in. `git status --porcelain` is what tells\n * those two apart, and it is one cheap local spawn per candidate.\n *\n * It returns the REASON rather than a bare boolean because the two ways of being held are not the\n * same sentence to a human: \"it has uncommitted files\" is a fact about the tree, and \"git could\n * not tell me\" is a fact about our evidence. Both spare the worktree — unknown must fail SAFE,\n * since the fail-safe direction for deleting a directory is to leave it — but reporting the\n * second as the first tells an operator to go look for edits that are not there.\n */\n workInFlight(worktreePath: string): WorktreeWorkInFlight {\n const result = spawnSync('git', ['status', '--porcelain'], { cwd: worktreePath, encoding: 'utf8' });\n if (result.status !== 0 || typeof result.stdout !== 'string') {\n return new WorktreeWorkInFlight(true,\n '`git status` could not report on it, so it is not provably empty');\n }\n if (result.stdout.trim() !== '') {\n return new WorktreeWorkInFlight(true, 'has uncommitted or untracked files');\n }\n return new WorktreeWorkInFlight(false, '');\n }\n\n /**\n * The worktree record for the tree rooted at `root`, or null when it is not one of git's\n * worktrees (or git could not answer). Callers use it to name the exact directory a\n * `git worktree remove` has to take — a reap instruction with the wrong path is worse than none.\n */\n currentWorktree(root: string): Worktree | null {\n const resolved = path.resolve(root);\n for (const tree of this.listWorktrees(root)) {\n if (path.resolve(tree.path) === resolved) return tree;\n }\n return null;\n }\n\n /**\n * Parse the porcelain records. A record starts at a `worktree <path>` line and runs to the blank\n * line; the FIRST record is the main worktree (git guarantees the ordering). A `prunable` worktree\n * still appears in the list, which is exactly why it can be reaped.\n */\n private parsePorcelain(out: string): Worktree[] {\n const trees: Worktree[] = [];\n let path = '';\n let branch = '';\n let prunable = false;\n let locked = false;\n let lockReason = '';\n\n const flush = (): void => {\n if (path === '') return;\n trees.push(new Worktree(path, branch, trees.length === 0, prunable, locked, lockReason));\n path = '';\n branch = '';\n prunable = false;\n locked = false;\n lockReason = '';\n };\n\n for (const raw of out.split('\\n')) {\n const line = raw.trim();\n if (line === '') {\n flush();\n } else if (line.startsWith(WORKTREE_KEY)) {\n // A new record begins — flush the previous one in case the blank line was missing.\n flush();\n path = line.slice(WORKTREE_KEY.length).trim();\n } else if (line.startsWith(BRANCH_KEY)) {\n const ref = line.slice(BRANCH_KEY.length).trim();\n branch = ref.startsWith(REFS_HEADS) ? ref.slice(REFS_HEADS.length) : ref;\n } else if (line === 'prunable' || line.startsWith('prunable ')) {\n prunable = true;\n } else if (line === LOCKED_KEY.trim() || line.startsWith(LOCKED_KEY)) {\n locked = true;\n // `locked` alone is a lock with no --reason; `locked <text>` carries the reason verbatim.\n lockReason = line.slice(LOCKED_KEY.length).trim();\n }\n }\n flush();\n\n return trees;\n }\n\n // Run a git command capturing trimmed stdout; ok=false on spawn failure or non-zero exit.\n private capture(repoRoot: string, args: string[]): CmdCapture {\n const result = spawnSync('git', args, { cwd: repoRoot, encoding: 'utf8' });\n if (result.status !== 0 || typeof result.stdout !== 'string') return { ok: false, out: '' };\n return { ok: true, out: result.stdout.trim() };\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"worktrees.js","sourceRoot":"","sources":["../../../../../packages/tooling/rules-config/src/worktrees.ts"],"names":[],"mappings":"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","sourcesContent":["import { spawnSync } from 'child_process';\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport { injectable, bindingScopeValues } from 'inversify';\n\nimport { toError } from './to-error';\n\n/**\n * Reading the git worktree list.\n *\n * WHY this exists separately from merged-branches: a worktree is a SECOND budget. Every worktree holds\n * a branch, so if worktree-held branches also counted against the local-branch cap, five worktrees\n * would consume the entire branch budget and no branch could ever be created again. The two are\n * therefore counted apart — held branches against the worktree cap, parked branches against the branch\n * cap — and this service is what tells the two apart.\n *\n * Unlike the merged-PR lookup, everything here is LOCAL and cheap (one `git worktree list`), so it is\n * safe to call on the guard's blocking path.\n */\n\n// `git worktree list --porcelain` emits blank-line-separated records. The first record is always the\n// main worktree. Keys are space-separated; `detached`, `bare` and `locked` may appear bare (no value).\nconst WORKTREE_KEY = 'worktree ';\nconst BRANCH_KEY = 'branch ';\nconst HEAD_KEY = 'HEAD ';\nconst LOCKED_KEY = 'locked ';\nconst REFS_HEADS = 'refs/heads/';\n\n/**\n * Whether a worktree is being worked in, and the reason in the words an operator gets shown.\n *\n * Data-only (per CLAUDE.md, classes for data). A bare boolean was the first cut and it collapsed\n * \"there are uncommitted files here\" into \"git would not answer me\" — two different things to tell\n * somebody who is deciding whether a directory is safe to remove.\n */\nexport class WorktreeWorkInFlight {\n held: boolean;\n reason: string;\n\n constructor(held: boolean, reason: string) {\n this.held = held;\n this.reason = reason;\n }\n}\n\n// Data-only (per CLAUDE.md, classes for data).\nexport class Worktree {\n path: string;\n // Short branch name (refs/heads/ stripped). Empty when the worktree is detached or bare.\n branch: string;\n /**\n * The commit this worktree's HEAD is at, verbatim from the `HEAD <sha>` line every porcelain record\n * already carries. '' only for a bare worktree, which has no HEAD at all.\n *\n * Load-bearing, and the reason it stopped being dropped by the parser: a BRANCH NAME does not\n * identify a tree. A second clone, or a stale worktree, can hold the same name at a different\n * commit — which is the wrong-objects-under-the-right-name failure `wp-land-pr` already refused to\n * make from the tree it happened to be standing in. Pairing the name with this sha turns that veto\n * into a SELECTION: the tree to archive from and reap is the one whose HEAD is the exact commit\n * GitHub squashed, whichever directory the operator is standing in. See LandedTreeResolver.\n */\n head: string;\n // The primary clone — the one that owns .git. Never counted against the cap, never removable.\n isMain: boolean;\n // git already knows this worktree's directory is gone; `git worktree prune` will clear it.\n prunable: boolean;\n // Somebody ran `git worktree lock`. WHO is a question only `lockReason` can answer.\n locked: boolean;\n /**\n * The `--reason` text verbatim, '' when the lock carried none.\n *\n * Load-bearing, not decoration: the Claude Code agent harness locks every worktree it opens for a\n * subagent and writes a machine-readable reason naming the agent and its pid. Without this field\n * `locked` collapsed a live agent, a dead agent and a human into one verdict, and wp-cleanup\n * reported all three as \"locked by a human\". See agent-worktree-lock.ts.\n */\n lockReason: string;\n\n // eslint-disable-next-line @typescript-eslint/max-params\n constructor(\n path: string,\n branch: string,\n // REQUIRED, with no default, for the same reason lockReason is: a defaulted '' would read as\n // \"this tree is not at any commit\", and every sha comparison built on it would silently decline.\n head: string,\n isMain: boolean,\n prunable: boolean,\n locked: boolean,\n // REQUIRED, with no default. A 5-arg construction would silently mean \"no reason recorded\",\n // which routes straight back to the bug this exists to fix — every agent lock unreadable and\n // its worktree spared forever. A missing argument must be a compile error, not a quiet ''.\n lockReason: string,\n ) {\n this.path = path;\n this.branch = branch;\n this.head = head;\n this.isMain = isMain;\n this.prunable = prunable;\n this.locked = locked;\n this.lockReason = lockReason;\n }\n}\n\n// Result of a captured git invocation: ok=false on spawn failure or non-zero exit.\ninterface CmdCapture {\n ok: boolean;\n out: string;\n}\n\n@injectable(bindingScopeValues.Singleton)\nexport class WorktreeService {\n /**\n * Every worktree, main one first. Fails SOFT to [] — a repo with no worktree support, or a git\n * that errors, must read as \"no worktrees\" so the cap fails OPEN rather than blocking on data we\n * do not have.\n */\n listWorktrees(repoRoot: string): Worktree[] {\n const result = this.capture(repoRoot, ['worktree', 'list', '--porcelain']);\n if (!result.ok || result.out === '') return [];\n return this.parsePorcelain(result.out);\n }\n\n // The linked worktrees — everything except the primary clone. This is what the cap counts.\n linkedWorktrees(repoRoot: string): Worktree[] {\n return this.listWorktrees(repoRoot).filter((tree: Worktree): boolean => !tree.isMain);\n }\n\n /**\n * Branch names checked out in ANY worktree, including the primary clone's own HEAD.\n *\n * Two callers, one reason: git flatly refuses to delete a branch that is checked out somewhere.\n * A held branch must never be proposed for `git branch -D` (the delete would fail and take the\n * whole reap command down with it), and it must not be counted as a parked branch either.\n */\n heldBranches(repoRoot: string): Set<string> {\n const held = new Set<string>();\n for (const tree of this.listWorktrees(repoRoot)) {\n if (tree.branch !== '') held.add(tree.branch);\n }\n return held;\n }\n\n /**\n * Am I standing in a LINKED worktree (as opposed to the primary clone)?\n *\n * This is the question every recovery message needs, because the two trees take different\n * commands: `git checkout main` fatals inside a linked worktree (\"main is already checked out\n * at <primary>\"), and a dead linked worktree is reaped with `git worktree remove`, not\n * `git branch -d`. A guard that cannot tell them apart must print BOTH forms and let the AI\n * guess — which is exactly how an AI ends up running the fatal one.\n *\n * The test is a single `statSync`, no process spawn: git gives a linked worktree a `.git` FILE\n * (a `gitdir:` pointer) where the primary clone has a `.git` DIRECTORY. This runs on the read\n * path, where reads vastly outnumber every other tool call, so the cost matters.\n *\n * It is a CHEAP FAST PATH over the authoritative answer, not a second authority. The authority is\n * `DotWebpieces.gitDirs` — `gitDir !== commonDir`, git's own canonical test, which also handles the\n * layouts a `.git` stat cannot see (`--separate-git-dir`, submodules). This is NOT calling it,\n * deliberately: that costs a `rev-parse` spawn per read. `worktree-identity.spec.ts` asserts the two\n * agree for the primary clone, an in-repo `.claude/worktrees/**` worktree and a sibling worktree\n * outside the repo — so if git ever changes the `.git` layout this fails there, not in the field.\n * Anything that needs to be RIGHT rather than cheap (tree identity, state paths) asks `gitDirs`.\n *\n * Returns FALSE on anything uncertain (no `.git` at all, unreadable, a submodule's `.git` file\n * in a non-worktree checkout). False is the fail-open direction here: callers then print both\n * forms rather than confidently printing the wrong one.\n *\n * This answers \"am I in a linked worktree\", which is a question about MESSAGE WORDING and nothing\n * else. It is deliberately NOT a way to ask \"which worktree am I in\" so as to then act on that tree:\n * a `currentWorktree(root)` used to exist for exactly that and it is deleted, because pnpm hoists a\n * bin's cwd out of a nested `.claude/worktrees/**` tree and made the answer confidently wrong. Work\n * that has to NAME a tree resolves it from a commit — see pr-gate's LandedTreeResolver.\n */\n isLinkedWorktree(root: string): boolean {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n return !fs.statSync(path.join(root, '.git')).isDirectory();\n } catch (err: unknown) {\n const error = toError(err);\n void error;\n return false;\n }\n }\n\n /**\n * Is somebody working in this worktree — and if so, WHY do we say that?\n *\n * THE ONE CHECK THAT MAKES A ZERO-COMMIT WORKTREE UNREAPABLE. A ref with no commits of its own\n * loses nothing when it is deleted — that is a property of the REF. A DIRECTORY is different:\n * every worktree looks exactly like that husk from `git worktree add -b` until its first commit,\n * which is precisely the window an agent is working in. `git status --porcelain` is what tells\n * those two apart, and it is one cheap local spawn per candidate.\n *\n * It returns the REASON rather than a bare boolean because the two ways of being held are not the\n * same sentence to a human: \"it has uncommitted files\" is a fact about the tree, and \"git could\n * not tell me\" is a fact about our evidence. Both spare the worktree — unknown must fail SAFE,\n * since the fail-safe direction for deleting a directory is to leave it — but reporting the\n * second as the first tells an operator to go look for edits that are not there.\n */\n workInFlight(worktreePath: string): WorktreeWorkInFlight {\n const result = spawnSync('git', ['status', '--porcelain'], { cwd: worktreePath, encoding: 'utf8' });\n if (result.status !== 0 || typeof result.stdout !== 'string') {\n return new WorktreeWorkInFlight(true,\n '`git status` could not report on it, so it is not provably empty');\n }\n if (result.stdout.trim() !== '') {\n return new WorktreeWorkInFlight(true, 'has uncommitted or untracked files');\n }\n return new WorktreeWorkInFlight(false, '');\n }\n\n /**\n * Parse the porcelain records. A record starts at a `worktree <path>` line and runs to the blank\n * line; the FIRST record is the main worktree (git guarantees the ordering). A `prunable` worktree\n * still appears in the list, which is exactly why it can be reaped.\n */\n private parsePorcelain(out: string): Worktree[] {\n const trees: Worktree[] = [];\n let path = '';\n let branch = '';\n let head = '';\n let prunable = false;\n let locked = false;\n let lockReason = '';\n\n const flush = (): void => {\n if (path === '') return;\n trees.push(new Worktree(path, branch, head, trees.length === 0, prunable, locked, lockReason));\n path = '';\n branch = '';\n head = '';\n prunable = false;\n locked = false;\n lockReason = '';\n };\n\n for (const raw of out.split('\\n')) {\n const line = raw.trim();\n if (line === '') {\n flush();\n } else if (line.startsWith(WORKTREE_KEY)) {\n // A new record begins — flush the previous one in case the blank line was missing.\n flush();\n path = line.slice(WORKTREE_KEY.length).trim();\n } else if (line.startsWith(HEAD_KEY)) {\n // Already on the wire — git emits it for every non-bare record, detached ones included.\n head = line.slice(HEAD_KEY.length).trim();\n } else if (line.startsWith(BRANCH_KEY)) {\n const ref = line.slice(BRANCH_KEY.length).trim();\n branch = ref.startsWith(REFS_HEADS) ? ref.slice(REFS_HEADS.length) : ref;\n } else if (line === 'prunable' || line.startsWith('prunable ')) {\n prunable = true;\n } else if (line === LOCKED_KEY.trim() || line.startsWith(LOCKED_KEY)) {\n locked = true;\n // `locked` alone is a lock with no --reason; `locked <text>` carries the reason verbatim.\n lockReason = line.slice(LOCKED_KEY.length).trim();\n }\n }\n flush();\n\n return trees;\n }\n\n // Run a git command capturing trimmed stdout; ok=false on spawn failure or non-zero exit.\n private capture(repoRoot: string, args: string[]): CmdCapture {\n const result = spawnSync('git', args, { cwd: repoRoot, encoding: 'utf8' });\n if (result.status !== 0 || typeof result.stdout !== 'string') return { ok: false, out: '' };\n return { ok: true, out: result.stdout.trim() };\n }\n}\n"]}
|
|
@@ -207,11 +207,20 @@ flow are not interchangeable.
|
|
|
207
207
|
5. **`pnpm wp-finish-upsert-pr`** — STAGE ③: requires stage ②'s receipt, your `review.json` (its `title`
|
|
208
208
|
becomes the PR title) and every reviewer's verdict, then pushes and creates/updates the PR. It re-runs
|
|
209
209
|
the build gate ONLY if HEAD moved since stage ②, so the three stages still cost one build.
|
|
210
|
-
6. **`pnpm wp-land-pr`** — squash-merge
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
210
|
+
6. **`pnpm wp-land-pr`** — squash-merge an already-posted PR into main with the gated commit body, then
|
|
211
|
+
archive the pre-squash tip and reap the landed worktree. It runs no build gate and re-renders nothing
|
|
212
|
+
— the bytes that land are the bytes stage ③ produced. NOT gated on `pr-gate.mergeMode`: running it IS
|
|
213
|
+
the intent to merge, so it works on a `mergeMode: NONE` repo whose policy is "a human decides when".
|
|
214
|
+
|
|
215
|
+
**`pnpm wp-land-pr --pr <n>` lands SOMEBODY ELSE'S PR**, and it is the form a coordinator wants. With
|
|
216
|
+
no flag the PR is this branch's, which only the agent standing on that branch can ask for — and most
|
|
217
|
+
of the time that agent is gone by the time the PR is landable (CI was still running when it finished,
|
|
218
|
+
it errored, or the work was picked up an hour later), leaving its worktree a directory nobody is in.
|
|
219
|
+
`--pr <n>` names the PR outright, and the bookkeeping still finds the right tree: the worktree to
|
|
220
|
+
archive from and reap is the one whose HEAD is the exact commit GitHub squashed — matched on the PR's
|
|
221
|
+
`(headRefName, headRefOid)` PAIR, never on a branch name — so it resolves identically from the primary
|
|
222
|
+
clone, from that worktree, or from an unrelated one. When nothing local holds that commit, the merge
|
|
223
|
+
still happens and the archive/reap half is skipped out loud rather than tagging the wrong objects.
|
|
215
224
|
|
|
216
225
|
**The commit body is the PR DESCRIPTION**, which stage ③ writes as one compact string (PR link, risk,
|
|
217
226
|
non-green flags, short summary, build-command footer) and reuses verbatim as its `--body-file`. The
|
|
@@ -46,7 +46,9 @@ the option you pick EXACTLY as written and run nothing else on that line.
|
|
|
46
46
|
|
|
47
47
|
### `U` — guard bin missing AND @webpieces/ai-hook-rules is not declared in package.json
|
|
48
48
|
|
|
49
|
-
- **Option 1 (preferred)**: `
|
|
49
|
+
- **Option 1 (preferred)**: `git checkout main && git pull origin main` ← pick this when @webpieces/ai-hook-rules arrives WITH the umbrella, so a tree that is behind explains this without anything being mis-declared — sync first, then install
|
|
50
|
+
- **Option 2**: `pnpm install` ← pick this when the sync (or a raised catalog pin in pnpm-workspace.yaml, which is editable while the block is up) gave the installer something new to do — a BARE install with nothing changed reports "Lockfile is up to date" and leaves the tree as broken as it found it
|
|
51
|
+
- **Option 3**: `pnpm add -D @webpieces/ai-hook-rules` ← pick this when nothing else worked and you need this session back — it is a session unblock, NOT a fix: revert it with 'pnpm remove @webpieces/ai-hook-rules' before committing, because a direct root dependency on it violates the umbrella rule
|
|
50
52
|
|
|
51
53
|
### `K` — guard bin present but CRASHED (exit code not 0 or 2 — corrupt node_modules)
|
|
52
54
|
|