@deftai/directive-core 0.87.0 → 0.89.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/authz/actions.d.ts +55 -0
- package/dist/authz/actions.js +125 -0
- package/dist/authz/classify.d.ts +23 -0
- package/dist/authz/classify.js +217 -0
- package/dist/authz/closed-verb.d.ts +42 -0
- package/dist/authz/closed-verb.js +279 -0
- package/dist/authz/evaluate.d.ts +41 -0
- package/dist/authz/evaluate.js +298 -0
- package/dist/authz/index.d.ts +15 -0
- package/dist/authz/index.js +15 -0
- package/dist/authz/origin.d.ts +28 -0
- package/dist/authz/origin.js +64 -0
- package/dist/authz/paths.d.ts +6 -0
- package/dist/authz/paths.js +19 -0
- package/dist/authz/store.d.ts +40 -0
- package/dist/authz/store.js +317 -0
- package/dist/authz/templates.d.ts +139 -0
- package/dist/authz/templates.js +241 -0
- package/dist/authz/types.d.ts +111 -0
- package/dist/authz/types.js +41 -0
- package/dist/authz/verb-classification.d.ts +44 -0
- package/dist/authz/verb-classification.js +237 -0
- package/dist/branch/evaluate.js +6 -1
- package/dist/cache/fetch.js +10 -5
- package/dist/cache/io.d.ts +9 -2
- package/dist/cache/io.js +30 -10
- package/dist/cache/scanner.d.ts +11 -1
- package/dist/cache/scanner.js +29 -4
- package/dist/cache/task-cache/store.js +11 -7
- package/dist/capacity/backfill.js +10 -4
- package/dist/category-b-namespace/index.js +10 -4
- package/dist/check/gate-lists.js +1 -0
- package/dist/codebase/default-extractor.js +3 -0
- package/dist/codebase/map.js +11 -4
- package/dist/content-contracts/skills/helpers.d.ts +10 -0
- package/dist/content-contracts/skills/helpers.js +35 -0
- package/dist/deposit/copy-tree.d.ts +19 -1
- package/dist/deposit/copy-tree.js +134 -5
- package/dist/doctor/doctor-state.js +28 -4
- package/dist/doctor/main.d.ts +10 -0
- package/dist/doctor/main.js +208 -0
- package/dist/doctor/types.d.ts +11 -0
- package/dist/escalation/actions.d.ts +63 -0
- package/dist/escalation/actions.js +137 -0
- package/dist/escalation/index.d.ts +8 -0
- package/dist/escalation/index.js +8 -0
- package/dist/escalation/paths.d.ts +3 -0
- package/dist/escalation/paths.js +10 -0
- package/dist/escalation/store.d.ts +17 -0
- package/dist/escalation/store.js +172 -0
- package/dist/escalation/types.d.ts +73 -0
- package/dist/escalation/types.js +43 -0
- package/dist/eval/crud-telemetry.js +30 -10
- package/dist/eval/health.js +22 -7
- package/dist/eval/readback.js +11 -10
- package/dist/eval/run.js +32 -16
- package/dist/events/attribution-enrichment.js +10 -4
- package/dist/finish-loop/directive-finish-loop.d.ts +37 -0
- package/dist/finish-loop/directive-finish-loop.js +239 -0
- package/dist/finish-loop/grant-gate.d.ts +31 -0
- package/dist/finish-loop/grant-gate.js +169 -0
- package/dist/finish-loop/index.d.ts +11 -0
- package/dist/finish-loop/index.js +11 -0
- package/dist/finish-loop/main.d.ts +35 -0
- package/dist/finish-loop/main.js +365 -0
- package/dist/finish-loop/pr-finish-loop.d.ts +34 -0
- package/dist/finish-loop/pr-finish-loop.js +217 -0
- package/dist/finish-loop/progress.d.ts +17 -0
- package/dist/finish-loop/progress.js +45 -0
- package/dist/finish-loop/queue.d.ts +16 -0
- package/dist/finish-loop/queue.js +65 -0
- package/dist/finish-loop/types.d.ts +52 -0
- package/dist/finish-loop/types.js +10 -0
- package/dist/fs/contained-write.d.ts +101 -0
- package/dist/fs/contained-write.js +281 -0
- package/dist/fs/projection-containment.d.ts +18 -0
- package/dist/fs/projection-containment.js +40 -0
- package/dist/hooks/classify/classify.d.ts +10 -0
- package/dist/hooks/classify/classify.js +37 -0
- package/dist/hooks/classify/index.d.ts +14 -0
- package/dist/hooks/classify/index.js +13 -0
- package/dist/hooks/classify/paths.d.ts +22 -0
- package/dist/hooks/classify/paths.js +72 -0
- package/dist/hooks/classify/payload.d.ts +16 -0
- package/dist/hooks/classify/payload.js +45 -0
- package/dist/hooks/classify/stdin.d.ts +12 -0
- package/dist/hooks/classify/stdin.js +63 -0
- package/dist/hooks/classify/tool-name.d.ts +18 -0
- package/dist/hooks/classify/tool-name.js +85 -0
- package/dist/hooks/classify/types.d.ts +41 -0
- package/dist/hooks/classify/types.js +7 -0
- package/dist/hooks/dispatcher.d.ts +30 -15
- package/dist/hooks/dispatcher.js +326 -134
- package/dist/hooks/fixtures/cases.d.ts +44 -0
- package/dist/hooks/fixtures/cases.js +595 -0
- package/dist/hooks/fixtures/index.d.ts +2 -0
- package/dist/hooks/fixtures/index.js +2 -0
- package/dist/hooks/index.d.ts +2 -0
- package/dist/hooks/index.js +2 -0
- package/dist/hooks/tools.d.ts +31 -0
- package/dist/hooks/tools.js +74 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +5 -0
- package/dist/init-deposit/agent-hooks.d.ts +1 -1
- package/dist/init-deposit/agent-hooks.js +65 -10
- package/dist/init-deposit/gitignore.js +11 -3
- package/dist/init-deposit/headless-manifest.js +12 -4
- package/dist/init-deposit/hygiene.d.ts +16 -0
- package/dist/init-deposit/hygiene.js +26 -0
- package/dist/init-deposit/init-dispatch.js +28 -0
- package/dist/init-deposit/migrate.d.ts +1 -1
- package/dist/init-deposit/migrate.js +13 -3
- package/dist/init-deposit/prettierignore.js +12 -5
- package/dist/init-deposit/refresh.js +38 -7
- package/dist/init-deposit/scaffold.js +42 -29
- package/dist/init-deposit/xbrief-projections.js +17 -10
- package/dist/intake/candidates-log.js +46 -35
- package/dist/intake/github-body-cli.d.ts +6 -0
- package/dist/intake/github-body-cli.js +17 -0
- package/dist/intake/github-body.d.ts +46 -3
- package/dist/intake/github-body.js +191 -20
- package/dist/intake/issue-emit.d.ts +1 -0
- package/dist/intake/issue-emit.js +41 -23
- package/dist/intake/issue-ingest.js +11 -2
- package/dist/lifecycle/event-detect.js +12 -4
- package/dist/lifecycle/events.js +29 -20
- package/dist/lifecycle/lifecycle-hygiene.js +9 -3
- package/dist/packs/pack-render.d.ts +33 -0
- package/dist/packs/pack-render.js +172 -13
- package/dist/packs/quarantine-ext.d.ts +10 -0
- package/dist/packs/quarantine-ext.js +26 -2
- package/dist/plan-sequence/store.js +11 -4
- package/dist/policy/hotfix-criteria.d.ts +79 -0
- package/dist/policy/hotfix-criteria.js +197 -0
- package/dist/policy/index.d.ts +5 -0
- package/dist/policy/index.js +32 -1
- package/dist/policy/intent-ceiling.d.ts +79 -0
- package/dist/policy/intent-ceiling.js +181 -0
- package/dist/policy/no-deft-directive.d.ts +59 -0
- package/dist/policy/no-deft-directive.js +110 -0
- package/dist/policy/org-force-on-migration.d.ts +52 -0
- package/dist/policy/org-force-on-migration.js +269 -27
- package/dist/policy/require-human-merge.d.ts +75 -0
- package/dist/policy/require-human-merge.js +324 -0
- package/dist/policy/resolve.js +17 -6
- package/dist/policy/runtime-authority.d.ts +56 -2
- package/dist/policy/runtime-authority.js +297 -3
- package/dist/policy/write-fence.d.ts +78 -0
- package/dist/policy/write-fence.js +164 -0
- package/dist/pr-wait-mergeable/cascade.d.ts +10 -0
- package/dist/pr-wait-mergeable/cascade.js +28 -0
- package/dist/preflight/evaluate.js +10 -0
- package/dist/product-signal/consent.js +21 -5
- package/dist/product-signal/submit.js +22 -12
- package/dist/release/build-dist-runner.js +5 -2
- package/dist/release/build-dist.d.ts +19 -1
- package/dist/release/build-dist.js +50 -2
- package/dist/release/native-steps.js +7 -2
- package/dist/release-publish/pipeline.d.ts +13 -0
- package/dist/release-publish/pipeline.js +46 -1
- package/dist/scope/audit-log.js +19 -24
- package/dist/scope/decompose.js +10 -5
- package/dist/scope/decomposed-refs.js +18 -3
- package/dist/scope/demote.js +9 -2
- package/dist/scope/undo.js +9 -2
- package/dist/session/release-availability.js +26 -6
- package/dist/session/ritual-sentinel.js +19 -12
- package/dist/session/session-start-hook.d.ts +3 -0
- package/dist/session/session-start-hook.js +15 -0
- package/dist/session/session-start.js +37 -0
- package/dist/staleness-tickler/state.js +26 -6
- package/dist/swarm/launch.js +13 -3
- package/dist/swarm/routing.js +9 -3
- package/dist/task-surface/index.js +24 -9
- package/dist/triage/bootstrap/gitignore.js +53 -10
- package/dist/triage/cache-path.js +10 -3
- package/dist/triage/welcome/summary.js +11 -4
- package/dist/triage/welcome/writers.js +45 -16
- package/dist/validate-content/validate-links.js +5 -0
- package/dist/value/readback.js +11 -6
- package/dist/vbrief-activate/activate.js +12 -4
- package/dist/vbrief-build/project-definition-io.js +14 -6
- package/dist/verify-source/contained-writes.d.ts +59 -0
- package/dist/verify-source/contained-writes.js +272 -0
- package/dist/verify-source/cursor-tier1.js +7 -2
- package/dist/verify-source/index.d.ts +1 -0
- package/dist/verify-source/index.js +1 -0
- package/dist/verify-source/openclaw-tier1.js +19 -1
- package/dist/verify-source/verify-stubs.js +3 -0
- package/dist/xbrief-migrate/migrate-project.js +26 -12
- package/dist/xbrief-migrate/transforms.d.ts +4 -2
- package/dist/xbrief-migrate/transforms.js +37 -14
- package/package.json +16 -3
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
import { existsSync,
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
2
|
import { join, resolve } from "node:path";
|
|
3
|
+
import { sortKeysDeep } from "../codebase/json.js";
|
|
3
4
|
import { readCorePackageVersion } from "../engine-version.js";
|
|
4
|
-
import {
|
|
5
|
+
import { containedWrite } from "../fs/contained-write.js";
|
|
5
6
|
import { atomicWriteProjectDefinition, projectDefinitionMutationLock, } from "../vbrief-build/project-definition-io.js";
|
|
7
|
+
import { isNoDeftDirectivePresent } from "./no-deft-directive.js";
|
|
6
8
|
import { migrateLegacyPolicyKey, PLAN_POLICY_KEY, readPlanPolicy } from "./plan-extensions.js";
|
|
7
9
|
import { appendAuditLog, loadProjectDefinition, projectDefinitionPath } from "./resolve.js";
|
|
8
10
|
import { detectOriginOrg, isTrustedOrgAutoEnable, } from "./value-feedback-autoenable.js";
|
|
@@ -20,6 +22,18 @@ const DEFAULT_PRODUCT_SIGNAL_SINK = "deftai/product-signal";
|
|
|
20
22
|
function markerPath(projectRoot) {
|
|
21
23
|
return resolve(projectRoot, ORG_FORCE_ON_MARKER_REL);
|
|
22
24
|
}
|
|
25
|
+
/** Normalize undefined → null and deep-sort keys so marker snapshots compare stably. */
|
|
26
|
+
export function normalizePolicySnapshot(raw) {
|
|
27
|
+
return sortKeysDeep(raw === undefined ? null : raw);
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Structural equality for policy snapshots (#2903).
|
|
31
|
+
* Key order independent via sortKeysDeep — discarded-PD recovery must not miss
|
|
32
|
+
* semantically equal blocks written with different JSON key order.
|
|
33
|
+
*/
|
|
34
|
+
export function deepEqualPolicySnapshot(a, b) {
|
|
35
|
+
return JSON.stringify(normalizePolicySnapshot(a)) === JSON.stringify(normalizePolicySnapshot(b));
|
|
36
|
+
}
|
|
23
37
|
function parseMarker(raw) {
|
|
24
38
|
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
25
39
|
return null;
|
|
@@ -33,7 +47,7 @@ function parseMarker(raw) {
|
|
|
33
47
|
typeof rec.directiveVersion !== "string") {
|
|
34
48
|
return null;
|
|
35
49
|
}
|
|
36
|
-
|
|
50
|
+
const marker = {
|
|
37
51
|
version: ORG_FORCE_ON_MARKER_VERSION,
|
|
38
52
|
appliedAt: rec.appliedAt,
|
|
39
53
|
originOrg: rec.originOrg,
|
|
@@ -41,6 +55,23 @@ function parseMarker(raw) {
|
|
|
41
55
|
productSignal: rec.productSignal,
|
|
42
56
|
directiveVersion: rec.directiveVersion,
|
|
43
57
|
};
|
|
58
|
+
// Optional #2903 previous* snapshots — preserve when present (including null).
|
|
59
|
+
if ("previousValueFeedback" in rec) {
|
|
60
|
+
return {
|
|
61
|
+
...marker,
|
|
62
|
+
previousValueFeedback: rec.previousValueFeedback,
|
|
63
|
+
...("previousProductSignal" in rec
|
|
64
|
+
? { previousProductSignal: rec.previousProductSignal }
|
|
65
|
+
: {}),
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
if ("previousProductSignal" in rec) {
|
|
69
|
+
return {
|
|
70
|
+
...marker,
|
|
71
|
+
previousProductSignal: rec.previousProductSignal,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
return marker;
|
|
44
75
|
}
|
|
45
76
|
/** Read the #2822 migration marker when present and valid. */
|
|
46
77
|
export function readOrgForceOnMarker(projectRoot) {
|
|
@@ -57,9 +88,13 @@ export function readOrgForceOnMarker(projectRoot) {
|
|
|
57
88
|
}
|
|
58
89
|
function writeOrgForceOnMarker(projectRoot, marker) {
|
|
59
90
|
const path = markerPath(projectRoot);
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
91
|
+
// #2951 Phase 2: product write sink routes through containedWrite.
|
|
92
|
+
containedWrite({
|
|
93
|
+
root: resolve(projectRoot),
|
|
94
|
+
target: path,
|
|
95
|
+
data: `${JSON.stringify(marker, null, 2)}\n`,
|
|
96
|
+
mode: "replace",
|
|
97
|
+
});
|
|
63
98
|
}
|
|
64
99
|
function readTypedBoolean(raw, key, fallback) {
|
|
65
100
|
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
|
|
@@ -81,6 +116,83 @@ function valueFeedbackNeedsForceOn(raw) {
|
|
|
81
116
|
function productSignalNeedsForceOn(raw) {
|
|
82
117
|
return !readTypedBoolean(raw, "enabled", false);
|
|
83
118
|
}
|
|
119
|
+
/**
|
|
120
|
+
* Sentinel previous* for an unforced field that had no stored baseline (legacy
|
|
121
|
+
* partial recovery). Must not deep-equal any real policy snapshot — including
|
|
122
|
+
* null/undefined after clear-value-feedback — or the next update would re-force.
|
|
123
|
+
*/
|
|
124
|
+
export const ORG_FORCE_ON_NO_BASELINE = {
|
|
125
|
+
"x-directive/org-force-on": "no-baseline",
|
|
126
|
+
};
|
|
127
|
+
/** Classic #2822 pre-migration all-false valueFeedback baseline (statusreport). */
|
|
128
|
+
export const PRE_MIGRATION_VALUE_FEEDBACK_BASELINE = {
|
|
129
|
+
enabled: false,
|
|
130
|
+
emitEvents: false,
|
|
131
|
+
sessionLine: false,
|
|
132
|
+
upstreamPrompt: false,
|
|
133
|
+
};
|
|
134
|
+
/** True when current VF looks like discarded pre-migration state (legacy markers). */
|
|
135
|
+
export function looksLikePreMigrationValueFeedback(raw) {
|
|
136
|
+
// Missing key (undefined/null) is NOT the classic baseline — that is the
|
|
137
|
+
// clear-value-feedback / never-typed shape. statusreport-class discard keeps
|
|
138
|
+
// an explicit all-false object in PROJECT-DEFINITION.
|
|
139
|
+
if (raw === undefined || raw === null) {
|
|
140
|
+
return false;
|
|
141
|
+
}
|
|
142
|
+
return deepEqualPolicySnapshot(raw, PRE_MIGRATION_VALUE_FEEDBACK_BASELINE);
|
|
143
|
+
}
|
|
144
|
+
/** True when current PS looks like discarded pre-migration state (legacy markers). */
|
|
145
|
+
export function looksLikePreMigrationProductSignal(raw) {
|
|
146
|
+
if (raw === undefined || raw === null) {
|
|
147
|
+
return true;
|
|
148
|
+
}
|
|
149
|
+
if (typeof raw !== "object" || Array.isArray(raw)) {
|
|
150
|
+
return false;
|
|
151
|
+
}
|
|
152
|
+
const block = raw;
|
|
153
|
+
if (block.enabled !== false) {
|
|
154
|
+
return false;
|
|
155
|
+
}
|
|
156
|
+
const keys = Object.keys(block);
|
|
157
|
+
// Bare {enabled:false} or default sink only — custom sinkRepo implies intent.
|
|
158
|
+
if (keys.length === 1) {
|
|
159
|
+
return true;
|
|
160
|
+
}
|
|
161
|
+
if (keys.length === 2 &&
|
|
162
|
+
typeof block.sinkRepo === "string" &&
|
|
163
|
+
block.sinkRepo.trim() === DEFAULT_PRODUCT_SIGNAL_SINK) {
|
|
164
|
+
return true;
|
|
165
|
+
}
|
|
166
|
+
return false;
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Incomplete migration (#2903): marker present, PD still needs force-on, and
|
|
170
|
+
* current typed block still equals the pre-migration snapshot (or legacy marker
|
|
171
|
+
* has no previous* field and current still looks like the classic baseline).
|
|
172
|
+
*
|
|
173
|
+
* Exact restore of the pre-migration snapshot is treated as incomplete by design
|
|
174
|
+
* (company policy #2376 / #2822 / #2903): discarded working-tree force-on and
|
|
175
|
+
* "put the old all-false block back" are not distinguishable without git history,
|
|
176
|
+
* and trusted-org local collection should stay ON. True intentional opt-out must
|
|
177
|
+
* use a shape that differs from previous*, `policy:clear-value-feedback`, or
|
|
178
|
+
* root `.no-deft-directive`. Outbound product-signal still requires D17 consent.
|
|
179
|
+
*
|
|
180
|
+
* Legacy markers without previous*: only re-apply fields that still look like the
|
|
181
|
+
* classic pre-migration baseline so a distinct intentional sibling opt-out is kept.
|
|
182
|
+
*/
|
|
183
|
+
export function isIncompleteForceOnField(needsForceOn, current, previousSnapshot, previousFieldPresent, field = "valueFeedback") {
|
|
184
|
+
if (!needsForceOn) {
|
|
185
|
+
return false;
|
|
186
|
+
}
|
|
187
|
+
// Legacy markers written before #2903 lack previous* — recover only classic
|
|
188
|
+
// baseline shapes (statusreport-class), not distinct intentional opt-outs.
|
|
189
|
+
if (!previousFieldPresent) {
|
|
190
|
+
return field === "valueFeedback"
|
|
191
|
+
? looksLikePreMigrationValueFeedback(current)
|
|
192
|
+
: looksLikePreMigrationProductSignal(current);
|
|
193
|
+
}
|
|
194
|
+
return deepEqualPolicySnapshot(current, previousSnapshot);
|
|
195
|
+
}
|
|
84
196
|
function ensurePolicyBlock(plan) {
|
|
85
197
|
migrateLegacyPolicyKey(plan);
|
|
86
198
|
const existing = plan[PLAN_POLICY_KEY];
|
|
@@ -94,31 +206,97 @@ function ensurePolicyBlock(plan) {
|
|
|
94
206
|
}
|
|
95
207
|
return plan[PLAN_POLICY_KEY];
|
|
96
208
|
}
|
|
97
|
-
function
|
|
209
|
+
function writeMarker(projectRoot, options, input) {
|
|
98
210
|
const originOrg = detectOriginOrg(projectRoot, options.autoEnable) ?? "unknown";
|
|
99
211
|
const now = options.now ?? new Date();
|
|
100
212
|
writeOrgForceOnMarker(projectRoot, {
|
|
101
213
|
version: ORG_FORCE_ON_MARKER_VERSION,
|
|
102
214
|
appliedAt: now.toISOString().replace(/\.\d{3}Z$/, "Z"),
|
|
103
215
|
originOrg,
|
|
104
|
-
valueFeedback,
|
|
105
|
-
productSignal,
|
|
216
|
+
valueFeedback: input.valueFeedback,
|
|
217
|
+
productSignal: input.productSignal,
|
|
106
218
|
directiveVersion: readCorePackageVersion(),
|
|
219
|
+
previousValueFeedback: normalizePolicySnapshot(input.previousValueFeedback),
|
|
220
|
+
previousProductSignal: normalizePolicySnapshot(input.previousProductSignal),
|
|
107
221
|
});
|
|
108
222
|
}
|
|
223
|
+
function readPolicyFieldSnapshots(projectRoot) {
|
|
224
|
+
const [data, err] = loadProjectDefinition(projectRoot);
|
|
225
|
+
if (data === null) {
|
|
226
|
+
return { ok: false, skippedReason: err ?? "project-definition-missing" };
|
|
227
|
+
}
|
|
228
|
+
const policyBlock = readPlanPolicy(data.plan);
|
|
229
|
+
const policyObj = typeof policyBlock === "object" && policyBlock !== null && !Array.isArray(policyBlock)
|
|
230
|
+
? policyBlock
|
|
231
|
+
: {};
|
|
232
|
+
const vfRaw = "valueFeedback" in policyObj ? policyObj.valueFeedback : undefined;
|
|
233
|
+
const psRaw = "productSignal" in policyObj ? policyObj.productSignal : undefined;
|
|
234
|
+
return {
|
|
235
|
+
ok: true,
|
|
236
|
+
vfRaw,
|
|
237
|
+
psRaw,
|
|
238
|
+
needsVf: valueFeedbackNeedsForceOn(vfRaw),
|
|
239
|
+
needsPs: productSignalNeedsForceOn(psRaw),
|
|
240
|
+
};
|
|
241
|
+
}
|
|
109
242
|
/**
|
|
110
243
|
* One-time trusted-org install/upgrade force-on for valueFeedback + productSignal
|
|
111
244
|
* (#2822). Idempotent via durable marker; non-trusted orgs are unchanged.
|
|
245
|
+
*
|
|
246
|
+
* #2903 verify-on-skip: when the marker is present but PROJECT-DEFINITION still
|
|
247
|
+
* matches the pre-migration snapshot (discarded / never-landed force-on write),
|
|
248
|
+
* re-apply. Intentional post-migration shapes that differ from both the previous
|
|
249
|
+
* snapshot and the force-on block keep the skip.
|
|
112
250
|
*/
|
|
113
251
|
export function runOrgForceOnMigration(projectRoot, options = {}) {
|
|
114
|
-
|
|
252
|
+
// #2926: per-project opt-out wins over ambient trusted-org force-on (v1).
|
|
253
|
+
if (isNoDeftDirectivePresent(projectRoot)) {
|
|
115
254
|
return {
|
|
116
255
|
ran: false,
|
|
117
|
-
skippedReason: "
|
|
256
|
+
skippedReason: "no-deft-directive",
|
|
118
257
|
valueFeedbackChanged: false,
|
|
119
258
|
productSignalChanged: false,
|
|
120
259
|
};
|
|
121
260
|
}
|
|
261
|
+
const existingMarker = readOrgForceOnMarker(projectRoot);
|
|
262
|
+
if (existingMarker !== null) {
|
|
263
|
+
const snapshots = readPolicyFieldSnapshots(projectRoot);
|
|
264
|
+
if (!snapshots.ok) {
|
|
265
|
+
return {
|
|
266
|
+
ran: false,
|
|
267
|
+
skippedReason: "marker-present",
|
|
268
|
+
valueFeedbackChanged: false,
|
|
269
|
+
productSignalChanged: false,
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
const previousVfPresent = "previousValueFeedback" in existingMarker;
|
|
273
|
+
const previousPsPresent = "previousProductSignal" in existingMarker;
|
|
274
|
+
const incompleteVf = isIncompleteForceOnField(snapshots.needsVf, snapshots.vfRaw, existingMarker.previousValueFeedback, previousVfPresent, "valueFeedback");
|
|
275
|
+
const incompletePs = isIncompleteForceOnField(snapshots.needsPs, snapshots.psRaw, existingMarker.previousProductSignal, previousPsPresent, "productSignal");
|
|
276
|
+
// Marker present and either already force-on shaped, or intentional opt-out
|
|
277
|
+
// (current differs from stored previous snapshot) → keep skip.
|
|
278
|
+
if (!incompleteVf && !incompletePs) {
|
|
279
|
+
return {
|
|
280
|
+
ran: false,
|
|
281
|
+
skippedReason: "marker-present",
|
|
282
|
+
valueFeedbackChanged: false,
|
|
283
|
+
productSignalChanged: false,
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
// Incomplete migration: fall through to trusted-org apply with only the
|
|
287
|
+
// incomplete fields re-forced.
|
|
288
|
+
if (!isTrustedOrgAutoEnable(projectRoot, options.autoEnable)) {
|
|
289
|
+
return {
|
|
290
|
+
ran: false,
|
|
291
|
+
skippedReason: "marker-present",
|
|
292
|
+
valueFeedbackChanged: false,
|
|
293
|
+
productSignalChanged: false,
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
// Pre-lock incomplete check is a fast path only; applyForceOn revalidates
|
|
297
|
+
// under the mutation lock against the latest PD (#2903 TOCTOU).
|
|
298
|
+
return applyForceOn(projectRoot, options, { existingMarker });
|
|
299
|
+
}
|
|
122
300
|
if (!isTrustedOrgAutoEnable(projectRoot, options.autoEnable)) {
|
|
123
301
|
return {
|
|
124
302
|
ran: false,
|
|
@@ -127,25 +305,22 @@ export function runOrgForceOnMigration(projectRoot, options = {}) {
|
|
|
127
305
|
productSignalChanged: false,
|
|
128
306
|
};
|
|
129
307
|
}
|
|
130
|
-
const
|
|
131
|
-
if (
|
|
308
|
+
const snapshots = readPolicyFieldSnapshots(projectRoot);
|
|
309
|
+
if (!snapshots.ok) {
|
|
132
310
|
return {
|
|
133
311
|
ran: false,
|
|
134
|
-
skippedReason:
|
|
312
|
+
skippedReason: snapshots.skippedReason,
|
|
135
313
|
valueFeedbackChanged: false,
|
|
136
314
|
productSignalChanged: false,
|
|
137
315
|
};
|
|
138
316
|
}
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
const needsPs = productSignalNeedsForceOn(psRaw);
|
|
147
|
-
if (!needsVf && !needsPs) {
|
|
148
|
-
writeMarkerForSkip(projectRoot, options, false, false);
|
|
317
|
+
if (!snapshots.needsVf && !snapshots.needsPs) {
|
|
318
|
+
writeMarker(projectRoot, options, {
|
|
319
|
+
valueFeedback: false,
|
|
320
|
+
productSignal: false,
|
|
321
|
+
previousValueFeedback: snapshots.vfRaw,
|
|
322
|
+
previousProductSignal: snapshots.psRaw,
|
|
323
|
+
});
|
|
149
324
|
return {
|
|
150
325
|
ran: false,
|
|
151
326
|
skippedReason: "already-enabled",
|
|
@@ -153,9 +328,15 @@ export function runOrgForceOnMigration(projectRoot, options = {}) {
|
|
|
153
328
|
productSignalChanged: false,
|
|
154
329
|
};
|
|
155
330
|
}
|
|
331
|
+
return applyForceOn(projectRoot, options, { existingMarker: null });
|
|
332
|
+
}
|
|
333
|
+
function applyForceOn(projectRoot, options, intent) {
|
|
156
334
|
const path = projectDefinitionPath(projectRoot);
|
|
157
335
|
let valueFeedbackChanged = false;
|
|
158
336
|
let productSignalChanged = false;
|
|
337
|
+
let ran = false;
|
|
338
|
+
let skippedReason = null;
|
|
339
|
+
const existing = intent.existingMarker;
|
|
159
340
|
projectDefinitionMutationLock(projectRoot, () => {
|
|
160
341
|
const parsed = JSON.parse(readFileSync(path, { encoding: "utf8" }));
|
|
161
342
|
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
@@ -176,6 +357,36 @@ export function runOrgForceOnMigration(projectRoot, options = {}) {
|
|
|
176
357
|
const policy = ensurePolicyBlock(plan);
|
|
177
358
|
const previousVf = policy.valueFeedback;
|
|
178
359
|
const previousPs = policy.productSignal;
|
|
360
|
+
// Revalidate under lock so a concurrent intentional opt-out between the
|
|
361
|
+
// unlocked snapshot check and lock acquisition is not overwritten (#2903).
|
|
362
|
+
let needsVf;
|
|
363
|
+
let needsPs;
|
|
364
|
+
if (existing !== null) {
|
|
365
|
+
const previousVfPresent = "previousValueFeedback" in existing;
|
|
366
|
+
const previousPsPresent = "previousProductSignal" in existing;
|
|
367
|
+
needsVf = isIncompleteForceOnField(valueFeedbackNeedsForceOn(previousVf), previousVf, existing.previousValueFeedback, previousVfPresent, "valueFeedback");
|
|
368
|
+
needsPs = isIncompleteForceOnField(productSignalNeedsForceOn(previousPs), previousPs, existing.previousProductSignal, previousPsPresent, "productSignal");
|
|
369
|
+
}
|
|
370
|
+
else {
|
|
371
|
+
needsVf = valueFeedbackNeedsForceOn(previousVf);
|
|
372
|
+
needsPs = productSignalNeedsForceOn(previousPs);
|
|
373
|
+
}
|
|
374
|
+
if (!needsVf && !needsPs) {
|
|
375
|
+
// Under-lock state no longer needs force-on (concurrent opt-out or already ON).
|
|
376
|
+
if (existing === null) {
|
|
377
|
+
writeMarker(projectRoot, options, {
|
|
378
|
+
valueFeedback: false,
|
|
379
|
+
productSignal: false,
|
|
380
|
+
previousValueFeedback: previousVf,
|
|
381
|
+
previousProductSignal: previousPs,
|
|
382
|
+
});
|
|
383
|
+
skippedReason = "already-enabled";
|
|
384
|
+
}
|
|
385
|
+
else {
|
|
386
|
+
skippedReason = "marker-present";
|
|
387
|
+
}
|
|
388
|
+
return { changed: false };
|
|
389
|
+
}
|
|
179
390
|
if (needsVf) {
|
|
180
391
|
policy.valueFeedback = { ...FORCE_ON_VALUE_FEEDBACK_BLOCK };
|
|
181
392
|
valueFeedbackChanged = valueFeedbackNeedsForceOn(previousVf);
|
|
@@ -193,18 +404,49 @@ export function runOrgForceOnMigration(projectRoot, options = {}) {
|
|
|
193
404
|
if (valueFeedbackChanged || productSignalChanged) {
|
|
194
405
|
atomicWriteProjectDefinition(path, rootData);
|
|
195
406
|
}
|
|
407
|
+
// Marker previous*: forced fields record pre-apply current; unforced fields
|
|
408
|
+
// keep the prior marker snapshot so intentional opt-outs are not rewritten
|
|
409
|
+
// into the incomplete-migration equality path on the next update (#2903 P1).
|
|
410
|
+
// When the prior marker had no previous* (legacy) and the field was not forced,
|
|
411
|
+
// store ORG_FORCE_ON_NO_BASELINE — not null/live shape — so clear-value-feedback
|
|
412
|
+
// (undefined→null) and intentional opt-outs never deep-equal previous*.
|
|
413
|
+
const markerPreviousVf = needsVf
|
|
414
|
+
? previousVf
|
|
415
|
+
: existing && "previousValueFeedback" in existing
|
|
416
|
+
? existing.previousValueFeedback
|
|
417
|
+
: ORG_FORCE_ON_NO_BASELINE;
|
|
418
|
+
const markerPreviousPs = needsPs
|
|
419
|
+
? previousPs
|
|
420
|
+
: existing && "previousProductSignal" in existing
|
|
421
|
+
? existing.previousProductSignal
|
|
422
|
+
: ORG_FORCE_ON_NO_BASELINE;
|
|
196
423
|
const actor = options.actor ?? "directive-update";
|
|
197
424
|
appendAuditLog(projectRoot, [
|
|
198
425
|
`actor=${actor}`,
|
|
199
426
|
"org-force-on-v2822",
|
|
200
427
|
`valueFeedback=${needsVf ? "forced-on" : "unchanged"}`,
|
|
201
428
|
`productSignal=${needsPs ? "forced-on" : "unchanged"}`,
|
|
202
|
-
`previousValueFeedback=${JSON.stringify(
|
|
203
|
-
`previousProductSignal=${JSON.stringify(
|
|
429
|
+
`previousValueFeedback=${JSON.stringify(normalizePolicySnapshot(markerPreviousVf))}`,
|
|
430
|
+
`previousProductSignal=${JSON.stringify(normalizePolicySnapshot(markerPreviousPs))}`,
|
|
204
431
|
].join(" "));
|
|
205
|
-
|
|
432
|
+
writeMarker(projectRoot, options, {
|
|
433
|
+
// OR with existing so partial re-apply does not clear install-force-on source.
|
|
434
|
+
valueFeedback: needsVf || (existing?.valueFeedback ?? false),
|
|
435
|
+
productSignal: needsPs || (existing?.productSignal ?? false),
|
|
436
|
+
previousValueFeedback: markerPreviousVf,
|
|
437
|
+
previousProductSignal: markerPreviousPs,
|
|
438
|
+
});
|
|
439
|
+
ran = true;
|
|
206
440
|
return { changed: valueFeedbackChanged || productSignalChanged };
|
|
207
441
|
});
|
|
442
|
+
if (!ran) {
|
|
443
|
+
return {
|
|
444
|
+
ran: false,
|
|
445
|
+
skippedReason: skippedReason ?? "marker-present",
|
|
446
|
+
valueFeedbackChanged: false,
|
|
447
|
+
productSignalChanged: false,
|
|
448
|
+
};
|
|
449
|
+
}
|
|
208
450
|
return {
|
|
209
451
|
ran: true,
|
|
210
452
|
skippedReason: null,
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Human merge gate (#1193 R3 / Wave 2 of #2948).
|
|
3
|
+
*
|
|
4
|
+
* When requireHumanMerge is true, agents may open PRs but must not merge.
|
|
5
|
+
* Three enforcement surfaces (mirror #747):
|
|
6
|
+
* (1) merge preflight refuses agent-initiated merge
|
|
7
|
+
* (2) verify:branch aggregate surfaces the policy
|
|
8
|
+
* (3) branch-protection / setup docs require ≥1 human reviewer
|
|
9
|
+
*
|
|
10
|
+
* Override: `task policy:allow-bot-merge -- --confirm` + `DEFT_ALLOW_BOT_MERGE=1`.
|
|
11
|
+
*/
|
|
12
|
+
export declare const FIELD_REQUIRE_HUMAN_MERGE = "plan.policy.requireHumanMerge";
|
|
13
|
+
export declare const FIELD_REQUIRE_HUMAN_MERGE_CLI_ALIAS = "requireHumanMerge";
|
|
14
|
+
export declare const FIELD_AUTO_DEPLOY_ON_MERGE = "plan.policy.autoDeployOnMerge";
|
|
15
|
+
/** Emergency env-var bypass for agent merge (#1193). */
|
|
16
|
+
export declare const ENV_ALLOW_BOT_MERGE = "DEFT_ALLOW_BOT_MERGE";
|
|
17
|
+
/** Legacy narrative key recognized at read time with deprecation warning. */
|
|
18
|
+
export declare const LEGACY_REQUIRE_HUMAN_MERGE_KEY = "Require human merge";
|
|
19
|
+
export type HumanMergeSource = "typed" | "legacy-narrative" | "auto-deploy-default" | "env-bypass" | "default";
|
|
20
|
+
export interface HumanMergePolicyResult {
|
|
21
|
+
/** True when agents must not merge. */
|
|
22
|
+
readonly requireHumanMerge: boolean;
|
|
23
|
+
readonly source: HumanMergeSource;
|
|
24
|
+
readonly deprecationWarning: string | null;
|
|
25
|
+
readonly error: string | null;
|
|
26
|
+
readonly autoDeployOnMerge: boolean;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Resolve effective human-merge policy (#1193).
|
|
30
|
+
*
|
|
31
|
+
* Order:
|
|
32
|
+
* 1. DEFT_ALLOW_BOT_MERGE=1 → requireHumanMerge effective false (env-bypass)
|
|
33
|
+
* 2. typed plan.policy.requireHumanMerge
|
|
34
|
+
* 3. legacy narrative "Require human merge"
|
|
35
|
+
* 4. autoDeployOnMerge true → default requireHumanMerge true
|
|
36
|
+
* 5. default false (no auto-deploy → agents may merge when other gates allow)
|
|
37
|
+
*/
|
|
38
|
+
export declare function resolveHumanMergePolicy(projectRoot: string, env?: NodeJS.ProcessEnv): HumanMergePolicyResult;
|
|
39
|
+
/** Session-start / AGENTS disclosure line when the human merge gate is ON. */
|
|
40
|
+
export declare function humanMergeDisclosureLine(result: HumanMergePolicyResult): string | null;
|
|
41
|
+
export interface AgentMergeEvaluateResult {
|
|
42
|
+
readonly exitCode: 0 | 1;
|
|
43
|
+
readonly allowed: boolean;
|
|
44
|
+
readonly message: string;
|
|
45
|
+
readonly policy: HumanMergePolicyResult;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Merge preflight: refuse agent-initiated merge when requireHumanMerge is true.
|
|
49
|
+
* Surface (1) of the three-surface enforcement pattern.
|
|
50
|
+
*/
|
|
51
|
+
export declare function evaluateAgentMerge(projectRoot: string, env?: NodeJS.ProcessEnv): AgentMergeEvaluateResult;
|
|
52
|
+
/**
|
|
53
|
+
* verify:branch aggregate surface (2): advisory line when gate is ON.
|
|
54
|
+
* Does not fail the branch gate on feature branches — merge refuse is surface (1).
|
|
55
|
+
*/
|
|
56
|
+
export declare function humanMergeBranchNote(result: HumanMergePolicyResult): string | null;
|
|
57
|
+
export interface HumanMergePolicyField {
|
|
58
|
+
readonly name: string;
|
|
59
|
+
readonly current: boolean;
|
|
60
|
+
readonly default: boolean;
|
|
61
|
+
readonly source: string;
|
|
62
|
+
}
|
|
63
|
+
/** Inspector for policy:show --field=requireHumanMerge. */
|
|
64
|
+
export declare function inspectRequireHumanMerge(data: Record<string, unknown> | null, projectRoot?: string): HumanMergePolicyField;
|
|
65
|
+
export declare const ALLOW_BOT_MERGE_CAPABILITY_COST: string;
|
|
66
|
+
/** Write requireHumanMerge=false (allow bot merge) with audit trail. */
|
|
67
|
+
export declare function setRequireHumanMerge(projectRoot: string, options: {
|
|
68
|
+
requireHumanMerge: boolean;
|
|
69
|
+
actor?: string;
|
|
70
|
+
note?: string;
|
|
71
|
+
}): {
|
|
72
|
+
changed: boolean;
|
|
73
|
+
auditEntry: string;
|
|
74
|
+
};
|
|
75
|
+
//# sourceMappingURL=require-human-merge.d.ts.map
|