@webpresso/agent-kit 3.3.3 → 3.3.4
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.
Potentially problematic release.
This version of @webpresso/agent-kit might be problematic. Click here for more details.
- package/THIRD-PARTY-NOTICES.md +12 -10
- package/catalog/agent/rules/changeset-release.md +15 -4
- package/catalog/agent/rules/managed-tool-binaries.md +67 -0
- package/catalog/agent/rules/parallel-orchestration-quota.md +99 -1
- package/catalog/agent/rules/pre-implementation.md +36 -20
- package/catalog/agent/rules/rtk-routing.md +22 -16
- package/catalog/agent/skills/autopilot/SKILL.md +4 -3
- package/catalog/agent/skills/ultragoal/SKILL.md +11 -9
- package/dist/esm/audit/hook-surface.js +5 -1
- package/dist/esm/audit/repo-guardrails.d.ts +8 -0
- package/dist/esm/audit/repo-guardrails.js +71 -1
- package/dist/esm/blueprint/core/parser.js +38 -12
- package/dist/esm/blueprint/execution/artifacts.js +8 -4
- package/dist/esm/blueprint/execution/frontmatter-keys.d.ts +17 -0
- package/dist/esm/blueprint/execution/frontmatter-keys.js +24 -0
- package/dist/esm/blueprint/execution/metadata.js +8 -4
- package/dist/esm/blueprint/lifecycle/audit.d.ts +21 -0
- package/dist/esm/blueprint/lifecycle/audit.js +89 -3
- package/dist/esm/blueprint/lifecycle/review-provenance.js +63 -10
- package/dist/esm/blueprint/lifecycle/rollout-anchor.d.ts +22 -0
- package/dist/esm/blueprint/lifecycle/rollout-anchor.js +37 -1
- package/dist/esm/blueprint/markdown/blocked-line.d.ts +49 -0
- package/dist/esm/blueprint/markdown/blocked-line.js +63 -0
- package/dist/esm/blueprint/markdown/helpers.js +10 -4
- package/dist/esm/blueprint/tracked-document/parser.js +3 -3
- package/dist/esm/blueprint/trust/gate-row.d.ts +55 -0
- package/dist/esm/blueprint/trust/gate-row.js +113 -0
- package/dist/esm/blueprint/trust/promotion.d.ts +17 -0
- package/dist/esm/blueprint/trust/promotion.js +19 -19
- package/dist/esm/build/atomic-file-copy.d.ts +32 -0
- package/dist/esm/build/atomic-file-copy.js +44 -0
- package/dist/esm/build/vendor-oxlint-plugins.d.ts +8 -0
- package/dist/esm/build/vendor-oxlint-plugins.js +53 -0
- package/dist/esm/cli/commands/blueprint/mutations.d.ts +15 -0
- package/dist/esm/cli/commands/blueprint/mutations.js +8 -2
- package/dist/esm/cli/commands/compile.d.ts +13 -0
- package/dist/esm/cli/commands/compile.js +182 -62
- package/dist/esm/cli/commands/format.js +17 -1
- package/dist/esm/cli/commands/gain/index.js +10 -3
- package/dist/esm/cli/commands/init/gitignore-patcher.js +6 -0
- package/dist/esm/cli/commands/init/index.js +29 -48
- package/dist/esm/cli/commands/init/mcp-spec.d.ts +48 -13
- package/dist/esm/cli/commands/init/mcp-spec.js +86 -30
- package/dist/esm/cli/commands/init/merge.js +30 -10
- package/dist/esm/cli/commands/init/scaffolders/codex-mcp/index.d.ts +56 -31
- package/dist/esm/cli/commands/init/scaffolders/codex-mcp/index.js +180 -43
- package/dist/esm/cli/commands/init/scaffolders/rtk/index.d.ts +17 -5
- package/dist/esm/cli/commands/init/scaffolders/rtk/index.js +56 -75
- package/dist/esm/cli/commands/init/scaffolders/runtime-check/index.js +7 -6
- package/dist/esm/cli/commands/init/untracked-collision.d.ts +12 -1
- package/dist/esm/cli/commands/init/untracked-collision.js +23 -1
- package/dist/esm/cli/commands/pr-help.js +1 -1
- package/dist/esm/cli/commands/pr.js +18 -4
- package/dist/esm/cli/commands/review.js +60 -6
- package/dist/esm/cli/commands/worktree/router-dispatch.d.ts +24 -2
- package/dist/esm/cli/commands/worktree/router-dispatch.js +80 -48
- package/dist/esm/cli/commands/worktree/router.js +5 -2
- package/dist/esm/cli/direct-provider-launch.js +12 -4
- package/dist/esm/compiler/flatten.d.ts +10 -1
- package/dist/esm/compiler/flatten.js +14 -4
- package/dist/esm/compose/admin-blocks-registry-integrity.d.ts +22 -0
- package/dist/esm/compose/admin-blocks-registry-integrity.js +90 -0
- package/dist/esm/compose/compose.d.ts +8 -0
- package/dist/esm/compose/compose.js +115 -8
- package/dist/esm/compose/ops-report-schema.js +17 -3
- package/dist/esm/compose/registry-resolve.d.ts +17 -0
- package/dist/esm/compose/registry-resolve.js +78 -11
- package/dist/esm/config/oxlint/graphql-conventions.js +2 -5
- package/dist/esm/config/oxlint/import-hygiene.js +21 -8
- package/dist/esm/config/oxlint/oxlint-config.fixture.d.ts +1 -1
- package/dist/esm/config/oxlint/oxlint-config.fixture.js +2 -3
- package/dist/esm/config/oxlint/oxlintrc.d.ts +5 -7
- package/dist/esm/config/oxlint/oxlintrc.js +18 -21
- package/dist/esm/config/oxlint/path-roles.js +53 -0
- package/dist/esm/config/oxlint/query-patterns.js +2 -5
- package/dist/esm/daemon/domains/blueprint-exec-ledger.d.ts +86 -0
- package/dist/esm/daemon/domains/blueprint-exec-ledger.js +134 -0
- package/dist/esm/daemon/domains/blueprint-exec.d.ts +106 -0
- package/dist/esm/daemon/domains/blueprint-exec.js +161 -0
- package/dist/esm/daemon/domains/launch.d.ts +25 -0
- package/dist/esm/daemon/domains/launch.js +30 -1
- package/dist/esm/daemon/handlers/blueprints-handlers.d.ts +12 -1
- package/dist/esm/daemon/handlers/blueprints-handlers.js +148 -6
- package/dist/esm/daemon/handlers/product.d.ts +10 -0
- package/dist/esm/daemon/handlers/product.js +10 -1
- package/dist/esm/daemon/methods.d.ts +21 -1
- package/dist/esm/daemon/methods.js +20 -0
- package/dist/esm/daemon/protocol/contract.d.ts +58 -0
- package/dist/esm/daemon/protocol/contract.js +38 -0
- package/dist/esm/daemon/server.js +7 -0
- package/dist/esm/docs-linter/blueprint-plan.js +2 -2
- package/dist/esm/errors/wp-error.js +21 -2
- package/dist/esm/hooks/doctor.d.ts +11 -0
- package/dist/esm/hooks/doctor.js +114 -45
- package/dist/esm/hooks/pretool-guard/dev-routing.js +1 -1
- package/dist/esm/hooks/pretool-guard/rtk-rewrite.d.ts +24 -0
- package/dist/esm/hooks/pretool-guard/rtk-rewrite.js +50 -0
- package/dist/esm/hooks/pretool-guard/runner.js +6 -0
- package/dist/esm/hooks/status/index.js +1 -1
- package/dist/esm/mcp/blueprint/_shared/lifecycle.js +39 -11
- package/dist/esm/mcp/blueprint/handlers/review-log.d.ts +14 -7
- package/dist/esm/mcp/blueprint/handlers/review-log.js +46 -22
- package/dist/esm/mcp/blueprint/registration.js +1 -1
- package/dist/esm/mcp/tools/_shared/redact.d.ts +22 -0
- package/dist/esm/mcp/tools/_shared/redact.js +114 -8
- package/dist/esm/mcp/tools/gain.js +28 -2
- package/dist/esm/mcp/tools/worktree.js +65 -3
- package/dist/esm/mcp/tools/wp-ui-compose.js +38 -4
- package/dist/esm/mcp/tools/wp-ui-preview.js +14 -7
- package/dist/esm/review/lifecycle-writers.d.ts +76 -0
- package/dist/esm/review/lifecycle-writers.js +330 -0
- package/dist/esm/review/subject.js +66 -32
- package/dist/esm/status/snapshot.d.ts +5 -0
- package/dist/esm/status/snapshot.js +27 -2
- package/dist/esm/test/shard-durations.json +0 -1
- package/dist/esm/test-helpers/global-setup.js +38 -3
- package/dist/esm/tool-runtime/managed-rtk.d.ts +44 -0
- package/dist/esm/tool-runtime/managed-rtk.js +275 -0
- package/dist/esm/tool-runtime/resolve-runner.js +18 -194
- package/dist/esm/utils/package-root.d.ts +32 -0
- package/dist/esm/utils/package-root.js +82 -0
- package/dist/esm/worktrees/git-metadata.d.ts +41 -0
- package/dist/esm/worktrees/git-metadata.js +48 -0
- package/package.json +16 -21
- package/dist/esm/config/oxlint/code-safety.d.ts +0 -18
- package/dist/esm/config/oxlint/foundation-purity.d.ts +0 -21
- package/dist/esm/config/oxlint/graphql-conventions.d.ts +0 -23
- package/dist/esm/config/oxlint/import-hygiene.d.ts +0 -34
- package/dist/esm/config/oxlint/index.d.ts +0 -22
- package/dist/esm/config/oxlint/index.js +0 -28
- package/dist/esm/config/oxlint/monorepo-paths.d.ts +0 -22
- package/dist/esm/config/oxlint/query-patterns.d.ts +0 -24
- package/dist/esm/config/oxlint/testing-quality.d.ts +0 -35
- package/dist/esm/config/oxlint/tier-boundaries.d.ts +0 -35
- package/dist/esm/output-transforms/rulesync.d.ts +0 -2
- package/dist/esm/output-transforms/rulesync.js +0 -79
|
@@ -1,24 +1,36 @@
|
|
|
1
|
-
import { spawnSync } from "node:child_process";
|
|
2
1
|
import type { MergeOptions } from "#cli/commands/init/merge";
|
|
3
2
|
import { type SpinnerFactory } from "#cli/commands/init/scaffolders/spinner";
|
|
3
|
+
import { type ManagedRtkDownloader } from "#tool-runtime/managed-rtk.js";
|
|
4
4
|
export interface EnsureRtkInput {
|
|
5
5
|
repoRoot: string;
|
|
6
6
|
options: MergeOptions;
|
|
7
|
-
|
|
7
|
+
/** @deprecated ignored — managed rtk no longer shells out to brew/PATH install */
|
|
8
|
+
spawn?: unknown;
|
|
8
9
|
pinFilePath?: string;
|
|
9
10
|
strict?: boolean;
|
|
10
11
|
spinnerFactory?: SpinnerFactory;
|
|
12
|
+
/** Test seam: inject archive downloader for ensureManagedRtk */
|
|
13
|
+
downloader?: ManagedRtkDownloader;
|
|
11
14
|
}
|
|
12
15
|
export type EnsureRtkResult = {
|
|
13
16
|
kind: "rtk-ok";
|
|
14
17
|
installed: boolean;
|
|
18
|
+
path: string;
|
|
15
19
|
} | {
|
|
16
20
|
kind: "rtk-skipped-dry-run";
|
|
17
21
|
} | {
|
|
18
22
|
kind: "rtk-not-found";
|
|
19
23
|
hint: string;
|
|
20
|
-
} | {
|
|
21
|
-
kind: "rtk-init-failed";
|
|
22
|
-
exitCode: number;
|
|
23
24
|
};
|
|
25
|
+
/**
|
|
26
|
+
* Strip freestanding RTK PreToolUse surface from project Claude settings and
|
|
27
|
+
* delete the retired script. Also removes the historical stale legacy-runtime
|
|
28
|
+
* PreToolUse group when a direct wp-pretool-guard hook is already present.
|
|
29
|
+
* Idempotent; no-op when settings are absent or already clean.
|
|
30
|
+
*/
|
|
31
|
+
export declare function stripFreestandingRtkHooks(repoRoot: string): void;
|
|
32
|
+
/**
|
|
33
|
+
* Ensure managed (embedded-cache) rtk is available and freestanding hooks are gone.
|
|
34
|
+
* No brew, no rtk init --auto-patch, no PATH install requirement.
|
|
35
|
+
*/
|
|
24
36
|
export declare function ensureRtk(input: EnsureRtkInput): EnsureRtkResult;
|
|
@@ -1,32 +1,28 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
1
|
+
import { existsSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
3
2
|
import { join } from "node:path";
|
|
4
3
|
import { makeNoopSpinnerFactory, } from "#cli/commands/init/scaffolders/spinner";
|
|
5
|
-
import {
|
|
6
|
-
const NOT_FOUND_HINT = "rtk
|
|
7
|
-
// `rtk --version` answers in ~10ms; 3s is generous headroom. Bounding the
|
|
8
|
-
// presence probe keeps a hung/wrong binary from stalling `wp setup`. The
|
|
9
|
-
// `brew install` / `rtk init` calls below are intentionally left unbounded —
|
|
10
|
-
// they are user-driven installs with inherited stdio where a fixed deadline
|
|
11
|
-
// would wrongly kill a legitimately slow run.
|
|
12
|
-
const RTK_PROBE_TIMEOUT_MS = 3000;
|
|
4
|
+
import { ensureManagedRtk } from "#tool-runtime/managed-rtk.js";
|
|
5
|
+
const NOT_FOUND_HINT = "managed rtk binary could not be installed (network/platform). Set WP_SKIP_RTK=1 to skip, or WP_RTK_BIN=/path/to/rtk.";
|
|
13
6
|
const RTK_HOOK_RELATIVE_PATH = join(".claude", "hooks", "rtk-rewrite.sh");
|
|
14
7
|
const CLAUDE_SETTINGS_RELATIVE_PATH = join(".claude", "settings.json");
|
|
15
8
|
const STALE_runtime_CLAUDE_COMMAND = "external-runtime/dist/scripts/codex-native-hook.js";
|
|
16
|
-
|
|
9
|
+
const FREESTANDING_RTK_HOOK_MARKERS = ["rtk-rewrite.sh", "rtk hook claude"];
|
|
10
|
+
/**
|
|
11
|
+
* Strip freestanding RTK PreToolUse surface from project Claude settings and
|
|
12
|
+
* delete the retired script. Also removes the historical stale legacy-runtime
|
|
13
|
+
* PreToolUse group when a direct wp-pretool-guard hook is already present.
|
|
14
|
+
* Idempotent; no-op when settings are absent or already clean.
|
|
15
|
+
*/
|
|
16
|
+
export function stripFreestandingRtkHooks(repoRoot) {
|
|
17
17
|
const hookPath = join(repoRoot, RTK_HOOK_RELATIVE_PATH);
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
catch {
|
|
26
|
-
return false;
|
|
18
|
+
if (existsSync(hookPath)) {
|
|
19
|
+
try {
|
|
20
|
+
rmSync(hookPath, { force: true });
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
// best-effort; doctor will still flag if the file remains
|
|
24
|
+
}
|
|
27
25
|
}
|
|
28
|
-
}
|
|
29
|
-
function normalizeClaudeRtkSettings(repoRoot) {
|
|
30
26
|
const settingsPath = join(repoRoot, CLAUDE_SETTINGS_RELATIVE_PATH);
|
|
31
27
|
if (!existsSync(settingsPath))
|
|
32
28
|
return;
|
|
@@ -41,73 +37,58 @@ function normalizeClaudeRtkSettings(repoRoot) {
|
|
|
41
37
|
if (!Array.isArray(groups) || groups.length === 0)
|
|
42
38
|
return;
|
|
43
39
|
const hasDirectWpGuard = groups.some((group) => (group.hooks ?? []).some((hook) => hook.command?.includes("wp-pretool-guard")));
|
|
44
|
-
if (!hasDirectWpGuard)
|
|
45
|
-
return;
|
|
46
40
|
let changed = false;
|
|
47
|
-
const nextGroups = groups
|
|
41
|
+
const nextGroups = groups
|
|
42
|
+
.map((group) => {
|
|
43
|
+
const hooks = group.hooks ?? [];
|
|
44
|
+
const nextHooks = hooks.filter((hook) => {
|
|
45
|
+
const command = hook.command ?? "";
|
|
46
|
+
if (FREESTANDING_RTK_HOOK_MARKERS.some((marker) => command.includes(marker))) {
|
|
47
|
+
changed = true;
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
return true;
|
|
51
|
+
});
|
|
52
|
+
if (nextHooks.length !== hooks.length) {
|
|
53
|
+
return { ...group, hooks: nextHooks };
|
|
54
|
+
}
|
|
55
|
+
return group;
|
|
56
|
+
})
|
|
57
|
+
.filter((group) => {
|
|
48
58
|
const commands = (group.hooks ?? []).map((hook) => hook.command ?? "");
|
|
49
|
-
|
|
50
|
-
|
|
59
|
+
if (commands.length === 0) {
|
|
60
|
+
changed = true;
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
const isStaleOmcGroup = hasDirectWpGuard &&
|
|
64
|
+
commands.some((command) => command.includes(STALE_runtime_CLAUDE_COMMAND));
|
|
65
|
+
if (isStaleOmcGroup) {
|
|
51
66
|
changed = true;
|
|
52
|
-
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
return true;
|
|
53
70
|
});
|
|
54
71
|
if (!changed)
|
|
55
72
|
return;
|
|
56
73
|
parsed.hooks = { ...parsed.hooks, PreToolUse: nextGroups };
|
|
57
74
|
writeFileSync(settingsPath, `${JSON.stringify(parsed, null, 2)}\n`, "utf8");
|
|
58
75
|
}
|
|
76
|
+
/**
|
|
77
|
+
* Ensure managed (embedded-cache) rtk is available and freestanding hooks are gone.
|
|
78
|
+
* No brew, no rtk init --auto-patch, no PATH install requirement.
|
|
79
|
+
*/
|
|
59
80
|
export function ensureRtk(input) {
|
|
60
81
|
if (input.options.dryRun)
|
|
61
82
|
return { kind: "rtk-skipped-dry-run" };
|
|
62
|
-
const spawn = input.spawn ?? spawnSync;
|
|
63
83
|
const spinner = (input.spinnerFactory ?? makeNoopSpinnerFactory())("rtk");
|
|
64
|
-
let installed = false;
|
|
65
84
|
spinner.start();
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
}
|
|
72
|
-
const install = spawn("brew", ["install", "rtk"], { stdio: "inherit" });
|
|
73
|
-
if (install.status !== 0) {
|
|
74
|
-
spinner.fail("rtk install failed");
|
|
75
|
-
return { kind: "rtk-not-found", hint: NOT_FOUND_HINT };
|
|
76
|
-
}
|
|
77
|
-
installed = true;
|
|
78
|
-
probe = spawn("rtk", ["--version"], { encoding: "utf8", timeout: RTK_PROBE_TIMEOUT_MS });
|
|
79
|
-
if (probe.error || (probe.status !== null && probe.status !== 0)) {
|
|
80
|
-
spinner.fail("rtk not found after install");
|
|
81
|
-
return { kind: "rtk-not-found", hint: NOT_FOUND_HINT };
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
const installedVersion = String(probe.stdout ?? "").trim();
|
|
85
|
-
const pinCheck = checkVersionPin("rtk", installedVersion, input.pinFilePath ?? join(input.repoRoot, "compatible-versions.json"));
|
|
86
|
-
if (!pinCheck.ok) {
|
|
87
|
-
if (input.strict) {
|
|
88
|
-
spinner.fail("rtk version mismatch");
|
|
89
|
-
return { kind: "rtk-init-failed", exitCode: -1 };
|
|
90
|
-
}
|
|
91
|
-
console.warn(pinCheck.warning);
|
|
92
|
-
}
|
|
93
|
-
if (hasInstalledRtkHook(input.repoRoot)) {
|
|
94
|
-
normalizeClaudeRtkSettings(input.repoRoot);
|
|
95
|
-
spinner.succeed("rtk ready");
|
|
96
|
-
return { kind: "rtk-ok", installed };
|
|
97
|
-
}
|
|
98
|
-
const result = spawn("rtk", ["init", "-g", "--auto-patch"], {
|
|
99
|
-
cwd: input.repoRoot,
|
|
100
|
-
stdio: "inherit",
|
|
101
|
-
env: {
|
|
102
|
-
...process.env,
|
|
103
|
-
RTK_TELEMETRY_DISABLED: "1",
|
|
104
|
-
},
|
|
105
|
-
});
|
|
106
|
-
if (result.status !== 0) {
|
|
107
|
-
spinner.fail("rtk init failed");
|
|
108
|
-
return { kind: "rtk-init-failed", exitCode: result.status ?? -1 };
|
|
85
|
+
// Always strip freestanding surface first so setup never leaves CLAUDE_PROJECT_DIR hooks.
|
|
86
|
+
stripFreestandingRtkHooks(input.repoRoot);
|
|
87
|
+
const ensured = ensureManagedRtk({ downloader: input.downloader });
|
|
88
|
+
if (!ensured.ok) {
|
|
89
|
+
spinner.fail("rtk not available");
|
|
90
|
+
return { kind: "rtk-not-found", hint: `${NOT_FOUND_HINT} (${ensured.error})` };
|
|
109
91
|
}
|
|
110
|
-
normalizeClaudeRtkSettings(input.repoRoot);
|
|
111
92
|
spinner.succeed("rtk ready");
|
|
112
|
-
return { kind: "rtk-ok", installed };
|
|
93
|
+
return { kind: "rtk-ok", installed: ensured.installed, path: ensured.path };
|
|
113
94
|
}
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Runtime detection for tools that webpresso
|
|
3
|
-
*
|
|
4
|
-
* or system package managers shouldn't be triggered silently).
|
|
2
|
+
* Runtime detection for host-install tools that webpresso does **not**
|
|
3
|
+
* managed-download (see catalog rule `managed-tool-binaries`).
|
|
5
4
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
5
|
+
* Contrasts with managed rtk (`ensureManagedRtk`): bun/vp/actionlint stay
|
|
6
|
+
* host package-manager installs. This probe only prints status + hints —
|
|
7
|
+
* it never curl-pipes or brew-installs silently.
|
|
8
|
+
*
|
|
9
|
+
* Auto-runs at the end of `wp setup`; one line per runtime.
|
|
9
10
|
*/
|
|
10
11
|
import { spawnSync } from "node:child_process";
|
|
11
12
|
function probeVersion(cmd, args = ["--version"]) {
|
|
@@ -1,14 +1,25 @@
|
|
|
1
1
|
import type { ConvergeUnit } from "./convergence.js";
|
|
2
|
-
export type CollisionResolution = "none" | "tracked" | "removed-identical" | "removed-owned";
|
|
2
|
+
export type CollisionResolution = "none" | "tracked" | "removed-identical" | "removed-owned" | "ignored-patch-derived" | "ignored-backed-up";
|
|
3
3
|
export interface CollisionResult {
|
|
4
4
|
readonly ok: boolean;
|
|
5
5
|
readonly resolution?: CollisionResolution;
|
|
6
6
|
readonly error?: string;
|
|
7
|
+
/** Set when the collision proceeded but the caller should surface a notice. */
|
|
8
|
+
readonly warning?: string;
|
|
7
9
|
}
|
|
8
10
|
export interface ResolveCollisionOptions {
|
|
9
11
|
readonly unit: ConvergeUnit;
|
|
10
12
|
readonly desiredContent: string | Uint8Array;
|
|
11
13
|
/** Compatibility input only. Unknown content remains protected when true. */
|
|
12
14
|
readonly force?: boolean;
|
|
15
|
+
/**
|
|
16
|
+
* True when `desiredContent` was computed FROM the existing file's own
|
|
17
|
+
* content (e.g. a JSON key-preserving patch), so a divergent unmarked
|
|
18
|
+
* existing file is safe to write through: the caller already folded the
|
|
19
|
+
* user's data into `desiredContent`.
|
|
20
|
+
*/
|
|
21
|
+
readonly patchDerived?: boolean;
|
|
13
22
|
}
|
|
23
|
+
/** True when `gitPath` is git-ignored in `repoRoot` (machine-local, never committed). */
|
|
24
|
+
export declare function isGitIgnored(repoRoot: string, gitPath: string): boolean;
|
|
14
25
|
export declare function resolveUntrackedCollision(options: ResolveCollisionOptions): CollisionResult;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { spawnSync } from "node:child_process";
|
|
2
|
-
import { existsSync, lstatSync, readFileSync, rmSync } from "node:fs";
|
|
2
|
+
import { existsSync, lstatSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
3
3
|
const GIT_TIMEOUT_MS = 15_000;
|
|
4
4
|
const OWNERSHIP_MARKER = /^\uFEFF?[ \t]*(?:#|\/\/|\/\*|<!--|")?[ \t]*(?:>>> managed by webpresso\b|Generated by webpresso\b)/iu;
|
|
5
5
|
function git(repoRoot, args) {
|
|
@@ -12,6 +12,25 @@ function git(repoRoot, args) {
|
|
|
12
12
|
});
|
|
13
13
|
return { ok: result.status === 0 && !result.error };
|
|
14
14
|
}
|
|
15
|
+
/** True when `gitPath` is git-ignored in `repoRoot` (machine-local, never committed). */
|
|
16
|
+
export function isGitIgnored(repoRoot, gitPath) {
|
|
17
|
+
return git(repoRoot, ["check-ignore", "--no-index", "-q", "--", gitPath]).ok;
|
|
18
|
+
}
|
|
19
|
+
function resolveIgnoredDivergence(options) {
|
|
20
|
+
const { unit } = options;
|
|
21
|
+
if (options.patchDerived) {
|
|
22
|
+
return { ok: true, resolution: "ignored-patch-derived" };
|
|
23
|
+
}
|
|
24
|
+
const current = readFileSync(unit.targetPath);
|
|
25
|
+
const backupPath = `${unit.targetPath}.bak`;
|
|
26
|
+
writeFileSync(backupPath, current);
|
|
27
|
+
return {
|
|
28
|
+
ok: true,
|
|
29
|
+
resolution: "ignored-backed-up",
|
|
30
|
+
warning: `Untracked, git-ignored ${unit.gitPath} had unmanaged content; ` +
|
|
31
|
+
`the original was backed up to ${unit.gitPath}.bak before it was overwritten.`,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
15
34
|
export function resolveUntrackedCollision(options) {
|
|
16
35
|
const { unit } = options;
|
|
17
36
|
if (!existsSync(unit.targetPath))
|
|
@@ -34,6 +53,9 @@ export function resolveUntrackedCollision(options) {
|
|
|
34
53
|
rmSync(unit.targetPath);
|
|
35
54
|
return { ok: true, resolution: "removed-owned" };
|
|
36
55
|
}
|
|
56
|
+
if (unit.scope === "repo-ignored") {
|
|
57
|
+
return resolveIgnoredDivergence(options);
|
|
58
|
+
}
|
|
37
59
|
return {
|
|
38
60
|
ok: false,
|
|
39
61
|
error: `Unknown untracked content blocks ${unit.gitPath}; it was preserved${options.force ? " despite --force" : ""}. ` +
|
|
@@ -53,7 +53,7 @@ export function getPrWaitCommandHelpText() {
|
|
|
53
53
|
" --branch <name> Branch selector (defaults to current branch)",
|
|
54
54
|
" --pr <number-or-url> Explicit PR selector",
|
|
55
55
|
" --until <target> ready | checks-terminal | checks-green | merged (default: checks-terminal)",
|
|
56
|
-
" --timeout-ms <ms> Overall deadline; for ready, per-probe timeout only",
|
|
56
|
+
" --timeout-ms <ms> Overall deadline, max 300000 (5min); for ready, per-probe timeout only",
|
|
57
57
|
" --poll-interval-ms <ms> Poll interval (default: 15000)",
|
|
58
58
|
" --include-checks Include final check snapshot (default: true)",
|
|
59
59
|
" --no-include-checks Omit final check snapshot from output",
|
|
@@ -2,15 +2,29 @@ import { PR_WAIT_UNTIL_VALUES, runPrWait } from "#mcp/tools/pr-wait.js";
|
|
|
2
2
|
import { buildStatusSnapshot, formatStatusSnapshot } from "#status/snapshot.js";
|
|
3
3
|
// PR help text lives in ./pr-help.ts (dependency-free) so the `wp pr --help`
|
|
4
4
|
// fast path can render it without importing this heavy command module.
|
|
5
|
-
|
|
5
|
+
// Mirrors the hard cap `readonlyOpsBaseSchema` enforces downstream for `pr
|
|
6
|
+
// wait` (src/mcp/tools/_readonly-ops.ts). `pr status` shares the same
|
|
7
|
+
// `--timeout-ms` flag but reaches `buildStatusSnapshot` directly, which has
|
|
8
|
+
// no schema of its own, so an over-cap value was silently accepted with no
|
|
9
|
+
// error and no clamp. Validating here makes both subcommands fail the same
|
|
10
|
+
// way, with a clear message, before any network probe starts, instead of
|
|
11
|
+
// relying on a downstream schema that not every code path shares.
|
|
12
|
+
const MAX_TIMEOUT_MS = 300_000;
|
|
13
|
+
function parsePositiveInteger(value, name, max) {
|
|
6
14
|
if (value === undefined)
|
|
7
15
|
return undefined;
|
|
8
16
|
const parsed = typeof value === "number" ? value : Number.parseInt(String(value), 10);
|
|
9
17
|
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
10
18
|
throw new Error(`${name} must be a positive integer`);
|
|
11
19
|
}
|
|
20
|
+
if (max !== undefined && parsed > max) {
|
|
21
|
+
throw new Error(`${name} must not exceed ${max}`);
|
|
22
|
+
}
|
|
12
23
|
return parsed;
|
|
13
24
|
}
|
|
25
|
+
function parseTimeoutMs(value) {
|
|
26
|
+
return parsePositiveInteger(value, "--timeout-ms", MAX_TIMEOUT_MS);
|
|
27
|
+
}
|
|
14
28
|
function normalizeUntil(value) {
|
|
15
29
|
if (value === undefined)
|
|
16
30
|
return undefined;
|
|
@@ -26,7 +40,7 @@ export function registerPrCommand(cli) {
|
|
|
26
40
|
.option("--branch <name>", "Branch selector")
|
|
27
41
|
.option("--pr <number-or-url>", "Explicit PR selector")
|
|
28
42
|
.option("--until <target>", "ready | checks-terminal | checks-green | merged")
|
|
29
|
-
.option("--timeout-ms <ms>", "Overall deadline; for ready, per-probe timeout only")
|
|
43
|
+
.option("--timeout-ms <ms>", "Overall deadline (max 300000ms / 5min); for ready, per-probe timeout only")
|
|
30
44
|
.option("--poll-interval-ms <ms>", "Poll interval")
|
|
31
45
|
.option("--include-checks", "Include final check snapshot", { default: true })
|
|
32
46
|
.option("--max-output-bytes <n>", "Per-command output budget")
|
|
@@ -41,7 +55,7 @@ export function registerPrCommand(cli) {
|
|
|
41
55
|
includePr: true,
|
|
42
56
|
maxOutputBytes: parsePositiveInteger(options.maxOutputBytes, "--max-output-bytes"),
|
|
43
57
|
pr: options.pr === undefined ? undefined : String(options.pr),
|
|
44
|
-
timeoutMs:
|
|
58
|
+
timeoutMs: parseTimeoutMs(options.timeoutMs),
|
|
45
59
|
});
|
|
46
60
|
console.log(options.json ? JSON.stringify(snapshot, null, 2) : formatStatusSnapshot(snapshot));
|
|
47
61
|
return options.requireReady && snapshot.status !== "ready" ? 1 : 0;
|
|
@@ -57,7 +71,7 @@ export function registerPrCommand(cli) {
|
|
|
57
71
|
maxOutputBytes: parsePositiveInteger(options.maxOutputBytes, "--max-output-bytes"),
|
|
58
72
|
pollIntervalMs: parsePositiveInteger(options.pollIntervalMs, "--poll-interval-ms"),
|
|
59
73
|
pr: options.pr === undefined ? undefined : String(options.pr),
|
|
60
|
-
timeoutMs:
|
|
74
|
+
timeoutMs: parseTimeoutMs(options.timeoutMs),
|
|
61
75
|
until,
|
|
62
76
|
});
|
|
63
77
|
console.log(JSON.stringify(payload, null, 2));
|
|
@@ -1463,6 +1463,35 @@ function gateFailureFromSummary(summary) {
|
|
|
1463
1463
|
...(summary.retryAfterMs === undefined ? {} : { retryAfterMs: summary.retryAfterMs }),
|
|
1464
1464
|
};
|
|
1465
1465
|
}
|
|
1466
|
+
// `providerErrorDetail`'s doc comment promises "sanitized, length-bounded"
|
|
1467
|
+
// text; a checkout-creation error's message/cause chain is target/environment
|
|
1468
|
+
// text (pnpm/git stderr), not attacker-shaped, but is still capped here so
|
|
1469
|
+
// the invariant holds regardless.
|
|
1470
|
+
const MAX_PROVIDER_ERROR_DETAIL_LENGTH = 2000;
|
|
1471
|
+
// Walks an `Error.cause` chain into a single human-readable line. Used to
|
|
1472
|
+
// surface the real reason a review checkout could not be created instead of
|
|
1473
|
+
// discarding it behind a fixed string (see the bare `catch` this replaces in
|
|
1474
|
+
// `invokeProvider`). Bounded depth + a `seen` set guard against a pathological
|
|
1475
|
+
// or cyclic cause chain.
|
|
1476
|
+
function formatErrorWithCauseChain(error, maxDepth = 5) {
|
|
1477
|
+
const parts = [];
|
|
1478
|
+
const seen = new Set();
|
|
1479
|
+
let current = error;
|
|
1480
|
+
for (let depth = 0; depth < maxDepth; depth += 1) {
|
|
1481
|
+
if (current === undefined || current === null || seen.has(current))
|
|
1482
|
+
break;
|
|
1483
|
+
seen.add(current);
|
|
1484
|
+
if (current instanceof Error) {
|
|
1485
|
+
parts.push(current.message);
|
|
1486
|
+
current = current.cause;
|
|
1487
|
+
}
|
|
1488
|
+
else {
|
|
1489
|
+
parts.push(String(current));
|
|
1490
|
+
break;
|
|
1491
|
+
}
|
|
1492
|
+
}
|
|
1493
|
+
return parts.join(" -> caused by: ");
|
|
1494
|
+
}
|
|
1466
1495
|
export async function invokeProvider(input, executeReview = runReviewExecution, runCatalogCommand = runCommand, createTargetCheckout = createReviewTargetCheckout,
|
|
1467
1496
|
// A thunk (not a checkout instance) so callers that never reach a real
|
|
1468
1497
|
// provider attempt — injected fakes included — never pay the checkout's
|
|
@@ -1475,19 +1504,34 @@ getSharedCheckout) {
|
|
|
1475
1504
|
checkout = existingCheckout ?? createTargetCheckout(input.projectRoot, input.targetSha);
|
|
1476
1505
|
checkout.beginAttempt();
|
|
1477
1506
|
}
|
|
1478
|
-
catch {
|
|
1507
|
+
catch (error) {
|
|
1508
|
+
// Bind + chain via Error.cause (ES2022) rather than discarding the real
|
|
1509
|
+
// failure — this exact swallow previously hid a genuine supply-chain
|
|
1510
|
+
// signal (`ERR_PNPM_TRUST_DOWNGRADE`) behind a fixed string for a full
|
|
1511
|
+
// day. See catalog/agent/rules/ts-coding-conventions.md.
|
|
1512
|
+
const isolationFailure = new Error("Unable to create the disposable review checkout; review was not started", { cause: error });
|
|
1513
|
+
let cleanupError;
|
|
1479
1514
|
if (ownsCheckout) {
|
|
1480
1515
|
try {
|
|
1481
1516
|
checkout?.cleanup();
|
|
1482
1517
|
}
|
|
1483
|
-
catch {
|
|
1484
|
-
//
|
|
1518
|
+
catch (cleanupFailure) {
|
|
1519
|
+
// Cleanup failing must not mask the primary isolation failure above;
|
|
1520
|
+
// capture it as secondary diagnostic context instead of discarding it.
|
|
1521
|
+
cleanupError = cleanupFailure;
|
|
1485
1522
|
}
|
|
1486
1523
|
}
|
|
1487
1524
|
const requestedModel = input.model ?? defaultReviewModel(input.provider) ?? "unresolved";
|
|
1488
1525
|
const model = resolveReviewModel(input.provider, requestedModel);
|
|
1526
|
+
// Chain from the original `error`, not `isolationFailure` — the latter's
|
|
1527
|
+
// own message is already the fixed prefix below, so chaining from it
|
|
1528
|
+
// would print that fixed sentence twice.
|
|
1529
|
+
const providerErrorDetail = (formatErrorWithCauseChain(error) +
|
|
1530
|
+
(cleanupError === undefined
|
|
1531
|
+
? ""
|
|
1532
|
+
: ` | cleanup after isolation failure also failed: ${formatErrorWithCauseChain(cleanupError)}`)).slice(0, MAX_PROVIDER_ERROR_DETAIL_LENGTH);
|
|
1489
1533
|
return {
|
|
1490
|
-
output:
|
|
1534
|
+
output: `${isolationFailure.message}: ${providerErrorDetail}`,
|
|
1491
1535
|
model,
|
|
1492
1536
|
reviewer: input.provider === "opencode" && model !== "unresolved"
|
|
1493
1537
|
? reviewerFamily(model)
|
|
@@ -1497,6 +1541,7 @@ getSharedCheckout) {
|
|
|
1497
1541
|
verdict: "no-verdict",
|
|
1498
1542
|
terminalClassification: "review-isolation-failure",
|
|
1499
1543
|
failureCode: "review-isolation-failure",
|
|
1544
|
+
providerErrorDetail,
|
|
1500
1545
|
};
|
|
1501
1546
|
}
|
|
1502
1547
|
try {
|
|
@@ -2127,21 +2172,30 @@ export async function runReviewGate(projectRoot, slug, input, dependencies = { i
|
|
|
2127
2172
|
// never need one (plan-purpose reuse of an existing approval, terminal
|
|
2128
2173
|
// rejections, exhausted scope). A setup failure is remembered so provider
|
|
2129
2174
|
// attempts still surface the same isolation-failure evidence.
|
|
2175
|
+
// Captures the real failure the first time checkoutFactory is
|
|
2176
|
+
// attempted this run, so the re-throw below (reached whenever a later
|
|
2177
|
+
// caller reuses the memoized failure) can chain to it via Error.cause
|
|
2178
|
+
// instead of discarding it — see ts-coding-conventions.md "never
|
|
2179
|
+
// swallow an error".
|
|
2180
|
+
let checkoutSetupError;
|
|
2130
2181
|
const ensureSharedCheckout = () => {
|
|
2131
2182
|
if (sharedCheckout !== undefined || checkoutSetupFailed)
|
|
2132
2183
|
return sharedCheckout;
|
|
2133
2184
|
try {
|
|
2134
2185
|
sharedCheckout = checkoutFactory(projectRoot, targetSha);
|
|
2135
2186
|
}
|
|
2136
|
-
catch {
|
|
2187
|
+
catch (error) {
|
|
2137
2188
|
checkoutSetupFailed = true;
|
|
2189
|
+
checkoutSetupError = error;
|
|
2138
2190
|
}
|
|
2139
2191
|
return sharedCheckout;
|
|
2140
2192
|
};
|
|
2141
2193
|
const createCheckoutForAttempt = () => {
|
|
2142
2194
|
// Only reachable when ensureSharedCheckout() already failed — a live
|
|
2143
2195
|
// shared checkout is always passed to the attempt as existingCheckout.
|
|
2144
|
-
throw new Error("review checkout setup failed"
|
|
2196
|
+
throw new Error("review checkout setup failed", {
|
|
2197
|
+
cause: checkoutSetupError,
|
|
2198
|
+
});
|
|
2145
2199
|
};
|
|
2146
2200
|
let sharedCheckoutCleaned = false;
|
|
2147
2201
|
const cleanupSharedCheckout = () => {
|
|
@@ -8,6 +8,18 @@ export interface WorktreeCommandOptions {
|
|
|
8
8
|
prefix?: string;
|
|
9
9
|
dryRun?: boolean;
|
|
10
10
|
force?: boolean;
|
|
11
|
+
/**
|
|
12
|
+
* Bypass the "primary must be on the expected branch" merge-cleanup
|
|
13
|
+
* refusal by switching a CLEAN primary checkout to that branch. Never
|
|
14
|
+
* bypasses the "primary is dirty" refusal — a dirty primary always
|
|
15
|
+
* refuses, regardless of this flag (see `decideMergeCleanup`).
|
|
16
|
+
*/
|
|
17
|
+
switchPrimary?: boolean;
|
|
18
|
+
/**
|
|
19
|
+
* @deprecated Renamed to `switchPrimary`. Kept as an accepted alias only
|
|
20
|
+
* (mapped from the CLI's legacy `--stash-primary` flag) — it no longer
|
|
21
|
+
* stashes anything; a dirty primary always refuses regardless of this flag.
|
|
22
|
+
*/
|
|
11
23
|
stashPrimary?: boolean;
|
|
12
24
|
noSetup?: boolean;
|
|
13
25
|
/** Dependency hydration for `new` only: install-locked (default) or reconcile. */
|
|
@@ -41,7 +53,6 @@ export interface MergeCleanupPlan {
|
|
|
41
53
|
readonly removeArgs: readonly string[];
|
|
42
54
|
readonly fetchArgs: readonly string[];
|
|
43
55
|
readonly mergeArgs: readonly string[];
|
|
44
|
-
readonly stashArgs?: readonly string[];
|
|
45
56
|
readonly switchArgs?: readonly string[];
|
|
46
57
|
readonly preservedPrimaryBranch?: string;
|
|
47
58
|
}
|
|
@@ -54,7 +65,18 @@ export interface MergeCleanupDecisionInput {
|
|
|
54
65
|
readonly currentBranch: string;
|
|
55
66
|
readonly repoDirty: boolean;
|
|
56
67
|
readonly targetDirty: boolean;
|
|
57
|
-
|
|
68
|
+
/**
|
|
69
|
+
* Bypasses the wrong-branch refusal for a CLEAN primary only. A dirty
|
|
70
|
+
* primary always refuses — see `decideMergeCleanup` — this never bypasses
|
|
71
|
+
* that refusal, and merge-cleanup never stashes primary state.
|
|
72
|
+
*/
|
|
73
|
+
readonly switchPrimary?: boolean;
|
|
74
|
+
/**
|
|
75
|
+
* When the primary is dirty, optional porcelain -z lists the offending
|
|
76
|
+
* paths in the refusal message. Omitted messages stay generic (e.g. for
|
|
77
|
+
* unit tests that don't care about message content).
|
|
78
|
+
*/
|
|
79
|
+
readonly repoPorcelainZ?: string;
|
|
58
80
|
/**
|
|
59
81
|
* When target is dirty, optional porcelain -z + slug enrich the refusal message
|
|
60
82
|
* (scaffold-only vs other dirt). Omitted messages stay generic for unit tests.
|