@webpresso/agent-kit 3.3.2 → 3.3.4
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/THIRD-PARTY-NOTICES.md +12 -10
- package/catalog/AGENTS.md.tpl +3 -5
- package/catalog/agent/rules/changeset-release.md +15 -4
- package/catalog/agent/rules/managed-tool-binaries.md +67 -0
- package/catalog/agent/rules/parallel-orchestration-quota.md +99 -1
- package/catalog/agent/rules/pre-implementation.md +36 -20
- package/catalog/agent/rules/rtk-routing.md +22 -16
- package/catalog/agent/skills/autopilot/SKILL.md +4 -3
- package/catalog/agent/skills/ultragoal/SKILL.md +11 -9
- package/catalog/agent/skills/verify/SKILL.md +12 -12
- package/catalog/compose/admin-blocks-registry.json +659 -0
- package/dist/esm/audit/hook-surface.js +5 -1
- package/dist/esm/audit/repo-guardrails.d.ts +8 -0
- package/dist/esm/audit/repo-guardrails.js +71 -1
- package/dist/esm/blueprint/core/parser.js +40 -16
- package/dist/esm/blueprint/core/validation/criteria.js +2 -1
- package/dist/esm/blueprint/execution/artifacts.js +8 -4
- package/dist/esm/blueprint/execution/frontmatter-keys.d.ts +17 -0
- package/dist/esm/blueprint/execution/frontmatter-keys.js +24 -0
- package/dist/esm/blueprint/execution/metadata.js +8 -4
- package/dist/esm/blueprint/lifecycle/audit.d.ts +21 -0
- package/dist/esm/blueprint/lifecycle/audit.js +89 -3
- package/dist/esm/blueprint/lifecycle/review-provenance.js +148 -40
- package/dist/esm/blueprint/lifecycle/rollout-anchor.d.ts +22 -0
- package/dist/esm/blueprint/lifecycle/rollout-anchor.js +37 -1
- package/dist/esm/blueprint/markdown/blocked-line.d.ts +49 -0
- package/dist/esm/blueprint/markdown/blocked-line.js +63 -0
- package/dist/esm/blueprint/markdown/helpers.js +10 -4
- package/dist/esm/blueprint/tracked-document/parser.js +3 -3
- package/dist/esm/blueprint/trust/gate-row.d.ts +55 -0
- package/dist/esm/blueprint/trust/gate-row.js +113 -0
- package/dist/esm/blueprint/trust/promotion.d.ts +17 -0
- package/dist/esm/blueprint/trust/promotion.js +19 -19
- package/dist/esm/build/atomic-file-copy.d.ts +32 -0
- package/dist/esm/build/atomic-file-copy.js +44 -0
- package/dist/esm/build/cli-mcp-parity.js +3 -2
- package/dist/esm/build/package-manifest.js +7 -2
- package/dist/esm/build/vendor-oxlint-plugins.d.ts +8 -0
- package/dist/esm/build/vendor-oxlint-plugins.js +53 -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.d.ts +15 -0
- package/dist/esm/cli/commands/blueprint/mutations.js +8 -2
- package/dist/esm/cli/commands/compile.d.ts +13 -0
- package/dist/esm/cli/commands/compile.js +182 -62
- package/dist/esm/cli/commands/format.js +17 -1
- package/dist/esm/cli/commands/gain/index.js +10 -3
- package/dist/esm/cli/commands/init/gitignore-patcher.js +6 -0
- package/dist/esm/cli/commands/init/index.js +29 -48
- package/dist/esm/cli/commands/init/mcp-spec.d.ts +54 -4
- package/dist/esm/cli/commands/init/mcp-spec.js +126 -22
- package/dist/esm/cli/commands/init/merge.js +30 -10
- package/dist/esm/cli/commands/init/scaffold-agents-md.js +5 -7
- package/dist/esm/cli/commands/init/scaffolders/codex-mcp/index.d.ts +56 -31
- package/dist/esm/cli/commands/init/scaffolders/codex-mcp/index.js +180 -43
- package/dist/esm/cli/commands/init/scaffolders/rtk/index.d.ts +17 -5
- package/dist/esm/cli/commands/init/scaffolders/rtk/index.js +56 -75
- package/dist/esm/cli/commands/init/scaffolders/runtime-check/index.js +7 -6
- package/dist/esm/cli/commands/init/untracked-collision.d.ts +12 -1
- package/dist/esm/cli/commands/init/untracked-collision.js +23 -1
- package/dist/esm/cli/commands/package-manager.js +8 -2
- package/dist/esm/cli/commands/pr-help.js +1 -1
- package/dist/esm/cli/commands/pr.js +18 -4
- package/dist/esm/cli/commands/review.js +60 -6
- package/dist/esm/cli/commands/worktree/router-dispatch.d.ts +24 -2
- package/dist/esm/cli/commands/worktree/router-dispatch.js +80 -48
- package/dist/esm/cli/commands/worktree/router.js +5 -2
- package/dist/esm/cli/direct-provider-launch.js +12 -4
- package/dist/esm/compiler/flatten.d.ts +10 -1
- package/dist/esm/compiler/flatten.js +14 -4
- package/dist/esm/compose/admin-blocks-registry-integrity.d.ts +22 -0
- package/dist/esm/compose/admin-blocks-registry-integrity.js +90 -0
- package/dist/esm/compose/compose.d.ts +46 -0
- package/dist/esm/compose/compose.js +250 -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 +60 -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 +64 -0
- package/dist/esm/compose/registry-resolve.js +200 -0
- package/dist/esm/config/oxlint/graphql-conventions.js +2 -5
- package/dist/esm/config/oxlint/import-hygiene.js +21 -8
- package/dist/esm/config/oxlint/oxlint-config.fixture.d.ts +1 -1
- package/dist/esm/config/oxlint/oxlint-config.fixture.js +2 -3
- package/dist/esm/config/oxlint/oxlintrc.d.ts +5 -7
- package/dist/esm/config/oxlint/oxlintrc.js +18 -21
- package/dist/esm/config/oxlint/path-roles.js +53 -0
- package/dist/esm/config/oxlint/query-patterns.js +2 -5
- package/dist/esm/daemon/domains/blueprint-exec-ledger.d.ts +86 -0
- package/dist/esm/daemon/domains/blueprint-exec-ledger.js +134 -0
- package/dist/esm/daemon/domains/blueprint-exec.d.ts +106 -0
- package/dist/esm/daemon/domains/blueprint-exec.js +161 -0
- package/dist/esm/daemon/domains/launch.d.ts +25 -0
- package/dist/esm/daemon/domains/launch.js +30 -1
- package/dist/esm/daemon/handlers/blueprints-handlers.d.ts +12 -1
- package/dist/esm/daemon/handlers/blueprints-handlers.js +148 -6
- package/dist/esm/daemon/handlers/product.d.ts +10 -0
- package/dist/esm/daemon/handlers/product.js +10 -1
- package/dist/esm/daemon/methods.d.ts +21 -1
- package/dist/esm/daemon/methods.js +20 -0
- package/dist/esm/daemon/protocol/contract.d.ts +58 -0
- package/dist/esm/daemon/protocol/contract.js +38 -0
- package/dist/esm/daemon/server.js +7 -0
- package/dist/esm/docs-linter/blueprint-plan.js +2 -2
- package/dist/esm/errors/wp-error.js +21 -2
- package/dist/esm/git/changed-files.d.ts +20 -0
- package/dist/esm/git/changed-files.js +63 -0
- package/dist/esm/hooks/doctor.d.ts +11 -0
- package/dist/esm/hooks/doctor.js +114 -45
- package/dist/esm/hooks/pretool-guard/dev-routing.js +1 -1
- package/dist/esm/hooks/pretool-guard/rtk-rewrite.d.ts +24 -0
- package/dist/esm/hooks/pretool-guard/rtk-rewrite.js +50 -0
- package/dist/esm/hooks/pretool-guard/runner.js +6 -0
- package/dist/esm/hooks/status/index.js +1 -1
- package/dist/esm/hooks/stop/qa-changed-files.js +6 -1
- package/dist/esm/mcp/blueprint/_shared/lifecycle.js +39 -11
- package/dist/esm/mcp/blueprint/handlers/review-log.d.ts +14 -7
- package/dist/esm/mcp/blueprint/handlers/review-log.js +46 -22
- package/dist/esm/mcp/blueprint/handlers/validate.js +6 -0
- package/dist/esm/mcp/blueprint/registration.js +1 -1
- 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/_shared/redact.d.ts +22 -0
- package/dist/esm/mcp/tools/_shared/redact.js +114 -8
- package/dist/esm/mcp/tools/gain.js +28 -2
- 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/worktree.js +65 -3
- 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 +96 -0
- package/dist/esm/mcp/tools/wp-ui-preview.d.ts +3 -0
- package/dist/esm/mcp/tools/wp-ui-preview.js +59 -0
- package/dist/esm/package.json +2 -0
- package/dist/esm/review/lifecycle-writers.d.ts +76 -0
- package/dist/esm/review/lifecycle-writers.js +330 -0
- package/dist/esm/review/subject.js +66 -32
- package/dist/esm/status/snapshot.d.ts +5 -0
- package/dist/esm/status/snapshot.js +27 -2
- package/dist/esm/test/shard-durations.json +0 -1
- package/dist/esm/test-helpers/global-setup.js +38 -3
- package/dist/esm/tool-runtime/managed-rtk.d.ts +44 -0
- package/dist/esm/tool-runtime/managed-rtk.js +275 -0
- package/dist/esm/tool-runtime/resolve-runner.js +18 -194
- package/dist/esm/utils/package-root.d.ts +32 -0
- package/dist/esm/utils/package-root.js +82 -0
- package/dist/esm/worktrees/git-metadata.d.ts +41 -0
- package/dist/esm/worktrees/git-metadata.js +48 -0
- package/package.json +27 -21
- package/dist/esm/config/oxlint/code-safety.d.ts +0 -18
- package/dist/esm/config/oxlint/foundation-purity.d.ts +0 -21
- package/dist/esm/config/oxlint/graphql-conventions.d.ts +0 -23
- package/dist/esm/config/oxlint/import-hygiene.d.ts +0 -34
- package/dist/esm/config/oxlint/index.d.ts +0 -22
- package/dist/esm/config/oxlint/index.js +0 -28
- package/dist/esm/config/oxlint/monorepo-paths.d.ts +0 -22
- package/dist/esm/config/oxlint/query-patterns.d.ts +0 -24
- package/dist/esm/config/oxlint/testing-quality.d.ts +0 -35
- package/dist/esm/config/oxlint/tier-boundaries.d.ts +0 -35
- package/dist/esm/output-transforms/rulesync.d.ts +0 -2
- package/dist/esm/output-transforms/rulesync.js +0 -79
|
@@ -210,6 +210,53 @@ export function auditDocsFrontmatter(rootDirectory = process.cwd(), options = {}
|
|
|
210
210
|
}
|
|
211
211
|
return result("Docs frontmatter", markdownFiles.length, violations);
|
|
212
212
|
}
|
|
213
|
+
const APPROVALS_KEY_LINE = /^approvals:\s*$/m;
|
|
214
|
+
/**
|
|
215
|
+
* Count `- ` list-item lines that follow a top-level `approvals:` key inside
|
|
216
|
+
* the RAW frontmatter text, stopping at the next top-level key. Returns -1
|
|
217
|
+
* when there is no `approvals:` key at all (nothing to check).
|
|
218
|
+
*
|
|
219
|
+
* This is deliberately independent of the YAML parser: it is the ground
|
|
220
|
+
* truth for "did the author intend approval entries here", used to catch a
|
|
221
|
+
* corrupted write where the YAML still happens to parse but the approvals
|
|
222
|
+
* mapping was silently truncated or displaced (e.g. a scalar-restore bug
|
|
223
|
+
* that inserts or drops lines near the approvals block).
|
|
224
|
+
*/
|
|
225
|
+
function countRawApprovalEntries(raw) {
|
|
226
|
+
const frontmatterBlock = /^---\r?\n([\s\S]*?)\r?\n---/.exec(raw)?.[1] ?? "";
|
|
227
|
+
const keyIndex = frontmatterBlock.search(APPROVALS_KEY_LINE);
|
|
228
|
+
if (keyIndex === -1)
|
|
229
|
+
return -1;
|
|
230
|
+
const afterKey = frontmatterBlock.slice(keyIndex).split("\n").slice(1);
|
|
231
|
+
let count = 0;
|
|
232
|
+
for (const line of afterKey) {
|
|
233
|
+
if (/^[A-Za-z0-9_]+:/.test(line))
|
|
234
|
+
break;
|
|
235
|
+
if (/^\s*-\s/.test(line))
|
|
236
|
+
count += 1;
|
|
237
|
+
}
|
|
238
|
+
return count;
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* Flags a blueprint whose raw frontmatter clearly lists one or more
|
|
242
|
+
* `approvals:` entries but whose YAML-parsed `approvals` field yields zero —
|
|
243
|
+
* the corruption signature `wp blueprint move --force-recovery` produces
|
|
244
|
+
* when it silently destroys approval authority without making the document
|
|
245
|
+
* fail to parse outright.
|
|
246
|
+
*/
|
|
247
|
+
export function detectSuspiciousZeroApprovals(raw, frontmatter) {
|
|
248
|
+
const rawEntryCount = countRawApprovalEntries(raw);
|
|
249
|
+
if (rawEntryCount <= 0)
|
|
250
|
+
return undefined;
|
|
251
|
+
const parsedApprovals = frontmatter["approvals"];
|
|
252
|
+
const parsedCount = Array.isArray(parsedApprovals) ? parsedApprovals.length : 0;
|
|
253
|
+
if (parsedCount > 0)
|
|
254
|
+
return undefined;
|
|
255
|
+
return (`Frontmatter parses to 0 approvals but the raw YAML lists ${rawEntryCount} ` +
|
|
256
|
+
"approval entr" +
|
|
257
|
+
(rawEntryCount === 1 ? "y" : "ies") +
|
|
258
|
+
" — likely corrupted frontmatter near the approvals block.");
|
|
259
|
+
}
|
|
213
260
|
export function auditBlueprintLifecycle(rootDirectory = process.cwd(), options = {}) {
|
|
214
261
|
const root = resolve(rootDirectory);
|
|
215
262
|
const blueprintsRoot = resolve(root, options.blueprintsRoot ?? "blueprints");
|
|
@@ -259,7 +306,30 @@ export function auditBlueprintLifecycle(rootDirectory = process.cwd(), options =
|
|
|
259
306
|
continue;
|
|
260
307
|
}
|
|
261
308
|
const raw = readFileSync(canonicalPath, "utf8");
|
|
262
|
-
|
|
309
|
+
let frontmatter;
|
|
310
|
+
try {
|
|
311
|
+
frontmatter = matter(raw).data;
|
|
312
|
+
}
|
|
313
|
+
catch (error) {
|
|
314
|
+
// A blueprint write (e.g. `wp blueprint move --force-recovery`) can
|
|
315
|
+
// emit invalid YAML — most commonly a folded-scalar continuation
|
|
316
|
+
// line stranded after an already-quoted scalar. Every downstream
|
|
317
|
+
// reader (including the approval gate) then fails the same way this
|
|
318
|
+
// parse does, so surface it as a first-class violation instead of
|
|
319
|
+
// letting the whole audit crash on the first corrupted file.
|
|
320
|
+
violations.push({
|
|
321
|
+
file: relativePath(root, canonicalPath),
|
|
322
|
+
message: `Frontmatter failed to parse as YAML: ${error instanceof Error ? error.message : String(error)}`,
|
|
323
|
+
});
|
|
324
|
+
continue;
|
|
325
|
+
}
|
|
326
|
+
const suspiciousApprovals = detectSuspiciousZeroApprovals(raw, frontmatter);
|
|
327
|
+
if (suspiciousApprovals) {
|
|
328
|
+
violations.push({
|
|
329
|
+
file: relativePath(root, canonicalPath),
|
|
330
|
+
message: suspiciousApprovals,
|
|
331
|
+
});
|
|
332
|
+
}
|
|
263
333
|
if (frontmatter.type !== "blueprint" && frontmatter.type !== "parent-roadmap") {
|
|
264
334
|
violations.push({
|
|
265
335
|
file: relativePath(root, canonicalPath),
|
|
@@ -6,7 +6,8 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import matter from "gray-matter";
|
|
8
8
|
import { planFrontmatterSchema, taskStatusSchema } from "#core/schema";
|
|
9
|
-
import {
|
|
9
|
+
import { extractBlockedReason } from "#markdown/blocked-line";
|
|
10
|
+
import { buildTaskHeadingRegex, isTaskHeaderLine } from "#markdown/task-heading";
|
|
10
11
|
function parseWithSchema(markdown, schema) {
|
|
11
12
|
const { data: rawData, content } = matter(markdown);
|
|
12
13
|
const data = schema.parse(rawData);
|
|
@@ -87,7 +88,13 @@ const SERIALIZE_OWNED_FRONTMATTER_KEYS = new Set([
|
|
|
87
88
|
"progress",
|
|
88
89
|
"completed_at",
|
|
89
90
|
]);
|
|
90
|
-
|
|
91
|
+
// `\r?$` is load-bearing on a CRLF checkout: `blueprint.raw` is stored verbatim
|
|
92
|
+
// and split on "\n", so every original line still carries a trailing `\r`. `.`
|
|
93
|
+
// does not match a line terminator, so without `\r?` the anchor cannot match,
|
|
94
|
+
// NO originals are collected, preservation bails, and matter.stringify's
|
|
95
|
+
// re-quoted scalars survive — silently invalidating every recorded approval on
|
|
96
|
+
// that checkout. `FRONTMATTER_MULTILINE_KEY` below already carries `\r?$`.
|
|
97
|
+
const FRONTMATTER_SCALAR_LINE = /^([A-Za-z0-9_]+):[ \t]+\S.*\r?$/;
|
|
91
98
|
function quoteGeneratedProgressScalar(serialized, progress) {
|
|
92
99
|
if (!progress)
|
|
93
100
|
return serialized;
|
|
@@ -108,7 +115,10 @@ function preserveOriginalFrontmatterScalars(serialized, originalRaw) {
|
|
|
108
115
|
for (const line of originalFrontmatter.split("\n")) {
|
|
109
116
|
const match = FRONTMATTER_SCALAR_LINE.exec(line);
|
|
110
117
|
if (match && !SERIALIZE_OWNED_FRONTMATTER_KEYS.has(match[1])) {
|
|
111
|
-
|
|
118
|
+
// Store without the source's line ending; the substitution below re-applies
|
|
119
|
+
// whichever ending the SERIALIZED document uses, so a CRLF original is
|
|
120
|
+
// never spliced into an LF document (or vice versa) as a mixed ending.
|
|
121
|
+
originalScalars.set(match[1], line.replace(/\r$/, ""));
|
|
112
122
|
}
|
|
113
123
|
}
|
|
114
124
|
if (originalScalars.size === 0)
|
|
@@ -116,13 +126,30 @@ function preserveOriginalFrontmatterScalars(serialized, originalRaw) {
|
|
|
116
126
|
const block = /^(---\r?\n)([\s\S]*?)(\r?\n---(?:\r?\n|$))/.exec(serialized);
|
|
117
127
|
if (!block)
|
|
118
128
|
return serialized;
|
|
119
|
-
const
|
|
120
|
-
|
|
121
|
-
|
|
129
|
+
const lines = block[2].split("\n");
|
|
130
|
+
const rebuiltLines = [];
|
|
131
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
132
|
+
const line = lines[index];
|
|
122
133
|
const match = FRONTMATTER_SCALAR_LINE.exec(line);
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
134
|
+
const originalScalar = match && originalScalars.get(match[1]);
|
|
135
|
+
if (!originalScalar) {
|
|
136
|
+
rebuiltLines.push(line);
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
rebuiltLines.push(line.endsWith("\r") ? `${originalScalar}\r` : originalScalar);
|
|
140
|
+
// A freshly re-dumped value for this key may not fit on one line: long
|
|
141
|
+
// plain/quoted scalars get YAML-folded by js-yaml into indented
|
|
142
|
+
// continuation lines directly beneath the `key:` line (no `key:` prefix
|
|
143
|
+
// of their own). We are replacing the whole value wholesale with the
|
|
144
|
+
// original single-line scalar, so those continuation lines must be
|
|
145
|
+
// dropped too — otherwise they survive as an orphaned mapping fragment
|
|
146
|
+
// right after an already-complete (and possibly already-quote-closed)
|
|
147
|
+
// scalar, which is invalid YAML.
|
|
148
|
+
while (index + 1 < lines.length && /^[ \t]/.test(lines[index + 1])) {
|
|
149
|
+
index += 1;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
const rebuilt = rebuiltLines.join("\n");
|
|
126
153
|
return `${serialized.slice(0, block.index)}${block[1]}${rebuilt}${block[3]}${serialized.slice(block.index + block[0].length)}`;
|
|
127
154
|
}
|
|
128
155
|
// Same residual as scalars, for multi-line blocks (YAML arrays/maps under a
|
|
@@ -328,18 +355,15 @@ function extractDepends(section) {
|
|
|
328
355
|
return ids.filter((id) => id !== "");
|
|
329
356
|
}
|
|
330
357
|
function extractBlocked(section) {
|
|
331
|
-
const
|
|
332
|
-
if (
|
|
358
|
+
const blockedText = extractBlockedReason(section);
|
|
359
|
+
if (blockedText === undefined)
|
|
333
360
|
return undefined;
|
|
334
|
-
|
|
335
|
-
if (blockedText === "" || blockedText.toLowerCase() === "none") {
|
|
361
|
+
if (blockedText.toLowerCase() === "none") {
|
|
336
362
|
return undefined;
|
|
337
363
|
}
|
|
338
364
|
return blockedText;
|
|
339
365
|
}
|
|
340
|
-
|
|
341
|
-
return /^####\s+(?:\[[^\]]+\]\s+)?Task\s+\d+(?:\.\d+)+:/.test(line);
|
|
342
|
-
}
|
|
366
|
+
const isTaskHeader = isTaskHeaderLine;
|
|
343
367
|
function isMetadataLine(line) {
|
|
344
368
|
return /^\*\*(Depends|Blocked|Status):\*\*/i.test(line);
|
|
345
369
|
}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Check acceptance criteria completion in plan markdown.
|
|
3
3
|
*/
|
|
4
|
+
import { buildTaskHeadingRegex } from "#markdown/task-heading";
|
|
4
5
|
const CHECKBOX_REGEX = /^- \[([ x])\]/gm;
|
|
5
|
-
const TASK_HEADER_REGEX =
|
|
6
|
+
const TASK_HEADER_REGEX = buildTaskHeadingRegex("gm");
|
|
6
7
|
const TASK_DONE_OR_DROPPED_REGEX = /^\*\*Status:\*\*\s*(done|dropped)\s*$/im;
|
|
7
8
|
const SECTION_BOUNDARY_REGEX = /\n##\s|\n---\n/;
|
|
8
9
|
export function checkAcceptanceCriteria(markdown) {
|
|
@@ -29,16 +29,20 @@ export function readBlueprintExecutionArtifacts(markdown) {
|
|
|
29
29
|
};
|
|
30
30
|
}
|
|
31
31
|
export function writeBlueprintExecutionArtifacts(markdown, artifacts) {
|
|
32
|
-
|
|
32
|
+
// Typed against the shared key union so a key can never be written here
|
|
33
|
+
// without also entering the plan-subject normalizer's strip set.
|
|
34
|
+
const fields = {
|
|
33
35
|
execution_artifacts: artifacts.artifacts,
|
|
34
36
|
execution_log_path: artifacts.logPath,
|
|
35
37
|
execution_verifications: artifacts.verifications,
|
|
36
|
-
}
|
|
38
|
+
};
|
|
39
|
+
return setBlueprintFrontmatterFields(markdown, fields);
|
|
37
40
|
}
|
|
38
41
|
export function clearBlueprintExecutionArtifacts(markdown) {
|
|
39
|
-
|
|
42
|
+
const fields = {
|
|
40
43
|
execution_artifacts: undefined,
|
|
41
44
|
execution_log_path: undefined,
|
|
42
45
|
execution_verifications: undefined,
|
|
43
|
-
}
|
|
46
|
+
};
|
|
47
|
+
return setBlueprintFrontmatterFields(markdown, fields);
|
|
44
48
|
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Single owner of the `execution_*` blueprint frontmatter key names.
|
|
3
|
+
*
|
|
4
|
+
* The execution backend writes these keys into `_overview.md`
|
|
5
|
+
* (`./metadata.js`, `./artifacts.js`), and the plan-subject normalizer
|
|
6
|
+
* (`#review/subject.js`) strips them so starting or advancing an execution does
|
|
7
|
+
* not dismiss a recorded plan approval. Both sides import this list, so a key
|
|
8
|
+
* added to one side without the other is a typecheck error rather than a silent
|
|
9
|
+
* digest leak.
|
|
10
|
+
*
|
|
11
|
+
* Deliberately dependency-free so the normalizer can import it without pulling
|
|
12
|
+
* in the lifecycle engine.
|
|
13
|
+
*/
|
|
14
|
+
export declare const EXECUTION_METADATA_FRONTMATTER_KEYS: readonly ["execution_backend", "execution_id", "execution_status", "execution_updated_at"];
|
|
15
|
+
export type ExecutionMetadataFrontmatterKey = (typeof EXECUTION_METADATA_FRONTMATTER_KEYS)[number];
|
|
16
|
+
export declare const EXECUTION_ARTIFACTS_FRONTMATTER_KEYS: readonly ["execution_artifacts", "execution_log_path", "execution_verifications"];
|
|
17
|
+
export type ExecutionArtifactsFrontmatterKey = (typeof EXECUTION_ARTIFACTS_FRONTMATTER_KEYS)[number];
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Single owner of the `execution_*` blueprint frontmatter key names.
|
|
3
|
+
*
|
|
4
|
+
* The execution backend writes these keys into `_overview.md`
|
|
5
|
+
* (`./metadata.js`, `./artifacts.js`), and the plan-subject normalizer
|
|
6
|
+
* (`#review/subject.js`) strips them so starting or advancing an execution does
|
|
7
|
+
* not dismiss a recorded plan approval. Both sides import this list, so a key
|
|
8
|
+
* added to one side without the other is a typecheck error rather than a silent
|
|
9
|
+
* digest leak.
|
|
10
|
+
*
|
|
11
|
+
* Deliberately dependency-free so the normalizer can import it without pulling
|
|
12
|
+
* in the lifecycle engine.
|
|
13
|
+
*/
|
|
14
|
+
export const EXECUTION_METADATA_FRONTMATTER_KEYS = [
|
|
15
|
+
"execution_backend",
|
|
16
|
+
"execution_id",
|
|
17
|
+
"execution_status",
|
|
18
|
+
"execution_updated_at",
|
|
19
|
+
];
|
|
20
|
+
export const EXECUTION_ARTIFACTS_FRONTMATTER_KEYS = [
|
|
21
|
+
"execution_artifacts",
|
|
22
|
+
"execution_log_path",
|
|
23
|
+
"execution_verifications",
|
|
24
|
+
];
|
|
@@ -28,18 +28,22 @@ export function readBlueprintExecutionMetadata(markdown) {
|
|
|
28
28
|
};
|
|
29
29
|
}
|
|
30
30
|
export function writeBlueprintExecutionMetadata(markdown, metadata) {
|
|
31
|
-
|
|
31
|
+
// Typed against the shared key union so a key can never be written here
|
|
32
|
+
// without also entering the plan-subject normalizer's strip set.
|
|
33
|
+
const fields = {
|
|
32
34
|
execution_backend: metadata.backend,
|
|
33
35
|
execution_id: metadata.executionId,
|
|
34
36
|
execution_status: metadata.status,
|
|
35
37
|
execution_updated_at: metadata.updatedAt,
|
|
36
|
-
}
|
|
38
|
+
};
|
|
39
|
+
return setBlueprintFrontmatterFields(markdown, fields);
|
|
37
40
|
}
|
|
38
41
|
export function clearBlueprintExecutionMetadata(markdown) {
|
|
39
|
-
|
|
42
|
+
const fields = {
|
|
40
43
|
execution_backend: undefined,
|
|
41
44
|
execution_id: undefined,
|
|
42
45
|
execution_status: undefined,
|
|
43
46
|
execution_updated_at: undefined,
|
|
44
|
-
}
|
|
47
|
+
};
|
|
48
|
+
return setBlueprintFrontmatterFields(markdown, fields);
|
|
45
49
|
}
|
|
@@ -47,6 +47,27 @@ export interface ApprovalGateEvaluation {
|
|
|
47
47
|
satisfied: boolean;
|
|
48
48
|
strongReviewers: string[];
|
|
49
49
|
}
|
|
50
|
+
/**
|
|
51
|
+
* Whether to explain a skipped legacy approval leg to the caller.
|
|
52
|
+
*
|
|
53
|
+
* Exported so the decision is unit-testable without standing up a v2 ledger
|
|
54
|
+
* fixture. Two properties matter and neither is obvious from the call site:
|
|
55
|
+
*
|
|
56
|
+
* 1. Never fail silently. Before this message existed a skipped leg reported as a
|
|
57
|
+
* bare "0 approvals" with no reason, which is the most expensive failure mode
|
|
58
|
+
* in this area.
|
|
59
|
+
* 2. Never cry wolf. A post-rollout blueprint that already holds content-bound v2
|
|
60
|
+
* authority and merely still carries a leftover frontmatter entry needs no
|
|
61
|
+
* message: the leg was not required. `validateApprovalGate` turns sink entries
|
|
62
|
+
* into `Review evidence integrity:` WARNINGS on an otherwise-passing audit, so
|
|
63
|
+
* an unconditional note would flag clean strict runs as suspect.
|
|
64
|
+
*/
|
|
65
|
+
export declare function shouldExplainSkippedLegacyLeg(input: {
|
|
66
|
+
readonly legacyLegApplies: boolean;
|
|
67
|
+
readonly includesPlanPurpose: boolean;
|
|
68
|
+
readonly approvals: unknown;
|
|
69
|
+
readonly currentReviewers: readonly string[];
|
|
70
|
+
}): boolean;
|
|
50
71
|
export declare function countDistinctProvenanceBackedApprovals(file: string, approvals: unknown): number;
|
|
51
72
|
export declare function evaluateApprovalGate(file: string, approvals: unknown, options?: {
|
|
52
73
|
readonly purpose?: "plan" | "delivery";
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
1
2
|
import { readFile } from "node:fs/promises";
|
|
2
3
|
import path from "node:path";
|
|
3
4
|
import matter from "gray-matter";
|
|
@@ -13,7 +14,7 @@ import { resolveBlueprintRoot } from "#utils/blueprint-root";
|
|
|
13
14
|
import { parseBlueprintDocumentRelativePath } from "#utils/document-paths.js";
|
|
14
15
|
import { relativeBlueprintSlug } from "#lifecycle/local.js";
|
|
15
16
|
import { collectV2ReviewProvenance, collectTrackedReviewProvenance, normalizeTrackedReviewApprovals, stableSlugForBlueprintFile, } from "#lifecycle/review-provenance.js";
|
|
16
|
-
import { isBlueprintExemptFromRollout, ROLLOUT_DATE } from "#lifecycle/rollout-anchor.js";
|
|
17
|
+
import { isBlueprintExemptFromRollout, LEGACY_APPROVAL_LEG_ANCHOR_COMMIT, LEGACY_APPROVAL_LEG_ROLLOUT_DATE, ROLLOUT_DATE, } from "#lifecycle/rollout-anchor.js";
|
|
17
18
|
import { MAINTAINER_OVERRIDE_REVIEWER } from "#review/events.js";
|
|
18
19
|
import { isPolicyApprovalVerdict } from "#review/verdict.js";
|
|
19
20
|
function isBlueprintOverview(file) {
|
|
@@ -106,14 +107,99 @@ function approvalReviewerKey(reviewer) {
|
|
|
106
107
|
const parts = normalized.split(/[/:@#]/).filter(Boolean);
|
|
107
108
|
return parts.at(-1) ?? normalized;
|
|
108
109
|
}
|
|
110
|
+
/**
|
|
111
|
+
* `created:` as written in the blueprint's own frontmatter.
|
|
112
|
+
*
|
|
113
|
+
* Only consulted on the non-agent-kit fallback path inside
|
|
114
|
+
* {@link isBlueprintExemptFromRollout} (where the anchor commit is absent and
|
|
115
|
+
* cannot answer the question), so the fact that the gated author writes this
|
|
116
|
+
* value does not weaken the gate where it is enforced. Read from disk rather than
|
|
117
|
+
* threaded through {@link evaluateApprovalGate}'s ten call sites: a call site
|
|
118
|
+
* that forgot the parameter would silently fail the leg closed and block
|
|
119
|
+
* promotion on a grandfathered blueprint.
|
|
120
|
+
*/
|
|
121
|
+
function readBlueprintCreated(file) {
|
|
122
|
+
try {
|
|
123
|
+
return matter(readFileSync(file, "utf8")).data.created;
|
|
124
|
+
}
|
|
125
|
+
catch {
|
|
126
|
+
return undefined;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* True when this blueprint may still satisfy the approval gate through the
|
|
131
|
+
* LEGACY frontmatter `approvals:` leg.
|
|
132
|
+
*
|
|
133
|
+
* That leg carries no content binding: `collectTrackedReviewProvenance` resolves
|
|
134
|
+
* a ledger from `approval.evidence`, matches a `reviews.md` record and requires a
|
|
135
|
+
* tracked artifact, but never reads or hashes the blueprint body — and the gate
|
|
136
|
+
* supplies no `targetHash`, so `matchesApproval`'s binding clause is skipped too.
|
|
137
|
+
* An approval recorded against one plan therefore kept satisfying the gate after
|
|
138
|
+
* the plan was rewritten into something else entirely.
|
|
139
|
+
*
|
|
140
|
+
* The leg is grandfathered rather than deleted because 172 already-landed
|
|
141
|
+
* blueprints resolve their approval through it; failing those would block
|
|
142
|
+
* ordinary work (a completed blueprint touched again in a PR is re-gated). New
|
|
143
|
+
* blueprints get the fail-closed behaviour: their authority must come from the
|
|
144
|
+
* content-addressed v2 `review-events.jsonl` ledger, which `wp review gate` and
|
|
145
|
+
* `wp review override` already write.
|
|
146
|
+
*
|
|
147
|
+
* `approval.targetHash`/`commit`/`rev` do NOT make an approval content-bound —
|
|
148
|
+
* those fields only cross-check the approval against its `reviews.md` record
|
|
149
|
+
* (`matchesApproval`), and neither is ever compared against document content. So
|
|
150
|
+
* presence of a binding-looking field is deliberately not an escape from this
|
|
151
|
+
* check.
|
|
152
|
+
*/
|
|
153
|
+
function legacyApprovalLegIsGrandfathered(file) {
|
|
154
|
+
return isBlueprintExemptFromRollout({
|
|
155
|
+
repoRoot: findGitRoot(file),
|
|
156
|
+
slug: stableSlugForBlueprintFile(file),
|
|
157
|
+
created: readBlueprintCreated(file),
|
|
158
|
+
rolloutDate: LEGACY_APPROVAL_LEG_ROLLOUT_DATE,
|
|
159
|
+
anchorCommit: LEGACY_APPROVAL_LEG_ANCHOR_COMMIT,
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
function hasPolicyApprovalEntry(approvals) {
|
|
163
|
+
return normalizeTrackedReviewApprovals(approvals).some((entry) => isPolicyApprovalVerdict(entry.verdict));
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Whether to explain a skipped legacy approval leg to the caller.
|
|
167
|
+
*
|
|
168
|
+
* Exported so the decision is unit-testable without standing up a v2 ledger
|
|
169
|
+
* fixture. Two properties matter and neither is obvious from the call site:
|
|
170
|
+
*
|
|
171
|
+
* 1. Never fail silently. Before this message existed a skipped leg reported as a
|
|
172
|
+
* bare "0 approvals" with no reason, which is the most expensive failure mode
|
|
173
|
+
* in this area.
|
|
174
|
+
* 2. Never cry wolf. A post-rollout blueprint that already holds content-bound v2
|
|
175
|
+
* authority and merely still carries a leftover frontmatter entry needs no
|
|
176
|
+
* message: the leg was not required. `validateApprovalGate` turns sink entries
|
|
177
|
+
* into `Review evidence integrity:` WARNINGS on an otherwise-passing audit, so
|
|
178
|
+
* an unconditional note would flag clean strict runs as suspect.
|
|
179
|
+
*/
|
|
180
|
+
export function shouldExplainSkippedLegacyLeg(input) {
|
|
181
|
+
return (!input.legacyLegApplies &&
|
|
182
|
+
input.includesPlanPurpose &&
|
|
183
|
+
hasPolicyApprovalEntry(input.approvals) &&
|
|
184
|
+
!evaluateApprovalReviewers(input.currentReviewers).satisfied);
|
|
185
|
+
}
|
|
109
186
|
function provenanceBackedApprovalReviewers(file, approvals, options = {}) {
|
|
110
187
|
const purposes = options.purpose
|
|
111
188
|
? [options.purpose]
|
|
112
189
|
: ["plan", "delivery"];
|
|
113
|
-
const
|
|
190
|
+
const current = purposes.flatMap((purpose) => collectV2ReviewProvenance(file, purpose, "HEAD", options.issuesSink).map((record) => record.reviewer));
|
|
191
|
+
const legacyLegApplies = purposes.includes("plan") && legacyApprovalLegIsGrandfathered(file);
|
|
192
|
+
if (shouldExplainSkippedLegacyLeg({
|
|
193
|
+
legacyLegApplies,
|
|
194
|
+
includesPlanPurpose: purposes.includes("plan"),
|
|
195
|
+
approvals,
|
|
196
|
+
currentReviewers: current,
|
|
197
|
+
})) {
|
|
198
|
+
options.issuesSink?.push("legacy frontmatter approval entries are not accepted for this blueprint: the unbound `approvals:` leg is grandfathered only for slugs tracked at the legacy-approval rollout anchor. Record the approval through `wp review gate` so it binds to the reviewed content.");
|
|
199
|
+
}
|
|
200
|
+
const legacy = legacyLegApplies
|
|
114
201
|
? collectTrackedReviewProvenance(file, approvals, options).map((record) => record.reviewer)
|
|
115
202
|
: [];
|
|
116
|
-
const current = purposes.flatMap((purpose) => collectV2ReviewProvenance(file, purpose, "HEAD", options.issuesSink).map((record) => record.reviewer));
|
|
117
203
|
return new Set([...legacy, ...current]);
|
|
118
204
|
}
|
|
119
205
|
export function countDistinctProvenanceBackedApprovals(file, approvals) {
|