@deftai/directive-core 0.107.0 → 0.109.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/dist/architecture/sor-preflight.js +2 -0
- package/dist/cache/operations.js +1 -1
- package/dist/check/cached-orchestrator.js +28 -3
- package/dist/check/gate-lists.d.ts +14 -0
- package/dist/check/gate-lists.js +26 -3
- package/dist/check/named-cause.js +18 -3
- package/dist/check/session-completed-ac.d.ts +1 -1
- package/dist/check/session-completed-ac.js +1 -1
- package/dist/codebase/provider.js +7 -1
- package/dist/consumer-check-contract/evaluate.d.ts +47 -0
- package/dist/consumer-check-contract/evaluate.js +181 -15
- package/dist/delivery-attempt/evaluate.d.ts +9 -1
- package/dist/delivery-attempt/evaluate.js +69 -0
- package/dist/delivery-attempt/handoff.js +1 -1
- package/dist/delivery-attempt/index.d.ts +1 -1
- package/dist/delivery-attempt/index.js +1 -1
- package/dist/deposit/live-procedure-exclusions.d.ts +18 -0
- package/dist/deposit/live-procedure-exclusions.js +110 -0
- package/dist/deposit/live-procedure-targets.d.ts +45 -0
- package/dist/deposit/live-procedure-targets.js +274 -0
- package/dist/deposit/python-free.d.ts +6 -0
- package/dist/deposit/python-free.js +15 -0
- package/dist/deposit/rewrite-deposit-links.d.ts +42 -0
- package/dist/deposit/rewrite-deposit-links.js +148 -0
- package/dist/deposit/run-stage-content-pack.d.ts +2 -0
- package/dist/deposit/run-stage-content-pack.js +3 -0
- package/dist/deposit/stage-content-pack.d.ts +17 -0
- package/dist/deposit/stage-content-pack.js +91 -0
- package/dist/design-critique/citation-grammar.d.ts +60 -0
- package/dist/design-critique/citation-grammar.js +303 -0
- package/dist/design-critique/completed-arc-record.d.ts +62 -0
- package/dist/design-critique/completed-arc-record.js +277 -0
- package/dist/freshness/bind.d.ts +32 -3
- package/dist/freshness/bind.js +58 -12
- package/dist/hooks/classify/host-session-identity.d.ts +89 -0
- package/dist/hooks/classify/host-session-identity.js +467 -0
- package/dist/hooks/classify/index.d.ts +3 -2
- package/dist/hooks/classify/index.js +3 -2
- package/dist/hooks/classify/paths.d.ts +9 -0
- package/dist/hooks/classify/paths.js +39 -0
- package/dist/hooks/classify/stdin.d.ts +5 -0
- package/dist/hooks/classify/stdin.js +94 -1
- package/dist/hooks/dest-form.d.ts +20 -1
- package/dist/hooks/dest-form.js +158 -21
- package/dist/hooks/dispatcher.d.ts +58 -3
- package/dist/hooks/dispatcher.js +762 -73
- package/dist/hooks/fixtures/cases.d.ts +22 -2
- package/dist/hooks/fixtures/cases.js +228 -0
- package/dist/hooks/git-destructive-log.d.ts +32 -0
- package/dist/hooks/git-destructive-log.js +46 -0
- package/dist/hooks/index.d.ts +2 -0
- package/dist/hooks/index.js +2 -0
- package/dist/hooks/scope.js +3 -1
- package/dist/hooks/shell-write-targets.d.ts +10 -0
- package/dist/hooks/shell-write-targets.js +274 -0
- package/dist/hooks/tools.d.ts +20 -2
- package/dist/hooks/tools.js +30 -1
- package/dist/init-deposit/agent-hooks.d.ts +2 -1
- package/dist/init-deposit/agent-hooks.js +8 -1
- package/dist/init-deposit/gitignore.d.ts +7 -0
- package/dist/init-deposit/gitignore.js +24 -0
- package/dist/init-deposit/hook-runtime-travel.d.ts +77 -0
- package/dist/init-deposit/hook-runtime-travel.js +269 -0
- package/dist/init-deposit/init-deposit.js +3 -0
- package/dist/init-deposit/refresh.js +7 -0
- package/dist/init-deposit/runtime-writers.d.ts +14 -0
- package/dist/init-deposit/runtime-writers.js +33 -0
- package/dist/intake/clause-derivation.js +9 -3
- package/dist/intake/issue-ingest.d.ts +6 -1
- package/dist/intake/issue-ingest.js +83 -4
- package/dist/intake/platform-capabilities.d.ts +9 -2
- package/dist/intake/platform-capabilities.js +38 -9
- package/dist/intake/reconcile-issues.js +13 -13
- package/dist/lifecycle/brief-envelope.d.ts +25 -0
- package/dist/lifecycle/brief-envelope.js +42 -0
- package/dist/lifecycle/index.d.ts +1 -0
- package/dist/lifecycle/index.js +1 -0
- package/dist/literal-acceptance/evaluate.js +14 -5
- package/dist/literal-acceptance/index.d.ts +1 -1
- package/dist/literal-acceptance/index.js +1 -1
- package/dist/literal-acceptance/run.d.ts +2 -0
- package/dist/literal-acceptance/run.js +19 -1
- package/dist/orphan-active/candidate-scope.d.ts +53 -0
- package/dist/orphan-active/candidate-scope.js +157 -0
- package/dist/orphan-active/evaluate.d.ts +52 -0
- package/dist/orphan-active/evaluate.js +250 -128
- package/dist/orphan-active/index.d.ts +2 -0
- package/dist/orphan-active/index.js +2 -0
- package/dist/orphan-active/issue-state.d.ts +121 -0
- package/dist/orphan-active/issue-state.js +278 -0
- package/dist/platform/cursor-managed-runtime.d.ts +95 -0
- package/dist/platform/cursor-managed-runtime.js +241 -0
- package/dist/platform/index.d.ts +1 -0
- package/dist/platform/index.js +1 -0
- package/dist/platform/platform-capabilities.d.ts +23 -1
- package/dist/platform/platform-capabilities.js +55 -15
- package/dist/policy/ceremony-dial.js +5 -9
- package/dist/policy/host-hooks.js +4 -9
- package/dist/policy/merge-approval-head.js +7 -6
- package/dist/policy/org-force-on-migration.js +5 -10
- package/dist/policy/plan-extensions.d.ts +14 -2
- package/dist/policy/plan-extensions.js +24 -3
- package/dist/policy/product-signal.js +5 -10
- package/dist/policy/require-human-merge.js +5 -9
- package/dist/policy/resolve.js +77 -16
- package/dist/policy/value-feedback.js +9 -18
- package/dist/pr-closeout-attestable/evaluate.d.ts +80 -0
- package/dist/pr-closeout-attestable/evaluate.js +306 -0
- package/dist/pr-closeout-attestable/index.d.ts +2 -0
- package/dist/pr-closeout-attestable/index.js +2 -0
- package/dist/pr-closing-keywords/gh.js +32 -9
- package/dist/pr-closing-keywords/main.d.ts +1 -0
- package/dist/pr-closing-keywords/main.js +68 -3
- package/dist/pr-closing-keywords/types.d.ts +2 -0
- package/dist/pr-merge-readiness/gh.js +32 -9
- package/dist/pr-protected-issues/gh.d.ts +6 -2
- package/dist/pr-protected-issues/gh.js +42 -11
- package/dist/pr-wait-mergeable/cascade.d.ts +9 -1
- package/dist/pr-wait-mergeable/cascade.js +21 -1
- package/dist/pr-wait-mergeable/types.d.ts +2 -0
- package/dist/pr-wait-mergeable/wrappers.d.ts +6 -0
- package/dist/pr-wait-mergeable/wrappers.js +14 -3
- package/dist/preflight/evaluate.d.ts +15 -0
- package/dist/preflight/evaluate.js +29 -3
- package/dist/product-first-done-gate/acceptance-resolver.d.ts +14 -2
- package/dist/product-first-done-gate/acceptance-resolver.js +13 -2
- package/dist/product-first-done-gate/evaluate.js +23 -9
- package/dist/product-first-done-gate/types.js +2 -0
- package/dist/release/cli-drift-report.d.ts +68 -0
- package/dist/release/cli-drift-report.js +189 -0
- package/dist/release/consumer-hard-stops.d.ts +48 -0
- package/dist/release/consumer-hard-stops.js +140 -0
- package/dist/release/consumer-readiness-disclosure.d.ts +18 -0
- package/dist/release/consumer-readiness-disclosure.js +51 -0
- package/dist/release/index.d.ts +3 -0
- package/dist/release/index.js +3 -0
- package/dist/release/issue-state-fetch.d.ts +5 -3
- package/dist/release/issue-state-fetch.js +86 -21
- package/dist/release/native-steps.js +1 -0
- package/dist/release/pipeline.js +74 -0
- package/dist/release/run-consumer-readiness.d.ts +15 -0
- package/dist/release/run-consumer-readiness.js +24 -0
- package/dist/release/types.d.ts +20 -0
- package/dist/render/constants.d.ts +4 -0
- package/dist/render/constants.js +11 -8
- package/dist/render/export-spec.js +31 -5
- package/dist/render/index.d.ts +1 -1
- package/dist/render/index.js +1 -1
- package/dist/render/prd-render.d.ts +4 -1
- package/dist/render/prd-render.js +82 -23
- package/dist/render/project-render.js +14 -7
- package/dist/render/scope-outlook.d.ts +2 -0
- package/dist/render/scope-outlook.js +3 -0
- package/dist/render/spec-render.js +2 -2
- package/dist/resolution/package-manager.d.ts +33 -4
- package/dist/resolution/package-manager.js +210 -9
- package/dist/review-monitor/constants.d.ts +7 -0
- package/dist/review-monitor/constants.js +7 -0
- package/dist/review-monitor/github-lease.d.ts +98 -1
- package/dist/review-monitor/github-lease.js +186 -4
- package/dist/review-monitor/lease-comment.d.ts +36 -0
- package/dist/review-monitor/lease-comment.js +103 -2
- package/dist/run-summary/types.d.ts +2 -2
- package/dist/scm/build-command.d.ts +2 -2
- package/dist/scm/build-command.js +2 -2
- package/dist/scm/call-shape.d.ts +25 -0
- package/dist/scm/call-shape.js +59 -0
- package/dist/scm/call.d.ts +7 -4
- package/dist/scm/call.js +48 -9
- package/dist/scm/design-critique-chip.d.ts +1 -0
- package/dist/scm/design-critique-chip.js +22 -6
- package/dist/scm/gh-rest.d.ts +34 -13
- package/dist/scm/gh-rest.js +168 -15
- package/dist/scm/index.d.ts +2 -0
- package/dist/scm/index.js +2 -0
- package/dist/scm/readiness.d.ts +2 -0
- package/dist/scm/readiness.js +28 -4
- package/dist/scm/spawn-status.d.ts +33 -0
- package/dist/scm/spawn-status.js +53 -0
- package/dist/scope/acceptance-evidence.d.ts +1 -1
- package/dist/scope/project-definition-sync.js +14 -7
- package/dist/scope/transition.js +4 -14
- package/dist/session/ac-pass-banking.d.ts +2 -2
- package/dist/session/ac-pass-banking.js +2 -2
- package/dist/session/git.d.ts +24 -0
- package/dist/session/git.js +138 -2
- package/dist/session/host-session-owner.d.ts +53 -0
- package/dist/session/host-session-owner.js +89 -0
- package/dist/session/index.d.ts +1 -0
- package/dist/session/index.js +1 -0
- package/dist/session/occupancy.d.ts +259 -13
- package/dist/session/occupancy.js +877 -33
- package/dist/session/ritual-sentinel.d.ts +5 -0
- package/dist/session/ritual-sentinel.js +5 -0
- package/dist/session/session-ready.d.ts +5 -1
- package/dist/session/session-ready.js +98 -8
- package/dist/session/session-start.d.ts +5 -1
- package/dist/session/session-start.js +32 -17
- package/dist/session/toolchain-preflight.d.ts +13 -3
- package/dist/session/toolchain-preflight.js +93 -18
- package/dist/session/verify-ac-session-cache.d.ts +2 -2
- package/dist/session/verify-ac-session-cache.js +2 -2
- package/dist/session/verify-session-ritual.d.ts +11 -0
- package/dist/session/verify-session-ritual.js +60 -15
- package/dist/slice/constants.d.ts +1 -1
- package/dist/spec-authority/constants.d.ts +10 -2
- package/dist/spec-authority/constants.js +53 -8
- package/dist/spec-authority/resolver.d.ts +3 -0
- package/dist/spec-authority/resolver.js +55 -7
- package/dist/subprocess/max-buffer.d.ts +14 -0
- package/dist/subprocess/max-buffer.js +15 -0
- package/dist/swarm/complete-cohort.d.ts +2 -0
- package/dist/swarm/complete-cohort.js +21 -13
- package/dist/swarm/index.d.ts +1 -0
- package/dist/swarm/index.js +1 -0
- package/dist/swarm/launch-cli.js +53 -33
- package/dist/swarm/launch.d.ts +8 -0
- package/dist/swarm/launch.js +63 -43
- package/dist/swarm/pre-dispatch-cli.js +2 -1
- package/dist/swarm/pre-dispatch.js +10 -1
- package/dist/swarm/subagent-status-dir.d.ts +27 -0
- package/dist/swarm/subagent-status-dir.js +42 -0
- package/dist/swarm/worktrees.d.ts +17 -3
- package/dist/swarm/worktrees.js +48 -4
- package/dist/triage/evaluate/worktrees.js +153 -6
- package/dist/triage/scope/mutations-core.d.ts +2 -6
- package/dist/triage/scope/mutations-core.js +23 -55
- package/dist/triage/scope-drift/add-ignore.js +5 -39
- package/dist/triage/subscribe/index.d.ts +2 -6
- package/dist/triage/subscribe/index.js +15 -65
- package/dist/triage/welcome/writers.js +15 -74
- package/dist/umbrella-current-shape/index.d.ts +51 -3
- package/dist/umbrella-current-shape/index.js +106 -18
- package/dist/validate-content/deposit-required.d.ts +39 -0
- package/dist/validate-content/deposit-required.js +147 -0
- package/dist/validate-content/index.d.ts +1 -0
- package/dist/validate-content/index.js +1 -0
- package/dist/validate-content/validate-links.d.ts +2 -3
- package/dist/validate-content/validate-links.js +29 -2
- package/dist/vbrief-activate/activate.d.ts +7 -2
- package/dist/vbrief-activate/activate.js +26 -13
- package/dist/vbrief-build/index.d.ts +2 -1
- package/dist/vbrief-build/index.js +2 -1
- package/dist/vbrief-build/parity-scenarios.js +5 -5
- package/dist/vbrief-build/project-definition-io.d.ts +63 -3
- package/dist/vbrief-build/project-definition-io.js +357 -31
- package/dist/vbrief-build/project-definition-mutation.d.ts +32 -0
- package/dist/vbrief-build/project-definition-mutation.js +43 -0
- package/dist/vbrief-validate/conformance.d.ts +1 -0
- package/dist/vbrief-validate/conformance.js +85 -8
- package/dist/vbrief-validate/main.js +2 -0
- package/dist/vbrief-validate/precutover.js +5 -10
- package/dist/verify-ac/clauses.d.ts +62 -2
- package/dist/verify-ac/clauses.js +183 -111
- package/dist/verify-ac/evaluate.d.ts +9 -0
- package/dist/verify-ac/evaluate.js +32 -9
- package/dist/verify-ac/index.d.ts +1 -1
- package/dist/verify-ac/index.js +1 -1
- package/dist/verify-env/node-runtime.d.ts +8 -4
- package/dist/verify-env/node-runtime.js +9 -6
- package/dist/verify-env/toolchain-check.d.ts +35 -3
- package/dist/verify-env/toolchain-check.js +155 -36
- package/dist/verify-source/deposit-closure.d.ts +23 -0
- package/dist/verify-source/deposit-closure.js +162 -0
- package/dist/verify-source/index.d.ts +2 -0
- package/dist/verify-source/index.js +2 -0
- package/dist/verify-source/project-definition-mutation-boundary.d.ts +104 -0
- package/dist/verify-source/project-definition-mutation-boundary.js +326 -0
- package/dist/verify-source/semantic-single-source.d.ts +36 -0
- package/dist/verify-source/semantic-single-source.js +349 -0
- package/package.json +7 -3
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
2
2
|
import { userInfo } from "node:os";
|
|
3
3
|
import { IDENTITY_LOCAL_USER, IDENTITY_REAL_ROOT, IDENTITY_SANDBOX_REMAPPED_LOCAL_USER, IDENTITY_UNKNOWN, RUNTIME_MODE_CLOUD_HEADLESS, RUNTIME_MODE_CURSOR_NATIVE_SANDBOX, RUNTIME_MODE_LOCAL_UNSANDBOXED, } from "./constants.js";
|
|
4
|
+
import { hasExplicitHostGhSelection, probeManagedRuntime, RUNTIME_REASON_CI_MARKER, RUNTIME_REASON_CURSOR_MARKER_AMBIGUOUS, RUNTIME_REASON_CURSOR_SANDBOX_MARKER, RUNTIME_REASON_EXPLICIT_HOST_GH, RUNTIME_REASON_MANAGED_RUNTIME_PROBE, RUNTIME_REASON_NO_RUNTIME_MARKER, } from "./cursor-managed-runtime.js";
|
|
4
5
|
import { detectEnvironmentContext, environmentContextToDict, } from "./shell-context.js";
|
|
5
6
|
const TRUTHY = new Set(["1", "true", "yes", "on"]);
|
|
6
7
|
const CURSOR_SIGNAL_VARS = [
|
|
@@ -69,16 +70,19 @@ export function classifyIdentityKind(options) {
|
|
|
69
70
|
}
|
|
70
71
|
return IDENTITY_LOCAL_USER;
|
|
71
72
|
}
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
return true;
|
|
73
|
+
/** Reason id when a non-Cursor CI/cloud marker forces cloud-headless, else null. */
|
|
74
|
+
function ciCloudReason(environ) {
|
|
75
75
|
// GROK_BUILD / DEFT_AGENT_RUNTIME=grok-build identify a local TUI, not CI (#3469).
|
|
76
76
|
const runtime = (environ.DEFT_AGENT_RUNTIME ?? "").trim().toLowerCase();
|
|
77
77
|
if (runtime === "cloud" || runtime === "headless")
|
|
78
|
-
return
|
|
79
|
-
if (envTruthy(environ, "GITHUB_ACTIONS") || envTruthy(environ, "BUILDKITE"))
|
|
80
|
-
return
|
|
81
|
-
|
|
78
|
+
return RUNTIME_REASON_CI_MARKER;
|
|
79
|
+
if (envTruthy(environ, "GITHUB_ACTIONS") || envTruthy(environ, "BUILDKITE")) {
|
|
80
|
+
return RUNTIME_REASON_CI_MARKER;
|
|
81
|
+
}
|
|
82
|
+
if (envTruthy(environ, "CI") && !envTruthy(environ, "CURSOR_COMPOSER")) {
|
|
83
|
+
return RUNTIME_REASON_CI_MARKER;
|
|
84
|
+
}
|
|
85
|
+
return null;
|
|
82
86
|
}
|
|
83
87
|
function isCursorNativeSandbox(environ, sandboxUidRemap) {
|
|
84
88
|
if (sandboxUidRemap)
|
|
@@ -87,12 +91,44 @@ function isCursorNativeSandbox(environ, sandboxUidRemap) {
|
|
|
87
91
|
return true;
|
|
88
92
|
return Boolean((environ.CURSOR_SANDBOX_LANDLOCK_STATUS ?? "").trim());
|
|
89
93
|
}
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
94
|
+
/**
|
|
95
|
+
* Classify runtime mode and name the reason for the verdict (#3859).
|
|
96
|
+
*
|
|
97
|
+
* Ordering matches intake/platform-capabilities.ts: CI markers, then a positive
|
|
98
|
+
* managed-runtime read, then sandbox, then the ambiguous Cursor hop. The Cursor
|
|
99
|
+
* hop previously ran first here; it now runs last so neither a CI marker nor a
|
|
100
|
+
* managed read can be overridden by an explicit host-gh selection.
|
|
101
|
+
*/
|
|
102
|
+
export function classifyRuntime(environ, sandboxUidRemap, options = {}) {
|
|
103
|
+
const ciReason = ciCloudReason(environ);
|
|
104
|
+
if (ciReason !== null)
|
|
105
|
+
return { mode: RUNTIME_MODE_CLOUD_HEADLESS, reason: ciReason };
|
|
106
|
+
const managedProbe = options.managedRuntimeProbe ?? probeManagedRuntime;
|
|
107
|
+
if (managedProbe(environ).verdict === "managed") {
|
|
108
|
+
return {
|
|
109
|
+
mode: RUNTIME_MODE_CLOUD_HEADLESS,
|
|
110
|
+
reason: RUNTIME_REASON_MANAGED_RUNTIME_PROBE,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
if (isCursorNativeSandbox(environ, sandboxUidRemap)) {
|
|
114
|
+
return {
|
|
115
|
+
mode: RUNTIME_MODE_CURSOR_NATIVE_SANDBOX,
|
|
116
|
+
reason: RUNTIME_REASON_CURSOR_SANDBOX_MARKER,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
if (envTruthy(environ, "CURSOR_AGENT")) {
|
|
120
|
+
if (hasExplicitHostGhSelection(environ)) {
|
|
121
|
+
return { mode: RUNTIME_MODE_LOCAL_UNSANDBOXED, reason: RUNTIME_REASON_EXPLICIT_HOST_GH };
|
|
122
|
+
}
|
|
123
|
+
return {
|
|
124
|
+
mode: RUNTIME_MODE_CLOUD_HEADLESS,
|
|
125
|
+
reason: RUNTIME_REASON_CURSOR_MARKER_AMBIGUOUS,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
return { mode: RUNTIME_MODE_LOCAL_UNSANDBOXED, reason: RUNTIME_REASON_NO_RUNTIME_MARKER };
|
|
129
|
+
}
|
|
130
|
+
export function classifyRuntimeMode(environ, sandboxUidRemap, options = {}) {
|
|
131
|
+
return classifyRuntime(environ, sandboxUidRemap, options).mode;
|
|
96
132
|
}
|
|
97
133
|
function readOwnership(path, sandboxUidRemap) {
|
|
98
134
|
try {
|
|
@@ -151,13 +187,16 @@ export function probeRuntimeCapabilities(options = {}) {
|
|
|
151
187
|
const uidMap = readUidMap(uidMapFile);
|
|
152
188
|
const sandboxUidRemap = detectSandboxUidRemap(uidMap, { effectiveUid, cursorOrigUid });
|
|
153
189
|
const identityKind = classifyIdentityKind({ effectiveUid, sandboxUidRemap });
|
|
154
|
-
const
|
|
190
|
+
const runtime = classifyRuntime(env, sandboxUidRemap, {
|
|
191
|
+
managedRuntimeProbe: options.managedRuntimeProbe,
|
|
192
|
+
});
|
|
155
193
|
const cwdPath = options.cwd ?? process.cwd();
|
|
156
194
|
const ownership = readOwnership(cwdPath, sandboxUidRemap);
|
|
157
195
|
return {
|
|
158
196
|
hostPlatform: environment.hostPlatform,
|
|
159
197
|
shell: environment.shell,
|
|
160
|
-
runtimeMode,
|
|
198
|
+
runtimeMode: runtime.mode,
|
|
199
|
+
runtimeModeReason: runtime.reason,
|
|
161
200
|
identityKind,
|
|
162
201
|
effectiveUid,
|
|
163
202
|
effectiveUsername,
|
|
@@ -176,6 +215,7 @@ export function reportToDict(report) {
|
|
|
176
215
|
return {
|
|
177
216
|
...environmentContextToDict({ hostPlatform: report.hostPlatform, shell: report.shell }),
|
|
178
217
|
runtime_mode: report.runtimeMode,
|
|
218
|
+
runtime_mode_reason: report.runtimeModeReason,
|
|
179
219
|
identity_kind: report.identityKind,
|
|
180
220
|
effective_uid: report.effectiveUid,
|
|
181
221
|
effective_username: report.effectiveUsername,
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
21
21
|
import { join } from "node:path";
|
|
22
22
|
import { RunSummaryEmitter } from "../run-summary/emit.js";
|
|
23
|
-
import {
|
|
23
|
+
import { withProjectDefinitionMutation } from "../vbrief-build/project-definition-mutation.js";
|
|
24
24
|
import { migrateLegacyPolicyKey, PLAN_POLICY_KEY, readPlanPolicy } from "./plan-extensions.js";
|
|
25
25
|
import { policyColonInvocation } from "./policy-invocation.js";
|
|
26
26
|
import { appendAuditLog, loadProjectDefinition, POLICY_AUDIT_NOOP_STDOUT, projectDefinitionPath, } from "./resolve.js";
|
|
@@ -510,14 +510,10 @@ export function setCeremonyDial(projectRoot, options) {
|
|
|
510
510
|
changed: false,
|
|
511
511
|
};
|
|
512
512
|
}
|
|
513
|
-
const
|
|
513
|
+
const _path = projectDefinitionPath(projectRoot);
|
|
514
514
|
try {
|
|
515
|
-
const { changed } =
|
|
516
|
-
const
|
|
517
|
-
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
518
|
-
throw new Error(`PROJECT-DEFINITION at ${path} top-level value is not a JSON object`);
|
|
519
|
-
}
|
|
520
|
-
const data = parsed;
|
|
515
|
+
const { changed } = withProjectDefinitionMutation(projectRoot, (mutation) => {
|
|
516
|
+
const data = mutation.load();
|
|
521
517
|
if (typeof data.plan !== "object" || data.plan === null || Array.isArray(data.plan)) {
|
|
522
518
|
if (data.plan === undefined) {
|
|
523
519
|
data.plan = {};
|
|
@@ -569,7 +565,7 @@ export function setCeremonyDial(projectRoot, options) {
|
|
|
569
565
|
legacyKeyMigrated;
|
|
570
566
|
policyBlock.ceremonyDial = nextBlock;
|
|
571
567
|
if (changedFlag) {
|
|
572
|
-
|
|
568
|
+
mutation.persist(data);
|
|
573
569
|
}
|
|
574
570
|
const actor = options.actor ?? policyColonInvocation("set-ceremony-dial");
|
|
575
571
|
const note = options.note ?? "";
|
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import { readFileSync } from "node:fs";
|
|
2
1
|
import { HOOK_HOSTS } from "../hooks/dispatcher.js";
|
|
3
|
-
import {
|
|
2
|
+
import { withProjectDefinitionMutation } from "../vbrief-build/project-definition-mutation.js";
|
|
4
3
|
import { migrateLegacyPolicyKey, PLAN_POLICY_KEY, readPlanPolicy } from "./plan-extensions.js";
|
|
5
4
|
import { policyColonInvocation } from "./policy-invocation.js";
|
|
6
5
|
import { appendAuditLog, loadProjectDefinition, POLICY_AUDIT_NOOP_STDOUT, projectDefinitionPath, } from "./resolve.js";
|
|
@@ -145,12 +144,8 @@ export function disableHostHooks(projectRoot, options) {
|
|
|
145
144
|
}
|
|
146
145
|
const path = projectDefinitionPath(projectRoot);
|
|
147
146
|
try {
|
|
148
|
-
const { changed } =
|
|
149
|
-
const
|
|
150
|
-
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
151
|
-
throw new Error(`PROJECT-DEFINITION at ${path} top-level value is not a JSON object`);
|
|
152
|
-
}
|
|
153
|
-
const data = parsed;
|
|
147
|
+
const { changed } = withProjectDefinitionMutation(projectRoot, (mutation) => {
|
|
148
|
+
const data = mutation.load();
|
|
154
149
|
if (typeof data.plan !== "object" || data.plan === null || Array.isArray(data.plan)) {
|
|
155
150
|
if (data.plan === undefined) {
|
|
156
151
|
data.plan = {};
|
|
@@ -178,7 +173,7 @@ export function disableHostHooks(projectRoot, options) {
|
|
|
178
173
|
const changedFlag = previous[options.host] !== false || legacyKeyMigrated;
|
|
179
174
|
policyBlock.hostHooks = next;
|
|
180
175
|
if (changedFlag) {
|
|
181
|
-
|
|
176
|
+
mutation.persist(data);
|
|
182
177
|
}
|
|
183
178
|
const actor = options.actor ?? disableHostHooksInvocation();
|
|
184
179
|
const note = options.note ?? "";
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
import { join } from "node:path";
|
|
14
14
|
import { DEFAULT_EVENT_LOG, readEvents } from "../lifecycle/events.js";
|
|
15
15
|
import { captureExec } from "../pr-wait-mergeable/wrappers.js";
|
|
16
|
-
import {
|
|
16
|
+
import { resolveBinaryForArgv } from "../scm/call-shape.js";
|
|
17
17
|
import { resolveHumanMergePolicy } from "./require-human-merge.js";
|
|
18
18
|
/** Event name recorded at Phase 5 → 6 approval. */
|
|
19
19
|
export const PLAN_APPROVED_EVENT = "plan:approved";
|
|
@@ -358,14 +358,14 @@ export function evaluateMergeApprovalHead(input) {
|
|
|
358
358
|
}
|
|
359
359
|
/** Live `gh pr merge <N> --disable-auto` (best-effort). */
|
|
360
360
|
export function disablePullRequestAutoMerge(prNumber, repo, options = {}) {
|
|
361
|
+
const args = ["pr", "merge", String(prNumber), "--disable-auto"];
|
|
361
362
|
let binary;
|
|
362
363
|
try {
|
|
363
|
-
binary =
|
|
364
|
+
binary = resolveBinaryForArgv("pr", args.slice(1));
|
|
364
365
|
}
|
|
365
366
|
catch {
|
|
366
367
|
return { ok: false, stderr: "gh CLI not found" };
|
|
367
368
|
}
|
|
368
|
-
const args = ["pr", "merge", String(prNumber), "--disable-auto"];
|
|
369
369
|
if (repo !== null && repo.length > 0) {
|
|
370
370
|
args.push("--repo", repo);
|
|
371
371
|
}
|
|
@@ -388,15 +388,16 @@ export function disablePullRequestAutoMerge(prNumber, repo, options = {}) {
|
|
|
388
388
|
export function fetchPrHeadShaRest(prNumber, repo, options = {}) {
|
|
389
389
|
if (repo === null || repo.length === 0)
|
|
390
390
|
return null;
|
|
391
|
+
const endpoint = `repos/${repo}/pulls/${prNumber}`;
|
|
392
|
+
const apiArgs = ["api", endpoint, "--jq", ".head.sha"];
|
|
391
393
|
let binary;
|
|
392
394
|
try {
|
|
393
|
-
binary =
|
|
395
|
+
binary = resolveBinaryForArgv(apiArgs[0] ?? "api", apiArgs.slice(1));
|
|
394
396
|
}
|
|
395
397
|
catch {
|
|
396
398
|
return null;
|
|
397
399
|
}
|
|
398
|
-
const
|
|
399
|
-
const result = captureExec(binary, ["api", endpoint, "--jq", ".head.sha"], (options.timeoutSec ?? 60) * 1000);
|
|
400
|
+
const result = captureExec(binary, apiArgs, (options.timeoutSec ?? 60) * 1000);
|
|
400
401
|
if (result.returncode !== 0)
|
|
401
402
|
return null;
|
|
402
403
|
return normalizeSha(result.stdout.trim());
|
|
@@ -3,10 +3,10 @@ import { join, resolve } from "node:path";
|
|
|
3
3
|
import { sortKeysDeep } from "../codebase/json.js";
|
|
4
4
|
import { readCorePackageVersion } from "../engine-version.js";
|
|
5
5
|
import { containedWrite } from "../fs/contained-write.js";
|
|
6
|
-
import {
|
|
6
|
+
import { withProjectDefinitionMutation } from "../vbrief-build/project-definition-mutation.js";
|
|
7
7
|
import { isNoDeftDirectivePresent } from "./no-deft-directive.js";
|
|
8
8
|
import { migrateLegacyPolicyKey, PLAN_POLICY_KEY, readPlanPolicy } from "./plan-extensions.js";
|
|
9
|
-
import { appendAuditLog, loadProjectDefinition
|
|
9
|
+
import { appendAuditLog, loadProjectDefinition } from "./resolve.js";
|
|
10
10
|
import { detectOriginOrg, isTrustedOrgAutoEnable, } from "./value-feedback-autoenable.js";
|
|
11
11
|
/** Durable once-per-checkout marker for #2822 trusted-org install force-on. */
|
|
12
12
|
export const ORG_FORCE_ON_MARKER_REL = join(".deft-cache", "org-force-on-v2822.json");
|
|
@@ -331,18 +331,13 @@ export function runOrgForceOnMigration(projectRoot, options = {}) {
|
|
|
331
331
|
return applyForceOn(projectRoot, options, { existingMarker: null });
|
|
332
332
|
}
|
|
333
333
|
function applyForceOn(projectRoot, options, intent) {
|
|
334
|
-
const path = projectDefinitionPath(projectRoot);
|
|
335
334
|
let valueFeedbackChanged = false;
|
|
336
335
|
let productSignalChanged = false;
|
|
337
336
|
let ran = false;
|
|
338
337
|
let skippedReason = null;
|
|
339
338
|
const existing = intent.existingMarker;
|
|
340
|
-
|
|
341
|
-
const
|
|
342
|
-
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
343
|
-
throw new Error(`PROJECT-DEFINITION at ${path} top-level value is not a JSON object`);
|
|
344
|
-
}
|
|
345
|
-
const rootData = parsed;
|
|
339
|
+
withProjectDefinitionMutation(projectRoot, (mutation) => {
|
|
340
|
+
const rootData = mutation.load();
|
|
346
341
|
if (typeof rootData.plan !== "object" ||
|
|
347
342
|
rootData.plan === null ||
|
|
348
343
|
Array.isArray(rootData.plan)) {
|
|
@@ -402,7 +397,7 @@ function applyForceOn(projectRoot, options, intent) {
|
|
|
402
397
|
productSignalChanged = productSignalNeedsForceOn(previousPs);
|
|
403
398
|
}
|
|
404
399
|
if (valueFeedbackChanged || productSignalChanged) {
|
|
405
|
-
|
|
400
|
+
mutation.persist(rootData);
|
|
406
401
|
}
|
|
407
402
|
// Marker previous*: forced fields record pre-apply current; unforced fields
|
|
408
403
|
// keep the prior marker snapshot so intentional opt-outs are not rewritten
|
|
@@ -54,11 +54,23 @@ export interface ShadowedPlanExtension {
|
|
|
54
54
|
/** The bare legacy key that is silently ignored (e.g. `policy`). */
|
|
55
55
|
readonly legacyKey: string;
|
|
56
56
|
/**
|
|
57
|
-
*
|
|
58
|
-
* `["triageScope", "wipCap"]`). Empty when the bare value is not
|
|
57
|
+
* Complete, sorted list of the exact sub-keys present in the shadowed bare
|
|
58
|
+
* object (e.g. `["triageScope", "wipCap"]`). Empty when the bare value is not
|
|
59
|
+
* an object.
|
|
60
|
+
*
|
|
61
|
+
* These are raw semantic keys, kept whole so machine-readable inventories
|
|
62
|
+
* (the doctor finding's `shadowed_sub_keys`) stay complete. Bounding and
|
|
63
|
+
* redaction belong at a display boundary -- see
|
|
64
|
+
* {@link summarizeShadowedPlanSubKeys} (#3796).
|
|
59
65
|
*/
|
|
60
66
|
readonly shadowedSubKeys: readonly string[];
|
|
61
67
|
}
|
|
68
|
+
/**
|
|
69
|
+
* Bounded, redacted presentation of shadowed sub-keys for a human diagnostic.
|
|
70
|
+
* Callers keep the raw semantic inventory; only the rendered string is capped
|
|
71
|
+
* and sanitised (#3796).
|
|
72
|
+
*/
|
|
73
|
+
export declare function summarizeShadowedPlanSubKeys(keys: readonly string[]): string[];
|
|
62
74
|
/**
|
|
63
75
|
* Detect every plan-extension key where a bare (legacy) block coexists with the
|
|
64
76
|
* namespaced form (#2301). Because `readPlanExtension` is namespace-first, the
|
|
@@ -77,6 +77,26 @@ export const SHADOWABLE_PLAN_EXTENSIONS = [
|
|
|
77
77
|
{ namespacedKey: PLAN_POLICY_KEY, legacyKey: LEGACY_PLAN_POLICY_KEY },
|
|
78
78
|
{ namespacedKey: PLAN_COMPLETED_NOTE_KEY, legacyKey: LEGACY_PLAN_COMPLETED_NOTE_KEY },
|
|
79
79
|
];
|
|
80
|
+
const SAFE_DIAGNOSTIC_KEY = /^[A-Za-z][A-Za-z0-9._/-]{0,31}$/;
|
|
81
|
+
const SENSITIVE_DIAGNOSTIC_KEY = /(?:auth|credential|password|private|secret|token|api.?key)/i;
|
|
82
|
+
const MAX_DIAGNOSTIC_KEYS = 8;
|
|
83
|
+
/**
|
|
84
|
+
* Bounded, redacted presentation of shadowed sub-keys for a human diagnostic.
|
|
85
|
+
* Callers keep the raw semantic inventory; only the rendered string is capped
|
|
86
|
+
* and sanitised (#3796).
|
|
87
|
+
*/
|
|
88
|
+
export function summarizeShadowedPlanSubKeys(keys) {
|
|
89
|
+
const ordered = [...keys].sort();
|
|
90
|
+
const summary = ordered
|
|
91
|
+
.slice(0, MAX_DIAGNOSTIC_KEYS)
|
|
92
|
+
.map((key) => SAFE_DIAGNOSTIC_KEY.test(key) && !SENSITIVE_DIAGNOSTIC_KEY.test(key)
|
|
93
|
+
? key
|
|
94
|
+
: `<redacted-key length=${[...key].length}>`);
|
|
95
|
+
if (ordered.length > MAX_DIAGNOSTIC_KEYS) {
|
|
96
|
+
summary.push(`<${ordered.length - MAX_DIAGNOSTIC_KEYS} more keys>`);
|
|
97
|
+
}
|
|
98
|
+
return summary;
|
|
99
|
+
}
|
|
80
100
|
/**
|
|
81
101
|
* Detect every plan-extension key where a bare (legacy) block coexists with the
|
|
82
102
|
* namespaced form (#2301). Because `readPlanExtension` is namespace-first, the
|
|
@@ -96,7 +116,7 @@ export function detectShadowedPlanExtensions(plan) {
|
|
|
96
116
|
}
|
|
97
117
|
const legacyValue = planObj[legacyKey];
|
|
98
118
|
const shadowedSubKeys = typeof legacyValue === "object" && legacyValue !== null && !Array.isArray(legacyValue)
|
|
99
|
-
? Object.keys(legacyValue)
|
|
119
|
+
? Object.keys(legacyValue).sort()
|
|
100
120
|
: [];
|
|
101
121
|
shadows.push({ namespacedKey, legacyKey, shadowedSubKeys });
|
|
102
122
|
}
|
|
@@ -108,8 +128,9 @@ export function detectShadowedPlanExtensions(plan) {
|
|
|
108
128
|
* prefix it (`[policy:show]`, a doctor finding, ...).
|
|
109
129
|
*/
|
|
110
130
|
export function describeShadowedPlanExtension(shadow) {
|
|
111
|
-
const
|
|
112
|
-
|
|
131
|
+
const displaySubKeys = summarizeShadowedPlanSubKeys(shadow.shadowedSubKeys);
|
|
132
|
+
const subKeys = displaySubKeys.length > 0
|
|
133
|
+
? ` Shadowed field(s): ${displaySubKeys
|
|
113
134
|
.map((k) => `plan.${shadow.legacyKey}.${k}`)
|
|
114
135
|
.join(", ")}.`
|
|
115
136
|
: "";
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { atomicWriteProjectDefinition, projectDefinitionMutationLock, } from "../vbrief-build/project-definition-io.js";
|
|
1
|
+
import { withProjectDefinitionMutation } from "../vbrief-build/project-definition-mutation.js";
|
|
3
2
|
import { productSignalInstallForceOnSource } from "./org-force-on-migration.js";
|
|
4
3
|
import { migrateLegacyPolicyKey, PLAN_POLICY_KEY, readPlanPolicy } from "./plan-extensions.js";
|
|
5
4
|
import { policyColonInvocation } from "./policy-invocation.js";
|
|
@@ -140,14 +139,10 @@ export function enableProductSignal(projectRoot, options) {
|
|
|
140
139
|
changed: false,
|
|
141
140
|
};
|
|
142
141
|
}
|
|
143
|
-
const
|
|
142
|
+
const _path = projectDefinitionPath(projectRoot);
|
|
144
143
|
try {
|
|
145
|
-
const { changed } =
|
|
146
|
-
const
|
|
147
|
-
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
148
|
-
throw new Error(`PROJECT-DEFINITION at ${path} top-level value is not a JSON object`);
|
|
149
|
-
}
|
|
150
|
-
const data = parsed;
|
|
144
|
+
const { changed } = withProjectDefinitionMutation(projectRoot, (mutation) => {
|
|
145
|
+
const data = mutation.load();
|
|
151
146
|
if (typeof data.plan !== "object" || data.plan === null || Array.isArray(data.plan)) {
|
|
152
147
|
if (data.plan === undefined) {
|
|
153
148
|
data.plan = {};
|
|
@@ -185,7 +180,7 @@ export function enableProductSignal(projectRoot, options) {
|
|
|
185
180
|
legacyKeyMigrated;
|
|
186
181
|
policyBlock.productSignal = nextBlock;
|
|
187
182
|
if (changedFlag) {
|
|
188
|
-
|
|
183
|
+
mutation.persist(data);
|
|
189
184
|
}
|
|
190
185
|
const actor = options.actor ?? "task product-signal:enable";
|
|
191
186
|
const note = options.note ?? "";
|
|
@@ -14,8 +14,8 @@
|
|
|
14
14
|
* `merge-approval-head.ts` (stale head → fail closed + disable auto-merge).
|
|
15
15
|
* That gate is complementary to this policy surface and does not replace it.
|
|
16
16
|
*/
|
|
17
|
-
import { existsSync
|
|
18
|
-
import {
|
|
17
|
+
import { existsSync } from "node:fs";
|
|
18
|
+
import { withProjectDefinitionMutation } from "../vbrief-build/project-definition-mutation.js";
|
|
19
19
|
import { migrateLegacyPolicyKey, PLAN_POLICY_KEY, readPlanPolicy } from "./plan-extensions.js";
|
|
20
20
|
import { policyColonInvocation } from "./policy-invocation.js";
|
|
21
21
|
import { appendAuditLog, loadProjectDefinition, projectDefinitionPath, stampChangedToken, } from "./resolve.js";
|
|
@@ -270,12 +270,8 @@ export function setRequireHumanMerge(projectRoot, options) {
|
|
|
270
270
|
if (!existsSync(path)) {
|
|
271
271
|
throw new Error(`PROJECT-DEFINITION not found at ${path}`);
|
|
272
272
|
}
|
|
273
|
-
return
|
|
274
|
-
const
|
|
275
|
-
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
276
|
-
throw new Error(`PROJECT-DEFINITION at ${path} top-level value is not a JSON object`);
|
|
277
|
-
}
|
|
278
|
-
const data = parsed;
|
|
273
|
+
return withProjectDefinitionMutation(projectRoot, (mutation) => {
|
|
274
|
+
const data = mutation.load();
|
|
279
275
|
if (typeof data.plan !== "object" || data.plan === null || Array.isArray(data.plan)) {
|
|
280
276
|
if (data.plan === undefined) {
|
|
281
277
|
data.plan = {};
|
|
@@ -322,7 +318,7 @@ export function setRequireHumanMerge(projectRoot, options) {
|
|
|
322
318
|
}
|
|
323
319
|
const auditEntry = stampChangedToken(parts.join(" "), changed);
|
|
324
320
|
if (changed) {
|
|
325
|
-
|
|
321
|
+
mutation.persist(data);
|
|
326
322
|
}
|
|
327
323
|
appendAuditLog(projectRoot, auditEntry, changed);
|
|
328
324
|
return { changed, auditEntry };
|
package/dist/policy/resolve.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from "node:fs";
|
|
2
2
|
import { join, resolve as pathResolve } from "node:path";
|
|
3
3
|
import { containedWrite } from "../fs/contained-write.js";
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import { migrateLegacyPolicyKey, PLAN_POLICY_KEY, readPlanPolicy } from "./plan-extensions.js";
|
|
4
|
+
import { projectDefinitionPath as resolveProjectDefinitionArtifactPath } from "../vbrief-build/project-definition-io.js";
|
|
5
|
+
import { withProjectDefinitionMutation } from "../vbrief-build/project-definition-mutation.js";
|
|
6
|
+
import { describeShadowedPlanExtension, detectShadowedPlanExtensions, LEGACY_PLAN_POLICY_KEY, migrateLegacyPolicyKey, PLAN_POLICY_KEY, readPlanPolicy, } from "./plan-extensions.js";
|
|
7
7
|
import { policyColonInvocation } from "./policy-invocation.js";
|
|
8
8
|
/** Filesystem-relative location of the project-definition xBRIEF (display/back-compat). */
|
|
9
9
|
export const PROJECT_DEFINITION_REL_PATH = "xbrief/PROJECT-DEFINITION.xbrief.json";
|
|
@@ -32,11 +32,13 @@ const TRUTHY = new Set(["1", "true", "yes", "on"]);
|
|
|
32
32
|
export function projectDefinitionPath(projectRoot) {
|
|
33
33
|
const root = pathResolve(projectRoot);
|
|
34
34
|
try {
|
|
35
|
-
return
|
|
35
|
+
return resolveProjectDefinitionArtifactPath(root);
|
|
36
36
|
}
|
|
37
37
|
catch {
|
|
38
|
-
//
|
|
39
|
-
//
|
|
38
|
+
// Policy reads remain fail-closed on pre-migration trees. The shared resolver
|
|
39
|
+
// intentionally throws for legacy-only lifecycle layouts, but policy callers
|
|
40
|
+
// need a predictable missing canonical path rather than an exception while
|
|
41
|
+
// init/update is still responsible for reporting the migration requirement.
|
|
40
42
|
return join(root, PROJECT_DEFINITION_REL_PATH);
|
|
41
43
|
}
|
|
42
44
|
}
|
|
@@ -70,6 +72,66 @@ function pythonRepr(value) {
|
|
|
70
72
|
return value ? "True" : "False";
|
|
71
73
|
return String(value);
|
|
72
74
|
}
|
|
75
|
+
/** Render configuration values without exposing arbitrary string/object content. */
|
|
76
|
+
function diagnosticValue(value) {
|
|
77
|
+
if (value === undefined || value === null)
|
|
78
|
+
return "None";
|
|
79
|
+
if (typeof value === "boolean")
|
|
80
|
+
return value ? "True" : "False";
|
|
81
|
+
if (typeof value === "number")
|
|
82
|
+
return Number.isFinite(value) ? String(value) : "<number>";
|
|
83
|
+
if (typeof value === "string")
|
|
84
|
+
return `<string length=${[...value].length}>`;
|
|
85
|
+
if (Array.isArray(value))
|
|
86
|
+
return `<list length=${value.length}>`;
|
|
87
|
+
if (typeof value === "object")
|
|
88
|
+
return `<dict keys=${Object.keys(value).length}>`;
|
|
89
|
+
return `<${typeof value}>`;
|
|
90
|
+
}
|
|
91
|
+
function objectKeys(value) {
|
|
92
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
93
|
+
return [];
|
|
94
|
+
}
|
|
95
|
+
return Object.keys(value).sort();
|
|
96
|
+
}
|
|
97
|
+
function keyInventory(value) {
|
|
98
|
+
return typeof value === "object" && value !== null && !Array.isArray(value)
|
|
99
|
+
? `<dict keys=${Object.keys(value).length}>`
|
|
100
|
+
: diagnosticValue(value);
|
|
101
|
+
}
|
|
102
|
+
function branchPolicyValue(value) {
|
|
103
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
104
|
+
return diagnosticValue(value);
|
|
105
|
+
}
|
|
106
|
+
return diagnosticValue(value.allowDirectCommitsToMaster);
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Refuse a policy write when the legacy and namespaced blocks coexist (#3609).
|
|
110
|
+
*
|
|
111
|
+
* A writer cannot safely decide how to combine arbitrary policy keys or resolve
|
|
112
|
+
* collisions. The diagnostic therefore inventories keys, exposes only the one
|
|
113
|
+
* value this writer owns, and gives the operator a lossless recovery sequence.
|
|
114
|
+
*/
|
|
115
|
+
function assertPolicyBlockIsUnambiguous(plan) {
|
|
116
|
+
const shadow = detectShadowedPlanExtensions(plan).find((candidate) => candidate.namespacedKey === PLAN_POLICY_KEY);
|
|
117
|
+
if (shadow === undefined) {
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
const namespaced = plan[PLAN_POLICY_KEY];
|
|
121
|
+
const legacy = plan[LEGACY_PLAN_POLICY_KEY];
|
|
122
|
+
const namespacedKeys = objectKeys(namespaced);
|
|
123
|
+
const legacyKeys = objectKeys(legacy);
|
|
124
|
+
const namespacedKeySet = new Set(namespacedKeys);
|
|
125
|
+
const collisions = legacyKeys.filter((key) => namespacedKeySet.has(key));
|
|
126
|
+
throw new Error(`${describeShadowedPlanExtension(shadow)} ` +
|
|
127
|
+
`Key inventory: namespaced=${keyInventory(namespaced)}; ` +
|
|
128
|
+
`bare=${keyInventory(legacy)}; collisions=<list length=${collisions.length}>. ` +
|
|
129
|
+
`Relevant branch-policy values: namespaced=${branchPolicyValue(namespaced)}; ` +
|
|
130
|
+
`bare=${branchPolicyValue(legacy)}. ` +
|
|
131
|
+
`Recovery: inventory both blocks; fold every bare-only key into ` +
|
|
132
|
+
`\`plan.${PLAN_POLICY_KEY}\`; explicitly resolve every collision; delete ` +
|
|
133
|
+
`\`plan.${LEGACY_PLAN_POLICY_KEY}\`; then rerun the policy command. No changes were written.`);
|
|
134
|
+
}
|
|
73
135
|
/** Best-effort coerce a legacy narrative value to a boolean. */
|
|
74
136
|
export function coerceLegacyNarrative(value) {
|
|
75
137
|
if (typeof value === "boolean") {
|
|
@@ -147,7 +209,7 @@ export function resolvePolicy(projectRoot) {
|
|
|
147
209
|
allowDirectCommits: false,
|
|
148
210
|
source: "default-fail-closed",
|
|
149
211
|
deprecationWarning: null,
|
|
150
|
-
error: `plan.policy.allowDirectCommitsToMaster must be a boolean; got ${pythonTypeName(raw)} (${
|
|
212
|
+
error: `plan.policy.allowDirectCommitsToMaster must be a boolean; got ${pythonTypeName(raw)} (${diagnosticValue(raw)})`,
|
|
151
213
|
};
|
|
152
214
|
}
|
|
153
215
|
return {
|
|
@@ -164,7 +226,7 @@ export function resolvePolicy(projectRoot) {
|
|
|
164
226
|
LEGACY_NARRATIVE_KEY in narratives) {
|
|
165
227
|
const { allow, raw } = coerceLegacyNarrative(narratives[LEGACY_NARRATIVE_KEY]);
|
|
166
228
|
const warn = `DEPRECATED: PROJECT-DEFINITION uses the legacy narrative key ` +
|
|
167
|
-
`'${LEGACY_NARRATIVE_KEY}' (${
|
|
229
|
+
`'${LEGACY_NARRATIVE_KEY}' (${diagnosticValue(raw)}). Migrate to typed ` +
|
|
168
230
|
`plan.policy.allowDirectCommitsToMaster (#746). Run ` +
|
|
169
231
|
`\`${policyColonInvocation("enforce-branches")}\` or ` +
|
|
170
232
|
`\`${policyColonInvocation("allow-direct-commits", " -- --confirm")}\` ` +
|
|
@@ -224,12 +286,8 @@ export function setPolicy(projectRoot, options) {
|
|
|
224
286
|
// Serialise the read-modify-write + audit-log append behind the shared
|
|
225
287
|
// PROJECT-DEFINITION mutation lock so a concurrent policy/ritual mutator
|
|
226
288
|
// cannot lose this update or desync the typed flag from the audit row (#1260).
|
|
227
|
-
return
|
|
228
|
-
const
|
|
229
|
-
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
230
|
-
throw new Error(`PROJECT-DEFINITION at ${path} top-level value is not a JSON object`);
|
|
231
|
-
}
|
|
232
|
-
const data = parsed;
|
|
289
|
+
return withProjectDefinitionMutation(projectRoot, (mutation) => {
|
|
290
|
+
const data = mutation.load();
|
|
233
291
|
if (typeof data.plan !== "object" || data.plan === null || Array.isArray(data.plan)) {
|
|
234
292
|
if (data.plan === undefined) {
|
|
235
293
|
data.plan = {};
|
|
@@ -239,6 +297,9 @@ export function setPolicy(projectRoot, options) {
|
|
|
239
297
|
}
|
|
240
298
|
}
|
|
241
299
|
const plan = data.plan;
|
|
300
|
+
// Repeat the shadow check inside the mutation lock so a concurrent writer
|
|
301
|
+
// cannot create a dual-block state between validation and persistence.
|
|
302
|
+
assertPolicyBlockIsUnambiguous(plan);
|
|
242
303
|
const legacyKeyMigrated = migrateLegacyPolicyKey(plan);
|
|
243
304
|
const existingPolicy = plan[PLAN_POLICY_KEY];
|
|
244
305
|
if (typeof existingPolicy !== "object" ||
|
|
@@ -267,7 +328,7 @@ export function setPolicy(projectRoot, options) {
|
|
|
267
328
|
const parts = [
|
|
268
329
|
`actor=${actor}`,
|
|
269
330
|
`allowDirectCommitsToMaster=${allowDirectCommits ? "true" : "false"}`,
|
|
270
|
-
`previous=${
|
|
331
|
+
`previous=${diagnosticValue(previous)}`,
|
|
271
332
|
];
|
|
272
333
|
if (legacyDropped) {
|
|
273
334
|
parts.push("legacy-narrative-migrated=true");
|
|
@@ -277,7 +338,7 @@ export function setPolicy(projectRoot, options) {
|
|
|
277
338
|
}
|
|
278
339
|
const auditEntry = stampChangedToken(parts.join(" "), changed);
|
|
279
340
|
if (changed) {
|
|
280
|
-
|
|
341
|
+
mutation.persist(data);
|
|
281
342
|
}
|
|
282
343
|
appendAuditLog(projectRoot, auditEntry, changed);
|
|
283
344
|
return { changed, auditEntry };
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { atomicWriteProjectDefinition, projectDefinitionMutationLock, } from "../vbrief-build/project-definition-io.js";
|
|
1
|
+
import { withProjectDefinitionMutation } from "../vbrief-build/project-definition-mutation.js";
|
|
3
2
|
import { valueFeedbackInstallForceOnSource } from "./org-force-on-migration.js";
|
|
4
3
|
import { migrateLegacyPolicyKey, PLAN_POLICY_KEY, readPlanPolicy } from "./plan-extensions.js";
|
|
5
4
|
import { policyColonInvocation } from "./policy-invocation.js";
|
|
@@ -219,14 +218,10 @@ export function enableValueFeedback(projectRoot, options) {
|
|
|
219
218
|
changed: false,
|
|
220
219
|
};
|
|
221
220
|
}
|
|
222
|
-
const
|
|
221
|
+
const _path = projectDefinitionPath(projectRoot);
|
|
223
222
|
try {
|
|
224
|
-
const { changed } =
|
|
225
|
-
const
|
|
226
|
-
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
227
|
-
throw new Error(`PROJECT-DEFINITION at ${path} top-level value is not a JSON object`);
|
|
228
|
-
}
|
|
229
|
-
const data = parsed;
|
|
223
|
+
const { changed } = withProjectDefinitionMutation(projectRoot, (mutation) => {
|
|
224
|
+
const data = mutation.load();
|
|
230
225
|
if (typeof data.plan !== "object" || data.plan === null || Array.isArray(data.plan)) {
|
|
231
226
|
if (data.plan === undefined) {
|
|
232
227
|
data.plan = {};
|
|
@@ -277,7 +272,7 @@ export function enableValueFeedback(projectRoot, options) {
|
|
|
277
272
|
legacyKeyMigrated;
|
|
278
273
|
policyBlock.valueFeedback = nextBlock;
|
|
279
274
|
if (changedFlag) {
|
|
280
|
-
|
|
275
|
+
mutation.persist(data);
|
|
281
276
|
}
|
|
282
277
|
const actor = options.actor ?? policyColonInvocation("enable-value-feedback");
|
|
283
278
|
const note = options.note ?? "";
|
|
@@ -317,14 +312,10 @@ export function enableValueFeedback(projectRoot, options) {
|
|
|
317
312
|
* updates will not re-force against intentional opt-out.
|
|
318
313
|
*/
|
|
319
314
|
export function clearValueFeedback(projectRoot, options = {}) {
|
|
320
|
-
const
|
|
315
|
+
const _path = projectDefinitionPath(projectRoot);
|
|
321
316
|
try {
|
|
322
|
-
const { changed } =
|
|
323
|
-
const
|
|
324
|
-
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
325
|
-
throw new Error(`PROJECT-DEFINITION at ${path} top-level value is not a JSON object`);
|
|
326
|
-
}
|
|
327
|
-
const data = parsed;
|
|
317
|
+
const { changed } = withProjectDefinitionMutation(projectRoot, (mutation) => {
|
|
318
|
+
const data = mutation.load();
|
|
328
319
|
if (typeof data.plan !== "object" || data.plan === null || Array.isArray(data.plan)) {
|
|
329
320
|
throw new Error("PROJECT-DEFINITION 'plan' is not an object");
|
|
330
321
|
}
|
|
@@ -342,7 +333,7 @@ export function clearValueFeedback(projectRoot, options = {}) {
|
|
|
342
333
|
}
|
|
343
334
|
const previous = policyBlock.valueFeedback;
|
|
344
335
|
delete policyBlock.valueFeedback;
|
|
345
|
-
|
|
336
|
+
mutation.persist(data);
|
|
346
337
|
const actor = options.actor ?? policyColonInvocation("clear-value-feedback");
|
|
347
338
|
const note = options.note ?? "";
|
|
348
339
|
const parts = [
|