@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.
Files changed (167) hide show
  1. package/THIRD-PARTY-NOTICES.md +12 -10
  2. package/catalog/AGENTS.md.tpl +3 -5
  3. package/catalog/agent/rules/changeset-release.md +15 -4
  4. package/catalog/agent/rules/managed-tool-binaries.md +67 -0
  5. package/catalog/agent/rules/parallel-orchestration-quota.md +99 -1
  6. package/catalog/agent/rules/pre-implementation.md +36 -20
  7. package/catalog/agent/rules/rtk-routing.md +22 -16
  8. package/catalog/agent/skills/autopilot/SKILL.md +4 -3
  9. package/catalog/agent/skills/ultragoal/SKILL.md +11 -9
  10. package/catalog/agent/skills/verify/SKILL.md +12 -12
  11. package/catalog/compose/admin-blocks-registry.json +659 -0
  12. package/dist/esm/audit/hook-surface.js +5 -1
  13. package/dist/esm/audit/repo-guardrails.d.ts +8 -0
  14. package/dist/esm/audit/repo-guardrails.js +71 -1
  15. package/dist/esm/blueprint/core/parser.js +40 -16
  16. package/dist/esm/blueprint/core/validation/criteria.js +2 -1
  17. package/dist/esm/blueprint/execution/artifacts.js +8 -4
  18. package/dist/esm/blueprint/execution/frontmatter-keys.d.ts +17 -0
  19. package/dist/esm/blueprint/execution/frontmatter-keys.js +24 -0
  20. package/dist/esm/blueprint/execution/metadata.js +8 -4
  21. package/dist/esm/blueprint/lifecycle/audit.d.ts +21 -0
  22. package/dist/esm/blueprint/lifecycle/audit.js +89 -3
  23. package/dist/esm/blueprint/lifecycle/review-provenance.js +148 -40
  24. package/dist/esm/blueprint/lifecycle/rollout-anchor.d.ts +22 -0
  25. package/dist/esm/blueprint/lifecycle/rollout-anchor.js +37 -1
  26. package/dist/esm/blueprint/markdown/blocked-line.d.ts +49 -0
  27. package/dist/esm/blueprint/markdown/blocked-line.js +63 -0
  28. package/dist/esm/blueprint/markdown/helpers.js +10 -4
  29. package/dist/esm/blueprint/tracked-document/parser.js +3 -3
  30. package/dist/esm/blueprint/trust/gate-row.d.ts +55 -0
  31. package/dist/esm/blueprint/trust/gate-row.js +113 -0
  32. package/dist/esm/blueprint/trust/promotion.d.ts +17 -0
  33. package/dist/esm/blueprint/trust/promotion.js +19 -19
  34. package/dist/esm/build/atomic-file-copy.d.ts +32 -0
  35. package/dist/esm/build/atomic-file-copy.js +44 -0
  36. package/dist/esm/build/cli-mcp-parity.js +3 -2
  37. package/dist/esm/build/package-manifest.js +7 -2
  38. package/dist/esm/build/vendor-oxlint-plugins.d.ts +8 -0
  39. package/dist/esm/build/vendor-oxlint-plugins.js +53 -0
  40. package/dist/esm/cli/commands/audit-core.js +9 -1
  41. package/dist/esm/cli/commands/audit.js +16 -5
  42. package/dist/esm/cli/commands/blueprint/mutations.d.ts +15 -0
  43. package/dist/esm/cli/commands/blueprint/mutations.js +8 -2
  44. package/dist/esm/cli/commands/compile.d.ts +13 -0
  45. package/dist/esm/cli/commands/compile.js +182 -62
  46. package/dist/esm/cli/commands/format.js +17 -1
  47. package/dist/esm/cli/commands/gain/index.js +10 -3
  48. package/dist/esm/cli/commands/init/gitignore-patcher.js +6 -0
  49. package/dist/esm/cli/commands/init/index.js +29 -48
  50. package/dist/esm/cli/commands/init/mcp-spec.d.ts +54 -4
  51. package/dist/esm/cli/commands/init/mcp-spec.js +126 -22
  52. package/dist/esm/cli/commands/init/merge.js +30 -10
  53. package/dist/esm/cli/commands/init/scaffold-agents-md.js +5 -7
  54. package/dist/esm/cli/commands/init/scaffolders/codex-mcp/index.d.ts +56 -31
  55. package/dist/esm/cli/commands/init/scaffolders/codex-mcp/index.js +180 -43
  56. package/dist/esm/cli/commands/init/scaffolders/rtk/index.d.ts +17 -5
  57. package/dist/esm/cli/commands/init/scaffolders/rtk/index.js +56 -75
  58. package/dist/esm/cli/commands/init/scaffolders/runtime-check/index.js +7 -6
  59. package/dist/esm/cli/commands/init/untracked-collision.d.ts +12 -1
  60. package/dist/esm/cli/commands/init/untracked-collision.js +23 -1
  61. package/dist/esm/cli/commands/package-manager.js +8 -2
  62. package/dist/esm/cli/commands/pr-help.js +1 -1
  63. package/dist/esm/cli/commands/pr.js +18 -4
  64. package/dist/esm/cli/commands/review.js +60 -6
  65. package/dist/esm/cli/commands/worktree/router-dispatch.d.ts +24 -2
  66. package/dist/esm/cli/commands/worktree/router-dispatch.js +80 -48
  67. package/dist/esm/cli/commands/worktree/router.js +5 -2
  68. package/dist/esm/cli/direct-provider-launch.js +12 -4
  69. package/dist/esm/compiler/flatten.d.ts +10 -1
  70. package/dist/esm/compiler/flatten.js +14 -4
  71. package/dist/esm/compose/admin-blocks-registry-integrity.d.ts +22 -0
  72. package/dist/esm/compose/admin-blocks-registry-integrity.js +90 -0
  73. package/dist/esm/compose/compose.d.ts +46 -0
  74. package/dist/esm/compose/compose.js +250 -0
  75. package/dist/esm/compose/escape-html.d.ts +5 -0
  76. package/dist/esm/compose/escape-html.js +12 -0
  77. package/dist/esm/compose/ops-report-render.d.ts +5 -0
  78. package/dist/esm/compose/ops-report-render.js +71 -0
  79. package/dist/esm/compose/ops-report-schema.d.ts +42 -0
  80. package/dist/esm/compose/ops-report-schema.js +60 -0
  81. package/dist/esm/compose/preview.d.ts +14 -0
  82. package/dist/esm/compose/preview.js +32 -0
  83. package/dist/esm/compose/registry-resolve.d.ts +64 -0
  84. package/dist/esm/compose/registry-resolve.js +200 -0
  85. package/dist/esm/config/oxlint/graphql-conventions.js +2 -5
  86. package/dist/esm/config/oxlint/import-hygiene.js +21 -8
  87. package/dist/esm/config/oxlint/oxlint-config.fixture.d.ts +1 -1
  88. package/dist/esm/config/oxlint/oxlint-config.fixture.js +2 -3
  89. package/dist/esm/config/oxlint/oxlintrc.d.ts +5 -7
  90. package/dist/esm/config/oxlint/oxlintrc.js +18 -21
  91. package/dist/esm/config/oxlint/path-roles.js +53 -0
  92. package/dist/esm/config/oxlint/query-patterns.js +2 -5
  93. package/dist/esm/daemon/domains/blueprint-exec-ledger.d.ts +86 -0
  94. package/dist/esm/daemon/domains/blueprint-exec-ledger.js +134 -0
  95. package/dist/esm/daemon/domains/blueprint-exec.d.ts +106 -0
  96. package/dist/esm/daemon/domains/blueprint-exec.js +161 -0
  97. package/dist/esm/daemon/domains/launch.d.ts +25 -0
  98. package/dist/esm/daemon/domains/launch.js +30 -1
  99. package/dist/esm/daemon/handlers/blueprints-handlers.d.ts +12 -1
  100. package/dist/esm/daemon/handlers/blueprints-handlers.js +148 -6
  101. package/dist/esm/daemon/handlers/product.d.ts +10 -0
  102. package/dist/esm/daemon/handlers/product.js +10 -1
  103. package/dist/esm/daemon/methods.d.ts +21 -1
  104. package/dist/esm/daemon/methods.js +20 -0
  105. package/dist/esm/daemon/protocol/contract.d.ts +58 -0
  106. package/dist/esm/daemon/protocol/contract.js +38 -0
  107. package/dist/esm/daemon/server.js +7 -0
  108. package/dist/esm/docs-linter/blueprint-plan.js +2 -2
  109. package/dist/esm/errors/wp-error.js +21 -2
  110. package/dist/esm/git/changed-files.d.ts +20 -0
  111. package/dist/esm/git/changed-files.js +63 -0
  112. package/dist/esm/hooks/doctor.d.ts +11 -0
  113. package/dist/esm/hooks/doctor.js +114 -45
  114. package/dist/esm/hooks/pretool-guard/dev-routing.js +1 -1
  115. package/dist/esm/hooks/pretool-guard/rtk-rewrite.d.ts +24 -0
  116. package/dist/esm/hooks/pretool-guard/rtk-rewrite.js +50 -0
  117. package/dist/esm/hooks/pretool-guard/runner.js +6 -0
  118. package/dist/esm/hooks/status/index.js +1 -1
  119. package/dist/esm/hooks/stop/qa-changed-files.js +6 -1
  120. package/dist/esm/mcp/blueprint/_shared/lifecycle.js +39 -11
  121. package/dist/esm/mcp/blueprint/handlers/review-log.d.ts +14 -7
  122. package/dist/esm/mcp/blueprint/handlers/review-log.js +46 -22
  123. package/dist/esm/mcp/blueprint/handlers/validate.js +6 -0
  124. package/dist/esm/mcp/blueprint/registration.js +1 -1
  125. package/dist/esm/mcp/tools/_names.d.ts +1 -1
  126. package/dist/esm/mcp/tools/_names.js +4 -0
  127. package/dist/esm/mcp/tools/_registry.js +8 -0
  128. package/dist/esm/mcp/tools/_shared/redact.d.ts +22 -0
  129. package/dist/esm/mcp/tools/_shared/redact.js +114 -8
  130. package/dist/esm/mcp/tools/gain.js +28 -2
  131. package/dist/esm/mcp/tools/release-progress.d.ts +9 -0
  132. package/dist/esm/mcp/tools/release-progress.js +110 -0
  133. package/dist/esm/mcp/tools/worktree.js +65 -3
  134. package/dist/esm/mcp/tools/wp-ui-blocks-list.d.ts +3 -0
  135. package/dist/esm/mcp/tools/wp-ui-blocks-list.js +43 -0
  136. package/dist/esm/mcp/tools/wp-ui-compose.d.ts +3 -0
  137. package/dist/esm/mcp/tools/wp-ui-compose.js +96 -0
  138. package/dist/esm/mcp/tools/wp-ui-preview.d.ts +3 -0
  139. package/dist/esm/mcp/tools/wp-ui-preview.js +59 -0
  140. package/dist/esm/package.json +2 -0
  141. package/dist/esm/review/lifecycle-writers.d.ts +76 -0
  142. package/dist/esm/review/lifecycle-writers.js +330 -0
  143. package/dist/esm/review/subject.js +66 -32
  144. package/dist/esm/status/snapshot.d.ts +5 -0
  145. package/dist/esm/status/snapshot.js +27 -2
  146. package/dist/esm/test/shard-durations.json +0 -1
  147. package/dist/esm/test-helpers/global-setup.js +38 -3
  148. package/dist/esm/tool-runtime/managed-rtk.d.ts +44 -0
  149. package/dist/esm/tool-runtime/managed-rtk.js +275 -0
  150. package/dist/esm/tool-runtime/resolve-runner.js +18 -194
  151. package/dist/esm/utils/package-root.d.ts +32 -0
  152. package/dist/esm/utils/package-root.js +82 -0
  153. package/dist/esm/worktrees/git-metadata.d.ts +41 -0
  154. package/dist/esm/worktrees/git-metadata.js +48 -0
  155. package/package.json +27 -21
  156. package/dist/esm/config/oxlint/code-safety.d.ts +0 -18
  157. package/dist/esm/config/oxlint/foundation-purity.d.ts +0 -21
  158. package/dist/esm/config/oxlint/graphql-conventions.d.ts +0 -23
  159. package/dist/esm/config/oxlint/import-hygiene.d.ts +0 -34
  160. package/dist/esm/config/oxlint/index.d.ts +0 -22
  161. package/dist/esm/config/oxlint/index.js +0 -28
  162. package/dist/esm/config/oxlint/monorepo-paths.d.ts +0 -22
  163. package/dist/esm/config/oxlint/query-patterns.d.ts +0 -24
  164. package/dist/esm/config/oxlint/testing-quality.d.ts +0 -35
  165. package/dist/esm/config/oxlint/tier-boundaries.d.ts +0 -35
  166. package/dist/esm/output-transforms/rulesync.d.ts +0 -2
  167. package/dist/esm/output-transforms/rulesync.js +0 -79
@@ -25,6 +25,8 @@
25
25
  "#worktrees/*": "./worktrees/*.js",
26
26
  "#fleet/*.js": "./fleet/*.js",
27
27
  "#fleet/*": "./fleet/*.js",
28
+ "#compose/*.js": "./compose/*.js",
29
+ "#compose/*": "./compose/*.js",
28
30
  "#cli/auto-update/*.js": "./cli/auto-update/*.js",
29
31
  "#cli/auto-update/*": "./cli/auto-update/*.js",
30
32
  "#cli/*.js": "./cli/*.js",
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Registry of every blueprint-document writer, classified by whether it leaves a
3
+ * recorded plan approval intact.
4
+ *
5
+ * The recurring bug class is "a lifecycle writer touches a region the plan-subject
6
+ * normalizer does not neutralize". Each recurrence looked different (a frontmatter
7
+ * key, a readiness stamp, a task status, a 5-cell gate row) but the shape was
8
+ * always the same, and every hand-maintained list of writers used to catch it
9
+ * drifted out of date — including a hand-written test mirror of one writer that
10
+ * implemented only half of it.
11
+ *
12
+ * So the writer list here is not a literal list of names. `discoverLifecycleWriterCandidates`
13
+ * derives it from source: every non-test top-level function under the blueprint
14
+ * mutation surface whose signature is a document transformer (takes
15
+ * `markdown`/`content`/`text: string`, returns `string`). The suite then requires
16
+ * every discovered candidate to be classified in `LIFECYCLE_WRITER_REGISTRY`, so a
17
+ * new writer added without a classification fails CI by construction.
18
+ *
19
+ * Classifications:
20
+ *
21
+ * - `digest-neutral` — asserted `digest(doc) === digest(apply(doc))` across a
22
+ * corpus spanning every supported Promotion Gates row shape.
23
+ * - `known-leak` — asserted to REALLY change the digest, so the classification
24
+ * cannot rot into an unchecked excuse, and required to carry a written reason.
25
+ * - `excluded` — not a document writer (parser helper, generic combinator, or a
26
+ * private function exercised through a registered composite). Requires a reason.
27
+ */
28
+ /**
29
+ * Source roots that make up the blueprint mutation surface. A document writer
30
+ * added anywhere under these paths is discovered automatically.
31
+ */
32
+ export declare const LIFECYCLE_WRITER_SCAN_ROOTS: readonly string[];
33
+ export interface LifecycleWriterCandidate {
34
+ /** Repo-relative module path, POSIX separators. */
35
+ readonly module: string;
36
+ /** Declared function name. */
37
+ readonly fn: string;
38
+ readonly exported: boolean;
39
+ }
40
+ export type LifecycleWriterEntry = {
41
+ readonly module: string;
42
+ readonly fn: string;
43
+ readonly classification: "digest-neutral";
44
+ readonly apply: (markdown: string) => string;
45
+ readonly note?: string;
46
+ } | {
47
+ readonly module: string;
48
+ readonly fn: string;
49
+ readonly classification: "known-leak";
50
+ readonly apply: (markdown: string) => string;
51
+ readonly reason: string;
52
+ } | {
53
+ readonly module: string;
54
+ readonly fn: string;
55
+ readonly classification: "excluded";
56
+ readonly reason: string;
57
+ };
58
+ /** Stable identity of a writer, used to match candidates against the registry. */
59
+ export declare function lifecycleWriterId(entry: {
60
+ module: string;
61
+ fn: string;
62
+ }): string;
63
+ /**
64
+ * Every document-transformer function declared under the blueprint mutation
65
+ * surface, derived from source rather than hand-listed.
66
+ */
67
+ export declare function discoverLifecycleWriterCandidates(repoRoot: string): readonly LifecycleWriterCandidate[];
68
+ export declare const LIFECYCLE_WRITER_REGISTRY: readonly LifecycleWriterEntry[];
69
+ /**
70
+ * Writers currently known to break a recorded plan approval. Empty: every
71
+ * document writer under the blueprint mutation surface is now asserted
72
+ * digest-neutral. Frozen at empty so that a new leak cannot be waved through as
73
+ * "another one of those" — growing this set is an explicit, reviewable registry
74
+ * edit, and the suite refuses a `known-leak` entry that does not actually leak.
75
+ */
76
+ export declare const KNOWN_LEAK_WRITER_IDS: readonly string[];
@@ -0,0 +1,330 @@
1
+ /**
2
+ * Registry of every blueprint-document writer, classified by whether it leaves a
3
+ * recorded plan approval intact.
4
+ *
5
+ * The recurring bug class is "a lifecycle writer touches a region the plan-subject
6
+ * normalizer does not neutralize". Each recurrence looked different (a frontmatter
7
+ * key, a readiness stamp, a task status, a 5-cell gate row) but the shape was
8
+ * always the same, and every hand-maintained list of writers used to catch it
9
+ * drifted out of date — including a hand-written test mirror of one writer that
10
+ * implemented only half of it.
11
+ *
12
+ * So the writer list here is not a literal list of names. `discoverLifecycleWriterCandidates`
13
+ * derives it from source: every non-test top-level function under the blueprint
14
+ * mutation surface whose signature is a document transformer (takes
15
+ * `markdown`/`content`/`text: string`, returns `string`). The suite then requires
16
+ * every discovered candidate to be classified in `LIFECYCLE_WRITER_REGISTRY`, so a
17
+ * new writer added without a classification fails CI by construction.
18
+ *
19
+ * Classifications:
20
+ *
21
+ * - `digest-neutral` — asserted `digest(doc) === digest(apply(doc))` across a
22
+ * corpus spanning every supported Promotion Gates row shape.
23
+ * - `known-leak` — asserted to REALLY change the digest, so the classification
24
+ * cannot rot into an unchecked excuse, and required to carry a written reason.
25
+ * - `excluded` — not a document writer (parser helper, generic combinator, or a
26
+ * private function exercised through a registered composite). Requires a reason.
27
+ */
28
+ import { readdirSync, readFileSync, statSync } from "node:fs";
29
+ import path from "node:path";
30
+ import { clearBlueprintExecutionArtifacts, writeBlueprintExecutionArtifacts, } from "#execution/artifacts.js";
31
+ import { clearBlueprintExecutionMetadata, writeBlueprintExecutionMetadata, } from "#execution/metadata.js";
32
+ import { setBlueprintFrontmatterFields } from "#lifecycle/engine.js";
33
+ import { checkAllCheckboxes, checkFirstCheckbox, completeTask, updateBlockedReason, updateTaskStatus, } from "#markdown/helpers.js";
34
+ import { updateGateLastResult, upsertReadinessValue } from "#trust/promotion.js";
35
+ import { applyVerification } from "#verification.js";
36
+ import { updateFrontmatterStatus, upsertCompletedAt } from "#cli/commands/blueprint/mutations.js";
37
+ /**
38
+ * Source roots that make up the blueprint mutation surface. A document writer
39
+ * added anywhere under these paths is discovered automatically.
40
+ */
41
+ export const LIFECYCLE_WRITER_SCAN_ROOTS = [
42
+ "src/blueprint",
43
+ "src/cli/commands/blueprint",
44
+ "src/mcp/blueprint",
45
+ ];
46
+ /**
47
+ * Parameter names this repo uses for "the whole blueprint document". Kept
48
+ * deliberately broad so a writer cannot dodge discovery by renaming its
49
+ * parameter.
50
+ */
51
+ const DOCUMENT_PARAMETER_NAMES = ["markdown", "content", "text"];
52
+ const DOCUMENT_PARAMETER_PATTERN = new RegExp(`\\b(?:${DOCUMENT_PARAMETER_NAMES.join("|")})\\s*:\\s*string`, "u");
53
+ const FUNCTION_DECLARATION_PATTERN = /(^|\n)(export\s+)?(?:async\s+)?function\s+([A-Za-z0-9_$]+)\s*\(/gu;
54
+ /** Stable identity of a writer, used to match candidates against the registry. */
55
+ export function lifecycleWriterId(entry) {
56
+ return `${entry.module}::${entry.fn}`;
57
+ }
58
+ function isScannableSource(name) {
59
+ return (name.endsWith(".ts") &&
60
+ !name.endsWith(".test.ts") &&
61
+ !name.endsWith(".d.ts") &&
62
+ !name.endsWith("test-harness.ts"));
63
+ }
64
+ function collectSourceFiles(directory, out) {
65
+ for (const name of readdirSync(directory)) {
66
+ const full = path.join(directory, name);
67
+ if (statSync(full).isDirectory())
68
+ collectSourceFiles(full, out);
69
+ else if (isScannableSource(name))
70
+ out.push(full);
71
+ }
72
+ }
73
+ /** Signature text between the declaration's parentheses, plus its return type. */
74
+ function readSignature(source, declarationEnd) {
75
+ const openParen = source.indexOf("(", declarationEnd);
76
+ let depth = 0;
77
+ let cursor = openParen;
78
+ for (; cursor < source.length; cursor += 1) {
79
+ const character = source[cursor];
80
+ if (character === "(")
81
+ depth += 1;
82
+ else if (character === ")") {
83
+ depth -= 1;
84
+ if (depth === 0)
85
+ break;
86
+ }
87
+ }
88
+ const brace = source.indexOf("{", cursor);
89
+ return {
90
+ params: source.slice(openParen + 1, cursor),
91
+ ret: source
92
+ .slice(cursor + 1, brace)
93
+ .replace(/^:\s*/u, "")
94
+ .trim(),
95
+ };
96
+ }
97
+ /**
98
+ * Every document-transformer function declared under the blueprint mutation
99
+ * surface, derived from source rather than hand-listed.
100
+ */
101
+ export function discoverLifecycleWriterCandidates(repoRoot) {
102
+ const candidates = [];
103
+ for (const root of LIFECYCLE_WRITER_SCAN_ROOTS) {
104
+ const files = [];
105
+ collectSourceFiles(path.join(repoRoot, root), files);
106
+ for (const file of files) {
107
+ const source = readFileSync(file, "utf8");
108
+ const pattern = new RegExp(FUNCTION_DECLARATION_PATTERN.source, "gu");
109
+ let match = pattern.exec(source);
110
+ while (match) {
111
+ const { params, ret } = readSignature(source, match.index + (match[1] ?? "").length);
112
+ if (ret === "string" && DOCUMENT_PARAMETER_PATTERN.test(params)) {
113
+ candidates.push({
114
+ module: path.relative(repoRoot, file).split(path.sep).join("/"),
115
+ fn: match[3] ?? "",
116
+ exported: Boolean(match[2]),
117
+ });
118
+ }
119
+ match = pattern.exec(source);
120
+ }
121
+ }
122
+ }
123
+ return candidates.toSorted((left, right) => lifecycleWriterId(left).localeCompare(lifecycleWriterId(right)));
124
+ }
125
+ const TASK_ID = "1.1";
126
+ const PASSING_TEST_EVIDENCE = [
127
+ {
128
+ kind: "test",
129
+ result: "pass",
130
+ ts: "2026-07-25T00:00:00.000Z",
131
+ command: "wp test",
132
+ exit_code: 0,
133
+ },
134
+ ];
135
+ const EXECUTION_METADATA = {
136
+ backend: "local-worktree",
137
+ executionId: "exec-1",
138
+ status: "running",
139
+ updatedAt: "2026-07-25T00:00:00.000Z",
140
+ };
141
+ const EXECUTION_ARTIFACTS = {
142
+ artifacts: ["dist/report.json"],
143
+ logPath: ".webpresso/logs/exec-1.log",
144
+ verifications: ["1.1"],
145
+ };
146
+ export const LIFECYCLE_WRITER_REGISTRY = [
147
+ // ---- Promotion / lifecycle frontmatter and trust stamps -----------------
148
+ {
149
+ module: "src/cli/commands/blueprint/mutations.ts",
150
+ fn: "updateFrontmatterStatus",
151
+ classification: "digest-neutral",
152
+ apply: (markdown) => updateFrontmatterStatus(markdown, "planned"),
153
+ },
154
+ {
155
+ module: "src/cli/commands/blueprint/mutations.ts",
156
+ fn: "upsertCompletedAt",
157
+ classification: "digest-neutral",
158
+ apply: (markdown) => upsertCompletedAt(markdown, "2026-07-25"),
159
+ },
160
+ {
161
+ module: "src/blueprint/trust/promotion.ts",
162
+ fn: "upsertReadinessValue",
163
+ classification: "digest-neutral",
164
+ apply: (markdown) => upsertReadinessValue(markdown, "verified-at", "2026-07-25T00:00:00.000Z"),
165
+ },
166
+ {
167
+ module: "src/blueprint/trust/promotion.ts",
168
+ fn: "updateGateLastResult",
169
+ classification: "digest-neutral",
170
+ apply: (markdown) => updateGateLastResult(markdown, "Lint", "pass at 2026-07-25T00:00:00.000Z"),
171
+ },
172
+ {
173
+ module: "src/blueprint/lifecycle/engine.ts",
174
+ fn: "setBlueprintFrontmatterFields",
175
+ classification: "digest-neutral",
176
+ apply: (markdown) => setBlueprintFrontmatterFields(markdown, {
177
+ progress: "50% (1/2 tasks done, 0 blocked, updated 2026-07-25)",
178
+ last_updated: "2026-07-25",
179
+ worktree_owner_id: "owner-abc123",
180
+ worktree_owner_branch: "bp/some-slug",
181
+ }),
182
+ note: "Exercised with the lifecycle intents its callers use; the keys are in the normalizer's strip set.",
183
+ },
184
+ // ---- Task execution state ----------------------------------------------
185
+ {
186
+ module: "src/blueprint/markdown/helpers.ts",
187
+ fn: "updateTaskStatus",
188
+ classification: "digest-neutral",
189
+ apply: (markdown) => updateTaskStatus(markdown, TASK_ID, "done"),
190
+ },
191
+ {
192
+ module: "src/blueprint/markdown/helpers.ts",
193
+ fn: "checkFirstCheckbox",
194
+ classification: "digest-neutral",
195
+ apply: (markdown) => checkFirstCheckbox(markdown, TASK_ID),
196
+ },
197
+ {
198
+ module: "src/blueprint/markdown/helpers.ts",
199
+ fn: "checkAllCheckboxes",
200
+ classification: "digest-neutral",
201
+ apply: (markdown) => checkAllCheckboxes(markdown, TASK_ID),
202
+ },
203
+ {
204
+ module: "src/blueprint/markdown/helpers.ts",
205
+ fn: "completeTask",
206
+ classification: "digest-neutral",
207
+ apply: (markdown) => completeTask(markdown, TASK_ID),
208
+ },
209
+ {
210
+ // Registered although it is not a discovered candidate (it returns a
211
+ // VerificationResult, not a string): it is the composite that actually runs
212
+ // on `wp_blueprint_task_verify`, and registering it is what lets the private
213
+ // `insertVerificationAfterStatus` / `removeVerificationFromTask` helpers be
214
+ // excluded honestly.
215
+ module: "src/blueprint/verification.ts",
216
+ fn: "applyVerification",
217
+ classification: "digest-neutral",
218
+ apply: (markdown) => {
219
+ const result = applyVerification(markdown, TASK_ID, PASSING_TEST_EVIDENCE);
220
+ if (!result.ok)
221
+ throw new Error(`applyVerification failed: ${result.failures.join("; ")}`);
222
+ return result.markdown;
223
+ },
224
+ },
225
+ {
226
+ module: "src/blueprint/markdown/helpers.ts",
227
+ fn: "updateBlockedReason",
228
+ classification: "digest-neutral",
229
+ apply: (markdown) => updateBlockedReason(markdown, TASK_ID, "waiting on an upstream release"),
230
+ note: "The `**Blocked:** <reason>` line it writes is stripped inside task sections by `neutralizeTaskBlockedReason`. Writer and normalizer share the grammar in src/blueprint/markdown/blocked-line.ts, so they cannot drift apart again.",
231
+ },
232
+ // ---- Execution metadata / artifacts (real _overview.md writers) ---------
233
+ {
234
+ module: "src/blueprint/execution/metadata.ts",
235
+ fn: "writeBlueprintExecutionMetadata",
236
+ classification: "digest-neutral",
237
+ apply: (markdown) => writeBlueprintExecutionMetadata(markdown, EXECUTION_METADATA),
238
+ note: "Its `execution_*` frontmatter keys are declared once in src/blueprint/execution/frontmatter-keys.ts, typed into this writer's field object and spread into the normalizer's strip set, so a new key cannot reach a document without also being stripped.",
239
+ },
240
+ {
241
+ module: "src/blueprint/execution/artifacts.ts",
242
+ fn: "writeBlueprintExecutionArtifacts",
243
+ classification: "digest-neutral",
244
+ apply: (markdown) => writeBlueprintExecutionArtifacts(markdown, EXECUTION_ARTIFACTS),
245
+ note: "Same shared key owner as writeBlueprintExecutionMetadata, for `execution_artifacts` / `execution_log_path` / `execution_verifications`.",
246
+ },
247
+ {
248
+ module: "src/blueprint/execution/metadata.ts",
249
+ fn: "clearBlueprintExecutionMetadata",
250
+ classification: "digest-neutral",
251
+ apply: (markdown) => clearBlueprintExecutionMetadata(writeBlueprintExecutionMetadata(markdown, EXECUTION_METADATA)),
252
+ note: "Asserted as the exact inverse of its writer: write-then-clear restores the original digest.",
253
+ },
254
+ {
255
+ module: "src/blueprint/execution/artifacts.ts",
256
+ fn: "clearBlueprintExecutionArtifacts",
257
+ classification: "digest-neutral",
258
+ apply: (markdown) => clearBlueprintExecutionArtifacts(writeBlueprintExecutionArtifacts(markdown, EXECUTION_ARTIFACTS)),
259
+ note: "Asserted as the exact inverse of its writer: write-then-clear restores the original digest.",
260
+ },
261
+ // ---- Not document writers ----------------------------------------------
262
+ {
263
+ module: "src/blueprint/lifecycle/engine.ts",
264
+ fn: "applyTaskIntent",
265
+ classification: "excluded",
266
+ reason: "Private composite: delegates entirely to updateTaskStatus, updateBlockedReason, and applyVerification, all registered above.",
267
+ },
268
+ {
269
+ module: "src/blueprint/execution/progress-bridge.ts",
270
+ fn: "applyIntent",
271
+ classification: "excluded",
272
+ reason: "Private composite: delegates to applyBlueprintLifecycle, whose document writes are the registered primitives above.",
273
+ },
274
+ {
275
+ module: "src/blueprint/verification.ts",
276
+ fn: "insertVerificationAfterStatus",
277
+ classification: "excluded",
278
+ reason: "Private helper, exercised through the registered applyVerification entry.",
279
+ },
280
+ {
281
+ module: "src/blueprint/verification.ts",
282
+ fn: "removeVerificationFromTask",
283
+ classification: "excluded",
284
+ reason: "Private helper, exercised through the registered applyVerification entry.",
285
+ },
286
+ {
287
+ module: "src/blueprint/verification.ts",
288
+ fn: "mapTaskSection",
289
+ classification: "excluded",
290
+ reason: "Generic combinator that applies a caller-supplied transform to one task section; it owns no lifecycle field of its own.",
291
+ },
292
+ {
293
+ module: "src/blueprint/trust/gate-row.ts",
294
+ fn: "mapPromotionGateRows",
295
+ classification: "excluded",
296
+ reason: "Generic combinator that applies a caller-supplied transform to Promotion Gates rows; its two callers (updateGateLastResult and neutralizePromotionGateResults) are what must agree, and the writer side is registered above.",
297
+ },
298
+ {
299
+ module: "src/blueprint/trust/dossier.ts",
300
+ fn: "stripFencedCodeBlocks",
301
+ classification: "excluded",
302
+ reason: "Parser helper: produces a derived string for Trust Dossier parsing that is never written back to disk.",
303
+ },
304
+ {
305
+ module: "src/blueprint/tech-debt/parser.ts",
306
+ fn: "extractTitle",
307
+ classification: "excluded",
308
+ reason: "Reads a tech-debt document (`h-NNN-slug.md`), not a blueprint overview, and returns a title rather than a document.",
309
+ },
310
+ {
311
+ module: "src/blueprint/trust/command-runner.ts",
312
+ fn: "tailBounded",
313
+ classification: "excluded",
314
+ reason: "Bounds captured subprocess output for an error message; never touches a blueprint document.",
315
+ },
316
+ {
317
+ module: "src/blueprint/context.ts",
318
+ fn: "clampText",
319
+ classification: "excluded",
320
+ reason: "Generic string truncation for context payloads; never writes a blueprint document.",
321
+ },
322
+ ];
323
+ /**
324
+ * Writers currently known to break a recorded plan approval. Empty: every
325
+ * document writer under the blueprint mutation surface is now asserted
326
+ * digest-neutral. Frozen at empty so that a new leak cannot be waved through as
327
+ * "another one of those" — growing this set is an explicit, reviewable registry
328
+ * edit, and the suite refuses a `known-leak` entry that does not actually leak.
329
+ */
330
+ export const KNOWN_LEAK_WRITER_IDS = LIFECYCLE_WRITER_REGISTRY.filter((entry) => entry.classification === "known-leak").map(lifecycleWriterId);
@@ -1,5 +1,9 @@
1
1
  import { execFileSync } from "node:child_process";
2
2
  import { createHash } from "node:crypto";
3
+ import { EXECUTION_ARTIFACTS_FRONTMATTER_KEYS, EXECUTION_METADATA_FRONTMATTER_KEYS, } from "#execution/frontmatter-keys.js";
4
+ import { isBlockedLine } from "#markdown/blocked-line.js";
5
+ import { isTaskHeaderLine } from "#markdown/task-heading.js";
6
+ import { mapPromotionGateRows, setLastResultCell } from "#trust/gate-row.js";
3
7
  const GIT_PROBE_TIMEOUT_MS = 5_000;
4
8
  const GIT_MAX_BUFFER_BYTES = 128 * 1024 * 1024;
5
9
  const DELIVERY_DOMAIN = Buffer.from("webpresso-review-subject\0v1\0", "ascii");
@@ -274,39 +278,16 @@ const PLAN_STATUS_PLACEHOLDER = "__wp-lifecycle-status__";
274
278
  const PLAN_GATE_RESULT_PLACEHOLDER = "__wp-gate-result__";
275
279
  // Neutralize the Promotion Gates "Last result" stamps that
276
280
  // applyPromotionTrustGate rewrites (updateGateLastResult in
277
- // src/blueprint/trust/promotion.ts). Scoped to the `### Promotion Gates`
278
- // subsection; every 4-cell table row is rewritten with trimmed cells and a
279
- // placeholder result cell, mirroring the writer's own whitespace
280
- // canonicalization so pre- and post-promotion rows normalize identically.
281
+ // src/blueprint/trust/promotion.ts). The row grammar and the section scope are
282
+ // NOT restated here: both come from #trust/gate-row.js, which the writer
283
+ // consumes too. That shared owner exists because the two sides previously
284
+ // implemented the grammar twice and diverged on cell count, section scope,
285
+ // trailing pipe, and trailing whitespace — each divergence letting a lifecycle
286
+ // stamp survive into the digest and dismiss the approval the promotion gates on.
281
287
  // Deliberate NO_CODE_CHANGE semantics: whitespace-only table edits and "Last
282
288
  // result" values are not plan substance.
283
289
  function neutralizePromotionGateResults(text) {
284
- const lines = text.split("\n");
285
- let inGatesSection = false;
286
- for (let index = 0; index < lines.length; index += 1) {
287
- const line = lines[index];
288
- const trimmed = line.trim();
289
- // Exact end-anchored heading match, mirroring the dossier parser's
290
- // sectionBlock regex — "### Promotion Gates Notes" is NOT the owned section.
291
- if (/^###\s+Promotion Gates\s*$/iu.test(line)) {
292
- inGatesSection = true;
293
- continue;
294
- }
295
- if (inGatesSection && /^#{1,6}\s/u.test(line)) {
296
- inGatesSection = false;
297
- continue;
298
- }
299
- if (!inGatesSection || !trimmed.startsWith("|") || !trimmed.endsWith("|"))
300
- continue;
301
- const cells = trimmed
302
- .slice(1, -1)
303
- .split("|")
304
- .map((cell) => cell.trim());
305
- if (cells.length !== 4)
306
- continue;
307
- lines[index] = `| ${cells[0]} | ${cells[1]} | ${cells[2]} | ${PLAN_GATE_RESULT_PLACEHOLDER} |`;
308
- }
309
- return lines.join("\n");
290
+ return mapPromotionGateRows(text, (cells) => setLastResultCell(cells, PLAN_GATE_RESULT_PLACEHOLDER));
310
291
  }
311
292
  // Frontmatter keys removed wholesale by the normalizer (value + folded
312
293
  // continuation lines). Each mirrors exactly one lifecycle writer:
@@ -320,11 +301,27 @@ const PLAN_LIFECYCLE_STRIPPED_KEYS_CORE = [
320
301
  "worktree_owner_id",
321
302
  "worktree_owner_branch",
322
303
  ];
323
- // Current plan-subject strip set: core lifecycle keys plus advisory `tags`.
304
+ // Current plan-subject strip set: core lifecycle keys, advisory `tags`, and the
305
+ // `execution_*` keys the execution backend stamps.
306
+ //
324
307
  // Tags are not plan substance; MCP put and CLI serialize disagree on array-item
325
308
  // quoting (`- "x"` vs `- x`). Approvals recorded before the tags strip must
326
309
  // still verify via `preTagsStripNormalizePlanSubjectContent`.
327
- const PLAN_LIFECYCLE_STRIPPED_KEYS = [...PLAN_LIFECYCLE_STRIPPED_KEYS_CORE, "tags"];
310
+ //
311
+ // The `execution_*` names come from `#execution/frontmatter-keys.js`, which the
312
+ // writers in `#execution/metadata.js` / `#execution/artifacts.js` type their
313
+ // field objects against — one owner, so a new execution key cannot enter the
314
+ // document without also entering this strip set. They are added to the CURRENT
315
+ // set only, never to `_CORE`: `_CORE` defines the frozen pre-tags compatibility
316
+ // tier, and editing it would retroactively change digests that tier already
317
+ // produced. Stripping a key no document carries is a no-op, and no tracked
318
+ // blueprint carries an `execution_*` key today, so nothing is restated.
319
+ const PLAN_LIFECYCLE_STRIPPED_KEYS = [
320
+ ...PLAN_LIFECYCLE_STRIPPED_KEYS_CORE,
321
+ "tags",
322
+ ...EXECUTION_METADATA_FRONTMATTER_KEYS,
323
+ ...EXECUTION_ARTIFACTS_FRONTMATTER_KEYS,
324
+ ];
328
325
  // Neutralize the lifecycle-owned frontmatter fields (`status:` value plus the
329
326
  // stripped keys above, including advisory `tags`) INSIDE the leading frontmatter block only. A body line
330
327
  // that merely looks like one of these (e.g. task text reading
@@ -393,8 +390,11 @@ function neutralizeReadinessStamps(text) {
393
390
  // lifecycle writer, scoped to the location that writer owns:
394
391
  // - frontmatter status/progress/owner fields (blueprint lifecycle mutations)
395
392
  // - frontmatter `completed_at:` line (upsertCompletedAt)
393
+ // - frontmatter `execution_*:` keys (writeBlueprintExecutionMetadata /
394
+ // writeBlueprintExecutionArtifacts)
396
395
  // - Readiness Verdict `- verified-at:`/`- verified-head:` items (upsertReadinessValue)
397
396
  // - Promotion Gates "Last result" cells (updateGateLastResult)
397
+ // - task-section `**Blocked:** <reason>` lines (updateBlockedReason)
398
398
  // Any other byte change — task text, acceptance criteria, scope, lookalike
399
399
  // lines outside the owned locations — still changes the digest and dismisses
400
400
  // prior plan approvals.
@@ -437,12 +437,46 @@ function neutralizeTaskExecutionState(text) {
437
437
  }
438
438
  return out.join("\n");
439
439
  }
440
+ // Remove the `**Blocked:** <reason>` line that `wp blueprint task block` writes
441
+ // into a task section (updateBlockedReason). The line grammar comes from
442
+ // `#markdown/blocked-line.js`, which the writer consumes too — same shared-owner
443
+ // arrangement as the Promotion Gates row grammar, and for the same reason.
444
+ //
445
+ // Scoped to task sections, mirroring the only place the writer can insert it.
446
+ // Unlike `**Status:**`, whose value is a closed enum, a blocked reason is free
447
+ // text, so a document-wide rule would have no grammar left to keep it honest: a
448
+ // Notes paragraph opening with the marker is plan substance and stays hashed.
449
+ //
450
+ // The writer emits the line followed by one blank line (insertAfterStatusLine
451
+ // appends `line + "\n\n"`), so exactly one following blank line is removed with
452
+ // it — that is the byte shape the writer owns, no more.
453
+ function neutralizeTaskBlockedReason(text) {
454
+ const lines = text.split("\n");
455
+ const out = [];
456
+ let inTaskSection = false;
457
+ for (let index = 0; index < lines.length; index += 1) {
458
+ const line = lines[index];
459
+ if (/^#{1,6}\s/u.test(line)) {
460
+ inTaskSection = isTaskHeaderLine(line);
461
+ out.push(line);
462
+ continue;
463
+ }
464
+ if (inTaskSection && isBlockedLine(line)) {
465
+ if ((lines[index + 1] ?? "").trim() === "")
466
+ index += 1;
467
+ continue;
468
+ }
469
+ out.push(line);
470
+ }
471
+ return out.join("\n");
472
+ }
440
473
  export function normalizePlanSubjectContent(content) {
441
474
  let text = typeof content === "string" ? content : content.toString("utf8");
442
475
  text = neutralizeLifecycleFrontmatter(text);
443
476
  text = neutralizeReadinessStamps(text);
444
477
  text = neutralizePromotionGateResults(text);
445
478
  text = neutralizeTaskExecutionState(text);
479
+ text = neutralizeTaskBlockedReason(text);
446
480
  return Buffer.from(text, "utf8");
447
481
  }
448
482
  /**
@@ -51,6 +51,10 @@ export declare const wpStatusSnapshotSchema: z.ZodObject<{
51
51
  path: z.ZodString;
52
52
  lifecycle: z.ZodString;
53
53
  approvalsSatisfied: z.ZodBoolean;
54
+ approvalsSatisfiedBy: z.ZodOptional<z.ZodEnum<{
55
+ delivery: "delivery";
56
+ plan: "plan";
57
+ }>>;
54
58
  }, z.core.$strip>>;
55
59
  ultragoal: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
56
60
  reviews: z.ZodOptional<z.ZodObject<{
@@ -127,6 +131,7 @@ export interface PrReadinessEvidence {
127
131
  readonly path: string;
128
132
  readonly lifecycle: string;
129
133
  readonly approvalsSatisfied: boolean;
134
+ readonly approvalsSatisfiedBy?: "plan" | "delivery";
130
135
  };
131
136
  }
132
137
  interface ReadinessEvaluation {
@@ -42,6 +42,9 @@ const statusContextSchema = z.object({
42
42
  path: z.string(),
43
43
  lifecycle: z.string(),
44
44
  approvalsSatisfied: z.boolean(),
45
+ // Which review purpose carried the satisfying approval ("plan" or
46
+ // "delivery"), so a satisfied/unsatisfied result is attributable.
47
+ approvalsSatisfiedBy: z.enum(["plan", "delivery"]).optional(),
45
48
  })
46
49
  .optional(),
47
50
  ultragoal: z.record(z.string(), z.unknown()).optional(),
@@ -319,6 +322,20 @@ function selectBlueprintFile(projectRoot, pr) {
319
322
  const priority = (file) => file.changeType === "ADDED" ? 0 : file.changeType === "RENAMED" ? 1 : 2;
320
323
  return candidates.toSorted((left, right) => priority(left) - priority(right))[0];
321
324
  }
325
+ // The authoritative CI gate (`validateApprovalGate` -> `provenanceBackedApprovalReviewers`)
326
+ // accepts an approval recorded for EITHER purpose: it defaults to
327
+ // `["plan", "delivery"]` when no purpose is pinned. This advisory snapshot used to
328
+ // query `"delivery"` only, so a blueprint approved at the plan boundary reported
329
+ // `review-provenance` unsatisfied while CI passed on the same commit — an advisory
330
+ // blocker stricter than the gate it advises about, which reads as a real problem.
331
+ // Union both purposes here so the two agree.
332
+ //
333
+ // Deliberately NOT reusing `provenanceBackedApprovalReviewers` directly: that helper
334
+ // resolves v2 provenance at `HEAD`, whereas this snapshot must answer for the PR's
335
+ // exact target commit. Matching its purpose set while keeping `targetHash` is the
336
+ // smallest change that makes the two evaluators agree without loosening exact-SHA
337
+ // semantics.
338
+ const APPROVAL_PURPOSES = ["plan", "delivery"];
322
339
  function resolveBlueprintEvidence(projectRoot, pr, targetHash) {
323
340
  const blueprintFile = selectBlueprintFile(projectRoot, pr);
324
341
  if (!blueprintFile || !targetHash)
@@ -327,12 +344,20 @@ function resolveBlueprintEvidence(projectRoot, pr, targetHash) {
327
344
  const authority = readReviewAuthorityAtRef(projectRoot, targetHash, blueprintFile.stableSlug, "delivery");
328
345
  if (authority.blueprintPath !== blueprintFile.path)
329
346
  return undefined;
330
- const approvals = collectV2ReviewProvenance(path.resolve(projectRoot, blueprintFile.path), "delivery", targetHash);
347
+ const blueprintPath = path.resolve(projectRoot, blueprintFile.path);
348
+ const perPurpose = APPROVAL_PURPOSES.map((purpose) => ({
349
+ purpose,
350
+ reviewers: collectV2ReviewProvenance(blueprintPath, purpose, targetHash).map((approval) => approval.reviewer),
351
+ }));
352
+ const satisfiedPurpose = perPurpose.find((entry) => evaluateApprovalReviewers(entry.reviewers).satisfied);
331
353
  return {
332
354
  path: blueprintFile.path,
333
355
  lifecycle: authority.lifecycle,
334
- approvalsSatisfied: evaluateApprovalReviewers(approvals.map((approval) => approval.reviewer))
356
+ approvalsSatisfied: evaluateApprovalReviewers(perPurpose.flatMap((entry) => entry.reviewers))
335
357
  .satisfied,
358
+ // Which purpose carried the approval, so a red/green flip is attributable
359
+ // instead of opaque.
360
+ approvalsSatisfiedBy: satisfiedPurpose?.purpose,
336
361
  };
337
362
  }
338
363
  catch {
@@ -560,7 +560,6 @@
560
560
  "src/audit/no-legacy-cli-bin.test.ts": 56.865957999999864,
561
561
  "src/mcp/tools/session-elision.test.ts": 4564.6535,
562
562
  "src/hooks/pretool-guard/validators/ux-quality.test.ts": 105.19341700000041,
563
- "src/config/oxlint/monorepo-paths.test.ts": 11.33191700000043,
564
563
  "src/cli/commands/init/source-repo-hook-policy.test.ts": 65.89804200000003,
565
564
  "src/secrets/config/schema.test.ts": 190.3588749999999,
566
565
  "scripts/bench/scenarios/_schema.test.ts": 53.78470799999968,