@webpresso/agent-kit 3.3.0 → 3.3.2
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/catalog/agent/rules/pre-implementation.md +30 -8
- package/catalog/agent/skills/ultragoal/SKILL.md +5 -18
- package/dist/esm/blueprint/core/validation/task-blocks.js +7 -1
- package/dist/esm/blueprint/markdown/task-heading.d.ts +21 -0
- package/dist/esm/blueprint/markdown/task-heading.js +25 -0
- package/dist/esm/blueprint/trust/command-runner.d.ts +40 -0
- package/dist/esm/blueprint/trust/command-runner.js +265 -41
- package/dist/esm/build/cli-mcp-parity.js +5 -0
- package/dist/esm/ci/native-session-memory-cache.d.ts +2 -1
- package/dist/esm/ci/native-session-memory-cache.js +2 -1
- package/dist/esm/ci/release-progress.d.ts +37 -0
- package/dist/esm/ci/release-progress.js +139 -0
- package/dist/esm/cli/cli.d.ts +1 -1
- package/dist/esm/cli/cli.js +9 -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/mutations.js +2 -3
- 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/init/convergence-apply.d.ts +28 -5
- package/dist/esm/cli/commands/init/convergence-apply.js +75 -13
- package/dist/esm/cli/commands/opencode-probe.d.ts +11 -0
- package/dist/esm/cli/commands/opencode-probe.js +1 -1
- package/dist/esm/cli/commands/quality-runner.js +6 -2
- package/dist/esm/cli/commands/release-progress.d.ts +53 -0
- package/dist/esm/cli/commands/release-progress.js +155 -0
- package/dist/esm/cli/commands/review.js +74 -11
- package/dist/esm/cli/commands/typecheck.js +1 -1
- package/dist/esm/cli/commands/worktree/router-dispatch.d.ts +8 -0
- package/dist/esm/cli/commands/worktree/router-dispatch.js +25 -1
- package/dist/esm/cli/utils.d.ts +8 -1
- package/dist/esm/cli/utils.js +12 -2
- package/dist/esm/hooks/pretool-guard/validators/plan-frontmatter.js +3 -2
- package/dist/esm/mcp/blueprint/handlers/document-mutations.js +2 -1
- package/dist/esm/mcp/blueprint/handlers/task-advance.js +2 -12
- package/dist/esm/review/delivery-verifier.d.ts +33 -2
- package/dist/esm/review/delivery-verifier.js +130 -24
- package/dist/esm/review/execution/review-checkout.d.ts +7 -0
- package/dist/esm/review/execution/review-checkout.js +416 -27
- package/dist/esm/review/execution/sandbox/adapter.d.ts +37 -0
- package/dist/esm/review/execution/sandbox/adapter.js +39 -0
- package/dist/esm/review/execution/sandbox/env.d.ts +12 -0
- package/dist/esm/review/execution/sandbox/env.js +37 -0
- package/dist/esm/review/execution/sandbox/index.d.ts +5 -0
- package/dist/esm/review/execution/sandbox/index.js +4 -0
- package/dist/esm/review/execution/sandbox/policy.d.ts +19 -0
- package/dist/esm/review/execution/sandbox/policy.js +95 -0
- package/dist/esm/review/execution/sandbox/probe.d.ts +20 -0
- package/dist/esm/review/execution/sandbox/probe.js +97 -0
- package/dist/esm/review/execution/sandbox/shell-quote.d.ts +14 -0
- package/dist/esm/review/execution/sandbox/shell-quote.js +18 -0
- package/dist/esm/review/execution/sandbox/tmpdir.d.ts +9 -0
- package/dist/esm/review/execution/sandbox/tmpdir.js +36 -0
- package/dist/esm/review/execution/sandbox/types.d.ts +31 -0
- package/dist/esm/review/execution/sandbox/types.js +6 -0
- package/dist/esm/review/execution/supervisor.js +44 -5
- package/dist/esm/review/execution/types.d.ts +2 -0
- package/dist/esm/worktrees/owner-dirt.d.ts +40 -0
- package/dist/esm/worktrees/owner-dirt.js +104 -0
- package/package.json +13 -12
|
@@ -15,6 +15,7 @@ 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";
|
|
20
21
|
import { buildRegistryRepoRootMap, classifyOrphanCandidate, discoverOrphanScanCandidates, resolveRepoRootFromGitPointer, } from "#worktrees/orphan-scan.js";
|
|
@@ -182,6 +183,13 @@ export function decideMergeCleanup(input) {
|
|
|
182
183
|
throw new Error(`wp worktree merge-cleanup refused: ${input.targetPath} is locked`);
|
|
183
184
|
}
|
|
184
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
|
+
}
|
|
185
193
|
throw new Error(`wp worktree merge-cleanup refused: ${input.targetPath} has uncommitted changes`);
|
|
186
194
|
}
|
|
187
195
|
if (input.repoDirty) {
|
|
@@ -561,6 +569,12 @@ function isDirty(path) {
|
|
|
561
569
|
return true;
|
|
562
570
|
return String(status.stdout ?? "").trim().length > 0;
|
|
563
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
|
+
}
|
|
564
578
|
function currentBranch(repoRoot) {
|
|
565
579
|
const result = spawnSync("git", ["branch", "--show-current"], {
|
|
566
580
|
cwd: repoRoot,
|
|
@@ -691,6 +705,14 @@ async function handleMergeCleanup(nameOrPath, opts) {
|
|
|
691
705
|
const scopedRegistryEntries = findRegistryCandidatesByPath(resolved)
|
|
692
706
|
.filter((candidate) => classifyMembership(candidate.entry.repoRoot, mergeAuthKey) === "same")
|
|
693
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);
|
|
694
716
|
const plan = decideMergeCleanup({
|
|
695
717
|
repoRoot,
|
|
696
718
|
targetPath: resolved,
|
|
@@ -699,8 +721,10 @@ async function handleMergeCleanup(nameOrPath, opts) {
|
|
|
699
721
|
registryEntries: scopedRegistryEntries,
|
|
700
722
|
currentBranch: currentBranch(repoRoot),
|
|
701
723
|
repoDirty: isDirty(repoRoot),
|
|
702
|
-
targetDirty
|
|
724
|
+
targetDirty,
|
|
703
725
|
stashPrimary: opts.stashPrimary === true,
|
|
726
|
+
...(targetPorcelainZ !== undefined ? { targetPorcelainZ } : {}),
|
|
727
|
+
...(targetBlueprintSlug !== undefined ? { targetBlueprintSlug } : {}),
|
|
704
728
|
});
|
|
705
729
|
if (opts.dryRun) {
|
|
706
730
|
console.log("[dry-run] Would run merge cleanup:");
|
package/dist/esm/cli/utils.d.ts
CHANGED
|
@@ -15,7 +15,14 @@ interface GetProjectRootOptions {
|
|
|
15
15
|
}
|
|
16
16
|
/**
|
|
17
17
|
* Walks upward from startDir looking for any marker in
|
|
18
|
-
* `PROJECT_ROOT_MARKERS` (priority order).
|
|
18
|
+
* `PROJECT_ROOT_MARKERS` (priority order). The walk is clamped at the
|
|
19
|
+
* enclosing git repository boundary: a directory containing `.git` (dir for
|
|
20
|
+
* a normal checkout, file for a linked worktree or submodule) is itself a
|
|
21
|
+
* project root when no marker matched at or below it. Without the clamp, a
|
|
22
|
+
* markerless git repo silently resolves to whatever ancestor happens to
|
|
23
|
+
* carry a `package.json` (e.g. editor-tooling junk in `$HOME`) and commands
|
|
24
|
+
* like `wp blueprint new` write outside the repo. Throws if neither a
|
|
25
|
+
* marker nor a git boundary is found.
|
|
19
26
|
*/
|
|
20
27
|
export declare function findProjectRoot(startDir: string): string;
|
|
21
28
|
export declare function getProjectRoot(options?: GetProjectRootOptions): string;
|
package/dist/esm/cli/utils.js
CHANGED
|
@@ -35,7 +35,14 @@ function findMarker(rootDir) {
|
|
|
35
35
|
}
|
|
36
36
|
/**
|
|
37
37
|
* Walks upward from startDir looking for any marker in
|
|
38
|
-
* `PROJECT_ROOT_MARKERS` (priority order).
|
|
38
|
+
* `PROJECT_ROOT_MARKERS` (priority order). The walk is clamped at the
|
|
39
|
+
* enclosing git repository boundary: a directory containing `.git` (dir for
|
|
40
|
+
* a normal checkout, file for a linked worktree or submodule) is itself a
|
|
41
|
+
* project root when no marker matched at or below it. Without the clamp, a
|
|
42
|
+
* markerless git repo silently resolves to whatever ancestor happens to
|
|
43
|
+
* carry a `package.json` (e.g. editor-tooling junk in `$HOME`) and commands
|
|
44
|
+
* like `wp blueprint new` write outside the repo. Throws if neither a
|
|
45
|
+
* marker nor a git boundary is found.
|
|
39
46
|
*/
|
|
40
47
|
export function findProjectRoot(startDir) {
|
|
41
48
|
let current = path.resolve(startDir);
|
|
@@ -43,9 +50,12 @@ export function findProjectRoot(startDir) {
|
|
|
43
50
|
if (findMarker(current)) {
|
|
44
51
|
return current;
|
|
45
52
|
}
|
|
53
|
+
if (existsSync(path.join(current, ".git"))) {
|
|
54
|
+
return current;
|
|
55
|
+
}
|
|
46
56
|
const parent = path.dirname(current);
|
|
47
57
|
if (parent === current) {
|
|
48
|
-
throw new Error(`Could not find project root (looked for ${PROJECT_ROOT_MARKERS.join(", ")}). Started from: ${startDir}`);
|
|
58
|
+
throw new Error(`Could not find project root (looked for ${PROJECT_ROOT_MARKERS.join(", ")} or a .git boundary). Started from: ${startDir}`);
|
|
49
59
|
}
|
|
50
60
|
current = parent;
|
|
51
61
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { parse as parseYaml } from "yaml";
|
|
2
2
|
import { getContent, getFilePath } from "#hooks/shared/types";
|
|
3
|
+
import { buildTaskHeadingRegex, buildWrongDepthTaskHeadingRegex } from "#markdown/task-heading";
|
|
3
4
|
import { getNonCanonicalPlanningPathViolation, isBlueprintPath, isCanonicalBlueprintDocumentPath, } from "./path-contract.js";
|
|
4
5
|
import { createSkipResult } from "./skip-result.js";
|
|
5
6
|
// Keep aligned with webpresso/blueprint planStatusSchema + plan type enum.
|
|
@@ -53,10 +54,10 @@ export function collectFieldViolations(data) {
|
|
|
53
54
|
return violations;
|
|
54
55
|
}
|
|
55
56
|
export function countTaskHeadings(content) {
|
|
56
|
-
return content.match(
|
|
57
|
+
return content.match(buildTaskHeadingRegex("gm"))?.length ?? 0;
|
|
57
58
|
}
|
|
58
59
|
export function detectWrongTaskFormat(content) {
|
|
59
|
-
return content.match(
|
|
60
|
+
return content.match(buildWrongDepthTaskHeadingRegex("gm"))?.length ?? 0;
|
|
60
61
|
}
|
|
61
62
|
export function validatePlanFrontmatter(input) {
|
|
62
63
|
const filePath = getFilePath(input);
|
|
@@ -4,6 +4,7 @@ import { z } from "zod";
|
|
|
4
4
|
import matter from "gray-matter";
|
|
5
5
|
import { parseBlueprint } from "#core/parser";
|
|
6
6
|
import { taskIdSchema } from "#core/schema";
|
|
7
|
+
import { formatTaskHeading } from "#markdown/task-heading";
|
|
7
8
|
import { evaluateApprovalGate, formatApprovalGateRequirement } from "#lifecycle/audit";
|
|
8
9
|
import { canonicalizeBlueprint } from "#lifecycle/canonicalize.js";
|
|
9
10
|
import { BlueprintCreationService } from "#service/BlueprintCreationService";
|
|
@@ -290,7 +291,7 @@ function renderBlueprintMarkdownFromDocument(slug, document) {
|
|
|
290
291
|
"",
|
|
291
292
|
];
|
|
292
293
|
const taskBlocks = document.tasks.flatMap((task) => [
|
|
293
|
-
|
|
294
|
+
formatTaskHeading(task.id, task.title),
|
|
294
295
|
"",
|
|
295
296
|
`**Status:** ${task.status}`,
|
|
296
297
|
...(task.wave ? [`**Wave:** ${task.wave}`] : []),
|
|
@@ -11,23 +11,13 @@ import { toStr } from "#mcp/blueprint/_shared/payload";
|
|
|
11
11
|
import { findBlueprintDir, resolveToolProject } from "#mcp/blueprint/_shared/project";
|
|
12
12
|
import { resolveSyncAdapter, runPlatformMutationSync } from "#mcp/blueprint/_shared/sync";
|
|
13
13
|
import { writeFileAtomic } from "#shared-utils/write-json-file.js";
|
|
14
|
-
import {
|
|
14
|
+
import { buildTaskHeaderRegexForId } from "#markdown/task-heading";
|
|
15
15
|
import { withMarkdownWriteLock } from "#db/paths.js";
|
|
16
16
|
import { resolveBlueprintRoot } from "#utils/blueprint-root.js";
|
|
17
17
|
const ALL_STATES = ["draft", "planned", "in-progress", "parked", "archived", "completed"];
|
|
18
|
-
/**
|
|
19
|
-
* Matches the canonical task-heading grammar `parser.ts`'s `extractTasks`
|
|
20
|
-
* uses (`^####\s+(?:\[lane\]\s+)?Task\s+<id>:`), not a looser ad hoc pattern.
|
|
21
|
-
* The id is anchored on a trailing colon so "1.1" cannot match "1.10", and
|
|
22
|
-
* an optional `[lane]` prefix is accepted the same way the canonical parser
|
|
23
|
-
* does.
|
|
24
|
-
*/
|
|
25
|
-
function buildTaskHeadingPattern(taskId) {
|
|
26
|
-
return new RegExp(`^####\\s+(?:\\[[^\\]]+\\]\\s+)?Task\\s+${escapeRegex(taskId)}:`, "u");
|
|
27
|
-
}
|
|
28
18
|
function findTaskStatusLine(markdown, taskId) {
|
|
29
19
|
const lines = markdown.split("\n");
|
|
30
|
-
const headingPattern =
|
|
20
|
+
const headingPattern = buildTaskHeaderRegexForId(taskId);
|
|
31
21
|
let inBlock = false;
|
|
32
22
|
for (let i = 0; i < lines.length; i++) {
|
|
33
23
|
const line = lines[i] ?? "";
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { executeAllowedWpCommand } from "#trust/command-runner.js";
|
|
1
|
+
import { executeAllowedWpCommand, type AllowedWpCommandResult } from "#trust/command-runner.js";
|
|
2
2
|
import type { ReviewTargetCheckout } from "#review/execution/review-checkout.js";
|
|
3
|
+
import type { SandboxAvailability } from "#review/execution/sandbox/types.js";
|
|
3
4
|
export interface ReviewGateVerificationCommand {
|
|
4
5
|
readonly command: string;
|
|
5
6
|
readonly outcome: "passed" | "failed" | "timed-out" | "deferred";
|
|
@@ -7,12 +8,29 @@ export interface ReviewGateVerificationCommand {
|
|
|
7
8
|
readonly exitCode: number | null;
|
|
8
9
|
readonly signal?: string;
|
|
9
10
|
}
|
|
11
|
+
/**
|
|
12
|
+
* How the gate commands were isolated. `srt` = OS-sandboxed; `disabled-by-operator`
|
|
13
|
+
* = an explicit `WP_REVIEW_UNSANDBOXED_GATES=1` override ran them unsandboxed;
|
|
14
|
+
* `unavailable` = no backend and no override, so no gate ran (fail-closed).
|
|
15
|
+
*/
|
|
16
|
+
export type ReviewSandboxMode = "srt" | "disabled-by-operator" | "unavailable";
|
|
10
17
|
export interface ReviewGateVerification {
|
|
11
18
|
readonly status: "passed" | "failed";
|
|
12
19
|
readonly commands: readonly ReviewGateVerificationCommand[];
|
|
13
20
|
readonly failureCode?: string;
|
|
14
21
|
readonly logPath?: string;
|
|
22
|
+
readonly sandbox?: ReviewSandboxMode;
|
|
23
|
+
readonly sandboxReason?: string;
|
|
15
24
|
}
|
|
25
|
+
/** Runs one gate command in the review checkout and returns its raw result. */
|
|
26
|
+
export type RunSandboxedGate = (args: {
|
|
27
|
+
readonly command: string;
|
|
28
|
+
readonly executionRoot: string;
|
|
29
|
+
readonly logRoot: string;
|
|
30
|
+
readonly runTmpDir: string;
|
|
31
|
+
readonly projectRoot: string;
|
|
32
|
+
readonly signal?: AbortSignal;
|
|
33
|
+
}) => Promise<AllowedWpCommandResult>;
|
|
16
34
|
export interface VerifyDeliveryGatesInput {
|
|
17
35
|
readonly purpose: "plan" | "delivery";
|
|
18
36
|
readonly blueprintContent: string;
|
|
@@ -20,8 +38,21 @@ export interface VerifyDeliveryGatesInput {
|
|
|
20
38
|
readonly gateId: string;
|
|
21
39
|
readonly checkout: ReviewTargetCheckout;
|
|
22
40
|
readonly signal?: AbortSignal;
|
|
41
|
+
/** Legacy/override execution path (unsandboxed). */
|
|
23
42
|
readonly executeCommand?: typeof executeAllowedWpCommand;
|
|
43
|
+
/** Test seam: resolve the sandbox backend. */
|
|
44
|
+
readonly resolveAvailability?: () => Promise<SandboxAvailability>;
|
|
45
|
+
/** Test seam: run a single sandboxed gate. */
|
|
46
|
+
readonly runSandboxedGate?: RunSandboxedGate;
|
|
24
47
|
}
|
|
25
|
-
export declare function verifyDeliveryPromotionGates(input: VerifyDeliveryGatesInput): ReviewGateVerification | undefined
|
|
48
|
+
export declare function verifyDeliveryPromotionGates(input: VerifyDeliveryGatesInput): Promise<ReviewGateVerification | undefined>;
|
|
49
|
+
/**
|
|
50
|
+
* Builds the real {@link RunSandboxedGate}. `cleanupSandbox` is a test-only
|
|
51
|
+
* seam (defaults to the real {@link cleanupSandboxCommand}) so unit tests can
|
|
52
|
+
* force the fail-closed "passing gate, leaky sandbox" path without a real srt
|
|
53
|
+
* backend; every non-test caller gets the real cleanup and byte-identical
|
|
54
|
+
* behavior to before this seam existed.
|
|
55
|
+
*/
|
|
56
|
+
export declare function createRunSandboxedGate(cleanupSandbox?: () => Promise<void>): RunSandboxedGate;
|
|
26
57
|
export declare function verificationPromptSummary(verification: ReviewGateVerification | undefined): string | undefined;
|
|
27
58
|
export declare function verificationArtifactSection(verification: ReviewGateVerification | undefined): string;
|
|
@@ -1,8 +1,14 @@
|
|
|
1
|
+
import { mkdtempSync, realpathSync, rmSync } from "node:fs";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
1
3
|
import path from "node:path";
|
|
2
|
-
import { executeAllowedWpCommand, relativeCommandLogPath, } from "#trust/command-runner.js";
|
|
4
|
+
import { executeAllowedWpCommand, rejectedInvocationResult, relativeCommandLogPath, resolveInvocationFromCommand, runResolvedInvocationAsync, } from "#trust/command-runner.js";
|
|
3
5
|
import { parseTrustDossier } from "#trust/dossier.js";
|
|
4
6
|
import { parseAllowedWpCommand } from "#trust/gates.js";
|
|
5
|
-
|
|
7
|
+
import { buildSandboxedGateEnv } from "#review/execution/sandbox/env.js";
|
|
8
|
+
import { cleanupSandboxCommand, wrapGateForSandbox } from "#review/execution/sandbox/adapter.js";
|
|
9
|
+
import { resolveSandboxAvailability } from "#review/execution/sandbox/probe.js";
|
|
10
|
+
const UNSANDBOXED_OVERRIDE_ENV = "WP_REVIEW_UNSANDBOXED_GATES";
|
|
11
|
+
export async function verifyDeliveryPromotionGates(input) {
|
|
6
12
|
if (input.purpose !== "delivery")
|
|
7
13
|
return undefined;
|
|
8
14
|
const parsed = parseTrustDossier(input.blueprintContent);
|
|
@@ -12,48 +18,82 @@ export function verifyDeliveryPromotionGates(input) {
|
|
|
12
18
|
parsed.violations[0]?.message === "missing Trust Dossier section") {
|
|
13
19
|
return undefined;
|
|
14
20
|
}
|
|
21
|
+
return { status: "failed", commands: [], failureCode: "malformed-trust-dossier" };
|
|
22
|
+
}
|
|
23
|
+
const gates = parsed.dossier?.gates ?? [];
|
|
24
|
+
if (gates.length === 0)
|
|
25
|
+
return undefined;
|
|
26
|
+
const overrideUnsandboxed = process.env[UNSANDBOXED_OVERRIDE_ENV] === "1";
|
|
27
|
+
const availability = await (input.resolveAvailability?.() ?? resolveSandboxAvailability());
|
|
28
|
+
if (availability.kind === "none" && !overrideUnsandboxed) {
|
|
15
29
|
return {
|
|
16
30
|
status: "failed",
|
|
17
31
|
commands: [],
|
|
18
|
-
failureCode: "
|
|
32
|
+
failureCode: "sandbox-unavailable",
|
|
33
|
+
sandbox: "unavailable",
|
|
34
|
+
sandboxReason: availability.reason,
|
|
19
35
|
};
|
|
20
36
|
}
|
|
21
|
-
const
|
|
22
|
-
if (gates.length === 0)
|
|
23
|
-
return undefined;
|
|
24
|
-
const commands = [];
|
|
25
|
-
const executeCommand = input.executeCommand ?? executeAllowedWpCommand;
|
|
37
|
+
const sandbox = overrideUnsandboxed ? "disabled-by-operator" : "srt";
|
|
26
38
|
const logRoot = path.join(input.projectRoot, ".webpresso", "logs", "delivery-verification", input.gateId);
|
|
27
|
-
|
|
39
|
+
const runTmpDir = sandbox === "srt" ? mkdtempSync(path.join(tmpdir(), "wp-review-run-")) : undefined;
|
|
40
|
+
try {
|
|
41
|
+
return await runGateLoop(input, { gates, sandbox, logRoot, runTmpDir });
|
|
42
|
+
}
|
|
43
|
+
finally {
|
|
44
|
+
if (runTmpDir !== undefined)
|
|
45
|
+
rmSync(runTmpDir, { recursive: true, force: true });
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
async function runGateLoop(input, ctx) {
|
|
49
|
+
const commands = [];
|
|
50
|
+
const runSandboxedGate = input.runSandboxedGate ?? defaultRunSandboxedGate;
|
|
51
|
+
for (const gate of ctx.gates) {
|
|
28
52
|
if (input.signal?.aborted) {
|
|
29
53
|
return {
|
|
30
54
|
status: "failed",
|
|
31
55
|
commands,
|
|
32
56
|
failureCode: "verification-cancelled",
|
|
57
|
+
sandbox: ctx.sandbox,
|
|
33
58
|
};
|
|
34
59
|
}
|
|
35
60
|
if (isApprovalDependentLifecycleGate(gate.command)) {
|
|
36
|
-
commands.push({
|
|
37
|
-
command: gate.command,
|
|
38
|
-
outcome: "deferred",
|
|
39
|
-
durationMs: 0,
|
|
40
|
-
exitCode: null,
|
|
41
|
-
});
|
|
61
|
+
commands.push({ command: gate.command, outcome: "deferred", durationMs: 0, exitCode: null });
|
|
42
62
|
continue;
|
|
43
63
|
}
|
|
44
|
-
const result =
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
64
|
+
const result = ctx.sandbox === "srt"
|
|
65
|
+
? await runSandboxedGate({
|
|
66
|
+
command: gate.command,
|
|
67
|
+
executionRoot: input.checkout.cwd,
|
|
68
|
+
logRoot: ctx.logRoot,
|
|
69
|
+
runTmpDir: ctx.runTmpDir ?? "",
|
|
70
|
+
projectRoot: input.projectRoot,
|
|
71
|
+
...(input.signal ? { signal: input.signal } : {}),
|
|
72
|
+
})
|
|
73
|
+
: (input.executeCommand ?? executeAllowedWpCommand)({
|
|
74
|
+
command: gate.command,
|
|
75
|
+
executionRoot: input.checkout.cwd,
|
|
76
|
+
logRoot: ctx.logRoot,
|
|
77
|
+
env: { CI: "1", NO_COLOR: "1", WP_FORCE_SOURCE: "1" },
|
|
78
|
+
});
|
|
50
79
|
commands.push(publicCommandResult(result));
|
|
80
|
+
// A cancellation DURING a gate (abort mid-execution) must report as
|
|
81
|
+
// cancelled, not a generic command failure.
|
|
82
|
+
if (input.signal?.aborted || result.failureCode === "command-cancelled") {
|
|
83
|
+
return {
|
|
84
|
+
status: "failed",
|
|
85
|
+
commands,
|
|
86
|
+
failureCode: "verification-cancelled",
|
|
87
|
+
sandbox: ctx.sandbox,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
51
90
|
if (result.outcome !== "passed") {
|
|
52
91
|
return {
|
|
53
92
|
status: "failed",
|
|
54
93
|
commands,
|
|
55
94
|
failureCode: result.failureCode ?? "verification-command-failed",
|
|
56
95
|
logPath: relativeCommandLogPath(input.projectRoot, result.logPath),
|
|
96
|
+
sandbox: ctx.sandbox,
|
|
57
97
|
};
|
|
58
98
|
}
|
|
59
99
|
const changes = input.checkout.changes();
|
|
@@ -63,12 +103,76 @@ export function verifyDeliveryPromotionGates(input) {
|
|
|
63
103
|
commands,
|
|
64
104
|
failureCode: "verification-worktree-contaminated",
|
|
65
105
|
logPath: relativeCommandLogPath(input.projectRoot, result.logPath),
|
|
106
|
+
sandbox: ctx.sandbox,
|
|
66
107
|
};
|
|
67
108
|
}
|
|
68
109
|
}
|
|
110
|
+
return { status: "passed", commands, sandbox: ctx.sandbox };
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Builds the real {@link RunSandboxedGate}. `cleanupSandbox` is a test-only
|
|
114
|
+
* seam (defaults to the real {@link cleanupSandboxCommand}) so unit tests can
|
|
115
|
+
* force the fail-closed "passing gate, leaky sandbox" path without a real srt
|
|
116
|
+
* backend; every non-test caller gets the real cleanup and byte-identical
|
|
117
|
+
* behavior to before this seam existed.
|
|
118
|
+
*/
|
|
119
|
+
export function createRunSandboxedGate(cleanupSandbox = cleanupSandboxCommand) {
|
|
120
|
+
return async (args) => {
|
|
121
|
+
const startedAt = Date.now();
|
|
122
|
+
const env = buildSandboxedGateEnv(args.runTmpDir);
|
|
123
|
+
let invocation;
|
|
124
|
+
try {
|
|
125
|
+
invocation = resolveInvocationFromCommand(args.executionRoot, args.command, env, {
|
|
126
|
+
replaceProcessEnv: true,
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
catch (error) {
|
|
130
|
+
return rejectedInvocationResult(args.command, args.logRoot, startedAt, error);
|
|
131
|
+
}
|
|
132
|
+
let result;
|
|
133
|
+
try {
|
|
134
|
+
// The wrap is INSIDE the try so a wrap failure (unresolvable srt, empty
|
|
135
|
+
// argv) still produces a persisted failed verification instead of escaping.
|
|
136
|
+
const wrapped = await wrapGateForSandbox(invocation, {
|
|
137
|
+
checkoutRoot: realpathSync(args.executionRoot),
|
|
138
|
+
runTmpDir: realpathSync(args.runTmpDir),
|
|
139
|
+
projectRoot: args.projectRoot,
|
|
140
|
+
});
|
|
141
|
+
result = await runResolvedInvocationAsync({ command: wrapped.command, args: [...wrapped.args], env: wrapped.env }, {
|
|
142
|
+
command: args.command,
|
|
143
|
+
executionRoot: args.executionRoot,
|
|
144
|
+
logRoot: args.logRoot,
|
|
145
|
+
startedAt,
|
|
146
|
+
...(args.signal ? { signal: args.signal } : {}),
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
catch (error) {
|
|
150
|
+
result = rejectedInvocationResult(args.command, args.logRoot, startedAt, error);
|
|
151
|
+
}
|
|
152
|
+
// Sandbox cleanup is part of the gate's integrity: an unbalanced srt counter
|
|
153
|
+
// or leftover mounts must NOT be reported as a clean pass. If cleanup fails
|
|
154
|
+
// after an otherwise-passing gate, convert the result to a failed
|
|
155
|
+
// verification and keep the gate output as secondary evidence. A gate that
|
|
156
|
+
// already failed keeps its more-informative primary failure.
|
|
157
|
+
try {
|
|
158
|
+
await cleanupSandbox();
|
|
159
|
+
}
|
|
160
|
+
catch (error) {
|
|
161
|
+
if (result.outcome === "passed")
|
|
162
|
+
return sandboxCleanupFailedResult(result, error);
|
|
163
|
+
}
|
|
164
|
+
return result;
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
const defaultRunSandboxedGate = createRunSandboxedGate();
|
|
168
|
+
function sandboxCleanupFailedResult(gateResult, error) {
|
|
169
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
69
170
|
return {
|
|
70
|
-
|
|
71
|
-
|
|
171
|
+
...gateResult,
|
|
172
|
+
outcome: "failed",
|
|
173
|
+
failureCode: "sandbox-cleanup-failed",
|
|
174
|
+
errorMessage: `sandbox cleanup did not complete after a passing gate: ${message}`,
|
|
175
|
+
stderr: `${gateResult.stderr}\n[sandbox-cleanup-failed] ${message}`.trim(),
|
|
72
176
|
};
|
|
73
177
|
}
|
|
74
178
|
export function verificationPromptSummary(verification) {
|
|
@@ -77,7 +181,7 @@ export function verificationPromptSummary(verification) {
|
|
|
77
181
|
if (verification.status !== "passed")
|
|
78
182
|
return undefined;
|
|
79
183
|
return [
|
|
80
|
-
|
|
184
|
+
`Host verification before outside review: passed (sandbox: ${verification.sandbox ?? "unknown"}).`,
|
|
81
185
|
...verification.commands.map((command, index) => `${index + 1}. ${command.command}: ${command.outcome === "deferred" ? "deferred until review publication" : "passed"}`),
|
|
82
186
|
].join("\n");
|
|
83
187
|
}
|
|
@@ -97,6 +201,8 @@ export function verificationArtifactSection(verification) {
|
|
|
97
201
|
"## Host Verification",
|
|
98
202
|
"",
|
|
99
203
|
`Status: ${verification.status}`,
|
|
204
|
+
`Sandbox: ${verification.sandbox ?? "unknown"}`,
|
|
205
|
+
...(verification.sandboxReason ? [`Sandbox detail: ${verification.sandboxReason}`] : []),
|
|
100
206
|
...verification.commands.map((command, index) => `${index + 1}. ${command.command} -> ${command.outcome} (${command.durationMs}ms, exit ${command.exitCode ?? "null"})`),
|
|
101
207
|
...(verification.failureCode ? [`Failure code: ${verification.failureCode}`] : []),
|
|
102
208
|
...(verification.logPath ? [`Log: ${verification.logPath}`] : []),
|
|
@@ -4,4 +4,11 @@ export interface ReviewTargetCheckout {
|
|
|
4
4
|
readonly changes: () => readonly string[];
|
|
5
5
|
readonly cleanup: () => void;
|
|
6
6
|
}
|
|
7
|
+
declare function statusEntryPath(line: string): string;
|
|
7
8
|
export declare function createReviewTargetCheckout(repositoryRoot: string, targetSha: string): ReviewTargetCheckout;
|
|
9
|
+
export declare function assertDependencyContainment(checkoutRoot: string): void;
|
|
10
|
+
/** Test-only surface for porcelain path parsing. */
|
|
11
|
+
export declare const reviewCheckoutInternals: {
|
|
12
|
+
statusEntryPath: typeof statusEntryPath;
|
|
13
|
+
};
|
|
14
|
+
export {};
|