@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
|
@@ -31,6 +31,23 @@ export interface OpencodeRotateDeps {
|
|
|
31
31
|
readonly materializeGlobalOpencodeAuth?: (key: string) => void;
|
|
32
32
|
readonly recordCooldown?: (canonicalId: string) => Promise<unknown>;
|
|
33
33
|
}
|
|
34
|
+
/**
|
|
35
|
+
* Cooldown to record when an account is known to be capped but the real reset
|
|
36
|
+
* window is unknown: it assumes the shortest real OpenCode Go window (5 hours).
|
|
37
|
+
*
|
|
38
|
+
* A manual `--wp-rotate` has no provider response at all, so it always lands
|
|
39
|
+
* here. The launch-time health probe (`#cli/commands/opencode-probe.js`) uses it
|
|
40
|
+
* only as a fallback, because that path *does* have a provider response and
|
|
41
|
+
* normally records the true parsed window instead.
|
|
42
|
+
*
|
|
43
|
+
* Recording no window at all would leave the record permanently suppressed —
|
|
44
|
+
* `suppressionExpired` only clears a record that carries `retryAfterMs` — so
|
|
45
|
+
* every rotated account would be cooled forever and selection would degrade to
|
|
46
|
+
* "first account" again. A too-short guess is self-correcting: the account is
|
|
47
|
+
* retried, and if it is still capped the gate (or the next probe/rotation)
|
|
48
|
+
* simply cools it again.
|
|
49
|
+
*/
|
|
50
|
+
export declare const OPENCODE_UNKNOWN_LIMIT_COOLDOWN_MS: number;
|
|
34
51
|
/**
|
|
35
52
|
* Cool the currently-materialized opencode-go account and materialize the next
|
|
36
53
|
* usable one into the global `auth.json`.
|
|
@@ -45,5 +62,11 @@ export interface OpencodeRotateDeps {
|
|
|
45
62
|
* Never throws: the caller launches the TUI regardless of the outcome.
|
|
46
63
|
*/
|
|
47
64
|
export declare function rotateActiveOpencodeAccount(cwd: string, secretProfile: string, deps?: OpencodeRotateDeps): Promise<OpencodeRotateOutcome>;
|
|
65
|
+
/**
|
|
66
|
+
* Render a cooldown remainder as a short ` (resets in ~Xd)` suffix, or the empty
|
|
67
|
+
* string when the window is unknown. Shared with the launch-time health probe so
|
|
68
|
+
* rotation and probe output cannot drift apart.
|
|
69
|
+
*/
|
|
70
|
+
export declare function formatOpencodeResetWait(waitMs: number | undefined): string;
|
|
48
71
|
/** Human-readable lines describing a rotation outcome, in print order. */
|
|
49
72
|
export declare function describeOpencodeRotate(outcome: OpencodeRotateOutcome): readonly string[];
|
|
@@ -5,15 +5,22 @@ import { resolveOpencodeAccountsGracefully } from "#runtime/opencode-account-mat
|
|
|
5
5
|
/** The `--wp-rotate` flag, namespaced like `--wp-permission-mode`. */
|
|
6
6
|
export const OPENCODE_ROTATE_FLAG = "--wp-rotate";
|
|
7
7
|
/**
|
|
8
|
-
*
|
|
9
|
-
* assumes the shortest real OpenCode Go window (5 hours).
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
8
|
+
* Cooldown to record when an account is known to be capped but the real reset
|
|
9
|
+
* window is unknown: it assumes the shortest real OpenCode Go window (5 hours).
|
|
10
|
+
*
|
|
11
|
+
* A manual `--wp-rotate` has no provider response at all, so it always lands
|
|
12
|
+
* here. The launch-time health probe (`#cli/commands/opencode-probe.js`) uses it
|
|
13
|
+
* only as a fallback, because that path *does* have a provider response and
|
|
14
|
+
* normally records the true parsed window instead.
|
|
15
|
+
*
|
|
16
|
+
* Recording no window at all would leave the record permanently suppressed —
|
|
17
|
+
* `suppressionExpired` only clears a record that carries `retryAfterMs` — so
|
|
18
|
+
* every rotated account would be cooled forever and selection would degrade to
|
|
19
|
+
* "first account" again. A too-short guess is self-correcting: the account is
|
|
20
|
+
* retried, and if it is still capped the gate (or the next probe/rotation)
|
|
21
|
+
* simply cools it again.
|
|
15
22
|
*/
|
|
16
|
-
const
|
|
23
|
+
export const OPENCODE_UNKNOWN_LIMIT_COOLDOWN_MS = 5 * 60 * 60 * 1_000;
|
|
17
24
|
function coolActiveAccount(canonicalId, deps) {
|
|
18
25
|
if (deps.recordCooldown)
|
|
19
26
|
return deps.recordCooldown(canonicalId);
|
|
@@ -23,7 +30,7 @@ function coolActiveAccount(canonicalId, deps) {
|
|
|
23
30
|
failureCode: "opencode-go-usage-limit",
|
|
24
31
|
terminalClassification: "provider-limit-reached",
|
|
25
32
|
limitWindow: "unknown",
|
|
26
|
-
retryAfterMs:
|
|
33
|
+
retryAfterMs: OPENCODE_UNKNOWN_LIMIT_COOLDOWN_MS,
|
|
27
34
|
});
|
|
28
35
|
}
|
|
29
36
|
/**
|
|
@@ -97,7 +104,12 @@ async function rotateOrThrow(cwd, secretProfile, deps) {
|
|
|
97
104
|
...(next.waitMs === undefined ? {} : { waitMs: next.waitMs }),
|
|
98
105
|
};
|
|
99
106
|
}
|
|
100
|
-
|
|
107
|
+
/**
|
|
108
|
+
* Render a cooldown remainder as a short ` (resets in ~Xd)` suffix, or the empty
|
|
109
|
+
* string when the window is unknown. Shared with the launch-time health probe so
|
|
110
|
+
* rotation and probe output cannot drift apart.
|
|
111
|
+
*/
|
|
112
|
+
export function formatOpencodeResetWait(waitMs) {
|
|
101
113
|
if (waitMs === undefined)
|
|
102
114
|
return "";
|
|
103
115
|
const hours = waitMs / 3_600_000;
|
|
@@ -127,7 +139,7 @@ export function describeOpencodeRotate(outcome) {
|
|
|
127
139
|
`wp opencode: now active -> opencode-go:${outcome.activeAccountId}`,
|
|
128
140
|
];
|
|
129
141
|
if (outcome.activeIsCooled) {
|
|
130
|
-
lines.push(`wp opencode: every account is within its cooldown; using the soonest-expiring one${
|
|
142
|
+
lines.push(`wp opencode: every account is within its cooldown; using the soonest-expiring one${formatOpencodeResetWait(outcome.waitMs)}. Run again after it resets, or add another OPENCODE_GO_KEY_<ID>.`);
|
|
131
143
|
}
|
|
132
144
|
return lines;
|
|
133
145
|
}
|
|
@@ -145,11 +145,15 @@ function renderSummaryLines(summary, transformed, entry, passed) {
|
|
|
145
145
|
else if (!passed && transformed.rawOutput?.trim()) {
|
|
146
146
|
lines.push(transformed.rawOutput.trimEnd());
|
|
147
147
|
}
|
|
148
|
-
|
|
148
|
+
// Any elided bytes must remain retrievable: fail, clip, OR compaction that
|
|
149
|
+
// recorded an elision without setting truncated. Hide Full log / retrieve
|
|
150
|
+
// hints only when the summary still contains the full content.
|
|
151
|
+
const hasElisions = (transformed.elisions?.length ?? 0) > 0;
|
|
152
|
+
if (!passed || transformed.truncated || hasElisions) {
|
|
149
153
|
lines.push(`Full log: wp logs ${entry.command}`);
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
154
|
+
if (hasElisions) {
|
|
155
|
+
lines.push(...transformed.elisions.map((elision) => `Retrieve elision: ${elision.retrieveTool} id=${elision.id}`));
|
|
156
|
+
}
|
|
153
157
|
}
|
|
154
158
|
if (transformed.warnings && transformed.warnings.length > 0) {
|
|
155
159
|
lines.push(...transformed.warnings.map((warning) => `Warning: ${warning}`));
|
|
@@ -1853,9 +1853,22 @@ function rejectedReviewSubjects(input) {
|
|
|
1853
1853
|
!isFormalGateRejection(event)) {
|
|
1854
1854
|
continue;
|
|
1855
1855
|
}
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1856
|
+
// A recorded rejection's `reviewedCommit` can be orphaned by a later rebase.
|
|
1857
|
+
// Rebuilding its subject then throws, and this loop had no guard, so the
|
|
1858
|
+
// failure propagated to the caller and took down the whole rejection
|
|
1859
|
+
// lineage. Skip only the unrebuildable event: the remaining rejections still
|
|
1860
|
+
// count toward the scope-exhaustion circuit breaker, which is the
|
|
1861
|
+
// fail-closed direction.
|
|
1862
|
+
let canonical;
|
|
1863
|
+
try {
|
|
1864
|
+
canonical =
|
|
1865
|
+
input.purpose === "plan"
|
|
1866
|
+
? createPlanSubjectAtRef(input.projectRoot, event.reviewedCommit, input.stableSlug)
|
|
1867
|
+
: createDeliverySubjectAtRef(input.projectRoot, event.reviewedCommit, input.stableSlug);
|
|
1868
|
+
}
|
|
1869
|
+
catch {
|
|
1870
|
+
continue;
|
|
1871
|
+
}
|
|
1859
1872
|
if (!bySubject.has(canonical.digest)) {
|
|
1860
1873
|
bySubject.set(canonical.digest, {
|
|
1861
1874
|
artifactPath: event.artifactPath,
|
|
@@ -2356,14 +2369,21 @@ export async function runReviewGate(projectRoot, slug, input, dependencies = { i
|
|
|
2356
2369
|
}
|
|
2357
2370
|
break;
|
|
2358
2371
|
}
|
|
2372
|
+
// OpenCode family-wide stop: `isProviderWideOpenCodeGateResult`
|
|
2373
|
+
// is exactly `reviewer === "opencode"`, so the final branch below
|
|
2374
|
+
// already covers provider-limit-reached for that case. Keep the
|
|
2375
|
+
// provider-limit break only for non-opencode providers here.
|
|
2359
2376
|
if (providerResult.terminalClassification === "provider-limit-reached" &&
|
|
2360
|
-
|
|
2377
|
+
provider !== "opencode") {
|
|
2361
2378
|
break;
|
|
2362
2379
|
}
|
|
2363
2380
|
if ((provider === "claude" || provider === "codex") &&
|
|
2364
2381
|
providerResult.status === "complete")
|
|
2365
2382
|
break;
|
|
2366
|
-
|
|
2383
|
+
// Also stops the opencode family on any result from the
|
|
2384
|
+
// family-agnostic "opencode" reviewer (complete, limit, or
|
|
2385
|
+
// other failure) once per-account failover has already run.
|
|
2386
|
+
if (provider === "opencode" && isProviderWideOpenCodeGateResult(providerResult))
|
|
2367
2387
|
break;
|
|
2368
2388
|
}
|
|
2369
2389
|
}
|
|
@@ -2779,6 +2799,8 @@ export function registerReviewCommand(cli, deps = {}) {
|
|
|
2779
2799
|
const slug = args[0];
|
|
2780
2800
|
if (!slug)
|
|
2781
2801
|
throw new Error("Usage: wp review gate <blueprint-slug> [--purpose plan|delivery] [--target <git-ref>] [--provider claude|codex|opencode|grok] [--model <model-id>] [--effort medium|high] [--resume <gate-id>] [--json]");
|
|
2802
|
+
if (options.targetId !== undefined || options.targetKind !== undefined)
|
|
2803
|
+
throw new Error("wp review gate does not accept --target-id/--target-kind: the blueprint slug is positional (wp review gate <blueprint-slug>). --target-id/--target-kind apply only to `wp review log`.");
|
|
2782
2804
|
const interrupt = createReviewInterruptScope();
|
|
2783
2805
|
try {
|
|
2784
2806
|
const result = await runReviewGate(projectRoot, slug, options, {
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { CAC } from "cac";
|
|
2
|
+
import type { AffectedResolutionDeps } from "#git/affected";
|
|
3
|
+
/**
|
|
4
|
+
* `wp rust-check` — the Rust counterpart of `wp typecheck`/`wp test`: proves
|
|
5
|
+
* an affected tracked Rust crate still compiles and passes its tests.
|
|
6
|
+
*
|
|
7
|
+
* Exists because `wp ci-preflight`'s other stages only resolve TS/JS
|
|
8
|
+
* affectedness, so a Rust-only branch previously reported every stage as
|
|
9
|
+
* "no affected files" and exited green without ever invoking `cargo` — a
|
|
10
|
+
* silent false pass (see `catalog/agent/rules/ci-cost-local-first.md`).
|
|
11
|
+
* This command always prints an explicit status line, whether or not it
|
|
12
|
+
* found Rust crates to check, so "green" is never ambiguous.
|
|
13
|
+
*/
|
|
14
|
+
export declare const RUST_CHECK_COMMAND_HELP: string;
|
|
15
|
+
interface RustCheckCommandDeps {
|
|
16
|
+
readonly getGitTopLevel?: AffectedResolutionDeps["getGitTopLevel"];
|
|
17
|
+
readonly getStagedFiles?: AffectedResolutionDeps["getStagedFiles"];
|
|
18
|
+
readonly getBranchChangedFiles?: AffectedResolutionDeps["getBranchChangedFiles"];
|
|
19
|
+
readonly runCargo?: (args: readonly string[], cwd: string) => {
|
|
20
|
+
readonly exitCode: number;
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
export declare function registerRustCheckCommand(cli: CAC, deps?: RustCheckCommandDeps): void;
|
|
24
|
+
export {};
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { addAffectedOptions, resolveAffectedTargets } from "#git/affected";
|
|
3
|
+
import { resolveAffectedRustManifests, trackedCrateForManifest } from "#rust/affected-manifests";
|
|
4
|
+
/**
|
|
5
|
+
* `wp rust-check` — the Rust counterpart of `wp typecheck`/`wp test`: proves
|
|
6
|
+
* an affected tracked Rust crate still compiles and passes its tests.
|
|
7
|
+
*
|
|
8
|
+
* Exists because `wp ci-preflight`'s other stages only resolve TS/JS
|
|
9
|
+
* affectedness, so a Rust-only branch previously reported every stage as
|
|
10
|
+
* "no affected files" and exited green without ever invoking `cargo` — a
|
|
11
|
+
* silent false pass (see `catalog/agent/rules/ci-cost-local-first.md`).
|
|
12
|
+
* This command always prints an explicit status line, whether or not it
|
|
13
|
+
* found Rust crates to check, so "green" is never ambiguous.
|
|
14
|
+
*/
|
|
15
|
+
export const RUST_CHECK_COMMAND_HELP = [
|
|
16
|
+
"Prove affected tracked Rust crates compile and pass tests (cargo check + cargo test).",
|
|
17
|
+
"",
|
|
18
|
+
"Examples:",
|
|
19
|
+
" wp rust-check --affected # staged changed files → owning crate(s)",
|
|
20
|
+
" wp rust-check --affected --branch # changed vs origin/${GITHUB_BASE_REF:-main}",
|
|
21
|
+
"",
|
|
22
|
+
"Tracked crates: native/session-memory-engine, native/dash-tui,",
|
|
23
|
+
"apps/desktop/src-tauri. spikes/** crates are intentionally excluded — no CI",
|
|
24
|
+
"workflow builds or tests them today.",
|
|
25
|
+
].join("\n");
|
|
26
|
+
export function registerRustCheckCommand(cli, deps = {}) {
|
|
27
|
+
addAffectedOptions(cli.command("rust-check", RUST_CHECK_COMMAND_HELP)).action(async (flags) => {
|
|
28
|
+
const affected = Boolean(flags.affected);
|
|
29
|
+
const branch = Boolean(flags.branch);
|
|
30
|
+
const cwd = process.cwd();
|
|
31
|
+
if (!affected && !branch) {
|
|
32
|
+
console.error("wp rust-check requires --affected (optionally with --branch).");
|
|
33
|
+
return 1;
|
|
34
|
+
}
|
|
35
|
+
const resolution = resolveAffectedTargets({
|
|
36
|
+
commandName: "rust-check",
|
|
37
|
+
cwd,
|
|
38
|
+
affected,
|
|
39
|
+
branch,
|
|
40
|
+
policy: "fail-closed",
|
|
41
|
+
mapChangedFiles: resolveAffectedRustManifests,
|
|
42
|
+
emptyMessage: rustCheckEmptyMessage,
|
|
43
|
+
degradedFallbackMessage: rustCheckDegradedMessage,
|
|
44
|
+
degradedFailClosedMessage: rustCheckDegradedMessage,
|
|
45
|
+
}, deps);
|
|
46
|
+
if (resolution.kind === "invalid") {
|
|
47
|
+
console.error(resolution.message);
|
|
48
|
+
return 1;
|
|
49
|
+
}
|
|
50
|
+
if (resolution.kind === "degraded-fail-closed" || resolution.kind === "degraded-fallback") {
|
|
51
|
+
console.error(resolution.message);
|
|
52
|
+
return 1;
|
|
53
|
+
}
|
|
54
|
+
if (resolution.kind === "empty") {
|
|
55
|
+
console.log(resolution.message);
|
|
56
|
+
return 0;
|
|
57
|
+
}
|
|
58
|
+
if (resolution.kind === "disabled") {
|
|
59
|
+
// Unreachable: the --affected/--branch guard above always sets
|
|
60
|
+
// `affected: true` before calling resolveAffectedTargets. Handled
|
|
61
|
+
// for exhaustiveness only.
|
|
62
|
+
console.error("wp rust-check requires --affected (optionally with --branch).");
|
|
63
|
+
return 1;
|
|
64
|
+
}
|
|
65
|
+
const manifests = resolution.targets;
|
|
66
|
+
console.log(`wp rust-check: ${manifests.length} tracked Rust crate(s) affected: ${manifests.join(", ")}`);
|
|
67
|
+
const runCargo = deps.runCargo ?? defaultRunCargo;
|
|
68
|
+
for (const manifest of manifests) {
|
|
69
|
+
const crate = trackedCrateForManifest(manifest);
|
|
70
|
+
if (!crate) {
|
|
71
|
+
console.error(`wp rust-check: no tracked crate metadata for ${manifest}.`);
|
|
72
|
+
return 1;
|
|
73
|
+
}
|
|
74
|
+
console.log(`wp rust-check: cargo check --manifest-path ${crate.manifest}`);
|
|
75
|
+
const checkResult = runCargo(["check", "--manifest-path", crate.manifest], resolution.cwd);
|
|
76
|
+
if (checkResult.exitCode !== 0) {
|
|
77
|
+
console.error(`wp rust-check: cargo check failed for ${crate.manifest}.`);
|
|
78
|
+
return checkResult.exitCode;
|
|
79
|
+
}
|
|
80
|
+
console.log(`wp rust-check: cargo ${crate.testArgs.join(" ")}`);
|
|
81
|
+
const testResult = runCargo(crate.testArgs, resolution.cwd);
|
|
82
|
+
if (testResult.exitCode !== 0) {
|
|
83
|
+
console.error(`wp rust-check: cargo test failed for ${crate.manifest}.`);
|
|
84
|
+
return testResult.exitCode;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
console.log(`wp rust-check: ${manifests.length} crate(s) passed: ${manifests.join(", ")}`);
|
|
88
|
+
return 0;
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
function rustCheckEmptyMessage(scope) {
|
|
92
|
+
const suffix = scope === "branch" ? "changed vs base ref" : "staged";
|
|
93
|
+
return (`wp rust-check: 0 tracked Rust crates affected (no ${suffix} files under ` +
|
|
94
|
+
"native/session-memory-engine, native/dash-tui, or apps/desktop/src-tauri; " +
|
|
95
|
+
"spikes/** Rust changes, if any, are intentionally not gated) — skipping cargo.");
|
|
96
|
+
}
|
|
97
|
+
function rustCheckDegradedMessage(reason) {
|
|
98
|
+
return `Unable to determine affected Rust files (${reason}); refusing to skip cargo verification.`;
|
|
99
|
+
}
|
|
100
|
+
function defaultRunCargo(args, cwd) {
|
|
101
|
+
const result = spawnSync("cargo", args, { cwd, stdio: "inherit" });
|
|
102
|
+
if (result.error) {
|
|
103
|
+
const isMissingBinary = result.error.code === "ENOENT";
|
|
104
|
+
console.error(isMissingBinary
|
|
105
|
+
? "wp rust-check: cargo not found on PATH; install the Rust toolchain (https://rustup.rs) to verify Rust changes."
|
|
106
|
+
: `wp rust-check: ${result.error.message}`);
|
|
107
|
+
return { exitCode: 1 };
|
|
108
|
+
}
|
|
109
|
+
return { exitCode: typeof result.status === "number" ? result.status : 1 };
|
|
110
|
+
}
|
|
@@ -6,6 +6,7 @@ export interface TypecheckOptions {
|
|
|
6
6
|
readonly cwd?: string;
|
|
7
7
|
readonly files?: readonly string[];
|
|
8
8
|
readonly packages?: readonly string[];
|
|
9
|
+
readonly tests?: boolean;
|
|
9
10
|
}
|
|
10
11
|
interface TypecheckCommandDeps {
|
|
11
12
|
readonly getGitTopLevel?: AffectedResolutionDeps["getGitTopLevel"];
|
|
@@ -23,6 +23,8 @@ export const TYPECHECK_COMMAND_HELP = [
|
|
|
23
23
|
" wp typecheck --affected # staged changed source → reverse-importer closure",
|
|
24
24
|
" wp typecheck --affected --branch # changed vs origin/${GITHUB_BASE_REF:-main}",
|
|
25
25
|
" wp typecheck --pretty",
|
|
26
|
+
" wp typecheck --tests # test-surface lane (tsconfig.test.json); required in",
|
|
27
|
+
" # ci-preflight and CI (not the default untargeted gate)",
|
|
26
28
|
].join("\n");
|
|
27
29
|
export function registerTypecheckCommand(cli, deps = {}) {
|
|
28
30
|
addAffectedOptions(cli
|
|
@@ -31,6 +33,7 @@ export function registerTypecheckCommand(cli, deps = {}) {
|
|
|
31
33
|
.option("--package <name>", "Run the owning-scope typecheck for an exact package.json name (repeatable)"))
|
|
32
34
|
.option("--pretty", "Keep TypeScript pretty output enabled")
|
|
33
35
|
.option("--full", "Print the full raw output instead of the default summary-first view")
|
|
36
|
+
.option("--tests", "Run the test-surface lane (tsconfig.test.json); required by ci-preflight and CI")
|
|
34
37
|
.action(async (targetsOrFlags, maybeFlags) => {
|
|
35
38
|
const positionalTargets = Array.isArray(targetsOrFlags)
|
|
36
39
|
? targetsOrFlags.filter((target) => typeof target === "string")
|
|
@@ -45,9 +48,29 @@ export function registerTypecheckCommand(cli, deps = {}) {
|
|
|
45
48
|
const packages = toArray(flags.package);
|
|
46
49
|
const affected = Boolean(flags.affected);
|
|
47
50
|
const branch = Boolean(flags.branch);
|
|
51
|
+
const tests = Boolean(flags.tests);
|
|
48
52
|
const cwd = process.cwd();
|
|
49
53
|
let files = explicitFiles;
|
|
50
54
|
let executionCwd = cwd;
|
|
55
|
+
if (tests && (affected || branch || explicitFiles.length > 0 || packages.length > 0)) {
|
|
56
|
+
console.error("--tests cannot be combined with --affected, --branch, --file, or --package.");
|
|
57
|
+
return 1;
|
|
58
|
+
}
|
|
59
|
+
if (tests) {
|
|
60
|
+
const result = await runTypecheckCommand({
|
|
61
|
+
pretty: Boolean(flags.pretty),
|
|
62
|
+
tests: true,
|
|
63
|
+
cwd,
|
|
64
|
+
});
|
|
65
|
+
emitCliCommandOutput({
|
|
66
|
+
entry: result.entry,
|
|
67
|
+
summary: result.entry.summary ?? "",
|
|
68
|
+
passed: result.exitCode === 0,
|
|
69
|
+
full: Boolean(flags.full),
|
|
70
|
+
toolName: "wp_typecheck",
|
|
71
|
+
});
|
|
72
|
+
return result.exitCode;
|
|
73
|
+
}
|
|
51
74
|
if (affected || branch) {
|
|
52
75
|
const resolution = resolveAffectedTargets({
|
|
53
76
|
commandName: "typecheck",
|
|
@@ -131,9 +154,14 @@ export async function runTypecheckCommand(options = {}) {
|
|
|
131
154
|
options.packages.length > 0) {
|
|
132
155
|
throw new Error("Cannot use both --file and --package for typecheck targeting.");
|
|
133
156
|
}
|
|
157
|
+
if (options.tests && ((options.files?.length ?? 0) > 0 || (options.packages?.length ?? 0) > 0)) {
|
|
158
|
+
throw new Error("Cannot use --tests together with --file or --package.");
|
|
159
|
+
}
|
|
134
160
|
const cwd = options.cwd ?? process.cwd();
|
|
135
161
|
repairCurrentManagedWorktreeDependencies(cwd);
|
|
136
|
-
const targeted = (options.files?.length ?? 0) > 0 ||
|
|
162
|
+
const targeted = (options.files?.length ?? 0) > 0 ||
|
|
163
|
+
(options.packages?.length ?? 0) > 0 ||
|
|
164
|
+
Boolean(options.tests);
|
|
137
165
|
const repoRoot = resolveProjectRoot({ cwd });
|
|
138
166
|
const plan = targeted
|
|
139
167
|
? planTypecheckExecution({
|
|
@@ -141,6 +169,7 @@ export async function runTypecheckCommand(options = {}) {
|
|
|
141
169
|
defaultScopeRoot: cwd,
|
|
142
170
|
files: options.files,
|
|
143
171
|
packages: options.packages,
|
|
172
|
+
tests: options.tests,
|
|
144
173
|
pretty: options.pretty,
|
|
145
174
|
})
|
|
146
175
|
: {
|
|
@@ -14,6 +14,12 @@ export interface WorktreeCommandOptions {
|
|
|
14
14
|
deps?: WorktreeDependencyHydration;
|
|
15
15
|
cwd?: string;
|
|
16
16
|
repo?: string;
|
|
17
|
+
/** `prune` only: filesystem-level orphan sweep instead of registry-only prune. */
|
|
18
|
+
orphans?: boolean;
|
|
19
|
+
/** `prune --orphans` only: scope the sweep to one repo namespace. */
|
|
20
|
+
repoId?: string;
|
|
21
|
+
/** `prune --orphans` only: alias for `--force` (execute the removal plan). */
|
|
22
|
+
yes?: boolean;
|
|
17
23
|
}
|
|
18
24
|
export interface WorktreeEntry {
|
|
19
25
|
path: string;
|
|
@@ -49,6 +55,12 @@ export interface MergeCleanupDecisionInput {
|
|
|
49
55
|
readonly repoDirty: boolean;
|
|
50
56
|
readonly targetDirty: boolean;
|
|
51
57
|
readonly stashPrimary?: boolean;
|
|
58
|
+
/**
|
|
59
|
+
* When target is dirty, optional porcelain -z + slug enrich the refusal message
|
|
60
|
+
* (scaffold-only vs other dirt). Omitted messages stay generic for unit tests.
|
|
61
|
+
*/
|
|
62
|
+
readonly targetPorcelainZ?: string;
|
|
63
|
+
readonly targetBlueprintSlug?: string;
|
|
52
64
|
}
|
|
53
65
|
export interface NewWorktreeTargetInput {
|
|
54
66
|
branch?: string;
|
|
@@ -91,4 +103,6 @@ export interface CreateManagedWorktreeResult {
|
|
|
91
103
|
export declare function formatWorktreeList(entries: WorktreeEntry[], currentWorktreePath: string): string[];
|
|
92
104
|
export declare function formatManagedWorktreeList(entries: readonly ManagedWorktreeEntry[]): string[];
|
|
93
105
|
export declare function createManagedWorktree(opts?: CreateManagedWorktreeOptions): Promise<CreateManagedWorktreeResult>;
|
|
106
|
+
/** `.../blueprints/<slug>/owner` → slug */
|
|
107
|
+
export declare function inferBlueprintSlugFromOwnerPath(ownerPath: string): string | undefined;
|
|
94
108
|
export declare function executeWorktreeSubcommand(subcommand: string, args: string[], opts: WorktreeCommandOptions): Promise<void>;
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* `wp worktree` subcommand dispatch.
|
|
3
3
|
*/
|
|
4
4
|
import { execFileSync, spawnSync } from "node:child_process";
|
|
5
|
-
import { existsSync, mkdirSync } from "node:fs";
|
|
5
|
+
import { existsSync, mkdirSync, readFileSync, rmSync } from "node:fs";
|
|
6
6
|
import { basename, dirname, join, relative, resolve } from "node:path";
|
|
7
7
|
import { findGitRoot } from "#cli/commands/init/detect-consumer.js";
|
|
8
8
|
import { removeCodexProjectTrust, removeCodexProjectTrusts, } from "#worktrees/codex-project-trust.js";
|
|
@@ -15,8 +15,10 @@ import { runWorktreeSetup, runWorktreeTeardown } from "#worktrees/lifecycle-scri
|
|
|
15
15
|
import { hydrateWorktreeDependencies, reconcileWorktreeDependencies, } from "#worktrees/dependencies.js";
|
|
16
16
|
import { projectWorktreeAgentSurfaces } from "#worktrees/project-agent-surfaces.js";
|
|
17
17
|
import { assertNotLocalMainBranch } from "#worktrees/main-ownership.js";
|
|
18
|
+
import { formatTargetDirtyRefusal } from "#worktrees/owner-dirt.js";
|
|
18
19
|
import { buildWorktreeInventory, canonicalizeWorktreePath, classifyMembership, isManagedWorktreePath, resolveRepoIdentity, } from "#worktrees/identity.js";
|
|
19
20
|
import { findRegistryCandidatesByPath, planStaleWorktreeRegistryPrune, readWorktreeRegistry, removeWorktreeRegistryEntries, repoScopedPathPredicate, upsertWorktreeRegistryEntry, } from "#worktrees/registry.js";
|
|
21
|
+
import { buildRegistryRepoRootMap, classifyOrphanCandidate, discoverOrphanScanCandidates, resolveRepoRootFromGitPointer, } from "#worktrees/orphan-scan.js";
|
|
20
22
|
function parseDepsOption(value) {
|
|
21
23
|
if (value === undefined || value === null || value === "")
|
|
22
24
|
return undefined;
|
|
@@ -181,6 +183,13 @@ export function decideMergeCleanup(input) {
|
|
|
181
183
|
throw new Error(`wp worktree merge-cleanup refused: ${input.targetPath} is locked`);
|
|
182
184
|
}
|
|
183
185
|
if (input.targetDirty) {
|
|
186
|
+
if (input.targetPorcelainZ !== undefined) {
|
|
187
|
+
throw new Error(formatTargetDirtyRefusal({
|
|
188
|
+
targetPath: input.targetPath,
|
|
189
|
+
porcelainZ: input.targetPorcelainZ,
|
|
190
|
+
slug: input.targetBlueprintSlug,
|
|
191
|
+
}));
|
|
192
|
+
}
|
|
184
193
|
throw new Error(`wp worktree merge-cleanup refused: ${input.targetPath} has uncommitted changes`);
|
|
185
194
|
}
|
|
186
195
|
if (input.repoDirty) {
|
|
@@ -560,6 +569,12 @@ function isDirty(path) {
|
|
|
560
569
|
return true;
|
|
561
570
|
return String(status.stdout ?? "").trim().length > 0;
|
|
562
571
|
}
|
|
572
|
+
/** `.../blueprints/<slug>/owner` → slug */
|
|
573
|
+
export function inferBlueprintSlugFromOwnerPath(ownerPath) {
|
|
574
|
+
const normalized = ownerPath.replace(/\\/gu, "/");
|
|
575
|
+
const match = /\/blueprints\/([^/]+)\/owner\/?$/u.exec(normalized);
|
|
576
|
+
return match?.[1];
|
|
577
|
+
}
|
|
563
578
|
function currentBranch(repoRoot) {
|
|
564
579
|
const result = spawnSync("git", ["branch", "--show-current"], {
|
|
565
580
|
cwd: repoRoot,
|
|
@@ -567,6 +582,113 @@ function currentBranch(repoRoot) {
|
|
|
567
582
|
});
|
|
568
583
|
return result.status === 0 ? String(result.stdout ?? "").trim() : "";
|
|
569
584
|
}
|
|
585
|
+
function formatOrphanScanBytes(bytes) {
|
|
586
|
+
if (bytes === "unknown")
|
|
587
|
+
return "-";
|
|
588
|
+
if (bytes < 1024)
|
|
589
|
+
return `${bytes}B`;
|
|
590
|
+
const units = ["K", "M", "G", "T"];
|
|
591
|
+
let value = bytes / 1024;
|
|
592
|
+
let unitIndex = 0;
|
|
593
|
+
while (value >= 1024 && unitIndex < units.length - 1) {
|
|
594
|
+
value /= 1024;
|
|
595
|
+
unitIndex += 1;
|
|
596
|
+
}
|
|
597
|
+
return `${value.toFixed(1)}${units[unitIndex]}`;
|
|
598
|
+
}
|
|
599
|
+
function summarizeOrphanScanRows(rows) {
|
|
600
|
+
const counts = new Map();
|
|
601
|
+
for (const row of rows)
|
|
602
|
+
counts.set(row.classification, (counts.get(row.classification) ?? 0) + 1);
|
|
603
|
+
const removable = rows.filter((row) => row.removable).length;
|
|
604
|
+
const parts = [...counts.entries()].map(([classification, count]) => `${classification}=${count}`);
|
|
605
|
+
return `${parts.join(", ")}, removable=${removable}`;
|
|
606
|
+
}
|
|
607
|
+
function printOrphanScanTable(rows) {
|
|
608
|
+
if (rows.length === 0) {
|
|
609
|
+
console.log("No candidate directories found under managed worktree roots.");
|
|
610
|
+
return;
|
|
611
|
+
}
|
|
612
|
+
console.log(`${"PATH".padEnd(72)} ${"CLASS".padEnd(13)} ${"DIRTY".padEnd(8)} ${"SIZE".padEnd(7)} ${"RM".padEnd(4)} REASON`);
|
|
613
|
+
for (const row of rows) {
|
|
614
|
+
const dirty = row.dirty === "unknown" ? "unknown" : row.dirty ? "yes" : "no";
|
|
615
|
+
console.log(`${row.path.padEnd(72)} ${row.classification.padEnd(13)} ${dirty.padEnd(8)} ${formatOrphanScanBytes(row.sizeBytes).padEnd(7)} ${(row.removable ? "yes" : "no").padEnd(4)} ${row.reason}`);
|
|
616
|
+
}
|
|
617
|
+
console.log("");
|
|
618
|
+
console.log(`Summary: ${summarizeOrphanScanRows(rows)}`);
|
|
619
|
+
}
|
|
620
|
+
function resolveOrphanScanDeps() {
|
|
621
|
+
const registryMap = buildRegistryRepoRootMap(readWorktreeRegistry().entries);
|
|
622
|
+
const readGitPointer = (path) => {
|
|
623
|
+
try {
|
|
624
|
+
return readFileSync(path, "utf8");
|
|
625
|
+
}
|
|
626
|
+
catch {
|
|
627
|
+
return undefined;
|
|
628
|
+
}
|
|
629
|
+
};
|
|
630
|
+
return {
|
|
631
|
+
resolveRepoRoot: (candidate) => registryMap.get(candidate.repoNamespace) ??
|
|
632
|
+
resolveRepoRootFromGitPointer(candidate.path, readGitPointer),
|
|
633
|
+
repoRootExists: existsSync,
|
|
634
|
+
buildInventory: buildWorktreeInventory,
|
|
635
|
+
probeDirty: (path) => {
|
|
636
|
+
const result = spawnSync("git", ["status", "--porcelain"], {
|
|
637
|
+
cwd: path,
|
|
638
|
+
encoding: "utf8",
|
|
639
|
+
timeout: 5000,
|
|
640
|
+
});
|
|
641
|
+
if (result.status !== 0)
|
|
642
|
+
return "unknown";
|
|
643
|
+
return String(result.stdout ?? "").trim().length > 0;
|
|
644
|
+
},
|
|
645
|
+
// Native `du` traverses in kernel/C code (no per-file V8 stat overhead),
|
|
646
|
+
// which is materially faster than the pure-JS walker at the scale a real
|
|
647
|
+
// managed-worktree tree reaches (hundreds of `node_modules` trees). This
|
|
648
|
+
// is only invoked for already-confirmed `orphan` candidates (see
|
|
649
|
+
// `finalizeRow` in orphan-scan.ts), so it never runs against `alive`
|
|
650
|
+
// worktrees. Bounded, non-retrying: a slow/failed probe throws and
|
|
651
|
+
// reports `sizeBytes: "unknown"` rather than blocking the scan.
|
|
652
|
+
measureSize: (path) => {
|
|
653
|
+
const result = spawnSync("du", ["-sk", path], { encoding: "utf8", timeout: 10000 });
|
|
654
|
+
if (result.status !== 0) {
|
|
655
|
+
throw new Error(`du -sk failed or timed out for ${path}`);
|
|
656
|
+
}
|
|
657
|
+
const kilobytes = Number.parseInt(String(result.stdout ?? "")
|
|
658
|
+
.trim()
|
|
659
|
+
.split(/\s+/u)[0] ?? "", 10);
|
|
660
|
+
if (Number.isNaN(kilobytes))
|
|
661
|
+
throw new Error(`du -sk produced unparseable output for ${path}`);
|
|
662
|
+
return kilobytes * 1024;
|
|
663
|
+
},
|
|
664
|
+
};
|
|
665
|
+
}
|
|
666
|
+
/**
|
|
667
|
+
* Filesystem-level orphan sweep: dry-run by DEFAULT, prints a classification
|
|
668
|
+
* table, and only removes directories (via `--force`/`--yes`) that are proven
|
|
669
|
+
* `orphan` (absent from their resolved primary repo's `git worktree list`)
|
|
670
|
+
* AND proven clean (`git status --porcelain` ran successfully with no
|
|
671
|
+
* output). `broken` and `unrecognized` rows are always report-only. See
|
|
672
|
+
* `#worktrees/orphan-scan.js` for the full classification contract.
|
|
673
|
+
*/
|
|
674
|
+
async function handlePruneOrphans(opts) {
|
|
675
|
+
const roots = resolveKnownManagedWorktreeRoots();
|
|
676
|
+
const candidates = discoverOrphanScanCandidates(roots, { repoNamespaceFilter: opts.repoId });
|
|
677
|
+
const deps = resolveOrphanScanDeps();
|
|
678
|
+
const rows = candidates.map((candidate) => classifyOrphanCandidate(candidate, deps));
|
|
679
|
+
printOrphanScanTable(rows);
|
|
680
|
+
if (!(opts.force === true || opts.yes === true)) {
|
|
681
|
+
console.log("");
|
|
682
|
+
console.log("Dry run only. Re-run with --force (or --yes) to remove the directories marked removable=yes.");
|
|
683
|
+
return;
|
|
684
|
+
}
|
|
685
|
+
const removable = rows.filter((row) => row.removable);
|
|
686
|
+
for (const row of removable) {
|
|
687
|
+
rmSync(row.path, { recursive: true, force: true });
|
|
688
|
+
console.log(`Removed ${row.path}`);
|
|
689
|
+
}
|
|
690
|
+
console.log(`Removed ${removable.length} orphan director${removable.length === 1 ? "y" : "ies"}.`);
|
|
691
|
+
}
|
|
570
692
|
async function handleMergeCleanup(nameOrPath, opts) {
|
|
571
693
|
const cwd = opts.cwd ?? process.cwd();
|
|
572
694
|
const callerRoot = resolveWorktreeRepoRoot(cwd);
|
|
@@ -583,6 +705,14 @@ async function handleMergeCleanup(nameOrPath, opts) {
|
|
|
583
705
|
const scopedRegistryEntries = findRegistryCandidatesByPath(resolved)
|
|
584
706
|
.filter((candidate) => classifyMembership(candidate.entry.repoRoot, mergeAuthKey) === "same")
|
|
585
707
|
.map((candidate) => candidate.entry);
|
|
708
|
+
const targetDirty = isDirty(resolved);
|
|
709
|
+
const targetPorcelainZ = targetDirty
|
|
710
|
+
? (() => {
|
|
711
|
+
const status = spawnSync("git", ["status", "--porcelain=v1", "--untracked-files=all", "--ignored=no", "-z"], { cwd: resolved, encoding: "utf8" });
|
|
712
|
+
return status.status === 0 ? String(status.stdout ?? "") : "?? (git-status-failed)\0";
|
|
713
|
+
})()
|
|
714
|
+
: undefined;
|
|
715
|
+
const targetBlueprintSlug = inferBlueprintSlugFromOwnerPath(resolved);
|
|
586
716
|
const plan = decideMergeCleanup({
|
|
587
717
|
repoRoot,
|
|
588
718
|
targetPath: resolved,
|
|
@@ -591,8 +721,10 @@ async function handleMergeCleanup(nameOrPath, opts) {
|
|
|
591
721
|
registryEntries: scopedRegistryEntries,
|
|
592
722
|
currentBranch: currentBranch(repoRoot),
|
|
593
723
|
repoDirty: isDirty(repoRoot),
|
|
594
|
-
targetDirty
|
|
724
|
+
targetDirty,
|
|
595
725
|
stashPrimary: opts.stashPrimary === true,
|
|
726
|
+
...(targetPorcelainZ !== undefined ? { targetPorcelainZ } : {}),
|
|
727
|
+
...(targetBlueprintSlug !== undefined ? { targetBlueprintSlug } : {}),
|
|
596
728
|
});
|
|
597
729
|
if (opts.dryRun) {
|
|
598
730
|
console.log("[dry-run] Would run merge cleanup:");
|
|
@@ -670,6 +802,10 @@ export async function executeWorktreeSubcommand(subcommand, args, opts) {
|
|
|
670
802
|
handleRefresh(opts);
|
|
671
803
|
return;
|
|
672
804
|
case "prune":
|
|
805
|
+
if (opts.orphans) {
|
|
806
|
+
await handlePruneOrphans(opts);
|
|
807
|
+
return;
|
|
808
|
+
}
|
|
673
809
|
await handlePrune(opts);
|
|
674
810
|
return;
|
|
675
811
|
case "migrate":
|