@oss-autopilot/core 3.4.1 → 3.6.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.
- package/dist/cli-registry.js +99 -0
- package/dist/cli.bundle.cjs +112 -105
- package/dist/commands/compliance-score.d.ts +21 -0
- package/dist/commands/compliance-score.js +156 -0
- package/dist/commands/daily.d.ts +8 -0
- package/dist/commands/daily.js +21 -0
- package/dist/commands/index.d.ts +6 -0
- package/dist/commands/index.js +6 -0
- package/dist/commands/list-mark-done.d.ts +48 -0
- package/dist/commands/list-mark-done.js +213 -0
- package/dist/commands/parse-list.js +86 -9
- package/dist/commands/repo-vet.d.ts +21 -0
- package/dist/commands/repo-vet.js +215 -0
- package/dist/commands/startup.js +41 -1
- package/dist/core/anti-llm-policy.d.ts +42 -13
- package/dist/core/anti-llm-policy.js +102 -13
- package/dist/core/ci-analysis.d.ts +32 -1
- package/dist/core/ci-analysis.js +92 -0
- package/dist/core/ci-enforced-tools.d.ts +35 -0
- package/dist/core/ci-enforced-tools.js +109 -0
- package/dist/core/comment-decision.d.ts +72 -0
- package/dist/core/comment-decision.js +74 -0
- package/dist/core/compliance-score.d.ts +127 -0
- package/dist/core/compliance-score.js +277 -0
- package/dist/core/config-registry.js +12 -0
- package/dist/core/contributing.d.ts +52 -0
- package/dist/core/contributing.js +139 -0
- package/dist/core/errors.d.ts +19 -0
- package/dist/core/errors.js +54 -0
- package/dist/core/extraction-categories.d.ts +55 -0
- package/dist/core/extraction-categories.js +108 -0
- package/dist/core/follow-up-history.d.ts +41 -0
- package/dist/core/follow-up-history.js +71 -0
- package/dist/core/gist-state-store.d.ts +30 -7
- package/dist/core/gist-state-store.js +87 -11
- package/dist/core/issue-conversation.js +1 -0
- package/dist/core/issue-effort.d.ts +29 -0
- package/dist/core/issue-effort.js +41 -0
- package/dist/core/maintainer-hints.d.ts +23 -0
- package/dist/core/maintainer-hints.js +36 -0
- package/dist/core/pr-monitor.d.ts +1 -1
- package/dist/core/pr-monitor.js +31 -11
- package/dist/core/pr-quality-rubric.d.ts +70 -0
- package/dist/core/pr-quality-rubric.js +121 -0
- package/dist/core/repo-vet.d.ts +90 -0
- package/dist/core/repo-vet.js +178 -0
- package/dist/core/state-schema.d.ts +77 -0
- package/dist/core/state-schema.js +84 -0
- package/dist/core/state.d.ts +7 -0
- package/dist/core/state.js +10 -0
- package/dist/core/strategy.d.ts +95 -0
- package/dist/core/strategy.js +270 -0
- package/dist/core/types.d.ts +51 -0
- package/dist/core/workflow-state.d.ts +56 -0
- package/dist/core/workflow-state.js +101 -0
- package/dist/formatters/json.d.ts +252 -0
- package/dist/formatters/json.js +153 -0
- package/package.json +1 -1
|
@@ -67,6 +67,45 @@ export const AnalyzedIssueConversationSchema = z.object({
|
|
|
67
67
|
repo: z.string(),
|
|
68
68
|
analyzedAt: z.string(),
|
|
69
69
|
});
|
|
70
|
+
/**
|
|
71
|
+
* One entry in a PR's follow-up history (#1277). Tier matches the
|
|
72
|
+
* cadence labels in `skills/pr-etiquette/SKILL.md` (light_check_in
|
|
73
|
+
* for 7-13 days, direct_check_in for 14-29 days, final_check_in for
|
|
74
|
+
* 30+ days). The `draftPath` is optional so an entry recorded from a
|
|
75
|
+
* direct `gh pr comment` (no draft) still validates.
|
|
76
|
+
*/
|
|
77
|
+
export const FollowUpEntrySchema = z.object({
|
|
78
|
+
tier: z.enum(['light_check_in', 'direct_check_in', 'final_check_in']),
|
|
79
|
+
timestamp: z.string().datetime(),
|
|
80
|
+
draftPath: z.string().optional(),
|
|
81
|
+
});
|
|
82
|
+
/**
|
|
83
|
+
* Pause-point snapshot for resumable workflows (#1280). When the user
|
|
84
|
+
* picks "Done for now" inside `draft-first-workflow.md`,
|
|
85
|
+
* `work-through-issues.md`, or `pre-commit-review.md`, the workflow
|
|
86
|
+
* records its position here. The next `/oss` run consults this state
|
|
87
|
+
* and offers "Resume / Restart / Discard" instead of restarting from
|
|
88
|
+
* the beginning.
|
|
89
|
+
*
|
|
90
|
+
* `stepData` is intentionally typed as `Record<string, unknown>` so
|
|
91
|
+
* each workflow can persist whatever per-step context it needs to
|
|
92
|
+
* resume cleanly (compliance-gap skip list, last review pass count,
|
|
93
|
+
* etc.) without forcing a tagged-union schema in shared state.
|
|
94
|
+
*/
|
|
95
|
+
export const WorkflowStateSchema = z.object({
|
|
96
|
+
workflowName: z.enum(['draft-first', 'work-through-issues', 'pre-commit-review']),
|
|
97
|
+
currentStep: z.string(),
|
|
98
|
+
branchName: z.string().optional(),
|
|
99
|
+
issueContext: z
|
|
100
|
+
.object({
|
|
101
|
+
title: z.string(),
|
|
102
|
+
url: z.string(),
|
|
103
|
+
})
|
|
104
|
+
.optional(),
|
|
105
|
+
completedSteps: z.array(z.string()).default([]),
|
|
106
|
+
stepData: z.record(z.string(), z.unknown()).default({}),
|
|
107
|
+
lastUpdatedAt: z.string().datetime(),
|
|
108
|
+
});
|
|
70
109
|
// ── 3. Contribution schemas ──────────────────────────────────────────
|
|
71
110
|
export const ContributionGuidelinesSchema = z.object({
|
|
72
111
|
branchNamingConvention: z.string().optional(),
|
|
@@ -161,6 +200,25 @@ export const AgentConfigSchema = z.object({
|
|
|
161
200
|
* the hook does nothing on every push unless the user explicitly enables it.
|
|
162
201
|
*/
|
|
163
202
|
autoFormatBeforePush: z.boolean().default(false),
|
|
203
|
+
/**
|
|
204
|
+
* Threshold (in minutes) for the SessionStart PR health one-liner (#1255).
|
|
205
|
+
* The cached digest only refreshes when the user runs `/oss`; SessionStart
|
|
206
|
+
* fires every session. Without a freshness gate the line drifts arbitrarily
|
|
207
|
+
* stale between runs. When the cache is older than this many minutes (and
|
|
208
|
+
* not yet 7 days old, which keeps the existing catch-up nudge), the line is
|
|
209
|
+
* suppressed entirely. Default 30 minutes.
|
|
210
|
+
*/
|
|
211
|
+
healthCheckFreshnessMinutes: z.number().int().positive().default(30),
|
|
212
|
+
/**
|
|
213
|
+
* Convergence cap for the multi-agent review loop in
|
|
214
|
+
* `workflows/dispatch-review.md` (#1275). When unset, the workflow
|
|
215
|
+
* falls back to per-mode defaults (5 for diff, 3 for plan). Lower
|
|
216
|
+
* values shorten the loop at the cost of skipping later iterations
|
|
217
|
+
* if findings persist; higher values give the loop more chances to
|
|
218
|
+
* converge before bailing. Optional — leave unset to use the
|
|
219
|
+
* defaults.
|
|
220
|
+
*/
|
|
221
|
+
reviewMaxPasses: z.number().int().positive().optional(),
|
|
164
222
|
/**
|
|
165
223
|
* Optional Ollama model for SLM pre-triage during issue vetting (#1122).
|
|
166
224
|
* Empty disables the feature. Recommended: `gemma4:e4b` (default for
|
|
@@ -227,6 +285,15 @@ export const AgentStateSchema = z.object({
|
|
|
227
285
|
config: AgentConfigSchema.default(() => AgentConfigSchema.parse({})),
|
|
228
286
|
lastRunAt: z.string().default(() => new Date().toISOString()),
|
|
229
287
|
lastDigestAt: z.string().optional(),
|
|
288
|
+
/**
|
|
289
|
+
* ISO timestamp of the most recent {@link computeStrategy} invocation
|
|
290
|
+
* embedded in a daily run output (#1270). The cadence gate in
|
|
291
|
+
* `daily.ts` consults this — strategy snapshots fire every 30 days OR
|
|
292
|
+
* when 5+ PRs have merged since the last snapshot, whichever comes
|
|
293
|
+
* first. Below {@link STRATEGY_MIN_PRS} merged PRs the gate stays
|
|
294
|
+
* silent regardless of cadence.
|
|
295
|
+
*/
|
|
296
|
+
lastStrategyAt: z.string().optional(),
|
|
230
297
|
lastDigest: DailyDigestSchema.optional(),
|
|
231
298
|
monthlyMergedCounts: z.record(z.string(), z.number()).optional(),
|
|
232
299
|
monthlyClosedCounts: z.record(z.string(), z.number()).optional(),
|
|
@@ -236,4 +303,21 @@ export const AgentStateSchema = z.object({
|
|
|
236
303
|
closedPRs: z.array(StoredClosedPRSchema).optional(),
|
|
237
304
|
analyzedIssueConversations: z.array(AnalyzedIssueConversationSchema).optional(),
|
|
238
305
|
activeIssues: z.array(TrackedIssueSchema).default([]),
|
|
306
|
+
/**
|
|
307
|
+
* Per-PR follow-up history (#1277). Keyed by PR URL. Each entry
|
|
308
|
+
* records a tier-bucketed follow-up that the user drafted (and
|
|
309
|
+
* presumably posted via `draft-review-post`). The dormant-pr
|
|
310
|
+
* workflow reads this before drafting to enforce the
|
|
311
|
+
* one-follow-up-per-timeframe rule documented in
|
|
312
|
+
* `skills/pr-etiquette/SKILL.md`.
|
|
313
|
+
*/
|
|
314
|
+
prFollowUpHistory: z.record(z.string(), z.array(FollowUpEntrySchema)).optional(),
|
|
315
|
+
/**
|
|
316
|
+
* Pause-point snapshot for resumable workflows (#1280). Set when
|
|
317
|
+
* the user picks "Done for now" mid-workflow; cleared when the
|
|
318
|
+
* workflow completes or the user explicitly discards. The router
|
|
319
|
+
* reads this on every `/oss` invocation and offers Resume /
|
|
320
|
+
* Restart / Discard when present.
|
|
321
|
+
*/
|
|
322
|
+
workflowState: WorkflowStateSchema.optional(),
|
|
239
323
|
});
|
package/dist/core/state.d.ts
CHANGED
|
@@ -174,6 +174,13 @@ export declare class StateManager {
|
|
|
174
174
|
* @param digest - The daily digest to store
|
|
175
175
|
*/
|
|
176
176
|
setLastDigest(digest: DailyDigest): void;
|
|
177
|
+
/**
|
|
178
|
+
* Persist the timestamp of the most recent strategy snapshot embedded
|
|
179
|
+
* in a daily run output (#1270). Called from the daily pipeline after
|
|
180
|
+
* `computeStrategy()` succeeds; the cadence gate in
|
|
181
|
+
* {@link shouldComputeStrategy} reads this on the next run.
|
|
182
|
+
*/
|
|
183
|
+
setLastStrategyAt(iso: string): void;
|
|
177
184
|
/**
|
|
178
185
|
* Update monthly merged PR counts for dashboard display.
|
|
179
186
|
* @param counts - Monthly merged PR counts keyed by YYYY-MM
|
package/dist/core/state.js
CHANGED
|
@@ -416,6 +416,16 @@ export class StateManager {
|
|
|
416
416
|
this.state.lastDigestAt = digest.generatedAt;
|
|
417
417
|
this.autoSave();
|
|
418
418
|
}
|
|
419
|
+
/**
|
|
420
|
+
* Persist the timestamp of the most recent strategy snapshot embedded
|
|
421
|
+
* in a daily run output (#1270). Called from the daily pipeline after
|
|
422
|
+
* `computeStrategy()` succeeds; the cadence gate in
|
|
423
|
+
* {@link shouldComputeStrategy} reads this on the next run.
|
|
424
|
+
*/
|
|
425
|
+
setLastStrategyAt(iso) {
|
|
426
|
+
this.state.lastStrategyAt = iso;
|
|
427
|
+
this.autoSave();
|
|
428
|
+
}
|
|
419
429
|
/**
|
|
420
430
|
* Update monthly merged PR counts for dashboard display.
|
|
421
431
|
* @param counts - Monthly merged PR counts keyed by YYYY-MM
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Contribution strategy compute (#1243).
|
|
3
|
+
*
|
|
4
|
+
* Extracts the deterministic compute layer from
|
|
5
|
+
* `agents/contribution-strategist.md` so the categorization,
|
|
6
|
+
* trajectory detection, and capacity overextension rules are typed,
|
|
7
|
+
* unit-testable, and consumable both by the agent (synthesis layer)
|
|
8
|
+
* and a future auto-display in `/oss`.
|
|
9
|
+
*
|
|
10
|
+
* Same architectural shape as success-grade (#858), linked-PR
|
|
11
|
+
* classifier (#910), and compliance-score (#1245). Pure function — no
|
|
12
|
+
* I/O, no global state, no LLM. The interpretive narrative ("you're
|
|
13
|
+
* doing X well, here's a growth opportunity") stays in the agent
|
|
14
|
+
* prompt.
|
|
15
|
+
*/
|
|
16
|
+
import type { AgentState } from './state-schema.js';
|
|
17
|
+
export type ContributorStyle = 'maintainer' | 'explorer' | 'specialist' | 'generalist';
|
|
18
|
+
export type TrajectoryDirection = 'growing' | 'steady' | 'declining';
|
|
19
|
+
export type CapacityAction = 'open_more' | 'follow_up_dormant' | 'wait_on_maintainers' | null;
|
|
20
|
+
export interface StrategyProfile {
|
|
21
|
+
style: ContributorStyle;
|
|
22
|
+
totalPRs: number;
|
|
23
|
+
mergedCount: number;
|
|
24
|
+
/** Merge rate as a 0-1 fraction. `0` when no PRs are tracked. */
|
|
25
|
+
mergeRate: number;
|
|
26
|
+
/** Top languages by repo PR count, descending. Empty when language data
|
|
27
|
+
* is not available in repoScores. */
|
|
28
|
+
primaryLanguages: string[];
|
|
29
|
+
/** Top repos by merged PR count, descending. */
|
|
30
|
+
favoriteRepos: string[];
|
|
31
|
+
}
|
|
32
|
+
export interface StrategyCapacity {
|
|
33
|
+
openPRCount: number;
|
|
34
|
+
dormantPRCount: number;
|
|
35
|
+
/** Distinct repos hosting at least one dormant PR. Surfaces alongside
|
|
36
|
+
* `dormantPRCount` so consumers can render "N PRs across M repos"
|
|
37
|
+
* without re-deriving from `openPRs` themselves. */
|
|
38
|
+
dormantRepoCount: number;
|
|
39
|
+
/** True when dormant PRs span >=2 distinct repos (a signal that the
|
|
40
|
+
* contributor is awaiting reviews from multiple maintainers, not just
|
|
41
|
+
* one slow project). Equivalent to `dormantPRCount >= 2 &&
|
|
42
|
+
* dormantRepoCount >= 2` — exposed as a separate boolean so callers
|
|
43
|
+
* with a strict yes/no presentation don't have to recompute. */
|
|
44
|
+
overExtended: boolean;
|
|
45
|
+
/** The single highest-priority next action — null when state is too
|
|
46
|
+
* thin to recommend anything. */
|
|
47
|
+
suggestedAction: CapacityAction;
|
|
48
|
+
}
|
|
49
|
+
export interface StrategyPatterns {
|
|
50
|
+
prTypeDistribution: {
|
|
51
|
+
docs: number;
|
|
52
|
+
fixes: number;
|
|
53
|
+
features: number;
|
|
54
|
+
refactors: number;
|
|
55
|
+
tests: number;
|
|
56
|
+
other: number;
|
|
57
|
+
};
|
|
58
|
+
trajectoryDirection: TrajectoryDirection;
|
|
59
|
+
averagePRSize: number;
|
|
60
|
+
}
|
|
61
|
+
export interface StrategyRecommendations {
|
|
62
|
+
languages: string[];
|
|
63
|
+
repos: string[];
|
|
64
|
+
issueTypes: string[];
|
|
65
|
+
avoidPatterns: string[];
|
|
66
|
+
}
|
|
67
|
+
export interface StrategyResult {
|
|
68
|
+
profile: StrategyProfile;
|
|
69
|
+
capacity: StrategyCapacity;
|
|
70
|
+
patterns: StrategyPatterns;
|
|
71
|
+
recommendations: StrategyRecommendations;
|
|
72
|
+
}
|
|
73
|
+
/** Minimum tracked PR history before strategy output is meaningful (#1243). */
|
|
74
|
+
export declare const STRATEGY_MIN_PRS = 10;
|
|
75
|
+
/**
|
|
76
|
+
* Compute the deterministic strategy signal from agent state. Returns
|
|
77
|
+
* null when state is thinner than {@link STRATEGY_MIN_PRS} merged PRs —
|
|
78
|
+
* the auto-display surface uses this null sentinel as the
|
|
79
|
+
* minimum-data gate (#1243).
|
|
80
|
+
*/
|
|
81
|
+
export declare function computeStrategy(state: AgentState): StrategyResult | null;
|
|
82
|
+
/** Cadence trigger thresholds for the auto-display in `/oss` (#1270). */
|
|
83
|
+
export declare const STRATEGY_CADENCE_DAYS = 30;
|
|
84
|
+
export declare const STRATEGY_CADENCE_MERGED_DELTA = 5;
|
|
85
|
+
/**
|
|
86
|
+
* Decide whether a strategy snapshot should be embedded in this daily run.
|
|
87
|
+
* Returns true when EITHER 30 days have elapsed since the last snapshot OR
|
|
88
|
+
* 5+ PRs have merged since then, AND the merge floor in
|
|
89
|
+
* {@link STRATEGY_MIN_PRS} is met. The caller is responsible for calling
|
|
90
|
+
* {@link computeStrategy} when this returns true and for persisting
|
|
91
|
+
* `state.lastStrategyAt` after a successful compute.
|
|
92
|
+
*
|
|
93
|
+
* `nowIso` is injected so tests can pin time without mocking `Date`.
|
|
94
|
+
*/
|
|
95
|
+
export declare function shouldComputeStrategy(state: AgentState, nowIso: string): boolean;
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Contribution strategy compute (#1243).
|
|
3
|
+
*
|
|
4
|
+
* Extracts the deterministic compute layer from
|
|
5
|
+
* `agents/contribution-strategist.md` so the categorization,
|
|
6
|
+
* trajectory detection, and capacity overextension rules are typed,
|
|
7
|
+
* unit-testable, and consumable both by the agent (synthesis layer)
|
|
8
|
+
* and a future auto-display in `/oss`.
|
|
9
|
+
*
|
|
10
|
+
* Same architectural shape as success-grade (#858), linked-PR
|
|
11
|
+
* classifier (#910), and compliance-score (#1245). Pure function — no
|
|
12
|
+
* I/O, no global state, no LLM. The interpretive narrative ("you're
|
|
13
|
+
* doing X well, here's a growth opportunity") stays in the agent
|
|
14
|
+
* prompt.
|
|
15
|
+
*/
|
|
16
|
+
/** Minimum tracked PR history before strategy output is meaningful (#1243). */
|
|
17
|
+
export const STRATEGY_MIN_PRS = 10;
|
|
18
|
+
/** How many top-N items each "primary" / "favorite" list returns. */
|
|
19
|
+
const TOP_N = 5;
|
|
20
|
+
/** Days since `mergedAt` after which a PR no longer counts as recent for
|
|
21
|
+
* the active-now PR-type distribution. 90 days matches the issue's
|
|
22
|
+
* proposed trajectory window. */
|
|
23
|
+
const RECENT_WINDOW_DAYS = 90;
|
|
24
|
+
/** Match repo from a GitHub URL: `https://github.com/owner/repo/pull/N`. */
|
|
25
|
+
const REPO_FROM_URL = /https:\/\/github\.com\/([^/]+)\/([^/]+)\/(?:issues|pull)\/\d+/i;
|
|
26
|
+
function parseRepo(url) {
|
|
27
|
+
const m = url.match(REPO_FROM_URL);
|
|
28
|
+
return m ? `${m[1]}/${m[2]}` : null;
|
|
29
|
+
}
|
|
30
|
+
function classifyTitle(title) {
|
|
31
|
+
const t = title.trim().toLowerCase();
|
|
32
|
+
// Match Conventional Commits prefix first; fall back to keyword search
|
|
33
|
+
// in the title body.
|
|
34
|
+
const prefix = t.match(/^(feat|fix|docs|refactor|test|perf|chore|build|ci|style|revert)(?:\([^)]+\))?!?:/);
|
|
35
|
+
const tag = prefix?.[1];
|
|
36
|
+
if (tag === 'docs')
|
|
37
|
+
return 'docs';
|
|
38
|
+
if (tag === 'fix' || tag === 'perf')
|
|
39
|
+
return 'fixes';
|
|
40
|
+
if (tag === 'feat')
|
|
41
|
+
return 'features';
|
|
42
|
+
if (tag === 'refactor')
|
|
43
|
+
return 'refactors';
|
|
44
|
+
if (tag === 'test')
|
|
45
|
+
return 'tests';
|
|
46
|
+
// Heuristic fallbacks for non-conventional titles.
|
|
47
|
+
if (/\b(?:doc|readme|comment|typo)\b/i.test(t))
|
|
48
|
+
return 'docs';
|
|
49
|
+
if (/\b(?:fix|bug|crash|regression|broken)\b/i.test(t))
|
|
50
|
+
return 'fixes';
|
|
51
|
+
if (/\b(?:add|implement|introduce|support|new)\b/i.test(t))
|
|
52
|
+
return 'features';
|
|
53
|
+
if (/\b(?:refactor|cleanup|extract|simplify|rename)\b/i.test(t))
|
|
54
|
+
return 'refactors';
|
|
55
|
+
if (/\btest|spec\b/i.test(t))
|
|
56
|
+
return 'tests';
|
|
57
|
+
return 'other';
|
|
58
|
+
}
|
|
59
|
+
function topNByCount(counts, n) {
|
|
60
|
+
return Object.entries(counts)
|
|
61
|
+
.sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]))
|
|
62
|
+
.slice(0, n)
|
|
63
|
+
.map(([name]) => name);
|
|
64
|
+
}
|
|
65
|
+
function determineStyle(_totalPRs, primaryLanguages, favoriteRepos) {
|
|
66
|
+
// `totalPRs` is reserved for future "explorer" classification (low-volume
|
|
67
|
+
// contributors). Today the function is only reachable from `computeStrategy`
|
|
68
|
+
// which gates at `merged.length >= STRATEGY_MIN_PRS` (10), so a `totalPRs < 5`
|
|
69
|
+
// branch could never fire — removed to avoid misleading future readers.
|
|
70
|
+
// Heavy concentration in 1-2 repos → maintainer; spread across many → generalist.
|
|
71
|
+
if (favoriteRepos.length === 1)
|
|
72
|
+
return 'maintainer';
|
|
73
|
+
if (primaryLanguages.length === 1 && favoriteRepos.length >= 2 && favoriteRepos.length <= 3) {
|
|
74
|
+
return 'specialist';
|
|
75
|
+
}
|
|
76
|
+
if (favoriteRepos.length >= 4)
|
|
77
|
+
return 'generalist';
|
|
78
|
+
return 'specialist';
|
|
79
|
+
}
|
|
80
|
+
function determineTrajectory(state) {
|
|
81
|
+
// Compare the last three months' merged PR counts against the prior
|
|
82
|
+
// three months. If each window has at least one merge, ratio >1.2 is
|
|
83
|
+
// growing, <0.8 is declining, otherwise steady.
|
|
84
|
+
const counts = state.monthlyMergedCounts ?? {};
|
|
85
|
+
const months = Object.keys(counts).sort();
|
|
86
|
+
if (months.length < 6)
|
|
87
|
+
return 'steady';
|
|
88
|
+
const recent = months.slice(-3).reduce((sum, m) => sum + (counts[m] ?? 0), 0);
|
|
89
|
+
const prior = months.slice(-6, -3).reduce((sum, m) => sum + (counts[m] ?? 0), 0);
|
|
90
|
+
if (prior === 0)
|
|
91
|
+
return recent > 0 ? 'growing' : 'steady';
|
|
92
|
+
const ratio = recent / prior;
|
|
93
|
+
if (ratio >= 1.2)
|
|
94
|
+
return 'growing';
|
|
95
|
+
if (ratio <= 0.8)
|
|
96
|
+
return 'declining';
|
|
97
|
+
return 'steady';
|
|
98
|
+
}
|
|
99
|
+
function recommendForOverExtension(openPRCount, dormantPRCount, overExtended) {
|
|
100
|
+
if (overExtended)
|
|
101
|
+
return 'follow_up_dormant';
|
|
102
|
+
if (dormantPRCount > 0 && openPRCount > 5)
|
|
103
|
+
return 'wait_on_maintainers';
|
|
104
|
+
if (openPRCount === 0)
|
|
105
|
+
return 'open_more';
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Compute the deterministic strategy signal from agent state. Returns
|
|
110
|
+
* null when state is thinner than {@link STRATEGY_MIN_PRS} merged PRs —
|
|
111
|
+
* the auto-display surface uses this null sentinel as the
|
|
112
|
+
* minimum-data gate (#1243).
|
|
113
|
+
*/
|
|
114
|
+
export function computeStrategy(state) {
|
|
115
|
+
const merged = state.mergedPRs ?? [];
|
|
116
|
+
const closed = state.closedPRs ?? [];
|
|
117
|
+
const totalPRs = merged.length + closed.length;
|
|
118
|
+
if (merged.length < STRATEGY_MIN_PRS)
|
|
119
|
+
return null;
|
|
120
|
+
// Per-repo PR counts (merged). Used both for "favorite repos" and to
|
|
121
|
+
// back-fill primary languages from `repoScores` entries that overlap.
|
|
122
|
+
const mergedByRepo = {};
|
|
123
|
+
for (const pr of merged) {
|
|
124
|
+
const repo = parseRepo(pr.url);
|
|
125
|
+
if (!repo)
|
|
126
|
+
continue;
|
|
127
|
+
mergedByRepo[repo] = (mergedByRepo[repo] ?? 0) + 1;
|
|
128
|
+
}
|
|
129
|
+
const favoriteRepos = topNByCount(mergedByRepo, TOP_N);
|
|
130
|
+
// Languages: weight a repo's language by that repo's merged count so
|
|
131
|
+
// `vercel/next.js` (TypeScript) counts more than a one-off PR to a
|
|
132
|
+
// Lua project even if both repos are in `repoScores`.
|
|
133
|
+
const languageCounts = {};
|
|
134
|
+
for (const [repo, count] of Object.entries(mergedByRepo)) {
|
|
135
|
+
const score = state.repoScores[repo];
|
|
136
|
+
const lang = score?.language;
|
|
137
|
+
if (!lang)
|
|
138
|
+
continue;
|
|
139
|
+
languageCounts[lang] = (languageCounts[lang] ?? 0) + count;
|
|
140
|
+
}
|
|
141
|
+
const primaryLanguages = topNByCount(languageCounts, TOP_N);
|
|
142
|
+
// PR-type distribution over the recent window.
|
|
143
|
+
const distribution = {
|
|
144
|
+
docs: 0,
|
|
145
|
+
fixes: 0,
|
|
146
|
+
features: 0,
|
|
147
|
+
refactors: 0,
|
|
148
|
+
tests: 0,
|
|
149
|
+
other: 0,
|
|
150
|
+
};
|
|
151
|
+
const cutoff = Date.now() - RECENT_WINDOW_DAYS * 86400000;
|
|
152
|
+
for (const pr of merged) {
|
|
153
|
+
const mergedAt = Date.parse(pr.mergedAt);
|
|
154
|
+
if (Number.isNaN(mergedAt) || mergedAt < cutoff)
|
|
155
|
+
continue;
|
|
156
|
+
distribution[classifyTitle(pr.title)] += 1;
|
|
157
|
+
}
|
|
158
|
+
// Capacity: read from the last digest. When no digest exists yet,
|
|
159
|
+
// default to zero (the agent has nothing to recommend without a
|
|
160
|
+
// digest). The DailyDigest schema does not store a `dormantCount`
|
|
161
|
+
// directly — derive it from the `waitingOnMaintainerPRs` array,
|
|
162
|
+
// which is the "PR flagged as awaiting maintainer review" bucket
|
|
163
|
+
// produced by `pr-monitor`.
|
|
164
|
+
const summary = state.lastDigest?.summary;
|
|
165
|
+
const openPRCount = summary?.totalActivePRs ?? 0;
|
|
166
|
+
const waiting = state.lastDigest?.waitingOnMaintainerPRs ?? [];
|
|
167
|
+
const dormantPRCount = waiting.length;
|
|
168
|
+
// Overextended: dormant PRs spread across 2+ repos. Same definition
|
|
169
|
+
// the issue body uses.
|
|
170
|
+
const dormantRepos = new Set(waiting
|
|
171
|
+
.map((pr) => (typeof pr.url === 'string' ? parseRepo(pr.url) : null))
|
|
172
|
+
.filter((r) => r !== null));
|
|
173
|
+
const overExtended = dormantPRCount >= 2 && dormantRepos.size >= 2;
|
|
174
|
+
const profile = {
|
|
175
|
+
style: determineStyle(totalPRs, primaryLanguages, favoriteRepos),
|
|
176
|
+
totalPRs,
|
|
177
|
+
mergedCount: merged.length,
|
|
178
|
+
mergeRate: totalPRs === 0 ? 0 : merged.length / totalPRs,
|
|
179
|
+
primaryLanguages,
|
|
180
|
+
favoriteRepos,
|
|
181
|
+
};
|
|
182
|
+
const capacity = {
|
|
183
|
+
openPRCount,
|
|
184
|
+
dormantPRCount,
|
|
185
|
+
dormantRepoCount: dormantRepos.size,
|
|
186
|
+
overExtended,
|
|
187
|
+
suggestedAction: recommendForOverExtension(openPRCount, dormantPRCount, overExtended),
|
|
188
|
+
};
|
|
189
|
+
const patterns = {
|
|
190
|
+
prTypeDistribution: distribution,
|
|
191
|
+
trajectoryDirection: determineTrajectory(state),
|
|
192
|
+
// Without per-PR diff size in StoredMergedPR, a true "average PR
|
|
193
|
+
// size" lookup is out of scope until that data is captured (#1243
|
|
194
|
+
// explicitly defers this). Surface 0 so downstream callers know the
|
|
195
|
+
// signal is unavailable rather than fabricating a value.
|
|
196
|
+
averagePRSize: 0,
|
|
197
|
+
};
|
|
198
|
+
const recommendations = {
|
|
199
|
+
// The deterministic recommendations layer reuses the profile
|
|
200
|
+
// signals — the agent's coaching prose layers on top of these.
|
|
201
|
+
languages: primaryLanguages,
|
|
202
|
+
repos: favoriteRepos,
|
|
203
|
+
issueTypes: deriveIssueTypePreferences(distribution),
|
|
204
|
+
avoidPatterns: deriveAvoidPatterns(capacity, patterns),
|
|
205
|
+
};
|
|
206
|
+
return { profile, capacity, patterns, recommendations };
|
|
207
|
+
}
|
|
208
|
+
/** Cadence trigger thresholds for the auto-display in `/oss` (#1270). */
|
|
209
|
+
export const STRATEGY_CADENCE_DAYS = 30;
|
|
210
|
+
export const STRATEGY_CADENCE_MERGED_DELTA = 5;
|
|
211
|
+
/**
|
|
212
|
+
* Decide whether a strategy snapshot should be embedded in this daily run.
|
|
213
|
+
* Returns true when EITHER 30 days have elapsed since the last snapshot OR
|
|
214
|
+
* 5+ PRs have merged since then, AND the merge floor in
|
|
215
|
+
* {@link STRATEGY_MIN_PRS} is met. The caller is responsible for calling
|
|
216
|
+
* {@link computeStrategy} when this returns true and for persisting
|
|
217
|
+
* `state.lastStrategyAt` after a successful compute.
|
|
218
|
+
*
|
|
219
|
+
* `nowIso` is injected so tests can pin time without mocking `Date`.
|
|
220
|
+
*/
|
|
221
|
+
export function shouldComputeStrategy(state, nowIso) {
|
|
222
|
+
const merged = state.mergedPRs ?? [];
|
|
223
|
+
if (merged.length < STRATEGY_MIN_PRS)
|
|
224
|
+
return false;
|
|
225
|
+
const lastIso = state.lastStrategyAt;
|
|
226
|
+
if (!lastIso)
|
|
227
|
+
return true;
|
|
228
|
+
// Time-based trigger: 30+ days since last snapshot.
|
|
229
|
+
const lastMs = Date.parse(lastIso);
|
|
230
|
+
const nowMs = Date.parse(nowIso);
|
|
231
|
+
if (Number.isFinite(lastMs) && Number.isFinite(nowMs)) {
|
|
232
|
+
const daysSince = (nowMs - lastMs) / (1000 * 60 * 60 * 24);
|
|
233
|
+
if (daysSince >= STRATEGY_CADENCE_DAYS)
|
|
234
|
+
return true;
|
|
235
|
+
}
|
|
236
|
+
else {
|
|
237
|
+
// Unparseable timestamps — fail open and recompute rather than
|
|
238
|
+
// silently never re-firing. The lastStrategyAt write below will
|
|
239
|
+
// refresh to a valid ISO string.
|
|
240
|
+
return true;
|
|
241
|
+
}
|
|
242
|
+
// Merge-count trigger: count PRs merged after `lastStrategyAt`.
|
|
243
|
+
// `mergedAt` is required on StoredMergedPRSchema; `Date.parse` returns
|
|
244
|
+
// NaN for malformed-but-stored data and Number.isFinite excludes those.
|
|
245
|
+
const mergedSince = merged.filter((pr) => {
|
|
246
|
+
const mergedMs = Date.parse(pr.mergedAt);
|
|
247
|
+
return Number.isFinite(mergedMs) && mergedMs > lastMs;
|
|
248
|
+
}).length;
|
|
249
|
+
return mergedSince >= STRATEGY_CADENCE_MERGED_DELTA;
|
|
250
|
+
}
|
|
251
|
+
function deriveIssueTypePreferences(distribution) {
|
|
252
|
+
// Recommend the user's two strongest PR types — they have a track
|
|
253
|
+
// record there, so issues in those buckets are higher-yield.
|
|
254
|
+
const ranked = Object.entries(distribution)
|
|
255
|
+
.filter(([type]) => type !== 'other')
|
|
256
|
+
.sort((a, b) => b[1] - a[1])
|
|
257
|
+
.slice(0, 2)
|
|
258
|
+
.map(([type]) => type);
|
|
259
|
+
return ranked;
|
|
260
|
+
}
|
|
261
|
+
function deriveAvoidPatterns(capacity, patterns) {
|
|
262
|
+
const out = [];
|
|
263
|
+
if (capacity.overExtended) {
|
|
264
|
+
out.push('opening new PRs while dormant ones await review across multiple repos');
|
|
265
|
+
}
|
|
266
|
+
if (patterns.trajectoryDirection === 'declining') {
|
|
267
|
+
out.push('drifting away from a previously productive cadence — capacity may be saturated');
|
|
268
|
+
}
|
|
269
|
+
return out;
|
|
270
|
+
}
|
package/dist/core/types.d.ts
CHANGED
|
@@ -19,6 +19,46 @@ export interface ClassifiedCheck {
|
|
|
19
19
|
category: CIFailureCategory;
|
|
20
20
|
conclusion?: string;
|
|
21
21
|
}
|
|
22
|
+
/**
|
|
23
|
+
* Mutually exclusive overall-CI categories produced by
|
|
24
|
+
* {@link categorizeCIStatus} (#1272). The 5-row truth table that lived
|
|
25
|
+
* as prose in `agents/pr-health-checker.md` — extracted so any consumer
|
|
26
|
+
* (the agent, the dashboard, future MCP surfaces) reads one typed field
|
|
27
|
+
* instead of re-deriving the table.
|
|
28
|
+
*
|
|
29
|
+
* - `all_passing` — every reported check is green
|
|
30
|
+
* - `failing` — at least one actionable failure (real test/lint/build
|
|
31
|
+
* issue), OR ciStatus reported failing without per-check detail (the
|
|
32
|
+
* honest answer when the legacy combined-status endpoint can't tell
|
|
33
|
+
* us what failed)
|
|
34
|
+
* - `fork_limitation` — failures exist but ALL of them are
|
|
35
|
+
* `fork_limitation` / `auth_gate` (Vercel preview, internal CI) — purely
|
|
36
|
+
* informational
|
|
37
|
+
* - `blocked` — checks are pending (awaiting trigger / completion), OR
|
|
38
|
+
* non-actionable failures include `infrastructure` (cancelled /
|
|
39
|
+
* timed-out runner — re-running often resolves)
|
|
40
|
+
* - `not_running` — no checks reported
|
|
41
|
+
*/
|
|
42
|
+
export type CIStatusCategory = 'all_passing' | 'failing' | 'fork_limitation' | 'blocked' | 'not_running';
|
|
43
|
+
/**
|
|
44
|
+
* Suggested action for the {@link CIStatusCategorization}. Hint, not
|
|
45
|
+
* enforcement — the consuming agent may still escalate or skip based on
|
|
46
|
+
* other PR context.
|
|
47
|
+
*/
|
|
48
|
+
export type CIStatusAction = 'none' | 'investigate' | 'request_rerun' | 'check_workflows' | 'informational';
|
|
49
|
+
/**
|
|
50
|
+
* Aggregate CI status produced by {@link categorizeCIStatus} (#1272).
|
|
51
|
+
* Derived from `ciStatus + failingCheckNames + classifiedChecks` —
|
|
52
|
+
* exposed on {@link FetchedPR} so agents read a single field instead
|
|
53
|
+
* of re-implementing the truth table.
|
|
54
|
+
*/
|
|
55
|
+
export interface CIStatusCategorization {
|
|
56
|
+
category: CIStatusCategory;
|
|
57
|
+
/** Short human-readable summary suitable for inline display. */
|
|
58
|
+
summary: string;
|
|
59
|
+
/** Suggested next action (hint, not enforcement). */
|
|
60
|
+
action: CIStatusAction;
|
|
61
|
+
}
|
|
22
62
|
/** CI status result returned by getCIStatus(). */
|
|
23
63
|
export interface CIStatusResult {
|
|
24
64
|
status: CIStatus;
|
|
@@ -115,6 +155,15 @@ export interface FetchedPR {
|
|
|
115
155
|
failingCheckNames: string[];
|
|
116
156
|
/** Failing checks with category classification (#81). Separates actionable failures from fork limitations and auth gates. */
|
|
117
157
|
classifiedChecks: ClassifiedCheck[];
|
|
158
|
+
/**
|
|
159
|
+
* Aggregate 5-state CI categorization (#1272). Derived from `ciStatus`,
|
|
160
|
+
* `failingCheckNames`, and `classifiedChecks` via `categorizeCIStatus()`
|
|
161
|
+
* — agents read this directly instead of re-deriving the truth table.
|
|
162
|
+
* Always populated on a fresh fetch (v2 architecture has no cached
|
|
163
|
+
* `FetchedPR` to migrate); pr-monitor's `fetchPRDetails` sets it on
|
|
164
|
+
* every PR before construction.
|
|
165
|
+
*/
|
|
166
|
+
ciCategorization: CIStatusCategorization;
|
|
118
167
|
hasMergeConflict: boolean;
|
|
119
168
|
reviewDecision: ReviewDecision;
|
|
120
169
|
/** How many commits the PR branch is behind the base branch. */
|
|
@@ -207,6 +256,8 @@ interface CommentedIssueBase {
|
|
|
207
256
|
title: string;
|
|
208
257
|
url: string;
|
|
209
258
|
userLastCommentedAt: string;
|
|
259
|
+
/** User's most recent comment body, truncated to 200 chars (+ "..." suffix when truncated). #1290 */
|
|
260
|
+
userLastCommentBody: string;
|
|
210
261
|
labels: string[];
|
|
211
262
|
daysSinceUserComment: number;
|
|
212
263
|
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workflow-state helpers (#1280).
|
|
3
|
+
*
|
|
4
|
+
* Centralizes the read/write logic for `state.workflowState`. Used
|
|
5
|
+
* by `draft-first-workflow.md`, `work-through-issues.md`, and
|
|
6
|
+
* `pre-commit-review.md` to record pause points and offer
|
|
7
|
+
* Resume / Restart / Discard at the next `/oss` invocation.
|
|
8
|
+
*
|
|
9
|
+
* Pure functions — callers manage state I/O. Same architectural
|
|
10
|
+
* shape as the recent #1277 (follow-up-history) helpers.
|
|
11
|
+
*/
|
|
12
|
+
import type { AgentState, WorkflowState } from './state-schema.js';
|
|
13
|
+
export type WorkflowName = WorkflowState['workflowName'];
|
|
14
|
+
/**
|
|
15
|
+
* Snapshot the user's current workflow position. Returns the
|
|
16
|
+
* patched state; callers persist via `StateManager.save()` (or the
|
|
17
|
+
* gist `checkpoint()` path).
|
|
18
|
+
*/
|
|
19
|
+
export declare function recordWorkflowPause(state: AgentState, next: Omit<WorkflowState, 'lastUpdatedAt'>, now?: Date): AgentState;
|
|
20
|
+
/**
|
|
21
|
+
* Read the current workflow snapshot, or null when no pause is
|
|
22
|
+
* recorded.
|
|
23
|
+
*/
|
|
24
|
+
export declare function getWorkflowState(state: AgentState): WorkflowState | null;
|
|
25
|
+
/**
|
|
26
|
+
* Discard the current pause snapshot. Used by:
|
|
27
|
+
* - the workflow completing normally,
|
|
28
|
+
* - the user picking "Discard and start fresh" at the resume
|
|
29
|
+
* prompt,
|
|
30
|
+
* - any consumer that detects the snapshot has gone stale (branch
|
|
31
|
+
* deleted, repo no longer reachable).
|
|
32
|
+
*/
|
|
33
|
+
export declare function clearWorkflowState(state: AgentState): AgentState;
|
|
34
|
+
/**
|
|
35
|
+
* Append a step name to `completedSteps` and update `currentStep`
|
|
36
|
+
* to the supplied next step. Convenience wrapper around the immer-
|
|
37
|
+
* style read/replace pattern. Returns the patched state.
|
|
38
|
+
*/
|
|
39
|
+
export declare function advanceWorkflowStep(state: AgentState, nextStep: string, now?: Date): AgentState;
|
|
40
|
+
/**
|
|
41
|
+
* Merge per-step data into `stepData` without overwriting other
|
|
42
|
+
* keys. Useful when a workflow's resume needs to restore non-trivial
|
|
43
|
+
* context (skipped compliance items, last review pass count, etc.).
|
|
44
|
+
*/
|
|
45
|
+
export declare function setStepData(state: AgentState, step: string, data: unknown, now?: Date): AgentState;
|
|
46
|
+
/**
|
|
47
|
+
* Read step data for a specific step, or undefined when none was
|
|
48
|
+
* stored. Callers typically narrow the unknown via a type guard.
|
|
49
|
+
*/
|
|
50
|
+
export declare function getStepData(state: AgentState, step: string): unknown;
|
|
51
|
+
/**
|
|
52
|
+
* Whether the recorded pause is on the supplied workflow. Useful
|
|
53
|
+
* for the router to decide whether to offer Resume vs ignore the
|
|
54
|
+
* snapshot when the current `/oss` invocation is unrelated.
|
|
55
|
+
*/
|
|
56
|
+
export declare function isPausedIn(state: AgentState, workflowName: WorkflowName): boolean;
|