@deftai/directive-core 0.106.0 → 0.108.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/check/cached-orchestrator.js +28 -3
- package/dist/check/gate-lists.js +2 -0
- package/dist/check/named-cause.js +18 -3
- package/dist/codebase/provider.js +7 -1
- package/dist/delivery-attempt/handoff.js +1 -1
- package/dist/design-critique/citation-grammar.d.ts +53 -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 +232 -0
- package/dist/design-critique/exclusive-chip.d.ts +33 -0
- package/dist/design-critique/exclusive-chip.js +68 -0
- package/dist/design-critique/parent-audit.d.ts +59 -0
- package/dist/design-critique/parent-audit.js +121 -0
- package/dist/doctor/checks.d.ts +6 -0
- package/dist/doctor/checks.js +35 -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 +76 -0
- package/dist/hooks/classify/host-session-identity.js +470 -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 +7 -0
- package/dist/hooks/classify/paths.js +35 -0
- package/dist/hooks/classify/stdin.d.ts +5 -0
- package/dist/hooks/classify/stdin.js +27 -0
- package/dist/hooks/dest-form.d.ts +46 -0
- package/dist/hooks/dest-form.js +575 -0
- package/dist/hooks/dispatcher.d.ts +69 -2
- package/dist/hooks/dispatcher.js +609 -45
- package/dist/hooks/fixtures/cases.d.ts +16 -2
- package/dist/hooks/fixtures/cases.js +175 -0
- package/dist/hooks/index.d.ts +1 -0
- package/dist/hooks/index.js +1 -0
- package/dist/hooks/scope.js +3 -1
- package/dist/init-deposit/agent-hooks.d.ts +2 -1
- package/dist/init-deposit/agent-hooks.js +8 -1
- package/dist/init-deposit/hook-runtime-travel.d.ts +77 -0
- package/dist/init-deposit/hook-runtime-travel.js +269 -0
- package/dist/intake/clause-derivation.js +9 -3
- package/dist/intake/github-auth-modes-cli.js +14 -2
- package/dist/intake/github-auth-modes.d.ts +54 -9
- package/dist/intake/github-auth-modes.js +326 -123
- package/dist/intake/issue-ingest.js +62 -2
- package/dist/intake/platform-capabilities.d.ts +9 -2
- package/dist/intake/platform-capabilities.js +38 -9
- package/dist/lifecycle/completed-tracked-on-delivery.d.ts +26 -0
- package/dist/lifecycle/completed-tracked-on-delivery.js +37 -14
- package/dist/lifecycle/completed-write-guard.d.ts +48 -0
- package/dist/lifecycle/completed-write-guard.js +373 -0
- package/dist/lifecycle/index.d.ts +1 -0
- package/dist/lifecycle/index.js +1 -0
- package/dist/literal-acceptance/capture.d.ts +16 -0
- package/dist/literal-acceptance/capture.js +75 -7
- package/dist/literal-acceptance/evaluate.js +25 -3
- package/dist/literal-acceptance/index.d.ts +2 -2
- package/dist/literal-acceptance/index.js +2 -2
- package/dist/literal-acceptance/run.js +9 -3
- package/dist/literal-acceptance/types.d.ts +6 -0
- package/dist/literal-acceptance/types.js +6 -0
- package/dist/orphan-active/evaluate.d.ts +25 -0
- package/dist/orphan-active/evaluate.js +193 -128
- package/dist/orphan-active/index.d.ts +1 -0
- package/dist/orphan-active/index.js +1 -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/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/runtime-authority.d.ts +18 -0
- package/dist/policy/runtime-authority.js +11 -0
- package/dist/policy/value-feedback.js +9 -18
- package/dist/policy/write-fence.js +3 -0
- 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-protected-issues/gh.d.ts +6 -2
- package/dist/pr-protected-issues/gh.js +7 -3
- 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 +11 -0
- package/dist/preflight/evaluate.d.ts +15 -0
- package/dist/preflight/evaluate.js +29 -3
- package/dist/preflight-cache/evaluate.js +9 -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 +53 -12
- package/dist/release/cli-drift-report.d.ts +68 -0
- package/dist/release/cli-drift-report.js +189 -0
- package/dist/release/index.d.ts +1 -0
- package/dist/release/index.js +1 -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 +19 -0
- package/dist/release/types.d.ts +14 -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 +182 -1
- package/dist/review-monitor/lease-comment.d.ts +36 -0
- package/dist/review-monitor/lease-comment.js +103 -2
- package/dist/scm/call.js +12 -3
- package/dist/scm/design-critique-chip.d.ts +38 -0
- package/dist/scm/design-critique-chip.js +187 -0
- package/dist/scm/gh-rest.d.ts +9 -1
- package/dist/scm/gh-rest.js +103 -0
- package/dist/scm/index.d.ts +1 -0
- package/dist/scm/index.js +1 -0
- package/dist/scm/main.d.ts +3 -0
- package/dist/scm/main.js +15 -1
- package/dist/scm/readiness-cli.d.ts +2 -0
- package/dist/scm/readiness-cli.js +42 -1
- package/dist/scm/readiness.d.ts +4 -1
- package/dist/scm/readiness.js +35 -5
- package/dist/scope/decompose.js +2 -1
- package/dist/scope/delivery-evidence.d.ts +27 -1
- package/dist/scope/index.d.ts +1 -0
- package/dist/scope/index.js +1 -0
- package/dist/scope/lifecycle-write.d.ts +24 -0
- package/dist/scope/lifecycle-write.js +64 -0
- package/dist/scope/main.js +3 -1
- package/dist/scope/project-definition-sync.js +14 -7
- package/dist/scope/transition.js +90 -11
- package/dist/scope-provenance/evaluate.js +5 -1
- package/dist/session/git.d.ts +42 -0
- package/dist/session/git.js +219 -3
- package/dist/session/occupancy.d.ts +239 -12
- package/dist/session/occupancy.js +820 -27
- 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 +17 -1
- package/dist/session/session-start.js +49 -17
- package/dist/session/toolchain-preflight.d.ts +13 -3
- package/dist/session/toolchain-preflight.js +93 -18
- package/dist/session/verify-session-ritual.d.ts +33 -3
- package/dist/session/verify-session-ritual.js +89 -19
- 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 +77 -0
- package/dist/swarm/launch.js +248 -38
- 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/actions/index.d.ts +4 -0
- package/dist/triage/actions/index.js +6 -0
- package/dist/triage/evaluate/evaluate.d.ts +11 -0
- package/dist/triage/evaluate/evaluate.js +169 -0
- package/dist/triage/evaluate/github.d.ts +16 -0
- package/dist/triage/evaluate/github.js +153 -0
- package/dist/triage/evaluate/index.d.ts +9 -0
- package/dist/triage/evaluate/index.js +8 -0
- package/dist/triage/evaluate/paths.d.ts +7 -0
- package/dist/triage/evaluate/paths.js +25 -0
- package/dist/triage/evaluate/sink.d.ts +5 -0
- package/dist/triage/evaluate/sink.js +52 -0
- package/dist/triage/evaluate/types.d.ts +93 -0
- package/dist/triage/evaluate/types.js +13 -0
- package/dist/triage/evaluate/validity.d.ts +8 -0
- package/dist/triage/evaluate/validity.js +87 -0
- package/dist/triage/evaluate/value.d.ts +12 -0
- package/dist/triage/evaluate/value.js +33 -0
- package/dist/triage/evaluate/wip-census.d.ts +5 -0
- package/dist/triage/evaluate/wip-census.js +39 -0
- package/dist/triage/evaluate/worktrees.d.ts +8 -0
- package/dist/triage/evaluate/worktrees.js +68 -0
- package/dist/triage/evaluate/xbrief-refs.d.ts +8 -0
- package/dist/triage/evaluate/xbrief-refs.js +60 -0
- package/dist/triage/help/registry-data.d.ts +21 -7
- package/dist/triage/help/registry-data.js +53 -5
- package/dist/triage/index.d.ts +1 -0
- package/dist/triage/index.js +1 -0
- 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/value/feedback-file.d.ts +17 -2
- package/dist/value/feedback-file.js +80 -6
- 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-reconcile/labels.d.ts +1 -0
- package/dist/vbrief-reconcile/labels.js +30 -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/vbrief-validation/story-quality.d.ts +7 -0
- package/dist/vbrief-validation/story-quality.js +8 -1
- package/dist/verify-ac/clauses.d.ts +62 -2
- package/dist/verify-ac/clauses.js +183 -111
- 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/project-definition-mutation-boundary.d.ts +104 -0
- package/dist/verify-source/project-definition-mutation-boundary.js +326 -0
- package/dist/xbrief-migrate/agents-header.js +69 -7
- 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 ?? "";
|
|
@@ -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 };
|
|
@@ -11,6 +11,22 @@ export interface RuntimeAuthorityScopes {
|
|
|
11
11
|
}
|
|
12
12
|
/** Layers that contribute to a resolved write fence (#516 / #2443 / #2948 Wave 3). */
|
|
13
13
|
export type WriteFenceSource = "project" | "story";
|
|
14
|
+
/**
|
|
15
|
+
* Whether recognized Shell dest-forms are enforced (#3438 / #3594).
|
|
16
|
+
*
|
|
17
|
+
* `off` (default) leaves Shell mutations exactly as they were before #3438:
|
|
18
|
+
* unrecognized and fail-open. `enforce` routes recognized dest-forms through
|
|
19
|
+
* `inspectMutationGates` and fail-closes targets it cannot prove.
|
|
20
|
+
*
|
|
21
|
+
* ! Default is `off` on purpose. Before #3438 Bash mutations were not gated at
|
|
22
|
+
* all, so defaulting to `enforce` would land new denials on every consumer with
|
|
23
|
+
* no opt-out. Opting IN via tracked project policy is a narrowing, which this
|
|
24
|
+
* fence model permits; a tracked switch that DISABLED enforcement would not be
|
|
25
|
+
* (see policy/deft-directive-disable.ts — repository-controlled content must
|
|
26
|
+
* not disable hooks for downstream clones).
|
|
27
|
+
*/
|
|
28
|
+
export type ShellDestFormsMode = "off" | "enforce";
|
|
29
|
+
export declare const SHELL_DEST_FORMS_MODES: readonly ShellDestFormsMode[];
|
|
14
30
|
export interface RuntimeAuthorityPolicy {
|
|
15
31
|
readonly enabled: boolean;
|
|
16
32
|
/** When non-empty, write targets must match at least one pattern. Empty = allow all paths. */
|
|
@@ -18,6 +34,8 @@ export interface RuntimeAuthorityPolicy {
|
|
|
18
34
|
/** Deny wins over allow. */
|
|
19
35
|
readonly denyPaths: readonly string[];
|
|
20
36
|
readonly scopes: RuntimeAuthorityScopes;
|
|
37
|
+
/** Shell dest-form enforcement (#3438 / #3594). Default `off`. */
|
|
38
|
+
readonly shellDestForms: ShellDestFormsMode;
|
|
21
39
|
/**
|
|
22
40
|
* Optional story-layer allow globs from `plan.metadata.swarm.file_scope`
|
|
23
41
|
* (populated by `resolveWriteFence` only). When non-empty, path must match
|
|
@@ -3,6 +3,7 @@ import { readPlanPolicy } from "./plan-extensions.js";
|
|
|
3
3
|
import { loadProjectDefinition } from "./resolve.js";
|
|
4
4
|
export const FIELD_RUNTIME_AUTHORITY = "plan.policy.runtimeAuthority";
|
|
5
5
|
export const FIELD_RUNTIME_AUTHORITY_CLI_ALIAS = "runtimeAuthority";
|
|
6
|
+
export const SHELL_DEST_FORMS_MODES = ["off", "enforce"];
|
|
6
7
|
export const DEFAULT_RUNTIME_AUTHORITY_SCOPES = {
|
|
7
8
|
edits: true,
|
|
8
9
|
push: false,
|
|
@@ -14,6 +15,7 @@ export const DEFAULT_RUNTIME_AUTHORITY_POLICY = {
|
|
|
14
15
|
allowPaths: [],
|
|
15
16
|
denyPaths: [],
|
|
16
17
|
scopes: DEFAULT_RUNTIME_AUTHORITY_SCOPES,
|
|
18
|
+
shellDestForms: "off",
|
|
17
19
|
};
|
|
18
20
|
function readBoolean(rec, key, fallback) {
|
|
19
21
|
if (key in rec && typeof rec[key] === "boolean") {
|
|
@@ -37,6 +39,11 @@ function readScopes(raw) {
|
|
|
37
39
|
merge: readBoolean(rec, "merge", DEFAULT_RUNTIME_AUTHORITY_SCOPES.merge),
|
|
38
40
|
};
|
|
39
41
|
}
|
|
42
|
+
function readShellDestForms(raw) {
|
|
43
|
+
// Unknown / malformed resolves to the safe default rather than throwing, but
|
|
44
|
+
// validateRuntimeAuthority reports it so a typo is not silent (#3594).
|
|
45
|
+
return raw === "enforce" ? "enforce" : DEFAULT_RUNTIME_AUTHORITY_POLICY.shellDestForms;
|
|
46
|
+
}
|
|
40
47
|
export function resolveRuntimeAuthorityPolicy(raw) {
|
|
41
48
|
if (raw === null || raw === undefined) {
|
|
42
49
|
return DEFAULT_RUNTIME_AUTHORITY_POLICY;
|
|
@@ -50,6 +57,7 @@ export function resolveRuntimeAuthorityPolicy(raw) {
|
|
|
50
57
|
allowPaths: readStringArray(rec.allowPaths),
|
|
51
58
|
denyPaths: readStringArray(rec.denyPaths),
|
|
52
59
|
scopes: readScopes(rec.scopes),
|
|
60
|
+
shellDestForms: readShellDestForms(rec.shellDestForms),
|
|
53
61
|
};
|
|
54
62
|
}
|
|
55
63
|
export function validateRuntimeAuthority(value) {
|
|
@@ -69,6 +77,9 @@ export function validateRuntimeAuthority(value) {
|
|
|
69
77
|
errors.push(`${FIELD_RUNTIME_AUTHORITY}.${key} must be an array of path globs`);
|
|
70
78
|
}
|
|
71
79
|
}
|
|
80
|
+
if ("shellDestForms" in rec && !SHELL_DEST_FORMS_MODES.includes(rec.shellDestForms)) {
|
|
81
|
+
errors.push(`${FIELD_RUNTIME_AUTHORITY}.shellDestForms must be one of ${SHELL_DEST_FORMS_MODES.join(" | ")}`);
|
|
82
|
+
}
|
|
72
83
|
if ("scopes" in rec) {
|
|
73
84
|
const scopes = rec.scopes;
|
|
74
85
|
if (typeof scopes !== "object" || scopes === null || Array.isArray(scopes)) {
|