patchwork-os 1.2.0-beta.2.canary.764 → 1.2.0-beta.2.canary.765
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/deploymentFreshness.d.ts +18 -0
- package/dist/deploymentFreshness.js +94 -0
- package/dist/deploymentFreshness.js.map +1 -1
- package/dist/index.js +75 -102
- package/dist/index.js.map +1 -1
- package/dist/maintenance/prOutcomeLedger.d.ts +12 -1
- package/dist/maintenance/prOutcomeLedger.js +28 -0
- package/dist/maintenance/prOutcomeLedger.js.map +1 -1
- package/dist/maintenance/sweep.d.ts +98 -0
- package/dist/maintenance/sweep.js +197 -0
- package/dist/maintenance/sweep.js.map +1 -0
- package/dist/maintenance/sweepCollect.d.ts +33 -0
- package/dist/maintenance/sweepCollect.js +109 -0
- package/dist/maintenance/sweepCollect.js.map +1 -0
- package/dist/privacy/undeclaredSteps.d.ts +14 -0
- package/dist/privacy/undeclaredSteps.js +44 -0
- package/dist/privacy/undeclaredSteps.js.map +1 -1
- package/package.json +2 -2
|
@@ -90,3 +90,21 @@ export declare function assessDeploymentFreshness(input: FreshnessInput): Freshn
|
|
|
90
90
|
/** mtime in ms, or undefined when the build cannot be located. */
|
|
91
91
|
export declare function buildTimeFromStat(stat: Stats | undefined): number | undefined;
|
|
92
92
|
export declare function formatFreshness(report: FreshnessReport, buildTimeMs: number): string;
|
|
93
|
+
/**
|
|
94
|
+
* Where the bridge lock files live. Honours `CLAUDE_CONFIG_DIR`.
|
|
95
|
+
*
|
|
96
|
+
* Extracted from the `doctor` handler so `patchwork sweep` counts bridges the
|
|
97
|
+
* same way `doctor` judges them. Two discoveries of "which bridges exist" would
|
|
98
|
+
* drift, and the drift is silent: the sweep would report a green gate over a set
|
|
99
|
+
* of locks the verb never looked at.
|
|
100
|
+
*/
|
|
101
|
+
export declare function bridgeLockDir(): string;
|
|
102
|
+
/** Every parseable `.lock` in `dir`. An unparseable one is skipped, not fixed. */
|
|
103
|
+
export declare function discoverLocks(dir?: string): BridgeLock[];
|
|
104
|
+
/**
|
|
105
|
+
* mtime of the installed build, or undefined when none can be located.
|
|
106
|
+
*
|
|
107
|
+
* Falls back to this process's own `dist` when the global package is absent
|
|
108
|
+
* (running from a checkout) — the honest reference in that case.
|
|
109
|
+
*/
|
|
110
|
+
export declare function installedBuildTimeMs(): number | undefined;
|
|
@@ -1,3 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Is the code that is RUNNING the code we think shipped?
|
|
3
|
+
*
|
|
4
|
+
* Every gate in this repo verifies the repository: tests, lint, coverage, the
|
|
5
|
+
* audit scripts, even the wiring guard that exists specifically to catch
|
|
6
|
+
* "declared but supplied nowhere". Not one of them looks at a running process.
|
|
7
|
+
*
|
|
8
|
+
* That was invisible until it wasn't. On 2026-08-19 both live bridges were found
|
|
9
|
+
* to contain neither the ADR-0021 privacy code nor the `butler` command — merged,
|
|
10
|
+
* wired, tested, gated, and absent from every process serving requests. Three
|
|
11
|
+
* separate workstreams had been blocked for five days on evidence those bridges
|
|
12
|
+
* structurally could not produce, and every check was green throughout.
|
|
13
|
+
*
|
|
14
|
+
* ## Why not compare versions
|
|
15
|
+
*
|
|
16
|
+
* The obvious check — running version vs installed version — would NOT have
|
|
17
|
+
* caught it. Both were `1.2.0-beta.2`: the package version had not been bumped
|
|
18
|
+
* between the stale install and the fresh one, because a version is a release
|
|
19
|
+
* marker and not a build identity. A check that reports OK in the exact case it
|
|
20
|
+
* was written for is worse than no check, so this compares TIME instead.
|
|
21
|
+
*
|
|
22
|
+
* A bridge that started BEFORE the currently-installed build was written is
|
|
23
|
+
* running something older, whatever its version string says. That is a fact
|
|
24
|
+
* about processes rather than about a git tree, which is the whole point.
|
|
25
|
+
*
|
|
26
|
+
* ## What it deliberately does not claim
|
|
27
|
+
*
|
|
28
|
+
* It cannot see inside a running process. A bridge launched from a repo checkout
|
|
29
|
+
* (`node dist/index.js`) loads that checkout's `dist`, not the global install, so
|
|
30
|
+
* comparing it against the global build time answers a slightly different
|
|
31
|
+
* question than it appears to. Those are reported as `unknown-origin` rather than
|
|
32
|
+
* silently judged — the failure this file exists to prevent is a confident
|
|
33
|
+
* statement about code nobody actually inspected.
|
|
34
|
+
*/
|
|
35
|
+
import { readdirSync, readFileSync, statSync } from "node:fs";
|
|
36
|
+
import os from "node:os";
|
|
37
|
+
import nodePath from "node:path";
|
|
38
|
+
import { fileURLToPath } from "node:url";
|
|
1
39
|
export function assessDeploymentFreshness(input) {
|
|
2
40
|
const findings = [];
|
|
3
41
|
for (const lock of input.locks) {
|
|
@@ -120,4 +158,60 @@ export function formatFreshness(report, buildTimeMs) {
|
|
|
120
158
|
}
|
|
121
159
|
return L.join("\n");
|
|
122
160
|
}
|
|
161
|
+
/**
|
|
162
|
+
* Where the bridge lock files live. Honours `CLAUDE_CONFIG_DIR`.
|
|
163
|
+
*
|
|
164
|
+
* Extracted from the `doctor` handler so `patchwork sweep` counts bridges the
|
|
165
|
+
* same way `doctor` judges them. Two discoveries of "which bridges exist" would
|
|
166
|
+
* drift, and the drift is silent: the sweep would report a green gate over a set
|
|
167
|
+
* of locks the verb never looked at.
|
|
168
|
+
*/
|
|
169
|
+
export function bridgeLockDir() {
|
|
170
|
+
return nodePath.join(process.env.CLAUDE_CONFIG_DIR || os.homedir() + nodePath.sep + ".claude", "ide");
|
|
171
|
+
}
|
|
172
|
+
/** Every parseable `.lock` in `dir`. An unparseable one is skipped, not fixed. */
|
|
173
|
+
export function discoverLocks(dir = bridgeLockDir()) {
|
|
174
|
+
const locks = [];
|
|
175
|
+
try {
|
|
176
|
+
for (const f of readdirSync(dir)) {
|
|
177
|
+
if (!f.endsWith(".lock"))
|
|
178
|
+
continue;
|
|
179
|
+
try {
|
|
180
|
+
const d = JSON.parse(readFileSync(nodePath.join(dir, f), "utf-8"));
|
|
181
|
+
if (typeof d?.pid === "number")
|
|
182
|
+
locks.push({ ...d, file: f });
|
|
183
|
+
}
|
|
184
|
+
catch {
|
|
185
|
+
// An unparseable lock is not this module's problem to solve.
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
catch {
|
|
190
|
+
// No lock dir ⇒ no bridges ⇒ "nothing running", which is legitimate.
|
|
191
|
+
}
|
|
192
|
+
return locks;
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* mtime of the installed build, or undefined when none can be located.
|
|
196
|
+
*
|
|
197
|
+
* Falls back to this process's own `dist` when the global package is absent
|
|
198
|
+
* (running from a checkout) — the honest reference in that case.
|
|
199
|
+
*/
|
|
200
|
+
export function installedBuildTimeMs() {
|
|
201
|
+
for (const candidate of [
|
|
202
|
+
nodePath.join(process.env.npm_config_prefix ?? "/opt/homebrew", "lib/node_modules/patchwork-os/dist/index.js"),
|
|
203
|
+
// `fileURLToPath`, NOT `.pathname`. On Windows a file URL's pathname is
|
|
204
|
+
// `/D:/a/...` — a leading slash before the drive letter — which statSync
|
|
205
|
+
// cannot open, so this fallback never resolved there.
|
|
206
|
+
fileURLToPath(new URL("./index.js", import.meta.url)),
|
|
207
|
+
]) {
|
|
208
|
+
try {
|
|
209
|
+
return statSync(candidate).mtimeMs;
|
|
210
|
+
}
|
|
211
|
+
catch {
|
|
212
|
+
// try the next candidate
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
return undefined;
|
|
216
|
+
}
|
|
123
217
|
//# sourceMappingURL=deploymentFreshness.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"deploymentFreshness.js","sourceRoot":"","sources":["../src/deploymentFreshness.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"deploymentFreshness.js","sourceRoot":"","sources":["../src/deploymentFreshness.ts"],"names":[],"mappings":"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"}
|
package/dist/index.js
CHANGED
|
@@ -179,6 +179,7 @@ async function downloadVsixFromOpenVsx() {
|
|
|
179
179
|
const KNOWN_SUBCOMMANDS = [
|
|
180
180
|
"init",
|
|
181
181
|
"evidence",
|
|
182
|
+
"sweep",
|
|
182
183
|
"patchwork-init",
|
|
183
184
|
"start-all",
|
|
184
185
|
"install-extension",
|
|
@@ -4189,30 +4190,13 @@ if (process.argv[2] === "pr-outcomes") {
|
|
|
4189
4190
|
process.exit(0);
|
|
4190
4191
|
}
|
|
4191
4192
|
(async () => {
|
|
4192
|
-
const {
|
|
4193
|
+
const { appendFileSync } = await import("node:fs");
|
|
4193
4194
|
const { join } = await import("node:path");
|
|
4194
4195
|
const { patchworkPath } = await import("./patchworkHome.js");
|
|
4195
4196
|
const { execFileSync } = await import("node:child_process");
|
|
4196
|
-
const { toObservation, dedupeAgainst, summarise, formatLedgerSummary } = await import("./maintenance/prOutcomeLedger.js");
|
|
4197
|
+
const { toObservation, dedupeAgainst, readObservations, summarise, formatLedgerSummary, } = await import("./maintenance/prOutcomeLedger.js");
|
|
4197
4198
|
const file = join(patchworkPath(), "pr_outcomes.jsonl");
|
|
4198
|
-
const read = () =>
|
|
4199
|
-
if (!existsSync(file))
|
|
4200
|
-
return [];
|
|
4201
|
-
const out = [];
|
|
4202
|
-
for (const line of readFileSync(file, "utf-8").split("\n")) {
|
|
4203
|
-
if (line.trim() === "")
|
|
4204
|
-
continue;
|
|
4205
|
-
try {
|
|
4206
|
-
out.push(JSON.parse(line));
|
|
4207
|
-
}
|
|
4208
|
-
catch {
|
|
4209
|
-
// A corrupt line is skipped, never repaired: rewriting an
|
|
4210
|
-
// append-only evidence file to make it parse is how evidence
|
|
4211
|
-
// quietly becomes fiction.
|
|
4212
|
-
}
|
|
4213
|
-
}
|
|
4214
|
-
return out;
|
|
4215
|
-
};
|
|
4199
|
+
const read = () => readObservations(file);
|
|
4216
4200
|
const sub = args[0] ?? "show";
|
|
4217
4201
|
if (sub === "show") {
|
|
4218
4202
|
const rows = read();
|
|
@@ -4331,6 +4315,71 @@ if (process.argv[2] === "pr-outcomes") {
|
|
|
4331
4315
|
process.exit(2);
|
|
4332
4316
|
})();
|
|
4333
4317
|
}
|
|
4318
|
+
if (process.argv[2] === "sweep") {
|
|
4319
|
+
const args = process.argv.slice(3);
|
|
4320
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
4321
|
+
process.stdout.write("Usage: patchwork sweep [--dir <path>] [--expect-running [N]] [--no-write] [--json]\n\n" +
|
|
4322
|
+
"What MOVED since the last sweep. Composes doctor, workers validate,\n" +
|
|
4323
|
+
"evidence, privacy undeclared and pr-outcomes into one reading, appends it\n" +
|
|
4324
|
+
"to sweep_snapshots.jsonl, and diffs it against the previous one.\n\n" +
|
|
4325
|
+
"Two of the readings are GATES and can fail this command: deployment\n" +
|
|
4326
|
+
"freshness and worker-manifest validity. Everything else is drift and is\n" +
|
|
4327
|
+
"reported without failing — the evidence ratios fall by construction as\n" +
|
|
4328
|
+
"ledgers accrue rows, and an undeclared agent step is ADR-0021's documented\n" +
|
|
4329
|
+
"default. Wiring those to an exit code would make sweep permanently red,\n" +
|
|
4330
|
+
"which is how a real warning gets ignored.\n\n" +
|
|
4331
|
+
"A first run is a BASELINE, never 'no changes': nothing was observed to\n" +
|
|
4332
|
+
"change, and there was nothing to compare against, are different facts.\n\n" +
|
|
4333
|
+
"The snapshot holds COUNTS ONLY — no recipe name, no path, no id — because\n" +
|
|
4334
|
+
"two of its inputs return operator data and a health check is the last\n" +
|
|
4335
|
+
"place anyone thinks to look for accumulated secrets.\n\n" +
|
|
4336
|
+
" --dir <path> ledger directory (default: $PATCHWORK_HOME)\n" +
|
|
4337
|
+
" --expect-running [N] bridges that ought to be up (default: no expectation)\n" +
|
|
4338
|
+
" --no-write report without appending a snapshot\n" +
|
|
4339
|
+
" --json emit the reading and the delta\n\n" +
|
|
4340
|
+
"Exits 1 only when a gate flipped healthy -> unhealthy.\n");
|
|
4341
|
+
process.exit(0);
|
|
4342
|
+
}
|
|
4343
|
+
(async () => {
|
|
4344
|
+
try {
|
|
4345
|
+
const { collectSweep } = await import("./maintenance/sweepCollect.js");
|
|
4346
|
+
const { appendSnapshot, diffReadings, formatSweep, readLastSnapshot } = await import("./maintenance/sweep.js");
|
|
4347
|
+
const { patchworkHome } = await import("./patchworkHome.js");
|
|
4348
|
+
const dirIdx = args.indexOf("--dir");
|
|
4349
|
+
const dir = dirIdx !== -1 && args[dirIdx + 1] ? args[dirIdx + 1] : patchworkHome();
|
|
4350
|
+
const expectIdx = args.indexOf("--expect-running");
|
|
4351
|
+
let expectRunning;
|
|
4352
|
+
if (expectIdx !== -1) {
|
|
4353
|
+
const raw = args[expectIdx + 1];
|
|
4354
|
+
const parsed = raw !== undefined && !raw.startsWith("--") ? Number(raw) : 1;
|
|
4355
|
+
if (!Number.isInteger(parsed) || parsed < 0) {
|
|
4356
|
+
process.stderr.write("[sweep] --expect-running takes a non-negative integer\n");
|
|
4357
|
+
process.exit(2);
|
|
4358
|
+
}
|
|
4359
|
+
expectRunning = parsed;
|
|
4360
|
+
}
|
|
4361
|
+
// Read the previous snapshot BEFORE appending this one, or the diff
|
|
4362
|
+
// compares the reading against itself and every sweep reports nothing.
|
|
4363
|
+
const previous = readLastSnapshot(dir);
|
|
4364
|
+
const reading = collectSweep({
|
|
4365
|
+
dir: dir,
|
|
4366
|
+
...(expectRunning !== undefined ? { expectRunning } : {}),
|
|
4367
|
+
});
|
|
4368
|
+
const delta = diffReadings(previous, reading);
|
|
4369
|
+
const wrote = !args.includes("--no-write");
|
|
4370
|
+
if (wrote)
|
|
4371
|
+
appendSnapshot(dir, reading);
|
|
4372
|
+
process.stdout.write(args.includes("--json")
|
|
4373
|
+
? `${JSON.stringify({ reading, delta }, null, 2)}\n`
|
|
4374
|
+
: formatSweep(reading, delta, { wrote }));
|
|
4375
|
+
process.exit(delta.regressed ? 1 : 0);
|
|
4376
|
+
}
|
|
4377
|
+
catch (err) {
|
|
4378
|
+
process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
4379
|
+
process.exit(1);
|
|
4380
|
+
}
|
|
4381
|
+
})();
|
|
4382
|
+
}
|
|
4334
4383
|
if (process.argv[2] === "evidence") {
|
|
4335
4384
|
const args = process.argv.slice(3);
|
|
4336
4385
|
if (args.includes("--help") || args.includes("-h")) {
|
|
@@ -4790,56 +4839,16 @@ if (process.argv[2] === "doctor" && process.argv[3] !== "health") {
|
|
|
4790
4839
|
process.exit(0);
|
|
4791
4840
|
}
|
|
4792
4841
|
(async () => {
|
|
4793
|
-
const {
|
|
4794
|
-
const { join } = await import("node:path");
|
|
4795
|
-
const os = await import("node:os");
|
|
4796
|
-
const { fileURLToPath } = await import("node:url");
|
|
4797
|
-
const { assessDeploymentFreshness, formatFreshness } = await import("./deploymentFreshness.js");
|
|
4842
|
+
const { assessDeploymentFreshness, discoverLocks, formatFreshness, installedBuildTimeMs, } = await import("./deploymentFreshness.js");
|
|
4798
4843
|
// The installed build we are comparing against. Falls back to this
|
|
4799
4844
|
// process's own dist when the global package is absent (running from a
|
|
4800
4845
|
// checkout), which is the honest reference in that case.
|
|
4801
|
-
|
|
4802
|
-
for (const candidate of [
|
|
4803
|
-
join(process.env.npm_config_prefix ?? "/opt/homebrew", "lib/node_modules/patchwork-os/dist/index.js"),
|
|
4804
|
-
// `fileURLToPath`, NOT `.pathname`. On Windows a file URL's pathname is
|
|
4805
|
-
// `/D:/a/...` — a leading slash before the drive letter — which statSync
|
|
4806
|
-
// cannot open, so this fallback never resolved there and `patchwork
|
|
4807
|
-
// doctor` from a checkout always exited 2 with "could not locate an
|
|
4808
|
-
// installed build". Found by the dispatch test added alongside this, on
|
|
4809
|
-
// the Windows CI cells only.
|
|
4810
|
-
fileURLToPath(new URL("./index.js", import.meta.url)),
|
|
4811
|
-
]) {
|
|
4812
|
-
try {
|
|
4813
|
-
buildTimeMs = statSync(candidate).mtimeMs;
|
|
4814
|
-
break;
|
|
4815
|
-
}
|
|
4816
|
-
catch {
|
|
4817
|
-
// try the next candidate
|
|
4818
|
-
}
|
|
4819
|
-
}
|
|
4846
|
+
const buildTimeMs = installedBuildTimeMs();
|
|
4820
4847
|
if (buildTimeMs === undefined) {
|
|
4821
4848
|
process.stderr.write("[deployment] could not locate an installed build to compare against\n");
|
|
4822
4849
|
process.exit(2);
|
|
4823
4850
|
}
|
|
4824
|
-
const
|
|
4825
|
-
const locks = [];
|
|
4826
|
-
try {
|
|
4827
|
-
for (const f of readdirSync(dir)) {
|
|
4828
|
-
if (!f.endsWith(".lock"))
|
|
4829
|
-
continue;
|
|
4830
|
-
try {
|
|
4831
|
-
const d = JSON.parse(readFileSync(join(dir, f), "utf-8"));
|
|
4832
|
-
if (typeof d?.pid === "number")
|
|
4833
|
-
locks.push({ ...d, file: f });
|
|
4834
|
-
}
|
|
4835
|
-
catch {
|
|
4836
|
-
// An unparseable lock is not this command's problem to solve.
|
|
4837
|
-
}
|
|
4838
|
-
}
|
|
4839
|
-
}
|
|
4840
|
-
catch {
|
|
4841
|
-
// No lock dir ⇒ no bridges ⇒ reported as "nothing running".
|
|
4842
|
-
}
|
|
4851
|
+
const locks = discoverLocks();
|
|
4843
4852
|
// `--expect-running [N]` (#1481). Only the caller knows how many bridges
|
|
4844
4853
|
// ought to be up — this command cannot discover it — so the expectation is
|
|
4845
4854
|
// stated rather than inferred. Bare `--expect-running` means "at least one",
|
|
@@ -4998,55 +5007,19 @@ if (process.argv[2] === "privacy") {
|
|
|
4998
5007
|
// every installed recipe, which agent steps declare no data_policy, and
|
|
4999
5008
|
// what flows into them".
|
|
5000
5009
|
if (args[0] === "undeclared") {
|
|
5001
|
-
const {
|
|
5002
|
-
const pathMod = await import("node:path");
|
|
5003
|
-
const { parse: parseYaml } = await import("yaml");
|
|
5004
|
-
const { undeclaredInRecipe, formatUndeclared } = await import("./privacy/undeclaredSteps.js");
|
|
5010
|
+
const { scanRecipeDir, formatUndeclared } = await import("./privacy/undeclaredSteps.js");
|
|
5005
5011
|
const dirIdx = args.indexOf("--dir");
|
|
5006
5012
|
const dir = dirIdx !== -1 && args[dirIdx + 1]
|
|
5007
5013
|
? args[dirIdx + 1]
|
|
5008
5014
|
: patchworkPath("recipes");
|
|
5009
|
-
let
|
|
5015
|
+
let report;
|
|
5010
5016
|
try {
|
|
5011
|
-
|
|
5017
|
+
report = scanRecipeDir(dir);
|
|
5012
5018
|
}
|
|
5013
5019
|
catch {
|
|
5014
5020
|
process.stderr.write(`[privacy] cannot read ${dir}\n`);
|
|
5015
5021
|
process.exit(1);
|
|
5016
5022
|
}
|
|
5017
|
-
let agentSteps = 0;
|
|
5018
|
-
let declared = 0;
|
|
5019
|
-
const undeclared = [];
|
|
5020
|
-
const unreadable = [];
|
|
5021
|
-
let recipesScanned = 0;
|
|
5022
|
-
for (const f of entries.sort()) {
|
|
5023
|
-
if (!/\.ya?ml$/i.test(f))
|
|
5024
|
-
continue;
|
|
5025
|
-
recipesScanned++;
|
|
5026
|
-
let parsed;
|
|
5027
|
-
try {
|
|
5028
|
-
parsed = parseYaml(readFileSync(pathMod.join(dir, f), "utf-8"));
|
|
5029
|
-
}
|
|
5030
|
-
catch {
|
|
5031
|
-
unreadable.push(f);
|
|
5032
|
-
continue;
|
|
5033
|
-
}
|
|
5034
|
-
const name = parsed?.name &&
|
|
5035
|
-
typeof parsed.name === "string"
|
|
5036
|
-
? parsed.name
|
|
5037
|
-
: f;
|
|
5038
|
-
const r = undeclaredInRecipe(name, parsed);
|
|
5039
|
-
agentSteps += r.agentSteps;
|
|
5040
|
-
declared += r.declared;
|
|
5041
|
-
undeclared.push(...r.steps);
|
|
5042
|
-
}
|
|
5043
|
-
const report = {
|
|
5044
|
-
recipesScanned,
|
|
5045
|
-
agentSteps,
|
|
5046
|
-
declared,
|
|
5047
|
-
undeclared,
|
|
5048
|
-
unreadable,
|
|
5049
|
-
};
|
|
5050
5023
|
process.stdout.write(args.includes("--json")
|
|
5051
5024
|
? `${JSON.stringify(report, null, 2)}\n`
|
|
5052
5025
|
: formatUndeclared(report));
|