crewly 1.20.419 → 1.20.421
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/config/skills/agent/core/project-tickets/SKILL.md +21 -0
- package/config/skills/agent/core/project-tickets/execute.sh +15 -5
- package/config/skills/agent/core/record-learning/SKILL.md +12 -1
- package/config/skills/agent/core/record-learning/execute.sh +24 -2
- package/config/skills/team-leader/decide-correction/SKILL.md +65 -0
- package/config/skills/team-leader/decide-correction/execute.sh +63 -0
- package/config/skills/team-leader/handle-failure/SKILL.md +2 -0
- package/config/skills/team-leader/handle-failure/execute.sh +18 -0
- package/dist/backend/backend/src/constants.d.ts +35 -0
- package/dist/backend/backend/src/constants.d.ts.map +1 -1
- package/dist/backend/backend/src/constants.js +36 -0
- package/dist/backend/backend/src/constants.js.map +1 -1
- package/dist/backend/backend/src/controllers/memory/memory.controller.d.ts +42 -2
- package/dist/backend/backend/src/controllers/memory/memory.controller.d.ts.map +1 -1
- package/dist/backend/backend/src/controllers/memory/memory.controller.js +111 -4
- package/dist/backend/backend/src/controllers/memory/memory.controller.js.map +1 -1
- package/dist/backend/backend/src/controllers/memory/memory.routes.d.ts +3 -0
- package/dist/backend/backend/src/controllers/memory/memory.routes.d.ts.map +1 -1
- package/dist/backend/backend/src/controllers/memory/memory.routes.js +7 -1
- package/dist/backend/backend/src/controllers/memory/memory.routes.js.map +1 -1
- package/dist/backend/backend/src/controllers/project-tickets/project-tickets.controller.d.ts +8 -0
- package/dist/backend/backend/src/controllers/project-tickets/project-tickets.controller.d.ts.map +1 -1
- package/dist/backend/backend/src/controllers/project-tickets/project-tickets.controller.js +28 -0
- package/dist/backend/backend/src/controllers/project-tickets/project-tickets.controller.js.map +1 -1
- package/dist/backend/backend/src/controllers/project-tickets/project-tickets.routes.d.ts +1 -0
- package/dist/backend/backend/src/controllers/project-tickets/project-tickets.routes.d.ts.map +1 -1
- package/dist/backend/backend/src/controllers/project-tickets/project-tickets.routes.js +4 -1
- package/dist/backend/backend/src/controllers/project-tickets/project-tickets.routes.js.map +1 -1
- package/dist/backend/backend/src/index.d.ts.map +1 -1
- package/dist/backend/backend/src/index.js +25 -0
- package/dist/backend/backend/src/index.js.map +1 -1
- package/dist/backend/backend/src/services/memory/correction-ladder.service.d.ts +225 -0
- package/dist/backend/backend/src/services/memory/correction-ladder.service.d.ts.map +1 -0
- package/dist/backend/backend/src/services/memory/correction-ladder.service.js +335 -0
- package/dist/backend/backend/src/services/memory/correction-ladder.service.js.map +1 -0
- package/dist/backend/backend/src/services/project-tickets/ticket-autopilot-retro.d.ts +99 -1
- package/dist/backend/backend/src/services/project-tickets/ticket-autopilot-retro.d.ts.map +1 -1
- package/dist/backend/backend/src/services/project-tickets/ticket-autopilot-retro.js +188 -16
- package/dist/backend/backend/src/services/project-tickets/ticket-autopilot-retro.js.map +1 -1
- package/dist/backend/backend/src/services/project-tickets/ticket-autopilot.service.d.ts +53 -4
- package/dist/backend/backend/src/services/project-tickets/ticket-autopilot.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/project-tickets/ticket-autopilot.service.js +226 -27
- package/dist/backend/backend/src/services/project-tickets/ticket-autopilot.service.js.map +1 -1
- package/dist/backend/build-info.json +2 -2
- package/dist/cli/backend/src/constants.d.ts +35 -0
- package/dist/cli/backend/src/constants.d.ts.map +1 -1
- package/dist/cli/backend/src/constants.js +36 -0
- package/dist/cli/backend/src/constants.js.map +1 -1
- package/package.json +1 -1
|
@@ -43,7 +43,7 @@ import { OPEN_TICKET_STATUSES, classifyStopReason, findStalledWork, normalizeRed
|
|
|
43
43
|
import { buildBudgetPausedMessage, buildDigestMessage, buildReplanBrief, buildSelfReviewBrief, buildTriageBrief, REPLAN_ASK, replanMetricRejection, STOP_REASON_WORDS, stopReasonText, } from './ticket-autopilot-messages.js';
|
|
44
44
|
import { autopilotRunTrace, startReplanTrace, startTriageTrace, traceAutopilotAction, createAutopilotTicketListener } from './ticket-autopilot-trace.js';
|
|
45
45
|
import { computeAutopilotStats, dayEndMs, daysBetween, dayStartMs, rangeDays, } from './ticket-autopilot-stats.js';
|
|
46
|
-
import { buildRetroBrief, duplicateOf, renderRetroMarkdown, RetroInputError, validateRetroInput, } from './ticket-autopilot-retro.js';
|
|
46
|
+
import { buildRetroBrief, buildWeeklyRetroBrief, isoWeekOf, isoWeekRange, previousIsoWeek, renderWeeklyRetroMarkdown, validateWeeklyRetroInput, duplicateOf, renderRetroMarkdown, RetroInputError, validateRetroInput, } from './ticket-autopilot-retro.js';
|
|
47
47
|
import { getTraceStore } from '../trace/trace-store.js';
|
|
48
48
|
import { clampStallMinutes, defaultStallMinutes } from '../trace/trace-metrics.js';
|
|
49
49
|
import { RestartDrainService } from '../restart/restart-drain.service.js';
|
|
@@ -331,6 +331,7 @@ export class TicketAutopilotService {
|
|
|
331
331
|
await this.processSelfReviews(projects).catch((err) => this.logger.warn('Autopilot self-review pass failed', { error: err instanceof Error ? err.message : String(err) }));
|
|
332
332
|
await this.processDigest(projects).catch((err) => this.logger.warn('Ticket digest pass failed', { error: err instanceof Error ? err.message : String(err) }));
|
|
333
333
|
await this.processRetros(projects).catch((err) => this.logger.warn('Autopilot retro pass failed', { error: err instanceof Error ? err.message : String(err) }));
|
|
334
|
+
await this.processWeeklyRetro().catch((err) => this.logger.warn('Weekly autopilot retro pass failed', { error: err instanceof Error ? err.message : String(err) }));
|
|
334
335
|
}
|
|
335
336
|
return out;
|
|
336
337
|
});
|
|
@@ -1448,8 +1449,17 @@ export class TicketAutopilotService {
|
|
|
1448
1449
|
throw new ProjectTicketError(400, `day ${retro.day} is in the future`);
|
|
1449
1450
|
const stats = await this.statsFor(project, retro.day, retro.day, null).catch(() => null);
|
|
1450
1451
|
const runTrace = autopilotRunTrace(project, new Date(dayStartMs(retro.day) + 12 * 3_600_000), false);
|
|
1451
|
-
const filed = await this.fileHarnessGaps(project, retro, retroDeps, now);
|
|
1452
1452
|
const wikiPath = `${TICKET_AUTOPILOT_CONSTANTS.RETRO_WIKI_DIR}/${retro.day}.md`;
|
|
1453
|
+
const filed = await this.fileHarnessGaps({
|
|
1454
|
+
key: `${project.id}:${retro.day}`,
|
|
1455
|
+
label: `the ${retro.day} autopilot retro of ${project.name}`,
|
|
1456
|
+
source: `retro:${project.name}:${retro.day}`,
|
|
1457
|
+
pageNote: `Retro: ${project.name} wiki, ${wikiPath}`,
|
|
1458
|
+
cardTitle: `Harness gaps from the ${project.name} retro (${retro.day})`,
|
|
1459
|
+
decisionProjectId: project.id,
|
|
1460
|
+
traceProject: project,
|
|
1461
|
+
traceData: { day: retro.day },
|
|
1462
|
+
}, retro.problems, retroDeps, now);
|
|
1453
1463
|
const markdown = renderRetroMarkdown({ project, retro, stats: stats?.days[0] ?? null, by, filed: filed.filed, runTraceId: runTrace });
|
|
1454
1464
|
const written = await retroDeps.writeWiki(project.path, wikiPath, markdown, by).catch((err) => {
|
|
1455
1465
|
this.logger.warn('Could not write the autopilot retro to the wiki', { projectId: project.id, error: err instanceof Error ? err.message : String(err) });
|
|
@@ -1467,21 +1477,36 @@ export class TicketAutopilotService {
|
|
|
1467
1477
|
now,
|
|
1468
1478
|
});
|
|
1469
1479
|
this.logger.info('Autopilot retro filed', { projectId: project.id, day: retro.day, by, written, filed: filed.filed.map((t) => t.id) });
|
|
1480
|
+
await this.rememberFiledRetro({ projectId: project.id, projectName: project.name, day: retro.day, at: now.getTime(), problems: retro.problems.length, gaps: filed.filed }, now);
|
|
1470
1481
|
return { day: retro.day, wikiPath, written, ...filed };
|
|
1471
1482
|
}
|
|
1472
1483
|
/**
|
|
1473
|
-
*
|
|
1474
|
-
*
|
|
1484
|
+
* Remember a filed daily retro for the weekly cross-team retro (one entry
|
|
1485
|
+
* per project and day, the latest wins; pruned after RETRO_FILED_MEMORY_DAYS).
|
|
1475
1486
|
*
|
|
1476
|
-
* @param
|
|
1477
|
-
* @param
|
|
1487
|
+
* @param entry - The retro
|
|
1488
|
+
* @param now - Clock
|
|
1489
|
+
*/
|
|
1490
|
+
async rememberFiledRetro(entry, now) {
|
|
1491
|
+
const state = await this.loadState();
|
|
1492
|
+
const keep = now.getTime() - TICKET_AUTOPILOT_CONSTANTS.RETRO_FILED_MEMORY_DAYS * 24 * 3_600_000;
|
|
1493
|
+
state.retro.filed = [...state.retro.filed.filter((f) => f.at >= keep && !(f.projectId === entry.projectId && f.day === entry.day)), entry];
|
|
1494
|
+
await this.saveState();
|
|
1495
|
+
}
|
|
1496
|
+
/**
|
|
1497
|
+
* File a retro's harness gaps on the Crewly project and ask the owner one
|
|
1498
|
+
* card for them. Shared by the daily retro (origin = a project and day)
|
|
1499
|
+
* and the weekly cross-team retro (origin = a week).
|
|
1500
|
+
*
|
|
1501
|
+
* @param origin - Where the gaps came from (words, source, card title, trace)
|
|
1502
|
+
* @param problems - The retro's problems (only `harness_gap` ones are filed)
|
|
1478
1503
|
* @param retroDeps - Side effects
|
|
1479
1504
|
* @param now - Clock
|
|
1480
1505
|
* @returns Filed, duplicate and over-cap gaps, and the card
|
|
1481
1506
|
*/
|
|
1482
|
-
async fileHarnessGaps(
|
|
1507
|
+
async fileHarnessGaps(origin, problems, retroDeps, now) {
|
|
1483
1508
|
const C = TICKET_AUTOPILOT_CONSTANTS;
|
|
1484
|
-
const gaps =
|
|
1509
|
+
const gaps = problems.filter((p) => p.class === 'harness_gap');
|
|
1485
1510
|
const out = { filed: [], duplicates: [], overCap: [], decisionId: null };
|
|
1486
1511
|
if (gaps.length === 0)
|
|
1487
1512
|
return out;
|
|
@@ -1507,14 +1532,7 @@ export class TicketAutopilotService {
|
|
|
1507
1532
|
out.overCap.push(gap.title);
|
|
1508
1533
|
continue;
|
|
1509
1534
|
}
|
|
1510
|
-
const description = [
|
|
1511
|
-
`Found by the ${retro.day} autopilot retro of ${project.name}.`,
|
|
1512
|
-
'',
|
|
1513
|
-
gap.detail ?? '',
|
|
1514
|
-
'',
|
|
1515
|
-
gap.evidence ? `Evidence: ${gap.evidence}` : '',
|
|
1516
|
-
`Retro: ${project.name} wiki, ${C.RETRO_WIKI_DIR}/${retro.day}.md`,
|
|
1517
|
-
]
|
|
1535
|
+
const description = [`Found by ${origin.label}.`, '', gap.detail ?? '', '', gap.evidence ? `Evidence: ${gap.evidence}` : '', origin.pageNote]
|
|
1518
1536
|
.filter((l, i, all) => l !== '' || (i > 0 && all[i - 1] !== ''))
|
|
1519
1537
|
.join('\n')
|
|
1520
1538
|
.trim();
|
|
@@ -1524,19 +1542,21 @@ export class TicketAutopilotService {
|
|
|
1524
1542
|
// Held until the owner approves: triage skips retro-pending tickets,
|
|
1525
1543
|
// and nothing but this retro's card lifts the hold.
|
|
1526
1544
|
labels: [...C.RETRO_GAP_LABELS, C.RETRO_PENDING_LABEL],
|
|
1527
|
-
source:
|
|
1545
|
+
source: origin.source,
|
|
1528
1546
|
});
|
|
1529
1547
|
out.filed.push({ id: ticket.id, title: ticket.title });
|
|
1530
1548
|
known.push(gap.title);
|
|
1531
1549
|
state.retro.gaps.push({ title: gap.title, at: now.getTime() });
|
|
1532
1550
|
state.retro.gapDays[today] = (state.retro.gapDays[today] ?? 0) + 1;
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1551
|
+
if (origin.traceProject) {
|
|
1552
|
+
traceAutopilotAction(origin.traceProject, 'retro_gap_ticket', {
|
|
1553
|
+
summary: `Harness gap filed on ${target.name} as ${ticket.id}: ${gap.title}`,
|
|
1554
|
+
outcome: 'queued',
|
|
1555
|
+
ticketId: ticket.id,
|
|
1556
|
+
data: { ...origin.traceData, harnessProject: target.name },
|
|
1557
|
+
now,
|
|
1558
|
+
});
|
|
1559
|
+
}
|
|
1540
1560
|
}
|
|
1541
1561
|
for (const d of Object.keys(state.retro.gapDays))
|
|
1542
1562
|
if (d < localDateKey(new Date(memory)))
|
|
@@ -1545,8 +1565,8 @@ export class TicketAutopilotService {
|
|
|
1545
1565
|
const n = out.filed.length;
|
|
1546
1566
|
try {
|
|
1547
1567
|
const decision = await retroDeps.askOwner({
|
|
1548
|
-
key: `retro:${
|
|
1549
|
-
title:
|
|
1568
|
+
key: `retro:${origin.key}`,
|
|
1569
|
+
title: origin.cardTitle,
|
|
1550
1570
|
question: `File ${n === 1 ? 'this harness gap' : `these ${n} harness gaps`} for the ${target.name} team?`,
|
|
1551
1571
|
body: out.filed.map((t) => `• *${t.id}* ${t.title}`),
|
|
1552
1572
|
approveLabel: C.RETRO_APPROVE_LABEL,
|
|
@@ -1554,7 +1574,7 @@ export class TicketAutopilotService {
|
|
|
1554
1574
|
deadline: new Date(now.getTime() + C.RETRO_DECISION_DEADLINE_MS),
|
|
1555
1575
|
});
|
|
1556
1576
|
out.decisionId = decision.id;
|
|
1557
|
-
state.retro.decisions[decision.id] = { projectId:
|
|
1577
|
+
state.retro.decisions[decision.id] = { projectId: origin.decisionProjectId, projectPath: target.path, ticketIds: out.filed.map((t) => t.id) };
|
|
1558
1578
|
}
|
|
1559
1579
|
catch (err) {
|
|
1560
1580
|
// No card, no approval: the tickets must not wait on a question nobody asked.
|
|
@@ -1570,6 +1590,183 @@ export class TicketAutopilotService {
|
|
|
1570
1590
|
await this.saveState();
|
|
1571
1591
|
return out;
|
|
1572
1592
|
}
|
|
1593
|
+
// ---------------------------------------------------------------------------
|
|
1594
|
+
// Weekly cross-team retro (#985)
|
|
1595
|
+
// ---------------------------------------------------------------------------
|
|
1596
|
+
/**
|
|
1597
|
+
* Once per ISO week, on or after Monday RETRO_HOUR_LOCAL, give the
|
|
1598
|
+
* orchestrator ONE WorkItem reviewing the previous week's daily retros
|
|
1599
|
+
* across projects. Skipped when no daily retro was filed for that week,
|
|
1600
|
+
* while the previous weekly item is still live, or when the global wiki
|
|
1601
|
+
* is not wired.
|
|
1602
|
+
*
|
|
1603
|
+
* @returns The WorkItem created, if any
|
|
1604
|
+
*/
|
|
1605
|
+
async processWeeklyRetro() {
|
|
1606
|
+
const C = TICKET_AUTOPILOT_CONSTANTS;
|
|
1607
|
+
const retroDeps = this.deps.retro;
|
|
1608
|
+
if (!retroDeps?.writeGlobalWiki)
|
|
1609
|
+
return null;
|
|
1610
|
+
const now = this.now();
|
|
1611
|
+
// Monday before the retro hour: the week has just closed, give the leads their morning.
|
|
1612
|
+
if (now.getDay() === C.WEEKLY_RETRO_WEEKDAY_LOCAL && now.getHours() < C.RETRO_HOUR_LOCAL)
|
|
1613
|
+
return null;
|
|
1614
|
+
const { week, start, end } = previousIsoWeek(localDateKey(now));
|
|
1615
|
+
const state = await this.loadState();
|
|
1616
|
+
const weekly = state.retro.weekly;
|
|
1617
|
+
if (weekly.scheduledFor === week)
|
|
1618
|
+
return null;
|
|
1619
|
+
if (weekly.retryAt && now.getTime() < weekly.retryAt)
|
|
1620
|
+
return null;
|
|
1621
|
+
const filed = state.retro.filed.filter((f) => f.day >= start && f.day <= end);
|
|
1622
|
+
// No lead filed a retro for that week (yet): nothing to read across teams.
|
|
1623
|
+
// Not remembered, so a retro filed later today still gets its weekly review.
|
|
1624
|
+
if (filed.length === 0)
|
|
1625
|
+
return null;
|
|
1626
|
+
const items = await this.deps.pool.getAllItems();
|
|
1627
|
+
if (items.some((wi) => wi.metadata?.kind === C.WEEKLY_RETRO_METADATA_KIND && LIVE_STATUSES.has(wi.status))) {
|
|
1628
|
+
this.logger.info('Weekly autopilot retro skipped: the previous one is still open', { week });
|
|
1629
|
+
return null;
|
|
1630
|
+
}
|
|
1631
|
+
const projects = await this.deps.directory.getProjects();
|
|
1632
|
+
const byProject = new Map();
|
|
1633
|
+
for (const f of filed)
|
|
1634
|
+
byProject.set(f.projectId, [...(byProject.get(f.projectId) ?? []), f]);
|
|
1635
|
+
const briefProjects = [];
|
|
1636
|
+
let readable = 0;
|
|
1637
|
+
for (const [projectId, retros] of byProject) {
|
|
1638
|
+
const project = projects.find((p) => p.id === projectId);
|
|
1639
|
+
const name = project?.name ?? retros[0].projectName;
|
|
1640
|
+
const stats = project ? await this.statsFor(project, start, end, null).catch(() => null) : null;
|
|
1641
|
+
if (stats && !stats.incomplete)
|
|
1642
|
+
readable += 1;
|
|
1643
|
+
briefProjects.push({
|
|
1644
|
+
id: projectId,
|
|
1645
|
+
name,
|
|
1646
|
+
stats: stats ? stats.total : null,
|
|
1647
|
+
retros: retros
|
|
1648
|
+
.slice()
|
|
1649
|
+
.sort((a, b) => a.day.localeCompare(b.day))
|
|
1650
|
+
.map((r) => {
|
|
1651
|
+
const rel = `${C.RETRO_WIKI_DIR}/${r.day}.md`;
|
|
1652
|
+
return { day: r.day, page: project?.path && retroDeps.retroPagePath ? retroDeps.retroPagePath(project.path, rel) : rel, problems: r.problems, gaps: r.gaps.length };
|
|
1653
|
+
}),
|
|
1654
|
+
});
|
|
1655
|
+
}
|
|
1656
|
+
if (readable === 0 && byProject.size > 0) {
|
|
1657
|
+
// Every project's traces are unreadable right now: try again later rather than brief with no numbers.
|
|
1658
|
+
weekly.failures = (weekly.failures ?? 0) + 1;
|
|
1659
|
+
weekly.retryAt = now.getTime() + retryBackoffMs(weekly.failures);
|
|
1660
|
+
await this.saveState();
|
|
1661
|
+
return null;
|
|
1662
|
+
}
|
|
1663
|
+
briefProjects.sort((a, b) => b.retros.length - a.retros.length || a.name.localeCompare(b.name));
|
|
1664
|
+
const gaps = filed.flatMap((f) => f.gaps.map((g) => ({ id: g.id, title: g.title, project: f.projectName, day: f.day })));
|
|
1665
|
+
const brief = buildWeeklyRetroBrief({ week, start, end, projects: briefProjects, gaps });
|
|
1666
|
+
const workItem = createWorkItem({
|
|
1667
|
+
type: C.RETRO_WORK_ITEM_TYPE,
|
|
1668
|
+
owner: 'orchestrator',
|
|
1669
|
+
target: ORCHESTRATOR_SESSION_NAME,
|
|
1670
|
+
title: `Weekly autopilot retro: ${week}`,
|
|
1671
|
+
description: `Read the ${week} daily retros of ${briefProjects.length} project${briefProjects.length === 1 ? '' : 's'} across teams: what repeated, what belongs to Crewly; classify each problem and submit the weekly retro.`,
|
|
1672
|
+
briefMarkdown: capBrief(brief),
|
|
1673
|
+
metadata: {
|
|
1674
|
+
kind: C.WEEKLY_RETRO_METADATA_KIND,
|
|
1675
|
+
week,
|
|
1676
|
+
start,
|
|
1677
|
+
end,
|
|
1678
|
+
projectIds: briefProjects.map((p) => p.id),
|
|
1679
|
+
requiresVerification: false,
|
|
1680
|
+
},
|
|
1681
|
+
});
|
|
1682
|
+
workItem.createdAt = now.toISOString();
|
|
1683
|
+
workItem.targetSource = 'assigned';
|
|
1684
|
+
await this.deps.pool.addToPool(workItem);
|
|
1685
|
+
state.retro.weekly = { scheduledFor: week, workItemId: workItem.id };
|
|
1686
|
+
await this.saveState();
|
|
1687
|
+
this.logger.info('Weekly autopilot retro scheduled', { week, projects: briefProjects.length, workItemId: workItem.id });
|
|
1688
|
+
return workItem;
|
|
1689
|
+
}
|
|
1690
|
+
/**
|
|
1691
|
+
* The orchestrator's (or owner's) weekly cross-team retro: write it to the
|
|
1692
|
+
* global wiki, file the harness gaps on the Crewly project (same dedupe,
|
|
1693
|
+
* cap and owner card as the daily retro).
|
|
1694
|
+
*
|
|
1695
|
+
* @param body - `{week, summary, problems}`
|
|
1696
|
+
* @param caller - Owner or orchestrator
|
|
1697
|
+
* @returns What was written and filed
|
|
1698
|
+
* @throws ProjectTicketError(400/403/503)
|
|
1699
|
+
*/
|
|
1700
|
+
async submitWeeklyRetro(body, caller) {
|
|
1701
|
+
const run = this.retroChain.then(() => this.submitWeeklyRetroNow(body, caller), () => this.submitWeeklyRetroNow(body, caller));
|
|
1702
|
+
this.retroChain = run.catch(() => undefined);
|
|
1703
|
+
return run;
|
|
1704
|
+
}
|
|
1705
|
+
/**
|
|
1706
|
+
* {@link submitWeeklyRetro}, serialised.
|
|
1707
|
+
*
|
|
1708
|
+
* @param body - Retro
|
|
1709
|
+
* @param caller - Caller
|
|
1710
|
+
* @returns What was written and filed
|
|
1711
|
+
*/
|
|
1712
|
+
async submitWeeklyRetroNow(body, caller) {
|
|
1713
|
+
const C = TICKET_AUTOPILOT_CONSTANTS;
|
|
1714
|
+
if (caller.session && caller.session !== ORCHESTRATOR_SESSION_NAME) {
|
|
1715
|
+
throw new ProjectTicketError(403, 'Only the owner or the orchestrator can file the weekly cross-team retro');
|
|
1716
|
+
}
|
|
1717
|
+
const retroDeps = this.deps.retro;
|
|
1718
|
+
if (!retroDeps?.writeGlobalWiki)
|
|
1719
|
+
throw new ProjectTicketError(503, 'Weekly retros are not available (the global wiki is not wired)');
|
|
1720
|
+
let retro;
|
|
1721
|
+
try {
|
|
1722
|
+
retro = validateWeeklyRetroInput(body);
|
|
1723
|
+
}
|
|
1724
|
+
catch (err) {
|
|
1725
|
+
if (err instanceof RetroInputError)
|
|
1726
|
+
throw new ProjectTicketError(400, err.message);
|
|
1727
|
+
throw err;
|
|
1728
|
+
}
|
|
1729
|
+
const by = caller.session ?? 'owner';
|
|
1730
|
+
const now = this.now();
|
|
1731
|
+
if (retro.week > isoWeekOf(localDateKey(now)))
|
|
1732
|
+
throw new ProjectTicketError(400, `week ${retro.week} is in the future`);
|
|
1733
|
+
const { start, end } = isoWeekRange(retro.week);
|
|
1734
|
+
const wikiPath = `${C.WEEKLY_RETRO_WIKI_DIR}/${retro.week}.md`;
|
|
1735
|
+
const filed = await this.fileHarnessGaps({
|
|
1736
|
+
key: `week:${retro.week}`,
|
|
1737
|
+
label: `the ${retro.week} weekly cross-team retro`,
|
|
1738
|
+
source: `retro:week:${retro.week}`,
|
|
1739
|
+
pageNote: `Retro: global wiki, ${wikiPath}`,
|
|
1740
|
+
cardTitle: `Harness gaps from the weekly cross-team retro (${retro.week})`,
|
|
1741
|
+
decisionProjectId: `week:${retro.week}`,
|
|
1742
|
+
traceProject: null,
|
|
1743
|
+
traceData: {},
|
|
1744
|
+
}, retro.problems, retroDeps, now);
|
|
1745
|
+
const state = await this.loadState();
|
|
1746
|
+
const projects = await this.deps.directory.getProjects();
|
|
1747
|
+
const byProject = new Map();
|
|
1748
|
+
for (const f of state.retro.filed.filter((f) => f.day >= start && f.day <= end))
|
|
1749
|
+
byProject.set(f.projectId, [...(byProject.get(f.projectId) ?? []), f]);
|
|
1750
|
+
const pageProjects = [];
|
|
1751
|
+
for (const [projectId, retros] of byProject) {
|
|
1752
|
+
const project = projects.find((p) => p.id === projectId);
|
|
1753
|
+
const stats = project ? await this.statsFor(project, start, end, null).catch(() => null) : null;
|
|
1754
|
+
pageProjects.push({
|
|
1755
|
+
id: projectId,
|
|
1756
|
+
name: project?.name ?? retros[0].projectName,
|
|
1757
|
+
stats: stats ? stats.total : null,
|
|
1758
|
+
retros: retros.map((r) => ({ day: r.day, page: `${C.RETRO_WIKI_DIR}/${r.day}.md`, problems: r.problems, gaps: r.gaps.length })).sort((a, b) => a.day.localeCompare(b.day)),
|
|
1759
|
+
});
|
|
1760
|
+
}
|
|
1761
|
+
pageProjects.sort((a, b) => a.name.localeCompare(b.name));
|
|
1762
|
+
const markdown = renderWeeklyRetroMarkdown({ week: retro.week, start, end, retro, by, projects: pageProjects, filed: filed.filed });
|
|
1763
|
+
const written = await retroDeps.writeGlobalWiki(wikiPath, markdown, by).catch((err) => {
|
|
1764
|
+
this.logger.warn('Could not write the weekly autopilot retro to the global wiki', { week: retro.week, error: err instanceof Error ? err.message : String(err) });
|
|
1765
|
+
return false;
|
|
1766
|
+
});
|
|
1767
|
+
this.logger.info('Weekly autopilot retro filed', { week: retro.week, by, written, problems: retro.problems.length, filed: filed.filed.map((t) => t.id) });
|
|
1768
|
+
return { week: retro.week, wikiPath, written, ...filed };
|
|
1769
|
+
}
|
|
1573
1770
|
/**
|
|
1574
1771
|
* Tell the lead of the harness project that the owner skipped a gap
|
|
1575
1772
|
* ticket someone already started (a `notify` WorkItem: the normal
|
|
@@ -2373,6 +2570,8 @@ export class TicketAutopilotService {
|
|
|
2373
2570
|
gapDays: raw.retro?.gapDays && typeof raw.retro.gapDays === 'object' ? raw.retro.gapDays : {},
|
|
2374
2571
|
gaps: Array.isArray(raw.retro?.gaps) ? raw.retro.gaps : [],
|
|
2375
2572
|
decisions: raw.retro?.decisions && typeof raw.retro.decisions === 'object' ? raw.retro.decisions : {},
|
|
2573
|
+
filed: Array.isArray(raw.retro?.filed) ? raw.retro.filed : [],
|
|
2574
|
+
weekly: raw.retro?.weekly && typeof raw.retro.weekly === 'object' ? raw.retro.weekly : {},
|
|
2376
2575
|
},
|
|
2377
2576
|
};
|
|
2378
2577
|
return this.state;
|