@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.
Files changed (70) hide show
  1. package/dist/commands/gateway/connect.d.ts +33 -0
  2. package/dist/commands/gateway/connect.js +130 -0
  3. package/dist/commands/gateway/connect.js.map +1 -0
  4. package/dist/commands/gateway/disconnect.d.ts +21 -0
  5. package/dist/commands/gateway/disconnect.js +69 -0
  6. package/dist/commands/gateway/disconnect.js.map +1 -0
  7. package/dist/commands/gateway/start.d.ts +23 -0
  8. package/dist/commands/gateway/start.js +133 -0
  9. package/dist/commands/gateway/start.js.map +1 -0
  10. package/dist/commands/gateway/status.d.ts +16 -0
  11. package/dist/commands/gateway/status.js +76 -0
  12. package/dist/commands/gateway/status.js.map +1 -0
  13. package/dist/commands/gateway/test.d.ts +22 -0
  14. package/dist/commands/gateway/test.js +83 -0
  15. package/dist/commands/gateway/test.js.map +1 -0
  16. package/dist/commands/orchestrate/index.js +11 -2
  17. package/dist/commands/orchestrate/index.js.map +1 -1
  18. package/dist/commands/pr/merge.js +44 -80
  19. package/dist/commands/pr/merge.js.map +1 -1
  20. package/dist/commands/reconcile.d.ts +29 -0
  21. package/dist/commands/reconcile.js +140 -0
  22. package/dist/commands/reconcile.js.map +1 -0
  23. package/dist/commands/work/ship.js +131 -61
  24. package/dist/commands/work/ship.js.map +1 -1
  25. package/dist/commands/work/start.js +30 -0
  26. package/dist/commands/work/start.js.map +1 -1
  27. package/dist/lib/events/events.d.ts +19 -0
  28. package/dist/lib/execution/prompt-watcher.d.ts +120 -0
  29. package/dist/lib/execution/prompt-watcher.js +222 -0
  30. package/dist/lib/execution/prompt-watcher.js.map +1 -0
  31. package/dist/lib/execution/spawner.js +31 -0
  32. package/dist/lib/execution/spawner.js.map +1 -1
  33. package/dist/lib/gateway/channel-factory.d.ts +13 -0
  34. package/dist/lib/gateway/channel-factory.js +37 -0
  35. package/dist/lib/gateway/channel-factory.js.map +1 -0
  36. package/dist/lib/gateway/channels/telegram.d.ts +115 -0
  37. package/dist/lib/gateway/channels/telegram.js +215 -0
  38. package/dist/lib/gateway/channels/telegram.js.map +1 -0
  39. package/dist/lib/gateway/router.d.ts +84 -0
  40. package/dist/lib/gateway/router.js +140 -0
  41. package/dist/lib/gateway/router.js.map +1 -0
  42. package/dist/lib/gateway/session-poker.d.ts +35 -0
  43. package/dist/lib/gateway/session-poker.js +85 -0
  44. package/dist/lib/gateway/session-poker.js.map +1 -0
  45. package/dist/lib/gateway/types.d.ts +124 -0
  46. package/dist/lib/gateway/types.js +17 -0
  47. package/dist/lib/gateway/types.js.map +1 -0
  48. package/dist/lib/machine-db.d.ts +87 -0
  49. package/dist/lib/machine-db.js +135 -0
  50. package/dist/lib/machine-db.js.map +1 -1
  51. package/dist/lib/orchestrate/index.d.ts +1 -1
  52. package/dist/lib/orchestrate/index.js +1 -1
  53. package/dist/lib/orchestrate/index.js.map +1 -1
  54. package/dist/lib/orchestrate/llm-agent.d.ts +7 -0
  55. package/dist/lib/orchestrate/llm-agent.js +48 -1
  56. package/dist/lib/orchestrate/llm-agent.js.map +1 -1
  57. package/dist/lib/pr/index.d.ts +43 -2
  58. package/dist/lib/pr/index.js +141 -4
  59. package/dist/lib/pr/index.js.map +1 -1
  60. package/dist/lib/reconcile/core.d.ts +62 -0
  61. package/dist/lib/reconcile/core.js +137 -0
  62. package/dist/lib/reconcile/core.js.map +1 -0
  63. package/dist/lib/reconcile/index.d.ts +59 -0
  64. package/dist/lib/reconcile/index.js +499 -0
  65. package/dist/lib/reconcile/index.js.map +1 -0
  66. package/dist/lib/reconcile/types.d.ts +150 -0
  67. package/dist/lib/reconcile/types.js +16 -0
  68. package/dist/lib/reconcile/types.js.map +1 -0
  69. package/oclif.manifest.json +1021 -679
  70. 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'>;