@deftai/directive-core 0.107.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 (271) hide show
  1. package/dist/architecture/sor-preflight.js +2 -0
  2. package/dist/cache/operations.js +1 -1
  3. package/dist/check/cached-orchestrator.js +28 -3
  4. package/dist/check/gate-lists.d.ts +14 -0
  5. package/dist/check/gate-lists.js +26 -3
  6. package/dist/check/named-cause.js +18 -3
  7. package/dist/check/session-completed-ac.d.ts +1 -1
  8. package/dist/check/session-completed-ac.js +1 -1
  9. package/dist/codebase/provider.js +7 -1
  10. package/dist/consumer-check-contract/evaluate.d.ts +47 -0
  11. package/dist/consumer-check-contract/evaluate.js +181 -15
  12. package/dist/delivery-attempt/evaluate.d.ts +9 -1
  13. package/dist/delivery-attempt/evaluate.js +69 -0
  14. package/dist/delivery-attempt/handoff.js +1 -1
  15. package/dist/delivery-attempt/index.d.ts +1 -1
  16. package/dist/delivery-attempt/index.js +1 -1
  17. package/dist/deposit/live-procedure-exclusions.d.ts +18 -0
  18. package/dist/deposit/live-procedure-exclusions.js +110 -0
  19. package/dist/deposit/live-procedure-targets.d.ts +45 -0
  20. package/dist/deposit/live-procedure-targets.js +274 -0
  21. package/dist/deposit/python-free.d.ts +6 -0
  22. package/dist/deposit/python-free.js +15 -0
  23. package/dist/deposit/rewrite-deposit-links.d.ts +42 -0
  24. package/dist/deposit/rewrite-deposit-links.js +148 -0
  25. package/dist/deposit/run-stage-content-pack.d.ts +2 -0
  26. package/dist/deposit/run-stage-content-pack.js +3 -0
  27. package/dist/deposit/stage-content-pack.d.ts +17 -0
  28. package/dist/deposit/stage-content-pack.js +91 -0
  29. package/dist/design-critique/citation-grammar.d.ts +60 -0
  30. package/dist/design-critique/citation-grammar.js +303 -0
  31. package/dist/design-critique/completed-arc-record.d.ts +62 -0
  32. package/dist/design-critique/completed-arc-record.js +277 -0
  33. package/dist/freshness/bind.d.ts +32 -3
  34. package/dist/freshness/bind.js +58 -12
  35. package/dist/hooks/classify/host-session-identity.d.ts +89 -0
  36. package/dist/hooks/classify/host-session-identity.js +467 -0
  37. package/dist/hooks/classify/index.d.ts +3 -2
  38. package/dist/hooks/classify/index.js +3 -2
  39. package/dist/hooks/classify/paths.d.ts +9 -0
  40. package/dist/hooks/classify/paths.js +39 -0
  41. package/dist/hooks/classify/stdin.d.ts +5 -0
  42. package/dist/hooks/classify/stdin.js +94 -1
  43. package/dist/hooks/dest-form.d.ts +20 -1
  44. package/dist/hooks/dest-form.js +158 -21
  45. package/dist/hooks/dispatcher.d.ts +58 -3
  46. package/dist/hooks/dispatcher.js +762 -73
  47. package/dist/hooks/fixtures/cases.d.ts +22 -2
  48. package/dist/hooks/fixtures/cases.js +228 -0
  49. package/dist/hooks/git-destructive-log.d.ts +32 -0
  50. package/dist/hooks/git-destructive-log.js +46 -0
  51. package/dist/hooks/index.d.ts +2 -0
  52. package/dist/hooks/index.js +2 -0
  53. package/dist/hooks/scope.js +3 -1
  54. package/dist/hooks/shell-write-targets.d.ts +10 -0
  55. package/dist/hooks/shell-write-targets.js +274 -0
  56. package/dist/hooks/tools.d.ts +20 -2
  57. package/dist/hooks/tools.js +30 -1
  58. package/dist/init-deposit/agent-hooks.d.ts +2 -1
  59. package/dist/init-deposit/agent-hooks.js +8 -1
  60. package/dist/init-deposit/gitignore.d.ts +7 -0
  61. package/dist/init-deposit/gitignore.js +24 -0
  62. package/dist/init-deposit/hook-runtime-travel.d.ts +77 -0
  63. package/dist/init-deposit/hook-runtime-travel.js +269 -0
  64. package/dist/init-deposit/init-deposit.js +3 -0
  65. package/dist/init-deposit/refresh.js +7 -0
  66. package/dist/init-deposit/runtime-writers.d.ts +14 -0
  67. package/dist/init-deposit/runtime-writers.js +33 -0
  68. package/dist/intake/clause-derivation.js +9 -3
  69. package/dist/intake/issue-ingest.d.ts +6 -1
  70. package/dist/intake/issue-ingest.js +83 -4
  71. package/dist/intake/platform-capabilities.d.ts +9 -2
  72. package/dist/intake/platform-capabilities.js +38 -9
  73. package/dist/intake/reconcile-issues.js +13 -13
  74. package/dist/lifecycle/brief-envelope.d.ts +25 -0
  75. package/dist/lifecycle/brief-envelope.js +42 -0
  76. package/dist/lifecycle/index.d.ts +1 -0
  77. package/dist/lifecycle/index.js +1 -0
  78. package/dist/literal-acceptance/evaluate.js +14 -5
  79. package/dist/literal-acceptance/index.d.ts +1 -1
  80. package/dist/literal-acceptance/index.js +1 -1
  81. package/dist/literal-acceptance/run.d.ts +2 -0
  82. package/dist/literal-acceptance/run.js +19 -1
  83. package/dist/orphan-active/candidate-scope.d.ts +53 -0
  84. package/dist/orphan-active/candidate-scope.js +157 -0
  85. package/dist/orphan-active/evaluate.d.ts +52 -0
  86. package/dist/orphan-active/evaluate.js +250 -128
  87. package/dist/orphan-active/index.d.ts +2 -0
  88. package/dist/orphan-active/index.js +2 -0
  89. package/dist/orphan-active/issue-state.d.ts +121 -0
  90. package/dist/orphan-active/issue-state.js +278 -0
  91. package/dist/platform/cursor-managed-runtime.d.ts +95 -0
  92. package/dist/platform/cursor-managed-runtime.js +241 -0
  93. package/dist/platform/index.d.ts +1 -0
  94. package/dist/platform/index.js +1 -0
  95. package/dist/platform/platform-capabilities.d.ts +23 -1
  96. package/dist/platform/platform-capabilities.js +55 -15
  97. package/dist/policy/ceremony-dial.js +5 -9
  98. package/dist/policy/host-hooks.js +4 -9
  99. package/dist/policy/merge-approval-head.js +7 -6
  100. package/dist/policy/org-force-on-migration.js +5 -10
  101. package/dist/policy/plan-extensions.d.ts +14 -2
  102. package/dist/policy/plan-extensions.js +24 -3
  103. package/dist/policy/product-signal.js +5 -10
  104. package/dist/policy/require-human-merge.js +5 -9
  105. package/dist/policy/resolve.js +77 -16
  106. package/dist/policy/value-feedback.js +9 -18
  107. package/dist/pr-closeout-attestable/evaluate.d.ts +80 -0
  108. package/dist/pr-closeout-attestable/evaluate.js +306 -0
  109. package/dist/pr-closeout-attestable/index.d.ts +2 -0
  110. package/dist/pr-closeout-attestable/index.js +2 -0
  111. package/dist/pr-closing-keywords/gh.js +32 -9
  112. package/dist/pr-closing-keywords/main.d.ts +1 -0
  113. package/dist/pr-closing-keywords/main.js +68 -3
  114. package/dist/pr-closing-keywords/types.d.ts +2 -0
  115. package/dist/pr-merge-readiness/gh.js +32 -9
  116. package/dist/pr-protected-issues/gh.d.ts +6 -2
  117. package/dist/pr-protected-issues/gh.js +42 -11
  118. package/dist/pr-wait-mergeable/cascade.d.ts +9 -1
  119. package/dist/pr-wait-mergeable/cascade.js +21 -1
  120. package/dist/pr-wait-mergeable/types.d.ts +2 -0
  121. package/dist/pr-wait-mergeable/wrappers.d.ts +6 -0
  122. package/dist/pr-wait-mergeable/wrappers.js +14 -3
  123. package/dist/preflight/evaluate.d.ts +15 -0
  124. package/dist/preflight/evaluate.js +29 -3
  125. package/dist/product-first-done-gate/acceptance-resolver.d.ts +14 -2
  126. package/dist/product-first-done-gate/acceptance-resolver.js +13 -2
  127. package/dist/product-first-done-gate/evaluate.js +23 -9
  128. package/dist/product-first-done-gate/types.js +2 -0
  129. package/dist/release/cli-drift-report.d.ts +68 -0
  130. package/dist/release/cli-drift-report.js +189 -0
  131. package/dist/release/consumer-hard-stops.d.ts +48 -0
  132. package/dist/release/consumer-hard-stops.js +140 -0
  133. package/dist/release/consumer-readiness-disclosure.d.ts +18 -0
  134. package/dist/release/consumer-readiness-disclosure.js +51 -0
  135. package/dist/release/index.d.ts +3 -0
  136. package/dist/release/index.js +3 -0
  137. package/dist/release/issue-state-fetch.d.ts +5 -3
  138. package/dist/release/issue-state-fetch.js +86 -21
  139. package/dist/release/native-steps.js +1 -0
  140. package/dist/release/pipeline.js +74 -0
  141. package/dist/release/run-consumer-readiness.d.ts +15 -0
  142. package/dist/release/run-consumer-readiness.js +24 -0
  143. package/dist/release/types.d.ts +20 -0
  144. package/dist/render/constants.d.ts +4 -0
  145. package/dist/render/constants.js +11 -8
  146. package/dist/render/export-spec.js +31 -5
  147. package/dist/render/index.d.ts +1 -1
  148. package/dist/render/index.js +1 -1
  149. package/dist/render/prd-render.d.ts +4 -1
  150. package/dist/render/prd-render.js +82 -23
  151. package/dist/render/project-render.js +14 -7
  152. package/dist/render/scope-outlook.d.ts +2 -0
  153. package/dist/render/scope-outlook.js +3 -0
  154. package/dist/render/spec-render.js +2 -2
  155. package/dist/resolution/package-manager.d.ts +33 -4
  156. package/dist/resolution/package-manager.js +210 -9
  157. package/dist/review-monitor/constants.d.ts +7 -0
  158. package/dist/review-monitor/constants.js +7 -0
  159. package/dist/review-monitor/github-lease.d.ts +98 -1
  160. package/dist/review-monitor/github-lease.js +186 -4
  161. package/dist/review-monitor/lease-comment.d.ts +36 -0
  162. package/dist/review-monitor/lease-comment.js +103 -2
  163. package/dist/run-summary/types.d.ts +2 -2
  164. package/dist/scm/build-command.d.ts +2 -2
  165. package/dist/scm/build-command.js +2 -2
  166. package/dist/scm/call-shape.d.ts +25 -0
  167. package/dist/scm/call-shape.js +59 -0
  168. package/dist/scm/call.d.ts +7 -4
  169. package/dist/scm/call.js +48 -9
  170. package/dist/scm/design-critique-chip.d.ts +1 -0
  171. package/dist/scm/design-critique-chip.js +22 -6
  172. package/dist/scm/gh-rest.d.ts +34 -13
  173. package/dist/scm/gh-rest.js +168 -15
  174. package/dist/scm/index.d.ts +2 -0
  175. package/dist/scm/index.js +2 -0
  176. package/dist/scm/readiness.d.ts +2 -0
  177. package/dist/scm/readiness.js +28 -4
  178. package/dist/scm/spawn-status.d.ts +33 -0
  179. package/dist/scm/spawn-status.js +53 -0
  180. package/dist/scope/acceptance-evidence.d.ts +1 -1
  181. package/dist/scope/project-definition-sync.js +14 -7
  182. package/dist/scope/transition.js +4 -14
  183. package/dist/session/ac-pass-banking.d.ts +2 -2
  184. package/dist/session/ac-pass-banking.js +2 -2
  185. package/dist/session/git.d.ts +24 -0
  186. package/dist/session/git.js +138 -2
  187. package/dist/session/host-session-owner.d.ts +53 -0
  188. package/dist/session/host-session-owner.js +89 -0
  189. package/dist/session/index.d.ts +1 -0
  190. package/dist/session/index.js +1 -0
  191. package/dist/session/occupancy.d.ts +259 -13
  192. package/dist/session/occupancy.js +877 -33
  193. package/dist/session/ritual-sentinel.d.ts +5 -0
  194. package/dist/session/ritual-sentinel.js +5 -0
  195. package/dist/session/session-ready.d.ts +5 -1
  196. package/dist/session/session-ready.js +98 -8
  197. package/dist/session/session-start.d.ts +5 -1
  198. package/dist/session/session-start.js +32 -17
  199. package/dist/session/toolchain-preflight.d.ts +13 -3
  200. package/dist/session/toolchain-preflight.js +93 -18
  201. package/dist/session/verify-ac-session-cache.d.ts +2 -2
  202. package/dist/session/verify-ac-session-cache.js +2 -2
  203. package/dist/session/verify-session-ritual.d.ts +11 -0
  204. package/dist/session/verify-session-ritual.js +60 -15
  205. package/dist/slice/constants.d.ts +1 -1
  206. package/dist/spec-authority/constants.d.ts +10 -2
  207. package/dist/spec-authority/constants.js +53 -8
  208. package/dist/spec-authority/resolver.d.ts +3 -0
  209. package/dist/spec-authority/resolver.js +55 -7
  210. package/dist/subprocess/max-buffer.d.ts +14 -0
  211. package/dist/subprocess/max-buffer.js +15 -0
  212. package/dist/swarm/complete-cohort.d.ts +2 -0
  213. package/dist/swarm/complete-cohort.js +21 -13
  214. package/dist/swarm/index.d.ts +1 -0
  215. package/dist/swarm/index.js +1 -0
  216. package/dist/swarm/launch-cli.js +53 -33
  217. package/dist/swarm/launch.d.ts +8 -0
  218. package/dist/swarm/launch.js +63 -43
  219. package/dist/swarm/pre-dispatch-cli.js +2 -1
  220. package/dist/swarm/pre-dispatch.js +10 -1
  221. package/dist/swarm/subagent-status-dir.d.ts +27 -0
  222. package/dist/swarm/subagent-status-dir.js +42 -0
  223. package/dist/swarm/worktrees.d.ts +17 -3
  224. package/dist/swarm/worktrees.js +48 -4
  225. package/dist/triage/evaluate/worktrees.js +153 -6
  226. package/dist/triage/scope/mutations-core.d.ts +2 -6
  227. package/dist/triage/scope/mutations-core.js +23 -55
  228. package/dist/triage/scope-drift/add-ignore.js +5 -39
  229. package/dist/triage/subscribe/index.d.ts +2 -6
  230. package/dist/triage/subscribe/index.js +15 -65
  231. package/dist/triage/welcome/writers.js +15 -74
  232. package/dist/umbrella-current-shape/index.d.ts +51 -3
  233. package/dist/umbrella-current-shape/index.js +106 -18
  234. package/dist/validate-content/deposit-required.d.ts +39 -0
  235. package/dist/validate-content/deposit-required.js +147 -0
  236. package/dist/validate-content/index.d.ts +1 -0
  237. package/dist/validate-content/index.js +1 -0
  238. package/dist/validate-content/validate-links.d.ts +2 -3
  239. package/dist/validate-content/validate-links.js +29 -2
  240. package/dist/vbrief-activate/activate.d.ts +7 -2
  241. package/dist/vbrief-activate/activate.js +26 -13
  242. package/dist/vbrief-build/index.d.ts +2 -1
  243. package/dist/vbrief-build/index.js +2 -1
  244. package/dist/vbrief-build/parity-scenarios.js +5 -5
  245. package/dist/vbrief-build/project-definition-io.d.ts +63 -3
  246. package/dist/vbrief-build/project-definition-io.js +357 -31
  247. package/dist/vbrief-build/project-definition-mutation.d.ts +32 -0
  248. package/dist/vbrief-build/project-definition-mutation.js +43 -0
  249. package/dist/vbrief-validate/conformance.d.ts +1 -0
  250. package/dist/vbrief-validate/conformance.js +85 -8
  251. package/dist/vbrief-validate/main.js +2 -0
  252. package/dist/vbrief-validate/precutover.js +5 -10
  253. package/dist/verify-ac/clauses.d.ts +62 -2
  254. package/dist/verify-ac/clauses.js +183 -111
  255. package/dist/verify-ac/evaluate.d.ts +9 -0
  256. package/dist/verify-ac/evaluate.js +32 -9
  257. package/dist/verify-ac/index.d.ts +1 -1
  258. package/dist/verify-ac/index.js +1 -1
  259. package/dist/verify-env/node-runtime.d.ts +8 -4
  260. package/dist/verify-env/node-runtime.js +9 -6
  261. package/dist/verify-env/toolchain-check.d.ts +35 -3
  262. package/dist/verify-env/toolchain-check.js +155 -36
  263. package/dist/verify-source/deposit-closure.d.ts +23 -0
  264. package/dist/verify-source/deposit-closure.js +162 -0
  265. package/dist/verify-source/index.d.ts +2 -0
  266. package/dist/verify-source/index.js +2 -0
  267. package/dist/verify-source/project-definition-mutation-boundary.d.ts +104 -0
  268. package/dist/verify-source/project-definition-mutation-boundary.js +326 -0
  269. package/dist/verify-source/semantic-single-source.d.ts +36 -0
  270. package/dist/verify-source/semantic-single-source.js +349 -0
  271. package/package.json +7 -3
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Merge-time closeout attestability gate (#3781).
3
+ *
4
+ * A pull request may leave a brief unattested; it may not MERGE one whose issue
5
+ * it closes in the same act. The trigger is the PR's structured closing
6
+ * references, never the branch diff: CI runs before the merge and the issue
7
+ * closes on it, so at check time no orphan exists yet and a diff-keyed gate can
8
+ * never fail the PR that introduces the problem (PR #3786's own merge gate
9
+ * returned success while creating one). The brief also need not be in the diff —
10
+ * for #3598 it landed on master seventeen hours before its closing PR.
11
+ *
12
+ * The rule itself is not restated here. `evaluateAcceptanceEvidenceGate` is the
13
+ * single decision procedure `scope:complete` enforces, and this gate calls it so
14
+ * the two cannot drift.
15
+ */
16
+ import { type GateRunner } from "../orphan-active/issue-state.js";
17
+ import type { RunGhFn } from "../pr-protected-issues/types.js";
18
+ import { type StrictAcceptanceAxis } from "../scope/acceptance-evidence.js";
19
+ export type OutputStream = "stdout" | "stderr" | "none";
20
+ /** One acceptance criterion the merge would strand without evidence or disposition. */
21
+ export interface UnattestedCriterion {
22
+ /** Plan-item path, e.g. `items[3]` or `items[1].subItems[0]`. */
23
+ readonly path: string;
24
+ readonly title: string;
25
+ /** Verbatim detail from `evaluateAcceptanceEvidenceGate` — the reason it refused. */
26
+ readonly detail: string;
27
+ /**
28
+ * Strict axes inferred for this criterion (#3240). Non-empty means `merge` and
29
+ * `review` evidence cannot satisfy it, so the message must say which kind can.
30
+ */
31
+ readonly requiredAxes: readonly StrictAcceptanceAxis[];
32
+ }
33
+ /** An active/running brief the PR's closing reference would orphan on merge. */
34
+ export interface CloseoutFinding {
35
+ /** Project-root-relative brief path. */
36
+ readonly briefPath: string;
37
+ /** The closing-referenced issue this brief tracks. */
38
+ readonly issue: number;
39
+ readonly unattested: readonly UnattestedCriterion[];
40
+ }
41
+ export interface PrCloseoutAttestableResult {
42
+ /** 0 attestable / 1 unattested closeout / 2 config or lookup error. */
43
+ readonly code: 0 | 1 | 2;
44
+ readonly message: string;
45
+ readonly stream: OutputStream;
46
+ readonly prNumber: number;
47
+ /** Structured closing-issue references read from the forge. */
48
+ readonly closingIssues: readonly number[];
49
+ readonly findings: readonly CloseoutFinding[];
50
+ /** True when the closing-reference read resolved through `ghx`, a cached GET proxy. */
51
+ readonly proxied: boolean;
52
+ }
53
+ /** Reads the PR's structured closing-issue references; `null` on lookup failure. */
54
+ export type FetchClosingIssuesFn = (prNumber: number, repo: string | null, runGh: RunGhFn) => number[] | null;
55
+ export interface EvaluateOptions {
56
+ readonly repo?: string | null;
57
+ /**
58
+ * SCM read seam plus its freshness basis. Defaults to `makeGateRunner()`, which
59
+ * pins plain `gh` when present so a cached `ghx` GET cannot fail this gate open.
60
+ */
61
+ readonly runner?: GateRunner;
62
+ readonly quiet?: boolean;
63
+ /** Closing-reference seam so tests do not need a forge. */
64
+ readonly fetchClosingIssues?: FetchClosingIssuesFn;
65
+ }
66
+ /**
67
+ * Fail closed when merging `prNumber` would close an issue whose brief is still
68
+ * `running` in `active/` with acceptance criteria carrying neither
69
+ * `x-directive/evidence` nor `x-directive/disposition`.
70
+ *
71
+ * Exit contract: 0 attestable (including "this PR closes nothing") / 1 unattested
72
+ * closeout / 2 config or closing-reference lookup error. A lookup that cannot be
73
+ * resolved is 2, not 0 — the gate never green-lights a merge it could not check.
74
+ *
75
+ * The brief is read from `projectRoot`'s working tree, which at merge time is the
76
+ * PR head checkout. That is the tree the merge lands, and it is the same
77
+ * working-tree basis `verify:orphan-active` uses.
78
+ */
79
+ export declare function evaluate(projectRoot: string, prNumber: number, options?: EvaluateOptions): PrCloseoutAttestableResult;
80
+ //# sourceMappingURL=evaluate.d.ts.map
@@ -0,0 +1,306 @@
1
+ /**
2
+ * Merge-time closeout attestability gate (#3781).
3
+ *
4
+ * A pull request may leave a brief unattested; it may not MERGE one whose issue
5
+ * it closes in the same act. The trigger is the PR's structured closing
6
+ * references, never the branch diff: CI runs before the merge and the issue
7
+ * closes on it, so at check time no orphan exists yet and a diff-keyed gate can
8
+ * never fail the PR that introduces the problem (PR #3786's own merge gate
9
+ * returned success while creating one). The brief also need not be in the diff —
10
+ * for #3598 it landed on master seventeen hours before its closing PR.
11
+ *
12
+ * The rule itself is not restated here. `evaluateAcceptanceEvidenceGate` is the
13
+ * single decision procedure `scope:complete` enforces, and this gate calls it so
14
+ * the two cannot drift.
15
+ */
16
+ import { existsSync, readdirSync, readFileSync } from "node:fs";
17
+ import { join, relative, resolve } from "node:path";
18
+ import { hasArtifactSuffix, resolveLifecycleRoot } from "../layout/resolve.js";
19
+ import { makeGateRunner } from "../orphan-active/issue-state.js";
20
+ import { collectGithubRefs } from "../orphan-active/refs.js";
21
+ import { fetchClosingIssuesReferences } from "../pr-protected-issues/gh.js";
22
+ import { ACCEPTANCE_DISPOSITION_KEY, ACCEPTANCE_DISPOSITIONS, ACCEPTANCE_EVIDENCE_KEY, ACCEPTANCE_EVIDENCE_KINDS, evaluateAcceptanceEvidenceGate, inferRequiredStrictAxes, } from "../scope/acceptance-evidence.js";
23
+ import { resolveRepo } from "../triage/queue/repo.js";
24
+ function readJson(path) {
25
+ try {
26
+ const parsed = JSON.parse(readFileSync(path, "utf8"));
27
+ return typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)
28
+ ? parsed
29
+ : null;
30
+ }
31
+ catch {
32
+ return null;
33
+ }
34
+ }
35
+ function planOf(data) {
36
+ const plan = data?.plan;
37
+ return typeof plan === "object" && plan !== null && !Array.isArray(plan)
38
+ ? plan
39
+ : null;
40
+ }
41
+ function relBriefPath(path, projectRoot) {
42
+ try {
43
+ return relative(resolve(projectRoot), resolve(path)).replace(/\\/g, "/");
44
+ }
45
+ catch {
46
+ return path.replace(/\\/g, "/");
47
+ }
48
+ }
49
+ function listActiveRunningBriefs(lifecycleRoot) {
50
+ const activeDir = join(lifecycleRoot, "active");
51
+ if (!existsSync(activeDir)) {
52
+ return [];
53
+ }
54
+ const out = [];
55
+ for (const entry of readdirSync(activeDir, { withFileTypes: true })) {
56
+ if (!entry.isFile() || !hasArtifactSuffix(entry.name)) {
57
+ continue;
58
+ }
59
+ const path = join(activeDir, entry.name);
60
+ const plan = planOf(readJson(path));
61
+ if (plan === null || String(plan.status ?? "").toLowerCase() !== "running") {
62
+ continue;
63
+ }
64
+ out.push({ path, plan });
65
+ }
66
+ return out.sort((a, b) => a.path.localeCompare(b.path));
67
+ }
68
+ /**
69
+ * Index plan items by the same path the acceptance gate reports, so a blocker can
70
+ * be mapped back to its item for axis inference. Traversal order mirrors
71
+ * `walkItems` in `scope/acceptance-evidence.ts`; the attestation rule stays there.
72
+ */
73
+ function indexPlanItems(items, pathPrefix, out) {
74
+ if (!Array.isArray(items)) {
75
+ return;
76
+ }
77
+ items.forEach((item, index) => {
78
+ if (item === null || typeof item !== "object" || Array.isArray(item)) {
79
+ return;
80
+ }
81
+ const obj = item;
82
+ const path = `${pathPrefix}[${index}]`;
83
+ out.set(path, obj);
84
+ indexPlanItems(obj.subItems, `${path}.subItems`, out);
85
+ indexPlanItems(obj.items, `${path}.items`, out);
86
+ });
87
+ }
88
+ const DISPOSITION_SHAPE = `${ACCEPTANCE_DISPOSITION_KEY} {disposition: ${ACCEPTANCE_DISPOSITIONS.join("|")}, ` +
89
+ `reason, provenance {kind: operator-cli|operator-session|human-event, actor: <non-agent>}, recorded_at}`;
90
+ function evidenceShape(kinds) {
91
+ return `${ACCEPTANCE_EVIDENCE_KEY} {kind: ${kinds}, pointer, recorded_at, recorded_by}`;
92
+ }
93
+ /**
94
+ * Name the exact shape this criterion needs. A single evidence.kind covers one
95
+ * axis, so two inferred axes cannot be satisfied by any kind — say that instead of
96
+ * printing a kind list that would still be refused (#3240 suitability rule).
97
+ */
98
+ function renderCriterion(criterion) {
99
+ const axes = criterion.requiredAxes;
100
+ const lines = [
101
+ ` - ${criterion.path} "${criterion.title}"`,
102
+ ` why: ${criterion.detail}`,
103
+ ];
104
+ if (axes.length > 1) {
105
+ lines.push(` axis: this criterion requires ${axes.join(" + ")}; no single evidence.kind covers`, " two axes, so evidence cannot satisfy it as written", ` needs: pin one axis with "requires": "${axes[0]}" then ` +
106
+ `${evidenceShape(axes[0])},`, " or split the criterion one axis per item,", ` or ${DISPOSITION_SHAPE}`);
107
+ return lines;
108
+ }
109
+ const kinds = axes.length === 1 ? axes[0] : ACCEPTANCE_EVIDENCE_KINDS.join("|");
110
+ lines.push(` needs: ${evidenceShape(kinds)}`, ` or ${DISPOSITION_SHAPE}`);
111
+ if (axes.length === 1) {
112
+ lines.push(` axis: this criterion requires ${axes[0]} — ` +
113
+ "merge and review evidence cannot satisfy it (#3240)");
114
+ }
115
+ return lines;
116
+ }
117
+ /**
118
+ * `gh` was absent so the closing-reference read went through `ghx`, a cached GET
119
+ * proxy this gate cannot inspect. Say so rather than implying a fresh read
120
+ * (#3767 / #3737).
121
+ */
122
+ const PROXIED_CAVEAT = " Note: `gh` was not on PATH, so the closing-reference read resolved through `ghx`, a cached\n" +
123
+ " GET proxy; freshness is bounded by that proxy, which this gate cannot inspect (#3737).";
124
+ function formatRefusal(prNumber, findings, projectRoot, proxied) {
125
+ const criteria = findings.reduce((sum, f) => sum + f.unattested.length, 0);
126
+ const briefNoun = findings.length === 1 ? "brief" : "briefs";
127
+ const criterionNoun = criteria === 1 ? "criterion" : "criteria";
128
+ const issues = findings.map((f) => `#${f.issue}`).join(", ");
129
+ const lines = [
130
+ `verify:pr-closeout-attestable: PR #${prNumber} closes ${issues}, leaving ${findings.length} ` +
131
+ `active/running ${briefNoun} with ${criteria} unattested acceptance ${criterionNoun} ` +
132
+ `(project_root=${projectRoot}).`,
133
+ " Merging now strands the brief on master: the issue closes, the brief stays running in",
134
+ " active/, and scope:complete then refuses it. A PR may leave a brief unattested; it may",
135
+ " not merge one whose issue is closing in the same act (#3781).",
136
+ " Unattested criteria:",
137
+ ];
138
+ for (const finding of findings) {
139
+ lines.push(` ${finding.briefPath} (closes #${finding.issue})`);
140
+ for (const criterion of finding.unattested) {
141
+ lines.push(...renderCriterion(criterion));
142
+ }
143
+ }
144
+ lines.push(" Evidence is not authenticated — recorded_by accepts any non-empty string. Record what you", " actually did: a pointer must be the artifact its kind names (a test run for test, this PR's", " merge for merge, a deployment for deploy). An agent may evidence a criterion; only", " human-origin provenance may waive one (#3240 / #2944).", " Remediation (performable by this PR's author): stamp the criteria above on the brief in this", " branch, commit, push, then re-run:", ` task verify:pr-closeout-attestable -- --pr ${prNumber}`, " Trigger is the PR's closing references, not the branch diff. A PR that leaves an unattested", " brief without closing its issue is unaffected.");
145
+ if (proxied) {
146
+ lines.push(PROXIED_CAVEAT);
147
+ }
148
+ return lines.join("\n");
149
+ }
150
+ function configError(prNumber, message, proxied = false) {
151
+ return {
152
+ code: 2,
153
+ message: `verify:pr-closeout-attestable: ${message}`,
154
+ stream: "stderr",
155
+ prNumber,
156
+ closingIssues: [],
157
+ findings: [],
158
+ proxied,
159
+ };
160
+ }
161
+ /**
162
+ * Fail closed when merging `prNumber` would close an issue whose brief is still
163
+ * `running` in `active/` with acceptance criteria carrying neither
164
+ * `x-directive/evidence` nor `x-directive/disposition`.
165
+ *
166
+ * Exit contract: 0 attestable (including "this PR closes nothing") / 1 unattested
167
+ * closeout / 2 config or closing-reference lookup error. A lookup that cannot be
168
+ * resolved is 2, not 0 — the gate never green-lights a merge it could not check.
169
+ *
170
+ * The brief is read from `projectRoot`'s working tree, which at merge time is the
171
+ * PR head checkout. That is the tree the merge lands, and it is the same
172
+ * working-tree basis `verify:orphan-active` uses.
173
+ */
174
+ export function evaluate(projectRoot, prNumber, options = {}) {
175
+ const root = resolve(projectRoot);
176
+ const quiet = options.quiet ?? false;
177
+ if (!existsSync(root)) {
178
+ return configError(prNumber, `project root does not exist: ${root}`);
179
+ }
180
+ let lifecycleRoot;
181
+ try {
182
+ lifecycleRoot = resolveLifecycleRoot(root);
183
+ }
184
+ catch (err) {
185
+ const message = err instanceof Error ? err.message : String(err);
186
+ // Consumers may still be on a legacy vbrief/-only layout (#2112). Closeout
187
+ // attestability applies to xbrief/active/ only — skip cleanly, not config fail.
188
+ if (message.includes("No xbrief/ layout found")) {
189
+ return {
190
+ code: 0,
191
+ message: quiet
192
+ ? ""
193
+ : "verify:pr-closeout-attestable: no xbrief/ lifecycle root; nothing to check.",
194
+ stream: quiet ? "none" : "stdout",
195
+ prNumber,
196
+ closingIssues: [],
197
+ findings: [],
198
+ proxied: false,
199
+ };
200
+ }
201
+ return configError(prNumber, message);
202
+ }
203
+ if (!existsSync(lifecycleRoot)) {
204
+ return {
205
+ code: 0,
206
+ message: quiet
207
+ ? ""
208
+ : "verify:pr-closeout-attestable: no xbrief/ lifecycle root; nothing to check.",
209
+ stream: quiet ? "none" : "stdout",
210
+ prNumber,
211
+ closingIssues: [],
212
+ findings: [],
213
+ proxied: false,
214
+ };
215
+ }
216
+ // Pin plain `gh` when it exists: `ghx` is a cached GET proxy and a stale
217
+ // closing-reference read would fail this gate open (#3767 / #3737).
218
+ const runner = options.runner ?? makeGateRunner();
219
+ const fetchClosing = options.fetchClosingIssues ?? fetchClosingIssuesReferences;
220
+ const repo = resolveRepo(options.repo, root);
221
+ if (repo === null || repo.length === 0) {
222
+ // Closing references are repository-scoped. Without the slug this gate could
223
+ // only compare bare numbers, and a same-numbered issue in an unrelated
224
+ // repository would block a valid merge.
225
+ return configError(prNumber, "cannot resolve OWNER/REPO for the closing-reference read. Pass --repo OWNER/REPO, " +
226
+ "set $GH_REPO, or run inside a checkout with a GitHub origin remote.", runner.proxied);
227
+ }
228
+ const linked = fetchClosing(prNumber, repo, runner.runGh);
229
+ if (linked === null) {
230
+ return configError(prNumber, `could not read closing-issue references for PR #${prNumber}` +
231
+ ` (repo=${repo}). ` +
232
+ "Refusing to certify the merge on an unverified lookup — retry after fixing gh auth, " +
233
+ "rate limit, or network.", runner.proxied);
234
+ }
235
+ const closingIssues = [...new Set(linked)].sort((a, b) => a - b);
236
+ if (closingIssues.length === 0) {
237
+ return {
238
+ code: 0,
239
+ message: quiet
240
+ ? ""
241
+ : `verify:pr-closeout-attestable: PR #${prNumber} closes no issue; closeout attestability does not apply.`,
242
+ stream: quiet ? "none" : "stdout",
243
+ prNumber,
244
+ closingIssues,
245
+ findings: [],
246
+ proxied: runner.proxied,
247
+ };
248
+ }
249
+ const closingSet = new Set(closingIssues);
250
+ const findings = [];
251
+ for (const brief of listActiveRunningBriefs(lifecycleRoot)) {
252
+ const { issues } = collectGithubRefs(brief.plan, repo);
253
+ // Match on (repo, number). Closing references are scoped to the PR's repository,
254
+ // so a bare-number match would let an unrelated brief tracking the same number in
255
+ // another repository block this merge. Refs with no repo of their own inherit the
256
+ // PR's repo from collectGithubRefs, which is the correct reading of a bare number.
257
+ // GitHub slugs are case-insensitive; a case-sensitive compare would let
258
+ // DeftAI/Directive vs deftai/directive miss and fail the gate open.
259
+ const issue = issues.find((ref) => ref.repo.toLowerCase() === repo.toLowerCase() && closingSet.has(ref.number))?.number;
260
+ if (issue === undefined) {
261
+ continue;
262
+ }
263
+ const gate = evaluateAcceptanceEvidenceGate(brief.plan);
264
+ if (gate.ok) {
265
+ continue;
266
+ }
267
+ const itemsByPath = new Map();
268
+ indexPlanItems(brief.plan.items, "items", itemsByPath);
269
+ const unattested = gate.reports
270
+ .filter((report) => report.outcome === "missing" || report.outcome === "invalid")
271
+ .map((report) => {
272
+ const item = itemsByPath.get(report.path);
273
+ return {
274
+ path: report.path,
275
+ title: report.title,
276
+ detail: report.detail,
277
+ requiredAxes: item === undefined ? [] : inferRequiredStrictAxes(item),
278
+ };
279
+ });
280
+ findings.push({ briefPath: relBriefPath(brief.path, root), issue, unattested });
281
+ }
282
+ if (findings.length > 0) {
283
+ return {
284
+ code: 1,
285
+ message: formatRefusal(prNumber, findings, root, runner.proxied),
286
+ stream: "stderr",
287
+ prNumber,
288
+ closingIssues,
289
+ findings,
290
+ proxied: runner.proxied,
291
+ };
292
+ }
293
+ const issueList = closingIssues.map((n) => `#${n}`).join(", ");
294
+ const pass = `verify:pr-closeout-attestable: PR #${prNumber} closes ${issueList}; ` +
295
+ "no active/running brief for those issues has unattested acceptance criteria.";
296
+ return {
297
+ code: 0,
298
+ message: quiet ? "" : runner.proxied ? `${pass}\n${PROXIED_CAVEAT}` : pass,
299
+ stream: quiet ? "none" : "stdout",
300
+ prNumber,
301
+ closingIssues,
302
+ findings: [],
303
+ proxied: runner.proxied,
304
+ };
305
+ }
306
+ //# sourceMappingURL=evaluate.js.map
@@ -0,0 +1,2 @@
1
+ export * from "./evaluate.js";
2
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,2 @@
1
+ export * from "./evaluate.js";
2
+ //# sourceMappingURL=index.js.map
@@ -1,5 +1,7 @@
1
1
  import { execFileSync } from "node:child_process";
2
- import { resolveBinary } from "../scm/binary.js";
2
+ import { defaultWhich } from "../scm/binary.js";
3
+ import { classifyScmArgv, resolveBinaryForArgv } from "../scm/call-shape.js";
4
+ import { ghxSpawnFallbackBinary } from "../scm/spawn-status.js";
3
5
  import { SUBPROCESS_MAX_BUFFER } from "../subprocess/max-buffer.js";
4
6
  import { GH_TIMEOUT_S } from "./constants.js";
5
7
  /** UTF-8-safe gh capture via execFile (no shell) — mirrors _safe_subprocess.run_text (#1366). */
@@ -7,18 +9,39 @@ export function defaultRunGh(cmd) {
7
9
  if (cmd.length === 0 || cmd[0] !== "gh") {
8
10
  return { returncode: -1, stdout: "", stderr: "expected gh as first argv element" };
9
11
  }
10
- const binary = resolveBinary();
11
12
  const args = cmd.slice(1);
13
+ const binary = resolveBinaryForArgv(args[0] ?? "", args.slice(1));
14
+ const execOpts = {
15
+ encoding: "utf8",
16
+ timeout: GH_TIMEOUT_S * 1000,
17
+ stdio: ["ignore", "pipe", "pipe"],
18
+ maxBuffer: SUBPROCESS_MAX_BUFFER,
19
+ };
20
+ const runOnce = (bin) => execFileSync(bin, args, execOpts);
12
21
  try {
13
- const stdout = execFileSync(binary, args, {
14
- encoding: "utf8",
15
- timeout: GH_TIMEOUT_S * 1000,
16
- stdio: ["ignore", "pipe", "pipe"],
17
- maxBuffer: SUBPROCESS_MAX_BUFFER,
18
- });
22
+ const stdout = runOnce(binary);
19
23
  return { returncode: 0, stdout: typeof stdout === "string" ? stdout : "", stderr: "" };
20
24
  }
21
- catch (err) {
25
+ catch (caught) {
26
+ let err = caught;
27
+ const first = err;
28
+ const retry = ghxSpawnFallbackBinary(classifyScmArgv(args[0] ?? "", args.slice(1)), binary, defaultWhich, {
29
+ status: typeof first.status === "number" ? first.status : null,
30
+ error: first.code === "ENOENT" || first.code === "ETIMEDOUT" || typeof first.status !== "number"
31
+ ? { code: first.code, message: first.message }
32
+ : undefined,
33
+ stdout: typeof first.stdout === "string" ? first.stdout : "",
34
+ stderr: typeof first.stderr === "string" ? first.stderr : "",
35
+ });
36
+ if (retry !== null) {
37
+ try {
38
+ const stdout = runOnce(retry);
39
+ return { returncode: 0, stdout: typeof stdout === "string" ? stdout : "", stderr: "" };
40
+ }
41
+ catch (retryErr) {
42
+ err = retryErr;
43
+ }
44
+ }
22
45
  const e = err;
23
46
  if (e.code === "ENOENT") {
24
47
  return { returncode: -1, stdout: "", stderr: "gh CLI not found. Install GitHub CLI." };
@@ -3,6 +3,7 @@ export declare function parseAllowList(values: readonly string[]): Set<number>;
3
3
  export declare function parseArgs(argv: readonly string[]): ParsedArgs;
4
4
  export interface RunOptions {
5
5
  readonly runGh?: RunGhFn;
6
+ readonly runGit?: RunGhFn;
6
7
  }
7
8
  export declare function run(argv: readonly string[], options?: RunOptions): number;
8
9
  export declare function cmdPrCheckClosingKeywords(argv: readonly string[], options?: RunOptions): number;
@@ -1,3 +1,5 @@
1
+ import { execFileSync } from "node:child_process";
2
+ import { SUBPROCESS_MAX_BUFFER } from "../subprocess/max-buffer.js";
1
3
  import { EXIT_CONFIG_ERROR, EXIT_HITS_FOUND, EXIT_OK } from "./constants.js";
2
4
  import { findAllClosingKeywordHits, findHits, renderHit } from "./detect.js";
3
5
  import { defaultRunGh, fetchPrBody, fetchPrCommitMessages } from "./gh.js";
@@ -23,6 +25,7 @@ function emptyParsed(error) {
23
25
  pr: null,
24
26
  bodyFile: null,
25
27
  commitsFile: null,
28
+ fromGitRange: null,
26
29
  repo: null,
27
30
  allowKnownFalsePositives: [],
28
31
  allowClose: [],
@@ -34,6 +37,7 @@ export function parseArgs(argv) {
34
37
  let pr = null;
35
38
  let bodyFile = null;
36
39
  let commitsFile = null;
40
+ let fromGitRange = null;
37
41
  let repo = null;
38
42
  let mode = "both";
39
43
  const allowKnownFalsePositives = [];
@@ -82,6 +86,17 @@ export function parseArgs(argv) {
82
86
  else if (arg?.startsWith("--commits-file=")) {
83
87
  commitsFile = arg.slice("--commits-file=".length);
84
88
  }
89
+ else if (arg === "--from-git-range") {
90
+ const value = argv[i + 1];
91
+ if (value === undefined) {
92
+ return emptyParsed("argument --from-git-range: expected one argument");
93
+ }
94
+ fromGitRange = value;
95
+ i += 1;
96
+ }
97
+ else if (arg?.startsWith("--from-git-range=")) {
98
+ fromGitRange = arg.slice("--from-git-range=".length);
99
+ }
85
100
  else if (arg === "--repo") {
86
101
  const value = argv[i + 1];
87
102
  if (value === undefined) {
@@ -140,7 +155,50 @@ export function parseArgs(argv) {
140
155
  return emptyParsed(`unrecognized arguments: ${arg}`);
141
156
  }
142
157
  }
143
- return { pr, bodyFile, commitsFile, repo, allowKnownFalsePositives, allowClose, mode };
158
+ return {
159
+ pr,
160
+ bodyFile,
161
+ commitsFile,
162
+ fromGitRange,
163
+ repo,
164
+ allowKnownFalsePositives,
165
+ allowClose,
166
+ mode,
167
+ };
168
+ }
169
+ function defaultRunGit(cmd) {
170
+ try {
171
+ const stdout = execFileSync("git", [...cmd], {
172
+ encoding: "utf8",
173
+ timeout: 30_000,
174
+ stdio: ["ignore", "pipe", "pipe"],
175
+ maxBuffer: SUBPROCESS_MAX_BUFFER,
176
+ });
177
+ return { returncode: 0, stdout: typeof stdout === "string" ? stdout : "", stderr: "" };
178
+ }
179
+ catch (err) {
180
+ const e = err;
181
+ return {
182
+ returncode: typeof e.status === "number" ? e.status : 1,
183
+ stdout: typeof e.stdout === "string" ? e.stdout : "",
184
+ stderr: typeof e.stderr === "string" ? e.stderr : String(e.message ?? ""),
185
+ };
186
+ }
187
+ }
188
+ function readGitRange(range, runGit) {
189
+ const result = runGit(["log", range, "--format=%B%n--END--"]);
190
+ if (result.returncode !== 0) {
191
+ process.stderr.write("Error: git log " +
192
+ range +
193
+ " failed: " +
194
+ result.stderr.trim() +
195
+ ". Recovery: git fetch origin master.\n");
196
+ return null;
197
+ }
198
+ return result.stdout
199
+ .split("\n--END--\n")
200
+ .map((part) => part.trim())
201
+ .filter((part) => part.length > 0);
144
202
  }
145
203
  function filterHits(hits, allowList) {
146
204
  return hits.filter((h) => !allowList.has(h.issueNumber));
@@ -220,8 +278,8 @@ export function run(argv, options = {}) {
220
278
  commitMessages = msgs;
221
279
  }
222
280
  else {
223
- if (args.bodyFile === null && args.commitsFile === null) {
224
- process.stderr.write("Error: must specify --pr OR --body-file / --commits-file.\n");
281
+ if (args.bodyFile === null && args.commitsFile === null && args.fromGitRange === null) {
282
+ process.stderr.write("Error: must specify --pr OR --body-file / --commits-file / --from-git-range.\n");
225
283
  return EXIT_CONFIG_ERROR;
226
284
  }
227
285
  if (args.bodyFile !== null) {
@@ -238,6 +296,13 @@ export function run(argv, options = {}) {
238
296
  }
239
297
  commitMessages = msgs;
240
298
  }
299
+ if (args.fromGitRange !== null) {
300
+ const msgs = readGitRange(args.fromGitRange, options.runGit ?? defaultRunGit);
301
+ if (msgs === null) {
302
+ return EXIT_CONFIG_ERROR;
303
+ }
304
+ commitMessages = [...commitMessages, ...msgs];
305
+ }
241
306
  }
242
307
  const runFp = args.mode === "fp" || args.mode === "both";
243
308
  const runIntent = args.mode === "intent" || args.mode === "both";
@@ -18,6 +18,8 @@ export interface ParsedArgs {
18
18
  readonly pr: number | null;
19
19
  readonly bodyFile: string | null;
20
20
  readonly commitsFile: string | null;
21
+ /** Offline git range (e.g. origin/master..HEAD) for check-graph wiring (#3969). */
22
+ readonly fromGitRange: string | null;
21
23
  readonly repo: string | null;
22
24
  readonly allowKnownFalsePositives: readonly string[];
23
25
  /** Intent-mode allowlist of issue numbers permitted to use real closing keywords (#3015). */
@@ -1,5 +1,7 @@
1
1
  import { execFileSync } from "node:child_process";
2
- import { resolveBinary } from "../scm/binary.js";
2
+ import { defaultWhich } from "../scm/binary.js";
3
+ import { classifyScmArgv, resolveBinaryForArgv } from "../scm/call-shape.js";
4
+ import { ghxSpawnFallbackBinary } from "../scm/spawn-status.js";
3
5
  import { SUBPROCESS_MAX_BUFFER } from "../subprocess/max-buffer.js";
4
6
  import { GH_TIMEOUT_S, GREPTILE_LOGIN } from "./constants.js";
5
7
  /** Display / inventory key for a required context (includes app binding when set). */
@@ -47,18 +49,39 @@ export function defaultRunGh(cmd) {
47
49
  if (cmd.length === 0 || cmd[0] !== "gh") {
48
50
  return { returncode: -1, stdout: "", stderr: "expected gh as first argv element" };
49
51
  }
50
- const binary = resolveBinary();
51
52
  const args = cmd.slice(1);
53
+ const binary = resolveBinaryForArgv(args[0] ?? "", args.slice(1));
54
+ const execOpts = {
55
+ encoding: "utf8",
56
+ timeout: GH_TIMEOUT_S * 1000,
57
+ stdio: ["ignore", "pipe", "pipe"],
58
+ maxBuffer: SUBPROCESS_MAX_BUFFER,
59
+ };
60
+ const runOnce = (bin) => execFileSync(bin, args, execOpts);
52
61
  try {
53
- const stdout = execFileSync(binary, args, {
54
- encoding: "utf8",
55
- timeout: GH_TIMEOUT_S * 1000,
56
- stdio: ["ignore", "pipe", "pipe"],
57
- maxBuffer: SUBPROCESS_MAX_BUFFER,
58
- });
62
+ const stdout = runOnce(binary);
59
63
  return { returncode: 0, stdout: typeof stdout === "string" ? stdout : "", stderr: "" };
60
64
  }
61
- catch (err) {
65
+ catch (caught) {
66
+ let err = caught;
67
+ const first = err;
68
+ const retry = ghxSpawnFallbackBinary(classifyScmArgv(args[0] ?? "", args.slice(1)), binary, defaultWhich, {
69
+ status: typeof first.status === "number" ? first.status : null,
70
+ error: first.code === "ENOENT" || first.code === "ETIMEDOUT" || typeof first.status !== "number"
71
+ ? { code: first.code, message: first.message }
72
+ : undefined,
73
+ stdout: typeof first.stdout === "string" ? first.stdout : "",
74
+ stderr: typeof first.stderr === "string" ? first.stderr : "",
75
+ });
76
+ if (retry !== null) {
77
+ try {
78
+ const stdout = runOnce(retry);
79
+ return { returncode: 0, stdout: typeof stdout === "string" ? stdout : "", stderr: "" };
80
+ }
81
+ catch (retryErr) {
82
+ err = retryErr;
83
+ }
84
+ }
62
85
  const e = err;
63
86
  if (e.code === "ENOENT") {
64
87
  return { returncode: -1, stdout: "", stderr: "gh CLI not found. Install GitHub CLI." };
@@ -1,6 +1,10 @@
1
1
  import type { RunGhFn, RunGhResult } from "./types.js";
2
- /** UTF-8-safe gh capture via execFile (no shell) — mirrors _safe_subprocess.run_text (#1366). */
3
- export declare function defaultRunGh(cmd: readonly string[]): RunGhResult;
2
+ /**
3
+ * UTF-8-safe gh capture via execFile (no shell) — mirrors _safe_subprocess.run_text (#1366).
4
+ * `binaryOverride` lets a caller pin the SCM binary (e.g. plain `gh` instead of
5
+ * the `ghx` cached proxy) when its guarantee depends on an uncached read (#3767).
6
+ */
7
+ export declare function defaultRunGh(cmd: readonly string[], binaryOverride?: string): RunGhResult;
4
8
  /**
5
9
  * Run ``gh pr view <N> --json closingIssuesReferences`` and return linked issue
6
10
  * numbers. Returns ``null`` on external error (caller maps to EXIT_EXTERNAL_ERROR).