opencode-workflow-guard 1.18.1 → 1.19.0
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/README.md +4 -4
- package/docs/installation.md +23 -0
- package/docs/policies.md +20 -0
- package/docs/testing.md +1 -0
- package/package.json +2 -2
- package/src/lib/audit.ts +52 -2
- package/src/lib/custom-tools.ts +154 -3
- package/src/lib/dogfood.ts +147 -0
- package/src/lib/evidence.ts +19 -1
- package/src/lib/guard-dispatcher.ts +30 -0
- package/src/lib/state.ts +221 -1
- package/src/lib/types.ts +64 -1
- package/src/lib/v2-plugin.ts +12 -1
- package/src/workflow-guard.ts +29 -2
- package/test/test.mts +626 -0
package/test/test.mts
CHANGED
|
@@ -55,10 +55,24 @@ import {
|
|
|
55
55
|
summarizeInput,
|
|
56
56
|
extractReviewFollowups,
|
|
57
57
|
buildReviewRubric,
|
|
58
|
+
buildWorkflowGuardSystemGuidance,
|
|
58
59
|
recordReviewResult,
|
|
59
60
|
getLastReviewResult,
|
|
61
|
+
recordDogfoodResult,
|
|
62
|
+
getLastDogfoodResultForWorkspace,
|
|
63
|
+
getDogfoodRecordsForWorkspace,
|
|
64
|
+
resetDogfoodState,
|
|
65
|
+
isDogfoodRequired,
|
|
66
|
+
getDogfoodDeployMatrix,
|
|
67
|
+
getDogfoodDepth,
|
|
68
|
+
isDogfoodActor,
|
|
69
|
+
evaluateDogfoodGate,
|
|
70
|
+
getDogfoodCacheFilePath,
|
|
71
|
+
persistDogfoodCache,
|
|
72
|
+
loadDogfoodCache,
|
|
60
73
|
verificationEvidence,
|
|
61
74
|
reviewEvidence,
|
|
75
|
+
dogfoodEvidence,
|
|
62
76
|
toolOutcomeEvidence,
|
|
63
77
|
policyDecisionEvidence,
|
|
64
78
|
lifecycleStateEvidence,
|
|
@@ -207,6 +221,10 @@ const call = (
|
|
|
207
221
|
) => guardToolCall(toolName, input, context);
|
|
208
222
|
const shell = (cmd: string) => call("bash", { command: cmd });
|
|
209
223
|
const blocked = (r: unknown): boolean => typeof r === "string";
|
|
224
|
+
// The guard's allow path returns undefined and its block path returns a string
|
|
225
|
+
// (see guardToolCall). Asserting exactly undefined keeps a stray object, such
|
|
226
|
+
// as an unrelated internal error, from reading as an allow.
|
|
227
|
+
const allowed = (r: unknown): boolean => r === undefined;
|
|
210
228
|
|
|
211
229
|
console.log("- Policy 1: native todo gate -");
|
|
212
230
|
todo("s-empty");
|
|
@@ -2888,6 +2906,614 @@ setWorkspaceRoot(root);
|
|
|
2888
2906
|
reloadProjectConfig(root);
|
|
2889
2907
|
resetReviewState();
|
|
2890
2908
|
|
|
2909
|
+
// Dogfood gate: off by default, blocks when opted in without fresh evidence,
|
|
2910
|
+
// refuses self-certification from an actor session, and never caches a HOLD.
|
|
2911
|
+
console.log("- Dogfood gate (pre-deployment evidence) -");
|
|
2912
|
+
const dogfoodPlugin = await (defaultExport?.server ?? WorkflowGuard)({
|
|
2913
|
+
directory: root,
|
|
2914
|
+
worktree: root,
|
|
2915
|
+
client: fakeClient as any,
|
|
2916
|
+
project: {} as any,
|
|
2917
|
+
experimental_workspace: {} as any,
|
|
2918
|
+
serverUrl: new URL("http://localhost:4096"),
|
|
2919
|
+
$: undefined as any,
|
|
2920
|
+
});
|
|
2921
|
+
const dogfoodRepo = mkdtempSync(join(tmpdir(), "wg-dogfood-"));
|
|
2922
|
+
spawnSync("git", ["init", "-b", "feature/dogfood"], { cwd: dogfoodRepo });
|
|
2923
|
+
spawnSync("git", ["config", "user.email", "test@test.local"], { cwd: dogfoodRepo });
|
|
2924
|
+
spawnSync("git", ["config", "user.name", "Test Runner"], { cwd: dogfoodRepo });
|
|
2925
|
+
mkdirSync(join(dogfoodRepo, ".opencode"), { recursive: true });
|
|
2926
|
+
writeFileSync(join(dogfoodRepo, "code.txt"), "shipped\n");
|
|
2927
|
+
spawnSync("git", ["add", "code.txt"], { cwd: dogfoodRepo });
|
|
2928
|
+
spawnSync("git", ["commit", "-m", "base"], { cwd: dogfoodRepo });
|
|
2929
|
+
setWorkspaceRoot(dogfoodRepo);
|
|
2930
|
+
reloadProjectConfig(dogfoodRepo);
|
|
2931
|
+
resetReviewState();
|
|
2932
|
+
resetDogfoodState();
|
|
2933
|
+
process.env.WORKFLOW_GUARD_REQUIRE_REVIEW = "0";
|
|
2934
|
+
|
|
2935
|
+
check("dogfood gate is off by default", isDogfoodRequired(dogfoodRepo) === false);
|
|
2936
|
+
check(
|
|
2937
|
+
"PR creation allowed without dogfood evidence when the gate is off",
|
|
2938
|
+
allowed(await shell("gh pr create --title t --body 'Changelog: update'")),
|
|
2939
|
+
);
|
|
2940
|
+
|
|
2941
|
+
writeFileSync(join(dogfoodRepo, ".opencode", "workflow-guard.json"), JSON.stringify({ requireReview: false, requireDogfood: true }));
|
|
2942
|
+
reloadProjectConfig(dogfoodRepo);
|
|
2943
|
+
check("requireDogfood: true enables the gate", isDogfoodRequired(dogfoodRepo) === true);
|
|
2944
|
+
check(
|
|
2945
|
+
"PR creation blocked when the dogfood gate is on and no run is recorded",
|
|
2946
|
+
blocked(await shell("gh pr create --title t --body 'Changelog: update'")),
|
|
2947
|
+
);
|
|
2948
|
+
const dogfoodBlockedStatus = JSON.parse(String(await dogfoodPlugin.tool?.guard_status?.execute({ directory: dogfoodRepo }, { sessionID: "s-dogfood", worktree: dogfoodRepo, directory: dogfoodRepo } as any)));
|
|
2949
|
+
check(
|
|
2950
|
+
"guard_status reports dogfood as outstanding work",
|
|
2951
|
+
dogfoodBlockedStatus.outstandingRequirements.includes("dogfood") && dogfoodBlockedStatus.projectConfig.requireDogfood === true,
|
|
2952
|
+
);
|
|
2953
|
+
|
|
2954
|
+
check("dogfood gate treats an unset deploy matrix as this host alone", getDogfoodDeployMatrix(dogfoodRepo).length === 0);
|
|
2955
|
+
writeFileSync(join(dogfoodRepo, ".opencode", "workflow-guard.json"), JSON.stringify({ requireReview: false, requireDogfood: true, dogfoodDeployMatrix: [" bluefin ", "bluefin", ""] }));
|
|
2956
|
+
reloadProjectConfig(dogfoodRepo);
|
|
2957
|
+
check("dogfood deploy matrix trims and de-duplicates host labels", JSON.stringify(getDogfoodDeployMatrix(dogfoodRepo)) === JSON.stringify(["bluefin"]));
|
|
2958
|
+
|
|
2959
|
+
// A HOLD is recorded in memory but never durable, and never satisfies the gate.
|
|
2960
|
+
const heldDogfood = recordDogfoodResult({
|
|
2961
|
+
verdict: "hold",
|
|
2962
|
+
scenarioId: "permission-rule-reorder",
|
|
2963
|
+
scenarioSha256: "a".repeat(64),
|
|
2964
|
+
taxonomySha256: "b".repeat(64),
|
|
2965
|
+
taxonomyClass: "permission-bypass",
|
|
2966
|
+
checks: [{ id: "exit-code", status: "pass" }],
|
|
2967
|
+
runCount: 1,
|
|
2968
|
+
host: { label: "bluefin" },
|
|
2969
|
+
workspace: dogfoodRepo,
|
|
2970
|
+
}, "s-dogfood");
|
|
2971
|
+
check("a HOLD verdict does not cache durable evidence", heldDogfood.verdict === "hold" && !existsSync(getDogfoodCacheFilePath()));
|
|
2972
|
+
check("a HOLD verdict leaves the gate unsatisfied", evaluateDogfoodGate(dogfoodRepo).missingHosts.length > 0);
|
|
2973
|
+
|
|
2974
|
+
const passedDogfood = recordDogfoodResult({
|
|
2975
|
+
verdict: "pass",
|
|
2976
|
+
scenarioId: "permission-rule-reorder",
|
|
2977
|
+
scenarioSha256: "a".repeat(64),
|
|
2978
|
+
taxonomySha256: "b".repeat(64),
|
|
2979
|
+
checks: [{ id: "exit-code", status: "pass" }],
|
|
2980
|
+
runCount: 1,
|
|
2981
|
+
host: { label: "bluefin" },
|
|
2982
|
+
workspace: dogfoodRepo,
|
|
2983
|
+
}, "s-dogfood");
|
|
2984
|
+
check("a PASS verdict caches durable evidence", passedDogfood.verdict === "pass" && existsSync(getDogfoodCacheFilePath()));
|
|
2985
|
+
check("a PASS verdict satisfies the gate for its own worktree", evaluateDogfoodGate(dogfoodRepo).missingHosts.length === 0 && evaluateDogfoodGate(dogfoodRepo).matched?.scenarioId === "permission-rule-reorder");
|
|
2986
|
+
check(
|
|
2987
|
+
"PR creation allowed once a passing run is recorded at the current tree",
|
|
2988
|
+
allowed(await shell("gh pr create --title t --body 'Changelog: update'")),
|
|
2989
|
+
);
|
|
2990
|
+
|
|
2991
|
+
// The evidence binds to tracked content: a tracked edit invalidates it.
|
|
2992
|
+
writeFileSync(join(dogfoodRepo, "code.txt"), "changed after the run\n");
|
|
2993
|
+
const staleDogfoodGate = evaluateDogfoodGate(dogfoodRepo);
|
|
2994
|
+
check(
|
|
2995
|
+
"a tracked edit after the run invalidates the evidence with a cause",
|
|
2996
|
+
staleDogfoodGate.missingHosts.length > 0 && /does not match the current worktree contents/.test(staleDogfoodGate.reason ?? ""),
|
|
2997
|
+
);
|
|
2998
|
+
check(
|
|
2999
|
+
"PR creation blocked again after the run's tree changed",
|
|
3000
|
+
blocked(await shell("gh pr create --title t --body 'Changelog: update'")),
|
|
3001
|
+
);
|
|
3002
|
+
spawnSync("git", ["checkout", "--", "code.txt"], { cwd: dogfoodRepo });
|
|
3003
|
+
spawnSync("git", ["commit", "--allow-empty", "-m", "empty commit after run"], { cwd: dogfoodRepo });
|
|
3004
|
+
const commitDriftGate = evaluateDogfoodGate(dogfoodRepo);
|
|
3005
|
+
check(
|
|
3006
|
+
"a new commit after the run is named as commit drift",
|
|
3007
|
+
commitDriftGate.missingHosts.length > 0 && /bound to commit/.test(commitDriftGate.reason ?? ""),
|
|
3008
|
+
);
|
|
3009
|
+
|
|
3010
|
+
// Deploy matrix: a pass on a host outside the matrix does not satisfy it.
|
|
3011
|
+
writeFileSync(join(dogfoodRepo, ".opencode", "workflow-guard.json"), JSON.stringify({ requireReview: false, requireDogfood: true, dogfoodDeployMatrix: ["bluefin", "windows-ci"] }));
|
|
3012
|
+
reloadProjectConfig(dogfoodRepo);
|
|
3013
|
+
const matrixGate = evaluateDogfoodGate(dogfoodRepo);
|
|
3014
|
+
check(
|
|
3015
|
+
"deploy matrix hosts beyond the recorded host stay unsatisfied",
|
|
3016
|
+
matrixGate.missingHosts.includes("bluefin") && matrixGate.missingHosts.includes("windows-ci"),
|
|
3017
|
+
);
|
|
3018
|
+
|
|
3019
|
+
// A second host's record satisfies the rest of the matrix, because records
|
|
3020
|
+
// accumulate instead of replacing each other. Both hosts re-run here at the
|
|
3021
|
+
// current commit, since the earlier bluefin record predates the empty commit.
|
|
3022
|
+
await dogfoodPlugin.tool?.record_dogfood?.execute(
|
|
3023
|
+
{ scenarioId: "permission-rule-reorder", scenarioSha256: "c".repeat(64), taxonomySha256: "d".repeat(64), verdict: "pass", checks: JSON.stringify([{ id: "exit-code", status: "pass" }]), hostLabel: "bluefin", hostOs: "linux", directory: dogfoodRepo },
|
|
3024
|
+
{ sessionID: "s-dogfood", worktree: dogfoodRepo, directory: dogfoodRepo } as any,
|
|
3025
|
+
);
|
|
3026
|
+
const windowsRecord = await dogfoodPlugin.tool?.record_dogfood?.execute(
|
|
3027
|
+
{ scenarioId: "permission-rule-reorder", scenarioSha256: "c".repeat(64), taxonomySha256: "d".repeat(64), verdict: "pass", checks: JSON.stringify([{ id: "exit-code", status: "pass" }]), hostLabel: "windows-ci", hostOs: "windows", directory: dogfoodRepo },
|
|
3028
|
+
{ sessionID: "s-dogfood", worktree: dogfoodRepo, directory: dogfoodRepo } as any,
|
|
3029
|
+
);
|
|
3030
|
+
check("a second matrix host can record its own pass", typeof windowsRecord === "string" && windowsRecord.includes("PASS") && windowsRecord.includes("windows-ci"));
|
|
3031
|
+
const satisfiedMatrix = evaluateDogfoodGate(dogfoodRepo);
|
|
3032
|
+
check(
|
|
3033
|
+
"a multi-host deploy matrix is satisfiable when every host has a fresh pass",
|
|
3034
|
+
satisfiedMatrix.missingHosts.length === 0 && (satisfiedMatrix.matchedRecords ?? []).length === 2,
|
|
3035
|
+
);
|
|
3036
|
+
const cachedPasses = loadDogfoodCache();
|
|
3037
|
+
check(
|
|
3038
|
+
"the durable cache keeps passing records for every matrix host",
|
|
3039
|
+
Array.isArray(cachedPasses) && cachedPasses.every((record) => record.verdict === "pass") && ["bluefin", "windows-ci"].every((host) => cachedPasses.some((record) => record.host.label === host)),
|
|
3040
|
+
);
|
|
3041
|
+
// The PR audit entry must name every host the matrix covered, not just the
|
|
3042
|
+
// newest record, or an auditor would read a multi-host pass as a single-host one.
|
|
3043
|
+
const matrixPr = await shell("gh pr create --title t --body 'Changelog: update'");
|
|
3044
|
+
check("a satisfied multi-host matrix allows PR creation", allowed(matrixPr));
|
|
3045
|
+
const matrixPrAudit = getRecentAuditEntries(500).filter(
|
|
3046
|
+
(entry) => entry.reason === "dogfood_evidence_matched" && (entry.evidence?.dogfoodCoveredHosts ?? []).length > 1,
|
|
3047
|
+
);
|
|
3048
|
+
check(
|
|
3049
|
+
"the PR preflight journals every host the dogfood matrix covered",
|
|
3050
|
+
matrixPrAudit.length > 0 &&
|
|
3051
|
+
JSON.stringify([...(matrixPrAudit.pop()?.evidence?.dogfoodCoveredHosts ?? [])].sort()) === JSON.stringify(["bluefin", "windows-ci"]),
|
|
3052
|
+
);
|
|
3053
|
+
|
|
3054
|
+
// A newer HOLD on a host supersedes that host's older pass: a regression the
|
|
3055
|
+
// host later found must not be hidden behind the earlier green run.
|
|
3056
|
+
recordDogfoodResult({
|
|
3057
|
+
verdict: "hold",
|
|
3058
|
+
scenarioId: "permission-rule-reorder",
|
|
3059
|
+
scenarioSha256: "c".repeat(64),
|
|
3060
|
+
taxonomySha256: "d".repeat(64),
|
|
3061
|
+
taxonomyClass: "permission-bypass",
|
|
3062
|
+
checks: [{ id: "exit-code", status: "hold" }],
|
|
3063
|
+
runCount: 1,
|
|
3064
|
+
host: { label: "windows-ci" },
|
|
3065
|
+
workspace: dogfoodRepo,
|
|
3066
|
+
}, "s-dogfood");
|
|
3067
|
+
const regressedGate = evaluateDogfoodGate(dogfoodRepo);
|
|
3068
|
+
check(
|
|
3069
|
+
"a newer HOLD on one host reopens the gate with its taxonomy class",
|
|
3070
|
+
regressedGate.missingHosts.includes("windows-ci") && /HOLD with class 'permission-bypass'/.test(regressedGate.reason ?? ""),
|
|
3071
|
+
);
|
|
3072
|
+
check(
|
|
3073
|
+
"a HOLD on one host does not discard another host's cached pass",
|
|
3074
|
+
loadDogfoodCache().some((record) => record.host.label === "bluefin") && !loadDogfoodCache().some((record) => record.verdict === "hold"),
|
|
3075
|
+
);
|
|
3076
|
+
|
|
3077
|
+
// A later PASS on a different host must not resurrect a pass that a newer HOLD
|
|
3078
|
+
// already superseded. The HOLD is never written to disk, so re-persisting every
|
|
3079
|
+
// pass would leave the old green run behind and a restart would serve it as
|
|
3080
|
+
// evidence for the held host.
|
|
3081
|
+
resetDogfoodState();
|
|
3082
|
+
recordDogfoodResult({
|
|
3083
|
+
verdict: "pass",
|
|
3084
|
+
scenarioId: "permission-rule-reorder",
|
|
3085
|
+
scenarioSha256: "c".repeat(64),
|
|
3086
|
+
taxonomySha256: "d".repeat(64),
|
|
3087
|
+
checks: [{ id: "exit-code", status: "pass" }],
|
|
3088
|
+
runCount: 1,
|
|
3089
|
+
host: { label: "bluefin" },
|
|
3090
|
+
workspace: dogfoodRepo,
|
|
3091
|
+
}, "s-dogfood");
|
|
3092
|
+
recordDogfoodResult({
|
|
3093
|
+
verdict: "hold",
|
|
3094
|
+
scenarioId: "permission-rule-reorder",
|
|
3095
|
+
scenarioSha256: "c".repeat(64),
|
|
3096
|
+
taxonomySha256: "d".repeat(64),
|
|
3097
|
+
taxonomyClass: "permission-bypass",
|
|
3098
|
+
checks: [{ id: "exit-code", status: "hold" }],
|
|
3099
|
+
runCount: 1,
|
|
3100
|
+
host: { label: "bluefin" },
|
|
3101
|
+
workspace: dogfoodRepo,
|
|
3102
|
+
}, "s-dogfood");
|
|
3103
|
+
recordDogfoodResult({
|
|
3104
|
+
verdict: "pass",
|
|
3105
|
+
scenarioId: "permission-rule-reorder",
|
|
3106
|
+
scenarioSha256: "c".repeat(64),
|
|
3107
|
+
taxonomySha256: "d".repeat(64),
|
|
3108
|
+
checks: [{ id: "exit-code", status: "pass" }],
|
|
3109
|
+
runCount: 1,
|
|
3110
|
+
host: { label: "windows-ci" },
|
|
3111
|
+
workspace: dogfoodRepo,
|
|
3112
|
+
}, "s-dogfood");
|
|
3113
|
+
check(
|
|
3114
|
+
"a later pass on another host does not resurrect a held host's cached pass",
|
|
3115
|
+
!loadDogfoodCache().some((record) => record.host.label === "bluefin") &&
|
|
3116
|
+
loadDogfoodCache().some((record) => record.host.label === "windows-ci"),
|
|
3117
|
+
);
|
|
3118
|
+
const diskAfterResurrection = loadDogfoodCache();
|
|
3119
|
+
resetDogfoodState();
|
|
3120
|
+
persistDogfoodCache(diskAfterResurrection);
|
|
3121
|
+
check(
|
|
3122
|
+
"a restart after that still reports the held host as unsatisfied",
|
|
3123
|
+
evaluateDogfoodGate(dogfoodRepo).missingHosts.includes("bluefin"),
|
|
3124
|
+
);
|
|
3125
|
+
|
|
3126
|
+
// A HOLD must evict only the holding host's passes. In a cold process the
|
|
3127
|
+
// in-memory list holds just the new record, so a filter over memory alone would
|
|
3128
|
+
// delete every other host's durable pass and re-open the gate for a host that
|
|
3129
|
+
// never changed.
|
|
3130
|
+
resetDogfoodState();
|
|
3131
|
+
recordDogfoodResult({
|
|
3132
|
+
verdict: "pass",
|
|
3133
|
+
scenarioId: "permission-rule-reorder",
|
|
3134
|
+
scenarioSha256: "e".repeat(64),
|
|
3135
|
+
taxonomySha256: "f".repeat(64),
|
|
3136
|
+
checks: [{ id: "exit-code", status: "pass" }],
|
|
3137
|
+
runCount: 1,
|
|
3138
|
+
host: { label: "bluefin" },
|
|
3139
|
+
workspace: dogfoodRepo,
|
|
3140
|
+
}, "s-dogfood");
|
|
3141
|
+
recordDogfoodResult({
|
|
3142
|
+
verdict: "pass",
|
|
3143
|
+
scenarioId: "permission-rule-reorder",
|
|
3144
|
+
scenarioSha256: "e".repeat(64),
|
|
3145
|
+
taxonomySha256: "f".repeat(64),
|
|
3146
|
+
checks: [{ id: "exit-code", status: "pass" }],
|
|
3147
|
+
runCount: 1,
|
|
3148
|
+
host: { label: "windows-ci" },
|
|
3149
|
+
workspace: dogfoodRepo,
|
|
3150
|
+
}, "s-dogfood");
|
|
3151
|
+
check("both hosts are durable before the regression", existsSync(getDogfoodCacheFilePath()));
|
|
3152
|
+
const diskBeforeHold = loadDogfoodCache();
|
|
3153
|
+
// Cold process: the cache file survives and memory is empty. `resetDogfoodState`
|
|
3154
|
+
// deletes both, so the cache is written back by hand to model a restart.
|
|
3155
|
+
resetDogfoodState();
|
|
3156
|
+
persistDogfoodCache(diskBeforeHold);
|
|
3157
|
+
recordDogfoodResult({
|
|
3158
|
+
verdict: "hold",
|
|
3159
|
+
scenarioId: "permission-rule-reorder",
|
|
3160
|
+
scenarioSha256: "e".repeat(64),
|
|
3161
|
+
taxonomySha256: "f".repeat(64),
|
|
3162
|
+
taxonomyClass: "permission-bypass",
|
|
3163
|
+
checks: [{ id: "exit-code", status: "hold" }],
|
|
3164
|
+
runCount: 1,
|
|
3165
|
+
host: { label: "windows-ci" },
|
|
3166
|
+
workspace: dogfoodRepo,
|
|
3167
|
+
}, "s-dogfood");
|
|
3168
|
+
check(
|
|
3169
|
+
"a HOLD in a cold process keeps another host's durable pass",
|
|
3170
|
+
loadDogfoodCache().some((record) => record.host.label === "bluefin"),
|
|
3171
|
+
);
|
|
3172
|
+
check(
|
|
3173
|
+
"a HOLD in a cold process removes only that host's cached pass",
|
|
3174
|
+
!loadDogfoodCache().some((record) => record.host.label === "windows-ci"),
|
|
3175
|
+
);
|
|
3176
|
+
// The durable cache is shared across repositories, so eviction is scoped to the
|
|
3177
|
+
// workspace that held. Otherwise a HOLD in one repository would erase another
|
|
3178
|
+
// repository's record for the same host label.
|
|
3179
|
+
const otherRepo = join(dogfoodRepo, "..", "dogfood-other-repo");
|
|
3180
|
+
mkdirSync(join(otherRepo, ".opencode"), { recursive: true });
|
|
3181
|
+
writeFileSync(join(otherRepo, ".opencode", "workflow-guard.json"), JSON.stringify({ requireReview: false, requireDogfood: true }));
|
|
3182
|
+
writeFileSync(join(otherRepo, "code.txt"), "other\n");
|
|
3183
|
+
spawnSync("git", ["init", "-q", "-b", "main"], { cwd: otherRepo });
|
|
3184
|
+
spawnSync("git", ["config", "user.email", "test@test.local"], { cwd: otherRepo });
|
|
3185
|
+
spawnSync("git", ["config", "user.name", "Test Runner"], { cwd: otherRepo });
|
|
3186
|
+
spawnSync("git", ["add", "-A"], { cwd: otherRepo });
|
|
3187
|
+
spawnSync("git", ["commit", "-qm", "init"], { cwd: otherRepo });
|
|
3188
|
+
recordDogfoodResult({
|
|
3189
|
+
verdict: "pass",
|
|
3190
|
+
scenarioId: "permission-rule-reorder",
|
|
3191
|
+
scenarioSha256: "e".repeat(64),
|
|
3192
|
+
taxonomySha256: "f".repeat(64),
|
|
3193
|
+
checks: [{ id: "exit-code", status: "pass" }],
|
|
3194
|
+
runCount: 1,
|
|
3195
|
+
host: { label: "windows-ci" },
|
|
3196
|
+
workspace: otherRepo,
|
|
3197
|
+
}, "s-dogfood");
|
|
3198
|
+
const passesBeforeScopedHold = loadDogfoodCache().filter((record) => record.workspace === otherRepo).length;
|
|
3199
|
+
recordDogfoodResult({
|
|
3200
|
+
verdict: "hold",
|
|
3201
|
+
scenarioId: "permission-rule-reorder",
|
|
3202
|
+
scenarioSha256: "e".repeat(64),
|
|
3203
|
+
taxonomySha256: "f".repeat(64),
|
|
3204
|
+
taxonomyClass: "permission-bypass",
|
|
3205
|
+
checks: [{ id: "exit-code", status: "hold" }],
|
|
3206
|
+
runCount: 1,
|
|
3207
|
+
host: { label: "windows-ci" },
|
|
3208
|
+
workspace: dogfoodRepo,
|
|
3209
|
+
}, "s-dogfood");
|
|
3210
|
+
check(
|
|
3211
|
+
"a HOLD in one workspace keeps another workspace's pass for the same host label",
|
|
3212
|
+
passesBeforeScopedHold > 0 && loadDogfoodCache().filter((record) => record.workspace === otherRepo).length === passesBeforeScopedHold,
|
|
3213
|
+
);
|
|
3214
|
+
// Simulate a further restart: drop in-memory state only, keeping the cache.
|
|
3215
|
+
const diskAfterColdHold = loadDogfoodCache();
|
|
3216
|
+
resetDogfoodState();
|
|
3217
|
+
persistDogfoodCache(diskAfterColdHold);
|
|
3218
|
+
const afterColdHold = evaluateDogfoodGate(dogfoodRepo);
|
|
3219
|
+
check(
|
|
3220
|
+
"after a cold HOLD the gate still needs the holding host, not the untouched one",
|
|
3221
|
+
afterColdHold.missingHosts.includes("windows-ci") && !afterColdHold.missingHosts.includes("bluefin"),
|
|
3222
|
+
);
|
|
3223
|
+
|
|
3224
|
+
// A HOLD must evict that host's cached passes. Otherwise a restart drops the
|
|
3225
|
+
// in-memory HOLD and serves the older green run, re-opening the gate on a host
|
|
3226
|
+
// whose most recent run held.
|
|
3227
|
+
resetDogfoodState();
|
|
3228
|
+
recordDogfoodResult({
|
|
3229
|
+
verdict: "pass",
|
|
3230
|
+
scenarioId: "permission-rule-reorder",
|
|
3231
|
+
scenarioSha256: "e".repeat(64),
|
|
3232
|
+
taxonomySha256: "f".repeat(64),
|
|
3233
|
+
checks: [{ id: "exit-code", status: "pass" }],
|
|
3234
|
+
runCount: 1,
|
|
3235
|
+
host: { label: "bluefin" },
|
|
3236
|
+
workspace: dogfoodRepo,
|
|
3237
|
+
}, "s-dogfood");
|
|
3238
|
+
check("the earlier pass is durable before the regression", existsSync(getDogfoodCacheFilePath()));
|
|
3239
|
+
recordDogfoodResult({
|
|
3240
|
+
verdict: "hold",
|
|
3241
|
+
scenarioId: "permission-rule-reorder",
|
|
3242
|
+
scenarioSha256: "e".repeat(64),
|
|
3243
|
+
taxonomySha256: "f".repeat(64),
|
|
3244
|
+
taxonomyClass: "permission-bypass",
|
|
3245
|
+
checks: [{ id: "exit-code", status: "hold" }],
|
|
3246
|
+
runCount: 1,
|
|
3247
|
+
host: { label: "bluefin" },
|
|
3248
|
+
workspace: dogfoodRepo,
|
|
3249
|
+
}, "s-dogfood");
|
|
3250
|
+
check(
|
|
3251
|
+
"a HOLD removes that host's cached pass from disk",
|
|
3252
|
+
!loadDogfoodCache().some((record) => record.host.label === "bluefin"),
|
|
3253
|
+
);
|
|
3254
|
+
// Simulate a restart: drop in-memory state only, keeping the disk cache.
|
|
3255
|
+
const diskAfterHold = loadDogfoodCache();
|
|
3256
|
+
resetDogfoodState();
|
|
3257
|
+
persistDogfoodCache(diskAfterHold);
|
|
3258
|
+
check(
|
|
3259
|
+
"a restart after a HOLD cannot re-satisfy the gate from a stale cached pass",
|
|
3260
|
+
evaluateDogfoodGate(dogfoodRepo).missingHosts.length > 0,
|
|
3261
|
+
);
|
|
3262
|
+
|
|
3263
|
+
// A PASS and a HOLD for one host can land in the same millisecond. The tie must
|
|
3264
|
+
// resolve to the HOLD in the durable view too, because the HOLD is never cached:
|
|
3265
|
+
// persisting the pass would let a restart serve it. Pinning Date.now is the only
|
|
3266
|
+
// way to exercise the tie, since two real runs cannot finish in one millisecond.
|
|
3267
|
+
// The HOLD is recorded first because memory is newest-first: a later PASS for
|
|
3268
|
+
// the same key is met first by the durable selector, which is where a strict
|
|
3269
|
+
// ">" tie-break picks the pass.
|
|
3270
|
+
resetDogfoodState();
|
|
3271
|
+
const realNow = Date.now;
|
|
3272
|
+
Date.now = () => 1_700_000_000_000;
|
|
3273
|
+
try {
|
|
3274
|
+
recordDogfoodResult({
|
|
3275
|
+
verdict: "hold",
|
|
3276
|
+
scenarioId: "permission-rule-reorder",
|
|
3277
|
+
scenarioSha256: "e".repeat(64),
|
|
3278
|
+
taxonomySha256: "f".repeat(64),
|
|
3279
|
+
taxonomyClass: "permission-bypass",
|
|
3280
|
+
checks: [{ id: "exit-code", status: "hold" }],
|
|
3281
|
+
runCount: 1,
|
|
3282
|
+
host: { label: "bluefin" },
|
|
3283
|
+
workspace: dogfoodRepo,
|
|
3284
|
+
}, "s-dogfood");
|
|
3285
|
+
recordDogfoodResult({
|
|
3286
|
+
verdict: "pass",
|
|
3287
|
+
scenarioId: "permission-rule-reorder",
|
|
3288
|
+
scenarioSha256: "e".repeat(64),
|
|
3289
|
+
taxonomySha256: "f".repeat(64),
|
|
3290
|
+
checks: [{ id: "exit-code", status: "pass" }],
|
|
3291
|
+
runCount: 1,
|
|
3292
|
+
host: { label: "bluefin" },
|
|
3293
|
+
workspace: dogfoodRepo,
|
|
3294
|
+
}, "s-dogfood");
|
|
3295
|
+
} finally {
|
|
3296
|
+
Date.now = realNow;
|
|
3297
|
+
}
|
|
3298
|
+
check(
|
|
3299
|
+
"a same-millisecond HOLD then PASS leaves no cached pass",
|
|
3300
|
+
!loadDogfoodCache().some((record) => record.host.label === "bluefin"),
|
|
3301
|
+
);
|
|
3302
|
+
const tiedDisk = loadDogfoodCache();
|
|
3303
|
+
resetDogfoodState();
|
|
3304
|
+
persistDogfoodCache(tiedDisk);
|
|
3305
|
+
check(
|
|
3306
|
+
"a restart after a same-millisecond HOLD then PASS stays unsatisfied",
|
|
3307
|
+
evaluateDogfoodGate(dogfoodRepo).missingHosts.includes("bluefin"),
|
|
3308
|
+
);
|
|
3309
|
+
|
|
3310
|
+
// Retention is per host. A global cap would let repeated passes on one host
|
|
3311
|
+
// evict another host's only pass and silently reopen the gate for a host that
|
|
3312
|
+
// never changed.
|
|
3313
|
+
resetDogfoodState();
|
|
3314
|
+
for (let run = 0; run < 6; run += 1) {
|
|
3315
|
+
recordDogfoodResult({
|
|
3316
|
+
verdict: "pass",
|
|
3317
|
+
scenarioId: "permission-rule-reorder",
|
|
3318
|
+
scenarioSha256: "c".repeat(64),
|
|
3319
|
+
taxonomySha256: "d".repeat(64),
|
|
3320
|
+
checks: [{ id: "exit-code", status: "pass" }],
|
|
3321
|
+
runCount: 1,
|
|
3322
|
+
host: { label: "bluefin" },
|
|
3323
|
+
workspace: dogfoodRepo,
|
|
3324
|
+
}, "s-dogfood");
|
|
3325
|
+
}
|
|
3326
|
+
recordDogfoodResult({
|
|
3327
|
+
verdict: "pass",
|
|
3328
|
+
scenarioId: "permission-rule-reorder",
|
|
3329
|
+
scenarioSha256: "c".repeat(64),
|
|
3330
|
+
taxonomySha256: "d".repeat(64),
|
|
3331
|
+
checks: [{ id: "exit-code", status: "pass" }],
|
|
3332
|
+
runCount: 1,
|
|
3333
|
+
host: { label: "windows-ci" },
|
|
3334
|
+
workspace: dogfoodRepo,
|
|
3335
|
+
}, "s-dogfood");
|
|
3336
|
+
check(
|
|
3337
|
+
"repeated passes on one host cannot evict another host's pass",
|
|
3338
|
+
getDogfoodRecordsForWorkspace(dogfoodRepo).some((record) => record.host.label === "windows-ci"),
|
|
3339
|
+
);
|
|
3340
|
+
check(
|
|
3341
|
+
"per-host retention keeps only the newest few runs for the busiest host",
|
|
3342
|
+
getDogfoodRecordsForWorkspace(dogfoodRepo).filter((record) => record.host.label === "bluefin").length <= 3,
|
|
3343
|
+
);
|
|
3344
|
+
resetDogfoodState();
|
|
3345
|
+
|
|
3346
|
+
// A HOLD on a host outside the deploy matrix cannot block the gate, so it must
|
|
3347
|
+
// not be reported as the reason: that would name the wrong host and remedy.
|
|
3348
|
+
writeFileSync(join(dogfoodRepo, ".opencode", "workflow-guard.json"), JSON.stringify({ requireReview: false, requireDogfood: true, dogfoodDeployMatrix: ["bluefin"] }));
|
|
3349
|
+
reloadProjectConfig(dogfoodRepo);
|
|
3350
|
+
recordDogfoodResult({
|
|
3351
|
+
verdict: "hold",
|
|
3352
|
+
scenarioId: "permission-rule-reorder",
|
|
3353
|
+
scenarioSha256: "e".repeat(64),
|
|
3354
|
+
taxonomySha256: "f".repeat(64),
|
|
3355
|
+
taxonomyClass: "permission-bypass",
|
|
3356
|
+
checks: [{ id: "exit-code", status: "hold" }],
|
|
3357
|
+
runCount: 1,
|
|
3358
|
+
host: { label: "windows-ci" },
|
|
3359
|
+
workspace: dogfoodRepo,
|
|
3360
|
+
}, "s-dogfood");
|
|
3361
|
+
const outsideMatrixHold = evaluateDogfoodGate(dogfoodRepo);
|
|
3362
|
+
check(
|
|
3363
|
+
"an out-of-matrix HOLD is not reported as the blocking cause",
|
|
3364
|
+
outsideMatrixHold.missingHosts.includes("bluefin") &&
|
|
3365
|
+
!outsideMatrixHold.missingHosts.includes("windows-ci") &&
|
|
3366
|
+
!/(HOLD|permission-bypass)/.test(outsideMatrixHold.reason ?? ""),
|
|
3367
|
+
);
|
|
3368
|
+
resetDogfoodState();
|
|
3369
|
+
writeFileSync(join(dogfoodRepo, ".opencode", "workflow-guard.json"), JSON.stringify({ requireReview: false, requireDogfood: true, dogfoodDeployMatrix: ["bluefin", "windows-ci"] }));
|
|
3370
|
+
reloadProjectConfig(dogfoodRepo);
|
|
3371
|
+
resetDogfoodState();
|
|
3372
|
+
|
|
3373
|
+
// Compaction must carry the dogfood state, so a context summary cannot lose the
|
|
3374
|
+
// only reminder that a pre-deployment run is still outstanding.
|
|
3375
|
+
const dogfoodRepoPlugin = await (defaultExport?.server ?? WorkflowGuard)({
|
|
3376
|
+
directory: dogfoodRepo,
|
|
3377
|
+
worktree: dogfoodRepo,
|
|
3378
|
+
client: fakeClient as any,
|
|
3379
|
+
project: {} as any,
|
|
3380
|
+
experimental_workspace: {} as any,
|
|
3381
|
+
serverUrl: new URL("http://localhost:4096"),
|
|
3382
|
+
$: undefined as any,
|
|
3383
|
+
});
|
|
3384
|
+
const dogfoodCompactOutput: { context: string[] } = { context: [] };
|
|
3385
|
+
const dogfoodCompactFn = dogfoodRepoPlugin["experimental.session.compacting"];
|
|
3386
|
+
if (typeof dogfoodCompactFn === "function") {
|
|
3387
|
+
await dogfoodCompactFn({ sessionID: "s-dogfood" } as any, dogfoodCompactOutput as any);
|
|
3388
|
+
}
|
|
3389
|
+
check(
|
|
3390
|
+
"compaction reports the missing dogfood hosts by name",
|
|
3391
|
+
dogfoodCompactOutput.context.some(
|
|
3392
|
+
(context) =>
|
|
3393
|
+
context.includes("Dogfood Evidence: MISSING") &&
|
|
3394
|
+
context.includes("bluefin") &&
|
|
3395
|
+
context.includes("windows-ci"),
|
|
3396
|
+
),
|
|
3397
|
+
);
|
|
3398
|
+
|
|
3399
|
+
// The system guidance advertises the gate only for a repository that enabled
|
|
3400
|
+
// it, so an unopted repository is never told to satisfy a requirement it has.
|
|
3401
|
+
check(
|
|
3402
|
+
"system guidance stays silent about dogfood when the gate is off",
|
|
3403
|
+
!buildWorkflowGuardSystemGuidance({ projectMemoryEnabled: false, learningEnabled: false }).includes("Dogfood Gate"),
|
|
3404
|
+
);
|
|
3405
|
+
check(
|
|
3406
|
+
"system guidance names the dogfood requirement when the gate is on",
|
|
3407
|
+
buildWorkflowGuardSystemGuidance({ projectMemoryEnabled: false, learningEnabled: false, dogfoodRequired: true }).includes("Dogfood Gate"),
|
|
3408
|
+
);
|
|
3409
|
+
|
|
3410
|
+
// Recursion refusal: the actor session cannot certify itself.
|
|
3411
|
+
delete process.env.WORKFLOW_GUARD_REQUIRE_REVIEW;
|
|
3412
|
+
process.env.WORKFLOW_GUARD_DOGFOOD_ACTOR = "1";
|
|
3413
|
+
process.env.WORKFLOW_GUARD_DOGFOOD_DEPTH = "1";
|
|
3414
|
+
check("actor marker sets the depth and the actor flag", isDogfoodActor() === true && getDogfoodDepth() === 1);
|
|
3415
|
+
delete process.env.WORKFLOW_GUARD_DOGFOOD_ACTOR;
|
|
3416
|
+
check("depth alone still marks the session as an actor", isDogfoodActor() === true);
|
|
3417
|
+
const actorRecordResult = await dogfoodPlugin.tool?.record_dogfood?.execute(
|
|
3418
|
+
{ scenarioId: "s", scenarioSha256: "a".repeat(64), taxonomySha256: "b".repeat(64), verdict: "pass", checks: JSON.stringify([{ id: "exit-code", status: "pass" }]), hostLabel: "bluefin", directory: dogfoodRepo },
|
|
3419
|
+
{ sessionID: "s-dogfood-actor", worktree: dogfoodRepo, directory: dogfoodRepo } as any,
|
|
3420
|
+
);
|
|
3421
|
+
check(
|
|
3422
|
+
"a dogfood actor is refused when it tries to record its own verdict",
|
|
3423
|
+
typeof actorRecordResult === "string" && actorRecordResult.includes("cannot certify itself"),
|
|
3424
|
+
);
|
|
3425
|
+
delete process.env.WORKFLOW_GUARD_DOGFOOD_DEPTH;
|
|
3426
|
+
process.env.WORKFLOW_GUARD_DOGFOOD_DEPTH = "not-a-number";
|
|
3427
|
+
check("an unparseable depth marker fails closed to a non-zero depth", getDogfoodDepth() === 1);
|
|
3428
|
+
delete process.env.WORKFLOW_GUARD_DOGFOOD_DEPTH;
|
|
3429
|
+
check("with no marker the session is not an actor", isDogfoodActor() === false && getDogfoodDepth() === 0);
|
|
3430
|
+
|
|
3431
|
+
// Verdict validation: a pass needs every check passing, and a HOLD names a class.
|
|
3432
|
+
const lyingPass = await dogfoodPlugin.tool?.record_dogfood?.execute(
|
|
3433
|
+
{ scenarioId: "s", scenarioSha256: "a".repeat(64), taxonomySha256: "b".repeat(64), verdict: "pass", checks: JSON.stringify([{ id: "exit-code", status: "pass" }, { id: "diff-allowlist", status: "skip" }]), hostLabel: "bluefin", directory: dogfoodRepo },
|
|
3434
|
+
{ sessionID: "s-dogfood", worktree: dogfoodRepo, directory: dogfoodRepo } as any,
|
|
3435
|
+
);
|
|
3436
|
+
check(
|
|
3437
|
+
"a pass with a skipped check is refused, so a gate cannot pass by omission",
|
|
3438
|
+
typeof lyingPass === "string" && lyingPass.includes("requires every check to be 'pass'"),
|
|
3439
|
+
);
|
|
3440
|
+
const classlessHold = await dogfoodPlugin.tool?.record_dogfood?.execute(
|
|
3441
|
+
{ scenarioId: "s", scenarioSha256: "a".repeat(64), taxonomySha256: "b".repeat(64), verdict: "hold", checks: JSON.stringify([{ id: "exit-code", status: "hold" }]), hostLabel: "bluefin", directory: dogfoodRepo },
|
|
3442
|
+
{ sessionID: "s-dogfood", worktree: dogfoodRepo, directory: dogfoodRepo } as any,
|
|
3443
|
+
);
|
|
3444
|
+
check(
|
|
3445
|
+
"a HOLD without a taxonomy class is refused so the failure stays diagnosable",
|
|
3446
|
+
typeof classlessHold === "string" && classlessHold.includes("must name its taxonomy class"),
|
|
3447
|
+
);
|
|
3448
|
+
const malformedChecks = await dogfoodPlugin.tool?.record_dogfood?.execute(
|
|
3449
|
+
{ scenarioId: "s", scenarioSha256: "a".repeat(64), taxonomySha256: "b".repeat(64), verdict: "pass", checks: "not json", hostLabel: "bluefin", directory: dogfoodRepo },
|
|
3450
|
+
{ sessionID: "s-dogfood", worktree: dogfoodRepo, directory: dogfoodRepo } as any,
|
|
3451
|
+
);
|
|
3452
|
+
check(
|
|
3453
|
+
"malformed check rows are refused instead of recorded as passing",
|
|
3454
|
+
typeof malformedChecks === "string" && malformedChecks.includes("checks must be a JSON array"),
|
|
3455
|
+
);
|
|
3456
|
+
const anonymousHost = await dogfoodPlugin.tool?.record_dogfood?.execute(
|
|
3457
|
+
{ scenarioId: "s", scenarioSha256: "a".repeat(64), taxonomySha256: "b".repeat(64), verdict: "pass", checks: JSON.stringify([{ id: "exit-code", status: "pass" }]), hostLabel: " ", directory: dogfoodRepo },
|
|
3458
|
+
{ sessionID: "s-dogfood", worktree: dogfoodRepo, directory: dogfoodRepo } as any,
|
|
3459
|
+
);
|
|
3460
|
+
check(
|
|
3461
|
+
"an empty host label is refused because the deploy matrix could never match it",
|
|
3462
|
+
typeof anonymousHost === "string" && anonymousHost.includes("scenarioId and hostLabel must both be non-empty"),
|
|
3463
|
+
);
|
|
3464
|
+
// An empty check list would slip past the per-row checks because `[].some(...)`
|
|
3465
|
+
// is false, so a pass would assert nothing.
|
|
3466
|
+
const noChecks = await dogfoodPlugin.tool?.record_dogfood?.execute(
|
|
3467
|
+
{ scenarioId: "s", scenarioSha256: "a".repeat(64), taxonomySha256: "b".repeat(64), verdict: "pass", checks: "[]", hostLabel: "bluefin", directory: dogfoodRepo },
|
|
3468
|
+
{ sessionID: "s-dogfood", worktree: dogfoodRepo, directory: dogfoodRepo } as any,
|
|
3469
|
+
);
|
|
3470
|
+
check(
|
|
3471
|
+
"a pass with no check rows is refused instead of asserting nothing",
|
|
3472
|
+
typeof noChecks === "string" && noChecks.includes("at least one check row is required") && evaluateDogfoodGate(dogfoodRepo).missingHosts.length > 0,
|
|
3473
|
+
);
|
|
3474
|
+
|
|
3475
|
+
// A real recorded verdict through the tool satisfies the gate and journals it.
|
|
3476
|
+
const goodDogfood = await dogfoodPlugin.tool?.record_dogfood?.execute(
|
|
3477
|
+
{
|
|
3478
|
+
scenarioId: "permission-rule-reorder",
|
|
3479
|
+
scenarioSha256: "c".repeat(64),
|
|
3480
|
+
taxonomySha256: "d".repeat(64),
|
|
3481
|
+
verdict: "pass",
|
|
3482
|
+
checks: JSON.stringify([{ id: "exit-code", status: "pass" }, { id: "diff-allowlist", status: "pass" }]),
|
|
3483
|
+
hostLabel: "bluefin",
|
|
3484
|
+
runCount: 3,
|
|
3485
|
+
hostOs: "linux",
|
|
3486
|
+
hostArch: "x64",
|
|
3487
|
+
directory: dogfoodRepo,
|
|
3488
|
+
},
|
|
3489
|
+
{ sessionID: "s-dogfood", worktree: dogfoodRepo, directory: dogfoodRepo } as any,
|
|
3490
|
+
);
|
|
3491
|
+
check("record_dogfood records a passing run through the tool", typeof goodDogfood === "string" && goodDogfood.includes("PASS") && goodDogfood.includes("bluefin"));
|
|
3492
|
+
const dogfoodAudit = getRecentAuditEntries(30).find((entry) => entry.tool === "record_dogfood.verdict" && entry.reason === "dogfood_passed");
|
|
3493
|
+
check(
|
|
3494
|
+
"the recorded dogfood verdict is journaled with its binding",
|
|
3495
|
+
dogfoodAudit?.evidence?.dogfoodVerdict === "pass" && dogfoodAudit?.evidence?.binding?.workspace === dogfoodRepo,
|
|
3496
|
+
);
|
|
3497
|
+
const dogfoodEvidenceRecord = dogfoodEvidence(getLastDogfoodResultForWorkspace(dogfoodRepo)!);
|
|
3498
|
+
check(
|
|
3499
|
+
"dogfood evidence carries the digest pair and is attestation-confidence",
|
|
3500
|
+
dogfoodEvidenceRecord.kind === "dogfood" && dogfoodEvidenceRecord.source.scenarioSha256 === "c".repeat(64) && dogfoodEvidenceRecord.confidence === "attestation",
|
|
3501
|
+
);
|
|
3502
|
+
|
|
3503
|
+
process.env.WORKFLOW_GUARD_REQUIRE_DOGFOOD = "0";
|
|
3504
|
+
writeFileSync(join(dogfoodRepo, ".opencode", "workflow-guard.json"), JSON.stringify({ requireReview: false, requireDogfood: true }));
|
|
3505
|
+
reloadProjectConfig(dogfoodRepo);
|
|
3506
|
+
check("WORKFLOW_GUARD_REQUIRE_DOGFOOD=0 overrides config true", isDogfoodRequired(dogfoodRepo) === false);
|
|
3507
|
+
delete process.env.WORKFLOW_GUARD_REQUIRE_DOGFOOD;
|
|
3508
|
+
process.env.WORKFLOW_GUARD_REQUIRE_DOGFOOD = "1";
|
|
3509
|
+
check("WORKFLOW_GUARD_REQUIRE_DOGFOOD=1 forces the gate on", isDogfoodRequired(dogfoodRepo) === true);
|
|
3510
|
+
delete process.env.WORKFLOW_GUARD_REQUIRE_DOGFOOD;
|
|
3511
|
+
resetDogfoodState();
|
|
3512
|
+
rmSync(dogfoodRepo, { recursive: true, force: true });
|
|
3513
|
+
setWorkspaceRoot(root);
|
|
3514
|
+
reloadProjectConfig(root);
|
|
3515
|
+
resetDogfoodState();
|
|
3516
|
+
|
|
2891
3517
|
// 12. Custom Plugin Tools & Event Auditing
|
|
2892
3518
|
console.log("- Custom Tools & Event Auditing -");
|
|
2893
3519
|
const customPlugin = await (defaultExport?.server ?? WorkflowGuard)({
|