@deftai/directive-core 0.108.0 → 0.109.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (138) hide show
  1. package/dist/cache/operations.js +1 -1
  2. package/dist/check/gate-lists.d.ts +14 -0
  3. package/dist/check/gate-lists.js +26 -3
  4. package/dist/check/session-completed-ac.d.ts +1 -1
  5. package/dist/check/session-completed-ac.js +1 -1
  6. package/dist/consumer-check-contract/evaluate.d.ts +47 -0
  7. package/dist/consumer-check-contract/evaluate.js +181 -15
  8. package/dist/delivery-attempt/evaluate.d.ts +9 -1
  9. package/dist/delivery-attempt/evaluate.js +69 -0
  10. package/dist/delivery-attempt/index.d.ts +1 -1
  11. package/dist/delivery-attempt/index.js +1 -1
  12. package/dist/deposit/live-procedure-exclusions.d.ts +18 -0
  13. package/dist/deposit/live-procedure-exclusions.js +110 -0
  14. package/dist/deposit/live-procedure-targets.d.ts +45 -0
  15. package/dist/deposit/live-procedure-targets.js +274 -0
  16. package/dist/deposit/python-free.d.ts +6 -0
  17. package/dist/deposit/python-free.js +15 -0
  18. package/dist/deposit/rewrite-deposit-links.d.ts +42 -0
  19. package/dist/deposit/rewrite-deposit-links.js +148 -0
  20. package/dist/deposit/run-stage-content-pack.d.ts +2 -0
  21. package/dist/deposit/run-stage-content-pack.js +3 -0
  22. package/dist/deposit/stage-content-pack.d.ts +17 -0
  23. package/dist/deposit/stage-content-pack.js +91 -0
  24. package/dist/design-critique/citation-grammar.d.ts +7 -0
  25. package/dist/design-critique/citation-grammar.js +1 -1
  26. package/dist/design-critique/completed-arc-record.d.ts +1 -1
  27. package/dist/design-critique/completed-arc-record.js +59 -14
  28. package/dist/hooks/classify/host-session-identity.d.ts +24 -11
  29. package/dist/hooks/classify/host-session-identity.js +46 -49
  30. package/dist/hooks/classify/index.d.ts +2 -2
  31. package/dist/hooks/classify/index.js +2 -2
  32. package/dist/hooks/classify/paths.d.ts +2 -0
  33. package/dist/hooks/classify/paths.js +8 -4
  34. package/dist/hooks/classify/stdin.js +69 -3
  35. package/dist/hooks/dest-form.d.ts +20 -1
  36. package/dist/hooks/dest-form.js +158 -21
  37. package/dist/hooks/dispatcher.d.ts +6 -2
  38. package/dist/hooks/dispatcher.js +258 -62
  39. package/dist/hooks/fixtures/cases.d.ts +6 -0
  40. package/dist/hooks/fixtures/cases.js +53 -0
  41. package/dist/hooks/git-destructive-log.d.ts +32 -0
  42. package/dist/hooks/git-destructive-log.js +46 -0
  43. package/dist/hooks/index.d.ts +2 -0
  44. package/dist/hooks/index.js +2 -0
  45. package/dist/hooks/shell-write-targets.d.ts +10 -0
  46. package/dist/hooks/shell-write-targets.js +274 -0
  47. package/dist/hooks/tools.d.ts +20 -2
  48. package/dist/hooks/tools.js +30 -1
  49. package/dist/init-deposit/gitignore.d.ts +7 -0
  50. package/dist/init-deposit/gitignore.js +24 -0
  51. package/dist/init-deposit/init-deposit.js +3 -0
  52. package/dist/init-deposit/refresh.js +7 -0
  53. package/dist/init-deposit/runtime-writers.d.ts +14 -0
  54. package/dist/init-deposit/runtime-writers.js +33 -0
  55. package/dist/intake/issue-ingest.d.ts +6 -1
  56. package/dist/intake/issue-ingest.js +21 -2
  57. package/dist/intake/reconcile-issues.js +13 -13
  58. package/dist/lifecycle/brief-envelope.d.ts +25 -0
  59. package/dist/lifecycle/brief-envelope.js +42 -0
  60. package/dist/lifecycle/index.d.ts +1 -0
  61. package/dist/lifecycle/index.js +1 -0
  62. package/dist/literal-acceptance/evaluate.js +14 -5
  63. package/dist/literal-acceptance/index.d.ts +1 -1
  64. package/dist/literal-acceptance/index.js +1 -1
  65. package/dist/literal-acceptance/run.d.ts +2 -0
  66. package/dist/literal-acceptance/run.js +19 -1
  67. package/dist/orphan-active/candidate-scope.d.ts +53 -0
  68. package/dist/orphan-active/candidate-scope.js +157 -0
  69. package/dist/orphan-active/evaluate.d.ts +27 -0
  70. package/dist/orphan-active/evaluate.js +63 -6
  71. package/dist/orphan-active/index.d.ts +1 -0
  72. package/dist/orphan-active/index.js +1 -0
  73. package/dist/policy/merge-approval-head.js +7 -6
  74. package/dist/pr-closing-keywords/gh.js +32 -9
  75. package/dist/pr-closing-keywords/main.d.ts +1 -0
  76. package/dist/pr-closing-keywords/main.js +68 -3
  77. package/dist/pr-closing-keywords/types.d.ts +2 -0
  78. package/dist/pr-merge-readiness/gh.js +32 -9
  79. package/dist/pr-protected-issues/gh.js +36 -9
  80. package/dist/pr-wait-mergeable/wrappers.js +3 -3
  81. package/dist/product-first-done-gate/evaluate.js +6 -2
  82. package/dist/product-first-done-gate/types.js +2 -0
  83. package/dist/release/consumer-hard-stops.d.ts +48 -0
  84. package/dist/release/consumer-hard-stops.js +140 -0
  85. package/dist/release/consumer-readiness-disclosure.d.ts +18 -0
  86. package/dist/release/consumer-readiness-disclosure.js +51 -0
  87. package/dist/release/index.d.ts +2 -0
  88. package/dist/release/index.js +2 -0
  89. package/dist/release/pipeline.js +55 -0
  90. package/dist/release/run-consumer-readiness.d.ts +15 -0
  91. package/dist/release/run-consumer-readiness.js +24 -0
  92. package/dist/release/types.d.ts +6 -0
  93. package/dist/review-monitor/github-lease.js +4 -3
  94. package/dist/run-summary/types.d.ts +2 -2
  95. package/dist/scm/build-command.d.ts +2 -2
  96. package/dist/scm/build-command.js +2 -2
  97. package/dist/scm/call-shape.d.ts +25 -0
  98. package/dist/scm/call-shape.js +59 -0
  99. package/dist/scm/call.d.ts +7 -4
  100. package/dist/scm/call.js +41 -11
  101. package/dist/scm/gh-rest.d.ts +25 -12
  102. package/dist/scm/gh-rest.js +65 -15
  103. package/dist/scm/index.d.ts +2 -0
  104. package/dist/scm/index.js +2 -0
  105. package/dist/scm/spawn-status.d.ts +33 -0
  106. package/dist/scm/spawn-status.js +53 -0
  107. package/dist/scope/acceptance-evidence.d.ts +1 -1
  108. package/dist/scope/transition.js +4 -14
  109. package/dist/session/ac-pass-banking.d.ts +2 -2
  110. package/dist/session/ac-pass-banking.js +2 -2
  111. package/dist/session/host-session-owner.d.ts +53 -0
  112. package/dist/session/host-session-owner.js +89 -0
  113. package/dist/session/index.d.ts +1 -0
  114. package/dist/session/index.js +1 -0
  115. package/dist/session/occupancy.d.ts +21 -1
  116. package/dist/session/occupancy.js +70 -12
  117. package/dist/session/verify-ac-session-cache.d.ts +2 -2
  118. package/dist/session/verify-ac-session-cache.js +2 -2
  119. package/dist/triage/evaluate/worktrees.js +153 -6
  120. package/dist/umbrella-current-shape/index.d.ts +51 -3
  121. package/dist/umbrella-current-shape/index.js +106 -18
  122. package/dist/validate-content/deposit-required.d.ts +39 -0
  123. package/dist/validate-content/deposit-required.js +147 -0
  124. package/dist/validate-content/index.d.ts +1 -0
  125. package/dist/validate-content/index.js +1 -0
  126. package/dist/validate-content/validate-links.d.ts +2 -3
  127. package/dist/validate-content/validate-links.js +29 -2
  128. package/dist/vbrief-activate/activate.d.ts +7 -2
  129. package/dist/vbrief-activate/activate.js +26 -13
  130. package/dist/verify-ac/evaluate.d.ts +9 -0
  131. package/dist/verify-ac/evaluate.js +32 -9
  132. package/dist/verify-source/deposit-closure.d.ts +23 -0
  133. package/dist/verify-source/deposit-closure.js +162 -0
  134. package/dist/verify-source/index.d.ts +2 -0
  135. package/dist/verify-source/index.js +2 -0
  136. package/dist/verify-source/semantic-single-source.d.ts +36 -0
  137. package/dist/verify-source/semantic-single-source.js +349 -0
  138. package/package.json +3 -3
@@ -3,12 +3,12 @@ import { mkdirSync, readdirSync, readFileSync, renameSync, statSync, writeFileSy
3
3
  import { join, resolve } from "node:path";
4
4
  import { assertProjectionContained, assertWriteTargetSafe, ProjectionContainmentError, } from "../fs/projection-containment.js";
5
5
  import { hasArtifactSuffix, resolveLifecycleRoot } from "../layout/resolve.js";
6
+ import { missingEnvelopeMessage, stampExistingEnvelopes } from "../lifecycle/brief-envelope.js";
6
7
  import { call } from "../scm/call.js";
7
8
  import { updateDecomposedChildBackReferences } from "../scope/decomposed-refs.js";
8
9
  import { resolveProjectRoot } from "../scope/project-context.js";
9
10
  import { resolveProjectRepo } from "../slice/project-context.js";
10
11
  import { FOLDER_ALLOWED_STATUSES } from "../vbrief-validate/constants.js";
11
- import { LEGACY_INFO_ROOT_KEY, MIGRATED_INFO_ROOT_KEY } from "../xbrief-migrate/constants.js";
12
12
  export const LIFECYCLE_FOLDERS = [
13
13
  "proposed",
14
14
  "pending",
@@ -713,14 +713,13 @@ export function applyLifecycleFixes(vbriefDir, report, projectRoot = null) {
713
713
  plan.status = terminalStatus;
714
714
  const stamp = utcNowIso();
715
715
  // Stamp `updated` into whichever info envelope the file already uses (#2346).
716
- // Canonical v0.8 briefs use `xBRIEFInfo`; stamping `vBRIEFInfo` unconditionally
717
- // appended a stray, version-less `vBRIEFInfo` block that then failed
718
- // `vbrief:validate` ('vBRIEFInfo.version' must be one of ..., got 'undefined').
719
- // Legacy briefs (or files without either envelope) keep the `vBRIEFInfo` key.
720
- const infoKey = MIGRATED_INFO_ROOT_KEY in data ? MIGRATED_INFO_ROOT_KEY : LEGACY_INFO_ROOT_KEY;
721
- const info = (data[infoKey] ?? {});
722
- data[infoKey] = info;
723
- info.updated = stamp;
716
+ // A file carrying neither envelope is already invalid; creating one here hid
717
+ // that cause behind a manufactured version-less `vBRIEFInfo` (#3933), so the
718
+ // sweep now refuses it by name and leaves the artifact in place.
719
+ if (stampExistingEnvelopes(data, stamp).length === 0) {
720
+ failures.push(missingEnvelopeMessage(relPath));
721
+ continue;
722
+ }
724
723
  plan.updated = stamp;
725
724
  propagateItemStatus(plan.items, terminalStatus, stamp);
726
725
  try {
@@ -780,12 +779,13 @@ export function repairCompletedStatusDrift(vbriefDir, drift, projectRoot = null)
780
779
  continue;
781
780
  }
782
781
  const stamp = utcNowIso();
782
+ // Same envelope policy as the closed-issue sweep above (#3933).
783
+ if (stampExistingEnvelopes(data, stamp).length === 0) {
784
+ failures.push(missingEnvelopeMessage(entry.rel_path));
785
+ continue;
786
+ }
783
787
  plan.status = "completed";
784
788
  plan.updated = stamp;
785
- const infoKey = MIGRATED_INFO_ROOT_KEY in data ? MIGRATED_INFO_ROOT_KEY : LEGACY_INFO_ROOT_KEY;
786
- const info = (data[infoKey] ?? {});
787
- data[infoKey] = info;
788
- info.updated = stamp;
789
789
  propagateItemStatus(plan.items, "completed", stamp);
790
790
  try {
791
791
  writeFileSync(src, `${JSON.stringify(data, null, 2)}\n`, "utf8");
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Shared document-envelope policy for lifecycle mutators (#3933).
3
+ *
4
+ * A brief carries one document envelope: `xBRIEFInfo` on canonical v0.8
5
+ * artifacts, `vBRIEFInfo` on legacy v0.6 ones. A mutator stamps whichever
6
+ * envelope the artifact already has and creates none. Creating one on a v0.8
7
+ * brief appends a version-less `vBRIEFInfo`, and `validateVbriefSchema`
8
+ * resolves `vBRIEFInfo` first, so the manufactured block wins and the brief the
9
+ * mutator just wrote fails validation (#2346 / #2862 / #3933).
10
+ */
11
+ import { LEGACY_INFO_ROOT_KEY, MIGRATED_INFO_ROOT_KEY } from "../xbrief-migrate/constants.js";
12
+ export type BriefEnvelopeKey = typeof MIGRATED_INFO_ROOT_KEY | typeof LEGACY_INFO_ROOT_KEY;
13
+ /** Envelope keys in canonical order: v0.8 first, legacy v0.6 second. */
14
+ export declare const BRIEF_ENVELOPE_KEYS: readonly BriefEnvelopeKey[];
15
+ /** Envelope keys the artifact already carries as JSON objects. */
16
+ export declare function presentEnvelopeKeys(data: Record<string, unknown>): BriefEnvelopeKey[];
17
+ /**
18
+ * Stamp `updated` on every envelope the artifact already carries; create none.
19
+ * Returns the stamped keys so a caller can refuse an envelope-less artifact by
20
+ * name instead of manufacturing one.
21
+ */
22
+ export declare function stampExistingEnvelopes(data: Record<string, unknown>, nowIso: string): BriefEnvelopeKey[];
23
+ /** Refusal naming both accepted envelopes, for a caller-supplied subject (#3933). */
24
+ export declare function missingEnvelopeMessage(subject: string): string;
25
+ //# sourceMappingURL=brief-envelope.d.ts.map
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Shared document-envelope policy for lifecycle mutators (#3933).
3
+ *
4
+ * A brief carries one document envelope: `xBRIEFInfo` on canonical v0.8
5
+ * artifacts, `vBRIEFInfo` on legacy v0.6 ones. A mutator stamps whichever
6
+ * envelope the artifact already has and creates none. Creating one on a v0.8
7
+ * brief appends a version-less `vBRIEFInfo`, and `validateVbriefSchema`
8
+ * resolves `vBRIEFInfo` first, so the manufactured block wins and the brief the
9
+ * mutator just wrote fails validation (#2346 / #2862 / #3933).
10
+ */
11
+ import { LEGACY_INFO_ROOT_KEY, MIGRATED_INFO_ROOT_KEY } from "../xbrief-migrate/constants.js";
12
+ /** Envelope keys in canonical order: v0.8 first, legacy v0.6 second. */
13
+ export const BRIEF_ENVELOPE_KEYS = [
14
+ MIGRATED_INFO_ROOT_KEY,
15
+ LEGACY_INFO_ROOT_KEY,
16
+ ];
17
+ /** Envelope keys the artifact already carries as JSON objects. */
18
+ export function presentEnvelopeKeys(data) {
19
+ return BRIEF_ENVELOPE_KEYS.filter((key) => {
20
+ const env = data[key];
21
+ return typeof env === "object" && env !== null && !Array.isArray(env);
22
+ });
23
+ }
24
+ /**
25
+ * Stamp `updated` on every envelope the artifact already carries; create none.
26
+ * Returns the stamped keys so a caller can refuse an envelope-less artifact by
27
+ * name instead of manufacturing one.
28
+ */
29
+ export function stampExistingEnvelopes(data, nowIso) {
30
+ const stamped = presentEnvelopeKeys(data);
31
+ for (const key of stamped) {
32
+ data[key].updated = nowIso;
33
+ }
34
+ return stamped;
35
+ }
36
+ /** Refusal naming both accepted envelopes, for a caller-supplied subject (#3933). */
37
+ export function missingEnvelopeMessage(subject) {
38
+ return (`${subject} carries neither \`${MIGRATED_INFO_ROOT_KEY}\` (v0.8) nor ` +
39
+ `\`${LEGACY_INFO_ROOT_KEY}\` (v0.6) -- malformed. Lifecycle mutators refuse ` +
40
+ "rather than create one (#3933).");
41
+ }
42
+ //# sourceMappingURL=brief-envelope.js.map
@@ -1,3 +1,4 @@
1
+ export * from "./brief-envelope.js";
1
2
  export * from "./completed-consistency.js";
2
3
  export * from "./completed-tracked-on-delivery.js";
3
4
  export * from "./completed-write-guard.js";
@@ -1,3 +1,4 @@
1
+ export * from "./brief-envelope.js";
1
2
  export * from "./completed-consistency.js";
2
3
  export * from "./completed-tracked-on-delivery.js";
3
4
  export * from "./completed-write-guard.js";
@@ -189,16 +189,25 @@ export function evaluateLiteralAcceptanceFromPlan(plan, options = {}) {
189
189
  // Rejected stated commands are fail-closed for completion (#3267 Greptile conf residual).
190
190
  // A safety-rejected shell-shaped acceptance line is not "optional diagnostic" — operators
191
191
  // must promote a safe alternative or remove the stated command from the task statement.
192
+ const mergedRejected = [];
193
+ const seenRejected = new Set();
194
+ for (const row of [...(result.rejected ?? []), ...resolved.rejected]) {
195
+ const key = `${row.command}\0${row.reason}`;
196
+ if (seenRejected.has(key))
197
+ continue;
198
+ seenRejected.add(key);
199
+ mergedRejected.push(row);
200
+ }
192
201
  let ok = result.ok;
193
202
  let code = result.code;
194
- let message = appendRejectedNote(result.message, resolved.rejected);
195
- if (resolved.rejected.length > 0) {
203
+ let message = appendRejectedNote(result.message, mergedRejected);
204
+ if (mergedRejected.length > 0) {
196
205
  ok = false;
197
206
  if (code === 0)
198
207
  code = 1;
199
- const ledger = formatRejectedLedger(resolved.rejected);
208
+ const ledger = formatRejectedLedger(mergedRejected);
200
209
  message =
201
- `Literal acceptance-command gate FAILED (#3267): ${resolved.rejected.length} ` +
210
+ `Literal acceptance-command gate FAILED (#3267): ${mergedRejected.length} ` +
202
211
  `safety-rejected stated command(s) block completion until resolved ` +
203
212
  `(promote a safe alternative or remove from the task statement).\n` +
204
213
  ledger +
@@ -212,7 +221,7 @@ export function evaluateLiteralAcceptanceFromPlan(plan, options = {}) {
212
221
  ...result,
213
222
  ok,
214
223
  code,
215
- rejected: resolved.rejected,
224
+ rejected: mergedRejected,
216
225
  advisoryRejected: resolved.advisoryRejected,
217
226
  transcriptPromptSkipped: resolved.transcriptPromptSkipped,
218
227
  message,
@@ -6,7 +6,7 @@
6
6
  */
7
7
  export { attachLiteralAcceptanceCommands, type CaptureLiteralAcceptanceResult, captureAndAttachLiteralAcceptance, captureLiteralAcceptanceCommands, captureLiteralAcceptanceCommandsDetailed, formatRejectedLedger, hasStructuredAcceptanceCommands, INLINE_PROSE_MENTION_REASON, isInlineProseMention, isProseDerivedRejection, readNotAcceptanceCommands, readStoredLiteralAcceptanceCommands, readStoredLiteralAcceptanceDetailed, } from "./capture.js";
8
8
  export { appendLiteralAcceptanceAdvisory, type EvaluateLiteralAcceptanceOptions, evaluateLiteralAcceptanceFromPath, evaluateLiteralAcceptanceFromPlan, formatLiteralAcceptanceAdvisory, isLiteralAcceptanceRequiredAtCeremonyDepth, LITERAL_ACCEPTANCE_ADVISORY_MARKER, type ResolvedLiteralAcceptance, resolveLiteralAcceptanceCommands, resolveLiteralAcceptanceDetailed, stripLiteralAcceptanceAdvisory, } from "./evaluate.js";
9
- export { defaultLiteralAcceptanceRunner, runLiteralAcceptanceCommand, runLiteralAcceptanceCommands, } from "./run.js";
9
+ export { defaultLiteralAcceptanceRunner, isSafetyRefusalRun, runLiteralAcceptanceCommand, runLiteralAcceptanceCommands, } from "./run.js";
10
10
  export { type CommandSafetyResult, evaluateCommandSafety, evaluateNoopDenylist, evaluateStampAcceptanceSafety, isExecutableLiteralSource, isNoopRefusalReason, isVerbatimStatementSpan, NOOP_ACCEPTANCE_REMEDIATION, REJECTED_NOOP_OUTCOME, type RejectedNoopOutcome, type StampAcceptanceCommand, type StampAcceptanceSafetyInput, type StampAcceptanceSafetyResult, type StampSourceRung, } from "./safety.js";
11
11
  export { EXECUTABLE_LITERAL_SOURCES, LITERAL_ACCEPTANCE_METADATA_KEY, LITERAL_ACCEPTANCE_METADATA_KEY_CAMEL, LITERAL_ACCEPTANCE_NOT_COMMANDS_METADATA_KEY, LITERAL_ACCEPTANCE_REJECTED_METADATA_KEY, type LiteralAcceptanceCommand, type LiteralAcceptanceGateResult, type LiteralAcceptanceRunner, type LiteralAcceptanceRunResult, type LiteralAcceptanceSource, type RejectedLiteralCommand, } from "./types.js";
12
12
  //# sourceMappingURL=index.d.ts.map
@@ -6,7 +6,7 @@
6
6
  */
7
7
  export { attachLiteralAcceptanceCommands, captureAndAttachLiteralAcceptance, captureLiteralAcceptanceCommands, captureLiteralAcceptanceCommandsDetailed, formatRejectedLedger, hasStructuredAcceptanceCommands, INLINE_PROSE_MENTION_REASON, isInlineProseMention, isProseDerivedRejection, readNotAcceptanceCommands, readStoredLiteralAcceptanceCommands, readStoredLiteralAcceptanceDetailed, } from "./capture.js";
8
8
  export { appendLiteralAcceptanceAdvisory, evaluateLiteralAcceptanceFromPath, evaluateLiteralAcceptanceFromPlan, formatLiteralAcceptanceAdvisory, isLiteralAcceptanceRequiredAtCeremonyDepth, LITERAL_ACCEPTANCE_ADVISORY_MARKER, resolveLiteralAcceptanceCommands, resolveLiteralAcceptanceDetailed, stripLiteralAcceptanceAdvisory, } from "./evaluate.js";
9
- export { defaultLiteralAcceptanceRunner, runLiteralAcceptanceCommand, runLiteralAcceptanceCommands, } from "./run.js";
9
+ export { defaultLiteralAcceptanceRunner, isSafetyRefusalRun, runLiteralAcceptanceCommand, runLiteralAcceptanceCommands, } from "./run.js";
10
10
  export { evaluateCommandSafety, evaluateNoopDenylist, evaluateStampAcceptanceSafety, isExecutableLiteralSource, isNoopRefusalReason, isVerbatimStatementSpan, NOOP_ACCEPTANCE_REMEDIATION, REJECTED_NOOP_OUTCOME, } from "./safety.js";
11
11
  export { EXECUTABLE_LITERAL_SOURCES, LITERAL_ACCEPTANCE_METADATA_KEY, LITERAL_ACCEPTANCE_METADATA_KEY_CAMEL, LITERAL_ACCEPTANCE_NOT_COMMANDS_METADATA_KEY, LITERAL_ACCEPTANCE_REJECTED_METADATA_KEY, } from "./types.js";
12
12
  //# sourceMappingURL=index.js.map
@@ -6,6 +6,8 @@
6
6
  * Self-chosen verification is supplementary — this gate only runs stored literals.
7
7
  */
8
8
  import type { LiteralAcceptanceCommand, LiteralAcceptanceGateResult, LiteralAcceptanceRunner, LiteralAcceptanceRunResult } from "./types.js";
9
+ /** True when the row is a pre-execution safety refusal, not a product measurement (#3615). */
10
+ export declare function isSafetyRefusalRun(run: Pick<LiteralAcceptanceRunResult, "exitCode" | "detail">): boolean;
9
11
  /** Default shell runner: exact command string via platform shell. */
10
12
  export declare function defaultLiteralAcceptanceRunner(input: {
11
13
  readonly command: string;
@@ -9,6 +9,13 @@ import { spawnSync } from "node:child_process";
9
9
  import { isAbsolute, resolve } from "node:path";
10
10
  import { isInlineProseMention } from "./capture.js";
11
11
  import { evaluateCommandSafety, isExecutableLiteralSource } from "./safety.js";
12
+ /** True when the row is a pre-execution safety refusal, not a product measurement (#3615). */
13
+ export function isSafetyRefusalRun(run) {
14
+ return run.exitCode === 2 && /^refused:\s*/i.test(run.detail);
15
+ }
16
+ function safetyRefusalReason(run) {
17
+ return run.detail.replace(/^refused:\s*/i, "").trim();
18
+ }
12
19
  /** Default shell runner: exact command string via platform shell. */
13
20
  export function defaultLiteralAcceptanceRunner(input) {
14
21
  // Windows: cmd.exe /c; POSIX: shell true uses sh -c. Exact string preserved.
@@ -173,6 +180,7 @@ export function runLiteralAcceptanceCommands(commands, options) {
173
180
  };
174
181
  }
175
182
  const runs = [];
183
+ const rejected = [];
176
184
  for (const cmd of executable) {
177
185
  if (typeof cmd.command !== "string" || cmd.command.trim().length === 0) {
178
186
  return {
@@ -181,9 +189,18 @@ export function runLiteralAcceptanceCommands(commands, options) {
181
189
  message: "Literal acceptance-command gate config error: empty command entry (#3267)",
182
190
  commands,
183
191
  runs,
192
+ rejected: rejected.length > 0 ? rejected : undefined,
184
193
  };
185
194
  }
186
- runs.push(runLiteralAcceptanceCommand(cmd, options));
195
+ const run = runLiteralAcceptanceCommand(cmd, options);
196
+ runs.push(run);
197
+ if (isSafetyRefusalRun(run)) {
198
+ rejected.push({
199
+ command: run.command,
200
+ reason: safetyRefusalReason(run),
201
+ sourceSpan: cmd.sourceSpan ?? null,
202
+ });
203
+ }
187
204
  }
188
205
  const failed = runs.filter((r) => !r.ok);
189
206
  if (failed.length === 0) {
@@ -213,6 +230,7 @@ export function runLiteralAcceptanceCommands(commands, options) {
213
230
  lines.join("\n"),
214
231
  commands,
215
232
  runs,
233
+ rejected: rejected.length > 0 ? rejected : undefined,
216
234
  };
217
235
  }
218
236
  function truncate(s, max) {
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Candidate diff ownership for the merge-chokepoint orphan run (#3893).
3
+ *
4
+ * The unscoped sweep enumerates every running brief under `active/`. Composed
5
+ * on `check:merge` that gives one stranded brief merge authority over every
6
+ * open PR, and N stranded briefs make N single-brief lifecycle PRs mutually
7
+ * unmergeable — each leaves the other orphan. The pre-merge run cannot see the
8
+ * residue its own candidate is about to create either: that candidate's linked
9
+ * PR still reads `merged_at: null` while the gate runs.
10
+ *
11
+ * Scoping the merge run to the candidate's own diff removes the coupling
12
+ * without touching the detector. Repo-wide truth stays enforced at the
13
+ * delivery tip (HEAD on the delivery line falls back to the full sweep) and
14
+ * after merge (`--issue N`, #3429).
15
+ */
16
+ import { type GitRunner } from "../session/git.js";
17
+ export type CandidateScope = {
18
+ readonly kind: "diff";
19
+ /** Ref the candidate was diffed against (e.g. `origin/master`). */
20
+ readonly baseRef: string;
21
+ /** Absolute, platform-normalized brief paths this candidate owns. */
22
+ readonly paths: ReadonlySet<string>;
23
+ } | {
24
+ readonly kind: "sweep";
25
+ /** Why this run stayed repo-wide; always printed with the verdict. */
26
+ readonly reason: string;
27
+ };
28
+ export interface CandidateScopeOptions {
29
+ /** Explicit base ref; defaults to `origin/<deliveryBranch>` then the branch. */
30
+ readonly baseRef?: string | null;
31
+ readonly runGit?: GitRunner;
32
+ }
33
+ /**
34
+ * One comparable form for a git path and a readdir path.
35
+ *
36
+ * Only the directory is canonicalized: the file itself may be a deletion in
37
+ * the candidate diff and no longer exist on disk.
38
+ */
39
+ export declare function normalizeScopePath(path: string): string;
40
+ /**
41
+ * Base ref for the candidate diff: explicit override, else `origin/<branch>`,
42
+ * else the local delivery branch. Null when none resolves.
43
+ */
44
+ export declare function resolveCandidateBaseRef(projectRoot: string, override: string | null | undefined, runGit: GitRunner): string | null;
45
+ /**
46
+ * Brief paths under `activeDir` that the candidate's own diff touches.
47
+ *
48
+ * Falls back to the repo-wide sweep — never to a narrower scan — when git,
49
+ * the base ref, or the diff is unavailable, so the flag cannot silently
50
+ * shrink what a merge gate looks at.
51
+ */
52
+ export declare function resolveCandidateScope(projectRoot: string, activeDir: string, options?: CandidateScopeOptions): CandidateScope;
53
+ //# sourceMappingURL=candidate-scope.d.ts.map
@@ -0,0 +1,157 @@
1
+ /**
2
+ * Candidate diff ownership for the merge-chokepoint orphan run (#3893).
3
+ *
4
+ * The unscoped sweep enumerates every running brief under `active/`. Composed
5
+ * on `check:merge` that gives one stranded brief merge authority over every
6
+ * open PR, and N stranded briefs make N single-brief lifecycle PRs mutually
7
+ * unmergeable — each leaves the other orphan. The pre-merge run cannot see the
8
+ * residue its own candidate is about to create either: that candidate's linked
9
+ * PR still reads `merged_at: null` while the gate runs.
10
+ *
11
+ * Scoping the merge run to the candidate's own diff removes the coupling
12
+ * without touching the detector. Repo-wide truth stays enforced at the
13
+ * delivery tip (HEAD on the delivery line falls back to the full sweep) and
14
+ * after merge (`--issue N`, #3429).
15
+ */
16
+ import { realpathSync } from "node:fs";
17
+ import { basename, dirname, join, relative, resolve } from "node:path";
18
+ import { resolveDeliveryBranch } from "../policy/delivery-branch.js";
19
+ import { defaultGitRunner } from "../session/git.js";
20
+ /** Symlink-resolved path, or the input when it cannot be resolved. */
21
+ function realpathOrSelf(path) {
22
+ try {
23
+ return realpathSync.native(path);
24
+ }
25
+ catch {
26
+ try {
27
+ return realpathSync(path);
28
+ }
29
+ catch {
30
+ return path;
31
+ }
32
+ }
33
+ }
34
+ /**
35
+ * Canonical directory: symlinks resolved, then precomposed. Git reports the
36
+ * canonical worktree root while the lifecycle root keeps the caller's
37
+ * spelling, and git precomposes Unicode (`core.precomposeunicode`) while some
38
+ * filesystems hand `readdir` the decomposed form.
39
+ */
40
+ function canonicalDir(path) {
41
+ return realpathOrSelf(resolve(path)).normalize("NFC");
42
+ }
43
+ /**
44
+ * One comparable form for a git path and a readdir path.
45
+ *
46
+ * Only the directory is canonicalized: the file itself may be a deletion in
47
+ * the candidate diff and no longer exist on disk.
48
+ */
49
+ export function normalizeScopePath(path) {
50
+ const abs = resolve(path);
51
+ const full = join(canonicalDir(dirname(abs)), basename(abs)).normalize("NFC");
52
+ return process.platform === "win32" ? full.toLowerCase() : full;
53
+ }
54
+ function sweep(reason) {
55
+ return { kind: "sweep", reason };
56
+ }
57
+ /**
58
+ * Split `-z` git output. NUL-delimited paths are never C-quoted, so a brief
59
+ * whose name git would quote (non-ASCII, quotes, backslashes) still compares
60
+ * equal to the readdir path instead of silently falling outside the candidate
61
+ * scope. `GitRunner` returns `-z` output as latin1 bytes, so decode to UTF-8.
62
+ */
63
+ function splitNulPaths(stdout) {
64
+ return stdout
65
+ .split("\0")
66
+ .map((entry) => Buffer.from(entry, "latin1").toString("utf8"))
67
+ .filter((entry) => entry.length > 0);
68
+ }
69
+ function refExists(cwd, ref, runGit) {
70
+ return runGit(cwd, ["rev-parse", "--verify", "-q", `${ref}^{commit}`]).code === 0;
71
+ }
72
+ /**
73
+ * Base ref for the candidate diff: explicit override, else `origin/<branch>`,
74
+ * else the local delivery branch. Null when none resolves.
75
+ */
76
+ export function resolveCandidateBaseRef(projectRoot, override, runGit) {
77
+ const explicit = override?.trim() ?? "";
78
+ if (explicit.length > 0) {
79
+ return refExists(projectRoot, explicit, runGit) ? explicit : null;
80
+ }
81
+ const branch = resolveDeliveryBranch(projectRoot, runGit).branch;
82
+ for (const candidate of [`origin/${branch}`, branch]) {
83
+ if (refExists(projectRoot, candidate, runGit)) {
84
+ return candidate;
85
+ }
86
+ }
87
+ return null;
88
+ }
89
+ /**
90
+ * Brief paths under `activeDir` that the candidate's own diff touches.
91
+ *
92
+ * Falls back to the repo-wide sweep — never to a narrower scan — when git,
93
+ * the base ref, or the diff is unavailable, so the flag cannot silently
94
+ * shrink what a merge gate looks at.
95
+ */
96
+ export function resolveCandidateScope(projectRoot, activeDir, options = {}) {
97
+ const runGit = options.runGit ?? defaultGitRunner;
98
+ const root = resolve(projectRoot);
99
+ const topLevel = runGit(root, ["rev-parse", "--show-toplevel"]);
100
+ if (topLevel.code !== 0) {
101
+ return sweep("not a git worktree, so no candidate diff exists");
102
+ }
103
+ const gitRoot = topLevel.stdout.trim();
104
+ if (gitRoot.length === 0) {
105
+ return sweep("git worktree root could not be resolved");
106
+ }
107
+ const baseRef = resolveCandidateBaseRef(root, options.baseRef, runGit);
108
+ if (baseRef === null) {
109
+ const requested = options.baseRef?.trim() ?? "";
110
+ return sweep(requested.length > 0
111
+ ? `base ref '${requested}' not found; fetch it or pass a resolvable --base-ref`
112
+ : "no delivery base ref found (no origin/<deliveryBranch> or local branch); fetch it or pass --base-ref");
113
+ }
114
+ // HEAD at or behind the delivery line is the delivery tip itself, not a
115
+ // candidate: keep repo-wide truth there.
116
+ if (runGit(root, ["merge-base", "--is-ancestor", "HEAD", baseRef]).code === 0) {
117
+ return sweep(`HEAD is on the ${baseRef} delivery line, so this run is the delivery-tip check`);
118
+ }
119
+ const mergeBase = runGit(root, ["merge-base", "HEAD", baseRef]);
120
+ if (mergeBase.code !== 0) {
121
+ return sweep(`could not compute the merge base with ${baseRef}`);
122
+ }
123
+ const base = mergeBase.stdout.trim();
124
+ if (base.length === 0) {
125
+ return sweep(`merge base with ${baseRef} is empty`);
126
+ }
127
+ // Canonicalize both sides before the relative: a symlinked checkout or an
128
+ // unequal Unicode spelling of the same root would otherwise read as `..`
129
+ // and restore the repo-wide sweep.
130
+ const activeRel = relative(canonicalDir(gitRoot), canonicalDir(activeDir)).replace(/\\/g, "/");
131
+ if (activeRel.length === 0 || activeRel.startsWith("..")) {
132
+ return sweep("active/ is outside the git worktree");
133
+ }
134
+ // Diff against the working tree, not HEAD: a locally activated brief is a
135
+ // pre-commit `task check` candidate too.
136
+ const changed = runGit(gitRoot, ["diff", "--name-only", "-z", base, "--", activeRel]);
137
+ if (changed.code !== 0) {
138
+ return sweep(`could not diff active/ against ${baseRef}`);
139
+ }
140
+ const untracked = runGit(gitRoot, [
141
+ "ls-files",
142
+ "--others",
143
+ "--exclude-standard",
144
+ "-z",
145
+ "--",
146
+ activeRel,
147
+ ]);
148
+ if (untracked.code !== 0) {
149
+ return sweep("could not list untracked briefs under active/");
150
+ }
151
+ const paths = new Set();
152
+ for (const rel of [...splitNulPaths(changed.stdout), ...splitNulPaths(untracked.stdout)]) {
153
+ paths.add(normalizeScopePath(resolve(gitRoot, rel)));
154
+ }
155
+ return { kind: "diff", baseRef, paths };
156
+ }
157
+ //# sourceMappingURL=candidate-scope.js.map
@@ -1,4 +1,5 @@
1
1
  import type { RunGhFn } from "../pr-protected-issues/types.js";
2
+ import type { GitRunner } from "../session/git.js";
2
3
  export type OutputStream = "stdout" | "stderr" | "none";
3
4
  export type OrphanKind = "shipped" | "unresolved";
4
5
  export interface OrphanActiveBrief {
@@ -22,12 +23,24 @@ export interface OrphanActiveBasis {
22
23
  readonly elapsedMs: number;
23
24
  readonly budgetMs: number;
24
25
  }
26
+ /** What merge-chokepoint scoping decided for this run (#3893). */
27
+ export interface OrphanActiveScopeSummary {
28
+ readonly kind: "diff" | "sweep";
29
+ /** Ref the candidate diff was taken against; null on a sweep. */
30
+ readonly baseRef: string | null;
31
+ /** Why the run stayed repo-wide; null on a candidate diff. */
32
+ readonly reason: string | null;
33
+ /** Running briefs left unevaluated because this candidate does not own them. */
34
+ readonly skipped: number;
35
+ }
25
36
  export interface EvaluateResult {
26
37
  readonly code: 0 | 1 | 2;
27
38
  readonly message: string;
28
39
  readonly stream: OutputStream;
29
40
  readonly orphans: readonly OrphanActiveBrief[];
30
41
  readonly basis: OrphanActiveBasis;
42
+ /** Null unless `changedOnly` was requested. */
43
+ readonly scope: OrphanActiveScopeSummary | null;
31
44
  }
32
45
  export interface EvaluateOptions {
33
46
  readonly quiet?: boolean;
@@ -38,6 +51,15 @@ export interface EvaluateOptions {
38
51
  readonly issue?: number | null;
39
52
  /** Monotonic clock seam so basis ages and budget checks are testable. */
40
53
  readonly nowMs?: () => number;
54
+ /**
55
+ * Merge-chokepoint scoping (#3893): evaluate only active/running briefs the
56
+ * candidate's own diff touches. Ignored with `issue`, which is already one
57
+ * origin. Falls back to the repo-wide sweep when the base ref is unresolvable.
58
+ */
59
+ readonly changedOnly?: boolean;
60
+ /** Base ref for `changedOnly`; defaults to `origin/<deliveryBranch>`. */
61
+ readonly baseRef?: string | null;
62
+ readonly runGit?: GitRunner;
41
63
  }
42
64
  /**
43
65
  * Pure evaluator for orphaned active/running xBRIEF detection (#2321 / #3429).
@@ -51,6 +73,11 @@ export interface EvaluateOptions {
51
73
  * one complete, fail-closed open-issue inventory and stays fail-open on
52
74
  * unknown so offline work is not network-authorized. A cache hit is honoured
53
75
  * only inside `ISSUE_CACHE_MAX_AGE_MS`, and every verdict reports its basis.
76
+ *
77
+ * `changedOnly` is the merge-chokepoint form (#3893): only briefs the
78
+ * candidate's own diff touches are evaluated, so a brief stranded by another
79
+ * merge cannot fail this candidate. It falls back to the repo-wide sweep at
80
+ * the delivery tip and whenever the base ref is unresolvable.
54
81
  */
55
82
  export declare function evaluate(projectRoot: string, options?: EvaluateOptions): EvaluateResult;
56
83
  //# sourceMappingURL=evaluate.d.ts.map