@webpresso/agent-kit 3.3.1 → 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 +15 -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/mutations.js +2 -3
- 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/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 +1 -0
- package/dist/esm/review/execution/review-checkout.js +386 -23
- 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/package.json +13 -12
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Classify a GitHub Actions Release run into operator-facing progress phases.
|
|
3
|
+
*
|
|
4
|
+
* After Version Packages merge, agents must not sit on silent `npm view` polls:
|
|
5
|
+
* merge, native matrix, and npm publish are separate states. This pure helper
|
|
6
|
+
* maps job list snapshots (from `gh run view --json jobs`) into those states.
|
|
7
|
+
*/
|
|
8
|
+
const NATIVE_JOB_PREFIX = "Session memory native";
|
|
9
|
+
function isNativeJob(name) {
|
|
10
|
+
return name.startsWith(NATIVE_JOB_PREFIX);
|
|
11
|
+
}
|
|
12
|
+
function isFailure(conclusion) {
|
|
13
|
+
return conclusion === "failure" || conclusion === "cancelled" || conclusion === "timed_out";
|
|
14
|
+
}
|
|
15
|
+
function isSuccess(conclusion) {
|
|
16
|
+
return conclusion === "success" || conclusion === "skipped";
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Classify release workflow jobs into a single progress phase for agent/ops reporting.
|
|
20
|
+
*/
|
|
21
|
+
export function classifyReleaseRunProgress(jobs) {
|
|
22
|
+
const nativeJobs = jobs.filter((job) => isNativeJob(job.name));
|
|
23
|
+
const detect = jobs.find((job) => job.name === "Detect publishable versions");
|
|
24
|
+
const release = jobs.find((job) => job.name === "Release");
|
|
25
|
+
const nativeDone = nativeJobs.filter((job) => job.status === "completed" && isSuccess(job.conclusion)).length;
|
|
26
|
+
const nativeFailed = nativeJobs.filter((job) => job.status === "completed" && isFailure(job.conclusion)).length;
|
|
27
|
+
const nativeInProgress = nativeJobs.filter((job) => job.status === "in_progress").length;
|
|
28
|
+
const native = {
|
|
29
|
+
done: nativeDone,
|
|
30
|
+
total: nativeJobs.length,
|
|
31
|
+
failed: nativeFailed,
|
|
32
|
+
inProgress: nativeInProgress,
|
|
33
|
+
};
|
|
34
|
+
const publishRunning = release?.status === "in_progress";
|
|
35
|
+
const publishDone = release?.status === "completed" && isSuccess(release.conclusion);
|
|
36
|
+
const publishFailed = release?.status === "completed" && isFailure(release.conclusion);
|
|
37
|
+
if (nativeFailed > 0 || publishFailed) {
|
|
38
|
+
return {
|
|
39
|
+
phase: "failed",
|
|
40
|
+
detail: nativeFailed > 0
|
|
41
|
+
? `Native matrix failed (${nativeFailed} cell(s))`
|
|
42
|
+
: `Release/publish job failed (${release?.conclusion ?? "unknown"})`,
|
|
43
|
+
native,
|
|
44
|
+
publishRunning,
|
|
45
|
+
publishDone,
|
|
46
|
+
publishFailed,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
if (publishDone) {
|
|
50
|
+
return {
|
|
51
|
+
phase: "done",
|
|
52
|
+
detail: "Release job completed successfully (check npm for version)",
|
|
53
|
+
native,
|
|
54
|
+
publishRunning,
|
|
55
|
+
publishDone,
|
|
56
|
+
publishFailed,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
if (publishRunning || (native.total > 0 && nativeDone === native.total && release)) {
|
|
60
|
+
return {
|
|
61
|
+
phase: "publish",
|
|
62
|
+
detail: publishRunning
|
|
63
|
+
? "Native matrix complete; Release/publish job in progress"
|
|
64
|
+
: "Native matrix complete; waiting for Release job",
|
|
65
|
+
native,
|
|
66
|
+
publishRunning,
|
|
67
|
+
publishDone,
|
|
68
|
+
publishFailed,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
if (native.total > 0 && (nativeInProgress > 0 || nativeDone < native.total)) {
|
|
72
|
+
return {
|
|
73
|
+
phase: "native-matrix",
|
|
74
|
+
detail: `Building session-memory natives ${nativeDone}/${native.total} complete` +
|
|
75
|
+
(nativeInProgress > 0 ? ` (${nativeInProgress} in progress)` : ""),
|
|
76
|
+
native,
|
|
77
|
+
publishRunning,
|
|
78
|
+
publishDone,
|
|
79
|
+
publishFailed,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
if (detect && detect.status !== "completed") {
|
|
83
|
+
return {
|
|
84
|
+
phase: "detect",
|
|
85
|
+
detail: "Detecting whether this run will publish",
|
|
86
|
+
native,
|
|
87
|
+
publishRunning,
|
|
88
|
+
publishDone,
|
|
89
|
+
publishFailed,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
return {
|
|
93
|
+
phase: "detect",
|
|
94
|
+
detail: "Release run starting",
|
|
95
|
+
native,
|
|
96
|
+
publishRunning,
|
|
97
|
+
publishDone,
|
|
98
|
+
publishFailed,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
/** Format one line for agent/ops logs (merge vs matrix vs publish). */
|
|
102
|
+
export function formatReleaseProgressLine(progress) {
|
|
103
|
+
return `release-progress phase=${progress.phase} native=${progress.native.done}/${progress.native.total} ${progress.detail}`;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Parse `gh run view <id> --json jobs` (or full run object with `jobs`) into snapshots.
|
|
107
|
+
* Production CLI and tests share this path so classification is never reimplemented.
|
|
108
|
+
*/
|
|
109
|
+
export function jobsFromGhRunViewJson(payload) {
|
|
110
|
+
if (payload == null || typeof payload !== "object") {
|
|
111
|
+
throw new Error("gh run view JSON must be an object with a jobs array");
|
|
112
|
+
}
|
|
113
|
+
const record = payload;
|
|
114
|
+
const jobsRaw = Array.isArray(payload)
|
|
115
|
+
? payload
|
|
116
|
+
: Array.isArray(record.jobs)
|
|
117
|
+
? record.jobs
|
|
118
|
+
: null;
|
|
119
|
+
if (!jobsRaw) {
|
|
120
|
+
throw new Error("gh run view JSON must include a jobs array");
|
|
121
|
+
}
|
|
122
|
+
return jobsRaw.map((entry, index) => {
|
|
123
|
+
if (entry == null || typeof entry !== "object") {
|
|
124
|
+
throw new Error(`jobs[${index}] must be an object`);
|
|
125
|
+
}
|
|
126
|
+
const job = entry;
|
|
127
|
+
const name = typeof job.name === "string" ? job.name : "";
|
|
128
|
+
const status = typeof job.status === "string" ? job.status : "";
|
|
129
|
+
if (!name || !status) {
|
|
130
|
+
throw new Error(`jobs[${index}] requires string name and status`);
|
|
131
|
+
}
|
|
132
|
+
const conclusion = job.conclusion === null || job.conclusion === undefined
|
|
133
|
+
? null
|
|
134
|
+
: typeof job.conclusion === "string"
|
|
135
|
+
? job.conclusion
|
|
136
|
+
: null;
|
|
137
|
+
return { name, status, conclusion };
|
|
138
|
+
});
|
|
139
|
+
}
|
package/dist/esm/cli/cli.d.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* Lazy-loads subcommand modules based on the first argv to keep startup
|
|
6
6
|
* cheap. Modeled on apps/cli-wp/src/cli.ts.
|
|
7
7
|
*/
|
|
8
|
-
declare const SUPPORTED_COMMANDS: readonly ["blueprint", "browser", "dash", "dash-helper-start", "dash-helper-complete", "claude", "codex", "grok", "opencode", "yolo", "config", "secrets", "roadmap", "status", "review", "sync", "audit", "qa", "ultragoal", "compile", "rule", "skill", "skills", "docs", "setup", "init", "dev", "deploy", "preview", "cleanup", "migrate", "doctor", "err", "test", "e2e", "ci", "ci-preflight", "public:readiness", "typecheck", "lint", "format", "rust-check", "logs", "session-id", "session-info", "session", "tech-debt", "pr", "fleet", "worktree", "mcp", "hook", "hooks", "self-install-guard", "optional-tool-refresh", "gain", "bench", "install", "add", "remove", "update", "exec", "run"];
|
|
8
|
+
declare const SUPPORTED_COMMANDS: readonly ["blueprint", "browser", "dash", "dash-helper-start", "dash-helper-complete", "claude", "codex", "grok", "opencode", "yolo", "config", "secrets", "roadmap", "status", "review", "sync", "audit", "qa", "ultragoal", "compile", "rule", "skill", "skills", "docs", "setup", "init", "dev", "deploy", "preview", "cleanup", "migrate", "doctor", "err", "test", "e2e", "ci", "ci-preflight", "release-progress", "public:readiness", "typecheck", "lint", "format", "rust-check", "logs", "session-id", "session-info", "session", "tech-debt", "pr", "fleet", "worktree", "mcp", "hook", "hooks", "self-install-guard", "optional-tool-refresh", "gain", "bench", "install", "add", "remove", "update", "exec", "run"];
|
|
9
9
|
export type SupportedCommand = (typeof SUPPORTED_COMMANDS)[number];
|
|
10
10
|
type RootHelpEntry = {
|
|
11
11
|
readonly command: string;
|
package/dist/esm/cli/cli.js
CHANGED
|
@@ -55,6 +55,7 @@ const SUPPORTED_COMMANDS = [
|
|
|
55
55
|
"e2e",
|
|
56
56
|
"ci",
|
|
57
57
|
"ci-preflight",
|
|
58
|
+
"release-progress",
|
|
58
59
|
"public:readiness",
|
|
59
60
|
"typecheck",
|
|
60
61
|
"lint",
|
|
@@ -118,6 +119,10 @@ const ROOT_HELP_CORE_ENTRIES = [
|
|
|
118
119
|
command: "ci-preflight",
|
|
119
120
|
description: "Run the locally-runnable required CI checks fail-fast (the pre-push gate)",
|
|
120
121
|
},
|
|
122
|
+
{
|
|
123
|
+
command: "release-progress",
|
|
124
|
+
description: "Report Release workflow phase (detect / native-matrix / publish) instead of silent npm view waits",
|
|
125
|
+
},
|
|
121
126
|
{
|
|
122
127
|
command: "public:readiness",
|
|
123
128
|
description: "Run the public package readiness gate through the portable wp surface",
|
|
@@ -375,6 +380,10 @@ const COMMAND_REGISTRARS = {
|
|
|
375
380
|
const { registerCiPreflightCommand } = await import("./commands/ci-preflight.js");
|
|
376
381
|
registerCiPreflightCommand(cli);
|
|
377
382
|
},
|
|
383
|
+
"release-progress": async (cli) => {
|
|
384
|
+
const { registerReleaseProgressCommand } = await import("./commands/release-progress.js");
|
|
385
|
+
registerReleaseProgressCommand(cli);
|
|
386
|
+
},
|
|
378
387
|
"public:readiness": async (cli) => {
|
|
379
388
|
const { registerPublicReadinessCommand } = await import("./commands/public-readiness.js");
|
|
380
389
|
registerPublicReadinessCommand(cli);
|
|
@@ -17,7 +17,7 @@ import { randomUUID } from "node:crypto";
|
|
|
17
17
|
import { tmpdir } from "node:os";
|
|
18
18
|
import path from "node:path";
|
|
19
19
|
import { parseBlueprint } from "#core/parser";
|
|
20
|
-
import {
|
|
20
|
+
import { buildTaskHeaderRegexForId } from "#markdown/task-heading";
|
|
21
21
|
import { openDb } from "#db/connection.js";
|
|
22
22
|
import { resolveBlueprintProjectionDbPath, withMarkdownWriteLock } from "#db/paths.js";
|
|
23
23
|
import { getBlueprintDocumentPaths } from "#utils/document-paths.js";
|
|
@@ -209,8 +209,7 @@ function upsertCompletedAt(content, isoDate) {
|
|
|
209
209
|
* Returns { lineIndex, currentStatus } or null if not found.
|
|
210
210
|
*/
|
|
211
211
|
function findTaskStatusLine(lines, taskId) {
|
|
212
|
-
const
|
|
213
|
-
const taskPattern = new RegExp(`^####\\s+(?:\\[[^\\]]+\\]\\s+)?Task\\s+${escapedId}[:\\s]`);
|
|
212
|
+
const taskPattern = buildTaskHeaderRegexForId(taskId);
|
|
214
213
|
let inBlock = false;
|
|
215
214
|
for (let i = 0; i < lines.length; i++) {
|
|
216
215
|
const line = lines[i] ?? "";
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `wp release-progress` — report Release workflow phase (detect / native-matrix / publish).
|
|
3
|
+
*
|
|
4
|
+
* Agents must not poll only `npm view` after Version Packages merge. This command
|
|
5
|
+
* loads the live GitHub Actions job list and classifies progress via
|
|
6
|
+
* {@link classifyReleaseRunProgress}.
|
|
7
|
+
*/
|
|
8
|
+
import type { CAC } from "cac";
|
|
9
|
+
import { type ReleaseJobSnapshot, type ReleaseProgress } from "#ci/release-progress.js";
|
|
10
|
+
export declare const RELEASE_PROGRESS_COMMAND_HELP: string;
|
|
11
|
+
export interface GhRunJobsFetcher {
|
|
12
|
+
(options: {
|
|
13
|
+
readonly runId: string;
|
|
14
|
+
readonly repo: string;
|
|
15
|
+
}): ReleaseJobSnapshot[];
|
|
16
|
+
}
|
|
17
|
+
export interface LatestReleaseRunResolver {
|
|
18
|
+
(options: {
|
|
19
|
+
readonly repo: string;
|
|
20
|
+
}): string;
|
|
21
|
+
}
|
|
22
|
+
/** Production fetcher: `gh run view --json jobs`. */
|
|
23
|
+
export declare function fetchReleaseJobsViaGh(options: {
|
|
24
|
+
readonly runId: string;
|
|
25
|
+
readonly repo: string;
|
|
26
|
+
}): ReleaseJobSnapshot[];
|
|
27
|
+
/** Production resolver: latest Release workflow run for the repo. */
|
|
28
|
+
export declare function resolveLatestReleaseRunIdViaGh(options: {
|
|
29
|
+
readonly repo: string;
|
|
30
|
+
}): string;
|
|
31
|
+
export interface RunReleaseProgressOptions {
|
|
32
|
+
readonly runId?: string | number;
|
|
33
|
+
readonly repo?: string;
|
|
34
|
+
readonly json?: boolean;
|
|
35
|
+
readonly fetchJobs?: GhRunJobsFetcher;
|
|
36
|
+
readonly resolveLatestRunId?: LatestReleaseRunResolver;
|
|
37
|
+
readonly stdout?: {
|
|
38
|
+
write(chunk: string): void;
|
|
39
|
+
};
|
|
40
|
+
readonly stderr?: {
|
|
41
|
+
write(chunk: string): void;
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Core command body — injectable for tests; production uses gh CLI defaults.
|
|
46
|
+
*/
|
|
47
|
+
export declare function runReleaseProgress(options?: RunReleaseProgressOptions): {
|
|
48
|
+
readonly exitCode: number;
|
|
49
|
+
readonly progress: ReleaseProgress;
|
|
50
|
+
readonly runId: string;
|
|
51
|
+
readonly line: string;
|
|
52
|
+
};
|
|
53
|
+
export declare function registerReleaseProgressCommand(cli: CAC): void;
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `wp release-progress` — report Release workflow phase (detect / native-matrix / publish).
|
|
3
|
+
*
|
|
4
|
+
* Agents must not poll only `npm view` after Version Packages merge. This command
|
|
5
|
+
* loads the live GitHub Actions job list and classifies progress via
|
|
6
|
+
* {@link classifyReleaseRunProgress}.
|
|
7
|
+
*/
|
|
8
|
+
import { spawnSync } from "node:child_process";
|
|
9
|
+
import { classifyReleaseRunProgress, formatReleaseProgressLine, jobsFromGhRunViewJson, } from "#ci/release-progress.js";
|
|
10
|
+
export const RELEASE_PROGRESS_COMMAND_HELP = [
|
|
11
|
+
"Report agent-kit Release workflow progress by phase.",
|
|
12
|
+
"",
|
|
13
|
+
"Phases: detect | native-matrix | publish | done | failed.",
|
|
14
|
+
"Use after merging Version Packages so long session-memory native builds are",
|
|
15
|
+
"not mistaken for a hung merge or silent npm wait.",
|
|
16
|
+
"",
|
|
17
|
+
"Examples:",
|
|
18
|
+
" wp release-progress",
|
|
19
|
+
" wp release-progress --run-id 30152651072",
|
|
20
|
+
" wp release-progress --repo webpresso/agent-kit --json",
|
|
21
|
+
].join("\n");
|
|
22
|
+
const DEFAULT_REPO = "webpresso/agent-kit";
|
|
23
|
+
const RELEASE_WORKFLOW = "release.yml";
|
|
24
|
+
function runGhJson(args) {
|
|
25
|
+
const result = spawnSync("gh", [...args], {
|
|
26
|
+
encoding: "utf8",
|
|
27
|
+
timeout: 60_000,
|
|
28
|
+
maxBuffer: 8 * 1024 * 1024,
|
|
29
|
+
});
|
|
30
|
+
if (result.error)
|
|
31
|
+
throw result.error;
|
|
32
|
+
if ((result.status ?? 1) !== 0) {
|
|
33
|
+
const stderr = (result.stderr ?? "").trim();
|
|
34
|
+
throw new Error(stderr || `gh ${args.join(" ")} failed (exit ${result.status ?? 1})`);
|
|
35
|
+
}
|
|
36
|
+
const stdout = (result.stdout ?? "").trim();
|
|
37
|
+
if (!stdout)
|
|
38
|
+
throw new Error(`gh ${args.join(" ")} returned empty stdout`);
|
|
39
|
+
return JSON.parse(stdout);
|
|
40
|
+
}
|
|
41
|
+
/** Production fetcher: `gh run view --json jobs`. */
|
|
42
|
+
export function fetchReleaseJobsViaGh(options) {
|
|
43
|
+
const payload = runGhJson([
|
|
44
|
+
"run",
|
|
45
|
+
"view",
|
|
46
|
+
options.runId,
|
|
47
|
+
"--repo",
|
|
48
|
+
options.repo,
|
|
49
|
+
"--json",
|
|
50
|
+
"jobs,status,conclusion,url,displayTitle",
|
|
51
|
+
]);
|
|
52
|
+
return jobsFromGhRunViewJson(payload);
|
|
53
|
+
}
|
|
54
|
+
/** Production resolver: latest Release workflow run for the repo. */
|
|
55
|
+
export function resolveLatestReleaseRunIdViaGh(options) {
|
|
56
|
+
const payload = runGhJson([
|
|
57
|
+
"run",
|
|
58
|
+
"list",
|
|
59
|
+
"--repo",
|
|
60
|
+
options.repo,
|
|
61
|
+
"--workflow",
|
|
62
|
+
RELEASE_WORKFLOW,
|
|
63
|
+
"--limit",
|
|
64
|
+
"1",
|
|
65
|
+
"--json",
|
|
66
|
+
"databaseId,status,displayTitle",
|
|
67
|
+
]);
|
|
68
|
+
if (!Array.isArray(payload) || payload.length === 0) {
|
|
69
|
+
throw new Error(`No Release workflow runs found for ${options.repo}`);
|
|
70
|
+
}
|
|
71
|
+
const first = payload[0];
|
|
72
|
+
const id = first.databaseId;
|
|
73
|
+
if (typeof id === "number" && Number.isFinite(id))
|
|
74
|
+
return String(id);
|
|
75
|
+
if (typeof id === "string" && id.trim())
|
|
76
|
+
return id.trim();
|
|
77
|
+
throw new Error("Latest Release run is missing databaseId");
|
|
78
|
+
}
|
|
79
|
+
function coerceNonEmptyString(value) {
|
|
80
|
+
if (typeof value === "string") {
|
|
81
|
+
const trimmed = value.trim();
|
|
82
|
+
return trimmed.length > 0 ? trimmed : undefined;
|
|
83
|
+
}
|
|
84
|
+
if (typeof value === "number" && Number.isFinite(value))
|
|
85
|
+
return String(value);
|
|
86
|
+
return undefined;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Core command body — injectable for tests; production uses gh CLI defaults.
|
|
90
|
+
*/
|
|
91
|
+
export function runReleaseProgress(options = {}) {
|
|
92
|
+
const repo = coerceNonEmptyString(options.repo) || DEFAULT_REPO;
|
|
93
|
+
const resolveLatest = options.resolveLatestRunId ?? resolveLatestReleaseRunIdViaGh;
|
|
94
|
+
const fetchJobs = options.fetchJobs ?? fetchReleaseJobsViaGh;
|
|
95
|
+
const stdout = options.stdout ?? process.stdout;
|
|
96
|
+
const stderr = options.stderr ?? process.stderr;
|
|
97
|
+
// cac may parse purely numeric --run-id values as numbers; coerce before trim.
|
|
98
|
+
const runId = coerceNonEmptyString(options.runId) || resolveLatest({ repo });
|
|
99
|
+
const jobs = fetchJobs({ runId, repo });
|
|
100
|
+
const progress = classifyReleaseRunProgress(jobs);
|
|
101
|
+
const line = formatReleaseProgressLine(progress);
|
|
102
|
+
if (options.json) {
|
|
103
|
+
stdout.write(`${JSON.stringify({
|
|
104
|
+
runId,
|
|
105
|
+
repo,
|
|
106
|
+
phase: progress.phase,
|
|
107
|
+
detail: progress.detail,
|
|
108
|
+
native: progress.native,
|
|
109
|
+
publishRunning: progress.publishRunning,
|
|
110
|
+
publishDone: progress.publishDone,
|
|
111
|
+
publishFailed: progress.publishFailed,
|
|
112
|
+
line,
|
|
113
|
+
}, null, 2)}\n`);
|
|
114
|
+
}
|
|
115
|
+
else {
|
|
116
|
+
stdout.write(`${line}\n`);
|
|
117
|
+
stdout.write(` run-id: ${runId}\n`);
|
|
118
|
+
stdout.write(` repo: ${repo}\n`);
|
|
119
|
+
if (progress.phase === "done") {
|
|
120
|
+
stdout.write(" next: npm view @webpresso/agent-kit version # only after phase=done\n");
|
|
121
|
+
}
|
|
122
|
+
else if (progress.phase === "native-matrix" || progress.phase === "publish") {
|
|
123
|
+
stdout.write(" note: do not treat npm view alone as progress; matrix/publish still running\n");
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
if (progress.phase === "failed") {
|
|
127
|
+
stderr.write(`wp release-progress: ${progress.detail}\n`);
|
|
128
|
+
return { exitCode: 1, progress, runId, line };
|
|
129
|
+
}
|
|
130
|
+
return { exitCode: 0, progress, runId, line };
|
|
131
|
+
}
|
|
132
|
+
export function registerReleaseProgressCommand(cli) {
|
|
133
|
+
cli
|
|
134
|
+
.command("release-progress", RELEASE_PROGRESS_COMMAND_HELP)
|
|
135
|
+
.option("--run-id <id>", "GitHub Actions run id (default: latest Release workflow run)")
|
|
136
|
+
.option("--repo <owner/name>", `GitHub repo (default: ${DEFAULT_REPO})`)
|
|
137
|
+
.option("--json", "Emit structured JSON")
|
|
138
|
+
.action((flags) => {
|
|
139
|
+
try {
|
|
140
|
+
const result = runReleaseProgress({
|
|
141
|
+
runId: flags.runId,
|
|
142
|
+
repo: flags.repo,
|
|
143
|
+
json: Boolean(flags.json),
|
|
144
|
+
});
|
|
145
|
+
process.exitCode = result.exitCode;
|
|
146
|
+
return result.exitCode;
|
|
147
|
+
}
|
|
148
|
+
catch (error) {
|
|
149
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
150
|
+
process.stderr.write(`wp release-progress: ${message}\n`);
|
|
151
|
+
process.exitCode = 1;
|
|
152
|
+
return 1;
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
}
|
|
@@ -167,6 +167,11 @@ function resolveReviewModel(provider, model) {
|
|
|
167
167
|
const fableVersion = /^fable[-\s]?(\d+)$/iu.exec(model)?.[1];
|
|
168
168
|
return fableVersion ? `claude-fable-${fableVersion}` : model;
|
|
169
169
|
}
|
|
170
|
+
// How many DISTINCT rejected semantic subjects are allowed before the anti-runaway
|
|
171
|
+
// budget cap trips. Identical resubmissions never reach here (they reuse the prior
|
|
172
|
+
// rejection for free), so this only bounds genuinely different converging attempts.
|
|
173
|
+
// A human may extend past it in place with WP_REVIEW_SCOPE_CONTINUE=1 (recorded).
|
|
174
|
+
const REVIEW_SUBJECT_REJECTION_LIMIT = 4;
|
|
170
175
|
function classificationForFailureCode(code) {
|
|
171
176
|
if (!code)
|
|
172
177
|
return undefined;
|
|
@@ -182,6 +187,7 @@ function isLocalReviewFailureCode(code) {
|
|
|
182
187
|
code === "expected-marker-missing" ||
|
|
183
188
|
code === "missing-terminal-result" ||
|
|
184
189
|
code === "semantic-progress-budget-exceeded" ||
|
|
190
|
+
code === "sandbox-unavailable" ||
|
|
185
191
|
code.startsWith("provider-stdin-"));
|
|
186
192
|
}
|
|
187
193
|
function reviewRunTerminalClassification(summary) {
|
|
@@ -1157,16 +1163,24 @@ const reviewGateAttemptSchema = z.object({
|
|
|
1157
1163
|
});
|
|
1158
1164
|
const reviewGateVerificationCommandSchema = z.object({
|
|
1159
1165
|
command: z.string(),
|
|
1160
|
-
|
|
1166
|
+
// "deferred" is produced for approval-dependent lifecycle gates
|
|
1167
|
+
// (delivery-verifier.ts) and persisted into gate state, so it MUST round-trip
|
|
1168
|
+
// through the reload schema or resumption/reuse of a lifecycle-gated review
|
|
1169
|
+
// throws "Review gate state is malformed".
|
|
1170
|
+
outcome: z.enum(["passed", "failed", "timed-out", "deferred"]),
|
|
1161
1171
|
durationMs: z.number(),
|
|
1162
1172
|
exitCode: z.number().nullable(),
|
|
1163
1173
|
signal: z.string().optional(),
|
|
1164
1174
|
});
|
|
1175
|
+
const _assertVerificationCommandReloadable = true;
|
|
1176
|
+
void _assertVerificationCommandReloadable;
|
|
1165
1177
|
const reviewGateVerificationSchema = z.object({
|
|
1166
1178
|
status: z.enum(["passed", "failed"]),
|
|
1167
1179
|
commands: z.array(reviewGateVerificationCommandSchema),
|
|
1168
1180
|
failureCode: z.string().optional(),
|
|
1169
1181
|
logPath: z.string().optional(),
|
|
1182
|
+
sandbox: z.enum(["srt", "disabled-by-operator", "unavailable"]).optional(),
|
|
1183
|
+
sandboxReason: z.string().optional(),
|
|
1170
1184
|
});
|
|
1171
1185
|
const reviewGateStateSchema = z
|
|
1172
1186
|
.object({
|
|
@@ -1259,15 +1273,42 @@ function buildGatePrompt(input) {
|
|
|
1259
1273
|
...(input.priorRejections && input.priorRejections.length > 0
|
|
1260
1274
|
? [
|
|
1261
1275
|
"",
|
|
1262
|
-
"Prior formal rejection evidence
|
|
1276
|
+
"Prior formal rejection evidence for this converging change:",
|
|
1263
1277
|
...input.priorRejections.map((entry) => `- ${entry.subjectDigest}: ${entry.artifactPath}`),
|
|
1264
|
-
"
|
|
1278
|
+
"For each prior finding, state its disposition (fixed / still-open / not-a-blocker) with evidence.",
|
|
1265
1279
|
"",
|
|
1266
1280
|
]
|
|
1267
1281
|
: []),
|
|
1268
|
-
"Inspect the target SHA and blueprint directly.
|
|
1282
|
+
"Inspect the target SHA and blueprint directly.",
|
|
1283
|
+
"",
|
|
1284
|
+
"This is a CONVERGENCE gate. Your goal is to surface EVERY blocker in ONE pass so the author can fix them all at once; a blocker you leave for a later round costs a full review cycle. Be exhaustive, not incremental.",
|
|
1285
|
+
"",
|
|
1286
|
+
'Walk ALL of these dimensions in order. For EACH, either report specific findings or write "no blocker". Do not skip a dimension and do not stop early:',
|
|
1287
|
+
"1. Correctness & logic — wrong results, edge cases, off-by-one, error handling.",
|
|
1288
|
+
"2. Security & trust boundaries — trace EVERY place external or target-controlled input, config, or code influences execution, file writes, network, or process spawning, and confirm each is confined. Untrusted code or config that runs with insufficient isolation is a blocker.",
|
|
1289
|
+
"3. Concurrency & races — shared state, ordering, cleanup-vs-use races.",
|
|
1290
|
+
"4. Resource, recovery & process lifecycle — timeouts, orphaned/daemonized processes, unbounded growth, cleanup ordering, and every failure/cancellation path.",
|
|
1291
|
+
"5. Test coverage adequacy — is every material claim backed by a test that would FAIL if the claim were false? Name the specific missing test; a claim asserted but not exercised by a test is a blocker.",
|
|
1292
|
+
"6. Scope & spec compliance — does the delivered code match the blueprint's stated decisions and claims?",
|
|
1293
|
+
"7. Documentation & lifecycle accuracy — do the blueprint's stated status, evidence, and verified-head match reality?",
|
|
1294
|
+
"",
|
|
1295
|
+
"For EVERY finding, include all three of:",
|
|
1296
|
+
"- Trigger: a concrete scenario or input that produces the failure. If you cannot name one, do not flag it as a blocker.",
|
|
1297
|
+
"- Severity: BLOCKER (must fix to ship) or NIT (non-blocking), plus a one-line confidence note.",
|
|
1298
|
+
"- Fix: the concrete change to make; when there is a real tradeoff, give 2 options with their costs so the author can choose and fix in one pass.",
|
|
1299
|
+
"",
|
|
1300
|
+
"Only flag as BLOCKER what a concrete failing scenario justifies. When impact is high (data loss, security, corruption) but your confidence is limited, still report it as a BLOCKER and state explicitly what remains uncertain — do not omit it.",
|
|
1301
|
+
"",
|
|
1302
|
+
"Before the verdict, do a COMPLETENESS pass: list what you did NOT inspect or could not verify that might still hide a blocker. If that list contains a plausible high-impact gap, convert it into a finding rather than leaving it for a later round.",
|
|
1303
|
+
...(input.purpose === "delivery"
|
|
1304
|
+
? [
|
|
1305
|
+
"",
|
|
1306
|
+
"Blueprint lifecycle scope: this delivery review judges the CODE at the target SHA. Blueprint lifecycle advancement — moving tasks to `done`, checking acceptance criteria, and transitioning the blueprint out of `draft` into `completed/` — happens AFTER this approval, at landing. A blueprint that is still `status: draft` with `todo` tasks and unchecked acceptance is EXPECTED here and is NOT a blocker: the lifecycle audit requires a draft to have zero checked acceptance, and a blueprint cannot leave `draft` without the very approval this review would grant. Do verify that the blueprint's stated verified-head matches the target and that its documented decisions match the delivered code; do NOT block on draft/todo lifecycle state.",
|
|
1307
|
+
]
|
|
1308
|
+
: []),
|
|
1309
|
+
"",
|
|
1269
1310
|
"End with exactly one standalone verdict line: APPROVED, APPROVE-WITH-NITS, or BLOCKED.",
|
|
1270
|
-
"APPROVE-WITH-NITS satisfies the gate only when every finding is
|
|
1311
|
+
"APPROVE-WITH-NITS satisfies the gate only when every finding is a NIT and carries a proposed fix.",
|
|
1271
1312
|
"If any unresolved correctness, security, recovery, test, or scope blocker remains, end with BLOCKED.",
|
|
1272
1313
|
].join("\n");
|
|
1273
1314
|
}
|
|
@@ -1886,7 +1927,7 @@ function rejectedReviewSubjects(input) {
|
|
|
1886
1927
|
*/
|
|
1887
1928
|
export function reviewGateNonConvergenceHint(slug, context = {}) {
|
|
1888
1929
|
if (context.status === "scope-exhausted") {
|
|
1889
|
-
return `hint: formal review budget is exhausted for ${slug}. Stop automated review retries and
|
|
1930
|
+
return `hint: formal review budget is exhausted for ${slug} after ${REVIEW_SUBJECT_REJECTION_LIMIT} rejected semantic subjects. Stop automated review retries. If you have addressed the prior findings and this is a good-faith converging attempt, a human may record an explicit scope decision and continue reviewing this blueprint in place by setting WP_REVIEW_SCOPE_CONTINUE=1 — no successor blueprint is needed.`;
|
|
1890
1931
|
}
|
|
1891
1932
|
if (context.failureCode === "provider-event-too-large" && context.gateId) {
|
|
1892
1933
|
return `hint: provider transport emitted a single provider JSON event above the bounded capture limit for ${slug}; no review verdict was produced. Retry the unchanged gate with "wp review gate ${slug} --resume ${context.gateId} --provider <provider>" and select a different provider when possible. Retrying the same provider/model is allowed for recovery but is likely to recur for deterministic oversized output. An independent "wp review run" can be recorded only as a human-adjudicated verdict through wp_blueprint_review_log; that tool cannot create a Claude/Codex/Grok/OpenCode-Go approval. The event-size and timeout bounds were not increased. See catalog/agent/rules/pre-implementation.md.`;
|
|
@@ -2114,7 +2155,7 @@ export async function runReviewGate(projectRoot, slug, input, dependencies = { i
|
|
|
2114
2155
|
// (verifyDeliveryPromotionGates is a no-op for every other purpose).
|
|
2115
2156
|
const deliveryCheckout = purpose === "delivery" ? ensureSharedCheckout() : undefined;
|
|
2116
2157
|
if (deliveryCheckout) {
|
|
2117
|
-
verification = verifyDeliveryPromotionGates({
|
|
2158
|
+
verification = await verifyDeliveryPromotionGates({
|
|
2118
2159
|
purpose,
|
|
2119
2160
|
blueprintContent,
|
|
2120
2161
|
projectRoot,
|
|
@@ -2122,6 +2163,9 @@ export async function runReviewGate(projectRoot, slug, input, dependencies = { i
|
|
|
2122
2163
|
checkout: deliveryCheckout,
|
|
2123
2164
|
...(dependencies.signal ? { signal: dependencies.signal } : {}),
|
|
2124
2165
|
});
|
|
2166
|
+
if (verification?.sandbox === "disabled-by-operator") {
|
|
2167
|
+
process.stderr.write("WARNING: WP_REVIEW_UNSANDBOXED_GATES=1 — delivery gates ran WITHOUT OS isolation (recorded as disabled-by-operator; approval reuse is disabled so this run publishes committed evidence).\n");
|
|
2168
|
+
}
|
|
2125
2169
|
const summary = verificationPromptSummary(verification);
|
|
2126
2170
|
if (summary) {
|
|
2127
2171
|
prompt = renderPrompt(summary);
|
|
@@ -2135,7 +2179,11 @@ export async function runReviewGate(projectRoot, slug, input, dependencies = { i
|
|
|
2135
2179
|
}
|
|
2136
2180
|
}
|
|
2137
2181
|
let approvalGate = currentApprovalGate();
|
|
2138
|
-
|
|
2182
|
+
// Under an unsandboxed operator override, never take the approval-reuse
|
|
2183
|
+
// fast path: force a fresh provider review so the run publishes a
|
|
2184
|
+
// committed artifact carrying `Sandbox: disabled-by-operator`. (The
|
|
2185
|
+
// override is also recorded in the persisted gate state on every path.)
|
|
2186
|
+
if (approvalGate.satisfied && verification?.sandbox !== "disabled-by-operator") {
|
|
2139
2187
|
const result = makeResult(approvalGate, reusedApprovals.length > 0 ? "approved" : "recorded-untracked");
|
|
2140
2188
|
assertReviewRefUnchanged(projectRoot, input.target ?? "HEAD", targetSha);
|
|
2141
2189
|
persistGateState(projectRoot, result);
|
|
@@ -2157,15 +2205,30 @@ export async function runReviewGate(projectRoot, slug, input, dependencies = { i
|
|
|
2157
2205
|
cleanupSharedCheckout();
|
|
2158
2206
|
return result;
|
|
2159
2207
|
}
|
|
2160
|
-
|
|
2161
|
-
|
|
2208
|
+
// Anti-runaway budget cap. Identical resubmissions are already
|
|
2209
|
+
// short-circuited for free by the currentSubjectRejection check above, so
|
|
2210
|
+
// this only ever fires on GENUINELY DIFFERENT (converging) subjects —
|
|
2211
|
+
// i.e. changes that addressed the prior findings. That is legitimate
|
|
2212
|
+
// iteration, not thrash, so a human MAY record an explicit in-place scope
|
|
2213
|
+
// decision to continue reviewing THIS blueprint instead of being forced to
|
|
2214
|
+
// fork a successor (which would only launder the counter). Automated runs
|
|
2215
|
+
// do not set the flag and still hard-stop.
|
|
2216
|
+
if (rejectedSubjects.length >= REVIEW_SUBJECT_REJECTION_LIMIT &&
|
|
2217
|
+
process.env["WP_REVIEW_SCOPE_CONTINUE"] !== "1") {
|
|
2218
|
+
const result = makeResult(approvalGate, "scope-exhausted", `Formal review budget exhausted after ${REVIEW_SUBJECT_REJECTION_LIMIT} rejected semantic subjects; no reviewer was invoked. If you have addressed the prior findings and this is a good-faith converging attempt, set WP_REVIEW_SCOPE_CONTINUE=1 to record an explicit human scope decision and continue reviewing this blueprint — no successor blueprint is needed. Automated runs must stop and escalate.`);
|
|
2162
2219
|
assertReviewRefUnchanged(projectRoot, input.target ?? "HEAD", targetSha);
|
|
2163
2220
|
persistGateState(projectRoot, result);
|
|
2164
2221
|
cleanupSharedCheckout();
|
|
2165
2222
|
return result;
|
|
2166
2223
|
}
|
|
2224
|
+
if (rejectedSubjects.length >= REVIEW_SUBJECT_REJECTION_LIMIT) {
|
|
2225
|
+
// Recorded for audit: the operator explicitly extended the budget past
|
|
2226
|
+
// the cap for this converging attempt. The reviewer's verdict is still
|
|
2227
|
+
// required — the approval gate itself is unchanged.
|
|
2228
|
+
gateIssues.push(`human scope-continue: WP_REVIEW_SCOPE_CONTINUE=1 extended the review budget past ${rejectedSubjects.length} rejected subjects`);
|
|
2229
|
+
}
|
|
2167
2230
|
if (rejectedSubjects.length > 0) {
|
|
2168
|
-
prompt = renderPrompt(verification ? verificationPromptSummary(verification) : undefined, rejectedSubjects.slice(0,
|
|
2231
|
+
prompt = renderPrompt(verification ? verificationPromptSummary(verification) : undefined, rejectedSubjects.slice(0, REVIEW_SUBJECT_REJECTION_LIMIT));
|
|
2169
2232
|
}
|
|
2170
2233
|
const artifactDirectory = path.join(path.dirname(location.path), "review-artifacts");
|
|
2171
2234
|
if (existsSync(artifactDirectory) && lstatSync(artifactDirectory).isSymbolicLink()) {
|
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);
|