@webpresso/agent-kit 3.3.3 → 3.3.5
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/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/dist/esm/audit/agents.js +126 -26
- 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 +38 -12
- 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 +63 -10
- 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/dossier.js +41 -1
- 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/vendor-oxlint-plugins.d.ts +11 -0
- package/dist/esm/build/vendor-oxlint-plugins.js +48 -0
- 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 +32 -52
- package/dist/esm/cli/commands/init/mcp-spec.d.ts +48 -13
- package/dist/esm/cli/commands/init/mcp-spec.js +86 -30
- package/dist/esm/cli/commands/init/merge.js +30 -10
- 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/pr-help.js +1 -1
- package/dist/esm/cli/commands/pr.js +18 -4
- package/dist/esm/cli/commands/review.d.ts +1 -0
- package/dist/esm/cli/commands/review.js +137 -7
- package/dist/esm/cli/commands/sync.d.ts +8 -6
- package/dist/esm/cli/commands/sync.js +11 -9
- 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 +8 -0
- package/dist/esm/compose/compose.js +115 -8
- package/dist/esm/compose/ops-report-schema.js +17 -3
- package/dist/esm/compose/registry-resolve.d.ts +17 -0
- package/dist/esm/compose/registry-resolve.js +78 -11
- 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 +19 -31
- package/dist/esm/config/oxlint/path-roles.js +53 -0
- package/dist/esm/config/oxlint/query-patterns.js +2 -5
- package/dist/esm/content/dispatch.js +4 -0
- package/dist/esm/content/loader.d.ts +1 -1
- package/dist/esm/content/loader.js +10 -6
- package/dist/esm/content/skill-policy.d.ts +11 -0
- package/dist/esm/content/skill-policy.js +33 -0
- 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/hooks/doctor.d.ts +11 -0
- package/dist/esm/hooks/doctor.js +114 -45
- package/dist/esm/hooks/pretool-guard/dev-routing.js +68 -5
- 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/pretool-guard/validators/worktree-discipline.js +62 -3
- package/dist/esm/hooks/status/index.js +1 -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/registration.js +1 -1
- 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/worktree.js +65 -3
- package/dist/esm/mcp/tools/wp-ui-compose.js +38 -4
- package/dist/esm/mcp/tools/wp-ui-preview.js +14 -7
- package/dist/esm/review/authority.js +56 -1
- package/dist/esm/review/delivery-verifier.d.ts +4 -0
- package/dist/esm/review/delivery-verifier.js +9 -1
- 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/symlinker/consumers.d.ts +3 -1
- package/dist/esm/symlinker/consumers.js +7 -1
- package/dist/esm/symlinker/unified-sync.d.ts +3 -3
- package/dist/esm/symlinker/unified-sync.js +88 -11
- package/dist/esm/test/shard-durations.json +0 -2
- 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 +16 -21
- package/dist/esm/cli/commands/init/scaffolders/subagents/index.d.ts +0 -6
- package/dist/esm/cli/commands/init/scaffolders/subagents/index.js +0 -77
- 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
|
@@ -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).
|
|
278
|
-
//
|
|
279
|
-
//
|
|
280
|
-
//
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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(
|
|
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 {
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
* the canonical `.agent/{rules,skills}` SSOT always project.
|
|
18
18
|
*
|
|
19
19
|
* The `UNIFIED_CONSUMERS` registry below describes per-IDE projection of the
|
|
20
|
-
* unified rule/skill content kinds (catalog ∪ consumer). Strategies:
|
|
20
|
+
* unified rule/skill/agent content kinds (catalog ∪ consumer). Strategies:
|
|
21
21
|
* - 'symlink': create a relative symlink to the source (file or dir)
|
|
22
22
|
* - 'copy': copy file or recursively copy dir tree
|
|
23
23
|
* - 'transform': run a transform function over the body and write the
|
|
@@ -116,6 +116,8 @@ export declare function unifiedRuleFilename(consumer: UnifiedConsumerConfig, slu
|
|
|
116
116
|
* `selectUnifiedConsumers(hosts)`:
|
|
117
117
|
* - `.agent/{rules,skills}/` (working dir, SSOT): symlink, always
|
|
118
118
|
* - `.claude/rules/`: symlink, always (rules are not plugin-delivered)
|
|
119
|
+
* - `.claude/agents/`: symlink, always (canonical subagents; Claude has no
|
|
120
|
+
* native subagent delivery channel, so this projection is not plugin-owned)
|
|
119
121
|
*
|
|
120
122
|
* Project-local legacy skill roots are intentionally omitted. Codex has no
|
|
121
123
|
* `.codex/agents/` consumer.
|