@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.
- package/dist/cli-registry.js +50 -0
- package/dist/cli.bundle.cjs +81 -78
- package/dist/commands/compliance-score.d.ts +21 -0
- package/dist/commands/compliance-score.js +156 -0
- package/dist/commands/index.d.ts +4 -0
- package/dist/commands/index.js +4 -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 +18 -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/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/placeholder-usernames.js +7 -0
- 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 +76 -0
- package/dist/core/state-schema.js +75 -0
- package/dist/core/strategy.d.ts +75 -0
- package/dist/core/strategy.js +226 -0
- package/dist/core/types.d.ts +2 -0
- package/dist/core/workflow-state.d.ts +56 -0
- package/dist/core/workflow-state.js +101 -0
- package/dist/formatters/json.d.ts +147 -0
- package/dist/formatters/json.js +79 -0
- 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}
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
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}
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
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
|
-
//
|
|
390
|
-
|
|
391
|
-
|
|
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;
|