@deftai/directive-core 0.79.2 → 0.79.4

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 (64) hide show
  1. package/dist/check/orchestrator.d.ts +4 -0
  2. package/dist/check/orchestrator.js +9 -1
  3. package/dist/coverage-hotspots/evaluate.d.ts +49 -0
  4. package/dist/coverage-hotspots/evaluate.js +262 -0
  5. package/dist/coverage-hotspots/index.d.ts +3 -0
  6. package/dist/coverage-hotspots/index.js +3 -0
  7. package/dist/coverage-hotspots/thresholds.d.ts +5 -0
  8. package/dist/coverage-hotspots/thresholds.js +48 -0
  9. package/dist/hooks/dispatcher.d.ts +7 -0
  10. package/dist/hooks/dispatcher.js +44 -4
  11. package/dist/index.d.ts +1 -0
  12. package/dist/index.js +1 -0
  13. package/dist/init-deposit/xbrief-projections.d.ts +7 -0
  14. package/dist/init-deposit/xbrief-projections.js +29 -1
  15. package/dist/integration-e2e/bootstrap-cache-module.d.ts +2 -4
  16. package/dist/integration-e2e/bootstrap-cache-module.js +2 -20
  17. package/dist/platform/resolve-version.d.ts +2 -6
  18. package/dist/platform/resolve-version.js +3 -129
  19. package/dist/pr-merge-readiness/gh.js +13 -5
  20. package/dist/pr-merge-readiness/test-gh-fixtures.helpers.d.ts +18 -0
  21. package/dist/pr-merge-readiness/test-gh-fixtures.helpers.js +76 -0
  22. package/dist/pr-monitor/monitor.js +10 -1
  23. package/dist/pr-wait-mergeable/cascade.js +8 -1
  24. package/dist/pr-wait-mergeable/classify.js +5 -1
  25. package/dist/pr-wait-mergeable/wrappers.js +15 -4
  26. package/dist/pr-watch/constants.d.ts +15 -0
  27. package/dist/pr-watch/constants.js +37 -0
  28. package/dist/pr-watch/index.d.ts +1 -1
  29. package/dist/pr-watch/index.js +1 -1
  30. package/dist/pr-watch/main.d.ts +3 -0
  31. package/dist/pr-watch/main.js +33 -3
  32. package/dist/pr-watch/probe.js +5 -1
  33. package/dist/pr-watch/types.d.ts +2 -0
  34. package/dist/pr-watch/watch.js +17 -1
  35. package/dist/release/constants.d.ts +5 -0
  36. package/dist/release/constants.js +14 -2
  37. package/dist/release/flags.js +16 -0
  38. package/dist/release/main.js +7 -0
  39. package/dist/release/pipeline.js +7 -3
  40. package/dist/release/preflight.js +8 -1
  41. package/dist/release/skip-ci-incident.d.ts +22 -0
  42. package/dist/release/skip-ci-incident.js +68 -0
  43. package/dist/release/types.d.ts +2 -0
  44. package/dist/release/version.js +68 -32
  45. package/dist/release-e2e/entrypoint-worker.js +1 -0
  46. package/dist/release-e2e/entrypoint.js +13 -0
  47. package/dist/review-monitor/constants.d.ts +14 -0
  48. package/dist/review-monitor/constants.js +55 -0
  49. package/dist/review-monitor/index.d.ts +5 -0
  50. package/dist/review-monitor/index.js +5 -0
  51. package/dist/review-monitor/record.d.ts +50 -0
  52. package/dist/review-monitor/record.js +178 -0
  53. package/dist/review-monitor/tier-detection.d.ts +15 -0
  54. package/dist/review-monitor/tier-detection.js +61 -0
  55. package/dist/review-monitor/verify.d.ts +32 -0
  56. package/dist/review-monitor/verify.js +171 -0
  57. package/dist/scope/undo.js +12 -2
  58. package/dist/triage/bootstrap/cache-module.d.ts +25 -0
  59. package/dist/triage/bootstrap/cache-module.js +39 -0
  60. package/dist/triage/bootstrap/index.d.ts +1 -0
  61. package/dist/triage/bootstrap/index.js +7 -82
  62. package/dist/xbrief-migrate/agents-header.d.ts +0 -11
  63. package/dist/xbrief-migrate/agents-header.js +10 -1
  64. package/package.json +7 -3
@@ -13,6 +13,8 @@ export interface WatchProbe {
13
13
  readonly hasBlocking: boolean;
14
14
  readonly errored: boolean;
15
15
  readonly ciFailures: number;
16
+ /** Failed required check identities (name + conclusion), when CI was probed. */
17
+ readonly ciFailedChecks: readonly string[];
16
18
  /** All required CI check-runs have a terminal conclusion (none pending). */
17
19
  readonly terminalCheckRun: boolean;
18
20
  readonly isClean: boolean;
@@ -1,5 +1,5 @@
1
1
  import { defaultRunGh } from "../pr-merge-readiness/gh.js";
2
- import { DEFAULT_MAX_WAIT_MINUTES, DEFAULT_POLL_SECONDS, DEFAULT_STALL_THRESHOLD, EXIT_CLEAN, EXIT_NEW_P0_P1, EXIT_TERMINAL_ERROR, VERDICT_CLEAN, VERDICT_CONFIG, VERDICT_ERRORED, VERDICT_NEW_P0_P1, VERDICT_PENDING, VERDICT_STALL, VERDICT_TIMEOUT, } from "./constants.js";
2
+ import { DEFAULT_CI_BLOCKED_THRESHOLD, DEFAULT_MAX_WAIT_MINUTES, DEFAULT_POLL_SECONDS, DEFAULT_STALL_THRESHOLD, EXIT_CLEAN, EXIT_NEW_P0_P1, EXIT_TERMINAL_ERROR, VERDICT_CI_BLOCKED, VERDICT_CLEAN, VERDICT_CONFIG, VERDICT_ERRORED, VERDICT_NEW_P0_P1, VERDICT_PENDING, VERDICT_STALL, VERDICT_TIMEOUT, } from "./constants.js";
3
3
  import { probeOnce } from "./probe.js";
4
4
  const systemMonotonicClock = {
5
5
  now() {
@@ -52,6 +52,7 @@ export function watch(prNumber, repo, options = {}) {
52
52
  const startedAt = clockFn.now();
53
53
  let lastProbe = null;
54
54
  let stallStreak = 0;
55
+ let ciBlockedStreak = 0;
55
56
  const build = (verdict, exitCode, probe, poll) => ({
56
57
  verdict,
57
58
  exitCode,
@@ -79,6 +80,20 @@ export function watch(prNumber, repo, options = {}) {
79
80
  if (probe.errored) {
80
81
  return build(VERDICT_ERRORED, EXIT_TERMINAL_ERROR, probe, poll);
81
82
  }
83
+ // #2688: Greptile side satisfied on HEAD but CI red — fail loud toward a
84
+ // fix loop instead of burning max-wait-minutes on idle Greptile polls.
85
+ if (probe.cleanGateHoldout === "ci_failures") {
86
+ ciBlockedStreak += 1;
87
+ }
88
+ else {
89
+ ciBlockedStreak = 0;
90
+ }
91
+ if (oneShot && probe.cleanGateHoldout === "ci_failures") {
92
+ return build(VERDICT_CI_BLOCKED, EXIT_TERMINAL_ERROR, probe, poll);
93
+ }
94
+ if (!oneShot && ciBlockedStreak >= DEFAULT_CI_BLOCKED_THRESHOLD) {
95
+ return build(VERDICT_CI_BLOCKED, EXIT_TERMINAL_ERROR, probe, poll);
96
+ }
82
97
  // Stall = a review IS present but stuck on a non-HEAD commit; surface it
83
98
  // rather than burning the whole cap waiting for a re-review that isn't coming.
84
99
  if (probe.found && !probe.shaMatch) {
@@ -112,6 +127,7 @@ export function watch(prNumber, repo, options = {}) {
112
127
  hasBlocking: false,
113
128
  errored: false,
114
129
  ciFailures: 0,
130
+ ciFailedChecks: [],
115
131
  terminalCheckRun: false,
116
132
  isClean: false,
117
133
  cleanGateHoldout: null,
@@ -20,6 +20,11 @@ export declare const DESTRUCTIVE_GH_GATE_BYPASS_ENV = "DEFT_ALLOW_DESTRUCTIVE_GH
20
20
  export declare const RELEASE_PREFLIGHT_ENV = "DEFT_RELEASE_PREFLIGHT";
21
21
  /** Set only by release Step-5 preflight when --allow-coverage-debt=#N is supplied (#2573). */
22
22
  export declare const COVERAGE_DEBT_ENV = "DEFT_ALLOW_COVERAGE_DEBT";
23
+ /** Hard wall-clock cap for release Step 5 `task check` / vitest coverage (#2652). */
24
+ export declare const RELEASE_CHECK_TIMEOUT_MS: number;
25
+ export declare const RELEASE_CHECK_TIMEOUT_MINUTES = 20;
26
+ /** Vitest coverage step cap in GHA CI (mirrors release Step 5 budget, #2652). */
27
+ export declare const CI_VITEST_COVERAGE_TIMEOUT_MINUTES = 20;
23
28
  export declare const PYPROJECT_VERSION_LINE_RE: RegExp;
24
29
  /** Byte-identical argparse --help from scripts/release.py (Python 3.12). */
25
30
  export declare const RELEASE_HELP: string;
@@ -28,10 +28,16 @@ export const DESTRUCTIVE_GH_GATE_BYPASS_ENV = "DEFT_ALLOW_DESTRUCTIVE_GH_VERBS";
28
28
  export const RELEASE_PREFLIGHT_ENV = "DEFT_RELEASE_PREFLIGHT";
29
29
  /** Set only by release Step-5 preflight when --allow-coverage-debt=#N is supplied (#2573). */
30
30
  export const COVERAGE_DEBT_ENV = "DEFT_ALLOW_COVERAGE_DEBT";
31
+ /** Hard wall-clock cap for release Step 5 `task check` / vitest coverage (#2652). */
32
+ export const RELEASE_CHECK_TIMEOUT_MS = 20 * 60 * 1000;
33
+ export const RELEASE_CHECK_TIMEOUT_MINUTES = 20;
34
+ /** Vitest coverage step cap in GHA CI (mirrors release Step 5 budget, #2652). */
35
+ export const CI_VITEST_COVERAGE_TIMEOUT_MINUTES = 20;
31
36
  export const PYPROJECT_VERSION_LINE_RE = /version\s*=\s*"[^"]*"/;
32
37
  /** Byte-identical argparse --help from scripts/release.py (Python 3.12). */
33
38
  export const RELEASE_HELP = "usage: release [-h] [--dry-run] [--skip-tag] [--skip-release] [--allow-dirty]\n" +
34
39
  " [--allow-vbrief-drift] [--allow-coverage-debt #N]\n" +
40
+ " [--allow-skip-ci #N]\n" +
35
41
  " [--skip-ci] [--skip-build] [--no-draft]\n" +
36
42
  " [--repo OWNER/REPO] [--base-branch BRANCH]\n" +
37
43
  " [--project-root PATH] [--summary TEXT]\n" +
@@ -66,11 +72,17 @@ export const RELEASE_HELP = "usage: release [-h] [--dry-run] [--skip-tag] [--ski
66
72
  " flag cites an operator-owned issue number (#2573).\n" +
67
73
  " PowerShell: use --allow-coverage-debt=N (no bare #)\n" +
68
74
  ' or quote "#N"; unquoted # starts a comment (#2621).\n' +
69
- " --skip-ci Skip Step 3 (task ci:local / task check fallback).\n" +
75
+ " --skip-ci Skip Step 5 (task ci:local / task check fallback).\n" +
70
76
  " Used by `task release:e2e` to keep wall-clock\n" +
71
77
  " manageable inside the auto-created temp repo (CI\n" +
72
78
  " semantics are covered by the unit-test suite, not the\n" +
73
- " e2e rehearsal).\n" +
79
+ " e2e rehearsal). Production cuts MUST pass\n" +
80
+ " --allow-skip-ci=#N citing the tracked incident (#2652);\n" +
81
+ " otherwise Step 5 runs with a hard timeout.\n" +
82
+ " --allow-skip-ci #N Acknowledge skipping Step 5 on a production cut (#2652).\n" +
83
+ " Emits a loud WARN — npm ships without vitest coverage.\n" +
84
+ " PowerShell: use --allow-skip-ci=N (no bare #) or quote\n" +
85
+ ' "#N"; unquoted # starts a comment (#2621).\n' +
74
86
  " --skip-build Skip Step 6 (task build). Used by `task release:e2e`\n" +
75
87
  " to keep wall-clock manageable; build artefacts are not\n" +
76
88
  " needed for the draft-release verification step.\n" +
@@ -1,5 +1,6 @@
1
1
  import { parseCoverageDebtIssueNumber } from "../vitest-runner/coverage-debt.js";
2
2
  import { DEFAULT_BASE_BRANCH, RELEASE_HELP } from "./constants.js";
3
+ import { parseSkipCiIncidentArgv } from "./skip-ci-incident.js";
3
4
  export function parseReleaseFlags(args) {
4
5
  let help = false;
5
6
  let dryRun = false;
@@ -8,6 +9,8 @@ export function parseReleaseFlags(args) {
8
9
  let allowDirty = false;
9
10
  let allowVbriefDrift = false;
10
11
  let allowCoverageDebtIssue = null;
12
+ const skipCiIncident = parseSkipCiIncidentArgv(args);
13
+ const allowSkipCiIssue = skipCiIncident.kind === "valid" ? skipCiIncident.issue : null;
11
14
  let skipCi = false;
12
15
  let skipBuild = false;
13
16
  let draft = true;
@@ -71,6 +74,15 @@ export function parseReleaseFlags(args) {
71
74
  else if (token === "--skip-ci") {
72
75
  skipCi = true;
73
76
  }
77
+ else if (token === "--allow-skip-ci") {
78
+ // Value consumed by parseSkipCiIncidentArgv above; advance past it here.
79
+ const value = takeValue(token, i);
80
+ if (value !== null)
81
+ i += 1;
82
+ }
83
+ else if (token.startsWith("--allow-skip-ci=")) {
84
+ // Value consumed by parseSkipCiIncidentArgv above.
85
+ }
74
86
  else if (token === "--skip-build") {
75
87
  skipBuild = true;
76
88
  }
@@ -132,6 +144,9 @@ export function parseReleaseFlags(args) {
132
144
  }
133
145
  i += 1;
134
146
  }
147
+ if (skipCiIncident.kind === "invalid") {
148
+ unknown.push(`--allow-skip-ci (${skipCiIncident.reason})`);
149
+ }
135
150
  return {
136
151
  help,
137
152
  version,
@@ -144,6 +159,7 @@ export function parseReleaseFlags(args) {
144
159
  allowDirty,
145
160
  allowVbriefDrift,
146
161
  allowCoverageDebtIssue,
162
+ allowSkipCiIssue,
147
163
  skipCi,
148
164
  skipBuild,
149
165
  draft,
@@ -2,6 +2,7 @@ import { EXIT_CONFIG_ERROR } from "./constants.js";
2
2
  import { formatReleaseHelp, parseReleaseFlags } from "./flags.js";
3
3
  import { resolveProjectRoot, resolveRepo } from "./paths.js";
4
4
  import { runPipeline } from "./pipeline.js";
5
+ import { validateSkipCiIncident } from "./skip-ci-incident.js";
5
6
  import { validateVersion } from "./version.js";
6
7
  export function cmdRelease(args, seams = {}) {
7
8
  const flags = parseReleaseFlags(args);
@@ -27,6 +28,11 @@ export function cmdRelease(args, seams = {}) {
27
28
  }
28
29
  const projectRoot = resolveProjectRoot(flags.projectRoot);
29
30
  const repo = resolveRepo(flags.repo, projectRoot, seams);
31
+ const skipCiGate = validateSkipCiIncident(flags.skipCi, flags.allowSkipCiIssue, process.env);
32
+ if (skipCiGate.kind === "invalid") {
33
+ process.stderr.write(`release: error: ${skipCiGate.reason}\n`);
34
+ return EXIT_CONFIG_ERROR;
35
+ }
30
36
  const config = {
31
37
  version: flags.version,
32
38
  repo,
@@ -42,6 +48,7 @@ export function cmdRelease(args, seams = {}) {
42
48
  summary: flags.summary,
43
49
  allowVbriefDrift: flags.allowVbriefDrift,
44
50
  allowCoverageDebtIssue: flags.allowCoverageDebtIssue,
51
+ allowSkipCiIssue: flags.allowSkipCiIssue,
45
52
  };
46
53
  return runPipeline(config, seams);
47
54
  }
@@ -2,12 +2,13 @@ import { existsSync, readFileSync, writeFileSync } from "node:fs";
2
2
  import { join } from "node:path";
3
3
  import { assertProjectionContained } from "../fs/projection-containment.js";
4
4
  import { prependUpgradeBanner, promoteChangelog, sectionForVersion } from "./changelog.js";
5
- import { EXIT_CONFIG_ERROR, EXIT_OK, EXIT_VIOLATION, RELEASE_ARTIFACTS, TOTAL_STEPS, VERIFY_DRAFT_INTERVAL_SECONDS, VERIFY_DRAFT_MAX_ATTEMPTS, } from "./constants.js";
5
+ import { EXIT_CONFIG_ERROR, EXIT_OK, EXIT_VIOLATION, RELEASE_ARTIFACTS, RELEASE_CHECK_TIMEOUT_MINUTES, TOTAL_STEPS, VERIFY_DRAFT_INTERVAL_SECONDS, VERIFY_DRAFT_MAX_ATTEMPTS, } from "./constants.js";
6
6
  import { checkTagAvailable, createGithubRelease, readTextFile, verifyReleaseDraft } from "./gh.js";
7
7
  import { checkGitClean, commitReleaseArtifacts, createTag, currentBranch, pushRelease, releaseCommitSubject, } from "./git.js";
8
8
  import { checkVbriefLifecycleSyncNative, refreshRoadmapNative, runBuildNative, } from "./native-steps.js";
9
9
  import { todayIso } from "./paths.js";
10
10
  import { runReleaseCheck } from "./preflight.js";
11
+ import { formatSkipCiIncidentWarning } from "./skip-ci-incident.js";
11
12
  import { isPrereleaseTag } from "./version.js";
12
13
  export function emit(step, label, status, target = process.stderr) {
13
14
  target.write(`[${step}/${TOTAL_STEPS}] ${label}... ${status}\n`);
@@ -108,13 +109,16 @@ export function runPipeline(config, seams = {}) {
108
109
  // golden-diff release-parity gate stays green until the oracle is retired.
109
110
  label = "Pre-flight CI (task ci:local | fallback task check)";
110
111
  if (config.skipCi) {
112
+ if (config.allowSkipCiIssue !== null && config.allowSkipCiIssue > 0) {
113
+ process.stderr.write(formatSkipCiIncidentWarning(config.allowSkipCiIssue));
114
+ }
111
115
  emit(5, label, "SKIP (--skip-ci)");
112
116
  }
113
117
  else if (config.dryRun) {
114
118
  const debtNote = config.allowCoverageDebtIssue !== null
115
119
  ? `; would pass --allow-coverage-debt=#${config.allowCoverageDebtIssue} to task check`
116
120
  : "";
117
- emit(5, label, `DRYRUN (would run task ci:local with task check fallback${debtNote})`);
121
+ emit(5, label, `DRYRUN (would run task ci:local with task check fallback${debtNote}; hard timeout ${RELEASE_CHECK_TIMEOUT_MINUTES}m)`);
118
122
  }
119
123
  else {
120
124
  const [ok, reason] = runCiFn(projectRoot, config.allowCoverageDebtIssue);
@@ -128,7 +132,7 @@ export function runPipeline(config, seams = {}) {
128
132
  const debtHint = config.allowCoverageDebtIssue === null
129
133
  ? "; pass --allow-coverage-debt=#N only after operator review"
130
134
  : "";
131
- emit(5, label, `FAIL (${reason}${debtHint})`);
135
+ emit(5, label, `FAIL (${reason}${debtHint}; Step 5 hard timeout is ${RELEASE_CHECK_TIMEOUT_MINUTES}m — cancel hung vitest and see docs/RELEASING.md § Vitest coverage hang recovery)`);
132
136
  return EXIT_VIOLATION;
133
137
  }
134
138
  }
@@ -8,7 +8,7 @@
8
8
  * dispatches `check:framework-source`.
9
9
  */
10
10
  import { dispatchTaskCheck } from "../check/orchestrator.js";
11
- import { COVERAGE_DEBT_ENV, RELEASE_PREFLIGHT_ENV } from "./constants.js";
11
+ import { COVERAGE_DEBT_ENV, RELEASE_CHECK_TIMEOUT_MS, 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
14
  export function releaseCheckEnv(options = {}) {
@@ -40,6 +40,7 @@ export function runReleaseCheck(projectRoot, seams = {}, allowCoverageDebtIssue
40
40
  const dispatch = seams.dispatchCheck ?? dispatchTaskCheck;
41
41
  const checkSeams = {
42
42
  ...seams.checkSeams,
43
+ timeoutMs: seams.checkSeams?.timeoutMs ?? RELEASE_CHECK_TIMEOUT_MS,
43
44
  env: releaseCheckEnv({
44
45
  base: seams.checkSeams?.env ?? process.env,
45
46
  allowCoverageDebtIssue,
@@ -49,6 +50,12 @@ export function runReleaseCheck(projectRoot, seams = {}, allowCoverageDebtIssue
49
50
  if (code === 0) {
50
51
  return [true, "ran native TypeScript task check"];
51
52
  }
53
+ if (code === 124) {
54
+ return [
55
+ false,
56
+ `task check timed out after ${RELEASE_CHECK_TIMEOUT_MS / 60_000}m (vitest coverage hang — see docs/RELEASING.md)`,
57
+ ];
58
+ }
52
59
  return [false, `task check failed (exit ${code})`];
53
60
  }
54
61
  //# sourceMappingURL=preflight.js.map
@@ -0,0 +1,22 @@
1
+ /** Parse `#2652`, `2652`, or bare numeric strings for skip-ci incident citation. */
2
+ export declare function parseSkipCiIncidentIssueNumber(raw: string): number | null;
3
+ export type SkipCiIncidentResolution = {
4
+ readonly kind: "none";
5
+ } | {
6
+ readonly kind: "valid";
7
+ readonly issue: number;
8
+ } | {
9
+ readonly kind: "invalid";
10
+ readonly reason: string;
11
+ };
12
+ /** Set by `task release:e2e` worker subprocesses — permits `--skip-ci` without issue citation. */
13
+ export declare const RELEASE_E2E_ENV = "DEFT_RELEASE_E2E";
14
+ export declare function parseSkipCiIncidentArgv(argv: readonly string[]): SkipCiIncidentResolution;
15
+ /**
16
+ * Production `--skip-ci` is an incident (#2652): require `--allow-skip-ci=#N` or
17
+ * run inside `task release:e2e` (`DEFT_RELEASE_E2E=1`).
18
+ */
19
+ export declare function validateSkipCiIncident(skipCi: boolean, allowSkipCiIssue: number | null, env?: NodeJS.ProcessEnv): SkipCiIncidentResolution;
20
+ /** Loud stderr banner when Step 5 is skipped with operator acknowledgment (#2652). */
21
+ export declare function formatSkipCiIncidentWarning(issue: number): string;
22
+ //# sourceMappingURL=skip-ci-incident.d.ts.map
@@ -0,0 +1,68 @@
1
+ /** Parse `#2652`, `2652`, or bare numeric strings for skip-ci incident citation. */
2
+ export function parseSkipCiIncidentIssueNumber(raw) {
3
+ const trimmed = raw.trim();
4
+ if (!trimmed)
5
+ return null;
6
+ const normalized = trimmed.startsWith("#") ? trimmed.slice(1) : trimmed;
7
+ if (!/^\d+$/.test(normalized))
8
+ return null;
9
+ const issue = Number.parseInt(normalized, 10);
10
+ return Number.isFinite(issue) && issue > 0 ? issue : null;
11
+ }
12
+ const SKIP_CI_FLAG = "--allow-skip-ci";
13
+ /** Set by `task release:e2e` worker subprocesses — permits `--skip-ci` without issue citation. */
14
+ export const RELEASE_E2E_ENV = "DEFT_RELEASE_E2E";
15
+ export function parseSkipCiIncidentArgv(argv) {
16
+ for (let i = 0; i < argv.length; i += 1) {
17
+ const token = argv[i] ?? "";
18
+ if (token === SKIP_CI_FLAG) {
19
+ const next = argv[i + 1];
20
+ if (next === undefined || next.startsWith("-")) {
21
+ return { kind: "invalid", reason: `${SKIP_CI_FLAG} requires an issue number (#N)` };
22
+ }
23
+ const issue = parseSkipCiIncidentIssueNumber(next);
24
+ return issue === null
25
+ ? { kind: "invalid", reason: `${SKIP_CI_FLAG} value must be #N or N` }
26
+ : { kind: "valid", issue };
27
+ }
28
+ if (token.startsWith(`${SKIP_CI_FLAG}=`)) {
29
+ const value = token.slice(SKIP_CI_FLAG.length + 1);
30
+ const issue = parseSkipCiIncidentIssueNumber(value);
31
+ return issue === null
32
+ ? { kind: "invalid", reason: `${SKIP_CI_FLAG}= value must be #N or N` }
33
+ : { kind: "valid", issue };
34
+ }
35
+ }
36
+ return { kind: "none" };
37
+ }
38
+ /**
39
+ * Production `--skip-ci` is an incident (#2652): require `--allow-skip-ci=#N` or
40
+ * run inside `task release:e2e` (`DEFT_RELEASE_E2E=1`).
41
+ */
42
+ export function validateSkipCiIncident(skipCi, allowSkipCiIssue, env = process.env) {
43
+ if (!skipCi) {
44
+ return { kind: "none" };
45
+ }
46
+ if (allowSkipCiIssue !== null) {
47
+ return { kind: "valid", issue: allowSkipCiIssue };
48
+ }
49
+ if (env[RELEASE_E2E_ENV] === "1") {
50
+ return { kind: "valid", issue: 0 };
51
+ }
52
+ return {
53
+ kind: "invalid",
54
+ reason: "production --skip-ci skips Step 5 vitest coverage and ships untested npm builds (#2652). " +
55
+ "Pass --allow-skip-ci=#N citing the tracked incident after operator review, " +
56
+ "or use `task release:e2e` for rehearsal-only skips.",
57
+ };
58
+ }
59
+ /** Loud stderr banner when Step 5 is skipped with operator acknowledgment (#2652). */
60
+ export function formatSkipCiIncidentWarning(issue) {
61
+ const cite = issue > 0 ? `#${issue}` : "release:e2e rehearsal";
62
+ return (`\n` +
63
+ `*** WARNING: release Step 5 CI/coverage SKIPPED (--skip-ci) ***\n` +
64
+ `*** This cut will NOT be validated by vitest coverage / task check. ***\n` +
65
+ `*** Incident citation: ${cite} — npm publish proceeds UNTESTED (#2652). ***\n` +
66
+ `*** Next production patch MUST cut without --skip-ci once the hang is fixed. ***\n\n`);
67
+ }
68
+ //# sourceMappingURL=skip-ci-incident.js.map
@@ -13,6 +13,7 @@ export interface ReleaseConfig {
13
13
  readonly summary: string | null;
14
14
  readonly allowVbriefDrift: boolean;
15
15
  readonly allowCoverageDebtIssue: number | null;
16
+ readonly allowSkipCiIssue: number | null;
16
17
  }
17
18
  export interface ReleaseFlags {
18
19
  readonly help: boolean;
@@ -30,6 +31,7 @@ export interface ReleaseFlags {
30
31
  readonly draft: boolean;
31
32
  readonly summary: string | null;
32
33
  readonly allowCoverageDebtIssue: number | null;
34
+ readonly allowSkipCiIssue: number | null;
33
35
  readonly unknown: readonly string[];
34
36
  }
35
37
  export interface SpawnResult {
@@ -1,4 +1,3 @@
1
- const PEP440_TAG_RE = /^v?(?<major>\d+)\.(?<minor>\d+)\.(?<patch>\d+)(?:-(?<kind>rc|alpha|beta|test)\.(?<num>\d+))?$/;
2
1
  const PRE_KIND_MAP = {
3
2
  alpha: "a",
4
3
  beta: "b",
@@ -17,6 +16,55 @@ export class NonPublishableVersionError extends Error {
17
16
  this.name = "NonPublishableVersionError";
18
17
  }
19
18
  }
19
+ /** Parse `[v]X.Y.Z[-(rc|alpha|beta|test).N]` via linear scan. */
20
+ function parseReleaseTag(version) {
21
+ let i = 0;
22
+ if (version[i] === "v" || version[i] === "V")
23
+ i += 1;
24
+ const readInt = () => {
25
+ const ch = version[i] ?? "";
26
+ if (i >= version.length || ch < "0" || ch > "9")
27
+ return null;
28
+ let n = 0;
29
+ while (i < version.length) {
30
+ const digit = version[i] ?? "";
31
+ if (digit < "0" || digit > "9")
32
+ break;
33
+ n = n * 10 + Number(digit);
34
+ i += 1;
35
+ }
36
+ return n;
37
+ };
38
+ const major = readInt();
39
+ if (major === null || version[i] !== ".")
40
+ return null;
41
+ i += 1;
42
+ const minor = readInt();
43
+ if (minor === null || version[i] !== ".")
44
+ return null;
45
+ i += 1;
46
+ const patch = readInt();
47
+ if (patch === null)
48
+ return null;
49
+ if (i >= version.length)
50
+ return { major, minor, patch, kind: null, num: null };
51
+ if (version[i] !== "-")
52
+ return null;
53
+ i += 1;
54
+ const kindStart = i;
55
+ while (i < version.length && version[i] !== ".")
56
+ i += 1;
57
+ if (i >= version.length || version[i] !== ".")
58
+ return null;
59
+ const kind = version.slice(kindStart, i);
60
+ if (!["rc", "alpha", "beta", "test"].includes(kind))
61
+ return null;
62
+ i += 1;
63
+ const num = readInt();
64
+ if (num === null || i !== version.length)
65
+ return null;
66
+ return { major, minor, patch, kind, num };
67
+ }
20
68
  /** Raise Error when version does not match strict X.Y.Z semver. */
21
69
  export function validateVersion(version) {
22
70
  if (!/^\d+\.\d+\.\d+$/.test(version)) {
@@ -27,7 +75,7 @@ export function validateVersion(version) {
27
75
  /** Return true when version carries a SemVer pre-release suffix (#425). */
28
76
  export function isPrereleaseTag(version) {
29
77
  let candidate = version.trim();
30
- if (candidate.startsWith("v")) {
78
+ if (candidate.startsWith("v") || candidate.startsWith("V")) {
31
79
  candidate = candidate.slice(1);
32
80
  }
33
81
  return candidate.includes("-");
@@ -41,32 +89,26 @@ export function toPep440(version) {
41
89
  if (!candidate) {
42
90
  throw new Error("version must be a non-empty string");
43
91
  }
44
- const match = PEP440_TAG_RE.exec(candidate);
45
- if (match?.groups === undefined) {
92
+ const parsed = parseReleaseTag(candidate);
93
+ if (parsed === null) {
46
94
  throw new Error(`Cannot normalize '${candidate}' to PEP 440: expected ` +
47
95
  "[v]X.Y.Z or [v]X.Y.Z-(rc|alpha|beta|test).N");
48
96
  }
49
- const major = Number(match.groups.major);
50
- const minor = Number(match.groups.minor);
51
- const patch = Number(match.groups.patch);
52
- const base = `${major}.${minor}.${patch}`;
53
- const kind = match.groups.kind;
54
- if (kind === undefined) {
97
+ const base = `${parsed.major}.${parsed.minor}.${parsed.patch}`;
98
+ if (parsed.kind === null)
55
99
  return base;
56
- }
57
- if (NON_PUBLISHABLE_KINDS.has(kind)) {
100
+ if (NON_PUBLISHABLE_KINDS.has(parsed.kind)) {
58
101
  throw new NonPublishableVersionError(`Version '${candidate}' carries non-publishable pre-release ` +
59
- `tag '${kind}'.${match.groups.num} -- release pipeline MUST ` +
102
+ `tag '${parsed.kind}'.${parsed.num} -- release pipeline MUST ` +
60
103
  "skip pyproject.toml [project].version sync for this tag.");
61
104
  }
62
- const pepKind = PRE_KIND_MAP[kind];
105
+ const pepKind = PRE_KIND_MAP[parsed.kind];
63
106
  if (pepKind === undefined) {
64
- throw new Error(`Unmapped pre-release kind '${kind}' for version '${candidate}'; ` +
65
- "add it to _PRE_KIND_MAP or _NON_PUBLISHABLE_KINDS to keep " +
66
- "_PEP440_TAG_RE in lockstep with the publishability classifier.");
107
+ throw new Error(`Unmapped pre-release kind '${parsed.kind}' for version '${candidate}'; ` +
108
+ "add it to PRE_KIND_MAP or NON_PUBLISHABLE_KINDS to keep the parser " +
109
+ "in lockstep with the publishability classifier.");
67
110
  }
68
- const pepNum = Number(match.groups.num);
69
- return `${base}${pepKind}${pepNum}`;
111
+ return `${base}${pepKind}${parsed.num}`;
70
112
  }
71
113
  export function isPublishable(version) {
72
114
  try {
@@ -79,25 +121,19 @@ export function isPublishable(version) {
79
121
  }
80
122
  function publishableVersionSortKey(version) {
81
123
  const candidate = version.trim();
82
- const match = PEP440_TAG_RE.exec(candidate);
83
- if (match?.groups === undefined) {
84
- throw new Error(`Cannot compare '${candidate}': expected ` + "[v]X.Y.Z or [v]X.Y.Z-(rc|alpha|beta).N");
124
+ const parsed = parseReleaseTag(candidate);
125
+ if (parsed === null) {
126
+ throw new Error(`Cannot compare '${candidate}': expected [v]X.Y.Z or [v]X.Y.Z-(rc|alpha|beta).N`);
85
127
  }
86
- const kind = match.groups.kind ?? "";
87
- if (NON_PUBLISHABLE_KINDS.has(kind)) {
88
- throw new NonPublishableVersionError(`Version '${candidate}' carries non-publishable pre-release tag '${kind}'.${match.groups.num}.`);
128
+ if (parsed.kind !== null && NON_PUBLISHABLE_KINDS.has(parsed.kind)) {
129
+ throw new NonPublishableVersionError(`Version '${candidate}' carries non-publishable pre-release tag '${parsed.kind}'.${parsed.num}.`);
89
130
  }
131
+ const kind = parsed.kind ?? "";
90
132
  const rank = PRERELEASE_RANK[kind];
91
133
  if (rank === undefined) {
92
134
  throw new Error(`Cannot compare '${candidate}': unsupported pre-release kind '${kind}'.`);
93
135
  }
94
- return [
95
- Number(match.groups.major),
96
- Number(match.groups.minor),
97
- Number(match.groups.patch),
98
- rank,
99
- Number(match.groups.num ?? 0),
100
- ];
136
+ return [parsed.major, parsed.minor, parsed.patch, rank, parsed.num ?? 0];
101
137
  }
102
138
  /** Compare two publishable Deft release versions using stable/prerelease ordering. */
103
139
  export function comparePublishableVersions(left, right) {
@@ -2,6 +2,7 @@ import { cmdRelease } from "../release/main.js";
2
2
  import { rollbackMain } from "./rollback-bridge.js";
3
3
  export function runWorkerEntrypoint(data) {
4
4
  process.env.DEFT_PROJECT_ROOT = data.cloneDir;
5
+ process.env.DEFT_RELEASE_E2E = "1";
5
6
  const stdoutChunks = [];
6
7
  const stderrChunks = [];
7
8
  const prevStdout = process.stdout.write.bind(process.stdout);
@@ -3,9 +3,11 @@ import { sep } from "node:path";
3
3
  import { fileURLToPath } from "node:url";
4
4
  import { MessageChannel, receiveMessageOnPort, Worker } from "node:worker_threads";
5
5
  import { cmdRelease } from "../release/main.js";
6
+ import { RELEASE_E2E_ENV } from "../release/skip-ci-incident.js";
6
7
  import { ENTRYPOINT_TIMEOUT_EXIT_CODE, EXIT_VIOLATION, RELEASE_ENTRYPOINT_TIMEOUT_SECONDS, ROLLBACK_ENTRYPOINT_TIMEOUT_SECONDS, } from "./constants.js";
7
8
  import { rollbackMain } from "./rollback-bridge.js";
8
9
  let activeRestoreOwner = null;
10
+ let priorReleaseE2eEnv;
9
11
  /** @internal Test hook for restore-owner branch coverage. */
10
12
  export function restoreProcessStateForTest(restoreOwner, oldCwd, oldProjectRoot) {
11
13
  restoreProcessState(restoreOwner, oldCwd, oldProjectRoot);
@@ -22,9 +24,18 @@ function restoreProcessState(restoreOwner, oldCwd, oldProjectRoot) {
22
24
  else {
23
25
  process.env.DEFT_PROJECT_ROOT = oldProjectRoot;
24
26
  }
27
+ if (priorReleaseE2eEnv === undefined) {
28
+ delete process.env[RELEASE_E2E_ENV];
29
+ }
30
+ else {
31
+ process.env[RELEASE_E2E_ENV] = priorReleaseE2eEnv;
32
+ }
33
+ priorReleaseE2eEnv = undefined;
25
34
  }
26
35
  function activateProcessState(restoreOwner, cloneDir) {
27
36
  activeRestoreOwner = restoreOwner;
37
+ priorReleaseE2eEnv = process.env[RELEASE_E2E_ENV];
38
+ process.env[RELEASE_E2E_ENV] = "1";
28
39
  process.env.DEFT_PROJECT_ROOT = cloneDir;
29
40
  mkdirSync(cloneDir, { recursive: true });
30
41
  process.chdir(cloneDir);
@@ -223,6 +234,8 @@ export function defaultRollbackEntrypoint(argv) {
223
234
  return rollbackMain(argv);
224
235
  }
225
236
  export function dispatchTaskRelease(cloneDir, version, repo, seams = {}) {
237
+ // Rehearsal --skip-ci is permitted via DEFT_RELEASE_E2E (set in activateProcessState /
238
+ // worker), not a fake production --allow-skip-ci citation (#2652 Greptile).
226
239
  const argv = [version, "--repo", repo, "--skip-ci", "--skip-build", "--allow-vbrief-drift"];
227
240
  if (seams.releaseEntrypoint) {
228
241
  const code = seams.releaseEntrypoint(argv);
@@ -0,0 +1,14 @@
1
+ /** Deterministic review-monitor gate (#2655). Three-state exit contract. */
2
+ export declare const EXIT_READY = 0;
3
+ export declare const EXIT_NOT_READY = 1;
4
+ export declare const EXIT_CONFIG_ERROR = 2;
5
+ export declare const SCHEMA_VERSION = 1;
6
+ export declare const REVIEW_MONITOR_FILENAME = "review-monitor.json";
7
+ export declare const REVIEW_MONITOR_RELPATH: readonly [".deft", "review-monitor.json"];
8
+ export declare const MONITORING_TIER_1 = 1;
9
+ export declare const MONITORING_TIER_2 = 2;
10
+ export declare const MONITORING_TIER_3 = 3;
11
+ export declare const DEFAULT_STALE_MINUTES = 30;
12
+ export declare const REVIEW_MONITOR_HELP: string;
13
+ export declare const REGISTER_HELP: string;
14
+ //# sourceMappingURL=constants.d.ts.map
@@ -0,0 +1,55 @@
1
+ /** Deterministic review-monitor gate (#2655). Three-state exit contract. */
2
+ export const EXIT_READY = 0;
3
+ export const EXIT_NOT_READY = 1;
4
+ export const EXIT_CONFIG_ERROR = 2;
5
+ export const SCHEMA_VERSION = 1;
6
+ export const REVIEW_MONITOR_FILENAME = "review-monitor.json";
7
+ export const REVIEW_MONITOR_RELPATH = [".deft", REVIEW_MONITOR_FILENAME];
8
+ export const MONITORING_TIER_1 = 1;
9
+ export const MONITORING_TIER_2 = 2;
10
+ export const MONITORING_TIER_3 = 3;
11
+ export const DEFAULT_STALE_MINUTES = 30;
12
+ export const REVIEW_MONITOR_HELP = "usage: task verify:review-monitor -- --pr <N> [options]\n" +
13
+ "\n" +
14
+ "Fail-closed gate: when Tier 1 (sub-agent primitive available), require a\n" +
15
+ "recorded active review-monitor before the parent may yield, Approach-3\n" +
16
+ "sleep-poll, or claim review ownership (#2655).\n" +
17
+ "\n" +
18
+ "options:\n" +
19
+ " -h, --help Show this help and exit 0\n" +
20
+ " --pr N Pull request number (required unless --help)\n" +
21
+ " --repo OWNER/REPO Repository (optional audit field)\n" +
22
+ " --head-sha SHA Expected HEAD SHA (optional freshness check)\n" +
23
+ " --project-root PATH Project root (default: cwd)\n" +
24
+ " --call-site SITE solo | swarm-phase5-6 | swarm-phase6-cascade\n" +
25
+ " --approach3 Assert Approach 3 (blocking poll) intent\n" +
26
+ " --approach3-warned User acknowledged Approach 3 warning (Tier 3 only)\n" +
27
+ " --json Emit structured JSON on stdout\n" +
28
+ "\n" +
29
+ "exit codes:\n" +
30
+ " 0 READY Tier 1 monitor recorded, or Tier 3 Approach 3 with warning ack\n" +
31
+ " 1 NOT READY Tier 1 without monitor, or Approach 3 blocked on Tier 1\n" +
32
+ " 2 CONFIG Usage / path / unreadable state error\n" +
33
+ "\n" +
34
+ "Register a monitor after spawning Approach 1:\n" +
35
+ " task review-monitor:register -- --pr <N> --monitor-agent-id <id> \\\n" +
36
+ " --platform-primitive cursor-task|spawn_subagent|start_agent \\\n" +
37
+ " [--head-sha SHA] [--repo OWNER/REPO]\n";
38
+ export const REGISTER_HELP = "usage: task review-monitor:register -- --pr <N> --monitor-agent-id <id> \\\n" +
39
+ " --platform-primitive <primitive> [options]\n" +
40
+ "\n" +
41
+ "Record an active review-monitor after spawning an Approach 1 poller (#2655).\n" +
42
+ "\n" +
43
+ "required:\n" +
44
+ " --pr N Pull request number\n" +
45
+ " --monitor-agent-id ID Stable poller agent id / Task handle\n" +
46
+ " --platform-primitive P start_agent | spawn_subagent | cursor-task\n" +
47
+ "\n" +
48
+ "options:\n" +
49
+ " --repo OWNER/REPO Repository\n" +
50
+ " --head-sha SHA HEAD SHA at monitor start\n" +
51
+ " --project-root PATH Project root (default: cwd)\n" +
52
+ " --parent-session-id ID Optional parent ritual / session id\n" +
53
+ "\n" +
54
+ "exit codes: 0 registered / 2 config error\n";
55
+ //# sourceMappingURL=constants.js.map
@@ -0,0 +1,5 @@
1
+ export * from "./constants.js";
2
+ export * from "./record.js";
3
+ export * from "./tier-detection.js";
4
+ export * from "./verify.js";
5
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,5 @@
1
+ export * from "./constants.js";
2
+ export * from "./record.js";
3
+ export * from "./tier-detection.js";
4
+ export * from "./verify.js";
5
+ //# sourceMappingURL=index.js.map