@oss-autopilot/core 3.4.0 → 3.5.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 (47) hide show
  1. package/dist/cli-registry.js +50 -0
  2. package/dist/cli.bundle.cjs +81 -78
  3. package/dist/commands/compliance-score.d.ts +21 -0
  4. package/dist/commands/compliance-score.js +156 -0
  5. package/dist/commands/index.d.ts +4 -0
  6. package/dist/commands/index.js +4 -0
  7. package/dist/commands/list-mark-done.d.ts +48 -0
  8. package/dist/commands/list-mark-done.js +213 -0
  9. package/dist/commands/parse-list.js +86 -9
  10. package/dist/commands/repo-vet.d.ts +21 -0
  11. package/dist/commands/repo-vet.js +215 -0
  12. package/dist/commands/startup.js +18 -0
  13. package/dist/core/ci-enforced-tools.d.ts +35 -0
  14. package/dist/core/ci-enforced-tools.js +109 -0
  15. package/dist/core/comment-decision.d.ts +72 -0
  16. package/dist/core/comment-decision.js +74 -0
  17. package/dist/core/compliance-score.d.ts +127 -0
  18. package/dist/core/compliance-score.js +277 -0
  19. package/dist/core/config-registry.js +12 -0
  20. package/dist/core/contributing.d.ts +52 -0
  21. package/dist/core/contributing.js +139 -0
  22. package/dist/core/extraction-categories.d.ts +55 -0
  23. package/dist/core/extraction-categories.js +108 -0
  24. package/dist/core/follow-up-history.d.ts +41 -0
  25. package/dist/core/follow-up-history.js +71 -0
  26. package/dist/core/gist-state-store.d.ts +30 -7
  27. package/dist/core/gist-state-store.js +87 -11
  28. package/dist/core/issue-conversation.js +1 -0
  29. package/dist/core/issue-effort.d.ts +29 -0
  30. package/dist/core/issue-effort.js +41 -0
  31. package/dist/core/maintainer-hints.d.ts +23 -0
  32. package/dist/core/maintainer-hints.js +36 -0
  33. package/dist/core/placeholder-usernames.js +7 -0
  34. package/dist/core/pr-quality-rubric.d.ts +70 -0
  35. package/dist/core/pr-quality-rubric.js +121 -0
  36. package/dist/core/repo-vet.d.ts +90 -0
  37. package/dist/core/repo-vet.js +178 -0
  38. package/dist/core/state-schema.d.ts +76 -0
  39. package/dist/core/state-schema.js +75 -0
  40. package/dist/core/strategy.d.ts +75 -0
  41. package/dist/core/strategy.js +226 -0
  42. package/dist/core/types.d.ts +2 -0
  43. package/dist/core/workflow-state.d.ts +56 -0
  44. package/dist/core/workflow-state.js +101 -0
  45. package/dist/formatters/json.d.ts +147 -0
  46. package/dist/formatters/json.js +79 -0
  47. package/package.json +1 -1
@@ -0,0 +1,41 @@
1
+ /**
2
+ * PR follow-up history helpers (#1277).
3
+ *
4
+ * Centralizes the read/write logic for `state.prFollowUpHistory`.
5
+ * Used by `workflows/dormant-pr-follow-up.md` to enforce the
6
+ * one-follow-up-per-timeframe rule documented in
7
+ * `skills/pr-etiquette/SKILL.md`.
8
+ *
9
+ * Pure functions — callers manage state I/O.
10
+ */
11
+ import type { AgentState } from './state-schema.js';
12
+ import type { FollowUpEntry } from './state-schema.js';
13
+ export type FollowUpTier = 'light_check_in' | 'direct_check_in' | 'final_check_in';
14
+ /**
15
+ * Tier window in days. The "no second ping in the same window" rule
16
+ * uses this to decide whether a recent follow-up still locks out the
17
+ * next draft. Sourced from `skills/pr-etiquette/SKILL.md`.
18
+ */
19
+ export declare const TIER_WINDOW_DAYS: Record<FollowUpTier, number>;
20
+ /**
21
+ * Read the existing follow-up entries for a PR. Returns an empty
22
+ * array when no entries exist or when the state field is unset.
23
+ */
24
+ export declare function getFollowUpHistory(state: AgentState, prUrl: string): FollowUpEntry[];
25
+ /**
26
+ * Find the most recent entry, or null when there are none. Used by
27
+ * the workflow's pre-draft gate.
28
+ */
29
+ export declare function lastFollowUp(state: AgentState, prUrl: string): FollowUpEntry | null;
30
+ /**
31
+ * Whether a follow-up of the given tier was drafted within the
32
+ * tier's own window. The workflow uses this to surface a warning
33
+ * before the user drafts a second ping in the same window. Different
34
+ * tiers are always allowed (escalating light → direct → final).
35
+ */
36
+ export declare function isWithinTierWindow(state: AgentState, prUrl: string, tier: FollowUpTier, now?: Date): boolean;
37
+ /**
38
+ * Record a new follow-up entry. Returns the next state; callers
39
+ * decide when to persist.
40
+ */
41
+ export declare function recordFollowUp(state: AgentState, prUrl: string, entry: FollowUpEntry): AgentState;
@@ -0,0 +1,71 @@
1
+ /**
2
+ * PR follow-up history helpers (#1277).
3
+ *
4
+ * Centralizes the read/write logic for `state.prFollowUpHistory`.
5
+ * Used by `workflows/dormant-pr-follow-up.md` to enforce the
6
+ * one-follow-up-per-timeframe rule documented in
7
+ * `skills/pr-etiquette/SKILL.md`.
8
+ *
9
+ * Pure functions — callers manage state I/O.
10
+ */
11
+ /**
12
+ * Tier window in days. The "no second ping in the same window" rule
13
+ * uses this to decide whether a recent follow-up still locks out the
14
+ * next draft. Sourced from `skills/pr-etiquette/SKILL.md`.
15
+ */
16
+ export const TIER_WINDOW_DAYS = {
17
+ light_check_in: 7,
18
+ direct_check_in: 14,
19
+ final_check_in: 30,
20
+ };
21
+ /**
22
+ * Read the existing follow-up entries for a PR. Returns an empty
23
+ * array when no entries exist or when the state field is unset.
24
+ */
25
+ export function getFollowUpHistory(state, prUrl) {
26
+ return state.prFollowUpHistory?.[prUrl] ?? [];
27
+ }
28
+ /**
29
+ * Find the most recent entry, or null when there are none. Used by
30
+ * the workflow's pre-draft gate.
31
+ */
32
+ export function lastFollowUp(state, prUrl) {
33
+ const entries = getFollowUpHistory(state, prUrl);
34
+ if (entries.length === 0)
35
+ return null;
36
+ // Newest by timestamp wins. Defensive sort because the persistence
37
+ // path doesn't guarantee insertion order across machines.
38
+ return [...entries].sort((a, b) => Date.parse(b.timestamp) - Date.parse(a.timestamp))[0];
39
+ }
40
+ /**
41
+ * Whether a follow-up of the given tier was drafted within the
42
+ * tier's own window. The workflow uses this to surface a warning
43
+ * before the user drafts a second ping in the same window. Different
44
+ * tiers are always allowed (escalating light → direct → final).
45
+ */
46
+ export function isWithinTierWindow(state, prUrl, tier, now = new Date()) {
47
+ const last = lastFollowUp(state, prUrl);
48
+ if (!last)
49
+ return false;
50
+ if (last.tier !== tier)
51
+ return false;
52
+ const ageMs = now.getTime() - Date.parse(last.timestamp);
53
+ if (Number.isNaN(ageMs))
54
+ return false;
55
+ const windowMs = TIER_WINDOW_DAYS[tier] * 86400000;
56
+ return ageMs < windowMs;
57
+ }
58
+ /**
59
+ * Record a new follow-up entry. Returns the next state; callers
60
+ * decide when to persist.
61
+ */
62
+ export function recordFollowUp(state, prUrl, entry) {
63
+ const next = {
64
+ ...state,
65
+ prFollowUpHistory: { ...(state.prFollowUpHistory ?? {}) },
66
+ };
67
+ const list = next.prFollowUpHistory[prUrl] ? [...next.prFollowUpHistory[prUrl]] : [];
68
+ list.push(entry);
69
+ next.prFollowUpHistory[prUrl] = list;
70
+ return next;
71
+ }
@@ -14,7 +14,7 @@
14
14
  * 6. Cache all Gist file contents in memory for session-scoped reads
15
15
  * 7. Write state to a local cache file for fallback
16
16
  *
17
- * Concurrency model (#1115):
17
+ * Concurrency model (#1115, #1235):
18
18
  *
19
19
  * Each fetch captures the response's `ETag` and stores it as
20
20
  * `lastFetchedEtag`. The next `push()` sends that ETag back as
@@ -24,12 +24,25 @@
24
24
  * On 412, the store attempts a single merge: it re-fetches the Gist
25
25
  * (refreshing the ETag), re-applies the in-memory dirty content on top of
26
26
  * the now-current remote state, and pushes once more. If that second push
27
- * also returns 412, `push()` throws {@link GistConcurrencyError} — local
28
- * mutations stay in memory so the caller can decide whether to refresh and
29
- * retry. The merge step is intentionally cheap (state is a single JSON
30
- * blob) and treats local dirty changes as authoritative for conflict
31
- * cells, which matches the "last-write-wins by intent" model the rest of
32
- * oss-autopilot already assumes for state.json.
27
+ * also returns 412, `push()` throws {@link GistConcurrencyError}.
28
+ *
29
+ * Per-file conflict policy on the merge re-apply step:
30
+ *
31
+ * - `state.json` is fail-loud. The `StateManager` contract advertises
32
+ * optimistic compare-and-swap (state.ts:285-296), so silently clobbering
33
+ * a concurrent remote write would violate it. To detect concurrent
34
+ * writes without per-file ETags (the Gist API only exposes one ETag for
35
+ * the whole gist), we snapshot each file's content at fetch time. If
36
+ * the post-refresh `state.json` differs from the snapshot we held
37
+ * before the refresh, another machine wrote it: `push()` throws
38
+ * `GistConcurrencyError` instead of overwriting.
39
+ * - Freeform documents (e.g. per-repo guidelines) keep last-write-wins.
40
+ * The `setDocument` / `setGuidelines` callers' intent on a manual write
41
+ * is "overwrite". Local dirty content is reapplied on top of the
42
+ * refreshed remote.
43
+ *
44
+ * In both cases, when the second push also 412s, local mutations stay in
45
+ * memory so the caller can decide whether to refresh and retry.
33
46
  */
34
47
  import { AgentState } from './types.js';
35
48
  /** Well-known Gist description used for search-based discovery. */
@@ -126,6 +139,16 @@ export declare class GistStateStore {
126
139
  private gistId;
127
140
  readonly cachedFiles: Map<string, string>;
128
141
  readonly dirtyFiles: Set<string>;
142
+ /**
143
+ * Per-file content snapshot captured at fetch time (#1235). The Gist API
144
+ * exposes a single ETag for the whole gist, so we cannot use `If-Match`
145
+ * to detect a per-file conflict on the 412 merge re-apply. Holding the
146
+ * baseline content lets us compare it against the freshly-fetched remote
147
+ * and decide whether `state.json` changed under us. Updated only by
148
+ * `fetchAndCache` and successful `push()`; not mutated by `setState` /
149
+ * `setDocument` so the baseline reflects the remote, not local edits.
150
+ */
151
+ private baselineFiles;
129
152
  /**
130
153
  * ETag captured from the most recent successful Gist fetch (#1115).
131
154
  * Sent back as `If-Match` on `update` so concurrent writes from another
@@ -14,7 +14,7 @@
14
14
  * 6. Cache all Gist file contents in memory for session-scoped reads
15
15
  * 7. Write state to a local cache file for fallback
16
16
  *
17
- * Concurrency model (#1115):
17
+ * Concurrency model (#1115, #1235):
18
18
  *
19
19
  * Each fetch captures the response's `ETag` and stores it as
20
20
  * `lastFetchedEtag`. The next `push()` sends that ETag back as
@@ -24,12 +24,25 @@
24
24
  * On 412, the store attempts a single merge: it re-fetches the Gist
25
25
  * (refreshing the ETag), re-applies the in-memory dirty content on top of
26
26
  * the now-current remote state, and pushes once more. If that second push
27
- * also returns 412, `push()` throws {@link GistConcurrencyError} — local
28
- * mutations stay in memory so the caller can decide whether to refresh and
29
- * retry. The merge step is intentionally cheap (state is a single JSON
30
- * blob) and treats local dirty changes as authoritative for conflict
31
- * cells, which matches the "last-write-wins by intent" model the rest of
32
- * oss-autopilot already assumes for state.json.
27
+ * also returns 412, `push()` throws {@link GistConcurrencyError}.
28
+ *
29
+ * Per-file conflict policy on the merge re-apply step:
30
+ *
31
+ * - `state.json` is fail-loud. The `StateManager` contract advertises
32
+ * optimistic compare-and-swap (state.ts:285-296), so silently clobbering
33
+ * a concurrent remote write would violate it. To detect concurrent
34
+ * writes without per-file ETags (the Gist API only exposes one ETag for
35
+ * the whole gist), we snapshot each file's content at fetch time. If
36
+ * the post-refresh `state.json` differs from the snapshot we held
37
+ * before the refresh, another machine wrote it: `push()` throws
38
+ * `GistConcurrencyError` instead of overwriting.
39
+ * - Freeform documents (e.g. per-repo guidelines) keep last-write-wins.
40
+ * The `setDocument` / `setGuidelines` callers' intent on a manual write
41
+ * is "overwrite". Local dirty content is reapplied on top of the
42
+ * refreshed remote.
43
+ *
44
+ * In both cases, when the second push also 412s, local mutations stay in
45
+ * memory so the caller can decide whether to refresh and retry.
33
46
  */
34
47
  import * as fs from 'node:fs';
35
48
  import { AgentStateSchema } from './state-schema.js';
@@ -77,6 +90,16 @@ export class GistStateStore {
77
90
  gistId = null;
78
91
  cachedFiles = new Map();
79
92
  dirtyFiles = new Set();
93
+ /**
94
+ * Per-file content snapshot captured at fetch time (#1235). The Gist API
95
+ * exposes a single ETag for the whole gist, so we cannot use `If-Match`
96
+ * to detect a per-file conflict on the 412 merge re-apply. Holding the
97
+ * baseline content lets us compare it against the freshly-fetched remote
98
+ * and decide whether `state.json` changed under us. Updated only by
99
+ * `fetchAndCache` and successful `push()`; not mutated by `setState` /
100
+ * `setDocument` so the baseline reflects the remote, not local edits.
101
+ */
102
+ baselineFiles = new Map();
80
103
  /**
81
104
  * ETag captured from the most recent successful Gist fetch (#1115).
82
105
  * Sent back as `If-Match` on `update` so concurrent writes from another
@@ -379,17 +402,61 @@ export class GistStateStore {
379
402
  if (content !== undefined)
380
403
  stagedContents[filename] = content;
381
404
  }
405
+ // Snapshot the full cache and baseline before refresh so a partial
406
+ // failure inside fetchAndCache (e.g. parse throws after the cache
407
+ // was already cleared and repopulated) can be rolled back without
408
+ // leaving the store with a stale ETag pointing at corrupt or
409
+ // half-populated state (#1235).
410
+ const preRefreshCache = new Map(this.cachedFiles);
411
+ const preRefreshBaseline = new Map(this.baselineFiles);
412
+ const preRefreshEtag = this.lastFetchedEtag;
413
+ const reapplyStaged = () => {
414
+ for (const [filename, content] of Object.entries(stagedContents)) {
415
+ this.cachedFiles.set(filename, content);
416
+ }
417
+ };
382
418
  try {
383
419
  await this.fetchAndCache(this.gistId);
384
420
  }
385
421
  catch (refreshErr) {
386
422
  warn(MODULE, `Merge refresh after 412 failed: ${refreshErr}`);
423
+ // Roll back any partial mutation from fetchAndCache so the
424
+ // caller's retry path sees the prior state (with local
425
+ // mutations re-applied on top).
426
+ this.cachedFiles.clear();
427
+ for (const [filename, content] of preRefreshCache) {
428
+ this.cachedFiles.set(filename, content);
429
+ }
430
+ this.baselineFiles = preRefreshBaseline;
431
+ this.lastFetchedEtag = preRefreshEtag;
432
+ reapplyStaged();
433
+ // Preserve the GistCorruptError type so callers don't retry
434
+ // against a corrupt remote (which a CONCURRENCY_ERROR would
435
+ // invite). Other failure types continue to surface as a
436
+ // concurrency error — the gist content is still likely fine.
437
+ if (refreshErr instanceof GistCorruptError)
438
+ throw refreshErr;
387
439
  throw new GistConcurrencyError(expectedEtag, null);
388
440
  }
389
- // Re-apply our staged dirty contents on top of the refreshed remote.
390
- for (const [filename, content] of Object.entries(stagedContents)) {
391
- this.cachedFiles.set(filename, content);
441
+ // Per-file conflict policy (#1235):
442
+ // - state.json must fail loud when its remote copy moved under us;
443
+ // silently overwriting would violate the StateManager
444
+ // optimistic-concurrency contract (state.ts:285-296).
445
+ // - Freeform documents (guidelines, etc.) keep last-write-wins —
446
+ // `setDocument` callers' intent on a manual write is "overwrite".
447
+ if (stagedContents[STATE_FILE_NAME] !== undefined) {
448
+ const baselineBeforeRefresh = preRefreshBaseline.get(STATE_FILE_NAME);
449
+ const remoteAfterRefresh = this.cachedFiles.get(STATE_FILE_NAME);
450
+ if (baselineBeforeRefresh !== remoteAfterRefresh) {
451
+ warn(MODULE, 'state.json changed remotely while a push was in flight; surfacing GistConcurrencyError instead of clobbering (#1235)');
452
+ // Preserve local state.json (and other staged dirty files) in
453
+ // memory so the caller can refresh and reapply if appropriate.
454
+ reapplyStaged();
455
+ throw new GistConcurrencyError(expectedEtag, this.lastFetchedEtag);
456
+ }
392
457
  }
458
+ // Re-apply our staged dirty contents on top of the refreshed remote.
459
+ reapplyStaged();
393
460
  result = await attempt(this.lastFetchedEtag);
394
461
  if (!result.ok && result.status === 412) {
395
462
  warn(MODULE, 'Second push after merge also returned 412; surfacing GistConcurrencyError');
@@ -405,8 +472,11 @@ export class GistStateStore {
405
472
  return false;
406
473
  }
407
474
  }
408
- // Success: flush dirty set and write local cache
475
+ // Success: flush dirty set and write local cache. Refresh the
476
+ // per-file baseline to reflect what we just wrote — the next 412
477
+ // merge re-apply check compares against this.
409
478
  this.dirtyFiles.clear();
479
+ this.baselineFiles = new Map(this.cachedFiles);
410
480
  const raw = this.cachedFiles.get(STATE_FILE_NAME);
411
481
  if (raw) {
412
482
  try {
@@ -489,6 +559,10 @@ export class GistStateStore {
489
559
  this.cachedFiles.set(filename, file.content);
490
560
  }
491
561
  }
562
+ // Snapshot the just-fetched contents as the per-file baseline (#1235).
563
+ // The 412 merge path uses this to detect whether a concurrent write
564
+ // changed `state.json` between this fetch and the next push attempt.
565
+ this.baselineFiles = new Map(this.cachedFiles);
492
566
  // Parse state.json
493
567
  const state = this.parseStateFromCache();
494
568
  // Write-through to local cache for degraded-mode fallback
@@ -578,6 +652,7 @@ export class GistStateStore {
578
652
  this.cachedFiles.set(filename, file.content);
579
653
  }
580
654
  }
655
+ this.baselineFiles = new Map(this.cachedFiles);
581
656
  // Write-through to local cache
582
657
  this.writeLocalStateCache(freshState);
583
658
  return { id: data.id, state: freshState };
@@ -602,6 +677,7 @@ export class GistStateStore {
602
677
  this.cachedFiles.set(filename, file.content);
603
678
  }
604
679
  }
680
+ this.baselineFiles = new Map(this.cachedFiles);
605
681
  // Write-through to local cache
606
682
  this.writeLocalStateCache(seedState);
607
683
  return { id: data.id, state: seedState };
@@ -205,6 +205,7 @@ export class IssueConversationMonitor {
205
205
  title: item.title,
206
206
  url: item.html_url,
207
207
  userLastCommentedAt: userLastComment.createdAt,
208
+ userLastCommentBody: userLastComment.body.slice(0, 200) + (userLastComment.body.length > 200 ? '...' : ''),
208
209
  labels,
209
210
  daysSinceUserComment: daysBetween(userLastCommentTime, new Date()),
210
211
  };
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Action-menu issue effort classification (#1264).
3
+ *
4
+ * Extracted from `workflows/action-menu.md`'s in-prompt truth table so
5
+ * the rules are typed, unit-testable, and tunable without editing
6
+ * markdown. Same architectural shape as the typed-core extractions in
7
+ * #1245 (compliance-score), #1242 (repo-vet), #1243 (strategy), and
8
+ * #1252 (pr-quality-rubric).
9
+ */
10
+ /** Issue types the action menu surfaces. Match the strings the CLI emits in
11
+ * `data.daily.actionableIssues[*].type`. */
12
+ export type ActionableIssueType = 'needs_response' | 'needs_changes' | 'incomplete_checklist' | 'ci_failing' | 'merge_conflict';
13
+ export type IssueEffort = 'small' | 'medium' | 'large';
14
+ /**
15
+ * Decide the effort label for an actionable issue based on its type and
16
+ * how many maintainer hints accompany it. Default falls through to
17
+ * `medium` for any unrecognized combination — the action-menu workflow
18
+ * still renders something useful when the CLI surfaces a new issue
19
+ * type before this function is updated.
20
+ *
21
+ * Truth table sourced verbatim from `workflows/action-menu.md`:
22
+ *
23
+ * | Effort | Condition |
24
+ * |--------|-----------|
25
+ * | small | needs_response with 0-1 hints, incomplete_checklist |
26
+ * | medium | needs_response with 2+ hints, needs_changes with 0-2 hints, ci_failing |
27
+ * | large | merge_conflict, needs_changes with 3+ hints |
28
+ */
29
+ export declare function computeIssueEffort(type: ActionableIssueType, hintCount: number): IssueEffort;
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Action-menu issue effort classification (#1264).
3
+ *
4
+ * Extracted from `workflows/action-menu.md`'s in-prompt truth table so
5
+ * the rules are typed, unit-testable, and tunable without editing
6
+ * markdown. Same architectural shape as the typed-core extractions in
7
+ * #1245 (compliance-score), #1242 (repo-vet), #1243 (strategy), and
8
+ * #1252 (pr-quality-rubric).
9
+ */
10
+ /**
11
+ * Decide the effort label for an actionable issue based on its type and
12
+ * how many maintainer hints accompany it. Default falls through to
13
+ * `medium` for any unrecognized combination — the action-menu workflow
14
+ * still renders something useful when the CLI surfaces a new issue
15
+ * type before this function is updated.
16
+ *
17
+ * Truth table sourced verbatim from `workflows/action-menu.md`:
18
+ *
19
+ * | Effort | Condition |
20
+ * |--------|-----------|
21
+ * | small | needs_response with 0-1 hints, incomplete_checklist |
22
+ * | medium | needs_response with 2+ hints, needs_changes with 0-2 hints, ci_failing |
23
+ * | large | merge_conflict, needs_changes with 3+ hints |
24
+ */
25
+ export function computeIssueEffort(type, hintCount) {
26
+ const hints = Math.max(0, hintCount);
27
+ switch (type) {
28
+ case 'needs_response':
29
+ return hints <= 1 ? 'small' : 'medium';
30
+ case 'incomplete_checklist':
31
+ return 'small';
32
+ case 'needs_changes':
33
+ return hints >= 3 ? 'large' : 'medium';
34
+ case 'ci_failing':
35
+ return 'medium';
36
+ case 'merge_conflict':
37
+ return 'large';
38
+ default:
39
+ return 'medium';
40
+ }
41
+ }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Maintainer-hint label constants (#1264).
3
+ *
4
+ * Extracted from `workflows/action-menu.md`'s in-prompt mapping so the
5
+ * label strings live in typed code instead of markdown. Adding a new
6
+ * hint type becomes a single-file change in core; the workflow gets
7
+ * the new label for free instead of requiring a parallel markdown
8
+ * edit. Same pattern as #1252.
9
+ */
10
+ export type MaintainerHint = 'demo_requested' | 'tests_requested' | 'changes_requested' | 'docs_requested' | 'rebase_requested';
11
+ /**
12
+ * Display labels the action-menu workflow renders for each hint type.
13
+ * Source of truth — both `data.daily.actionableIssues[*].maintainerHintsLabel`
14
+ * (computed by the CLI) and the workflow's prose docs reference this map.
15
+ */
16
+ export declare const MAINTAINER_HINT_LABELS: Record<MaintainerHint, string>;
17
+ /**
18
+ * Format an array of hints as a comma-joined human label, in the same
19
+ * order the caller passed them. Unknown hint values are dropped silently —
20
+ * a future CLI version might emit a hint type the workflow doesn't yet
21
+ * know about; rendering "undefined" would be worse than omitting it.
22
+ */
23
+ export declare function formatMaintainerHints(hints: readonly MaintainerHint[] | readonly string[]): string;
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Maintainer-hint label constants (#1264).
3
+ *
4
+ * Extracted from `workflows/action-menu.md`'s in-prompt mapping so the
5
+ * label strings live in typed code instead of markdown. Adding a new
6
+ * hint type becomes a single-file change in core; the workflow gets
7
+ * the new label for free instead of requiring a parallel markdown
8
+ * edit. Same pattern as #1252.
9
+ */
10
+ /**
11
+ * Display labels the action-menu workflow renders for each hint type.
12
+ * Source of truth — both `data.daily.actionableIssues[*].maintainerHintsLabel`
13
+ * (computed by the CLI) and the workflow's prose docs reference this map.
14
+ */
15
+ export const MAINTAINER_HINT_LABELS = {
16
+ demo_requested: 'demo/screenshot requested',
17
+ tests_requested: 'tests requested',
18
+ changes_requested: 'code changes requested',
19
+ docs_requested: 'documentation requested',
20
+ rebase_requested: 'rebase requested',
21
+ };
22
+ /**
23
+ * Format an array of hints as a comma-joined human label, in the same
24
+ * order the caller passed them. Unknown hint values are dropped silently —
25
+ * a future CLI version might emit a hint type the workflow doesn't yet
26
+ * know about; rendering "undefined" would be worse than omitting it.
27
+ */
28
+ export function formatMaintainerHints(hints) {
29
+ const labels = [];
30
+ for (const h of hints) {
31
+ const label = MAINTAINER_HINT_LABELS[h];
32
+ if (label)
33
+ labels.push(label);
34
+ }
35
+ return labels.join(', ');
36
+ }
@@ -20,6 +20,13 @@ const PLACEHOLDER_USERNAMES = [
20
20
  'example-user',
21
21
  'your-username',
22
22
  'your-github-username',
23
+ // GitHub's mascot accounts. Real users on github.com but seeded into countless
24
+ // example configs, READMEs, and SDK docs (Octokit's quickstarts use `octocat`
25
+ // as the canonical login). Treating them as placeholders keeps a stale example
26
+ // value from silently swapping a real user's PR feed with the mascot's open
27
+ // PRs in violet-org/boysenberry-repo (octocat's public test fixtures).
28
+ 'octocat',
29
+ 'monalisa',
23
30
  ];
24
31
  const KNOWN_PLACEHOLDER_USERNAMES = new Set(PLACEHOLDER_USERNAMES);
25
32
  export function isPlaceholderUsername(username) {
@@ -0,0 +1,70 @@
1
+ /**
2
+ * Canonical PR quality rubric (#1252).
3
+ *
4
+ * Single source of truth for the PR-quality checks shared between
5
+ * `skills/pr-etiquette/SKILL.md` (human-facing checklist) and
6
+ * `agents/pr-compliance-checker.md` (agent-facing scoring via
7
+ * `computeComplianceScore` in compliance-score.ts).
8
+ *
9
+ * Both surfaces had the same checks listed independently, with the
10
+ * "< 10 files, < 400 lines ideal" threshold duplicated. Drift was
11
+ * inevitable as either surface evolved. This module exports the
12
+ * rubric as structured data so the two consumers cannot drift
13
+ * silently — `compliance-score.ts` re-exports its WEIGHTS /
14
+ * FOCUSED_CHANGES from here, and the skill prose references this
15
+ * file as the canonical definition.
16
+ *
17
+ * Same architectural shape as the typed-core extractions in
18
+ * #858 / #910 / #911 / #1245 / #1242 / #1243 — pull the rubric out
19
+ * of prose into typed code so it's testable and tunable.
20
+ */
21
+ export type PRQualityTier = 'required' | 'conditional' | 'optional';
22
+ export interface PRQualityCheck {
23
+ /** Stable identifier referenced by the agent's scoring code. */
24
+ id: 'issueReference' | 'description' | 'title' | 'focusedChanges' | 'minimalDiff' | 'tests' | 'docs' | 'branch' | 'screenshots';
25
+ /** Human-facing label as it appears in the skill checklist. */
26
+ label: string;
27
+ /** One-line description for both the skill checklist and the
28
+ * agent's recommendation prose. */
29
+ description: string;
30
+ tier: PRQualityTier;
31
+ /**
32
+ * Percent weight in the agent's compliance score, when the check
33
+ * is part of the scored set. `null` for checks the skill lists but
34
+ * the agent doesn't currently score (Minimal Diff, Docs, Screenshots).
35
+ */
36
+ weight: number | null;
37
+ }
38
+ /**
39
+ * Canonical "focused changes" thresholds. Mirrored by
40
+ * compliance-score.ts's `FOCUSED_CHANGES` constant — that file
41
+ * imports these values.
42
+ */
43
+ export declare const FOCUSED_CHANGES_THRESHOLDS: {
44
+ readonly passFiles: 10;
45
+ readonly passLines: 400;
46
+ readonly warnFiles: 20;
47
+ readonly warnLines: 800;
48
+ };
49
+ /**
50
+ * Title byte budget. Mirrored by compliance-score.ts's
51
+ * `TITLE_LENGTH_BUDGET` — that file imports this value.
52
+ */
53
+ export declare const TITLE_LENGTH_BUDGET = 72;
54
+ /**
55
+ * The full rubric. The order is the order the skill renders the
56
+ * checklist in; the weights total 100 across the scored entries.
57
+ */
58
+ export declare const PR_QUALITY_RUBRIC: readonly PRQualityCheck[];
59
+ /**
60
+ * Look up a check by id. Used by `compliance-score.ts` to wire the
61
+ * scored checks to their canonical weight without hardcoding the
62
+ * value in two places.
63
+ */
64
+ export declare function getRubricCheck(id: PRQualityCheck['id']): PRQualityCheck | undefined;
65
+ /**
66
+ * Sum of the weights for scored checks. Should equal 100. Tests pin
67
+ * this so a future edit that fails to balance the weights surfaces
68
+ * at CI time rather than silently shipping a < 100 rubric.
69
+ */
70
+ export declare function totalScoredWeight(): number;