@deftai/directive-core 0.86.0 → 0.88.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/cache/scanner.d.ts +11 -1
- package/dist/cache/scanner.js +29 -4
- package/dist/check/gate-lists.js +2 -0
- 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/main.d.ts +6 -5
- package/dist/doctor/main.js +80 -18
- package/dist/doctor/taskfile.d.ts +8 -0
- package/dist/doctor/taskfile.js +19 -0
- package/dist/fs/projection-containment.d.ts +18 -0
- package/dist/fs/projection-containment.js +40 -0
- package/dist/hooks/dispatcher.d.ts +34 -2
- package/dist/hooks/dispatcher.js +234 -21
- package/dist/hooks/tools.d.ts +31 -0
- package/dist/hooks/tools.js +74 -0
- package/dist/init-deposit/agent-hooks.d.ts +1 -1
- package/dist/init-deposit/agent-hooks.js +38 -2
- 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/prettierignore.js +2 -2
- package/dist/init-deposit/refresh.js +38 -7
- package/dist/init-deposit/scaffold.js +7 -3
- package/dist/init-deposit/xbrief-projections.js +6 -6
- package/dist/intake/issue-emit.d.ts +45 -2
- package/dist/intake/issue-emit.js +420 -17
- package/dist/intake/issue-ingest.js +65 -6
- package/dist/packs/pack-render.d.ts +33 -0
- package/dist/packs/pack-render.js +155 -9
- package/dist/packs/quarantine-ext.d.ts +10 -0
- package/dist/packs/quarantine-ext.js +26 -2
- package/dist/platform/platform-capabilities.js +3 -0
- package/dist/policy/index.d.ts +1 -0
- package/dist/policy/index.js +1 -0
- package/dist/policy/no-deft-directive.d.ts +59 -0
- package/dist/policy/no-deft-directive.js +103 -0
- package/dist/policy/org-force-on-migration.d.ts +52 -0
- package/dist/policy/org-force-on-migration.js +260 -22
- package/dist/policy/runtime-authority.d.ts +41 -0
- package/dist/policy/runtime-authority.js +274 -0
- package/dist/review-monitor/constants.js +3 -2
- package/dist/review-monitor/tier-detection.d.ts +6 -2
- package/dist/review-monitor/tier-detection.js +27 -2
- package/dist/scope/transition.js +43 -0
- package/dist/session/release-availability.d.ts +2 -0
- package/dist/session/release-availability.js +23 -8
- 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 +30 -0
- package/dist/swarm/routing-set-cli.js +5 -10
- package/dist/swarm/routing.d.ts +3 -2
- package/dist/swarm/routing.js +16 -4
- package/dist/triage/help/registry-data.d.ts +7 -7
- package/dist/triage/help/registry-data.js +15 -6
- package/dist/triage/queue/index.d.ts +1 -0
- package/dist/triage/queue/index.js +1 -0
- package/dist/triage/queue/show.d.ts +69 -0
- package/dist/triage/queue/show.js +293 -0
- package/dist/triage/scope/cli.js +3 -0
- package/dist/triage/scope/coverage.d.ts +2 -0
- package/dist/triage/scope/coverage.js +18 -3
- 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.d.ts +37 -0
- package/dist/verify-source/openclaw-tier1.js +105 -0
- package/dist/xbrief-migrate/migrate-project.js +9 -5
- package/package.json +4 -3
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } 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
5
|
import { assertWriteTargetSafe } from "../fs/projection-containment.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) {
|
|
@@ -81,6 +112,83 @@ function valueFeedbackNeedsForceOn(raw) {
|
|
|
81
112
|
function productSignalNeedsForceOn(raw) {
|
|
82
113
|
return !readTypedBoolean(raw, "enabled", false);
|
|
83
114
|
}
|
|
115
|
+
/**
|
|
116
|
+
* Sentinel previous* for an unforced field that had no stored baseline (legacy
|
|
117
|
+
* partial recovery). Must not deep-equal any real policy snapshot — including
|
|
118
|
+
* null/undefined after clear-value-feedback — or the next update would re-force.
|
|
119
|
+
*/
|
|
120
|
+
export const ORG_FORCE_ON_NO_BASELINE = {
|
|
121
|
+
"x-directive/org-force-on": "no-baseline",
|
|
122
|
+
};
|
|
123
|
+
/** Classic #2822 pre-migration all-false valueFeedback baseline (statusreport). */
|
|
124
|
+
export const PRE_MIGRATION_VALUE_FEEDBACK_BASELINE = {
|
|
125
|
+
enabled: false,
|
|
126
|
+
emitEvents: false,
|
|
127
|
+
sessionLine: false,
|
|
128
|
+
upstreamPrompt: false,
|
|
129
|
+
};
|
|
130
|
+
/** True when current VF looks like discarded pre-migration state (legacy markers). */
|
|
131
|
+
export function looksLikePreMigrationValueFeedback(raw) {
|
|
132
|
+
// Missing key (undefined/null) is NOT the classic baseline — that is the
|
|
133
|
+
// clear-value-feedback / never-typed shape. statusreport-class discard keeps
|
|
134
|
+
// an explicit all-false object in PROJECT-DEFINITION.
|
|
135
|
+
if (raw === undefined || raw === null) {
|
|
136
|
+
return false;
|
|
137
|
+
}
|
|
138
|
+
return deepEqualPolicySnapshot(raw, PRE_MIGRATION_VALUE_FEEDBACK_BASELINE);
|
|
139
|
+
}
|
|
140
|
+
/** True when current PS looks like discarded pre-migration state (legacy markers). */
|
|
141
|
+
export function looksLikePreMigrationProductSignal(raw) {
|
|
142
|
+
if (raw === undefined || raw === null) {
|
|
143
|
+
return true;
|
|
144
|
+
}
|
|
145
|
+
if (typeof raw !== "object" || Array.isArray(raw)) {
|
|
146
|
+
return false;
|
|
147
|
+
}
|
|
148
|
+
const block = raw;
|
|
149
|
+
if (block.enabled !== false) {
|
|
150
|
+
return false;
|
|
151
|
+
}
|
|
152
|
+
const keys = Object.keys(block);
|
|
153
|
+
// Bare {enabled:false} or default sink only — custom sinkRepo implies intent.
|
|
154
|
+
if (keys.length === 1) {
|
|
155
|
+
return true;
|
|
156
|
+
}
|
|
157
|
+
if (keys.length === 2 &&
|
|
158
|
+
typeof block.sinkRepo === "string" &&
|
|
159
|
+
block.sinkRepo.trim() === DEFAULT_PRODUCT_SIGNAL_SINK) {
|
|
160
|
+
return true;
|
|
161
|
+
}
|
|
162
|
+
return false;
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Incomplete migration (#2903): marker present, PD still needs force-on, and
|
|
166
|
+
* current typed block still equals the pre-migration snapshot (or legacy marker
|
|
167
|
+
* has no previous* field and current still looks like the classic baseline).
|
|
168
|
+
*
|
|
169
|
+
* Exact restore of the pre-migration snapshot is treated as incomplete by design
|
|
170
|
+
* (company policy #2376 / #2822 / #2903): discarded working-tree force-on and
|
|
171
|
+
* "put the old all-false block back" are not distinguishable without git history,
|
|
172
|
+
* and trusted-org local collection should stay ON. True intentional opt-out must
|
|
173
|
+
* use a shape that differs from previous*, `policy:clear-value-feedback`, or
|
|
174
|
+
* root `.no-deft-directive`. Outbound product-signal still requires D17 consent.
|
|
175
|
+
*
|
|
176
|
+
* Legacy markers without previous*: only re-apply fields that still look like the
|
|
177
|
+
* classic pre-migration baseline so a distinct intentional sibling opt-out is kept.
|
|
178
|
+
*/
|
|
179
|
+
export function isIncompleteForceOnField(needsForceOn, current, previousSnapshot, previousFieldPresent, field = "valueFeedback") {
|
|
180
|
+
if (!needsForceOn) {
|
|
181
|
+
return false;
|
|
182
|
+
}
|
|
183
|
+
// Legacy markers written before #2903 lack previous* — recover only classic
|
|
184
|
+
// baseline shapes (statusreport-class), not distinct intentional opt-outs.
|
|
185
|
+
if (!previousFieldPresent) {
|
|
186
|
+
return field === "valueFeedback"
|
|
187
|
+
? looksLikePreMigrationValueFeedback(current)
|
|
188
|
+
: looksLikePreMigrationProductSignal(current);
|
|
189
|
+
}
|
|
190
|
+
return deepEqualPolicySnapshot(current, previousSnapshot);
|
|
191
|
+
}
|
|
84
192
|
function ensurePolicyBlock(plan) {
|
|
85
193
|
migrateLegacyPolicyKey(plan);
|
|
86
194
|
const existing = plan[PLAN_POLICY_KEY];
|
|
@@ -94,31 +202,97 @@ function ensurePolicyBlock(plan) {
|
|
|
94
202
|
}
|
|
95
203
|
return plan[PLAN_POLICY_KEY];
|
|
96
204
|
}
|
|
97
|
-
function
|
|
205
|
+
function writeMarker(projectRoot, options, input) {
|
|
98
206
|
const originOrg = detectOriginOrg(projectRoot, options.autoEnable) ?? "unknown";
|
|
99
207
|
const now = options.now ?? new Date();
|
|
100
208
|
writeOrgForceOnMarker(projectRoot, {
|
|
101
209
|
version: ORG_FORCE_ON_MARKER_VERSION,
|
|
102
210
|
appliedAt: now.toISOString().replace(/\.\d{3}Z$/, "Z"),
|
|
103
211
|
originOrg,
|
|
104
|
-
valueFeedback,
|
|
105
|
-
productSignal,
|
|
212
|
+
valueFeedback: input.valueFeedback,
|
|
213
|
+
productSignal: input.productSignal,
|
|
106
214
|
directiveVersion: readCorePackageVersion(),
|
|
215
|
+
previousValueFeedback: normalizePolicySnapshot(input.previousValueFeedback),
|
|
216
|
+
previousProductSignal: normalizePolicySnapshot(input.previousProductSignal),
|
|
107
217
|
});
|
|
108
218
|
}
|
|
219
|
+
function readPolicyFieldSnapshots(projectRoot) {
|
|
220
|
+
const [data, err] = loadProjectDefinition(projectRoot);
|
|
221
|
+
if (data === null) {
|
|
222
|
+
return { ok: false, skippedReason: err ?? "project-definition-missing" };
|
|
223
|
+
}
|
|
224
|
+
const policyBlock = readPlanPolicy(data.plan);
|
|
225
|
+
const policyObj = typeof policyBlock === "object" && policyBlock !== null && !Array.isArray(policyBlock)
|
|
226
|
+
? policyBlock
|
|
227
|
+
: {};
|
|
228
|
+
const vfRaw = "valueFeedback" in policyObj ? policyObj.valueFeedback : undefined;
|
|
229
|
+
const psRaw = "productSignal" in policyObj ? policyObj.productSignal : undefined;
|
|
230
|
+
return {
|
|
231
|
+
ok: true,
|
|
232
|
+
vfRaw,
|
|
233
|
+
psRaw,
|
|
234
|
+
needsVf: valueFeedbackNeedsForceOn(vfRaw),
|
|
235
|
+
needsPs: productSignalNeedsForceOn(psRaw),
|
|
236
|
+
};
|
|
237
|
+
}
|
|
109
238
|
/**
|
|
110
239
|
* One-time trusted-org install/upgrade force-on for valueFeedback + productSignal
|
|
111
240
|
* (#2822). Idempotent via durable marker; non-trusted orgs are unchanged.
|
|
241
|
+
*
|
|
242
|
+
* #2903 verify-on-skip: when the marker is present but PROJECT-DEFINITION still
|
|
243
|
+
* matches the pre-migration snapshot (discarded / never-landed force-on write),
|
|
244
|
+
* re-apply. Intentional post-migration shapes that differ from both the previous
|
|
245
|
+
* snapshot and the force-on block keep the skip.
|
|
112
246
|
*/
|
|
113
247
|
export function runOrgForceOnMigration(projectRoot, options = {}) {
|
|
114
|
-
|
|
248
|
+
// #2926: per-project opt-out wins over ambient trusted-org force-on (v1).
|
|
249
|
+
if (isNoDeftDirectivePresent(projectRoot)) {
|
|
115
250
|
return {
|
|
116
251
|
ran: false,
|
|
117
|
-
skippedReason: "
|
|
252
|
+
skippedReason: "no-deft-directive",
|
|
118
253
|
valueFeedbackChanged: false,
|
|
119
254
|
productSignalChanged: false,
|
|
120
255
|
};
|
|
121
256
|
}
|
|
257
|
+
const existingMarker = readOrgForceOnMarker(projectRoot);
|
|
258
|
+
if (existingMarker !== null) {
|
|
259
|
+
const snapshots = readPolicyFieldSnapshots(projectRoot);
|
|
260
|
+
if (!snapshots.ok) {
|
|
261
|
+
return {
|
|
262
|
+
ran: false,
|
|
263
|
+
skippedReason: "marker-present",
|
|
264
|
+
valueFeedbackChanged: false,
|
|
265
|
+
productSignalChanged: false,
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
const previousVfPresent = "previousValueFeedback" in existingMarker;
|
|
269
|
+
const previousPsPresent = "previousProductSignal" in existingMarker;
|
|
270
|
+
const incompleteVf = isIncompleteForceOnField(snapshots.needsVf, snapshots.vfRaw, existingMarker.previousValueFeedback, previousVfPresent, "valueFeedback");
|
|
271
|
+
const incompletePs = isIncompleteForceOnField(snapshots.needsPs, snapshots.psRaw, existingMarker.previousProductSignal, previousPsPresent, "productSignal");
|
|
272
|
+
// Marker present and either already force-on shaped, or intentional opt-out
|
|
273
|
+
// (current differs from stored previous snapshot) → keep skip.
|
|
274
|
+
if (!incompleteVf && !incompletePs) {
|
|
275
|
+
return {
|
|
276
|
+
ran: false,
|
|
277
|
+
skippedReason: "marker-present",
|
|
278
|
+
valueFeedbackChanged: false,
|
|
279
|
+
productSignalChanged: false,
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
// Incomplete migration: fall through to trusted-org apply with only the
|
|
283
|
+
// incomplete fields re-forced.
|
|
284
|
+
if (!isTrustedOrgAutoEnable(projectRoot, options.autoEnable)) {
|
|
285
|
+
return {
|
|
286
|
+
ran: false,
|
|
287
|
+
skippedReason: "marker-present",
|
|
288
|
+
valueFeedbackChanged: false,
|
|
289
|
+
productSignalChanged: false,
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
// Pre-lock incomplete check is a fast path only; applyForceOn revalidates
|
|
293
|
+
// under the mutation lock against the latest PD (#2903 TOCTOU).
|
|
294
|
+
return applyForceOn(projectRoot, options, { existingMarker });
|
|
295
|
+
}
|
|
122
296
|
if (!isTrustedOrgAutoEnable(projectRoot, options.autoEnable)) {
|
|
123
297
|
return {
|
|
124
298
|
ran: false,
|
|
@@ -127,25 +301,22 @@ export function runOrgForceOnMigration(projectRoot, options = {}) {
|
|
|
127
301
|
productSignalChanged: false,
|
|
128
302
|
};
|
|
129
303
|
}
|
|
130
|
-
const
|
|
131
|
-
if (
|
|
304
|
+
const snapshots = readPolicyFieldSnapshots(projectRoot);
|
|
305
|
+
if (!snapshots.ok) {
|
|
132
306
|
return {
|
|
133
307
|
ran: false,
|
|
134
|
-
skippedReason:
|
|
308
|
+
skippedReason: snapshots.skippedReason,
|
|
135
309
|
valueFeedbackChanged: false,
|
|
136
310
|
productSignalChanged: false,
|
|
137
311
|
};
|
|
138
312
|
}
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
const needsPs = productSignalNeedsForceOn(psRaw);
|
|
147
|
-
if (!needsVf && !needsPs) {
|
|
148
|
-
writeMarkerForSkip(projectRoot, options, false, false);
|
|
313
|
+
if (!snapshots.needsVf && !snapshots.needsPs) {
|
|
314
|
+
writeMarker(projectRoot, options, {
|
|
315
|
+
valueFeedback: false,
|
|
316
|
+
productSignal: false,
|
|
317
|
+
previousValueFeedback: snapshots.vfRaw,
|
|
318
|
+
previousProductSignal: snapshots.psRaw,
|
|
319
|
+
});
|
|
149
320
|
return {
|
|
150
321
|
ran: false,
|
|
151
322
|
skippedReason: "already-enabled",
|
|
@@ -153,9 +324,15 @@ export function runOrgForceOnMigration(projectRoot, options = {}) {
|
|
|
153
324
|
productSignalChanged: false,
|
|
154
325
|
};
|
|
155
326
|
}
|
|
327
|
+
return applyForceOn(projectRoot, options, { existingMarker: null });
|
|
328
|
+
}
|
|
329
|
+
function applyForceOn(projectRoot, options, intent) {
|
|
156
330
|
const path = projectDefinitionPath(projectRoot);
|
|
157
331
|
let valueFeedbackChanged = false;
|
|
158
332
|
let productSignalChanged = false;
|
|
333
|
+
let ran = false;
|
|
334
|
+
let skippedReason = null;
|
|
335
|
+
const existing = intent.existingMarker;
|
|
159
336
|
projectDefinitionMutationLock(projectRoot, () => {
|
|
160
337
|
const parsed = JSON.parse(readFileSync(path, { encoding: "utf8" }));
|
|
161
338
|
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
@@ -176,6 +353,36 @@ export function runOrgForceOnMigration(projectRoot, options = {}) {
|
|
|
176
353
|
const policy = ensurePolicyBlock(plan);
|
|
177
354
|
const previousVf = policy.valueFeedback;
|
|
178
355
|
const previousPs = policy.productSignal;
|
|
356
|
+
// Revalidate under lock so a concurrent intentional opt-out between the
|
|
357
|
+
// unlocked snapshot check and lock acquisition is not overwritten (#2903).
|
|
358
|
+
let needsVf;
|
|
359
|
+
let needsPs;
|
|
360
|
+
if (existing !== null) {
|
|
361
|
+
const previousVfPresent = "previousValueFeedback" in existing;
|
|
362
|
+
const previousPsPresent = "previousProductSignal" in existing;
|
|
363
|
+
needsVf = isIncompleteForceOnField(valueFeedbackNeedsForceOn(previousVf), previousVf, existing.previousValueFeedback, previousVfPresent, "valueFeedback");
|
|
364
|
+
needsPs = isIncompleteForceOnField(productSignalNeedsForceOn(previousPs), previousPs, existing.previousProductSignal, previousPsPresent, "productSignal");
|
|
365
|
+
}
|
|
366
|
+
else {
|
|
367
|
+
needsVf = valueFeedbackNeedsForceOn(previousVf);
|
|
368
|
+
needsPs = productSignalNeedsForceOn(previousPs);
|
|
369
|
+
}
|
|
370
|
+
if (!needsVf && !needsPs) {
|
|
371
|
+
// Under-lock state no longer needs force-on (concurrent opt-out or already ON).
|
|
372
|
+
if (existing === null) {
|
|
373
|
+
writeMarker(projectRoot, options, {
|
|
374
|
+
valueFeedback: false,
|
|
375
|
+
productSignal: false,
|
|
376
|
+
previousValueFeedback: previousVf,
|
|
377
|
+
previousProductSignal: previousPs,
|
|
378
|
+
});
|
|
379
|
+
skippedReason = "already-enabled";
|
|
380
|
+
}
|
|
381
|
+
else {
|
|
382
|
+
skippedReason = "marker-present";
|
|
383
|
+
}
|
|
384
|
+
return { changed: false };
|
|
385
|
+
}
|
|
179
386
|
if (needsVf) {
|
|
180
387
|
policy.valueFeedback = { ...FORCE_ON_VALUE_FEEDBACK_BLOCK };
|
|
181
388
|
valueFeedbackChanged = valueFeedbackNeedsForceOn(previousVf);
|
|
@@ -193,18 +400,49 @@ export function runOrgForceOnMigration(projectRoot, options = {}) {
|
|
|
193
400
|
if (valueFeedbackChanged || productSignalChanged) {
|
|
194
401
|
atomicWriteProjectDefinition(path, rootData);
|
|
195
402
|
}
|
|
403
|
+
// Marker previous*: forced fields record pre-apply current; unforced fields
|
|
404
|
+
// keep the prior marker snapshot so intentional opt-outs are not rewritten
|
|
405
|
+
// into the incomplete-migration equality path on the next update (#2903 P1).
|
|
406
|
+
// When the prior marker had no previous* (legacy) and the field was not forced,
|
|
407
|
+
// store ORG_FORCE_ON_NO_BASELINE — not null/live shape — so clear-value-feedback
|
|
408
|
+
// (undefined→null) and intentional opt-outs never deep-equal previous*.
|
|
409
|
+
const markerPreviousVf = needsVf
|
|
410
|
+
? previousVf
|
|
411
|
+
: existing && "previousValueFeedback" in existing
|
|
412
|
+
? existing.previousValueFeedback
|
|
413
|
+
: ORG_FORCE_ON_NO_BASELINE;
|
|
414
|
+
const markerPreviousPs = needsPs
|
|
415
|
+
? previousPs
|
|
416
|
+
: existing && "previousProductSignal" in existing
|
|
417
|
+
? existing.previousProductSignal
|
|
418
|
+
: ORG_FORCE_ON_NO_BASELINE;
|
|
196
419
|
const actor = options.actor ?? "directive-update";
|
|
197
420
|
appendAuditLog(projectRoot, [
|
|
198
421
|
`actor=${actor}`,
|
|
199
422
|
"org-force-on-v2822",
|
|
200
423
|
`valueFeedback=${needsVf ? "forced-on" : "unchanged"}`,
|
|
201
424
|
`productSignal=${needsPs ? "forced-on" : "unchanged"}`,
|
|
202
|
-
`previousValueFeedback=${JSON.stringify(
|
|
203
|
-
`previousProductSignal=${JSON.stringify(
|
|
425
|
+
`previousValueFeedback=${JSON.stringify(normalizePolicySnapshot(markerPreviousVf))}`,
|
|
426
|
+
`previousProductSignal=${JSON.stringify(normalizePolicySnapshot(markerPreviousPs))}`,
|
|
204
427
|
].join(" "));
|
|
205
|
-
|
|
428
|
+
writeMarker(projectRoot, options, {
|
|
429
|
+
// OR with existing so partial re-apply does not clear install-force-on source.
|
|
430
|
+
valueFeedback: needsVf || (existing?.valueFeedback ?? false),
|
|
431
|
+
productSignal: needsPs || (existing?.productSignal ?? false),
|
|
432
|
+
previousValueFeedback: markerPreviousVf,
|
|
433
|
+
previousProductSignal: markerPreviousPs,
|
|
434
|
+
});
|
|
435
|
+
ran = true;
|
|
206
436
|
return { changed: valueFeedbackChanged || productSignalChanged };
|
|
207
437
|
});
|
|
438
|
+
if (!ran) {
|
|
439
|
+
return {
|
|
440
|
+
ran: false,
|
|
441
|
+
skippedReason: skippedReason ?? "marker-present",
|
|
442
|
+
valueFeedbackChanged: false,
|
|
443
|
+
productSignalChanged: false,
|
|
444
|
+
};
|
|
445
|
+
}
|
|
208
446
|
return {
|
|
209
447
|
ran: true,
|
|
210
448
|
skippedReason: null,
|
|
@@ -51,4 +51,45 @@ export interface RuntimeAuthorityDirectWriteResult {
|
|
|
51
51
|
* Unclassifiable paths (null) pass — host gap documented for Shell/MCP.
|
|
52
52
|
*/
|
|
53
53
|
export declare function evaluateRuntimeAuthorityDirectWrite(input: RuntimeAuthorityDirectWriteInput): RuntimeAuthorityDirectWriteResult;
|
|
54
|
+
/** Classifiable shell/MCP operation for scopes.push / scopes.merge (#2711). */
|
|
55
|
+
export type RuntimeAuthorityShellOp = "push" | "merge";
|
|
56
|
+
/**
|
|
57
|
+
* List all classifiable push/merge ops in a shell command (#2711).
|
|
58
|
+
* Scans every list/pipeline/newline segment so compound commands like
|
|
59
|
+
* `gh pr merge 1 && git push` surface both ops (dispatcher evaluates each).
|
|
60
|
+
* Newlines are delimiters so multi-line scripts cannot hide a later push/merge.
|
|
61
|
+
*/
|
|
62
|
+
export declare function listShellOps(command: string): RuntimeAuthorityShellOp[];
|
|
63
|
+
/**
|
|
64
|
+
* Classify a shell command string for push/merge scopes (#2711).
|
|
65
|
+
* Unclassifiable commands return null (fail open at the gate).
|
|
66
|
+
* When a compound command has multiple ops, returns the first of listShellOps
|
|
67
|
+
* (push before merge); prefer listShellOps + evaluate-each for enforcement.
|
|
68
|
+
*
|
|
69
|
+
* Patterns (intentionally narrow; prefer false-open over false-deny):
|
|
70
|
+
* - push: `git push`, `git.exe push`, with optional env / -C / -c prefixes
|
|
71
|
+
* - merge: `gh pr merge`, `gh.exe pr merge`
|
|
72
|
+
*/
|
|
73
|
+
export declare function classifyShellCommand(command: string): RuntimeAuthorityShellOp | null;
|
|
74
|
+
/**
|
|
75
|
+
* Classify an MCP (or MCP-like) tool name + optional argument blob for push/merge (#2711).
|
|
76
|
+
* Returns null when the tool is not a known push/merge mutation (fail open).
|
|
77
|
+
*/
|
|
78
|
+
export declare function classifyMcpTool(toolName: string, argsText?: string | null): RuntimeAuthorityShellOp | null;
|
|
79
|
+
export interface RuntimeAuthorityShellOpInput {
|
|
80
|
+
readonly policy: RuntimeAuthorityPolicy;
|
|
81
|
+
readonly op: RuntimeAuthorityShellOp | null;
|
|
82
|
+
}
|
|
83
|
+
export interface RuntimeAuthorityShellOpResult {
|
|
84
|
+
readonly allowed: boolean;
|
|
85
|
+
readonly reason: string | null;
|
|
86
|
+
readonly code: "runtime-policy-deny-scope" | null;
|
|
87
|
+
/** True when op was null — gate should fail open. */
|
|
88
|
+
readonly unclassifiable: boolean;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Evaluate scopes.push / scopes.merge for a classifiable shell/MCP operation (#2711).
|
|
92
|
+
* null op → allow (unclassifiable fail-open). disabled policy → allow.
|
|
93
|
+
*/
|
|
94
|
+
export declare function evaluateRuntimeAuthorityShellOp(input: RuntimeAuthorityShellOpInput): RuntimeAuthorityShellOpResult;
|
|
54
95
|
//# sourceMappingURL=runtime-authority.d.ts.map
|