@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
@@ -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 };
@@ -2,6 +2,13 @@ import { classifyMonitorOutcome, parseMonitorPayload } from "./classify.js";
2
2
  import { EXIT_CONFIG_ERROR, EXIT_MERGED, EXIT_TIMEOUT_OR_ESCALATION } from "./constants.js";
3
3
  import { makeResult } from "./result.js";
4
4
  import { runGhMerge, runMonitor, runProtectedCheck } from "./wrappers.js";
5
+ /** Node module-not-found / missing script — not a protected-issue overlap (#2667). */
6
+ function isProtectedCheckConfigFailure(stderr) {
7
+ const tail = stderr.trim();
8
+ return (tail.includes("MODULE_NOT_FOUND") ||
9
+ tail.includes("Cannot find module") ||
10
+ tail.includes("protected-check script not found:"));
11
+ }
5
12
  /** Run protected-check -> wait -> merge cascade (#1369). */
6
13
  export function waitMergeableAndMerge(prNumber, repo, options) {
7
14
  const protectedFn = options.protectedFn ?? runProtectedCheck;
@@ -17,7 +24,7 @@ export function waitMergeableAndMerge(prNumber, repo, options) {
17
24
  stderr: prcStderr,
18
25
  protected: [...protectedIssues],
19
26
  };
20
- if (prcRc === 1) {
27
+ if (prcRc === 1 && !isProtectedCheckConfigFailure(prcStderr)) {
21
28
  return makeResult({
22
29
  prNumber,
23
30
  repo,
@@ -5,7 +5,11 @@ export function classifyMonitorOutcome(monitorReturncode, monitorPayload) {
5
5
  return ["clean", EXIT_MERGED];
6
6
  }
7
7
  if (monitorReturncode === 1) {
8
- return ["cap-reached", EXIT_TIMEOUT_OR_ESCALATION];
8
+ // Bare exit 1 (MODULE_NOT_FOUND / missing script) is not budget exhaustion (#2673).
9
+ if (monitorPayload.monitor_result === "CAP-REACHED") {
10
+ return ["cap-reached", EXIT_TIMEOUT_OR_ESCALATION];
11
+ }
12
+ return ["config-error", EXIT_CONFIG_ERROR];
9
13
  }
10
14
  if (monitorReturncode === 2) {
11
15
  return ["config-error", EXIT_CONFIG_ERROR];
@@ -56,14 +56,17 @@ export function cliScriptPath(name) {
56
56
  const candidates = [];
57
57
  try {
58
58
  const require = createRequire(import.meta.url);
59
- const pkgJson = require.resolve("@deftai/directive/package.json");
60
- candidates.push(resolve(dirname(pkgJson), "dist", script));
59
+ // package.json is not an exported subpath under @deftai/directive exports (#2667).
60
+ const mainEntry = require.resolve("@deftai/directive");
61
+ candidates.push(resolve(dirname(mainEntry), script));
61
62
  }
62
63
  catch {
63
64
  // Core does not declare a runtime dependency on the CLI package; ignore.
64
65
  }
65
66
  // Monorepo: packages/core/dist/pr-wait-mergeable -> packages/cli/dist
66
67
  candidates.push(resolve(here, "../../../cli/dist", script));
68
+ // npm hoisted: .../@deftai/directive-core/dist/... -> .../@deftai/directive/dist
69
+ candidates.push(resolve(here, "../../../directive/dist", script));
67
70
  // npm nested: .../directive/node_modules/@deftai/directive-core/dist/... -> .../directive/dist
68
71
  candidates.push(resolve(here, "../../../../dist", script));
69
72
  for (const candidate of candidates) {
@@ -75,9 +78,13 @@ export function cliScriptPath(name) {
75
78
  }
76
79
  /** Invoke pr-protected-issues CLI and return (returncode, stdout, stderr). */
77
80
  export function runProtectedCheck(prNumber, repo, protectedIssues, options = {}) {
81
+ const scriptPath = cliScriptPath("pr-protected-issues");
82
+ if (!existsSync(scriptPath)) {
83
+ return [2, "", `protected-check script not found: ${scriptPath}`];
84
+ }
78
85
  const node = options.nodeExecutable ?? process.execPath;
79
86
  const cmd = [
80
- cliScriptPath("pr-protected-issues"),
87
+ scriptPath,
81
88
  String(prNumber),
82
89
  "--protected",
83
90
  protectedIssues.map(String).join(","),
@@ -92,10 +99,14 @@ export function runProtectedCheck(prNumber, repo, protectedIssues, options = {})
92
99
  }
93
100
  /** Invoke pr-monitor CLI with --json and return (returncode, stdout, stderr). */
94
101
  export function runMonitor(prNumber, repo, capMinutes, options = {}) {
102
+ const scriptPath = cliScriptPath("pr-monitor");
103
+ if (!existsSync(scriptPath)) {
104
+ return [2, "", `monitor script not found: ${scriptPath}`];
105
+ }
95
106
  const node = options.nodeExecutable ?? process.execPath;
96
107
  const timeoutSec = options.timeout ?? capMinutes * 60 + 60;
97
108
  const cmd = [
98
- cliScriptPath("pr-monitor"),
109
+ scriptPath,
99
110
  String(prNumber),
100
111
  "--repo",
101
112
  repo,
@@ -17,12 +17,22 @@ export declare const VERDICT_NEW_P0_P1 = "NEW_P0_P1";
17
17
  export declare const VERDICT_ERRORED = "ERRORED";
18
18
  export declare const VERDICT_STALL = "STALL";
19
19
  export declare const VERDICT_TIMEOUT = "TIMEOUT";
20
+ /**
21
+ * Greptile side of the clean gate is satisfied on HEAD but required CI is red
22
+ * (#2688). Exit 2 — fail-loud toward a CI fix loop instead of idle-polling.
23
+ */
24
+ export declare const VERDICT_CI_BLOCKED = "CI_BLOCKED";
20
25
  /** --one-shot only: a single probe with no terminal verdict yet. */
21
26
  export declare const VERDICT_PENDING = "PENDING";
22
27
  /** External/config fault mid-probe (unresolvable repo/HEAD, gh unavailable). */
23
28
  export declare const VERDICT_CONFIG = "CONFIG";
24
29
  export declare const DEFAULT_MAX_WAIT_MINUTES = 30;
25
30
  export declare const DEFAULT_POLL_SECONDS = 90;
31
+ /**
32
+ * Usage for `task pr:watch -- --help` / `-h` (#2652 / #1056).
33
+ * Canonical surface is the Task verb; engine stem is `pr-watch`.
34
+ */
35
+ export declare const WATCH_HELP: string;
26
36
  /**
27
37
  * Consecutive polls with a review PRESENT but stuck on a stale (non-HEAD)
28
38
  * commit before the loop surfaces STALL instead of waiting the full cap. Keeps
@@ -30,4 +40,9 @@ export declare const DEFAULT_POLL_SECONDS = 90;
30
40
  * whole 30-minute budget (#1039 STALL terminal).
31
41
  */
32
42
  export declare const DEFAULT_STALL_THRESHOLD = 3;
43
+ /**
44
+ * Consecutive polls with clean_gate_holdout=ci_failures (Greptile otherwise
45
+ * satisfied) before CI_BLOCKED (#2688). Same default as SHA STALL.
46
+ */
47
+ export declare const DEFAULT_CI_BLOCKED_THRESHOLD = 3;
33
48
  //# sourceMappingURL=constants.d.ts.map
@@ -17,12 +17,44 @@ export const VERDICT_NEW_P0_P1 = "NEW_P0_P1";
17
17
  export const VERDICT_ERRORED = "ERRORED";
18
18
  export const VERDICT_STALL = "STALL";
19
19
  export const VERDICT_TIMEOUT = "TIMEOUT";
20
+ /**
21
+ * Greptile side of the clean gate is satisfied on HEAD but required CI is red
22
+ * (#2688). Exit 2 — fail-loud toward a CI fix loop instead of idle-polling.
23
+ */
24
+ export const VERDICT_CI_BLOCKED = "CI_BLOCKED";
20
25
  /** --one-shot only: a single probe with no terminal verdict yet. */
21
26
  export const VERDICT_PENDING = "PENDING";
22
27
  /** External/config fault mid-probe (unresolvable repo/HEAD, gh unavailable). */
23
28
  export const VERDICT_CONFIG = "CONFIG";
24
29
  export const DEFAULT_MAX_WAIT_MINUTES = 30;
25
30
  export const DEFAULT_POLL_SECONDS = 90;
31
+ /**
32
+ * Usage for `task pr:watch -- --help` / `-h` (#2652 / #1056).
33
+ * Canonical surface is the Task verb; engine stem is `pr-watch`.
34
+ */
35
+ export const WATCH_HELP = "usage: task pr:watch -- <pr_number> [options]\n" +
36
+ "\n" +
37
+ "Blocking poll of a PR Greptile/SLizard review to a terminal three-state\n" +
38
+ "verdict (#1056). The invocation IS the wait — an orchestrator that promises\n" +
39
+ "to poll cannot silently forget. Canonical: `task pr:watch -- <N>`.\n" +
40
+ "Engine / CLI stem: `pr-watch` (also `directive pr watch` / `directive pr:watch`).\n" +
41
+ "\n" +
42
+ "positional arguments:\n" +
43
+ " pr_number GitHub pull request number (required unless --help)\n" +
44
+ "\n" +
45
+ "options:\n" +
46
+ " -h, --help Show this help and exit 0\n" +
47
+ " --one-shot Single probe (PENDING with no terminal verdict → exit 2)\n" +
48
+ " --json Emit the AC-4 JSON shape on stdout\n" +
49
+ " --max-wait-minutes N Cap for the blocking poll (default: 30)\n" +
50
+ " --poll-seconds N Seconds between probes (default: 90)\n" +
51
+ " --repo OWNER/REPO Override repository (default: GH_REPO / origin)\n" +
52
+ " --project-root PATH Chdir before probing (optional)\n" +
53
+ "\n" +
54
+ "exit codes:\n" +
55
+ " 0 CLEAN SHA-matched review, confidence > 3, no P0/P1, CI green\n" +
56
+ " 1 NEW_P0_P1 Blocking findings on the current (SHA-matched) review\n" +
57
+ " 2 ERRORED | STALL | TIMEOUT | CI_BLOCKED | config / usage error\n";
26
58
  /**
27
59
  * Consecutive polls with a review PRESENT but stuck on a stale (non-HEAD)
28
60
  * commit before the loop surfaces STALL instead of waiting the full cap. Keeps
@@ -30,4 +62,9 @@ export const DEFAULT_POLL_SECONDS = 90;
30
62
  * whole 30-minute budget (#1039 STALL terminal).
31
63
  */
32
64
  export const DEFAULT_STALL_THRESHOLD = 3;
65
+ /**
66
+ * Consecutive polls with clean_gate_holdout=ci_failures (Greptile otherwise
67
+ * satisfied) before CI_BLOCKED (#2688). Same default as SHA STALL.
68
+ */
69
+ export const DEFAULT_CI_BLOCKED_THRESHOLD = 3;
33
70
  //# sourceMappingURL=constants.js.map
@@ -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) {
@@ -130,6 +151,7 @@ export function watchResultToJson(result) {
130
151
  p1_count: p.p1Count,
131
152
  errored: p.errored,
132
153
  ci_failures: p.ciFailures,
154
+ ci_failed_checks: [...p.ciFailedChecks],
133
155
  is_clean: p.isClean,
134
156
  clean_gate_holdout: p.cleanGateHoldout,
135
157
  elapsed_seconds: result.elapsedSeconds,
@@ -150,6 +172,9 @@ export function printWatchHuman(result) {
150
172
  lines.push(` Findings: P0=${p.p0Count} P1=${p.p1Count}`);
151
173
  lines.push(` Errored sentinel: ${p.errored}`);
152
174
  lines.push(` CI failures: ${p.ciFailures}`);
175
+ if (p.ciFailedChecks.length > 0) {
176
+ lines.push(` Failed checks: ${p.ciFailedChecks.join("; ")}`);
177
+ }
153
178
  if (p.cleanGateHoldout !== null) {
154
179
  lines.push(` Clean-gate holdout: ${p.cleanGateHoldout}`);
155
180
  }
@@ -161,8 +186,13 @@ export function printWatchHuman(result) {
161
186
  }
162
187
  export function runWatch(argv, options = {}) {
163
188
  const args = parseWatchArgs(argv);
189
+ if (args.help) {
190
+ process.stdout.write(formatWatchHelp());
191
+ return EXIT_CLEAN;
192
+ }
164
193
  if (args.error !== undefined) {
165
194
  process.stderr.write(`pr_watch: ${args.error}\n`);
195
+ process.stderr.write(`Try: task pr:watch -- --help\n`);
166
196
  return EXIT_TERMINAL_ERROR;
167
197
  }
168
198
  let restoreCwd = null;
@@ -14,6 +14,7 @@ function errorProbe(headSha, message) {
14
14
  hasBlocking: false,
15
15
  errored: false,
16
16
  ciFailures: 0,
17
+ ciFailedChecks: [],
17
18
  terminalCheckRun: false,
18
19
  isClean: false,
19
20
  cleanGateHoldout: null,
@@ -61,12 +62,14 @@ export function probeOnce(prNumber, repoArg, runGh = defaultRunGh) {
61
62
  // ci_failures=0 / terminal so the Greptile verdict drives; the merge button
62
63
  // still owns the hard CI gate via pr:merge-ready (#796).
63
64
  let ciFailures = 0;
65
+ let ciFailedChecks = [];
64
66
  let terminalCheckRun = true;
65
67
  if (repo !== null) {
66
68
  const check = fetchCheckRunsRest(headSha, repo, runGh);
67
69
  if (check.summary !== null) {
68
70
  const ci = evaluateCiGate(check.checkRuns, {});
69
- ciFailures = ci.summary.failed_required.length;
71
+ ciFailedChecks = ci.summary.failed_required;
72
+ ciFailures = ciFailedChecks.length;
70
73
  terminalCheckRun = ci.summary.pending_required.length === 0;
71
74
  }
72
75
  }
@@ -91,6 +94,7 @@ export function probeOnce(prNumber, repoArg, runGh = defaultRunGh) {
91
94
  hasBlocking: findings.has_blocking,
92
95
  errored,
93
96
  ciFailures,
97
+ ciFailedChecks,
94
98
  terminalCheckRun,
95
99
  isClean,
96
100
  cleanGateHoldout,