@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
package/dist/esm/hooks/doctor.js
CHANGED
|
@@ -27,6 +27,8 @@ import { isAgentKitSourceRepo } from "#cli/commands/init/source-repo-hook-policy
|
|
|
27
27
|
import { findAgentKitPackageRoot, resolveAgentKitPackageRoot, } from "#cli/commands/init/package-root";
|
|
28
28
|
import { readInstalledHooksMap } from "#hooks/shared/installed-hooks.js";
|
|
29
29
|
import { auditCodexConfigPaths, checkCodexConfigPathIntegrity } from "#hooks/codex-config-paths.js";
|
|
30
|
+
import { findInstalledCodexPluginMcpManifests, pruneOwnedMcpServerBlock, } from "#cli/commands/init/scaffolders/codex-mcp/index.js";
|
|
31
|
+
import { isAgentKitOwnedWpCommand, WEBPRESSO_MCP_SERVER_NAME, } from "#cli/commands/init/mcp-spec.js";
|
|
30
32
|
import { parseProjectMcpServers, scanCodexConfigDrift } from "#hooks/host-config-contract.js";
|
|
31
33
|
import { defaultCodexConfigFilePath } from "#cli/commands/init/scaffolders/agent-hooks/codex-trust-sync.js";
|
|
32
34
|
import { expectedRootWpBinRelativePath, formatRootLauncherContractFailure, rootContractMode, validateRootLauncherContract, } from "#launcher/root-contract.js";
|
|
@@ -55,7 +57,10 @@ function createOwnedProbeSettler(child, timeoutMs, timeoutResult, resolve) {
|
|
|
55
57
|
return settle;
|
|
56
58
|
}
|
|
57
59
|
const RTK_REQUESTED_MARKER = join(".agent", ".rtk-requested");
|
|
58
|
-
const RTK_INSTALL_HINT = "rtk requested
|
|
60
|
+
const RTK_INSTALL_HINT = "rtk requested but managed binary missing; re-run wp setup (or set WP_RTK_BIN)";
|
|
61
|
+
const RTK_FREESTANDING_HOOK_REL = join(".claude", "hooks", "rtk-rewrite.sh");
|
|
62
|
+
const RTK_FREESTANDING_SETTINGS_REL = join(".claude", "settings.json");
|
|
63
|
+
const RTK_FREESTANDING_REPAIR = "stale freestanding rtk-rewrite surface; run wp setup (ensureRtk strips rtk-rewrite.sh — pretool-guard owns rewrite)";
|
|
59
64
|
const HOST_SMOKE_ENV = "WP_RUN_HOST_SMOKE";
|
|
60
65
|
const HOOK_PROBE_TIMEOUT_ENV = "WP_DOCTOR_HOOK_TIMEOUT_MS";
|
|
61
66
|
const WRAPPER_PROBE_ENV = "WP_DOCTOR_WRAPPER_PROBE";
|
|
@@ -347,6 +352,62 @@ function checkConsumerCodexHookPaths(cwd = process.cwd()) {
|
|
|
347
352
|
function wasRtkRequested(cwd = process.cwd()) {
|
|
348
353
|
return tryAccess(join(cwd, RTK_REQUESTED_MARKER));
|
|
349
354
|
}
|
|
355
|
+
export function checkFreestandingRtkSurface(cwd = process.cwd()) {
|
|
356
|
+
const hookPath = join(cwd, RTK_FREESTANDING_HOOK_REL);
|
|
357
|
+
const settingsPath = join(cwd, RTK_FREESTANDING_SETTINGS_REL);
|
|
358
|
+
const hookPresent = tryAccess(hookPath);
|
|
359
|
+
const markers = [];
|
|
360
|
+
if (hookPresent)
|
|
361
|
+
markers.push(RTK_FREESTANDING_HOOK_REL);
|
|
362
|
+
if (tryAccess(settingsPath)) {
|
|
363
|
+
try {
|
|
364
|
+
const settings = readFileSync(settingsPath, "utf8");
|
|
365
|
+
if (settings.includes("rtk-rewrite.sh")) {
|
|
366
|
+
markers.push(`${RTK_FREESTANDING_SETTINGS_REL} rtk-rewrite.sh`);
|
|
367
|
+
}
|
|
368
|
+
if (settings.includes("rtk hook claude")) {
|
|
369
|
+
markers.push(`${RTK_FREESTANDING_SETTINGS_REL} rtk hook claude`);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
catch {
|
|
373
|
+
// ignore unreadable settings
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
if (markers.length === 0)
|
|
377
|
+
return null;
|
|
378
|
+
return {
|
|
379
|
+
name: "freestanding rtk PreToolUse retired",
|
|
380
|
+
ok: false,
|
|
381
|
+
detail: `${RTK_FREESTANDING_REPAIR} (found: ${markers.join(", ")})`,
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
/** Non-mutating warn for user-global freestanding RTK rewriters (rtk init -g). */
|
|
385
|
+
export function checkGlobalFreestandingRtkSurface() {
|
|
386
|
+
const globalSettings = join(homedir(), ".claude", "settings.json");
|
|
387
|
+
const globalHook = join(homedir(), ".claude", "hooks", "rtk-rewrite.sh");
|
|
388
|
+
const markers = [];
|
|
389
|
+
if (tryAccess(globalHook))
|
|
390
|
+
markers.push("~/.claude/hooks/rtk-rewrite.sh");
|
|
391
|
+
if (tryAccess(globalSettings)) {
|
|
392
|
+
try {
|
|
393
|
+
const settings = readFileSync(globalSettings, "utf8");
|
|
394
|
+
if (settings.includes("rtk-rewrite.sh"))
|
|
395
|
+
markers.push("~/.claude/settings.json rtk-rewrite.sh");
|
|
396
|
+
if (settings.includes("rtk hook claude"))
|
|
397
|
+
markers.push("~/.claude/settings.json rtk hook claude");
|
|
398
|
+
}
|
|
399
|
+
catch {
|
|
400
|
+
// ignore
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
if (markers.length === 0)
|
|
404
|
+
return null;
|
|
405
|
+
return {
|
|
406
|
+
name: "global freestanding rtk (warn)",
|
|
407
|
+
ok: true,
|
|
408
|
+
detail: `WARNING: global freestanding RTK PreToolUse still present (${markers.join(", ")}); remove manually or re-run rtk uninstall — project setup does not mutate ~/.claude. Dual rewriters may race updatedInput with wp-pretool-guard.`,
|
|
409
|
+
};
|
|
410
|
+
}
|
|
350
411
|
function shouldRunHostChecks(mode) {
|
|
351
412
|
if (mode === "skip")
|
|
352
413
|
return false;
|
|
@@ -393,36 +454,13 @@ function resolveRequestedHosts(mode, hostNames) {
|
|
|
393
454
|
export function checkRtkOnPath(cwd) {
|
|
394
455
|
if (!wasRtkRequested(cwd))
|
|
395
456
|
return Promise.resolve(null);
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
const
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
let stderr = "";
|
|
404
|
-
const settle = createOwnedProbeSettler(child, timeoutMs, () => ({
|
|
405
|
-
name: "rtk on PATH",
|
|
406
|
-
ok: false,
|
|
407
|
-
detail: `rtk probe timed out after ${timeoutMs}ms`,
|
|
408
|
-
}), resolve);
|
|
409
|
-
child.stdout.on("data", (chunk) => {
|
|
410
|
-
stdout += String(chunk);
|
|
411
|
-
});
|
|
412
|
-
child.stderr?.on("data", (chunk) => {
|
|
413
|
-
stderr += String(chunk);
|
|
414
|
-
});
|
|
415
|
-
child.on("error", () => {
|
|
416
|
-
settle({ name: "rtk on PATH", ok: false, detail: RTK_INSTALL_HINT });
|
|
417
|
-
});
|
|
418
|
-
child.on("close", (code) => {
|
|
419
|
-
if (code === 0) {
|
|
420
|
-
settle({ name: "rtk on PATH", ok: true, detail: stdout.trim() || "rtk present" });
|
|
421
|
-
return;
|
|
422
|
-
}
|
|
423
|
-
const suffix = stderr.trim().length > 0 ? ` (${stderr.trim()})` : "";
|
|
424
|
-
settle({ name: "rtk on PATH", ok: false, detail: `${RTK_INSTALL_HINT}${suffix}` });
|
|
425
|
-
});
|
|
457
|
+
// Managed binary (Webpresso cache / WP_RTK_BIN), not bare PATH brew install.
|
|
458
|
+
return import("#tool-runtime/managed-rtk.js").then(({ isManagedRtkAvailable, resolveManagedRtkExecutable }) => {
|
|
459
|
+
const path = resolveManagedRtkExecutable();
|
|
460
|
+
if (path && isManagedRtkAvailable()) {
|
|
461
|
+
return { name: "rtk managed", ok: true, detail: path };
|
|
462
|
+
}
|
|
463
|
+
return { name: "rtk managed", ok: false, detail: RTK_INSTALL_HINT };
|
|
426
464
|
});
|
|
427
465
|
}
|
|
428
466
|
async function probeHookBin(wpCli, hookName, checkStdin) {
|
|
@@ -1184,19 +1222,44 @@ async function checkCodexHost() {
|
|
|
1184
1222
|
detail: `missing MCP entry (webpresso=${hasAgentKit})`,
|
|
1185
1223
|
};
|
|
1186
1224
|
}
|
|
1187
|
-
const
|
|
1188
|
-
if (
|
|
1189
|
-
|
|
1190
|
-
.map((finding) => `${finding.location}: ${finding.path} (${finding.reason === "missing" ? "path does not exist" : "volatile versioned path"})`)
|
|
1191
|
-
.join("; ");
|
|
1192
|
-
return {
|
|
1193
|
-
name: "Codex host integration",
|
|
1194
|
-
ok: false,
|
|
1195
|
-
detail: `webpresso MCP listed but launch paths are broken or rot-prone — ${detail}`,
|
|
1196
|
-
};
|
|
1197
|
-
}
|
|
1225
|
+
const legacy = classifyLegacyCodexWebpressoChannel();
|
|
1226
|
+
if (legacy)
|
|
1227
|
+
return { name: "Codex host integration", ok: false, detail: legacy };
|
|
1198
1228
|
return { name: "Codex host integration", ok: true, detail: "webpresso MCP visible" };
|
|
1199
1229
|
}
|
|
1230
|
+
/**
|
|
1231
|
+
* Codex must receive `webpresso` over exactly one channel — the bundled Codex
|
|
1232
|
+
* plugin. Report the retired user-global `[mcp_servers.webpresso]` block that
|
|
1233
|
+
* `wp setup` removes, and call out a double registration distinctly. A
|
|
1234
|
+
* user-authored block (a live `wp` outside every agent-kit root) is never
|
|
1235
|
+
* removed by setup, so it gets its own manual-action finding.
|
|
1236
|
+
*/
|
|
1237
|
+
export function classifyLegacyCodexWebpressoChannel(configPath = defaultCodexConfigFilePath(), homeDir = process.env.HOME || homedir()) {
|
|
1238
|
+
const outcome = classifyCodexConfigWebpressoBlock(configPath);
|
|
1239
|
+
if (outcome === null || outcome.kind === "absent")
|
|
1240
|
+
return null;
|
|
1241
|
+
if (outcome.kind === "ambiguous") {
|
|
1242
|
+
return `${configPath} declares webpresso in a TOML spelling wp setup cannot rewrite; remove the [mcp_servers.webpresso] definition by hand — the bundled Codex plugin is the single delivery channel`;
|
|
1243
|
+
}
|
|
1244
|
+
if (outcome.kind === "preserved") {
|
|
1245
|
+
return `${configPath} declares a user-authored webpresso MCP server (${outcome.command ?? "no command"}); wp setup preserves it. Remove it by hand to use the bundled Codex plugin as the single delivery channel`;
|
|
1246
|
+
}
|
|
1247
|
+
const dualChannel = findInstalledCodexPluginMcpManifests(homeDir).length > 0;
|
|
1248
|
+
const prefix = dualChannel
|
|
1249
|
+
? "webpresso is registered over BOTH delivery channels — the bundled Codex plugin AND a legacy"
|
|
1250
|
+
: "webpresso is still registered over the retired legacy";
|
|
1251
|
+
return `${prefix} [mcp_servers.webpresso] block in ${configPath} (command = ${outcome.command}); run \`wp setup\` — it removes the legacy block`;
|
|
1252
|
+
}
|
|
1253
|
+
function classifyCodexConfigWebpressoBlock(configPath) {
|
|
1254
|
+
try {
|
|
1255
|
+
if (!existsSync(configPath))
|
|
1256
|
+
return null;
|
|
1257
|
+
return pruneOwnedMcpServerBlock(readFileSync(configPath, "utf8"), WEBPRESSO_MCP_SERVER_NAME, isAgentKitOwnedWpCommand);
|
|
1258
|
+
}
|
|
1259
|
+
catch {
|
|
1260
|
+
return null;
|
|
1261
|
+
}
|
|
1262
|
+
}
|
|
1200
1263
|
async function checkOpenCodeHost(cwd = process.cwd()) {
|
|
1201
1264
|
const available = await runCommand("opencode", ["--version"]);
|
|
1202
1265
|
if (!available.ok) {
|
|
@@ -1430,9 +1493,8 @@ export function checkPackagedHostArtifacts(cwd = process.cwd()) {
|
|
|
1430
1493
|
const serverRecord = server && typeof server === "object" && !Array.isArray(server)
|
|
1431
1494
|
? server
|
|
1432
1495
|
: null;
|
|
1433
|
-
if (serverRecord?.command !== "
|
|
1434
|
-
|
|
1435
|
-
failures.push("Codex adapter MCP surface must expose direct webpresso server map launching ${PLUGIN_ROOT}/bin/wp mcp");
|
|
1496
|
+
if (serverRecord?.command !== "wp" || !stringArrayEquals(serverRecord.args, ["mcp"])) {
|
|
1497
|
+
failures.push("Codex adapter MCP surface must expose direct webpresso server map launching wp mcp");
|
|
1436
1498
|
}
|
|
1437
1499
|
}
|
|
1438
1500
|
if (codexHooks) {
|
|
@@ -2138,6 +2200,13 @@ export async function runHooksDoctor(opts = {}) {
|
|
|
2138
2200
|
const rtkCheck = await checkRtkOnPath(opts.cwd);
|
|
2139
2201
|
if (rtkCheck)
|
|
2140
2202
|
checks.push(rtkCheck);
|
|
2203
|
+
const freestandingRtk = checkFreestandingRtkSurface(opts.cwd);
|
|
2204
|
+
if (freestandingRtk)
|
|
2205
|
+
checks.push(freestandingRtk);
|
|
2206
|
+
// Non-mutating WARN only — never mutates ~/.claude; surfaces dual-rewriter risk.
|
|
2207
|
+
const globalFreestandingRtk = checkGlobalFreestandingRtkSurface();
|
|
2208
|
+
if (globalFreestandingRtk)
|
|
2209
|
+
checks.push(globalFreestandingRtk);
|
|
2141
2210
|
if (shouldRunHostChecks(hostMode)) {
|
|
2142
2211
|
for (const host of requestedHosts) {
|
|
2143
2212
|
if (host === "codex") {
|
|
@@ -23,7 +23,7 @@ function routeRawMutatingGitWorktree(command) {
|
|
|
23
23
|
return {
|
|
24
24
|
action: "deny",
|
|
25
25
|
tool: "wp_worktree",
|
|
26
|
-
guidance: "Use the `wp_worktree` MCP tool (or `wp worktree`
|
|
26
|
+
guidance: "Use the `wp_worktree` MCP tool (or `wp worktree prune --all` / `wp worktree remove <target>` when MCP is unavailable) instead — managed repositories may mutate linked worktrees only through agent-kit so owner bindings and the global registry stay consistent. Both prune paths also run `git worktree prune`, so a hand-deleted worktree directory still releases the branch it claimed.",
|
|
27
27
|
code: "GUARD_RAW_GIT_MUTATION",
|
|
28
28
|
};
|
|
29
29
|
}
|
|
@@ -296,11 +296,11 @@ const SAFE_PASSTHROUGH_PREFIXES = [
|
|
|
296
296
|
const SANDBOX_PREFIXES = [
|
|
297
297
|
{
|
|
298
298
|
prefix: "grep",
|
|
299
|
-
guidance: "
|
|
299
|
+
guidance: "Recursive/directory-wide grep is unbounded; narrow to explicit file target(s) (`grep -n pattern file`) to pass through directly. Otherwise use wp_session_batch_execute for bounded search so output is capped, indexed, and recallable instead of flooding the transcript — note wp_session_batch_execute runs argv directly (no shell) and cannot express `|`/`>`. If MCP is unavailable, use a bounded `rg -n ... | sed -n '1,120p'` excerpt instead of recursive raw grep output",
|
|
300
300
|
},
|
|
301
301
|
{
|
|
302
302
|
prefix: "find",
|
|
303
|
-
guidance: "Use wp_session_batch_execute for bounded shell gathering/search commands so output is capped, indexed, and recallable instead of flooding the transcript
|
|
303
|
+
guidance: "Use wp_session_batch_execute for bounded shell gathering/search commands so output is capped, indexed, and recallable instead of flooding the transcript — note wp_session_batch_execute runs argv directly (no shell) and cannot express `|`/`>`. If MCP is unavailable, use `rg --files ... | sed -n '1,120p'` or another explicit bounded listing",
|
|
304
304
|
},
|
|
305
305
|
{
|
|
306
306
|
prefix: "cat",
|
|
@@ -324,7 +324,7 @@ const SANDBOX_PREFIXES = [
|
|
|
324
324
|
},
|
|
325
325
|
{
|
|
326
326
|
prefix: "git log",
|
|
327
|
-
guidance: "
|
|
327
|
+
guidance: "Add an explicit count bound (`-N`, `-n N`, or `--max-count=N`) to pass through directly — e.g. `git log --oneline -50`. Otherwise use wp_session_batch_execute for bounded git log shell gathering so output is capped, indexed, and recallable. If MCP is unavailable, use an explicit bound directly, e.g. `git log --oneline -n 50`",
|
|
328
328
|
},
|
|
329
329
|
{
|
|
330
330
|
prefix: "git diff",
|
|
@@ -332,7 +332,7 @@ const SANDBOX_PREFIXES = [
|
|
|
332
332
|
},
|
|
333
333
|
{
|
|
334
334
|
prefix: "git show",
|
|
335
|
-
guidance: "
|
|
335
|
+
guidance: "Add `--stat`/`--name-only`/`--name-status`/`--shortstat` to pass through directly — e.g. `git show --stat HEAD` — or use wp_session_batch_execute or wp_session_execute for bounded gathering of the full patch",
|
|
336
336
|
},
|
|
337
337
|
{
|
|
338
338
|
prefix: "vp run build",
|
|
@@ -782,6 +782,63 @@ const BOUNDED_GIT_DIFF = new RegExp(String.raw `^git\s+${SIMPLE_GIT_GLOBAL_OPTIO
|
|
|
782
782
|
function isBoundedGitDiffRead(command) {
|
|
783
783
|
return BOUNDED_GIT_DIFF.test(command);
|
|
784
784
|
}
|
|
785
|
+
// `git log`/`git show` reads are bounded (small, predictable output) exactly when
|
|
786
|
+
// they carry an explicit count/summary bound the same way BOUNDED_GIT_DIFF already
|
|
787
|
+
// recognizes `--stat`/`--name-only`/etc. as bounded. Unbounded forms (bare `git log`,
|
|
788
|
+
// full-patch `git show <ref>`) still fall through to the SANDBOX_PREFIXES rules below.
|
|
789
|
+
const SIMPLE_GIT_LOG_PREFIX = new RegExp(String.raw `^git\s+${SIMPLE_GIT_GLOBAL_OPTION}log\b`, "u");
|
|
790
|
+
const GIT_LOG_COUNT_BOUND = /(?:^|\s)(?:-\d+|-n[=\s]?\d+|--max-count(?:=|\s+)\d+)(?:\s|$)/u;
|
|
791
|
+
function isBoundedGitLogRead(command) {
|
|
792
|
+
return SIMPLE_GIT_LOG_PREFIX.test(command) && GIT_LOG_COUNT_BOUND.test(command);
|
|
793
|
+
}
|
|
794
|
+
const BOUNDED_GIT_SHOW = new RegExp(String.raw `^git\s+${SIMPLE_GIT_GLOBAL_OPTION}show\b(?=[\s\S]*(?:--stat\b|--shortstat\b|--name-only\b|--name-status\b))(?![\s\S]*(?:--patch\b|-p\b))`, "u");
|
|
795
|
+
function isBoundedGitShowRead(command) {
|
|
796
|
+
return BOUNDED_GIT_SHOW.test(command);
|
|
797
|
+
}
|
|
798
|
+
// A grep read is bounded when it targets explicit file argument(s) (not a whole
|
|
799
|
+
// directory tree via -r/-R/--recursive/--include/--exclude*) and supplies at least
|
|
800
|
+
// one non-flag file target beyond the pattern itself. Recursive/directory-wide grep
|
|
801
|
+
// stays sandboxed since its output size is unpredictable.
|
|
802
|
+
const GREP_RECURSIVE_LONG_FLAGS = new Set([
|
|
803
|
+
"--recursive",
|
|
804
|
+
"--include",
|
|
805
|
+
"--exclude",
|
|
806
|
+
"--exclude-dir",
|
|
807
|
+
"--exclude-from",
|
|
808
|
+
]);
|
|
809
|
+
function isBoundedGrepRead(command) {
|
|
810
|
+
const tokens = tokenizeCommand(command);
|
|
811
|
+
if (tokens[0] !== "grep")
|
|
812
|
+
return false;
|
|
813
|
+
const positionals = [];
|
|
814
|
+
let optionsEnded = false;
|
|
815
|
+
for (let index = 1; index < tokens.length; index += 1) {
|
|
816
|
+
const token = tokens[index];
|
|
817
|
+
if (!token)
|
|
818
|
+
continue;
|
|
819
|
+
if (!optionsEnded && token === "--") {
|
|
820
|
+
optionsEnded = true;
|
|
821
|
+
continue;
|
|
822
|
+
}
|
|
823
|
+
if (!optionsEnded && token !== "-" && token.startsWith("-")) {
|
|
824
|
+
const longFlag = token.split("=")[0] ?? token;
|
|
825
|
+
if (GREP_RECURSIVE_LONG_FLAGS.has(longFlag))
|
|
826
|
+
return false;
|
|
827
|
+
// Strip a trailing bundled numeric argument (e.g. the `2` in `-rA2` for
|
|
828
|
+
// `-r -A 2`) before checking for a bundled `r`/`R` — otherwise a
|
|
829
|
+
// recursive short flag bundled with a value-taking flag like `-A`/`-B`/
|
|
830
|
+
// `-C`/`-m` slips past undetected.
|
|
831
|
+
const shortCluster = token.slice(1).replace(/\d+$/u, "");
|
|
832
|
+
if (/^[A-Za-z]+$/u.test(shortCluster) && /[rR]/u.test(shortCluster))
|
|
833
|
+
return false;
|
|
834
|
+
continue;
|
|
835
|
+
}
|
|
836
|
+
positionals.push(token);
|
|
837
|
+
}
|
|
838
|
+
// First positional is the search pattern; at least one more explicit file
|
|
839
|
+
// target is required to count as a bounded (non-stdin, non-directory) read.
|
|
840
|
+
return positionals.length >= 2;
|
|
841
|
+
}
|
|
785
842
|
function escapedRegexToken(value) {
|
|
786
843
|
return value.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
|
|
787
844
|
}
|
|
@@ -982,6 +1039,12 @@ export function routeCommand(command, _sessionId) {
|
|
|
982
1039
|
}
|
|
983
1040
|
if (isBoundedGitDiffRead(trimmed))
|
|
984
1041
|
return { action: { action: "passthrough" } };
|
|
1042
|
+
if (isBoundedGitLogRead(trimmed))
|
|
1043
|
+
return { action: { action: "passthrough" } };
|
|
1044
|
+
if (isBoundedGitShowRead(trimmed))
|
|
1045
|
+
return { action: { action: "passthrough" } };
|
|
1046
|
+
if (isBoundedGrepRead(trimmed))
|
|
1047
|
+
return { action: { action: "passthrough" } };
|
|
985
1048
|
// Explicit passthroughs (audits, safe git/nav commands)
|
|
986
1049
|
for (const prefix of PASSTHROUGH_PREFIXES) {
|
|
987
1050
|
if (matchesPrefix(trimmed, prefix))
|
|
@@ -0,0 +1,24 @@
|
|
|
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
|
+
export type RtkRewriteDecision = {
|
|
7
|
+
readonly action: "rewrite";
|
|
8
|
+
readonly command: string;
|
|
9
|
+
} | {
|
|
10
|
+
readonly action: "noop";
|
|
11
|
+
readonly reason: string;
|
|
12
|
+
};
|
|
13
|
+
export type RtkPresenceProbe = () => boolean;
|
|
14
|
+
export type RtkExecutableResolver = () => string | null;
|
|
15
|
+
/**
|
|
16
|
+
* Decide whether to rewrite a Bash command through managed rtk.
|
|
17
|
+
*/
|
|
18
|
+
export declare function decideRtkRewrite(command: string, isRtkPresent?: RtkPresenceProbe, resolveExecutable?: RtkExecutableResolver): RtkRewriteDecision;
|
|
19
|
+
/**
|
|
20
|
+
* POSIX single-quote wrap for absolute managed paths.
|
|
21
|
+
* Refuse NUL/newline/single-quote — those cannot be safely embedded in `'…'`.
|
|
22
|
+
* Double-quote wrapping is intentionally avoided ($ / ` / " injection).
|
|
23
|
+
*/
|
|
24
|
+
export declare function shellQuoteExecutable(path: string): string | null;
|
|
@@ -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") {
|
|
@@ -850,7 +850,9 @@ const GIT_GLOBAL_RUN = `(?:-C\\s+${QUOTED_ARG}\\s+|-c\\s+${QUOTED_ARG}\\s+|--git
|
|
|
850
850
|
// branch flags are allowed.
|
|
851
851
|
const FORBIDDEN_SUBCOMMAND = "(commit\\b|switch\\b(?!\\s+(?:-h|--help)\\b)|checkout\\s+(?!--(?:\\s|$))(?:(?:\\S+\\s+)*)\\S|branch\\s+(?:(?!-|--)\\S|--track\\b|--no-track\\b|--create-reflog\\b|--force\\b|-f\\b|-c\\b|-C\\b|-m\\b|-M\\b))";
|
|
852
852
|
const FORBIDDEN_OP = new RegExp(`\\bgit\\s+(${GIT_GLOBAL_RUN})${FORBIDDEN_SUBCOMMAND}`, "gu");
|
|
853
|
-
|
|
853
|
+
// Verb-only: deliberately does NOT slurp trailing arguments itself (see
|
|
854
|
+
// mainOwnershipGitOps below for why a naive `[^;&|(){}]*` capture is unsafe).
|
|
855
|
+
const MAIN_OWNERSHIP_VERB = new RegExp(`\\bgit\\s+(${GIT_GLOBAL_RUN})(switch\\b|checkout\\b|branch\\b|worktree\\s+add\\b)`, "gu");
|
|
854
856
|
function labelFor(subcommand) {
|
|
855
857
|
if (subcommand.startsWith("commit"))
|
|
856
858
|
return "git commit";
|
|
@@ -958,12 +960,69 @@ function mainOwnershipLabel(words) {
|
|
|
958
960
|
return "git worktree add main";
|
|
959
961
|
return null;
|
|
960
962
|
}
|
|
963
|
+
/**
|
|
964
|
+
* Quote-aware segment bounds for `command`, split on the same unquoted
|
|
965
|
+
* `;&|(){}` control characters as {@link splitShellSegments} (kept in sync
|
|
966
|
+
* intentionally), but returning OFFSETS instead of trimmed substrings so a
|
|
967
|
+
* caller can slice the original command text for a match that falls inside a
|
|
968
|
+
* given segment.
|
|
969
|
+
*/
|
|
970
|
+
function segmentBounds(command) {
|
|
971
|
+
const bounds = [];
|
|
972
|
+
let start = 0;
|
|
973
|
+
let quote = null;
|
|
974
|
+
let escaped = false;
|
|
975
|
+
for (let i = 0; i < command.length; i += 1) {
|
|
976
|
+
const ch = command[i];
|
|
977
|
+
if (ch === undefined)
|
|
978
|
+
continue;
|
|
979
|
+
if (escaped) {
|
|
980
|
+
escaped = false;
|
|
981
|
+
continue;
|
|
982
|
+
}
|
|
983
|
+
if (quote !== "'" && ch === "\\") {
|
|
984
|
+
escaped = true;
|
|
985
|
+
continue;
|
|
986
|
+
}
|
|
987
|
+
if ((ch === '"' || ch === "'") && (quote === null || quote === ch)) {
|
|
988
|
+
quote = quote === ch ? null : ch;
|
|
989
|
+
continue;
|
|
990
|
+
}
|
|
991
|
+
if (!quote && /[;&|(){}]/u.test(ch)) {
|
|
992
|
+
bounds.push({ start, end: i });
|
|
993
|
+
start = i + 1;
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
bounds.push({ start, end: command.length });
|
|
997
|
+
return bounds;
|
|
998
|
+
}
|
|
999
|
+
/**
|
|
1000
|
+
* Main-ownership candidate ops (`git switch|checkout|branch|worktree add`).
|
|
1001
|
+
*
|
|
1002
|
+
* MAIN_OWNERSHIP_VERB intentionally does NOT slurp trailing arguments itself
|
|
1003
|
+
* (unlike the pre-fix implementation) — a naive `[^;&|(){}]*` character class
|
|
1004
|
+
* is not quote-aware, so a read-only `git branch --format='%(refname:short)'`
|
|
1005
|
+
* has its captured subcommand text truncated mid-quote at the `(` inside the
|
|
1006
|
+
* quoted format string, producing an unterminated quote that shellWords()
|
|
1007
|
+
* reports as "ambiguous" and gets fail-closed blocked — purely because of a
|
|
1008
|
+
* paren inside its OWN quoted argument, not because it mutates anything.
|
|
1009
|
+
* Bounding the subcommand text to the enclosing quote-aware shell segment
|
|
1010
|
+
* (segmentBounds, sharing splitShellSegments' control-char set) fixes that
|
|
1011
|
+
* without weakening real-mutation detection: genuinely ambiguous syntax
|
|
1012
|
+
* (nested shells, unresolved substitutions, unterminated quotes) still
|
|
1013
|
+
* produces "ambiguous" and fails closed exactly as before.
|
|
1014
|
+
*/
|
|
961
1015
|
function mainOwnershipGitOps(command) {
|
|
962
1016
|
if (!/\bgit\b/u.test(command))
|
|
963
1017
|
return [];
|
|
964
1018
|
const ops = [];
|
|
965
|
-
|
|
966
|
-
|
|
1019
|
+
const segments = segmentBounds(command);
|
|
1020
|
+
for (const m of command.matchAll(MAIN_OWNERSHIP_VERB)) {
|
|
1021
|
+
const verb = m[2] ?? "";
|
|
1022
|
+
const verbStart = (m.index ?? 0) + m[0].length - verb.length;
|
|
1023
|
+
const segment = segments.find((s) => verbStart >= s.start && verbStart < s.end) ??
|
|
1024
|
+
{ start: verbStart, end: command.length };
|
|
1025
|
+
const subcommand = command.slice(verbStart, segment.end);
|
|
967
1026
|
const words = shellWords(subcommand);
|
|
968
1027
|
if (words === "ambiguous") {
|
|
969
1028
|
ops.push({ label: "git main", globals: m[1] ?? "", index: m.index ?? 0 });
|
|
@@ -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
|