@webpresso/agent-kit 3.1.28 → 3.1.30
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/catalog/AGENTS.md.tpl +4 -4
- package/catalog/agent/rules/ci-cost-local-first.md +139 -0
- package/catalog/agent/rules/ci-test-perf.md +5 -1
- package/catalog/agent/rules/cmd-execution.md +9 -0
- package/catalog/agent/rules/pre-implementation.md +2 -1
- package/catalog/agent/rules/test-scan-perf.md +80 -0
- package/catalog/agent/rules/workflow-skills-routing.md +4 -2
- package/catalog/agent/skills/fix/SKILL.md +3 -0
- package/catalog/agent/skills/verify/SKILL.md +18 -0
- package/dist/esm/audit/blueprint-pr-coverage.js +68 -7
- package/dist/esm/audit/registry.d.ts +4 -4
- package/dist/esm/audit/registry.js +3 -0
- package/dist/esm/audit/roadmap-links.js +6 -0
- package/dist/esm/audit/test-scan-perf.d.ts +18 -0
- package/dist/esm/audit/test-scan-perf.js +275 -0
- package/dist/esm/blueprint/lifecycle/engine.d.ts +9 -0
- package/dist/esm/blueprint/lifecycle/engine.js +53 -6
- package/dist/esm/blueprint/lifecycle/review-provenance.d.ts +6 -0
- package/dist/esm/blueprint/lifecycle/review-provenance.js +93 -12
- package/dist/esm/blueprint/markdown/helpers.js +6 -2
- package/dist/esm/ci/act-replay.js +110 -7
- package/dist/esm/ci/act-runner.d.ts +4 -0
- package/dist/esm/ci/act-runner.js +85 -26
- package/dist/esm/ci/act-worktree-git.d.ts +11 -0
- package/dist/esm/ci/act-worktree-git.js +219 -0
- package/dist/esm/cli/commands/audit.js +3 -0
- package/dist/esm/cli/commands/blueprint/mutations.js +35 -18
- package/dist/esm/cli/commands/blueprint/router.js +27 -2
- package/dist/esm/cli/commands/ci.d.ts +8 -1
- package/dist/esm/cli/commands/ci.js +80 -21
- package/dist/esm/cli/commands/dash/keymap.js +1 -0
- package/dist/esm/cli/commands/dash/launch-preferences.d.ts +15 -0
- package/dist/esm/cli/commands/dash/launch-preferences.js +46 -17
- package/dist/esm/cli/commands/dash/plan-once-label.d.ts +7 -0
- package/dist/esm/cli/commands/dash/plan-once-label.js +5 -0
- package/dist/esm/cli/commands/dash/plan-once-membership.d.ts +67 -0
- package/dist/esm/cli/commands/dash/plan-once-membership.js +151 -0
- package/dist/esm/cli/commands/dash/plan-once-merge.d.ts +143 -0
- package/dist/esm/cli/commands/dash/plan-once-merge.js +238 -0
- package/dist/esm/cli/commands/dash/plan-once.d.ts +22 -2
- package/dist/esm/cli/commands/dash/plan-once.js +41 -4
- package/dist/esm/cli/commands/dash/tui/attention-hud.d.ts +9 -0
- package/dist/esm/cli/commands/dash/tui/attention-hud.js +9 -0
- package/dist/esm/cli/commands/dash/tui/chat-bridge.d.ts +41 -0
- package/dist/esm/cli/commands/dash/tui/chat-bridge.js +73 -0
- package/dist/esm/cli/commands/dash/tui/chat-mode.d.ts +47 -0
- package/dist/esm/cli/commands/dash/tui/chat-mode.js +98 -0
- package/dist/esm/cli/commands/dash/tui/frame.d.ts +3 -0
- package/dist/esm/cli/commands/dash/tui/frame.js +3 -1
- package/dist/esm/cli/commands/dash/tui/palette.d.ts +1 -1
- package/dist/esm/cli/commands/dash/tui/palette.js +14 -0
- package/dist/esm/cli/commands/dash/tui/runtime.js +239 -4
- package/dist/esm/cli/commands/dash/tui/state.d.ts +41 -0
- package/dist/esm/cli/commands/dash/tui/state.js +257 -4
- package/dist/esm/cli/commands/format.js +0 -2
- package/dist/esm/cli/commands/init/config.d.ts +0 -1
- package/dist/esm/cli/commands/init/config.js +1 -4
- package/dist/esm/cli/commands/init/scaffolders/agent-hooks/emitters/claude.d.ts +5 -6
- package/dist/esm/cli/commands/init/scaffolders/agent-hooks/emitters/claude.js +6 -13
- package/dist/esm/cli/commands/init/scaffolders/agent-hooks/ir.d.ts +0 -2
- package/dist/esm/cli/commands/init/scaffolders/agent-hooks/ir.js +0 -2
- package/dist/esm/cli/commands/init/scaffolders/agent-hooks/merge.d.ts +6 -3
- package/dist/esm/cli/commands/init/scaffolders/agent-hooks/merge.js +39 -26
- package/dist/esm/cli/commands/init/scaffolders/codex-mcp/index.js +61 -23
- package/dist/esm/cli/commands/logs.d.ts +15 -0
- package/dist/esm/cli/commands/logs.js +192 -3
- package/dist/esm/cli/commands/quality-log-store.d.ts +1 -0
- package/dist/esm/cli/commands/quality-log-store.js +187 -51
- package/dist/esm/cli/commands/review.js +5 -8
- package/dist/esm/cli/commands/secrets.d.ts +1 -0
- package/dist/esm/cli/commands/secrets.js +4 -1
- package/dist/esm/cli/direct-provider-launch.js +25 -1
- package/dist/esm/hooks/doctor.d.ts +15 -0
- package/dist/esm/hooks/doctor.js +56 -0
- package/dist/esm/hooks/pretool-guard/validators/forbidden-commands.js +4 -4
- package/dist/esm/hooks/pretool-guard/validators/mcp-redirect.d.ts +0 -1
- package/dist/esm/hooks/pretool-guard/validators/mcp-redirect.js +8 -5
- package/dist/esm/hooks/pretool-guard/validators/worktree-discipline.d.ts +9 -7
- package/dist/esm/hooks/pretool-guard/validators/worktree-discipline.js +126 -14
- package/dist/esm/mcp/tools/_shared/audit-kinds.d.ts +1 -1
- package/dist/esm/mcp/tools/audit.d.ts +2 -1
- package/dist/esm/mcp/tools/audit.js +10 -0
- package/dist/esm/mcp/tools/audits.d.ts +1 -0
- package/dist/esm/mcp/tools/ci-act.js +9 -11
- package/dist/esm/paths/state-root.js +3 -0
- package/dist/esm/review/authority.js +17 -2
- package/dist/esm/review/availability.js +18 -5
- package/dist/esm/review/subject.d.ts +15 -0
- package/dist/esm/review/subject.js +105 -10
- package/dist/esm/runtime/executor.d.ts +1 -0
- package/dist/esm/runtime/executor.js +2 -1
- package/dist/esm/secret-gate/runner.d.ts +1 -0
- package/dist/esm/secret-gate/runner.js +4 -1
- package/dist/esm/worktrees/current.d.ts +1 -0
- package/dist/esm/worktrees/current.js +1 -0
- package/dist/esm/worktrees/project-agent-surfaces.d.ts +1 -0
- package/dist/esm/worktrees/project-agent-surfaces.js +2 -1
- package/package.json +14 -14
|
@@ -25,13 +25,15 @@ const VALIDATOR_NAME = "worktree-discipline";
|
|
|
25
25
|
* effective cwd elsewhere (e.g. CI) → ALLOW (rule scoped to local ~/repos primaries)
|
|
26
26
|
* ```
|
|
27
27
|
*
|
|
28
|
-
* The "effective cwd" is the tool's ambient `input.cwd`, lowered
|
|
29
|
-
* success-gated
|
|
30
|
-
* `cd <worktree> && git commit
|
|
31
|
-
* EVERY `git -C <dir>` flag on the
|
|
32
|
-
* cumulatively).
|
|
33
|
-
*
|
|
34
|
-
*
|
|
28
|
+
* The "effective cwd" is the tool's ambient `input.cwd`, lowered by a
|
|
29
|
+
* success-gated `&&` chain that governs the forbidden git op, e.g.
|
|
30
|
+
* `cd <worktree> && git commit`, `cd <worktree> && ./bin/wp … && git commit`,
|
|
31
|
+
* or `(cd <worktree> && git commit)`, then by EVERY `git -C <dir>` flag on the
|
|
32
|
+
* matched invocation (git applies them cumulatively). Intermediate non-cwd
|
|
33
|
+
* commands after a proven `cd` leave the chain cwd unchanged. Unsupported
|
|
34
|
+
* control flow (`;` / `|` / braces), option-bearing `cd`, `cd -`, or
|
|
35
|
+
* unresolvable `cd`/`-C` targets fail closed because they could hide a
|
|
36
|
+
* primary-checkout mutation. Prefer `git -C <worktree> ...` or running the
|
|
35
37
|
* command with the tool cwd already set to the worktree.
|
|
36
38
|
*
|
|
37
39
|
* Known best-effort limits (acceptable for an agent guard with a documented
|
|
@@ -634,12 +636,13 @@ const DIR_TOKEN = String.raw `(?:"[^"]+"|'[^']+'|[^\s;&|(){}]+)`;
|
|
|
634
636
|
const QUOTED_ARG = String.raw `(?:"[^"]+"|'[^']+'|\S+)`;
|
|
635
637
|
const CD_TARGET = String.raw `(?:--\s+)?(${DIR_TOKEN}|-)`;
|
|
636
638
|
const CD_LEADER = String.raw `(?:[A-Za-z_]\w*=\S*\s+)*(?:command\s+|builtin\s+)?(?:cd|pushd)\s+`;
|
|
637
|
-
//
|
|
638
|
-
//
|
|
639
|
-
|
|
639
|
+
// Pure cd/pushd segment that updates the success-gated cwd (optional leading env /
|
|
640
|
+
// command|builtin wrapper). Trailing whitespace is allowed; nothing else.
|
|
641
|
+
const PURE_CD_SEGMENT = new RegExp(String.raw `^\s*${CD_LEADER}${CD_TARGET}\s*$`, "u");
|
|
642
|
+
// Legacy pure trailing-cd chain (also used for subshell-local `(cd X && git …)`).
|
|
640
643
|
const CD_CHAIN = new RegExp(String.raw `(?:^|[;&|(){}]\s*)?((?:\s*${CD_LEADER}${CD_TARGET}\s*&&)+)\s*$`, "u");
|
|
641
644
|
const CD_IN_CHAIN = new RegExp(String.raw `(?:^|&&)\s*${CD_LEADER}${CD_TARGET}\s*(?=&&)`, "gu");
|
|
642
|
-
const CWD_WORD = new RegExp(String.raw `(?:^|[;{}()&|]\s*|\s+)(?:[A-Za-z_]\w*=\S*\s+)*(?:command\s+|builtin\s+)?(?:cd|pushd)\b`, "gu");
|
|
645
|
+
const CWD_WORD = new RegExp(String.raw `(?:^|[;{}()&|]\s*|\s+)(?:[A-Za-z_]\w*=\S*\s+)*(?:command\s+|builtin\s+)?(?:cd|pushd|popd)\b`, "gu");
|
|
643
646
|
const DASH_C = new RegExp(String.raw `-C\s+(${DIR_TOKEN})`, "gu");
|
|
644
647
|
function isInsideSingleOrDoubleQuotes(command, index) {
|
|
645
648
|
let quote = null;
|
|
@@ -667,6 +670,27 @@ function hasUnquotedCwdCommand(command) {
|
|
|
667
670
|
}
|
|
668
671
|
return false;
|
|
669
672
|
}
|
|
673
|
+
/** True when unquoted `;` `|` `{` `}` appear (breaks a pure `&&` agent chain). */
|
|
674
|
+
function hasUnquotedUnsafeAndChainControl(command) {
|
|
675
|
+
for (let i = 0; i < command.length; i += 1) {
|
|
676
|
+
const ch = command[i];
|
|
677
|
+
if (ch !== ";" && ch !== "|" && ch !== "{" && ch !== "}" && ch !== "\n")
|
|
678
|
+
continue;
|
|
679
|
+
if (!isInsideSingleOrDoubleQuotes(command, i))
|
|
680
|
+
return true;
|
|
681
|
+
}
|
|
682
|
+
return false;
|
|
683
|
+
}
|
|
684
|
+
function hasUnquotedParen(command) {
|
|
685
|
+
for (let i = 0; i < command.length; i += 1) {
|
|
686
|
+
const ch = command[i];
|
|
687
|
+
if (ch !== "(" && ch !== ")")
|
|
688
|
+
continue;
|
|
689
|
+
if (!isInsideSingleOrDoubleQuotes(command, i))
|
|
690
|
+
return true;
|
|
691
|
+
}
|
|
692
|
+
return false;
|
|
693
|
+
}
|
|
670
694
|
/** Apply one dir token to `cwd`, or signal ambiguity if it cannot be statically resolved. */
|
|
671
695
|
function applyDir(token, cwd) {
|
|
672
696
|
if (!token)
|
|
@@ -683,11 +707,85 @@ function isTrustedCwdChainStart(commandPrefix, chainStart) {
|
|
|
683
707
|
const before = commandPrefix.slice(0, chainStart).trimEnd();
|
|
684
708
|
return before.endsWith("&&") || before.endsWith("(") || before.endsWith("{");
|
|
685
709
|
}
|
|
686
|
-
|
|
710
|
+
/**
|
|
711
|
+
* Split a pure `&&` command prefix into segments. Returns null when the prefix
|
|
712
|
+
* uses `;` / `|` / braces / newlines we refuse to model.
|
|
713
|
+
*/
|
|
714
|
+
function splitSuccessGatedAndSegments(commandPrefix) {
|
|
715
|
+
if (!commandPrefix.trim())
|
|
716
|
+
return [];
|
|
717
|
+
if (hasUnquotedUnsafeAndChainControl(commandPrefix))
|
|
718
|
+
return null;
|
|
719
|
+
const segments = [];
|
|
720
|
+
let current = "";
|
|
721
|
+
let quote = null;
|
|
722
|
+
let escaped = false;
|
|
723
|
+
for (let i = 0; i < commandPrefix.length; i += 1) {
|
|
724
|
+
const ch = commandPrefix[i] ?? "";
|
|
725
|
+
if (escaped) {
|
|
726
|
+
current += ch;
|
|
727
|
+
escaped = false;
|
|
728
|
+
continue;
|
|
729
|
+
}
|
|
730
|
+
if (quote !== "'" && ch === "\\") {
|
|
731
|
+
escaped = true;
|
|
732
|
+
current += ch;
|
|
733
|
+
continue;
|
|
734
|
+
}
|
|
735
|
+
if ((ch === '"' || ch === "'") && (quote === null || quote === ch)) {
|
|
736
|
+
quote = quote === ch ? null : ch;
|
|
737
|
+
current += ch;
|
|
738
|
+
continue;
|
|
739
|
+
}
|
|
740
|
+
if (!quote && ch === "&" && commandPrefix[i + 1] === "&") {
|
|
741
|
+
if (current.trim())
|
|
742
|
+
segments.push(current);
|
|
743
|
+
current = "";
|
|
744
|
+
i += 1;
|
|
745
|
+
continue;
|
|
746
|
+
}
|
|
747
|
+
current += ch;
|
|
748
|
+
}
|
|
749
|
+
if (quote || escaped)
|
|
750
|
+
return null;
|
|
751
|
+
if (current.trim())
|
|
752
|
+
segments.push(current);
|
|
753
|
+
return segments;
|
|
754
|
+
}
|
|
755
|
+
/**
|
|
756
|
+
* Walk pure `&&` segments: resolvable cd/pushd updates cwd; other segments must
|
|
757
|
+
* not smuggle unquoted cwd changes. Enables
|
|
758
|
+
* `cd <managed-worktree> && ./bin/wp … && git commit`.
|
|
759
|
+
*/
|
|
760
|
+
function applyAndSegmentCwdChain(commandPrefix, baseCwd) {
|
|
761
|
+
const segments = splitSuccessGatedAndSegments(commandPrefix);
|
|
762
|
+
if (segments === null)
|
|
763
|
+
return { ambiguous: true };
|
|
764
|
+
if (segments.length === 0)
|
|
765
|
+
return { ambiguous: false, cwd: baseCwd };
|
|
766
|
+
let cwd = baseCwd;
|
|
767
|
+
for (const segment of segments) {
|
|
768
|
+
const pureCd = PURE_CD_SEGMENT.exec(segment);
|
|
769
|
+
if (pureCd) {
|
|
770
|
+
const next = applyDir(pureCd[1], cwd);
|
|
771
|
+
if (next === "ambiguous")
|
|
772
|
+
return { ambiguous: true };
|
|
773
|
+
cwd = next.cwd;
|
|
774
|
+
continue;
|
|
775
|
+
}
|
|
776
|
+
if (hasUnquotedCwdCommand(segment))
|
|
777
|
+
return { ambiguous: true };
|
|
778
|
+
}
|
|
779
|
+
return { ambiguous: false, cwd };
|
|
780
|
+
}
|
|
781
|
+
/**
|
|
782
|
+
* Legacy trailing pure-cd chain (and subshell-local `(cd X && git …)` forms).
|
|
783
|
+
* Kept for paren-scoped ops; intermediate non-cd commands are handled by
|
|
784
|
+
* {@link applyAndSegmentCwdChain} when no unquoted parens are present.
|
|
785
|
+
*/
|
|
786
|
+
function applyLegacyTrailingCdChain(commandPrefix, baseCwd) {
|
|
687
787
|
const chain = CD_CHAIN.exec(commandPrefix);
|
|
688
788
|
const chainStart = chain?.index ?? commandPrefix.length;
|
|
689
|
-
// If there is any other unquoted cwd-changing syntax before this simple chain,
|
|
690
|
-
// we cannot prove the chain governs the op. Fail closed.
|
|
691
789
|
if (hasUnquotedCwdCommand(commandPrefix.slice(0, chainStart)))
|
|
692
790
|
return { ambiguous: true };
|
|
693
791
|
if (!chain)
|
|
@@ -703,6 +801,20 @@ function applySuccessGatedCwdChain(commandPrefix, baseCwd) {
|
|
|
703
801
|
}
|
|
704
802
|
return { ambiguous: false, cwd };
|
|
705
803
|
}
|
|
804
|
+
/**
|
|
805
|
+
* Success-gated cwd chain for a forbidden git op's command prefix.
|
|
806
|
+
*
|
|
807
|
+
* Prefer the `&&`-segment walk when the prefix has no unquoted parentheses so
|
|
808
|
+
* agents can run tools between a managed `cd` and a mutation. When parentheses
|
|
809
|
+
* appear (subshell-local ops), fall back to the legacy trailing-cd chain so
|
|
810
|
+
* non-persistent `(cd …) && git …` stays fail-closed.
|
|
811
|
+
*/
|
|
812
|
+
function applySuccessGatedCwdChain(commandPrefix, baseCwd) {
|
|
813
|
+
if (!hasUnquotedParen(commandPrefix)) {
|
|
814
|
+
return applyAndSegmentCwdChain(commandPrefix, baseCwd);
|
|
815
|
+
}
|
|
816
|
+
return applyLegacyTrailingCdChain(commandPrefix, baseCwd);
|
|
817
|
+
}
|
|
706
818
|
/**
|
|
707
819
|
* The directory the git op actually runs in: `baseCwd` lowered by the
|
|
708
820
|
* success-gated cwd chain that directly governs this op, then through EVERY
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
export declare const MCP_AUDIT_KINDS: readonly ["test-smells", "tph-e2e", "agents", "catalog-drift", "package-surface", "reference-parity-matrix", "docs-frontmatter", "blueprint-readme-drift", "blueprint-pr-coverage", "blueprint-lifecycle", "blueprint-trust", "architecture-drift", "cloudflare-deploy-contract", "absolute-path-policy", "no-first-party-mjs", "no-math-random", "roadmap-links", "bundle-budget", "commit-message", "tech-debt", "supported-agent-clis", "hook-surface", "harness-surfaces", "weakness-mining", "harness-overlay-evidence", "ai-contracts", "atomic-state-writes", "no-relative-package-scripts", "toolchain-isolation", "open-source-licenses", "secrets-policy", "no-dev-vars", "github-actions-secrets", "secret-provider-quarantine", "security-quality-regressions", "secrets-config", "consumer-agent-kit-dependency", "ci-test-perf", "session-memory-hardcut", "worktree-main-ownership", "sast"];
|
|
1
|
+
export declare const MCP_AUDIT_KINDS: readonly ["test-smells", "tph-e2e", "agents", "catalog-drift", "package-surface", "reference-parity-matrix", "docs-frontmatter", "blueprint-readme-drift", "blueprint-pr-coverage", "blueprint-lifecycle", "blueprint-trust", "architecture-drift", "cloudflare-deploy-contract", "absolute-path-policy", "no-first-party-mjs", "no-math-random", "roadmap-links", "bundle-budget", "commit-message", "tech-debt", "supported-agent-clis", "hook-surface", "harness-surfaces", "weakness-mining", "harness-overlay-evidence", "ai-contracts", "atomic-state-writes", "no-relative-package-scripts", "toolchain-isolation", "open-source-licenses", "secrets-policy", "no-dev-vars", "github-actions-secrets", "secret-provider-quarantine", "security-quality-regressions", "secrets-config", "consumer-agent-kit-dependency", "ci-test-perf", "test-scan-perf", "session-memory-hardcut", "worktree-main-ownership", "sast"];
|
|
2
2
|
export type MCPAuditKind = (typeof MCP_AUDIT_KINDS)[number];
|
|
3
3
|
export declare function isMCPAuditKind(value: string): value is MCPAuditKind;
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
*/
|
|
16
16
|
import { z } from "zod";
|
|
17
17
|
import type { ToolDescriptor } from "#mcp/auto-discover";
|
|
18
|
-
export declare const KINDS: readonly ["test-smells", "tph-e2e", "agents", "catalog-drift", "package-surface", "reference-parity-matrix", "docs-frontmatter", "blueprint-readme-drift", "blueprint-pr-coverage", "blueprint-lifecycle", "blueprint-trust", "architecture-drift", "cloudflare-deploy-contract", "absolute-path-policy", "no-first-party-mjs", "no-math-random", "roadmap-links", "bundle-budget", "commit-message", "tech-debt", "supported-agent-clis", "hook-surface", "harness-surfaces", "weakness-mining", "harness-overlay-evidence", "ai-contracts", "atomic-state-writes", "no-relative-package-scripts", "toolchain-isolation", "open-source-licenses", "secrets-policy", "no-dev-vars", "github-actions-secrets", "secret-provider-quarantine", "security-quality-regressions", "secrets-config", "consumer-agent-kit-dependency", "ci-test-perf", "session-memory-hardcut", "worktree-main-ownership", "sast"];
|
|
18
|
+
export declare const KINDS: readonly ["test-smells", "tph-e2e", "agents", "catalog-drift", "package-surface", "reference-parity-matrix", "docs-frontmatter", "blueprint-readme-drift", "blueprint-pr-coverage", "blueprint-lifecycle", "blueprint-trust", "architecture-drift", "cloudflare-deploy-contract", "absolute-path-policy", "no-first-party-mjs", "no-math-random", "roadmap-links", "bundle-budget", "commit-message", "tech-debt", "supported-agent-clis", "hook-surface", "harness-surfaces", "weakness-mining", "harness-overlay-evidence", "ai-contracts", "atomic-state-writes", "no-relative-package-scripts", "toolchain-isolation", "open-source-licenses", "secrets-policy", "no-dev-vars", "github-actions-secrets", "secret-provider-quarantine", "security-quality-regressions", "secrets-config", "consumer-agent-kit-dependency", "ci-test-perf", "test-scan-perf", "session-memory-hardcut", "worktree-main-ownership", "sast"];
|
|
19
19
|
export declare const inputSchema: z.ZodObject<{
|
|
20
20
|
kind: z.ZodEnum<{
|
|
21
21
|
"absolute-path-policy": "absolute-path-policy";
|
|
@@ -54,6 +54,7 @@ export declare const inputSchema: z.ZodObject<{
|
|
|
54
54
|
"session-memory-hardcut": "session-memory-hardcut";
|
|
55
55
|
"supported-agent-clis": "supported-agent-clis";
|
|
56
56
|
"tech-debt": "tech-debt";
|
|
57
|
+
"test-scan-perf": "test-scan-perf";
|
|
57
58
|
"test-smells": "test-smells";
|
|
58
59
|
"toolchain-isolation": "toolchain-isolation";
|
|
59
60
|
"tph-e2e": "tph-e2e";
|
|
@@ -464,6 +464,16 @@ export async function dispatchAudit(input) {
|
|
|
464
464
|
details: auditResult,
|
|
465
465
|
};
|
|
466
466
|
}
|
|
467
|
+
case "test-scan-perf": {
|
|
468
|
+
const { auditTestScanPerf } = await import("#audit/test-scan-perf");
|
|
469
|
+
const auditResult = auditTestScanPerf(input.cwd ?? input.directory ?? process.cwd());
|
|
470
|
+
return {
|
|
471
|
+
passed: auditResult.ok,
|
|
472
|
+
summary: summarizeRepoAudit(kind, auditResult),
|
|
473
|
+
kind,
|
|
474
|
+
details: auditResult,
|
|
475
|
+
};
|
|
476
|
+
}
|
|
467
477
|
case "hook-surface": {
|
|
468
478
|
const { auditHookSurface } = await import("#audit/hook-surface");
|
|
469
479
|
const auditResult = auditHookSurface(input.cwd ?? input.directory);
|
|
@@ -50,6 +50,7 @@ declare const inputSchema: z.ZodObject<{
|
|
|
50
50
|
"session-memory-hardcut": "session-memory-hardcut";
|
|
51
51
|
"supported-agent-clis": "supported-agent-clis";
|
|
52
52
|
"tech-debt": "tech-debt";
|
|
53
|
+
"test-scan-perf": "test-scan-perf";
|
|
53
54
|
"test-smells": "test-smells";
|
|
54
55
|
"toolchain-isolation": "toolchain-isolation";
|
|
55
56
|
"tph-e2e": "tph-e2e";
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import { DEFAULT_CI_ACT_TIMEOUT_MS, MAX_CI_ACT_TIMEOUT_MS } from "#cli/commands/ci";
|
|
3
|
-
import { buildPublicCiActCommand, preparePublicCiActExecution, resolveCiActExecutionMode, resolveCiActSecretEnvProfile, sanitizePublicCiActArgv, } from "#ci/act-runner.js";
|
|
2
|
+
import { DEFAULT_CI_ACT_TIMEOUT_MS, MAX_CI_ACT_TIMEOUT_MS, SMOKE_CI_ACT_TIMEOUT_MS, } from "#cli/commands/ci";
|
|
3
|
+
import { buildPublicCiActCommand, preparePublicCiActExecution, resolveCiActExecutionMode, resolveCiActSecretEnvProfile, sanitizePublicCiActArgv, shouldForceCiActSecretGate, } from "#ci/act-runner.js";
|
|
4
4
|
import { isSecretGateRuntimeProfile, runSecretGateCommand } from "#secret-gate/runner.js";
|
|
5
5
|
import { clipRawOutput, createSummaryOutputSchema, createSummaryResult } from "./_shared/result.js";
|
|
6
6
|
import { resolveProjectRoot } from "./_shared/project-root.js";
|
|
@@ -10,6 +10,7 @@ const inputSchema = z
|
|
|
10
10
|
cwd: z.string().optional(),
|
|
11
11
|
workflowPath: z.string(),
|
|
12
12
|
job: z.string().optional(),
|
|
13
|
+
matrix: z.array(z.string()).optional().default([]),
|
|
13
14
|
eventName: z
|
|
14
15
|
.enum(["pull_request", "push", "workflow_dispatch"])
|
|
15
16
|
.optional()
|
|
@@ -24,13 +25,8 @@ const inputSchema = z
|
|
|
24
25
|
}),
|
|
25
26
|
secretProfile: z.string().optional(),
|
|
26
27
|
mode: z.enum(["direct", "replay"]).optional().default("direct"),
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
.int()
|
|
30
|
-
.positive()
|
|
31
|
-
.max(MAX_CI_ACT_TIMEOUT_MS)
|
|
32
|
-
.optional()
|
|
33
|
-
.default(DEFAULT_CI_ACT_TIMEOUT_MS),
|
|
28
|
+
smoke: z.boolean().optional().default(false),
|
|
29
|
+
timeoutMs: z.number().int().positive().max(MAX_CI_ACT_TIMEOUT_MS).optional(),
|
|
34
30
|
containerArchitecture: z.string().optional(),
|
|
35
31
|
platformImage: z.string().optional().default("ghcr.io/catthehacker/ubuntu:full-latest"),
|
|
36
32
|
execute: z.boolean().optional().default(false),
|
|
@@ -96,6 +92,7 @@ const tool = {
|
|
|
96
92
|
handler: async (raw, extra) => {
|
|
97
93
|
const input = inputSchema.parse(raw ?? {});
|
|
98
94
|
const cwd = resolveProjectRoot(input.cwd ? { cwd: input.cwd } : {});
|
|
95
|
+
const timeoutMs = input.timeoutMs ?? (input.smoke ? SMOKE_CI_ACT_TIMEOUT_MS : DEFAULT_CI_ACT_TIMEOUT_MS);
|
|
99
96
|
if (!input.execute) {
|
|
100
97
|
return createSummaryResult({
|
|
101
98
|
passed: true,
|
|
@@ -108,13 +105,14 @@ const tool = {
|
|
|
108
105
|
const secretProfile = resolveCiActSecretEnvProfile({ ...input, cwd }) ?? "preview";
|
|
109
106
|
const result = await runSecretGateCommand({
|
|
110
107
|
cwd,
|
|
108
|
+
commandCwd: prepared.cwd,
|
|
111
109
|
sink: "act",
|
|
112
110
|
profile: secretProfile,
|
|
113
111
|
envProfile: input.envProfile,
|
|
114
|
-
forceSecretGate:
|
|
112
|
+
forceSecretGate: shouldForceCiActSecretGate(input.envProfile),
|
|
115
113
|
command: "act",
|
|
116
114
|
args: prepared.command.actArgs,
|
|
117
|
-
timeoutMs
|
|
115
|
+
timeoutMs,
|
|
118
116
|
signal: extra?.signal,
|
|
119
117
|
});
|
|
120
118
|
const merged = [result.stdout, result.stderr].filter(Boolean).join("\n");
|
|
@@ -247,6 +247,9 @@ export async function getSurfacePathAsync(name, scope, cwd = resolveCwd()) {
|
|
|
247
247
|
export async function withLock(scope, fn, cwd) {
|
|
248
248
|
const lockTarget = await getSurfacePathAsync(".lock", scope, cwd);
|
|
249
249
|
mkdirSync(dirname(lockTarget), { recursive: true });
|
|
250
|
+
// Keep retries short so lock-contention probes (review gate contender tests)
|
|
251
|
+
// still fail closed in a few seconds. Dual-writer create loops retry at the
|
|
252
|
+
// call site when they need longer budgets under CI load.
|
|
250
253
|
const release = await lockfile.lock(lockTarget, {
|
|
251
254
|
realpath: false,
|
|
252
255
|
retries: { retries: 5, minTimeout: 50, maxTimeout: 500, factor: 2 },
|
|
@@ -9,7 +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 { createDeliverySubjectAtRef, createPlanSubjectAtRef, resolveReviewCommit, } from "./subject.js";
|
|
12
|
+
import { createDeliverySubjectAtRef, createLegacyPlanSubjectAtRef, createPlanSubjectAtRef, resolveReviewCommit, } from "./subject.js";
|
|
13
13
|
const GIT_TIMEOUT_MS = 5_000;
|
|
14
14
|
const GIT_MAX_BUFFER_BYTES = 128 * 1024 * 1024;
|
|
15
15
|
const REGULAR_MODE_PATTERN = /^100(?:644|755)$/u;
|
|
@@ -126,12 +126,27 @@ export function readReviewAuthorityAtRef(cwd, ref, stableSlug, purpose) {
|
|
|
126
126
|
const subject = purpose === "plan"
|
|
127
127
|
? createPlanSubjectAtRef(cwd, commit, stableSlug, overview.path)
|
|
128
128
|
: createDeliverySubjectAtRef(cwd, commit, stableSlug, overview.path);
|
|
129
|
-
|
|
129
|
+
let evaluation = evaluateReviewEvents(events, {
|
|
130
130
|
purpose,
|
|
131
131
|
blueprintSlug: stableSlug,
|
|
132
132
|
subjectScheme: subject.scheme,
|
|
133
133
|
subjectDigest: subject.digest,
|
|
134
134
|
}, compromisedIds);
|
|
135
|
+
if (purpose === "plan" && evaluation.approvals.length === 0) {
|
|
136
|
+
// Digest-compatibility fallback: events recorded before the lifecycle
|
|
137
|
+
// stripped-key extension bound their subjectDigest to the legacy
|
|
138
|
+
// normalization. Accept those for the same tracked content; new events
|
|
139
|
+
// always record the current digest (see legacyNormalizePlanSubjectContent).
|
|
140
|
+
const legacySubject = createLegacyPlanSubjectAtRef(cwd, commit, stableSlug, overview.path);
|
|
141
|
+
if (legacySubject.digest !== subject.digest) {
|
|
142
|
+
evaluation = evaluateReviewEvents(events, {
|
|
143
|
+
purpose,
|
|
144
|
+
blueprintSlug: stableSlug,
|
|
145
|
+
subjectScheme: legacySubject.scheme,
|
|
146
|
+
subjectDigest: legacySubject.digest,
|
|
147
|
+
}, compromisedIds);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
135
150
|
return {
|
|
136
151
|
commit,
|
|
137
152
|
blueprintPath: overview.path,
|
|
@@ -3,11 +3,7 @@ import path from "node:path";
|
|
|
3
3
|
import { NotInGitRepoError, withLock } from "#paths/state-root.js";
|
|
4
4
|
import { writeJsonFileAtomic } from "#shared-utils/write-json-file.js";
|
|
5
5
|
export const REVIEW_AVAILABILITY_SCHEMA_VERSION = 1;
|
|
6
|
-
const FAILURE_CLASSIFICATIONS = new Set([
|
|
7
|
-
"provider-limit-reached",
|
|
8
|
-
"provider-unavailable",
|
|
9
|
-
"provider-failed",
|
|
10
|
-
]);
|
|
6
|
+
const FAILURE_CLASSIFICATIONS = new Set(["provider-limit-reached", "provider-unavailable"]);
|
|
11
7
|
const SUCCESS_CLASSIFICATIONS = new Set([
|
|
12
8
|
"complete",
|
|
13
9
|
"approved",
|
|
@@ -117,6 +113,10 @@ export function saveReviewAvailability(projectRoot, state) {
|
|
|
117
113
|
function shouldSuppress(event) {
|
|
118
114
|
return FAILURE_CLASSIFICATIONS.has(event.terminalClassification);
|
|
119
115
|
}
|
|
116
|
+
function suppressedRecordStillApplies(record) {
|
|
117
|
+
return (record.terminalClassification !== undefined &&
|
|
118
|
+
FAILURE_CLASSIFICATIONS.has(record.terminalClassification));
|
|
119
|
+
}
|
|
120
120
|
function isAvailabilitySuccess(event) {
|
|
121
121
|
return SUCCESS_CLASSIFICATIONS.has(event.terminalClassification);
|
|
122
122
|
}
|
|
@@ -155,6 +155,15 @@ function nextRecord(previous, event, input) {
|
|
|
155
155
|
const success = isAvailabilitySuccess(event);
|
|
156
156
|
if (!input.suppress) {
|
|
157
157
|
const preserveSuppression = !success && previous?.suppressed === true;
|
|
158
|
+
if (preserveSuppression) {
|
|
159
|
+
return {
|
|
160
|
+
...previous,
|
|
161
|
+
...(event.model ? { lastModel: event.model } : {}),
|
|
162
|
+
...(event.effort ? { lastEffort: event.effort } : {}),
|
|
163
|
+
...(event.stage ? { lastStage: event.stage } : {}),
|
|
164
|
+
suppressed: true,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
158
167
|
return {
|
|
159
168
|
...common,
|
|
160
169
|
...(success
|
|
@@ -244,6 +253,10 @@ export function suppressedReviewers(state) {
|
|
|
244
253
|
export function pruneExpiredAvailabilitySuppressions(state, nowMs = Date.now()) {
|
|
245
254
|
const pruned = [];
|
|
246
255
|
const pruneMap = (records) => Object.fromEntries(Object.entries(records).map(([key, record]) => {
|
|
256
|
+
if (record.suppressed && !suppressedRecordStillApplies(record)) {
|
|
257
|
+
pruned.push(key);
|
|
258
|
+
return [key, { ...record, suppressed: false }];
|
|
259
|
+
}
|
|
247
260
|
if (record.suppressed &&
|
|
248
261
|
record.retryAfterMs !== undefined &&
|
|
249
262
|
record.lastFailureAt !== undefined) {
|
|
@@ -18,7 +18,22 @@ declare function updateField(hash: Hash, value: Buffer | string): void;
|
|
|
18
18
|
export declare function createDeliverySubjectAtRef(cwd: string, ref: string, blueprintSlug: string, blueprintPath?: string): ReviewSubject;
|
|
19
19
|
export declare function createSourceSubjectAtRef(cwd: string, ref: string): ReviewSubject;
|
|
20
20
|
export declare function normalizePlanSubjectContent(content: Buffer | string): Buffer;
|
|
21
|
+
/**
|
|
22
|
+
* Plan-subject digest for in-memory (working-tree) blueprint content. Used to
|
|
23
|
+
* accept working-ledger review events that digest-bind the exact bytes being
|
|
24
|
+
* committed (pre-commit runs before those events reach any git ref).
|
|
25
|
+
*/
|
|
26
|
+
export declare function createPlanSubjectDigestForContent(blueprintSlug: string, content: Buffer | string): string;
|
|
27
|
+
/**
|
|
28
|
+
* Digest-compatibility variant for review events recorded before the
|
|
29
|
+
* stripped-key extension of `neutralizeLifecycleFrontmatter`. Verification
|
|
30
|
+
* accepts a match against either normalization; new digests always use
|
|
31
|
+
* `normalizePlanSubjectContent`.
|
|
32
|
+
*/
|
|
33
|
+
export declare function legacyNormalizePlanSubjectContent(content: Buffer | string): Buffer;
|
|
21
34
|
export declare function createPlanSubjectAtRef(cwd: string, ref: string, blueprintSlug: string, blueprintPath?: string): ReviewSubject;
|
|
35
|
+
/** Plan subject digested with the pre-extension normalization (see above). */
|
|
36
|
+
export declare function createLegacyPlanSubjectAtRef(cwd: string, ref: string, blueprintSlug: string, blueprintPath?: string): ReviewSubject;
|
|
22
37
|
export declare const reviewSubjectInternals: {
|
|
23
38
|
isExcludedReviewEnvelope: typeof isExcludedReviewEnvelope;
|
|
24
39
|
isReviewEvidencePath: typeof isReviewEvidencePath;
|
|
@@ -255,12 +255,39 @@ function neutralizePromotionGateResults(text) {
|
|
|
255
255
|
}
|
|
256
256
|
return lines.join("\n");
|
|
257
257
|
}
|
|
258
|
-
//
|
|
259
|
-
//
|
|
258
|
+
// Frontmatter keys removed wholesale by the normalizer (value + folded
|
|
259
|
+
// continuation lines). Each mirrors exactly one lifecycle writer:
|
|
260
|
+
// `completed_at` (upsertCompletedAt) and the `wp blueprint start` /
|
|
261
|
+
// progress-stamp fields (setBlueprintFrontmatterFields lifecycle intents).
|
|
262
|
+
const PLAN_LIFECYCLE_STRIPPED_KEYS = [
|
|
263
|
+
"completed_at",
|
|
264
|
+
"last_updated",
|
|
265
|
+
"progress",
|
|
266
|
+
"worktree_owner_id",
|
|
267
|
+
"worktree_owner_branch",
|
|
268
|
+
];
|
|
269
|
+
// Neutralize the lifecycle-owned frontmatter fields (`status:` value plus the
|
|
270
|
+
// stripped keys above) INSIDE the leading frontmatter block only. A body line
|
|
260
271
|
// that merely looks like one of these (e.g. task text reading
|
|
261
272
|
// "completed_at: must remain unset until release") is plan substance and must
|
|
262
|
-
// keep affecting the digest.
|
|
273
|
+
// keep affecting the digest. Stripping removes the key line and any indented
|
|
274
|
+
// continuation lines so folded (`>-`) values normalize away too.
|
|
263
275
|
function neutralizeLifecycleFrontmatter(text) {
|
|
276
|
+
const match = /^---\r?\n[\s\S]*?\r?\n---(?:\r?\n|$)/u.exec(text);
|
|
277
|
+
if (!match)
|
|
278
|
+
return text;
|
|
279
|
+
let frontmatter = match[0];
|
|
280
|
+
frontmatter = frontmatter.replace(/^(status:\s*)(['"]?)[^'"\r\n]+?(['"]?)(\s*)$/m, `$1${PLAN_STATUS_PLACEHOLDER}$4`);
|
|
281
|
+
for (const key of PLAN_LIFECYCLE_STRIPPED_KEYS) {
|
|
282
|
+
frontmatter = frontmatter.replace(new RegExp(String.raw `^${key}:[^\n]*\n?(?:[ \t]+[^\n]*\n?)*`, "gmu"), "");
|
|
283
|
+
}
|
|
284
|
+
return `${frontmatter}${text.slice(match[0].length)}`;
|
|
285
|
+
}
|
|
286
|
+
// The pre-extension normalizer (status + completed_at only). Review events
|
|
287
|
+
// recorded before the stripped-key extension bound their subjectDigest to this
|
|
288
|
+
// normalization; verification accepts either digest so committed approvals are
|
|
289
|
+
// not stranded by the rollout. New events always record the current digest.
|
|
290
|
+
function legacyNeutralizeLifecycleFrontmatter(text) {
|
|
264
291
|
const match = /^---\r?\n[\s\S]*?\r?\n---(?:\r?\n|$)/u.exec(text);
|
|
265
292
|
if (!match)
|
|
266
293
|
return text;
|
|
@@ -305,21 +332,85 @@ function neutralizeReadinessStamps(text) {
|
|
|
305
332
|
// NO_CODE_CHANGE / GitLab patch-id semantics: approvals are re-derivable from
|
|
306
333
|
// substance-only digests, not carried). Each rule mirrors exactly one
|
|
307
334
|
// lifecycle writer, scoped to the location that writer owns:
|
|
308
|
-
// - frontmatter
|
|
309
|
-
// - frontmatter `completed_at:` line
|
|
335
|
+
// - frontmatter status/progress/owner fields (blueprint lifecycle mutations)
|
|
336
|
+
// - frontmatter `completed_at:` line (upsertCompletedAt)
|
|
310
337
|
// - Readiness Verdict `- verified-at:`/`- verified-head:` items (upsertReadinessValue)
|
|
311
338
|
// - Promotion Gates "Last result" cells (updateGateLastResult)
|
|
312
339
|
// Any other byte change — task text, acceptance criteria, scope, lookalike
|
|
313
340
|
// lines outside the owned locations — still changes the digest and dismisses
|
|
314
341
|
// prior plan approvals.
|
|
342
|
+
const PLAN_TASK_STATUS_PLACEHOLDER = "__wp-task-status__";
|
|
343
|
+
// Neutralize the task-execution state the lifecycle engine writes
|
|
344
|
+
// (updateTaskStatus, applyVerification): `**Status:** <status>` lines,
|
|
345
|
+
// acceptance checkbox marks, and `**Verification:**` +
|
|
346
|
+
// ```webpresso-evidence-v1 fenced evidence blocks. Task execution is lifecycle
|
|
347
|
+
// state, not plan substance — without this, the engine's own task_verify /
|
|
348
|
+
// finalize writes dismiss the plan approval and no completion commit can ever
|
|
349
|
+
// carry authority. Acceptance-criteria TEXT stays substance; only the check
|
|
350
|
+
// mark is neutralized.
|
|
351
|
+
function neutralizeTaskExecutionState(text) {
|
|
352
|
+
const lines = text.split("\n");
|
|
353
|
+
const out = [];
|
|
354
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
355
|
+
const line = lines[index];
|
|
356
|
+
if (/^\*\*Verification:\*\*\s*$/u.test(line)) {
|
|
357
|
+
// Skip the writer-owned block: "**Verification:**", blank(s), the
|
|
358
|
+
// evidence fence, and one trailing blank line.
|
|
359
|
+
let cursor = index + 1;
|
|
360
|
+
while (cursor < lines.length && lines[cursor].trim() === "")
|
|
361
|
+
cursor += 1;
|
|
362
|
+
if (/^```webpresso-evidence-v1\s*$/u.test(lines[cursor] ?? "")) {
|
|
363
|
+
cursor += 1;
|
|
364
|
+
while (cursor < lines.length && !/^```\s*$/u.test(lines[cursor]))
|
|
365
|
+
cursor += 1;
|
|
366
|
+
cursor += 1; // closing fence
|
|
367
|
+
if ((lines[cursor] ?? "").trim() === "")
|
|
368
|
+
cursor += 1;
|
|
369
|
+
index = cursor - 1;
|
|
370
|
+
continue;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
if (/^\*\*Status:\*\* (?:todo|in-progress|blocked|done|dropped)\s*$/u.test(line)) {
|
|
374
|
+
out.push(`**Status:** ${PLAN_TASK_STATUS_PLACEHOLDER}`);
|
|
375
|
+
continue;
|
|
376
|
+
}
|
|
377
|
+
out.push(line.replace(/^(\s*)- \[[xX ]\] /u, "$1- [_] "));
|
|
378
|
+
}
|
|
379
|
+
return out.join("\n");
|
|
380
|
+
}
|
|
315
381
|
export function normalizePlanSubjectContent(content) {
|
|
316
382
|
let text = typeof content === "string" ? content : content.toString("utf8");
|
|
317
383
|
text = neutralizeLifecycleFrontmatter(text);
|
|
318
384
|
text = neutralizeReadinessStamps(text);
|
|
319
385
|
text = neutralizePromotionGateResults(text);
|
|
386
|
+
text = neutralizeTaskExecutionState(text);
|
|
320
387
|
return Buffer.from(text, "utf8");
|
|
321
388
|
}
|
|
322
|
-
|
|
389
|
+
/**
|
|
390
|
+
* Plan-subject digest for in-memory (working-tree) blueprint content. Used to
|
|
391
|
+
* accept working-ledger review events that digest-bind the exact bytes being
|
|
392
|
+
* committed (pre-commit runs before those events reach any git ref).
|
|
393
|
+
*/
|
|
394
|
+
export function createPlanSubjectDigestForContent(blueprintSlug, content) {
|
|
395
|
+
const hash = createHash("sha256").update(PLAN_DOMAIN);
|
|
396
|
+
updateField(hash, blueprintSlug);
|
|
397
|
+
updateField(hash, normalizePlanSubjectContent(content));
|
|
398
|
+
return hash.digest("hex");
|
|
399
|
+
}
|
|
400
|
+
/**
|
|
401
|
+
* Digest-compatibility variant for review events recorded before the
|
|
402
|
+
* stripped-key extension of `neutralizeLifecycleFrontmatter`. Verification
|
|
403
|
+
* accepts a match against either normalization; new digests always use
|
|
404
|
+
* `normalizePlanSubjectContent`.
|
|
405
|
+
*/
|
|
406
|
+
export function legacyNormalizePlanSubjectContent(content) {
|
|
407
|
+
let text = typeof content === "string" ? content : content.toString("utf8");
|
|
408
|
+
text = legacyNeutralizeLifecycleFrontmatter(text);
|
|
409
|
+
text = neutralizeReadinessStamps(text);
|
|
410
|
+
text = neutralizePromotionGateResults(text);
|
|
411
|
+
return Buffer.from(text, "utf8");
|
|
412
|
+
}
|
|
413
|
+
function planSubjectAtRef(cwd, ref, blueprintSlug, normalize, blueprintPath) {
|
|
323
414
|
const commit = resolveReviewCommit(cwd, ref);
|
|
324
415
|
const entries = parseTree(cwd, commit);
|
|
325
416
|
const blueprintRoot = findBlueprintRoot(entries, blueprintSlug, blueprintPath);
|
|
@@ -333,10 +424,7 @@ export function createPlanSubjectAtRef(cwd, ref, blueprintSlug, blueprintPath) {
|
|
|
333
424
|
throw new Error("Tracked blueprint overview content is unavailable.");
|
|
334
425
|
const hash = createHash("sha256").update(PLAN_DOMAIN);
|
|
335
426
|
updateField(hash, blueprintSlug);
|
|
336
|
-
|
|
337
|
-
// safe pre-merge: no committed review-events.jsonl exists anywhere, so no
|
|
338
|
-
// recorded plan digest can break.
|
|
339
|
-
updateField(hash, normalizePlanSubjectContent(content));
|
|
427
|
+
updateField(hash, normalize(content));
|
|
340
428
|
return {
|
|
341
429
|
scheme: "wp-blueprint-v1",
|
|
342
430
|
digest: hash.digest("hex"),
|
|
@@ -344,6 +432,13 @@ export function createPlanSubjectAtRef(cwd, ref, blueprintSlug, blueprintPath) {
|
|
|
344
432
|
files: [overviewPath.toString("utf8")],
|
|
345
433
|
};
|
|
346
434
|
}
|
|
435
|
+
export function createPlanSubjectAtRef(cwd, ref, blueprintSlug, blueprintPath) {
|
|
436
|
+
return planSubjectAtRef(cwd, ref, blueprintSlug, normalizePlanSubjectContent, blueprintPath);
|
|
437
|
+
}
|
|
438
|
+
/** Plan subject digested with the pre-extension normalization (see above). */
|
|
439
|
+
export function createLegacyPlanSubjectAtRef(cwd, ref, blueprintSlug, blueprintPath) {
|
|
440
|
+
return planSubjectAtRef(cwd, ref, blueprintSlug, legacyNormalizePlanSubjectContent, blueprintPath);
|
|
441
|
+
}
|
|
347
442
|
export const reviewSubjectInternals = {
|
|
348
443
|
isExcludedReviewEnvelope,
|
|
349
444
|
isReviewEvidencePath,
|
|
@@ -14,6 +14,7 @@ export interface ResolveRuntimeEnvironmentOptions {
|
|
|
14
14
|
}
|
|
15
15
|
export interface RuntimeSpawnOptions {
|
|
16
16
|
readonly cwd?: string;
|
|
17
|
+
readonly envCwd?: string;
|
|
17
18
|
readonly profile?: RuntimeSelector;
|
|
18
19
|
readonly secretProfile?: string;
|
|
19
20
|
readonly environment?: string;
|
|
@@ -82,9 +82,10 @@ export function resolveRuntimeEnvironment(options = {}) {
|
|
|
82
82
|
}
|
|
83
83
|
export function buildRuntimeSpawnOptions(options = {}) {
|
|
84
84
|
const cwd = options.cwd ?? process.cwd();
|
|
85
|
+
const envCwd = options.envCwd ?? cwd;
|
|
85
86
|
const cache = options.cache ?? createRuntimeEnvCache();
|
|
86
87
|
const resolvedEnv = resolveRuntimeEnvironment({
|
|
87
|
-
cwd,
|
|
88
|
+
cwd: envCwd,
|
|
88
89
|
profile: options.profile,
|
|
89
90
|
secretProfile: options.secretProfile,
|
|
90
91
|
environment: options.environment,
|
|
@@ -11,6 +11,7 @@ export interface SecretGateCommandOptions {
|
|
|
11
11
|
readonly command: string;
|
|
12
12
|
readonly args?: readonly string[];
|
|
13
13
|
readonly cwd?: string;
|
|
14
|
+
readonly commandCwd?: string;
|
|
14
15
|
readonly timeoutMs?: number;
|
|
15
16
|
readonly signal?: AbortSignal;
|
|
16
17
|
readonly forceSecretGate?: boolean;
|
|
@@ -35,6 +35,7 @@ export function buildSecretGateCommand(options) {
|
|
|
35
35
|
sink,
|
|
36
36
|
"--profile",
|
|
37
37
|
profile,
|
|
38
|
+
...(options.commandCwd ? ["--command-cwd", options.commandCwd] : []),
|
|
38
39
|
"--",
|
|
39
40
|
options.command,
|
|
40
41
|
...(options.args ?? []),
|
|
@@ -45,9 +46,11 @@ export function runSecretGateCommand(options) {
|
|
|
45
46
|
const timeoutMs = options.timeoutMs ?? 30_000;
|
|
46
47
|
const maxOutputBytes = options.maxOutputBytes ?? DEFAULT_MAX_OUTPUT_BYTES;
|
|
47
48
|
const command = buildSecretGateCommand(options);
|
|
49
|
+
const runsInnerCommandDirectly = command.command === options.command;
|
|
50
|
+
const spawnCwd = runsInnerCommandDirectly && options.commandCwd ? options.commandCwd : options.cwd;
|
|
48
51
|
return new Promise((resolve) => {
|
|
49
52
|
const child = spawn(command.command, [...command.args], {
|
|
50
|
-
cwd:
|
|
53
|
+
cwd: spawnCwd,
|
|
51
54
|
env: buildRuntimeProcessEnv(options.cwd, process.env),
|
|
52
55
|
detached: process.platform !== "win32",
|
|
53
56
|
});
|