@devinnn/docdrift 0.1.1 → 0.1.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.
package/dist/src/index.js CHANGED
@@ -1,4 +1,37 @@
1
1
  "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
2
35
  var __importDefault = (this && this.__importDefault) || function (mod) {
3
36
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
37
  };
@@ -7,10 +40,12 @@ exports.STATE_PATH = void 0;
7
40
  exports.runDetect = runDetect;
8
41
  exports.runDocDrift = runDocDrift;
9
42
  exports.runValidate = runValidate;
43
+ exports.runSlaCheck = runSlaCheck;
10
44
  exports.runStatus = runStatus;
11
45
  exports.resolveTrigger = resolveTrigger;
12
46
  exports.parseDurationHours = parseDurationHours;
13
47
  exports.requireSha = requireSha;
48
+ exports.resolveBaseHead = resolveBaseHead;
14
49
  const node_path_1 = __importDefault(require("node:path"));
15
50
  const load_1 = require("./config/load");
16
51
  const validate_1 = require("./config/validate");
@@ -21,6 +56,7 @@ const engine_1 = require("./policy/engine");
21
56
  const state_1 = require("./policy/state");
22
57
  const log_1 = require("./utils/log");
23
58
  const prompts_1 = require("./devin/prompts");
59
+ const glob_1 = require("./utils/glob");
24
60
  const schemas_1 = require("./devin/schemas");
25
61
  const v1_1 = require("./devin/v1");
26
62
  function parseStructured(session) {
@@ -48,29 +84,20 @@ function inferQuestions(structured) {
48
84
  "What are the exact user-visible semantics after this merge?",
49
85
  ];
50
86
  }
51
- async function executeSession(input) {
87
+ async function executeSessionSingle(input) {
52
88
  const attachmentUrls = [];
53
89
  for (const attachmentPath of input.attachmentPaths) {
54
90
  const url = await (0, v1_1.devinUploadAttachment)(input.apiKey, attachmentPath);
55
91
  attachmentUrls.push(url);
56
92
  }
57
- const prompt = input.item.mode === "autogen"
58
- ? (0, prompts_1.buildAutogenPrompt)({
59
- item: input.item,
60
- attachmentUrls,
61
- verificationCommands: input.config.policy.verification.commands,
62
- allowlist: input.config.policy.allowlist,
63
- confidenceThreshold: input.config.policy.confidence.autopatchThreshold,
64
- customAppend: input.config.devin.customInstructionContent ?? undefined,
65
- })
66
- : (0, prompts_1.buildConceptualPrompt)({
67
- item: input.item,
68
- attachmentUrls,
69
- verificationCommands: input.config.policy.verification.commands,
70
- allowlist: input.config.policy.allowlist,
71
- confidenceThreshold: input.config.policy.confidence.autopatchThreshold,
72
- customAppend: input.config.devin.customInstructionContent ?? undefined,
73
- });
93
+ const prompt = (0, prompts_1.buildWholeDocsitePrompt)({
94
+ aggregated: input.aggregated,
95
+ config: input.config,
96
+ attachmentUrls,
97
+ runGate: input.runGate,
98
+ trigger: input.trigger,
99
+ prNumber: input.prNumber,
100
+ });
74
101
  const session = await (0, v1_1.devinCreateSession)(input.apiKey, {
75
102
  prompt,
76
103
  unlisted: input.config.devin.unlisted,
@@ -132,14 +159,16 @@ async function runDetect(options) {
132
159
  throw new Error(`Config validation failed:\n${runtimeValidation.errors.join("\n")}`);
133
160
  }
134
161
  const repo = process.env.GITHUB_REPOSITORY ?? "local/docdrift";
135
- const { report } = await (0, detect_1.buildDriftReport)({
136
- config,
162
+ const normalized = (0, load_1.loadNormalizedConfig)();
163
+ const { report, runGate } = await (0, detect_1.buildDriftReport)({
164
+ config: normalized,
137
165
  repo,
138
166
  baseSha: options.baseSha,
139
167
  headSha: options.headSha,
140
168
  trigger: options.trigger ?? "manual",
169
+ prNumber: options.prNumber,
141
170
  });
142
- (0, log_1.logInfo)(`Drift items detected: ${report.items.length}`);
171
+ (0, log_1.logInfo)(`Drift items detected: ${report.items.length} (runGate: ${runGate})`);
143
172
  return { hasDrift: report.items.length > 0 };
144
173
  }
145
174
  async function runDocDrift(options) {
@@ -148,183 +177,245 @@ async function runDocDrift(options) {
148
177
  if (runtimeValidation.errors.length) {
149
178
  throw new Error(`Config validation failed:\n${runtimeValidation.errors.join("\n")}`);
150
179
  }
180
+ const normalized = (0, load_1.loadNormalizedConfig)();
151
181
  const repo = process.env.GITHUB_REPOSITORY ?? "local/docdrift";
152
182
  const commitSha = process.env.GITHUB_SHA ?? options.headSha;
153
183
  const githubToken = process.env.GITHUB_TOKEN;
154
184
  const devinApiKey = process.env.DEVIN_API_KEY;
155
- const { report, runInfo, evidenceRoot } = await (0, detect_1.buildDriftReport)({
156
- config,
185
+ const { report, aggregated, runInfo, evidenceRoot, runGate } = await (0, detect_1.buildDriftReport)({
186
+ config: normalized,
157
187
  repo,
158
188
  baseSha: options.baseSha,
159
189
  headSha: options.headSha,
160
190
  trigger: options.trigger ?? "manual",
191
+ prNumber: options.prNumber,
161
192
  });
162
- const docAreaByName = new Map(config.docAreas.map((area) => [area.name, area]));
193
+ // Gate: no run (spec drift, conceptual-only, or infer) exit early, no session
194
+ if (runGate === "none" || report.items.length === 0) {
195
+ (0, log_1.logInfo)("No drift; skipping session");
196
+ return [];
197
+ }
198
+ const item = report.items[0];
199
+ const docAreaConfig = {
200
+ name: "docsite",
201
+ mode: "autogen",
202
+ owners: { reviewers: [] },
203
+ detect: { openapi: { exportCmd: normalized.openapi.export, generatedPath: normalized.openapi.generated, publishedPath: normalized.openapi.published }, paths: [] },
204
+ patch: { targets: [], requireHumanConfirmation: false },
205
+ };
163
206
  let state = (0, state_1.loadState)();
164
207
  const startedAt = Date.now();
165
208
  const results = [];
166
209
  const metrics = {
167
- driftItemsDetected: report.items.length,
210
+ driftItemsDetected: 1,
168
211
  prsOpened: 0,
169
212
  issuesOpened: 0,
170
213
  blockedCount: 0,
171
214
  timeToSessionTerminalMs: [],
172
- docAreaCounts: {},
215
+ docAreaCounts: { docsite: 1 },
173
216
  noiseRateProxy: 0,
174
217
  };
175
- for (const item of report.items) {
176
- metrics.docAreaCounts[item.docArea] = (metrics.docAreaCounts[item.docArea] ?? 0) + 1;
177
- const areaConfig = docAreaByName.get(item.docArea);
178
- if (!areaConfig) {
179
- continue;
180
- }
181
- const decision = (0, engine_1.decidePolicy)({
182
- item,
183
- docAreaConfig: areaConfig,
184
- config,
218
+ const decision = (0, engine_1.decidePolicy)({
219
+ item,
220
+ docAreaConfig,
221
+ config,
222
+ state,
223
+ repo,
224
+ baseSha: options.baseSha,
225
+ headSha: options.headSha,
226
+ });
227
+ if (decision.action === "NOOP") {
228
+ results.push({
229
+ docArea: item.docArea,
230
+ decision,
231
+ outcome: "NO_CHANGE",
232
+ summary: decision.reason,
233
+ });
234
+ (0, bundle_1.writeMetrics)(metrics);
235
+ return results;
236
+ }
237
+ if (decision.action === "UPDATE_EXISTING_PR") {
238
+ const existingPr = state.areaLatestPr["docsite"];
239
+ results.push({
240
+ docArea: item.docArea,
241
+ decision,
242
+ outcome: existingPr ? "NO_CHANGE" : "BLOCKED",
243
+ summary: existingPr ? `Bundled into existing PR: ${existingPr}` : "PR cap reached",
244
+ prUrl: existingPr,
245
+ });
246
+ state = (0, engine_1.applyDecisionToState)({
185
247
  state,
186
- repo,
187
- baseSha: options.baseSha,
188
- headSha: options.headSha,
248
+ decision,
249
+ docArea: "docsite",
250
+ outcome: existingPr ? "NO_CHANGE" : "BLOCKED",
251
+ link: existingPr,
189
252
  });
190
- if (decision.action === "NOOP") {
191
- results.push({
192
- docArea: item.docArea,
193
- decision,
194
- outcome: "NO_CHANGE",
195
- summary: decision.reason,
196
- });
197
- continue;
198
- }
199
- if (decision.action === "UPDATE_EXISTING_PR") {
200
- const existingPr = state.areaLatestPr[item.docArea];
201
- const summary = existingPr
202
- ? `Bundled into existing PR: ${existingPr}`
203
- : "PR cap reached and no existing area PR; escalated";
204
- const outcome = existingPr ? "NO_CHANGE" : "BLOCKED";
205
- results.push({
206
- docArea: item.docArea,
207
- decision,
208
- outcome,
209
- summary,
210
- prUrl: existingPr,
211
- });
212
- state = (0, engine_1.applyDecisionToState)({
213
- state,
214
- decision,
215
- docArea: item.docArea,
216
- outcome,
217
- link: existingPr,
218
- });
219
- continue;
220
- }
221
- const bundle = await (0, bundle_1.buildEvidenceBundle)({ runInfo, item, evidenceRoot });
222
- const attachmentPaths = [...new Set([bundle.archivePath, ...bundle.attachmentPaths])];
223
- let sessionOutcome = {
224
- outcome: "NO_CHANGE",
225
- summary: "Skipped Devin session",
226
- verification: config.policy.verification.commands.map((command) => ({
253
+ (0, state_1.saveState)(state);
254
+ (0, bundle_1.writeMetrics)(metrics);
255
+ return results;
256
+ }
257
+ const bundle = await (0, bundle_1.buildEvidenceBundle)({ runInfo, item, evidenceRoot });
258
+ const attachmentPaths = [...new Set([bundle.archivePath, ...bundle.attachmentPaths])];
259
+ let sessionOutcome = {
260
+ outcome: "NO_CHANGE",
261
+ summary: "Skipped Devin session",
262
+ verification: normalized.policy.verification.commands.map((command) => ({
263
+ command,
264
+ result: "not run",
265
+ })),
266
+ };
267
+ if (devinApiKey) {
268
+ const sessionStart = Date.now();
269
+ sessionOutcome = await executeSessionSingle({
270
+ apiKey: devinApiKey,
271
+ repository: repo,
272
+ item,
273
+ aggregated: aggregated,
274
+ attachmentPaths,
275
+ config: normalized,
276
+ runGate,
277
+ trigger: runInfo.trigger,
278
+ prNumber: runInfo.prNumber,
279
+ });
280
+ metrics.timeToSessionTerminalMs.push(Date.now() - sessionStart);
281
+ }
282
+ else {
283
+ (0, log_1.logWarn)("DEVIN_API_KEY not set; running fallback behavior", { docArea: item.docArea });
284
+ sessionOutcome = {
285
+ outcome: "BLOCKED",
286
+ summary: "DEVIN_API_KEY missing; cannot start Devin session",
287
+ questions: ["Set DEVIN_API_KEY in environment or GitHub Actions secrets"],
288
+ verification: normalized.policy.verification.commands.map((command) => ({
227
289
  command,
228
290
  result: "not run",
229
291
  })),
230
292
  };
231
- if (devinApiKey) {
232
- const sessionStart = Date.now();
233
- sessionOutcome = await executeSession({
234
- apiKey: devinApiKey,
293
+ }
294
+ let issueUrl;
295
+ if (sessionOutcome.outcome === "PR_OPENED" && sessionOutcome.prUrl) {
296
+ metrics.prsOpened += 1;
297
+ state.lastDocDriftPrUrl = sessionOutcome.prUrl;
298
+ state.lastDocDriftPrOpenedAt = new Date().toISOString();
299
+ if (githubToken && runInfo.trigger === "pull_request" && runInfo.prNumber) {
300
+ await (0, client_1.postPrComment)({
301
+ token: githubToken,
235
302
  repository: repo,
236
- item,
237
- attachmentPaths,
238
- config,
303
+ prNumber: runInfo.prNumber,
304
+ body: `## Doc drift detected\n\nDraft doc PR: ${sessionOutcome.prUrl}\n\nMerge your API changes first, then review and merge this doc PR.`,
239
305
  });
240
- metrics.timeToSessionTerminalMs.push(Date.now() - sessionStart);
241
- }
242
- else {
243
- (0, log_1.logWarn)("DEVIN_API_KEY not set; running fallback behavior", { docArea: item.docArea });
244
- sessionOutcome = {
245
- outcome: "BLOCKED",
246
- summary: "DEVIN_API_KEY missing; cannot start Devin session",
247
- questions: ["Set DEVIN_API_KEY in environment or GitHub Actions secrets"],
248
- verification: config.policy.verification.commands.map((command) => ({
249
- command,
250
- result: "not run",
251
- })),
252
- };
253
306
  }
254
- let issueUrl;
255
- if (githubToken &&
256
- (decision.action === "OPEN_ISSUE" ||
257
- sessionOutcome.outcome === "BLOCKED" ||
258
- sessionOutcome.outcome === "NO_CHANGE")) {
307
+ const touchedRequireReview = (item.impactedDocs ?? []).filter((p) => normalized.requireHumanReview.some((glob) => (0, glob_1.matchesGlob)(glob, p)));
308
+ if (githubToken && touchedRequireReview.length > 0) {
259
309
  issueUrl = await (0, client_1.createIssue)({
260
310
  token: githubToken,
261
311
  repository: repo,
262
312
  issue: {
263
- title: `[docdrift] ${item.docArea}: docs drift requires input`,
264
- body: (0, client_1.renderBlockedIssueBody)({
265
- docArea: item.docArea,
266
- evidenceSummary: item.summary,
267
- questions: sessionOutcome.questions ?? [
268
- "Please confirm intended behavior and doc wording.",
269
- ],
270
- sessionUrl: sessionOutcome.sessionUrl,
313
+ title: "[docdrift] Docs out of sync — review doc drift PR",
314
+ body: (0, client_1.renderRequireHumanReviewIssueBody)({
315
+ prUrl: sessionOutcome.prUrl,
316
+ touchedPaths: touchedRequireReview,
271
317
  }),
272
318
  labels: ["docdrift"],
273
319
  },
274
320
  });
275
321
  metrics.issuesOpened += 1;
276
- sessionOutcome.outcome = "ISSUE_OPENED";
277
- }
278
- if (sessionOutcome.outcome === "PR_OPENED") {
279
- metrics.prsOpened += 1;
280
322
  }
281
- if (sessionOutcome.outcome === "BLOCKED") {
282
- metrics.blockedCount += 1;
323
+ }
324
+ else if (githubToken &&
325
+ (decision.action === "OPEN_ISSUE" ||
326
+ sessionOutcome.outcome === "BLOCKED" ||
327
+ sessionOutcome.outcome === "NO_CHANGE")) {
328
+ issueUrl = await (0, client_1.createIssue)({
329
+ token: githubToken,
330
+ repository: repo,
331
+ issue: {
332
+ title: "[docdrift] docsite: docs drift requires input",
333
+ body: (0, client_1.renderBlockedIssueBody)({
334
+ docArea: item.docArea,
335
+ evidenceSummary: item.summary,
336
+ questions: sessionOutcome.questions ?? [
337
+ "Please confirm intended behavior and doc wording.",
338
+ ],
339
+ sessionUrl: sessionOutcome.sessionUrl,
340
+ }),
341
+ labels: ["docdrift"],
342
+ },
343
+ });
344
+ metrics.issuesOpened += 1;
345
+ if (sessionOutcome.outcome !== "PR_OPENED") {
346
+ sessionOutcome.outcome = "ISSUE_OPENED";
283
347
  }
284
- const result = {
348
+ }
349
+ if (sessionOutcome.outcome === "BLOCKED") {
350
+ metrics.blockedCount += 1;
351
+ }
352
+ const result = {
353
+ docArea: item.docArea,
354
+ decision,
355
+ outcome: sessionOutcome.outcome,
356
+ summary: sessionOutcome.summary,
357
+ sessionUrl: sessionOutcome.sessionUrl,
358
+ prUrl: sessionOutcome.prUrl,
359
+ issueUrl,
360
+ };
361
+ results.push(result);
362
+ state = (0, engine_1.applyDecisionToState)({
363
+ state,
364
+ decision,
365
+ docArea: "docsite",
366
+ outcome: sessionOutcome.outcome,
367
+ link: sessionOutcome.prUrl ?? issueUrl,
368
+ });
369
+ if (sessionOutcome.outcome === "PR_OPENED" && sessionOutcome.prUrl) {
370
+ state.lastDocDriftPrUrl = sessionOutcome.prUrl;
371
+ state.lastDocDriftPrOpenedAt = new Date().toISOString();
372
+ }
373
+ (0, state_1.saveState)(state);
374
+ if (githubToken) {
375
+ const body = (0, client_1.renderRunComment)({
285
376
  docArea: item.docArea,
286
- decision,
287
- outcome: sessionOutcome.outcome,
288
377
  summary: sessionOutcome.summary,
378
+ decision: decision.action,
379
+ outcome: sessionOutcome.outcome,
289
380
  sessionUrl: sessionOutcome.sessionUrl,
290
381
  prUrl: sessionOutcome.prUrl,
291
382
  issueUrl,
292
- };
293
- results.push(result);
294
- if (githubToken) {
295
- const body = (0, client_1.renderRunComment)({
296
- docArea: item.docArea,
297
- summary: sessionOutcome.summary,
298
- decision: decision.action,
299
- outcome: sessionOutcome.outcome,
300
- sessionUrl: sessionOutcome.sessionUrl,
301
- prUrl: sessionOutcome.prUrl,
302
- issueUrl,
303
- validation: sessionOutcome.verification,
304
- });
305
- await (0, client_1.postCommitComment)({
383
+ validation: sessionOutcome.verification,
384
+ });
385
+ await (0, client_1.postCommitComment)({
386
+ token: githubToken,
387
+ repository: repo,
388
+ commitSha,
389
+ body,
390
+ });
391
+ }
392
+ const slaDays = normalized.policy.slaDays ?? 0;
393
+ if (githubToken && slaDays > 0 && state.lastDocDriftPrUrl && state.lastDocDriftPrOpenedAt) {
394
+ const openedAt = Date.parse(state.lastDocDriftPrOpenedAt);
395
+ const daysOld = (Date.now() - openedAt) / (24 * 60 * 60 * 1000);
396
+ const lastSla = state.lastSlaIssueOpenedAt ? Date.parse(state.lastSlaIssueOpenedAt) : 0;
397
+ const slaCooldown = 6 * 24 * 60 * 60 * 1000;
398
+ if (daysOld >= slaDays && Date.now() - lastSla > slaCooldown) {
399
+ const slaIssueUrl = await (0, client_1.createIssue)({
306
400
  token: githubToken,
307
401
  repository: repo,
308
- commitSha,
309
- body,
402
+ issue: {
403
+ title: "[docdrift] Docs out of sync — merge doc drift PR(s)",
404
+ body: (0, client_1.renderSlaIssueBody)({
405
+ prUrls: [state.lastDocDriftPrUrl],
406
+ slaDays,
407
+ }),
408
+ labels: ["docdrift"],
409
+ },
310
410
  });
411
+ state.lastSlaIssueOpenedAt = new Date().toISOString();
412
+ (0, state_1.saveState)(state);
311
413
  }
312
- state = (0, engine_1.applyDecisionToState)({
313
- state,
314
- decision,
315
- docArea: item.docArea,
316
- outcome: sessionOutcome.outcome,
317
- link: sessionOutcome.prUrl ?? issueUrl,
318
- });
319
414
  }
320
- (0, state_1.saveState)(state);
321
- metrics.noiseRateProxy =
322
- metrics.driftItemsDetected === 0
323
- ? 0
324
- : Number((metrics.prsOpened / metrics.driftItemsDetected).toFixed(4));
415
+ metrics.noiseRateProxy = metrics.prsOpened;
325
416
  (0, bundle_1.writeMetrics)(metrics);
326
417
  (0, log_1.logInfo)("Run complete", {
327
- items: report.items.length,
418
+ items: 1,
328
419
  elapsedMs: Date.now() - startedAt,
329
420
  });
330
421
  return results;
@@ -338,6 +429,54 @@ async function runValidate() {
338
429
  runtimeValidation.warnings.forEach((warning) => (0, log_1.logWarn)(warning));
339
430
  (0, log_1.logInfo)("Config is valid");
340
431
  }
432
+ async function runSlaCheck() {
433
+ const githubToken = process.env.GITHUB_TOKEN;
434
+ if (!githubToken) {
435
+ throw new Error("GITHUB_TOKEN is required for sla-check command");
436
+ }
437
+ const repo = process.env.GITHUB_REPOSITORY;
438
+ if (!repo) {
439
+ throw new Error("GITHUB_REPOSITORY is required for sla-check command");
440
+ }
441
+ const normalized = (0, load_1.loadNormalizedConfig)();
442
+ const slaDays = normalized.policy.slaDays ?? 0;
443
+ const slaLabel = normalized.policy.slaLabel ?? "docdrift";
444
+ if (slaDays <= 0) {
445
+ (0, log_1.logInfo)("SLA check disabled (slaDays <= 0)");
446
+ return { issueOpened: false };
447
+ }
448
+ const cutoff = new Date(Date.now() - slaDays * 24 * 60 * 60 * 1000);
449
+ const openPrs = await (0, client_1.listOpenPrsWithLabel)(githubToken, repo, slaLabel);
450
+ const stalePrs = openPrs.filter((pr) => {
451
+ const created = pr.created_at ? Date.parse(pr.created_at) : Date.now();
452
+ return Number.isFinite(created) && created <= cutoff.getTime();
453
+ });
454
+ if (stalePrs.length === 0) {
455
+ (0, log_1.logInfo)("No doc-drift PRs open longer than slaDays; nothing to do");
456
+ return { issueOpened: false };
457
+ }
458
+ let state = (0, state_1.loadState)();
459
+ const lastSla = state.lastSlaIssueOpenedAt ? Date.parse(state.lastSlaIssueOpenedAt) : 0;
460
+ const slaCooldown = 6 * 24 * 60 * 60 * 1000;
461
+ if (Date.now() - lastSla < slaCooldown) {
462
+ (0, log_1.logInfo)("SLA issue cooldown; skipping");
463
+ return { issueOpened: false };
464
+ }
465
+ const prUrls = stalePrs.map((p) => p.url).filter(Boolean);
466
+ await (0, client_1.createIssue)({
467
+ token: githubToken,
468
+ repository: repo,
469
+ issue: {
470
+ title: "[docdrift] Docs out of sync — merge doc drift PR(s)",
471
+ body: (0, client_1.renderSlaIssueBody)({ prUrls, slaDays }),
472
+ labels: ["docdrift"],
473
+ },
474
+ });
475
+ state.lastSlaIssueOpenedAt = new Date().toISOString();
476
+ (0, state_1.saveState)(state);
477
+ (0, log_1.logInfo)(`Opened SLA issue for ${prUrls.length} stale PR(s)`);
478
+ return { issueOpened: true };
479
+ }
341
480
  async function runStatus(sinceHours = 24) {
342
481
  const apiKey = process.env.DEVIN_API_KEY;
343
482
  if (!apiKey) {
@@ -361,12 +500,12 @@ async function runStatus(sinceHours = 24) {
361
500
  }
362
501
  }
363
502
  function resolveTrigger(eventName) {
364
- if (eventName === "push") {
503
+ if (eventName === "push")
365
504
  return "push";
366
- }
367
- if (eventName === "schedule") {
505
+ if (eventName === "schedule")
368
506
  return "schedule";
369
- }
507
+ if (eventName === "pull_request")
508
+ return "pull_request";
370
509
  return "manual";
371
510
  }
372
511
  function parseDurationHours(value) {
@@ -386,4 +525,12 @@ function requireSha(value, label) {
386
525
  }
387
526
  return value;
388
527
  }
528
+ async function resolveBaseHead(baseArg, headArg) {
529
+ const headRef = headArg ?? process.env.GITHUB_SHA ?? "HEAD";
530
+ if (baseArg) {
531
+ return { baseSha: baseArg, headSha: headRef };
532
+ }
533
+ const { resolveDefaultBaseHead } = await Promise.resolve().then(() => __importStar(require("./utils/git")));
534
+ return resolveDefaultBaseHead(headRef);
535
+ }
389
536
  exports.STATE_PATH = node_path_1.default.resolve(".docdrift", "state.json");
@@ -21,7 +21,8 @@ function decidePolicy(input) {
21
21
  const capReached = prCountToday >= config.policy.prCaps.maxPrsPerDay;
22
22
  const areaDailyKey = `${today}:${item.docArea}`;
23
23
  const exceedsFileCap = item.impactedDocs.length > config.policy.prCaps.maxFilesTouched;
24
- const hasPathOutsideAllowlist = item.impactedDocs.some((filePath) => filePath && !(0, glob_1.isPathAllowed)(filePath, config.policy.allowlist));
24
+ const exclude = "exclude" in config && Array.isArray(config.exclude) ? config.exclude : [];
25
+ const hasPathOutsideAllowlist = item.impactedDocs.some((filePath) => filePath && !(0, glob_1.isPathAllowedAndNotExcluded)(filePath, config.policy.allowlist, exclude));
25
26
  let action = "NOOP";
26
27
  let reason = "No action needed";
27
28
  if (hasPathOutsideAllowlist) {