@deftai/directive-core 0.108.0 → 0.109.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (138) hide show
  1. package/dist/cache/operations.js +1 -1
  2. package/dist/check/gate-lists.d.ts +14 -0
  3. package/dist/check/gate-lists.js +26 -3
  4. package/dist/check/session-completed-ac.d.ts +1 -1
  5. package/dist/check/session-completed-ac.js +1 -1
  6. package/dist/consumer-check-contract/evaluate.d.ts +47 -0
  7. package/dist/consumer-check-contract/evaluate.js +181 -15
  8. package/dist/delivery-attempt/evaluate.d.ts +9 -1
  9. package/dist/delivery-attempt/evaluate.js +69 -0
  10. package/dist/delivery-attempt/index.d.ts +1 -1
  11. package/dist/delivery-attempt/index.js +1 -1
  12. package/dist/deposit/live-procedure-exclusions.d.ts +18 -0
  13. package/dist/deposit/live-procedure-exclusions.js +110 -0
  14. package/dist/deposit/live-procedure-targets.d.ts +45 -0
  15. package/dist/deposit/live-procedure-targets.js +274 -0
  16. package/dist/deposit/python-free.d.ts +6 -0
  17. package/dist/deposit/python-free.js +15 -0
  18. package/dist/deposit/rewrite-deposit-links.d.ts +42 -0
  19. package/dist/deposit/rewrite-deposit-links.js +148 -0
  20. package/dist/deposit/run-stage-content-pack.d.ts +2 -0
  21. package/dist/deposit/run-stage-content-pack.js +3 -0
  22. package/dist/deposit/stage-content-pack.d.ts +17 -0
  23. package/dist/deposit/stage-content-pack.js +91 -0
  24. package/dist/design-critique/citation-grammar.d.ts +7 -0
  25. package/dist/design-critique/citation-grammar.js +1 -1
  26. package/dist/design-critique/completed-arc-record.d.ts +1 -1
  27. package/dist/design-critique/completed-arc-record.js +59 -14
  28. package/dist/hooks/classify/host-session-identity.d.ts +24 -11
  29. package/dist/hooks/classify/host-session-identity.js +46 -49
  30. package/dist/hooks/classify/index.d.ts +2 -2
  31. package/dist/hooks/classify/index.js +2 -2
  32. package/dist/hooks/classify/paths.d.ts +2 -0
  33. package/dist/hooks/classify/paths.js +8 -4
  34. package/dist/hooks/classify/stdin.js +69 -3
  35. package/dist/hooks/dest-form.d.ts +20 -1
  36. package/dist/hooks/dest-form.js +158 -21
  37. package/dist/hooks/dispatcher.d.ts +6 -2
  38. package/dist/hooks/dispatcher.js +258 -62
  39. package/dist/hooks/fixtures/cases.d.ts +6 -0
  40. package/dist/hooks/fixtures/cases.js +53 -0
  41. package/dist/hooks/git-destructive-log.d.ts +32 -0
  42. package/dist/hooks/git-destructive-log.js +46 -0
  43. package/dist/hooks/index.d.ts +2 -0
  44. package/dist/hooks/index.js +2 -0
  45. package/dist/hooks/shell-write-targets.d.ts +10 -0
  46. package/dist/hooks/shell-write-targets.js +274 -0
  47. package/dist/hooks/tools.d.ts +20 -2
  48. package/dist/hooks/tools.js +30 -1
  49. package/dist/init-deposit/gitignore.d.ts +7 -0
  50. package/dist/init-deposit/gitignore.js +24 -0
  51. package/dist/init-deposit/init-deposit.js +3 -0
  52. package/dist/init-deposit/refresh.js +7 -0
  53. package/dist/init-deposit/runtime-writers.d.ts +14 -0
  54. package/dist/init-deposit/runtime-writers.js +33 -0
  55. package/dist/intake/issue-ingest.d.ts +6 -1
  56. package/dist/intake/issue-ingest.js +21 -2
  57. package/dist/intake/reconcile-issues.js +13 -13
  58. package/dist/lifecycle/brief-envelope.d.ts +25 -0
  59. package/dist/lifecycle/brief-envelope.js +42 -0
  60. package/dist/lifecycle/index.d.ts +1 -0
  61. package/dist/lifecycle/index.js +1 -0
  62. package/dist/literal-acceptance/evaluate.js +14 -5
  63. package/dist/literal-acceptance/index.d.ts +1 -1
  64. package/dist/literal-acceptance/index.js +1 -1
  65. package/dist/literal-acceptance/run.d.ts +2 -0
  66. package/dist/literal-acceptance/run.js +19 -1
  67. package/dist/orphan-active/candidate-scope.d.ts +53 -0
  68. package/dist/orphan-active/candidate-scope.js +157 -0
  69. package/dist/orphan-active/evaluate.d.ts +27 -0
  70. package/dist/orphan-active/evaluate.js +63 -6
  71. package/dist/orphan-active/index.d.ts +1 -0
  72. package/dist/orphan-active/index.js +1 -0
  73. package/dist/policy/merge-approval-head.js +7 -6
  74. package/dist/pr-closing-keywords/gh.js +32 -9
  75. package/dist/pr-closing-keywords/main.d.ts +1 -0
  76. package/dist/pr-closing-keywords/main.js +68 -3
  77. package/dist/pr-closing-keywords/types.d.ts +2 -0
  78. package/dist/pr-merge-readiness/gh.js +32 -9
  79. package/dist/pr-protected-issues/gh.js +36 -9
  80. package/dist/pr-wait-mergeable/wrappers.js +3 -3
  81. package/dist/product-first-done-gate/evaluate.js +6 -2
  82. package/dist/product-first-done-gate/types.js +2 -0
  83. package/dist/release/consumer-hard-stops.d.ts +48 -0
  84. package/dist/release/consumer-hard-stops.js +140 -0
  85. package/dist/release/consumer-readiness-disclosure.d.ts +18 -0
  86. package/dist/release/consumer-readiness-disclosure.js +51 -0
  87. package/dist/release/index.d.ts +2 -0
  88. package/dist/release/index.js +2 -0
  89. package/dist/release/pipeline.js +55 -0
  90. package/dist/release/run-consumer-readiness.d.ts +15 -0
  91. package/dist/release/run-consumer-readiness.js +24 -0
  92. package/dist/release/types.d.ts +6 -0
  93. package/dist/review-monitor/github-lease.js +4 -3
  94. package/dist/run-summary/types.d.ts +2 -2
  95. package/dist/scm/build-command.d.ts +2 -2
  96. package/dist/scm/build-command.js +2 -2
  97. package/dist/scm/call-shape.d.ts +25 -0
  98. package/dist/scm/call-shape.js +59 -0
  99. package/dist/scm/call.d.ts +7 -4
  100. package/dist/scm/call.js +41 -11
  101. package/dist/scm/gh-rest.d.ts +25 -12
  102. package/dist/scm/gh-rest.js +65 -15
  103. package/dist/scm/index.d.ts +2 -0
  104. package/dist/scm/index.js +2 -0
  105. package/dist/scm/spawn-status.d.ts +33 -0
  106. package/dist/scm/spawn-status.js +53 -0
  107. package/dist/scope/acceptance-evidence.d.ts +1 -1
  108. package/dist/scope/transition.js +4 -14
  109. package/dist/session/ac-pass-banking.d.ts +2 -2
  110. package/dist/session/ac-pass-banking.js +2 -2
  111. package/dist/session/host-session-owner.d.ts +53 -0
  112. package/dist/session/host-session-owner.js +89 -0
  113. package/dist/session/index.d.ts +1 -0
  114. package/dist/session/index.js +1 -0
  115. package/dist/session/occupancy.d.ts +21 -1
  116. package/dist/session/occupancy.js +70 -12
  117. package/dist/session/verify-ac-session-cache.d.ts +2 -2
  118. package/dist/session/verify-ac-session-cache.js +2 -2
  119. package/dist/triage/evaluate/worktrees.js +153 -6
  120. package/dist/umbrella-current-shape/index.d.ts +51 -3
  121. package/dist/umbrella-current-shape/index.js +106 -18
  122. package/dist/validate-content/deposit-required.d.ts +39 -0
  123. package/dist/validate-content/deposit-required.js +147 -0
  124. package/dist/validate-content/index.d.ts +1 -0
  125. package/dist/validate-content/index.js +1 -0
  126. package/dist/validate-content/validate-links.d.ts +2 -3
  127. package/dist/validate-content/validate-links.js +29 -2
  128. package/dist/vbrief-activate/activate.d.ts +7 -2
  129. package/dist/vbrief-activate/activate.js +26 -13
  130. package/dist/verify-ac/evaluate.d.ts +9 -0
  131. package/dist/verify-ac/evaluate.js +32 -9
  132. package/dist/verify-source/deposit-closure.d.ts +23 -0
  133. package/dist/verify-source/deposit-closure.js +162 -0
  134. package/dist/verify-source/index.d.ts +2 -0
  135. package/dist/verify-source/index.js +2 -0
  136. package/dist/verify-source/semantic-single-source.d.ts +36 -0
  137. package/dist/verify-source/semantic-single-source.js +349 -0
  138. package/package.json +3 -3
@@ -3,6 +3,7 @@ import { join, relative, resolve } from "node:path";
3
3
  import { hasArtifactSuffix, resolveLifecycleRoot } from "../layout/resolve.js";
4
4
  import { ScmStubError } from "../scm/errors.js";
5
5
  import { resolveRepo } from "../triage/queue/repo.js";
6
+ import { normalizeScopePath, resolveCandidateScope, } from "./candidate-scope.js";
6
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) {
@@ -220,7 +221,34 @@ function basisLines(tally, basis) {
220
221
  }
221
222
  return lines;
222
223
  }
223
- function formatRefusal(orphans, projectRoot, issueFilter, tally, basis) {
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) {
224
252
  const shippedOrphans = orphans.filter((orphan) => orphan.kind === "shipped");
225
253
  const unresolvedOrphans = orphans.filter((orphan) => orphan.kind === "unresolved");
226
254
  const noun = orphans.length === 1 ? "" : "s";
@@ -243,6 +271,8 @@ function formatRefusal(orphans, projectRoot, issueFilter, tally, basis) {
243
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.");
244
272
  }
245
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).
246
276
  const retry = issueFilter === null
247
277
  ? "task verify:orphan-active"
248
278
  : `task verify:orphan-active -- --issue ${issueFilter}`;
@@ -252,7 +282,7 @@ function formatRefusal(orphans, projectRoot, issueFilter, tally, basis) {
252
282
  for (const orphan of orphans) {
253
283
  lines.push(` - ${orphan.path} (${orphan.reason})`);
254
284
  }
255
- lines.push(...basisLines(tally, basis));
285
+ lines.push(...basisLines(tally, basis), ...scopeLines(scope, skipped));
256
286
  return lines.join("\n");
257
287
  }
258
288
  function emptyBasis(budgetMs) {
@@ -279,6 +309,11 @@ function emptyBasis(budgetMs) {
279
309
  * one complete, fail-closed open-issue inventory and stays fail-open on
280
310
  * unknown so offline work is not network-authorized. A cache hit is honoured
281
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.
282
317
  */
283
318
  export function evaluate(projectRoot, options = {}) {
284
319
  const root = resolve(projectRoot);
@@ -295,6 +330,7 @@ export function evaluate(projectRoot, options = {}) {
295
330
  stream: "stderr",
296
331
  orphans: [],
297
332
  basis: emptyBasis(budgetMs),
333
+ scope: null,
298
334
  };
299
335
  }
300
336
  let lifecycleRoot;
@@ -312,6 +348,7 @@ export function evaluate(projectRoot, options = {}) {
312
348
  stream: quiet ? "none" : "stdout",
313
349
  orphans: [],
314
350
  basis: emptyBasis(budgetMs),
351
+ scope: null,
315
352
  };
316
353
  }
317
354
  return {
@@ -320,6 +357,7 @@ export function evaluate(projectRoot, options = {}) {
320
357
  stream: "stderr",
321
358
  orphans: [],
322
359
  basis: emptyBasis(budgetMs),
360
+ scope: null,
323
361
  };
324
362
  }
325
363
  if (!existsSync(lifecycleRoot)) {
@@ -329,6 +367,7 @@ export function evaluate(projectRoot, options = {}) {
329
367
  stream: quiet ? "none" : "stdout",
330
368
  orphans: [],
331
369
  basis: emptyBasis(budgetMs),
370
+ scope: null,
332
371
  };
333
372
  }
334
373
  const defaultRepo = resolveRepo(options.repo, root);
@@ -349,9 +388,23 @@ export function evaluate(projectRoot, options = {}) {
349
388
  };
350
389
  try {
351
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;
352
400
  const orphans = [];
353
401
  let scanned = 0;
354
- for (const brief of listActiveRunningBriefs(root)) {
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
+ }
355
408
  scanned += 1;
356
409
  const { issues, prs } = collectGithubRefs(brief.plan, defaultRepo);
357
410
  // --issue N is one origin: briefs that name that issue. PR-only briefs stay on the unscoped scan (#3429).
@@ -380,27 +433,30 @@ export function evaluate(projectRoot, options = {}) {
380
433
  elapsedMs: Math.max(0, clock() - startedMs),
381
434
  budgetMs,
382
435
  };
436
+ const scopeSummary = summarizeScope(scope, skipped);
383
437
  if (orphans.length > 0) {
384
438
  return {
385
439
  code: 1,
386
- message: formatRefusal(orphans, root, issueFilter, tally, basis),
440
+ message: formatRefusal(orphans, root, issueFilter, tally, basis, scope, skipped),
387
441
  stream: "stderr",
388
442
  orphans,
389
443
  basis,
444
+ scope: scopeSummary,
390
445
  };
391
446
  }
392
447
  if (quiet) {
393
- return { code: 0, message: "", stream: "none", orphans: [], basis };
448
+ return { code: 0, message: "", stream: "none", orphans: [], basis, scope: scopeSummary };
394
449
  }
395
450
  const issueNote = issueFilter === null ? "" : ` for issue #${issueFilter}`;
396
451
  const headline = `verify:orphan-active: no orphaned active/running xBRIEFs${issueNote} ` +
397
452
  `(scanned ${scanned} running brief${scanned === 1 ? "" : "s"} in active/).`;
398
453
  return {
399
454
  code: 0,
400
- message: [headline, ...basisLines(tally, basis)].join("\n"),
455
+ message: [headline, ...basisLines(tally, basis), ...scopeLines(scope, skipped)].join("\n"),
401
456
  stream: "stdout",
402
457
  orphans: [],
403
458
  basis,
459
+ scope: scopeSummary,
404
460
  };
405
461
  }
406
462
  catch (err) {
@@ -411,6 +467,7 @@ export function evaluate(projectRoot, options = {}) {
411
467
  stream: "stderr",
412
468
  orphans: [],
413
469
  basis: emptyBasis(budgetMs),
470
+ scope: null,
414
471
  };
415
472
  }
416
473
  throw err;
@@ -1,3 +1,4 @@
1
+ export * from "./candidate-scope.js";
1
2
  export * from "./evaluate.js";
2
3
  export * from "./issue-state.js";
3
4
  export * from "./refs.js";
@@ -1,3 +1,4 @@
1
+ export * from "./candidate-scope.js";
1
2
  export * from "./evaluate.js";
2
3
  export * from "./issue-state.js";
3
4
  export * from "./refs.js";
@@ -13,7 +13,7 @@
13
13
  import { join } from "node:path";
14
14
  import { DEFAULT_EVENT_LOG, readEvents } from "../lifecycle/events.js";
15
15
  import { captureExec } from "../pr-wait-mergeable/wrappers.js";
16
- import { resolveBinary } from "../scm/binary.js";
16
+ import { resolveBinaryForArgv } from "../scm/call-shape.js";
17
17
  import { resolveHumanMergePolicy } from "./require-human-merge.js";
18
18
  /** Event name recorded at Phase 5 → 6 approval. */
19
19
  export const PLAN_APPROVED_EVENT = "plan:approved";
@@ -358,14 +358,14 @@ export function evaluateMergeApprovalHead(input) {
358
358
  }
359
359
  /** Live `gh pr merge <N> --disable-auto` (best-effort). */
360
360
  export function disablePullRequestAutoMerge(prNumber, repo, options = {}) {
361
+ const args = ["pr", "merge", String(prNumber), "--disable-auto"];
361
362
  let binary;
362
363
  try {
363
- binary = resolveBinary();
364
+ binary = resolveBinaryForArgv("pr", args.slice(1));
364
365
  }
365
366
  catch {
366
367
  return { ok: false, stderr: "gh CLI not found" };
367
368
  }
368
- const args = ["pr", "merge", String(prNumber), "--disable-auto"];
369
369
  if (repo !== null && repo.length > 0) {
370
370
  args.push("--repo", repo);
371
371
  }
@@ -388,15 +388,16 @@ export function disablePullRequestAutoMerge(prNumber, repo, options = {}) {
388
388
  export function fetchPrHeadShaRest(prNumber, repo, options = {}) {
389
389
  if (repo === null || repo.length === 0)
390
390
  return null;
391
+ const endpoint = `repos/${repo}/pulls/${prNumber}`;
392
+ const apiArgs = ["api", endpoint, "--jq", ".head.sha"];
391
393
  let binary;
392
394
  try {
393
- binary = resolveBinary();
395
+ binary = resolveBinaryForArgv(apiArgs[0] ?? "api", apiArgs.slice(1));
394
396
  }
395
397
  catch {
396
398
  return null;
397
399
  }
398
- const endpoint = `repos/${repo}/pulls/${prNumber}`;
399
- const result = captureExec(binary, ["api", endpoint, "--jq", ".head.sha"], (options.timeoutSec ?? 60) * 1000);
400
+ const result = captureExec(binary, apiArgs, (options.timeoutSec ?? 60) * 1000);
400
401
  if (result.returncode !== 0)
401
402
  return null;
402
403
  return normalizeSha(result.stdout.trim());
@@ -1,5 +1,7 @@
1
1
  import { execFileSync } from "node:child_process";
2
- import { resolveBinary } from "../scm/binary.js";
2
+ import { defaultWhich } from "../scm/binary.js";
3
+ import { classifyScmArgv, resolveBinaryForArgv } from "../scm/call-shape.js";
4
+ import { ghxSpawnFallbackBinary } from "../scm/spawn-status.js";
3
5
  import { SUBPROCESS_MAX_BUFFER } from "../subprocess/max-buffer.js";
4
6
  import { GH_TIMEOUT_S } from "./constants.js";
5
7
  /** UTF-8-safe gh capture via execFile (no shell) — mirrors _safe_subprocess.run_text (#1366). */
@@ -7,18 +9,39 @@ export function defaultRunGh(cmd) {
7
9
  if (cmd.length === 0 || cmd[0] !== "gh") {
8
10
  return { returncode: -1, stdout: "", stderr: "expected gh as first argv element" };
9
11
  }
10
- const binary = resolveBinary();
11
12
  const args = cmd.slice(1);
13
+ const binary = resolveBinaryForArgv(args[0] ?? "", args.slice(1));
14
+ const execOpts = {
15
+ encoding: "utf8",
16
+ timeout: GH_TIMEOUT_S * 1000,
17
+ stdio: ["ignore", "pipe", "pipe"],
18
+ maxBuffer: SUBPROCESS_MAX_BUFFER,
19
+ };
20
+ const runOnce = (bin) => execFileSync(bin, args, execOpts);
12
21
  try {
13
- const stdout = execFileSync(binary, args, {
14
- encoding: "utf8",
15
- timeout: GH_TIMEOUT_S * 1000,
16
- stdio: ["ignore", "pipe", "pipe"],
17
- maxBuffer: SUBPROCESS_MAX_BUFFER,
18
- });
22
+ const stdout = runOnce(binary);
19
23
  return { returncode: 0, stdout: typeof stdout === "string" ? stdout : "", stderr: "" };
20
24
  }
21
- catch (err) {
25
+ catch (caught) {
26
+ let err = caught;
27
+ const first = err;
28
+ const retry = ghxSpawnFallbackBinary(classifyScmArgv(args[0] ?? "", args.slice(1)), binary, defaultWhich, {
29
+ status: typeof first.status === "number" ? first.status : null,
30
+ error: first.code === "ENOENT" || first.code === "ETIMEDOUT" || typeof first.status !== "number"
31
+ ? { code: first.code, message: first.message }
32
+ : undefined,
33
+ stdout: typeof first.stdout === "string" ? first.stdout : "",
34
+ stderr: typeof first.stderr === "string" ? first.stderr : "",
35
+ });
36
+ if (retry !== null) {
37
+ try {
38
+ const stdout = runOnce(retry);
39
+ return { returncode: 0, stdout: typeof stdout === "string" ? stdout : "", stderr: "" };
40
+ }
41
+ catch (retryErr) {
42
+ err = retryErr;
43
+ }
44
+ }
22
45
  const e = err;
23
46
  if (e.code === "ENOENT") {
24
47
  return { returncode: -1, stdout: "", stderr: "gh CLI not found. Install GitHub CLI." };
@@ -3,6 +3,7 @@ export declare function parseAllowList(values: readonly string[]): Set<number>;
3
3
  export declare function parseArgs(argv: readonly string[]): ParsedArgs;
4
4
  export interface RunOptions {
5
5
  readonly runGh?: RunGhFn;
6
+ readonly runGit?: RunGhFn;
6
7
  }
7
8
  export declare function run(argv: readonly string[], options?: RunOptions): number;
8
9
  export declare function cmdPrCheckClosingKeywords(argv: readonly string[], options?: RunOptions): number;
@@ -1,3 +1,5 @@
1
+ import { execFileSync } from "node:child_process";
2
+ import { SUBPROCESS_MAX_BUFFER } from "../subprocess/max-buffer.js";
1
3
  import { EXIT_CONFIG_ERROR, EXIT_HITS_FOUND, EXIT_OK } from "./constants.js";
2
4
  import { findAllClosingKeywordHits, findHits, renderHit } from "./detect.js";
3
5
  import { defaultRunGh, fetchPrBody, fetchPrCommitMessages } from "./gh.js";
@@ -23,6 +25,7 @@ function emptyParsed(error) {
23
25
  pr: null,
24
26
  bodyFile: null,
25
27
  commitsFile: null,
28
+ fromGitRange: null,
26
29
  repo: null,
27
30
  allowKnownFalsePositives: [],
28
31
  allowClose: [],
@@ -34,6 +37,7 @@ export function parseArgs(argv) {
34
37
  let pr = null;
35
38
  let bodyFile = null;
36
39
  let commitsFile = null;
40
+ let fromGitRange = null;
37
41
  let repo = null;
38
42
  let mode = "both";
39
43
  const allowKnownFalsePositives = [];
@@ -82,6 +86,17 @@ export function parseArgs(argv) {
82
86
  else if (arg?.startsWith("--commits-file=")) {
83
87
  commitsFile = arg.slice("--commits-file=".length);
84
88
  }
89
+ else if (arg === "--from-git-range") {
90
+ const value = argv[i + 1];
91
+ if (value === undefined) {
92
+ return emptyParsed("argument --from-git-range: expected one argument");
93
+ }
94
+ fromGitRange = value;
95
+ i += 1;
96
+ }
97
+ else if (arg?.startsWith("--from-git-range=")) {
98
+ fromGitRange = arg.slice("--from-git-range=".length);
99
+ }
85
100
  else if (arg === "--repo") {
86
101
  const value = argv[i + 1];
87
102
  if (value === undefined) {
@@ -140,7 +155,50 @@ export function parseArgs(argv) {
140
155
  return emptyParsed(`unrecognized arguments: ${arg}`);
141
156
  }
142
157
  }
143
- return { pr, bodyFile, commitsFile, repo, allowKnownFalsePositives, allowClose, mode };
158
+ return {
159
+ pr,
160
+ bodyFile,
161
+ commitsFile,
162
+ fromGitRange,
163
+ repo,
164
+ allowKnownFalsePositives,
165
+ allowClose,
166
+ mode,
167
+ };
168
+ }
169
+ function defaultRunGit(cmd) {
170
+ try {
171
+ const stdout = execFileSync("git", [...cmd], {
172
+ encoding: "utf8",
173
+ timeout: 30_000,
174
+ stdio: ["ignore", "pipe", "pipe"],
175
+ maxBuffer: SUBPROCESS_MAX_BUFFER,
176
+ });
177
+ return { returncode: 0, stdout: typeof stdout === "string" ? stdout : "", stderr: "" };
178
+ }
179
+ catch (err) {
180
+ const e = err;
181
+ return {
182
+ returncode: typeof e.status === "number" ? e.status : 1,
183
+ stdout: typeof e.stdout === "string" ? e.stdout : "",
184
+ stderr: typeof e.stderr === "string" ? e.stderr : String(e.message ?? ""),
185
+ };
186
+ }
187
+ }
188
+ function readGitRange(range, runGit) {
189
+ const result = runGit(["log", range, "--format=%B%n--END--"]);
190
+ if (result.returncode !== 0) {
191
+ process.stderr.write("Error: git log " +
192
+ range +
193
+ " failed: " +
194
+ result.stderr.trim() +
195
+ ". Recovery: git fetch origin master.\n");
196
+ return null;
197
+ }
198
+ return result.stdout
199
+ .split("\n--END--\n")
200
+ .map((part) => part.trim())
201
+ .filter((part) => part.length > 0);
144
202
  }
145
203
  function filterHits(hits, allowList) {
146
204
  return hits.filter((h) => !allowList.has(h.issueNumber));
@@ -220,8 +278,8 @@ export function run(argv, options = {}) {
220
278
  commitMessages = msgs;
221
279
  }
222
280
  else {
223
- if (args.bodyFile === null && args.commitsFile === null) {
224
- process.stderr.write("Error: must specify --pr OR --body-file / --commits-file.\n");
281
+ if (args.bodyFile === null && args.commitsFile === null && args.fromGitRange === null) {
282
+ process.stderr.write("Error: must specify --pr OR --body-file / --commits-file / --from-git-range.\n");
225
283
  return EXIT_CONFIG_ERROR;
226
284
  }
227
285
  if (args.bodyFile !== null) {
@@ -238,6 +296,13 @@ export function run(argv, options = {}) {
238
296
  }
239
297
  commitMessages = msgs;
240
298
  }
299
+ if (args.fromGitRange !== null) {
300
+ const msgs = readGitRange(args.fromGitRange, options.runGit ?? defaultRunGit);
301
+ if (msgs === null) {
302
+ return EXIT_CONFIG_ERROR;
303
+ }
304
+ commitMessages = [...commitMessages, ...msgs];
305
+ }
241
306
  }
242
307
  const runFp = args.mode === "fp" || args.mode === "both";
243
308
  const runIntent = args.mode === "intent" || args.mode === "both";
@@ -18,6 +18,8 @@ export interface ParsedArgs {
18
18
  readonly pr: number | null;
19
19
  readonly bodyFile: string | null;
20
20
  readonly commitsFile: string | null;
21
+ /** Offline git range (e.g. origin/master..HEAD) for check-graph wiring (#3969). */
22
+ readonly fromGitRange: string | null;
21
23
  readonly repo: string | null;
22
24
  readonly allowKnownFalsePositives: readonly string[];
23
25
  /** Intent-mode allowlist of issue numbers permitted to use real closing keywords (#3015). */
@@ -1,5 +1,7 @@
1
1
  import { execFileSync } from "node:child_process";
2
- import { resolveBinary } from "../scm/binary.js";
2
+ import { defaultWhich } from "../scm/binary.js";
3
+ import { classifyScmArgv, resolveBinaryForArgv } from "../scm/call-shape.js";
4
+ import { ghxSpawnFallbackBinary } from "../scm/spawn-status.js";
3
5
  import { SUBPROCESS_MAX_BUFFER } from "../subprocess/max-buffer.js";
4
6
  import { GH_TIMEOUT_S, GREPTILE_LOGIN } from "./constants.js";
5
7
  /** Display / inventory key for a required context (includes app binding when set). */
@@ -47,18 +49,39 @@ export function defaultRunGh(cmd) {
47
49
  if (cmd.length === 0 || cmd[0] !== "gh") {
48
50
  return { returncode: -1, stdout: "", stderr: "expected gh as first argv element" };
49
51
  }
50
- const binary = resolveBinary();
51
52
  const args = cmd.slice(1);
53
+ const binary = resolveBinaryForArgv(args[0] ?? "", args.slice(1));
54
+ const execOpts = {
55
+ encoding: "utf8",
56
+ timeout: GH_TIMEOUT_S * 1000,
57
+ stdio: ["ignore", "pipe", "pipe"],
58
+ maxBuffer: SUBPROCESS_MAX_BUFFER,
59
+ };
60
+ const runOnce = (bin) => execFileSync(bin, args, execOpts);
52
61
  try {
53
- const stdout = execFileSync(binary, args, {
54
- encoding: "utf8",
55
- timeout: GH_TIMEOUT_S * 1000,
56
- stdio: ["ignore", "pipe", "pipe"],
57
- maxBuffer: SUBPROCESS_MAX_BUFFER,
58
- });
62
+ const stdout = runOnce(binary);
59
63
  return { returncode: 0, stdout: typeof stdout === "string" ? stdout : "", stderr: "" };
60
64
  }
61
- catch (err) {
65
+ catch (caught) {
66
+ let err = caught;
67
+ const first = err;
68
+ const retry = ghxSpawnFallbackBinary(classifyScmArgv(args[0] ?? "", args.slice(1)), binary, defaultWhich, {
69
+ status: typeof first.status === "number" ? first.status : null,
70
+ error: first.code === "ENOENT" || first.code === "ETIMEDOUT" || typeof first.status !== "number"
71
+ ? { code: first.code, message: first.message }
72
+ : undefined,
73
+ stdout: typeof first.stdout === "string" ? first.stdout : "",
74
+ stderr: typeof first.stderr === "string" ? first.stderr : "",
75
+ });
76
+ if (retry !== null) {
77
+ try {
78
+ const stdout = runOnce(retry);
79
+ return { returncode: 0, stdout: typeof stdout === "string" ? stdout : "", stderr: "" };
80
+ }
81
+ catch (retryErr) {
82
+ err = retryErr;
83
+ }
84
+ }
62
85
  const e = err;
63
86
  if (e.code === "ENOENT") {
64
87
  return { returncode: -1, stdout: "", stderr: "gh CLI not found. Install GitHub CLI." };
@@ -1,5 +1,7 @@
1
1
  import { execFileSync } from "node:child_process";
2
- import { resolveBinary } from "../scm/binary.js";
2
+ import { defaultWhich } from "../scm/binary.js";
3
+ import { classifyScmArgv, resolveBinaryForArgv } from "../scm/call-shape.js";
4
+ import { ghxSpawnFallbackBinary } from "../scm/spawn-status.js";
3
5
  import { SUBPROCESS_MAX_BUFFER } from "../subprocess/max-buffer.js";
4
6
  import { GH_TIMEOUT_S } from "./constants.js";
5
7
  /**
@@ -11,18 +13,43 @@ export function defaultRunGh(cmd, binaryOverride) {
11
13
  if (cmd.length === 0 || cmd[0] !== "gh") {
12
14
  return { returncode: -1, stdout: "", stderr: "expected gh as first argv element" };
13
15
  }
14
- const binary = binaryOverride ?? resolveBinary();
15
16
  const args = cmd.slice(1);
17
+ const binary = binaryOverride ?? resolveBinaryForArgv(args[0] ?? "", args.slice(1));
18
+ const execOpts = {
19
+ encoding: "utf8",
20
+ timeout: GH_TIMEOUT_S * 1000,
21
+ stdio: ["ignore", "pipe", "pipe"],
22
+ maxBuffer: SUBPROCESS_MAX_BUFFER,
23
+ };
24
+ const runOnce = (bin) => execFileSync(bin, args, execOpts);
16
25
  try {
17
- const stdout = execFileSync(binary, args, {
18
- encoding: "utf8",
19
- timeout: GH_TIMEOUT_S * 1000,
20
- stdio: ["ignore", "pipe", "pipe"],
21
- maxBuffer: SUBPROCESS_MAX_BUFFER,
22
- });
26
+ const stdout = runOnce(binary);
23
27
  return { returncode: 0, stdout: typeof stdout === "string" ? stdout : "", stderr: "" };
24
28
  }
25
- catch (err) {
29
+ catch (caught) {
30
+ let err = caught;
31
+ const first = err;
32
+ const retry = binaryOverride === undefined
33
+ ? ghxSpawnFallbackBinary(classifyScmArgv(args[0] ?? "", args.slice(1)), binary, defaultWhich, {
34
+ status: typeof first.status === "number" ? first.status : null,
35
+ error: first.code === "ENOENT" ||
36
+ first.code === "ETIMEDOUT" ||
37
+ typeof first.status !== "number"
38
+ ? { code: first.code, message: first.message }
39
+ : undefined,
40
+ stdout: typeof first.stdout === "string" ? first.stdout : "",
41
+ stderr: typeof first.stderr === "string" ? first.stderr : "",
42
+ })
43
+ : null;
44
+ if (retry !== null) {
45
+ try {
46
+ const stdout = runOnce(retry);
47
+ return { returncode: 0, stdout: typeof stdout === "string" ? stdout : "", stderr: "" };
48
+ }
49
+ catch (retryErr) {
50
+ err = retryErr;
51
+ }
52
+ }
26
53
  const e = err;
27
54
  if (e.code === "ENOENT") {
28
55
  return { returncode: -1, stdout: "", stderr: "gh CLI not found. Install GitHub CLI." };
@@ -4,7 +4,7 @@ import { createRequire } from "node:module";
4
4
  import { dirname, resolve } from "node:path";
5
5
  import { fileURLToPath } from "node:url";
6
6
  import { evaluate as evaluateCloseoutAttestable } from "../pr-closeout-attestable/evaluate.js";
7
- import { resolveBinary } from "../scm/binary.js";
7
+ import { resolveBinaryForArgv } from "../scm/call-shape.js";
8
8
  import { SUBPROCESS_MAX_BUFFER } from "../subprocess/max-buffer.js";
9
9
  /** UTF-8-safe subprocess capture via spawnSync (no shell) — mirrors #1366. */
10
10
  export function captureExec(executable, args, timeoutMs, options = {}) {
@@ -140,14 +140,14 @@ export function runMonitor(prNumber, repo, capMinutes, options = {}) {
140
140
  /** Invoke ``gh pr merge --squash --delete-branch --admin`` [``--match-head-commit``]. */
141
141
  export function runGhMerge(prNumber, repo, options = {}) {
142
142
  const timeoutSec = options.timeout ?? 120;
143
+ const args = ["pr", "merge", String(prNumber), "--squash", "--delete-branch", "--admin"];
143
144
  let binary;
144
145
  try {
145
- binary = resolveBinary();
146
+ binary = resolveBinaryForArgv("pr", args.slice(1));
146
147
  }
147
148
  catch {
148
149
  return [-1, "", "gh CLI not found. Install GitHub CLI."];
149
150
  }
150
- const args = ["pr", "merge", String(prNumber), "--squash", "--delete-branch", "--admin"];
151
151
  if (repo) {
152
152
  args.push("--repo", repo);
153
153
  }
@@ -10,7 +10,7 @@
10
10
  import { existsSync, readFileSync } from "node:fs";
11
11
  import { basename, isAbsolute, relative, resolve } from "node:path";
12
12
  import { emitAcceptanceStampFromPlan, MISSING_AMBIGUITY_ATTESTATION_CAUSE, stampedAmbiguityAttestationError, } from "../intake/clause-derivation.js";
13
- import { appendLiteralAcceptanceAdvisory, evaluateLiteralAcceptanceFromPlan, isExecutableLiteralSource, isInlineProseMention, isNoopRefusalReason, readNotAcceptanceCommands, readStoredLiteralAcceptanceCommands, runLiteralAcceptanceCommands, stripLiteralAcceptanceAdvisory, } from "../literal-acceptance/index.js";
13
+ import { appendLiteralAcceptanceAdvisory, evaluateLiteralAcceptanceFromPlan, isExecutableLiteralSource, isInlineProseMention, isNoopRefusalReason, isSafetyRefusalRun, readNotAcceptanceCommands, readStoredLiteralAcceptanceCommands, runLiteralAcceptanceCommands, stripLiteralAcceptanceAdvisory, } from "../literal-acceptance/index.js";
14
14
  import { ENV_RUN_SUMMARY_PATH, RunSummaryEmitter, } from "../run-summary/index.js";
15
15
  import { maybeBankOnAcPass } from "../session/ac-pass-banking.js";
16
16
  import { resolveAcReuse, resolveScopeIdForAcReuse, snapshotFromReuseFields, } from "../session/ac-pass-reuse.js";
@@ -634,6 +634,10 @@ function annotate(result, acceptance, quiet) {
634
634
  message = "";
635
635
  }
636
636
  const commandCount = Math.max(result.commands.length, acceptance.commands.length);
637
+ const executedRuns = result.runs.filter((run) => !isSafetyRefusalRun(run));
638
+ const refusedRuns = result.runs.filter(isSafetyRefusalRun);
639
+ const hasRefusal = refusedRuns.length > 0 || (result.rejected?.length ?? 0) > 0;
640
+ const servedFrom = executedRuns.length > 0 ? "executed" : hasRefusal ? "refused" : "executed";
637
641
  return {
638
642
  ...result,
639
643
  message,
@@ -648,7 +652,7 @@ function annotate(result, acceptance, quiet) {
648
652
  rejectedCount: result.rejected?.length ?? 0,
649
653
  }),
650
654
  resolvedCommandCount: result.runs.length > 0 ? result.runs.length : commandCount,
651
- servedFrom: "executed",
655
+ servedFrom,
652
656
  };
653
657
  }
654
658
  /**
@@ -26,7 +26,9 @@ export const HYGIENE_GATE_ID_PREFIXES = [
26
26
  "verify:rule-ownership",
27
27
  "verify:biome-config",
28
28
  "verify:content-manifest",
29
+ "verify:deposit-closure",
29
30
  "verify:skill-external-fetch-gate",
31
+ "verify:semantic-single-source",
30
32
  "verify:cursor-tier1",
31
33
  "verify:openclaw-tier1",
32
34
  "verify:go-freeze",
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Check 4 -- consumer hard-stop census (#3900 / #3713 / #3969).
3
+ *
4
+ * Enumerates open issues by privileged label only.
5
+ * BLOCKER is the sole permitted consumer title classification (inbound flare).
6
+ * The adoption-blocker label is never derived from a title.
7
+ * Issue bodies are not read into the verdict.
8
+ *
9
+ * parseClosesSet composes citation-grammar classifyPosition; do not add a
10
+ * third position classifier (#3969).
11
+ */
12
+ export declare const CONSUMER_HARD_STOP_TITLE_RE: RegExp;
13
+ export declare const ADOPTION_BLOCKER_LABEL = "adoption-blocker";
14
+ export declare const BLOCKS_RELEASE_TAG_LABEL = "blocks-release-tag";
15
+ /** Maintainer-applied labels that can block a cut. Titles cannot. */
16
+ export declare const PRIVILEGED_HARD_STOP_LABELS: readonly string[];
17
+ export interface HardStopIssue {
18
+ readonly number: number;
19
+ readonly title: string;
20
+ readonly labels: readonly string[];
21
+ }
22
+ export interface HardStopCensusEntry {
23
+ readonly number: number;
24
+ readonly title: string;
25
+ readonly viaTitle: boolean;
26
+ readonly viaLabel: boolean;
27
+ }
28
+ export interface HardStopCensusResult {
29
+ readonly code: 0 | 1 | 2;
30
+ readonly message: string;
31
+ readonly stream: "stdout" | "stderr";
32
+ readonly matches: readonly HardStopCensusEntry[];
33
+ readonly shipsPast: readonly HardStopCensusEntry[];
34
+ }
35
+ /** Privileged labels only. Title is recorded as a flare, never as a match. Never inspect a body field. */
36
+ export declare function classifyHardStop(issue: HardStopIssue): HardStopCensusEntry | null;
37
+ export declare function enumerateConsumerHardStops(issues: readonly HardStopIssue[]): HardStopCensusEntry[];
38
+ /**
39
+ * Extract Closes/Fixes/Resolves #N from changelog Unreleased text.
40
+ * Never from issue bodies. Hits in refused markdown positions are not claims.
41
+ */
42
+ export declare function parseClosesSet(changelogText: string): Set<number>;
43
+ export declare function evaluateConsumerHardStopCensus(options: {
44
+ readonly issues: readonly HardStopIssue[];
45
+ readonly closesSet: ReadonlySet<number>;
46
+ }): HardStopCensusResult;
47
+ export declare function issueFromInventoryRow(row: Record<string, unknown>): HardStopIssue | null;
48
+ //# sourceMappingURL=consumer-hard-stops.d.ts.map