@deftai/directive-core 0.106.0 → 0.108.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 (249) hide show
  1. package/dist/architecture/sor-preflight.js +2 -0
  2. package/dist/check/cached-orchestrator.js +28 -3
  3. package/dist/check/gate-lists.js +2 -0
  4. package/dist/check/named-cause.js +18 -3
  5. package/dist/codebase/provider.js +7 -1
  6. package/dist/delivery-attempt/handoff.js +1 -1
  7. package/dist/design-critique/citation-grammar.d.ts +53 -0
  8. package/dist/design-critique/citation-grammar.js +303 -0
  9. package/dist/design-critique/completed-arc-record.d.ts +62 -0
  10. package/dist/design-critique/completed-arc-record.js +232 -0
  11. package/dist/design-critique/exclusive-chip.d.ts +33 -0
  12. package/dist/design-critique/exclusive-chip.js +68 -0
  13. package/dist/design-critique/parent-audit.d.ts +59 -0
  14. package/dist/design-critique/parent-audit.js +121 -0
  15. package/dist/doctor/checks.d.ts +6 -0
  16. package/dist/doctor/checks.js +35 -0
  17. package/dist/freshness/bind.d.ts +32 -3
  18. package/dist/freshness/bind.js +58 -12
  19. package/dist/hooks/classify/host-session-identity.d.ts +76 -0
  20. package/dist/hooks/classify/host-session-identity.js +470 -0
  21. package/dist/hooks/classify/index.d.ts +3 -2
  22. package/dist/hooks/classify/index.js +3 -2
  23. package/dist/hooks/classify/paths.d.ts +7 -0
  24. package/dist/hooks/classify/paths.js +35 -0
  25. package/dist/hooks/classify/stdin.d.ts +5 -0
  26. package/dist/hooks/classify/stdin.js +27 -0
  27. package/dist/hooks/dest-form.d.ts +46 -0
  28. package/dist/hooks/dest-form.js +575 -0
  29. package/dist/hooks/dispatcher.d.ts +69 -2
  30. package/dist/hooks/dispatcher.js +609 -45
  31. package/dist/hooks/fixtures/cases.d.ts +16 -2
  32. package/dist/hooks/fixtures/cases.js +175 -0
  33. package/dist/hooks/index.d.ts +1 -0
  34. package/dist/hooks/index.js +1 -0
  35. package/dist/hooks/scope.js +3 -1
  36. package/dist/init-deposit/agent-hooks.d.ts +2 -1
  37. package/dist/init-deposit/agent-hooks.js +8 -1
  38. package/dist/init-deposit/hook-runtime-travel.d.ts +77 -0
  39. package/dist/init-deposit/hook-runtime-travel.js +269 -0
  40. package/dist/intake/clause-derivation.js +9 -3
  41. package/dist/intake/github-auth-modes-cli.js +14 -2
  42. package/dist/intake/github-auth-modes.d.ts +54 -9
  43. package/dist/intake/github-auth-modes.js +326 -123
  44. package/dist/intake/issue-ingest.js +62 -2
  45. package/dist/intake/platform-capabilities.d.ts +9 -2
  46. package/dist/intake/platform-capabilities.js +38 -9
  47. package/dist/lifecycle/completed-tracked-on-delivery.d.ts +26 -0
  48. package/dist/lifecycle/completed-tracked-on-delivery.js +37 -14
  49. package/dist/lifecycle/completed-write-guard.d.ts +48 -0
  50. package/dist/lifecycle/completed-write-guard.js +373 -0
  51. package/dist/lifecycle/index.d.ts +1 -0
  52. package/dist/lifecycle/index.js +1 -0
  53. package/dist/literal-acceptance/capture.d.ts +16 -0
  54. package/dist/literal-acceptance/capture.js +75 -7
  55. package/dist/literal-acceptance/evaluate.js +25 -3
  56. package/dist/literal-acceptance/index.d.ts +2 -2
  57. package/dist/literal-acceptance/index.js +2 -2
  58. package/dist/literal-acceptance/run.js +9 -3
  59. package/dist/literal-acceptance/types.d.ts +6 -0
  60. package/dist/literal-acceptance/types.js +6 -0
  61. package/dist/orphan-active/evaluate.d.ts +25 -0
  62. package/dist/orphan-active/evaluate.js +193 -128
  63. package/dist/orphan-active/index.d.ts +1 -0
  64. package/dist/orphan-active/index.js +1 -0
  65. package/dist/orphan-active/issue-state.d.ts +121 -0
  66. package/dist/orphan-active/issue-state.js +278 -0
  67. package/dist/platform/cursor-managed-runtime.d.ts +95 -0
  68. package/dist/platform/cursor-managed-runtime.js +241 -0
  69. package/dist/platform/index.d.ts +1 -0
  70. package/dist/platform/index.js +1 -0
  71. package/dist/platform/platform-capabilities.d.ts +23 -1
  72. package/dist/platform/platform-capabilities.js +55 -15
  73. package/dist/policy/ceremony-dial.js +5 -9
  74. package/dist/policy/host-hooks.js +4 -9
  75. package/dist/policy/org-force-on-migration.js +5 -10
  76. package/dist/policy/plan-extensions.d.ts +14 -2
  77. package/dist/policy/plan-extensions.js +24 -3
  78. package/dist/policy/product-signal.js +5 -10
  79. package/dist/policy/require-human-merge.js +5 -9
  80. package/dist/policy/resolve.js +77 -16
  81. package/dist/policy/runtime-authority.d.ts +18 -0
  82. package/dist/policy/runtime-authority.js +11 -0
  83. package/dist/policy/value-feedback.js +9 -18
  84. package/dist/policy/write-fence.js +3 -0
  85. package/dist/pr-closeout-attestable/evaluate.d.ts +80 -0
  86. package/dist/pr-closeout-attestable/evaluate.js +306 -0
  87. package/dist/pr-closeout-attestable/index.d.ts +2 -0
  88. package/dist/pr-closeout-attestable/index.js +2 -0
  89. package/dist/pr-protected-issues/gh.d.ts +6 -2
  90. package/dist/pr-protected-issues/gh.js +7 -3
  91. package/dist/pr-wait-mergeable/cascade.d.ts +9 -1
  92. package/dist/pr-wait-mergeable/cascade.js +21 -1
  93. package/dist/pr-wait-mergeable/types.d.ts +2 -0
  94. package/dist/pr-wait-mergeable/wrappers.d.ts +6 -0
  95. package/dist/pr-wait-mergeable/wrappers.js +11 -0
  96. package/dist/preflight/evaluate.d.ts +15 -0
  97. package/dist/preflight/evaluate.js +29 -3
  98. package/dist/preflight-cache/evaluate.js +9 -3
  99. package/dist/product-first-done-gate/acceptance-resolver.d.ts +14 -2
  100. package/dist/product-first-done-gate/acceptance-resolver.js +13 -2
  101. package/dist/product-first-done-gate/evaluate.js +53 -12
  102. package/dist/release/cli-drift-report.d.ts +68 -0
  103. package/dist/release/cli-drift-report.js +189 -0
  104. package/dist/release/index.d.ts +1 -0
  105. package/dist/release/index.js +1 -0
  106. package/dist/release/issue-state-fetch.d.ts +5 -3
  107. package/dist/release/issue-state-fetch.js +86 -21
  108. package/dist/release/native-steps.js +1 -0
  109. package/dist/release/pipeline.js +19 -0
  110. package/dist/release/types.d.ts +14 -0
  111. package/dist/render/constants.d.ts +4 -0
  112. package/dist/render/constants.js +11 -8
  113. package/dist/render/export-spec.js +31 -5
  114. package/dist/render/index.d.ts +1 -1
  115. package/dist/render/index.js +1 -1
  116. package/dist/render/prd-render.d.ts +4 -1
  117. package/dist/render/prd-render.js +82 -23
  118. package/dist/render/project-render.js +14 -7
  119. package/dist/render/scope-outlook.d.ts +2 -0
  120. package/dist/render/scope-outlook.js +3 -0
  121. package/dist/render/spec-render.js +2 -2
  122. package/dist/resolution/package-manager.d.ts +33 -4
  123. package/dist/resolution/package-manager.js +210 -9
  124. package/dist/review-monitor/constants.d.ts +7 -0
  125. package/dist/review-monitor/constants.js +7 -0
  126. package/dist/review-monitor/github-lease.d.ts +98 -1
  127. package/dist/review-monitor/github-lease.js +182 -1
  128. package/dist/review-monitor/lease-comment.d.ts +36 -0
  129. package/dist/review-monitor/lease-comment.js +103 -2
  130. package/dist/scm/call.js +12 -3
  131. package/dist/scm/design-critique-chip.d.ts +38 -0
  132. package/dist/scm/design-critique-chip.js +187 -0
  133. package/dist/scm/gh-rest.d.ts +9 -1
  134. package/dist/scm/gh-rest.js +103 -0
  135. package/dist/scm/index.d.ts +1 -0
  136. package/dist/scm/index.js +1 -0
  137. package/dist/scm/main.d.ts +3 -0
  138. package/dist/scm/main.js +15 -1
  139. package/dist/scm/readiness-cli.d.ts +2 -0
  140. package/dist/scm/readiness-cli.js +42 -1
  141. package/dist/scm/readiness.d.ts +4 -1
  142. package/dist/scm/readiness.js +35 -5
  143. package/dist/scope/decompose.js +2 -1
  144. package/dist/scope/delivery-evidence.d.ts +27 -1
  145. package/dist/scope/index.d.ts +1 -0
  146. package/dist/scope/index.js +1 -0
  147. package/dist/scope/lifecycle-write.d.ts +24 -0
  148. package/dist/scope/lifecycle-write.js +64 -0
  149. package/dist/scope/main.js +3 -1
  150. package/dist/scope/project-definition-sync.js +14 -7
  151. package/dist/scope/transition.js +90 -11
  152. package/dist/scope-provenance/evaluate.js +5 -1
  153. package/dist/session/git.d.ts +42 -0
  154. package/dist/session/git.js +219 -3
  155. package/dist/session/occupancy.d.ts +239 -12
  156. package/dist/session/occupancy.js +820 -27
  157. package/dist/session/ritual-sentinel.d.ts +5 -0
  158. package/dist/session/ritual-sentinel.js +5 -0
  159. package/dist/session/session-ready.d.ts +5 -1
  160. package/dist/session/session-ready.js +98 -8
  161. package/dist/session/session-start.d.ts +17 -1
  162. package/dist/session/session-start.js +49 -17
  163. package/dist/session/toolchain-preflight.d.ts +13 -3
  164. package/dist/session/toolchain-preflight.js +93 -18
  165. package/dist/session/verify-session-ritual.d.ts +33 -3
  166. package/dist/session/verify-session-ritual.js +89 -19
  167. package/dist/slice/constants.d.ts +1 -1
  168. package/dist/spec-authority/constants.d.ts +10 -2
  169. package/dist/spec-authority/constants.js +53 -8
  170. package/dist/spec-authority/resolver.d.ts +3 -0
  171. package/dist/spec-authority/resolver.js +55 -7
  172. package/dist/subprocess/max-buffer.d.ts +14 -0
  173. package/dist/subprocess/max-buffer.js +15 -0
  174. package/dist/swarm/complete-cohort.d.ts +2 -0
  175. package/dist/swarm/complete-cohort.js +21 -13
  176. package/dist/swarm/index.d.ts +1 -0
  177. package/dist/swarm/index.js +1 -0
  178. package/dist/swarm/launch-cli.js +53 -33
  179. package/dist/swarm/launch.d.ts +77 -0
  180. package/dist/swarm/launch.js +248 -38
  181. package/dist/swarm/pre-dispatch-cli.js +2 -1
  182. package/dist/swarm/pre-dispatch.js +10 -1
  183. package/dist/swarm/subagent-status-dir.d.ts +27 -0
  184. package/dist/swarm/subagent-status-dir.js +42 -0
  185. package/dist/swarm/worktrees.d.ts +17 -3
  186. package/dist/swarm/worktrees.js +48 -4
  187. package/dist/triage/actions/index.d.ts +4 -0
  188. package/dist/triage/actions/index.js +6 -0
  189. package/dist/triage/evaluate/evaluate.d.ts +11 -0
  190. package/dist/triage/evaluate/evaluate.js +169 -0
  191. package/dist/triage/evaluate/github.d.ts +16 -0
  192. package/dist/triage/evaluate/github.js +153 -0
  193. package/dist/triage/evaluate/index.d.ts +9 -0
  194. package/dist/triage/evaluate/index.js +8 -0
  195. package/dist/triage/evaluate/paths.d.ts +7 -0
  196. package/dist/triage/evaluate/paths.js +25 -0
  197. package/dist/triage/evaluate/sink.d.ts +5 -0
  198. package/dist/triage/evaluate/sink.js +52 -0
  199. package/dist/triage/evaluate/types.d.ts +93 -0
  200. package/dist/triage/evaluate/types.js +13 -0
  201. package/dist/triage/evaluate/validity.d.ts +8 -0
  202. package/dist/triage/evaluate/validity.js +87 -0
  203. package/dist/triage/evaluate/value.d.ts +12 -0
  204. package/dist/triage/evaluate/value.js +33 -0
  205. package/dist/triage/evaluate/wip-census.d.ts +5 -0
  206. package/dist/triage/evaluate/wip-census.js +39 -0
  207. package/dist/triage/evaluate/worktrees.d.ts +8 -0
  208. package/dist/triage/evaluate/worktrees.js +68 -0
  209. package/dist/triage/evaluate/xbrief-refs.d.ts +8 -0
  210. package/dist/triage/evaluate/xbrief-refs.js +60 -0
  211. package/dist/triage/help/registry-data.d.ts +21 -7
  212. package/dist/triage/help/registry-data.js +53 -5
  213. package/dist/triage/index.d.ts +1 -0
  214. package/dist/triage/index.js +1 -0
  215. package/dist/triage/scope/mutations-core.d.ts +2 -6
  216. package/dist/triage/scope/mutations-core.js +23 -55
  217. package/dist/triage/scope-drift/add-ignore.js +5 -39
  218. package/dist/triage/subscribe/index.d.ts +2 -6
  219. package/dist/triage/subscribe/index.js +15 -65
  220. package/dist/triage/welcome/writers.js +15 -74
  221. package/dist/value/feedback-file.d.ts +17 -2
  222. package/dist/value/feedback-file.js +80 -6
  223. package/dist/vbrief-build/index.d.ts +2 -1
  224. package/dist/vbrief-build/index.js +2 -1
  225. package/dist/vbrief-build/parity-scenarios.js +5 -5
  226. package/dist/vbrief-build/project-definition-io.d.ts +63 -3
  227. package/dist/vbrief-build/project-definition-io.js +357 -31
  228. package/dist/vbrief-build/project-definition-mutation.d.ts +32 -0
  229. package/dist/vbrief-build/project-definition-mutation.js +43 -0
  230. package/dist/vbrief-reconcile/labels.d.ts +1 -0
  231. package/dist/vbrief-reconcile/labels.js +30 -0
  232. package/dist/vbrief-validate/conformance.d.ts +1 -0
  233. package/dist/vbrief-validate/conformance.js +85 -8
  234. package/dist/vbrief-validate/main.js +2 -0
  235. package/dist/vbrief-validate/precutover.js +5 -10
  236. package/dist/vbrief-validation/story-quality.d.ts +7 -0
  237. package/dist/vbrief-validation/story-quality.js +8 -1
  238. package/dist/verify-ac/clauses.d.ts +62 -2
  239. package/dist/verify-ac/clauses.js +183 -111
  240. package/dist/verify-ac/index.d.ts +1 -1
  241. package/dist/verify-ac/index.js +1 -1
  242. package/dist/verify-env/node-runtime.d.ts +8 -4
  243. package/dist/verify-env/node-runtime.js +9 -6
  244. package/dist/verify-env/toolchain-check.d.ts +35 -3
  245. package/dist/verify-env/toolchain-check.js +155 -36
  246. package/dist/verify-source/project-definition-mutation-boundary.d.ts +104 -0
  247. package/dist/verify-source/project-definition-mutation-boundary.js +326 -0
  248. package/dist/xbrief-migrate/agents-header.js +69 -7
  249. package/package.json +7 -3
@@ -1,9 +1,9 @@
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 { AGGREGATE_LATENCY_BUDGET_MS, formatAge, makeGateRunner, OpenIssueInventory, resolveIssueStateAggregate, resolveIssueStateScoped, SCOPED_LATENCY_BUDGET_MS, } from "./issue-state.js";
7
7
  import { collectGithubRefs } from "./refs.js";
8
8
  function readJson(path) {
9
9
  try {
@@ -60,61 +60,62 @@ function listActiveRunningBriefs(projectRoot) {
60
60
  }
61
61
  return out.sort((a, b) => a.path.localeCompare(b.path));
62
62
  }
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";
79
- }
80
- if (state === "open") {
81
- return "open";
63
+ /**
64
+ * Records how each verdict was reached so the summary can distinguish a
65
+ * verified pass from an unverified one (#3767).
66
+ */
67
+ class BasisTally {
68
+ inventory = 0;
69
+ live = 0;
70
+ cache = 0;
71
+ maxCacheAgeMs = 0;
72
+ unverifiedReasons = [];
73
+ record(ref, resolution) {
74
+ switch (resolution.basis) {
75
+ case "inventory":
76
+ this.inventory += 1;
77
+ break;
78
+ case "live":
79
+ this.live += 1;
80
+ break;
81
+ case "cache":
82
+ this.cache += 1;
83
+ this.maxCacheAgeMs = Math.max(this.maxCacheAgeMs, resolution.cacheAgeMs);
84
+ break;
85
+ default: {
86
+ const line = `#${ref.number} (${resolution.detail})`;
87
+ if (!this.unverifiedReasons.includes(line)) {
88
+ this.unverifiedReasons.push(line);
89
+ }
90
+ }
91
+ }
82
92
  }
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;
93
+ get unverified() {
94
+ return this.unverifiedReasons.length;
90
95
  }
91
- try {
92
- const payload = JSON.parse(result.stdout);
93
- if (payload === null || typeof payload !== "object") {
94
- return null;
96
+ summary() {
97
+ const parts = [];
98
+ if (this.inventory > 0) {
99
+ parts.push(`inventory ${this.inventory}`);
95
100
  }
96
- const state = payload.state;
97
- if (state === "closed") {
98
- return "closed";
101
+ if (this.live > 0) {
102
+ parts.push(`live ${this.live}`);
99
103
  }
100
- if (state === "open") {
101
- return "open";
104
+ if (this.cache > 0) {
105
+ parts.push(`cache ${this.cache} (max age ${formatAge(this.maxCacheAgeMs)})`);
102
106
  }
103
- return null;
104
- }
105
- catch {
106
- return null;
107
+ if (this.unverified > 0) {
108
+ parts.push(`unverified ${this.unverified}`);
109
+ }
110
+ return parts.length === 0 ? "no GitHub lookups" : parts.join(", ");
107
111
  }
108
112
  }
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);
113
+ function resolveIssueState(ref, ctx, scoped, tally) {
114
+ const resolution = scoped
115
+ ? resolveIssueStateScoped(ref, ctx)
116
+ : resolveIssueStateAggregate(ref, ctx);
117
+ tally.record(ref, resolution);
118
+ return resolution.state;
118
119
  }
119
120
  function fetchPrMerged(ref, runGh) {
120
121
  const path = `repos/${ref.repo}/pulls/${ref.number}`;
@@ -144,11 +145,18 @@ function shipped(reason) {
144
145
  function unresolved(reason) {
145
146
  return { orphaned: true, reason, kind: "unresolved" };
146
147
  }
147
- function assessOrphanSignature(issueRefs, prRefs, projectRoot, runGh, skipGh, selectedIssue) {
148
+ function assessOrphanSignature(issueRefs, prRefs, ctx, selectedIssue, tally) {
149
+ const runGh = ctx.runGh;
150
+ const skipGh = ctx.skipGh;
151
+ const scoped = selectedIssue !== null;
148
152
  if (selectedIssue !== null) {
149
- // Confirmed closed origin is shipped even if a linked PR lookup fails.
150
153
  const selectedRef = issueRefs.find((ref) => ref.number === selectedIssue);
151
- const selectedState = selectedRef === undefined ? null : resolveIssueState(selectedRef, projectRoot, runGh, skipGh);
154
+ if (selectedRef === undefined) {
155
+ // evaluate() only reaches here for briefs that name the issue.
156
+ return PASS;
157
+ }
158
+ // Confirmed closed origin is shipped even if a linked PR lookup fails.
159
+ const selectedState = resolveIssueState(selectedRef, ctx, scoped, tally);
152
160
  if (selectedState === "closed") {
153
161
  return shipped(`issue #${selectedIssue} is closed`);
154
162
  }
@@ -165,25 +173,8 @@ function assessOrphanSignature(issueRefs, prRefs, projectRoot, runGh, skipGh, se
165
173
  }
166
174
  }
167
175
  // --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`);
176
+ if (selectedState === null) {
177
+ return unresolved(`issue #${selectedIssue} state could not be resolved`);
187
178
  }
188
179
  return PASS;
189
180
  }
@@ -199,26 +190,37 @@ function assessOrphanSignature(issueRefs, prRefs, projectRoot, runGh, skipGh, se
199
190
  if (issueRefs.length === 0) {
200
191
  return PASS;
201
192
  }
202
- let resolved = 0;
203
193
  for (const ref of issueRefs) {
204
- const state = resolveIssueState(ref, projectRoot, runGh, skipGh);
205
- if (state === null) {
206
- return PASS;
207
- }
208
- resolved += 1;
194
+ const state = resolveIssueState(ref, ctx, scoped, tally);
195
+ // Unknown keeps the sweep fail-open so offline work is not network-authorized;
196
+ // the unverified basis line stops the pass reading as verified evidence (#3767).
209
197
  if (state !== "closed") {
210
198
  return PASS;
211
199
  }
212
200
  }
213
- if (resolved > 0) {
214
- return shipped("all referenced issues are closed");
215
- }
216
- return PASS;
201
+ return shipped("all referenced issues are closed");
217
202
  }
218
203
  function briefReferencesIssue(issues, issue) {
219
204
  return issues.some((ref) => ref.number === issue);
220
205
  }
221
- function formatRefusal(orphans, projectRoot, issueFilter) {
206
+ /** Basis, ghx caveat, and budget lines shared by the pass and refusal messages (#3767). */
207
+ function basisLines(tally, basis) {
208
+ const lines = [` Basis: ${tally.summary()}.`];
209
+ if (tally.unverified > 0) {
210
+ lines.push(" UNVERIFIED: state could not be established for the references below, so this run is", " not evidence that they are unshipped:");
211
+ for (const reason of tally.unverifiedReasons) {
212
+ lines.push(` - ${reason}`);
213
+ }
214
+ }
215
+ if (basis.proxied) {
216
+ lines.push(" Note: reads resolved through `ghx`, a cached GET proxy; freshness is bounded by that", " proxy, which this gate cannot inspect (#3737).");
217
+ }
218
+ if (basis.elapsedMs > basis.budgetMs) {
219
+ lines.push(` Budget: took ${formatAge(basis.elapsedMs)} against a ${formatAge(basis.budgetMs)} budget (#3767).`);
220
+ }
221
+ return lines;
222
+ }
223
+ function formatRefusal(orphans, projectRoot, issueFilter, tally, basis) {
222
224
  const shippedOrphans = orphans.filter((orphan) => orphan.kind === "shipped");
223
225
  const unresolvedOrphans = orphans.filter((orphan) => orphan.kind === "unresolved");
224
226
  const noun = orphans.length === 1 ? "" : "s";
@@ -250,27 +252,49 @@ function formatRefusal(orphans, projectRoot, issueFilter) {
250
252
  for (const orphan of orphans) {
251
253
  lines.push(` - ${orphan.path} (${orphan.reason})`);
252
254
  }
255
+ lines.push(...basisLines(tally, basis));
253
256
  return lines.join("\n");
254
257
  }
258
+ function emptyBasis(budgetMs) {
259
+ return {
260
+ inventory: 0,
261
+ live: 0,
262
+ cache: 0,
263
+ unverified: 0,
264
+ maxCacheAgeMs: null,
265
+ proxied: false,
266
+ elapsedMs: 0,
267
+ budgetMs,
268
+ };
269
+ }
255
270
  /**
256
271
  * Pure evaluator for orphaned active/running xBRIEF detection (#2321 / #3429).
257
272
  * Fails when active/ briefs with plan.status==running reference only closed
258
273
  * issues and/or a merged PR — the stop-at:pr-open orphan signature.
259
274
  * Pass `issue` to scan one origin after merge. Confirmed shipped prints
260
275
  * scope:complete; unresolved lookup still exits 1 with a retry remediation.
276
+ *
277
+ * Issue state resolves by query shape (#3767): scoped `--issue N` takes an
278
+ * authoritative read and stays fail-closed on unknown; the unscoped sweep uses
279
+ * one complete, fail-closed open-issue inventory and stays fail-open on
280
+ * unknown so offline work is not network-authorized. A cache hit is honoured
281
+ * only inside `ISSUE_CACHE_MAX_AGE_MS`, and every verdict reports its basis.
261
282
  */
262
283
  export function evaluate(projectRoot, options = {}) {
263
284
  const root = resolve(projectRoot);
264
285
  const quiet = options.quiet ?? false;
265
286
  const skipGh = options.skipGh ?? false;
266
- const runGh = options.runGh ?? defaultRunGh;
267
- const defaultRepo = resolveRepo(options.repo, root);
287
+ const issueFilter = options.issue ?? null;
288
+ const budgetMs = issueFilter === null ? AGGREGATE_LATENCY_BUDGET_MS : SCOPED_LATENCY_BUDGET_MS;
289
+ const clock = options.nowMs ?? Date.now;
290
+ const startedMs = clock();
268
291
  if (!existsSync(root)) {
269
292
  return {
270
293
  code: 2,
271
294
  message: `verify:orphan-active: project root does not exist: ${root}`,
272
295
  stream: "stderr",
273
296
  orphans: [],
297
+ basis: emptyBasis(budgetMs),
274
298
  };
275
299
  }
276
300
  let lifecycleRoot;
@@ -282,14 +306,12 @@ export function evaluate(projectRoot, options = {}) {
282
306
  // Consumer/greenfield fixtures may still use legacy vbrief/ only (#2112).
283
307
  // Orphan detection applies only to xbrief/active/ — skip cleanly, not config fail.
284
308
  if (message.includes("No xbrief/ layout found")) {
285
- if (quiet) {
286
- return { code: 0, message: "", stream: "none", orphans: [] };
287
- }
288
309
  return {
289
310
  code: 0,
290
- message: "verify:orphan-active: no xbrief/ lifecycle root; nothing to scan.",
291
- stream: "stdout",
311
+ message: quiet ? "" : "verify:orphan-active: no xbrief/ lifecycle root; nothing to scan.",
312
+ stream: quiet ? "none" : "stdout",
292
313
  orphans: [],
314
+ basis: emptyBasis(budgetMs),
293
315
  };
294
316
  }
295
317
  return {
@@ -297,58 +319,101 @@ export function evaluate(projectRoot, options = {}) {
297
319
  message: `verify:orphan-active: ${message}`,
298
320
  stream: "stderr",
299
321
  orphans: [],
322
+ basis: emptyBasis(budgetMs),
300
323
  };
301
324
  }
302
325
  if (!existsSync(lifecycleRoot)) {
303
- if (quiet) {
304
- return { code: 0, message: "", stream: "none", orphans: [] };
305
- }
306
326
  return {
307
327
  code: 0,
308
- message: "verify:orphan-active: no xbrief/ lifecycle root; nothing to scan.",
309
- stream: "stdout",
328
+ message: quiet ? "" : "verify:orphan-active: no xbrief/ lifecycle root; nothing to scan.",
329
+ stream: quiet ? "none" : "stdout",
310
330
  orphans: [],
331
+ basis: emptyBasis(budgetMs),
311
332
  };
312
333
  }
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;
334
+ const defaultRepo = resolveRepo(options.repo, root);
335
+ // Resolve the SCM binary lazily: an offline `--skip-gh` run or an empty
336
+ // active/ must not pay for (or fail on) binary resolution it never uses.
337
+ let runner = options.runGh === undefined ? null : { runGh: options.runGh, proxied: false };
338
+ const runGh = (cmd) => {
339
+ runner ??= makeGateRunner();
340
+ return runner.runGh(cmd);
341
+ };
342
+ const tally = new BasisTally();
343
+ const ctx = {
344
+ projectRoot: root,
345
+ runGh,
346
+ skipGh,
347
+ nowMs: clock(),
348
+ inventory: new OpenIssueInventory(runGh),
349
+ };
350
+ try {
351
+ // #3774: missing gh/ghx is config (code 2), not an uncaught throw.
352
+ const orphans = [];
353
+ let scanned = 0;
354
+ for (const brief of listActiveRunningBriefs(root)) {
355
+ scanned += 1;
356
+ const { issues, prs } = collectGithubRefs(brief.plan, defaultRepo);
357
+ // --issue N is one origin: briefs that name that issue. PR-only briefs stay on the unscoped scan (#3429).
358
+ if (issueFilter !== null && !briefReferencesIssue(issues, issueFilter)) {
359
+ continue;
360
+ }
361
+ if (issues.length === 0 && prs.length === 0) {
362
+ continue;
363
+ }
364
+ const assessment = assessOrphanSignature(issues, prs, ctx, issueFilter, tally);
365
+ if (assessment.orphaned && assessment.reason !== null && assessment.kind !== null) {
366
+ orphans.push({
367
+ path: relBriefPath(brief.path, root),
368
+ reason: assessment.reason,
369
+ kind: assessment.kind,
370
+ });
371
+ }
320
372
  }
321
- if (issues.length === 0 && prs.length === 0) {
322
- continue;
373
+ const basis = {
374
+ inventory: tally.inventory,
375
+ live: tally.live,
376
+ cache: tally.cache,
377
+ unverified: tally.unverified,
378
+ maxCacheAgeMs: tally.cache > 0 ? tally.maxCacheAgeMs : null,
379
+ proxied: runner?.proxied ?? false,
380
+ elapsedMs: Math.max(0, clock() - startedMs),
381
+ budgetMs,
382
+ };
383
+ if (orphans.length > 0) {
384
+ return {
385
+ code: 1,
386
+ message: formatRefusal(orphans, root, issueFilter, tally, basis),
387
+ stream: "stderr",
388
+ orphans,
389
+ basis,
390
+ };
323
391
  }
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
- });
392
+ if (quiet) {
393
+ return { code: 0, message: "", stream: "none", orphans: [], basis };
331
394
  }
332
- }
333
- if (orphans.length > 0) {
395
+ const issueNote = issueFilter === null ? "" : ` for issue #${issueFilter}`;
396
+ const headline = `verify:orphan-active: no orphaned active/running xBRIEFs${issueNote} ` +
397
+ `(scanned ${scanned} running brief${scanned === 1 ? "" : "s"} in active/).`;
334
398
  return {
335
- code: 1,
336
- message: formatRefusal(orphans, root, issueFilter),
337
- stream: "stderr",
338
- orphans,
399
+ code: 0,
400
+ message: [headline, ...basisLines(tally, basis)].join("\n"),
401
+ stream: "stdout",
402
+ orphans: [],
403
+ basis,
339
404
  };
340
405
  }
341
- if (quiet) {
342
- return { code: 0, message: "", stream: "none", orphans: [] };
406
+ catch (err) {
407
+ if (err instanceof ScmStubError) {
408
+ return {
409
+ code: 2,
410
+ message: `verify:orphan-active: ${err.message}`,
411
+ stream: "stderr",
412
+ orphans: [],
413
+ basis: emptyBasis(budgetMs),
414
+ };
415
+ }
416
+ throw err;
343
417
  }
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
418
  }
354
419
  //# sourceMappingURL=evaluate.js.map
@@ -1,3 +1,4 @@
1
1
  export * from "./evaluate.js";
2
+ export * from "./issue-state.js";
2
3
  export * from "./refs.js";
3
4
  //# sourceMappingURL=index.d.ts.map
@@ -1,3 +1,4 @@
1
1
  export * from "./evaluate.js";
2
+ export * from "./issue-state.js";
2
3
  export * from "./refs.js";
3
4
  //# 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