@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
|
@@ -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);
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export interface AbandonBlueprintOptions {
|
|
2
|
+
readonly projectRoot?: string;
|
|
3
|
+
readonly confirm?: boolean;
|
|
4
|
+
/** Injected for tests. */
|
|
5
|
+
readonly resolveOwnerPath?: (repoRoot: string, slug: string) => string;
|
|
6
|
+
readonly readPorcelainZ?: (cwd: string) => string;
|
|
7
|
+
readonly hasCommittedDraft?: (cwd: string, slug: string) => boolean;
|
|
8
|
+
readonly isPrimaryCheckout?: (ownerPath: string, repoRoot: string) => boolean;
|
|
9
|
+
}
|
|
10
|
+
export interface AbandonBlueprintResult {
|
|
11
|
+
readonly slug: string;
|
|
12
|
+
readonly ownerPath: string;
|
|
13
|
+
readonly preview: boolean;
|
|
14
|
+
readonly message: string;
|
|
15
|
+
readonly paths: readonly string[];
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Abandon an uncommitted draft scaffold so merge-cleanup can proceed.
|
|
19
|
+
* Without --confirm: print preview and throw (exit non-zero at CLI).
|
|
20
|
+
*/
|
|
21
|
+
export declare function abandonBlueprint(rawSlug: string, options?: AbandonBlueprintOptions): Promise<AbandonBlueprintResult>;
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { existsSync, lstatSync, rmSync } from "node:fs";
|
|
2
|
+
import { join, resolve, sep } from "node:path";
|
|
3
|
+
import { spawnSync } from "node:child_process";
|
|
4
|
+
import { isValidBlueprintSlug, relativeBlueprintSlug } from "#lifecycle/local.js";
|
|
5
|
+
import { resolveProjectRoot } from "#mcp/tools/_shared/project-root.js";
|
|
6
|
+
import { classifyOwnerDirt, draftScaffoldPrefix, formatBoundedPaths, } from "#worktrees/owner-dirt.js";
|
|
7
|
+
import { resolveOwnerWorktreePath } from "#worktrees/location.js";
|
|
8
|
+
function gitStdout(cwd, args) {
|
|
9
|
+
const result = spawnSync("git", [...args], { cwd, encoding: "utf8" });
|
|
10
|
+
if (result.status !== 0) {
|
|
11
|
+
const err = String(result.stderr ?? result.stdout ?? "git failed").trim();
|
|
12
|
+
throw new Error(err || `git ${args.join(" ")} failed`);
|
|
13
|
+
}
|
|
14
|
+
return String(result.stdout ?? "");
|
|
15
|
+
}
|
|
16
|
+
function defaultReadPorcelainZ(cwd) {
|
|
17
|
+
return gitStdout(cwd, [
|
|
18
|
+
"status",
|
|
19
|
+
"--porcelain=v1",
|
|
20
|
+
"--untracked-files=all",
|
|
21
|
+
"--ignored=no",
|
|
22
|
+
"-z",
|
|
23
|
+
]);
|
|
24
|
+
}
|
|
25
|
+
function defaultHasCommittedDraft(cwd, slug) {
|
|
26
|
+
const prefix = draftScaffoldPrefix(slug);
|
|
27
|
+
const mergeBase = spawnSync("git", ["merge-base", "HEAD", "origin/main"], {
|
|
28
|
+
cwd,
|
|
29
|
+
encoding: "utf8",
|
|
30
|
+
});
|
|
31
|
+
const range = mergeBase.status === 0 && String(mergeBase.stdout ?? "").trim()
|
|
32
|
+
? `${String(mergeBase.stdout).trim()}..HEAD`
|
|
33
|
+
: "HEAD";
|
|
34
|
+
const log = spawnSync("git", ["log", "--oneline", range, "--", prefix], {
|
|
35
|
+
cwd,
|
|
36
|
+
encoding: "utf8",
|
|
37
|
+
});
|
|
38
|
+
if (log.status !== 0) {
|
|
39
|
+
// Fail closed: if history probe fails, do not delete.
|
|
40
|
+
return true;
|
|
41
|
+
}
|
|
42
|
+
return String(log.stdout ?? "").trim().length > 0;
|
|
43
|
+
}
|
|
44
|
+
function assertPathInsideDraftRoot(ownerPath, draftAbs) {
|
|
45
|
+
const draftRoot = resolve(ownerPath, "blueprints", "draft") + sep;
|
|
46
|
+
const target = resolve(draftAbs);
|
|
47
|
+
if (target !== resolve(ownerPath, "blueprints", "draft") && !target.startsWith(draftRoot)) {
|
|
48
|
+
throw new Error(`Refusing abandon: path escapes blueprints/draft/ (${target}). next: inspect worktree layout`);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
function isOwnerPathForSlug(path, slug) {
|
|
52
|
+
const normalized = path.replace(/\\/gu, "/");
|
|
53
|
+
return (normalized.endsWith(`/blueprints/${slug}/owner`) ||
|
|
54
|
+
normalized.endsWith(`/blueprints/${slug}/owner/`));
|
|
55
|
+
}
|
|
56
|
+
function resolveOwnerForSlug(repoRoot, slug, resolveOwnerPath) {
|
|
57
|
+
const cwd = resolve(repoRoot);
|
|
58
|
+
// Already inside the owner worktree for this slug (typical agent cwd).
|
|
59
|
+
if (isOwnerPathForSlug(cwd, slug)) {
|
|
60
|
+
return cwd;
|
|
61
|
+
}
|
|
62
|
+
const ownerPath = resolve(resolveOwnerPath(repoRoot, slug));
|
|
63
|
+
if (ownerPath === cwd) {
|
|
64
|
+
throw new Error(`Refusing abandon: resolved owner is the primary checkout (${ownerPath}). next: abandon only from a bp/<slug> owner worktree, or remove primary draft manually after review`);
|
|
65
|
+
}
|
|
66
|
+
if (!isOwnerPathForSlug(ownerPath, slug)) {
|
|
67
|
+
throw new Error(`Refusing abandon: worktree ${ownerPath} is not the owner path for ${slug}. next: run from bp/${slug} owner or pass the correct project root`);
|
|
68
|
+
}
|
|
69
|
+
return ownerPath;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Abandon an uncommitted draft scaffold so merge-cleanup can proceed.
|
|
73
|
+
* Without --confirm: print preview and throw (exit non-zero at CLI).
|
|
74
|
+
*/
|
|
75
|
+
export async function abandonBlueprint(rawSlug, options = {}) {
|
|
76
|
+
const slug = relativeBlueprintSlug(rawSlug.trim());
|
|
77
|
+
if (!isValidBlueprintSlug(slug)) {
|
|
78
|
+
throw new Error(`Refusing abandon: invalid blueprint slug ${JSON.stringify(rawSlug)}. next: use a kebab-case slug like my-feature`);
|
|
79
|
+
}
|
|
80
|
+
const projectRoot = options.projectRoot
|
|
81
|
+
? resolveProjectRoot({ cwd: options.projectRoot, explicitCwd: options.projectRoot })
|
|
82
|
+
: resolveProjectRoot();
|
|
83
|
+
const resolveOwnerPath = options.resolveOwnerPath ?? resolveOwnerWorktreePath;
|
|
84
|
+
const readPorcelainZ = options.readPorcelainZ ?? defaultReadPorcelainZ;
|
|
85
|
+
const hasCommittedDraft = options.hasCommittedDraft ?? defaultHasCommittedDraft;
|
|
86
|
+
const ownerPath = resolveOwnerForSlug(projectRoot, slug, resolveOwnerPath);
|
|
87
|
+
const draftRel = draftScaffoldPrefix(slug);
|
|
88
|
+
const draftAbs = join(ownerPath, draftRel);
|
|
89
|
+
assertPathInsideDraftRoot(ownerPath, draftAbs);
|
|
90
|
+
if (existsSync(draftAbs)) {
|
|
91
|
+
const st = lstatSync(draftAbs);
|
|
92
|
+
if (st.isSymbolicLink()) {
|
|
93
|
+
// Only remove the link itself when confirmed — never follow.
|
|
94
|
+
if (options.confirm !== true) {
|
|
95
|
+
throw new Error([
|
|
96
|
+
`Would remove symlink draft scaffold ${draftRel} in ${ownerPath}`,
|
|
97
|
+
"Re-run with --confirm to remove the symlink only (not the target).",
|
|
98
|
+
].join("\n"));
|
|
99
|
+
}
|
|
100
|
+
rmSync(draftAbs, { force: true });
|
|
101
|
+
const afterLink = classifyOwnerDirt(readPorcelainZ(ownerPath), slug);
|
|
102
|
+
if (afterLink.kind !== "clean") {
|
|
103
|
+
throw new Error(`Abandon incomplete; remaining dirt:\n${formatBoundedPaths(afterLink.paths)}\nnext: git status in ${ownerPath}`);
|
|
104
|
+
}
|
|
105
|
+
return {
|
|
106
|
+
slug,
|
|
107
|
+
ownerPath,
|
|
108
|
+
preview: false,
|
|
109
|
+
paths: [draftRel],
|
|
110
|
+
message: `Abandoned draft symlink scaffold for ${slug}; worktree clean — next: wp worktree merge-cleanup bp/${slug}`,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
if (hasCommittedDraft(ownerPath, slug)) {
|
|
115
|
+
throw new Error([
|
|
116
|
+
`Refusing abandon: commits on this branch touch ${draftRel}.`,
|
|
117
|
+
"next: land the draft with the PR, or revert those commits (git revert / interactive rebase) — abandon only deletes uncommitted scaffolds",
|
|
118
|
+
].join("\n"));
|
|
119
|
+
}
|
|
120
|
+
const porcelainZ = readPorcelainZ(ownerPath);
|
|
121
|
+
const classification = classifyOwnerDirt(porcelainZ, slug);
|
|
122
|
+
if (classification.kind === "clean" && !existsSync(draftAbs)) {
|
|
123
|
+
throw new Error(`Nothing to abandon for ${slug}: no draft scaffold dirt. next: wp worktree merge-cleanup bp/${slug}`);
|
|
124
|
+
}
|
|
125
|
+
if (classification.kind === "other") {
|
|
126
|
+
throw new Error([
|
|
127
|
+
`Refusing abandon: owner has non-scaffold dirt for ${slug}:`,
|
|
128
|
+
formatBoundedPaths(classification.paths),
|
|
129
|
+
`next: commit or stash non-scaffold paths in ${ownerPath}, then re-run abandon or merge-cleanup`,
|
|
130
|
+
].join("\n"));
|
|
131
|
+
}
|
|
132
|
+
// scaffold-only or clean-but-empty-dir leftover with no porcelain (empty dirs invisible)
|
|
133
|
+
const paths = classification.paths.length > 0 ? classification.paths : existsSync(draftAbs) ? [draftRel] : [];
|
|
134
|
+
if (options.confirm !== true) {
|
|
135
|
+
throw new Error([
|
|
136
|
+
`Would abandon draft scaffold for ${slug} in ${ownerPath}:`,
|
|
137
|
+
formatBoundedPaths(paths.length > 0 ? paths : [draftRel]),
|
|
138
|
+
"Re-run with --confirm to delete. next: wp blueprint abandon <slug> --confirm",
|
|
139
|
+
].join("\n"));
|
|
140
|
+
}
|
|
141
|
+
// Unstage any index entries under the scaffold, then delete the tree.
|
|
142
|
+
const cached = spawnSync("git", ["rm", "-r", "--cached", "--ignore-unmatch", "--", draftRel], {
|
|
143
|
+
cwd: ownerPath,
|
|
144
|
+
encoding: "utf8",
|
|
145
|
+
});
|
|
146
|
+
if (cached.status !== 0 && cached.status !== null) {
|
|
147
|
+
// ignore-unmatch should make this soft; still fail closed on hard errors
|
|
148
|
+
const err = String(cached.stderr ?? "").trim();
|
|
149
|
+
if (err && !/did not match/iu.test(err)) {
|
|
150
|
+
throw new Error(`git rm --cached failed: ${err}. next: git status in ${ownerPath}`);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
if (existsSync(draftAbs)) {
|
|
154
|
+
assertPathInsideDraftRoot(ownerPath, draftAbs);
|
|
155
|
+
const st = lstatSync(draftAbs);
|
|
156
|
+
if (st.isSymbolicLink()) {
|
|
157
|
+
rmSync(draftAbs, { force: true });
|
|
158
|
+
}
|
|
159
|
+
else if (st.isDirectory() || st.isFile()) {
|
|
160
|
+
rmSync(draftAbs, { recursive: true, force: true });
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
const after = classifyOwnerDirt(readPorcelainZ(ownerPath), slug);
|
|
164
|
+
if (after.kind !== "clean") {
|
|
165
|
+
throw new Error([
|
|
166
|
+
`Abandon incomplete; remaining dirt:`,
|
|
167
|
+
formatBoundedPaths(after.paths),
|
|
168
|
+
`next: git status in ${ownerPath}`,
|
|
169
|
+
].join("\n"));
|
|
170
|
+
}
|
|
171
|
+
return {
|
|
172
|
+
slug,
|
|
173
|
+
ownerPath,
|
|
174
|
+
preview: false,
|
|
175
|
+
paths,
|
|
176
|
+
message: `Abandoned draft scaffold for ${slug}; worktree clean — next: wp worktree merge-cleanup bp/${slug}`,
|
|
177
|
+
};
|
|
178
|
+
}
|
|
@@ -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] ?? "";
|
|
@@ -6,6 +6,7 @@ import { executeBlueprintDbSubcommand } from "./db-commands.js";
|
|
|
6
6
|
import { getBlueprintSubcommandNames } from "./router-output.js";
|
|
7
7
|
import { listTemplates, resolveTemplate } from "./template-resolver.js";
|
|
8
8
|
import { TRUST_DOSSIER_SCAFFOLD } from "#trust/scaffold.js";
|
|
9
|
+
import { abandonBlueprint } from "./abandon.js";
|
|
9
10
|
// ---------------------------------------------------------------------------
|
|
10
11
|
// Platform template fetcher (injectable for tests)
|
|
11
12
|
// ---------------------------------------------------------------------------
|
|
@@ -261,6 +262,26 @@ export async function executeBlueprintSubcommand(subcommand, args, options, deps
|
|
|
261
262
|
deps.printBlueprintOutput(options.json ? result : result.message, options.json);
|
|
262
263
|
return;
|
|
263
264
|
}
|
|
265
|
+
case "abandon": {
|
|
266
|
+
const slug = args[0];
|
|
267
|
+
if (!slug) {
|
|
268
|
+
throw new Error("Usage: wp blueprint abandon <slug> --confirm");
|
|
269
|
+
}
|
|
270
|
+
const result = await abandonBlueprint(slug, {
|
|
271
|
+
projectRoot: options.projectRoot,
|
|
272
|
+
confirm: options.confirm === true,
|
|
273
|
+
});
|
|
274
|
+
deps.printBlueprintOutput(options.json
|
|
275
|
+
? {
|
|
276
|
+
slug: result.slug,
|
|
277
|
+
ownerPath: result.ownerPath,
|
|
278
|
+
preview: result.preview,
|
|
279
|
+
paths: result.paths,
|
|
280
|
+
message: result.message,
|
|
281
|
+
}
|
|
282
|
+
: result.message, options.json);
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
264
285
|
case "finalize": {
|
|
265
286
|
const slug = args[0];
|
|
266
287
|
if (!slug) {
|
|
@@ -42,6 +42,8 @@ export interface BlueprintCommandOptions extends BlueprintAuditOptions, Blueprin
|
|
|
42
42
|
template?: string;
|
|
43
43
|
templatesDir?: string;
|
|
44
44
|
to?: string;
|
|
45
|
+
/** Required for destructive `blueprint abandon`. */
|
|
46
|
+
confirm?: boolean;
|
|
45
47
|
"--": string[];
|
|
46
48
|
}
|
|
47
49
|
export type { AdvanceTaskResult, PromoteBlueprintResult };
|
|
@@ -175,9 +175,11 @@ export async function createBlueprint(goal, options = {}) {
|
|
|
175
175
|
: probeService;
|
|
176
176
|
// Single full compile lives inside create() on the write root.
|
|
177
177
|
const created = await writeService.create({ complexity, goal, type });
|
|
178
|
+
const draftHint = "Draft is uncommitted — include blueprints/draft/" +
|
|
179
|
+
`${created.slug}/ in your first commit, or run: wp blueprint abandon ${created.slug} --confirm before merge-cleanup.`;
|
|
178
180
|
const message = authoring.routed
|
|
179
|
-
? `Created ${created.type} draft/${created.slug} (routed from primary-like checkout)
|
|
180
|
-
: `Created ${created.type} draft/${created.slug}
|
|
181
|
+
? `Created ${created.type} draft/${created.slug} (routed from primary-like checkout).\n${draftHint}`
|
|
182
|
+
: `Created ${created.type} draft/${created.slug}.\n${draftHint}`;
|
|
181
183
|
return {
|
|
182
184
|
...created,
|
|
183
185
|
message,
|
|
@@ -541,6 +543,7 @@ export function registerBlueprintRouter(cli) {
|
|
|
541
543
|
.option("--template <name>", "Template name to scaffold new blueprint from (see --list-templates)")
|
|
542
544
|
.option("--list-templates", "List available template names and exit")
|
|
543
545
|
.option("--templates-dir <path>", "Override the templates directory (default: docs/templates/)")
|
|
546
|
+
.option("--confirm", "Confirm destructive blueprint abandon")
|
|
544
547
|
.action(async (subcommand, args, options) => {
|
|
545
548
|
try {
|
|
546
549
|
await executeBlueprintSubcommand(subcommand, args, options, {
|
|
@@ -12,6 +12,16 @@ export declare const GIT_PROBE_TIMEOUT_MS = 15000;
|
|
|
12
12
|
* raise/lower via `WP_SETUP_COMMIT_TIMEOUT_MS` or `AuthorCommitOptions.commitTimeoutMs`.
|
|
13
13
|
*/
|
|
14
14
|
export declare const DEFAULT_GIT_COMMIT_TIMEOUT_MS = 600000;
|
|
15
|
+
/** Max stdout+stderr retained from probe git subprocesses (Node spawnSync maxBuffer). */
|
|
16
|
+
export declare const GIT_MAX_BUFFER: number;
|
|
17
|
+
/**
|
|
18
|
+
* Max stdout+stderr for the single hookful `git commit`. Quiet hooks cost nothing
|
|
19
|
+
* (Node does not preallocate); chatty consumer format+audit chains can exceed 1 MiB
|
|
20
|
+
* and would otherwise die with ENOBUFS+SIGTERM mid-commit.
|
|
21
|
+
*/
|
|
22
|
+
export declare const GIT_COMMIT_MAX_BUFFER: number;
|
|
23
|
+
/** True when spawnSync killed the child for maxBuffer / stdio overflow. */
|
|
24
|
+
export declare function isBufferOverflowErrorCode(code: string | null | undefined): boolean;
|
|
15
25
|
/**
|
|
16
26
|
* Resolve the hookful-commit timeout. Preference order:
|
|
17
27
|
* 1. explicit `commitTimeoutMs` option (tests / programmatic callers)
|
|
@@ -38,21 +48,34 @@ export declare function formatCommitInterruptError(options: {
|
|
|
38
48
|
readonly signal: NodeJS.Signals;
|
|
39
49
|
readonly hookOutput?: string;
|
|
40
50
|
}): string;
|
|
51
|
+
/**
|
|
52
|
+
* First-class diagnosis when Node spawnSync kills the child because hook/git
|
|
53
|
+
* output exceeded maxBuffer (ENOBUFS / ERR_CHILD_PROCESS_STDIO_MAXBUFFER).
|
|
54
|
+
* Distinct from budget timeout and from external signal kills.
|
|
55
|
+
*/
|
|
56
|
+
export declare function formatCommitBufferError(options: {
|
|
57
|
+
readonly maxBufferBytes: number;
|
|
58
|
+
readonly errorCode?: string | null;
|
|
59
|
+
readonly hookOutput?: string;
|
|
60
|
+
}): string;
|
|
41
61
|
/**
|
|
42
62
|
* Classify a failed hookful `git commit` result.
|
|
43
63
|
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
64
|
+
* Order (first match wins):
|
|
65
|
+
* 1. `timedOut` (spawnSync ETIMEDOUT) → budget timeout diagnosis
|
|
66
|
+
* 2. buffer overflow (ENOBUFS / ERR_CHILD_PROCESS_STDIO_MAXBUFFER) → buffer-limit diagnosis
|
|
67
|
+
* 3. non-null `signal` → interrupt diagnosis (names the signal)
|
|
68
|
+
* 4. otherwise → hook/git stderr when present, else spawn errorCode / generic preserved-index
|
|
47
69
|
*
|
|
48
|
-
* Never lead with bare hook "passed" lines for
|
|
70
|
+
* Never lead with bare hook "passed" lines for first-class failure paths.
|
|
49
71
|
*/
|
|
50
72
|
export declare function classifyCommitFailure(result: {
|
|
51
73
|
readonly timedOut: boolean;
|
|
52
74
|
readonly signal: NodeJS.Signals | null;
|
|
53
75
|
readonly stderr: string;
|
|
54
76
|
readonly stdout: string;
|
|
55
|
-
|
|
77
|
+
readonly errorCode?: string | null;
|
|
78
|
+
}, timeoutMs: number, maxBufferBytes?: number): string;
|
|
56
79
|
/**
|
|
57
80
|
* Prefix every line of a setup failure so multi-line timeout diagnoses stay
|
|
58
81
|
* labeled when printed via console.error (avoids a single `wp setup: ` on the
|