@deftai/directive-core 0.79.2 → 0.79.3

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 (37) hide show
  1. package/dist/check/orchestrator.d.ts +4 -0
  2. package/dist/check/orchestrator.js +9 -1
  3. package/dist/platform/resolve-version.d.ts +2 -6
  4. package/dist/platform/resolve-version.js +3 -129
  5. package/dist/pr-merge-readiness/gh.js +13 -5
  6. package/dist/pr-merge-readiness/test-gh-fixtures.helpers.d.ts +18 -0
  7. package/dist/pr-merge-readiness/test-gh-fixtures.helpers.js +76 -0
  8. package/dist/pr-monitor/monitor.js +10 -1
  9. package/dist/pr-watch/constants.d.ts +5 -0
  10. package/dist/pr-watch/constants.js +27 -0
  11. package/dist/pr-watch/index.d.ts +1 -1
  12. package/dist/pr-watch/index.js +1 -1
  13. package/dist/pr-watch/main.d.ts +3 -0
  14. package/dist/pr-watch/main.js +29 -3
  15. package/dist/release/constants.d.ts +5 -0
  16. package/dist/release/constants.js +14 -2
  17. package/dist/release/flags.js +16 -0
  18. package/dist/release/main.js +7 -0
  19. package/dist/release/pipeline.js +7 -3
  20. package/dist/release/preflight.js +8 -1
  21. package/dist/release/skip-ci-incident.d.ts +22 -0
  22. package/dist/release/skip-ci-incident.js +68 -0
  23. package/dist/release/types.d.ts +2 -0
  24. package/dist/release/version.js +68 -32
  25. package/dist/release-e2e/entrypoint-worker.js +1 -0
  26. package/dist/release-e2e/entrypoint.js +13 -0
  27. package/dist/review-monitor/constants.d.ts +14 -0
  28. package/dist/review-monitor/constants.js +55 -0
  29. package/dist/review-monitor/index.d.ts +5 -0
  30. package/dist/review-monitor/index.js +5 -0
  31. package/dist/review-monitor/record.d.ts +50 -0
  32. package/dist/review-monitor/record.js +178 -0
  33. package/dist/review-monitor/tier-detection.d.ts +15 -0
  34. package/dist/review-monitor/tier-detection.js +61 -0
  35. package/dist/review-monitor/verify.d.ts +32 -0
  36. package/dist/review-monitor/verify.js +171 -0
  37. package/package.json +7 -3
@@ -20,12 +20,16 @@ export interface CheckOrchestratorSeams {
20
20
  cwd: string;
21
21
  stdio: string;
22
22
  env?: NodeJS.ProcessEnv;
23
+ timeoutMs?: number;
23
24
  }) => {
24
25
  status: number | null;
26
+ signal?: NodeJS.Signals | null;
25
27
  error?: Error;
26
28
  };
27
29
  /** Child-process environment (default: process.env). */
28
30
  readonly env?: NodeJS.ProcessEnv;
31
+ /** Wall-clock spawn timeout in milliseconds (default: none). */
32
+ readonly timeoutMs?: number;
29
33
  }
30
34
  /**
31
35
  * True when `path` is the directive framework source checkout root (not a
@@ -78,11 +78,16 @@ export function dispatchTaskCheck(frameworkRoot, projectRoot, seams = {}) {
78
78
  cwd,
79
79
  stdio: "inherit",
80
80
  env: seams.env,
81
+ timeoutMs: seams.timeoutMs,
81
82
  });
82
83
  if (result.error !== undefined) {
83
84
  process.stderr.write(`check: failed to invoke task: ${result.error.message}\n`);
84
85
  return 2;
85
86
  }
87
+ if (result.signal === "SIGTERM" && result.status === null && seams.timeoutMs !== undefined) {
88
+ process.stderr.write(`check: timed out after ${seams.timeoutMs / 60_000}m (Step 5 vitest coverage budget; #2652)\n`);
89
+ return 124;
90
+ }
86
91
  return result.status ?? 1;
87
92
  }
88
93
  function defaultSpawn(cmd, args, opts) {
@@ -90,7 +95,10 @@ function defaultSpawn(cmd, args, opts) {
90
95
  cwd: opts.cwd,
91
96
  stdio: opts.stdio,
92
97
  env: opts.env ?? process.env,
98
+ ...(opts.timeoutMs !== undefined
99
+ ? { timeout: opts.timeoutMs, killSignal: "SIGTERM" }
100
+ : {}),
93
101
  });
94
- return { status: result.status, error: result.error };
102
+ return { status: result.status, signal: result.signal, error: result.error };
95
103
  }
96
104
  //# sourceMappingURL=orchestrator.js.map
@@ -1,10 +1,6 @@
1
+ import { isPublishable, NonPublishableVersionError, toPep440 } from "../release/version.js";
1
2
  import { DEV_FALLBACK, ENV_VAR } from "./constants.js";
2
- export { DEV_FALLBACK, ENV_VAR };
3
- export declare class NonPublishableVersionError extends Error {
4
- constructor(message: string);
5
- }
6
- export declare function toPep440(version: string): string;
7
- export declare function isPublishable(version: string): boolean;
3
+ export { DEV_FALLBACK, ENV_VAR, isPublishable, NonPublishableVersionError, toPep440 };
8
4
  export declare function tagNameFromRef(ref: string): string;
9
5
  export declare function latestPublishableTag(tags: Iterable<string>): string | null;
10
6
  export declare function payloadIsOwnGitRoot(payloadDir: string): boolean;
@@ -2,113 +2,15 @@ import { execFileSync } from "node:child_process";
2
2
  import { existsSync, readFileSync } from "node:fs";
3
3
  import { dirname, join, resolve } from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
+ import { comparePublishableVersions, isPublishable, NonPublishableVersionError, toPep440, } from "../release/version.js";
5
6
  import { DEV_FALLBACK, ENV_VAR } from "./constants.js";
6
- export { DEV_FALLBACK, ENV_VAR };
7
- export class NonPublishableVersionError extends Error {
8
- constructor(message) {
9
- super(message);
10
- this.name = "NonPublishableVersionError";
11
- }
12
- }
13
- const PRE_KIND_MAP = {
14
- alpha: "a",
15
- beta: "b",
16
- rc: "rc",
17
- };
18
- const NON_PUBLISHABLE_KINDS = new Set(["test"]);
19
- const PRERELEASE_RANK = {
20
- alpha: 0,
21
- beta: 1,
22
- rc: 2,
23
- "": 3,
24
- };
7
+ export { DEV_FALLBACK, ENV_VAR, isPublishable, NonPublishableVersionError, toPep440 };
25
8
  function frameworkRoot() {
26
9
  const envRoot = process.env.DEFT_ROOT?.trim();
27
10
  if (envRoot)
28
11
  return resolve(envRoot);
29
12
  return resolve(dirname(fileURLToPath(import.meta.url)), "..", "..", "..", "..");
30
13
  }
31
- /** Parse `[v]X.Y.Z[-(rc|alpha|beta|test).N]` via linear scan. */
32
- function parsePep440Tag(version) {
33
- let i = 0;
34
- if (version[i] === "v" || version[i] === "V")
35
- i += 1;
36
- const readInt = () => {
37
- const ch = version[i] ?? "";
38
- if (i >= version.length || ch < "0" || ch > "9")
39
- return null;
40
- let n = 0;
41
- while (i < version.length) {
42
- const digit = version[i] ?? "";
43
- if (digit < "0" || digit > "9")
44
- break;
45
- n = n * 10 + Number(digit);
46
- i += 1;
47
- }
48
- return n;
49
- };
50
- const major = readInt();
51
- if (major === null || version[i] !== ".")
52
- return null;
53
- i += 1;
54
- const minor = readInt();
55
- if (minor === null || version[i] !== ".")
56
- return null;
57
- i += 1;
58
- const patch = readInt();
59
- if (patch === null)
60
- return null;
61
- if (i >= version.length)
62
- return { major, minor, patch, kind: null, num: null };
63
- if (version[i] !== "-")
64
- return null;
65
- i += 1;
66
- const kindStart = i;
67
- while (i < version.length && version[i] !== ".")
68
- i += 1;
69
- if (i >= version.length || version[i] !== ".")
70
- return null;
71
- const kind = version.slice(kindStart, i);
72
- if (!["rc", "alpha", "beta", "test"].includes(kind))
73
- return null;
74
- i += 1;
75
- const num = readInt();
76
- if (num === null || i !== version.length)
77
- return null;
78
- return { major, minor, patch, kind, num };
79
- }
80
- export function toPep440(version) {
81
- if (typeof version !== "string") {
82
- throw new Error(`version must be a string, got ${typeof version}`);
83
- }
84
- const candidate = version.trim();
85
- if (!candidate)
86
- throw new Error("version must be a non-empty string");
87
- const parsed = parsePep440Tag(candidate);
88
- if (parsed === null) {
89
- throw new Error(`Cannot normalize ${JSON.stringify(candidate)} to PEP 440: expected [v]X.Y.Z or [v]X.Y.Z-(rc|alpha|beta|test).N`);
90
- }
91
- const base = `${parsed.major}.${parsed.minor}.${parsed.patch}`;
92
- if (parsed.kind === null)
93
- return base;
94
- if (NON_PUBLISHABLE_KINDS.has(parsed.kind)) {
95
- throw new NonPublishableVersionError(`Version ${JSON.stringify(candidate)} carries non-publishable pre-release tag ${JSON.stringify(parsed.kind)}.${parsed.num} -- release pipeline MUST skip pyproject.toml [project].version sync for this tag.`);
96
- }
97
- const pepKind = PRE_KIND_MAP[parsed.kind];
98
- if (pepKind === undefined) {
99
- throw new Error(`Unmapped pre-release kind ${JSON.stringify(parsed.kind)} for version ${JSON.stringify(candidate)}; add it to PRE_KIND_MAP or NON_PUBLISHABLE_KINDS to keep parser in lockstep with the publishability classifier.`);
100
- }
101
- return `${base}${pepKind}${parsed.num}`;
102
- }
103
- export function isPublishable(version) {
104
- try {
105
- toPep440(version);
106
- return true;
107
- }
108
- catch {
109
- return false;
110
- }
111
- }
112
14
  export function tagNameFromRef(ref) {
113
15
  let candidate = ref.trim();
114
16
  if (!candidate)
@@ -123,49 +25,21 @@ export function tagNameFromRef(ref) {
123
25
  candidate = candidate.slice(prefix.length);
124
26
  return candidate.trim();
125
27
  }
126
- function publishableTagSortKey(version) {
127
- const candidate = version.trim();
128
- const parsed = parsePep440Tag(candidate);
129
- if (parsed === null) {
130
- throw new Error(`Cannot sort ${JSON.stringify(candidate)}: expected [v]X.Y.Z or [v]X.Y.Z-(rc|alpha|beta).N`);
131
- }
132
- if (parsed.kind !== null && NON_PUBLISHABLE_KINDS.has(parsed.kind)) {
133
- throw new NonPublishableVersionError(`Version ${JSON.stringify(candidate)} carries non-publishable pre-release tag ${JSON.stringify(parsed.kind)}.`);
134
- }
135
- const kind = parsed.kind ?? "";
136
- const prereleaseRank = PRERELEASE_RANK[kind];
137
- if (prereleaseRank === undefined) {
138
- throw new Error(`Unmapped pre-release kind ${JSON.stringify(parsed.kind)} for ${JSON.stringify(candidate)}`);
139
- }
140
- return [parsed.major, parsed.minor, parsed.patch, prereleaseRank, parsed.num ?? 0];
141
- }
142
28
  export function latestPublishableTag(tags) {
143
29
  let bestTag = null;
144
- let bestKey = null;
145
30
  for (const raw of tags) {
146
31
  const tag = tagNameFromRef(raw);
147
32
  if (!tag || !isPublishable(tag))
148
33
  continue;
149
34
  try {
150
- const key = publishableTagSortKey(tag);
151
- if (bestKey === null || compareTuple(key, bestKey) > 0) {
35
+ if (bestTag === null || comparePublishableVersions(tag, bestTag) > 0) {
152
36
  bestTag = tag;
153
- bestKey = key;
154
37
  }
155
38
  }
156
39
  catch { }
157
40
  }
158
41
  return bestTag;
159
42
  }
160
- function compareTuple(a, b) {
161
- for (let i = 0; i < a.length; i += 1) {
162
- const av = a[i] ?? 0;
163
- const bv = b[i] ?? 0;
164
- if (av !== bv)
165
- return av > bv ? 1 : -1;
166
- }
167
- return 0;
168
- }
169
43
  function readManifestTag(baseDir) {
170
44
  const manifest = join(baseDir, "VERSION");
171
45
  try {
@@ -66,7 +66,10 @@ export function fetchGreptileCommentBody(prNumber, repo, runGh) {
66
66
  `repos/${resolvedRepo}/issues/${prNumber}/comments`,
67
67
  "--paginate",
68
68
  "--jq",
69
- `[.[] | select(.user.login == "${GREPTILE_LOGIN}")] | last | .body // ""`,
69
+ // Prefer most recently *updated* Greptile summary. Greptile edits the
70
+ // primary rolling summary in place; a later-created duplicate can stay
71
+ // SHA-stale and must not win over the refreshed primary (#1056 / #2658).
72
+ `[.[] | select(.user.login == "${GREPTILE_LOGIN}")] | sort_by(.updated_at) | last | .body // ""`,
70
73
  ];
71
74
  const { returncode, stdout, stderr } = runGh(cmd);
72
75
  if (returncode !== 0) {
@@ -123,7 +126,8 @@ function parsePaginatedComments(text) {
123
126
  }
124
127
  idx = end;
125
128
  }
126
- const greptileBodies = [];
129
+ let bestBody = "";
130
+ let bestUpdatedAt = "";
127
131
  for (const c of comments) {
128
132
  if (c !== null &&
129
133
  typeof c === "object" &&
@@ -136,13 +140,17 @@ function parsePaginatedComments(text) {
136
140
  c.user.login === GREPTILE_LOGIN &&
137
141
  "body" in c &&
138
142
  typeof c.body === "string") {
139
- greptileBodies.push(c.body);
143
+ const updatedAt = "updated_at" in c && typeof c.updated_at === "string" ? c.updated_at : "";
144
+ if (updatedAt >= bestUpdatedAt) {
145
+ bestUpdatedAt = updatedAt;
146
+ bestBody = c.body;
147
+ }
140
148
  }
141
149
  }
142
- if (greptileBodies.length === 0) {
150
+ if (bestBody.length === 0 && bestUpdatedAt.length === 0) {
143
151
  return { body: "", error: "" };
144
152
  }
145
- return { body: greptileBodies[greptileBodies.length - 1] ?? "", error: "" };
153
+ return { body: bestBody, error: "" };
146
154
  }
147
155
  /** Mirrors Python json.JSONDecoder.raw_decode for concatenated paginate arrays. */
148
156
  class PaginatedJsonDecoder {
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Test-only gh doubles for pr-monitor / merge-readiness suites (#2652).
3
+ * Not part of the production CLI surface; excluded from coverage via `*.helpers.ts`.
4
+ */
5
+ import type { RunGhFn } from "./types.js";
6
+ /** Empty GraphQL reviewThreads page for inline Greptile findings (#2620 / #2652). */
7
+ export declare const EMPTY_REVIEW_THREADS_GRAPHQL: string;
8
+ /** Prepend a graphql reviewThreads stub to any hermetic runGh test double. */
9
+ export declare function withGraphqlInlineStub(runGh: RunGhFn): RunGhFn;
10
+ export interface FakeRunGhOptions {
11
+ readonly headSha?: string;
12
+ readonly headOk?: boolean;
13
+ readonly commentsBody?: string;
14
+ readonly commentsOk?: boolean;
15
+ }
16
+ /** Hermetic runGh stub for monitor / merge-readiness unit tests (#2260 / #2652). */
17
+ export declare function fakeRunGhForMonitor(options?: FakeRunGhOptions): RunGhFn;
18
+ //# sourceMappingURL=test-gh-fixtures.helpers.d.ts.map
@@ -0,0 +1,76 @@
1
+ /** Empty GraphQL reviewThreads page for inline Greptile findings (#2620 / #2652). */
2
+ export const EMPTY_REVIEW_THREADS_GRAPHQL = JSON.stringify({
3
+ data: {
4
+ repository: {
5
+ pullRequest: {
6
+ reviewThreads: {
7
+ pageInfo: { hasNextPage: false, endCursor: null },
8
+ nodes: [],
9
+ },
10
+ },
11
+ },
12
+ },
13
+ });
14
+ /** Prepend a graphql reviewThreads stub to any hermetic runGh test double. */
15
+ export function withGraphqlInlineStub(runGh) {
16
+ return (cmd) => {
17
+ if (cmd.join(" ").includes("graphql")) {
18
+ return { returncode: 0, stdout: EMPTY_REVIEW_THREADS_GRAPHQL, stderr: "" };
19
+ }
20
+ return runGh(cmd);
21
+ };
22
+ }
23
+ /** Hermetic runGh stub for monitor / merge-readiness unit tests (#2260 / #2652). */
24
+ export function fakeRunGhForMonitor(options = {}) {
25
+ const headSha = options.headSha ?? "abc1234567890def1234567890abcdef12345678";
26
+ const headOk = options.headOk ?? true;
27
+ const commentsOk = options.commentsOk ?? true;
28
+ const commentsBody = options.commentsBody ?? "";
29
+ return (cmd) => {
30
+ if (!headOk) {
31
+ return { returncode: 1, stdout: "", stderr: "all-down" };
32
+ }
33
+ const joined = cmd.join(" ");
34
+ if (joined.includes("graphql")) {
35
+ return { returncode: 0, stdout: EMPTY_REVIEW_THREADS_GRAPHQL, stderr: "" };
36
+ }
37
+ if (joined.includes("headRefOid")) {
38
+ return { returncode: 0, stdout: `${headSha}\n`, stderr: "" };
39
+ }
40
+ if (joined.includes("/comments")) {
41
+ return commentsOk
42
+ ? { returncode: 0, stdout: commentsBody, stderr: "" }
43
+ : { returncode: 1, stdout: "", stderr: "boom" };
44
+ }
45
+ if (joined.includes("/check-runs")) {
46
+ return {
47
+ returncode: 0,
48
+ stdout: JSON.stringify({
49
+ check_runs: [
50
+ {
51
+ name: "TypeScript (build + lint + test)",
52
+ status: "completed",
53
+ conclusion: "success",
54
+ },
55
+ ],
56
+ }),
57
+ stderr: "",
58
+ };
59
+ }
60
+ if (joined.includes("/pulls/")) {
61
+ return {
62
+ returncode: 0,
63
+ stdout: JSON.stringify({
64
+ state: "open",
65
+ merged: false,
66
+ mergeable: true,
67
+ mergeable_state: "clean",
68
+ head: { sha: headSha },
69
+ }),
70
+ stderr: "",
71
+ };
72
+ }
73
+ return { returncode: 1, stdout: "", stderr: `unexpected: ${joined}` };
74
+ };
75
+ }
76
+ //# sourceMappingURL=test-gh-fixtures.helpers.js.map
@@ -1,4 +1,4 @@
1
- import { cadenceIntervalAfterPoll } from "./cadence.js";
1
+ import { cadenceIntervalAfterPoll, cadenceIntervals } from "./cadence.js";
2
2
  import { DEFAULT_CADENCE, EXIT_CAP_REACHED, EXIT_CLEAN, EXIT_CONFIG_ERROR, EXIT_PR_TERMINAL, } from "./constants.js";
3
3
  import { callReadiness } from "./readiness.js";
4
4
  const systemMonotonicClock = {
@@ -127,12 +127,21 @@ export function monitor(prNumber, repo, options = {}) {
127
127
  const sleepFn = options.sleepFn ?? defaultSleep;
128
128
  const callReadinessFn = options.callReadinessFn ?? ((n, r) => callReadiness(n, r, { runGh: options.runGh }));
129
129
  const startedAt = clockFn.now();
130
+ const intervalSchedule = cadenceIntervals(cadence);
131
+ const minCadenceSec = intervalSchedule.length > 0
132
+ ? Math.min(...intervalSchedule)
133
+ : cadenceIntervalAfterPoll(1, cadence);
134
+ // Fail closed when an injected clock never advances (infinite poll loop; #2652).
135
+ const maxPolls = Math.min(10_000, Math.ceil(capSeconds / Math.max(minCadenceSec, 1)) + intervalSchedule.length + 2);
130
136
  let pollIndex = 0;
131
137
  let lastPayload = {};
132
138
  let lastExit = EXIT_CAP_REACHED;
133
139
  let priorCadenceSeconds = cadenceIntervalAfterPoll(1, cadence);
134
140
  while (true) {
135
141
  pollIndex += 1;
142
+ if (pollIndex > maxPolls) {
143
+ return { exitCode: EXIT_CAP_REACHED, payload: lastPayload, pollCount: pollIndex - 1 };
144
+ }
136
145
  const elapsed = clockFn.now() - startedAt;
137
146
  if (elapsed > capSeconds) {
138
147
  return { exitCode: EXIT_CAP_REACHED, payload: lastPayload, pollCount: pollIndex - 1 };
@@ -23,6 +23,11 @@ export declare const VERDICT_PENDING = "PENDING";
23
23
  export declare const VERDICT_CONFIG = "CONFIG";
24
24
  export declare const DEFAULT_MAX_WAIT_MINUTES = 30;
25
25
  export declare const DEFAULT_POLL_SECONDS = 90;
26
+ /**
27
+ * Usage for `task pr:watch -- --help` / `-h` (#2652 / #1056).
28
+ * Canonical surface is the Task verb; engine stem is `pr-watch`.
29
+ */
30
+ export declare const WATCH_HELP: string;
26
31
  /**
27
32
  * Consecutive polls with a review PRESENT but stuck on a stale (non-HEAD)
28
33
  * commit before the loop surfaces STALL instead of waiting the full cap. Keeps
@@ -23,6 +23,33 @@ export const VERDICT_PENDING = "PENDING";
23
23
  export const VERDICT_CONFIG = "CONFIG";
24
24
  export const DEFAULT_MAX_WAIT_MINUTES = 30;
25
25
  export const DEFAULT_POLL_SECONDS = 90;
26
+ /**
27
+ * Usage for `task pr:watch -- --help` / `-h` (#2652 / #1056).
28
+ * Canonical surface is the Task verb; engine stem is `pr-watch`.
29
+ */
30
+ export const WATCH_HELP = "usage: task pr:watch -- <pr_number> [options]\n" +
31
+ "\n" +
32
+ "Blocking poll of a PR Greptile/SLizard review to a terminal three-state\n" +
33
+ "verdict (#1056). The invocation IS the wait — an orchestrator that promises\n" +
34
+ "to poll cannot silently forget. Canonical: `task pr:watch -- <N>`.\n" +
35
+ "Engine / CLI stem: `pr-watch` (also `directive pr watch` / `directive pr:watch`).\n" +
36
+ "\n" +
37
+ "positional arguments:\n" +
38
+ " pr_number GitHub pull request number (required unless --help)\n" +
39
+ "\n" +
40
+ "options:\n" +
41
+ " -h, --help Show this help and exit 0\n" +
42
+ " --one-shot Single probe (PENDING with no terminal verdict → exit 2)\n" +
43
+ " --json Emit the AC-4 JSON shape on stdout\n" +
44
+ " --max-wait-minutes N Cap for the blocking poll (default: 30)\n" +
45
+ " --poll-seconds N Seconds between probes (default: 90)\n" +
46
+ " --repo OWNER/REPO Override repository (default: GH_REPO / origin)\n" +
47
+ " --project-root PATH Chdir before probing (optional)\n" +
48
+ "\n" +
49
+ "exit codes:\n" +
50
+ " 0 CLEAN SHA-matched review, confidence > 3, no P0/P1, CI green\n" +
51
+ " 1 NEW_P0_P1 Blocking findings on the current (SHA-matched) review\n" +
52
+ " 2 ERRORED | STALL | TIMEOUT | config / usage error\n";
26
53
  /**
27
54
  * Consecutive polls with a review PRESENT but stuck on a stale (non-HEAD)
28
55
  * commit before the loop surfaces STALL instead of waiting the full cap. Keeps
@@ -1,5 +1,5 @@
1
1
  export * from "./constants.js";
2
- export { cmdPrWatch, emitWatchJson, type ParsedWatchArgs, parseWatchArgs, printWatchHuman, type RunWatchOptions, runWatch, watchResultToJson, } from "./main.js";
2
+ export { cmdPrWatch, emitWatchJson, formatWatchHelp, type ParsedWatchArgs, parseWatchArgs, printWatchHuman, type RunWatchOptions, runWatch, watchResultToJson, } from "./main.js";
3
3
  export { probeOnce } from "./probe.js";
4
4
  export * from "./types.js";
5
5
  export { formatWatchStatus, watch } from "./watch.js";
@@ -1,5 +1,5 @@
1
1
  export * from "./constants.js";
2
- export { cmdPrWatch, emitWatchJson, parseWatchArgs, printWatchHuman, runWatch, watchResultToJson, } from "./main.js";
2
+ export { cmdPrWatch, emitWatchJson, formatWatchHelp, parseWatchArgs, printWatchHuman, runWatch, watchResultToJson, } from "./main.js";
3
3
  export { probeOnce } from "./probe.js";
4
4
  export * from "./types.js";
5
5
  export { formatWatchStatus, watch } from "./watch.js";
@@ -7,9 +7,12 @@ export interface ParsedWatchArgs {
7
7
  readonly oneShot: boolean;
8
8
  readonly emitJson: boolean;
9
9
  readonly projectRoot: string | null;
10
+ readonly help: boolean;
10
11
  readonly error?: string;
11
12
  }
12
13
  export declare function parseWatchArgs(argv: readonly string[]): ParsedWatchArgs;
14
+ /** Canonical help text for `task pr:watch -- --help` (#2652). */
15
+ export declare function formatWatchHelp(): string;
13
16
  /** AC-4 stable --json shape (same field set as the #1039 Tier-1 instrumentation line). */
14
17
  export declare function watchResultToJson(result: WatchResult): Record<string, unknown>;
15
18
  export declare function emitWatchJson(result: WatchResult): string;
@@ -1,7 +1,7 @@
1
1
  import { existsSync } from "node:fs";
2
2
  import { resolve } from "node:path";
3
3
  import { defaultRunGh } from "../pr-merge-readiness/gh.js";
4
- import { DEFAULT_MAX_WAIT_MINUTES, DEFAULT_POLL_SECONDS, EXIT_TERMINAL_ERROR, } from "./constants.js";
4
+ import { DEFAULT_MAX_WAIT_MINUTES, DEFAULT_POLL_SECONDS, EXIT_CLEAN, EXIT_TERMINAL_ERROR, WATCH_HELP, } from "./constants.js";
5
5
  import { watch } from "./watch.js";
6
6
  function fail(base, error) {
7
7
  return { ...base, error };
@@ -15,6 +15,7 @@ export function parseWatchArgs(argv) {
15
15
  oneShot: false,
16
16
  emitJson: false,
17
17
  projectRoot: null,
18
+ help: false,
18
19
  };
19
20
  let prNumber = null;
20
21
  let repo = null;
@@ -23,6 +24,7 @@ export function parseWatchArgs(argv) {
23
24
  let oneShot = false;
24
25
  let emitJson = false;
25
26
  let projectRoot = null;
27
+ let help = false;
26
28
  const takePositive = (label, raw) => {
27
29
  if (raw === undefined) {
28
30
  return { error: `argument ${label}: expected one argument` };
@@ -35,7 +37,10 @@ export function parseWatchArgs(argv) {
35
37
  };
36
38
  for (let i = 0; i < argv.length; i += 1) {
37
39
  const arg = argv[i];
38
- if (arg === "--json") {
40
+ if (arg === "--help" || arg === "-h") {
41
+ help = true;
42
+ }
43
+ else if (arg === "--json") {
39
44
  emitJson = true;
40
45
  }
41
46
  else if (arg === "--one-shot") {
@@ -103,10 +108,26 @@ export function parseWatchArgs(argv) {
103
108
  return fail(acc, `unrecognized arguments: ${arg}`);
104
109
  }
105
110
  }
111
+ if (help) {
112
+ return { prNumber, repo, maxWaitMinutes, pollSeconds, oneShot, emitJson, projectRoot, help };
113
+ }
106
114
  if (prNumber === null) {
107
115
  return fail(acc, "the following arguments are required: pr_number");
108
116
  }
109
- return { prNumber, repo, maxWaitMinutes, pollSeconds, oneShot, emitJson, projectRoot };
117
+ return {
118
+ prNumber,
119
+ repo,
120
+ maxWaitMinutes,
121
+ pollSeconds,
122
+ oneShot,
123
+ emitJson,
124
+ projectRoot,
125
+ help,
126
+ };
127
+ }
128
+ /** Canonical help text for `task pr:watch -- --help` (#2652). */
129
+ export function formatWatchHelp() {
130
+ return WATCH_HELP;
110
131
  }
111
132
  /** Match Python json.dumps(..., indent=2) default ensure_ascii=True. */
112
133
  function pythonJsonDumps(value) {
@@ -161,8 +182,13 @@ export function printWatchHuman(result) {
161
182
  }
162
183
  export function runWatch(argv, options = {}) {
163
184
  const args = parseWatchArgs(argv);
185
+ if (args.help) {
186
+ process.stdout.write(formatWatchHelp());
187
+ return EXIT_CLEAN;
188
+ }
164
189
  if (args.error !== undefined) {
165
190
  process.stderr.write(`pr_watch: ${args.error}\n`);
191
+ process.stderr.write(`Try: task pr:watch -- --help\n`);
166
192
  return EXIT_TERMINAL_ERROR;
167
193
  }
168
194
  let restoreCwd = 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,