@webpieces/rules-config 0.4.731 → 0.4.733
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 +1 -1
- package/src/checklist-override.d.ts +106 -0
- package/src/checklist-override.js +207 -0
- package/src/checklist-override.js.map +1 -0
- package/src/exclude-paths.js +7 -5
- package/src/exclude-paths.js.map +1 -1
- package/src/index.d.ts +2 -1
- package/src/index.js +10 -3
- package/src/index.js.map +1 -1
- package/src/load-template.d.ts +8 -1
- package/src/load-template.js +22 -2
- package/src/load-template.js.map +1 -1
- package/src/minimatch-interop.d.ts +40 -0
- package/src/minimatch-interop.js +32 -0
- package/src/minimatch-interop.js.map +1 -0
- package/src/review-json-data.d.ts +3 -2
- package/src/review-json-data.js +19 -11
- package/src/review-json-data.js.map +1 -1
- package/src/review-json.d.ts +34 -12
- package/src/review-json.js +91 -43
- package/src/review-json.js.map +1 -1
- package/src/stale-bin-sweep.d.ts +68 -0
- package/src/stale-bin-sweep.js +226 -0
- package/src/stale-bin-sweep.js.map +1 -0
- package/templates/webpieces.review-checklists.md +46 -6
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.staleBinSweeper = exports.StaleBinSweeper = exports.StaleBinRemoval = void 0;
|
|
4
|
+
const tslib_1 = require("tslib");
|
|
5
|
+
const fs = tslib_1.__importStar(require("fs"));
|
|
6
|
+
const path = tslib_1.__importStar(require("path"));
|
|
7
|
+
const to_error_1 = require("./to-error");
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
// SWEEP DANGLING `node_modules/.bin/wp-*` SYMLINKS.
|
|
10
|
+
//
|
|
11
|
+
// THE DEFECT. pnpm's linker ADDS a link for every entry in the current manifest's `bin` map, but it never
|
|
12
|
+
// sweeps `.bin` for orphans left behind by a PREVIOUS version of the SAME package. `@webpieces/pr-gate`
|
|
13
|
+
// and `@webpieces/ai-hook-rules` are upgraded, never removed, so nothing ever triggers a delete: every
|
|
14
|
+
// bin either package has EVER shipped stays linked forever, pointing at a script that is no longer on
|
|
15
|
+
// disk. A scan of nine clones on ONE machine found 18 distinct dangling `wp-*` names.
|
|
16
|
+
//
|
|
17
|
+
// THE DEFECT IS ONGOING, AND A ROUTINE RENAME PRODUCES IT. Measured on this repo's own upgrade to
|
|
18
|
+
// 0.4.728, DURING the session that wrote this file: PR #743 hard-renamed one `wp-*` command with no alias,
|
|
19
|
+
// so the new manifest declares `wp-sync-main` and no longer declares its predecessor. One `pnpm install`
|
|
20
|
+
// later, `.bin` held the NEW link created that minute AND the predecessor's link still sitting there from
|
|
21
|
+
// the previous install, dangling — reproduced independently in two separate trees of this repo. So this is
|
|
22
|
+
// not a historical mess left by a deleted feature that a one-time migration could mop up: it is
|
|
23
|
+
// regenerated by the most ordinary change a package can make, on every clone, every time. That is the
|
|
24
|
+
// argument for the call site below — the sweep has to ride a path that runs ROUTINELY, because the defect
|
|
25
|
+
// is created routinely.
|
|
26
|
+
//
|
|
27
|
+
// The predecessor's NAME is deliberately not written here. `no-old-sync-main-name` forbids the dead
|
|
28
|
+
// spelling in tracked source and blocked an earlier draft of this very comment, which is the rule working:
|
|
29
|
+
// a retired command named in source is exactly how a dead name outlives its tooling — the thing this
|
|
30
|
+
// module exists to clean up, one level out. PR #743 and the issue this shipped under carry the literal
|
|
31
|
+
// name; nothing in the code needs it, because the predicate below is structural.
|
|
32
|
+
//
|
|
33
|
+
// WHY IT MATTERS MORE THAN TIDINESS. `ls node_modules/.bin` lists them, so a dangling entry ADVERTISES a
|
|
34
|
+
// capability that does not exist — a human and an AI were both misled by that listing before checking the
|
|
35
|
+
// link target — and the failure it eventually produces is self-referential and useless:
|
|
36
|
+
// `Command "wp-authorize" not found / Did you mean "pnpm wp-authorize"?`. This is `upgrade-shim.ts`'s own
|
|
37
|
+
// governing principle one level out: an entry pointing at a missing file is WORSE than absence.
|
|
38
|
+
//
|
|
39
|
+
// WHY A PREFIX AND NOT A NAME LIST. A hardcoded list of retired bin names would go stale in exactly the
|
|
40
|
+
// way the symlinks did — the list assembled from the two names that prompted this would have caught 2 of
|
|
41
|
+
// the 18, and it would NOT have caught the rename above, which had not happened yet when the list would
|
|
42
|
+
// have been written. That is the general case: the next orphan is always created by a release later than
|
|
43
|
+
// any list. A name list is also unwriteable here on purpose — see the note above about naming dead
|
|
44
|
+
// commands in source. The predicate is structural instead — a `wp-` prefixed entry that is a SYMLINK whose target
|
|
45
|
+
// does not exist — so it needs no maintenance and cannot miss a name nobody has thought of. The prefix is
|
|
46
|
+
// the whole safety story: another package's bins are never touched.
|
|
47
|
+
//
|
|
48
|
+
// WHY NOT A postinstall HOOK. `setupDebugging.md` records the postinstall approach as ABANDONED in this
|
|
49
|
+
// repo. The call site is the `wp-*` startup pass that regenerates `.webpieces/instruct-ai/*` — see
|
|
50
|
+
// TemplateWriter — and that placement is the load-bearing half of the fix. This is not a tidy-up for one
|
|
51
|
+
// repo: every developer's machine has this graveyard, and a cure that only cleans the tree somebody happens
|
|
52
|
+
// to run it in is worthless. Riding the pass EVERY `wp-*` command takes is what makes the RELEASED sweep
|
|
53
|
+
// reach every clone on every machine, healing each one the next time any `wp-*` command runs there.
|
|
54
|
+
//
|
|
55
|
+
// NOT ALSO CALLED FROM `wp-upgrade-shim`, though it would read naturally there (that bin already deletes a
|
|
56
|
+
// retired FILE on the same principle). `upgrade-shim.ts` may import only `fs`/`path` so it still runs on a
|
|
57
|
+
// tree too broken to load the rule engine: this package's barrel would pull in inversify and the config
|
|
58
|
+
// loader, and a subpath import would resolve against the INSTALLED rules-config, which is a release behind
|
|
59
|
+
// the local source — so a spawned `wp-upgrade-shim` would die on module resolution, in the one command an
|
|
60
|
+
// L0-blocked session has left. Its header records that, and points here.
|
|
61
|
+
//
|
|
62
|
+
// DEPENDENCY-FREE ANYWAY: `fs`, `path` and `toError`. No inversify (see StaleBinSweeper), so this module
|
|
63
|
+
// stays cheap for the startup path it runs on and importable by anything that later needs it.
|
|
64
|
+
// ---------------------------------------------------------------------------
|
|
65
|
+
// The one place the prefix is spelled. Everything outside it belongs to some other package.
|
|
66
|
+
const WP_BIN_PREFIX = 'wp-';
|
|
67
|
+
/**
|
|
68
|
+
* One dangling entry the sweep ACTED ON: what was linked, the target that was not there, and — when the
|
|
69
|
+
* removal itself failed — why. Data-only (per CLAUDE.md).
|
|
70
|
+
*
|
|
71
|
+
* `failure` exists because the removal is an `fs.rmSync`, not just a probe: an unwritable `.bin` (EACCES,
|
|
72
|
+
* a read-only mount) would otherwise heal silently-never while the tree kept advertising a command that
|
|
73
|
+
* does not exist, which is the precise defect this module exists to remove. Not thrown — the sweep is a
|
|
74
|
+
* courtesy and must never fail the `wp-*` command that called it — so the diagnostic rides back here and
|
|
75
|
+
* {@link StaleBinSweeper.report} states both outcomes.
|
|
76
|
+
*/
|
|
77
|
+
class StaleBinRemoval {
|
|
78
|
+
name; // the bin name as it appeared in .bin (e.g. 'wp-authorize')
|
|
79
|
+
target; // the link target that does not exist, as recorded in the symlink
|
|
80
|
+
failure; // '' = removed; non-empty = still there, and this is why
|
|
81
|
+
constructor(name, target, failure = '') {
|
|
82
|
+
this.name = name;
|
|
83
|
+
this.target = target;
|
|
84
|
+
this.failure = failure;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
exports.StaleBinRemoval = StaleBinRemoval;
|
|
88
|
+
/**
|
|
89
|
+
* Removes `node_modules/.bin/wp-*` entries whose symlink target is gone.
|
|
90
|
+
*
|
|
91
|
+
* Deliberately NOT `@injectable`: decorating it would import inversify, and this module must stay loadable
|
|
92
|
+
* by `wp-upgrade-shim` on a tree that cannot build a DI container (see the header). Callers use the shared
|
|
93
|
+
* {@link staleBinSweeper} instance, which is also what makes {@link sweepOnce}'s memo process-wide.
|
|
94
|
+
*/
|
|
95
|
+
class StaleBinSweeper {
|
|
96
|
+
// Roots already swept in THIS process. `writeTemplate` is called several times per `wp-*` command (once
|
|
97
|
+
// per instruct-ai doc), and a sweep that reported per call would print the same removals repeatedly.
|
|
98
|
+
swept = new Set();
|
|
99
|
+
/** Where the bins live for a tree. Public so a test can point at a fixture without guessing the layout. */
|
|
100
|
+
binDir(repoRoot) {
|
|
101
|
+
return path.join(repoRoot, 'node_modules', '.bin');
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Sweep once per root per process, returning what was removed ([] on every later call for the same
|
|
105
|
+
* root, and [] when there was nothing to remove — the two are indistinguishable to a caller ON PURPOSE,
|
|
106
|
+
* because both mean "say nothing").
|
|
107
|
+
*/
|
|
108
|
+
sweepOnce(repoRoot) {
|
|
109
|
+
if (this.swept.has(repoRoot))
|
|
110
|
+
return [];
|
|
111
|
+
this.swept.add(repoRoot);
|
|
112
|
+
return this.sweep(repoRoot);
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Remove every dangling `wp-*` symlink under the tree's `.bin`, returning what went. [] when the
|
|
116
|
+
* directory does not exist (a linked worktree with no install of its own is the common case) or when
|
|
117
|
+
* everything there resolves.
|
|
118
|
+
*
|
|
119
|
+
* BEST EFFORT, PER ENTRY. A `.bin` that cannot be read, or one entry that cannot be removed, must never
|
|
120
|
+
* take down the `wp-*` command that called this — the sweep is a courtesy, not the command's job.
|
|
121
|
+
*/
|
|
122
|
+
sweep(repoRoot) {
|
|
123
|
+
const dir = this.binDir(repoRoot);
|
|
124
|
+
const removed = [];
|
|
125
|
+
for (const name of this.entries(dir)) {
|
|
126
|
+
if (!name.startsWith(WP_BIN_PREFIX))
|
|
127
|
+
continue;
|
|
128
|
+
const removal = this.removeIfDangling(path.join(dir, name), name);
|
|
129
|
+
if (removal !== null)
|
|
130
|
+
removed.push(removal);
|
|
131
|
+
}
|
|
132
|
+
return removed;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* The lines a caller prints for a sweep. [] for an empty sweep, so REMOVING NOTHING IS SILENT — this
|
|
136
|
+
* runs on every `wp-*` command and the common case must add no noise at all.
|
|
137
|
+
*
|
|
138
|
+
* Rendered here rather than at each call site so the two callers cannot describe the same act
|
|
139
|
+
* differently; each still chooses its own output channel and its own leading icon convention.
|
|
140
|
+
*/
|
|
141
|
+
report(removed) {
|
|
142
|
+
if (removed.length === 0)
|
|
143
|
+
return [];
|
|
144
|
+
const gone = removed.filter((r) => r.failure === '');
|
|
145
|
+
const stuck = removed.filter((r) => r.failure !== '');
|
|
146
|
+
const lines = [];
|
|
147
|
+
if (gone.length > 0) {
|
|
148
|
+
lines.push(`✅ @webpieces: removed ${gone.length} dangling node_modules/.bin/wp-* symlink(s) left by an earlier release:`);
|
|
149
|
+
for (const r of gone)
|
|
150
|
+
lines.push(` ${r.name} -> ${r.target} (target missing)`);
|
|
151
|
+
lines.push(' They pointed at scripts this release no longer ships, so they could only ever fail on execution.');
|
|
152
|
+
}
|
|
153
|
+
// A failed removal is STATED, never quietly dropped: the entry is still there, still advertising a
|
|
154
|
+
// command that does not exist, and only a human can fix an unwritable .bin.
|
|
155
|
+
if (stuck.length > 0) {
|
|
156
|
+
lines.push(`⚠️ @webpieces: ${stuck.length} dangling node_modules/.bin/wp-* symlink(s) could NOT be removed:`);
|
|
157
|
+
for (const r of stuck)
|
|
158
|
+
lines.push(` ${r.name} -> ${r.target} (target missing; ${r.failure})`);
|
|
159
|
+
lines.push(' They still advertise commands this release does not ship. Nothing else is affected — remove them by hand.');
|
|
160
|
+
}
|
|
161
|
+
return lines;
|
|
162
|
+
}
|
|
163
|
+
// The directory listing, or [] when there is no .bin (or it cannot be read).
|
|
164
|
+
entries(dir) {
|
|
165
|
+
// webpieces-disable no-unmanaged-exceptions -- chokepoint: an unreadable .bin means "nothing to sweep", never a failed wp-* command
|
|
166
|
+
// eslint-disable-next-line @webpieces/no-unmanaged-exceptions
|
|
167
|
+
try {
|
|
168
|
+
if (!fs.existsSync(dir))
|
|
169
|
+
return [];
|
|
170
|
+
return fs.readdirSync(dir);
|
|
171
|
+
}
|
|
172
|
+
catch (err) {
|
|
173
|
+
const error = (0, to_error_1.toError)(err);
|
|
174
|
+
void error; // best effort: no readable .bin means there is nothing to sweep
|
|
175
|
+
return [];
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Remove ONE entry if — and only if — it is a symlink whose target does not exist. Returns what was
|
|
180
|
+
* acted on, with `failure` set when the entry was dangling but could not be removed; `null` when the
|
|
181
|
+
* entry was not a dangling `wp-*` link at all.
|
|
182
|
+
*/
|
|
183
|
+
removeIfDangling(full, name) {
|
|
184
|
+
const target = this.danglingTarget(full);
|
|
185
|
+
if (target === null)
|
|
186
|
+
return null;
|
|
187
|
+
// webpieces-disable no-unmanaged-exceptions -- chokepoint: an unremovable entry is REPORTED, never fatal to the wp-* command that called the sweep
|
|
188
|
+
// eslint-disable-next-line @webpieces/no-unmanaged-exceptions
|
|
189
|
+
try {
|
|
190
|
+
fs.rmSync(full, { force: true });
|
|
191
|
+
return new StaleBinRemoval(name, target);
|
|
192
|
+
}
|
|
193
|
+
catch (err) {
|
|
194
|
+
const error = (0, to_error_1.toError)(err);
|
|
195
|
+
return new StaleBinRemoval(name, target, error.message);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* The link target of a DANGLING symlink, or `null` when this entry is not one — a real file, a live
|
|
200
|
+
* link, or something we cannot stat. `existsSync` FOLLOWS symlinks, so a false answer on a path `lstat`
|
|
201
|
+
* calls a link is exactly the dangling case, with no need to resolve the target ourselves.
|
|
202
|
+
*
|
|
203
|
+
* Split from the removal so the two catches mean different things: an unreadable entry is simply not
|
|
204
|
+
* ours to touch, while a failed REMOVAL is a fact worth printing.
|
|
205
|
+
*/
|
|
206
|
+
danglingTarget(full) {
|
|
207
|
+
// webpieces-disable no-unmanaged-exceptions -- chokepoint: an entry we cannot stat is not ours to touch
|
|
208
|
+
// eslint-disable-next-line @webpieces/no-unmanaged-exceptions
|
|
209
|
+
try {
|
|
210
|
+
if (!fs.lstatSync(full).isSymbolicLink())
|
|
211
|
+
return null;
|
|
212
|
+
if (fs.existsSync(full))
|
|
213
|
+
return null;
|
|
214
|
+
return fs.readlinkSync(full);
|
|
215
|
+
}
|
|
216
|
+
catch (err) {
|
|
217
|
+
const error = (0, to_error_1.toError)(err);
|
|
218
|
+
void error; // best effort: an entry we cannot read is left exactly as it is
|
|
219
|
+
return null;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
exports.StaleBinSweeper = StaleBinSweeper;
|
|
224
|
+
// The shared instance — the memo in `sweepOnce` is per-instance, so every caller must use this one.
|
|
225
|
+
exports.staleBinSweeper = new StaleBinSweeper();
|
|
226
|
+
//# sourceMappingURL=stale-bin-sweep.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stale-bin-sweep.js","sourceRoot":"","sources":["../../../../../packages/tooling/rules-config/src/stale-bin-sweep.ts"],"names":[],"mappings":"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","sourcesContent":["import * as fs from 'fs';\nimport * as path from 'path';\nimport { toError } from './to-error';\n\n// ---------------------------------------------------------------------------\n// SWEEP DANGLING `node_modules/.bin/wp-*` SYMLINKS.\n//\n// THE DEFECT. pnpm's linker ADDS a link for every entry in the current manifest's `bin` map, but it never\n// sweeps `.bin` for orphans left behind by a PREVIOUS version of the SAME package. `@webpieces/pr-gate`\n// and `@webpieces/ai-hook-rules` are upgraded, never removed, so nothing ever triggers a delete: every\n// bin either package has EVER shipped stays linked forever, pointing at a script that is no longer on\n// disk. A scan of nine clones on ONE machine found 18 distinct dangling `wp-*` names.\n//\n// THE DEFECT IS ONGOING, AND A ROUTINE RENAME PRODUCES IT. Measured on this repo's own upgrade to\n// 0.4.728, DURING the session that wrote this file: PR #743 hard-renamed one `wp-*` command with no alias,\n// so the new manifest declares `wp-sync-main` and no longer declares its predecessor. One `pnpm install`\n// later, `.bin` held the NEW link created that minute AND the predecessor's link still sitting there from\n// the previous install, dangling — reproduced independently in two separate trees of this repo. So this is\n// not a historical mess left by a deleted feature that a one-time migration could mop up: it is\n// regenerated by the most ordinary change a package can make, on every clone, every time. That is the\n// argument for the call site below — the sweep has to ride a path that runs ROUTINELY, because the defect\n// is created routinely.\n//\n// The predecessor's NAME is deliberately not written here. `no-old-sync-main-name` forbids the dead\n// spelling in tracked source and blocked an earlier draft of this very comment, which is the rule working:\n// a retired command named in source is exactly how a dead name outlives its tooling — the thing this\n// module exists to clean up, one level out. PR #743 and the issue this shipped under carry the literal\n// name; nothing in the code needs it, because the predicate below is structural.\n//\n// WHY IT MATTERS MORE THAN TIDINESS. `ls node_modules/.bin` lists them, so a dangling entry ADVERTISES a\n// capability that does not exist — a human and an AI were both misled by that listing before checking the\n// link target — and the failure it eventually produces is self-referential and useless:\n// `Command \"wp-authorize\" not found / Did you mean \"pnpm wp-authorize\"?`. This is `upgrade-shim.ts`'s own\n// governing principle one level out: an entry pointing at a missing file is WORSE than absence.\n//\n// WHY A PREFIX AND NOT A NAME LIST. A hardcoded list of retired bin names would go stale in exactly the\n// way the symlinks did — the list assembled from the two names that prompted this would have caught 2 of\n// the 18, and it would NOT have caught the rename above, which had not happened yet when the list would\n// have been written. That is the general case: the next orphan is always created by a release later than\n// any list. A name list is also unwriteable here on purpose — see the note above about naming dead\n// commands in source. The predicate is structural instead — a `wp-` prefixed entry that is a SYMLINK whose target\n// does not exist — so it needs no maintenance and cannot miss a name nobody has thought of. The prefix is\n// the whole safety story: another package's bins are never touched.\n//\n// WHY NOT A postinstall HOOK. `setupDebugging.md` records the postinstall approach as ABANDONED in this\n// repo. The call site is the `wp-*` startup pass that regenerates `.webpieces/instruct-ai/*` — see\n// TemplateWriter — and that placement is the load-bearing half of the fix. This is not a tidy-up for one\n// repo: every developer's machine has this graveyard, and a cure that only cleans the tree somebody happens\n// to run it in is worthless. Riding the pass EVERY `wp-*` command takes is what makes the RELEASED sweep\n// reach every clone on every machine, healing each one the next time any `wp-*` command runs there.\n//\n// NOT ALSO CALLED FROM `wp-upgrade-shim`, though it would read naturally there (that bin already deletes a\n// retired FILE on the same principle). `upgrade-shim.ts` may import only `fs`/`path` so it still runs on a\n// tree too broken to load the rule engine: this package's barrel would pull in inversify and the config\n// loader, and a subpath import would resolve against the INSTALLED rules-config, which is a release behind\n// the local source — so a spawned `wp-upgrade-shim` would die on module resolution, in the one command an\n// L0-blocked session has left. Its header records that, and points here.\n//\n// DEPENDENCY-FREE ANYWAY: `fs`, `path` and `toError`. No inversify (see StaleBinSweeper), so this module\n// stays cheap for the startup path it runs on and importable by anything that later needs it.\n// ---------------------------------------------------------------------------\n\n// The one place the prefix is spelled. Everything outside it belongs to some other package.\nconst WP_BIN_PREFIX = 'wp-';\n\n/**\n * One dangling entry the sweep ACTED ON: what was linked, the target that was not there, and — when the\n * removal itself failed — why. Data-only (per CLAUDE.md).\n *\n * `failure` exists because the removal is an `fs.rmSync`, not just a probe: an unwritable `.bin` (EACCES,\n * a read-only mount) would otherwise heal silently-never while the tree kept advertising a command that\n * does not exist, which is the precise defect this module exists to remove. Not thrown — the sweep is a\n * courtesy and must never fail the `wp-*` command that called it — so the diagnostic rides back here and\n * {@link StaleBinSweeper.report} states both outcomes.\n */\nexport class StaleBinRemoval {\n name: string; // the bin name as it appeared in .bin (e.g. 'wp-authorize')\n target: string; // the link target that does not exist, as recorded in the symlink\n failure: string; // '' = removed; non-empty = still there, and this is why\n\n constructor(name: string, target: string, failure = '') {\n this.name = name;\n this.target = target;\n this.failure = failure;\n }\n}\n\n/**\n * Removes `node_modules/.bin/wp-*` entries whose symlink target is gone.\n *\n * Deliberately NOT `@injectable`: decorating it would import inversify, and this module must stay loadable\n * by `wp-upgrade-shim` on a tree that cannot build a DI container (see the header). Callers use the shared\n * {@link staleBinSweeper} instance, which is also what makes {@link sweepOnce}'s memo process-wide.\n */\nexport class StaleBinSweeper {\n // Roots already swept in THIS process. `writeTemplate` is called several times per `wp-*` command (once\n // per instruct-ai doc), and a sweep that reported per call would print the same removals repeatedly.\n private readonly swept = new Set<string>();\n\n /** Where the bins live for a tree. Public so a test can point at a fixture without guessing the layout. */\n binDir(repoRoot: string): string {\n return path.join(repoRoot, 'node_modules', '.bin');\n }\n\n /**\n * Sweep once per root per process, returning what was removed ([] on every later call for the same\n * root, and [] when there was nothing to remove — the two are indistinguishable to a caller ON PURPOSE,\n * because both mean \"say nothing\").\n */\n sweepOnce(repoRoot: string): StaleBinRemoval[] {\n if (this.swept.has(repoRoot)) return [];\n this.swept.add(repoRoot);\n return this.sweep(repoRoot);\n }\n\n /**\n * Remove every dangling `wp-*` symlink under the tree's `.bin`, returning what went. [] when the\n * directory does not exist (a linked worktree with no install of its own is the common case) or when\n * everything there resolves.\n *\n * BEST EFFORT, PER ENTRY. A `.bin` that cannot be read, or one entry that cannot be removed, must never\n * take down the `wp-*` command that called this — the sweep is a courtesy, not the command's job.\n */\n sweep(repoRoot: string): StaleBinRemoval[] {\n const dir = this.binDir(repoRoot);\n const removed: StaleBinRemoval[] = [];\n for (const name of this.entries(dir)) {\n if (!name.startsWith(WP_BIN_PREFIX)) continue;\n const removal = this.removeIfDangling(path.join(dir, name), name);\n if (removal !== null) removed.push(removal);\n }\n return removed;\n }\n\n /**\n * The lines a caller prints for a sweep. [] for an empty sweep, so REMOVING NOTHING IS SILENT — this\n * runs on every `wp-*` command and the common case must add no noise at all.\n *\n * Rendered here rather than at each call site so the two callers cannot describe the same act\n * differently; each still chooses its own output channel and its own leading icon convention.\n */\n report(removed: readonly StaleBinRemoval[]): string[] {\n if (removed.length === 0) return [];\n const gone = removed.filter((r: StaleBinRemoval): boolean => r.failure === '');\n const stuck = removed.filter((r: StaleBinRemoval): boolean => r.failure !== '');\n const lines: string[] = [];\n if (gone.length > 0) {\n lines.push(`✅ @webpieces: removed ${gone.length} dangling node_modules/.bin/wp-* symlink(s) left by an earlier release:`);\n for (const r of gone) lines.push(` ${r.name} -> ${r.target} (target missing)`);\n lines.push(' They pointed at scripts this release no longer ships, so they could only ever fail on execution.');\n }\n // A failed removal is STATED, never quietly dropped: the entry is still there, still advertising a\n // command that does not exist, and only a human can fix an unwritable .bin.\n if (stuck.length > 0) {\n lines.push(`⚠️ @webpieces: ${stuck.length} dangling node_modules/.bin/wp-* symlink(s) could NOT be removed:`);\n for (const r of stuck) lines.push(` ${r.name} -> ${r.target} (target missing; ${r.failure})`);\n lines.push(' They still advertise commands this release does not ship. Nothing else is affected — remove them by hand.');\n }\n return lines;\n }\n\n // The directory listing, or [] when there is no .bin (or it cannot be read).\n private entries(dir: string): string[] {\n // webpieces-disable no-unmanaged-exceptions -- chokepoint: an unreadable .bin means \"nothing to sweep\", never a failed wp-* command\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n if (!fs.existsSync(dir)) return [];\n return fs.readdirSync(dir);\n } catch (err: unknown) {\n const error = toError(err);\n void error; // best effort: no readable .bin means there is nothing to sweep\n return [];\n }\n }\n\n /**\n * Remove ONE entry if — and only if — it is a symlink whose target does not exist. Returns what was\n * acted on, with `failure` set when the entry was dangling but could not be removed; `null` when the\n * entry was not a dangling `wp-*` link at all.\n */\n private removeIfDangling(full: string, name: string): StaleBinRemoval | null {\n const target = this.danglingTarget(full);\n if (target === null) return null;\n // webpieces-disable no-unmanaged-exceptions -- chokepoint: an unremovable entry is REPORTED, never fatal to the wp-* command that called the sweep\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n fs.rmSync(full, { force: true });\n return new StaleBinRemoval(name, target);\n } catch (err: unknown) {\n const error = toError(err);\n return new StaleBinRemoval(name, target, error.message);\n }\n }\n\n /**\n * The link target of a DANGLING symlink, or `null` when this entry is not one — a real file, a live\n * link, or something we cannot stat. `existsSync` FOLLOWS symlinks, so a false answer on a path `lstat`\n * calls a link is exactly the dangling case, with no need to resolve the target ourselves.\n *\n * Split from the removal so the two catches mean different things: an unreadable entry is simply not\n * ours to touch, while a failed REMOVAL is a fact worth printing.\n */\n private danglingTarget(full: string): string | null {\n // webpieces-disable no-unmanaged-exceptions -- chokepoint: an entry we cannot stat is not ours to touch\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n if (!fs.lstatSync(full).isSymbolicLink()) return null;\n if (fs.existsSync(full)) return null;\n return fs.readlinkSync(full);\n } catch (err: unknown) {\n const error = toError(err);\n void error; // best effort: an entry we cannot read is left exactly as it is\n return null;\n }\n }\n}\n\n// The shared instance — the memo in `sweepOnce` is per-instance, so every caller must use this one.\nexport const staleBinSweeper = new StaleBinSweeper();\n"]}
|
|
@@ -130,8 +130,7 @@ whose `subagent` has no `.claude/agents/<subagent>.md`, so this should surface a
|
|
|
130
130
|
{
|
|
131
131
|
"id": "<the checklist id / subagent name>",
|
|
132
132
|
"status": "green | yellow | red",
|
|
133
|
-
"output": "what you checked and what you found"
|
|
134
|
-
"override": ""
|
|
133
|
+
"output": "what you checked and what you found"
|
|
135
134
|
}
|
|
136
135
|
```
|
|
137
136
|
|
|
@@ -139,16 +138,57 @@ whose `subagent` has no `.claude/agents/<subagent>.md`, so this should surface a
|
|
|
139
138
|
| --- | --- |
|
|
140
139
|
| `green` | 🟢 passes, nothing to flag |
|
|
141
140
|
| `yellow` | 🟡 **passes with concerns.** Blocks nothing; your `output` is published on the PR for a human to read |
|
|
142
|
-
| `red`
|
|
143
|
-
| `red` +
|
|
141
|
+
| `red` | 🔴 **`wp-finish` refuses to open the PR** and prints your `output` verbatim |
|
|
142
|
+
| `red` + an `override-<id>.json` | 🟠 ships anyway; the human's stated reason is published on the PR |
|
|
144
143
|
|
|
145
144
|
> **The `success` boolean is REMOVED — there is no compatibility mode.** A verdict file still using it is
|
|
146
145
|
> rejected with a message naming the replacement. It was removed because a boolean gave a reviewer no way
|
|
147
146
|
> to say *"this is fine, but someone should look at X"*: the only route to raising a concern was to fail the
|
|
148
147
|
> PR and then override your own failure, which reads on the dashboard as a deliberately-accepted defect.
|
|
149
148
|
|
|
150
|
-
**
|
|
151
|
-
|
|
149
|
+
> **The `override` field is REMOVED from this file too, and there is no compatibility mode** — including
|
|
150
|
+
> `"override": ""`, which is rejected as well, because the copy left in a reviewer's file is what teaches
|
|
151
|
+
> the next reviewer that the field still exists. The ship-anyway decision MOVED to its own
|
|
152
|
+
> `override-<id>.json`; see below.
|
|
153
|
+
|
|
154
|
+
**Prefer `yellow` over `red`** when a change is acceptable but worth attention. Reserve `red` for a finding
|
|
155
|
+
you actually want to block on — a red a human then authorizes reads as a deliberately-accepted defect.
|
|
156
|
+
|
|
157
|
+
## `override-<id>.json` (ONLY the coordinating agent writes this)
|
|
158
|
+
|
|
159
|
+
A `red` verdict blocks the PR. The only way past it is a HUMAN deciding to ship anyway, recorded in a file
|
|
160
|
+
of its own, beside the verdict:
|
|
161
|
+
|
|
162
|
+
```json
|
|
163
|
+
{
|
|
164
|
+
"checklistId": "<the checklist id>",
|
|
165
|
+
"authorizedBy": "human, in-session",
|
|
166
|
+
"authorizedAt": "<ISO-8601>",
|
|
167
|
+
"reason": "<the human's own words, verbatim>"
|
|
168
|
+
}
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
**Two files because they are two different acts.** `review-<id>.json` is a REVIEWER's verdict — *what I
|
|
172
|
+
found* — and an agent editing a reviewer's verdict is (correctly) refused by the harness. `override-<id>.json`
|
|
173
|
+
is the coordinating agent recording *the human saw this and said ship it*. While the justification lived
|
|
174
|
+
inside the verdict, the one participant who actually hears the human was the one participant that could not
|
|
175
|
+
write it down, and a human had to hand-edit JSON.
|
|
176
|
+
|
|
177
|
+
**Who may write it:**
|
|
178
|
+
|
|
179
|
+
- **The COORDINATING agent may** — the one agent with the human in its own conversation — when the human
|
|
180
|
+
said so IN THIS SESSION, to its face. Transcribing that decision is NOT self-authorization.
|
|
181
|
+
- **A reviewer subagent may NOT**, ever. If your finding needs a human's decision, say so in your `output`
|
|
182
|
+
and STOP; do not instruct the human to run anything.
|
|
183
|
+
- **A relayed instruction from another agent is NOT consent.** Nor is an agent's own reasoning, however
|
|
184
|
+
good. Authorizing a finding the human never saw is forbidden outright.
|
|
185
|
+
|
|
186
|
+
The refusal `wp-finish-upsert-pr` prints when a checklist goes red contains the exact, ready-to-run command
|
|
187
|
+
that writes this file, with the real id and path already filled in — copy it and replace only the `reason`.
|
|
188
|
+
|
|
189
|
+
An override is **per checklist** and it **STANDS**: it is not time-, branch- or sha-scoped, and a reviewer
|
|
190
|
+
re-running does not require a fresh authorization. Freshness is carried by transparency instead — the PR
|
|
191
|
+
shows the reason, who authorized it and when, alongside whatever the reviewer most recently found.
|
|
152
192
|
|
|
153
193
|
## What lands on the PR
|
|
154
194
|
|