@oss-autopilot/core 1.17.4 → 3.0.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 (49) hide show
  1. package/README.md +1 -1
  2. package/dist/cli-registry.js +417 -326
  3. package/dist/cli.bundle.cjs +99 -96
  4. package/dist/commands/daily-render.d.ts +39 -0
  5. package/dist/commands/daily-render.js +189 -0
  6. package/dist/commands/dashboard-data.js +9 -3
  7. package/dist/commands/index.d.ts +4 -8
  8. package/dist/commands/index.js +3 -5
  9. package/dist/commands/list-move-tier.d.ts +46 -0
  10. package/dist/commands/list-move-tier.js +192 -0
  11. package/dist/commands/pr-template.js +2 -1
  12. package/dist/commands/state-cmd.d.ts +10 -1
  13. package/dist/commands/state-cmd.js +22 -3
  14. package/dist/commands/track.d.ts +7 -28
  15. package/dist/commands/track.js +8 -30
  16. package/dist/core/auth.d.ts +50 -0
  17. package/dist/core/auth.js +160 -0
  18. package/dist/core/concurrency.d.ts +7 -0
  19. package/dist/core/concurrency.js +9 -0
  20. package/dist/core/daily-logic.d.ts +10 -42
  21. package/dist/core/daily-logic.js +14 -201
  22. package/dist/core/dates.d.ts +37 -0
  23. package/dist/core/dates.js +60 -0
  24. package/dist/core/errors.d.ts +14 -0
  25. package/dist/core/errors.js +22 -0
  26. package/dist/core/gist-state-store.d.ts +48 -2
  27. package/dist/core/gist-state-store.js +120 -24
  28. package/dist/core/github-stats.js +1 -1
  29. package/dist/core/http-cache.js +1 -1
  30. package/dist/core/index.d.ts +5 -1
  31. package/dist/core/index.js +5 -1
  32. package/dist/core/issue-conversation.js +3 -2
  33. package/dist/core/paths.d.ts +68 -0
  34. package/dist/core/paths.js +106 -0
  35. package/dist/core/pr-monitor.js +3 -1
  36. package/dist/core/repo-score-manager.js +1 -1
  37. package/dist/core/state-persistence.js +1 -1
  38. package/dist/core/state.d.ts +16 -2
  39. package/dist/core/state.js +42 -7
  40. package/dist/core/types.d.ts +57 -0
  41. package/dist/core/urls.d.ts +63 -0
  42. package/dist/core/urls.js +101 -0
  43. package/dist/formatters/json.d.ts +464 -74
  44. package/dist/formatters/json.js +380 -0
  45. package/package.json +3 -3
  46. package/dist/commands/read.d.ts +0 -18
  47. package/dist/commands/read.js +0 -20
  48. package/dist/core/utils.d.ts +0 -303
  49. package/dist/core/utils.js +0 -529
@@ -1,23 +1,16 @@
1
1
  /**
2
- * Track/Untrack commands (v2 semantics — see #1001)
2
+ * Track command (v2 semantics — see #1001)
3
3
  *
4
- * **These commands do not mutate state.** In v2, PRs are discovered and
4
+ * **This command does not mutate state.** In v2, PRs are discovered and
5
5
  * enriched automatically on every `daily` run — there is no local tracking
6
- * list to add to or remove from. The commands are preserved for backwards
7
- * compatibility with v1 callers, but:
6
+ * list to add to. `runTrack` is an **informational lookup** that fetches PR
7
+ * metadata from GitHub and returns it; useful for inspecting a specific
8
+ * PR's shape without waiting for the next `daily` run. Nothing is persisted.
8
9
  *
9
- * - `runTrack` is an **informational lookup** that fetches PR metadata from
10
- * GitHub and returns it. Useful for inspecting a specific PR's shape
11
- * without waiting for the next `daily` run. Nothing is persisted.
12
- * - `runUntrack` is **deprecated** and always a no-op. Use `shelve` to hide
13
- * a PR from the daily digest.
10
+ * The `runUntrack` v1→v2 stub was removed in v4 (#1133). Use `shelve`/
11
+ * `unshelve` to hide a PR from the daily digest.
14
12
  */
15
13
  import type { TrackOutput } from '../formatters/json.js';
16
- export interface UntrackOutput {
17
- removed: boolean;
18
- url: string;
19
- message: string;
20
- }
21
14
  /**
22
15
  * Fetch metadata for a PR URL (informational — does not persist).
23
16
  *
@@ -33,17 +26,3 @@ export interface UntrackOutput {
33
26
  export declare function runTrack(options: {
34
27
  prUrl: string;
35
28
  }): Promise<TrackOutput>;
36
- /**
37
- * @deprecated No-op in v2. Use `runShelve` to hide a PR from the daily digest.
38
- *
39
- * Kept for backwards compatibility with v1 callers. PRs are fetched fresh
40
- * on each `daily` run, so there is no local tracking list to remove from.
41
- *
42
- * @param options - Untrack options
43
- * @param options.prUrl - Full GitHub PR URL (validated but not used)
44
- * @returns Output object with `removed: false` and a message explaining v2 behavior
45
- * @throws {ValidationError} If the URL is not a valid GitHub PR URL
46
- */
47
- export declare function runUntrack(options: {
48
- prUrl: string;
49
- }): Promise<UntrackOutput>;
@@ -1,21 +1,19 @@
1
1
  /**
2
- * Track/Untrack commands (v2 semantics — see #1001)
2
+ * Track command (v2 semantics — see #1001)
3
3
  *
4
- * **These commands do not mutate state.** In v2, PRs are discovered and
4
+ * **This command does not mutate state.** In v2, PRs are discovered and
5
5
  * enriched automatically on every `daily` run — there is no local tracking
6
- * list to add to or remove from. The commands are preserved for backwards
7
- * compatibility with v1 callers, but:
6
+ * list to add to. `runTrack` is an **informational lookup** that fetches PR
7
+ * metadata from GitHub and returns it; useful for inspecting a specific
8
+ * PR's shape without waiting for the next `daily` run. Nothing is persisted.
8
9
  *
9
- * - `runTrack` is an **informational lookup** that fetches PR metadata from
10
- * GitHub and returns it. Useful for inspecting a specific PR's shape
11
- * without waiting for the next `daily` run. Nothing is persisted.
12
- * - `runUntrack` is **deprecated** and always a no-op. Use `shelve` to hide
13
- * a PR from the daily digest.
10
+ * The `runUntrack` v1→v2 stub was removed in v4 (#1133). Use `shelve`/
11
+ * `unshelve` to hide a PR from the daily digest.
14
12
  */
15
13
  import { getOctokit, requireGitHubToken } from '../core/index.js';
16
14
  import { ValidationError } from '../core/errors.js';
17
15
  import { validateUrl, PR_URL_PATTERN, validateGitHubUrl } from './validation.js';
18
- import { parseGitHubUrl } from '../core/utils.js';
16
+ import { parseGitHubUrl } from '../core/urls.js';
19
17
  /**
20
18
  * Fetch metadata for a PR URL (informational — does not persist).
21
19
  *
@@ -48,23 +46,3 @@ export async function runTrack(options) {
48
46
  },
49
47
  };
50
48
  }
51
- /**
52
- * @deprecated No-op in v2. Use `runShelve` to hide a PR from the daily digest.
53
- *
54
- * Kept for backwards compatibility with v1 callers. PRs are fetched fresh
55
- * on each `daily` run, so there is no local tracking list to remove from.
56
- *
57
- * @param options - Untrack options
58
- * @param options.prUrl - Full GitHub PR URL (validated but not used)
59
- * @returns Output object with `removed: false` and a message explaining v2 behavior
60
- * @throws {ValidationError} If the URL is not a valid GitHub PR URL
61
- */
62
- export async function runUntrack(options) {
63
- validateUrl(options.prUrl);
64
- validateGitHubUrl(options.prUrl, PR_URL_PATTERN, 'PR');
65
- return {
66
- removed: false,
67
- url: options.prUrl,
68
- message: 'In v2, PRs are fetched fresh on each daily run — there is no local tracking list to remove from.',
69
- };
70
- }
@@ -0,0 +1,50 @@
1
+ /**
2
+ * GitHub authentication: token resolution + username detection.
3
+ *
4
+ * Both sync and async token getters share a process-level cache so a
5
+ * successful async fetch makes subsequent sync calls instant.
6
+ *
7
+ * Extracted from utils.ts under #1116.
8
+ */
9
+ /**
10
+ * Retrieves a GitHub authentication token, checking sources in priority order.
11
+ *
12
+ * Checks `GITHUB_TOKEN` environment variable first, then falls back to `gh auth token`
13
+ * from the GitHub CLI. The result is cached after the first successful lookup (or first
14
+ * failed attempt), so subsequent calls are instant and do not spawn subprocesses.
15
+ *
16
+ * @returns The GitHub token string, or `null` if no token is available
17
+ */
18
+ export declare function getGitHubToken(): string | null;
19
+ /**
20
+ * Returns a GitHub token or throws an error with setup instructions.
21
+ *
22
+ * Delegates to {@link getGitHubToken} and throws if no token is found. Use this
23
+ * in commands that cannot proceed without authentication.
24
+ *
25
+ * @throws {ConfigurationError} If no token is available, with instructions for `gh auth login` or setting `GITHUB_TOKEN`
26
+ */
27
+ export declare function requireGitHubToken(): string;
28
+ /**
29
+ * Resets the cached GitHub token and fetch-attempted flag.
30
+ *
31
+ * Intended for use in tests to ensure a clean state between test cases.
32
+ * After calling this, the next call to {@link getGitHubToken} will re-fetch the token.
33
+ */
34
+ export declare function resetGitHubTokenCache(): void;
35
+ /**
36
+ * Asynchronous version of {@link getGitHubToken}.
37
+ *
38
+ * Uses `execFile` (non-blocking) instead of `execFileSync` to avoid blocking
39
+ * the event loop during CLI cold start. Shares the same cache as the synchronous
40
+ * version, so a successful async fetch makes subsequent sync calls instant.
41
+ */
42
+ export declare function getGitHubTokenAsync(): Promise<string | null>;
43
+ /**
44
+ * Detect the authenticated GitHub username via the `gh` CLI.
45
+ *
46
+ * Runs `gh api user --jq '.login'` asynchronously and validates the result
47
+ * against GitHub's username rules. Never throws — returns `null` on any failure
48
+ * (gh not installed, not authenticated, invalid output, etc.).
49
+ */
50
+ export declare function detectGitHubUsername(): Promise<string | null>;
@@ -0,0 +1,160 @@
1
+ /**
2
+ * GitHub authentication: token resolution + username detection.
3
+ *
4
+ * Both sync and async token getters share a process-level cache so a
5
+ * successful async fetch makes subsequent sync calls instant.
6
+ *
7
+ * Extracted from utils.ts under #1116.
8
+ */
9
+ import { execFileSync, execFile } from 'child_process';
10
+ import { ConfigurationError } from './errors.js';
11
+ import { debug } from './logger.js';
12
+ const MODULE = 'auth';
13
+ // Cached GitHub token (fetched once per session)
14
+ let cachedGitHubToken = null;
15
+ let tokenFetchAttempted = false;
16
+ /**
17
+ * Retrieves a GitHub authentication token, checking sources in priority order.
18
+ *
19
+ * Checks `GITHUB_TOKEN` environment variable first, then falls back to `gh auth token`
20
+ * from the GitHub CLI. The result is cached after the first successful lookup (or first
21
+ * failed attempt), so subsequent calls are instant and do not spawn subprocesses.
22
+ *
23
+ * @returns The GitHub token string, or `null` if no token is available
24
+ */
25
+ export function getGitHubToken() {
26
+ if (cachedGitHubToken) {
27
+ return cachedGitHubToken;
28
+ }
29
+ if (tokenFetchAttempted) {
30
+ return null;
31
+ }
32
+ tokenFetchAttempted = true;
33
+ if (process.env.GITHUB_TOKEN) {
34
+ cachedGitHubToken = process.env.GITHUB_TOKEN;
35
+ return cachedGitHubToken;
36
+ }
37
+ try {
38
+ const token = execFileSync('gh', ['auth', 'token'], {
39
+ encoding: 'utf-8',
40
+ stdio: ['pipe', 'pipe', 'pipe'],
41
+ timeout: 2000,
42
+ }).trim();
43
+ if (token && token.length > 0) {
44
+ cachedGitHubToken = token;
45
+ debug(MODULE, 'Using GitHub token from gh CLI');
46
+ return cachedGitHubToken;
47
+ }
48
+ }
49
+ catch (err) {
50
+ debug(MODULE, 'gh auth token failed (CLI unavailable or not authenticated)', err);
51
+ }
52
+ return null;
53
+ }
54
+ /**
55
+ * Returns a GitHub token or throws an error with setup instructions.
56
+ *
57
+ * Delegates to {@link getGitHubToken} and throws if no token is found. Use this
58
+ * in commands that cannot proceed without authentication.
59
+ *
60
+ * @throws {ConfigurationError} If no token is available, with instructions for `gh auth login` or setting `GITHUB_TOKEN`
61
+ */
62
+ export function requireGitHubToken() {
63
+ const token = getGitHubToken();
64
+ if (!token) {
65
+ throw new ConfigurationError('GitHub authentication required.\n\n' +
66
+ 'Options:\n' +
67
+ ' 1. Use gh CLI: gh auth login\n' +
68
+ ' 2. Set GITHUB_TOKEN environment variable\n\n' +
69
+ 'The gh CLI is recommended - install from https://cli.github.com');
70
+ }
71
+ return token;
72
+ }
73
+ /**
74
+ * Resets the cached GitHub token and fetch-attempted flag.
75
+ *
76
+ * Intended for use in tests to ensure a clean state between test cases.
77
+ * After calling this, the next call to {@link getGitHubToken} will re-fetch the token.
78
+ */
79
+ export function resetGitHubTokenCache() {
80
+ cachedGitHubToken = null;
81
+ tokenFetchAttempted = false;
82
+ }
83
+ /**
84
+ * Asynchronous version of {@link getGitHubToken}.
85
+ *
86
+ * Uses `execFile` (non-blocking) instead of `execFileSync` to avoid blocking
87
+ * the event loop during CLI cold start. Shares the same cache as the synchronous
88
+ * version, so a successful async fetch makes subsequent sync calls instant.
89
+ */
90
+ export async function getGitHubTokenAsync() {
91
+ if (cachedGitHubToken) {
92
+ return cachedGitHubToken;
93
+ }
94
+ if (tokenFetchAttempted) {
95
+ return null;
96
+ }
97
+ tokenFetchAttempted = true;
98
+ if (process.env.GITHUB_TOKEN) {
99
+ cachedGitHubToken = process.env.GITHUB_TOKEN;
100
+ return cachedGitHubToken;
101
+ }
102
+ try {
103
+ const token = await new Promise((resolve, reject) => {
104
+ execFile('gh', ['auth', 'token'], { encoding: 'utf-8', timeout: 2000 }, (error, stdout) => {
105
+ if (error) {
106
+ reject(error);
107
+ }
108
+ else {
109
+ resolve(stdout.trim());
110
+ }
111
+ });
112
+ });
113
+ if (token && token.length > 0) {
114
+ cachedGitHubToken = token;
115
+ debug(MODULE, 'Using GitHub token from gh CLI (async)');
116
+ return cachedGitHubToken;
117
+ }
118
+ }
119
+ catch (err) {
120
+ debug(MODULE, 'gh auth token failed (CLI unavailable or not authenticated)', err);
121
+ }
122
+ return null;
123
+ }
124
+ /**
125
+ * GitHub username validation pattern.
126
+ * Usernames must start with an alphanumeric character, can contain hyphens
127
+ * (but not consecutive ones and not at the end), and be 1-39 characters.
128
+ */
129
+ const GITHUB_USERNAME_RE = /^[a-zA-Z0-9](?:[a-zA-Z0-9]|-(?=[a-zA-Z0-9])){0,38}$/;
130
+ /**
131
+ * Detect the authenticated GitHub username via the `gh` CLI.
132
+ *
133
+ * Runs `gh api user --jq '.login'` asynchronously and validates the result
134
+ * against GitHub's username rules. Never throws — returns `null` on any failure
135
+ * (gh not installed, not authenticated, invalid output, etc.).
136
+ */
137
+ export async function detectGitHubUsername() {
138
+ try {
139
+ const login = await new Promise((resolve, reject) => {
140
+ execFile('gh', ['api', 'user', '--jq', '.login'], { encoding: 'utf-8', timeout: 5000 }, (error, stdout) => {
141
+ if (error) {
142
+ reject(error);
143
+ }
144
+ else {
145
+ resolve(stdout.trim());
146
+ }
147
+ });
148
+ });
149
+ if (login && GITHUB_USERNAME_RE.test(login)) {
150
+ debug(MODULE, `Detected GitHub username: ${login}`);
151
+ return login;
152
+ }
153
+ debug(MODULE, `gh api user returned invalid username: "${login}"`);
154
+ return null;
155
+ }
156
+ catch (err) {
157
+ debug(MODULE, 'detectGitHubUsername failed', err);
158
+ return null;
159
+ }
160
+ }
@@ -1,3 +1,10 @@
1
+ /**
2
+ * Concurrency primitives shared across modules.
3
+ */
4
+ /** Default concurrency limit for parallel GitHub API requests. */
5
+ export declare const DEFAULT_CONCURRENCY = 5;
6
+ /** Async sleep — exported for mockability in tests. */
7
+ export declare function sleep(ms: number): Promise<void>;
1
8
  /**
2
9
  * Runs a worker pool that processes items with bounded concurrency.
3
10
  * N workers consume from a shared index. On any worker error, remaining
@@ -1,3 +1,12 @@
1
+ /**
2
+ * Concurrency primitives shared across modules.
3
+ */
4
+ /** Default concurrency limit for parallel GitHub API requests. */
5
+ export const DEFAULT_CONCURRENCY = 5;
6
+ /** Async sleep — exported for mockability in tests. */
7
+ export function sleep(ms) {
8
+ return new Promise((resolve) => setTimeout(resolve, ms));
9
+ }
1
10
  /**
2
11
  * Runs a worker pool that processes items with bounded concurrency.
3
12
  * N workers consume from a shared index. On any worker error, remaining
@@ -1,17 +1,20 @@
1
1
  /**
2
- * Domain logic extracted from src/commands/daily.ts.
2
+ * Domain aggregation logic extracted from src/commands/daily.ts.
3
3
  *
4
- * Pure or near-pure functions that operate on FetchedPR data:
4
+ * Pure or near-pure functions that operate on FetchedPR data and produce
5
+ * the data types that the renderers / JSON output consume:
5
6
  * - computeRepoSignals / groupPRsByRepo — per-repo aggregations
6
7
  * - assessCapacity — capacity assessment against active PRs
7
8
  * - collectActionableIssues — ordered list of issues needing attention
8
9
  * - computeActionMenu — pre-computed action menu for orchestration
9
10
  * - toShelvedPRRef — lightweight projection for digest display
10
- * - formatActionHint — human-readable maintainer action hint label
11
- * - formatBriefSummary / formatSummary / printDigest — rendering
11
+ *
12
+ * Rendering functions (formatActionHint, formatBriefSummary, formatSummary,
13
+ * printDigest) live in src/commands/daily-render.ts (#1117). This file
14
+ * re-exports them at the bottom so existing imports keep working without
15
+ * a sweep.
12
16
  */
13
- import type { FetchedPR, FetchedPRStatus, StalenessTier, ActionReason, DailyDigest, AgentState, ShelvedPRRef, MaintainerActionHint, ComputedRepoSignals, RepoGroup, CommentedIssue, CommentedIssueWithResponse } from './types.js';
14
- import type { CapacityAssessment, ActionableIssue, ActionMenu } from '../formatters/json.js';
17
+ import type { FetchedPR, FetchedPRStatus, StalenessTier, ActionReason, AgentState, ShelvedPRRef, ComputedRepoSignals, RepoGroup, CommentedIssue, CapacityAssessment, ActionableIssue, ActionMenu } from './types.js';
15
18
  /**
16
19
  * Statuses indicating action needed from the contributor.
17
20
  * Used for auto-unshelving shelved PRs.
@@ -85,12 +88,6 @@ export declare function assessCapacity(activePRs: FetchedPR[], maxActivePRs: num
85
88
  * @returns Ordered actionable issues grouped by priority
86
89
  */
87
90
  export declare function collectActionableIssues(prs: FetchedPR[], lastDigestAt?: string): ActionableIssue[];
88
- /**
89
- * Format a maintainer action hint as a human-readable label.
90
- * @param hint - The maintainer action hint enum value
91
- * @returns Human-readable label (e.g., "tests requested")
92
- */
93
- export declare function formatActionHint(hint: MaintainerActionHint): string;
94
91
  /**
95
92
  * Compute the action menu from PR data and capacity.
96
93
  * The orchestration layer can insert issue-list options (e.g., "Pick from list")
@@ -102,33 +99,4 @@ export declare function formatActionHint(hint: MaintainerActionHint): string;
102
99
  * @returns Action menu with context flags for orchestration
103
100
  */
104
101
  export declare function computeActionMenu(actionableIssues: ActionableIssue[], capacity: CapacityAssessment, commentedIssues?: CommentedIssue[]): ActionMenu;
105
- /**
106
- * Format a brief one-liner summary for the action-first flow.
107
- *
108
- * @param digest - The daily digest containing PR summary stats
109
- * @param issueCount - Number of actionable issues needing attention
110
- * @param issueResponseCount - Number of issue replies from maintainers
111
- * @returns One-line status string (e.g., "3 Active PRs | 1 needs attention | 2 issue replies")
112
- */
113
- export declare function formatBriefSummary(digest: DailyDigest, issueCount: number, issueResponseCount?: number): string;
114
- /**
115
- * Format the full dashboard summary as markdown.
116
- * Used in JSON output for Claude to display verbatim — includes all PR sections,
117
- * issue replies, and capacity status.
118
- *
119
- * @param digest - The daily digest with all PR categories
120
- * @param capacity - Current capacity assessment for display
121
- * @param issueResponses - Issue replies from maintainers to display
122
- * @returns Multi-line markdown string suitable for terminal or chat display
123
- */
124
- export declare function formatSummary(digest: DailyDigest, capacity: CapacityAssessment, issueResponses?: CommentedIssueWithResponse[]): string;
125
- /**
126
- * Print digest to console as plain text.
127
- * Unified renderer: uses the same section ordering as {@link formatSummary}
128
- * but outputs plain text with console.log instead of markdown links.
129
- *
130
- * @param digest - The daily digest with all PR categories
131
- * @param capacity - Current capacity assessment for display
132
- * @param commentedIssues - All commented issues (filtered to responses internally)
133
- */
134
- export declare function printDigest(digest: DailyDigest, capacity: CapacityAssessment, commentedIssues?: CommentedIssue[]): void;
102
+ export { formatActionHint, formatBriefSummary, formatSummary, printDigest } from '../commands/daily-render.js';
@@ -1,16 +1,19 @@
1
1
  /**
2
- * Domain logic extracted from src/commands/daily.ts.
2
+ * Domain aggregation logic extracted from src/commands/daily.ts.
3
3
  *
4
- * Pure or near-pure functions that operate on FetchedPR data:
4
+ * Pure or near-pure functions that operate on FetchedPR data and produce
5
+ * the data types that the renderers / JSON output consume:
5
6
  * - computeRepoSignals / groupPRsByRepo — per-repo aggregations
6
7
  * - assessCapacity — capacity assessment against active PRs
7
8
  * - collectActionableIssues — ordered list of issues needing attention
8
9
  * - computeActionMenu — pre-computed action menu for orchestration
9
10
  * - toShelvedPRRef — lightweight projection for digest display
10
- * - formatActionHint — human-readable maintainer action hint label
11
- * - formatBriefSummary / formatSummary / printDigest — rendering
11
+ *
12
+ * Rendering functions (formatActionHint, formatBriefSummary, formatSummary,
13
+ * printDigest) live in src/commands/daily-render.ts (#1117). This file
14
+ * re-exports them at the bottom so existing imports keep working without
15
+ * a sweep.
12
16
  */
13
- import { formatRelativeTime } from './utils.js';
14
17
  import { warn } from './logger.js';
15
18
  import { errorMessage } from './errors.js';
16
19
  import { getStateManager } from './state.js';
@@ -277,25 +280,6 @@ export function collectActionableIssues(prs, lastDigestAt) {
277
280
  }
278
281
  return issues;
279
282
  }
280
- /**
281
- * Format a maintainer action hint as a human-readable label.
282
- * @param hint - The maintainer action hint enum value
283
- * @returns Human-readable label (e.g., "tests requested")
284
- */
285
- export function formatActionHint(hint) {
286
- switch (hint) {
287
- case 'demo_requested':
288
- return 'demo/screenshot requested';
289
- case 'tests_requested':
290
- return 'tests requested';
291
- case 'changes_requested':
292
- return 'code changes requested';
293
- case 'docs_requested':
294
- return 'documentation requested';
295
- case 'rebase_requested':
296
- return 'rebase requested';
297
- }
298
- }
299
283
  /**
300
284
  * Compute the action menu from PR data and capacity.
301
285
  * The orchestration layer can insert issue-list options (e.g., "Pick from list")
@@ -369,181 +353,10 @@ export function computeActionMenu(actionableIssues, capacity, commentedIssues =
369
353
  };
370
354
  }
371
355
  // ---------------------------------------------------------------------------
372
- // Rendering / formatting
356
+ // Rendering re-exports (back-compat)
373
357
  // ---------------------------------------------------------------------------
374
- /**
375
- * Format a brief one-liner summary for the action-first flow.
376
- *
377
- * @param digest - The daily digest containing PR summary stats
378
- * @param issueCount - Number of actionable issues needing attention
379
- * @param issueResponseCount - Number of issue replies from maintainers
380
- * @returns One-line status string (e.g., "3 Active PRs | 1 needs attention | 2 issue replies")
381
- */
382
- export function formatBriefSummary(digest, issueCount, issueResponseCount = 0) {
383
- const attentionText = issueCount > 0 ? `${issueCount} need${issueCount === 1 ? 's' : ''} attention` : 'all on track';
384
- const issueReplyText = issueResponseCount > 0 ? ` | ${issueResponseCount} issue repl${issueResponseCount === 1 ? 'y' : 'ies'}` : '';
385
- return `\u{1F4CA} ${digest.summary.totalActivePRs} Active PRs | ${attentionText}${issueReplyText}`;
386
- }
387
- /**
388
- * Format the full dashboard summary as markdown.
389
- * Used in JSON output for Claude to display verbatim — includes all PR sections,
390
- * issue replies, and capacity status.
391
- *
392
- * @param digest - The daily digest with all PR categories
393
- * @param capacity - Current capacity assessment for display
394
- * @param issueResponses - Issue replies from maintainers to display
395
- * @returns Multi-line markdown string suitable for terminal or chat display
396
- */
397
- export function formatSummary(digest, capacity, issueResponses = []) {
398
- const lines = [];
399
- // Header
400
- lines.push('## OSS Dashboard');
401
- lines.push('');
402
- lines.push(`\u{1F4CA} **${digest.summary.totalActivePRs} Active PRs** | ${digest.summary.totalMergedAllTime} Merged | ${digest.summary.mergeRate}% Merge Rate`);
403
- lines.push('\u2713 Dashboard generated \u2014 say "open dashboard" to view in browser');
404
- lines.push('');
405
- // Needs Addressing
406
- if (digest.needsAddressingPRs.length > 0) {
407
- lines.push('### \u274C Needs Addressing');
408
- for (const pr of digest.needsAddressingPRs) {
409
- lines.push(`- [${pr.repo}#${pr.number}](${pr.url}): ${pr.title}`);
410
- lines.push(` \u2514\u2500 ${pr.displayLabel} ${pr.displayDescription}`);
411
- }
412
- lines.push('');
413
- }
414
- // Waiting on Maintainer
415
- if (digest.waitingOnMaintainerPRs.length > 0) {
416
- lines.push('### \u23F3 Waiting on Maintainer');
417
- for (const pr of digest.waitingOnMaintainerPRs) {
418
- lines.push(`- [${pr.repo}#${pr.number}](${pr.url}): ${pr.title}`);
419
- lines.push(` \u2514\u2500 ${pr.displayDescription}`);
420
- }
421
- lines.push('');
422
- }
423
- // Recently Merged (wins!)
424
- if (digest.recentlyMergedPRs.length > 0) {
425
- lines.push('### \u{1F389} Recently Merged');
426
- for (const pr of digest.recentlyMergedPRs) {
427
- const mergedDate = pr.mergedAt ? new Date(pr.mergedAt).toLocaleDateString() : '';
428
- lines.push(`- [${pr.repo}#${pr.number}](${pr.url}): ${pr.title}${mergedDate ? ` (merged ${mergedDate})` : ''}`);
429
- }
430
- lines.push('');
431
- }
432
- // Recently Closed (closed without merge)
433
- if (digest.recentlyClosedPRs.length > 0) {
434
- lines.push('### \u{1F6AB} Recently Closed');
435
- for (const pr of digest.recentlyClosedPRs) {
436
- const closedDate = pr.closedAt ? new Date(pr.closedAt).toLocaleDateString() : '';
437
- lines.push(`- [${pr.repo}#${pr.number}](${pr.url}): ${pr.title}${closedDate ? ` (closed ${closedDate})` : ''}`);
438
- }
439
- lines.push('');
440
- }
441
- // Auto-unshelved (important: maintainer engagement on shelved PRs)
442
- if (digest.autoUnshelvedPRs.length > 0) {
443
- lines.push('### \u{1F514} Auto-Unshelved');
444
- lines.push('> These PRs were shelved but a maintainer engaged \u2014 moved back to active.');
445
- for (const pr of digest.autoUnshelvedPRs) {
446
- lines.push(`- [${pr.repo}#${pr.number}](${pr.url}): ${pr.title} (${pr.status.replace(/_/g, ' ')})`);
447
- }
448
- lines.push('');
449
- }
450
- // Shelved PRs (dimmed, excluded from capacity)
451
- if (digest.shelvedPRs.length > 0) {
452
- lines.push('### \u{1F4E6} Shelved');
453
- for (const pr of digest.shelvedPRs) {
454
- lines.push(`- [${pr.repo}#${pr.number}](${pr.url}): ${pr.title}`);
455
- }
456
- lines.push('');
457
- }
458
- // Issue Replies
459
- if (issueResponses.length > 0) {
460
- lines.push('### \u{1F4AC} Issue Replies');
461
- for (const issue of issueResponses) {
462
- lines.push(`- [${issue.repo}#${issue.number}](${issue.url}): ${issue.title}`);
463
- const timeAgo = formatRelativeTime(issue.lastResponseAt);
464
- lines.push(` \u2514\u2500 @${issue.lastResponseAuthor}: "${issue.lastResponseBody.slice(0, 80)}${issue.lastResponseBody.length > 80 ? '...' : ''}"${timeAgo ? ` (${timeAgo})` : ''}`);
465
- }
466
- lines.push('');
467
- }
468
- // Capacity
469
- const capacityIcon = capacity.hasCapacity ? '\u2705' : '\u26A0\uFE0F';
470
- const capacityLabel = capacity.hasCapacity ? 'Ready for new work' : 'Focus on existing PRs';
471
- const shelvedNote = capacity.shelvedPRCount > 0 ? ` + ${capacity.shelvedPRCount} shelved` : '';
472
- lines.push(`**Capacity:** ${capacityIcon} ${capacityLabel} (${capacity.activePRCount}/${capacity.maxActivePRs} PRs${shelvedNote})`);
473
- return lines.join('\n');
474
- }
475
- /**
476
- * Print digest to console as plain text.
477
- * Unified renderer: uses the same section ordering as {@link formatSummary}
478
- * but outputs plain text with console.log instead of markdown links.
479
- *
480
- * @param digest - The daily digest with all PR categories
481
- * @param capacity - Current capacity assessment for display
482
- * @param commentedIssues - All commented issues (filtered to responses internally)
483
- */
484
- export function printDigest(digest, capacity, commentedIssues = []) {
485
- console.log('\n\u{1F4CA} OSS Daily Check\n');
486
- console.log(`Active PRs: ${digest.summary.totalActivePRs}`);
487
- console.log(`Needing Attention: ${digest.summary.totalNeedingAttention}`);
488
- console.log(`Merged (all time): ${digest.summary.totalMergedAllTime}`);
489
- console.log(`Merge Rate: ${digest.summary.mergeRate}%`);
490
- console.log(`\nCapacity: ${capacity.hasCapacity ? '\u2705 Ready for new work' : '\u26A0\uFE0F Focus on existing work'}`);
491
- console.log(` ${capacity.reason}\n`);
492
- if (digest.needsAddressingPRs.length > 0) {
493
- console.log('\u274C Needs Addressing:');
494
- for (const pr of digest.needsAddressingPRs) {
495
- console.log(` - ${pr.repo}#${pr.number}: ${pr.title}`);
496
- console.log(` ${pr.displayLabel} ${pr.displayDescription}`);
497
- }
498
- console.log('');
499
- }
500
- if (digest.waitingOnMaintainerPRs.length > 0) {
501
- console.log('\u23F3 Waiting on Maintainer:');
502
- for (const pr of digest.waitingOnMaintainerPRs) {
503
- console.log(` - ${pr.repo}#${pr.number}: ${pr.title}`);
504
- console.log(` ${pr.displayDescription}`);
505
- }
506
- console.log('');
507
- }
508
- if (digest.recentlyMergedPRs.length > 0) {
509
- console.log('\u{1F389} Recently Merged:');
510
- for (const pr of digest.recentlyMergedPRs) {
511
- const mergedDate = pr.mergedAt ? new Date(pr.mergedAt).toLocaleDateString() : '';
512
- console.log(` - ${pr.repo}#${pr.number}: ${pr.title}${mergedDate ? ` (merged ${mergedDate})` : ''}`);
513
- }
514
- console.log('');
515
- }
516
- if (digest.recentlyClosedPRs.length > 0) {
517
- console.log('\u{1F6AB} Recently Closed:');
518
- for (const pr of digest.recentlyClosedPRs) {
519
- const closedDate = pr.closedAt ? new Date(pr.closedAt).toLocaleDateString() : '';
520
- console.log(` - ${pr.repo}#${pr.number}: ${pr.title}${closedDate ? ` (closed ${closedDate})` : ''}`);
521
- }
522
- console.log('');
523
- }
524
- if (digest.autoUnshelvedPRs.length > 0) {
525
- console.log('\u{1F514} Auto-Unshelved:');
526
- for (const pr of digest.autoUnshelvedPRs) {
527
- console.log(` - ${pr.repo}#${pr.number}: ${pr.title} (${pr.status.replace(/_/g, ' ')})`);
528
- }
529
- console.log('');
530
- }
531
- if (digest.shelvedPRs.length > 0) {
532
- console.log('\u{1F4E6} Shelved:');
533
- for (const pr of digest.shelvedPRs) {
534
- console.log(` - ${pr.repo}#${pr.number}: ${pr.title}`);
535
- }
536
- console.log('');
537
- }
538
- const issueResponses = commentedIssues.filter((i) => i.status === 'new_response');
539
- if (issueResponses.length > 0) {
540
- console.log('\u{1F4AC} Issue Replies:');
541
- for (const issue of issueResponses) {
542
- console.log(` - ${issue.repo}#${issue.number}: ${issue.title}`);
543
- console.log(` @${issue.lastResponseAuthor}: ${issue.lastResponseBody.slice(0, 80)}${issue.lastResponseBody.length > 80 ? '...' : ''}`);
544
- }
545
- console.log('');
546
- }
547
- console.log('Run with --json for structured output');
548
- console.log('Run "dashboard serve" for browser view');
549
- }
358
+ //
359
+ // The actual implementations live in src/commands/daily-render.ts. Existing
360
+ // callers that import these from `core/daily-logic` continue to work; new
361
+ // callers should import from `commands/daily-render` directly.
362
+ export { formatActionHint, formatBriefSummary, formatSummary, printDigest } from '../commands/daily-render.js';