@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
@@ -1,9 +1,10 @@
1
1
  import { existsSync, readdirSync, readFileSync } from "node:fs";
2
2
  import { join, relative, resolve } from "node:path";
3
3
  import { hasArtifactSuffix, resolveLifecycleRoot } from "../layout/resolve.js";
4
- import { defaultRunGh } from "../pr-protected-issues/gh.js";
5
- import { CACHE_DIR_NAME, CACHE_SOURCE_GITHUB_ISSUE } from "../triage/queue/constants.js";
4
+ import { ScmStubError } from "../scm/errors.js";
6
5
  import { resolveRepo } from "../triage/queue/repo.js";
6
+ import { normalizeScopePath, resolveCandidateScope, } from "./candidate-scope.js";
7
+ import { AGGREGATE_LATENCY_BUDGET_MS, formatAge, makeGateRunner, OpenIssueInventory, resolveIssueStateAggregate, resolveIssueStateScoped, SCOPED_LATENCY_BUDGET_MS, } from "./issue-state.js";
7
8
  import { collectGithubRefs } from "./refs.js";
8
9
  function readJson(path) {
9
10
  try {
@@ -60,61 +61,62 @@ function listActiveRunningBriefs(projectRoot) {
60
61
  }
61
62
  return out.sort((a, b) => a.path.localeCompare(b.path));
62
63
  }
63
- function readCachedIssueState(projectRoot, ref) {
64
- const [owner, name] = ref.repo.split("/", 2);
65
- if (!owner || !name) {
66
- return null;
67
- }
68
- const rawPath = join(projectRoot, CACHE_DIR_NAME, CACHE_SOURCE_GITHUB_ISSUE, owner, name, String(ref.number), "raw.json");
69
- if (!existsSync(rawPath)) {
70
- return null;
71
- }
72
- const raw = readJson(rawPath);
73
- if (raw === null) {
74
- return null;
75
- }
76
- const state = typeof raw.state === "string" ? raw.state.toLowerCase() : "";
77
- if (state === "closed") {
78
- return "closed";
64
+ /**
65
+ * Records how each verdict was reached so the summary can distinguish a
66
+ * verified pass from an unverified one (#3767).
67
+ */
68
+ class BasisTally {
69
+ inventory = 0;
70
+ live = 0;
71
+ cache = 0;
72
+ maxCacheAgeMs = 0;
73
+ unverifiedReasons = [];
74
+ record(ref, resolution) {
75
+ switch (resolution.basis) {
76
+ case "inventory":
77
+ this.inventory += 1;
78
+ break;
79
+ case "live":
80
+ this.live += 1;
81
+ break;
82
+ case "cache":
83
+ this.cache += 1;
84
+ this.maxCacheAgeMs = Math.max(this.maxCacheAgeMs, resolution.cacheAgeMs);
85
+ break;
86
+ default: {
87
+ const line = `#${ref.number} (${resolution.detail})`;
88
+ if (!this.unverifiedReasons.includes(line)) {
89
+ this.unverifiedReasons.push(line);
90
+ }
91
+ }
92
+ }
79
93
  }
80
- if (state === "open") {
81
- return "open";
94
+ get unverified() {
95
+ return this.unverifiedReasons.length;
82
96
  }
83
- return null;
84
- }
85
- function fetchIssueStateLive(ref, runGh) {
86
- const path = `repos/${ref.repo}/issues/${ref.number}`;
87
- const result = runGh(["gh", "api", path]);
88
- if (result.returncode !== 0) {
89
- return null;
90
- }
91
- try {
92
- const payload = JSON.parse(result.stdout);
93
- if (payload === null || typeof payload !== "object") {
94
- return null;
97
+ summary() {
98
+ const parts = [];
99
+ if (this.inventory > 0) {
100
+ parts.push(`inventory ${this.inventory}`);
95
101
  }
96
- const state = payload.state;
97
- if (state === "closed") {
98
- return "closed";
102
+ if (this.live > 0) {
103
+ parts.push(`live ${this.live}`);
99
104
  }
100
- if (state === "open") {
101
- return "open";
105
+ if (this.cache > 0) {
106
+ parts.push(`cache ${this.cache} (max age ${formatAge(this.maxCacheAgeMs)})`);
102
107
  }
103
- return null;
104
- }
105
- catch {
106
- return null;
108
+ if (this.unverified > 0) {
109
+ parts.push(`unverified ${this.unverified}`);
110
+ }
111
+ return parts.length === 0 ? "no GitHub lookups" : parts.join(", ");
107
112
  }
108
113
  }
109
- function resolveIssueState(ref, projectRoot, runGh, skipGh) {
110
- const cached = readCachedIssueState(projectRoot, ref);
111
- if (cached !== null) {
112
- return cached;
113
- }
114
- if (skipGh) {
115
- return null;
116
- }
117
- return fetchIssueStateLive(ref, runGh);
114
+ function resolveIssueState(ref, ctx, scoped, tally) {
115
+ const resolution = scoped
116
+ ? resolveIssueStateScoped(ref, ctx)
117
+ : resolveIssueStateAggregate(ref, ctx);
118
+ tally.record(ref, resolution);
119
+ return resolution.state;
118
120
  }
119
121
  function fetchPrMerged(ref, runGh) {
120
122
  const path = `repos/${ref.repo}/pulls/${ref.number}`;
@@ -144,11 +146,18 @@ function shipped(reason) {
144
146
  function unresolved(reason) {
145
147
  return { orphaned: true, reason, kind: "unresolved" };
146
148
  }
147
- function assessOrphanSignature(issueRefs, prRefs, projectRoot, runGh, skipGh, selectedIssue) {
149
+ function assessOrphanSignature(issueRefs, prRefs, ctx, selectedIssue, tally) {
150
+ const runGh = ctx.runGh;
151
+ const skipGh = ctx.skipGh;
152
+ const scoped = selectedIssue !== null;
148
153
  if (selectedIssue !== null) {
149
- // Confirmed closed origin is shipped even if a linked PR lookup fails.
150
154
  const selectedRef = issueRefs.find((ref) => ref.number === selectedIssue);
151
- const selectedState = selectedRef === undefined ? null : resolveIssueState(selectedRef, projectRoot, runGh, skipGh);
155
+ if (selectedRef === undefined) {
156
+ // evaluate() only reaches here for briefs that name the issue.
157
+ return PASS;
158
+ }
159
+ // Confirmed closed origin is shipped even if a linked PR lookup fails.
160
+ const selectedState = resolveIssueState(selectedRef, ctx, scoped, tally);
152
161
  if (selectedState === "closed") {
153
162
  return shipped(`issue #${selectedIssue} is closed`);
154
163
  }
@@ -165,25 +174,8 @@ function assessOrphanSignature(issueRefs, prRefs, projectRoot, runGh, skipGh, se
165
174
  }
166
175
  }
167
176
  // --issue N is one origin: sibling open/unknown must not mask it (#3429).
168
- if (selectedRef !== undefined) {
169
- if (selectedState === null) {
170
- return unresolved(`issue #${selectedIssue} state could not be resolved`);
171
- }
172
- return PASS;
173
- }
174
- const issueStates = issueRefs.map((ref) => ({
175
- ref,
176
- state: resolveIssueState(ref, projectRoot, runGh, skipGh),
177
- }));
178
- const unknownIssue = issueStates.find((row) => row.state === null);
179
- if (unknownIssue !== undefined) {
180
- return unresolved(`issue #${unknownIssue.ref.number} state could not be resolved`);
181
- }
182
- if (issueStates.some((row) => row.state === "closed")) {
183
- return shipped("all referenced issues are closed");
184
- }
185
- if (prRefs.length > 0 && skipGh) {
186
- return unresolved(`linked PR #${prRefs[0]?.number} state could not be resolved`);
177
+ if (selectedState === null) {
178
+ return unresolved(`issue #${selectedIssue} state could not be resolved`);
187
179
  }
188
180
  return PASS;
189
181
  }
@@ -199,26 +191,64 @@ function assessOrphanSignature(issueRefs, prRefs, projectRoot, runGh, skipGh, se
199
191
  if (issueRefs.length === 0) {
200
192
  return PASS;
201
193
  }
202
- let resolved = 0;
203
194
  for (const ref of issueRefs) {
204
- const state = resolveIssueState(ref, projectRoot, runGh, skipGh);
205
- if (state === null) {
206
- return PASS;
207
- }
208
- resolved += 1;
195
+ const state = resolveIssueState(ref, ctx, scoped, tally);
196
+ // Unknown keeps the sweep fail-open so offline work is not network-authorized;
197
+ // the unverified basis line stops the pass reading as verified evidence (#3767).
209
198
  if (state !== "closed") {
210
199
  return PASS;
211
200
  }
212
201
  }
213
- if (resolved > 0) {
214
- return shipped("all referenced issues are closed");
215
- }
216
- return PASS;
202
+ return shipped("all referenced issues are closed");
217
203
  }
218
204
  function briefReferencesIssue(issues, issue) {
219
205
  return issues.some((ref) => ref.number === issue);
220
206
  }
221
- function formatRefusal(orphans, projectRoot, issueFilter) {
207
+ /** Basis, ghx caveat, and budget lines shared by the pass and refusal messages (#3767). */
208
+ function basisLines(tally, basis) {
209
+ const lines = [` Basis: ${tally.summary()}.`];
210
+ if (tally.unverified > 0) {
211
+ lines.push(" UNVERIFIED: state could not be established for the references below, so this run is", " not evidence that they are unshipped:");
212
+ for (const reason of tally.unverifiedReasons) {
213
+ lines.push(` - ${reason}`);
214
+ }
215
+ }
216
+ if (basis.proxied) {
217
+ lines.push(" Note: reads resolved through `ghx`, a cached GET proxy; freshness is bounded by that", " proxy, which this gate cannot inspect (#3737).");
218
+ }
219
+ if (basis.elapsedMs > basis.budgetMs) {
220
+ lines.push(` Budget: took ${formatAge(basis.elapsedMs)} against a ${formatAge(basis.budgetMs)} budget (#3767).`);
221
+ }
222
+ return lines;
223
+ }
224
+ /** Keep an operator-supplied ref (or a reason quoting one) on a single line. */
225
+ function oneLine(text) {
226
+ return text.replace(/\r?\n/g, " ");
227
+ }
228
+ /** One operator-visible line naming what this run was allowed to look at (#3893). */
229
+ function scopeLines(scope, skipped) {
230
+ if (scope === null) {
231
+ return [];
232
+ }
233
+ if (scope.kind === "sweep") {
234
+ return [` Scope: repo-wide sweep -- ${oneLine(scope.reason)} (#3893).`];
235
+ }
236
+ const noun = skipped === 1 ? "brief" : "briefs";
237
+ const verb = skipped === 1 ? "was" : "were";
238
+ return [
239
+ ` Scope: candidate diff against ${oneLine(scope.baseRef)}; ${skipped} running ${noun} outside ` +
240
+ `this branch's diff ${verb} not evaluated (#3893).`,
241
+ ];
242
+ }
243
+ function summarizeScope(scope, skipped) {
244
+ if (scope === null) {
245
+ return null;
246
+ }
247
+ return scope.kind === "sweep"
248
+ ? { kind: "sweep", baseRef: null, reason: scope.reason, skipped }
249
+ : { kind: "diff", baseRef: scope.baseRef, reason: null, skipped };
250
+ }
251
+ function formatRefusal(orphans, projectRoot, issueFilter, tally, basis, scope, skipped) {
222
252
  const shippedOrphans = orphans.filter((orphan) => orphan.kind === "shipped");
223
253
  const unresolvedOrphans = orphans.filter((orphan) => orphan.kind === "unresolved");
224
254
  const noun = orphans.length === 1 ? "" : "s";
@@ -241,6 +271,8 @@ function formatRefusal(orphans, projectRoot, issueFilter) {
241
271
  lines.push(" task scope:cancel -- xbrief/active/<file>.xbrief.json # when abandoning", " For stop-at:pr-open workers the orchestrator owns post-merge complete/cancel;", " for drive-to:merge-ready workers scope:complete is part of the worker unit (#2321 / #3429).", " Or run task swarm:finalize-cohort / task swarm:complete-cohort after cohort merge.");
242
272
  }
243
273
  if (unresolvedOrphans.length > 0) {
274
+ // Only the scoped `--issue N` arm can report unresolved, so the retry never
275
+ // needs a --changed-only form (the sweep arm fails open on unknown).
244
276
  const retry = issueFilter === null
245
277
  ? "task verify:orphan-active"
246
278
  : `task verify:orphan-active -- --issue ${issueFilter}`;
@@ -250,27 +282,55 @@ function formatRefusal(orphans, projectRoot, issueFilter) {
250
282
  for (const orphan of orphans) {
251
283
  lines.push(` - ${orphan.path} (${orphan.reason})`);
252
284
  }
285
+ lines.push(...basisLines(tally, basis), ...scopeLines(scope, skipped));
253
286
  return lines.join("\n");
254
287
  }
288
+ function emptyBasis(budgetMs) {
289
+ return {
290
+ inventory: 0,
291
+ live: 0,
292
+ cache: 0,
293
+ unverified: 0,
294
+ maxCacheAgeMs: null,
295
+ proxied: false,
296
+ elapsedMs: 0,
297
+ budgetMs,
298
+ };
299
+ }
255
300
  /**
256
301
  * Pure evaluator for orphaned active/running xBRIEF detection (#2321 / #3429).
257
302
  * Fails when active/ briefs with plan.status==running reference only closed
258
303
  * issues and/or a merged PR — the stop-at:pr-open orphan signature.
259
304
  * Pass `issue` to scan one origin after merge. Confirmed shipped prints
260
305
  * scope:complete; unresolved lookup still exits 1 with a retry remediation.
306
+ *
307
+ * Issue state resolves by query shape (#3767): scoped `--issue N` takes an
308
+ * authoritative read and stays fail-closed on unknown; the unscoped sweep uses
309
+ * one complete, fail-closed open-issue inventory and stays fail-open on
310
+ * unknown so offline work is not network-authorized. A cache hit is honoured
311
+ * only inside `ISSUE_CACHE_MAX_AGE_MS`, and every verdict reports its basis.
312
+ *
313
+ * `changedOnly` is the merge-chokepoint form (#3893): only briefs the
314
+ * candidate's own diff touches are evaluated, so a brief stranded by another
315
+ * merge cannot fail this candidate. It falls back to the repo-wide sweep at
316
+ * the delivery tip and whenever the base ref is unresolvable.
261
317
  */
262
318
  export function evaluate(projectRoot, options = {}) {
263
319
  const root = resolve(projectRoot);
264
320
  const quiet = options.quiet ?? false;
265
321
  const skipGh = options.skipGh ?? false;
266
- const runGh = options.runGh ?? defaultRunGh;
267
- const defaultRepo = resolveRepo(options.repo, root);
322
+ const issueFilter = options.issue ?? null;
323
+ const budgetMs = issueFilter === null ? AGGREGATE_LATENCY_BUDGET_MS : SCOPED_LATENCY_BUDGET_MS;
324
+ const clock = options.nowMs ?? Date.now;
325
+ const startedMs = clock();
268
326
  if (!existsSync(root)) {
269
327
  return {
270
328
  code: 2,
271
329
  message: `verify:orphan-active: project root does not exist: ${root}`,
272
330
  stream: "stderr",
273
331
  orphans: [],
332
+ basis: emptyBasis(budgetMs),
333
+ scope: null,
274
334
  };
275
335
  }
276
336
  let lifecycleRoot;
@@ -282,14 +342,13 @@ export function evaluate(projectRoot, options = {}) {
282
342
  // Consumer/greenfield fixtures may still use legacy vbrief/ only (#2112).
283
343
  // Orphan detection applies only to xbrief/active/ — skip cleanly, not config fail.
284
344
  if (message.includes("No xbrief/ layout found")) {
285
- if (quiet) {
286
- return { code: 0, message: "", stream: "none", orphans: [] };
287
- }
288
345
  return {
289
346
  code: 0,
290
- message: "verify:orphan-active: no xbrief/ lifecycle root; nothing to scan.",
291
- stream: "stdout",
347
+ message: quiet ? "" : "verify:orphan-active: no xbrief/ lifecycle root; nothing to scan.",
348
+ stream: quiet ? "none" : "stdout",
292
349
  orphans: [],
350
+ basis: emptyBasis(budgetMs),
351
+ scope: null,
293
352
  };
294
353
  }
295
354
  return {
@@ -297,58 +356,121 @@ export function evaluate(projectRoot, options = {}) {
297
356
  message: `verify:orphan-active: ${message}`,
298
357
  stream: "stderr",
299
358
  orphans: [],
359
+ basis: emptyBasis(budgetMs),
360
+ scope: null,
300
361
  };
301
362
  }
302
363
  if (!existsSync(lifecycleRoot)) {
303
- if (quiet) {
304
- return { code: 0, message: "", stream: "none", orphans: [] };
305
- }
306
364
  return {
307
365
  code: 0,
308
- message: "verify:orphan-active: no xbrief/ lifecycle root; nothing to scan.",
309
- stream: "stdout",
366
+ message: quiet ? "" : "verify:orphan-active: no xbrief/ lifecycle root; nothing to scan.",
367
+ stream: quiet ? "none" : "stdout",
310
368
  orphans: [],
369
+ basis: emptyBasis(budgetMs),
370
+ scope: null,
311
371
  };
312
372
  }
313
- const issueFilter = options.issue ?? null;
314
- const orphans = [];
315
- for (const brief of listActiveRunningBriefs(root)) {
316
- const { issues, prs } = collectGithubRefs(brief.plan, defaultRepo);
317
- // --issue N is one origin: briefs that name that issue. PR-only briefs stay on the unscoped scan (#3429).
318
- if (issueFilter !== null && !briefReferencesIssue(issues, issueFilter)) {
319
- continue;
373
+ const defaultRepo = resolveRepo(options.repo, root);
374
+ // Resolve the SCM binary lazily: an offline `--skip-gh` run or an empty
375
+ // active/ must not pay for (or fail on) binary resolution it never uses.
376
+ let runner = options.runGh === undefined ? null : { runGh: options.runGh, proxied: false };
377
+ const runGh = (cmd) => {
378
+ runner ??= makeGateRunner();
379
+ return runner.runGh(cmd);
380
+ };
381
+ const tally = new BasisTally();
382
+ const ctx = {
383
+ projectRoot: root,
384
+ runGh,
385
+ skipGh,
386
+ nowMs: clock(),
387
+ inventory: new OpenIssueInventory(runGh),
388
+ };
389
+ try {
390
+ // #3774: missing gh/ghx is config (code 2), not an uncaught throw.
391
+ const briefs = listActiveRunningBriefs(root);
392
+ // Merge-chokepoint scoping (#3893). `--issue N` is already one origin, and
393
+ // an empty active/ has nothing to scope, so neither pays for the git reads.
394
+ const scope = options.changedOnly === true && issueFilter === null && briefs.length > 0
395
+ ? resolveCandidateScope(root, join(lifecycleRoot, "active"), {
396
+ baseRef: options.baseRef,
397
+ runGit: options.runGit,
398
+ })
399
+ : null;
400
+ const orphans = [];
401
+ let scanned = 0;
402
+ let skipped = 0;
403
+ for (const brief of briefs) {
404
+ if (scope?.kind === "diff" && !scope.paths.has(normalizeScopePath(brief.path))) {
405
+ skipped += 1;
406
+ continue;
407
+ }
408
+ scanned += 1;
409
+ const { issues, prs } = collectGithubRefs(brief.plan, defaultRepo);
410
+ // --issue N is one origin: briefs that name that issue. PR-only briefs stay on the unscoped scan (#3429).
411
+ if (issueFilter !== null && !briefReferencesIssue(issues, issueFilter)) {
412
+ continue;
413
+ }
414
+ if (issues.length === 0 && prs.length === 0) {
415
+ continue;
416
+ }
417
+ const assessment = assessOrphanSignature(issues, prs, ctx, issueFilter, tally);
418
+ if (assessment.orphaned && assessment.reason !== null && assessment.kind !== null) {
419
+ orphans.push({
420
+ path: relBriefPath(brief.path, root),
421
+ reason: assessment.reason,
422
+ kind: assessment.kind,
423
+ });
424
+ }
320
425
  }
321
- if (issues.length === 0 && prs.length === 0) {
322
- continue;
426
+ const basis = {
427
+ inventory: tally.inventory,
428
+ live: tally.live,
429
+ cache: tally.cache,
430
+ unverified: tally.unverified,
431
+ maxCacheAgeMs: tally.cache > 0 ? tally.maxCacheAgeMs : null,
432
+ proxied: runner?.proxied ?? false,
433
+ elapsedMs: Math.max(0, clock() - startedMs),
434
+ budgetMs,
435
+ };
436
+ const scopeSummary = summarizeScope(scope, skipped);
437
+ if (orphans.length > 0) {
438
+ return {
439
+ code: 1,
440
+ message: formatRefusal(orphans, root, issueFilter, tally, basis, scope, skipped),
441
+ stream: "stderr",
442
+ orphans,
443
+ basis,
444
+ scope: scopeSummary,
445
+ };
323
446
  }
324
- const assessment = assessOrphanSignature(issues, prs, root, runGh, skipGh, issueFilter);
325
- if (assessment.orphaned && assessment.reason !== null && assessment.kind !== null) {
326
- orphans.push({
327
- path: relBriefPath(brief.path, root),
328
- reason: assessment.reason,
329
- kind: assessment.kind,
330
- });
447
+ if (quiet) {
448
+ return { code: 0, message: "", stream: "none", orphans: [], basis, scope: scopeSummary };
331
449
  }
332
- }
333
- if (orphans.length > 0) {
450
+ const issueNote = issueFilter === null ? "" : ` for issue #${issueFilter}`;
451
+ const headline = `verify:orphan-active: no orphaned active/running xBRIEFs${issueNote} ` +
452
+ `(scanned ${scanned} running brief${scanned === 1 ? "" : "s"} in active/).`;
334
453
  return {
335
- code: 1,
336
- message: formatRefusal(orphans, root, issueFilter),
337
- stream: "stderr",
338
- orphans,
454
+ code: 0,
455
+ message: [headline, ...basisLines(tally, basis), ...scopeLines(scope, skipped)].join("\n"),
456
+ stream: "stdout",
457
+ orphans: [],
458
+ basis,
459
+ scope: scopeSummary,
339
460
  };
340
461
  }
341
- if (quiet) {
342
- return { code: 0, message: "", stream: "none", orphans: [] };
462
+ catch (err) {
463
+ if (err instanceof ScmStubError) {
464
+ return {
465
+ code: 2,
466
+ message: `verify:orphan-active: ${err.message}`,
467
+ stream: "stderr",
468
+ orphans: [],
469
+ basis: emptyBasis(budgetMs),
470
+ scope: null,
471
+ };
472
+ }
473
+ throw err;
343
474
  }
344
- const scanned = listActiveRunningBriefs(root).length;
345
- const issueNote = issueFilter === null ? "" : ` for issue #${issueFilter}`;
346
- return {
347
- code: 0,
348
- message: `verify:orphan-active: no orphaned active/running xBRIEFs${issueNote} ` +
349
- `(scanned ${scanned} running brief${scanned === 1 ? "" : "s"} in active/).`,
350
- stream: "stdout",
351
- orphans: [],
352
- };
353
475
  }
354
476
  //# sourceMappingURL=evaluate.js.map
@@ -1,3 +1,5 @@
1
+ export * from "./candidate-scope.js";
1
2
  export * from "./evaluate.js";
3
+ export * from "./issue-state.js";
2
4
  export * from "./refs.js";
3
5
  //# sourceMappingURL=index.d.ts.map
@@ -1,3 +1,5 @@
1
+ export * from "./candidate-scope.js";
1
2
  export * from "./evaluate.js";
3
+ export * from "./issue-state.js";
2
4
  export * from "./refs.js";
3
5
  //# sourceMappingURL=index.js.map
@@ -0,0 +1,121 @@
1
+ import type { RunGhFn } from "../pr-protected-issues/types.js";
2
+ import type { IssueRef } from "./refs.js";
3
+ export type IssueState = "open" | "closed";
4
+ /**
5
+ * How a verdict was reached (#3767). `unverified` means the gate could not
6
+ * establish state, so a pass on that reference proves nothing.
7
+ */
8
+ export type StateBasis = "inventory" | "live" | "cache" | "unverified";
9
+ export type StateResolution =
10
+ /** Resolved from the complete open-issue inventory or an authoritative read. */
11
+ {
12
+ readonly state: IssueState;
13
+ readonly basis: "inventory" | "live";
14
+ }
15
+ /** Resolved from a triage-cache entry inside the freshness bound. */
16
+ | {
17
+ readonly state: IssueState;
18
+ readonly basis: "cache";
19
+ readonly cacheAgeMs: number;
20
+ }
21
+ /** State could not be established; a pass on this reference proves nothing. */
22
+ | {
23
+ readonly state: null;
24
+ readonly basis: "unverified";
25
+ readonly detail: string;
26
+ };
27
+ /**
28
+ * A cache hit older than this is no longer evidence (#3767). The measured
29
+ * defect was a ~12 h old cached `open` suppressing the live read that would
30
+ * have corrected it; 15 min keeps warm-cache offline runs working while making
31
+ * an overnight entry non-authoritative.
32
+ */
33
+ export declare const ISSUE_CACHE_MAX_AGE_MS: number;
34
+ /**
35
+ * Latency budget for scoped `--issue N`: one authoritative issue read plus at
36
+ * most one linked-PR read. Measured ~0.75 s per `gh api` read; the headroom
37
+ * covers a slow network or a `ghx` proxy hop.
38
+ */
39
+ export declare const SCOPED_LATENCY_BUDGET_MS = 5000;
40
+ /**
41
+ * Latency budget for the unscoped aggregate sweep. One complete open-issue
42
+ * inventory measured 4.2 s constant on deftai/directive (560 open issues),
43
+ * against 14.2 s and 76.6 s for per-brief reads at the WIP cap of 20 on two
44
+ * hosts. The budget is sized for the inventory plus a few confirming reads and
45
+ * keeps the gate inside the fast-preflight tier.
46
+ */
47
+ export declare const AGGREGATE_LATENCY_BUDGET_MS = 15000;
48
+ export interface GateRunner {
49
+ readonly runGh: RunGhFn;
50
+ /**
51
+ * True when reads resolve through `ghx`, a cached GET proxy. The freshness
52
+ * guarantee is then bounded by that proxy, which this gate cannot inspect.
53
+ */
54
+ readonly proxied: boolean;
55
+ }
56
+ /**
57
+ * Pin plain `gh` for this gate's authoritative reads (#3767). `resolveBinary`
58
+ * prefers `ghx` — a cached GET proxy — so an unpinned "live" read is itself a
59
+ * cache. When `gh` is absent we still run, and report the proxy caveat rather
60
+ * than claiming freshness we cannot deliver. Whether `ghx` should be preferred
61
+ * at all is #3737.
62
+ */
63
+ export declare function makeGateRunner(): GateRunner;
64
+ interface CachedIssue {
65
+ readonly state: IssueState | null;
66
+ readonly ageMs: number | null;
67
+ }
68
+ /** Read the triage-cache issue state together with the entry's age (#3767). */
69
+ export declare function readCachedIssue(projectRoot: string, ref: IssueRef, nowMs: number): CachedIssue;
70
+ /** Authoritative single-issue REST read. Returns `null` on any failure. */
71
+ export declare function fetchIssueStateLive(ref: IssueRef, runGh: RunGhFn): IssueState | null;
72
+ type InventoryLookup = {
73
+ numbers: ReadonlySet<number>;
74
+ } | {
75
+ error: string;
76
+ };
77
+ /**
78
+ * One complete open-issue inventory per repo per run (#3767, reusing #3752).
79
+ * `restIssueListOpenInventory` fails closed on command failure, non-array JSON,
80
+ * malformed rows, buffer exhaustion, and pagination cap — so absence from a
81
+ * successful inventory really does mean "not open", and a truncated read is an
82
+ * error rather than a silent "closed".
83
+ */
84
+ export declare class OpenIssueInventory {
85
+ private readonly runGh;
86
+ private readonly byRepo;
87
+ private readonly confirmations;
88
+ constructor(runGh: RunGhFn);
89
+ lookup(repo: string): InventoryLookup;
90
+ /**
91
+ * Confirm an inventory absence with one authoritative read, reused for the
92
+ * rest of this run when several briefs name the same issue (#3767).
93
+ */
94
+ confirm(ref: IssueRef): IssueState | null;
95
+ private fetch;
96
+ }
97
+ export interface ResolveContext {
98
+ readonly projectRoot: string;
99
+ readonly runGh: RunGhFn;
100
+ readonly skipGh: boolean;
101
+ readonly nowMs: number;
102
+ readonly inventory: OpenIssueInventory;
103
+ }
104
+ /** Human-readable duration for basis reporting: `42s`, `9m`, `12h`, `3d`. */
105
+ export declare function formatAge(ms: number): string;
106
+ /**
107
+ * Scoped `--issue N`: a proof obligation before DONE, so read authoritatively
108
+ * first and accept a cache hit only inside the freshness bound. Unknown stays
109
+ * fail-closed at the call site.
110
+ */
111
+ export declare function resolveIssueStateScoped(ref: IssueRef, ctx: ResolveContext): StateResolution;
112
+ /**
113
+ * Unscoped aggregate: resolve every reference from one complete open-issue
114
+ * inventory rather than a live read per brief. Membership means open. Absence
115
+ * means "not open", which is the direction that tells an operator to run
116
+ * `scope:complete`, so it is confirmed by one authoritative read before the
117
+ * gate acts on it.
118
+ */
119
+ export declare function resolveIssueStateAggregate(ref: IssueRef, ctx: ResolveContext): StateResolution;
120
+ export {};
121
+ //# sourceMappingURL=issue-state.d.ts.map