@webpresso/agent-kit 3.3.1 → 3.3.3
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/AGENTS.md.tpl +3 -5
- package/catalog/agent/rules/pre-implementation.md +15 -8
- package/catalog/agent/skills/ultragoal/SKILL.md +5 -18
- package/catalog/agent/skills/verify/SKILL.md +12 -12
- package/catalog/compose/admin-blocks-registry.json +659 -0
- package/dist/esm/blueprint/core/parser.js +2 -4
- package/dist/esm/blueprint/core/validation/criteria.js +2 -1
- package/dist/esm/blueprint/core/validation/task-blocks.js +7 -1
- package/dist/esm/blueprint/lifecycle/review-provenance.js +85 -30
- 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 +6 -0
- package/dist/esm/build/package-manifest.js +7 -2
- 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/audit-core.js +9 -1
- package/dist/esm/cli/commands/audit.js +16 -5
- package/dist/esm/cli/commands/blueprint/mutations.js +2 -3
- package/dist/esm/cli/commands/init/mcp-spec.d.ts +18 -3
- package/dist/esm/cli/commands/init/mcp-spec.js +54 -6
- package/dist/esm/cli/commands/init/scaffold-agents-md.js +5 -7
- package/dist/esm/cli/commands/package-manager.js +8 -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/utils.d.ts +8 -1
- package/dist/esm/cli/utils.js +12 -2
- package/dist/esm/compose/compose.d.ts +38 -0
- package/dist/esm/compose/compose.js +143 -0
- package/dist/esm/compose/escape-html.d.ts +5 -0
- package/dist/esm/compose/escape-html.js +12 -0
- package/dist/esm/compose/ops-report-render.d.ts +5 -0
- package/dist/esm/compose/ops-report-render.js +71 -0
- package/dist/esm/compose/ops-report-schema.d.ts +42 -0
- package/dist/esm/compose/ops-report-schema.js +46 -0
- package/dist/esm/compose/preview.d.ts +14 -0
- package/dist/esm/compose/preview.js +32 -0
- package/dist/esm/compose/registry-resolve.d.ts +47 -0
- package/dist/esm/compose/registry-resolve.js +133 -0
- package/dist/esm/git/changed-files.d.ts +20 -0
- package/dist/esm/git/changed-files.js +63 -0
- package/dist/esm/hooks/pretool-guard/validators/plan-frontmatter.js +3 -2
- package/dist/esm/hooks/stop/qa-changed-files.js +6 -1
- 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/mcp/blueprint/handlers/validate.js +6 -0
- package/dist/esm/mcp/tools/_names.d.ts +1 -1
- package/dist/esm/mcp/tools/_names.js +4 -0
- package/dist/esm/mcp/tools/_registry.js +8 -0
- package/dist/esm/mcp/tools/release-progress.d.ts +9 -0
- package/dist/esm/mcp/tools/release-progress.js +110 -0
- package/dist/esm/mcp/tools/wp-ui-blocks-list.d.ts +3 -0
- package/dist/esm/mcp/tools/wp-ui-blocks-list.js +43 -0
- package/dist/esm/mcp/tools/wp-ui-compose.d.ts +3 -0
- package/dist/esm/mcp/tools/wp-ui-compose.js +62 -0
- package/dist/esm/mcp/tools/wp-ui-preview.d.ts +3 -0
- package/dist/esm/mcp/tools/wp-ui-preview.js +52 -0
- package/dist/esm/package.json +2 -0
- package/dist/esm/review/delivery-verifier.d.ts +33 -2
- package/dist/esm/review/delivery-verifier.js +130 -24
- package/dist/esm/review/execution/review-checkout.d.ts +1 -0
- package/dist/esm/review/execution/review-checkout.js +386 -23
- package/dist/esm/review/execution/sandbox/adapter.d.ts +37 -0
- package/dist/esm/review/execution/sandbox/adapter.js +39 -0
- package/dist/esm/review/execution/sandbox/env.d.ts +12 -0
- package/dist/esm/review/execution/sandbox/env.js +37 -0
- package/dist/esm/review/execution/sandbox/index.d.ts +5 -0
- package/dist/esm/review/execution/sandbox/index.js +4 -0
- package/dist/esm/review/execution/sandbox/policy.d.ts +19 -0
- package/dist/esm/review/execution/sandbox/policy.js +95 -0
- package/dist/esm/review/execution/sandbox/probe.d.ts +20 -0
- package/dist/esm/review/execution/sandbox/probe.js +97 -0
- package/dist/esm/review/execution/sandbox/shell-quote.d.ts +14 -0
- package/dist/esm/review/execution/sandbox/shell-quote.js +18 -0
- package/dist/esm/review/execution/sandbox/tmpdir.d.ts +9 -0
- package/dist/esm/review/execution/sandbox/tmpdir.js +36 -0
- package/dist/esm/review/execution/sandbox/types.d.ts +31 -0
- package/dist/esm/review/execution/sandbox/types.js +6 -0
- package/dist/esm/review/execution/supervisor.js +44 -5
- package/dist/esm/review/execution/types.d.ts +2 -0
- package/package.json +24 -12
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
/** Flat section only — no nesting (plan Fx17). */
|
|
3
|
+
export declare const opsReportSectionSchema: z.ZodObject<{
|
|
4
|
+
heading: z.ZodString;
|
|
5
|
+
body: z.ZodOptional<z.ZodString>;
|
|
6
|
+
list: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
7
|
+
}, z.core.$strict>;
|
|
8
|
+
export declare const opsReportMetricSchema: z.ZodObject<{
|
|
9
|
+
label: z.ZodString;
|
|
10
|
+
value: z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>;
|
|
11
|
+
}, z.core.$strict>;
|
|
12
|
+
export declare const opsReportTableSchema: z.ZodObject<{
|
|
13
|
+
title: z.ZodOptional<z.ZodString>;
|
|
14
|
+
headers: z.ZodArray<z.ZodString>;
|
|
15
|
+
rows: z.ZodArray<z.ZodArray<z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>>>;
|
|
16
|
+
}, z.core.$strict>;
|
|
17
|
+
/**
|
|
18
|
+
* Minimal ops-report IR. Not a general page AST.
|
|
19
|
+
* Nesting under sections is rejected by the section schema (flat only).
|
|
20
|
+
*/
|
|
21
|
+
/** Flat ops-report only; section objects are `.strict()` so nested keys fail at the section boundary. */
|
|
22
|
+
export declare const opsReportSpecSchema: z.ZodObject<{
|
|
23
|
+
title: z.ZodString;
|
|
24
|
+
generatedAt: z.ZodString;
|
|
25
|
+
metrics: z.ZodDefault<z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
26
|
+
label: z.ZodString;
|
|
27
|
+
value: z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>;
|
|
28
|
+
}, z.core.$strict>>>>;
|
|
29
|
+
tables: z.ZodDefault<z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
30
|
+
title: z.ZodOptional<z.ZodString>;
|
|
31
|
+
headers: z.ZodArray<z.ZodString>;
|
|
32
|
+
rows: z.ZodArray<z.ZodArray<z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>>>;
|
|
33
|
+
}, z.core.$strict>>>>;
|
|
34
|
+
warnings: z.ZodDefault<z.ZodOptional<z.ZodArray<z.ZodString>>>;
|
|
35
|
+
sections: z.ZodDefault<z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
36
|
+
heading: z.ZodString;
|
|
37
|
+
body: z.ZodOptional<z.ZodString>;
|
|
38
|
+
list: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
39
|
+
}, z.core.$strict>>>>;
|
|
40
|
+
}, z.core.$strict>;
|
|
41
|
+
export type OpsReportSpec = z.infer<typeof opsReportSpecSchema>;
|
|
42
|
+
export type OpsReportSection = z.infer<typeof opsReportSectionSchema>;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
/** Flat section only — no nesting (plan Fx17). */
|
|
3
|
+
export const opsReportSectionSchema = z
|
|
4
|
+
.object({
|
|
5
|
+
heading: z.string().min(1),
|
|
6
|
+
body: z.string().optional(),
|
|
7
|
+
list: z.array(z.string()).optional(),
|
|
8
|
+
})
|
|
9
|
+
.strict()
|
|
10
|
+
.superRefine((section, ctx) => {
|
|
11
|
+
if (section.body === undefined && section.list === undefined) {
|
|
12
|
+
ctx.addIssue({
|
|
13
|
+
code: "custom",
|
|
14
|
+
message: "section requires body and/or list",
|
|
15
|
+
path: ["body"],
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
});
|
|
19
|
+
export const opsReportMetricSchema = z
|
|
20
|
+
.object({
|
|
21
|
+
label: z.string().min(1),
|
|
22
|
+
value: z.union([z.string(), z.number()]),
|
|
23
|
+
})
|
|
24
|
+
.strict();
|
|
25
|
+
export const opsReportTableSchema = z
|
|
26
|
+
.object({
|
|
27
|
+
title: z.string().optional(),
|
|
28
|
+
headers: z.array(z.string()).min(1),
|
|
29
|
+
rows: z.array(z.array(z.union([z.string(), z.number()]))),
|
|
30
|
+
})
|
|
31
|
+
.strict();
|
|
32
|
+
/**
|
|
33
|
+
* Minimal ops-report IR. Not a general page AST.
|
|
34
|
+
* Nesting under sections is rejected by the section schema (flat only).
|
|
35
|
+
*/
|
|
36
|
+
/** Flat ops-report only; section objects are `.strict()` so nested keys fail at the section boundary. */
|
|
37
|
+
export const opsReportSpecSchema = z
|
|
38
|
+
.object({
|
|
39
|
+
title: z.string().min(1),
|
|
40
|
+
generatedAt: z.string().min(1),
|
|
41
|
+
metrics: z.array(opsReportMetricSchema).optional().default([]),
|
|
42
|
+
tables: z.array(opsReportTableSchema).optional().default([]),
|
|
43
|
+
warnings: z.array(z.string()).optional().default([]),
|
|
44
|
+
sections: z.array(opsReportSectionSchema).optional().default([]),
|
|
45
|
+
})
|
|
46
|
+
.strict();
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export interface PreviewOpsReportOptions {
|
|
2
|
+
readonly cwd?: string;
|
|
3
|
+
readonly spec: unknown;
|
|
4
|
+
/** Override output path (tests). */
|
|
5
|
+
readonly outputPath?: string;
|
|
6
|
+
}
|
|
7
|
+
export interface PreviewOpsReportResult {
|
|
8
|
+
readonly path: string;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Validate ops-report spec, write HTML under state-root (or override), return path only.
|
|
12
|
+
* No browser open.
|
|
13
|
+
*/
|
|
14
|
+
export declare function previewOpsReport(options: PreviewOpsReportOptions): PreviewOpsReportResult;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { mkdirSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { getSurfacePath, NotInGitRepoError } from "#paths/state-root.js";
|
|
4
|
+
import { composeValidate } from "./compose.js";
|
|
5
|
+
import { renderOpsReportHtml } from "./ops-report-render.js";
|
|
6
|
+
/**
|
|
7
|
+
* Validate ops-report spec, write HTML under state-root (or override), return path only.
|
|
8
|
+
* No browser open.
|
|
9
|
+
*/
|
|
10
|
+
export function previewOpsReport(options) {
|
|
11
|
+
const composed = composeValidate({ profile: "ops-report", spec: options.spec }, { cwd: options.cwd });
|
|
12
|
+
if (!composed.ok) {
|
|
13
|
+
const detail = composed.diagnostics.map((d) => `${d.path}: ${d.message}`).join("; ");
|
|
14
|
+
throw new Error(`ops-report compose failed: ${detail}`);
|
|
15
|
+
}
|
|
16
|
+
const html = renderOpsReportHtml(composed.spec);
|
|
17
|
+
const path = options.outputPath ??
|
|
18
|
+
(() => {
|
|
19
|
+
try {
|
|
20
|
+
return getSurfacePath("compose/ops-report.html", "worktree", options.cwd);
|
|
21
|
+
}
|
|
22
|
+
catch (error) {
|
|
23
|
+
if (error instanceof NotInGitRepoError) {
|
|
24
|
+
return join(options.cwd ?? process.cwd(), ".webpresso", "compose", "ops-report.html");
|
|
25
|
+
}
|
|
26
|
+
throw error;
|
|
27
|
+
}
|
|
28
|
+
})();
|
|
29
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
30
|
+
writeFileSync(path, html, "utf8");
|
|
31
|
+
return { path };
|
|
32
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export declare const blockRegistrySchema: z.ZodObject<{
|
|
3
|
+
$schema: z.ZodOptional<z.ZodString>;
|
|
4
|
+
version: z.ZodString;
|
|
5
|
+
minVersion: z.ZodOptional<z.ZodString>;
|
|
6
|
+
blocks: z.ZodArray<z.ZodObject<{
|
|
7
|
+
name: z.ZodString;
|
|
8
|
+
view: z.ZodEnum<{
|
|
9
|
+
detail: "detail";
|
|
10
|
+
form: "form";
|
|
11
|
+
list: "list";
|
|
12
|
+
}>;
|
|
13
|
+
description: z.ZodOptional<z.ZodString>;
|
|
14
|
+
props: z.ZodOptional<z.ZodArray<z.ZodUnknown>>;
|
|
15
|
+
variants: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
16
|
+
slots: z.ZodOptional<z.ZodArray<z.ZodUnknown>>;
|
|
17
|
+
permissions: z.ZodOptional<z.ZodArray<z.ZodUnknown>>;
|
|
18
|
+
deprecated: z.ZodOptional<z.ZodBoolean>;
|
|
19
|
+
}, z.core.$loose>>;
|
|
20
|
+
}, z.core.$strip>;
|
|
21
|
+
export type BlockRegistry = z.infer<typeof blockRegistrySchema>;
|
|
22
|
+
export type RegistrySource = "project" | "snapshot";
|
|
23
|
+
export interface ResolvedBlockRegistry {
|
|
24
|
+
readonly registry: BlockRegistry;
|
|
25
|
+
readonly source: RegistrySource;
|
|
26
|
+
readonly registryVersion: string;
|
|
27
|
+
readonly path: string;
|
|
28
|
+
readonly warnings: readonly string[];
|
|
29
|
+
/** True when a `.webpresso/` tree was found but project registry was missing. */
|
|
30
|
+
readonly webpressoPresentWithoutRegistry: boolean;
|
|
31
|
+
}
|
|
32
|
+
export declare function defaultSnapshotPath(): string;
|
|
33
|
+
/**
|
|
34
|
+
* Two-tier registry resolution (v0):
|
|
35
|
+
* 1. Nearest project `.webpresso/` (within git/webpressorc boundary) — if it has
|
|
36
|
+
* generated blocks registry, use it
|
|
37
|
+
* 2. Else catalog snapshot (with warning when that project `.webpresso/` lacks registry)
|
|
38
|
+
*
|
|
39
|
+
* Never dynamically imports @webpresso/schema.
|
|
40
|
+
* Never climbs past the project boundary into `~/.webpresso` (managed worktree host)
|
|
41
|
+
* or other ancestor projects (avoids silent wrong-project catalog).
|
|
42
|
+
*/
|
|
43
|
+
export declare function resolveBlockRegistry(options?: {
|
|
44
|
+
readonly cwd?: string;
|
|
45
|
+
readonly snapshotPath?: string;
|
|
46
|
+
}): ResolvedBlockRegistry;
|
|
47
|
+
export declare function blockNamesForView(registry: BlockRegistry, view: "list" | "detail" | "form"): ReadonlySet<string>;
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { dirname, isAbsolute, join, resolve } from "node:path";
|
|
4
|
+
import { resolvePackageAssetPreferred } from "#utils/package-assets.js";
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
const REGISTRY_RELATIVE = join("generated", "frontend", "blocks", "registry.json");
|
|
7
|
+
const MAX_WALK = 48;
|
|
8
|
+
/** Stop climbing past these markers (mirrors project-root clamps elsewhere). */
|
|
9
|
+
function isProjectBoundary(dir) {
|
|
10
|
+
return existsSync(join(dir, ".git")) || existsSync(join(dir, ".webpressorc.json"));
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* `~/.webpresso` is the managed-worktree host, never a product project root.
|
|
14
|
+
* Matching it would mis-label every uninitialized checkout under $HOME.
|
|
15
|
+
*/
|
|
16
|
+
function isManagedHostWebpresso(candidate) {
|
|
17
|
+
return resolve(candidate) === resolve(join(homedir(), ".webpresso"));
|
|
18
|
+
}
|
|
19
|
+
const blockDefinitionSchema = z
|
|
20
|
+
.object({
|
|
21
|
+
name: z.string(),
|
|
22
|
+
view: z.enum(["list", "detail", "form"]),
|
|
23
|
+
description: z.string().optional(),
|
|
24
|
+
props: z.array(z.unknown()).optional(),
|
|
25
|
+
variants: z.array(z.string()).optional(),
|
|
26
|
+
slots: z.array(z.unknown()).optional(),
|
|
27
|
+
permissions: z.array(z.unknown()).optional(),
|
|
28
|
+
deprecated: z.boolean().optional(),
|
|
29
|
+
})
|
|
30
|
+
.passthrough();
|
|
31
|
+
export const blockRegistrySchema = z.object({
|
|
32
|
+
$schema: z.string().optional(),
|
|
33
|
+
version: z.string(),
|
|
34
|
+
minVersion: z.string().optional(),
|
|
35
|
+
blocks: z.array(blockDefinitionSchema).min(1),
|
|
36
|
+
});
|
|
37
|
+
/**
|
|
38
|
+
* Walk up for nearest project `.webpresso` directory (not a file).
|
|
39
|
+
* Clamped at the enclosing project boundary (`.git` / `.webpressorc.json`)
|
|
40
|
+
* and never treats `~/.webpresso` (managed worktree host) as a project root.
|
|
41
|
+
*/
|
|
42
|
+
function walkUpForWebpressoDir(startDir) {
|
|
43
|
+
let dir = resolve(startDir);
|
|
44
|
+
for (let i = 0; i < MAX_WALK; i++) {
|
|
45
|
+
const candidate = join(dir, ".webpresso");
|
|
46
|
+
if (existsSync(candidate)) {
|
|
47
|
+
try {
|
|
48
|
+
if (statSync(candidate).isDirectory() && !isManagedHostWebpresso(candidate)) {
|
|
49
|
+
return candidate;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
// ignore unreadable markers
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
// Do not climb past the repo / consumer root into $HOME tooling dirs.
|
|
57
|
+
if (isProjectBoundary(dir))
|
|
58
|
+
return null;
|
|
59
|
+
const parent = dirname(dir);
|
|
60
|
+
if (parent === dir)
|
|
61
|
+
return null;
|
|
62
|
+
dir = parent;
|
|
63
|
+
}
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
export function defaultSnapshotPath() {
|
|
67
|
+
// Portable across src/ and dist/esm/ via package-root walk (no fixed-depth joins).
|
|
68
|
+
const assetPath = resolvePackageAssetPreferred(["catalog/compose/admin-blocks-registry.json"]);
|
|
69
|
+
if (!existsSync(assetPath)) {
|
|
70
|
+
throw new Error(`admin-blocks-registry.json snapshot not found at package asset path: ${assetPath}`);
|
|
71
|
+
}
|
|
72
|
+
return assetPath;
|
|
73
|
+
}
|
|
74
|
+
function loadRegistryFile(path) {
|
|
75
|
+
try {
|
|
76
|
+
const raw = readFileSync(path, "utf8");
|
|
77
|
+
const parsed = JSON.parse(raw);
|
|
78
|
+
return blockRegistrySchema.parse(parsed);
|
|
79
|
+
}
|
|
80
|
+
catch (error) {
|
|
81
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
82
|
+
throw new Error(`Failed to load block registry at ${path}: ${detail}`, { cause: error });
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Two-tier registry resolution (v0):
|
|
87
|
+
* 1. Nearest project `.webpresso/` (within git/webpressorc boundary) — if it has
|
|
88
|
+
* generated blocks registry, use it
|
|
89
|
+
* 2. Else catalog snapshot (with warning when that project `.webpresso/` lacks registry)
|
|
90
|
+
*
|
|
91
|
+
* Never dynamically imports @webpresso/schema.
|
|
92
|
+
* Never climbs past the project boundary into `~/.webpresso` (managed worktree host)
|
|
93
|
+
* or other ancestor projects (avoids silent wrong-project catalog).
|
|
94
|
+
*/
|
|
95
|
+
export function resolveBlockRegistry(options = {}) {
|
|
96
|
+
const start = options.cwd
|
|
97
|
+
? isAbsolute(options.cwd)
|
|
98
|
+
? options.cwd
|
|
99
|
+
: resolve(process.cwd(), options.cwd)
|
|
100
|
+
: process.cwd();
|
|
101
|
+
const webpressoRoot = walkUpForWebpressoDir(start);
|
|
102
|
+
const warnings = [];
|
|
103
|
+
if (webpressoRoot) {
|
|
104
|
+
const projectPath = join(webpressoRoot, REGISTRY_RELATIVE);
|
|
105
|
+
if (existsSync(projectPath)) {
|
|
106
|
+
const registry = loadRegistryFile(projectPath);
|
|
107
|
+
return {
|
|
108
|
+
registry,
|
|
109
|
+
source: "project",
|
|
110
|
+
registryVersion: registry.version,
|
|
111
|
+
path: projectPath,
|
|
112
|
+
warnings,
|
|
113
|
+
webpressoPresentWithoutRegistry: false,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
const snapshotPath = options.snapshotPath ?? defaultSnapshotPath();
|
|
118
|
+
const registry = loadRegistryFile(snapshotPath);
|
|
119
|
+
if (webpressoRoot) {
|
|
120
|
+
warnings.push(`Project has .webpresso/ at ${webpressoRoot} but no ${join(".webpresso", REGISTRY_RELATIVE)}; using catalog snapshot ${registry.version}. Run schema frontend codegen / converge to materialize a project registry.`);
|
|
121
|
+
}
|
|
122
|
+
return {
|
|
123
|
+
registry,
|
|
124
|
+
source: "snapshot",
|
|
125
|
+
registryVersion: registry.version,
|
|
126
|
+
path: snapshotPath,
|
|
127
|
+
warnings,
|
|
128
|
+
webpressoPresentWithoutRegistry: webpressoRoot !== null,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
export function blockNamesForView(registry, view) {
|
|
132
|
+
return new Set(registry.blocks.filter((block) => block.view === view).map((block) => block.name));
|
|
133
|
+
}
|
|
@@ -5,6 +5,26 @@ export interface ChangedFilesResult {
|
|
|
5
5
|
readonly reason: ChangedFilesReason;
|
|
6
6
|
}
|
|
7
7
|
export declare function defaultBranchBaseRef(env?: NodeJS.ProcessEnv): string;
|
|
8
|
+
/**
|
|
9
|
+
* Latest fork point of `HEAD` from the base branch, given a caller-supplied base.
|
|
10
|
+
*
|
|
11
|
+
* CI passes `--base "$PR_BASE_SHA"` (`github.event.pull_request.base.sha`),
|
|
12
|
+
* which is pinned when the pull request is opened and does NOT advance as the
|
|
13
|
+
* base branch does. Once the branch is rebased onto a newer base — the
|
|
14
|
+
* documented recovery flow for a stale branch — that pinned SHA is no longer the
|
|
15
|
+
* fork point, so `<pinned>...HEAD` reports every commit merged into the base
|
|
16
|
+
* branch since the PR opened as if this branch had authored it. Scoped gates
|
|
17
|
+
* then fail a branch for unrelated, already-merged work.
|
|
18
|
+
*
|
|
19
|
+
* `git merge-base` against the LIVE base branch ref is recomputed from current
|
|
20
|
+
* history, so it tracks the rebase and yields the true fork point. Prefer it,
|
|
21
|
+
* but only when it is provably a descendant of the caller's base (never widen
|
|
22
|
+
* the range the caller asked for) and does not swallow `HEAD` (a base branch
|
|
23
|
+
* that already contains `HEAD` would otherwise empty the scope). Otherwise the
|
|
24
|
+
* caller's base is returned verbatim, so an unrelated or unresolvable live ref
|
|
25
|
+
* can never silently replace an explicit base.
|
|
26
|
+
*/
|
|
27
|
+
export declare function resolveBranchDiffBase(cwd?: string, base?: string, liveBaseRef?: string): string;
|
|
8
28
|
export declare function getGitTopLevel(cwd?: string): string | null;
|
|
9
29
|
export declare function getStagedFiles(cwd?: string): ChangedFilesResult;
|
|
10
30
|
export declare function getBranchChangedFiles(cwd?: string, base?: string): ChangedFilesResult;
|
|
@@ -5,6 +5,69 @@ export function defaultBranchBaseRef(env = process.env) {
|
|
|
5
5
|
const githubBaseRef = env.GITHUB_BASE_REF?.trim();
|
|
6
6
|
return `origin/${githubBaseRef && githubBaseRef.length > 0 ? githubBaseRef : "main"}`;
|
|
7
7
|
}
|
|
8
|
+
/**
|
|
9
|
+
* Latest fork point of `HEAD` from the base branch, given a caller-supplied base.
|
|
10
|
+
*
|
|
11
|
+
* CI passes `--base "$PR_BASE_SHA"` (`github.event.pull_request.base.sha`),
|
|
12
|
+
* which is pinned when the pull request is opened and does NOT advance as the
|
|
13
|
+
* base branch does. Once the branch is rebased onto a newer base — the
|
|
14
|
+
* documented recovery flow for a stale branch — that pinned SHA is no longer the
|
|
15
|
+
* fork point, so `<pinned>...HEAD` reports every commit merged into the base
|
|
16
|
+
* branch since the PR opened as if this branch had authored it. Scoped gates
|
|
17
|
+
* then fail a branch for unrelated, already-merged work.
|
|
18
|
+
*
|
|
19
|
+
* `git merge-base` against the LIVE base branch ref is recomputed from current
|
|
20
|
+
* history, so it tracks the rebase and yields the true fork point. Prefer it,
|
|
21
|
+
* but only when it is provably a descendant of the caller's base (never widen
|
|
22
|
+
* the range the caller asked for) and does not swallow `HEAD` (a base branch
|
|
23
|
+
* that already contains `HEAD` would otherwise empty the scope). Otherwise the
|
|
24
|
+
* caller's base is returned verbatim, so an unrelated or unresolvable live ref
|
|
25
|
+
* can never silently replace an explicit base.
|
|
26
|
+
*/
|
|
27
|
+
export function resolveBranchDiffBase(cwd = process.cwd(), base = defaultBranchBaseRef(), liveBaseRef = defaultBranchBaseRef()) {
|
|
28
|
+
const forkPoint = readMergeBase(cwd, liveBaseRef, "HEAD");
|
|
29
|
+
if (forkPoint === null || forkPoint === readCommit(cwd, "HEAD"))
|
|
30
|
+
return base;
|
|
31
|
+
return isAncestorCommit(cwd, base, forkPoint) ? forkPoint : base;
|
|
32
|
+
}
|
|
33
|
+
function readCommit(cwd, ref) {
|
|
34
|
+
try {
|
|
35
|
+
const output = execFileSync("git", ["rev-parse", "--verify", "--quiet", `${ref}^{commit}`], {
|
|
36
|
+
cwd,
|
|
37
|
+
encoding: "utf8",
|
|
38
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
39
|
+
}).trim();
|
|
40
|
+
return output.length > 0 ? output : null;
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
function readMergeBase(cwd, left, right) {
|
|
47
|
+
try {
|
|
48
|
+
const output = execFileSync("git", ["merge-base", left, right], {
|
|
49
|
+
cwd,
|
|
50
|
+
encoding: "utf8",
|
|
51
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
52
|
+
}).trim();
|
|
53
|
+
return output.length > 0 ? output : null;
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
function isAncestorCommit(cwd, ancestor, descendant) {
|
|
60
|
+
try {
|
|
61
|
+
execFileSync("git", ["merge-base", "--is-ancestor", ancestor, descendant], {
|
|
62
|
+
cwd,
|
|
63
|
+
stdio: ["ignore", "ignore", "ignore"],
|
|
64
|
+
});
|
|
65
|
+
return true;
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
8
71
|
export function getGitTopLevel(cwd = process.cwd()) {
|
|
9
72
|
try {
|
|
10
73
|
const output = execFileSync("git", ["rev-parse", "--show-toplevel"], {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { parse as parseYaml } from "yaml";
|
|
2
2
|
import { getContent, getFilePath } from "#hooks/shared/types";
|
|
3
|
+
import { buildTaskHeadingRegex, buildWrongDepthTaskHeadingRegex } from "#markdown/task-heading";
|
|
3
4
|
import { getNonCanonicalPlanningPathViolation, isBlueprintPath, isCanonicalBlueprintDocumentPath, } from "./path-contract.js";
|
|
4
5
|
import { createSkipResult } from "./skip-result.js";
|
|
5
6
|
// Keep aligned with webpresso/blueprint planStatusSchema + plan type enum.
|
|
@@ -53,10 +54,10 @@ export function collectFieldViolations(data) {
|
|
|
53
54
|
return violations;
|
|
54
55
|
}
|
|
55
56
|
export function countTaskHeadings(content) {
|
|
56
|
-
return content.match(
|
|
57
|
+
return content.match(buildTaskHeadingRegex("gm"))?.length ?? 0;
|
|
57
58
|
}
|
|
58
59
|
export function detectWrongTaskFormat(content) {
|
|
59
|
-
return content.match(
|
|
60
|
+
return content.match(buildWrongDepthTaskHeadingRegex("gm"))?.length ?? 0;
|
|
60
61
|
}
|
|
61
62
|
export function validatePlanFrontmatter(input) {
|
|
62
63
|
const filePath = getFilePath(input);
|
|
@@ -45,12 +45,17 @@ export function filterQaFiles(files) {
|
|
|
45
45
|
export function getTypecheckFiles(files) {
|
|
46
46
|
return files.filter((f) => TYPECHECKABLE_EXTENSIONS.has(extname(f)));
|
|
47
47
|
}
|
|
48
|
+
const TEST_SOURCE_EXTENSIONS = new Set([".ts", ".tsx", ".mts", ".cts", ".js", ".jsx"]);
|
|
48
49
|
export function findTestFiles(sourceFile, projectDir) {
|
|
49
50
|
const ext = extname(sourceFile);
|
|
50
51
|
const base = basename(sourceFile, ext);
|
|
51
52
|
const dir = dirname(sourceFile);
|
|
52
|
-
|
|
53
|
+
// Only treat real test modules as tests. Names like `tsconfig.test.json` end
|
|
54
|
+
// with `.test` after stripping the extension and must not become vitest filters.
|
|
55
|
+
if (TEST_SOURCE_EXTENSIONS.has(ext) &&
|
|
56
|
+
(base.endsWith(".test") || base.endsWith(".integration.test"))) {
|
|
53
57
|
return [sourceFile];
|
|
58
|
+
}
|
|
54
59
|
const pattern = join(dir, `${base}.{test,integration.test}{.ts,.tsx}`);
|
|
55
60
|
return globSync(pattern, { cwd: projectDir, withFileTypes: false });
|
|
56
61
|
}
|
|
@@ -4,6 +4,7 @@ import { z } from "zod";
|
|
|
4
4
|
import matter from "gray-matter";
|
|
5
5
|
import { parseBlueprint } from "#core/parser";
|
|
6
6
|
import { taskIdSchema } from "#core/schema";
|
|
7
|
+
import { formatTaskHeading } from "#markdown/task-heading";
|
|
7
8
|
import { evaluateApprovalGate, formatApprovalGateRequirement } from "#lifecycle/audit";
|
|
8
9
|
import { canonicalizeBlueprint } from "#lifecycle/canonicalize.js";
|
|
9
10
|
import { BlueprintCreationService } from "#service/BlueprintCreationService";
|
|
@@ -290,7 +291,7 @@ function renderBlueprintMarkdownFromDocument(slug, document) {
|
|
|
290
291
|
"",
|
|
291
292
|
];
|
|
292
293
|
const taskBlocks = document.tasks.flatMap((task) => [
|
|
293
|
-
|
|
294
|
+
formatTaskHeading(task.id, task.title),
|
|
294
295
|
"",
|
|
295
296
|
`**Status:** ${task.status}`,
|
|
296
297
|
...(task.wave ? [`**Wave:** ${task.wave}`] : []),
|
|
@@ -11,23 +11,13 @@ import { toStr } from "#mcp/blueprint/_shared/payload";
|
|
|
11
11
|
import { findBlueprintDir, resolveToolProject } from "#mcp/blueprint/_shared/project";
|
|
12
12
|
import { resolveSyncAdapter, runPlatformMutationSync } from "#mcp/blueprint/_shared/sync";
|
|
13
13
|
import { writeFileAtomic } from "#shared-utils/write-json-file.js";
|
|
14
|
-
import {
|
|
14
|
+
import { buildTaskHeaderRegexForId } from "#markdown/task-heading";
|
|
15
15
|
import { withMarkdownWriteLock } from "#db/paths.js";
|
|
16
16
|
import { resolveBlueprintRoot } from "#utils/blueprint-root.js";
|
|
17
17
|
const ALL_STATES = ["draft", "planned", "in-progress", "parked", "archived", "completed"];
|
|
18
|
-
/**
|
|
19
|
-
* Matches the canonical task-heading grammar `parser.ts`'s `extractTasks`
|
|
20
|
-
* uses (`^####\s+(?:\[lane\]\s+)?Task\s+<id>:`), not a looser ad hoc pattern.
|
|
21
|
-
* The id is anchored on a trailing colon so "1.1" cannot match "1.10", and
|
|
22
|
-
* an optional `[lane]` prefix is accepted the same way the canonical parser
|
|
23
|
-
* does.
|
|
24
|
-
*/
|
|
25
|
-
function buildTaskHeadingPattern(taskId) {
|
|
26
|
-
return new RegExp(`^####\\s+(?:\\[[^\\]]+\\]\\s+)?Task\\s+${escapeRegex(taskId)}:`, "u");
|
|
27
|
-
}
|
|
28
18
|
function findTaskStatusLine(markdown, taskId) {
|
|
29
19
|
const lines = markdown.split("\n");
|
|
30
|
-
const headingPattern =
|
|
20
|
+
const headingPattern = buildTaskHeaderRegexForId(taskId);
|
|
31
21
|
let inBlock = false;
|
|
32
22
|
for (let i = 0; i < lines.length; i++) {
|
|
33
23
|
const line = lines[i] ?? "";
|
|
@@ -41,6 +41,12 @@ export function runValidate(filePath) {
|
|
|
41
41
|
gaps.push(`Invalid frontmatter field: last_updated (${lastUpdated})`);
|
|
42
42
|
}
|
|
43
43
|
const body = fm.content;
|
|
44
|
+
// Deliberately LOOSER than the canonical grammar in `#markdown/task-heading`
|
|
45
|
+
// (`Task\s+\S` accepts any id, not just `\d+(?:\.\d+)+`, and needs no title).
|
|
46
|
+
// This is a gap REPORTER: it has to see a malformed heading in order to tell
|
|
47
|
+
// the author about it. Converging it onto the strict matcher would make
|
|
48
|
+
// exactly the malformed tasks it exists to catch invisible again — the
|
|
49
|
+
// reported gaps would silently drop to zero. Do not "de-duplicate" this.
|
|
44
50
|
const taskHeaderRegex = /^####\s+(?:\[[^\]]+\]\s+)?Task\s+\S/m;
|
|
45
51
|
if (!taskHeaderRegex.test(body))
|
|
46
52
|
gaps.push('No "#### Task" sections found');
|
|
@@ -8,4 +8,4 @@
|
|
|
8
8
|
* is enforced by `_names.test.ts` against `COMPILED_TOOL_REGISTRY`, so this list
|
|
9
9
|
* cannot silently drift.
|
|
10
10
|
*/
|
|
11
|
-
export declare const WP_TOOL_NAMES: readonly ["wp_audit", "wp_audits", "wp_bench", "wp_ci_act", "wp_ci_preflight", "wp_e2e", "wp_fleet_status", "wp_format", "wp_gain", "wp_lint", "wp_pr_status", "wp_pr_upsert", "wp_pr_wait", "wp_qa", "wp_release_readiness", "wp_review_run", "wp_run_wait", "wp_session_batch_execute", "wp_session_capture", "wp_session_context", "wp_session_doctor", "wp_session_execute", "wp_session_execute_file", "wp_session_fetch_and_index", "wp_session_id", "wp_session_index", "wp_session_info", "wp_session_purge", "wp_session_retrieve", "wp_session_restore", "wp_session_search", "wp_session_snapshot", "wp_session_stats", "wp_test", "wp_typecheck", "wp_ultragoal_cancel", "wp_ultragoal_handoff", "wp_ultragoal_new", "wp_ultragoal_run", "wp_ultragoal_status", "wp_worker_tail", "wp_worktree"];
|
|
11
|
+
export declare const WP_TOOL_NAMES: readonly ["wp_audit", "wp_audits", "wp_bench", "wp_ci_act", "wp_ci_preflight", "wp_e2e", "wp_fleet_status", "wp_format", "wp_gain", "wp_lint", "wp_pr_status", "wp_pr_upsert", "wp_pr_wait", "wp_qa", "wp_release_progress", "wp_release_readiness", "wp_review_run", "wp_run_wait", "wp_session_batch_execute", "wp_session_capture", "wp_session_context", "wp_session_doctor", "wp_session_execute", "wp_session_execute_file", "wp_session_fetch_and_index", "wp_session_id", "wp_session_index", "wp_session_info", "wp_session_purge", "wp_session_retrieve", "wp_session_restore", "wp_session_search", "wp_session_snapshot", "wp_session_stats", "wp_test", "wp_typecheck", "wp_ui_blocks_list", "wp_ui_compose", "wp_ui_preview", "wp_ultragoal_cancel", "wp_ultragoal_handoff", "wp_ultragoal_new", "wp_ultragoal_run", "wp_ultragoal_status", "wp_worker_tail", "wp_worktree"];
|
|
@@ -23,6 +23,7 @@ export const WP_TOOL_NAMES = [
|
|
|
23
23
|
"wp_pr_upsert",
|
|
24
24
|
"wp_pr_wait",
|
|
25
25
|
"wp_qa",
|
|
26
|
+
"wp_release_progress",
|
|
26
27
|
"wp_release_readiness",
|
|
27
28
|
"wp_review_run",
|
|
28
29
|
"wp_run_wait",
|
|
@@ -44,6 +45,9 @@ export const WP_TOOL_NAMES = [
|
|
|
44
45
|
"wp_session_stats",
|
|
45
46
|
"wp_test",
|
|
46
47
|
"wp_typecheck",
|
|
48
|
+
"wp_ui_blocks_list",
|
|
49
|
+
"wp_ui_compose",
|
|
50
|
+
"wp_ui_preview",
|
|
47
51
|
"wp_ultragoal_cancel",
|
|
48
52
|
"wp_ultragoal_handoff",
|
|
49
53
|
"wp_ultragoal_new",
|
|
@@ -12,6 +12,7 @@ import prStatus from "./pr-status.js";
|
|
|
12
12
|
import prUpsert from "./pr-upsert.js";
|
|
13
13
|
import prWait from "./pr-wait.js";
|
|
14
14
|
import qa from "./qa.js";
|
|
15
|
+
import releaseProgress from "./release-progress.js";
|
|
15
16
|
import releaseReadiness from "./release-readiness.js";
|
|
16
17
|
import reviewRun from "./review-run.js";
|
|
17
18
|
import runWait from "./run-wait.js";
|
|
@@ -33,6 +34,9 @@ import sessionSnapshot from "./session-snapshot.js";
|
|
|
33
34
|
import sessionStats from "./session-stats.js";
|
|
34
35
|
import test from "./test.js";
|
|
35
36
|
import typecheck from "./typecheck.js";
|
|
37
|
+
import uiBlocksList from "./wp-ui-blocks-list.js";
|
|
38
|
+
import uiCompose from "./wp-ui-compose.js";
|
|
39
|
+
import uiPreview from "./wp-ui-preview.js";
|
|
36
40
|
import ultragoalCancel from "./ultragoal-cancel.js";
|
|
37
41
|
import ultragoalHandoff from "./ultragoal-handoff.js";
|
|
38
42
|
import ultragoalNew from "./ultragoal-new.js";
|
|
@@ -55,6 +59,7 @@ export const COMPILED_TOOL_REGISTRY = [
|
|
|
55
59
|
prUpsert,
|
|
56
60
|
prWait,
|
|
57
61
|
qa,
|
|
62
|
+
releaseProgress,
|
|
58
63
|
releaseReadiness,
|
|
59
64
|
reviewRun,
|
|
60
65
|
runWait,
|
|
@@ -76,6 +81,9 @@ export const COMPILED_TOOL_REGISTRY = [
|
|
|
76
81
|
sessionStats,
|
|
77
82
|
test,
|
|
78
83
|
typecheck,
|
|
84
|
+
uiBlocksList,
|
|
85
|
+
uiCompose,
|
|
86
|
+
uiPreview,
|
|
79
87
|
ultragoalCancel,
|
|
80
88
|
ultragoalHandoff,
|
|
81
89
|
ultragoalNew,
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { ToolDescriptor } from "#mcp/auto-discover";
|
|
2
|
+
import { type RunReleaseProgressOptions } from "#cli/commands/release-progress.js";
|
|
3
|
+
export type ReleaseProgressToolDeps = Pick<RunReleaseProgressOptions, "fetchJobs" | "resolveLatestRunId">;
|
|
4
|
+
/**
|
|
5
|
+
* Build the tool descriptor. Tests inject gh seams; production uses defaults.
|
|
6
|
+
*/
|
|
7
|
+
export declare function createReleaseProgressTool(deps?: ReleaseProgressToolDeps): ToolDescriptor;
|
|
8
|
+
declare const tool: ToolDescriptor;
|
|
9
|
+
export default tool;
|