@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,5 +1,4 @@
1
- import { readFileSync } from "node:fs";
2
- import { atomicWriteProjectDefinition, projectDefinitionMutationLock, } from "../vbrief-build/project-definition-io.js";
1
+ import { withProjectDefinitionMutation } from "../vbrief-build/project-definition-mutation.js";
3
2
  import { valueFeedbackInstallForceOnSource } from "./org-force-on-migration.js";
4
3
  import { migrateLegacyPolicyKey, PLAN_POLICY_KEY, readPlanPolicy } from "./plan-extensions.js";
5
4
  import { policyColonInvocation } from "./policy-invocation.js";
@@ -219,14 +218,10 @@ export function enableValueFeedback(projectRoot, options) {
219
218
  changed: false,
220
219
  };
221
220
  }
222
- const path = projectDefinitionPath(projectRoot);
221
+ const _path = projectDefinitionPath(projectRoot);
223
222
  try {
224
- const { changed } = projectDefinitionMutationLock(projectRoot, () => {
225
- const parsed = JSON.parse(readFileSync(path, { encoding: "utf8" }));
226
- if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
227
- throw new Error(`PROJECT-DEFINITION at ${path} top-level value is not a JSON object`);
228
- }
229
- const data = parsed;
223
+ const { changed } = withProjectDefinitionMutation(projectRoot, (mutation) => {
224
+ const data = mutation.load();
230
225
  if (typeof data.plan !== "object" || data.plan === null || Array.isArray(data.plan)) {
231
226
  if (data.plan === undefined) {
232
227
  data.plan = {};
@@ -277,7 +272,7 @@ export function enableValueFeedback(projectRoot, options) {
277
272
  legacyKeyMigrated;
278
273
  policyBlock.valueFeedback = nextBlock;
279
274
  if (changedFlag) {
280
- atomicWriteProjectDefinition(path, data);
275
+ mutation.persist(data);
281
276
  }
282
277
  const actor = options.actor ?? policyColonInvocation("enable-value-feedback");
283
278
  const note = options.note ?? "";
@@ -317,14 +312,10 @@ export function enableValueFeedback(projectRoot, options) {
317
312
  * updates will not re-force against intentional opt-out.
318
313
  */
319
314
  export function clearValueFeedback(projectRoot, options = {}) {
320
- const path = projectDefinitionPath(projectRoot);
315
+ const _path = projectDefinitionPath(projectRoot);
321
316
  try {
322
- const { changed } = projectDefinitionMutationLock(projectRoot, () => {
323
- const parsed = JSON.parse(readFileSync(path, { encoding: "utf8" }));
324
- if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
325
- throw new Error(`PROJECT-DEFINITION at ${path} top-level value is not a JSON object`);
326
- }
327
- const data = parsed;
317
+ const { changed } = withProjectDefinitionMutation(projectRoot, (mutation) => {
318
+ const data = mutation.load();
328
319
  if (typeof data.plan !== "object" || data.plan === null || Array.isArray(data.plan)) {
329
320
  throw new Error("PROJECT-DEFINITION 'plan' is not an object");
330
321
  }
@@ -342,7 +333,7 @@ export function clearValueFeedback(projectRoot, options = {}) {
342
333
  }
343
334
  const previous = policyBlock.valueFeedback;
344
335
  delete policyBlock.valueFeedback;
345
- atomicWriteProjectDefinition(path, data);
336
+ mutation.persist(data);
346
337
  const actor = options.actor ?? policyColonInvocation("clear-value-feedback");
347
338
  const note = options.note ?? "";
348
339
  const parts = [
@@ -140,6 +140,9 @@ export function resolveWriteFence(projectPolicy, storyFileScope, options) {
140
140
  allowPaths,
141
141
  denyPaths,
142
142
  scopes,
143
+ // Carry the project setting through: resolving a write fence must not
144
+ // silently enable Shell dest-form enforcement for a story (#3438 / #3594).
145
+ shellDestForms: projectPolicy.shellDestForms,
143
146
  storyAllowPaths: storyAllow,
144
147
  fenceSources: sources,
145
148
  };
@@ -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,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).
@@ -2,12 +2,16 @@ import { execFileSync } from "node:child_process";
2
2
  import { resolveBinary } from "../scm/binary.js";
3
3
  import { SUBPROCESS_MAX_BUFFER } from "../subprocess/max-buffer.js";
4
4
  import { GH_TIMEOUT_S } from "./constants.js";
5
- /** UTF-8-safe gh capture via execFile (no shell) — mirrors _safe_subprocess.run_text (#1366). */
6
- export function defaultRunGh(cmd) {
5
+ /**
6
+ * UTF-8-safe gh capture via execFile (no shell) — mirrors _safe_subprocess.run_text (#1366).
7
+ * `binaryOverride` lets a caller pin the SCM binary (e.g. plain `gh` instead of
8
+ * the `ghx` cached proxy) when its guarantee depends on an uncached read (#3767).
9
+ */
10
+ export function defaultRunGh(cmd, binaryOverride) {
7
11
  if (cmd.length === 0 || cmd[0] !== "gh") {
8
12
  return { returncode: -1, stdout: "", stderr: "expected gh as first argv element" };
9
13
  }
10
- const binary = resolveBinary();
14
+ const binary = binaryOverride ?? resolveBinary();
11
15
  const args = cmd.slice(1);
12
16
  try {
13
17
  const stdout = execFileSync(binary, args, {
@@ -1,7 +1,7 @@
1
1
  import { type EnforceMergeApprovalHeadInput, type MergeApprovalHeadResult } from "../policy/merge-approval-head.js";
2
2
  import { type AgentMergeEvaluateResult } from "../policy/require-human-merge.js";
3
3
  import { type SemanticGreenFn } from "./semantic-green.js";
4
- import type { MergeFn, MonitorFn, ProtectedCheckFn, WaitMergeableResult } from "./types.js";
4
+ import type { CloseoutAttestableFn, MergeFn, MonitorFn, ProtectedCheckFn, WaitMergeableResult } from "./types.js";
5
5
  /** Post-merge umbrella checklist + current-shape refresh (#1649). */
6
6
  export type UmbrellaReconcileFn = (projectRoot: string, repo: string) => void;
7
7
  export interface WaitMergeableOptions {
@@ -32,6 +32,14 @@ export interface WaitMergeableOptions {
32
32
  readonly skipMergeApprovalHeadGate?: boolean;
33
33
  /** Inject head-bound approval enforcer (unit tests). */
34
34
  readonly mergeApprovalHeadFn?: (input: EnforceMergeApprovalHeadInput) => MergeApprovalHeadResult;
35
+ /**
36
+ * When true, skip the merge-time closeout attestability gate (#3781).
37
+ * Default: same as skipHumanMergeGate, matching the head-gate convention —
38
+ * test harnesses that skip human-merge also skip this unless they opt in.
39
+ */
40
+ readonly skipCloseoutAttestableGate?: boolean;
41
+ /** Inject the closeout attestability gate (unit tests). */
42
+ readonly closeoutAttestableFn?: CloseoutAttestableFn;
35
43
  /** Inject PR HEAD fetch (unit tests; defaults to REST `pulls/<N>`). */
36
44
  readonly fetchPrHeadShaFn?: (prNumber: number, repo: string | null) => string | null;
37
45
  /**
@@ -6,7 +6,7 @@ import { classifyMonitorOutcome, parseMonitorPayload } from "./classify.js";
6
6
  import { EXIT_CONFIG_ERROR, EXIT_MERGED, EXIT_TIMEOUT_OR_ESCALATION } from "./constants.js";
7
7
  import { makeResult } from "./result.js";
8
8
  import { evaluateSemanticGreen } from "./semantic-green.js";
9
- import { runGhMerge, runMonitor, runProtectedCheck } from "./wrappers.js";
9
+ import { runCloseoutAttestableCheck, runGhMerge, runMonitor, runProtectedCheck, } from "./wrappers.js";
10
10
  /** Node module-not-found / missing script — not a protected-issue overlap (#2667). */
11
11
  function isProtectedCheckConfigFailure(stderr) {
12
12
  const tail = stderr.trim();
@@ -228,6 +228,26 @@ export function waitMergeableAndMerge(prNumber, repo, options) {
228
228
  }
229
229
  }
230
230
  }
231
+ // #3781: last gate before the merge call. A PR may leave a brief unattested; it
232
+ // may not merge one whose issue it closes in the same act. Keyed on the PR's
233
+ // closing references, so it fires even when the brief is not in the branch diff.
234
+ const skipCloseoutGate = options.skipCloseoutAttestableGate === true ||
235
+ (options.skipCloseoutAttestableGate === undefined && options.skipHumanMergeGate === true);
236
+ if (!skipCloseoutGate) {
237
+ const closeoutFn = options.closeoutAttestableFn ?? runCloseoutAttestableCheck;
238
+ const [coRc, , coStderr] = closeoutFn(prNumber, repo, projectRoot);
239
+ if (coRc !== 0) {
240
+ return makeResult({
241
+ prNumber,
242
+ repo,
243
+ outcome: coRc === 1 ? "closeout-unattested" : "config-error",
244
+ exitCode: coRc === 1 ? EXIT_TIMEOUT_OR_ESCALATION : EXIT_CONFIG_ERROR,
245
+ monitorResult: monitorPayload,
246
+ protectedCheck: protectedCheckPayload,
247
+ error: coStderr.trim(),
248
+ });
249
+ }
250
+ }
231
251
  const [mergeRc, mergeStdout, mergeStderr] = mergeFn(prNumber, repo, {
232
252
  matchHeadCommit,
233
253
  });
@@ -18,4 +18,6 @@ export type MonitorFn = (prNumber: number, repo: string, capMinutes: number, opt
18
18
  export type MergeFn = (prNumber: number, repo: string | null, options?: {
19
19
  readonly matchHeadCommit?: string | null;
20
20
  }) => SubprocessTriple;
21
+ /** Merge-time closeout attestability gate (#3781): 0 attestable / 1 unattested / 2 lookup. */
22
+ export type CloseoutAttestableFn = (prNumber: number, repo: string | null, projectRoot: string) => SubprocessTriple;
21
23
  //# sourceMappingURL=types.d.ts.map
@@ -32,6 +32,12 @@ export interface RunProtectedCheckOptions {
32
32
  }
33
33
  /** Invoke pr-protected-issues CLI and return (returncode, stdout, stderr). */
34
34
  export declare function runProtectedCheck(prNumber: number, repo: string | null, protectedIssues: readonly number[], options?: RunProtectedCheckOptions): SubprocessTriple;
35
+ /**
36
+ * Merge-time closeout attestability gate (#3781). In-process: the evaluator lives
37
+ * in this package, and a subprocess hop would only add a script-path failure mode
38
+ * to a gate that must fail closed.
39
+ */
40
+ export declare function runCloseoutAttestableCheck(prNumber: number, repo: string | null, projectRoot: string): SubprocessTriple;
35
41
  export interface RunMonitorOptions {
36
42
  readonly nodeExecutable?: string;
37
43
  readonly timeout?: number;
@@ -3,6 +3,7 @@ import { existsSync } from "node:fs";
3
3
  import { createRequire } from "node:module";
4
4
  import { dirname, resolve } from "node:path";
5
5
  import { fileURLToPath } from "node:url";
6
+ import { evaluate as evaluateCloseoutAttestable } from "../pr-closeout-attestable/evaluate.js";
6
7
  import { resolveBinary } from "../scm/binary.js";
7
8
  import { SUBPROCESS_MAX_BUFFER } from "../subprocess/max-buffer.js";
8
9
  /** UTF-8-safe subprocess capture via spawnSync (no shell) — mirrors #1366. */
@@ -97,6 +98,16 @@ export function runProtectedCheck(prNumber, repo, protectedIssues, options = {})
97
98
  });
98
99
  return [result.returncode, result.stdout, result.stderr];
99
100
  }
101
+ /**
102
+ * Merge-time closeout attestability gate (#3781). In-process: the evaluator lives
103
+ * in this package, and a subprocess hop would only add a script-path failure mode
104
+ * to a gate that must fail closed.
105
+ */
106
+ export function runCloseoutAttestableCheck(prNumber, repo, projectRoot) {
107
+ const result = evaluateCloseoutAttestable(projectRoot, prNumber, { repo });
108
+ const text = result.message.length > 0 ? `${result.message}\n` : "";
109
+ return result.code === 0 ? [0, text, ""] : [result.code, "", text];
110
+ }
100
111
  /** Invoke pr-monitor CLI with --json and return (returncode, stdout, stderr). */
101
112
  export function runMonitor(prNumber, repo, capMinutes, options = {}) {
102
113
  const scriptPath = cliScriptPath("pr-monitor");
@@ -6,6 +6,19 @@ export declare const ACTIVE_FOLDER = "active";
6
6
  export declare const ELIGIBLE_STATUS = "running";
7
7
  /** Actionable redirect appended to every reject path (#810 / #2449). */
8
8
  export declare const ACTIVATE_HINT = "Run `task scope:activate -- {path}` (or legacy `task vbrief:activate -- {path}`) before spawning an implementation agent.";
9
+ /**
10
+ * Recovery for the origin-freshness reject (#3828).
11
+ *
12
+ * Freshness is only reached after the active/ + running checks pass, so
13
+ * `ACTIVATE_HINT` names a transition that cannot apply here: `activate` accepts
14
+ * `pending/` alone and fails with `Invalid transition: 'activate' requires file
15
+ * in pending/`. `block` -> `unblock` is in place (`targetFolder: null`), requires
16
+ * and restores `running`, and stamps `plan.updated` plus `xBRIEFInfo.updated`,
17
+ * the field this reject compares. It is interim: the pair also records a
18
+ * `blocked` -> `running` transition for a brief that was never blocked, a
19
+ * tradeoff accepted knowingly. #3857 owns the dedicated acknowledge verb.
20
+ */
21
+ export declare const ORIGIN_FRESHNESS_HINT: string;
9
22
  /** Lifecycle folder names eligible for implementation (#810). */
10
23
  export declare const ELIGIBLE_LIFECYCLE_DIRS: readonly ["xbrief/active", "vbrief/active"];
11
24
  export declare const PREFLIGHT_USAGE_HINT = "Expected: `task xbrief:preflight -- xbrief/active/<story>.xbrief.json` (legacy: `task vbrief:preflight -- <path>`).";
@@ -32,6 +45,8 @@ export interface EvaluateOptions {
32
45
  }
33
46
  /** Substitute `{path}` without `$`-pattern expansion in user paths (#1721). */
34
47
  export declare function formatActivateHint(path: string): string;
48
+ /** Substitute every `{path}` without `$`-pattern expansion in user paths (#1721). */
49
+ export declare function formatOriginFreshnessHint(path: string): string;
35
50
  /**
36
51
  * Pure evaluator — returns `{ exitCode, message }`. Never throws; every error
37
52
  * path collapses to exit 1 with an actionable message. Faithful to