@webpresso/agent-kit 3.2.0 → 3.3.1
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/bin/_run.js +4 -1
- package/catalog/agent/rules/pre-implementation.md +15 -0
- package/dist/esm/audit/blueprint-trust.js +14 -10
- package/dist/esm/audit/changeset-evidence.d.ts +23 -0
- package/dist/esm/audit/changeset-evidence.js +30 -0
- package/dist/esm/audit/changeset-required.d.ts +10 -0
- package/dist/esm/audit/changeset-required.js +73 -0
- package/dist/esm/audit/ci-guardrails-detection.d.ts +27 -0
- package/dist/esm/audit/ci-guardrails-detection.js +68 -0
- package/dist/esm/audit/ci-guardrails-wiring.js +3 -13
- package/dist/esm/audit/registry.d.ts +4 -4
- package/dist/esm/audit/registry.js +6 -1
- package/dist/esm/blueprint/core/parser.js +61 -4
- package/dist/esm/blueprint/core/schema.d.ts +1 -0
- package/dist/esm/blueprint/core/schema.js +26 -0
- package/dist/esm/blueprint/lifecycle/audit.js +22 -10
- package/dist/esm/blueprint/lifecycle/review-provenance.d.ts +6 -0
- package/dist/esm/blueprint/lifecycle/review-provenance.js +39 -8
- package/dist/esm/blueprint/lifecycle/rollout-anchor.d.ts +97 -0
- package/dist/esm/blueprint/lifecycle/rollout-anchor.js +161 -0
- package/dist/esm/blueprint/sync/client.d.ts +2 -1
- package/dist/esm/blueprint/sync/client.js +1 -1
- package/dist/esm/blueprint/trust/dossier.d.ts +1 -0
- package/dist/esm/blueprint/trust/dossier.js +40 -6
- package/dist/esm/blueprint/trust/promotion.js +12 -0
- package/dist/esm/build/cli-mcp-parity.js +5 -0
- package/dist/esm/cli/cli.d.ts +1 -1
- package/dist/esm/cli/cli.js +9 -0
- package/dist/esm/cli/commands/agent-launch.d.ts +2 -0
- package/dist/esm/cli/commands/agent-launch.js +31 -20
- package/dist/esm/cli/commands/audit.js +4 -0
- package/dist/esm/cli/commands/blueprint/abandon.d.ts +21 -0
- package/dist/esm/cli/commands/blueprint/abandon.js +178 -0
- package/dist/esm/cli/commands/blueprint/router-dispatch.js +21 -0
- package/dist/esm/cli/commands/blueprint/router-output.js +1 -0
- package/dist/esm/cli/commands/blueprint/router.d.ts +2 -0
- package/dist/esm/cli/commands/blueprint/router.js +5 -2
- package/dist/esm/cli/commands/ci-preflight.js +24 -3
- package/dist/esm/cli/commands/init/convergence-apply.d.ts +87 -0
- package/dist/esm/cli/commands/init/convergence-apply.js +189 -23
- package/dist/esm/cli/commands/init/index.d.ts +2 -0
- package/dist/esm/cli/commands/init/index.js +2 -2
- package/dist/esm/cli/commands/init/package-root.d.ts +7 -0
- package/dist/esm/cli/commands/init/package-root.js +10 -2
- package/dist/esm/cli/commands/init/scaffold-base-kit.d.ts +12 -3
- package/dist/esm/cli/commands/init/scaffold-base-kit.js +31 -30
- package/dist/esm/cli/commands/init/scaffolders/agent-hooks/index.js +3 -1
- package/dist/esm/cli/commands/init/scaffolders/file-transaction.d.ts +17 -2
- package/dist/esm/cli/commands/init/scaffolders/file-transaction.js +189 -17
- package/dist/esm/cli/commands/opencode-probe.d.ts +118 -0
- package/dist/esm/cli/commands/opencode-probe.js +323 -0
- package/dist/esm/cli/commands/opencode-rotate.d.ts +23 -0
- package/dist/esm/cli/commands/opencode-rotate.js +23 -11
- package/dist/esm/cli/commands/quality-runner.js +8 -4
- package/dist/esm/cli/commands/review.js +27 -5
- package/dist/esm/cli/commands/rust-check.d.ts +24 -0
- package/dist/esm/cli/commands/rust-check.js +110 -0
- package/dist/esm/cli/commands/typecheck.d.ts +1 -0
- package/dist/esm/cli/commands/typecheck.js +30 -1
- package/dist/esm/cli/commands/worktree/router-dispatch.d.ts +14 -0
- package/dist/esm/cli/commands/worktree/router-dispatch.js +138 -2
- package/dist/esm/cli/commands/worktree/router.js +10 -0
- package/dist/esm/hooks/doctor.d.ts +2 -0
- package/dist/esm/hooks/doctor.js +37 -1
- package/dist/esm/hooks/pretool-guard/validators/worktree-discipline.js +6 -1
- package/dist/esm/hooks/shared/types.js +27 -1
- package/dist/esm/hooks/stop/qa-changed-files.js +18 -0
- package/dist/esm/mcp/blueprint/handlers/document-mutations.d.ts +5 -0
- package/dist/esm/mcp/blueprint/handlers/document-mutations.js +31 -7
- package/dist/esm/mcp/tools/_shared/audit-kinds.d.ts +1 -1
- package/dist/esm/mcp/tools/audit.d.ts +2 -1
- package/dist/esm/mcp/tools/audit.js +12 -0
- package/dist/esm/mcp/tools/audits.d.ts +1 -0
- package/dist/esm/mcp/tools/session-fetch-and-index.d.ts +1 -1
- package/dist/esm/package.json +2 -0
- package/dist/esm/platform/client.d.ts +7 -1
- package/dist/esm/platform/github-contract-source.d.ts +2 -1
- package/dist/esm/pr-description/contract.js +24 -4
- package/dist/esm/review/authority.js +14 -9
- package/dist/esm/review/availability.d.ts +11 -0
- package/dist/esm/review/availability.js +18 -3
- package/dist/esm/review/execution/adapters.d.ts +11 -1
- package/dist/esm/review/execution/adapters.js +10 -1
- package/dist/esm/review/execution/artifacts.d.ts +1 -1
- package/dist/esm/review/execution/review-checkout.d.ts +6 -0
- package/dist/esm/review/execution/review-checkout.js +61 -16
- package/dist/esm/review/execution/supervisor.js +31 -4
- package/dist/esm/review/execution/types.d.ts +1 -0
- package/dist/esm/review/subject.d.ts +13 -0
- package/dist/esm/review/subject.js +42 -4
- package/dist/esm/rust/affected-manifests.d.ts +38 -0
- package/dist/esm/rust/affected-manifests.js +69 -0
- package/dist/esm/session-memory/fetch-index.d.ts +1 -1
- package/dist/esm/status/snapshot.js +2 -6
- package/dist/esm/test-helpers/global-setup.d.ts +0 -1
- package/dist/esm/test-helpers/global-setup.js +18 -15
- package/dist/esm/typecheck/planner.d.ts +9 -1
- package/dist/esm/typecheck/planner.js +31 -0
- package/dist/esm/worktrees/orphan-scan.d.ts +50 -0
- package/dist/esm/worktrees/orphan-scan.js +211 -0
- package/dist/esm/worktrees/owner-dirt.d.ts +40 -0
- package/dist/esm/worktrees/owner-dirt.js +104 -0
- package/package.json +14 -12
|
@@ -23,24 +23,34 @@ function frame(hash, label, value) {
|
|
|
23
23
|
hash.update(`${label}\0${bytes.byteLength}\0`, "utf8");
|
|
24
24
|
hash.update(bytes);
|
|
25
25
|
}
|
|
26
|
-
|
|
26
|
+
// Fingerprint over the filtered line set `status()` produces below, minus
|
|
27
|
+
// pure gitignored (`!!`) entries, so the two contamination checks
|
|
28
|
+
// (`changes()`'s status() diff and the caller worktree fingerprint) cannot
|
|
29
|
+
// drift apart on what counts as "source" content while still tolerating
|
|
30
|
+
// expected runtime bookkeeping.
|
|
31
|
+
//
|
|
32
|
+
// Before the status()-alignment fix, the fingerprint hashed the raw
|
|
33
|
+
// unfiltered `git status` output (without `--ignored`), so anything status()
|
|
34
|
+
// already tolerates (the self-hosting bootstrap's idempotent `package.json`
|
|
35
|
+
// trailing-whitespace normalization, and non-source ignored paths under
|
|
36
|
+
// dist/.tshy/node_modules) could still flip the fingerprint and false-flag a
|
|
37
|
+
// clean caller worktree as contaminated.
|
|
38
|
+
//
|
|
39
|
+
// Reusing status() alone re-introduces a different false positive: the
|
|
40
|
+
// supervisor writes process records under gitignored `.webpresso/` (and similar
|
|
41
|
+
// runtime roots) during the run. Those appear as `!!` lines only when
|
|
42
|
+
// `--ignored` is on. Dropping `!!` here restores the prior fingerprint
|
|
43
|
+
// contract for ignored paths while keeping the package.json / non-`!!` filters
|
|
44
|
+
// shared with status(). Checkout-side status() still reports non-derived
|
|
45
|
+
// gitignored writes (e.g. a stray `.env`) on the disposable review tree.
|
|
46
|
+
function worktreeFingerprint(cwd, ignoredPaths = new Set()) {
|
|
27
47
|
const hash = createHash("sha256");
|
|
28
|
-
const
|
|
29
|
-
frame(hash, "
|
|
48
|
+
const lines = status(cwd, ignoredPaths).filter((line) => !line.startsWith("!!"));
|
|
49
|
+
frame(hash, "status-lines", lines.join("\n"));
|
|
30
50
|
frame(hash, "staged", gitBytes(cwd, ["diff", "--cached", "--binary", "--no-ext-diff", "HEAD", "--"]));
|
|
31
|
-
const records = porcelain.split("\0").filter(Boolean);
|
|
32
51
|
const dirtyPaths = new Set();
|
|
33
|
-
for (
|
|
34
|
-
|
|
35
|
-
dirtyPaths.add(record.slice(3));
|
|
36
|
-
if (record.slice(0, 2).match(/[RC]/u)) {
|
|
37
|
-
const pairedPath = records[index + 1];
|
|
38
|
-
if (pairedPath) {
|
|
39
|
-
dirtyPaths.add(pairedPath);
|
|
40
|
-
index += 1;
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
}
|
|
52
|
+
for (const line of lines)
|
|
53
|
+
dirtyPaths.add(statusEntryPath(line));
|
|
44
54
|
for (const relativePath of [...dirtyPaths].filter(Boolean).toSorted()) {
|
|
45
55
|
const absolutePath = join(cwd, relativePath);
|
|
46
56
|
frame(hash, "dirty-path", relativePath);
|
|
@@ -108,13 +118,44 @@ function isNormalizedPackageJsonModification(cwd, line) {
|
|
|
108
118
|
return false;
|
|
109
119
|
}
|
|
110
120
|
}
|
|
121
|
+
// Single source of truth for turning a `git status --porcelain=v1` line into
|
|
122
|
+
// its relative path, shared by status()'s filter predicate and
|
|
123
|
+
// worktreeFingerprint()'s dirty-path hashing so both stay aligned by
|
|
124
|
+
// construction.
|
|
125
|
+
//
|
|
126
|
+
// Rename/copy lines use non-null porcelain form `XY ORIG -> DEST` (or quoted
|
|
127
|
+
// paths). The destination is the live path whose bytes must be fingerprinted;
|
|
128
|
+
// using the raw `ORIG -> DEST` tail makes lstat miss both sides and silently
|
|
129
|
+
// drops content from the hash (regression vs paired-record `-z` parsing).
|
|
130
|
+
function statusEntryPath(line) {
|
|
131
|
+
const tail = line.slice(3).replace(/\/$/u, "");
|
|
132
|
+
const renameSep = " -> ";
|
|
133
|
+
const sep = tail.lastIndexOf(renameSep);
|
|
134
|
+
const pathPart = sep === -1 ? tail : tail.slice(sep + renameSep.length);
|
|
135
|
+
return unquotePorcelainPath(pathPart);
|
|
136
|
+
}
|
|
137
|
+
/** Strip git porcelain double-quotes and reverse C-style escapes for a path. */
|
|
138
|
+
function unquotePorcelainPath(pathPart) {
|
|
139
|
+
if (!pathPart.startsWith('"') || !pathPart.endsWith('"') || pathPart.length < 2) {
|
|
140
|
+
return pathPart;
|
|
141
|
+
}
|
|
142
|
+
return pathPart.slice(1, -1).replace(/\\([\\"ntr])/gu, (_m, ch) => {
|
|
143
|
+
if (ch === "n")
|
|
144
|
+
return "\n";
|
|
145
|
+
if (ch === "t")
|
|
146
|
+
return "\t";
|
|
147
|
+
if (ch === "r")
|
|
148
|
+
return "\r";
|
|
149
|
+
return ch;
|
|
150
|
+
});
|
|
151
|
+
}
|
|
111
152
|
function status(cwd, ignoredPaths = new Set()) {
|
|
112
153
|
const output = git(cwd, ["status", "--porcelain=v1", "--untracked-files=all", "--ignored"]);
|
|
113
154
|
return output
|
|
114
155
|
.split(/\r?\n/u)
|
|
115
156
|
.filter((line) => line.length > 0)
|
|
116
157
|
.filter((line) => {
|
|
117
|
-
const relativePath = line
|
|
158
|
+
const relativePath = statusEntryPath(line);
|
|
118
159
|
if (ignoredPaths.has(relativePath))
|
|
119
160
|
return false;
|
|
120
161
|
if (line.startsWith("!!") && isNonSourceIgnoredPath(relativePath))
|
|
@@ -226,3 +267,7 @@ function projectCallerDependencies(repositoryRoot, checkoutRoot) {
|
|
|
226
267
|
projected.add(dependencyPath);
|
|
227
268
|
return projected;
|
|
228
269
|
}
|
|
270
|
+
/** Test-only surface for porcelain path parsing. */
|
|
271
|
+
export const reviewCheckoutInternals = {
|
|
272
|
+
statusEntryPath,
|
|
273
|
+
};
|
|
@@ -123,6 +123,10 @@ export async function superviseReviewExecution(input, adapter, deps = {}) {
|
|
|
123
123
|
let fallbackSelectionTimer;
|
|
124
124
|
let pendingFallback;
|
|
125
125
|
const seenFrontiers = new Set();
|
|
126
|
+
const droppedEventWarnings = [];
|
|
127
|
+
const recordDroppedProviderEvent = (reason) => {
|
|
128
|
+
droppedEventWarnings.push(reason);
|
|
129
|
+
};
|
|
126
130
|
const decoder = new StringDecoder("utf8");
|
|
127
131
|
const diagnosticDecoder = new StringDecoder("utf8");
|
|
128
132
|
let buffered = "";
|
|
@@ -230,19 +234,41 @@ export async function superviseReviewExecution(input, adapter, deps = {}) {
|
|
|
230
234
|
const processLine = (line) => {
|
|
231
235
|
if (line.trim().length === 0 || status !== null)
|
|
232
236
|
return;
|
|
233
|
-
|
|
234
|
-
fail("protocol-unsupported", "provider-event-too-large");
|
|
235
|
-
return;
|
|
236
|
-
}
|
|
237
|
+
const oversized = Buffer.byteLength(line, "utf8") > MAX_PROVIDER_EVENT_BYTES;
|
|
237
238
|
let value;
|
|
238
239
|
try {
|
|
239
240
|
value = JSON.parse(line);
|
|
240
241
|
}
|
|
241
242
|
catch {
|
|
243
|
+
// An oversized frame that also fails to parse carries no recoverable
|
|
244
|
+
// signal (a genuinely truncated/malformed line looks the same either
|
|
245
|
+
// way) -- drop it with a warning instead of failing a run that may
|
|
246
|
+
// still receive a complete, well-formed terminal verdict later. A
|
|
247
|
+
// normal-sized malformed line is still a protocol violation.
|
|
248
|
+
if (oversized) {
|
|
249
|
+
recordDroppedProviderEvent("provider-event-too-large-unparseable");
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
242
252
|
fail("protocol-unsupported", "invalid-json-event");
|
|
243
253
|
return;
|
|
244
254
|
}
|
|
245
255
|
const event = adapter.parseEvent(value);
|
|
256
|
+
if (oversized && (event.kind === "terminal" || event.kind === "failure")) {
|
|
257
|
+
// Terminal/verdict and failure frames are load-bearing: an oversized
|
|
258
|
+
// one is treated as a protocol violation, same as before this fix.
|
|
259
|
+
fail("protocol-unsupported", "provider-event-too-large");
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
if (oversized) {
|
|
263
|
+
// Non-terminal (progress/neutral/unsupported) oversized frames are
|
|
264
|
+
// dropped rather than failing the whole run -- a provider that echoes
|
|
265
|
+
// e.g. a large lockfile read in a progress frame must not discard an
|
|
266
|
+
// otherwise-complete review verdict. See catalog/agent/rules
|
|
267
|
+
// /no-timeout-as-fix.md: the byte bound stays in place, we just stop
|
|
268
|
+
// treating "one big frame" as fatal to the whole run.
|
|
269
|
+
recordDroppedProviderEvent(`provider-event-too-large-${event.kind}`);
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
246
272
|
if (event.kind === "unsupported") {
|
|
247
273
|
fail("protocol-unsupported", "unknown-provider-event");
|
|
248
274
|
return;
|
|
@@ -433,6 +459,7 @@ export async function superviseReviewExecution(input, adapter, deps = {}) {
|
|
|
433
459
|
...(limitWindow === undefined ? {} : { limitWindow }),
|
|
434
460
|
...(retryAfterMs === undefined ? {} : { retryAfterMs }),
|
|
435
461
|
summaryPath: artifacts.summaryPath,
|
|
462
|
+
...(droppedEventWarnings.length > 0 ? { warnings: [...droppedEventWarnings] } : {}),
|
|
436
463
|
};
|
|
437
464
|
}
|
|
438
465
|
function recordReviewProcessStart(reviewChild, processGroupId) {
|
|
@@ -43,9 +43,22 @@ export declare function createPlanSubjectDigestForContent(blueprintSlug: string,
|
|
|
43
43
|
* `normalizePlanSubjectContent`.
|
|
44
44
|
*/
|
|
45
45
|
export declare function legacyNormalizePlanSubjectContent(content: Buffer | string): Buffer;
|
|
46
|
+
/**
|
|
47
|
+
* Digest-compatibility variant for review events recorded after the lifecycle
|
|
48
|
+
* stripped-key extension but before the advisory `tags` strip. Verification
|
|
49
|
+
* accepts a match against this tier so existing plan approvals on tagged
|
|
50
|
+
* blueprints are not stranded by the tags-strip rollout.
|
|
51
|
+
*/
|
|
52
|
+
export declare function preTagsStripNormalizePlanSubjectContent(content: Buffer | string): Buffer;
|
|
46
53
|
export declare function createPlanSubjectAtRef(cwd: string, ref: string, blueprintSlug: string, blueprintPath?: string): ReviewSubject;
|
|
47
54
|
/** Plan subject digested with the pre-extension normalization (see above). */
|
|
48
55
|
export declare function createLegacyPlanSubjectAtRef(cwd: string, ref: string, blueprintSlug: string, blueprintPath?: string): ReviewSubject;
|
|
56
|
+
/** Plan subject digested without the advisory `tags` strip (pre-tags-strip tier). */
|
|
57
|
+
export declare function createPreTagsStripPlanSubjectAtRef(cwd: string, ref: string, blueprintSlug: string, blueprintPath?: string): ReviewSubject;
|
|
58
|
+
/** Working-ledger digest for the pre-tags-strip compatibility tier. */
|
|
59
|
+
export declare function createPreTagsStripPlanSubjectDigestForContent(blueprintSlug: string, content: Buffer | string): string;
|
|
60
|
+
/** Working-ledger digest for the pre-extension legacy compatibility tier. */
|
|
61
|
+
export declare function createLegacyPlanSubjectDigestForContent(blueprintSlug: string, content: Buffer | string): string;
|
|
49
62
|
export declare const reviewSubjectInternals: {
|
|
50
63
|
isExcludedReviewEnvelope: typeof isExcludedReviewEnvelope;
|
|
51
64
|
isReviewEvidencePath: typeof isReviewEvidencePath;
|
|
@@ -312,26 +312,32 @@ function neutralizePromotionGateResults(text) {
|
|
|
312
312
|
// continuation lines). Each mirrors exactly one lifecycle writer:
|
|
313
313
|
// `completed_at` (upsertCompletedAt) and the `wp blueprint start` /
|
|
314
314
|
// progress-stamp fields (setBlueprintFrontmatterFields lifecycle intents).
|
|
315
|
-
|
|
315
|
+
// Lifecycle writers only — the pre-tags-strip normalization (compat tier).
|
|
316
|
+
const PLAN_LIFECYCLE_STRIPPED_KEYS_CORE = [
|
|
316
317
|
"completed_at",
|
|
317
318
|
"last_updated",
|
|
318
319
|
"progress",
|
|
319
320
|
"worktree_owner_id",
|
|
320
321
|
"worktree_owner_branch",
|
|
321
322
|
];
|
|
323
|
+
// Current plan-subject strip set: core lifecycle keys plus advisory `tags`.
|
|
324
|
+
// Tags are not plan substance; MCP put and CLI serialize disagree on array-item
|
|
325
|
+
// quoting (`- "x"` vs `- x`). Approvals recorded before the tags strip must
|
|
326
|
+
// still verify via `preTagsStripNormalizePlanSubjectContent`.
|
|
327
|
+
const PLAN_LIFECYCLE_STRIPPED_KEYS = [...PLAN_LIFECYCLE_STRIPPED_KEYS_CORE, "tags"];
|
|
322
328
|
// Neutralize the lifecycle-owned frontmatter fields (`status:` value plus the
|
|
323
|
-
// stripped keys above) INSIDE the leading frontmatter block only. A body line
|
|
329
|
+
// stripped keys above, including advisory `tags`) INSIDE the leading frontmatter block only. A body line
|
|
324
330
|
// that merely looks like one of these (e.g. task text reading
|
|
325
331
|
// "completed_at: must remain unset until release") is plan substance and must
|
|
326
332
|
// keep affecting the digest. Stripping removes the key line and any indented
|
|
327
333
|
// continuation lines so folded (`>-`) values normalize away too.
|
|
328
|
-
function neutralizeLifecycleFrontmatter(text) {
|
|
334
|
+
function neutralizeLifecycleFrontmatter(text, strippedKeys = PLAN_LIFECYCLE_STRIPPED_KEYS) {
|
|
329
335
|
const match = /^---\r?\n[\s\S]*?\r?\n---(?:\r?\n|$)/u.exec(text);
|
|
330
336
|
if (!match)
|
|
331
337
|
return text;
|
|
332
338
|
let frontmatter = match[0];
|
|
333
339
|
frontmatter = frontmatter.replace(/^(status:\s*)(['"]?)[^'"\r\n]+?(['"]?)(\s*)$/m, `$1${PLAN_STATUS_PLACEHOLDER}$4`);
|
|
334
|
-
for (const key of
|
|
340
|
+
for (const key of strippedKeys) {
|
|
335
341
|
frontmatter = frontmatter.replace(new RegExp(String.raw `^${key}:[^\n]*\n?(?:[ \t]+[^\n]*\n?)*`, "gmu"), "");
|
|
336
342
|
}
|
|
337
343
|
return `${frontmatter}${text.slice(match[0].length)}`;
|
|
@@ -463,6 +469,20 @@ export function legacyNormalizePlanSubjectContent(content) {
|
|
|
463
469
|
text = neutralizePromotionGateResults(text);
|
|
464
470
|
return Buffer.from(text, "utf8");
|
|
465
471
|
}
|
|
472
|
+
/**
|
|
473
|
+
* Digest-compatibility variant for review events recorded after the lifecycle
|
|
474
|
+
* stripped-key extension but before the advisory `tags` strip. Verification
|
|
475
|
+
* accepts a match against this tier so existing plan approvals on tagged
|
|
476
|
+
* blueprints are not stranded by the tags-strip rollout.
|
|
477
|
+
*/
|
|
478
|
+
export function preTagsStripNormalizePlanSubjectContent(content) {
|
|
479
|
+
let text = typeof content === "string" ? content : content.toString("utf8");
|
|
480
|
+
text = neutralizeLifecycleFrontmatter(text, PLAN_LIFECYCLE_STRIPPED_KEYS_CORE);
|
|
481
|
+
text = neutralizeReadinessStamps(text);
|
|
482
|
+
text = neutralizePromotionGateResults(text);
|
|
483
|
+
text = neutralizeTaskExecutionState(text);
|
|
484
|
+
return Buffer.from(text, "utf8");
|
|
485
|
+
}
|
|
466
486
|
function planSubjectAtRef(cwd, ref, blueprintSlug, normalize, blueprintPath) {
|
|
467
487
|
const commit = resolveReviewCommit(cwd, ref);
|
|
468
488
|
const entries = parseTree(cwd, commit);
|
|
@@ -491,6 +511,24 @@ export function createPlanSubjectAtRef(cwd, ref, blueprintSlug, blueprintPath) {
|
|
|
491
511
|
export function createLegacyPlanSubjectAtRef(cwd, ref, blueprintSlug, blueprintPath) {
|
|
492
512
|
return planSubjectAtRef(cwd, ref, blueprintSlug, legacyNormalizePlanSubjectContent, blueprintPath);
|
|
493
513
|
}
|
|
514
|
+
/** Plan subject digested without the advisory `tags` strip (pre-tags-strip tier). */
|
|
515
|
+
export function createPreTagsStripPlanSubjectAtRef(cwd, ref, blueprintSlug, blueprintPath) {
|
|
516
|
+
return planSubjectAtRef(cwd, ref, blueprintSlug, preTagsStripNormalizePlanSubjectContent, blueprintPath);
|
|
517
|
+
}
|
|
518
|
+
/** Working-ledger digest for the pre-tags-strip compatibility tier. */
|
|
519
|
+
export function createPreTagsStripPlanSubjectDigestForContent(blueprintSlug, content) {
|
|
520
|
+
const hash = createHash("sha256").update(PLAN_DOMAIN);
|
|
521
|
+
updateField(hash, blueprintSlug);
|
|
522
|
+
updateField(hash, preTagsStripNormalizePlanSubjectContent(content));
|
|
523
|
+
return hash.digest("hex");
|
|
524
|
+
}
|
|
525
|
+
/** Working-ledger digest for the pre-extension legacy compatibility tier. */
|
|
526
|
+
export function createLegacyPlanSubjectDigestForContent(blueprintSlug, content) {
|
|
527
|
+
const hash = createHash("sha256").update(PLAN_DOMAIN);
|
|
528
|
+
updateField(hash, blueprintSlug);
|
|
529
|
+
updateField(hash, legacyNormalizePlanSubjectContent(content));
|
|
530
|
+
return hash.digest("hex");
|
|
531
|
+
}
|
|
494
532
|
export const reviewSubjectInternals = {
|
|
495
533
|
isExcludedReviewEnvelope,
|
|
496
534
|
isReviewEvidencePath,
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Maps changed repo paths to the tracked Rust crate(s) they belong to.
|
|
3
|
+
*
|
|
4
|
+
* Pure and dependency-free so it is unit-testable without shelling out to
|
|
5
|
+
* `cargo` or `git` — see `#cli/commands/rust-check.js` for the CLI command
|
|
6
|
+
* that spawns `cargo check`/`cargo test` for whatever this resolves.
|
|
7
|
+
*/
|
|
8
|
+
export interface TrackedRustCrate {
|
|
9
|
+
/** Crate directory relative to the repo root, e.g. "native/dash-tui". */
|
|
10
|
+
readonly root: string;
|
|
11
|
+
/** Manifest path relative to the repo root. */
|
|
12
|
+
readonly manifest: string;
|
|
13
|
+
/** `cargo` args (after `cargo`) that prove this crate's tests still pass. */
|
|
14
|
+
readonly testArgs: readonly string[];
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* The Rust crates `wp ci-preflight` proves compile + pass on an affected
|
|
18
|
+
* change. Mirrors exactly what `.github/workflows/ci.agent-kit.yml` (native
|
|
19
|
+
* session-memory + dash-tui jobs) and `.github/workflows/desktop-perf.yml`
|
|
20
|
+
* (desktop `src-tauri`) already build and test in CI — no new cargo
|
|
21
|
+
* invocation shape is invented here.
|
|
22
|
+
*
|
|
23
|
+
* Deliberately EXCLUDES the `spikes/**` crates (`spikes/pty-spawn-transport/
|
|
24
|
+
* rust`, `spikes/desktop-pty-loadtest/rust`,
|
|
25
|
+
* `spikes/desktop-pty-loadtest/src-tauri`): no CI workflow builds or tests
|
|
26
|
+
* them today — they are scratch/prototype crates with no compile guarantee.
|
|
27
|
+
* Gating pre-push on them would fail closed on code nobody has ever promised
|
|
28
|
+
* compiles. Revisit this list if a spike graduates to a tracked crate.
|
|
29
|
+
*/
|
|
30
|
+
export declare const TRACKED_RUST_CRATES: readonly TrackedRustCrate[];
|
|
31
|
+
/**
|
|
32
|
+
* Returns the sorted, deduped list of tracked-crate manifest paths touched by
|
|
33
|
+
* `files`. Rust files under an untracked crate (e.g. `spikes/**`) or outside
|
|
34
|
+
* any crate are ignored, not escalated to "affects everything" — each
|
|
35
|
+
* manifest maps back to `TRACKED_RUST_CRATES` for its `testArgs`.
|
|
36
|
+
*/
|
|
37
|
+
export declare function resolveAffectedRustManifests(files: readonly string[]): readonly string[];
|
|
38
|
+
export declare function trackedCrateForManifest(manifest: string): TrackedRustCrate | undefined;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Maps changed repo paths to the tracked Rust crate(s) they belong to.
|
|
3
|
+
*
|
|
4
|
+
* Pure and dependency-free so it is unit-testable without shelling out to
|
|
5
|
+
* `cargo` or `git` — see `#cli/commands/rust-check.js` for the CLI command
|
|
6
|
+
* that spawns `cargo check`/`cargo test` for whatever this resolves.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* The Rust crates `wp ci-preflight` proves compile + pass on an affected
|
|
10
|
+
* change. Mirrors exactly what `.github/workflows/ci.agent-kit.yml` (native
|
|
11
|
+
* session-memory + dash-tui jobs) and `.github/workflows/desktop-perf.yml`
|
|
12
|
+
* (desktop `src-tauri`) already build and test in CI — no new cargo
|
|
13
|
+
* invocation shape is invented here.
|
|
14
|
+
*
|
|
15
|
+
* Deliberately EXCLUDES the `spikes/**` crates (`spikes/pty-spawn-transport/
|
|
16
|
+
* rust`, `spikes/desktop-pty-loadtest/rust`,
|
|
17
|
+
* `spikes/desktop-pty-loadtest/src-tauri`): no CI workflow builds or tests
|
|
18
|
+
* them today — they are scratch/prototype crates with no compile guarantee.
|
|
19
|
+
* Gating pre-push on them would fail closed on code nobody has ever promised
|
|
20
|
+
* compiles. Revisit this list if a spike graduates to a tracked crate.
|
|
21
|
+
*/
|
|
22
|
+
export const TRACKED_RUST_CRATES = [
|
|
23
|
+
{
|
|
24
|
+
root: "native/session-memory-engine",
|
|
25
|
+
manifest: "native/session-memory-engine/Cargo.toml",
|
|
26
|
+
testArgs: ["test", "--manifest-path", "native/session-memory-engine/Cargo.toml", "--all"],
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
root: "native/dash-tui",
|
|
30
|
+
manifest: "native/dash-tui/Cargo.toml",
|
|
31
|
+
testArgs: ["test", "--manifest-path", "native/dash-tui/Cargo.toml"],
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
root: "apps/desktop/src-tauri",
|
|
35
|
+
manifest: "apps/desktop/src-tauri/Cargo.toml",
|
|
36
|
+
testArgs: ["test", "--manifest-path", "apps/desktop/src-tauri/Cargo.toml", "--lib"],
|
|
37
|
+
},
|
|
38
|
+
];
|
|
39
|
+
function isRustSignalFile(file) {
|
|
40
|
+
return (file.endsWith(".rs") ||
|
|
41
|
+
file === "Cargo.toml" ||
|
|
42
|
+
file.endsWith("/Cargo.toml") ||
|
|
43
|
+
file === "Cargo.lock" ||
|
|
44
|
+
file.endsWith("/Cargo.lock"));
|
|
45
|
+
}
|
|
46
|
+
function owningTrackedCrate(file) {
|
|
47
|
+
return TRACKED_RUST_CRATES.find((candidate) => file === candidate.root || file.startsWith(`${candidate.root}/`));
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Returns the sorted, deduped list of tracked-crate manifest paths touched by
|
|
51
|
+
* `files`. Rust files under an untracked crate (e.g. `spikes/**`) or outside
|
|
52
|
+
* any crate are ignored, not escalated to "affects everything" — each
|
|
53
|
+
* manifest maps back to `TRACKED_RUST_CRATES` for its `testArgs`.
|
|
54
|
+
*/
|
|
55
|
+
export function resolveAffectedRustManifests(files) {
|
|
56
|
+
const manifests = new Set();
|
|
57
|
+
for (const rawFile of files) {
|
|
58
|
+
const file = rawFile.replaceAll("\\", "/");
|
|
59
|
+
if (!isRustSignalFile(file))
|
|
60
|
+
continue;
|
|
61
|
+
const crate = owningTrackedCrate(file);
|
|
62
|
+
if (crate)
|
|
63
|
+
manifests.add(crate.manifest);
|
|
64
|
+
}
|
|
65
|
+
return [...manifests].sort();
|
|
66
|
+
}
|
|
67
|
+
export function trackedCrateForManifest(manifest) {
|
|
68
|
+
return TRACKED_RUST_CRATES.find((candidate) => candidate.manifest === manifest);
|
|
69
|
+
}
|
|
@@ -20,7 +20,7 @@ export interface FetchAndIndexOptions {
|
|
|
20
20
|
signal?: AbortSignal;
|
|
21
21
|
}
|
|
22
22
|
export declare function fetchAndIndex(options: FetchAndIndexOptions, deps?: {
|
|
23
|
-
readonly fetchImpl?:
|
|
23
|
+
readonly fetchImpl?: (input: string | URL | Request, init?: RequestInit) => Promise<Response>;
|
|
24
24
|
/** Internal/test-only escape hatch for loopback fixtures; never exposed in CLI/MCP input. */
|
|
25
25
|
readonly allowedHosts?: readonly string[];
|
|
26
26
|
}): Promise<SessionMemoryChunk[]>;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import { z } from "zod";
|
|
3
|
+
import { hasChangesetEvidence } from "#audit/changeset-evidence.js";
|
|
3
4
|
import { evaluateApprovalReviewers } from "#lifecycle/audit.js";
|
|
4
5
|
import { collectV2ReviewProvenance } from "#lifecycle/review-provenance.js";
|
|
5
6
|
import { evaluateRequiredChecks } from "#mcp/tools/_shared/required-checks.js";
|
|
@@ -210,12 +211,7 @@ function hasDisclosure(body, label) {
|
|
|
210
211
|
return new RegExp(`(?:^|\\n)\\s*(?:-\\s*)?${escaped}:\\s*\\S`, "iu").test(body);
|
|
211
212
|
}
|
|
212
213
|
function hasReleaseEvidence(pr) {
|
|
213
|
-
|
|
214
|
-
if (files.length > 0 && files.every((file) => file.endsWith(".md")))
|
|
215
|
-
return true;
|
|
216
|
-
if (files.some((file) => /^\.changeset\/[^/]+\.md$/u.test(file)))
|
|
217
|
-
return true;
|
|
218
|
-
return /Changeset-exempt:\s*\S/iu.test(stringField(pr.body) ?? "");
|
|
214
|
+
return hasChangesetEvidence(prFiles(pr), [stringField(pr.body) ?? ""]);
|
|
219
215
|
}
|
|
220
216
|
export function evaluatePrReadiness(evidence) {
|
|
221
217
|
const blockers = [];
|
|
@@ -11,10 +11,6 @@ const MIGRATION_SENTINEL = join(REPO_ROOT, "dist", "esm", "blueprint", "db", "mi
|
|
|
11
11
|
// Lock target lives outside dist/ (tshy rewrites dist/) and under the always-present, gitignored
|
|
12
12
|
// node_modules/.cache so a concurrent test run cannot clobber it mid-build.
|
|
13
13
|
const BUILD_LOCK_TARGET = join(REPO_ROOT, "node_modules", ".cache", "wp-test-setup");
|
|
14
|
-
const DIST_PREP_PROJECTS = new Set(["subprocess", "serial-subprocess"]);
|
|
15
|
-
export function shouldPrepareDistForProject(projectName) {
|
|
16
|
-
return projectName !== undefined && DIST_PREP_PROJECTS.has(projectName);
|
|
17
|
-
}
|
|
18
14
|
// Serialize the dist build across concurrent test runs sharing this checkout: tshy's non-atomic
|
|
19
15
|
// dist/package.json writes corrupt each other if two runs build at once. proper-lockfile gives a
|
|
20
16
|
// bounded wait plus native stale-owner recovery (a dead builder's lock is reclaimed after `stale`).
|
|
@@ -33,19 +29,21 @@ async function withDistBuildLock(fn) {
|
|
|
33
29
|
await release();
|
|
34
30
|
}
|
|
35
31
|
}
|
|
36
|
-
//
|
|
37
|
-
//
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
32
|
+
// h-019: scripts/run-vitest-projects.ts spawns the "unit" and "subprocess" vitest projects as two
|
|
33
|
+
// independent, unsynchronized child processes (Promise.all) — the same exposure exists for any two
|
|
34
|
+
// `vitest run --project <name>` invocations that happen to share this checkout. tshy's package.json
|
|
35
|
+
// write below is non-atomic (truncate+write, not write-temp+rename): a project whose setup() returned
|
|
36
|
+
// without taking this lock could start reading package.json (module resolution, etc.) while a sibling
|
|
37
|
+
// project's process was mid-tshy-write, observing a truncated/empty file (JSONError "File is empty").
|
|
38
|
+
// Every project therefore passes through this SAME gate before its tests can start, whether or not
|
|
39
|
+
// that project needs dist output for itself: whichever process reaches the lock first performs the
|
|
40
|
+
// idempotent, re-checked-under-lock build; every other process blocks until that release, then finds
|
|
41
|
+
// the sentinels already satisfied and returns immediately. In the steady state (dist/ already built)
|
|
42
|
+
// this is a single existsSync check with no lock contention at all.
|
|
43
|
+
async function ensureDistPrepared() {
|
|
42
44
|
// Fast path: nothing to build. Only pay the lock when a build artifact is actually missing.
|
|
43
|
-
if (existsSync(DIST_SENTINEL) && existsSync(MIGRATION_SENTINEL))
|
|
44
|
-
if (project?.name === "serial-subprocess") {
|
|
45
|
-
ensureHostRuntimeArtifactsForSerialTests({ rootDir: REPO_ROOT });
|
|
46
|
-
}
|
|
45
|
+
if (existsSync(DIST_SENTINEL) && existsSync(MIGRATION_SENTINEL))
|
|
47
46
|
return;
|
|
48
|
-
}
|
|
49
47
|
await withDistBuildLock(async () => {
|
|
50
48
|
let builtDist = false;
|
|
51
49
|
// Re-check under the lock: another run may have completed the build while we waited.
|
|
@@ -83,6 +81,11 @@ export async function setup(project) {
|
|
|
83
81
|
});
|
|
84
82
|
}
|
|
85
83
|
});
|
|
84
|
+
}
|
|
85
|
+
// Vitest passes the selected TestProject to globalSetup, once per project process.
|
|
86
|
+
export async function setup(project) {
|
|
87
|
+
syncSastRuleAssets(REPO_ROOT);
|
|
88
|
+
await ensureDistPrepared();
|
|
86
89
|
if (project?.name === "serial-subprocess") {
|
|
87
90
|
ensureHostRuntimeArtifactsForSerialTests({ rootDir: REPO_ROOT });
|
|
88
91
|
}
|
|
@@ -13,7 +13,7 @@ export interface PlannedTypecheckCommand {
|
|
|
13
13
|
}
|
|
14
14
|
export interface TypecheckExecutionPlan {
|
|
15
15
|
readonly repoRoot: string;
|
|
16
|
-
readonly mode: "default" | "files" | "packages";
|
|
16
|
+
readonly mode: "default" | "files" | "packages" | "tests";
|
|
17
17
|
readonly commands: readonly PlannedTypecheckCommand[];
|
|
18
18
|
readonly resolvedScopes: readonly TypecheckScope[];
|
|
19
19
|
readonly preambleLine?: string;
|
|
@@ -24,6 +24,14 @@ export interface PlanTypecheckExecutionOptions {
|
|
|
24
24
|
readonly files?: readonly string[];
|
|
25
25
|
readonly packages?: readonly string[];
|
|
26
26
|
readonly pretty?: boolean;
|
|
27
|
+
/**
|
|
28
|
+
* Run the separate test-surface typecheck lane (`tsconfig.test.json`)
|
|
29
|
+
* instead of the default production-source lane. Mutually exclusive with
|
|
30
|
+
* `files`/`packages`. This lane is invoked via `wp typecheck --tests` and is
|
|
31
|
+
* it is not part of the default `wp typecheck` gate or `ci-preflight`
|
|
32
|
+
* until the test-surface error corpus is burned down to zero.
|
|
33
|
+
*/
|
|
34
|
+
readonly tests?: boolean;
|
|
27
35
|
}
|
|
28
36
|
export declare function planTypecheckExecution(options: PlanTypecheckExecutionOptions): TypecheckExecutionPlan;
|
|
29
37
|
export declare function formatResolvedScopesLine(scopes: readonly TypecheckScope[]): string;
|
|
@@ -3,6 +3,7 @@ import { isAbsolute, join, relative, resolve } from "node:path";
|
|
|
3
3
|
import { getPackageScript, readPackageJson, isRecursiveWpScript } from "#cli/package-scripts.js";
|
|
4
4
|
import { getWorkspacePackages } from "#quality-engine/target-resolver.js";
|
|
5
5
|
import { getManagedRunner, resolveEffectiveRunnerRoot } from "#tool-runtime";
|
|
6
|
+
const TEST_TSCONFIG_FILENAME = "tsconfig.test.json";
|
|
6
7
|
const GENERATED_ROOT_PREFIXES = [".webpresso/generated"];
|
|
7
8
|
const OUTPUT_SEGMENTS = new Set(["coverage", "dist", "node_modules"]);
|
|
8
9
|
export function planTypecheckExecution(options) {
|
|
@@ -14,6 +15,17 @@ export function planTypecheckExecution(options) {
|
|
|
14
15
|
if (files.length > 0 && packages.length > 0) {
|
|
15
16
|
throw new Error("Cannot use both --file and --package for typecheck targeting.");
|
|
16
17
|
}
|
|
18
|
+
if (options.tests) {
|
|
19
|
+
if (files.length > 0 || packages.length > 0) {
|
|
20
|
+
throw new Error("Cannot use --tests together with --file or --package.");
|
|
21
|
+
}
|
|
22
|
+
return {
|
|
23
|
+
repoRoot,
|
|
24
|
+
mode: "tests",
|
|
25
|
+
commands: [buildTestsCommand(defaultScopeRoot, options.pretty)],
|
|
26
|
+
resolvedScopes: [],
|
|
27
|
+
};
|
|
28
|
+
}
|
|
17
29
|
if (packages.length > 0) {
|
|
18
30
|
const scopes = resolvePackageScopes(repoRoot, realRepoRoot, packages);
|
|
19
31
|
return {
|
|
@@ -63,6 +75,25 @@ function buildUntargetedCommand(cwd, pretty = false) {
|
|
|
63
75
|
cwd,
|
|
64
76
|
};
|
|
65
77
|
}
|
|
78
|
+
function buildTestsCommand(cwd, pretty = false) {
|
|
79
|
+
const testTsconfigPath = join(cwd, TEST_TSCONFIG_FILENAME);
|
|
80
|
+
if (!existsSync(testTsconfigPath)) {
|
|
81
|
+
throw new Error(`--tests requires ${TEST_TSCONFIG_FILENAME} at the repo root (not found at ${testTsconfigPath}).`);
|
|
82
|
+
}
|
|
83
|
+
const resolution = getManagedRunner("tsc");
|
|
84
|
+
return {
|
|
85
|
+
scope: null,
|
|
86
|
+
command: resolution.command,
|
|
87
|
+
args: [
|
|
88
|
+
...resolution.args,
|
|
89
|
+
"--noEmit",
|
|
90
|
+
"-p",
|
|
91
|
+
TEST_TSCONFIG_FILENAME,
|
|
92
|
+
...(pretty ? [] : ["--pretty", "false"]),
|
|
93
|
+
],
|
|
94
|
+
cwd,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
66
97
|
function buildTargetedCommand(scope, pretty = false) {
|
|
67
98
|
const checkTypesScript = getPackageScript(scope.root, "check-types");
|
|
68
99
|
if (checkTypesScript && !isRecursiveWpScript(checkTypesScript, "typecheck")) {
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { type WorktreeInventory } from "./identity.js";
|
|
2
|
+
export type OrphanClassification = "alive" | "orphan" | "broken" | "unrecognized";
|
|
3
|
+
export interface OrphanScanCandidate {
|
|
4
|
+
readonly path: string;
|
|
5
|
+
readonly repoNamespace: string;
|
|
6
|
+
}
|
|
7
|
+
export interface OrphanScanRow extends OrphanScanCandidate {
|
|
8
|
+
readonly classification: OrphanClassification;
|
|
9
|
+
readonly dirty: boolean | "unknown";
|
|
10
|
+
/**
|
|
11
|
+
* Recursive size, computed ONLY for the final `orphan` classification.
|
|
12
|
+
* `alive` worktrees are actively worked on (often large `node_modules`
|
|
13
|
+
* trees) and are never removable, and `broken`/`unrecognized` rows need
|
|
14
|
+
* manual review regardless of size, so walking their full tree would burn
|
|
15
|
+
* significant time for no decision-relevant signal. `"unknown"` also
|
|
16
|
+
* covers a measurement failure/timeout on an orphan candidate.
|
|
17
|
+
*/
|
|
18
|
+
readonly sizeBytes: number | "unknown";
|
|
19
|
+
readonly removable: boolean;
|
|
20
|
+
readonly reason: string;
|
|
21
|
+
}
|
|
22
|
+
export interface OrphanClassifyDeps {
|
|
23
|
+
readonly resolveRepoRoot: (candidate: OrphanScanCandidate) => string | undefined;
|
|
24
|
+
readonly repoRootExists: (repoRoot: string) => boolean;
|
|
25
|
+
readonly buildInventory: (repoRoot: string) => WorktreeInventory;
|
|
26
|
+
readonly probeDirty: (path: string) => boolean | "unknown";
|
|
27
|
+
readonly measureSize: (path: string) => number;
|
|
28
|
+
}
|
|
29
|
+
/** Classify a single on-disk candidate. Pure aside from the injected deps. */
|
|
30
|
+
export declare function classifyOrphanCandidate(candidate: OrphanScanCandidate, deps: OrphanClassifyDeps): OrphanScanRow;
|
|
31
|
+
/** Namespace-keyed lookup of a known-live `repoRoot`, first-registered-wins. */
|
|
32
|
+
export declare function buildRegistryRepoRootMap(entries: readonly {
|
|
33
|
+
readonly repoNamespace: string;
|
|
34
|
+
readonly repoRoot: string;
|
|
35
|
+
}[]): ReadonlyMap<string, string>;
|
|
36
|
+
/**
|
|
37
|
+
* Fallback primary-repo resolver for candidates the registry never recorded:
|
|
38
|
+
* a linked worktree's `.git` is a file containing `gitdir: <repoRoot>/.git/
|
|
39
|
+
* worktrees/<name>`. Returns `undefined` when unreadable or unparseable
|
|
40
|
+
* (e.g. `.git` is a directory, or the pointer target was already reaped).
|
|
41
|
+
*/
|
|
42
|
+
export declare function resolveRepoRootFromGitPointer(path: string, readGitPointer: (gitPath: string) => string | undefined): string | undefined;
|
|
43
|
+
export interface DiscoverOrphanCandidatesOptions {
|
|
44
|
+
readonly repoNamespaceFilter?: string;
|
|
45
|
+
readonly dirExists?: (path: string) => boolean;
|
|
46
|
+
}
|
|
47
|
+
/** Walk every known managed-worktree root and collect worktree-shaped leaf dirs. */
|
|
48
|
+
export declare function discoverOrphanScanCandidates(roots: readonly string[], options?: DiscoverOrphanCandidatesOptions): OrphanScanCandidate[];
|
|
49
|
+
/** Bounded recursive directory size, symlinks excluded, unreadable entries skipped. */
|
|
50
|
+
export declare function measureDirectorySizeBytes(path: string): number;
|