crewly 1.20.288 → 1.20.290

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.
Files changed (139) hide show
  1. package/dist/backend/backend/src/constants.d.ts +55 -0
  2. package/dist/backend/backend/src/constants.d.ts.map +1 -1
  3. package/dist/backend/backend/src/constants.js +56 -1
  4. package/dist/backend/backend/src/constants.js.map +1 -1
  5. package/dist/backend/backend/src/controllers/chat/agent-reply.controller.d.ts.map +1 -1
  6. package/dist/backend/backend/src/controllers/chat/agent-reply.controller.js +5 -0
  7. package/dist/backend/backend/src/controllers/chat/agent-reply.controller.js.map +1 -1
  8. package/dist/backend/backend/src/controllers/project-tickets/project-tickets.controller.d.ts +8 -0
  9. package/dist/backend/backend/src/controllers/project-tickets/project-tickets.controller.d.ts.map +1 -1
  10. package/dist/backend/backend/src/controllers/project-tickets/project-tickets.controller.js +18 -1
  11. package/dist/backend/backend/src/controllers/project-tickets/project-tickets.controller.js.map +1 -1
  12. package/dist/backend/backend/src/index.d.ts.map +1 -1
  13. package/dist/backend/backend/src/index.js +36 -14
  14. package/dist/backend/backend/src/index.js.map +1 -1
  15. package/dist/backend/backend/src/services/agent/active-work-briefing.service.d.ts +11 -0
  16. package/dist/backend/backend/src/services/agent/active-work-briefing.service.d.ts.map +1 -1
  17. package/dist/backend/backend/src/services/agent/active-work-briefing.service.js +32 -11
  18. package/dist/backend/backend/src/services/agent/active-work-briefing.service.js.map +1 -1
  19. package/dist/backend/backend/src/services/agent/follow-through.service.d.ts +132 -0
  20. package/dist/backend/backend/src/services/agent/follow-through.service.d.ts.map +1 -0
  21. package/dist/backend/backend/src/services/agent/follow-through.service.js +199 -0
  22. package/dist/backend/backend/src/services/agent/follow-through.service.js.map +1 -0
  23. package/dist/backend/backend/src/services/agent/follow-through.wiring.d.ts +62 -0
  24. package/dist/backend/backend/src/services/agent/follow-through.wiring.d.ts.map +1 -0
  25. package/dist/backend/backend/src/services/agent/follow-through.wiring.js +158 -0
  26. package/dist/backend/backend/src/services/agent/follow-through.wiring.js.map +1 -0
  27. package/dist/backend/backend/src/services/agent/idle-detection.service.d.ts +24 -6
  28. package/dist/backend/backend/src/services/agent/idle-detection.service.d.ts.map +1 -1
  29. package/dist/backend/backend/src/services/agent/idle-detection.service.js +46 -11
  30. package/dist/backend/backend/src/services/agent/idle-detection.service.js.map +1 -1
  31. package/dist/backend/backend/src/services/agent/idle-work-guard.d.ts +61 -0
  32. package/dist/backend/backend/src/services/agent/idle-work-guard.d.ts.map +1 -0
  33. package/dist/backend/backend/src/services/agent/idle-work-guard.js +81 -0
  34. package/dist/backend/backend/src/services/agent/idle-work-guard.js.map +1 -0
  35. package/dist/backend/backend/src/services/agent/stated-intent.d.ts +55 -0
  36. package/dist/backend/backend/src/services/agent/stated-intent.d.ts.map +1 -0
  37. package/dist/backend/backend/src/services/agent/stated-intent.js +142 -0
  38. package/dist/backend/backend/src/services/agent/stated-intent.js.map +1 -0
  39. package/dist/backend/backend/src/services/briefing/briefing.service.d.ts.map +1 -1
  40. package/dist/backend/backend/src/services/briefing/briefing.service.js +3 -0
  41. package/dist/backend/backend/src/services/briefing/briefing.service.js.map +1 -1
  42. package/dist/backend/backend/src/services/drive/drive-briefing-snapshot.d.ts +17 -0
  43. package/dist/backend/backend/src/services/drive/drive-briefing-snapshot.d.ts.map +1 -1
  44. package/dist/backend/backend/src/services/drive/drive-briefing-snapshot.js +64 -3
  45. package/dist/backend/backend/src/services/drive/drive-briefing-snapshot.js.map +1 -1
  46. package/dist/backend/backend/src/services/drive/drive-briefing.contract.d.ts +15 -0
  47. package/dist/backend/backend/src/services/drive/drive-briefing.contract.d.ts.map +1 -1
  48. package/dist/backend/backend/src/services/messaging/owner-thread-sentinel.service.d.ts +10 -0
  49. package/dist/backend/backend/src/services/messaging/owner-thread-sentinel.service.d.ts.map +1 -1
  50. package/dist/backend/backend/src/services/messaging/owner-thread-sentinel.service.js +25 -0
  51. package/dist/backend/backend/src/services/messaging/owner-thread-sentinel.service.js.map +1 -1
  52. package/dist/backend/backend/src/services/monitoring/agent-turn-state.d.ts +16 -0
  53. package/dist/backend/backend/src/services/monitoring/agent-turn-state.d.ts.map +1 -1
  54. package/dist/backend/backend/src/services/monitoring/agent-turn-state.js +31 -0
  55. package/dist/backend/backend/src/services/monitoring/agent-turn-state.js.map +1 -1
  56. package/dist/backend/backend/src/services/project-tickets/project-ticket-workflow.service.d.ts +17 -1
  57. package/dist/backend/backend/src/services/project-tickets/project-ticket-workflow.service.d.ts.map +1 -1
  58. package/dist/backend/backend/src/services/project-tickets/project-ticket-workflow.service.js +1 -1
  59. package/dist/backend/backend/src/services/project-tickets/project-ticket-workflow.service.js.map +1 -1
  60. package/dist/backend/backend/src/services/project-tickets/project-ticket.service.d.ts +5 -0
  61. package/dist/backend/backend/src/services/project-tickets/project-ticket.service.d.ts.map +1 -1
  62. package/dist/backend/backend/src/services/project-tickets/project-ticket.service.js +1 -1
  63. package/dist/backend/backend/src/services/project-tickets/project-ticket.service.js.map +1 -1
  64. package/dist/backend/backend/src/services/project-tickets/ticket-hygiene.d.ts +160 -0
  65. package/dist/backend/backend/src/services/project-tickets/ticket-hygiene.d.ts.map +1 -0
  66. package/dist/backend/backend/src/services/project-tickets/ticket-hygiene.js +256 -0
  67. package/dist/backend/backend/src/services/project-tickets/ticket-hygiene.js.map +1 -0
  68. package/dist/backend/backend/src/services/project-tickets/ticket-hygiene.service.d.ts +135 -0
  69. package/dist/backend/backend/src/services/project-tickets/ticket-hygiene.service.d.ts.map +1 -0
  70. package/dist/backend/backend/src/services/project-tickets/ticket-hygiene.service.js +366 -0
  71. package/dist/backend/backend/src/services/project-tickets/ticket-hygiene.service.js.map +1 -0
  72. package/dist/backend/backend/src/services/v3/ticket-hygiene.d.ts +22 -0
  73. package/dist/backend/backend/src/services/v3/ticket-hygiene.d.ts.map +1 -1
  74. package/dist/backend/backend/src/services/v3/ticket-hygiene.js +34 -1
  75. package/dist/backend/backend/src/services/v3/ticket-hygiene.js.map +1 -1
  76. package/dist/backend/backend/src/services/v3/ticket-intake.service.d.ts.map +1 -1
  77. package/dist/backend/backend/src/services/v3/ticket-intake.service.js +1 -3
  78. package/dist/backend/backend/src/services/v3/ticket-intake.service.js.map +1 -1
  79. package/dist/backend/backend/src/services/v3/ticket-review.service.d.ts +2 -0
  80. package/dist/backend/backend/src/services/v3/ticket-review.service.d.ts.map +1 -1
  81. package/dist/backend/backend/src/services/v3/ticket-review.service.js +40 -7
  82. package/dist/backend/backend/src/services/v3/ticket-review.service.js.map +1 -1
  83. package/dist/backend/backend/src/services/v3/ticket-work-guard.d.ts +81 -0
  84. package/dist/backend/backend/src/services/v3/ticket-work-guard.d.ts.map +1 -0
  85. package/dist/backend/backend/src/services/v3/ticket-work-guard.js +122 -0
  86. package/dist/backend/backend/src/services/v3/ticket-work-guard.js.map +1 -0
  87. package/dist/backend/backend/src/types/v2/work-item.types.d.ts +1 -1
  88. package/dist/backend/backend/src/types/v2/work-item.types.d.ts.map +1 -1
  89. package/dist/backend/backend/src/types/v2/work-item.types.js +1 -0
  90. package/dist/backend/backend/src/types/v2/work-item.types.js.map +1 -1
  91. package/dist/backend/build-info.json +2 -2
  92. package/dist/cli/backend/src/constants.d.ts +55 -0
  93. package/dist/cli/backend/src/constants.d.ts.map +1 -1
  94. package/dist/cli/backend/src/constants.js +56 -1
  95. package/dist/cli/backend/src/constants.js.map +1 -1
  96. package/dist/cli/backend/src/services/agent/active-work-briefing.service.d.ts +11 -0
  97. package/dist/cli/backend/src/services/agent/active-work-briefing.service.d.ts.map +1 -1
  98. package/dist/cli/backend/src/services/agent/active-work-briefing.service.js +32 -11
  99. package/dist/cli/backend/src/services/agent/active-work-briefing.service.js.map +1 -1
  100. package/dist/cli/backend/src/services/agent/follow-through.service.d.ts +132 -0
  101. package/dist/cli/backend/src/services/agent/follow-through.service.d.ts.map +1 -0
  102. package/dist/cli/backend/src/services/agent/follow-through.service.js +199 -0
  103. package/dist/cli/backend/src/services/agent/follow-through.service.js.map +1 -0
  104. package/dist/cli/backend/src/services/agent/stated-intent.d.ts +55 -0
  105. package/dist/cli/backend/src/services/agent/stated-intent.d.ts.map +1 -0
  106. package/dist/cli/backend/src/services/agent/stated-intent.js +142 -0
  107. package/dist/cli/backend/src/services/agent/stated-intent.js.map +1 -0
  108. package/dist/cli/backend/src/services/messaging/owner-thread-sentinel.service.d.ts +10 -0
  109. package/dist/cli/backend/src/services/messaging/owner-thread-sentinel.service.d.ts.map +1 -1
  110. package/dist/cli/backend/src/services/messaging/owner-thread-sentinel.service.js +25 -0
  111. package/dist/cli/backend/src/services/messaging/owner-thread-sentinel.service.js.map +1 -1
  112. package/dist/cli/backend/src/services/monitoring/agent-turn-state.d.ts +16 -0
  113. package/dist/cli/backend/src/services/monitoring/agent-turn-state.d.ts.map +1 -1
  114. package/dist/cli/backend/src/services/monitoring/agent-turn-state.js +31 -0
  115. package/dist/cli/backend/src/services/monitoring/agent-turn-state.js.map +1 -1
  116. package/dist/cli/backend/src/services/project-tickets/project-ticket.service.d.ts +5 -0
  117. package/dist/cli/backend/src/services/project-tickets/project-ticket.service.d.ts.map +1 -1
  118. package/dist/cli/backend/src/services/project-tickets/project-ticket.service.js +1 -1
  119. package/dist/cli/backend/src/services/project-tickets/project-ticket.service.js.map +1 -1
  120. package/dist/cli/backend/src/services/v3/ticket-hygiene.d.ts +22 -0
  121. package/dist/cli/backend/src/services/v3/ticket-hygiene.d.ts.map +1 -1
  122. package/dist/cli/backend/src/services/v3/ticket-hygiene.js +34 -1
  123. package/dist/cli/backend/src/services/v3/ticket-hygiene.js.map +1 -1
  124. package/dist/cli/backend/src/services/v3/ticket-intake.service.d.ts.map +1 -1
  125. package/dist/cli/backend/src/services/v3/ticket-intake.service.js +1 -3
  126. package/dist/cli/backend/src/services/v3/ticket-intake.service.js.map +1 -1
  127. package/dist/cli/backend/src/services/v3/ticket-review.service.d.ts +2 -0
  128. package/dist/cli/backend/src/services/v3/ticket-review.service.d.ts.map +1 -1
  129. package/dist/cli/backend/src/services/v3/ticket-review.service.js +40 -7
  130. package/dist/cli/backend/src/services/v3/ticket-review.service.js.map +1 -1
  131. package/dist/cli/backend/src/services/v3/ticket-work-guard.d.ts +81 -0
  132. package/dist/cli/backend/src/services/v3/ticket-work-guard.d.ts.map +1 -0
  133. package/dist/cli/backend/src/services/v3/ticket-work-guard.js +122 -0
  134. package/dist/cli/backend/src/services/v3/ticket-work-guard.js.map +1 -0
  135. package/dist/cli/backend/src/types/v2/work-item.types.d.ts +1 -1
  136. package/dist/cli/backend/src/types/v2/work-item.types.d.ts.map +1 -1
  137. package/dist/cli/backend/src/types/v2/work-item.types.js +1 -0
  138. package/dist/cli/backend/src/types/v2/work-item.types.js.map +1 -1
  139. package/package.json +1 -1
@@ -0,0 +1,256 @@
1
+ /**
2
+ * Ticket hygiene, pure half: when is a ticket stale, which stale tickets go to
3
+ * which lead today, and the brief that lead receives.
4
+ *
5
+ * Kept free of I/O so the thresholds, the cooldown and the batching are easy to
6
+ * test; {@link ./ticket-hygiene.service} does the reading and writing.
7
+ *
8
+ * @module services/project-tickets/ticket-hygiene
9
+ */
10
+ import { ORCHESTRATOR_SESSION_NAME, TICKET_HYGIENE_CONSTANTS } from '../../constants.js';
11
+ import { getTeamLeads } from '../../utils/team.utils.js';
12
+ const C = TICKET_HYGIENE_CONSTANTS;
13
+ const DAY_MS = 24 * 60 * 60 * 1000;
14
+ /** Statuses of a ticket that still counts as open work. */
15
+ export const OPEN_TICKET_STATUSES = new Set(['backlog', 'ready', 'in_progress', 'review']);
16
+ /** Labels that say "leave this alone for now" (the autopilot's park / owner-wait labels). */
17
+ const QUIET_LABELS = new Set(['parked', 'deferred', 'needs-owner']);
18
+ /**
19
+ * The days of silence after which a ticket of this status is stale.
20
+ *
21
+ * @param status - Ticket status
22
+ * @returns Days, or null when the status is not open
23
+ */
24
+ export function staleThresholdDays(status) {
25
+ if (status === 'in_progress' || status === 'review')
26
+ return C.STALE_ACTIVE_DAYS;
27
+ if (status === 'ready' || status === 'backlog')
28
+ return C.STALE_IDLE_DAYS;
29
+ return null;
30
+ }
31
+ /**
32
+ * The time of a ticket's last real activity: the later of `updatedAt` and its
33
+ * newest Log line, ignoring lines the hygiene sweep wrote itself (a flag the
34
+ * sweep adds is not someone working on the ticket).
35
+ *
36
+ * @param t - Ticket
37
+ * @returns Epoch ms (0 when nothing parses)
38
+ */
39
+ export function ticketLastActivityMs(t) {
40
+ let best = Date.parse(t.updatedAt);
41
+ if (!Number.isFinite(best))
42
+ best = Date.parse(t.createdAt);
43
+ if (!Number.isFinite(best))
44
+ best = 0;
45
+ for (const line of t.log ?? []) {
46
+ const m = /^(\S+) · ([^·]+?) · /.exec(line);
47
+ if (!m || m[2] === C.ACTOR)
48
+ continue;
49
+ const at = Date.parse(m[1]);
50
+ if (Number.isFinite(at) && at > best)
51
+ best = at;
52
+ }
53
+ return best;
54
+ }
55
+ /**
56
+ * Whether a ticket is stale right now.
57
+ *
58
+ * Not stale: closed tickets, tickets labelled parked / deferred / needs-owner,
59
+ * tickets deferred to a later date, and a `review` ticket that waits for the
60
+ * owner (`ownerReview`) — nobody on the team can decide that one.
61
+ *
62
+ * @param t - Ticket
63
+ * @param nowMs - Clock
64
+ * @returns Staleness
65
+ */
66
+ export function ticketStaleness(t, nowMs) {
67
+ const lastMs = ticketLastActivityMs(t);
68
+ const thresholdDays = staleThresholdDays(t.status);
69
+ const idleDays = lastMs > 0 ? Math.max(0, Math.floor((nowMs - lastMs) / DAY_MS)) : 0;
70
+ const base = { idleDays, lastActivityAt: new Date(lastMs).toISOString(), thresholdDays };
71
+ if (thresholdDays === null || lastMs === 0)
72
+ return { ...base, stale: false };
73
+ if ((t.labels ?? []).some((l) => QUIET_LABELS.has(l)))
74
+ return { ...base, stale: false };
75
+ if (t.status === 'review' && t.ownerReview)
76
+ return { ...base, stale: false };
77
+ if (t.deferUntil) {
78
+ const until = Date.parse(t.deferUntil);
79
+ if (Number.isFinite(until) && until > nowMs)
80
+ return { ...base, stale: false };
81
+ }
82
+ return { ...base, stale: nowMs - lastMs >= thresholdDays * DAY_MS };
83
+ }
84
+ /**
85
+ * Whether a name looks like an agent session (`<team>-<name>-<8 hex>`), as
86
+ * opposed to a person's name. Only agent-looking assignees can be orphaned.
87
+ *
88
+ * @param name - Assignee
89
+ * @returns True for a session-shaped name
90
+ */
91
+ export function looksLikeAgentSession(name) {
92
+ return /^[a-z0-9][a-z0-9-]*-[0-9a-f]{8}$/.test(name);
93
+ }
94
+ /**
95
+ * Whether a ticket's assignee or team no longer exists.
96
+ *
97
+ * @param t - Ticket
98
+ * @param teams - All teams
99
+ * @returns The missing parts, or null when both (that are set) exist
100
+ */
101
+ export function orphanReason(t, teams) {
102
+ const live = teams.filter((x) => !x.archived);
103
+ const reason = {};
104
+ if (t.team && !live.some((x) => x.id === t.team))
105
+ reason.team = t.team;
106
+ if (t.assignee && t.assignee !== ORCHESTRATOR_SESSION_NAME && looksLikeAgentSession(t.assignee)) {
107
+ const known = live.some((x) => (x.members ?? []).some((m) => m.sessionName === t.assignee || m.agentId === t.assignee));
108
+ if (!known)
109
+ reason.assignee = t.assignee;
110
+ }
111
+ return reason.assignee || reason.team ? reason : null;
112
+ }
113
+ /** Key of a ticket in the "already sent" memory. */
114
+ export function ticketKey(projectPath, id) {
115
+ return `${projectPath}#${id}`;
116
+ }
117
+ /**
118
+ * The session that decides a team's stale tickets: its first lead that has a
119
+ * session, else the orchestrator.
120
+ *
121
+ * @param team - Team (null = no team)
122
+ * @returns Session and whether it is the fallback
123
+ */
124
+ export function reviewTargetOf(team) {
125
+ if (team) {
126
+ const lead = getTeamLeads(team).find((m) => !!(m.sessionName || m.agentId));
127
+ if (lead)
128
+ return { target: (lead.sessionName || lead.agentId), fallback: false };
129
+ }
130
+ return { target: ORCHESTRATOR_SESSION_NAME, fallback: true };
131
+ }
132
+ /**
133
+ * Group the stale tickets by owning team, drop the ones sent within the
134
+ * cooldown, and cap each batch. A team with nothing stale has no batch.
135
+ *
136
+ * A ticket belongs to its `team`, else to the team its assignee is on, else to
137
+ * the only team working on its project, else to the orchestrator.
138
+ *
139
+ * @param input - Tickets, teams, clock, memory
140
+ * @returns One batch per team that has something to decide
141
+ */
142
+ export function selectReviewBatches(input) {
143
+ const live = input.teams.filter((t) => !t.archived);
144
+ const byId = new Map(live.map((t) => [t.id, t]));
145
+ const teamOfSession = new Map();
146
+ for (const t of live)
147
+ for (const m of t.members ?? [])
148
+ for (const s of [m.sessionName, m.agentId])
149
+ if (s && !teamOfSession.has(s))
150
+ teamOfSession.set(s, t);
151
+ const cooldownMs = C.REVIEW_COOLDOWN_DAYS * DAY_MS;
152
+ const groups = new Map();
153
+ for (const ticket of input.tickets) {
154
+ if (!OPEN_TICKET_STATUSES.has(ticket.status))
155
+ continue;
156
+ const staleness = ticketStaleness(ticket, input.nowMs);
157
+ const orphan = input.orphans?.get(ticketKey(ticket.projectPath, ticket.id));
158
+ const reasons = [];
159
+ if (staleness.stale)
160
+ reasons.push('stale');
161
+ if (orphan)
162
+ reasons.push('orphaned');
163
+ if (reasons.length === 0)
164
+ continue;
165
+ const sent = input.sentAt[ticketKey(ticket.projectPath, ticket.id)];
166
+ if (sent !== undefined && input.nowMs - sent < cooldownMs)
167
+ continue;
168
+ const projectTeams = live.filter((t) => (t.projectIds ?? []).includes(ticket.projectId));
169
+ const team = (ticket.team && byId.get(ticket.team)) ||
170
+ (ticket.assignee ? teamOfSession.get(ticket.assignee) : undefined) ||
171
+ // A project with exactly one team: that team owns its unowned tickets.
172
+ (projectTeams.length === 1 ? projectTeams[0] : undefined) ||
173
+ null;
174
+ if (team && input.skipTeamIds?.has(team.id))
175
+ continue;
176
+ const key = team ? team.id : 'orchestrator';
177
+ const g = groups.get(key) ?? { team, list: [] };
178
+ g.list.push({ ticket, staleness, reasons });
179
+ groups.set(key, g);
180
+ }
181
+ const batches = [];
182
+ for (const [key, g] of groups) {
183
+ const sorted = g.list.sort((a, b) => b.staleness.idleDays - a.staleness.idleDays || a.ticket.projectPath.localeCompare(b.ticket.projectPath) || a.ticket.id.localeCompare(b.ticket.id, undefined, { numeric: true }));
184
+ const { target, fallback } = reviewTargetOf(g.team);
185
+ batches.push({
186
+ key,
187
+ teamId: g.team?.id ?? null,
188
+ teamName: g.team?.name ?? 'No team',
189
+ target,
190
+ fallbackToOrchestrator: fallback,
191
+ candidates: sorted.slice(0, C.MAX_TICKETS_PER_REVIEW),
192
+ more: Math.max(0, sorted.length - C.MAX_TICKETS_PER_REVIEW),
193
+ });
194
+ }
195
+ return batches.sort((a, b) => a.key.localeCompare(b.key));
196
+ }
197
+ /**
198
+ * The brief the lead receives (WorkItem `briefMarkdown`): the list, the four
199
+ * ways to decide, and the rule that nothing here goes to the owner.
200
+ *
201
+ * @param batch - The batch
202
+ * @returns Markdown
203
+ */
204
+ export function buildReviewBrief(batch) {
205
+ const tk = '$AGENT_SKILLS_PATH/core/project-tickets/execute.sh';
206
+ const n = batch.candidates.length;
207
+ const lines = [
208
+ `# Ticket review — ${batch.teamName} (${n} ticket${n === 1 ? '' : 's'})`,
209
+ '',
210
+ 'These tickets have had no activity for a while, so status views (Drive mode, boards) may show them wrongly.',
211
+ `Decide every ticket below, then complete this WorkItem (complete-task with its id) with one line per ticket saying what you did. This is internal housekeeping: do not message the owner about it.`,
212
+ '',
213
+ '## For each ticket, do one of these',
214
+ '',
215
+ `1. **Finished** — a ticket in progress or in review: \`bash ${tk} update --project <PROJECT> --id <ID> --status done --note "<what shipped / where>"\`. A backlog or ready ticket cannot go straight to done: cancel it with the note "already done: <evidence>".`,
216
+ `2. **No longer wanted** — \`bash ${tk} update --project <PROJECT> --id <ID> --status cancelled --note "<why>"\`.`,
217
+ `3. **Still valid** — leave it and say so: \`bash ${tk} log --project <PROJECT> --id <ID> --note "still valid: <one line — status / next step / blocker>"\`. A ticket not worth doing soon: \`… update --project <PROJECT> --id <ID> --defer-until YYYY-MM-DD\`.`,
218
+ `4. **Status is wrong** — move it (\`… update --project <PROJECT> --id <ID> --status ready|review|…\`) or reassign it (\`… assign --project <PROJECT> --id <ID> --to <member>\`) with a one-line note. A ticket marked orphaned lost its assignee or team: give it a new owner or cancel it.`,
219
+ '',
220
+ 'Look at the ticket (`… show --project <PROJECT> --id <ID>`) and its linked work before deciding; do not close work you cannot confirm. Any of these actions counts as the review, so a reviewed ticket is not listed again for a few days.',
221
+ '',
222
+ '## Tickets',
223
+ '',
224
+ ];
225
+ for (const c of batch.candidates) {
226
+ const t = c.ticket;
227
+ const last = c.staleness.lastActivityAt.slice(0, 10);
228
+ const bits = [
229
+ `**${t.id}**`,
230
+ `[${t.status}]`,
231
+ clip(t.title, C.TITLE_MAX_CHARS),
232
+ `project \`${t.projectId}\` (${t.projectName})`,
233
+ `no activity for ${c.staleness.idleDays} day${c.staleness.idleDays === 1 ? '' : 's'} (last ${last})`,
234
+ t.assignee ? `assignee ${t.assignee}` : 'unassigned',
235
+ ];
236
+ if (c.reasons.includes('orphaned'))
237
+ bits.push('ORPHANED (assignee or team no longer exists)');
238
+ lines.push(`- ${bits.join(' · ')}`);
239
+ }
240
+ if (batch.more > 0)
241
+ lines.push('', `${batch.more} more stale ticket${batch.more === 1 ? '' : 's'} will be listed in a later review.`);
242
+ lines.push('', `List all tickets of a project: \`bash ${tk} list --project <PROJECT>\`.`);
243
+ return lines.join('\n');
244
+ }
245
+ /**
246
+ * Cut a one-line string.
247
+ *
248
+ * @param text - Text
249
+ * @param max - Max characters
250
+ * @returns Text on one line, within `max`
251
+ */
252
+ function clip(text, max) {
253
+ const one = String(text ?? '').replace(/\s+/g, ' ').trim();
254
+ return one.length <= max ? one : `${one.slice(0, max - 1)}…`;
255
+ }
256
+ //# sourceMappingURL=ticket-hygiene.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ticket-hygiene.js","sourceRoot":"","sources":["../../../../../../backend/src/services/project-tickets/ticket-hygiene.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,135 @@
1
+ /**
2
+ * Ticket hygiene service — keeps project tickets honest.
3
+ *
4
+ * Two jobs, both cheap:
5
+ *
6
+ * 1. **Auto-reconcile (no agent).** Each sweep closes or advances tickets
7
+ * whose state is objectively settled (the linked WorkItem is verified /
8
+ * done) and flags tickets whose assignee or team no longer exists. Every
9
+ * change is logged and written to the ticket's Log with the reason
10
+ * `auto-reconcile`.
11
+ * 2. **Stale review (one lead, once a day).** Open tickets with no activity
12
+ * past a threshold are batched into ONE WorkItem per team for the team lead
13
+ * (the orchestrator when the team has none) to close, cancel or annotate.
14
+ *
15
+ * Loop safety: at most one live review item per team, at most one batch per
16
+ * team per day, a ticket is not re-sent within the cooldown, and the sweep's
17
+ * own Log lines never count as ticket activity. Nothing here messages the owner.
18
+ *
19
+ * @module services/project-tickets/ticket-hygiene.service
20
+ */
21
+ import { type ComponentLogger } from '../core/logger.service.js';
22
+ import { type WorkItem } from '../../types/v2/work-item.types.js';
23
+ import type { ProjectTicketService } from './project-ticket.service.js';
24
+ import { type ChainEnd, type ProjectTicketDirectory } from './project-ticket-workflow.service.js';
25
+ /** The pool operations the service uses. */
26
+ export interface TicketHygienePool {
27
+ getAllItems(): Promise<WorkItem[]>;
28
+ addToPool(workItem: WorkItem): Promise<void>;
29
+ cancelQueued(workItemId: string, reason: string): Promise<void>;
30
+ }
31
+ /** Dependencies (injectable for tests). */
32
+ export interface TicketHygieneDeps {
33
+ tickets: Pick<ProjectTicketService, 'list' | 'mutate'>;
34
+ pool: TicketHygienePool;
35
+ directory: ProjectTicketDirectory;
36
+ /** Follow a WorkItem to where its work stands (the workflow service's) */
37
+ followChain: (workItemId: string) => Promise<ChainEnd>;
38
+ /** Where the bookkeeping lives */
39
+ stateFile: string;
40
+ logger?: ComponentLogger;
41
+ now?: () => Date;
42
+ }
43
+ /** One automatic change, as logged. */
44
+ export interface HygieneChange {
45
+ projectId: string;
46
+ ticketId: string;
47
+ kind: 'closed' | 'advanced' | 'flagged' | 'unflagged';
48
+ message: string;
49
+ }
50
+ /** What a run did. */
51
+ export interface HygieneRunResult {
52
+ changes: HygieneChange[];
53
+ /** Review items created (one per team) */
54
+ reviewItems: Array<{
55
+ teamKey: string;
56
+ target: string;
57
+ workItemId: string;
58
+ tickets: number;
59
+ }>;
60
+ }
61
+ export declare class TicketHygieneService {
62
+ private readonly deps;
63
+ private static instance;
64
+ private readonly logger;
65
+ private state;
66
+ private timer;
67
+ private firstTimer;
68
+ private running;
69
+ /**
70
+ * @param deps - Ticket store, pool, directory, chain follower, state file
71
+ */
72
+ constructor(deps: TicketHygieneDeps);
73
+ /** @returns The wired instance, or null */
74
+ static getInstance(): TicketHygieneService | null;
75
+ /** @param service - Instance to install (null clears) */
76
+ static setInstance(service: TicketHygieneService | null): void;
77
+ /**
78
+ * Start the periodic sweep: a first run a few minutes after boot, then every
79
+ * {@link TICKET_HYGIENE_CONSTANTS.SWEEP_INTERVAL_MS}.
80
+ *
81
+ * @param intervalMs - Sweep interval (0 disables the timers)
82
+ * @param firstDelayMs - Delay of the first run
83
+ */
84
+ start(intervalMs?: number, firstDelayMs?: number): void;
85
+ /** Stop the timers. */
86
+ stop(): void;
87
+ /**
88
+ * One sweep: reconcile settled / orphaned tickets, then queue today's lead
89
+ * reviews. Safe to call at any time (a run already in progress is skipped).
90
+ *
91
+ * @returns What changed and which review items went out
92
+ */
93
+ runOnce(): Promise<HygieneRunResult>;
94
+ /**
95
+ * Close or advance an `in_progress` / `review` ticket whose linked work is
96
+ * all verified / done, honouring the done gate: a ticket with `ownerReview`
97
+ * lands in `review` (and stays there, waiting for the owner) instead of `done`.
98
+ *
99
+ * Only these two statuses are touched, so a person's move of a ticket back to
100
+ * `ready` / `backlog` is never fought.
101
+ *
102
+ * @param projectId - Project id (for the log)
103
+ * @param ticket - The ticket
104
+ * @param poolItems - Pool snapshot
105
+ * @returns The change, or null when nothing is settled
106
+ */
107
+ private reconcileSettled;
108
+ /**
109
+ * Keep the `orphaned` label in step with whether the assignee / team still
110
+ * exist. Bookkeeping only: `updatedAt` is left alone so a flag never makes a
111
+ * stale ticket look fresh.
112
+ *
113
+ * @param projectId - Project id (for the log)
114
+ * @param ticket - The ticket
115
+ * @param orphan - Missing parts, or null
116
+ * @returns The change, or null when the label already matches
117
+ */
118
+ private reconcileOrphanFlag;
119
+ /**
120
+ * Send today's batches: one WorkItem per team that has stale tickets, no
121
+ * more than one per team per day, none while a previous one is still live.
122
+ *
123
+ * @param tickets - Open tickets after reconcile
124
+ * @param teams - All teams
125
+ * @param orphans - Orphaned tickets
126
+ * @param nowMs - Clock
127
+ * @returns The items created
128
+ */
129
+ private queueReviews;
130
+ private now;
131
+ private pruneSent;
132
+ private loadState;
133
+ private saveState;
134
+ }
135
+ //# sourceMappingURL=ticket-hygiene.service.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ticket-hygiene.service.d.ts","sourceRoot":"","sources":["../../../../../../backend/src/services/project-tickets/ticket-hygiene.service.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAIH,OAAO,EAAiB,KAAK,eAAe,EAAE,MAAM,2BAA2B,CAAC;AAEhF,OAAO,EAA4C,KAAK,QAAQ,EAAE,MAAM,mCAAmC,CAAC;AAI5G,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,6BAA6B,CAAC;AACxE,OAAO,EAAiB,KAAK,QAAQ,EAAE,KAAK,sBAAsB,EAAE,MAAM,sCAAsC,CAAC;AAcjH,4CAA4C;AAC5C,MAAM,WAAW,iBAAiB;IAChC,WAAW,IAAI,OAAO,CAAC,QAAQ,EAAE,CAAC,CAAC;IACnC,SAAS,CAAC,QAAQ,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC7C,YAAY,CAAC,UAAU,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACjE;AAED,2CAA2C;AAC3C,MAAM,WAAW,iBAAiB;IAChC,OAAO,EAAE,IAAI,CAAC,oBAAoB,EAAE,MAAM,GAAG,QAAQ,CAAC,CAAC;IACvD,IAAI,EAAE,iBAAiB,CAAC;IACxB,SAAS,EAAE,sBAAsB,CAAC;IAClC,0EAA0E;IAC1E,WAAW,EAAE,CAAC,UAAU,EAAE,MAAM,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAC;IACvD,kCAAkC;IAClC,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,eAAe,CAAC;IACzB,GAAG,CAAC,EAAE,MAAM,IAAI,CAAC;CAClB;AAUD,uCAAuC;AACvC,MAAM,WAAW,aAAa;IAC5B,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,QAAQ,GAAG,UAAU,GAAG,SAAS,GAAG,WAAW,CAAC;IACtD,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,sBAAsB;AACtB,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,aAAa,EAAE,CAAC;IACzB,0CAA0C;IAC1C,WAAW,EAAE,KAAK,CAAC;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CAC9F;AAED,qBAAa,oBAAoB;IAYnB,OAAO,CAAC,QAAQ,CAAC,IAAI;IAXjC,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAqC;IAE5D,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAkB;IACzC,OAAO,CAAC,KAAK,CAA6B;IAC1C,OAAO,CAAC,KAAK,CAA+C;IAC5D,OAAO,CAAC,UAAU,CAA8C;IAChE,OAAO,CAAC,OAAO,CAAS;IAExB;;OAEG;gBAC0B,IAAI,EAAE,iBAAiB;IAIpD,2CAA2C;IAC3C,MAAM,CAAC,WAAW,IAAI,oBAAoB,GAAG,IAAI;IAIjD,yDAAyD;IACzD,MAAM,CAAC,WAAW,CAAC,OAAO,EAAE,oBAAoB,GAAG,IAAI,GAAG,IAAI;IAI9D;;;;;;OAMG;IACH,KAAK,CAAC,UAAU,GAAE,MAA4B,EAAE,YAAY,GAAE,MAA+B,GAAG,IAAI;IAcpG,uBAAuB;IACvB,IAAI,IAAI,IAAI;IAOZ;;;;;OAKG;IACG,OAAO,IAAI,OAAO,CAAC,gBAAgB,CAAC;IA4D1C;;;;;;;;;;;;OAYG;YACW,gBAAgB;IAiC9B;;;;;;;;;OASG;YACW,mBAAmB;IAwBjC;;;;;;;;;OASG;YACW,YAAY;IA6E1B,OAAO,CAAC,GAAG;IAIX,OAAO,CAAC,SAAS;YAKH,SAAS;YAUT,SAAS;CAQxB"}