@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
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-repo extraction category configuration (#1284).
|
|
3
|
+
*
|
|
4
|
+
* The `extract-learnings` MCP prompt produces a structured markdown
|
|
5
|
+
* document organized into category sections. The default category
|
|
6
|
+
* set is sensible for typical web/library OSS work; specialized
|
|
7
|
+
* repos (security-focused, performance-critical, accessibility-
|
|
8
|
+
* forward) benefit from a tailored taxonomy.
|
|
9
|
+
*
|
|
10
|
+
* This module is the single source of truth for:
|
|
11
|
+
* - The default category list.
|
|
12
|
+
* - Validation of custom category lists (non-empty, no duplicates,
|
|
13
|
+
* reasonable string lengths).
|
|
14
|
+
* - Resolution: given a repo's optional override, produce the list
|
|
15
|
+
* of categories the prompt and the storage layer should use.
|
|
16
|
+
*
|
|
17
|
+
* Pure typed helper — no I/O. Same architectural shape as the recent
|
|
18
|
+
* #1252 / #1264 / #1286 / #1279 / #1277 extractions.
|
|
19
|
+
*
|
|
20
|
+
* Out of scope (deferred per #1284):
|
|
21
|
+
* - Wiring `categories` into the `guidelines store` / `guidelines view`
|
|
22
|
+
* shape so the override persists alongside the markdown.
|
|
23
|
+
* - Updating the `extract-learnings` MCP prompt to consume the
|
|
24
|
+
* resolved category list at run time.
|
|
25
|
+
* - Detecting repo type from signals (SECURITY.md presence, repo
|
|
26
|
+
* topics, etc.) to suggest categories.
|
|
27
|
+
*/
|
|
28
|
+
/**
|
|
29
|
+
* The default category list used by `extract-learnings` when no
|
|
30
|
+
* per-repo override is configured. Order matters: the prompt
|
|
31
|
+
* renders sections in this order, so `Code Style` first /
|
|
32
|
+
* `Other` last is the established convention.
|
|
33
|
+
*/
|
|
34
|
+
export const DEFAULT_EXTRACTION_CATEGORIES = [
|
|
35
|
+
'Code Style',
|
|
36
|
+
'Process',
|
|
37
|
+
'Architecture',
|
|
38
|
+
'Testing',
|
|
39
|
+
'Other',
|
|
40
|
+
];
|
|
41
|
+
/**
|
|
42
|
+
* Reasonable bounds on an override list. The prompt produces output
|
|
43
|
+
* organized into one heading per category; very long lists become
|
|
44
|
+
* unreadable, very long category names blow out heading width.
|
|
45
|
+
*/
|
|
46
|
+
const CATEGORY_NAME_MAX_LENGTH = 40;
|
|
47
|
+
const MAX_CATEGORIES = 12;
|
|
48
|
+
/**
|
|
49
|
+
* Validate a user-supplied category list. Returns a structured
|
|
50
|
+
* result rather than throwing — the CLI / slash-command callers
|
|
51
|
+
* surface error strings inline.
|
|
52
|
+
*/
|
|
53
|
+
export function validateCategories(input) {
|
|
54
|
+
const errors = [];
|
|
55
|
+
if (input.length === 0) {
|
|
56
|
+
errors.push('At least one category is required.');
|
|
57
|
+
return { ok: false, errors };
|
|
58
|
+
}
|
|
59
|
+
if (input.length > MAX_CATEGORIES) {
|
|
60
|
+
errors.push(`At most ${MAX_CATEGORIES} categories are supported (received ${input.length}).`);
|
|
61
|
+
}
|
|
62
|
+
const seen = new Set();
|
|
63
|
+
const cleaned = [];
|
|
64
|
+
for (const raw of input) {
|
|
65
|
+
const trimmed = raw.trim();
|
|
66
|
+
if (trimmed.length === 0) {
|
|
67
|
+
errors.push('Category names cannot be empty or whitespace-only.');
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
if (trimmed.length > CATEGORY_NAME_MAX_LENGTH) {
|
|
71
|
+
errors.push(`Category "${trimmed.slice(0, 30)}…" is longer than ${CATEGORY_NAME_MAX_LENGTH} characters.`);
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
const lowerKey = trimmed.toLowerCase();
|
|
75
|
+
if (seen.has(lowerKey)) {
|
|
76
|
+
errors.push(`Duplicate category "${trimmed}" (case-insensitive).`);
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
seen.add(lowerKey);
|
|
80
|
+
cleaned.push(trimmed);
|
|
81
|
+
}
|
|
82
|
+
if (errors.length > 0) {
|
|
83
|
+
return { ok: false, errors };
|
|
84
|
+
}
|
|
85
|
+
// Always ensure an "Other" bucket exists at the end so the prompt
|
|
86
|
+
// has a place to put feedback that doesn't fit the user's chosen
|
|
87
|
+
// categories. Append silently if missing rather than treating the
|
|
88
|
+
// omission as an error — most users won't think to add it.
|
|
89
|
+
if (!seen.has('other')) {
|
|
90
|
+
cleaned.push('Other');
|
|
91
|
+
}
|
|
92
|
+
return { ok: true, errors: [], normalized: cleaned };
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Resolve the categories the prompt + storage layer should use for a
|
|
96
|
+
* specific extraction. Falls back to the default list when no
|
|
97
|
+
* override is supplied or when the override fails validation.
|
|
98
|
+
*/
|
|
99
|
+
export function resolveCategories(override) {
|
|
100
|
+
if (!override || override.length === 0) {
|
|
101
|
+
return DEFAULT_EXTRACTION_CATEGORIES;
|
|
102
|
+
}
|
|
103
|
+
const result = validateCategories(override);
|
|
104
|
+
if (!result.ok || !result.normalized) {
|
|
105
|
+
return DEFAULT_EXTRACTION_CATEGORIES;
|
|
106
|
+
}
|
|
107
|
+
return result.normalized;
|
|
108
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
import { FetchedPR, DailyDigest, ClosedPR, MergedPR, StarFilter } from './types.js';
|
|
16
16
|
import { type PRCountsResult } from './github-stats.js';
|
|
17
17
|
export { computeDisplayLabel } from './display-utils.js';
|
|
18
|
-
export { classifyCICheck, classifyFailingChecks, getCIStatus } from './ci-analysis.js';
|
|
18
|
+
export { categorizeCIStatus, classifyCICheck, classifyFailingChecks, getCIStatus } from './ci-analysis.js';
|
|
19
19
|
export { isConditionalChecklistItem } from './checklist-analysis.js';
|
|
20
20
|
export { determineStatus } from './status-determination.js';
|
|
21
21
|
/**
|