@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
|
@@ -30,13 +30,20 @@ const reviewLogSchema = z.object({
|
|
|
30
30
|
});
|
|
31
31
|
/**
|
|
32
32
|
* MCP wrapper around the SAME reviews.md-ledger writer `logReviewEntry` that
|
|
33
|
-
* (an unwired) `wp review log` uses
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
* trust model a hand-authored `approvals:` block already had (the
|
|
37
|
-
* is caller-asserted, verified only by the referenced
|
|
38
|
-
* entry being tracked in git) — this tool just makes that
|
|
39
|
-
* reachable from MCP instead of a raw Write/Edit or a Bash/python
|
|
33
|
+
* (an unwired) `wp review log` uses. For NON-approval verdicts it also appends
|
|
34
|
+
* the matching frontmatter `approvals:` entry, so rejection attribution stays
|
|
35
|
+
* visible where readers already look. This is the tracked/frontmatter provenance
|
|
36
|
+
* path — the same trust model a hand-authored `approvals:` block already had (the
|
|
37
|
+
* reviewer id is caller-asserted, verified only by the referenced
|
|
38
|
+
* artifact/reviews.md entry being tracked in git) — this tool just makes that
|
|
39
|
+
* existing path reachable from MCP instead of a raw Write/Edit or a Bash/python
|
|
40
|
+
* workaround.
|
|
41
|
+
*
|
|
42
|
+
* It never writes a POLICY-APPROVAL entry (approve / approve-with-nits) into
|
|
43
|
+
* frontmatter. `collectTrackedReviewProvenance` — the legacy promotion leg — only
|
|
44
|
+
* counts policy approvals and never reads or hashes the blueprint body, so such
|
|
45
|
+
* an entry would be gate authority that survives an arbitrary rewrite of the
|
|
46
|
+
* plan. See `LEGACY_APPROVAL_LEG_ANCHOR_COMMIT` in `#lifecycle/rollout-anchor.js`.
|
|
40
47
|
*
|
|
41
48
|
* A policy-approval verdict (approve / approve-with-nits) REQUIRES
|
|
42
49
|
* reviewer==="human" — matching `logReviewEvent`'s existing v2-ledger
|
|
@@ -143,22 +150,39 @@ export async function handleReviewLog(projectResolver, cwd, raw) {
|
|
|
143
150
|
...(rev ? { rev } : {}),
|
|
144
151
|
...(note ? { note } : {}),
|
|
145
152
|
});
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
const
|
|
159
|
-
|
|
153
|
+
// Policy-approval verdicts are NOT projected into frontmatter `approvals:`.
|
|
154
|
+
// That field feeds `collectTrackedReviewProvenance`, the legacy promotion leg
|
|
155
|
+
// that carries no content binding at all: it matches a reviews.md record and
|
|
156
|
+
// a tracked artifact but never reads or hashes the blueprint body, and the
|
|
157
|
+
// gate supplies no `targetHash`. Writing an approval there would mint
|
|
158
|
+
// authority that survives an arbitrary rewrite of the plan. Non-approval
|
|
159
|
+
// verdicts (reject / no-verdict) are still recorded, because the legacy leg
|
|
160
|
+
// only ever counts policy approvals -- they are attribution, never authority.
|
|
161
|
+
//
|
|
162
|
+
// Nothing is lost: the approval is fully recorded in reviews.md and in the
|
|
163
|
+
// tracked artifact. Satisfying the promotion gate still requires a converged
|
|
164
|
+
// `wp review gate` run (content-addressed v2 ledger) or `wp review override`.
|
|
165
|
+
const mintsGateAuthority = isPolicyApprovalVerdict(entry.verdict);
|
|
166
|
+
if (!mintsGateAuthority) {
|
|
167
|
+
const overviewRaw = readFileSync(found.path, "utf8");
|
|
168
|
+
const parsed = matter(overviewRaw);
|
|
169
|
+
const existingApprovals = Array.isArray(parsed.data.approvals) ? parsed.data.approvals : [];
|
|
170
|
+
const approvalEntry = {
|
|
171
|
+
reviewer: entry.reviewer,
|
|
172
|
+
verdict: entry.verdict,
|
|
173
|
+
evidence: entry.evidence ?? "reviews.md",
|
|
174
|
+
artifact: entry.artifact ?? normalizedArtifactPath,
|
|
175
|
+
...(entry.commit ? { commit: entry.commit } : {}),
|
|
176
|
+
...(entry.rev ? { rev: entry.rev } : {}),
|
|
177
|
+
};
|
|
178
|
+
const updatedData = { ...parsed.data, approvals: [...existingApprovals, approvalEntry] };
|
|
179
|
+
const rendered = matter.stringify(parsed.content, updatedData);
|
|
180
|
+
writeFileAtomic(found.path, rendered, { encoding: "utf8" });
|
|
181
|
+
}
|
|
160
182
|
return finishPayload({
|
|
161
|
-
summary:
|
|
183
|
+
summary: mintsGateAuthority
|
|
184
|
+
? `Logged review ${entry.id} -> ${entry.blueprintSlug} in reviews.md (no frontmatter approval recorded: an approval verdict must bind to reviewed content via wp review gate)`
|
|
185
|
+
: `Logged review ${entry.id} -> ${entry.blueprintSlug} and recorded approvals frontmatter`,
|
|
162
186
|
event_id: entry.id,
|
|
163
187
|
blueprint_slug: entry.blueprintSlug,
|
|
164
188
|
reviewer: entry.reviewer,
|
|
@@ -289,7 +289,7 @@ export async function registerBlueprintTools(registrar, cwd, projectResolver = c
|
|
|
289
289
|
next_action: nextActionOutputSchema,
|
|
290
290
|
},
|
|
291
291
|
}, (r) => handleTaskVerify(projectResolver, cwd, r), { title: "Blueprint Task Verify", destructiveHint: false, openWorldHint: false });
|
|
292
|
-
registrar.registerTool("wp_blueprint_review_log", 'Record a tracked review-ledger entry for a blueprint: writes the given artifact content under blueprints/<state>/<slug>/review-artifacts
|
|
292
|
+
registrar.registerTool("wp_blueprint_review_log", 'Record a tracked review-ledger entry for a blueprint: writes the given artifact content under blueprints/<state>/<slug>/review-artifacts/ and appends the matching structured entry to reviews.md (via the same logReviewEntry writer, distinct from the CLI\'s own `wp review log` subcommand). For reject/no-verdict it ALSO appends the corresponding `approvals:` frontmatter entry to _overview.md, so rejection attribution stays visible. It NEVER writes an approve/approve-with-nits entry into `approvals:`: that field feeds the legacy promotion path (`collectTrackedReviewProvenance`), which never reads or hashes the blueprint body, so an approval recorded there would keep satisfying the gate after the plan was rewritten. A `verdict` of approve or approve-with-nits additionally REQUIRES reviewer="human". Satisfying the promotion gate requires a converged `wp review gate` run (content-addressed v2 ledger) or `wp review override`. Use this tool for reject/no-verdict entries from any reviewer, or to record a human-adjudicated approve/approve-with-nits in reviews.md — including the audit-trail note when `wp review gate` fails to converge. Returns { event_id, blueprint_slug, reviewer, verdict, status, artifact_path }.', {
|
|
293
293
|
type: "object",
|
|
294
294
|
properties: {
|
|
295
295
|
project_id: { type: "string" },
|
|
@@ -1 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Redaction for the shared read-only MCP path (`wp_pr_status`, `wp_pr_wait`,
|
|
3
|
+
* `wp_run_wait` error tails, `_readonly-ops`, worker tails, hook logs — 11
|
|
4
|
+
* consumers). Output reaches agent context, session memory, and committed
|
|
5
|
+
* review artifacts in a public repository, so a credential surviving here is
|
|
6
|
+
* irreversible and externally exploitable.
|
|
7
|
+
*
|
|
8
|
+
* Three properties are load-bearing; changing any of them breaks a consumer:
|
|
9
|
+
*
|
|
10
|
+
* 1. {@link mask} is **partial disclosure by design** — first and last two
|
|
11
|
+
* characters survive above six, and the mask is deterministic.
|
|
12
|
+
* `run-wait.ts` redacts both sides of its SHA comparison and relies on
|
|
13
|
+
* identical input producing identical output.
|
|
14
|
+
* 2. The generic 40+-character rule masks full commit SHAs as a side effect,
|
|
15
|
+
* compensated downstream by that same mask-aware equality. Do not "fix" it
|
|
16
|
+
* here without updating those consumers.
|
|
17
|
+
* 3. **Every quantifier is bounded.** Callers pass unbounded log text —
|
|
18
|
+
* `run-wait.ts` redacts the whole tail *before* clipping it to 2000 bytes —
|
|
19
|
+
* so an unbounded `[A-Za-z0-9_]*` around a keyword alternation backtracks
|
|
20
|
+
* quadratically and a hostile CI log can stall the MCP path for tens of
|
|
21
|
+
* seconds. Keep the `{0,64}` bounds.
|
|
22
|
+
*/
|
|
1
23
|
export declare function redactText(value: string | undefined): string | undefined;
|
|
@@ -1,22 +1,128 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Redaction for the shared read-only MCP path (`wp_pr_status`, `wp_pr_wait`,
|
|
3
|
+
* `wp_run_wait` error tails, `_readonly-ops`, worker tails, hook logs — 11
|
|
4
|
+
* consumers). Output reaches agent context, session memory, and committed
|
|
5
|
+
* review artifacts in a public repository, so a credential surviving here is
|
|
6
|
+
* irreversible and externally exploitable.
|
|
7
|
+
*
|
|
8
|
+
* Three properties are load-bearing; changing any of them breaks a consumer:
|
|
9
|
+
*
|
|
10
|
+
* 1. {@link mask} is **partial disclosure by design** — first and last two
|
|
11
|
+
* characters survive above six, and the mask is deterministic.
|
|
12
|
+
* `run-wait.ts` redacts both sides of its SHA comparison and relies on
|
|
13
|
+
* identical input producing identical output.
|
|
14
|
+
* 2. The generic 40+-character rule masks full commit SHAs as a side effect,
|
|
15
|
+
* compensated downstream by that same mask-aware equality. Do not "fix" it
|
|
16
|
+
* here without updating those consumers.
|
|
17
|
+
* 3. **Every quantifier is bounded.** Callers pass unbounded log text —
|
|
18
|
+
* `run-wait.ts` redacts the whole tail *before* clipping it to 2000 bytes —
|
|
19
|
+
* so an unbounded `[A-Za-z0-9_]*` around a keyword alternation backtracks
|
|
20
|
+
* quadratically and a hostile CI log can stall the MCP path for tens of
|
|
21
|
+
* seconds. Keep the `{0,64}` bounds.
|
|
22
|
+
*/
|
|
23
|
+
/** Key-name fragments that advertise a secret. */
|
|
24
|
+
const SECRET_WORD = "PASSWORD|PASSWD|SECRET|TOKEN|API[_-]?KEY|ACCESS[_-]?KEY|PRIVATE[_-]?KEY|CREDENTIALS?";
|
|
25
|
+
/**
|
|
26
|
+
* Optional opening quote OUTSIDE the capture, then the value up to the first
|
|
27
|
+
* closing delimiter.
|
|
28
|
+
*
|
|
29
|
+
* Both halves matter. Excluding quotes from the class stops a greedy capture
|
|
30
|
+
* pulling the closing quote into the mask (`sk***6"` instead of `sk***56`).
|
|
31
|
+
* Consuming an optional *opening* quote is what keeps `KEY="secret"` matching
|
|
32
|
+
* at all — omitting it makes the `+` match zero characters, the pattern fail,
|
|
33
|
+
* and the most common real-world shape (quoted shell/JSON/YAML values) sail
|
|
34
|
+
* through completely unredacted.
|
|
35
|
+
*/
|
|
36
|
+
// `\x60` is the backtick: a literal one cannot appear inside a String.raw
|
|
37
|
+
// template, and `\`` is an invalid escape under the `u` flag.
|
|
38
|
+
const QUOTED_VALUE = String.raw `["'\x60]?([^\s"'\x60,;)\]}\[]+)`;
|
|
39
|
+
/** As above, with a length floor — see the `:` rule below. */
|
|
40
|
+
const QUOTED_VALUE_LONG = String.raw `["'\x60]?([^\s"'\x60,;)\]}\[]{12,})`;
|
|
41
|
+
/**
|
|
42
|
+
* Optional closing quote on the KEY, before the separator. JSON and quoted-key
|
|
43
|
+
* YAML put it there — `{"password":"…"}` — and without this the separator match
|
|
44
|
+
* fails on the key's own closing quote and the whole line sails through.
|
|
45
|
+
*/
|
|
46
|
+
const KEY_CLOSE = String.raw `["']?[ \t]*`;
|
|
47
|
+
/**
|
|
48
|
+
* Credential value for the keyword rules (`Bearer`, `Basic`, `Cookie`), which
|
|
49
|
+
* carry no assignment signal at all — and whose keywords are ordinary English
|
|
50
|
+
* words. Without a floor, `Basic checks passed` becomes
|
|
51
|
+
* `Basic [REDACTED] passed`, and that reaches users: `pr-upsert.ts` pushes PR
|
|
52
|
+
* **titles** through this function, so "Add Bearer token support" would render
|
|
53
|
+
* corrupted.
|
|
54
|
+
*
|
|
55
|
+
* So the value must be ≥12 characters AND contain at least one uppercase
|
|
56
|
+
* letter, digit or base64 punctuation — enough to separate `dXNlcjpwYXNzd29yZA==`
|
|
57
|
+
* from `authentication`.
|
|
58
|
+
*
|
|
59
|
+
* The keyword rules therefore drop the `i` flag and spell each casing out.
|
|
60
|
+
* Under `i`, `[A-Z]` also matches lowercase, so this lookahead would be
|
|
61
|
+
* silently satisfied by any long lowercase word and the check would do nothing.
|
|
62
|
+
*/
|
|
63
|
+
const KEYWORD_CRED = String.raw `["'\x60]?((?:(?=[^\s"'\x60,;)\]}\[]*[A-Z0-9+/=_-])[^\s"'\x60,;)\]}\[]{12,}|[^\s"'\x60,;)\]}\[]{24,}))`;
|
|
64
|
+
/**
|
|
65
|
+
* Patterns whose **first capture group** is the secret. Only that group is
|
|
66
|
+
* masked, so the header name, config key or URL host stays readable — a
|
|
67
|
+
* redactor that destroys the line it protects gets routed around.
|
|
68
|
+
*/
|
|
69
|
+
const CAPTURING_PATTERNS = [
|
|
70
|
+
// Originally-named credential env assignments.
|
|
71
|
+
new RegExp(String.raw `\b(?:GH_PACKAGES_TOKEN|GITHUB_TOKEN|GITHUB_PAT|NEON_API_KEY(?:_PLATFORM)?|CHEF_CI_TOKEN)\b` +
|
|
72
|
+
KEY_CLOSE +
|
|
73
|
+
String.raw `=[ \t]*` +
|
|
74
|
+
QUOTED_VALUE, "giu"),
|
|
75
|
+
// Generic secret-named key with `=`. Assignment is an unambiguous signal, so
|
|
76
|
+
// no length floor: `PASSWORD=x` should mask even when short.
|
|
77
|
+
new RegExp(String.raw `\b[A-Za-z0-9_]{0,64}(?:${SECRET_WORD})[A-Za-z0-9_]{0,64}` +
|
|
78
|
+
KEY_CLOSE +
|
|
79
|
+
String.raw `=[ \t]*` +
|
|
80
|
+
QUOTED_VALUE, "giu"),
|
|
81
|
+
// Generic secret-named key with `:`. Deliberately stricter than `=`, because
|
|
82
|
+
// `:` is overloaded across YAML, tables, markdown and plain prose — without a
|
|
83
|
+
// floor this masks `tokens: 512`, `secret: true`, `credentials: verified for
|
|
84
|
+
// 3 users` and `**Token:** an opaque handle`, corrupting ordinary CI output.
|
|
85
|
+
// A 12-character floor admits real secrets while excluding those.
|
|
86
|
+
new RegExp(String.raw `\b[A-Za-z0-9_]{0,64}(?:${SECRET_WORD})[A-Za-z0-9_]{0,64}` +
|
|
87
|
+
KEY_CLOSE +
|
|
88
|
+
String.raw `:[ \t]*` +
|
|
89
|
+
QUOTED_VALUE_LONG, "giu"),
|
|
90
|
+
// `Authorization: Bearer <token>` / bare `Bearer <token>`. Casings spelled
|
|
91
|
+
// out because `KEYWORD_CRED`'s lookahead is case-sensitive by necessity.
|
|
92
|
+
new RegExp(String.raw `\b(?:Bearer|bearer|BEARER)\s+` + KEYWORD_CRED, "gu"),
|
|
93
|
+
// `Authorization: Basic <base64>` — same header family, and the base64 is
|
|
94
|
+
// usually under the generic 40-character floor.
|
|
95
|
+
new RegExp(String.raw `\b(?:Basic|basic|BASIC)\s+` + KEYWORD_CRED, "gu"),
|
|
96
|
+
// Session cookies: the cookie NAME (`session`, `sid`) never matches a
|
|
97
|
+
// secret-word rule, so the value needs its own case or session tokens ride
|
|
98
|
+
// through CI and proxy logs intact. Anchoring on `name=` both avoids matching
|
|
99
|
+
// prose after a bare `Cookie:` and keeps the cookie name readable in output.
|
|
100
|
+
new RegExp(String.raw `\b(?:Set-|set-|SET-)?(?:Cookie|cookie|COOKIE)\s*:\s*[^\s=;]{1,64}=` + KEYWORD_CRED, "gu"),
|
|
101
|
+
// Credential-bearing URL userinfo: `https://user:pass@host/…`. Masking the
|
|
102
|
+
// whole userinfo keeps the host, which is what makes the log useful.
|
|
103
|
+
/:\/\/([^\s/@]+)@/gu,
|
|
3
104
|
];
|
|
4
|
-
|
|
105
|
+
/** Patterns where the entire match is the secret. */
|
|
106
|
+
const WHOLE_MATCH_PATTERNS = [
|
|
5
107
|
/\bghp_[A-Za-z0-9]{20,}\b/gu,
|
|
6
108
|
/\b(?:cf|sk|tok)_[A-Za-z0-9_-]{16,}\b/gu,
|
|
109
|
+
// JWTs, whose segments routinely fall under the generic 40-character floor.
|
|
110
|
+
/\beyJ[A-Za-z0-9_-]{4,}\.[A-Za-z0-9_-]{4,}\.[A-Za-z0-9_-]{4,}\b/gu,
|
|
7
111
|
/\b[A-Za-z0-9+/_-]{40,}\b/gu,
|
|
8
112
|
];
|
|
9
113
|
export function redactText(value) {
|
|
10
114
|
if (!value)
|
|
11
115
|
return value;
|
|
12
116
|
let output = value;
|
|
13
|
-
for (const pattern of
|
|
14
|
-
output = output.replace(pattern, (whole,
|
|
15
|
-
const trimmed = String(
|
|
16
|
-
|
|
117
|
+
for (const pattern of CAPTURING_PATTERNS) {
|
|
118
|
+
output = output.replace(pattern, (whole, secret) => {
|
|
119
|
+
const trimmed = String(secret ?? "").trim();
|
|
120
|
+
if (trimmed.length === 0)
|
|
121
|
+
return whole;
|
|
122
|
+
return whole.replace(trimmed, mask(trimmed));
|
|
17
123
|
});
|
|
18
124
|
}
|
|
19
|
-
for (const pattern of
|
|
125
|
+
for (const pattern of WHOLE_MATCH_PATTERNS) {
|
|
20
126
|
output = output.replace(pattern, (token) => mask(token));
|
|
21
127
|
}
|
|
22
128
|
return output;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
+
import { resolveManagedRtkExecutable } from "#tool-runtime/managed-rtk.js";
|
|
2
3
|
import { createSummaryOutputSchema, createSummaryResult } from "./_shared/result.js";
|
|
3
4
|
import { readonlyOpsBaseSchema, resolveReadonlyCwd, runReadonlyCommand } from "./_readonly-ops.js";
|
|
4
5
|
const inputSchema = readonlyOpsBaseSchema
|
|
@@ -19,8 +20,12 @@ const outputSchema = createSummaryOutputSchema({
|
|
|
19
20
|
}),
|
|
20
21
|
});
|
|
21
22
|
function commandFor(input) {
|
|
22
|
-
if (input.source === "rtk")
|
|
23
|
-
|
|
23
|
+
if (input.source === "rtk") {
|
|
24
|
+
const rtkBin = resolveManagedRtkExecutable();
|
|
25
|
+
if (!rtkBin)
|
|
26
|
+
return null;
|
|
27
|
+
return { command: rtkBin, args: ["gain", "--format", "json"] };
|
|
28
|
+
}
|
|
24
29
|
return { command: "./bin/wp", args: input.sessions ? ["gain", "--sessions"] : ["gain"] };
|
|
25
30
|
}
|
|
26
31
|
const tool = {
|
|
@@ -39,6 +44,27 @@ const tool = {
|
|
|
39
44
|
const input = inputSchema.parse(raw ?? {});
|
|
40
45
|
const cwd = resolveReadonlyCwd(input);
|
|
41
46
|
const command = commandFor(input);
|
|
47
|
+
if (command === null) {
|
|
48
|
+
return createSummaryResult({
|
|
49
|
+
passed: false,
|
|
50
|
+
summary: "managed RTK not available (wp setup --with rtk or WP_RTK_BIN)",
|
|
51
|
+
exitCode: 1,
|
|
52
|
+
counts: { commandCount: 0, passedCount: 0, failedCount: 1 },
|
|
53
|
+
details: {
|
|
54
|
+
cwd,
|
|
55
|
+
source: input.source,
|
|
56
|
+
format: input.format,
|
|
57
|
+
sessions: input.sessions,
|
|
58
|
+
command: {},
|
|
59
|
+
result: { error: "managed_rtk_missing" },
|
|
60
|
+
},
|
|
61
|
+
rawOutput: "",
|
|
62
|
+
truncated: false,
|
|
63
|
+
timedOut: false,
|
|
64
|
+
aborted: false,
|
|
65
|
+
warnings: ["managed RTK binary missing"],
|
|
66
|
+
});
|
|
67
|
+
}
|
|
42
68
|
const result = await runReadonlyCommand("gain", command.command, command.args, {
|
|
43
69
|
cwd,
|
|
44
70
|
timeoutMs: input.timeoutMs,
|
|
@@ -15,6 +15,7 @@ import { gitBranchExists, listEntries, resolveNewWorktreeTarget, resolveWorktree
|
|
|
15
15
|
import { deriveRepoNamespace, resolveKnownManagedWorktreeRoots, resolveWorktreeRoot, } from "#worktrees/location.js";
|
|
16
16
|
import { readRepoOriginUrl, repoManagedRoot } from "#worktrees/manager.js";
|
|
17
17
|
import { buildWorktreeInventory, canonicalizeWorktreePath, isManagedWorktreePath, resolveRepoIdentity, } from "#worktrees/identity.js";
|
|
18
|
+
import { isStaleWorktreeRegistration, pruneStaleGitWorktreeMetadata, } from "#worktrees/git-metadata.js";
|
|
18
19
|
import { findRegistryCandidatesByPath, pruneStaleWorktreeRegistryEntries, removeWorktreeRegistryEntries, repoScopedPathPredicate, upsertWorktreeRegistryEntry, } from "#worktrees/registry.js";
|
|
19
20
|
const ACTIONS = ["list", "root", "new", "remove", "prune", "refresh"];
|
|
20
21
|
const MUTATING_ACTIONS = new Set(["new", "remove", "prune", "refresh"]);
|
|
@@ -105,6 +106,53 @@ function isRegisteredManagedWorktree(resolvedPath, _context) {
|
|
|
105
106
|
// a legacy-root worktree registered by an earlier binary authorizes removal.
|
|
106
107
|
return findRegistryCandidatesByPath(canonicalizeWorktreePath(resolvedPath)).length > 0;
|
|
107
108
|
}
|
|
109
|
+
/**
|
|
110
|
+
* A stale registration may already have lost its registry entry to an earlier
|
|
111
|
+
* prune, so registration alone cannot gate reconciling it. Fall back to the
|
|
112
|
+
* managed-root boundary, which still keeps unmanaged worktrees protected.
|
|
113
|
+
*/
|
|
114
|
+
function isRemovableManagedTarget(resolved, context, stale) {
|
|
115
|
+
if (isRegisteredManagedWorktree(resolved, context))
|
|
116
|
+
return true;
|
|
117
|
+
if (!stale)
|
|
118
|
+
return false;
|
|
119
|
+
return isManagedWorktreePath(canonicalizeWorktreePath(resolved), resolveKnownManagedWorktreeRoots());
|
|
120
|
+
}
|
|
121
|
+
/** Reconcile git + registry for a worktree whose directory is already gone. */
|
|
122
|
+
function clearStaleRegistration(input, repoRoot, resolved, entries, entry) {
|
|
123
|
+
const gitPruned = pruneStaleGitWorktreeMetadata(repoRoot, entries);
|
|
124
|
+
if (gitPruned.error) {
|
|
125
|
+
return {
|
|
126
|
+
passed: false,
|
|
127
|
+
summary: `wp_worktree remove failed: git worktree prune failed: ${gitPruned.error}`,
|
|
128
|
+
action: input.action,
|
|
129
|
+
executed: false,
|
|
130
|
+
warnings: ["git_worktree_prune_failed"],
|
|
131
|
+
nextAction: "Inspect the repository's git worktree admin records and retry.",
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
const warnings = [];
|
|
135
|
+
try {
|
|
136
|
+
removeWorktreeRegistryEntries(repoScopedPathPredicate(resolved, { authoritativeRepoKey: resolveRepoIdentity(repoRoot) }));
|
|
137
|
+
}
|
|
138
|
+
catch {
|
|
139
|
+
warnings.push("registry_cleanup_failed");
|
|
140
|
+
}
|
|
141
|
+
return {
|
|
142
|
+
passed: true,
|
|
143
|
+
summary: `Cleared stale worktree registration ${resolved} (directory already gone)`,
|
|
144
|
+
action: input.action,
|
|
145
|
+
executed: true,
|
|
146
|
+
removed: {
|
|
147
|
+
path: resolved,
|
|
148
|
+
...(entry?.branch ? { branch: entry.branch.replace("refs/heads/", "") } : {}),
|
|
149
|
+
},
|
|
150
|
+
warnings,
|
|
151
|
+
...(warnings.includes("registry_cleanup_failed")
|
|
152
|
+
? { nextAction: "Run wp_worktree prune with execute:true to repair stale registry metadata." }
|
|
153
|
+
: {}),
|
|
154
|
+
};
|
|
155
|
+
}
|
|
108
156
|
function refreshManagedEntries(repoRoot) {
|
|
109
157
|
const { originUrl, repoNamespace } = managedContext(repoRoot);
|
|
110
158
|
const knownRoots = resolveKnownManagedWorktreeRoots();
|
|
@@ -234,6 +282,7 @@ function handleRemove(input, repoRoot) {
|
|
|
234
282
|
}
|
|
235
283
|
const entry = entries.find((candidate) => candidate.path === resolved);
|
|
236
284
|
const context = managedContext(repoRoot);
|
|
285
|
+
const stale = isStaleWorktreeRegistration(entry, resolved, existsSync);
|
|
237
286
|
if (resolved === repoRoot) {
|
|
238
287
|
return {
|
|
239
288
|
passed: false,
|
|
@@ -243,7 +292,7 @@ function handleRemove(input, repoRoot) {
|
|
|
243
292
|
warnings: ["main_checkout_protected"],
|
|
244
293
|
};
|
|
245
294
|
}
|
|
246
|
-
if (!
|
|
295
|
+
if (!isRemovableManagedTarget(resolved, context, stale)) {
|
|
247
296
|
return {
|
|
248
297
|
passed: false,
|
|
249
298
|
summary: `wp_worktree remove refused: ${resolved} is not a registered managed worktree for this repository`,
|
|
@@ -263,6 +312,10 @@ function handleRemove(input, repoRoot) {
|
|
|
263
312
|
nextAction: "Unlock the worktree explicitly with git after confirming it is safe, then retry.",
|
|
264
313
|
};
|
|
265
314
|
}
|
|
315
|
+
// Checked before the dirty gate: `git status` fails in a missing directory,
|
|
316
|
+
// which would otherwise be reported as "uncommitted changes".
|
|
317
|
+
if (stale)
|
|
318
|
+
return clearStaleRegistration(input, repoRoot, resolved, entries, entry);
|
|
266
319
|
if (isDirty(resolved)) {
|
|
267
320
|
return {
|
|
268
321
|
passed: false,
|
|
@@ -368,6 +421,10 @@ const tool = {
|
|
|
368
421
|
warnings: [],
|
|
369
422
|
}, false);
|
|
370
423
|
}
|
|
424
|
+
// Reconcile git's own admin records FIRST: a hand-deleted worktree
|
|
425
|
+
// directory leaves a record that still claims its branch, and it would
|
|
426
|
+
// otherwise read as live below and pin its registry entry in place too.
|
|
427
|
+
const gitPruned = pruneStaleGitWorktreeMetadata(repoRoot, listEntries(repoRoot));
|
|
371
428
|
// Repository-scoped, three-state prune across every known root: only THIS
|
|
372
429
|
// repo's entries proven stale against a COMPLETE git-live inventory drop;
|
|
373
430
|
// unknown, other-repo, and live-legacy entries are retained. `existsSync`
|
|
@@ -377,12 +434,17 @@ const tool = {
|
|
|
377
434
|
inventory,
|
|
378
435
|
scope: { authoritativeRepoKey: resolveRepoIdentity(repoRoot) },
|
|
379
436
|
});
|
|
437
|
+
const gitSummary = gitPruned.error
|
|
438
|
+
? ""
|
|
439
|
+
: ` and ${gitPruned.prunedPaths.length} stale git worktree registration${gitPruned.prunedPaths.length === 1 ? "" : "s"}`;
|
|
380
440
|
return result({
|
|
381
441
|
passed: true,
|
|
382
|
-
summary: `Pruned ${pruned.removed.length} stale managed registry entr${pruned.removed.length === 1 ? "y" : "ies"}`,
|
|
442
|
+
summary: `Pruned ${pruned.removed.length} stale managed registry entr${pruned.removed.length === 1 ? "y" : "ies"}${gitSummary}`,
|
|
383
443
|
action: input.action,
|
|
384
444
|
executed: true,
|
|
385
|
-
warnings:
|
|
445
|
+
warnings: gitPruned.error
|
|
446
|
+
? [...pruned.warnings, `git_worktree_prune_failed:${gitPruned.error}`]
|
|
447
|
+
: pruned.warnings,
|
|
386
448
|
}, false);
|
|
387
449
|
}
|
|
388
450
|
catch (error) {
|
|
@@ -29,20 +29,52 @@ const tool = {
|
|
|
29
29
|
openWorldHint: false,
|
|
30
30
|
},
|
|
31
31
|
handler: async (raw) => {
|
|
32
|
-
const
|
|
32
|
+
const parsed = inputSchema.safeParse(raw ?? {});
|
|
33
|
+
if (!parsed.success) {
|
|
34
|
+
const summary = `Invalid wp_ui_compose input: ${parsed.error.issues.map((i) => i.message).join("; ")}`;
|
|
35
|
+
return {
|
|
36
|
+
content: [{ type: "text", text: summary }],
|
|
37
|
+
structuredContent: {
|
|
38
|
+
passed: false,
|
|
39
|
+
summary,
|
|
40
|
+
ok: false,
|
|
41
|
+
diagnostics: parsed.error.issues.map((i) => ({
|
|
42
|
+
path: i.path.length > 0 ? i.path.join(".") : "(root)",
|
|
43
|
+
message: i.message,
|
|
44
|
+
})),
|
|
45
|
+
},
|
|
46
|
+
isError: true,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
const input = parsed.data;
|
|
50
|
+
if (input.profile === "admin-crud") {
|
|
51
|
+
if (input.spec === null || typeof input.spec !== "object" || Array.isArray(input.spec)) {
|
|
52
|
+
const summary = "Invalid wp_ui_compose admin-crud spec: expected a non-null object (not string/array/empty coercion)";
|
|
53
|
+
return {
|
|
54
|
+
content: [{ type: "text", text: summary }],
|
|
55
|
+
structuredContent: {
|
|
56
|
+
passed: false,
|
|
57
|
+
summary,
|
|
58
|
+
ok: false,
|
|
59
|
+
diagnostics: [{ path: "spec", message: summary }],
|
|
60
|
+
},
|
|
61
|
+
isError: true,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
}
|
|
33
65
|
const composeInput = input.profile === "admin-crud"
|
|
34
66
|
? {
|
|
35
67
|
profile: "admin-crud",
|
|
36
68
|
kind: input.kind,
|
|
37
|
-
spec: input.spec
|
|
38
|
-
? input.spec
|
|
39
|
-
: {},
|
|
69
|
+
spec: input.spec,
|
|
40
70
|
}
|
|
41
71
|
: { profile: "ops-report", spec: input.spec };
|
|
42
72
|
const result = composeValidate(composeInput, { cwd: input.cwd });
|
|
43
73
|
const summary = result.ok
|
|
44
74
|
? `compose ok (${result.profile})`
|
|
45
75
|
: `compose failed (${result.profile}): ${result.diagnostics.map((d) => d.message).join("; ")}`;
|
|
76
|
+
// MCP tool execution errors (incl. domain validation) use isError:true so
|
|
77
|
+
// models self-correct; diagnostics stay in content text + structuredContent.
|
|
46
78
|
return {
|
|
47
79
|
content: [{ type: "text", text: summary }],
|
|
48
80
|
structuredContent: {
|
|
@@ -55,7 +87,9 @@ const tool = {
|
|
|
55
87
|
registrySource: result.registrySource,
|
|
56
88
|
registryVersion: result.registryVersion,
|
|
57
89
|
warnings: result.warnings,
|
|
90
|
+
blockTypesValidated: result.blockTypesValidated,
|
|
58
91
|
},
|
|
92
|
+
...(result.ok ? {} : { isError: true }),
|
|
59
93
|
};
|
|
60
94
|
},
|
|
61
95
|
};
|
|
@@ -19,9 +19,20 @@ const tool = {
|
|
|
19
19
|
openWorldHint: false,
|
|
20
20
|
},
|
|
21
21
|
handler: async (raw) => {
|
|
22
|
+
const parsed = inputSchema.safeParse(raw ?? {});
|
|
23
|
+
if (!parsed.success) {
|
|
24
|
+
const message = `Invalid wp_ui_preview input: ${parsed.error.issues.map((i) => i.message).join("; ")} (ops-report profile only; admin-crud is not supported)`;
|
|
25
|
+
return {
|
|
26
|
+
content: [{ type: "text", text: message }],
|
|
27
|
+
structuredContent: {
|
|
28
|
+
passed: false,
|
|
29
|
+
summary: message,
|
|
30
|
+
},
|
|
31
|
+
isError: true,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
22
34
|
try {
|
|
23
|
-
const
|
|
24
|
-
const { path } = previewOpsReport({ cwd: input.cwd, spec: input.spec });
|
|
35
|
+
const { path } = previewOpsReport({ cwd: parsed.data.cwd, spec: parsed.data.spec });
|
|
25
36
|
const summary = `ops-report preview written: ${path}`;
|
|
26
37
|
return {
|
|
27
38
|
content: [{ type: "text", text: summary }],
|
|
@@ -33,11 +44,7 @@ const tool = {
|
|
|
33
44
|
};
|
|
34
45
|
}
|
|
35
46
|
catch (error) {
|
|
36
|
-
const message = error instanceof
|
|
37
|
-
? `Invalid wp_ui_preview input: ${error.issues.map((i) => i.message).join("; ")} (ops-report profile only; admin-crud is not supported)`
|
|
38
|
-
: error instanceof Error
|
|
39
|
-
? error.message
|
|
40
|
-
: String(error);
|
|
47
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
41
48
|
return {
|
|
42
49
|
content: [{ type: "text", text: message }],
|
|
43
50
|
structuredContent: {
|
|
@@ -9,6 +9,7 @@ import { createHash } from "node:crypto";
|
|
|
9
9
|
import { existsSync, lstatSync, readFileSync } from "node:fs";
|
|
10
10
|
import path from "node:path";
|
|
11
11
|
import { evaluateReviewEvents, parseReviewEventLog } from "./events.js";
|
|
12
|
+
import { isPolicyApprovalVerdict } from "./verdict.js";
|
|
12
13
|
import { createDeliverySubjectAtRef, createLegacyPlanSubjectAtRef, createPlanSubjectAtRef, createPreTagsStripPlanSubjectAtRef, resolveReviewCommit, } from "./subject.js";
|
|
13
14
|
const GIT_TIMEOUT_MS = 5_000;
|
|
14
15
|
const GIT_MAX_BUFFER_BYTES = 128 * 1024 * 1024;
|
|
@@ -114,6 +115,54 @@ function markCompromisedArtifactEvents(cwd, entries, blueprintDirectory, events)
|
|
|
114
115
|
}
|
|
115
116
|
return { compromisedIds, issues };
|
|
116
117
|
}
|
|
118
|
+
/** Bounded `git branch -a --contains <commit>` probe, tolerant of a pruned/unknown object. */
|
|
119
|
+
function branchesContainingCommit(cwd, commit) {
|
|
120
|
+
try {
|
|
121
|
+
const output = gitBuffer(cwd, ["branch", "-a", "--contains", commit]).toString("utf8");
|
|
122
|
+
return output
|
|
123
|
+
.split("\n")
|
|
124
|
+
.map((line) => line.trim())
|
|
125
|
+
.filter((line) => line.length > 0);
|
|
126
|
+
}
|
|
127
|
+
catch {
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
// Diagnostic-only: names historical ledger approvals whose `reviewedCommit` is
|
|
132
|
+
// no longer reachable from any branch (typically because a rebase rewrote the
|
|
133
|
+
// history that commit lived on). Such an approval no longer matches the
|
|
134
|
+
// current subjectDigest, so it already contributes zero approvals under the
|
|
135
|
+
// existing policy in ./events.ts -- this only makes the reason visible instead
|
|
136
|
+
// of the operator seeing a bare "approvals does not satisfy the promotion
|
|
137
|
+
// reviewer mix" and mistaking a dead commit ref for a genuinely missing
|
|
138
|
+
// review. Never mutates `approvals`/`matching`; policy is unchanged.
|
|
139
|
+
function markOrphanedApprovalEvents(cwd, events, current) {
|
|
140
|
+
const issues = [];
|
|
141
|
+
const seen = new Set();
|
|
142
|
+
for (const event of events) {
|
|
143
|
+
if (event.purpose !== current.purpose ||
|
|
144
|
+
event.blueprintSlug !== current.blueprintSlug ||
|
|
145
|
+
event.subjectScheme !== current.subjectScheme ||
|
|
146
|
+
event.subjectDigest === current.subjectDigest ||
|
|
147
|
+
event.status !== "complete" ||
|
|
148
|
+
!isPolicyApprovalVerdict(event.verdict)) {
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
const key = `${event.reviewer}:${event.reviewedCommit}`;
|
|
152
|
+
if (seen.has(key))
|
|
153
|
+
continue;
|
|
154
|
+
seen.add(key);
|
|
155
|
+
const branches = branchesContainingCommit(cwd, event.reviewedCommit);
|
|
156
|
+
if (branches === null || branches.length === 0) {
|
|
157
|
+
issues.push(`Historical approval from ${event.reviewer} (event ${event.id}) reviewed commit ` +
|
|
158
|
+
`${event.reviewedCommit}, which is unreachable from any branch (git branch -a ` +
|
|
159
|
+
`--contains ${event.reviewedCommit} found nothing) -- likely orphaned by a rebase. ` +
|
|
160
|
+
"This approval does not count toward the current subject; rebase this worktree onto " +
|
|
161
|
+
"the latest base and re-run the gate.");
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return issues;
|
|
165
|
+
}
|
|
117
166
|
export function readReviewAuthorityAtRef(cwd, ref, stableSlug, purpose) {
|
|
118
167
|
const commit = resolveReviewCommit(cwd, ref);
|
|
119
168
|
const entries = parseRefTree(cwd, commit);
|
|
@@ -131,6 +180,12 @@ export function readReviewAuthorityAtRef(cwd, ref, stableSlug, purpose) {
|
|
|
131
180
|
const subject = purpose === "plan"
|
|
132
181
|
? createPlanSubjectAtRef(cwd, commit, stableSlug, overview.path)
|
|
133
182
|
: createDeliverySubjectAtRef(cwd, commit, stableSlug, overview.path);
|
|
183
|
+
const orphanIssues = markOrphanedApprovalEvents(cwd, events, {
|
|
184
|
+
purpose,
|
|
185
|
+
blueprintSlug: stableSlug,
|
|
186
|
+
subjectScheme: subject.scheme,
|
|
187
|
+
subjectDigest: subject.digest,
|
|
188
|
+
});
|
|
134
189
|
let evaluation = evaluateReviewEvents(events, {
|
|
135
190
|
purpose,
|
|
136
191
|
blueprintSlug: stableSlug,
|
|
@@ -166,7 +221,7 @@ export function readReviewAuthorityAtRef(cwd, ref, stableSlug, purpose) {
|
|
|
166
221
|
subject,
|
|
167
222
|
events,
|
|
168
223
|
...evaluation,
|
|
169
|
-
issues,
|
|
224
|
+
issues: [...issues, ...orphanIssues],
|
|
170
225
|
};
|
|
171
226
|
}
|
|
172
227
|
export function readWorkingReviewEvents(blueprintPath, stableSlug) {
|
|
@@ -21,6 +21,10 @@ export interface ReviewGateVerification {
|
|
|
21
21
|
readonly logPath?: string;
|
|
22
22
|
readonly sandbox?: ReviewSandboxMode;
|
|
23
23
|
readonly sandboxReason?: string;
|
|
24
|
+
readonly violations?: readonly {
|
|
25
|
+
readonly section: string;
|
|
26
|
+
readonly message: string;
|
|
27
|
+
}[];
|
|
24
28
|
}
|
|
25
29
|
/** Runs one gate command in the review checkout and returns its raw result. */
|
|
26
30
|
export type RunSandboxedGate = (args: {
|
|
@@ -18,7 +18,15 @@ export async function verifyDeliveryPromotionGates(input) {
|
|
|
18
18
|
parsed.violations[0]?.message === "missing Trust Dossier section") {
|
|
19
19
|
return undefined;
|
|
20
20
|
}
|
|
21
|
-
return {
|
|
21
|
+
return {
|
|
22
|
+
status: "failed",
|
|
23
|
+
commands: [],
|
|
24
|
+
failureCode: "malformed-trust-dossier",
|
|
25
|
+
violations: parsed.violations.map((violation) => ({
|
|
26
|
+
section: violation.section,
|
|
27
|
+
message: violation.message,
|
|
28
|
+
})),
|
|
29
|
+
};
|
|
22
30
|
}
|
|
23
31
|
const gates = parsed.dossier?.gates ?? [];
|
|
24
32
|
if (gates.length === 0)
|