@webpresso/agent-kit 3.3.3 → 3.3.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/THIRD-PARTY-NOTICES.md +12 -10
- package/catalog/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/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/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 +8 -0
- package/dist/esm/build/vendor-oxlint-plugins.js +53 -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 +29 -48
- 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.js +60 -6
- package/dist/esm/cli/commands/worktree/router-dispatch.d.ts +24 -2
- package/dist/esm/cli/commands/worktree/router-dispatch.js +80 -48
- package/dist/esm/cli/commands/worktree/router.js +5 -2
- package/dist/esm/cli/direct-provider-launch.js +12 -4
- package/dist/esm/compiler/flatten.d.ts +10 -1
- package/dist/esm/compiler/flatten.js +14 -4
- package/dist/esm/compose/admin-blocks-registry-integrity.d.ts +22 -0
- package/dist/esm/compose/admin-blocks-registry-integrity.js +90 -0
- package/dist/esm/compose/compose.d.ts +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 +18 -21
- package/dist/esm/config/oxlint/path-roles.js +53 -0
- package/dist/esm/config/oxlint/query-patterns.js +2 -5
- package/dist/esm/daemon/domains/blueprint-exec-ledger.d.ts +86 -0
- package/dist/esm/daemon/domains/blueprint-exec-ledger.js +134 -0
- package/dist/esm/daemon/domains/blueprint-exec.d.ts +106 -0
- package/dist/esm/daemon/domains/blueprint-exec.js +161 -0
- package/dist/esm/daemon/domains/launch.d.ts +25 -0
- package/dist/esm/daemon/domains/launch.js +30 -1
- package/dist/esm/daemon/handlers/blueprints-handlers.d.ts +12 -1
- package/dist/esm/daemon/handlers/blueprints-handlers.js +148 -6
- package/dist/esm/daemon/handlers/product.d.ts +10 -0
- package/dist/esm/daemon/handlers/product.js +10 -1
- package/dist/esm/daemon/methods.d.ts +21 -1
- package/dist/esm/daemon/methods.js +20 -0
- package/dist/esm/daemon/protocol/contract.d.ts +58 -0
- package/dist/esm/daemon/protocol/contract.js +38 -0
- package/dist/esm/daemon/server.js +7 -0
- package/dist/esm/docs-linter/blueprint-plan.js +2 -2
- package/dist/esm/errors/wp-error.js +21 -2
- package/dist/esm/hooks/doctor.d.ts +11 -0
- package/dist/esm/hooks/doctor.js +114 -45
- package/dist/esm/hooks/pretool-guard/dev-routing.js +1 -1
- package/dist/esm/hooks/pretool-guard/rtk-rewrite.d.ts +24 -0
- package/dist/esm/hooks/pretool-guard/rtk-rewrite.js +50 -0
- package/dist/esm/hooks/pretool-guard/runner.js +6 -0
- package/dist/esm/hooks/status/index.js +1 -1
- package/dist/esm/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/lifecycle-writers.d.ts +76 -0
- package/dist/esm/review/lifecycle-writers.js +330 -0
- package/dist/esm/review/subject.js +66 -32
- package/dist/esm/status/snapshot.d.ts +5 -0
- package/dist/esm/status/snapshot.js +27 -2
- package/dist/esm/test/shard-durations.json +0 -1
- package/dist/esm/test-helpers/global-setup.js +38 -3
- package/dist/esm/tool-runtime/managed-rtk.d.ts +44 -0
- package/dist/esm/tool-runtime/managed-rtk.js +275 -0
- package/dist/esm/tool-runtime/resolve-runner.js +18 -194
- package/dist/esm/utils/package-root.d.ts +32 -0
- package/dist/esm/utils/package-root.js +82 -0
- package/dist/esm/worktrees/git-metadata.d.ts +41 -0
- package/dist/esm/worktrees/git-metadata.js +48 -0
- package/package.json +16 -21
- package/dist/esm/config/oxlint/code-safety.d.ts +0 -18
- package/dist/esm/config/oxlint/foundation-purity.d.ts +0 -21
- package/dist/esm/config/oxlint/graphql-conventions.d.ts +0 -23
- package/dist/esm/config/oxlint/import-hygiene.d.ts +0 -34
- package/dist/esm/config/oxlint/index.d.ts +0 -22
- package/dist/esm/config/oxlint/index.js +0 -28
- package/dist/esm/config/oxlint/monorepo-paths.d.ts +0 -22
- package/dist/esm/config/oxlint/query-patterns.d.ts +0 -24
- package/dist/esm/config/oxlint/testing-quality.d.ts +0 -35
- package/dist/esm/config/oxlint/tier-boundaries.d.ts +0 -35
- package/dist/esm/output-transforms/rulesync.d.ts +0 -2
- package/dist/esm/output-transforms/rulesync.js +0 -79
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RTK rewrite inside path-stable pretool-guard (no freestanding consumer hook).
|
|
3
|
+
*
|
|
4
|
+
* v1: exact `git status` → `<managed-rtk> git status` when managed rtk is available.
|
|
5
|
+
*/
|
|
6
|
+
import { isManagedRtkAvailable, resolveManagedRtkExecutable } from "#tool-runtime/managed-rtk.js";
|
|
7
|
+
const UNCERTAIN_SHELL_RE = /[\n\r|;&<>`$(){}[\]\\'"#*?]/u;
|
|
8
|
+
/**
|
|
9
|
+
* Decide whether to rewrite a Bash command through managed rtk.
|
|
10
|
+
*/
|
|
11
|
+
export function decideRtkRewrite(command, isRtkPresent = isManagedRtkAvailable, resolveExecutable = resolveManagedRtkExecutable) {
|
|
12
|
+
if (command !== command.trim() || command.length === 0) {
|
|
13
|
+
return { action: "noop", reason: "uncertain-parse" };
|
|
14
|
+
}
|
|
15
|
+
if (UNCERTAIN_SHELL_RE.test(command)) {
|
|
16
|
+
return { action: "noop", reason: "uncertain-parse" };
|
|
17
|
+
}
|
|
18
|
+
// already managed absolute path or bare rtk prefix
|
|
19
|
+
if (command.startsWith("rtk ") || command.includes("/rtk ")) {
|
|
20
|
+
return { action: "noop", reason: "already-rtk" };
|
|
21
|
+
}
|
|
22
|
+
if (command !== "git status") {
|
|
23
|
+
return { action: "noop", reason: "not-eligible" };
|
|
24
|
+
}
|
|
25
|
+
if (!isRtkPresent()) {
|
|
26
|
+
return { action: "noop", reason: "rtk-missing" };
|
|
27
|
+
}
|
|
28
|
+
const executable = resolveExecutable();
|
|
29
|
+
if (!executable) {
|
|
30
|
+
return { action: "noop", reason: "rtk-missing" };
|
|
31
|
+
}
|
|
32
|
+
const bin = shellQuoteExecutable(executable);
|
|
33
|
+
if (!bin) {
|
|
34
|
+
return { action: "noop", reason: "rtk-missing" };
|
|
35
|
+
}
|
|
36
|
+
return { action: "rewrite", command: `${bin} git status` };
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* POSIX single-quote wrap for absolute managed paths.
|
|
40
|
+
* Refuse NUL/newline/single-quote — those cannot be safely embedded in `'…'`.
|
|
41
|
+
* Double-quote wrapping is intentionally avoided ($ / ` / " injection).
|
|
42
|
+
*/
|
|
43
|
+
export function shellQuoteExecutable(path) {
|
|
44
|
+
if (path.length === 0 || /[\0\n\r']/.test(path))
|
|
45
|
+
return null;
|
|
46
|
+
// Unquoted only for the common managed-cache shape (no shell metacharacters).
|
|
47
|
+
if (/^[A-Za-z0-9_./:@+=,-]+$/.test(path))
|
|
48
|
+
return path;
|
|
49
|
+
return `'${path}'`;
|
|
50
|
+
}
|
|
@@ -11,6 +11,7 @@ import { VALIDATORS } from "./validators/index.js";
|
|
|
11
11
|
import { isDirectEntrypoint } from "#hooks/shared/direct-entrypoint";
|
|
12
12
|
import { optimizeToolInput, } from "./optimization.js";
|
|
13
13
|
import { recordOptimizationEvent } from "./optimization-events.js";
|
|
14
|
+
import { decideRtkRewrite } from "./rtk-rewrite.js";
|
|
14
15
|
const RED = "\x1b[31m";
|
|
15
16
|
const YELLOW = "\x1b[33m";
|
|
16
17
|
const DIM = "\x1b[2m";
|
|
@@ -256,6 +257,11 @@ export function processValidation(inputJson, options = {}) {
|
|
|
256
257
|
process.exit(2);
|
|
257
258
|
}
|
|
258
259
|
if (command) {
|
|
260
|
+
const rtkRewrite = decideRtkRewrite(command);
|
|
261
|
+
if (rtkRewrite.action === "rewrite") {
|
|
262
|
+
writeUpdatedInput(input, rtkRewrite.command);
|
|
263
|
+
process.exit(0);
|
|
264
|
+
}
|
|
259
265
|
const originalAction = routeCommand(command)?.action ?? { action: "passthrough" };
|
|
260
266
|
const optimization = optimizeToolInput(input, originalAction, options.optimizationRules);
|
|
261
267
|
if (optimization.action === "rewrite") {
|
|
@@ -109,7 +109,7 @@ function hasValidCodexPluginArtifacts(repoRoot) {
|
|
|
109
109
|
const hookMap = hooks?.hooks;
|
|
110
110
|
return (plugin?.mcpServers === "./.mcp.json" &&
|
|
111
111
|
plugin.hooks === "./hooks/hooks.json" &&
|
|
112
|
-
serverRecord?.command === "
|
|
112
|
+
serverRecord?.command === "wp" &&
|
|
113
113
|
stringArrayEquals(serverRecord.args, ["mcp"]) &&
|
|
114
114
|
hookMap !== null &&
|
|
115
115
|
typeof hookMap === "object" &&
|
|
@@ -94,21 +94,49 @@ export async function applyLocalBlueprintTransition(input) {
|
|
|
94
94
|
mkdirSync(path.dirname(found.shape === "flat" ? destination.flat : destination.directory), {
|
|
95
95
|
recursive: true,
|
|
96
96
|
});
|
|
97
|
+
const sourcePath = found.shape === "flat" ? overviewPath : found.dir;
|
|
98
|
+
const destinationPath = found.shape === "flat" ? destination.flat : destination.directory;
|
|
97
99
|
let finalOverviewPath = overviewPath;
|
|
100
|
+
let renamed = false;
|
|
98
101
|
if (found.state !== to_state) {
|
|
99
102
|
const { renameSync } = await import("node:fs");
|
|
100
|
-
renameSync(
|
|
103
|
+
renameSync(sourcePath, destinationPath);
|
|
104
|
+
renamed = true;
|
|
101
105
|
finalOverviewPath = found.shape === "flat" ? destination.flat : destination.folder;
|
|
102
106
|
}
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
107
|
+
// Atomicity: everything from here on can still fail (re-ingest, projection
|
|
108
|
+
// lookup). The rename above may already have happened, and the frontmatter
|
|
109
|
+
// write below always happens before those checks run. Roll BOTH back
|
|
110
|
+
// before rethrowing so a failed transition never leaves the blueprint
|
|
111
|
+
// stranded in the destination lifecycle directory, nor leaves stale
|
|
112
|
+
// frontmatter (bumped last_updated/progress/status) on an otherwise
|
|
113
|
+
// unmoved file — either the transition fully lands (moved + content
|
|
114
|
+
// updated + re-ingested + visible in the projection) or the working tree
|
|
115
|
+
// is restored to exactly what it was before this call.
|
|
116
|
+
try {
|
|
117
|
+
writeFileAtomic(finalOverviewPath, updated, "utf8");
|
|
118
|
+
await reIngest(projectCwd);
|
|
119
|
+
const refreshed = getCurrentProjectBlueprint(projectCwd, slug);
|
|
120
|
+
if (!refreshed.blueprint) {
|
|
121
|
+
throw new Error(`Blueprint "${slug}" did not appear in the projection after transition`);
|
|
122
|
+
}
|
|
123
|
+
return {
|
|
124
|
+
blueprint: refreshed.blueprint,
|
|
125
|
+
overviewPath: finalOverviewPath,
|
|
126
|
+
fromState: found.state,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
catch (error) {
|
|
130
|
+
try {
|
|
131
|
+
if (renamed) {
|
|
132
|
+
const { renameSync } = await import("node:fs");
|
|
133
|
+
renameSync(destinationPath, sourcePath);
|
|
134
|
+
}
|
|
135
|
+
writeFileAtomic(overviewPath, markdown, "utf8");
|
|
136
|
+
}
|
|
137
|
+
catch (rollbackError) {
|
|
138
|
+
throw new Error(`Transition to "${to_state}" failed and automatic rollback also failed; the blueprint may be left ${renamed ? `moved at ${destinationPath}` : "with partially-updated frontmatter"}. Original error: ${error instanceof Error ? error.message : String(error)}. Rollback error: ${rollbackError instanceof Error ? rollbackError.message : String(rollbackError)}`);
|
|
139
|
+
}
|
|
140
|
+
throw error;
|
|
108
141
|
}
|
|
109
|
-
return {
|
|
110
|
-
blueprint: refreshed.blueprint,
|
|
111
|
-
overviewPath: finalOverviewPath,
|
|
112
|
-
fromState: found.state,
|
|
113
|
-
};
|
|
114
142
|
}
|
|
@@ -2,13 +2,20 @@ import type { ProjectResolver } from "#project-resolver.js";
|
|
|
2
2
|
import type { ToolHandlerResult } from "#mcp/auto-discover.js";
|
|
3
3
|
/**
|
|
4
4
|
* MCP wrapper around the SAME reviews.md-ledger writer `logReviewEntry` that
|
|
5
|
-
* (an unwired) `wp review log` uses
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* trust model a hand-authored `approvals:` block already had (the
|
|
9
|
-
* is caller-asserted, verified only by the referenced
|
|
10
|
-
* entry being tracked in git) — this tool just makes that
|
|
11
|
-
* reachable from MCP instead of a raw Write/Edit or a Bash/python
|
|
5
|
+
* (an unwired) `wp review log` uses. For NON-approval verdicts it also appends
|
|
6
|
+
* the matching frontmatter `approvals:` entry, so rejection attribution stays
|
|
7
|
+
* visible where readers already look. This is the tracked/frontmatter provenance
|
|
8
|
+
* path — the same trust model a hand-authored `approvals:` block already had (the
|
|
9
|
+
* reviewer id is caller-asserted, verified only by the referenced
|
|
10
|
+
* artifact/reviews.md entry being tracked in git) — this tool just makes that
|
|
11
|
+
* existing path reachable from MCP instead of a raw Write/Edit or a Bash/python
|
|
12
|
+
* workaround.
|
|
13
|
+
*
|
|
14
|
+
* It never writes a POLICY-APPROVAL entry (approve / approve-with-nits) into
|
|
15
|
+
* frontmatter. `collectTrackedReviewProvenance` — the legacy promotion leg — only
|
|
16
|
+
* counts policy approvals and never reads or hashes the blueprint body, so such
|
|
17
|
+
* an entry would be gate authority that survives an arbitrary rewrite of the
|
|
18
|
+
* plan. See `LEGACY_APPROVAL_LEG_ANCHOR_COMMIT` in `#lifecycle/rollout-anchor.js`.
|
|
12
19
|
*
|
|
13
20
|
* A policy-approval verdict (approve / approve-with-nits) REQUIRES
|
|
14
21
|
* reviewer==="human" — matching `logReviewEvent`'s existing v2-ledger
|
|
@@ -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) {
|