@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.
Files changed (62) hide show
  1. package/catalog/agent/rules/pre-implementation.md +30 -8
  2. package/catalog/agent/skills/ultragoal/SKILL.md +5 -18
  3. package/dist/esm/blueprint/core/validation/task-blocks.js +7 -1
  4. package/dist/esm/blueprint/markdown/task-heading.d.ts +21 -0
  5. package/dist/esm/blueprint/markdown/task-heading.js +25 -0
  6. package/dist/esm/blueprint/trust/command-runner.d.ts +40 -0
  7. package/dist/esm/blueprint/trust/command-runner.js +265 -41
  8. package/dist/esm/build/cli-mcp-parity.js +5 -0
  9. package/dist/esm/ci/native-session-memory-cache.d.ts +2 -1
  10. package/dist/esm/ci/native-session-memory-cache.js +2 -1
  11. package/dist/esm/ci/release-progress.d.ts +37 -0
  12. package/dist/esm/ci/release-progress.js +139 -0
  13. package/dist/esm/cli/cli.d.ts +1 -1
  14. package/dist/esm/cli/cli.js +9 -0
  15. package/dist/esm/cli/commands/blueprint/abandon.d.ts +21 -0
  16. package/dist/esm/cli/commands/blueprint/abandon.js +178 -0
  17. package/dist/esm/cli/commands/blueprint/mutations.js +2 -3
  18. package/dist/esm/cli/commands/blueprint/router-dispatch.js +21 -0
  19. package/dist/esm/cli/commands/blueprint/router-output.js +1 -0
  20. package/dist/esm/cli/commands/blueprint/router.d.ts +2 -0
  21. package/dist/esm/cli/commands/blueprint/router.js +5 -2
  22. package/dist/esm/cli/commands/init/convergence-apply.d.ts +28 -5
  23. package/dist/esm/cli/commands/init/convergence-apply.js +75 -13
  24. package/dist/esm/cli/commands/opencode-probe.d.ts +11 -0
  25. package/dist/esm/cli/commands/opencode-probe.js +1 -1
  26. package/dist/esm/cli/commands/quality-runner.js +6 -2
  27. package/dist/esm/cli/commands/release-progress.d.ts +53 -0
  28. package/dist/esm/cli/commands/release-progress.js +155 -0
  29. package/dist/esm/cli/commands/review.js +74 -11
  30. package/dist/esm/cli/commands/typecheck.js +1 -1
  31. package/dist/esm/cli/commands/worktree/router-dispatch.d.ts +8 -0
  32. package/dist/esm/cli/commands/worktree/router-dispatch.js +25 -1
  33. package/dist/esm/cli/utils.d.ts +8 -1
  34. package/dist/esm/cli/utils.js +12 -2
  35. package/dist/esm/hooks/pretool-guard/validators/plan-frontmatter.js +3 -2
  36. package/dist/esm/mcp/blueprint/handlers/document-mutations.js +2 -1
  37. package/dist/esm/mcp/blueprint/handlers/task-advance.js +2 -12
  38. package/dist/esm/review/delivery-verifier.d.ts +33 -2
  39. package/dist/esm/review/delivery-verifier.js +130 -24
  40. package/dist/esm/review/execution/review-checkout.d.ts +7 -0
  41. package/dist/esm/review/execution/review-checkout.js +416 -27
  42. package/dist/esm/review/execution/sandbox/adapter.d.ts +37 -0
  43. package/dist/esm/review/execution/sandbox/adapter.js +39 -0
  44. package/dist/esm/review/execution/sandbox/env.d.ts +12 -0
  45. package/dist/esm/review/execution/sandbox/env.js +37 -0
  46. package/dist/esm/review/execution/sandbox/index.d.ts +5 -0
  47. package/dist/esm/review/execution/sandbox/index.js +4 -0
  48. package/dist/esm/review/execution/sandbox/policy.d.ts +19 -0
  49. package/dist/esm/review/execution/sandbox/policy.js +95 -0
  50. package/dist/esm/review/execution/sandbox/probe.d.ts +20 -0
  51. package/dist/esm/review/execution/sandbox/probe.js +97 -0
  52. package/dist/esm/review/execution/sandbox/shell-quote.d.ts +14 -0
  53. package/dist/esm/review/execution/sandbox/shell-quote.js +18 -0
  54. package/dist/esm/review/execution/sandbox/tmpdir.d.ts +9 -0
  55. package/dist/esm/review/execution/sandbox/tmpdir.js +36 -0
  56. package/dist/esm/review/execution/sandbox/types.d.ts +31 -0
  57. package/dist/esm/review/execution/sandbox/types.js +6 -0
  58. package/dist/esm/review/execution/supervisor.js +44 -5
  59. package/dist/esm/review/execution/types.d.ts +2 -0
  60. package/dist/esm/worktrees/owner-dirt.d.ts +40 -0
  61. package/dist/esm/worktrees/owner-dirt.js +104 -0
  62. 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
+ }
@@ -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;
@@ -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 { escapeRegex } from "#utils/string";
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 escapedId = escapeRegex(taskId);
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) {
@@ -18,6 +18,7 @@ export const BLUEPRINT_SUBCOMMANDS = [
18
18
  },
19
19
  { name: "start", usage: ["start <slug>"] },
20
20
  { name: "park", usage: ["park <slug>"] },
21
+ { name: "abandon", usage: ["abandon <slug> --confirm"] },
21
22
  {
22
23
  name: "task",
23
24
  usage: [
@@ -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
- * - `timedOut` (spawnSync ETIMEDOUT) → budget timeout diagnosis
45
- * - non-null `signal` without timedOutinterrupt diagnosis (names the signal)
46
- * - otherwise hook/git stderr when present, else a generic preserved-index message
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 timeout/interrupt paths.
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
- }, timeoutMs: number): string;
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