@deftai/directive-core 0.78.0 → 0.79.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 (99) hide show
  1. package/dist/cache/empty-populate.d.ts +44 -0
  2. package/dist/cache/empty-populate.js +117 -0
  3. package/dist/cache/index.d.ts +1 -0
  4. package/dist/cache/index.js +1 -0
  5. package/dist/check-updates/index.d.ts +10 -0
  6. package/dist/check-updates/index.js +81 -1
  7. package/dist/doctor/main.js +13 -2
  8. package/dist/doctor/payload-staleness.js +7 -1
  9. package/dist/hooks/dispatcher.d.ts +1 -1
  10. package/dist/hooks/dispatcher.js +19 -17
  11. package/dist/init-deposit/agent-hooks.d.ts +1 -1
  12. package/dist/init-deposit/agent-hooks.js +10 -0
  13. package/dist/init-deposit/hygiene.js +1 -0
  14. package/dist/init-deposit/init-deposit.js +2 -0
  15. package/dist/init-deposit/prettierignore.d.ts +21 -0
  16. package/dist/init-deposit/prettierignore.js +83 -0
  17. package/dist/init-deposit/refresh.d.ts +3 -1
  18. package/dist/init-deposit/refresh.js +29 -40
  19. package/dist/init-deposit/scaffold.js +6 -12
  20. package/dist/init-deposit/xbrief-projections.d.ts +18 -0
  21. package/dist/init-deposit/xbrief-projections.js +117 -0
  22. package/dist/intake/reconcile-issues.d.ts +10 -0
  23. package/dist/intake/reconcile-issues.js +115 -2
  24. package/dist/issue-sync/index.d.ts +2 -0
  25. package/dist/issue-sync/index.js +2 -0
  26. package/dist/issue-sync/sync-from-xbrief-cli.d.ts +11 -0
  27. package/dist/issue-sync/sync-from-xbrief-cli.js +44 -0
  28. package/dist/issue-sync/sync-from-xbrief.d.ts +46 -0
  29. package/dist/issue-sync/sync-from-xbrief.js +208 -0
  30. package/dist/platform/index.d.ts +1 -0
  31. package/dist/platform/index.js +1 -0
  32. package/dist/platform/platform-capabilities.d.ts +5 -0
  33. package/dist/platform/platform-capabilities.js +9 -0
  34. package/dist/platform/shell-context.d.ts +30 -0
  35. package/dist/platform/shell-context.js +111 -0
  36. package/dist/pr-monitor/cadence.d.ts +6 -0
  37. package/dist/pr-monitor/cadence.js +13 -0
  38. package/dist/pr-monitor/index.d.ts +1 -1
  39. package/dist/pr-monitor/index.js +1 -1
  40. package/dist/pr-monitor/monitor.d.ts +7 -0
  41. package/dist/pr-monitor/monitor.js +59 -13
  42. package/dist/preflight-cache/evaluate.d.ts +3 -8
  43. package/dist/preflight-cache/evaluate.js +19 -19
  44. package/dist/release/constants.d.ts +2 -0
  45. package/dist/release/constants.js +8 -1
  46. package/dist/release/flags.js +26 -0
  47. package/dist/release/issue-state-fetch.d.ts +27 -0
  48. package/dist/release/issue-state-fetch.js +146 -0
  49. package/dist/release/main.js +1 -0
  50. package/dist/release/native-steps.js +6 -4
  51. package/dist/release/pipeline.js +15 -5
  52. package/dist/release/preflight.d.ts +6 -2
  53. package/dist/release/preflight.js +19 -5
  54. package/dist/release/types.d.ts +3 -1
  55. package/dist/release-e2e/constants.js +6 -6
  56. package/dist/release-e2e/flags.d.ts +2 -0
  57. package/dist/release-e2e/flags.js +8 -4
  58. package/dist/release-e2e/main.js +15 -9
  59. package/dist/release-e2e/types.d.ts +4 -2
  60. package/dist/resolution/classify.d.ts +1 -1
  61. package/dist/resolution/classify.js +36 -11
  62. package/dist/scope/project-definition-sync.js +2 -150
  63. package/dist/scope/registry-artifact-sync.d.ts +3 -0
  64. package/dist/scope/registry-artifact-sync.js +159 -0
  65. package/dist/scope/specification-sync.d.ts +3 -0
  66. package/dist/scope/specification-sync.js +16 -0
  67. package/dist/scope/transition.js +40 -12
  68. package/dist/session/cache-recovery.d.ts +19 -0
  69. package/dist/session/cache-recovery.js +26 -0
  70. package/dist/session/session-start.d.ts +2 -0
  71. package/dist/session/session-start.js +14 -3
  72. package/dist/session/verify-session-ritual.d.ts +1 -0
  73. package/dist/session/verify-session-ritual.js +1 -0
  74. package/dist/triage/welcome/default-mode.js +2 -0
  75. package/dist/triage/welcome/onboard.js +2 -0
  76. package/dist/ts-check-lane/run-lane.d.ts +5 -0
  77. package/dist/ts-check-lane/run-lane.js +9 -0
  78. package/dist/vbrief-reconcile/labels.d.ts +3 -0
  79. package/dist/vbrief-reconcile/labels.js +14 -0
  80. package/dist/vbrief-reconcile/main.d.ts +1 -0
  81. package/dist/vbrief-reconcile/main.js +13 -3
  82. package/dist/vbrief-reconcile/parity-scenarios.js +18 -4
  83. package/dist/vbrief-reconcile/repo-guard.d.ts +17 -0
  84. package/dist/vbrief-reconcile/repo-guard.js +33 -0
  85. package/dist/vbrief-reconcile/umbrellas.d.ts +3 -0
  86. package/dist/vbrief-reconcile/umbrellas.js +17 -1
  87. package/dist/verify-env/agent-hooks.js +4 -3
  88. package/dist/verify-env/verify-hooks-installed.js +5 -5
  89. package/dist/vitest-runner/coverage-debt-teardown.d.ts +3 -0
  90. package/dist/vitest-runner/coverage-debt-teardown.js +33 -0
  91. package/dist/vitest-runner/coverage-debt.d.ts +50 -0
  92. package/dist/vitest-runner/coverage-debt.js +155 -0
  93. package/dist/vitest-runner/win32-coverage-tmp-setup.d.ts +12 -0
  94. package/dist/vitest-runner/win32-coverage-tmp-setup.js +24 -0
  95. package/dist/xbrief-migrate/detect.js +46 -1
  96. package/dist/xbrief-migrate/migrate-project.js +19 -4
  97. package/dist/xbrief-migrate/migration-containment.d.ts +6 -0
  98. package/dist/xbrief-migrate/migration-containment.js +37 -0
  99. package/package.json +7 -3
@@ -13,8 +13,11 @@
13
13
  import { execFileSync } from "node:child_process";
14
14
  import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
15
15
  import { join, relative } from "node:path";
16
+ import { isTriageCacheEmpty, maybeAutoPopulateEmptyCache } from "../cache/empty-populate.js";
16
17
  import { probeCacheDrift } from "../cache/fetch.js";
17
18
  import { resolveProjectDefinitionPath } from "../layout/resolve.js";
19
+ import { recoveryHintForStaleFailure } from "../session/cache-recovery.js";
20
+ export { recoveryHintForStaleFailure } from "../session/cache-recovery.js";
18
21
  import { readPlanPolicy } from "../policy/plan-extensions.js";
19
22
  import { latestDecisionForIssue as auditLatestDecisionForIssue } from "../triage/actions/candidates-log.js";
20
23
  import { resolveCandidatesLogPath } from "../triage/cache-path.js";
@@ -280,22 +283,6 @@ function candidatesLogState(candidatesPath) {
280
283
  // Main evaluate function
281
284
  // ---------------------------------------------------------------------------
282
285
  const REMEDIATION_NO_CACHE = [" Recovery: run `deft triage:bootstrap` to seed the cache."].join("\n");
283
- const REMEDIATION_STALE_FORCE = [
284
- " Recovery: run `deft cache:fetch-all --force` to refresh and reconcile upstream state.",
285
- ].join("\n");
286
- const REMEDIATION_STALE_PLAIN = [
287
- " Recovery: run `deft cache:fetch-all` to refresh and reconcile upstream state.",
288
- ].join("\n");
289
- /**
290
- * Branch-aware recovery hint (#1953 Option 3).
291
- * Age-stale (or age+drift mixed) → --force bypasses TTL; drift-only → plain refetch.
292
- */
293
- export function recoveryHintForStaleFailure(causes) {
294
- if (causes.ageStale) {
295
- return REMEDIATION_STALE_FORCE;
296
- }
297
- return REMEDIATION_STALE_PLAIN;
298
- }
299
286
  function formatDriftFailure(repo, drift) {
300
287
  const parts = [];
301
288
  if (drift.stateDriftNumbers.length > 0) {
@@ -308,7 +295,7 @@ function formatDriftFailure(repo, drift) {
308
295
  code: 1,
309
296
  message: [
310
297
  `❌ deft cache-fresh: stale-by-drift -- ${parts.join("; ")} (${repo}).`,
311
- recoveryHintForStaleFailure({ ageStale: false, driftDetected: true }),
298
+ recoveryHintForStaleFailure({ ageStale: false, driftDetected: true }, repo),
312
299
  ].join("\n"),
313
300
  };
314
301
  }
@@ -332,10 +319,23 @@ const REMEDIATION_NO_CANDIDATES = [
332
319
  * Faithful port of scripts/preflight_cache.py::evaluate().
333
320
  */
334
321
  export function evaluate(projectRoot, options = {}) {
322
+ return evaluateWithContext(projectRoot, options, { afterEmptyPopulate: false });
323
+ }
324
+ function evaluateWithContext(projectRoot, options, ctx) {
325
+ const allowMissingBootstrap = options.allowMissingBootstrap ?? false;
326
+ const autoPopulateEmpty = options.autoPopulateEmpty !== false;
327
+ if (!allowMissingBootstrap &&
328
+ autoPopulateEmpty &&
329
+ !ctx.afterEmptyPopulate &&
330
+ isTriageCacheEmpty(projectRoot)) {
331
+ const populated = maybeAutoPopulateEmptyCache(projectRoot, { repo: options.repo ?? null });
332
+ if (populated.populated) {
333
+ return evaluateWithContext(projectRoot, options, { afterEmptyPopulate: true });
334
+ }
335
+ }
335
336
  const source = options.source ?? DEFAULT_SOURCE;
336
337
  const allowStale = options.allowStale ?? false;
337
338
  const releasePreflightStaleBypass = releasePreflightBypassActive();
338
- const allowMissingBootstrap = options.allowMissingBootstrap ?? false;
339
339
  const nowFn = options.nowFn ?? (() => new Date());
340
340
  const envMaxAge = process.env[ENV_MAX_AGE_HOURS];
341
341
  const envMaxAgeParsed = envMaxAge !== undefined ? Number.parseInt(envMaxAge, 10) : null;
@@ -453,7 +453,7 @@ export function evaluate(projectRoot, options = {}) {
453
453
  code: 1,
454
454
  message: [
455
455
  `❌ deft cache-fresh: cache is ${ageH.toFixed(1)}h old (max-age=${maxAgeHours}h); oldest in-scope entry ${display}.`,
456
- recoveryHintForStaleFailure({ ageStale: true, driftDetected: false }),
456
+ recoveryHintForStaleFailure({ ageStale: true, driftDetected: false }, resolvedRepo),
457
457
  ].join("\n"),
458
458
  };
459
459
  }
@@ -18,6 +18,8 @@ export declare const BRANCH_GATE_BYPASS_ENV = "DEFT_ALLOW_DEFAULT_BRANCH_COMMIT"
18
18
  export declare const DESTRUCTIVE_GH_GATE_BYPASS_ENV = "DEFT_ALLOW_DESTRUCTIVE_GH_VERBS";
19
19
  /** Set by releaseSubprocessEnv() so Step-5 pre-flight skips triage-cache staleness (#2386). */
20
20
  export declare const RELEASE_PREFLIGHT_ENV = "DEFT_RELEASE_PREFLIGHT";
21
+ /** Set only by release Step-5 preflight when --allow-coverage-debt=#N is supplied (#2573). */
22
+ export declare const COVERAGE_DEBT_ENV = "DEFT_ALLOW_COVERAGE_DEBT";
21
23
  export declare const PYPROJECT_VERSION_LINE_RE: RegExp;
22
24
  /** Byte-identical argparse --help from scripts/release.py (Python 3.12). */
23
25
  export declare const RELEASE_HELP: string;
@@ -26,10 +26,13 @@ export const BRANCH_GATE_BYPASS_ENV = "DEFT_ALLOW_DEFAULT_BRANCH_COMMIT";
26
26
  export const DESTRUCTIVE_GH_GATE_BYPASS_ENV = "DEFT_ALLOW_DESTRUCTIVE_GH_VERBS";
27
27
  /** Set by releaseSubprocessEnv() so Step-5 pre-flight skips triage-cache staleness (#2386). */
28
28
  export const RELEASE_PREFLIGHT_ENV = "DEFT_RELEASE_PREFLIGHT";
29
+ /** Set only by release Step-5 preflight when --allow-coverage-debt=#N is supplied (#2573). */
30
+ export const COVERAGE_DEBT_ENV = "DEFT_ALLOW_COVERAGE_DEBT";
29
31
  export const PYPROJECT_VERSION_LINE_RE = /version\s*=\s*"[^"]*"/;
30
32
  /** Byte-identical argparse --help from scripts/release.py (Python 3.12). */
31
33
  export const RELEASE_HELP = "usage: release [-h] [--dry-run] [--skip-tag] [--skip-release] [--allow-dirty]\n" +
32
- " [--allow-vbrief-drift] [--skip-ci] [--skip-build] [--no-draft]\n" +
34
+ " [--allow-vbrief-drift] [--allow-coverage-debt #N]\n" +
35
+ " [--skip-ci] [--skip-build] [--no-draft]\n" +
33
36
  " [--repo OWNER/REPO] [--base-branch BRANCH]\n" +
34
37
  " [--project-root PATH] [--summary TEXT]\n" +
35
38
  " version\n" +
@@ -57,6 +60,10 @@ export const RELEASE_HELP = "usage: release [-h] [--dry-run] [--skip-tag] [--ski
57
60
  " may still live in non-terminal folders. The clean path\n" +
58
61
  " is to run `task reconcile:issues -- --apply-lifecycle-\n" +
59
62
  " fixes` first.\n" +
63
+ " --allow-coverage-debt #N\n" +
64
+ " Acknowledge a hairline coverage miss: Step 5 passes\n" +
65
+ " only when metrics sit below the 85% goal and this\n" +
66
+ " flag cites an operator-owned issue number (#2573).\n" +
60
67
  " --skip-ci Skip Step 3 (task ci:local / task check fallback).\n" +
61
68
  " Used by `task release:e2e` to keep wall-clock\n" +
62
69
  " manageable inside the auto-created temp repo (CI\n" +
@@ -1,3 +1,4 @@
1
+ import { parseCoverageDebtIssueNumber } from "../vitest-runner/coverage-debt.js";
1
2
  import { DEFAULT_BASE_BRANCH, RELEASE_HELP } from "./constants.js";
2
3
  export function parseReleaseFlags(args) {
3
4
  let help = false;
@@ -6,6 +7,7 @@ export function parseReleaseFlags(args) {
6
7
  let skipRelease = false;
7
8
  let allowDirty = false;
8
9
  let allowVbriefDrift = false;
10
+ let allowCoverageDebtIssue = null;
9
11
  let skipCi = false;
10
12
  let skipBuild = false;
11
13
  let draft = true;
@@ -43,6 +45,29 @@ export function parseReleaseFlags(args) {
43
45
  else if (token === "--allow-vbrief-drift") {
44
46
  allowVbriefDrift = true;
45
47
  }
48
+ else if (token === "--allow-coverage-debt") {
49
+ const value = takeValue(token, i);
50
+ if (value !== null) {
51
+ const issue = parseCoverageDebtIssueNumber(value);
52
+ if (issue === null) {
53
+ unknown.push(`${token} (malformed issue number)`);
54
+ }
55
+ else {
56
+ allowCoverageDebtIssue = issue;
57
+ }
58
+ i += 1;
59
+ }
60
+ }
61
+ else if (token.startsWith("--allow-coverage-debt=")) {
62
+ const value = token.slice("--allow-coverage-debt=".length);
63
+ const issue = parseCoverageDebtIssueNumber(value);
64
+ if (issue === null) {
65
+ unknown.push("--allow-coverage-debt= (malformed issue number)");
66
+ }
67
+ else {
68
+ allowCoverageDebtIssue = issue;
69
+ }
70
+ }
46
71
  else if (token === "--skip-ci") {
47
72
  skipCi = true;
48
73
  }
@@ -118,6 +143,7 @@ export function parseReleaseFlags(args) {
118
143
  skipRelease,
119
144
  allowDirty,
120
145
  allowVbriefDrift,
146
+ allowCoverageDebtIssue,
121
147
  skipCi,
122
148
  skipBuild,
123
149
  draft,
@@ -0,0 +1,27 @@
1
+ import { type FetchIssueStatesOptions } from "../intake/reconcile-issues.js";
2
+ import { type CallOptions, type CompletedProcess } from "../scm/call.js";
3
+ export type ScmCallFn = (source: string, verb: string, args: readonly string[] | null, options?: CallOptions) => CompletedProcess;
4
+ /** Max seconds to sleep before the single rate-limit retry (operator-locked cap). */
5
+ export declare const MAX_RATE_LIMIT_RETRY_SLEEP_S = 120;
6
+ export type SleepFn = (seconds: number) => void;
7
+ export interface RateLimitProbe {
8
+ readonly coreRemaining: number | null;
9
+ readonly coreResetUnix: number | null;
10
+ readonly graphqlRemaining: number | null;
11
+ }
12
+ export interface FetchIssueStatesForReleaseOptions extends FetchIssueStatesOptions {
13
+ readonly sleep?: SleepFn;
14
+ readonly now?: () => number;
15
+ }
16
+ export type FetchIssueStatesForReleaseResult = {
17
+ readonly ok: true;
18
+ readonly states: Map<number, import("../intake/reconcile-issues.js").IssueState>;
19
+ } | {
20
+ readonly ok: false;
21
+ readonly reason: string;
22
+ };
23
+ export declare function probeGithubRateLimit(scmCall: ScmCallFn, cwd?: string): RateLimitProbe | null;
24
+ export declare function computeRateLimitSleepSeconds(stderr: string, probe: RateLimitProbe | null, nowSec: number): number;
25
+ export declare function formatRateLimitFailureDetails(probe: RateLimitProbe | null, rateLimitRelated: boolean): string;
26
+ export declare function fetchIssueStatesForRelease(repo: string, issueNumbers: ReadonlySet<number>, options?: FetchIssueStatesForReleaseOptions): FetchIssueStatesForReleaseResult;
27
+ //# sourceMappingURL=issue-state-fetch.d.ts.map
@@ -0,0 +1,146 @@
1
+ /**
2
+ * Release Step 3 issue-state fetch with capped rate-limit retry (#2577).
3
+ */
4
+ import { detectRateLimit } from "../cache/fetch.js";
5
+ import { fetchIssueStates } from "../intake/reconcile-issues.js";
6
+ import { call } from "../scm/call.js";
7
+ /** Max seconds to sleep before the single rate-limit retry (operator-locked cap). */
8
+ export const MAX_RATE_LIMIT_RETRY_SLEEP_S = 120;
9
+ const X_RATE_LIMIT_RESET_RE = /X-RateLimit-Reset:\s*(\d+)/i;
10
+ function defaultSleep(seconds) {
11
+ const ms = Math.max(0, Math.trunc(seconds * 1000));
12
+ if (ms === 0) {
13
+ return;
14
+ }
15
+ const shared = new Int32Array(new SharedArrayBuffer(4));
16
+ Atomics.wait(shared, 0, 0, ms);
17
+ }
18
+ function parseRateLimitProbe(stdout) {
19
+ try {
20
+ const body = JSON.parse(stdout);
21
+ if (typeof body !== "object" || body === null) {
22
+ return null;
23
+ }
24
+ const resources = body.resources;
25
+ if (typeof resources !== "object" || resources === null) {
26
+ return null;
27
+ }
28
+ const core = resources.core;
29
+ const graphql = resources.graphql;
30
+ if (typeof core !== "object" ||
31
+ core === null ||
32
+ Array.isArray(core) ||
33
+ typeof graphql !== "object" ||
34
+ graphql === null ||
35
+ Array.isArray(graphql)) {
36
+ return null;
37
+ }
38
+ const coreRemainingRaw = core.remaining;
39
+ const coreResetRaw = core.reset;
40
+ const graphqlRemainingRaw = graphql.remaining;
41
+ const coreRemaining = coreRemainingRaw === undefined ? null : Number.parseInt(String(coreRemainingRaw), 10);
42
+ const coreResetUnix = coreResetRaw === undefined ? null : Number.parseInt(String(coreResetRaw), 10);
43
+ const graphqlRemaining = graphqlRemainingRaw === undefined ? null : Number.parseInt(String(graphqlRemainingRaw), 10);
44
+ if ((coreRemaining !== null && Number.isNaN(coreRemaining)) ||
45
+ (coreResetUnix !== null && Number.isNaN(coreResetUnix)) ||
46
+ (graphqlRemaining !== null && Number.isNaN(graphqlRemaining))) {
47
+ return null;
48
+ }
49
+ return { coreRemaining, coreResetUnix, graphqlRemaining };
50
+ }
51
+ catch {
52
+ return null;
53
+ }
54
+ }
55
+ export function probeGithubRateLimit(scmCall, cwd) {
56
+ let result;
57
+ try {
58
+ result = scmCall("github-issue", "api", ["rate_limit"], { timeout: 30, cwd });
59
+ }
60
+ catch {
61
+ return null;
62
+ }
63
+ if (result.returncode !== 0 || result.stdout.trim().length === 0) {
64
+ return null;
65
+ }
66
+ return parseRateLimitProbe(result.stdout);
67
+ }
68
+ export function computeRateLimitSleepSeconds(stderr, probe, nowSec) {
69
+ const [isRateLimit, retryAfter] = detectRateLimit(stderr);
70
+ if (!isRateLimit) {
71
+ return 0;
72
+ }
73
+ let candidate = retryAfter;
74
+ const resetMatch = X_RATE_LIMIT_RESET_RE.exec(stderr);
75
+ if (resetMatch?.[1]) {
76
+ const resetAt = Number.parseInt(resetMatch[1], 10);
77
+ if (!Number.isNaN(resetAt)) {
78
+ candidate = Math.max(0, resetAt - nowSec + 1);
79
+ }
80
+ }
81
+ else if (probe !== null && probe.coreResetUnix !== null) {
82
+ candidate = Math.max(0, probe.coreResetUnix - nowSec + 1);
83
+ }
84
+ return Math.min(Math.max(1, candidate), MAX_RATE_LIMIT_RETRY_SLEEP_S);
85
+ }
86
+ export function formatRateLimitFailureDetails(probe, rateLimitRelated) {
87
+ const lines = [];
88
+ if (probe !== null) {
89
+ const resetHint = probe.coreResetUnix !== null
90
+ ? `core resets at ${new Date(probe.coreResetUnix * 1000).toISOString()}`
91
+ : "run `gh api rate_limit` for reset time";
92
+ lines.push(`GitHub rate limit probe: core.remaining=${probe.coreRemaining ?? "?"} graphql.remaining=${probe.graphqlRemaining ?? "?"} (${resetHint})`);
93
+ }
94
+ else {
95
+ lines.push("Probe `gh api rate_limit` for core.remaining and reset time.");
96
+ }
97
+ if (rateLimitRelated) {
98
+ lines.push("Recovery: wait for the REST core bucket to reset, then re-run `task release`.");
99
+ lines.push("If local lifecycle validation is clean (`task vbrief:validate` exits 0), you may pass `--allow-vbrief-drift` to skip Step 3 for this cut.");
100
+ }
101
+ return lines.join("\n");
102
+ }
103
+ export function fetchIssueStatesForRelease(repo, issueNumbers, options = {}) {
104
+ const sleepFn = options.sleep ?? defaultSleep;
105
+ const nowFn = options.now ?? (() => Math.floor(Date.now() / 1000));
106
+ const baseScmCall = options.scmCall ?? call;
107
+ const cwd = options.cwd ?? undefined;
108
+ let sawRateLimit = false;
109
+ let lastRateLimitStderr = "";
110
+ const trackingScmCall = (source, verb, args, callOptions) => {
111
+ const result = baseScmCall(source, verb, args, callOptions);
112
+ if (result.returncode !== 0 && detectRateLimit(result.stderr)[0]) {
113
+ sawRateLimit = true;
114
+ lastRateLimitStderr = result.stderr;
115
+ }
116
+ return result;
117
+ };
118
+ let states = fetchIssueStates(repo, issueNumbers, { ...options, scmCall: trackingScmCall });
119
+ if (states !== null) {
120
+ return { ok: true, states };
121
+ }
122
+ if (sawRateLimit) {
123
+ const probe = probeGithubRateLimit(baseScmCall, cwd);
124
+ const sleepS = computeRateLimitSleepSeconds(lastRateLimitStderr, probe, nowFn());
125
+ process.stderr.write(`[release Step 3] GitHub REST rate limit hit; sleeping ${sleepS}s before one retry\n`);
126
+ sleepFn(sleepS);
127
+ sawRateLimit = false;
128
+ lastRateLimitStderr = "";
129
+ states = fetchIssueStates(repo, issueNumbers, { ...options, scmCall: trackingScmCall });
130
+ if (states !== null) {
131
+ return { ok: true, states };
132
+ }
133
+ }
134
+ const rateLimitRelated = sawRateLimit || lastRateLimitStderr.length > 0;
135
+ if (rateLimitRelated) {
136
+ const probe = probeGithubRateLimit(baseScmCall, cwd);
137
+ const details = formatRateLimitFailureDetails(probe, true);
138
+ process.stderr.write(`${details}\n`);
139
+ return {
140
+ ok: false,
141
+ reason: "GitHub REST rate limit exhausted (see stderr for recovery steps)",
142
+ };
143
+ }
144
+ return { ok: false, reason: "failed to fetch issue states from gh" };
145
+ }
146
+ //# sourceMappingURL=issue-state-fetch.js.map
@@ -41,6 +41,7 @@ export function cmdRelease(args, seams = {}) {
41
41
  skipBuild: flags.skipBuild,
42
42
  summary: flags.summary,
43
43
  allowVbriefDrift: flags.allowVbriefDrift,
44
+ allowCoverageDebtIssue: flags.allowCoverageDebtIssue,
44
45
  };
45
46
  return runPipeline(config, seams);
46
47
  }
@@ -6,9 +6,10 @@ import { spawnSync } from "node:child_process";
6
6
  import { existsSync } from "node:fs";
7
7
  import { dirname, join } from "node:path";
8
8
  import { fileURLToPath } from "node:url";
9
- import { fetchIssueStates, isTerminalLifecyclePath, reconcile, scanVbriefDir, } from "../intake/reconcile-issues.js";
9
+ import { isTerminalLifecyclePath, reconcile, scanVbriefDir } from "../intake/reconcile-issues.js";
10
10
  import { LEGACY_ARTIFACT_DIR, MIGRATED_ARTIFACT_DIR, resolveLifecycleFolder, resolveLifecycleRoot, } from "../layout/resolve.js";
11
11
  import { renderRoadmap } from "../render/roadmap-render.js";
12
+ import { fetchIssueStatesForRelease } from "./issue-state-fetch.js";
12
13
  const BUILD_DIST_RUNNER = join(dirname(fileURLToPath(import.meta.url)), "build-dist-runner.js");
13
14
  export function refreshRoadmapNative(projectRoot) {
14
15
  const pending = resolveLifecycleFolder(projectRoot, "pending");
@@ -35,12 +36,13 @@ export function checkVbriefLifecycleSyncNative(projectRoot, repo) {
35
36
  }
36
37
  try {
37
38
  const issueToVbriefs = scanVbriefDir(vbriefDir);
38
- const issueStateMap = fetchIssueStates(repo, new Set(issueToVbriefs.keys()), {
39
+ const fetchResult = fetchIssueStatesForRelease(repo, new Set(issueToVbriefs.keys()), {
39
40
  cwd: projectRoot,
40
41
  });
41
- if (issueStateMap === null) {
42
- return [false, -1, "failed to fetch issue states from gh"];
42
+ if (!fetchResult.ok) {
43
+ return [false, -1, fetchResult.reason];
43
44
  }
45
+ const issueStateMap = fetchResult.states;
44
46
  const report = reconcile(issueToVbriefs, issueStateMap);
45
47
  const mismatches = [];
46
48
  for (const entry of report.no_open_issue) {
@@ -21,7 +21,8 @@ export function runPipeline(config, seams = {}) {
21
21
  const readFile = seams.readFile ?? ((p) => readFileSync(p, "utf8"));
22
22
  const writeFile = seams.writeFile ?? ((p, c) => writeFileSync(p, c, "utf8"));
23
23
  const fileExists = seams.fileExists ?? ((p) => existsSync(p));
24
- const runCiFn = seams.runCi ?? ((root) => runReleaseCheck(root));
24
+ const runCiFn = seams.runCi ??
25
+ ((root, debtIssue) => runReleaseCheck(root, {}, debtIssue));
25
26
  const refreshRoadmapFn = seams.refreshRoadmap ?? ((root) => refreshRoadmapNative(root));
26
27
  const checkVbriefFn = seams.checkVbriefLifecycleSync ??
27
28
  ((root, repo) => checkVbriefLifecycleSyncNative(root, repo));
@@ -110,15 +111,24 @@ export function runPipeline(config, seams = {}) {
110
111
  emit(5, label, "SKIP (--skip-ci)");
111
112
  }
112
113
  else if (config.dryRun) {
113
- emit(5, label, "DRYRUN (would run task ci:local with task check fallback)");
114
+ const debtNote = config.allowCoverageDebtIssue !== null
115
+ ? `; would pass --allow-coverage-debt=#${config.allowCoverageDebtIssue} to task check`
116
+ : "";
117
+ emit(5, label, `DRYRUN (would run task ci:local with task check fallback${debtNote})`);
114
118
  }
115
119
  else {
116
- const [ok, reason] = runCiFn(projectRoot);
120
+ const [ok, reason] = runCiFn(projectRoot, config.allowCoverageDebtIssue);
117
121
  if (ok) {
118
- emit(5, label, `OK (${reason})`);
122
+ const debtNote = config.allowCoverageDebtIssue !== null
123
+ ? ` (coverage-debt acknowledged #${config.allowCoverageDebtIssue})`
124
+ : "";
125
+ emit(5, label, `OK (${reason}${debtNote})`);
119
126
  }
120
127
  else {
121
- emit(5, label, `FAIL (${reason})`);
128
+ const debtHint = config.allowCoverageDebtIssue === null
129
+ ? "; pass --allow-coverage-debt=#N only after operator review"
130
+ : "";
131
+ emit(5, label, `FAIL (${reason}${debtHint})`);
122
132
  return EXIT_VIOLATION;
123
133
  }
124
134
  }
@@ -8,8 +8,12 @@
8
8
  * dispatches `check:framework-source`.
9
9
  */
10
10
  import { type CheckOrchestratorSeams } from "../check/orchestrator.js";
11
+ export interface ReleaseCheckEnvOptions {
12
+ readonly base?: NodeJS.ProcessEnv;
13
+ readonly allowCoverageDebtIssue?: number | null;
14
+ }
11
15
  /** Step-5-only env: branch bypass plus release pre-flight cache staleness tolerance (#2386). */
12
- export declare function releaseCheckEnv(base?: NodeJS.ProcessEnv): NodeJS.ProcessEnv;
16
+ export declare function releaseCheckEnv(options?: ReleaseCheckEnvOptions): NodeJS.ProcessEnv;
13
17
  /** Seams for test isolation of the native release pre-flight. */
14
18
  export interface ReleasePreflightSeams {
15
19
  /** Override the check dispatcher (default: dispatchTaskCheck from check/orchestrator). */
@@ -25,5 +29,5 @@ export interface ReleasePreflightSeams {
25
29
  * and the project root -- the orchestrator then resolves the framework-source
26
30
  * context and runs `check:framework-source`.
27
31
  */
28
- export declare function runReleaseCheck(projectRoot: string, seams?: ReleasePreflightSeams): [boolean, string];
32
+ export declare function runReleaseCheck(projectRoot: string, seams?: ReleasePreflightSeams, allowCoverageDebtIssue?: number | null): [boolean, string];
29
33
  //# sourceMappingURL=preflight.d.ts.map
@@ -8,14 +8,25 @@
8
8
  * dispatches `check:framework-source`.
9
9
  */
10
10
  import { dispatchTaskCheck } from "../check/orchestrator.js";
11
- import { RELEASE_PREFLIGHT_ENV } from "./constants.js";
11
+ import { COVERAGE_DEBT_ENV, RELEASE_PREFLIGHT_ENV } from "./constants.js";
12
12
  import { releaseSubprocessEnv } from "./git.js";
13
13
  /** Step-5-only env: branch bypass plus release pre-flight cache staleness tolerance (#2386). */
14
- export function releaseCheckEnv(base = process.env) {
15
- return {
14
+ export function releaseCheckEnv(options = {}) {
15
+ const base = options.base ?? process.env;
16
+ const allowCoverageDebtIssue = options.allowCoverageDebtIssue ?? null;
17
+ const env = {
16
18
  ...releaseSubprocessEnv(base),
17
19
  [RELEASE_PREFLIGHT_ENV]: "1",
18
20
  };
21
+ if (allowCoverageDebtIssue !== null) {
22
+ env[COVERAGE_DEBT_ENV] = String(allowCoverageDebtIssue);
23
+ }
24
+ else {
25
+ // Scrub ambient parent-shell debt so nested unit tests and unpaid checks
26
+ // do not inherit a prior --allow-coverage-debt from the release process (#2618).
27
+ delete env[COVERAGE_DEBT_ENV];
28
+ }
29
+ return env;
19
30
  }
20
31
  /**
21
32
  * Run the native TypeScript `task check` as the release pre-flight.
@@ -25,11 +36,14 @@ export function releaseCheckEnv(base = process.env) {
25
36
  * and the project root -- the orchestrator then resolves the framework-source
26
37
  * context and runs `check:framework-source`.
27
38
  */
28
- export function runReleaseCheck(projectRoot, seams = {}) {
39
+ export function runReleaseCheck(projectRoot, seams = {}, allowCoverageDebtIssue = null) {
29
40
  const dispatch = seams.dispatchCheck ?? dispatchTaskCheck;
30
41
  const checkSeams = {
31
42
  ...seams.checkSeams,
32
- env: releaseCheckEnv(seams.checkSeams?.env ?? process.env),
43
+ env: releaseCheckEnv({
44
+ base: seams.checkSeams?.env ?? process.env,
45
+ allowCoverageDebtIssue,
46
+ }),
33
47
  };
34
48
  const code = dispatch(projectRoot, projectRoot, checkSeams);
35
49
  if (code === 0) {
@@ -12,6 +12,7 @@ export interface ReleaseConfig {
12
12
  readonly skipBuild: boolean;
13
13
  readonly summary: string | null;
14
14
  readonly allowVbriefDrift: boolean;
15
+ readonly allowCoverageDebtIssue: number | null;
15
16
  }
16
17
  export interface ReleaseFlags {
17
18
  readonly help: boolean;
@@ -28,6 +29,7 @@ export interface ReleaseFlags {
28
29
  readonly skipBuild: boolean;
29
30
  readonly draft: boolean;
30
31
  readonly summary: string | null;
32
+ readonly allowCoverageDebtIssue: number | null;
31
33
  readonly unknown: readonly string[];
32
34
  }
33
35
  export interface SpawnResult {
@@ -48,7 +50,7 @@ export interface ReleaseSeams {
48
50
  readonly writeFile?: (path: string, content: string) => void;
49
51
  readonly readFile?: (path: string) => string;
50
52
  readonly fileExists?: (path: string) => boolean;
51
- readonly runCi?: (projectRoot: string) => [boolean, string];
53
+ readonly runCi?: (projectRoot: string, allowCoverageDebtIssue: number | null) => [boolean, string];
52
54
  readonly refreshRoadmap?: (projectRoot: string) => [boolean, string];
53
55
  readonly checkVbriefLifecycleSync?: (projectRoot: string, repo: string) => [boolean, number, string];
54
56
  readonly runBuild?: (projectRoot: string, version: string | null) => [boolean, string];
@@ -28,20 +28,20 @@ export const RELEASE_ENTRYPOINT_TIMEOUT_SECONDS = 600.0;
28
28
  export const ROLLBACK_ENTRYPOINT_TIMEOUT_SECONDS = 300.0;
29
29
  export const ENTRYPOINT_TIMEOUT_EXIT_CODE = 124;
30
30
  /** Byte-identical --help from scripts/release_e2e.py (Python 3.12 argparse). */
31
- export const RELEASE_E2E_HELP = "usage: release_e2e [-h] [--owner OWNER] [--dry-run] [--keep-repo]\n" +
31
+ export const RELEASE_E2E_HELP = "usage: release_e2e [-h] [--owner OWNER] [--dry-run] [--destroy-repo]\n" +
32
32
  " [--project-root PATH] [--skip-npm]\n" +
33
33
  "\n" +
34
- "End-to-end release rehearsal against an auto-created+destroyed temp GitHub\n" +
35
- "repo (#716 safety hardening Q1).\n" +
34
+ "End-to-end release rehearsal against an auto-created temp GitHub repo\n" +
35
+ "(#716 safety hardening Q1; #2572 defaults to keep+report cleanup).\n" +
36
36
  "\n" +
37
37
  "options:\n" +
38
38
  " -h, --help show this help message and exit\n" +
39
39
  " --owner OWNER GitHub owner under which to create the temp repo\n" +
40
40
  " (default: deftai).\n" +
41
41
  " --dry-run Print the pipeline plan without invoking gh.\n" +
42
- " --keep-repo Skip destroying the temp repo at the end (use only when\n" +
43
- " manually debugging a failed rehearsal; remember to\n" +
44
- " clean up by hand).\n" +
42
+ " --destroy-repo Attempt gh repo delete after rehearsal (privileged\n" +
43
+ " environments with delete_repo permission only; default\n" +
44
+ " keeps the temp repo and reports manual cleanup).\n" +
45
45
  " --project-root PATH Repository root (default: $DEFT_PROJECT_ROOT or\n" +
46
46
  " scripts/.. ).\n" +
47
47
  " --skip-npm Skip the npm publish dry-run rehearsal step (#1910).\n" +
@@ -1,5 +1,7 @@
1
1
  import type { E2ESeams } from "./types.js";
2
2
  export declare function emit(label: string, status: string): void;
3
+ /** Copy-paste cleanup hint for leftover release-e2e temp repos (#2572). */
4
+ export declare function formatTempRepoCleanupHint(owner: string, slug: string): string;
3
5
  export declare function generateRepoSlug(seams?: E2ESeams): string;
4
6
  export declare function parseE2EFlags(argv: readonly string[]): import("./types.js").ParsedE2EFlags;
5
7
  //# sourceMappingURL=flags.d.ts.map
@@ -3,6 +3,10 @@ import { REPO_SLUG_PREFIX } from "./constants.js";
3
3
  export function emit(label, status) {
4
4
  process.stderr.write(`[e2e] ${label}... ${status}\n`);
5
5
  }
6
+ /** Copy-paste cleanup hint for leftover release-e2e temp repos (#2572). */
7
+ export function formatTempRepoCleanupHint(owner, slug) {
8
+ return `gh repo delete ${owner}/${slug} --yes`;
9
+ }
6
10
  export function generateRepoSlug(seams = {}) {
7
11
  if (seams.generateRepoSlug) {
8
12
  return seams.generateRepoSlug();
@@ -22,7 +26,7 @@ export function parseE2EFlags(argv) {
22
26
  let help = false;
23
27
  let owner = "deftai";
24
28
  let dryRun = false;
25
- let keepRepo = false;
29
+ let destroyRepo = false;
26
30
  let projectRoot = null;
27
31
  let skipNpm = false;
28
32
  let legacyBridge = false;
@@ -37,8 +41,8 @@ export function parseE2EFlags(argv) {
37
41
  else if (arg === "--dry-run") {
38
42
  dryRun = true;
39
43
  }
40
- else if (arg === "--keep-repo") {
41
- keepRepo = true;
44
+ else if (arg === "--destroy-repo") {
45
+ destroyRepo = true;
42
46
  }
43
47
  else if (arg === "--skip-npm") {
44
48
  skipNpm = true;
@@ -79,6 +83,6 @@ export function parseE2EFlags(argv) {
79
83
  unknown.push(arg);
80
84
  }
81
85
  }
82
- return { help, owner, dryRun, keepRepo, projectRoot, skipNpm, legacyBridge, unknown };
86
+ return { help, owner, dryRun, destroyRepo, projectRoot, skipNpm, legacyBridge, unknown };
83
87
  }
84
88
  //# sourceMappingURL=flags.js.map
@@ -1,6 +1,6 @@
1
1
  import { resolveProjectRoot } from "../release/paths.js";
2
2
  import { EXIT_CONFIG_ERROR, EXIT_OK, EXIT_VIOLATION, RELEASE_E2E_HELP } from "./constants.js";
3
- import { emit, generateRepoSlug, parseE2EFlags } from "./flags.js";
3
+ import { emit, formatTempRepoCleanupHint, generateRepoSlug, parseE2EFlags } from "./flags.js";
4
4
  import { destroyTempRepo, provisionTempRepo } from "./gh-ops.js";
5
5
  import { runLegacyBridgeLeg } from "./legacy-bridge-leg.js";
6
6
  import { runRehearsal } from "./rehearsal.js";
@@ -18,7 +18,13 @@ export function runE2e(config, seams = {}) {
18
18
  emit("Legacy-bridge leg", "DRYRUN (would run the pinned legacy -> bridge -> npm-hybrid migration leg; reads the " +
19
19
  "Tier-0 SoT lastGoInstaller(), pending-pin while null)");
20
20
  }
21
- emit("Destroy temp repo", `DRYRUN (would run \`gh repo delete ${owner}/${slug} --yes\`)`);
21
+ const cleanupHint = formatTempRepoCleanupHint(owner, slug);
22
+ if (config.destroyRepo) {
23
+ emit("Destroy temp repo", `DRYRUN (would run \`${cleanupHint}\`)`);
24
+ }
25
+ else {
26
+ emit("Temp repo cleanup", `DRYRUN (would keep ${owner}/${slug}; manual cleanup: ${cleanupHint})`);
27
+ }
22
28
  return EXIT_OK;
23
29
  }
24
30
  // Opt-in #1912 leg: local + self-contained (no temp GitHub repo), so run it
@@ -49,19 +55,19 @@ export function runE2e(config, seams = {}) {
49
55
  }
50
56
  }
51
57
  finally {
52
- if (config.keepRepo) {
53
- emit(`Destroy ${owner}/${slug}`, "SKIP (--keep-repo set; manual cleanup required: " +
54
- `gh repo delete ${owner}/${slug} --yes)`);
55
- }
56
- else {
58
+ const cleanupHint = formatTempRepoCleanupHint(owner, slug);
59
+ if (config.destroyRepo) {
57
60
  const [destroyOk, destroyReason] = destroyTempRepo(owner, slug, seams);
58
61
  if (destroyOk) {
59
62
  emit(`Destroy ${owner}/${slug}`, `OK (${destroyReason})`);
60
63
  }
61
64
  else {
62
- emit(`Destroy ${owner}/${slug}`, `WARN (${destroyReason}); manual cleanup hint: gh repo delete ${owner}/${slug} --yes`);
65
+ emit(`Destroy ${owner}/${slug}`, `WARN (${destroyReason}); manual cleanup: ${cleanupHint}`);
63
66
  }
64
67
  }
68
+ else {
69
+ emit(`Temp repo ${owner}/${slug}`, `KEEP (manual cleanup: ${cleanupHint})`);
70
+ }
65
71
  }
66
72
  return rehearsalRc !== EXIT_OK ? rehearsalRc : legacyBridgeRc;
67
73
  }
@@ -84,7 +90,7 @@ export function cmdReleaseE2e(args, seams = {}) {
84
90
  owner: flags.owner,
85
91
  projectRoot,
86
92
  dryRun: flags.dryRun,
87
- keepRepo: flags.keepRepo,
93
+ destroyRepo: flags.destroyRepo,
88
94
  skipNpm: flags.skipNpm,
89
95
  legacyBridge: flags.legacyBridge,
90
96
  repoSlug: null,