@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
|
@@ -14,25 +14,44 @@ export const GIT_PROBE_TIMEOUT_MS = 15_000;
|
|
|
14
14
|
* raise/lower via `WP_SETUP_COMMIT_TIMEOUT_MS` or `AuthorCommitOptions.commitTimeoutMs`.
|
|
15
15
|
*/
|
|
16
16
|
export const DEFAULT_GIT_COMMIT_TIMEOUT_MS = 600_000;
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
/** Max stdout+stderr retained from probe git subprocesses (Node spawnSync maxBuffer). */
|
|
18
|
+
export const GIT_MAX_BUFFER = 1024 * 1024;
|
|
19
|
+
/**
|
|
20
|
+
* Max stdout+stderr for the single hookful `git commit`. Quiet hooks cost nothing
|
|
21
|
+
* (Node does not preallocate); chatty consumer format+audit chains can exceed 1 MiB
|
|
22
|
+
* and would otherwise die with ENOBUFS+SIGTERM mid-commit.
|
|
23
|
+
*/
|
|
24
|
+
export const GIT_COMMIT_MAX_BUFFER = 16 * 1024 * 1024;
|
|
25
|
+
/** Hook output snippet size kept in first-class failure diagnoses (tail, not head). */
|
|
26
|
+
const HOOK_OUTPUT_SNIPPET_CHARS = 800;
|
|
27
|
+
/** spawnSync error codes that mean maxBuffer / stdio overflow killed the child. */
|
|
28
|
+
const BUFFER_OVERFLOW_ERROR_CODES = new Set(["ENOBUFS", "ERR_CHILD_PROCESS_STDIO_MAXBUFFER"]);
|
|
29
|
+
function git(repoRoot, args, timeoutMs, maxBuffer = GIT_MAX_BUFFER) {
|
|
19
30
|
const result = spawnSync("git", [...args], {
|
|
20
31
|
cwd: repoRoot,
|
|
21
32
|
encoding: "utf8",
|
|
22
33
|
// Node: timeout > 0 arms SIGTERM; 0 / omitted means no artificial wall.
|
|
23
34
|
...(timeoutMs > 0 ? { timeout: timeoutMs } : {}),
|
|
24
|
-
maxBuffer
|
|
35
|
+
maxBuffer,
|
|
25
36
|
stdio: ["ignore", "pipe", "pipe"],
|
|
26
37
|
});
|
|
27
|
-
const
|
|
38
|
+
const errorCode = result.error !== undefined && "code" in result.error && result.error.code != null
|
|
39
|
+
? String(result.error.code)
|
|
40
|
+
: null;
|
|
41
|
+
const timedOut = errorCode === "ETIMEDOUT";
|
|
28
42
|
return {
|
|
29
43
|
ok: result.status === 0 && !result.error,
|
|
30
44
|
stdout: (result.stdout ?? "").trim(),
|
|
31
45
|
stderr: (result.stderr ?? "").trim(),
|
|
32
46
|
timedOut,
|
|
33
47
|
signal: result.signal,
|
|
48
|
+
errorCode,
|
|
34
49
|
};
|
|
35
50
|
}
|
|
51
|
+
/** True when spawnSync killed the child for maxBuffer / stdio overflow. */
|
|
52
|
+
export function isBufferOverflowErrorCode(code) {
|
|
53
|
+
return typeof code === "string" && BUFFER_OVERFLOW_ERROR_CODES.has(code);
|
|
54
|
+
}
|
|
36
55
|
/**
|
|
37
56
|
* Resolve the hookful-commit timeout. Preference order:
|
|
38
57
|
* 1. explicit `commitTimeoutMs` option (tests / programmatic callers)
|
|
@@ -60,7 +79,10 @@ function appendHookOutput(diagnosis, hookOutput) {
|
|
|
60
79
|
const trimmed = hookOutput?.trim();
|
|
61
80
|
if (!trimmed)
|
|
62
81
|
return diagnosis;
|
|
63
|
-
|
|
82
|
+
// Keep the tail: failing audits / last lines at kill time matter more than husky banner.
|
|
83
|
+
const snippet = trimmed.length > HOOK_OUTPUT_SNIPPET_CHARS
|
|
84
|
+
? `…${trimmed.slice(-HOOK_OUTPUT_SNIPPET_CHARS)}`
|
|
85
|
+
: trimmed;
|
|
64
86
|
return `${diagnosis}\n--- hook output (truncated) ---\n${snippet}`;
|
|
65
87
|
}
|
|
66
88
|
/** First-class timeout diagnosis; partial hook stdout/stderr is diagnostic only. */
|
|
@@ -87,24 +109,57 @@ export function formatCommitInterruptError(options) {
|
|
|
87
109
|
`Partial hook lines such as "format applied" / "audit … passed" are not success.`;
|
|
88
110
|
return appendHookOutput(diagnosis, options.hookOutput);
|
|
89
111
|
}
|
|
112
|
+
/**
|
|
113
|
+
* First-class diagnosis when Node spawnSync kills the child because hook/git
|
|
114
|
+
* output exceeded maxBuffer (ENOBUFS / ERR_CHILD_PROCESS_STDIO_MAXBUFFER).
|
|
115
|
+
* Distinct from budget timeout and from external signal kills.
|
|
116
|
+
*/
|
|
117
|
+
export function formatCommitBufferError(options) {
|
|
118
|
+
const code = options.errorCode?.trim() || "ENOBUFS";
|
|
119
|
+
const diagnosis = `Convergence commit failed because git/hook output exceeded the spawnSync maxBuffer limit ` +
|
|
120
|
+
`(${options.maxBufferBytes} bytes; Node error ${code}), so no setup commit was authored. ` +
|
|
121
|
+
`This is a buffer/output-limit failure, not a time budget timeout and not an external kill. ` +
|
|
122
|
+
`Staged and worktree changes were preserved for remediation. ` +
|
|
123
|
+
`Reduce hook verbosity, raise the maxBuffer only with measured need, or fix chatty hooks. ` +
|
|
124
|
+
`Partial hook lines such as "format applied" / "audit … passed" are not success.`;
|
|
125
|
+
return appendHookOutput(diagnosis, options.hookOutput);
|
|
126
|
+
}
|
|
90
127
|
/**
|
|
91
128
|
* Classify a failed hookful `git commit` result.
|
|
92
129
|
*
|
|
93
|
-
*
|
|
94
|
-
*
|
|
95
|
-
*
|
|
130
|
+
* Order (first match wins):
|
|
131
|
+
* 1. `timedOut` (spawnSync ETIMEDOUT) → budget timeout diagnosis
|
|
132
|
+
* 2. buffer overflow (ENOBUFS / ERR_CHILD_PROCESS_STDIO_MAXBUFFER) → buffer-limit diagnosis
|
|
133
|
+
* 3. non-null `signal` → interrupt diagnosis (names the signal)
|
|
134
|
+
* 4. otherwise → hook/git stderr when present, else spawn errorCode / generic preserved-index
|
|
96
135
|
*
|
|
97
|
-
* Never lead with bare hook "passed" lines for
|
|
136
|
+
* Never lead with bare hook "passed" lines for first-class failure paths.
|
|
98
137
|
*/
|
|
99
|
-
export function classifyCommitFailure(result, timeoutMs) {
|
|
138
|
+
export function classifyCommitFailure(result, timeoutMs, maxBufferBytes = GIT_COMMIT_MAX_BUFFER) {
|
|
100
139
|
const hookOutput = [result.stderr, result.stdout].filter(Boolean).join("\n");
|
|
101
140
|
if (result.timedOut) {
|
|
102
141
|
return formatCommitTimeoutError({ timeoutMs, hookOutput });
|
|
103
142
|
}
|
|
143
|
+
if (isBufferOverflowErrorCode(result.errorCode)) {
|
|
144
|
+
return formatCommitBufferError({
|
|
145
|
+
maxBufferBytes,
|
|
146
|
+
errorCode: result.errorCode,
|
|
147
|
+
hookOutput,
|
|
148
|
+
});
|
|
149
|
+
}
|
|
104
150
|
if (result.signal) {
|
|
105
151
|
return formatCommitInterruptError({ signal: result.signal, hookOutput });
|
|
106
152
|
}
|
|
107
|
-
|
|
153
|
+
if (hookOutput) {
|
|
154
|
+
if (result.errorCode) {
|
|
155
|
+
return `${hookOutput}\n(spawn error ${result.errorCode})`;
|
|
156
|
+
}
|
|
157
|
+
return hookOutput;
|
|
158
|
+
}
|
|
159
|
+
if (result.errorCode) {
|
|
160
|
+
return `git commit failed (spawn error ${result.errorCode}); hook changes were preserved.`;
|
|
161
|
+
}
|
|
162
|
+
return "git commit failed; hook changes were preserved.";
|
|
108
163
|
}
|
|
109
164
|
/**
|
|
110
165
|
* Prefix every line of a setup failure so multi-line timeout diagnoses stay
|
|
@@ -230,13 +285,20 @@ export function authorConvergenceCommit(options) {
|
|
|
230
285
|
const commitTimeoutMs = resolveCommitTimeoutMs({
|
|
231
286
|
commitTimeoutMs: options.commitTimeoutMs,
|
|
232
287
|
});
|
|
233
|
-
const
|
|
288
|
+
const budgetLabel = commitTimeoutMs > 0
|
|
289
|
+
? commitTimeoutMs >= 60_000
|
|
290
|
+
? `up to ${Math.round(commitTimeoutMs / 60_000)}m`
|
|
291
|
+
: `up to ${commitTimeoutMs}ms`
|
|
292
|
+
: "no artificial time wall";
|
|
293
|
+
// stderr: setup success path stays machine-friendly on stdout; hooks can take minutes.
|
|
294
|
+
console.error(`wp setup: running repository pre-commit hooks (${budgetLabel})…`);
|
|
295
|
+
const commit = git(options.repoRoot, ["commit", "-m", options.message], commitTimeoutMs, GIT_COMMIT_MAX_BUFFER);
|
|
234
296
|
if (!commit.ok) {
|
|
235
297
|
return {
|
|
236
298
|
status: "failed",
|
|
237
299
|
ok: false,
|
|
238
300
|
allowlist,
|
|
239
|
-
error: classifyCommitFailure(commit, commitTimeoutMs),
|
|
301
|
+
error: classifyCommitFailure(commit, commitTimeoutMs, GIT_COMMIT_MAX_BUFFER),
|
|
240
302
|
};
|
|
241
303
|
}
|
|
242
304
|
const committedHead = probe(["rev-parse", "--verify", "HEAD"]);
|
|
@@ -71,6 +71,17 @@ export interface OpencodeProbeRunnerInput {
|
|
|
71
71
|
}
|
|
72
72
|
export type OpencodeProbeRunner = (input: OpencodeProbeRunnerInput) => Promise<OpencodeProbeResult>;
|
|
73
73
|
export declare function isOpencodeProbeEnabled(env?: NodeJS.ProcessEnv): boolean;
|
|
74
|
+
/**
|
|
75
|
+
* Spawn a bounded `opencode run` against one account's isolated credential
|
|
76
|
+
* store and classify the first conclusive thing it says.
|
|
77
|
+
*
|
|
78
|
+
* There is no OpenCode Go HTTP endpoint anywhere in this codebase — the review
|
|
79
|
+
* adapter also reaches the provider only by spawning this binary — so a
|
|
80
|
+
* subprocess is the only available probe vehicle. Per-account isolation is the
|
|
81
|
+
* same `XDG_DATA_HOME` trick the review adapter uses, so a probe can never race
|
|
82
|
+
* a live interactive TUI's global `auth.json`.
|
|
83
|
+
*/
|
|
84
|
+
export declare function spawnOpencodeProbe(input: OpencodeProbeRunnerInput): Promise<OpencodeProbeResult>;
|
|
74
85
|
export interface ProbeOpencodeAccountDeps {
|
|
75
86
|
readonly run?: OpencodeProbeRunner;
|
|
76
87
|
readonly materialize?: (key: string, dataDir: string) => void;
|
|
@@ -157,7 +157,7 @@ function classifyChunk(captured) {
|
|
|
157
157
|
* same `XDG_DATA_HOME` trick the review adapter uses, so a probe can never race
|
|
158
158
|
* a live interactive TUI's global `auth.json`.
|
|
159
159
|
*/
|
|
160
|
-
function spawnOpencodeProbe(input) {
|
|
160
|
+
export function spawnOpencodeProbe(input) {
|
|
161
161
|
return new Promise((resolve) => {
|
|
162
162
|
let probeCwd;
|
|
163
163
|
try {
|
|
@@ -145,9 +145,13 @@ function renderSummaryLines(summary, transformed, entry, passed) {
|
|
|
145
145
|
else if (!passed && transformed.rawOutput?.trim()) {
|
|
146
146
|
lines.push(transformed.rawOutput.trimEnd());
|
|
147
147
|
}
|
|
148
|
-
|
|
148
|
+
// Any elided bytes must remain retrievable: fail, clip, OR compaction that
|
|
149
|
+
// recorded an elision without setting truncated. Hide Full log / retrieve
|
|
150
|
+
// hints only when the summary still contains the full content.
|
|
151
|
+
const hasElisions = (transformed.elisions?.length ?? 0) > 0;
|
|
152
|
+
if (!passed || transformed.truncated || hasElisions) {
|
|
149
153
|
lines.push(`Full log: wp logs ${entry.command}`);
|
|
150
|
-
if (
|
|
154
|
+
if (hasElisions) {
|
|
151
155
|
lines.push(...transformed.elisions.map((elision) => `Retrieve elision: ${elision.retrieveTool} id=${elision.id}`));
|
|
152
156
|
}
|
|
153
157
|
}
|
|
@@ -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()) {
|
|
@@ -24,7 +24,7 @@ export const TYPECHECK_COMMAND_HELP = [
|
|
|
24
24
|
" wp typecheck --affected --branch # changed vs origin/${GITHUB_BASE_REF:-main}",
|
|
25
25
|
" wp typecheck --pretty",
|
|
26
26
|
" wp typecheck --tests # test-surface lane (tsconfig.test.json); required in",
|
|
27
|
-
" #
|
|
27
|
+
" # ci-preflight and CI (not the default untargeted gate)",
|
|
28
28
|
].join("\n");
|
|
29
29
|
export function registerTypecheckCommand(cli, deps = {}) {
|
|
30
30
|
addAffectedOptions(cli
|
|
@@ -55,6 +55,12 @@ export interface MergeCleanupDecisionInput {
|
|
|
55
55
|
readonly repoDirty: boolean;
|
|
56
56
|
readonly targetDirty: boolean;
|
|
57
57
|
readonly stashPrimary?: boolean;
|
|
58
|
+
/**
|
|
59
|
+
* When target is dirty, optional porcelain -z + slug enrich the refusal message
|
|
60
|
+
* (scaffold-only vs other dirt). Omitted messages stay generic for unit tests.
|
|
61
|
+
*/
|
|
62
|
+
readonly targetPorcelainZ?: string;
|
|
63
|
+
readonly targetBlueprintSlug?: string;
|
|
58
64
|
}
|
|
59
65
|
export interface NewWorktreeTargetInput {
|
|
60
66
|
branch?: string;
|
|
@@ -97,4 +103,6 @@ export interface CreateManagedWorktreeResult {
|
|
|
97
103
|
export declare function formatWorktreeList(entries: WorktreeEntry[], currentWorktreePath: string): string[];
|
|
98
104
|
export declare function formatManagedWorktreeList(entries: readonly ManagedWorktreeEntry[]): string[];
|
|
99
105
|
export declare function createManagedWorktree(opts?: CreateManagedWorktreeOptions): Promise<CreateManagedWorktreeResult>;
|
|
106
|
+
/** `.../blueprints/<slug>/owner` → slug */
|
|
107
|
+
export declare function inferBlueprintSlugFromOwnerPath(ownerPath: string): string | undefined;
|
|
100
108
|
export declare function executeWorktreeSubcommand(subcommand: string, args: string[], opts: WorktreeCommandOptions): Promise<void>;
|