@deftai/directive-core 0.67.0 → 0.68.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (90) hide show
  1. package/dist/check/index.d.ts +1 -1
  2. package/dist/check/index.js +1 -1
  3. package/dist/check/orchestrator.d.ts +14 -4
  4. package/dist/check/orchestrator.js +30 -6
  5. package/dist/doctor/checks.js +11 -11
  6. package/dist/doctor/constants.d.ts +2 -0
  7. package/dist/doctor/constants.js +2 -0
  8. package/dist/doctor/payload-staleness.js +15 -6
  9. package/dist/forward-coverage/evaluate.d.ts +55 -0
  10. package/dist/forward-coverage/evaluate.js +254 -0
  11. package/dist/index.d.ts +1 -0
  12. package/dist/index.js +1 -0
  13. package/dist/init-deposit/constants.d.ts +14 -0
  14. package/dist/init-deposit/constants.js +10 -0
  15. package/dist/init-deposit/hygiene.d.ts +50 -0
  16. package/dist/init-deposit/hygiene.js +176 -0
  17. package/dist/init-deposit/init-deposit.d.ts +1 -0
  18. package/dist/init-deposit/init-deposit.js +7 -1
  19. package/dist/init-deposit/refresh.d.ts +2 -0
  20. package/dist/init-deposit/refresh.js +48 -30
  21. package/dist/init-deposit/scaffold.d.ts +3 -4
  22. package/dist/init-deposit/scaffold.js +3 -24
  23. package/dist/integration-e2e/helpers.d.ts +0 -1
  24. package/dist/integration-e2e/helpers.js +3 -7
  25. package/dist/layout/lifecycle-ref.d.ts +15 -0
  26. package/dist/layout/lifecycle-ref.js +55 -0
  27. package/dist/platform/agents-md.d.ts +28 -0
  28. package/dist/platform/agents-md.js +34 -0
  29. package/dist/policy/resolve.js +56 -46
  30. package/dist/pr-merge-readiness/compute.d.ts +10 -0
  31. package/dist/pr-merge-readiness/compute.js +53 -17
  32. package/dist/pr-merge-readiness/index.d.ts +2 -1
  33. package/dist/pr-merge-readiness/index.js +1 -0
  34. package/dist/pr-merge-readiness/mergeability.d.ts +43 -0
  35. package/dist/pr-merge-readiness/mergeability.js +105 -0
  36. package/dist/pr-monitor/index.d.ts +1 -1
  37. package/dist/pr-monitor/index.js +1 -1
  38. package/dist/pr-monitor/monitor.d.ts +8 -2
  39. package/dist/pr-monitor/monitor.js +35 -6
  40. package/dist/pr-wait-mergeable/wrappers.d.ts +12 -1
  41. package/dist/pr-wait-mergeable/wrappers.js +13 -5
  42. package/dist/pr-watch/constants.d.ts +33 -0
  43. package/dist/pr-watch/constants.js +33 -0
  44. package/dist/pr-watch/index.d.ts +6 -0
  45. package/dist/pr-watch/index.js +6 -0
  46. package/dist/pr-watch/main.d.ts +21 -0
  47. package/dist/pr-watch/main.js +206 -0
  48. package/dist/pr-watch/probe.d.ts +13 -0
  49. package/dist/pr-watch/probe.js +100 -0
  50. package/dist/pr-watch/types.d.ts +47 -0
  51. package/dist/pr-watch/types.js +2 -0
  52. package/dist/pr-watch/watch.d.ts +12 -0
  53. package/dist/pr-watch/watch.js +122 -0
  54. package/dist/render/project-render.js +114 -89
  55. package/dist/scope/capacity-stamp.d.ts +24 -2
  56. package/dist/scope/capacity-stamp.js +80 -28
  57. package/dist/scope/decomposed-refs.js +2 -2
  58. package/dist/scope/project-definition-sync.js +3 -3
  59. package/dist/scope/vbrief-ref.d.ts +2 -1
  60. package/dist/scope/vbrief-ref.js +3 -2
  61. package/dist/spec-authority/constants.d.ts +6 -0
  62. package/dist/spec-authority/constants.js +11 -0
  63. package/dist/spec-authority/resolver.js +3 -3
  64. package/dist/swarm/finalize-cohort-cli.d.ts +5 -0
  65. package/dist/swarm/finalize-cohort-cli.js +100 -0
  66. package/dist/swarm/finalize-cohort.d.ts +39 -0
  67. package/dist/swarm/finalize-cohort.js +589 -0
  68. package/dist/swarm/index.d.ts +2 -0
  69. package/dist/swarm/index.js +2 -0
  70. package/dist/swarm/launch.d.ts +7 -0
  71. package/dist/swarm/launch.js +17 -5
  72. package/dist/triage/queue/index.d.ts +1 -0
  73. package/dist/triage/queue/index.js +1 -0
  74. package/dist/triage/queue/project.d.ts +6 -1
  75. package/dist/triage/queue/project.js +9 -4
  76. package/dist/triage/queue/reconcile-live-state.d.ts +42 -0
  77. package/dist/triage/queue/reconcile-live-state.js +54 -0
  78. package/dist/triage/scope/mutations-core.js +91 -81
  79. package/dist/triage/scope/resolve.js +4 -3
  80. package/dist/triage/scope-drift/add-ignore.js +33 -32
  81. package/dist/triage/scope-drift/scope-rules.js +3 -3
  82. package/dist/triage/subscribe/index.js +51 -45
  83. package/dist/triage/welcome/writers.js +79 -64
  84. package/dist/vbrief-build/project-definition-io.js +8 -3
  85. package/dist/vbrief-validate/paths.js +8 -4
  86. package/dist/vbrief-validate/precutover.d.ts +1 -1
  87. package/dist/vbrief-validate/precutover.js +12 -10
  88. package/package.json +7 -7
  89. package/dist/install-upgrade/index.d.ts +0 -13
  90. package/dist/install-upgrade/index.js +0 -251
@@ -0,0 +1,33 @@
1
+ /**
2
+ * task pr:watch -- deterministic PR-verdict polling surface (#1056).
3
+ *
4
+ * Three-state exit contract (AC-1): mirrors scripts/preflight_branch.py (#747)
5
+ * and pr:merge-ready (#796) -- the invocation IS the wait, so an orchestrator
6
+ * cannot promise to poll and then silently forget (2026-05-11 three-strikes on
7
+ * #1051 / #1054).
8
+ */
9
+ /** CLEAN: SHA-matched, non-errored, no P0/P1, confidence > 3, CI green. */
10
+ export declare const EXIT_CLEAN = 0;
11
+ /** NEW_P0_P1: blocking findings on the CURRENT (SHA-matched) review. */
12
+ export declare const EXIT_NEW_P0_P1 = 1;
13
+ /** ERRORED | STALL | TIMEOUT | config-error all collapse to a single non-zero. */
14
+ export declare const EXIT_TERMINAL_ERROR = 2;
15
+ export declare const VERDICT_CLEAN = "CLEAN";
16
+ export declare const VERDICT_NEW_P0_P1 = "NEW_P0_P1";
17
+ export declare const VERDICT_ERRORED = "ERRORED";
18
+ export declare const VERDICT_STALL = "STALL";
19
+ export declare const VERDICT_TIMEOUT = "TIMEOUT";
20
+ /** --one-shot only: a single probe with no terminal verdict yet. */
21
+ export declare const VERDICT_PENDING = "PENDING";
22
+ /** External/config fault mid-probe (unresolvable repo/HEAD, gh unavailable). */
23
+ export declare const VERDICT_CONFIG = "CONFIG";
24
+ export declare const DEFAULT_MAX_WAIT_MINUTES = 30;
25
+ export declare const DEFAULT_POLL_SECONDS = 90;
26
+ /**
27
+ * Consecutive polls with a review PRESENT but stuck on a stale (non-HEAD)
28
+ * commit before the loop surfaces STALL instead of waiting the full cap. Keeps
29
+ * a Greptile review that never re-reviews the new HEAD from silently eating the
30
+ * whole 30-minute budget (#1039 STALL terminal).
31
+ */
32
+ export declare const DEFAULT_STALL_THRESHOLD = 3;
33
+ //# sourceMappingURL=constants.d.ts.map
@@ -0,0 +1,33 @@
1
+ /**
2
+ * task pr:watch -- deterministic PR-verdict polling surface (#1056).
3
+ *
4
+ * Three-state exit contract (AC-1): mirrors scripts/preflight_branch.py (#747)
5
+ * and pr:merge-ready (#796) -- the invocation IS the wait, so an orchestrator
6
+ * cannot promise to poll and then silently forget (2026-05-11 three-strikes on
7
+ * #1051 / #1054).
8
+ */
9
+ /** CLEAN: SHA-matched, non-errored, no P0/P1, confidence > 3, CI green. */
10
+ export const EXIT_CLEAN = 0;
11
+ /** NEW_P0_P1: blocking findings on the CURRENT (SHA-matched) review. */
12
+ export const EXIT_NEW_P0_P1 = 1;
13
+ /** ERRORED | STALL | TIMEOUT | config-error all collapse to a single non-zero. */
14
+ export const EXIT_TERMINAL_ERROR = 2;
15
+ export const VERDICT_CLEAN = "CLEAN";
16
+ export const VERDICT_NEW_P0_P1 = "NEW_P0_P1";
17
+ export const VERDICT_ERRORED = "ERRORED";
18
+ export const VERDICT_STALL = "STALL";
19
+ export const VERDICT_TIMEOUT = "TIMEOUT";
20
+ /** --one-shot only: a single probe with no terminal verdict yet. */
21
+ export const VERDICT_PENDING = "PENDING";
22
+ /** External/config fault mid-probe (unresolvable repo/HEAD, gh unavailable). */
23
+ export const VERDICT_CONFIG = "CONFIG";
24
+ export const DEFAULT_MAX_WAIT_MINUTES = 30;
25
+ export const DEFAULT_POLL_SECONDS = 90;
26
+ /**
27
+ * Consecutive polls with a review PRESENT but stuck on a stale (non-HEAD)
28
+ * commit before the loop surfaces STALL instead of waiting the full cap. Keeps
29
+ * a Greptile review that never re-reviews the new HEAD from silently eating the
30
+ * whole 30-minute budget (#1039 STALL terminal).
31
+ */
32
+ export const DEFAULT_STALL_THRESHOLD = 3;
33
+ //# sourceMappingURL=constants.js.map
@@ -0,0 +1,6 @@
1
+ export * from "./constants.js";
2
+ export { cmdPrWatch, emitWatchJson, type ParsedWatchArgs, parseWatchArgs, printWatchHuman, type RunWatchOptions, runWatch, watchResultToJson, } from "./main.js";
3
+ export { probeOnce } from "./probe.js";
4
+ export * from "./types.js";
5
+ export { formatWatchStatus, watch } from "./watch.js";
6
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,6 @@
1
+ export * from "./constants.js";
2
+ export { cmdPrWatch, emitWatchJson, parseWatchArgs, printWatchHuman, runWatch, watchResultToJson, } from "./main.js";
3
+ export { probeOnce } from "./probe.js";
4
+ export * from "./types.js";
5
+ export { formatWatchStatus, watch } from "./watch.js";
6
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,21 @@
1
+ import type { WatchOptions, WatchResult } from "./types.js";
2
+ export interface ParsedWatchArgs {
3
+ readonly prNumber: number | null;
4
+ readonly repo: string | null;
5
+ readonly maxWaitMinutes: number;
6
+ readonly pollSeconds: number;
7
+ readonly oneShot: boolean;
8
+ readonly emitJson: boolean;
9
+ readonly projectRoot: string | null;
10
+ readonly error?: string;
11
+ }
12
+ export declare function parseWatchArgs(argv: readonly string[]): ParsedWatchArgs;
13
+ /** AC-4 stable --json shape (same field set as the #1039 Tier-1 instrumentation line). */
14
+ export declare function watchResultToJson(result: WatchResult): Record<string, unknown>;
15
+ export declare function emitWatchJson(result: WatchResult): string;
16
+ export declare function printWatchHuman(result: WatchResult): string;
17
+ export interface RunWatchOptions extends WatchOptions {
18
+ }
19
+ export declare function runWatch(argv: readonly string[], options?: RunWatchOptions): number;
20
+ export declare function cmdPrWatch(argv: readonly string[], options?: RunWatchOptions): number;
21
+ //# sourceMappingURL=main.d.ts.map
@@ -0,0 +1,206 @@
1
+ import { existsSync } from "node:fs";
2
+ import { resolve } from "node:path";
3
+ import { defaultRunGh } from "../pr-merge-readiness/gh.js";
4
+ import { DEFAULT_MAX_WAIT_MINUTES, DEFAULT_POLL_SECONDS, EXIT_TERMINAL_ERROR, } from "./constants.js";
5
+ import { watch } from "./watch.js";
6
+ function fail(base, error) {
7
+ return { ...base, error };
8
+ }
9
+ export function parseWatchArgs(argv) {
10
+ const acc = {
11
+ prNumber: null,
12
+ repo: null,
13
+ maxWaitMinutes: DEFAULT_MAX_WAIT_MINUTES,
14
+ pollSeconds: DEFAULT_POLL_SECONDS,
15
+ oneShot: false,
16
+ emitJson: false,
17
+ projectRoot: null,
18
+ };
19
+ let prNumber = null;
20
+ let repo = null;
21
+ let maxWaitMinutes = DEFAULT_MAX_WAIT_MINUTES;
22
+ let pollSeconds = DEFAULT_POLL_SECONDS;
23
+ let oneShot = false;
24
+ let emitJson = false;
25
+ let projectRoot = null;
26
+ const takePositive = (label, raw) => {
27
+ if (raw === undefined) {
28
+ return { error: `argument ${label}: expected one argument` };
29
+ }
30
+ const parsed = Number(raw);
31
+ if (!Number.isFinite(parsed) || parsed < 0) {
32
+ return { error: `invalid ${label} value: ${raw}` };
33
+ }
34
+ return { value: parsed };
35
+ };
36
+ for (let i = 0; i < argv.length; i += 1) {
37
+ const arg = argv[i];
38
+ if (arg === "--json") {
39
+ emitJson = true;
40
+ }
41
+ else if (arg === "--one-shot") {
42
+ oneShot = true;
43
+ }
44
+ else if (arg === "--repo") {
45
+ const value = argv[i + 1];
46
+ if (value === undefined) {
47
+ return fail(acc, "argument --repo: expected one argument");
48
+ }
49
+ repo = value;
50
+ i += 1;
51
+ }
52
+ else if (arg?.startsWith("--repo=")) {
53
+ repo = arg.slice("--repo=".length);
54
+ }
55
+ else if (arg === "--max-wait-minutes") {
56
+ const r = takePositive("--max-wait-minutes", argv[i + 1]);
57
+ if ("error" in r)
58
+ return fail(acc, r.error);
59
+ maxWaitMinutes = r.value;
60
+ i += 1;
61
+ }
62
+ else if (arg?.startsWith("--max-wait-minutes=")) {
63
+ const r = takePositive("--max-wait-minutes", arg.slice("--max-wait-minutes=".length));
64
+ if ("error" in r)
65
+ return fail(acc, r.error);
66
+ maxWaitMinutes = r.value;
67
+ }
68
+ else if (arg === "--poll-seconds") {
69
+ const r = takePositive("--poll-seconds", argv[i + 1]);
70
+ if ("error" in r)
71
+ return fail(acc, r.error);
72
+ pollSeconds = r.value;
73
+ i += 1;
74
+ }
75
+ else if (arg?.startsWith("--poll-seconds=")) {
76
+ const r = takePositive("--poll-seconds", arg.slice("--poll-seconds=".length));
77
+ if ("error" in r)
78
+ return fail(acc, r.error);
79
+ pollSeconds = r.value;
80
+ }
81
+ else if (arg === "--project-root") {
82
+ const value = argv[i + 1];
83
+ if (value === undefined) {
84
+ return fail(acc, "argument --project-root: expected one argument");
85
+ }
86
+ projectRoot = value;
87
+ i += 1;
88
+ }
89
+ else if (arg?.startsWith("--project-root=")) {
90
+ projectRoot = arg.slice("--project-root=".length);
91
+ }
92
+ else if (arg?.startsWith("-")) {
93
+ return fail(acc, `unrecognized arguments: ${arg}`);
94
+ }
95
+ else if (prNumber === null) {
96
+ const n = Number(arg);
97
+ if (!Number.isInteger(n) || n <= 0) {
98
+ return fail(acc, `invalid PR number: ${arg}`);
99
+ }
100
+ prNumber = n;
101
+ }
102
+ else {
103
+ return fail(acc, `unrecognized arguments: ${arg}`);
104
+ }
105
+ }
106
+ if (prNumber === null) {
107
+ return fail(acc, "the following arguments are required: pr_number");
108
+ }
109
+ return { prNumber, repo, maxWaitMinutes, pollSeconds, oneShot, emitJson, projectRoot };
110
+ }
111
+ /** Match Python json.dumps(..., indent=2) default ensure_ascii=True. */
112
+ function pythonJsonDumps(value) {
113
+ const json = JSON.stringify(value, null, 2);
114
+ return json.replace(/[\u007f-\uffff]/g, (ch) => {
115
+ const code = ch.charCodeAt(0);
116
+ return `\\u${code.toString(16).padStart(4, "0")}`;
117
+ });
118
+ }
119
+ /** AC-4 stable --json shape (same field set as the #1039 Tier-1 instrumentation line). */
120
+ export function watchResultToJson(result) {
121
+ const p = result.probe;
122
+ return {
123
+ verdict: result.verdict,
124
+ pr_number: result.prNumber,
125
+ head_sha: p.headSha,
126
+ last_reviewed_sha: p.lastReviewedSha,
127
+ sha_match: p.shaMatch,
128
+ confidence: p.confidence,
129
+ p0_count: p.p0Count,
130
+ p1_count: p.p1Count,
131
+ errored: p.errored,
132
+ ci_failures: p.ciFailures,
133
+ is_clean: p.isClean,
134
+ clean_gate_holdout: p.cleanGateHoldout,
135
+ elapsed_seconds: result.elapsedSeconds,
136
+ poll_count: result.pollCount,
137
+ };
138
+ }
139
+ export function emitWatchJson(result) {
140
+ return `${pythonJsonDumps(watchResultToJson(result))}\n`;
141
+ }
142
+ export function printWatchHuman(result) {
143
+ const p = result.probe;
144
+ const lines = [];
145
+ lines.push(`PR #${result.prNumber} pr:watch verdict: ${result.verdict}`);
146
+ lines.push(` HEAD SHA: ${p.headSha ?? "<unknown>"}`);
147
+ lines.push(` Greptile reviewed: ${p.lastReviewedSha ?? "<not parsed>"}`);
148
+ lines.push(` SHA match: ${p.shaMatch}`);
149
+ lines.push(` Confidence: ${p.confidence !== null ? String(p.confidence) : "<not parsed>"}/5`);
150
+ lines.push(` Findings: P0=${p.p0Count} P1=${p.p1Count}`);
151
+ lines.push(` Errored sentinel: ${p.errored}`);
152
+ lines.push(` CI failures: ${p.ciFailures}`);
153
+ if (p.cleanGateHoldout !== null) {
154
+ lines.push(` Clean-gate holdout: ${p.cleanGateHoldout}`);
155
+ }
156
+ if (p.error !== null) {
157
+ lines.push(` Error: ${p.error}`);
158
+ }
159
+ lines.push(` Polls / elapsed: ${result.pollCount} poll(s) / ${result.elapsedSeconds}s`);
160
+ return `${lines.join("\n")}\n`;
161
+ }
162
+ export function runWatch(argv, options = {}) {
163
+ const args = parseWatchArgs(argv);
164
+ if (args.error !== undefined) {
165
+ process.stderr.write(`pr_watch: ${args.error}\n`);
166
+ return EXIT_TERMINAL_ERROR;
167
+ }
168
+ let restoreCwd = null;
169
+ if (args.projectRoot !== null) {
170
+ const target = resolve(args.projectRoot);
171
+ if (!existsSync(target)) {
172
+ process.stderr.write(`pr_watch: --project-root does not exist: ${target}\n`);
173
+ return EXIT_TERMINAL_ERROR;
174
+ }
175
+ restoreCwd = process.cwd();
176
+ process.chdir(target);
177
+ }
178
+ try {
179
+ const result = watch(args.prNumber, args.repo ?? process.env.GH_REPO ?? null, {
180
+ maxWaitMinutes: args.maxWaitMinutes,
181
+ pollSeconds: args.pollSeconds,
182
+ oneShot: args.oneShot,
183
+ runGh: options.runGh ?? defaultRunGh,
184
+ sleepFn: options.sleepFn,
185
+ clockFn: options.clockFn,
186
+ probeFn: options.probeFn,
187
+ stallThreshold: options.stallThreshold,
188
+ });
189
+ if (args.emitJson) {
190
+ process.stdout.write(emitWatchJson(result));
191
+ }
192
+ else {
193
+ process.stdout.write(printWatchHuman(result));
194
+ }
195
+ return result.exitCode;
196
+ }
197
+ finally {
198
+ if (restoreCwd !== null) {
199
+ process.chdir(restoreCwd);
200
+ }
201
+ }
202
+ }
203
+ export function cmdPrWatch(argv, options = {}) {
204
+ return runWatch(argv, options);
205
+ }
206
+ //# sourceMappingURL=main.js.map
@@ -0,0 +1,13 @@
1
+ import type { RunGhFn } from "../pr-merge-readiness/types.js";
2
+ import type { WatchProbe } from "./types.js";
3
+ /**
4
+ * Run one PR-verdict probe: resolve HEAD, fetch the latest Greptile/SLizard
5
+ * rolling-summary body, and score it through the CANONICAL shared detector
6
+ * (`detect` / `parseConfidence` / `parseLastReviewedSha*` / `evaluateCleanGate`
7
+ * from content-contracts/skills/greptile-detector.ts -- the same module the
8
+ * swarm poller template and its #910/#1035/#1039 tests consume). No second
9
+ * detector (#1056 AC-2). All gh access routes through the injectable RunGhFn
10
+ * seam, which defaults to the UTF-8-safe execFile capture (#1366).
11
+ */
12
+ export declare function probeOnce(prNumber: number, repoArg: string | null, runGh?: RunGhFn): WatchProbe;
13
+ //# sourceMappingURL=probe.d.ts.map
@@ -0,0 +1,100 @@
1
+ import { detect, evaluateCleanGate, parseConfidence, parseLastReviewedShaMarkdownLink, parseLastReviewedShaNaiveInline, } from "../content-contracts/skills/greptile-detector.js";
2
+ import { evaluateCiGate } from "../pr-merge-readiness/ci-gate.js";
3
+ import { GREPTILE_ERRORED_SENTINEL } from "../pr-merge-readiness/constants.js";
4
+ import { defaultRunGh, fetchCheckRunsRest, fetchGreptileBodyRest, fetchGreptileCommentBody, fetchPrHeadSha, fetchPrHeadShaRest, resolveRepo, } from "../pr-merge-readiness/gh.js";
5
+ function errorProbe(headSha, message) {
6
+ return {
7
+ found: false,
8
+ headSha,
9
+ lastReviewedSha: null,
10
+ shaMatch: false,
11
+ confidence: null,
12
+ p0Count: 0,
13
+ p1Count: 0,
14
+ hasBlocking: false,
15
+ errored: false,
16
+ ciFailures: 0,
17
+ terminalCheckRun: false,
18
+ isClean: false,
19
+ cleanGateHoldout: null,
20
+ error: message,
21
+ };
22
+ }
23
+ /**
24
+ * Run one PR-verdict probe: resolve HEAD, fetch the latest Greptile/SLizard
25
+ * rolling-summary body, and score it through the CANONICAL shared detector
26
+ * (`detect` / `parseConfidence` / `parseLastReviewedSha*` / `evaluateCleanGate`
27
+ * from content-contracts/skills/greptile-detector.ts -- the same module the
28
+ * swarm poller template and its #910/#1035/#1039 tests consume). No second
29
+ * detector (#1056 AC-2). All gh access routes through the injectable RunGhFn
30
+ * seam, which defaults to the UTF-8-safe execFile capture (#1366).
31
+ */
32
+ export function probeOnce(prNumber, repoArg, runGh = defaultRunGh) {
33
+ const resolved = resolveRepo(repoArg, runGh);
34
+ const repo = resolved.repo;
35
+ // 1. HEAD SHA -- primary `gh pr view`, then REST fallback when a repo resolved.
36
+ let headSha = fetchPrHeadSha(prNumber, repo, runGh);
37
+ if (headSha === null && repo !== null) {
38
+ headSha = fetchPrHeadShaRest(prNumber, repo, runGh).sha;
39
+ }
40
+ if (headSha === null) {
41
+ const detail = repo === null
42
+ ? `could not resolve repo (${resolved.error}); run inside a repo or pass --repo OWNER/REPO`
43
+ : "could not resolve PR HEAD sha (gh pr view + REST both failed)";
44
+ return errorProbe(null, detail);
45
+ }
46
+ // 2. Latest Greptile body -- primary jq path, then REST fallback.
47
+ let body = fetchGreptileCommentBody(prNumber, repo, runGh);
48
+ if (body === null && repo !== null) {
49
+ body = fetchGreptileBodyRest(prNumber, repo, runGh).body;
50
+ }
51
+ if (body === null) {
52
+ return errorProbe(headSha, "could not fetch Greptile comment body (primary + REST both failed)");
53
+ }
54
+ const trimmed = body.trim();
55
+ const found = trimmed.length > 0;
56
+ const errored = trimmed.startsWith(GREPTILE_ERRORED_SENTINEL);
57
+ const findings = detect(body);
58
+ const confidence = parseConfidence(body);
59
+ const lastReviewedSha = parseLastReviewedShaMarkdownLink(body) ?? parseLastReviewedShaNaiveInline(body);
60
+ // 3. CI failures (best-effort). When check-runs are unreachable we degrade to
61
+ // ci_failures=0 / terminal so the Greptile verdict drives; the merge button
62
+ // still owns the hard CI gate via pr:merge-ready (#796).
63
+ let ciFailures = 0;
64
+ let terminalCheckRun = true;
65
+ if (repo !== null) {
66
+ const check = fetchCheckRunsRest(headSha, repo, runGh);
67
+ if (check.summary !== null) {
68
+ const ci = evaluateCiGate(check.checkRuns, {});
69
+ ciFailures = ci.summary.failed_required.length;
70
+ terminalCheckRun = ci.summary.pending_required.length === 0;
71
+ }
72
+ }
73
+ const shaMatch = lastReviewedSha !== null && lastReviewedSha === headSha;
74
+ const [isClean, cleanGateHoldout] = evaluateCleanGate({
75
+ lastReviewedSha,
76
+ headSha,
77
+ hasBlocking: findings.has_blocking,
78
+ confidence,
79
+ ciFailures,
80
+ errored,
81
+ terminalCheckRun,
82
+ });
83
+ return {
84
+ found,
85
+ headSha,
86
+ lastReviewedSha,
87
+ shaMatch,
88
+ confidence,
89
+ p0Count: findings.p0_count,
90
+ p1Count: findings.p1_count,
91
+ hasBlocking: findings.has_blocking,
92
+ errored,
93
+ ciFailures,
94
+ terminalCheckRun,
95
+ isClean,
96
+ cleanGateHoldout,
97
+ error: null,
98
+ };
99
+ }
100
+ //# sourceMappingURL=probe.js.map
@@ -0,0 +1,47 @@
1
+ import type { RunGhFn } from "../pr-merge-readiness/types.js";
2
+ /** One resolved poll snapshot -- the parsed verdict + SHA-match gate for a PR. */
3
+ export interface WatchProbe {
4
+ /** A Greptile/SLizard rolling-summary comment was present (non-empty body). */
5
+ readonly found: boolean;
6
+ readonly headSha: string | null;
7
+ readonly lastReviewedSha: string | null;
8
+ /** last_reviewed_sha === head_sha -- the SHA-match gate (#1056 stale-review guard). */
9
+ readonly shaMatch: boolean;
10
+ readonly confidence: number | null;
11
+ readonly p0Count: number;
12
+ readonly p1Count: number;
13
+ readonly hasBlocking: boolean;
14
+ readonly errored: boolean;
15
+ readonly ciFailures: number;
16
+ /** All required CI check-runs have a terminal conclusion (none pending). */
17
+ readonly terminalCheckRun: boolean;
18
+ readonly isClean: boolean;
19
+ /** First unmet clean-gate condition (evaluateCleanGate holdout), or null when clean. */
20
+ readonly cleanGateHoldout: string | null;
21
+ /** Non-null when the probe hit an external/config fault (unresolvable repo/HEAD, gh down). */
22
+ readonly error: string | null;
23
+ }
24
+ export type SleepFn = (seconds: number) => void;
25
+ export interface MonotonicClock {
26
+ now(): number;
27
+ }
28
+ export type ProbeFn = (prNumber: number, repo: string | null, runGh: RunGhFn) => WatchProbe;
29
+ export interface WatchOptions {
30
+ readonly maxWaitMinutes?: number;
31
+ readonly pollSeconds?: number;
32
+ readonly oneShot?: boolean;
33
+ readonly stallThreshold?: number;
34
+ readonly runGh?: RunGhFn;
35
+ readonly sleepFn?: SleepFn;
36
+ readonly clockFn?: MonotonicClock;
37
+ readonly probeFn?: ProbeFn;
38
+ }
39
+ export interface WatchResult {
40
+ readonly verdict: string;
41
+ readonly exitCode: number;
42
+ readonly prNumber: number;
43
+ readonly probe: WatchProbe;
44
+ readonly elapsedSeconds: number;
45
+ readonly pollCount: number;
46
+ }
47
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1,12 @@
1
+ import type { WatchOptions, WatchProbe, WatchResult } from "./types.js";
2
+ /** One-line stderr status mirror per poll (Tier 1 instrumentation shape, #1039). */
3
+ export declare function formatWatchStatus(poll: number, maxPolls: number, probe: WatchProbe, elapsedSeconds: number): string;
4
+ /**
5
+ * Blocking-by-default poll to a terminal three-state verdict (#1056). Default
6
+ * behaviour is a synchronous wait, so a linear caller (human shell, CI step,
7
+ * single-process agent) gets the verdict for free; --one-shot degrades to a
8
+ * single probe. SHA-match gating means a stale review posted BEFORE your push
9
+ * is never read as the verdict.
10
+ */
11
+ export declare function watch(prNumber: number, repo: string | null, options?: WatchOptions): WatchResult;
12
+ //# sourceMappingURL=watch.d.ts.map
@@ -0,0 +1,122 @@
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";
3
+ import { probeOnce } from "./probe.js";
4
+ const systemMonotonicClock = {
5
+ now() {
6
+ return performance.now() / 1000;
7
+ },
8
+ };
9
+ /** Synchronous, non-CPU-spinning sleep (Atomics.wait); injectable in tests. */
10
+ function defaultSleep(seconds) {
11
+ const ms = Math.max(0, Math.trunc(seconds * 1000));
12
+ if (ms === 0) {
13
+ return;
14
+ }
15
+ const shared = new Int32Array(new SharedArrayBuffer(4));
16
+ Atomics.wait(shared, 0, 0, ms);
17
+ }
18
+ function shortSha(sha) {
19
+ return sha === null ? "<none>" : sha.slice(0, 12);
20
+ }
21
+ /** One-line stderr status mirror per poll (Tier 1 instrumentation shape, #1039). */
22
+ export function formatWatchStatus(poll, maxPolls, probe, elapsedSeconds) {
23
+ if (probe.error !== null) {
24
+ return `[pr:watch poll ${poll}/${maxPolls}] CONFIG-ERROR elapsed=${elapsedSeconds}s -- ${probe.error}`;
25
+ }
26
+ return (`[pr:watch poll ${poll}/${maxPolls}] head=${shortSha(probe.headSha)} ` +
27
+ `last_reviewed=${shortSha(probe.lastReviewedSha)} sha_match=${probe.shaMatch} ` +
28
+ `confidence=${probe.confidence} p0=${probe.p0Count} p1=${probe.p1Count} ` +
29
+ `errored=${probe.errored} ci_failures=${probe.ciFailures} is_clean=${probe.isClean} ` +
30
+ `clean_gate_holdout=${probe.cleanGateHoldout} elapsed=${elapsedSeconds}s`);
31
+ }
32
+ /**
33
+ * Blocking-by-default poll to a terminal three-state verdict (#1056). Default
34
+ * behaviour is a synchronous wait, so a linear caller (human shell, CI step,
35
+ * single-process agent) gets the verdict for free; --one-shot degrades to a
36
+ * single probe. SHA-match gating means a stale review posted BEFORE your push
37
+ * is never read as the verdict.
38
+ */
39
+ export function watch(prNumber, repo, options = {}) {
40
+ const maxWaitMinutes = options.maxWaitMinutes ?? DEFAULT_MAX_WAIT_MINUTES;
41
+ const pollSeconds = Math.max(1, options.pollSeconds ?? DEFAULT_POLL_SECONDS);
42
+ const oneShot = options.oneShot ?? false;
43
+ const stallThreshold = options.stallThreshold ?? DEFAULT_STALL_THRESHOLD;
44
+ const runGh = options.runGh ?? defaultRunGh;
45
+ const clockFn = options.clockFn ?? systemMonotonicClock;
46
+ const sleepFn = options.sleepFn ?? defaultSleep;
47
+ const probeFn = options.probeFn ?? probeOnce;
48
+ const capSeconds = Math.max(0, maxWaitMinutes * 60);
49
+ // Bounded loop (time is still the authority via the cap check below) so no
50
+ // unbounded `while (true)`; +1 covers the trailing poll after the final wait.
51
+ const maxPolls = oneShot ? 1 : Math.max(1, Math.ceil(capSeconds / pollSeconds) + 1);
52
+ const startedAt = clockFn.now();
53
+ let lastProbe = null;
54
+ let stallStreak = 0;
55
+ const build = (verdict, exitCode, probe, poll) => ({
56
+ verdict,
57
+ exitCode,
58
+ prNumber,
59
+ probe,
60
+ elapsedSeconds: Math.round(clockFn.now() - startedAt),
61
+ pollCount: poll,
62
+ });
63
+ for (let poll = 1; poll <= maxPolls; poll += 1) {
64
+ const probe = probeFn(prNumber, repo, runGh);
65
+ lastProbe = probe;
66
+ const elapsed = Math.round(clockFn.now() - startedAt);
67
+ process.stderr.write(`${formatWatchStatus(poll, maxPolls, probe, elapsed)}\n`);
68
+ if (probe.error !== null) {
69
+ return build(VERDICT_CONFIG, EXIT_TERMINAL_ERROR, probe, poll);
70
+ }
71
+ if (probe.isClean) {
72
+ return build(VERDICT_CLEAN, EXIT_CLEAN, probe, poll);
73
+ }
74
+ // SHA-match gate: blocking findings only count as a verdict when the review
75
+ // is on the CURRENT HEAD -- a stale pre-push review is not read as NEW_P0_P1.
76
+ if (probe.hasBlocking && probe.shaMatch) {
77
+ return build(VERDICT_NEW_P0_P1, EXIT_NEW_P0_P1, probe, poll);
78
+ }
79
+ if (probe.errored) {
80
+ return build(VERDICT_ERRORED, EXIT_TERMINAL_ERROR, probe, poll);
81
+ }
82
+ // Stall = a review IS present but stuck on a non-HEAD commit; surface it
83
+ // rather than burning the whole cap waiting for a re-review that isn't coming.
84
+ if (probe.found && !probe.shaMatch) {
85
+ stallStreak += 1;
86
+ }
87
+ else {
88
+ stallStreak = 0;
89
+ }
90
+ if (stallStreak >= stallThreshold) {
91
+ return build(VERDICT_STALL, EXIT_TERMINAL_ERROR, probe, poll);
92
+ }
93
+ if (oneShot) {
94
+ return build(VERDICT_PENDING, EXIT_TERMINAL_ERROR, probe, poll);
95
+ }
96
+ const elapsedNow = clockFn.now() - startedAt;
97
+ if (poll >= maxPolls || elapsedNow + pollSeconds >= capSeconds) {
98
+ return build(VERDICT_TIMEOUT, EXIT_TERMINAL_ERROR, probe, poll);
99
+ }
100
+ sleepFn(pollSeconds);
101
+ }
102
+ // Unreachable in practice (the in-loop cap check returns TIMEOUT first); kept
103
+ // as a total-function fallback so the return type is honoured.
104
+ const probe = lastProbe ?? {
105
+ found: false,
106
+ headSha: null,
107
+ lastReviewedSha: null,
108
+ shaMatch: false,
109
+ confidence: null,
110
+ p0Count: 0,
111
+ p1Count: 0,
112
+ hasBlocking: false,
113
+ errored: false,
114
+ ciFailures: 0,
115
+ terminalCheckRun: false,
116
+ isClean: false,
117
+ cleanGateHoldout: null,
118
+ error: null,
119
+ };
120
+ return build(VERDICT_TIMEOUT, EXIT_TERMINAL_ERROR, probe, maxPolls);
121
+ }
122
+ //# sourceMappingURL=watch.js.map