@tablation/crew 0.44.0 → 0.45.1
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.js
CHANGED
|
@@ -3234,8 +3234,6 @@ var DEFAULT_CONTRACT = {
|
|
|
3234
3234
|
parent: "ticket_id",
|
|
3235
3235
|
body: "body",
|
|
3236
3236
|
author: "team_member_id",
|
|
3237
|
-
kind: "kind",
|
|
3238
|
-
eventKind: "event",
|
|
3239
3237
|
createdAt: "created_at",
|
|
3240
3238
|
ship: "ship_id"
|
|
3241
3239
|
},
|
|
@@ -3345,6 +3343,7 @@ function validateContract(c) {
|
|
|
3345
3343
|
}
|
|
3346
3344
|
return problems;
|
|
3347
3345
|
}
|
|
3346
|
+
var EVENT_LOG_MISSING = "the Issues table has no event log \u2014 crew writes its audit trail there (CREW-1574). In the app, open the Issues table's Data & lifecycle tab and turn on Event log, then rerun.";
|
|
3348
3347
|
|
|
3349
3348
|
// src/shell.ts
|
|
3350
3349
|
import { spawn } from "child_process";
|
|
@@ -5209,18 +5208,65 @@ var Tracker = class {
|
|
|
5209
5208
|
await this.client.records.update(this.models.crew, memberId, patch2);
|
|
5210
5209
|
}
|
|
5211
5210
|
/**
|
|
5212
|
-
*
|
|
5213
|
-
*
|
|
5214
|
-
*
|
|
5211
|
+
* One entry in the runner's audit trail, written to the Issues table's
|
|
5212
|
+
* event log (CREW-1574) rather than to Comments, so it can never be mistaken
|
|
5213
|
+
* for something a seat or person said. `type` is a short snake_case name
|
|
5214
|
+
* (`pushed`, `claimed`, `bounced`, …); `body` is the human message and keeps
|
|
5215
|
+
* whatever wording the caller already used. Attributed to the seat's Crew
|
|
5216
|
+
* row, with the posting ship in `metadata`.
|
|
5215
5217
|
*/
|
|
5216
|
-
async postEvent(ticketId, body, memberId) {
|
|
5218
|
+
async postEvent(ticketId, body, memberId, type = "note") {
|
|
5217
5219
|
const auth = await this.authorshipProbe();
|
|
5218
|
-
await this.
|
|
5219
|
-
|
|
5220
|
-
body,
|
|
5221
|
-
|
|
5222
|
-
|
|
5223
|
-
|
|
5220
|
+
const res = await this.eventsFetch("POST", `/records/${ticketId}/events`, {
|
|
5221
|
+
type,
|
|
5222
|
+
message: body,
|
|
5223
|
+
actorRecordId: memberId,
|
|
5224
|
+
...auth.shipId ? { metadata: { ship_id: auth.shipId } } : {}
|
|
5225
|
+
});
|
|
5226
|
+
if (res.status === 404) throw new ConfigError(EVENT_LOG_MISSING);
|
|
5227
|
+
if (!res.ok) throw new Error(`posting a ${type} event failed: HTTP ${res.status}`);
|
|
5228
|
+
}
|
|
5229
|
+
/**
|
|
5230
|
+
* Whether the Issues table has an event log: `GET /data-models/<issues>/events`
|
|
5231
|
+
* answers 200 when it does and 404 when the setting is off. Crew requires it
|
|
5232
|
+
* (CREW-1574); `connect` and doctor report `false` by name.
|
|
5233
|
+
*/
|
|
5234
|
+
async hasEventLog() {
|
|
5235
|
+
const res = await this.eventsFetch("GET", "/events?limit=1");
|
|
5236
|
+
if (res.status === 404) return false;
|
|
5237
|
+
if (!res.ok) throw new Error(`reading the event log failed: HTTP ${res.status}`);
|
|
5238
|
+
return true;
|
|
5239
|
+
}
|
|
5240
|
+
/**
|
|
5241
|
+
* Events of `type` across the Issues table, newest first. The runner's own
|
|
5242
|
+
* dedupe/escalation markers (`stalled`, `gate_red`, `conflict_handed_back`,
|
|
5243
|
+
* `stranded_flagged`) live here now, so the readers that used to scan
|
|
5244
|
+
* Comments for them read this instead. A tracker without the log answers 404
|
|
5245
|
+
* and reads as no events: the writes fail by name (`postEvent`), and a
|
|
5246
|
+
* poll must not die on a read.
|
|
5247
|
+
*/
|
|
5248
|
+
async events(type, limit = 500) {
|
|
5249
|
+
const filters = encodeURIComponent(JSON.stringify([{ columnName: "event_type", operator: "EQ", value: type }]));
|
|
5250
|
+
const res = await this.eventsFetch("GET", `/events?limit=${limit}&filters=${filters}`);
|
|
5251
|
+
if (res.status === 404) return [];
|
|
5252
|
+
if (!res.ok) throw new Error(`reading ${type} events failed: HTTP ${res.status}`);
|
|
5253
|
+
const rows = await res.json();
|
|
5254
|
+
return rows.filter((e) => e.event_type === type).sort((a, b) => Date.parse(b.occurred_at ?? b.created_at ?? "") - Date.parse(a.occurred_at ?? a.created_at ?? ""));
|
|
5255
|
+
}
|
|
5256
|
+
/** The newest event of `type` on a ticket, or undefined — e.g. the `pushed` tip QA verifies against. */
|
|
5257
|
+
async latestEvent(ticketId, type) {
|
|
5258
|
+
return (await this.events(type)).find((e) => e.record_id === ticketId);
|
|
5259
|
+
}
|
|
5260
|
+
/** The event-log routes hang off the Issues data model; @tablation/client has no resource for them yet. */
|
|
5261
|
+
eventsFetch(method, path, body) {
|
|
5262
|
+
return fetch(`${this.route.baseUrl}/api/data-models/${this.models.issues}${path}`, {
|
|
5263
|
+
method,
|
|
5264
|
+
headers: {
|
|
5265
|
+
Authorization: `Bearer ${resolveApiKey(this.route)}`,
|
|
5266
|
+
"User-Agent": this.userAgent,
|
|
5267
|
+
...body === void 0 ? {} : { "Content-Type": "application/json" }
|
|
5268
|
+
},
|
|
5269
|
+
...body === void 0 ? {} : { body: JSON.stringify(body) }
|
|
5224
5270
|
});
|
|
5225
5271
|
}
|
|
5226
5272
|
/**
|
|
@@ -5495,7 +5541,7 @@ async function applySweep(writer, steps, memberId, log) {
|
|
|
5495
5541
|
{ ticket: step.ticket.issue_id, step: "sweep" }
|
|
5496
5542
|
);
|
|
5497
5543
|
try {
|
|
5498
|
-
await writer.postEvent(step.ticket.id, sweepComment(step), memberId);
|
|
5544
|
+
await writer.postEvent(step.ticket.id, sweepComment(step), memberId, "blocked_sweep");
|
|
5499
5545
|
} catch {
|
|
5500
5546
|
log.warn("status changed but the note failed to post", { ticket: step.ticket.issue_id, step: "sweep" });
|
|
5501
5547
|
}
|
|
@@ -5608,7 +5654,8 @@ async function noteReaccepted(tracker, result, seat, shipLabel) {
|
|
|
5608
5654
|
await tracker.postEvent(
|
|
5609
5655
|
result.ticket.id,
|
|
5610
5656
|
`Re-accepted while held by ${shipLabel}; resuming there, not re-claiming. Setting a held ticket back to \`accepted\` is not needed \u2014 to move it to another ship, clear \`held_by_ship_id\`.`,
|
|
5611
|
-
seat
|
|
5657
|
+
seat,
|
|
5658
|
+
"reaccepted"
|
|
5612
5659
|
);
|
|
5613
5660
|
} catch {
|
|
5614
5661
|
}
|
|
@@ -5641,7 +5688,8 @@ async function noteTakeover(tracker, result, seat, ships, myShipName, continuing
|
|
|
5641
5688
|
await tracker.postEvent(
|
|
5642
5689
|
result.ticket.id,
|
|
5643
5690
|
`Taken over from ${shipName(ships, result.tookOverFrom)} (not seen recently) by ${myShipName}; ` + (sha ? `continuing from \`${sha}\` on origin.` : "no branch commit was found on origin to continue from."),
|
|
5644
|
-
seat
|
|
5691
|
+
seat,
|
|
5692
|
+
"taken_over"
|
|
5645
5693
|
);
|
|
5646
5694
|
} catch {
|
|
5647
5695
|
}
|
|
@@ -5711,7 +5759,7 @@ function buildingRoleHasWork(role, i) {
|
|
|
5711
5759
|
);
|
|
5712
5760
|
const watchedIds = new Set(watched.map((t) => t.id));
|
|
5713
5761
|
const newComments = i.comments.filter(
|
|
5714
|
-
(c) => c.created_at > i.watermark && c.team_member_id !== me &&
|
|
5762
|
+
(c) => c.created_at > i.watermark && c.team_member_id !== me && watchedIds.has(c.ticket_id)
|
|
5715
5763
|
).length;
|
|
5716
5764
|
const unassigned = mine.filter(
|
|
5717
5765
|
(t) => t.status === "in_progress" && !t.assignee_id && !i.blocked.has(t.id)
|
|
@@ -5968,14 +6016,14 @@ function filedBy(t, i) {
|
|
|
5968
6016
|
return t.reporter_name ? `${t.reporter_name} (unverified)` : "\u2014";
|
|
5969
6017
|
}
|
|
5970
6018
|
function lastComment(i, ticketId) {
|
|
5971
|
-
const forTicket = i.comments.filter((c) => c.ticket_id === ticketId
|
|
6019
|
+
const forTicket = i.comments.filter((c) => c.ticket_id === ticketId).sort((a, b) => a.created_at < b.created_at ? -1 : a.created_at > b.created_at ? 1 : 0);
|
|
5972
6020
|
const last = forTicket[forTicket.length - 1];
|
|
5973
6021
|
if (!last) return "\u2014";
|
|
5974
6022
|
return `${last.created_at.slice(0, 16)}Z ${author(last, i)}`;
|
|
5975
6023
|
}
|
|
5976
6024
|
function newFromOthers(i, ticketId) {
|
|
5977
6025
|
return i.comments.filter(
|
|
5978
|
-
(c) => c.ticket_id === ticketId && c.team_member_id !== i.me && c.created_at > i.watermark
|
|
6026
|
+
(c) => c.ticket_id === ticketId && c.team_member_id !== i.me && c.created_at > i.watermark
|
|
5979
6027
|
).length;
|
|
5980
6028
|
}
|
|
5981
6029
|
function who(t, i) {
|
|
@@ -6177,7 +6225,7 @@ var ms = (iso) => {
|
|
|
6177
6225
|
const n = iso ? Date.parse(iso) : NaN;
|
|
6178
6226
|
return Number.isFinite(n) ? n : 0;
|
|
6179
6227
|
};
|
|
6180
|
-
function planStalled(tickets, comments, ships, contract, holds2 = /* @__PURE__ */ new Set(), now = Date.now(), wm) {
|
|
6228
|
+
function planStalled(tickets, comments, ships, contract, holds2 = /* @__PURE__ */ new Set(), now = Date.now(), wm, events = []) {
|
|
6181
6229
|
const watched = /* @__PURE__ */ new Set([contract.statuses.building, contract.statuses.handoff, contract.statuses.verifying]);
|
|
6182
6230
|
const byTicket = /* @__PURE__ */ new Map();
|
|
6183
6231
|
for (const c of comments) {
|
|
@@ -6190,7 +6238,7 @@ function planStalled(tickets, comments, ships, contract, holds2 = /* @__PURE__ *
|
|
|
6190
6238
|
if (!watched.has(t.status)) continue;
|
|
6191
6239
|
if (t.assignee_id && holds2.has(t.assignee_id)) continue;
|
|
6192
6240
|
const mine = byTicket.get(t.id) ?? [];
|
|
6193
|
-
const lastMarker = Math.max(0, ...
|
|
6241
|
+
const lastMarker = Math.max(0, ...events.filter((e) => e.record_id === t.id && e.event_type === "stalled").map((e) => ms(e.occurred_at ?? e.created_at ?? "")));
|
|
6194
6242
|
const lastActivityAt = Math.max(
|
|
6195
6243
|
ms(t.updated_at),
|
|
6196
6244
|
...mine.filter((c) => !(c.body ?? "").includes(STALLED_MARKER)).map((c) => ms(c.created_at))
|
|
@@ -6232,7 +6280,7 @@ async function applyStalled(writer, stalled, ships, memberId, myShipId, log, dry
|
|
|
6232
6280
|
continue;
|
|
6233
6281
|
}
|
|
6234
6282
|
try {
|
|
6235
|
-
await writer.postEvent(s.ticket.id, stalledComment(s, ships, now), memberId);
|
|
6283
|
+
await writer.postEvent(s.ticket.id, stalledComment(s, ships, now), memberId, "stalled");
|
|
6236
6284
|
r.announced++;
|
|
6237
6285
|
log.warn(`stalled at ${s.ticket.status} \u2014 ${s.holderShipId ? "flagged on the holder" : "commented, no holder"}`, x);
|
|
6238
6286
|
} catch (e) {
|
|
@@ -6266,13 +6314,14 @@ async function decideCycle(o) {
|
|
|
6266
6314
|
const { route, ship, state, emit } = o;
|
|
6267
6315
|
const tracker = new Tracker(route, ship);
|
|
6268
6316
|
emit.enter("poll");
|
|
6269
|
-
const [tickets, comments, crewRows, epicRows, ships, projectModes] = await Promise.all([
|
|
6317
|
+
const [tickets, comments, crewRows, epicRows, ships, projectModes, stalledEvents] = await Promise.all([
|
|
6270
6318
|
tracker.openTickets(),
|
|
6271
6319
|
tracker.comments(200),
|
|
6272
6320
|
tracker.crewRows(),
|
|
6273
6321
|
tracker.epicRows(),
|
|
6274
6322
|
tracker.shipRows(),
|
|
6275
|
-
tracker.projectWorkModes()
|
|
6323
|
+
tracker.projectWorkModes(),
|
|
6324
|
+
tracker.events("stalled")
|
|
6276
6325
|
]);
|
|
6277
6326
|
emit.emit(`${tickets.length} open ticket(s), ${comments.length} comment(s)`, {
|
|
6278
6327
|
data: { tickets: tickets.length, comments: comments.length }
|
|
@@ -6316,7 +6365,7 @@ async function decideCycle(o) {
|
|
|
6316
6365
|
}
|
|
6317
6366
|
const sweep = planSweep(tickets, info, blocked, workModes);
|
|
6318
6367
|
const holdReleases = planHoldReleases(tickets, tracker.contract);
|
|
6319
|
-
const stalled = planStalled(tickets, comments, ships, tracker.contract, holds2, Date.now(), workModes);
|
|
6368
|
+
const stalled = planStalled(tickets, comments, ships, tracker.contract, holds2, Date.now(), workModes, stalledEvents);
|
|
6320
6369
|
const skipped = skippedByMode(tickets, workModes);
|
|
6321
6370
|
if (skipped.manual || skipped.hybrid) {
|
|
6322
6371
|
emit.emit(`sweeps skipped ${skipped.manual} manual/project-less and ${skipped.hybrid} unassigned hybrid ticket(s)`, {
|
|
@@ -6602,15 +6651,15 @@ function worktreeForBranch(cwd, branch) {
|
|
|
6602
6651
|
}
|
|
6603
6652
|
return null;
|
|
6604
6653
|
}
|
|
6605
|
-
var priorBounces = (ticket,
|
|
6606
|
-
function planConflictBounce(cwd, ticket, branch, base, paths,
|
|
6654
|
+
var priorBounces = (ticket, events) => events.filter((e) => e.record_id === ticket.id && e.event_type === "conflict_handed_back").length;
|
|
6655
|
+
function planConflictBounce(cwd, ticket, branch, base, paths, events) {
|
|
6607
6656
|
return {
|
|
6608
6657
|
ticket,
|
|
6609
6658
|
branch,
|
|
6610
6659
|
base,
|
|
6611
6660
|
paths,
|
|
6612
6661
|
worktree: worktreeForBranch(cwd, branch),
|
|
6613
|
-
priorBounces: priorBounces(ticket,
|
|
6662
|
+
priorBounces: priorBounces(ticket, events)
|
|
6614
6663
|
};
|
|
6615
6664
|
}
|
|
6616
6665
|
function refreshBranch(worktree, base) {
|
|
@@ -6656,7 +6705,7 @@ async function applyConflictBounce(writer, b, contract, memberId, log, dryRun) {
|
|
|
6656
6705
|
}
|
|
6657
6706
|
try {
|
|
6658
6707
|
await writer.updateTicket(b.ticket.id, { status: contract.statuses.needsHuman }, b.ticket.updated_at);
|
|
6659
|
-
await writer.postEvent(b.ticket.id, escalateComment(b), memberId).catch(() => {
|
|
6708
|
+
await writer.postEvent(b.ticket.id, escalateComment(b), memberId, "conflict_escalated").catch(() => {
|
|
6660
6709
|
log.warn("escalated, but the note failed to post", t);
|
|
6661
6710
|
});
|
|
6662
6711
|
log.warn(`conflicts again after a hand-back \u2014 escalated to ${contract.statuses.needsHuman}`, t);
|
|
@@ -6693,7 +6742,7 @@ async function applyConflictBounce(writer, b, contract, memberId, log, dryRun) {
|
|
|
6693
6742
|
{ status: contract.statuses.building, assignee_id: null },
|
|
6694
6743
|
b.ticket.updated_at
|
|
6695
6744
|
);
|
|
6696
|
-
await writer.postEvent(b.ticket.id, handBackComment(b, merged), memberId).catch(() => {
|
|
6745
|
+
await writer.postEvent(b.ticket.id, handBackComment(b, merged), memberId, "conflict_handed_back").catch(() => {
|
|
6697
6746
|
log.warn("handed back, but the note failed to post", t);
|
|
6698
6747
|
});
|
|
6699
6748
|
log.emit(
|
|
@@ -6772,7 +6821,7 @@ async function applyBuildFailureBounce(writer, b, contract, memberId, log, dryRu
|
|
|
6772
6821
|
{ status: contract.statuses.building, assignee_id: null },
|
|
6773
6822
|
b.ticket.updated_at
|
|
6774
6823
|
);
|
|
6775
|
-
await writer.postEvent(b.ticket.id, buildFailureComment(b), memberId).catch(() => {
|
|
6824
|
+
await writer.postEvent(b.ticket.id, buildFailureComment(b), memberId, "build_failed").catch(() => {
|
|
6776
6825
|
log.warn("handed back, but the note failed to post", t);
|
|
6777
6826
|
});
|
|
6778
6827
|
log.emit(`handed back to ${contract.statuses.building} \u2014 breaks the ${b.hook} hook once merged with the rest`, t);
|
|
@@ -6789,14 +6838,14 @@ async function applyBuildFailureBounce(writer, b, contract, memberId, log, dryRu
|
|
|
6789
6838
|
// src/stranded-verified.ts
|
|
6790
6839
|
var STRANDED_MARKER = "<!-- crew:stranded-verified -->";
|
|
6791
6840
|
var STRANDED_ESCALATE_AFTER_MS = 30 * 60 * 1e3;
|
|
6792
|
-
var priorStrandedFlags = (ticket,
|
|
6793
|
-
function planStrandedVerified(candidate, base, remote,
|
|
6794
|
-
const stamps =
|
|
6841
|
+
var priorStrandedFlags = (ticket, events) => events.filter((e) => e.record_id === ticket.id && e.event_type === "stranded_flagged").length;
|
|
6842
|
+
function planStrandedVerified(candidate, base, remote, events) {
|
|
6843
|
+
const stamps = events.filter((e) => e.record_id === candidate.ticket.id && e.event_type === "stranded_flagged").map((e) => Date.parse(e.occurred_at ?? e.created_at ?? "")).filter((n) => !Number.isNaN(n));
|
|
6795
6844
|
return {
|
|
6796
6845
|
candidate,
|
|
6797
6846
|
base,
|
|
6798
6847
|
remote,
|
|
6799
|
-
priorFlags: priorStrandedFlags(candidate.ticket,
|
|
6848
|
+
priorFlags: priorStrandedFlags(candidate.ticket, events),
|
|
6800
6849
|
lastFlaggedAt: stamps.length ? Math.max(...stamps) : void 0
|
|
6801
6850
|
};
|
|
6802
6851
|
}
|
|
@@ -6834,7 +6883,7 @@ async function applyStrandedVerified(writer, s, contract, operatorId, memberId,
|
|
|
6834
6883
|
status: contract.statuses.needsHuman,
|
|
6835
6884
|
...operatorId ? { assignee_id: operatorId } : {}
|
|
6836
6885
|
});
|
|
6837
|
-
await writer.postEvent(s.candidate.ticket.id, escalateComment2(s), memberId).catch(() => {
|
|
6886
|
+
await writer.postEvent(s.candidate.ticket.id, escalateComment2(s), memberId, "stranded_escalated").catch(() => {
|
|
6838
6887
|
log.warn("escalated, but the note failed to post", t);
|
|
6839
6888
|
});
|
|
6840
6889
|
log.warn(`still nothing to merge after a flag \u2014 escalated to ${contract.statuses.needsHuman}`, t);
|
|
@@ -6848,7 +6897,7 @@ async function applyStrandedVerified(writer, s, contract, operatorId, memberId,
|
|
|
6848
6897
|
return { kind: "flagged" };
|
|
6849
6898
|
}
|
|
6850
6899
|
try {
|
|
6851
|
-
await writer.postEvent(s.candidate.ticket.id, flagComment(s), memberId).catch(() => {
|
|
6900
|
+
await writer.postEvent(s.candidate.ticket.id, flagComment(s), memberId, "stranded_flagged").catch(() => {
|
|
6852
6901
|
log.warn("flagged, but the note failed to post", t);
|
|
6853
6902
|
});
|
|
6854
6903
|
log.emit("flagged as stranded at verified \u2014 will escalate if still unplaceable next cycle", t);
|
|
@@ -6883,7 +6932,7 @@ ${g.tail}
|
|
|
6883
6932
|
\`\`\`
|
|
6884
6933
|
`;
|
|
6885
6934
|
}
|
|
6886
|
-
var alreadyReported = (ticket, g,
|
|
6935
|
+
var alreadyReported = (ticket, g, events) => events.some((e) => e.record_id === ticket.id && e.event_type === "gate_red" && (e.message ?? "").includes(`${GATE_RED_MARKER} ${g.sha.slice(0, 8)}`));
|
|
6887
6936
|
|
|
6888
6937
|
// src/agent.ts
|
|
6889
6938
|
import { spawn as spawn2 } from "child_process";
|
|
@@ -8495,7 +8544,7 @@ async function enforceHandoff(i) {
|
|
|
8495
8544
|
return { kind: "unverified", why: v.why };
|
|
8496
8545
|
}
|
|
8497
8546
|
if (v.kind === "pushed") {
|
|
8498
|
-
await i.writer.postEvent(t.id, pushedEventBody(v.sha, v.remote, v.branch), i.seatId);
|
|
8547
|
+
await i.writer.postEvent(t.id, pushedEventBody(v.sha, v.remote, v.branch), i.seatId, "pushed");
|
|
8499
8548
|
i.log.emit(`${t.issue_id} pushed at ${v.sha.slice(0, 7)} \u2014 handed on to QA`, log);
|
|
8500
8549
|
return { kind: "pushed", sha: v.sha };
|
|
8501
8550
|
}
|
|
@@ -8505,7 +8554,7 @@ async function enforceHandoff(i) {
|
|
|
8505
8554
|
...t.assignee_id ? {} : { assignee_id: i.seatId },
|
|
8506
8555
|
...i.hold && !heldBy ? { [i.hold.column]: i.hold.shipId } : {}
|
|
8507
8556
|
});
|
|
8508
|
-
await i.writer.postEvent(t.id, bounceComment(v, i.shipName), i.seatId).catch(() => {
|
|
8557
|
+
await i.writer.postEvent(t.id, bounceComment(v, i.shipName), i.seatId, "bounced").catch(() => {
|
|
8509
8558
|
i.log.warn("bounced, but the note failed to post", log);
|
|
8510
8559
|
});
|
|
8511
8560
|
i.log.warn(`${t.issue_id} bounced back to ${i.contract.statuses.building}: ${v.reason}`, log);
|
|
@@ -8563,7 +8612,7 @@ async function retireRunWorktree(tracker, dir, cfg, t, role, seatId, shipName2,
|
|
|
8563
8612
|
if (o.kind === "removed") log.emit(`${t.issue_id}: removed the run's worktree ${o.path} \u2014 the branch is on ${cfg.branch.remote}`, at);
|
|
8564
8613
|
else if (o.kind === "kept-handoff") {
|
|
8565
8614
|
log.emit(`${t.issue_id}: kept worktree ${o.path} \u2014 this repo's handoff hook serves from it`, at);
|
|
8566
|
-
await tracker.postEvent(t.id, `worktree kept on ship ${shipName2} at \`${o.path}\`: the repo's \`handoff\` hook serves from it. QA on another ship cuts its own from \`${cfg.branch.remote}/${branch}\` instead.`, seatId);
|
|
8615
|
+
await tracker.postEvent(t.id, `worktree kept on ship ${shipName2} at \`${o.path}\`: the repo's \`handoff\` hook serves from it. QA on another ship cuts its own from \`${cfg.branch.remote}/${branch}\` instead.`, seatId, "worktree_kept");
|
|
8567
8616
|
} else if (o.kind === "kept-dirty") log.warn(`${t.issue_id}: left worktree ${o.path} \u2014 it has uncommitted changes`, at);
|
|
8568
8617
|
else if (o.kind === "kept-unpushed") log.warn(`${t.issue_id}: left worktree ${o.path} \u2014 its branch has commits that are not on ${cfg.branch.remote}`, at);
|
|
8569
8618
|
} catch (e) {
|
|
@@ -9092,7 +9141,7 @@ ${h.prFailure.tail}
|
|
|
9092
9141
|
[contract.columns.status]: reviewing,
|
|
9093
9142
|
...h.prRef && columns.prRef ? { [contract.columns.prRef]: h.prRef } : {}
|
|
9094
9143
|
}, t.updated_at);
|
|
9095
|
-
await tracker.postEvent(t.id, lines.join("\n\n"), seat);
|
|
9144
|
+
await tracker.postEvent(t.id, lines.join("\n\n"), seat, "review");
|
|
9096
9145
|
emit.emit(`moved to ${reviewing}`, { ticket: t.issue_id });
|
|
9097
9146
|
} catch (e) {
|
|
9098
9147
|
emit.warn(`could not move to ${reviewing}: ${e.message}`, { ticket: t.issue_id });
|
|
@@ -9107,7 +9156,7 @@ ${h.prFailure.tail}
|
|
|
9107
9156
|
}
|
|
9108
9157
|
try {
|
|
9109
9158
|
await tracker.updateTicket(t.id, { [contract.columns.status]: contract.statuses.handoff }, t.updated_at);
|
|
9110
|
-
await tracker.postEvent(t.id, body, seat);
|
|
9159
|
+
await tracker.postEvent(t.id, body, seat, "requeued");
|
|
9111
9160
|
emit.emit(`returned to ${contract.statuses.handoff} \u2014 branch moved past ${q.verified.slice(0, 8)}`, { ticket: t.issue_id });
|
|
9112
9161
|
} catch (e) {
|
|
9113
9162
|
emit.warn(`could not return to ${contract.statuses.handoff}: ${e.message}`, { ticket: t.issue_id });
|
|
@@ -9936,7 +9985,7 @@ async function applyFailureAlert(writer, n, route, openTickets, memberId, log, d
|
|
|
9936
9985
|
}
|
|
9937
9986
|
try {
|
|
9938
9987
|
await writer.postEvent(existing.id, `${FAILURE_ALERT_MARKER}
|
|
9939
|
-
Still failing: ${n.detail ?? n.headline}`, memberId);
|
|
9988
|
+
Still failing: ${n.detail ?? n.headline}`, memberId, "failure_alert");
|
|
9940
9989
|
log.warn(`release failure recurring \u2014 commented on ${label}`, { step: "release" });
|
|
9941
9990
|
return { kind: "commented", issueId: label };
|
|
9942
9991
|
} catch (e) {
|
|
@@ -10561,6 +10610,13 @@ async function discover(o) {
|
|
|
10561
10610
|
} catch {
|
|
10562
10611
|
}
|
|
10563
10612
|
}
|
|
10613
|
+
if (out.models.issues) {
|
|
10614
|
+
try {
|
|
10615
|
+
await get(o, `/data-models/${out.models.issues}/events?limit=1`);
|
|
10616
|
+
} catch (e) {
|
|
10617
|
+
if (e instanceof ConnectHttpError && e.status === 404) problems.push(EVENT_LOG_MISSING);
|
|
10618
|
+
}
|
|
10619
|
+
}
|
|
10564
10620
|
out.areaModelId = find("Projects");
|
|
10565
10621
|
out.reposModelId = find("Repos");
|
|
10566
10622
|
out.shipsModelId = find("Ships");
|
|
@@ -13105,11 +13161,11 @@ ${changed2.map((c) => ` - ${c}
|
|
|
13105
13161
|
if (outcome.gateRed && !dryRun) {
|
|
13106
13162
|
try {
|
|
13107
13163
|
const g = outcome.gateRed;
|
|
13108
|
-
const
|
|
13164
|
+
const reported = await tracker.events("gate_red");
|
|
13109
13165
|
const seat = releaseSeat(c);
|
|
13110
13166
|
for (const t of gateRedTickets(outcome.merged, outcome.decision.merges)) {
|
|
13111
|
-
if (alreadyReported(t, g,
|
|
13112
|
-
await tracker.postEvent(t.id, gateRedComment(g), seat);
|
|
13167
|
+
if (alreadyReported(t, g, reported)) continue;
|
|
13168
|
+
await tracker.postEvent(t.id, gateRedComment(g), seat, "gate_red");
|
|
13113
13169
|
remit.warn(`test gate red for ${g.count} cycles \u2014 noted on the ticket`, { ticket: t.issue_id, step: "merge" });
|
|
13114
13170
|
}
|
|
13115
13171
|
state.release(scope).noteTestGateReported(g.sha);
|
|
@@ -13157,7 +13213,8 @@ ${changed2.map((c) => ` - ${c}
|
|
|
13157
13213
|
}
|
|
13158
13214
|
}
|
|
13159
13215
|
if (outcome.conflicts?.length || outcome.unbuildable?.length) {
|
|
13160
|
-
const
|
|
13216
|
+
const handedBack = await tracker.events("conflict_handed_back");
|
|
13217
|
+
const flagged = await tracker.events("stranded_flagged");
|
|
13161
13218
|
const seat = releaseSeat(c);
|
|
13162
13219
|
for (const f of outcome.conflicts ?? []) {
|
|
13163
13220
|
try {
|
|
@@ -13167,7 +13224,7 @@ ${changed2.map((c) => ` - ${c}
|
|
|
13167
13224
|
f.candidate.branch,
|
|
13168
13225
|
repo.branch.base,
|
|
13169
13226
|
f.paths,
|
|
13170
|
-
|
|
13227
|
+
handedBack
|
|
13171
13228
|
);
|
|
13172
13229
|
const r = await applyConflictBounce(tracker, bounce, tracker.contract, seat, remit, dryRun);
|
|
13173
13230
|
if (r.kind === "failed") {
|
|
@@ -13182,7 +13239,7 @@ ${changed2.map((c) => ` - ${c}
|
|
|
13182
13239
|
}
|
|
13183
13240
|
for (const cand of outcome.unbuildable ?? []) {
|
|
13184
13241
|
try {
|
|
13185
|
-
const stranded = planStrandedVerified(cand, repo.branch.base, repo.branch.remote,
|
|
13242
|
+
const stranded = planStrandedVerified(cand, repo.branch.base, repo.branch.remote, flagged);
|
|
13186
13243
|
const r = await applyStrandedVerified(
|
|
13187
13244
|
tracker,
|
|
13188
13245
|
stranded,
|
package/package.json
CHANGED
|
@@ -56,7 +56,7 @@ pass.
|
|
|
56
56
|
4. **Get the branch and read the diff.** The branch on the remote is what
|
|
57
57
|
you test, and you never need the builder's worktree: cut your own with
|
|
58
58
|
`crew worktree <number>` (it prints the path), unless the builder's
|
|
59
|
-
hand-off event says its ship kept a worktree and a server for you, in
|
|
59
|
+
hand-off event (in the ticket's event log) says its ship kept a worktree and a server for you, in
|
|
60
60
|
which case you may open that URL instead. Remove yours when you hand the
|
|
61
61
|
ticket on. The digest's `branch` column reads `<name> (on <remote> only)`
|
|
62
62
|
when another ship built it — still testable; only **MISSING** (gone
|
|
@@ -65,7 +65,9 @@ pass.
|
|
|
65
65
|
|
|
66
66
|
**Name the sha you test.** Your starting comment states the
|
|
67
67
|
`<remote>/<branch>` tip you fetched. The runner records the pushed tip on
|
|
68
|
-
the ticket
|
|
68
|
+
the ticket's activity (its event log, not a comment) as a `pushed` event
|
|
69
|
+
reading `pushed <sha> to <remote>/<branch>`; read it with the events list
|
|
70
|
+
on the Issues table, filtered to the `pushed` type. If your tip differs
|
|
69
71
|
from the latest such event, say so and verify the remote tip anyway — the
|
|
70
72
|
remote is the only git truth.
|
|
71
73
|
|
|
@@ -363,7 +363,7 @@ before going further, and never change a ticket's `report_type` yourself.
|
|
|
363
363
|
`fixed` and `<remote>/<branch>` is missing or not at your worktree's
|
|
364
364
|
HEAD, it sets the ticket back to `in_progress` with an event saying what
|
|
365
365
|
it found, and QA never sees it. When it matches, the runner records
|
|
366
|
-
`pushed <sha> to <remote>/<branch>`
|
|
366
|
+
`pushed <sha> to <remote>/<branch>` in the ticket's activity (its event log); you still state the sha
|
|
367
367
|
in your last progress comment.
|
|
368
368
|
|
|
369
369
|
**The branch on the remote is what QA tests.** Leave it unmerged, and do
|
|
@@ -55,7 +55,7 @@ pass.
|
|
|
55
55
|
4. **Get the branch and read the diff.** The branch on the remote is what
|
|
56
56
|
you test, and you never need the builder's worktree: cut your own with
|
|
57
57
|
`crew worktree <number>` (it prints the path), unless the builder's
|
|
58
|
-
hand-off event says its ship kept a worktree and a server for you, in
|
|
58
|
+
hand-off event (in the ticket's event log) says its ship kept a worktree and a server for you, in
|
|
59
59
|
which case you may open that URL instead. Remove yours when you hand the
|
|
60
60
|
ticket on. The digest's `branch` column reads `<name> (on <remote> only)`
|
|
61
61
|
when another ship built it — still testable; only **MISSING** (gone
|
|
@@ -64,7 +64,9 @@ pass.
|
|
|
64
64
|
|
|
65
65
|
**Name the sha you test.** Your starting comment states the
|
|
66
66
|
`<remote>/<branch>` tip you fetched. The runner records the pushed tip on
|
|
67
|
-
the ticket
|
|
67
|
+
the ticket's activity (its event log, not a comment) as a `pushed` event
|
|
68
|
+
reading `pushed <sha> to <remote>/<branch>`; read it with the events list
|
|
69
|
+
on the Issues table, filtered to the `pushed` type. If your tip differs
|
|
68
70
|
from the latest such event, say so and verify the remote tip anyway — the
|
|
69
71
|
remote is the only git truth.
|
|
70
72
|
|
|
@@ -363,7 +363,7 @@ before going further, and never change a ticket's `report_type` yourself.
|
|
|
363
363
|
`fixed` and `<remote>/<branch>` is missing or not at your worktree's
|
|
364
364
|
HEAD, it sets the ticket back to `in_progress` with an event saying what
|
|
365
365
|
it found, and QA never sees it. When it matches, the runner records
|
|
366
|
-
`pushed <sha> to <remote>/<branch>`
|
|
366
|
+
`pushed <sha> to <remote>/<branch>` in the ticket's activity (its event log); you still state the sha
|
|
367
367
|
in your last progress comment.
|
|
368
368
|
|
|
369
369
|
**The branch on the remote is what QA tests.** Leave it unmerged, and do
|