@webpieces/pr-gate 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 +3 -3
- package/src/scripts/commands/land-pr-command.d.ts +75 -6
- package/src/scripts/commands/land-pr-command.js +154 -38
- package/src/scripts/commands/land-pr-command.js.map +1 -1
- package/src/scripts/commands/reap-worktree-command.d.ts +18 -3
- package/src/scripts/commands/reap-worktree-command.js +44 -10
- package/src/scripts/commands/reap-worktree-command.js.map +1 -1
- package/src/scripts/pr-gate-app.d.ts +6 -3
- package/src/scripts/pr-gate-app.js +6 -3
- package/src/scripts/pr-gate-app.js.map +1 -1
- package/src/scripts/workflow/landed-tree-resolver.d.ts +75 -0
- package/src/scripts/workflow/landed-tree-resolver.js +112 -0
- package/src/scripts/workflow/landed-tree-resolver.js.map +1 -0
- package/src/scripts/workflow/landed-worktree-reaper.d.ts +42 -10
- package/src/scripts/workflow/landed-worktree-reaper.js +62 -21
- package/src/scripts/workflow/landed-worktree-reaper.js.map +1 -1
- package/src/scripts/wp-land-pr.js +14 -7
- package/src/scripts/wp-land-pr.js.map +1 -1
- package/src/scripts/wp-reap-worktree.js +4 -3
- package/src/scripts/wp-reap-worktree.js.map +1 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { RepoRootFinder } from '@webpieces/rules-config';
|
|
1
|
+
import { RepoRootFinder, WorktreeService } from '@webpieces/rules-config';
|
|
2
2
|
import { ReapOutcomeSignal } from '../workflow/reap-outcome';
|
|
3
3
|
import { WorktreeCleanupSection } from './worktree-cleanup';
|
|
4
4
|
/**
|
|
@@ -11,7 +11,8 @@ import { WorktreeCleanupSection } from './worktree-cleanup';
|
|
|
11
11
|
* PR. LandedWorktreeReaper spawns it by absolute path; see that class for why a child rather than a
|
|
12
12
|
* `process.chdir`.
|
|
13
13
|
*
|
|
14
|
-
* WHAT IT WILL NOT TAKE ON TRUST. The parent tells it a path
|
|
14
|
+
* WHAT IT WILL NOT TAKE ON TRUST. The parent tells it a path, a branch and the HEAD sha it selected the
|
|
15
|
+
* tree by; it believes none of the three.
|
|
15
16
|
* The verdict is recomputed here, from scratch, by the same MergedBranchesService that backs
|
|
16
17
|
* `wp-cleanup` — so a worktree is removed only when it is PROVABLY dead (its PR is merged), and never
|
|
17
18
|
* because a caller said so. That is what keeps "never remove a tree still holding unmerged work" true
|
|
@@ -25,7 +26,8 @@ export declare class ReapWorktreeCommand {
|
|
|
25
26
|
private readonly repoRootFinder;
|
|
26
27
|
private readonly worktreeSection;
|
|
27
28
|
private readonly signal;
|
|
28
|
-
|
|
29
|
+
private readonly worktrees;
|
|
30
|
+
constructor(repoRootFinder: RepoRootFinder, worktreeSection: WorktreeCleanupSection, signal: ReapOutcomeSignal, worktrees: WorktreeService);
|
|
29
31
|
/**
|
|
30
32
|
* Every exit from here states an OUTCOME, because the exit code cannot: refusals exit 0 (see
|
|
31
33
|
* `resolveTarget`), so a parent with only a status has no way to tell "removed" from "declined to
|
|
@@ -45,6 +47,19 @@ export declare class ReapWorktreeCommand {
|
|
|
45
47
|
* emits a ReapOutcomeSignal — the caller must be able to tell this exit 0 from a successful one.
|
|
46
48
|
*/
|
|
47
49
|
private resolveTarget;
|
|
50
|
+
/**
|
|
51
|
+
* That worktree's HEAD as git reports it right now, '' when git cannot place the path at all.
|
|
52
|
+
* '' can never equal a real sha, so "git would not answer" fails SAFE — it declines the removal.
|
|
53
|
+
*/
|
|
54
|
+
private headOf;
|
|
48
55
|
private render;
|
|
56
|
+
/**
|
|
57
|
+
* `<worktree-path> <branch> <head-sha>`, all three required. The sha is not optional and has no
|
|
58
|
+
* default: a defaulted '' would mean "do not check", i.e. exactly the name-only removal both
|
|
59
|
+
* processes exist to refuse, so a caller that omits it must fail to run rather than run wider.
|
|
60
|
+
*
|
|
61
|
+
* Thrown rather than printed: bad argv means the caller is broken, and there is no landed PR in this
|
|
62
|
+
* process whose success a non-zero exit could misreport.
|
|
63
|
+
*/
|
|
49
64
|
private parse;
|
|
50
65
|
}
|
|
@@ -18,7 +18,8 @@ const SEP = '━━━━━━━━━━━━━━━━━━━━━━
|
|
|
18
18
|
* PR. LandedWorktreeReaper spawns it by absolute path; see that class for why a child rather than a
|
|
19
19
|
* `process.chdir`.
|
|
20
20
|
*
|
|
21
|
-
* WHAT IT WILL NOT TAKE ON TRUST. The parent tells it a path
|
|
21
|
+
* WHAT IT WILL NOT TAKE ON TRUST. The parent tells it a path, a branch and the HEAD sha it selected the
|
|
22
|
+
* tree by; it believes none of the three.
|
|
22
23
|
* The verdict is recomputed here, from scratch, by the same MergedBranchesService that backs
|
|
23
24
|
* `wp-cleanup` — so a worktree is removed only when it is PROVABLY dead (its PR is merged), and never
|
|
24
25
|
* because a caller said so. That is what keeps "never remove a tree still holding unmerged work" true
|
|
@@ -32,10 +33,12 @@ let ReapWorktreeCommand = class ReapWorktreeCommand {
|
|
|
32
33
|
repoRootFinder;
|
|
33
34
|
worktreeSection;
|
|
34
35
|
signal;
|
|
35
|
-
|
|
36
|
+
worktrees;
|
|
37
|
+
constructor(repoRootFinder, worktreeSection, signal, worktrees) {
|
|
36
38
|
this.repoRootFinder = repoRootFinder;
|
|
37
39
|
this.worktreeSection = worktreeSection;
|
|
38
40
|
this.signal = signal;
|
|
41
|
+
this.worktrees = worktrees;
|
|
39
42
|
}
|
|
40
43
|
/**
|
|
41
44
|
* Every exit from here states an OUTCOME, because the exit code cannot: refusals exit 0 (see
|
|
@@ -90,6 +93,17 @@ let ReapWorktreeCommand = class ReapWorktreeCommand {
|
|
|
90
93
|
+ ' Refusing to remove a worktree whose branch changed since the PR landed.\n');
|
|
91
94
|
return null;
|
|
92
95
|
}
|
|
96
|
+
// …and the NAME is not the identity. The parent selected this directory because its HEAD was the
|
|
97
|
+
// exact commit GitHub squashed; that selection is recomputed here from git rather than taken on
|
|
98
|
+
// trust, so a commit made in that tree between landing and reaping — work the archive tag of the
|
|
99
|
+
// squashed tip does not contain — stops the removal instead of being buried with it.
|
|
100
|
+
const here = this.headOf(repoRoot, wanted);
|
|
101
|
+
if (here !== request.head) {
|
|
102
|
+
process.stdout.write(`\n ⚠️ ${request.worktreePath} is at ${here === '' ? '(unknown)' : here}, not the landed\n`
|
|
103
|
+
+ ` commit ${request.head}.\n`
|
|
104
|
+
+ ' Refusing to remove a worktree whose HEAD moved since the PR landed.\n');
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
93
107
|
if (!target.deletable) {
|
|
94
108
|
process.stdout.write(`\n ⚠️ ${request.worktreePath} is not provably dead: ${target.reason}\n`
|
|
95
109
|
+ ' Refusing to remove a worktree that may still hold unmerged work.\n');
|
|
@@ -97,6 +111,15 @@ let ReapWorktreeCommand = class ReapWorktreeCommand {
|
|
|
97
111
|
}
|
|
98
112
|
return target;
|
|
99
113
|
}
|
|
114
|
+
/**
|
|
115
|
+
* That worktree's HEAD as git reports it right now, '' when git cannot place the path at all.
|
|
116
|
+
* '' can never equal a real sha, so "git would not answer" fails SAFE — it declines the removal.
|
|
117
|
+
*/
|
|
118
|
+
headOf(repoRoot, resolvedPath) {
|
|
119
|
+
const tree = this.worktrees.listWorktrees(repoRoot)
|
|
120
|
+
.find((candidate) => path.resolve(candidate.path) === resolvedPath);
|
|
121
|
+
return tree?.head ?? '';
|
|
122
|
+
}
|
|
100
123
|
// The reap report, plus the spared case — which report() renders as '' because from wp-cleanup's
|
|
101
124
|
// point of view "nothing happened" is a whole answer. Here it never is: we asked for exactly one.
|
|
102
125
|
render(repoRoot, result) {
|
|
@@ -108,18 +131,25 @@ let ReapWorktreeCommand = class ReapWorktreeCommand {
|
|
|
108
131
|
}
|
|
109
132
|
return this.worktreeSection.report(repoRoot, result);
|
|
110
133
|
}
|
|
111
|
-
|
|
112
|
-
|
|
134
|
+
/**
|
|
135
|
+
* `<worktree-path> <branch> <head-sha>`, all three required. The sha is not optional and has no
|
|
136
|
+
* default: a defaulted '' would mean "do not check", i.e. exactly the name-only removal both
|
|
137
|
+
* processes exist to refuse, so a caller that omits it must fail to run rather than run wider.
|
|
138
|
+
*
|
|
139
|
+
* Thrown rather than printed: bad argv means the caller is broken, and there is no landed PR in this
|
|
140
|
+
* process whose success a non-zero exit could misreport.
|
|
141
|
+
*/
|
|
113
142
|
parse(args) {
|
|
114
143
|
const worktreePath = args[0] ?? '';
|
|
115
144
|
const branch = args[1] ?? '';
|
|
116
|
-
|
|
145
|
+
const head = args[2] ?? '';
|
|
146
|
+
if (worktreePath === '' || branch === '' || head === '') {
|
|
117
147
|
throw new rules_config_1.InformAiError('\n' + SEP + '❌ wp-reap-worktree: missing arguments\n' + SEP + '\n'
|
|
118
|
-
+ 'Usage: node wp-reap-worktree.js <worktree-path> <branch>\n\n'
|
|
148
|
+
+ 'Usage: node wp-reap-worktree.js <worktree-path> <branch> <head-sha>\n\n'
|
|
119
149
|
+ 'This is an INTERNAL entry point, spawned by `pnpm wp-land-pr` with cwd set to the\n'
|
|
120
150
|
+ 'primary clone. To clean up by hand, run `pnpm wp-cleanup` from the primary clone.\n' + SEP);
|
|
121
151
|
}
|
|
122
|
-
return new ReapRequest(worktreePath, branch);
|
|
152
|
+
return new ReapRequest(worktreePath, branch, head);
|
|
123
153
|
}
|
|
124
154
|
};
|
|
125
155
|
exports.ReapWorktreeCommand = ReapWorktreeCommand;
|
|
@@ -127,15 +157,19 @@ exports.ReapWorktreeCommand = ReapWorktreeCommand = tslib_1.__decorate([
|
|
|
127
157
|
(0, inversify_1.injectable)(inversify_1.bindingScopeValues.Singleton),
|
|
128
158
|
tslib_1.__metadata("design:paramtypes", [rules_config_1.RepoRootFinder,
|
|
129
159
|
worktree_cleanup_1.WorktreeCleanupSection,
|
|
130
|
-
reap_outcome_1.ReapOutcomeSignal
|
|
160
|
+
reap_outcome_1.ReapOutcomeSignal,
|
|
161
|
+
rules_config_1.WorktreeService])
|
|
131
162
|
], ReapWorktreeCommand);
|
|
132
|
-
// Data-only (per CLAUDE.md, classes for data): the
|
|
163
|
+
// Data-only (per CLAUDE.md, classes for data): the argv values this entry point takes.
|
|
133
164
|
class ReapRequest {
|
|
134
165
|
worktreePath;
|
|
135
166
|
branch;
|
|
136
|
-
|
|
167
|
+
/** The commit the parent proved this worktree was at — re-verified here, never trusted. */
|
|
168
|
+
head;
|
|
169
|
+
constructor(worktreePath, branch, head) {
|
|
137
170
|
this.worktreePath = worktreePath;
|
|
138
171
|
this.branch = branch;
|
|
172
|
+
this.head = head;
|
|
139
173
|
}
|
|
140
174
|
}
|
|
141
175
|
//# sourceMappingURL=reap-worktree-command.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"reap-worktree-command.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/pr-gate/src/scripts/commands/reap-worktree-command.ts"],"names":[],"mappings":"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","sourcesContent":["import * as path from 'path';\nimport {\n DeletableWorktree,\n InformAiError,\n RepoRootFinder,\n WorktreeReapResult,\n loadAndValidate,\n} from '@webpieces/rules-config';\nimport { injectable, bindingScopeValues } from 'inversify';\n\nimport {\n ReapOutcomeSignal,\n REAP_OUTCOME_FAILED,\n REAP_OUTCOME_REFUSED,\n REAP_OUTCOME_REMOVED,\n} from '../workflow/reap-outcome';\n\nimport { WorktreeCleanupSection } from './worktree-cleanup';\n\nconst SEP = '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\n';\n\n/**\n * The CHILD side of `wp-land-pr`'s re-exec: reap ONE named worktree, from a process whose cwd is the\n * primary clone.\n *\n * It is not a published `bin`. Nothing about it is a verb a human should reach for — `pnpm wp-cleanup`\n * is that verb, and it does strictly more. This exists so the parent can hand off a SINGLE, already\n * identified corpse without also reaping every other agent's worktree as a side effect of landing one\n * PR. LandedWorktreeReaper spawns it by absolute path; see that class for why a child rather than a\n * `process.chdir`.\n *\n * WHAT IT WILL NOT TAKE ON TRUST. The parent tells it a path and a branch; it believes neither.\n * The verdict is recomputed here, from scratch, by the same MergedBranchesService that backs\n * `wp-cleanup` — so a worktree is removed only when it is PROVABLY dead (its PR is merged), and never\n * because a caller said so. That is what keeps \"never remove a tree still holding unmerged work\" true\n * even if the parent is wrong, out of date, or someone runs this by hand.\n *\n * The primary clone can never be a target: `classifyWorktrees` drops the main worktree from the\n * verdicts outright, so a path pointing at it simply does not resolve to a target — and WorktreeReaper\n * refuses it a second time by name. Neither rail is load-bearing alone.\n */\n@injectable(bindingScopeValues.Singleton)\nexport class ReapWorktreeCommand {\n constructor(\n private readonly repoRootFinder: RepoRootFinder,\n private readonly worktreeSection: WorktreeCleanupSection,\n private readonly signal: ReapOutcomeSignal,\n ) {}\n\n /**\n * Every exit from here states an OUTCOME, because the exit code cannot: refusals exit 0 (see\n * `resolveTarget`), so a parent with only a status has no way to tell \"removed\" from \"declined to\n * remove\". The token is the last thing written, and LandedWorktreeReaper strips it before printing.\n */\n async run(args: string[]): Promise<void> {\n await Promise.resolve();\n const request = this.parse(args);\n const repoRoot = this.repoRootFinder.resolveRepoRoot(process.cwd());\n\n const target = this.resolveTarget(repoRoot, request);\n if (target === null) {\n process.stdout.write(this.signal.line(REAP_OUTCOME_REFUSED));\n return;\n }\n\n const retention = loadAndValidate(repoRoot).prGate.landPr.branchRetention;\n const result = this.worktreeSection.reap(repoRoot, 'wp-land-pr', [target], retention);\n process.stdout.write(this.render(repoRoot, result));\n process.stdout.write(this.signal.line(this.outcomeOf(result)));\n }\n\n /**\n * `reaped` is the only entry that means the DIRECTORY is gone. A tree that reached the reaper and\n * came back in neither list was spared by one of WorktreeReaper's own rails (it is the cwd, it is\n * the primary clone) — nothing was attempted, which is a refusal, not a failure.\n */\n private outcomeOf(result: WorktreeReapResult): string {\n if (result.reaped.length > 0) return REAP_OUTCOME_REMOVED;\n return result.failed.length > 0 ? REAP_OUTCOME_FAILED : REAP_OUTCOME_REFUSED;\n }\n\n /**\n * The verdict for the requested path, or null after printing WHY it is not reapable. Refusals are\n * printed rather than thrown: the PR they follow is already merged, and exiting non-zero after a\n * successful landing would report a landed PR as a failed command. That is exactly why `run` also\n * emits a ReapOutcomeSignal — the caller must be able to tell this exit 0 from a successful one.\n */\n private resolveTarget(repoRoot: string, request: ReapRequest): DeletableWorktree | null {\n const wanted = path.resolve(request.worktreePath);\n // `false`: this reaps ONE named worktree after its PR landed, and it is not the place to\n // override anybody's lock — a lock still standing here is exactly the refusal this prints.\n const target = this.worktreeSection.verdicts(repoRoot, false)\n .find((tree: DeletableWorktree): boolean => path.resolve(tree.path) === wanted);\n\n if (target === undefined) {\n process.stdout.write(\n `\\n ℹ️ ${request.worktreePath} is not a removable worktree of this repo (it may be the\\n`\n + ' primary clone, or already gone). Nothing removed.\\n');\n return null;\n }\n // The branch moved under us between landing and reaping — somebody checked something else out\n // there. Whatever that is, it is not the corpse we were asked to bury.\n if (target.branch !== request.branch) {\n process.stdout.write(\n `\\n ⚠️ ${request.worktreePath} now holds '${target.branch}', not '${request.branch}'.\\n`\n + ' Refusing to remove a worktree whose branch changed since the PR landed.\\n');\n return null;\n }\n if (!target.deletable) {\n process.stdout.write(\n `\\n ⚠️ ${request.worktreePath} is not provably dead: ${target.reason}\\n`\n + ' Refusing to remove a worktree that may still hold unmerged work.\\n');\n return null;\n }\n return target;\n }\n\n // The reap report, plus the spared case — which report() renders as '' because from wp-cleanup's\n // point of view \"nothing happened\" is a whole answer. Here it never is: we asked for exactly one.\n private render(repoRoot: string, result: WorktreeReapResult): string {\n if (result.reaped.length === 0 && result.failed.length === 0) {\n let out = '\\n' + SEP + '🛑 Nothing removed\\n' + SEP + '\\n';\n for (const tree of result.spared) out += ` · ${tree.path} — ${tree.reason}\\n`;\n return out;\n }\n return this.worktreeSection.report(repoRoot, result);\n }\n\n // `<worktree-path> <branch>`, both required. Thrown rather than printed: bad argv means the caller\n // is broken, and there is no landed PR in this process whose success a non-zero exit could misreport.\n private parse(args: string[]): ReapRequest {\n const worktreePath = args[0] ?? '';\n const branch = args[1] ?? '';\n if (worktreePath === '' || branch === '') {\n throw new InformAiError(\n '\\n' + SEP + '❌ wp-reap-worktree: missing arguments\\n' + SEP + '\\n'\n + 'Usage: node wp-reap-worktree.js <worktree-path> <branch>\\n\\n'\n + 'This is an INTERNAL entry point, spawned by `pnpm wp-land-pr` with cwd set to the\\n'\n + 'primary clone. To clean up by hand, run `pnpm wp-cleanup` from the primary clone.\\n' + SEP);\n }\n return new ReapRequest(worktreePath, branch);\n }\n}\n\n// Data-only (per CLAUDE.md, classes for data): the pair of argv values this entry point takes.\nclass ReapRequest {\n worktreePath: string;\n branch: string;\n\n constructor(worktreePath: string, branch: string) {\n this.worktreePath = worktreePath;\n this.branch = branch;\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"reap-worktree-command.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/pr-gate/src/scripts/commands/reap-worktree-command.ts"],"names":[],"mappings":"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","sourcesContent":["import * as path from 'path';\nimport {\n DeletableWorktree,\n InformAiError,\n RepoRootFinder,\n Worktree,\n WorktreeReapResult,\n WorktreeService,\n loadAndValidate,\n} from '@webpieces/rules-config';\nimport { injectable, bindingScopeValues } from 'inversify';\n\nimport {\n ReapOutcomeSignal,\n REAP_OUTCOME_FAILED,\n REAP_OUTCOME_REFUSED,\n REAP_OUTCOME_REMOVED,\n} from '../workflow/reap-outcome';\n\nimport { WorktreeCleanupSection } from './worktree-cleanup';\n\nconst SEP = '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\n';\n\n/**\n * The CHILD side of `wp-land-pr`'s re-exec: reap ONE named worktree, from a process whose cwd is the\n * primary clone.\n *\n * It is not a published `bin`. Nothing about it is a verb a human should reach for — `pnpm wp-cleanup`\n * is that verb, and it does strictly more. This exists so the parent can hand off a SINGLE, already\n * identified corpse without also reaping every other agent's worktree as a side effect of landing one\n * PR. LandedWorktreeReaper spawns it by absolute path; see that class for why a child rather than a\n * `process.chdir`.\n *\n * WHAT IT WILL NOT TAKE ON TRUST. The parent tells it a path, a branch and the HEAD sha it selected the\n * tree by; it believes none of the three.\n * The verdict is recomputed here, from scratch, by the same MergedBranchesService that backs\n * `wp-cleanup` — so a worktree is removed only when it is PROVABLY dead (its PR is merged), and never\n * because a caller said so. That is what keeps \"never remove a tree still holding unmerged work\" true\n * even if the parent is wrong, out of date, or someone runs this by hand.\n *\n * The primary clone can never be a target: `classifyWorktrees` drops the main worktree from the\n * verdicts outright, so a path pointing at it simply does not resolve to a target — and WorktreeReaper\n * refuses it a second time by name. Neither rail is load-bearing alone.\n */\n@injectable(bindingScopeValues.Singleton)\nexport class ReapWorktreeCommand {\n constructor(\n private readonly repoRootFinder: RepoRootFinder,\n private readonly worktreeSection: WorktreeCleanupSection,\n private readonly signal: ReapOutcomeSignal,\n private readonly worktrees: WorktreeService,\n ) {}\n\n /**\n * Every exit from here states an OUTCOME, because the exit code cannot: refusals exit 0 (see\n * `resolveTarget`), so a parent with only a status has no way to tell \"removed\" from \"declined to\n * remove\". The token is the last thing written, and LandedWorktreeReaper strips it before printing.\n */\n async run(args: string[]): Promise<void> {\n await Promise.resolve();\n const request = this.parse(args);\n const repoRoot = this.repoRootFinder.resolveRepoRoot(process.cwd());\n\n const target = this.resolveTarget(repoRoot, request);\n if (target === null) {\n process.stdout.write(this.signal.line(REAP_OUTCOME_REFUSED));\n return;\n }\n\n const retention = loadAndValidate(repoRoot).prGate.landPr.branchRetention;\n const result = this.worktreeSection.reap(repoRoot, 'wp-land-pr', [target], retention);\n process.stdout.write(this.render(repoRoot, result));\n process.stdout.write(this.signal.line(this.outcomeOf(result)));\n }\n\n /**\n * `reaped` is the only entry that means the DIRECTORY is gone. A tree that reached the reaper and\n * came back in neither list was spared by one of WorktreeReaper's own rails (it is the cwd, it is\n * the primary clone) — nothing was attempted, which is a refusal, not a failure.\n */\n private outcomeOf(result: WorktreeReapResult): string {\n if (result.reaped.length > 0) return REAP_OUTCOME_REMOVED;\n return result.failed.length > 0 ? REAP_OUTCOME_FAILED : REAP_OUTCOME_REFUSED;\n }\n\n /**\n * The verdict for the requested path, or null after printing WHY it is not reapable. Refusals are\n * printed rather than thrown: the PR they follow is already merged, and exiting non-zero after a\n * successful landing would report a landed PR as a failed command. That is exactly why `run` also\n * emits a ReapOutcomeSignal — the caller must be able to tell this exit 0 from a successful one.\n */\n private resolveTarget(repoRoot: string, request: ReapRequest): DeletableWorktree | null {\n const wanted = path.resolve(request.worktreePath);\n // `false`: this reaps ONE named worktree after its PR landed, and it is not the place to\n // override anybody's lock — a lock still standing here is exactly the refusal this prints.\n const target = this.worktreeSection.verdicts(repoRoot, false)\n .find((tree: DeletableWorktree): boolean => path.resolve(tree.path) === wanted);\n\n if (target === undefined) {\n process.stdout.write(\n `\\n ℹ️ ${request.worktreePath} is not a removable worktree of this repo (it may be the\\n`\n + ' primary clone, or already gone). Nothing removed.\\n');\n return null;\n }\n // The branch moved under us between landing and reaping — somebody checked something else out\n // there. Whatever that is, it is not the corpse we were asked to bury.\n if (target.branch !== request.branch) {\n process.stdout.write(\n `\\n ⚠️ ${request.worktreePath} now holds '${target.branch}', not '${request.branch}'.\\n`\n + ' Refusing to remove a worktree whose branch changed since the PR landed.\\n');\n return null;\n }\n // …and the NAME is not the identity. The parent selected this directory because its HEAD was the\n // exact commit GitHub squashed; that selection is recomputed here from git rather than taken on\n // trust, so a commit made in that tree between landing and reaping — work the archive tag of the\n // squashed tip does not contain — stops the removal instead of being buried with it.\n const here = this.headOf(repoRoot, wanted);\n if (here !== request.head) {\n process.stdout.write(\n `\\n ⚠️ ${request.worktreePath} is at ${here === '' ? '(unknown)' : here}, not the landed\\n`\n + ` commit ${request.head}.\\n`\n + ' Refusing to remove a worktree whose HEAD moved since the PR landed.\\n');\n return null;\n }\n if (!target.deletable) {\n process.stdout.write(\n `\\n ⚠️ ${request.worktreePath} is not provably dead: ${target.reason}\\n`\n + ' Refusing to remove a worktree that may still hold unmerged work.\\n');\n return null;\n }\n return target;\n }\n\n /**\n * That worktree's HEAD as git reports it right now, '' when git cannot place the path at all.\n * '' can never equal a real sha, so \"git would not answer\" fails SAFE — it declines the removal.\n */\n private headOf(repoRoot: string, resolvedPath: string): string {\n const tree = this.worktrees.listWorktrees(repoRoot)\n .find((candidate: Worktree): boolean => path.resolve(candidate.path) === resolvedPath);\n return tree?.head ?? '';\n }\n\n // The reap report, plus the spared case — which report() renders as '' because from wp-cleanup's\n // point of view \"nothing happened\" is a whole answer. Here it never is: we asked for exactly one.\n private render(repoRoot: string, result: WorktreeReapResult): string {\n if (result.reaped.length === 0 && result.failed.length === 0) {\n let out = '\\n' + SEP + '🛑 Nothing removed\\n' + SEP + '\\n';\n for (const tree of result.spared) out += ` · ${tree.path} — ${tree.reason}\\n`;\n return out;\n }\n return this.worktreeSection.report(repoRoot, result);\n }\n\n /**\n * `<worktree-path> <branch> <head-sha>`, all three required. The sha is not optional and has no\n * default: a defaulted '' would mean \"do not check\", i.e. exactly the name-only removal both\n * processes exist to refuse, so a caller that omits it must fail to run rather than run wider.\n *\n * Thrown rather than printed: bad argv means the caller is broken, and there is no landed PR in this\n * process whose success a non-zero exit could misreport.\n */\n private parse(args: string[]): ReapRequest {\n const worktreePath = args[0] ?? '';\n const branch = args[1] ?? '';\n const head = args[2] ?? '';\n if (worktreePath === '' || branch === '' || head === '') {\n throw new InformAiError(\n '\\n' + SEP + '❌ wp-reap-worktree: missing arguments\\n' + SEP + '\\n'\n + 'Usage: node wp-reap-worktree.js <worktree-path> <branch> <head-sha>\\n\\n'\n + 'This is an INTERNAL entry point, spawned by `pnpm wp-land-pr` with cwd set to the\\n'\n + 'primary clone. To clean up by hand, run `pnpm wp-cleanup` from the primary clone.\\n' + SEP);\n }\n return new ReapRequest(worktreePath, branch, head);\n }\n}\n\n// Data-only (per CLAUDE.md, classes for data): the argv values this entry point takes.\nclass ReapRequest {\n worktreePath: string;\n branch: string;\n /** The commit the parent proved this worktree was at — re-verified here, never trusted. */\n head: string;\n\n constructor(worktreePath: string, branch: string, head: string) {\n this.worktreePath = worktreePath;\n this.branch = branch;\n this.head = head;\n }\n}\n"]}
|
|
@@ -5,7 +5,7 @@ import { FinishUpsertPrCommand } from './commands/finish-upsert-pr-command';
|
|
|
5
5
|
import { CleanupCommand } from './commands/cleanup-command';
|
|
6
6
|
import { CleanupOptions } from './commands/cleanup-options';
|
|
7
7
|
import { CheckoutCleanMainCommand } from './commands/checkout-clean-main-command';
|
|
8
|
-
import { LandPrCommand } from './commands/land-pr-command';
|
|
8
|
+
import { LandPrCommand, LandPrRequest } from './commands/land-pr-command';
|
|
9
9
|
import { CheckPrCommand } from './commands/check-pr-command';
|
|
10
10
|
import { ReviewUpsertPrCommand, ReviewUpsertPrOptions } from './commands/review-upsert-pr-command';
|
|
11
11
|
import { ReapWorktreeCommand } from './commands/reap-worktree-command';
|
|
@@ -82,8 +82,11 @@ export declare class PrGateApp {
|
|
|
82
82
|
* rather than surviving beside this.
|
|
83
83
|
*/
|
|
84
84
|
checkoutCleanMain(): Promise<void>;
|
|
85
|
-
/**
|
|
86
|
-
|
|
85
|
+
/**
|
|
86
|
+
* `wp-land-pr`: squash-merge a PR into main with the compact commit body — this branch's by default,
|
|
87
|
+
* or the one `--pr <n>` names, which is how a coordinator finishes a gone agent's work.
|
|
88
|
+
*/
|
|
89
|
+
landPr(request: LandPrRequest): Promise<void>;
|
|
87
90
|
/**
|
|
88
91
|
* `wp-review-upsert-pr`: STAGE ② — validate the 3-point merge, run the build gate, extract this
|
|
89
92
|
* branch's diff, and brief the reviewer subagents. Unlike the report-only command it replaces, this CAN
|
|
@@ -133,10 +133,13 @@ let PrGateApp = class PrGateApp {
|
|
|
133
133
|
this.assertNoResolveInProgress('wp-checkout-clean-main');
|
|
134
134
|
return this.checkoutCleanMainCommand.run();
|
|
135
135
|
}
|
|
136
|
-
/**
|
|
137
|
-
|
|
136
|
+
/**
|
|
137
|
+
* `wp-land-pr`: squash-merge a PR into main with the compact commit body — this branch's by default,
|
|
138
|
+
* or the one `--pr <n>` names, which is how a coordinator finishes a gone agent's work.
|
|
139
|
+
*/
|
|
140
|
+
landPr(request) {
|
|
138
141
|
this.assertNoResolveInProgress('wp-land-pr');
|
|
139
|
-
return this.landPrCommand.run();
|
|
142
|
+
return this.landPrCommand.run(request);
|
|
140
143
|
}
|
|
141
144
|
/**
|
|
142
145
|
* `wp-review-upsert-pr`: STAGE ② — validate the 3-point merge, run the build gate, extract this
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pr-gate-app.js","sourceRoot":"","sources":["../../../../../../packages/tooling/pr-gate/src/scripts/pr-gate-app.ts"],"names":[],"mappings":"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","sourcesContent":["import { DocumentDesign } from '@webpieces/rules-config';\nimport { injectable, bindingScopeValues } from 'inversify';\nimport { StartUpdateCommand } from './commands/start-update-command';\nimport { FinishUpdateCommand } from './commands/finish-update-command';\nimport { StartUpsertPrCommand } from './commands/start-upsert-pr-command';\nimport { FinishUpsertPrCommand } from './commands/finish-upsert-pr-command';\nimport { CleanupCommand } from './commands/cleanup-command';\nimport { CleanupOptions } from './commands/cleanup-options';\nimport { CheckoutCleanMainCommand } from './commands/checkout-clean-main-command';\nimport { LandPrCommand } from './commands/land-pr-command';\nimport { CheckPrCommand } from './commands/check-pr-command';\nimport { ReviewUpsertPrCommand, ReviewUpsertPrOptions } from './commands/review-upsert-pr-command';\nimport { ReapWorktreeCommand } from './commands/reap-worktree-command';\nimport { BuildCommand, BuildOptions } from './commands/build-command';\nimport { PushDevCommand, PushDevOptions } from './commands/push-dev-command';\nimport { FinishPushDevCommand, FinishPushDevOptions } from './commands/finish-push-dev-command';\nimport { PushDevStateStore } from './workflow/push-dev-state';\nimport { RepoRootFinder } from '@webpieces/rules-config';\n\n/**\n * The pr-gate application root. `container.get(PrGateApp)` resolves the entire workflow DAG (the command\n * classes → the injected git/merge/dashboard services). `@DocumentDesign` marks it the\n * top-of-DAG the DI-design analyzer roots on, so `role:app` pr-gate draws its design. Each `bin/*`\n * entry resolves THIS and calls the matching command method.\n */\n@DocumentDesign()\n@injectable(bindingScopeValues.Singleton)\nexport class PrGateApp {\n constructor(\n private readonly startUpdateCommand: StartUpdateCommand,\n private readonly finishUpdateCommand: FinishUpdateCommand,\n private readonly startUpsertPrCommand: StartUpsertPrCommand,\n private readonly finishUpsertPrCommand: FinishUpsertPrCommand,\n private readonly cleanupCommand: CleanupCommand,\n private readonly checkoutCleanMainCommand: CheckoutCleanMainCommand,\n private readonly landPrCommand: LandPrCommand,\n private readonly checkPrCommand: CheckPrCommand,\n private readonly reviewUpsertPrCommand: ReviewUpsertPrCommand,\n private readonly reapWorktreeCommand: ReapWorktreeCommand,\n private readonly buildCommand: BuildCommand,\n private readonly pushDevCommand: PushDevCommand,\n private readonly finishPushDevCommand: FinishPushDevCommand,\n private readonly pushDevStateStore: PushDevStateStore,\n private readonly repoRootFinder: RepoRootFinder,\n ) {}\n\n /**\n * Refuse `command` while a `wp-push-dev --resolve` is half-finished.\n *\n * Enforced HERE, at the one place every bin funnels through, rather than in each command: a resolve\n * parks the checkout on a throwaway branch, so every command below would act on a branch that is not\n * the one it thinks it is. Putting the check in nine constructors is nine chances to forget it in the\n * tenth. PushDevStateStore owns the blocked list AND renders the hint from it, so the two cannot drift.\n */\n private assertNoResolveInProgress(command: string): void {\n if (!this.pushDevStateStore.isBlockedDuringResolve(command)) return;\n this.pushDevStateStore.assertIdle(this.repoRootFinder.resolveRepoRoot(process.cwd()), command);\n }\n\n /** `wp-push-dev`: publish a DISPOSABLE copy of this branch for the shared dev environment. No PR, no build. */\n pushDev(opts: PushDevOptions = new PushDevOptions()): Promise<void> {\n return this.pushDevCommand.run(opts);\n }\n\n /** `wp-finish-push-dev`: commit a resolved dev composition and publish it (conflict path only). */\n finishPushDev(opts: FinishPushDevOptions = new FinishPushDevOptions()): Promise<void> {\n return this.finishPushDevCommand.run(opts);\n }\n\n /**\n * INTERNAL (`wp-reap-worktree.js`, no `bin` entry): remove ONE named worktree and its branch.\n * `wp-land-pr` spawns it with cwd = the primary clone so the tree it just landed from can be\n * reaped by a process that is not standing in it.\n */\n reapWorktree(args: string[]): Promise<void> {\n return this.reapWorktreeCommand.run(args);\n }\n\n /**\n * `wp-build`: run this repo's ONE configured build (`commands.pr-gate.buildCommand`) — the same\n * command, resolved by the same resolver, that the PR gate's build stage runs. Not blocked during a\n * `wp-push-dev --resolve`: it mutates nothing and reads no branch state.\n */\n build(opts: BuildOptions): Promise<void> {\n return this.buildCommand.run(opts);\n }\n\n /** `wp-start-update`: 3-point squash-update from main (no PR). */\n startUpdate(): Promise<void> {\n this.assertNoResolveInProgress('wp-start-update');\n return this.startUpdateCommand.run();\n }\n\n /** `wp-finish-update`: validate + finalize a resolved 3-point merge (no PR). */\n finishUpdate(): Promise<void> {\n this.assertNoResolveInProgress('wp-finish-update');\n return this.finishUpdateCommand.run();\n }\n\n /** `wp-start-upsert-pr`: update from main (3-point merge), hand off review.json. No build gate, no push. */\n startUpsertPr(): Promise<void> {\n this.assertNoResolveInProgress('wp-start-upsert-pr');\n return this.startUpsertPrCommand.run();\n }\n\n /** `wp-finish-upsert-pr`: finalize merge, authoritative build gate, dashboard, create/update PR. */\n finishUpsertPr(): Promise<void> {\n this.assertNoResolveInProgress('wp-finish-upsert-pr');\n return this.finishUpsertPrCommand.run();\n }\n\n /**\n * `wp-cleanup`: reap what is provably merged AND every zero-commit husk, then report the rest with\n * the exact flags that take it. `options` carries what argv said — see CleanupOptions.\n */\n cleanup(options: CleanupOptions): Promise<void> {\n this.assertNoResolveInProgress('wp-cleanup');\n return this.cleanupCommand.run(options);\n }\n\n /**\n * `wp-checkout-clean-main`: go to main, fast-forward it, reap dead worktrees and branches, and sweep\n * the orphan directories a project move leaves behind. Replaces `git checkout main && git pull\n * origin main` outright — see CheckoutCleanMainCommand for why the old pair must stop being accepted\n * rather than surviving beside this.\n */\n checkoutCleanMain(): Promise<void> {\n this.assertNoResolveInProgress('wp-checkout-clean-main');\n return this.checkoutCleanMainCommand.run();\n }\n\n /** `wp-land-pr`: squash-merge this branch's PR into main with the compact commit body. */\n landPr(): Promise<void> {\n this.assertNoResolveInProgress('wp-land-pr');\n return this.landPrCommand.run();\n }\n\n /**\n * `wp-review-upsert-pr`: STAGE ② — validate the 3-point merge, run the build gate, extract this\n * branch's diff, and brief the reviewer subagents. Unlike the report-only command it replaces, this CAN\n * fail — before any reviewer is spawned, so a broken branch costs no reviewer tokens.\n */\n reviewUpsertPr(opts: ReviewUpsertPrOptions = new ReviewUpsertPrOptions()): Promise<void> {\n this.assertNoResolveInProgress('wp-review-upsert-pr');\n return this.reviewUpsertPrCommand.run(opts);\n }\n\n /** `wp-check-pr`: READ-ONLY CI check — verify the PR body carries a valid HMAC gate token for its head sha. */\n checkPr(): Promise<void> {\n return this.checkPrCommand.run();\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"pr-gate-app.js","sourceRoot":"","sources":["../../../../../../packages/tooling/pr-gate/src/scripts/pr-gate-app.ts"],"names":[],"mappings":";;;;AAAA,0DAAyD;AACzD,yCAA2D;AAC3D,0EAAqE;AACrE,4EAAuE;AACvE,gFAA0E;AAC1E,kFAA4E;AAC5E,gEAA4D;AAE5D,wFAAkF;AAClF,gEAA0E;AAC1E,kEAA6D;AAC7D,kFAAmG;AACnG,4EAAuE;AACvE,4DAAsE;AACtE,kEAA6E;AAC7E,gFAAgG;AAChG,8DAA8D;AAC9D,0DAAyD;AAEzD;;;;;GAKG;AAGI,IAAM,SAAS,GAAf,MAAM,SAAS;IAEG;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IAfrB,YACqB,kBAAsC,EACtC,mBAAwC,EACxC,oBAA0C,EAC1C,qBAA4C,EAC5C,cAA8B,EAC9B,wBAAkD,EAClD,aAA4B,EAC5B,cAA8B,EAC9B,qBAA4C,EAC5C,mBAAwC,EACxC,YAA0B,EAC1B,cAA8B,EAC9B,oBAA0C,EAC1C,iBAAoC,EACpC,cAA8B;QAd9B,uBAAkB,GAAlB,kBAAkB,CAAoB;QACtC,wBAAmB,GAAnB,mBAAmB,CAAqB;QACxC,yBAAoB,GAApB,oBAAoB,CAAsB;QAC1C,0BAAqB,GAArB,qBAAqB,CAAuB;QAC5C,mBAAc,GAAd,cAAc,CAAgB;QAC9B,6BAAwB,GAAxB,wBAAwB,CAA0B;QAClD,kBAAa,GAAb,aAAa,CAAe;QAC5B,mBAAc,GAAd,cAAc,CAAgB;QAC9B,0BAAqB,GAArB,qBAAqB,CAAuB;QAC5C,wBAAmB,GAAnB,mBAAmB,CAAqB;QACxC,iBAAY,GAAZ,YAAY,CAAc;QAC1B,mBAAc,GAAd,cAAc,CAAgB;QAC9B,yBAAoB,GAApB,oBAAoB,CAAsB;QAC1C,sBAAiB,GAAjB,iBAAiB,CAAmB;QACpC,mBAAc,GAAd,cAAc,CAAgB;IAChD,CAAC;IAEJ;;;;;;;OAOG;IACK,yBAAyB,CAAC,OAAe;QAC7C,IAAI,CAAC,IAAI,CAAC,iBAAiB,CAAC,sBAAsB,CAAC,OAAO,CAAC;YAAE,OAAO;QACpE,IAAI,CAAC,iBAAiB,CAAC,UAAU,CAAC,IAAI,CAAC,cAAc,CAAC,eAAe,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC,EAAE,OAAO,CAAC,CAAC;IACnG,CAAC;IAED,+GAA+G;IAC/G,OAAO,CAAC,OAAuB,IAAI,iCAAc,EAAE;QAC/C,OAAO,IAAI,CAAC,cAAc,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IACzC,CAAC;IAED,mGAAmG;IACnG,aAAa,CAAC,OAA6B,IAAI,8CAAoB,EAAE;QACjE,OAAO,IAAI,CAAC,oBAAoB,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IAC/C,CAAC;IAED;;;;OAIG;IACH,YAAY,CAAC,IAAc;QACvB,OAAO,IAAI,CAAC,mBAAmB,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IAC9C,CAAC;IAED;;;;OAIG;IACH,KAAK,CAAC,IAAkB;QACpB,OAAO,IAAI,CAAC,YAAY,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IACvC,CAAC;IAED,kEAAkE;IAClE,WAAW;QACP,IAAI,CAAC,yBAAyB,CAAC,iBAAiB,CAAC,CAAC;QAClD,OAAO,IAAI,CAAC,kBAAkB,CAAC,GAAG,EAAE,CAAC;IACzC,CAAC;IAED,gFAAgF;IAChF,YAAY;QACR,IAAI,CAAC,yBAAyB,CAAC,kBAAkB,CAAC,CAAC;QACnD,OAAO,IAAI,CAAC,mBAAmB,CAAC,GAAG,EAAE,CAAC;IAC1C,CAAC;IAED,4GAA4G;IAC5G,aAAa;QACT,IAAI,CAAC,yBAAyB,CAAC,oBAAoB,CAAC,CAAC;QACrD,OAAO,IAAI,CAAC,oBAAoB,CAAC,GAAG,EAAE,CAAC;IAC3C,CAAC;IAED,oGAAoG;IACpG,cAAc;QACV,IAAI,CAAC,yBAAyB,CAAC,qBAAqB,CAAC,CAAC;QACtD,OAAO,IAAI,CAAC,qBAAqB,CAAC,GAAG,EAAE,CAAC;IAC5C,CAAC;IAED;;;OAGG;IACH,OAAO,CAAC,OAAuB;QAC3B,IAAI,CAAC,yBAAyB,CAAC,YAAY,CAAC,CAAC;QAC7C,OAAO,IAAI,CAAC,cAAc,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;IAC5C,CAAC;IAED;;;;;OAKG;IACH,iBAAiB;QACb,IAAI,CAAC,yBAAyB,CAAC,wBAAwB,CAAC,CAAC;QACzD,OAAO,IAAI,CAAC,wBAAwB,CAAC,GAAG,EAAE,CAAC;IAC/C,CAAC;IAED;;;OAGG;IACH,MAAM,CAAC,OAAsB;QACzB,IAAI,CAAC,yBAAyB,CAAC,YAAY,CAAC,CAAC;QAC7C,OAAO,IAAI,CAAC,aAAa,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;IAC3C,CAAC;IAED;;;;OAIG;IACH,cAAc,CAAC,OAA8B,IAAI,gDAAqB,EAAE;QACpE,IAAI,CAAC,yBAAyB,CAAC,qBAAqB,CAAC,CAAC;QACtD,OAAO,IAAI,CAAC,qBAAqB,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IAChD,CAAC;IAED,+GAA+G;IAC/G,OAAO;QACH,OAAO,IAAI,CAAC,cAAc,CAAC,GAAG,EAAE,CAAC;IACrC,CAAC;CACJ,CAAA;AA/HY,8BAAS;oBAAT,SAAS;IAFrB,IAAA,6BAAc,GAAE;IAChB,IAAA,sBAAU,EAAC,8BAAkB,CAAC,SAAS,CAAC;6CAGI,yCAAkB;QACjB,2CAAmB;QAClB,8CAAoB;QACnB,gDAAqB;QAC5B,gCAAc;QACJ,sDAAwB;QACnC,+BAAa;QACZ,iCAAc;QACP,gDAAqB;QACvB,2CAAmB;QAC1B,4BAAY;QACV,iCAAc;QACR,8CAAoB;QACvB,kCAAiB;QACpB,6BAAc;GAhB1C,SAAS,CA+HrB","sourcesContent":["import { DocumentDesign } from '@webpieces/rules-config';\nimport { injectable, bindingScopeValues } from 'inversify';\nimport { StartUpdateCommand } from './commands/start-update-command';\nimport { FinishUpdateCommand } from './commands/finish-update-command';\nimport { StartUpsertPrCommand } from './commands/start-upsert-pr-command';\nimport { FinishUpsertPrCommand } from './commands/finish-upsert-pr-command';\nimport { CleanupCommand } from './commands/cleanup-command';\nimport { CleanupOptions } from './commands/cleanup-options';\nimport { CheckoutCleanMainCommand } from './commands/checkout-clean-main-command';\nimport { LandPrCommand, LandPrRequest } from './commands/land-pr-command';\nimport { CheckPrCommand } from './commands/check-pr-command';\nimport { ReviewUpsertPrCommand, ReviewUpsertPrOptions } from './commands/review-upsert-pr-command';\nimport { ReapWorktreeCommand } from './commands/reap-worktree-command';\nimport { BuildCommand, BuildOptions } from './commands/build-command';\nimport { PushDevCommand, PushDevOptions } from './commands/push-dev-command';\nimport { FinishPushDevCommand, FinishPushDevOptions } from './commands/finish-push-dev-command';\nimport { PushDevStateStore } from './workflow/push-dev-state';\nimport { RepoRootFinder } from '@webpieces/rules-config';\n\n/**\n * The pr-gate application root. `container.get(PrGateApp)` resolves the entire workflow DAG (the command\n * classes → the injected git/merge/dashboard services). `@DocumentDesign` marks it the\n * top-of-DAG the DI-design analyzer roots on, so `role:app` pr-gate draws its design. Each `bin/*`\n * entry resolves THIS and calls the matching command method.\n */\n@DocumentDesign()\n@injectable(bindingScopeValues.Singleton)\nexport class PrGateApp {\n constructor(\n private readonly startUpdateCommand: StartUpdateCommand,\n private readonly finishUpdateCommand: FinishUpdateCommand,\n private readonly startUpsertPrCommand: StartUpsertPrCommand,\n private readonly finishUpsertPrCommand: FinishUpsertPrCommand,\n private readonly cleanupCommand: CleanupCommand,\n private readonly checkoutCleanMainCommand: CheckoutCleanMainCommand,\n private readonly landPrCommand: LandPrCommand,\n private readonly checkPrCommand: CheckPrCommand,\n private readonly reviewUpsertPrCommand: ReviewUpsertPrCommand,\n private readonly reapWorktreeCommand: ReapWorktreeCommand,\n private readonly buildCommand: BuildCommand,\n private readonly pushDevCommand: PushDevCommand,\n private readonly finishPushDevCommand: FinishPushDevCommand,\n private readonly pushDevStateStore: PushDevStateStore,\n private readonly repoRootFinder: RepoRootFinder,\n ) {}\n\n /**\n * Refuse `command` while a `wp-push-dev --resolve` is half-finished.\n *\n * Enforced HERE, at the one place every bin funnels through, rather than in each command: a resolve\n * parks the checkout on a throwaway branch, so every command below would act on a branch that is not\n * the one it thinks it is. Putting the check in nine constructors is nine chances to forget it in the\n * tenth. PushDevStateStore owns the blocked list AND renders the hint from it, so the two cannot drift.\n */\n private assertNoResolveInProgress(command: string): void {\n if (!this.pushDevStateStore.isBlockedDuringResolve(command)) return;\n this.pushDevStateStore.assertIdle(this.repoRootFinder.resolveRepoRoot(process.cwd()), command);\n }\n\n /** `wp-push-dev`: publish a DISPOSABLE copy of this branch for the shared dev environment. No PR, no build. */\n pushDev(opts: PushDevOptions = new PushDevOptions()): Promise<void> {\n return this.pushDevCommand.run(opts);\n }\n\n /** `wp-finish-push-dev`: commit a resolved dev composition and publish it (conflict path only). */\n finishPushDev(opts: FinishPushDevOptions = new FinishPushDevOptions()): Promise<void> {\n return this.finishPushDevCommand.run(opts);\n }\n\n /**\n * INTERNAL (`wp-reap-worktree.js`, no `bin` entry): remove ONE named worktree and its branch.\n * `wp-land-pr` spawns it with cwd = the primary clone so the tree it just landed from can be\n * reaped by a process that is not standing in it.\n */\n reapWorktree(args: string[]): Promise<void> {\n return this.reapWorktreeCommand.run(args);\n }\n\n /**\n * `wp-build`: run this repo's ONE configured build (`commands.pr-gate.buildCommand`) — the same\n * command, resolved by the same resolver, that the PR gate's build stage runs. Not blocked during a\n * `wp-push-dev --resolve`: it mutates nothing and reads no branch state.\n */\n build(opts: BuildOptions): Promise<void> {\n return this.buildCommand.run(opts);\n }\n\n /** `wp-start-update`: 3-point squash-update from main (no PR). */\n startUpdate(): Promise<void> {\n this.assertNoResolveInProgress('wp-start-update');\n return this.startUpdateCommand.run();\n }\n\n /** `wp-finish-update`: validate + finalize a resolved 3-point merge (no PR). */\n finishUpdate(): Promise<void> {\n this.assertNoResolveInProgress('wp-finish-update');\n return this.finishUpdateCommand.run();\n }\n\n /** `wp-start-upsert-pr`: update from main (3-point merge), hand off review.json. No build gate, no push. */\n startUpsertPr(): Promise<void> {\n this.assertNoResolveInProgress('wp-start-upsert-pr');\n return this.startUpsertPrCommand.run();\n }\n\n /** `wp-finish-upsert-pr`: finalize merge, authoritative build gate, dashboard, create/update PR. */\n finishUpsertPr(): Promise<void> {\n this.assertNoResolveInProgress('wp-finish-upsert-pr');\n return this.finishUpsertPrCommand.run();\n }\n\n /**\n * `wp-cleanup`: reap what is provably merged AND every zero-commit husk, then report the rest with\n * the exact flags that take it. `options` carries what argv said — see CleanupOptions.\n */\n cleanup(options: CleanupOptions): Promise<void> {\n this.assertNoResolveInProgress('wp-cleanup');\n return this.cleanupCommand.run(options);\n }\n\n /**\n * `wp-checkout-clean-main`: go to main, fast-forward it, reap dead worktrees and branches, and sweep\n * the orphan directories a project move leaves behind. Replaces `git checkout main && git pull\n * origin main` outright — see CheckoutCleanMainCommand for why the old pair must stop being accepted\n * rather than surviving beside this.\n */\n checkoutCleanMain(): Promise<void> {\n this.assertNoResolveInProgress('wp-checkout-clean-main');\n return this.checkoutCleanMainCommand.run();\n }\n\n /**\n * `wp-land-pr`: squash-merge a PR into main with the compact commit body — this branch's by default,\n * or the one `--pr <n>` names, which is how a coordinator finishes a gone agent's work.\n */\n landPr(request: LandPrRequest): Promise<void> {\n this.assertNoResolveInProgress('wp-land-pr');\n return this.landPrCommand.run(request);\n }\n\n /**\n * `wp-review-upsert-pr`: STAGE ② — validate the 3-point merge, run the build gate, extract this\n * branch's diff, and brief the reviewer subagents. Unlike the report-only command it replaces, this CAN\n * fail — before any reviewer is spawned, so a broken branch costs no reviewer tokens.\n */\n reviewUpsertPr(opts: ReviewUpsertPrOptions = new ReviewUpsertPrOptions()): Promise<void> {\n this.assertNoResolveInProgress('wp-review-upsert-pr');\n return this.reviewUpsertPrCommand.run(opts);\n }\n\n /** `wp-check-pr`: READ-ONLY CI check — verify the PR body carries a valid HMAC gate token for its head sha. */\n checkPr(): Promise<void> {\n return this.checkPrCommand.run();\n }\n}\n"]}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { Worktree, WorktreeService } from '@webpieces/rules-config';
|
|
2
|
+
/**
|
|
3
|
+
* WHICH LOCAL TREE HOLDS THE COMMIT GITHUB JUST SQUASHED — asked as a fact, answered from anywhere.
|
|
4
|
+
*
|
|
5
|
+
* `wp-land-pr` does two things with different scopes: the MERGE belongs to the PR and is reachable from
|
|
6
|
+
* any directory, while the BOOKKEEPING (archive the pre-squash tip, promote merge-info, reap the landed
|
|
7
|
+
* worktree) belongs to the tree that really holds those objects. Until now the second half was resolved
|
|
8
|
+
* from `process.cwd()` — and `pnpm` hoists a bin's cwd to the workspace root, so a command run inside
|
|
9
|
+
* `<primary>/.claude/worktrees/agent-<id>` executed in the PRIMARY CLONE. Every agent worktree is nested
|
|
10
|
+
* like that, so the answer was wrong in exactly the case the mechanism was built for.
|
|
11
|
+
*
|
|
12
|
+
* The fix is to stop asking "where am I" and start asking "where is it". Both halves of that question are
|
|
13
|
+
* facts nobody has to record:
|
|
14
|
+
*
|
|
15
|
+
* `gh pr view <n> --json headRefName,headRefOid` → the branch, and the exact commit being squashed
|
|
16
|
+
* `git worktree list --porcelain` → every (path, branch, HEAD sha) in this repo
|
|
17
|
+
*
|
|
18
|
+
* The worktree list lives in the COMMON dir, so it reads identically from the primary clone, from any
|
|
19
|
+
* linked worktree, and from a coordinator that never saw the agent that built the branch. That is what
|
|
20
|
+
* makes `wp-land-pr --pr <n>` able to finish a dead agent's work.
|
|
21
|
+
*
|
|
22
|
+
* ─── WHY THE PAIR, AND NEVER THE NAME ALONE ───────────────────────────────────────────────────────
|
|
23
|
+
* A branch NAME does not identify a tree. A second clone, or a worktree somebody has since committed to,
|
|
24
|
+
* holds the same name at a different commit, and archiving `archive/<date>/<branch>` from there tags the
|
|
25
|
+
* wrong objects under the right name. So the selection key is `(headRefName, headRefOid)` together, and a
|
|
26
|
+
* name-only match is never enough to remove or archive anything — in this process or in the child that
|
|
27
|
+
* re-verifies it (see ReapWorktreeCommand).
|
|
28
|
+
*
|
|
29
|
+
* ─── WHY NOT A `.webpieces/**` RECEIPT ────────────────────────────────────────────────────────────
|
|
30
|
+
* The obvious alternative is for `wp-finish-upsert-pr` to write `{ pr, branch, worktreePath, headRefOid }`
|
|
31
|
+
* to a sidecar and for landing to read it back. `decisions/0005-the-pr-description-is-the-merge-body.md`
|
|
32
|
+
* already deleted that pattern once: a recorded claim can only ever be missing, stale, or on the wrong
|
|
33
|
+
* computer — and a worktree PATH is more volatile than the commit body that argument was made about,
|
|
34
|
+
* since the directory is moved, reaped, or recreated under a new agent id between finish and land.
|
|
35
|
+
* `(headRefOid, git worktree list)` is a pair of facts, needs no stored state, and works from a fresh
|
|
36
|
+
* clone. Nothing here reads state.
|
|
37
|
+
*/
|
|
38
|
+
/** A linked worktree holds the branch AT the squashed commit: archive here, and reap THAT directory. */
|
|
39
|
+
export declare const LANDED_TREE_WORKTREE = "worktree";
|
|
40
|
+
/** No worktree holds it, but the branch ref in this repo IS the squashed commit: archive, nothing to reap. */
|
|
41
|
+
export declare const LANDED_TREE_BRANCH_ONLY = "branch-only";
|
|
42
|
+
/** The name is here, the commit is not. Merge stands; bookkeeping is declined out loud. */
|
|
43
|
+
export declare const LANDED_TREE_WRONG_TIP = "wrong-tip";
|
|
44
|
+
/** The branch is not in this repo at all — landed from a tree that does not hold it. */
|
|
45
|
+
export declare const LANDED_TREE_ABSENT = "absent";
|
|
46
|
+
/**
|
|
47
|
+
* Data-only (per CLAUDE.md, classes for data). Where the landed commit lives locally, and therefore
|
|
48
|
+
* whether the bookkeeping half may run at all.
|
|
49
|
+
*
|
|
50
|
+
* `bookkeepingAllowed` is PRECOMPUTED rather than re-derived by the caller, for the same reason
|
|
51
|
+
* `EffectiveTree.redirected` and `WorktreeReapHandoff.canReap` are: a deletion rule spelled out twice is
|
|
52
|
+
* a deletion rule that eventually disagrees with itself.
|
|
53
|
+
*/
|
|
54
|
+
export declare class LandedTree {
|
|
55
|
+
readonly kind: string;
|
|
56
|
+
/** The linked worktree to reap — non-null ONLY for {@link LANDED_TREE_WORKTREE}. */
|
|
57
|
+
readonly worktree: Worktree | null;
|
|
58
|
+
/** The sha found locally under that branch name, '' when the branch is absent. Printed on a mismatch. */
|
|
59
|
+
readonly localSha: string;
|
|
60
|
+
readonly bookkeepingAllowed: boolean;
|
|
61
|
+
constructor(kind: string, worktree: Worktree | null, localSha: string);
|
|
62
|
+
}
|
|
63
|
+
export declare class LandedTreeResolver {
|
|
64
|
+
private readonly worktrees;
|
|
65
|
+
constructor(worktrees: WorktreeService);
|
|
66
|
+
/**
|
|
67
|
+
* The four-row table this whole design exists to implement, in order of what it can prove.
|
|
68
|
+
*
|
|
69
|
+
* `headRefOid === ''` means `gh` did not tell us which commit it is squashing. That cannot be turned
|
|
70
|
+
* into a mismatch — a fact we do not have is not evidence of disagreement — so the local tip is taken
|
|
71
|
+
* as the landed one, which is exactly what the old `ref.headRefOid !== ''` guard did.
|
|
72
|
+
*/
|
|
73
|
+
resolve(repoRoot: string, branch: string, headRefOid: string): LandedTree;
|
|
74
|
+
private revParse;
|
|
75
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.LandedTreeResolver = exports.LandedTree = exports.LANDED_TREE_ABSENT = exports.LANDED_TREE_WRONG_TIP = exports.LANDED_TREE_BRANCH_ONLY = exports.LANDED_TREE_WORKTREE = void 0;
|
|
4
|
+
const tslib_1 = require("tslib");
|
|
5
|
+
const child_process_1 = require("child_process");
|
|
6
|
+
const rules_config_1 = require("@webpieces/rules-config");
|
|
7
|
+
const inversify_1 = require("inversify");
|
|
8
|
+
/**
|
|
9
|
+
* WHICH LOCAL TREE HOLDS THE COMMIT GITHUB JUST SQUASHED — asked as a fact, answered from anywhere.
|
|
10
|
+
*
|
|
11
|
+
* `wp-land-pr` does two things with different scopes: the MERGE belongs to the PR and is reachable from
|
|
12
|
+
* any directory, while the BOOKKEEPING (archive the pre-squash tip, promote merge-info, reap the landed
|
|
13
|
+
* worktree) belongs to the tree that really holds those objects. Until now the second half was resolved
|
|
14
|
+
* from `process.cwd()` — and `pnpm` hoists a bin's cwd to the workspace root, so a command run inside
|
|
15
|
+
* `<primary>/.claude/worktrees/agent-<id>` executed in the PRIMARY CLONE. Every agent worktree is nested
|
|
16
|
+
* like that, so the answer was wrong in exactly the case the mechanism was built for.
|
|
17
|
+
*
|
|
18
|
+
* The fix is to stop asking "where am I" and start asking "where is it". Both halves of that question are
|
|
19
|
+
* facts nobody has to record:
|
|
20
|
+
*
|
|
21
|
+
* `gh pr view <n> --json headRefName,headRefOid` → the branch, and the exact commit being squashed
|
|
22
|
+
* `git worktree list --porcelain` → every (path, branch, HEAD sha) in this repo
|
|
23
|
+
*
|
|
24
|
+
* The worktree list lives in the COMMON dir, so it reads identically from the primary clone, from any
|
|
25
|
+
* linked worktree, and from a coordinator that never saw the agent that built the branch. That is what
|
|
26
|
+
* makes `wp-land-pr --pr <n>` able to finish a dead agent's work.
|
|
27
|
+
*
|
|
28
|
+
* ─── WHY THE PAIR, AND NEVER THE NAME ALONE ───────────────────────────────────────────────────────
|
|
29
|
+
* A branch NAME does not identify a tree. A second clone, or a worktree somebody has since committed to,
|
|
30
|
+
* holds the same name at a different commit, and archiving `archive/<date>/<branch>` from there tags the
|
|
31
|
+
* wrong objects under the right name. So the selection key is `(headRefName, headRefOid)` together, and a
|
|
32
|
+
* name-only match is never enough to remove or archive anything — in this process or in the child that
|
|
33
|
+
* re-verifies it (see ReapWorktreeCommand).
|
|
34
|
+
*
|
|
35
|
+
* ─── WHY NOT A `.webpieces/**` RECEIPT ────────────────────────────────────────────────────────────
|
|
36
|
+
* The obvious alternative is for `wp-finish-upsert-pr` to write `{ pr, branch, worktreePath, headRefOid }`
|
|
37
|
+
* to a sidecar and for landing to read it back. `decisions/0005-the-pr-description-is-the-merge-body.md`
|
|
38
|
+
* already deleted that pattern once: a recorded claim can only ever be missing, stale, or on the wrong
|
|
39
|
+
* computer — and a worktree PATH is more volatile than the commit body that argument was made about,
|
|
40
|
+
* since the directory is moved, reaped, or recreated under a new agent id between finish and land.
|
|
41
|
+
* `(headRefOid, git worktree list)` is a pair of facts, needs no stored state, and works from a fresh
|
|
42
|
+
* clone. Nothing here reads state.
|
|
43
|
+
*/
|
|
44
|
+
/** A linked worktree holds the branch AT the squashed commit: archive here, and reap THAT directory. */
|
|
45
|
+
exports.LANDED_TREE_WORKTREE = 'worktree';
|
|
46
|
+
/** No worktree holds it, but the branch ref in this repo IS the squashed commit: archive, nothing to reap. */
|
|
47
|
+
exports.LANDED_TREE_BRANCH_ONLY = 'branch-only';
|
|
48
|
+
/** The name is here, the commit is not. Merge stands; bookkeeping is declined out loud. */
|
|
49
|
+
exports.LANDED_TREE_WRONG_TIP = 'wrong-tip';
|
|
50
|
+
/** The branch is not in this repo at all — landed from a tree that does not hold it. */
|
|
51
|
+
exports.LANDED_TREE_ABSENT = 'absent';
|
|
52
|
+
/**
|
|
53
|
+
* Data-only (per CLAUDE.md, classes for data). Where the landed commit lives locally, and therefore
|
|
54
|
+
* whether the bookkeeping half may run at all.
|
|
55
|
+
*
|
|
56
|
+
* `bookkeepingAllowed` is PRECOMPUTED rather than re-derived by the caller, for the same reason
|
|
57
|
+
* `EffectiveTree.redirected` and `WorktreeReapHandoff.canReap` are: a deletion rule spelled out twice is
|
|
58
|
+
* a deletion rule that eventually disagrees with itself.
|
|
59
|
+
*/
|
|
60
|
+
class LandedTree {
|
|
61
|
+
kind;
|
|
62
|
+
/** The linked worktree to reap — non-null ONLY for {@link LANDED_TREE_WORKTREE}. */
|
|
63
|
+
worktree;
|
|
64
|
+
/** The sha found locally under that branch name, '' when the branch is absent. Printed on a mismatch. */
|
|
65
|
+
localSha;
|
|
66
|
+
bookkeepingAllowed;
|
|
67
|
+
constructor(kind, worktree, localSha) {
|
|
68
|
+
this.kind = kind;
|
|
69
|
+
this.worktree = worktree;
|
|
70
|
+
this.localSha = localSha;
|
|
71
|
+
this.bookkeepingAllowed = kind === exports.LANDED_TREE_WORKTREE || kind === exports.LANDED_TREE_BRANCH_ONLY;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
exports.LandedTree = LandedTree;
|
|
75
|
+
let LandedTreeResolver = class LandedTreeResolver {
|
|
76
|
+
worktrees;
|
|
77
|
+
constructor(worktrees) {
|
|
78
|
+
this.worktrees = worktrees;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* The four-row table this whole design exists to implement, in order of what it can prove.
|
|
82
|
+
*
|
|
83
|
+
* `headRefOid === ''` means `gh` did not tell us which commit it is squashing. That cannot be turned
|
|
84
|
+
* into a mismatch — a fact we do not have is not evidence of disagreement — so the local tip is taken
|
|
85
|
+
* as the landed one, which is exactly what the old `ref.headRefOid !== ''` guard did.
|
|
86
|
+
*/
|
|
87
|
+
resolve(repoRoot, branch, headRefOid) {
|
|
88
|
+
// Only LINKED worktrees are reapable; the primary clone is the thing reaped FROM, never a target.
|
|
89
|
+
const holder = this.worktrees.linkedWorktrees(repoRoot)
|
|
90
|
+
.find((tree) => tree.branch === branch) ?? null;
|
|
91
|
+
const local = holder !== null ? holder.head : this.revParse(repoRoot, branch);
|
|
92
|
+
if (local === '')
|
|
93
|
+
return new LandedTree(exports.LANDED_TREE_ABSENT, null, '');
|
|
94
|
+
if (headRefOid !== '' && local !== headRefOid) {
|
|
95
|
+
return new LandedTree(exports.LANDED_TREE_WRONG_TIP, null, local);
|
|
96
|
+
}
|
|
97
|
+
return holder !== null
|
|
98
|
+
? new LandedTree(exports.LANDED_TREE_WORKTREE, holder, local)
|
|
99
|
+
: new LandedTree(exports.LANDED_TREE_BRANCH_ONLY, null, local);
|
|
100
|
+
}
|
|
101
|
+
// Best-effort sha of a ref — '' when it cannot resolve, which reads as "the branch is not here".
|
|
102
|
+
revParse(repoRoot, ref) {
|
|
103
|
+
const result = (0, child_process_1.spawnSync)('git', ['rev-parse', ref], { cwd: repoRoot, encoding: 'utf8' });
|
|
104
|
+
return result.status === 0 ? (result.stdout ?? '').trim() : '';
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
exports.LandedTreeResolver = LandedTreeResolver;
|
|
108
|
+
exports.LandedTreeResolver = LandedTreeResolver = tslib_1.__decorate([
|
|
109
|
+
(0, inversify_1.injectable)(inversify_1.bindingScopeValues.Singleton),
|
|
110
|
+
tslib_1.__metadata("design:paramtypes", [rules_config_1.WorktreeService])
|
|
111
|
+
], LandedTreeResolver);
|
|
112
|
+
//# sourceMappingURL=landed-tree-resolver.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"landed-tree-resolver.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/pr-gate/src/scripts/workflow/landed-tree-resolver.ts"],"names":[],"mappings":";;;;AAAA,iDAA0C;AAC1C,0DAAoE;AACpE,yCAA2D;AAE3D;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AAEH,wGAAwG;AAC3F,QAAA,oBAAoB,GAAG,UAAU,CAAC;AAC/C,8GAA8G;AACjG,QAAA,uBAAuB,GAAG,aAAa,CAAC;AACrD,2FAA2F;AAC9E,QAAA,qBAAqB,GAAG,WAAW,CAAC;AACjD,wFAAwF;AAC3E,QAAA,kBAAkB,GAAG,QAAQ,CAAC;AAE3C;;;;;;;GAOG;AACH,MAAa,UAAU;IACV,IAAI,CAAS;IACtB,oFAAoF;IAC3E,QAAQ,CAAkB;IACnC,yGAAyG;IAChG,QAAQ,CAAS;IACjB,kBAAkB,CAAU;IAErC,YAAY,IAAY,EAAE,QAAyB,EAAE,QAAgB;QACjE,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;QACjB,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAC;QACzB,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAC;QACzB,IAAI,CAAC,kBAAkB,GAAG,IAAI,KAAK,4BAAoB,IAAI,IAAI,KAAK,+BAAuB,CAAC;IAChG,CAAC;CACJ;AAdD,gCAcC;AAGM,IAAM,kBAAkB,GAAxB,MAAM,kBAAkB;IACE;IAA7B,YAA6B,SAA0B;QAA1B,cAAS,GAAT,SAAS,CAAiB;IAAG,CAAC;IAE3D;;;;;;OAMG;IACH,OAAO,CAAC,QAAgB,EAAE,MAAc,EAAE,UAAkB;QACxD,kGAAkG;QAClG,MAAM,MAAM,GAAG,IAAI,CAAC,SAAS,CAAC,eAAe,CAAC,QAAQ,CAAC;aAClD,IAAI,CAAC,CAAC,IAAc,EAAW,EAAE,CAAC,IAAI,CAAC,MAAM,KAAK,MAAM,CAAC,IAAI,IAAI,CAAC;QACvE,MAAM,KAAK,GAAG,MAAM,KAAK,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC;QAE9E,IAAI,KAAK,KAAK,EAAE;YAAE,OAAO,IAAI,UAAU,CAAC,0BAAkB,EAAE,IAAI,EAAE,EAAE,CAAC,CAAC;QACtE,IAAI,UAAU,KAAK,EAAE,IAAI,KAAK,KAAK,UAAU,EAAE,CAAC;YAC5C,OAAO,IAAI,UAAU,CAAC,6BAAqB,EAAE,IAAI,EAAE,KAAK,CAAC,CAAC;QAC9D,CAAC;QACD,OAAO,MAAM,KAAK,IAAI;YAClB,CAAC,CAAC,IAAI,UAAU,CAAC,4BAAoB,EAAE,MAAM,EAAE,KAAK,CAAC;YACrD,CAAC,CAAC,IAAI,UAAU,CAAC,+BAAuB,EAAE,IAAI,EAAE,KAAK,CAAC,CAAC;IAC/D,CAAC;IAED,iGAAiG;IACzF,QAAQ,CAAC,QAAgB,EAAE,GAAW;QAC1C,MAAM,MAAM,GAAG,IAAA,yBAAS,EAAC,KAAK,EAAE,CAAC,WAAW,EAAE,GAAG,CAAC,EAAE,EAAE,GAAG,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,EAAE,CAAC,CAAC;QACzF,OAAO,MAAM,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,MAAM,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;IACnE,CAAC;CACJ,CAAA;AA9BY,gDAAkB;6BAAlB,kBAAkB;IAD9B,IAAA,sBAAU,EAAC,8BAAkB,CAAC,SAAS,CAAC;6CAEG,8BAAe;GAD9C,kBAAkB,CA8B9B","sourcesContent":["import { spawnSync } from 'child_process';\nimport { Worktree, WorktreeService } from '@webpieces/rules-config';\nimport { injectable, bindingScopeValues } from 'inversify';\n\n/**\n * WHICH LOCAL TREE HOLDS THE COMMIT GITHUB JUST SQUASHED — asked as a fact, answered from anywhere.\n *\n * `wp-land-pr` does two things with different scopes: the MERGE belongs to the PR and is reachable from\n * any directory, while the BOOKKEEPING (archive the pre-squash tip, promote merge-info, reap the landed\n * worktree) belongs to the tree that really holds those objects. Until now the second half was resolved\n * from `process.cwd()` — and `pnpm` hoists a bin's cwd to the workspace root, so a command run inside\n * `<primary>/.claude/worktrees/agent-<id>` executed in the PRIMARY CLONE. Every agent worktree is nested\n * like that, so the answer was wrong in exactly the case the mechanism was built for.\n *\n * The fix is to stop asking \"where am I\" and start asking \"where is it\". Both halves of that question are\n * facts nobody has to record:\n *\n * `gh pr view <n> --json headRefName,headRefOid` → the branch, and the exact commit being squashed\n * `git worktree list --porcelain` → every (path, branch, HEAD sha) in this repo\n *\n * The worktree list lives in the COMMON dir, so it reads identically from the primary clone, from any\n * linked worktree, and from a coordinator that never saw the agent that built the branch. That is what\n * makes `wp-land-pr --pr <n>` able to finish a dead agent's work.\n *\n * ─── WHY THE PAIR, AND NEVER THE NAME ALONE ───────────────────────────────────────────────────────\n * A branch NAME does not identify a tree. A second clone, or a worktree somebody has since committed to,\n * holds the same name at a different commit, and archiving `archive/<date>/<branch>` from there tags the\n * wrong objects under the right name. So the selection key is `(headRefName, headRefOid)` together, and a\n * name-only match is never enough to remove or archive anything — in this process or in the child that\n * re-verifies it (see ReapWorktreeCommand).\n *\n * ─── WHY NOT A `.webpieces/**` RECEIPT ────────────────────────────────────────────────────────────\n * The obvious alternative is for `wp-finish-upsert-pr` to write `{ pr, branch, worktreePath, headRefOid }`\n * to a sidecar and for landing to read it back. `decisions/0005-the-pr-description-is-the-merge-body.md`\n * already deleted that pattern once: a recorded claim can only ever be missing, stale, or on the wrong\n * computer — and a worktree PATH is more volatile than the commit body that argument was made about,\n * since the directory is moved, reaped, or recreated under a new agent id between finish and land.\n * `(headRefOid, git worktree list)` is a pair of facts, needs no stored state, and works from a fresh\n * clone. Nothing here reads state.\n */\n\n/** A linked worktree holds the branch AT the squashed commit: archive here, and reap THAT directory. */\nexport const LANDED_TREE_WORKTREE = 'worktree';\n/** No worktree holds it, but the branch ref in this repo IS the squashed commit: archive, nothing to reap. */\nexport const LANDED_TREE_BRANCH_ONLY = 'branch-only';\n/** The name is here, the commit is not. Merge stands; bookkeeping is declined out loud. */\nexport const LANDED_TREE_WRONG_TIP = 'wrong-tip';\n/** The branch is not in this repo at all — landed from a tree that does not hold it. */\nexport const LANDED_TREE_ABSENT = 'absent';\n\n/**\n * Data-only (per CLAUDE.md, classes for data). Where the landed commit lives locally, and therefore\n * whether the bookkeeping half may run at all.\n *\n * `bookkeepingAllowed` is PRECOMPUTED rather than re-derived by the caller, for the same reason\n * `EffectiveTree.redirected` and `WorktreeReapHandoff.canReap` are: a deletion rule spelled out twice is\n * a deletion rule that eventually disagrees with itself.\n */\nexport class LandedTree {\n readonly kind: string;\n /** The linked worktree to reap — non-null ONLY for {@link LANDED_TREE_WORKTREE}. */\n readonly worktree: Worktree | null;\n /** The sha found locally under that branch name, '' when the branch is absent. Printed on a mismatch. */\n readonly localSha: string;\n readonly bookkeepingAllowed: boolean;\n\n constructor(kind: string, worktree: Worktree | null, localSha: string) {\n this.kind = kind;\n this.worktree = worktree;\n this.localSha = localSha;\n this.bookkeepingAllowed = kind === LANDED_TREE_WORKTREE || kind === LANDED_TREE_BRANCH_ONLY;\n }\n}\n\n@injectable(bindingScopeValues.Singleton)\nexport class LandedTreeResolver {\n constructor(private readonly worktrees: WorktreeService) {}\n\n /**\n * The four-row table this whole design exists to implement, in order of what it can prove.\n *\n * `headRefOid === ''` means `gh` did not tell us which commit it is squashing. That cannot be turned\n * into a mismatch — a fact we do not have is not evidence of disagreement — so the local tip is taken\n * as the landed one, which is exactly what the old `ref.headRefOid !== ''` guard did.\n */\n resolve(repoRoot: string, branch: string, headRefOid: string): LandedTree {\n // Only LINKED worktrees are reapable; the primary clone is the thing reaped FROM, never a target.\n const holder = this.worktrees.linkedWorktrees(repoRoot)\n .find((tree: Worktree): boolean => tree.branch === branch) ?? null;\n const local = holder !== null ? holder.head : this.revParse(repoRoot, branch);\n\n if (local === '') return new LandedTree(LANDED_TREE_ABSENT, null, '');\n if (headRefOid !== '' && local !== headRefOid) {\n return new LandedTree(LANDED_TREE_WRONG_TIP, null, local);\n }\n return holder !== null\n ? new LandedTree(LANDED_TREE_WORKTREE, holder, local)\n : new LandedTree(LANDED_TREE_BRANCH_ONLY, null, local);\n }\n\n // Best-effort sha of a ref — '' when it cannot resolve, which reads as \"the branch is not here\".\n private revParse(repoRoot: string, ref: string): string {\n const result = spawnSync('git', ['rev-parse', ref], { cwd: repoRoot, encoding: 'utf8' });\n return result.status === 0 ? (result.stdout ?? '').trim() : '';\n }\n}\n"]}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { WorktreeService } from '@webpieces/rules-config';
|
|
1
|
+
import { Worktree, WorktreeService } from '@webpieces/rules-config';
|
|
2
2
|
import { ReapOutcomeSignal } from './reap-outcome';
|
|
3
3
|
/**
|
|
4
4
|
* Data-only (per CLAUDE.md, classes for data). The decision about the just-landed worktree: which
|
|
@@ -8,15 +8,30 @@ export declare class WorktreeReapHandoff {
|
|
|
8
8
|
/** The linked worktree holding the branch that was just landed. */
|
|
9
9
|
readonly worktreePath: string;
|
|
10
10
|
readonly branch: string;
|
|
11
|
+
/**
|
|
12
|
+
* That worktree's HEAD — which is, by construction, the exact commit GitHub squashed (see
|
|
13
|
+
* LandedTreeResolver). Carried so the child can re-verify the SELECTION rather than take a path on
|
|
14
|
+
* trust: a branch name is not an identity, and this is the half that makes it one.
|
|
15
|
+
*/
|
|
16
|
+
readonly head: string;
|
|
11
17
|
/** The primary clone — the child's `cwd`, and the directory a human must `cd` to afterwards. */
|
|
12
18
|
readonly primaryPath: string;
|
|
13
19
|
/** The compiled entry the child runs. '' when it could not be located on disk. */
|
|
14
20
|
readonly entryScript: string;
|
|
15
21
|
/** '' when the hand-off can run; otherwise the reason it cannot, in one human-readable clause. */
|
|
16
22
|
readonly blockedBecause: string;
|
|
23
|
+
/**
|
|
24
|
+
* Is the OPERATOR'S OWN shell inside the directory about to be removed?
|
|
25
|
+
*
|
|
26
|
+
* True is the `/full-cycle` case (the agent lands its own PR from its own worktree) and it is the
|
|
27
|
+
* only case where "your cwd no longer exists" is a true sentence worth shouting. False is the
|
|
28
|
+
* coordinator case — `wp-land-pr --pr <n>` from the primary clone — where saying it would send a
|
|
29
|
+
* reader chasing a directory move they do not need to make.
|
|
30
|
+
*/
|
|
31
|
+
readonly standingHere: boolean;
|
|
17
32
|
/** Precomputed (as EffectiveTree does with `redirected`) so no caller re-derives the rule. */
|
|
18
33
|
readonly canReap: boolean;
|
|
19
|
-
constructor(worktreePath: string, branch: string, primaryPath: string, entryScript: string, blockedBecause: string);
|
|
34
|
+
constructor(worktreePath: string, branch: string, head: string, primaryPath: string, entryScript: string, blockedBecause: string, standingHere: boolean);
|
|
20
35
|
}
|
|
21
36
|
export declare class LandedWorktreeReaper {
|
|
22
37
|
private readonly worktrees;
|
|
@@ -25,12 +40,27 @@ export declare class LandedWorktreeReaper {
|
|
|
25
40
|
/**
|
|
26
41
|
* Is there a worktree to reap at all, and can the hand-off run?
|
|
27
42
|
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
43
|
+
* IT IS TOLD WHICH TREE, AND NO LONGER LOOKS. This used to call `currentWorktree(repoRoot)` and reap
|
|
44
|
+
* only when the tree it was STANDING IN held the landed branch — which made the whole #512 mechanism
|
|
45
|
+
* dead code in the one case it was built for, because `pnpm` hoists a bin's cwd out of a nested
|
|
46
|
+
* `.claude/worktrees/**` worktree and into the primary clone, where `isMain` is true and this
|
|
47
|
+
* returned null. Worse, "the tree I am in" was never the right question: a coordinator landing a dead
|
|
48
|
+
* agent's PR is standing somewhere else entirely. LandedTreeResolver answers the right one — which
|
|
49
|
+
* tree's HEAD is the commit GitHub squashed — and hands the answer here.
|
|
50
|
+
*
|
|
51
|
+
* `null` means there is genuinely no worktree to reap: the branch lived only in the primary clone, or
|
|
52
|
+
* nowhere the sha agrees with. That is the ordinary `pnpm wp-cleanup` case and needs no special
|
|
53
|
+
* sentence. A non-null handoff with `canReap === false` means there IS a corpse but the re-exec is
|
|
54
|
+
* not safely achievable, which is the case that must keep the manual notice.
|
|
55
|
+
*/
|
|
56
|
+
plan(repoRoot: string, landed: Worktree | null, invocationCwd: string): WorktreeReapHandoff | null;
|
|
57
|
+
/**
|
|
58
|
+
* Is `cwd` the worktree itself or somewhere beneath it? A path COMPARISON, not a git question:
|
|
59
|
+
* the operator may have been in a subdirectory when they ran the command, and that shell is just as
|
|
60
|
+
* dead once the tree goes. Both sides are resolved first so `.`/`..` and a trailing slash cannot
|
|
61
|
+
* turn a match into a miss.
|
|
32
62
|
*/
|
|
33
|
-
|
|
63
|
+
private isInside;
|
|
34
64
|
/**
|
|
35
65
|
* Run the reap in a child process rooted in the primary clone, and render what happened.
|
|
36
66
|
*
|
|
@@ -57,9 +87,11 @@ export declare class LandedWorktreeReaper {
|
|
|
57
87
|
*/
|
|
58
88
|
manualNotice(handoff: WorktreeReapHandoff): string;
|
|
59
89
|
/**
|
|
60
|
-
* The one thing a human or agent MUST be told after a successful reap
|
|
61
|
-
*
|
|
62
|
-
* mystery ENOENT
|
|
90
|
+
* The one thing a human or agent MUST be told after a successful reap — but only when it is TRUE of
|
|
91
|
+
* them. Landing from inside the tree that just went (the `/full-cycle` case) leaves the shell in a
|
|
92
|
+
* deleted directory and every following relative path is a mystery ENOENT until they move. Landing a
|
|
93
|
+
* dead agent's PR from the primary clone removes somebody ELSE's directory, and telling that operator
|
|
94
|
+
* to `cd` somewhere sends them chasing a move they do not need to make.
|
|
63
95
|
*/
|
|
64
96
|
private afterReap;
|
|
65
97
|
/**
|