standup-mcp 0.1.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/LICENSE +21 -0
- package/README.md +234 -0
- package/dist/analytics/blockers.js +122 -0
- package/dist/analytics/digest.js +35 -0
- package/dist/analytics/draft.js +129 -0
- package/dist/analytics/grouping.js +44 -0
- package/dist/analytics/weekly.js +42 -0
- package/dist/config.js +67 -0
- package/dist/demo.js +93 -0
- package/dist/format.js +66 -0
- package/dist/index.js +63 -0
- package/dist/normalize.js +101 -0
- package/dist/provider.js +76 -0
- package/dist/sources/github.js +192 -0
- package/dist/sources/jira.js +192 -0
- package/dist/sources/linear.js +176 -0
- package/dist/sources/mock.js +257 -0
- package/dist/sources/slack.js +185 -0
- package/dist/tools/blockers.js +20 -0
- package/dist/tools/digest.js +16 -0
- package/dist/tools/registry.js +13 -0
- package/dist/tools/sources.js +29 -0
- package/dist/tools/standup.js +26 -0
- package/dist/tools/util.js +35 -0
- package/dist/tools/weekly.js +25 -0
- package/dist/types.js +10 -0
- package/dist/window.js +62 -0
- package/package.json +61 -0
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Live Jira Cloud source. Implements ActivitySource against the REST Platform
|
|
3
|
+
* API v3 with basic auth (email + API token). Read-only.
|
|
4
|
+
*
|
|
5
|
+
* Activity comes from per-issue changelogs filtered to status transitions the
|
|
6
|
+
* authenticated user made; open items come from the user's in-progress issues.
|
|
7
|
+
* Search uses the enhanced JQL endpoint (POST /rest/api/3/search/jql), not the
|
|
8
|
+
* deprecated GET /rest/api/3/search.
|
|
9
|
+
*/
|
|
10
|
+
import { truncate } from "../normalize.js";
|
|
11
|
+
const REQUEST_TIMEOUT_MS = 30_000;
|
|
12
|
+
const TWO_DAYS_MS = 2 * 24 * 60 * 60 * 1000;
|
|
13
|
+
const CHANGELOG_ISSUE_CAP = 20;
|
|
14
|
+
export class JiraSource {
|
|
15
|
+
creds;
|
|
16
|
+
id = "jira";
|
|
17
|
+
isDemo = false;
|
|
18
|
+
authHeader;
|
|
19
|
+
constructor(creds) {
|
|
20
|
+
this.creds = creds;
|
|
21
|
+
const token = Buffer.from(`${creds.email}:${creds.apiToken}`).toString("base64");
|
|
22
|
+
this.authHeader = `Basic ${token}`;
|
|
23
|
+
}
|
|
24
|
+
// --- HTTP plumbing -------------------------------------------------------
|
|
25
|
+
async request(path, init) {
|
|
26
|
+
const url = path.startsWith("http") ? path : `${this.creds.baseUrl}${path}`;
|
|
27
|
+
const controller = new AbortController();
|
|
28
|
+
const timer = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
|
|
29
|
+
try {
|
|
30
|
+
const headers = {
|
|
31
|
+
Authorization: this.authHeader,
|
|
32
|
+
Accept: "application/json",
|
|
33
|
+
};
|
|
34
|
+
if (init?.body !== undefined)
|
|
35
|
+
headers["Content-Type"] = "application/json";
|
|
36
|
+
const res = await fetch(url, {
|
|
37
|
+
method: init?.method ?? "GET",
|
|
38
|
+
headers,
|
|
39
|
+
body: init?.body !== undefined ? JSON.stringify(init.body) : undefined,
|
|
40
|
+
signal: controller.signal,
|
|
41
|
+
});
|
|
42
|
+
if (!res.ok) {
|
|
43
|
+
const body = await res.text().catch(() => "");
|
|
44
|
+
throw new Error(`Jira API ${res.status} ${res.statusText} for ${path}` +
|
|
45
|
+
(body ? `: ${body.slice(0, 300)}` : ""));
|
|
46
|
+
}
|
|
47
|
+
return (await res.json());
|
|
48
|
+
}
|
|
49
|
+
catch (err) {
|
|
50
|
+
if (err instanceof Error && err.name === "AbortError") {
|
|
51
|
+
throw new Error(`Jira API request timed out after 30s: ${path}`);
|
|
52
|
+
}
|
|
53
|
+
throw err;
|
|
54
|
+
}
|
|
55
|
+
finally {
|
|
56
|
+
clearTimeout(timer);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
// --- ActivitySource surface ----------------------------------------------
|
|
60
|
+
async resolveIdentity() {
|
|
61
|
+
const me = await this.request("/rest/api/3/myself");
|
|
62
|
+
return me.accountId;
|
|
63
|
+
}
|
|
64
|
+
async checkConnection() {
|
|
65
|
+
try {
|
|
66
|
+
const me = await this.request("/rest/api/3/myself");
|
|
67
|
+
const name = me.displayName ?? "unknown";
|
|
68
|
+
return {
|
|
69
|
+
source: "jira",
|
|
70
|
+
ok: true,
|
|
71
|
+
identity: name,
|
|
72
|
+
detail: `authenticated as ${name}`,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
catch (err) {
|
|
76
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
77
|
+
return { source: "jira", ok: false, detail: message };
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
async fetchActivity(window, _actor) {
|
|
81
|
+
const events = [];
|
|
82
|
+
let myAccountId;
|
|
83
|
+
try {
|
|
84
|
+
myAccountId = await this.resolveIdentity();
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
return events;
|
|
88
|
+
}
|
|
89
|
+
if (!myAccountId)
|
|
90
|
+
return events;
|
|
91
|
+
const dateOnly = window.since.slice(0, 10);
|
|
92
|
+
const jql = `updated >= "${dateOnly}" ` +
|
|
93
|
+
"AND (assignee = currentUser() OR reporter = currentUser()) " +
|
|
94
|
+
"ORDER BY updated DESC";
|
|
95
|
+
let issues;
|
|
96
|
+
try {
|
|
97
|
+
const data = await this.request("/rest/api/3/search/jql", {
|
|
98
|
+
method: "POST",
|
|
99
|
+
body: { jql, fields: ["summary", "status"], maxResults: 50 },
|
|
100
|
+
});
|
|
101
|
+
issues = data.issues ?? [];
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
return events;
|
|
105
|
+
}
|
|
106
|
+
const sinceMs = Date.parse(window.since);
|
|
107
|
+
// Cap changelog fetches to bound the number of calls.
|
|
108
|
+
for (const issue of issues.slice(0, CHANGELOG_ISSUE_CAP)) {
|
|
109
|
+
let entries;
|
|
110
|
+
try {
|
|
111
|
+
const cl = await this.request(`/rest/api/3/issue/${issue.key}/changelog?maxResults=100`);
|
|
112
|
+
entries = cl.values ?? [];
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
continue; // skip this issue on failure, keep what we have
|
|
116
|
+
}
|
|
117
|
+
for (const entry of entries) {
|
|
118
|
+
if (entry.author?.accountId !== myAccountId)
|
|
119
|
+
continue;
|
|
120
|
+
if (Date.parse(entry.created) < sinceMs)
|
|
121
|
+
continue;
|
|
122
|
+
const author = entry.author.displayName ?? "unknown";
|
|
123
|
+
for (const item of entry.items ?? []) {
|
|
124
|
+
if (item.field !== "status")
|
|
125
|
+
continue;
|
|
126
|
+
const fromStatus = item.fromString ?? undefined;
|
|
127
|
+
const toStatus = item.toString ?? undefined;
|
|
128
|
+
const summary = issue.fields.summary ?? issue.key;
|
|
129
|
+
events.push({
|
|
130
|
+
source: "jira",
|
|
131
|
+
kind: "issue_moved",
|
|
132
|
+
timestamp: entry.created,
|
|
133
|
+
actor: author,
|
|
134
|
+
title: `${issue.key} moved ${fromStatus ?? "?"} to ` +
|
|
135
|
+
`${toStatus ?? "?"}`,
|
|
136
|
+
workItem: issue.key,
|
|
137
|
+
workItemTitle: truncate(summary),
|
|
138
|
+
fromStatus,
|
|
139
|
+
toStatus,
|
|
140
|
+
signals: [],
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return events;
|
|
146
|
+
}
|
|
147
|
+
async fetchOpenItems(actor) {
|
|
148
|
+
const events = [];
|
|
149
|
+
let issues;
|
|
150
|
+
try {
|
|
151
|
+
const data = await this.request("/rest/api/3/search/jql", {
|
|
152
|
+
method: "POST",
|
|
153
|
+
body: {
|
|
154
|
+
jql: 'assignee = currentUser() AND statusCategory = "In Progress" ' +
|
|
155
|
+
"ORDER BY updated DESC",
|
|
156
|
+
fields: ["summary", "status", "updated", "labels"],
|
|
157
|
+
maxResults: 30,
|
|
158
|
+
},
|
|
159
|
+
});
|
|
160
|
+
issues = data.issues ?? [];
|
|
161
|
+
}
|
|
162
|
+
catch {
|
|
163
|
+
return events;
|
|
164
|
+
}
|
|
165
|
+
for (const issue of issues) {
|
|
166
|
+
const fields = issue.fields;
|
|
167
|
+
const summary = fields.summary ?? issue.key;
|
|
168
|
+
const status = fields.status?.name ?? "In Progress";
|
|
169
|
+
const updated = fields.updated ?? new Date().toISOString();
|
|
170
|
+
const signals = [];
|
|
171
|
+
const labels = fields.labels ?? [];
|
|
172
|
+
if (labels.some((label) => /block|impediment/i.test(label))) {
|
|
173
|
+
signals.push("blocked");
|
|
174
|
+
}
|
|
175
|
+
else if (Date.now() - Date.parse(updated) >= TWO_DAYS_MS) {
|
|
176
|
+
signals.push("stale");
|
|
177
|
+
}
|
|
178
|
+
events.push({
|
|
179
|
+
source: "jira",
|
|
180
|
+
kind: "issue_moved",
|
|
181
|
+
timestamp: updated,
|
|
182
|
+
actor: actor.displayName,
|
|
183
|
+
title: `${issue.key} ${truncate(summary)} (${status})`,
|
|
184
|
+
workItem: issue.key,
|
|
185
|
+
workItemTitle: truncate(summary),
|
|
186
|
+
toStatus: status,
|
|
187
|
+
signals,
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
return events;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Live Linear client. Implements ActivitySource against the Linear GraphQL API
|
|
3
|
+
* with a personal API key. Read-only: every call is a query, never a mutation.
|
|
4
|
+
*
|
|
5
|
+
* Auth note: Linear personal API keys go in the Authorization header RAW, with
|
|
6
|
+
* no "Bearer " prefix. Errors arrive either as a non-2xx HTTP status or as a
|
|
7
|
+
* populated `errors` array on a 200 body, so both paths throw.
|
|
8
|
+
*/
|
|
9
|
+
import { detectTextSignals, truncate } from "../normalize.js";
|
|
10
|
+
const ENDPOINT = "https://api.linear.app/graphql";
|
|
11
|
+
const REQUEST_TIMEOUT_MS = 30_000;
|
|
12
|
+
const STALE_AFTER_MS = 2 * 24 * 60 * 60 * 1000; // 2 days
|
|
13
|
+
const VIEWER_QUERY = `{ viewer { id name email } }`;
|
|
14
|
+
const ACTIVITY_QUERY = `query($since: DateTimeOrDuration!) { issues(filter: { assignee: { isMe: { eq: true } }, updatedAt: { gte: $since } }, first: 50) { nodes { identifier title updatedAt state { name } history(first: 20) { nodes { createdAt fromState { name } toState { name } } } comments(first: 10) { nodes { createdAt body user { isMe } } } } } }`;
|
|
15
|
+
const OPEN_ITEMS_QUERY = `{ issues(filter: { assignee: { isMe: { eq: true } }, state: { type: { eq: "started" } } }, first: 50) { nodes { identifier title updatedAt state { name } labels { nodes { name } } } } }`;
|
|
16
|
+
export class LinearSource {
|
|
17
|
+
creds;
|
|
18
|
+
id = "linear";
|
|
19
|
+
isDemo = false;
|
|
20
|
+
constructor(creds) {
|
|
21
|
+
this.creds = creds;
|
|
22
|
+
}
|
|
23
|
+
// --- HTTP plumbing -------------------------------------------------------
|
|
24
|
+
async query(query, variables) {
|
|
25
|
+
const controller = new AbortController();
|
|
26
|
+
const timer = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
|
|
27
|
+
try {
|
|
28
|
+
const res = await fetch(ENDPOINT, {
|
|
29
|
+
method: "POST",
|
|
30
|
+
headers: {
|
|
31
|
+
// Personal API keys are sent raw, NOT as "Bearer ...".
|
|
32
|
+
Authorization: this.creds.apiKey,
|
|
33
|
+
"Content-Type": "application/json",
|
|
34
|
+
},
|
|
35
|
+
body: JSON.stringify({ query, variables }),
|
|
36
|
+
signal: controller.signal,
|
|
37
|
+
});
|
|
38
|
+
if (!res.ok) {
|
|
39
|
+
const body = await res.text().catch(() => "");
|
|
40
|
+
throw new Error(`Linear API ${res.status} ${res.statusText}` +
|
|
41
|
+
(body ? `: ${body.slice(0, 300)}` : ""));
|
|
42
|
+
}
|
|
43
|
+
const json = (await res.json());
|
|
44
|
+
if (json.errors?.length) {
|
|
45
|
+
throw new Error(`Linear API error: ${json.errors.map((e) => e.message).join("; ")}`);
|
|
46
|
+
}
|
|
47
|
+
return json.data;
|
|
48
|
+
}
|
|
49
|
+
catch (err) {
|
|
50
|
+
if (err instanceof Error && err.name === "AbortError") {
|
|
51
|
+
throw new Error("Linear API request timed out after 30s");
|
|
52
|
+
}
|
|
53
|
+
throw err;
|
|
54
|
+
}
|
|
55
|
+
finally {
|
|
56
|
+
clearTimeout(timer);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
// --- Activity source surface ---------------------------------------------
|
|
60
|
+
async resolveIdentity() {
|
|
61
|
+
try {
|
|
62
|
+
const data = await this.query(VIEWER_QUERY);
|
|
63
|
+
return data.viewer.id;
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
// Identity resolution is best-effort; a failure should not abort actor
|
|
67
|
+
// matching across the other sources.
|
|
68
|
+
return undefined;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
async checkConnection() {
|
|
72
|
+
try {
|
|
73
|
+
const data = await this.query(VIEWER_QUERY);
|
|
74
|
+
const name = data.viewer.name;
|
|
75
|
+
return {
|
|
76
|
+
source: "linear",
|
|
77
|
+
ok: true,
|
|
78
|
+
identity: name,
|
|
79
|
+
detail: `authenticated as ${name}`,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
catch (err) {
|
|
83
|
+
return {
|
|
84
|
+
source: "linear",
|
|
85
|
+
ok: false,
|
|
86
|
+
detail: err instanceof Error ? err.message : String(err),
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
async fetchActivity(window, actor) {
|
|
91
|
+
try {
|
|
92
|
+
const data = await this.query(ACTIVITY_QUERY, { since: window.since });
|
|
93
|
+
const sinceMs = Date.parse(window.since);
|
|
94
|
+
const events = [];
|
|
95
|
+
for (const issue of data.issues.nodes) {
|
|
96
|
+
// State transitions within the window. The first history entry of an
|
|
97
|
+
// issue has no fromState, so guard the title on it.
|
|
98
|
+
for (const h of issue.history.nodes) {
|
|
99
|
+
if (Date.parse(h.createdAt) < sinceMs || !h.toState)
|
|
100
|
+
continue;
|
|
101
|
+
const from = h.fromState?.name;
|
|
102
|
+
const to = h.toState.name;
|
|
103
|
+
events.push({
|
|
104
|
+
source: "linear",
|
|
105
|
+
kind: "issue_moved",
|
|
106
|
+
timestamp: h.createdAt,
|
|
107
|
+
actor: actor.displayName,
|
|
108
|
+
title: from
|
|
109
|
+
? `${issue.identifier} moved ${from} → ${to}`
|
|
110
|
+
: `${issue.identifier} moved to ${to}`,
|
|
111
|
+
workItem: issue.identifier,
|
|
112
|
+
workItemTitle: issue.title,
|
|
113
|
+
fromStatus: from,
|
|
114
|
+
toStatus: to,
|
|
115
|
+
signals: [],
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
// Comments the user authored within the window.
|
|
119
|
+
for (const c of issue.comments.nodes) {
|
|
120
|
+
// The spec compares Date.parse(createdAt) to the raw ISO string,
|
|
121
|
+
// which is always false (number vs string). Resolved toward the
|
|
122
|
+
// history rule above: parse both sides.
|
|
123
|
+
if (c.user?.isMe !== true || Date.parse(c.createdAt) < sinceMs) {
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
events.push({
|
|
127
|
+
source: "linear",
|
|
128
|
+
kind: "issue_commented",
|
|
129
|
+
timestamp: c.createdAt,
|
|
130
|
+
actor: actor.displayName,
|
|
131
|
+
title: `${issue.identifier} comment`,
|
|
132
|
+
body: truncate(c.body, 200),
|
|
133
|
+
workItem: issue.identifier,
|
|
134
|
+
workItemTitle: issue.title,
|
|
135
|
+
signals: detectTextSignals(c.body),
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return events;
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
// A GraphQL/transport failure for one source must not sink the standup;
|
|
143
|
+
// the aggregate provider tolerates an empty slice.
|
|
144
|
+
return [];
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
async fetchOpenItems(actor) {
|
|
148
|
+
try {
|
|
149
|
+
const data = await this.query(OPEN_ITEMS_QUERY);
|
|
150
|
+
const now = Date.now();
|
|
151
|
+
return data.issues.nodes.map((issue) => {
|
|
152
|
+
const stateName = issue.state?.name;
|
|
153
|
+
const blocked = issue.labels.nodes.some((l) => /block/i.test(l.name));
|
|
154
|
+
const signals = blocked
|
|
155
|
+
? ["blocked"]
|
|
156
|
+
: now - Date.parse(issue.updatedAt) >= STALE_AFTER_MS
|
|
157
|
+
? ["stale"]
|
|
158
|
+
: [];
|
|
159
|
+
return {
|
|
160
|
+
source: "linear",
|
|
161
|
+
kind: "issue_moved",
|
|
162
|
+
timestamp: issue.updatedAt,
|
|
163
|
+
actor: actor.displayName,
|
|
164
|
+
title: `${issue.identifier} ${issue.title} (${stateName ?? "In Progress"})`,
|
|
165
|
+
workItem: issue.identifier,
|
|
166
|
+
workItemTitle: issue.title,
|
|
167
|
+
toStatus: stateName,
|
|
168
|
+
signals,
|
|
169
|
+
};
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
catch {
|
|
173
|
+
return [];
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Demo provider: a synthetic but realistic day of one developer's activity
|
|
3
|
+
* across all four sources. This is what makes `npx standup-mcp --demo` produce
|
|
4
|
+
* a believable standup with zero setup, and it is the share-worthy demo.
|
|
5
|
+
*
|
|
6
|
+
* The story: Jordan Lee, a fintech engineer. Yesterday they pushed a biometric
|
|
7
|
+
* re-auth feature (PROJ-412), shipped a card-dispute fix (PROJ-407), advanced a
|
|
8
|
+
* Linear rate-limit task (ENG-88), reviewed a teammate's PR, and said in Slack
|
|
9
|
+
* they are blocked on vendor sandbox creds. The engine should surface that
|
|
10
|
+
* blocker on its own, group GitHub + Jira + Linear + Slack by work item, and
|
|
11
|
+
* propose a sensible "Today".
|
|
12
|
+
*/
|
|
13
|
+
import { ALL_SOURCES } from "../types.js";
|
|
14
|
+
const ACTOR = {
|
|
15
|
+
displayName: "Jordan Lee",
|
|
16
|
+
github: "jordanlee",
|
|
17
|
+
jira: "jordan@acmebank.dev",
|
|
18
|
+
linear: "jordan",
|
|
19
|
+
slack: "U07JORDAN",
|
|
20
|
+
};
|
|
21
|
+
export class MockProvider {
|
|
22
|
+
isDemo = true;
|
|
23
|
+
sources = ALL_SOURCES;
|
|
24
|
+
now;
|
|
25
|
+
constructor(now = Date.now()) {
|
|
26
|
+
this.now = now;
|
|
27
|
+
}
|
|
28
|
+
/** ISO timestamp `hours` ago. */
|
|
29
|
+
t(hours) {
|
|
30
|
+
return new Date(this.now - hours * 3_600_000).toISOString();
|
|
31
|
+
}
|
|
32
|
+
async resolveActor() {
|
|
33
|
+
return ACTOR;
|
|
34
|
+
}
|
|
35
|
+
async fetchActivity() {
|
|
36
|
+
return this.events();
|
|
37
|
+
}
|
|
38
|
+
async fetchOpenItems() {
|
|
39
|
+
return this.openItems();
|
|
40
|
+
}
|
|
41
|
+
async checkConnections() {
|
|
42
|
+
return ALL_SOURCES.map((source) => ({
|
|
43
|
+
source,
|
|
44
|
+
ok: true,
|
|
45
|
+
identity: ACTOR[source] ?? "demo",
|
|
46
|
+
detail: "demo data (no real connection)",
|
|
47
|
+
}));
|
|
48
|
+
}
|
|
49
|
+
events() {
|
|
50
|
+
const PROJ412 = "Biometric re-auth";
|
|
51
|
+
const PROJ407 = "Card dispute webhook";
|
|
52
|
+
const ENG88 = "Rate-limit the export endpoint";
|
|
53
|
+
return [
|
|
54
|
+
// --- PROJ-412: GitHub commits + PR, Jira move, Slack blocker ---------
|
|
55
|
+
{
|
|
56
|
+
source: "github",
|
|
57
|
+
kind: "commit",
|
|
58
|
+
timestamp: this.t(28),
|
|
59
|
+
actor: ACTOR.github,
|
|
60
|
+
title: "scaffold biometric prompt component",
|
|
61
|
+
workItem: "PROJ-412",
|
|
62
|
+
workItemTitle: PROJ412,
|
|
63
|
+
signals: [],
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
source: "github",
|
|
67
|
+
kind: "commit",
|
|
68
|
+
timestamp: this.t(26),
|
|
69
|
+
actor: ACTOR.github,
|
|
70
|
+
title: "wire WebAuthn challenge endpoint",
|
|
71
|
+
workItem: "PROJ-412",
|
|
72
|
+
workItemTitle: PROJ412,
|
|
73
|
+
signals: [],
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
source: "github",
|
|
77
|
+
kind: "commit",
|
|
78
|
+
timestamp: this.t(24),
|
|
79
|
+
actor: ACTOR.github,
|
|
80
|
+
title: "handle expired challenge edge case",
|
|
81
|
+
workItem: "PROJ-412",
|
|
82
|
+
workItemTitle: PROJ412,
|
|
83
|
+
signals: [],
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
source: "github",
|
|
87
|
+
kind: "pr_opened",
|
|
88
|
+
timestamp: this.t(23),
|
|
89
|
+
actor: ACTOR.github,
|
|
90
|
+
title: "Opened PR #128: Add biometric re-auth",
|
|
91
|
+
workItem: "PROJ-412",
|
|
92
|
+
workItemTitle: PROJ412,
|
|
93
|
+
url: "https://github.com/acmebank/app/pull/128",
|
|
94
|
+
signals: [],
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
source: "jira",
|
|
98
|
+
kind: "issue_moved",
|
|
99
|
+
timestamp: this.t(27),
|
|
100
|
+
actor: ACTOR.displayName,
|
|
101
|
+
title: "PROJ-412 moved To Do → In Progress",
|
|
102
|
+
workItem: "PROJ-412",
|
|
103
|
+
workItemTitle: PROJ412,
|
|
104
|
+
fromStatus: "To Do",
|
|
105
|
+
toStatus: "In Progress",
|
|
106
|
+
signals: [],
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
source: "slack",
|
|
110
|
+
kind: "message",
|
|
111
|
+
timestamp: this.t(17),
|
|
112
|
+
actor: ACTOR.displayName,
|
|
113
|
+
title: "#payments: blocked on vendor sandbox creds for PROJ-412",
|
|
114
|
+
body: "Blocked on the vendor sandbox creds for PROJ-412. Waiting on infra to provision them before I can test the re-auth flow end to end.",
|
|
115
|
+
workItem: "PROJ-412",
|
|
116
|
+
signals: ["blocked"],
|
|
117
|
+
},
|
|
118
|
+
// --- PROJ-407: merged PR + Jira move to In Review --------------------
|
|
119
|
+
{
|
|
120
|
+
source: "github",
|
|
121
|
+
kind: "pr_merged",
|
|
122
|
+
timestamp: this.t(30),
|
|
123
|
+
actor: ACTOR.github,
|
|
124
|
+
title: "Merged PR #125: Card dispute webhook retry/backoff",
|
|
125
|
+
workItem: "PROJ-407",
|
|
126
|
+
workItemTitle: PROJ407,
|
|
127
|
+
url: "https://github.com/acmebank/app/pull/125",
|
|
128
|
+
signals: [],
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
source: "jira",
|
|
132
|
+
kind: "issue_moved",
|
|
133
|
+
timestamp: this.t(29),
|
|
134
|
+
actor: ACTOR.displayName,
|
|
135
|
+
title: "PROJ-407 moved In Progress → In Review",
|
|
136
|
+
workItem: "PROJ-407",
|
|
137
|
+
workItemTitle: PROJ407,
|
|
138
|
+
fromStatus: "In Progress",
|
|
139
|
+
toStatus: "In Review",
|
|
140
|
+
signals: [],
|
|
141
|
+
},
|
|
142
|
+
// --- ENG-88 (Linear): state change, commits, comment -----------------
|
|
143
|
+
{
|
|
144
|
+
source: "linear",
|
|
145
|
+
kind: "issue_moved",
|
|
146
|
+
timestamp: this.t(26),
|
|
147
|
+
actor: ACTOR.displayName,
|
|
148
|
+
title: "ENG-88 moved Backlog → In Progress",
|
|
149
|
+
workItem: "ENG-88",
|
|
150
|
+
workItemTitle: ENG88,
|
|
151
|
+
fromStatus: "Backlog",
|
|
152
|
+
toStatus: "In Progress",
|
|
153
|
+
signals: [],
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
source: "github",
|
|
157
|
+
kind: "commit",
|
|
158
|
+
timestamp: this.t(22),
|
|
159
|
+
actor: ACTOR.github,
|
|
160
|
+
title: "add token-bucket limiter for /export",
|
|
161
|
+
workItem: "ENG-88",
|
|
162
|
+
workItemTitle: ENG88,
|
|
163
|
+
signals: [],
|
|
164
|
+
},
|
|
165
|
+
{
|
|
166
|
+
source: "github",
|
|
167
|
+
kind: "commit",
|
|
168
|
+
timestamp: this.t(21),
|
|
169
|
+
actor: ACTOR.github,
|
|
170
|
+
title: "tests for limiter window",
|
|
171
|
+
workItem: "ENG-88",
|
|
172
|
+
workItemTitle: ENG88,
|
|
173
|
+
signals: [],
|
|
174
|
+
},
|
|
175
|
+
{
|
|
176
|
+
source: "linear",
|
|
177
|
+
kind: "issue_commented",
|
|
178
|
+
timestamp: this.t(20),
|
|
179
|
+
actor: ACTOR.displayName,
|
|
180
|
+
title: "ENG-88 comment: documented the 429 retry-after header",
|
|
181
|
+
body: "Documented the 429 retry-after header behaviour in the spec.",
|
|
182
|
+
workItem: "ENG-88",
|
|
183
|
+
workItemTitle: ENG88,
|
|
184
|
+
signals: [],
|
|
185
|
+
},
|
|
186
|
+
{
|
|
187
|
+
source: "slack",
|
|
188
|
+
kind: "message",
|
|
189
|
+
timestamp: this.t(18),
|
|
190
|
+
actor: ACTOR.displayName,
|
|
191
|
+
title: "#eng: shared the limiter approach, ENG-88 ready for QA tomorrow",
|
|
192
|
+
body: "Shared the limiter approach in #eng. ENG-88 should be ready for QA tomorrow.",
|
|
193
|
+
workItem: "ENG-88",
|
|
194
|
+
signals: [],
|
|
195
|
+
},
|
|
196
|
+
// --- Loose review (no ticket) -> Other activity ----------------------
|
|
197
|
+
{
|
|
198
|
+
source: "github",
|
|
199
|
+
kind: "pr_reviewed",
|
|
200
|
+
timestamp: this.t(25),
|
|
201
|
+
actor: ACTOR.github,
|
|
202
|
+
title: "Reviewed PR #129: Tidy currency formatting helpers",
|
|
203
|
+
url: "https://github.com/acmebank/app/pull/129",
|
|
204
|
+
signals: [],
|
|
205
|
+
},
|
|
206
|
+
];
|
|
207
|
+
}
|
|
208
|
+
openItems() {
|
|
209
|
+
return [
|
|
210
|
+
// In-progress tickets -> "Continue ..."
|
|
211
|
+
{
|
|
212
|
+
source: "jira",
|
|
213
|
+
kind: "issue_moved",
|
|
214
|
+
timestamp: this.t(27),
|
|
215
|
+
actor: ACTOR.displayName,
|
|
216
|
+
title: "PROJ-412 Biometric re-auth (In Progress)",
|
|
217
|
+
workItem: "PROJ-412",
|
|
218
|
+
workItemTitle: "Biometric re-auth",
|
|
219
|
+
toStatus: "In Progress",
|
|
220
|
+
signals: [],
|
|
221
|
+
},
|
|
222
|
+
{
|
|
223
|
+
source: "linear",
|
|
224
|
+
kind: "issue_moved",
|
|
225
|
+
timestamp: this.t(26),
|
|
226
|
+
actor: ACTOR.displayName,
|
|
227
|
+
title: "ENG-88 Rate-limit the export endpoint (In Progress)",
|
|
228
|
+
workItem: "ENG-88",
|
|
229
|
+
workItemTitle: "Rate-limit the export endpoint",
|
|
230
|
+
toStatus: "In Progress",
|
|
231
|
+
signals: [],
|
|
232
|
+
},
|
|
233
|
+
// Your open PR awaiting review -> "Land ..." + a review-pending signal.
|
|
234
|
+
{
|
|
235
|
+
source: "github",
|
|
236
|
+
kind: "pr_opened",
|
|
237
|
+
timestamp: this.t(23),
|
|
238
|
+
actor: ACTOR.github,
|
|
239
|
+
title: "Opened PR #128: Add biometric re-auth",
|
|
240
|
+
workItem: "PROJ-412",
|
|
241
|
+
workItemTitle: "Biometric re-auth",
|
|
242
|
+
url: "https://github.com/acmebank/app/pull/128",
|
|
243
|
+
signals: ["review_pending"],
|
|
244
|
+
},
|
|
245
|
+
// A review requested of you -> "Review ..."
|
|
246
|
+
{
|
|
247
|
+
source: "github",
|
|
248
|
+
kind: "pr_review_requested",
|
|
249
|
+
timestamp: this.t(20),
|
|
250
|
+
actor: ACTOR.github,
|
|
251
|
+
title: "Review PR #130: Refactor request logging",
|
|
252
|
+
url: "https://github.com/acmebank/app/pull/130",
|
|
253
|
+
signals: [],
|
|
254
|
+
},
|
|
255
|
+
];
|
|
256
|
+
}
|
|
257
|
+
}
|