@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.
Files changed (70) hide show
  1. package/dist/cache/scanner.d.ts +11 -1
  2. package/dist/cache/scanner.js +29 -4
  3. package/dist/check/gate-lists.js +2 -0
  4. package/dist/content-contracts/skills/helpers.d.ts +10 -0
  5. package/dist/content-contracts/skills/helpers.js +35 -0
  6. package/dist/deposit/copy-tree.d.ts +19 -1
  7. package/dist/deposit/copy-tree.js +134 -5
  8. package/dist/doctor/main.d.ts +6 -5
  9. package/dist/doctor/main.js +80 -18
  10. package/dist/doctor/taskfile.d.ts +8 -0
  11. package/dist/doctor/taskfile.js +19 -0
  12. package/dist/fs/projection-containment.d.ts +18 -0
  13. package/dist/fs/projection-containment.js +40 -0
  14. package/dist/hooks/dispatcher.d.ts +34 -2
  15. package/dist/hooks/dispatcher.js +234 -21
  16. package/dist/hooks/tools.d.ts +31 -0
  17. package/dist/hooks/tools.js +74 -0
  18. package/dist/init-deposit/agent-hooks.d.ts +1 -1
  19. package/dist/init-deposit/agent-hooks.js +38 -2
  20. package/dist/init-deposit/hygiene.d.ts +16 -0
  21. package/dist/init-deposit/hygiene.js +26 -0
  22. package/dist/init-deposit/init-dispatch.js +28 -0
  23. package/dist/init-deposit/prettierignore.js +2 -2
  24. package/dist/init-deposit/refresh.js +38 -7
  25. package/dist/init-deposit/scaffold.js +7 -3
  26. package/dist/init-deposit/xbrief-projections.js +6 -6
  27. package/dist/intake/issue-emit.d.ts +45 -2
  28. package/dist/intake/issue-emit.js +420 -17
  29. package/dist/intake/issue-ingest.js +65 -6
  30. package/dist/packs/pack-render.d.ts +33 -0
  31. package/dist/packs/pack-render.js +155 -9
  32. package/dist/packs/quarantine-ext.d.ts +10 -0
  33. package/dist/packs/quarantine-ext.js +26 -2
  34. package/dist/platform/platform-capabilities.js +3 -0
  35. package/dist/policy/index.d.ts +1 -0
  36. package/dist/policy/index.js +1 -0
  37. package/dist/policy/no-deft-directive.d.ts +59 -0
  38. package/dist/policy/no-deft-directive.js +103 -0
  39. package/dist/policy/org-force-on-migration.d.ts +52 -0
  40. package/dist/policy/org-force-on-migration.js +260 -22
  41. package/dist/policy/runtime-authority.d.ts +41 -0
  42. package/dist/policy/runtime-authority.js +274 -0
  43. package/dist/review-monitor/constants.js +3 -2
  44. package/dist/review-monitor/tier-detection.d.ts +6 -2
  45. package/dist/review-monitor/tier-detection.js +27 -2
  46. package/dist/scope/transition.js +43 -0
  47. package/dist/session/release-availability.d.ts +2 -0
  48. package/dist/session/release-availability.js +23 -8
  49. package/dist/session/session-start-hook.d.ts +3 -0
  50. package/dist/session/session-start-hook.js +15 -0
  51. package/dist/session/session-start.js +30 -0
  52. package/dist/swarm/routing-set-cli.js +5 -10
  53. package/dist/swarm/routing.d.ts +3 -2
  54. package/dist/swarm/routing.js +16 -4
  55. package/dist/triage/help/registry-data.d.ts +7 -7
  56. package/dist/triage/help/registry-data.js +15 -6
  57. package/dist/triage/queue/index.d.ts +1 -0
  58. package/dist/triage/queue/index.js +1 -0
  59. package/dist/triage/queue/show.d.ts +69 -0
  60. package/dist/triage/queue/show.js +293 -0
  61. package/dist/triage/scope/cli.js +3 -0
  62. package/dist/triage/scope/coverage.d.ts +2 -0
  63. package/dist/triage/scope/coverage.js +18 -3
  64. package/dist/verify-source/cursor-tier1.js +7 -2
  65. package/dist/verify-source/index.d.ts +1 -0
  66. package/dist/verify-source/index.js +1 -0
  67. package/dist/verify-source/openclaw-tier1.d.ts +37 -0
  68. package/dist/verify-source/openclaw-tier1.js +105 -0
  69. package/dist/xbrief-migrate/migrate-project.js +9 -5
  70. 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
- return {
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 writeMarkerForSkip(projectRoot, options, valueFeedback, productSignal) {
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
- if (readOrgForceOnMarker(projectRoot) !== null) {
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: "marker-present",
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 [data, err] = loadProjectDefinition(projectRoot);
131
- if (data === null) {
304
+ const snapshots = readPolicyFieldSnapshots(projectRoot);
305
+ if (!snapshots.ok) {
132
306
  return {
133
307
  ran: false,
134
- skippedReason: err ?? "project-definition-missing",
308
+ skippedReason: snapshots.skippedReason,
135
309
  valueFeedbackChanged: false,
136
310
  productSignalChanged: false,
137
311
  };
138
312
  }
139
- const policyBlock = readPlanPolicy(data.plan);
140
- const policyObj = typeof policyBlock === "object" && policyBlock !== null && !Array.isArray(policyBlock)
141
- ? policyBlock
142
- : {};
143
- const vfRaw = "valueFeedback" in policyObj ? policyObj.valueFeedback : undefined;
144
- const psRaw = "productSignal" in policyObj ? policyObj.productSignal : undefined;
145
- const needsVf = valueFeedbackNeedsForceOn(vfRaw);
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(previousVf ?? null)}`,
203
- `previousProductSignal=${JSON.stringify(previousPs ?? null)}`,
425
+ `previousValueFeedback=${JSON.stringify(normalizePolicySnapshot(markerPreviousVf))}`,
426
+ `previousProductSignal=${JSON.stringify(normalizePolicySnapshot(markerPreviousPs))}`,
204
427
  ].join(" "));
205
- writeMarkerForSkip(projectRoot, options, needsVf, needsPs);
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