@deftai/directive-core 0.79.1 → 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 (76) hide show
  1. package/dist/cache/fetch.js +14 -4
  2. package/dist/capacity/backfill.js +4 -2
  3. package/dist/category-b-namespace/index.js +31 -6
  4. package/dist/check/orchestrator.d.ts +4 -0
  5. package/dist/check/orchestrator.js +9 -1
  6. package/dist/content-contracts/skills/skill-frontmatter.js +6 -2
  7. package/dist/eval/health.js +8 -1
  8. package/dist/eval/run.js +2 -0
  9. package/dist/fs/projection-containment.d.ts +11 -0
  10. package/dist/fs/projection-containment.js +62 -1
  11. package/dist/hooks/dispatcher.d.ts +1 -1
  12. package/dist/hooks/dispatcher.js +66 -7
  13. package/dist/init-deposit/hygiene.js +3 -0
  14. package/dist/intake/reconcile-issues.d.ts +1 -1
  15. package/dist/intake/reconcile-issues.js +25 -2
  16. package/dist/issue-sync/sync-from-xbrief-cli.d.ts +1 -0
  17. package/dist/issue-sync/sync-from-xbrief-cli.js +3 -0
  18. package/dist/issue-sync/sync-from-xbrief.d.ts +4 -0
  19. package/dist/issue-sync/sync-from-xbrief.js +12 -0
  20. package/dist/lifecycle/event.d.ts +15 -0
  21. package/dist/lifecycle/event.js +163 -0
  22. package/dist/lifecycle/index.d.ts +1 -0
  23. package/dist/lifecycle/index.js +1 -0
  24. package/dist/orchestration/verify-judgment-gates.js +18 -2
  25. package/dist/platform/resolve-version.d.ts +2 -6
  26. package/dist/platform/resolve-version.js +3 -129
  27. package/dist/pr-merge-readiness/compute.d.ts +2 -1
  28. package/dist/pr-merge-readiness/compute.js +23 -6
  29. package/dist/pr-merge-readiness/evaluate.d.ts +2 -1
  30. package/dist/pr-merge-readiness/evaluate.js +46 -31
  31. package/dist/pr-merge-readiness/gh.js +13 -5
  32. package/dist/pr-merge-readiness/greptile-inline.d.ts +27 -0
  33. package/dist/pr-merge-readiness/greptile-inline.js +239 -0
  34. package/dist/pr-merge-readiness/index.d.ts +1 -0
  35. package/dist/pr-merge-readiness/index.js +1 -0
  36. package/dist/pr-merge-readiness/mergeability.d.ts +2 -1
  37. package/dist/pr-merge-readiness/mergeability.js +7 -1
  38. package/dist/pr-merge-readiness/test-gh-fixtures.helpers.d.ts +18 -0
  39. package/dist/pr-merge-readiness/test-gh-fixtures.helpers.js +76 -0
  40. package/dist/pr-monitor/monitor.js +10 -1
  41. package/dist/pr-watch/constants.d.ts +5 -0
  42. package/dist/pr-watch/constants.js +27 -0
  43. package/dist/pr-watch/index.d.ts +1 -1
  44. package/dist/pr-watch/index.js +1 -1
  45. package/dist/pr-watch/main.d.ts +3 -0
  46. package/dist/pr-watch/main.js +29 -3
  47. package/dist/release/constants.d.ts +5 -0
  48. package/dist/release/constants.js +14 -2
  49. package/dist/release/flags.js +16 -0
  50. package/dist/release/main.js +7 -0
  51. package/dist/release/pipeline.js +7 -3
  52. package/dist/release/preflight.js +8 -1
  53. package/dist/release/skip-ci-incident.d.ts +22 -0
  54. package/dist/release/skip-ci-incident.js +68 -0
  55. package/dist/release/types.d.ts +2 -0
  56. package/dist/release/version.js +68 -32
  57. package/dist/release-e2e/entrypoint-worker.js +1 -0
  58. package/dist/release-e2e/entrypoint.js +13 -0
  59. package/dist/review-monitor/constants.d.ts +14 -0
  60. package/dist/review-monitor/constants.js +55 -0
  61. package/dist/review-monitor/index.d.ts +5 -0
  62. package/dist/review-monitor/index.js +5 -0
  63. package/dist/review-monitor/record.d.ts +50 -0
  64. package/dist/review-monitor/record.js +178 -0
  65. package/dist/review-monitor/tier-detection.d.ts +15 -0
  66. package/dist/review-monitor/tier-detection.js +61 -0
  67. package/dist/review-monitor/verify.d.ts +32 -0
  68. package/dist/review-monitor/verify.js +171 -0
  69. package/dist/scope/demote.js +20 -0
  70. package/dist/swarm/worktrees.d.ts +3 -0
  71. package/dist/swarm/worktrees.js +18 -2
  72. package/dist/task-surface/index.js +3 -2
  73. package/dist/vitest-runner/win32-coverage-tmp-setup.d.ts +16 -8
  74. package/dist/vitest-runner/win32-coverage-tmp-setup.js +41 -14
  75. package/dist/xbrief-migrate/migration-containment.js +9 -25
  76. package/package.json +7 -3
@@ -0,0 +1,163 @@
1
+ import { resolve } from "node:path";
2
+ import { emit, main as eventsMain, readEvents } from "./events.js";
3
+ const APPROVAL_PHRASES = new Set(["yes", "confirmed", "approve", "other"]);
4
+ function parseBooleanFlag(value) {
5
+ return ["1", "true", "yes"].includes(value.toLowerCase());
6
+ }
7
+ function repositoryFromPlanRef(planRef) {
8
+ const match = /^https?:\/\/github\.com\/([^/]+\/[^/]+)\/pull\/\d+/i.exec(planRef.trim());
9
+ return match?.[1];
10
+ }
11
+ function dedupeKey(payload) {
12
+ const planRef = payload.plan_ref;
13
+ const approver = payload.approver;
14
+ if (typeof planRef !== "string" || typeof approver !== "string") {
15
+ return null;
16
+ }
17
+ const parts = [planRef, approver];
18
+ if (typeof payload.pr_number === "number") {
19
+ parts.push(String(payload.pr_number));
20
+ }
21
+ if (typeof payload.head_sha === "string" && payload.head_sha.length > 0) {
22
+ parts.push(payload.head_sha);
23
+ }
24
+ return parts.join("|");
25
+ }
26
+ function findExistingPlanApproved(key, logPath) {
27
+ for (const record of readEvents(logPath)) {
28
+ if (record.event !== "plan:approved") {
29
+ continue;
30
+ }
31
+ if (dedupeKey(record.payload) === key) {
32
+ return record;
33
+ }
34
+ }
35
+ return null;
36
+ }
37
+ function validatePlanApprovedPayload(payload) {
38
+ const phrase = payload.approval_phrase;
39
+ if (phrase === undefined) {
40
+ return;
41
+ }
42
+ if (typeof phrase !== "string") {
43
+ throw new Error("approval_phrase must be a string");
44
+ }
45
+ const normalized = phrase.toLowerCase();
46
+ if (!APPROVAL_PHRASES.has(normalized)) {
47
+ throw new Error(`invalid approval_phrase '${phrase}'; expected one of yes, confirmed, approve, other`);
48
+ }
49
+ }
50
+ /** Parse `emit <event> ...` argv for lifecycle:event approval recording. */
51
+ export function parseEmitInvocation(args) {
52
+ const payload = {};
53
+ let name;
54
+ let log;
55
+ let i = 0;
56
+ if (args.length > 0 && !args[0]?.startsWith("--")) {
57
+ name = args[0];
58
+ i = 1;
59
+ }
60
+ if (name === undefined) {
61
+ throw new Error("event name required");
62
+ }
63
+ while (i < args.length) {
64
+ const arg = args[i];
65
+ if (arg === "--payload") {
66
+ const raw = args[i + 1];
67
+ if (raw === undefined) {
68
+ throw new Error("--payload requires a value");
69
+ }
70
+ const data = JSON.parse(raw);
71
+ if (typeof data !== "object" || data === null || Array.isArray(data)) {
72
+ throw new Error("--payload must be a JSON object");
73
+ }
74
+ Object.assign(payload, data);
75
+ i += 2;
76
+ continue;
77
+ }
78
+ if (arg === "--log") {
79
+ log = args[i + 1];
80
+ i += 2;
81
+ continue;
82
+ }
83
+ const flagMap = {
84
+ "--plan-ref": "plan_ref",
85
+ "--approver": "approver",
86
+ "--approval-phrase": "approval_phrase",
87
+ "--head-sha": "head_sha",
88
+ };
89
+ if (arg === "--pr-number") {
90
+ payload.pr_number = Number.parseInt(args[i + 1] ?? "", 10);
91
+ i += 2;
92
+ continue;
93
+ }
94
+ const field = flagMap[arg ?? ""];
95
+ if (field !== undefined) {
96
+ payload[field] = args[i + 1];
97
+ i += 2;
98
+ continue;
99
+ }
100
+ if (arg === "--inline") {
101
+ payload.inline = parseBooleanFlag(args[i + 1] ?? "");
102
+ i += 2;
103
+ continue;
104
+ }
105
+ throw new Error(`unrecognized arguments: ${arg}`);
106
+ }
107
+ return { name, payload, log };
108
+ }
109
+ function enrichPlanApprovedPayload(payload) {
110
+ const next = { ...payload };
111
+ if (typeof next.plan_ref === "string" && next.repository === undefined) {
112
+ const repository = repositoryFromPlanRef(next.plan_ref);
113
+ if (repository !== undefined) {
114
+ next.repository = repository;
115
+ }
116
+ }
117
+ return next;
118
+ }
119
+ function emitPlanApprovedIdempotent(payload, options = {}) {
120
+ const enriched = enrichPlanApprovedPayload(payload);
121
+ validatePlanApprovedPayload(enriched);
122
+ const key = dedupeKey(enriched);
123
+ if (key !== null) {
124
+ const existing = findExistingPlanApproved(key, options.logPath ?? null);
125
+ if (existing !== null) {
126
+ return existing;
127
+ }
128
+ }
129
+ return emit("plan:approved", enriched, options);
130
+ }
131
+ /** CLI entry for `lifecycle:event` / review-cycle approval recorder (#2631). */
132
+ export function runLifecycleEvent(argv, options = {}) {
133
+ if (options.projectRoot !== undefined) {
134
+ process.chdir(resolve(options.projectRoot));
135
+ }
136
+ if (argv.length === 0 || argv[0] === "-h" || argv[0] === "--help" || argv[0] === "help") {
137
+ process.stderr.write("usage: lifecycle:event emit plan:approved --plan-ref <url> --approver <login> " +
138
+ "[--approval-phrase yes|confirmed|approve] [--pr-number N] [--head-sha SHA]\n");
139
+ return argv.length === 0 ? 2 : 0;
140
+ }
141
+ if (argv[0] !== "emit") {
142
+ return eventsMain(argv);
143
+ }
144
+ if (argv[1] !== "plan:approved") {
145
+ return eventsMain(argv);
146
+ }
147
+ try {
148
+ const parsed = parseEmitInvocation(argv.slice(1));
149
+ const record = emitPlanApprovedIdempotent(parsed.payload, { logPath: parsed.log ?? null });
150
+ process.stdout.write(`${JSON.stringify(record)}\n`);
151
+ return 0;
152
+ }
153
+ catch (error) {
154
+ const message = error instanceof Error ? error.message : String(error);
155
+ process.stderr.write(`lifecycle:event failed: ${message}\n`);
156
+ return 2;
157
+ }
158
+ }
159
+ /** Process entrypoint mirroring other lifecycle CLI modules. */
160
+ export function main(argv = process.argv.slice(2)) {
161
+ return runLifecycleEvent(argv);
162
+ }
163
+ //# sourceMappingURL=event.js.map
@@ -1,3 +1,4 @@
1
+ export * from "./event.js";
1
2
  export * as eventDetect from "./event-detect.js";
2
3
  export * as events from "./events.js";
3
4
  export * as lifecycleHygiene from "./lifecycle-hygiene.js";
@@ -1,3 +1,4 @@
1
+ export * from "./event.js";
1
2
  export * as eventDetect from "./event-detect.js";
2
3
  export * as events from "./events.js";
3
4
  export * as lifecycleHygiene from "./lifecycle-hygiene.js";
@@ -3,8 +3,9 @@
3
3
  */
4
4
  import { execFileSync } from "node:child_process";
5
5
  import { createHash, randomUUID } from "node:crypto";
6
- import { appendFileSync, existsSync, mkdirSync, readFileSync, statSync } from "node:fs";
6
+ import { appendFileSync, existsSync, lstatSync, mkdirSync, readFileSync, statSync } from "node:fs";
7
7
  import { join, resolve } from "node:path";
8
+ import { assertProjectionContained, assertWriteTargetSafe, ProjectionContainmentError, } from "../fs/projection-containment.js";
8
9
  import { resolveAuditDir } from "../layout/resolve.js";
9
10
  import { resolveJudgmentGates } from "./judgment-policy.js";
10
11
  import { matchAny } from "./pathspec.js";
@@ -163,7 +164,22 @@ export function readClearances(projectRoot, logPath) {
163
164
  }
164
165
  export function recordClearance(projectRoot, options) {
165
166
  const path = options.log_path ?? clearanceLogPath(projectRoot);
166
- mkdirSync(resolveAuditDir(projectRoot), { recursive: true });
167
+ const auditDir = resolveAuditDir(projectRoot);
168
+ assertProjectionContained(projectRoot, auditDir);
169
+ assertWriteTargetSafe(projectRoot, path);
170
+ try {
171
+ const info = lstatSync(auditDir);
172
+ if (info.isSymbolicLink()) {
173
+ throw new ProjectionContainmentError(`projection write refused: audit directory ${auditDir} must not be a symlink`, { projectDir: resolve(projectRoot), targetPath: auditDir, offendingPath: auditDir });
174
+ }
175
+ }
176
+ catch (err) {
177
+ if (err instanceof ProjectionContainmentError) {
178
+ throw err;
179
+ }
180
+ // Audit dir does not exist yet; mkdirSync below will create it under containment.
181
+ }
182
+ mkdirSync(auditDir, { recursive: true });
167
183
  const entry = {
168
184
  clearance_id: randomUUID(),
169
185
  timestamp: utcNowIso(options.now),
@@ -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 {
@@ -1,9 +1,10 @@
1
1
  import type { CiGateOptions } from "./ci-gate.js";
2
2
  import { isMergeReady } from "./evaluate.js";
3
+ import { type InlineGreptileFindings } from "./greptile-inline.js";
3
4
  import { type MergeabilitySignal } from "./mergeability.js";
4
5
  import type { SlizardGateOptions } from "./slizard-gate.js";
5
6
  import type { GateResult, RunGhFn } from "./types.js";
6
- declare function buildGateResult(prNumber: number, repo: string | null, headSha: string | null, body: string, via: string, partialData?: Record<string, unknown>, error?: string | null): GateResult;
7
+ declare function buildGateResult(prNumber: number, repo: string | null, headSha: string | null, body: string, via: string, partialData?: Record<string, unknown>, error?: string | null, inline?: InlineGreptileFindings | null): GateResult;
7
8
  /** Injectable GitHub-mergeability read seam (#2260) — keeps unit tests hermetic. */
8
9
  export type FetchMergeabilityFn = (prNumber: number, repo: string, runGh: RunGhFn) => MergeabilitySignal;
9
10
  export interface ComputeGateOptions extends CiGateOptions, SlizardGateOptions {
@@ -2,12 +2,13 @@ import { buildCiSummaryLine, evaluateCiGate } from "./ci-gate.js";
2
2
  import { FALLBACK2_NOT_CLEAN_MSG, VIA_ERROR, VIA_FALLBACK1, VIA_FALLBACK2, VIA_PRIMARY, } from "./constants.js";
3
3
  import { evaluateGates, isMergeReady } from "./evaluate.js";
4
4
  import { fetchCheckRunsRest, fetchGreptileBodyRest, fetchGreptileCommentBody, fetchPrHeadSha, fetchPrHeadShaRest, resolveRepo, } from "./gh.js";
5
+ import { fetchUnresolvedGreptileInlineFindings, inlineFindingsToDict, } from "./greptile-inline.js";
5
6
  import { fetchMergeability, isGithubMergeableClean, mergeabilityToDict, verdictBlockIsSoftOnly, verdictShaIsStale, } from "./mergeability.js";
6
7
  import { emptyVerdict, parseGreptileBody } from "./parse.js";
7
8
  import { evaluateSlizardGate, isSlizardCheck } from "./slizard-gate.js";
8
- function buildGateResult(prNumber, repo, headSha, body, via, partialData = {}, error = null) {
9
+ function buildGateResult(prNumber, repo, headSha, body, via, partialData = {}, error = null, inline = null) {
9
10
  const verdict = parseGreptileBody(body);
10
- const failures = evaluateGates(prNumber, headSha, verdict);
11
+ const failures = evaluateGates(prNumber, headSha, verdict, inline);
11
12
  return {
12
13
  prNumber,
13
14
  repo,
@@ -92,10 +93,24 @@ function applyCiGateForHead(repo, headSha, runGh, options) {
92
93
  * is only soft-blocked -- absent or stale for the current head) reconcile
93
94
  * against GitHub's own mergeability (#2260). Shared by primary + fallback1.
94
95
  */
96
+ function loadInlineGreptileFindings(prNumber, repo, headSha, runGh) {
97
+ const resolved = resolveRepo(repo, runGh);
98
+ if (resolved.repo === null) {
99
+ return {
100
+ p0Count: 0,
101
+ p1Count: 0,
102
+ unresolvedThreadCount: 0,
103
+ error: resolved.error || "repo unresolved for inline reviewThreads lookup; pass --repo OWNER/REPO",
104
+ };
105
+ }
106
+ return fetchUnresolvedGreptileInlineFindings(prNumber, resolved.repo, headSha, runGh);
107
+ }
95
108
  function finalizeVerdictGate(prNumber, repo, headSha, verdict, runGh, options) {
96
- const failures = evaluateGates(prNumber, headSha, verdict);
97
109
  const partialData = {};
98
110
  const resolved = resolveRepo(repo, runGh);
111
+ const inline = loadInlineGreptileFindings(prNumber, repo, headSha, runGh);
112
+ partialData.greptile_inline = inlineFindingsToDict(inline);
113
+ const failures = evaluateGates(prNumber, headSha, verdict, inline);
99
114
  if (failures.length === 0) {
100
115
  const ci = applyCiGateForHead(resolved.repo, headSha, runGh, options);
101
116
  failures.push(...ci.failures);
@@ -108,7 +123,7 @@ function finalizeVerdictGate(prNumber, repo, headSha, verdict, runGh, options) {
108
123
  // blocking. Only reconcile a SOFT block (verdict absent / stale head SHA).
109
124
  if (options.disableMergeabilityReconcile === true ||
110
125
  resolved.repo === null ||
111
- !verdictBlockIsSoftOnly(verdict, headSha)) {
126
+ !verdictBlockIsSoftOnly(verdict, headSha, inline)) {
112
127
  return { failures, partialData };
113
128
  }
114
129
  const ci = applyCiGateForHead(resolved.repo, headSha, runGh, options);
@@ -143,17 +158,19 @@ function computePrimary(prNumber, repo, runGh, options) {
143
158
  return { result: null, partial };
144
159
  }
145
160
  partial.head_sha = headSha;
161
+ const resolved = resolveRepo(repo, runGh);
162
+ const effectiveRepo = resolved.repo ?? repo;
146
163
  const body = fetchGreptileCommentBody(prNumber, repo, runGh);
147
164
  if (body === null) {
148
165
  partial.primary_error = "gh api /issues/<N>/comments --jq returned non-zero";
149
166
  return { result: null, partial };
150
167
  }
151
168
  const verdict = parseGreptileBody(body);
152
- const { failures, partialData } = finalizeVerdictGate(prNumber, repo, headSha, verdict, runGh, options);
169
+ const { failures, partialData } = finalizeVerdictGate(prNumber, effectiveRepo, headSha, verdict, runGh, options);
153
170
  return {
154
171
  result: {
155
172
  prNumber,
156
- repo,
173
+ repo: effectiveRepo,
157
174
  headSha,
158
175
  verdict,
159
176
  failures,
@@ -1,5 +1,6 @@
1
+ import type { InlineGreptileFindings } from "./greptile-inline.js";
1
2
  import type { GreptileVerdict } from "./types.js";
2
3
  /** Return failure messages (empty list == merge-ready). */
3
- export declare function evaluateGates(_prNumber: number, headSha: string | null, verdict: GreptileVerdict): string[];
4
+ export declare function evaluateGates(_prNumber: number, headSha: string | null, verdict: GreptileVerdict, inline?: InlineGreptileFindings | null): string[];
4
5
  export declare function isMergeReady(failures: readonly string[]): boolean;
5
6
  //# sourceMappingURL=evaluate.d.ts.map
@@ -1,6 +1,6 @@
1
1
  import { INFORMAL_CLEAN_DIAGNOSTIC } from "./constants.js";
2
2
  /** Return failure messages (empty list == merge-ready). */
3
- export function evaluateGates(_prNumber, headSha, verdict) {
3
+ export function evaluateGates(_prNumber, headSha, verdict, inline = null) {
4
4
  const failures = [];
5
5
  if (!verdict.found) {
6
6
  failures.push("No Greptile rolling-summary comment found on the PR. " +
@@ -8,40 +8,55 @@ export function evaluateGates(_prNumber, headSha, verdict) {
8
8
  "Wait for the review to land before merging (see #796 late-bot-review re-check).");
9
9
  return failures;
10
10
  }
11
- if (verdict.excludedAuthor) {
12
- return failures;
13
- }
14
- if (verdict.errored) {
15
- failures.push("Greptile review is in the ERRORED state on the current HEAD (#526). " +
16
- "Retry via @greptileai or escalate per " +
17
- "skills/deft-directive-swarm/SKILL.md Phase 6 Step 1.");
11
+ if (!verdict.excludedAuthor) {
12
+ if (verdict.errored) {
13
+ failures.push("Greptile review is in the ERRORED state on the current HEAD (#526). " +
14
+ "Retry via @greptileai or escalate per " +
15
+ "skills/deft-directive-swarm/SKILL.md Phase 6 Step 1.");
16
+ }
17
+ if (verdict.informalClean) {
18
+ failures.push(INFORMAL_CLEAN_DIAGNOSTIC);
19
+ return appendInlineFailures(failures, inline);
20
+ }
21
+ if (verdict.lastReviewedSha === null) {
22
+ failures.push("Could not parse `Last reviewed commit:` from Greptile body. " +
23
+ "The comment may be malformed or Greptile may still be writing it -- re-fetch.");
24
+ }
25
+ else if (headSha &&
26
+ !(headSha.startsWith(verdict.lastReviewedSha) || verdict.lastReviewedSha.startsWith(headSha))) {
27
+ failures.push(`Greptile last reviewed ${verdict.lastReviewedSha} but PR HEAD is ${headSha}. ` +
28
+ "Review is stale -- wait for Greptile to re-review the latest commit.");
29
+ }
30
+ if (verdict.confidence === null) {
31
+ failures.push("Could not parse `Confidence Score: X/5` from Greptile body. " +
32
+ "Confidence is a required exit-condition input per " +
33
+ "skills/deft-directive-review-cycle/SKILL.md Phase 2 Step 6.");
34
+ }
35
+ else if (verdict.confidence <= 3) {
36
+ failures.push(`Greptile confidence is ${verdict.confidence}/5; exit condition requires > 3. ` +
37
+ "Address remaining findings or push clarifying changes.");
38
+ }
39
+ if (verdict.p0Count > 0 || verdict.p1Count > 0) {
40
+ failures.push(`Greptile reports ${verdict.p0Count} P0 and ${verdict.p1Count} P1 findings ` +
41
+ "on the current HEAD. All P0 / P1 findings MUST be addressed before merge " +
42
+ "(P2 findings are non-blocking).");
43
+ }
18
44
  }
19
- if (verdict.informalClean) {
20
- failures.push(INFORMAL_CLEAN_DIAGNOSTIC);
45
+ return appendInlineFailures(failures, inline);
46
+ }
47
+ function appendInlineFailures(failures, inline) {
48
+ if (inline === null) {
21
49
  return failures;
22
50
  }
23
- if (verdict.lastReviewedSha === null) {
24
- failures.push("Could not parse `Last reviewed commit:` from Greptile body. " +
25
- "The comment may be malformed or Greptile may still be writing it -- re-fetch.");
26
- }
27
- else if (headSha &&
28
- !(headSha.startsWith(verdict.lastReviewedSha) || verdict.lastReviewedSha.startsWith(headSha))) {
29
- failures.push(`Greptile last reviewed ${verdict.lastReviewedSha} but PR HEAD is ${headSha}. ` +
30
- "Review is stale -- wait for Greptile to re-review the latest commit.");
31
- }
32
- if (verdict.confidence === null) {
33
- failures.push("Could not parse `Confidence Score: X/5` from Greptile body. " +
34
- "Confidence is a required exit-condition input per " +
35
- "skills/deft-directive-review-cycle/SKILL.md Phase 2 Step 6.");
36
- }
37
- else if (verdict.confidence <= 3) {
38
- failures.push(`Greptile confidence is ${verdict.confidence}/5; exit condition requires > 3. ` +
39
- "Address remaining findings or push clarifying changes.");
51
+ if (inline.error !== null) {
52
+ failures.push("Could not verify Greptile inline review comments on the current HEAD (#2620). " +
53
+ `Root cause: ${inline.error}`);
40
54
  }
41
- if (verdict.p0Count > 0 || verdict.p1Count > 0) {
42
- failures.push(`Greptile reports ${verdict.p0Count} P0 and ${verdict.p1Count} P1 findings ` +
43
- "on the current HEAD. All P0 / P1 findings MUST be addressed before merge " +
44
- "(P2 findings are non-blocking).");
55
+ else if (inline.p0Count > 0 || inline.p1Count > 0) {
56
+ failures.push(`Greptile has ${inline.p0Count} unresolved inline P0 and ${inline.p1Count} unresolved inline P1 ` +
57
+ `review comment(s) on the current HEAD across ${inline.unresolvedThreadCount} open thread(s). ` +
58
+ "Resolve or address all blocking inline threads before merge -- rolling-summary badge counts alone " +
59
+ "are insufficient (#2620).");
45
60
  }
46
61
  return failures;
47
62
  }
@@ -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,27 @@
1
+ import type { RunGhFn } from "./types.js";
2
+ /** Unresolved Greptile inline P0/P1 on the current PR HEAD (#2620). */
3
+ export interface InlineGreptileFindings {
4
+ readonly p0Count: number;
5
+ readonly p1Count: number;
6
+ readonly unresolvedThreadCount: number;
7
+ readonly error: string | null;
8
+ }
9
+ export interface InlineReviewComment {
10
+ readonly authorLogin: string;
11
+ readonly body: string;
12
+ readonly path: string | null;
13
+ readonly commitOid: string | null;
14
+ }
15
+ export interface InlineReviewThread {
16
+ readonly isResolved: boolean;
17
+ readonly isOutdated: boolean;
18
+ readonly comments: readonly InlineReviewComment[];
19
+ }
20
+ /** True when a review comment commit SHA matches the current PR head (#2620 AC-2). */
21
+ export declare function headShaMatches(commentSha: string, headSha: string): boolean;
22
+ export declare function inlineFindingsToDict(findings: InlineGreptileFindings): Record<string, unknown>;
23
+ /** Score unresolved Greptile inline P0/P1 threads pinned to the current HEAD (#2620). */
24
+ export declare function evaluateInlineReviewThreads(threads: readonly InlineReviewThread[], headSha: string): InlineGreptileFindings;
25
+ /** Fetch unresolved Greptile inline P0/P1 on the current HEAD via reviewThreads GraphQL (#2620). */
26
+ export declare function fetchUnresolvedGreptileInlineFindings(prNumber: number, repo: string, headSha: string, runGh: RunGhFn): InlineGreptileFindings;
27
+ //# sourceMappingURL=greptile-inline.d.ts.map