@proletariat/cli 0.3.111 → 0.3.113
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/commands/gateway/connect.d.ts +33 -0
- package/dist/commands/gateway/connect.js +130 -0
- package/dist/commands/gateway/connect.js.map +1 -0
- package/dist/commands/gateway/disconnect.d.ts +21 -0
- package/dist/commands/gateway/disconnect.js +69 -0
- package/dist/commands/gateway/disconnect.js.map +1 -0
- package/dist/commands/gateway/start.d.ts +23 -0
- package/dist/commands/gateway/start.js +133 -0
- package/dist/commands/gateway/start.js.map +1 -0
- package/dist/commands/gateway/status.d.ts +16 -0
- package/dist/commands/gateway/status.js +76 -0
- package/dist/commands/gateway/status.js.map +1 -0
- package/dist/commands/gateway/test.d.ts +22 -0
- package/dist/commands/gateway/test.js +83 -0
- package/dist/commands/gateway/test.js.map +1 -0
- package/dist/commands/orchestrate/index.js +11 -2
- package/dist/commands/orchestrate/index.js.map +1 -1
- package/dist/commands/pr/merge.js +44 -80
- package/dist/commands/pr/merge.js.map +1 -1
- package/dist/commands/reconcile.d.ts +29 -0
- package/dist/commands/reconcile.js +140 -0
- package/dist/commands/reconcile.js.map +1 -0
- package/dist/commands/work/ship.js +131 -61
- package/dist/commands/work/ship.js.map +1 -1
- package/dist/commands/work/start.js +30 -0
- package/dist/commands/work/start.js.map +1 -1
- package/dist/lib/events/events.d.ts +19 -0
- package/dist/lib/execution/prompt-watcher.d.ts +120 -0
- package/dist/lib/execution/prompt-watcher.js +222 -0
- package/dist/lib/execution/prompt-watcher.js.map +1 -0
- package/dist/lib/execution/spawner.js +31 -0
- package/dist/lib/execution/spawner.js.map +1 -1
- package/dist/lib/gateway/channel-factory.d.ts +13 -0
- package/dist/lib/gateway/channel-factory.js +37 -0
- package/dist/lib/gateway/channel-factory.js.map +1 -0
- package/dist/lib/gateway/channels/telegram.d.ts +115 -0
- package/dist/lib/gateway/channels/telegram.js +215 -0
- package/dist/lib/gateway/channels/telegram.js.map +1 -0
- package/dist/lib/gateway/router.d.ts +84 -0
- package/dist/lib/gateway/router.js +140 -0
- package/dist/lib/gateway/router.js.map +1 -0
- package/dist/lib/gateway/session-poker.d.ts +35 -0
- package/dist/lib/gateway/session-poker.js +85 -0
- package/dist/lib/gateway/session-poker.js.map +1 -0
- package/dist/lib/gateway/types.d.ts +124 -0
- package/dist/lib/gateway/types.js +17 -0
- package/dist/lib/gateway/types.js.map +1 -0
- package/dist/lib/machine-db.d.ts +87 -0
- package/dist/lib/machine-db.js +135 -0
- package/dist/lib/machine-db.js.map +1 -1
- package/dist/lib/orchestrate/index.d.ts +1 -1
- package/dist/lib/orchestrate/index.js +1 -1
- package/dist/lib/orchestrate/index.js.map +1 -1
- package/dist/lib/orchestrate/llm-agent.d.ts +7 -0
- package/dist/lib/orchestrate/llm-agent.js +48 -1
- package/dist/lib/orchestrate/llm-agent.js.map +1 -1
- package/dist/lib/pr/index.d.ts +43 -2
- package/dist/lib/pr/index.js +141 -4
- package/dist/lib/pr/index.js.map +1 -1
- package/dist/lib/reconcile/core.d.ts +62 -0
- package/dist/lib/reconcile/core.js +137 -0
- package/dist/lib/reconcile/core.js.map +1 -0
- package/dist/lib/reconcile/index.d.ts +59 -0
- package/dist/lib/reconcile/index.js +499 -0
- package/dist/lib/reconcile/index.js.map +1 -0
- package/dist/lib/reconcile/types.d.ts +150 -0
- package/dist/lib/reconcile/types.js +16 -0
- package/dist/lib/reconcile/types.js.map +1 -0
- package/oclif.manifest.json +1021 -679
- package/package.json +1 -1
|
@@ -0,0 +1,499 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tier 2 State Reconciler — Runner (PRLT-1280)
|
|
3
|
+
*
|
|
4
|
+
* This module owns the IO: it talks to the database, the GitHub CLI, and
|
|
5
|
+
* the provider adapter layer. The pure rule logic lives in ./core.ts so
|
|
6
|
+
* it can be unit-tested without these dependencies.
|
|
7
|
+
*
|
|
8
|
+
* Scope (narrow — the ticket is very explicit about what this is NOT):
|
|
9
|
+
* - Single idempotent reconciliation function on a schedule
|
|
10
|
+
* - Provider-agnostic — the reconciler does not branch on provider type
|
|
11
|
+
* - Fires normal state-transition code path so hooks & cleanup fire too
|
|
12
|
+
*
|
|
13
|
+
* Everything outside that — webhook listeners, LLM escalation, human
|
|
14
|
+
* inbox, supervision tree — is explicitly out of scope per PRLT-1280.
|
|
15
|
+
*/
|
|
16
|
+
import { PMO_TABLES } from '../pmo/schema.js';
|
|
17
|
+
import { resolveTicketProvider } from '../providers/resolver.js';
|
|
18
|
+
import { getPRForBranch, getPRByNumber, searchAllPRsForTicket } from '../pr/index.js';
|
|
19
|
+
import { getWorkflowConfig } from '../work-lifecycle/settings.js';
|
|
20
|
+
import { buildTransition, deriveExpectedState, formatTransitionLogLine } from './core.js';
|
|
21
|
+
export { deriveExpectedState, buildTransition, formatTransitionLogLine } from './core.js';
|
|
22
|
+
/**
|
|
23
|
+
* Non-terminal state categories that the reconciler scans.
|
|
24
|
+
*
|
|
25
|
+
* The ticket spec says:
|
|
26
|
+
* "Query all tickets in non-terminal states across all connected
|
|
27
|
+
* providers (Linear, PMO, Notion, Asana, Jira, Shortcut)"
|
|
28
|
+
*
|
|
29
|
+
* Terminal states (`completed`, `canceled`) are deliberately excluded —
|
|
30
|
+
* the reconciler never resurrects a closed ticket.
|
|
31
|
+
*/
|
|
32
|
+
const NON_TERMINAL_CATEGORIES = [
|
|
33
|
+
'triage',
|
|
34
|
+
'backlog',
|
|
35
|
+
'unstarted',
|
|
36
|
+
'started',
|
|
37
|
+
];
|
|
38
|
+
/**
|
|
39
|
+
* Default live PR lookup. Always goes to the remote — the Tier 2
|
|
40
|
+
* reconciler must not depend on any local PR cache.
|
|
41
|
+
*/
|
|
42
|
+
export const defaultPRLookup = (ref, cwd) => {
|
|
43
|
+
switch (ref.kind) {
|
|
44
|
+
case 'branch':
|
|
45
|
+
return getPRForBranch(ref.branch, cwd);
|
|
46
|
+
case 'number':
|
|
47
|
+
return getPRByNumber(ref.number, cwd);
|
|
48
|
+
case 'url': {
|
|
49
|
+
// `gh pr view <url>` works with either a number or URL; reuse it.
|
|
50
|
+
const num = parsePRNumberFromUrl(ref.url);
|
|
51
|
+
if (num !== null)
|
|
52
|
+
return getPRByNumber(num, cwd);
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
/**
|
|
58
|
+
* Default branch search. Searches GitHub for PRs whose head branch
|
|
59
|
+
* starts with any of the given ticket IDs.
|
|
60
|
+
*/
|
|
61
|
+
export const defaultBranchSearch = (ticketIds, cwd) => {
|
|
62
|
+
return searchAllPRsForTicket(ticketIds, cwd);
|
|
63
|
+
};
|
|
64
|
+
/**
|
|
65
|
+
* Run one reconciliation cycle.
|
|
66
|
+
*
|
|
67
|
+
* Strictly idempotent: if a ticket is already in its expected state the
|
|
68
|
+
* runner computes no transition and touches nothing. Running twice
|
|
69
|
+
* back-to-back produces no redundant side effects.
|
|
70
|
+
*/
|
|
71
|
+
export async function runReconcile(db, storage, options = {}) {
|
|
72
|
+
const startedAt = new Date();
|
|
73
|
+
const { dryRun = false, cwd, log, projectId } = options;
|
|
74
|
+
const prLookup = options.prLookup ?? defaultPRLookup;
|
|
75
|
+
const branchSearch = options.branchSearch ?? defaultBranchSearch;
|
|
76
|
+
// Workflow config is used to resolve "Done" / "In Progress" / "Review"
|
|
77
|
+
// column names for the ticket's board. Fall back gracefully if the
|
|
78
|
+
// settings table is missing (older databases, unit tests).
|
|
79
|
+
let workflowConfig;
|
|
80
|
+
try {
|
|
81
|
+
workflowConfig = getWorkflowConfig(db);
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
workflowConfig = undefined;
|
|
85
|
+
}
|
|
86
|
+
// Gather every non-terminal ticket in scope. Either one project if
|
|
87
|
+
// -P was passed, or every project in the workspace.
|
|
88
|
+
const projectIds = projectId ? [projectId] : await listAllProjectIds(storage);
|
|
89
|
+
const tickets = [];
|
|
90
|
+
for (const pid of projectIds) {
|
|
91
|
+
for (const category of NON_TERMINAL_CATEGORIES) {
|
|
92
|
+
try {
|
|
93
|
+
// eslint-disable-next-line no-await-in-loop -- sequential is fine; small N
|
|
94
|
+
const batch = await storage.listTickets(pid, { statusCategory: category });
|
|
95
|
+
tickets.push(...batch);
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
// Tolerate transient read errors on a single category; keep going.
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
log?.(`Scanning ${tickets.length} non-terminal ticket(s) across ${projectIds.length} project(s)...`);
|
|
103
|
+
const transitions = [];
|
|
104
|
+
const errors = [];
|
|
105
|
+
for (const ticket of tickets) {
|
|
106
|
+
try {
|
|
107
|
+
// eslint-disable-next-line no-await-in-loop -- sequential is fine; small N
|
|
108
|
+
const linkedPRs = await gatherLinkedPRs(db, storage, ticket, prLookup, branchSearch, cwd, log);
|
|
109
|
+
if (linkedPRs.length === 0) {
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
// Resolve the provider for this ticket so we can log which backend
|
|
113
|
+
// owns it. The actual move happens through the same resolver later
|
|
114
|
+
// in applyTransition() — this is just for the transition record.
|
|
115
|
+
const providerName = providerNameFor(db, storage, ticket);
|
|
116
|
+
const decision = deriveExpectedState(toReconcileTicket(ticket), linkedPRs, workflowConfig);
|
|
117
|
+
if (!decision)
|
|
118
|
+
continue;
|
|
119
|
+
transitions.push(buildTransition({
|
|
120
|
+
ticket: toReconcileTicket(ticket),
|
|
121
|
+
provider: providerName,
|
|
122
|
+
targetState: decision.targetState,
|
|
123
|
+
reason: decision.reason,
|
|
124
|
+
driver: decision.driver,
|
|
125
|
+
}));
|
|
126
|
+
}
|
|
127
|
+
catch (err) {
|
|
128
|
+
errors.push({ ticketId: ticket.id, error: err instanceof Error ? err.message : String(err) });
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
// Apply (or dry-run) every transition. Failures on one ticket do not
|
|
132
|
+
// stop the others — the reconciler is a best-effort safety net.
|
|
133
|
+
const applied = [];
|
|
134
|
+
const skipped = [];
|
|
135
|
+
const failed = [];
|
|
136
|
+
for (const transition of transitions) {
|
|
137
|
+
log?.(formatTransitionLogLine(transition));
|
|
138
|
+
if (dryRun) {
|
|
139
|
+
skipped.push(transition);
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
try {
|
|
143
|
+
// eslint-disable-next-line no-await-in-loop
|
|
144
|
+
await applyTransition(db, storage, transition);
|
|
145
|
+
applied.push(transition);
|
|
146
|
+
// Dual-identity tickets (PRLT-1288): reconcile BOTH sides.
|
|
147
|
+
// If primary provider is external (Linear, etc.), also update local PMO mirror.
|
|
148
|
+
// If primary provider is PMO, also update external provider if the ticket has one.
|
|
149
|
+
if (transition.provider !== 'pmo') {
|
|
150
|
+
// eslint-disable-next-line no-await-in-loop
|
|
151
|
+
await reconcilePmoMirror(db, storage, transition, log);
|
|
152
|
+
}
|
|
153
|
+
else {
|
|
154
|
+
// eslint-disable-next-line no-await-in-loop
|
|
155
|
+
await reconcileExternalMirror(db, storage, transition, log);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
catch (err) {
|
|
159
|
+
failed.push({
|
|
160
|
+
transition,
|
|
161
|
+
error: err instanceof Error ? err.message : String(err),
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
const finishedAt = new Date();
|
|
166
|
+
return {
|
|
167
|
+
checked: tickets.length,
|
|
168
|
+
applied,
|
|
169
|
+
skipped,
|
|
170
|
+
failed,
|
|
171
|
+
errors,
|
|
172
|
+
startedAt: startedAt.toISOString(),
|
|
173
|
+
finishedAt: finishedAt.toISOString(),
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
// -----------------------------------------------------------------------------
|
|
177
|
+
// PR lookup
|
|
178
|
+
// -----------------------------------------------------------------------------
|
|
179
|
+
/**
|
|
180
|
+
* Gather every PR linked to a ticket by:
|
|
181
|
+
* 1. the ticket's branch (if set),
|
|
182
|
+
* 2. PR URLs stored in `pmo_external_execution_prs` under any of the
|
|
183
|
+
* ticket's external-source identities, and
|
|
184
|
+
* 3. (PRLT-1288 fallback) searching GitHub by branch name pattern when
|
|
185
|
+
* sources 1 & 2 yield nothing.
|
|
186
|
+
*
|
|
187
|
+
* Every lookup goes through the live PR lookup function — no cache.
|
|
188
|
+
* When source 3 finds a PR, the ticket's metadata is backfilled with
|
|
189
|
+
* `pr_number` and `pr_url` so future cycles skip the search.
|
|
190
|
+
*/
|
|
191
|
+
async function gatherLinkedPRs(db, storage, ticket, prLookup, branchSearch, cwd, log) {
|
|
192
|
+
const results = [];
|
|
193
|
+
const seen = new Set(); // de-dupe by PR number
|
|
194
|
+
const push = (pr, source) => {
|
|
195
|
+
if (!pr)
|
|
196
|
+
return;
|
|
197
|
+
if (seen.has(pr.number))
|
|
198
|
+
return;
|
|
199
|
+
seen.add(pr.number);
|
|
200
|
+
results.push({ pr, source });
|
|
201
|
+
};
|
|
202
|
+
// 1. Ticket branch
|
|
203
|
+
if (ticket.branch) {
|
|
204
|
+
push(prLookup({ kind: 'branch', branch: ticket.branch }, cwd), 'ticket_branch');
|
|
205
|
+
}
|
|
206
|
+
// 2. PR URLs stored in the external execution map for this ticket.
|
|
207
|
+
// A ticket can carry multiple external identities (metadata.external_id,
|
|
208
|
+
// external_key) and can have been linked to multiple PRs over time.
|
|
209
|
+
const prUrls = listLinkedPrUrls(db, ticket);
|
|
210
|
+
for (const url of prUrls) {
|
|
211
|
+
push(prLookup({ kind: 'url', url }, cwd), 'external_map');
|
|
212
|
+
}
|
|
213
|
+
// 3. PRLT-1288: Branch search fallback — when sources 1 & 2 found nothing,
|
|
214
|
+
// search GitHub by branch name prefix using every known alias for this
|
|
215
|
+
// ticket (ticket ID, external_key, external_id).
|
|
216
|
+
if (results.length === 0) {
|
|
217
|
+
const searchIds = collectSearchIds(ticket);
|
|
218
|
+
if (searchIds.length > 0) {
|
|
219
|
+
const found = branchSearch(searchIds, cwd);
|
|
220
|
+
for (const pr of found) {
|
|
221
|
+
push(pr, 'branch_search');
|
|
222
|
+
}
|
|
223
|
+
// Backfill: write pr_number & pr_url onto ticket metadata so future
|
|
224
|
+
// reconcile cycles skip the search. Use the first (best) match.
|
|
225
|
+
if (results.length > 0) {
|
|
226
|
+
const best = results[0].pr;
|
|
227
|
+
await backfillPRMetadata(storage, ticket, best, log);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
return results;
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* Collect every identifier that can serve as a branch-name prefix when
|
|
235
|
+
* searching GitHub for PRs associated with a ticket.
|
|
236
|
+
*
|
|
237
|
+
* Covers: ticket ID (TKT-xxx), external_key (PRLT-xxx), external_id
|
|
238
|
+
* (if it looks like a ticket key, not a UUID).
|
|
239
|
+
*/
|
|
240
|
+
function collectSearchIds(ticket) {
|
|
241
|
+
const ids = new Set();
|
|
242
|
+
const md = ticket.metadata ?? {};
|
|
243
|
+
// The ticket's own ID (e.g. TKT-042)
|
|
244
|
+
ids.add(ticket.id);
|
|
245
|
+
// External key — typically the Linear identifier (e.g. PRLT-1266)
|
|
246
|
+
if (md.external_key)
|
|
247
|
+
ids.add(md.external_key);
|
|
248
|
+
// External ID — only include if it looks like a ticket key, not a UUID
|
|
249
|
+
if (md.external_id && /^[A-Z]+-\d+$/i.test(md.external_id)) {
|
|
250
|
+
ids.add(md.external_id);
|
|
251
|
+
}
|
|
252
|
+
return [...ids];
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* Backfill pr_number and pr_url onto a ticket's metadata so future
|
|
256
|
+
* reconcile cycles find the PR directly without searching GitHub.
|
|
257
|
+
*
|
|
258
|
+
* Best-effort: failures are logged but never block the reconcile cycle.
|
|
259
|
+
*/
|
|
260
|
+
async function backfillPRMetadata(storage, ticket, pr, log) {
|
|
261
|
+
try {
|
|
262
|
+
const updatedMetadata = {
|
|
263
|
+
...(ticket.metadata ?? {}),
|
|
264
|
+
pr_number: String(pr.number),
|
|
265
|
+
pr_url: pr.url,
|
|
266
|
+
};
|
|
267
|
+
await storage.updateTicket(ticket.id, { metadata: updatedMetadata });
|
|
268
|
+
log?.(`[reconcile] backfilled pr_number=${pr.number} on ${ticket.id} (found via branch search)`);
|
|
269
|
+
}
|
|
270
|
+
catch (err) {
|
|
271
|
+
log?.(`[reconcile] failed to backfill PR metadata on ${ticket.id}: ` +
|
|
272
|
+
(err instanceof Error ? err.message : String(err)));
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* Read every PR URL stored for this ticket in `pmo_external_execution_prs`.
|
|
277
|
+
* Keyed by (provider, external_id) — the reconciler doesn't care which
|
|
278
|
+
* provider it came from, so we union across all of them.
|
|
279
|
+
*/
|
|
280
|
+
function listLinkedPrUrls(db, ticket) {
|
|
281
|
+
const urls = new Set();
|
|
282
|
+
const md = ticket.metadata ?? {};
|
|
283
|
+
// Collect every identifier that might key the external mapping table.
|
|
284
|
+
const externalIds = new Set();
|
|
285
|
+
if (md.external_id)
|
|
286
|
+
externalIds.add(md.external_id);
|
|
287
|
+
if (md.external_key)
|
|
288
|
+
externalIds.add(md.external_key);
|
|
289
|
+
// A PMO-native ticket's id can also appear in the map under provider='pmo'.
|
|
290
|
+
externalIds.add(ticket.id);
|
|
291
|
+
if (externalIds.size === 0)
|
|
292
|
+
return [];
|
|
293
|
+
let stmt;
|
|
294
|
+
try {
|
|
295
|
+
stmt = db.prepare(`
|
|
296
|
+
SELECT pr_url FROM ${PMO_TABLES.external_execution_prs}
|
|
297
|
+
WHERE external_id = ?
|
|
298
|
+
`);
|
|
299
|
+
}
|
|
300
|
+
catch {
|
|
301
|
+
// Table may not exist yet (fresh DB or older schema).
|
|
302
|
+
return [];
|
|
303
|
+
}
|
|
304
|
+
for (const extId of externalIds) {
|
|
305
|
+
try {
|
|
306
|
+
const rows = stmt.all(extId);
|
|
307
|
+
for (const row of rows)
|
|
308
|
+
urls.add(row.pr_url);
|
|
309
|
+
}
|
|
310
|
+
catch {
|
|
311
|
+
// Non-fatal: skip this id.
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
return [...urls];
|
|
315
|
+
}
|
|
316
|
+
/**
|
|
317
|
+
* Parse a PR number out of a github.com URL.
|
|
318
|
+
* Accepts both `/pull/123` and `/pull/123/...` shapes.
|
|
319
|
+
*/
|
|
320
|
+
function parsePRNumberFromUrl(url) {
|
|
321
|
+
const m = url.match(/\/pull\/(\d+)(?:\b|\/)/);
|
|
322
|
+
if (!m)
|
|
323
|
+
return null;
|
|
324
|
+
const n = Number.parseInt(m[1], 10);
|
|
325
|
+
return Number.isFinite(n) ? n : null;
|
|
326
|
+
}
|
|
327
|
+
// -----------------------------------------------------------------------------
|
|
328
|
+
// Apply (goes through normal state-transition code path)
|
|
329
|
+
// -----------------------------------------------------------------------------
|
|
330
|
+
/**
|
|
331
|
+
* Apply a transition through the same provider adapter layer that
|
|
332
|
+
* `prlt ticket move` uses. This is load-bearing: it ensures hooks,
|
|
333
|
+
* provider sync, and worktree cleanup all fire.
|
|
334
|
+
*/
|
|
335
|
+
async function applyTransition(db, storage, transition) {
|
|
336
|
+
const ticket = await storage.getTicket(transition.ticketId);
|
|
337
|
+
if (!ticket) {
|
|
338
|
+
throw new Error(`Ticket ${transition.ticketId} vanished between scan and apply`);
|
|
339
|
+
}
|
|
340
|
+
const provider = resolveTicketProvider(ticket.id, ticket.projectId ?? transition.projectId, db, storage, ticket.metadata);
|
|
341
|
+
const result = await provider.moveTicket(ticket.id, transition.toState);
|
|
342
|
+
if (!result.success) {
|
|
343
|
+
throw new Error(result.error ?? 'moveTicket failed without an error message');
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
/**
|
|
347
|
+
* For tickets whose source of truth is an external provider (Linear,
|
|
348
|
+
* Jira, etc.), the local PMO board can still hold a mirror row. After
|
|
349
|
+
* successfully moving the external ticket, reconcile the local PMO side
|
|
350
|
+
* too so `prlt board` does not drift.
|
|
351
|
+
*
|
|
352
|
+
* Best effort — failures here never block the primary transition.
|
|
353
|
+
*/
|
|
354
|
+
async function reconcilePmoMirror(db, storage, transition, log) {
|
|
355
|
+
try {
|
|
356
|
+
const mirror = await storage.getTicket(transition.ticketId);
|
|
357
|
+
if (!mirror)
|
|
358
|
+
return;
|
|
359
|
+
// Only move if the mirror truly holds a different status — that way
|
|
360
|
+
// we stay idempotent and do not fire redundant hooks.
|
|
361
|
+
if (mirror.statusName && mirror.statusName.toLowerCase() === transition.toState.toLowerCase()) {
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
// Use the pmo provider directly (bypass resolver so we definitely
|
|
365
|
+
// hit the local mirror, not bounce back to Linear).
|
|
366
|
+
const pmoProvider = resolveTicketProvider(mirror.id, mirror.projectId ?? transition.projectId, db, storage,
|
|
367
|
+
// Strip external_source so the resolver picks PMO.
|
|
368
|
+
null);
|
|
369
|
+
if (pmoProvider.name !== 'pmo')
|
|
370
|
+
return;
|
|
371
|
+
const result = await pmoProvider.moveTicket(mirror.id, transition.toState);
|
|
372
|
+
if (!result.success && log) {
|
|
373
|
+
log(`[reconcile] pmo mirror update for ${mirror.id} failed: ${result.error}`);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
catch (err) {
|
|
377
|
+
log?.(`[reconcile] pmo mirror update for ${transition.ticketId} errored: ` +
|
|
378
|
+
(err instanceof Error ? err.message : String(err)));
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
/**
|
|
382
|
+
* For dual-identity tickets whose primary provider resolved to PMO
|
|
383
|
+
* (e.g. because external_source metadata was missing), check whether
|
|
384
|
+
* the ticket also has an external identity (Linear, Jira, etc.) and
|
|
385
|
+
* try to update that side too.
|
|
386
|
+
*
|
|
387
|
+
* Best effort — failures here never block the primary transition.
|
|
388
|
+
*/
|
|
389
|
+
async function reconcileExternalMirror(db, storage, transition, log) {
|
|
390
|
+
try {
|
|
391
|
+
const ticket = await storage.getTicket(transition.ticketId);
|
|
392
|
+
if (!ticket)
|
|
393
|
+
return;
|
|
394
|
+
const md = ticket.metadata ?? {};
|
|
395
|
+
// Only act if the ticket has external-provider metadata
|
|
396
|
+
if (!md.external_source && !md.external_key && !md.external_id)
|
|
397
|
+
return;
|
|
398
|
+
// Build metadata that forces the resolver to pick the external provider
|
|
399
|
+
const externalMetadata = { ...md };
|
|
400
|
+
// If external_source is missing but external_key/external_id exists,
|
|
401
|
+
// try to infer the source from the key format (PRLT-xxx → linear)
|
|
402
|
+
if (!externalMetadata.external_source && externalMetadata.external_key) {
|
|
403
|
+
if (/^PRLT-\d+$/.test(externalMetadata.external_key)) {
|
|
404
|
+
externalMetadata.external_source = 'linear';
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
if (!externalMetadata.external_source)
|
|
408
|
+
return;
|
|
409
|
+
const externalProvider = resolveTicketProvider(ticket.id, ticket.projectId ?? transition.projectId, db, storage, externalMetadata);
|
|
410
|
+
// Don't bounce back to PMO — that's the primary provider we already used
|
|
411
|
+
if (externalProvider.name === 'pmo')
|
|
412
|
+
return;
|
|
413
|
+
const result = await externalProvider.moveTicket(ticket.id, transition.toState);
|
|
414
|
+
if (result.success) {
|
|
415
|
+
log?.(`[reconcile] also moved ${ticket.id} on ${externalProvider.name} → ${transition.toState}`);
|
|
416
|
+
}
|
|
417
|
+
else if (log) {
|
|
418
|
+
log(`[reconcile] external mirror update for ${ticket.id} on ${externalProvider.name} failed: ${result.error}`);
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
catch (err) {
|
|
422
|
+
log?.(`[reconcile] external mirror update for ${transition.ticketId} errored: ` +
|
|
423
|
+
(err instanceof Error ? err.message : String(err)));
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
// -----------------------------------------------------------------------------
|
|
427
|
+
// Project / provider resolution helpers
|
|
428
|
+
// -----------------------------------------------------------------------------
|
|
429
|
+
async function listAllProjectIds(storage) {
|
|
430
|
+
try {
|
|
431
|
+
const projects = await storage.listProjects();
|
|
432
|
+
return projects.map(p => p.id);
|
|
433
|
+
}
|
|
434
|
+
catch {
|
|
435
|
+
return [];
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
/**
|
|
439
|
+
* Resolve the provider name for a ticket without triggering any state
|
|
440
|
+
* resolution or event emission. Used purely so the transition record
|
|
441
|
+
* can log which backend owns the ticket.
|
|
442
|
+
*/
|
|
443
|
+
function providerNameFor(db, storage, ticket) {
|
|
444
|
+
try {
|
|
445
|
+
const provider = resolveTicketProvider(ticket.id, ticket.projectId ?? '', db, storage, ticket.metadata);
|
|
446
|
+
return provider.name;
|
|
447
|
+
}
|
|
448
|
+
catch {
|
|
449
|
+
return 'pmo';
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
function toReconcileTicket(ticket) {
|
|
453
|
+
return {
|
|
454
|
+
id: ticket.id,
|
|
455
|
+
title: ticket.title,
|
|
456
|
+
projectId: ticket.projectId,
|
|
457
|
+
statusName: ticket.statusName,
|
|
458
|
+
statusCategory: ticket.statusCategory,
|
|
459
|
+
branch: ticket.branch,
|
|
460
|
+
metadata: ticket.metadata,
|
|
461
|
+
};
|
|
462
|
+
}
|
|
463
|
+
// -----------------------------------------------------------------------------
|
|
464
|
+
// Watch loop
|
|
465
|
+
// -----------------------------------------------------------------------------
|
|
466
|
+
/**
|
|
467
|
+
* Run the reconciler on a timer until the caller-supplied `shouldStop`
|
|
468
|
+
* callback returns true.
|
|
469
|
+
*
|
|
470
|
+
* Each iteration runs one full {@link runReconcile} cycle and then sleeps
|
|
471
|
+
* for `intervalMs` before the next one. Errors inside a cycle are caught
|
|
472
|
+
* and surfaced via `log` — a single bad cycle never kills the watcher.
|
|
473
|
+
*/
|
|
474
|
+
export async function watchReconcile(db, storage, options = {}) {
|
|
475
|
+
const { intervalMs = 5 * 60 * 1000, shouldStop = () => false, onCycle, sleep = ms => new Promise(resolve => setTimeout(resolve, ms)), log, } = options;
|
|
476
|
+
// Cycle counter for tests that want to bound the loop.
|
|
477
|
+
let cycle = 0;
|
|
478
|
+
while (!shouldStop()) {
|
|
479
|
+
try {
|
|
480
|
+
// eslint-disable-next-line no-await-in-loop
|
|
481
|
+
const report = await runReconcile(db, storage, options);
|
|
482
|
+
cycle += 1;
|
|
483
|
+
log?.(`[reconcile] cycle ${cycle} complete — checked=${report.checked} ` +
|
|
484
|
+
`applied=${report.applied.length} skipped=${report.skipped.length} ` +
|
|
485
|
+
`failed=${report.failed.length}`);
|
|
486
|
+
onCycle?.(report);
|
|
487
|
+
}
|
|
488
|
+
catch (err) {
|
|
489
|
+
log?.(`[reconcile] cycle failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
490
|
+
}
|
|
491
|
+
if (shouldStop())
|
|
492
|
+
return;
|
|
493
|
+
// eslint-disable-next-line no-await-in-loop
|
|
494
|
+
await sleep(intervalMs);
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
// Exported for tests that want to call the PR URL parser directly.
|
|
498
|
+
export { parsePRNumberFromUrl as _parsePRNumberFromUrl };
|
|
499
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/lib/reconcile/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tier 2 State Reconciler — Types (PRLT-1280)
|
|
3
|
+
*
|
|
4
|
+
* The reconciler is the safety net that fixes board drift when Tier 1
|
|
5
|
+
* (event-driven hooks) misses an event. It polls the provider (GitHub for
|
|
6
|
+
* PR state) and fires normal ticket transitions when it detects drift.
|
|
7
|
+
*
|
|
8
|
+
* Design constraints:
|
|
9
|
+
* - Provider-agnostic: the reconciler core does not branch on provider type.
|
|
10
|
+
* It routes every transition through the existing provider adapter layer.
|
|
11
|
+
* - Idempotent: running twice back-to-back must be safe. Every check asks
|
|
12
|
+
* "is current == expected?" and no-ops when they match.
|
|
13
|
+
* - No LLM, no supervision tree, no daemon rewrite. One function on a timer.
|
|
14
|
+
*/
|
|
15
|
+
import type { PRInfo } from '../pr/index.js';
|
|
16
|
+
import type { Ticket } from '../pmo/types.js';
|
|
17
|
+
/**
|
|
18
|
+
* Source of a linked PR. Used for logging and to decide which PMO mapping
|
|
19
|
+
* to touch when the reconciler sees a transition is needed.
|
|
20
|
+
*/
|
|
21
|
+
export type LinkedPRSource = 'ticket_branch' | 'external_map' | 'branch_search';
|
|
22
|
+
/**
|
|
23
|
+
* A PR linked to a ticket, along with its provenance.
|
|
24
|
+
*/
|
|
25
|
+
export interface LinkedPR {
|
|
26
|
+
/** Live PR info as reported by GitHub at the time of the reconcile cycle */
|
|
27
|
+
pr: PRInfo;
|
|
28
|
+
/** How this PR was discovered for the ticket */
|
|
29
|
+
source: LinkedPRSource;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* A single transition the reconciler is about to apply (or would apply in dry-run).
|
|
33
|
+
*/
|
|
34
|
+
export interface ReconcileTransition {
|
|
35
|
+
/** Ticket being moved */
|
|
36
|
+
ticketId: string;
|
|
37
|
+
/** Ticket display title (for logging) */
|
|
38
|
+
ticketTitle: string;
|
|
39
|
+
/** The project the ticket belongs to */
|
|
40
|
+
projectId: string;
|
|
41
|
+
/** Provider name that owns this ticket (from resolver) */
|
|
42
|
+
provider: string;
|
|
43
|
+
/** Ticket's state before the transition */
|
|
44
|
+
fromState: string | null;
|
|
45
|
+
/** Ticket's state after the transition (target column name) */
|
|
46
|
+
toState: string;
|
|
47
|
+
/** PR number that drove this transition, when available */
|
|
48
|
+
prNumber?: number;
|
|
49
|
+
/** PR state that drove this transition (MERGED, CLOSED, OPEN) */
|
|
50
|
+
prState?: PRInfo['state'];
|
|
51
|
+
/** Human-readable reason for the transition */
|
|
52
|
+
reason: string;
|
|
53
|
+
/** ISO timestamp the reconciler decided on this transition */
|
|
54
|
+
decidedAt: string;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Options for running a reconcile cycle.
|
|
58
|
+
*/
|
|
59
|
+
export interface ReconcileOptions {
|
|
60
|
+
/** If true, compute transitions but do not apply them */
|
|
61
|
+
dryRun?: boolean;
|
|
62
|
+
/** Working directory for GitHub CLI calls (git remote, gh pr view) */
|
|
63
|
+
cwd?: string;
|
|
64
|
+
/** Optional callback for human-readable logs */
|
|
65
|
+
log?: (msg: string) => void;
|
|
66
|
+
/**
|
|
67
|
+
* Restrict reconciliation to a specific project. When omitted the
|
|
68
|
+
* reconciler runs workspace-wide across every project.
|
|
69
|
+
*/
|
|
70
|
+
projectId?: string;
|
|
71
|
+
/**
|
|
72
|
+
* Injectable PR lookup. Tests use this to avoid the real gh CLI.
|
|
73
|
+
* Implementations must query live — the reconciler is not allowed to
|
|
74
|
+
* depend on a local PR cache.
|
|
75
|
+
*/
|
|
76
|
+
prLookup?: PRLookupFn;
|
|
77
|
+
/**
|
|
78
|
+
* Injectable branch search. Used when a ticket has no linked PRs
|
|
79
|
+
* from branch or external map — falls back to searching GitHub by
|
|
80
|
+
* branch name pattern. Tests use this to avoid the real gh CLI.
|
|
81
|
+
*/
|
|
82
|
+
branchSearch?: BranchSearchFn;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Function signature for live PR lookup.
|
|
86
|
+
*
|
|
87
|
+
* The reconciler calls this once per PR reference. Implementations must
|
|
88
|
+
* hit the remote GitHub API (gh CLI or the REST client) — a stale local
|
|
89
|
+
* cache defeats the whole point of a Tier 2 reconciler.
|
|
90
|
+
*/
|
|
91
|
+
export type PRLookupFn = (ref: PRLookupRef, cwd?: string) => PRInfo | null;
|
|
92
|
+
/**
|
|
93
|
+
* A PR reference that can be resolved to a PRInfo.
|
|
94
|
+
*
|
|
95
|
+
* Tickets link to PRs via either their ticket branch (common for
|
|
96
|
+
* prlt-managed work) or via a stored URL in the external execution
|
|
97
|
+
* mapping table (common when the PR was opened outside prlt).
|
|
98
|
+
*/
|
|
99
|
+
export type PRLookupRef = {
|
|
100
|
+
kind: 'branch';
|
|
101
|
+
branch: string;
|
|
102
|
+
} | {
|
|
103
|
+
kind: 'number';
|
|
104
|
+
number: number;
|
|
105
|
+
} | {
|
|
106
|
+
kind: 'url';
|
|
107
|
+
url: string;
|
|
108
|
+
};
|
|
109
|
+
/**
|
|
110
|
+
* Function signature for searching GitHub by branch name prefix.
|
|
111
|
+
*
|
|
112
|
+
* When a ticket has no `pr_number` metadata, no branch, and no entries
|
|
113
|
+
* in the external execution map, the reconciler falls back to searching
|
|
114
|
+
* GitHub for PRs whose head branch starts with any of the ticket's
|
|
115
|
+
* known identifiers (e.g. `PRLT-1234/`, `TKT-081/`).
|
|
116
|
+
*
|
|
117
|
+
* Returns ALL matching PRs so the reconciler can evaluate them all.
|
|
118
|
+
*/
|
|
119
|
+
export type BranchSearchFn = (ticketIds: string[], cwd?: string) => PRInfo[];
|
|
120
|
+
/**
|
|
121
|
+
* Report returned after a reconcile cycle completes.
|
|
122
|
+
*/
|
|
123
|
+
export interface ReconcileReport {
|
|
124
|
+
/** Total number of tickets considered (across all scanned projects) */
|
|
125
|
+
checked: number;
|
|
126
|
+
/** Transitions that were applied (or would be applied in dry-run) */
|
|
127
|
+
applied: ReconcileTransition[];
|
|
128
|
+
/** Transitions that the reconciler decided on but did not execute because of --dry-run */
|
|
129
|
+
skipped: ReconcileTransition[];
|
|
130
|
+
/** Transitions that failed to apply */
|
|
131
|
+
failed: Array<{
|
|
132
|
+
transition: ReconcileTransition;
|
|
133
|
+
error: string;
|
|
134
|
+
}>;
|
|
135
|
+
/** Non-fatal errors encountered while gathering state */
|
|
136
|
+
errors: Array<{
|
|
137
|
+
ticketId: string;
|
|
138
|
+
error: string;
|
|
139
|
+
}>;
|
|
140
|
+
/** ISO timestamp when the cycle started */
|
|
141
|
+
startedAt: string;
|
|
142
|
+
/** ISO timestamp when the cycle finished */
|
|
143
|
+
finishedAt: string;
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Minimal view of a ticket the reconciler actually reads. Narrow surface
|
|
147
|
+
* makes the reconciler easy to unit-test without fabricating full Ticket
|
|
148
|
+
* objects.
|
|
149
|
+
*/
|
|
150
|
+
export type ReconcileTicket = Pick<Ticket, 'id' | 'title' | 'projectId' | 'statusName' | 'statusCategory' | 'branch' | 'metadata'>;
|