specrails-desktop 2.49.2 → 2.51.0

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 (134) hide show
  1. package/client/dist/assets/ActivityFeedPage-Bu6_Fxfa.js +1 -0
  2. package/client/dist/assets/{AgentBrowserCapture-DBGaKW4u.js → AgentBrowserCapture-ChIVaQ4N.js} +1 -1
  3. package/client/dist/assets/AgentModeAnalyticsPane-Dq59o2ZO.js +2 -0
  4. package/client/dist/assets/AgentModeCodePane-VjaDQOAZ.js +2 -0
  5. package/client/dist/assets/{AgentModeJobsPane-DkhcBtoZ.js → AgentModeJobsPane-DQST3Y8L.js} +1 -1
  6. package/client/dist/assets/AgentsPage-9ajYQTPV.js +87 -0
  7. package/client/dist/assets/{AnalyticsPage-DisrtBr7.js → AnalyticsPage-DgHAcKnO.js} +1 -1
  8. package/client/dist/assets/CodePage-tDBdWNkc.js +3 -0
  9. package/client/dist/assets/{DesktopAnalyticsPage-BFB-Pn-D.js → DesktopAnalyticsPage-fxggfmsY.js} +1 -1
  10. package/client/dist/assets/{DocsDialog-BwiFrwEJ.js → DocsDialog-0J6gUakl.js} +1 -1
  11. package/client/dist/assets/{DocsPage-ByS0ZCco.js → DocsPage-bQ6h2xx0.js} +1 -1
  12. package/client/dist/assets/{ExportDropdown-IPutNYvD.js → ExportDropdown-y2k4CoRJ.js} +1 -1
  13. package/client/dist/assets/InteractiveJobComposer-BI-y4fFH.js +19 -0
  14. package/client/dist/assets/{JobDetailModal-BnO_V8KR.js → JobDetailModal-B4cur32x.js} +1 -1
  15. package/client/dist/assets/JobDetailPage--VZYZ76E.js +1 -0
  16. package/client/dist/assets/JobsPage-K4wj2mXV.js +1 -0
  17. package/client/dist/assets/{LoopBuilderPage-WmqNpDw2.js → LoopBuilderPage-Cu9bNaZf.js} +2 -2
  18. package/client/dist/assets/{LoopPreviewModal-BY5dDezY.js → LoopPreviewModal-DmwboMLZ.js} +1 -1
  19. package/client/dist/assets/{LoopsPage-BcegqJyY.js → LoopsPage-Db8wSZSW.js} +1 -1
  20. package/client/dist/assets/{MinimizedChatsContext-CX1i2kQ8.js → MinimizedChatsContext-CoRyZxu4.js} +1 -1
  21. package/client/dist/assets/{PluginsPage-BhG3qPda.js → PluginsPage-A4dSzbGE.js} +2 -2
  22. package/client/dist/assets/{ProjectSettingsDialog-JQdLwCy2.js → ProjectSettingsDialog-DLrka37M.js} +1 -1
  23. package/client/dist/assets/{RepositoryDeliveries-DqmlG0Zs.js → RepositoryDeliveries-FYr8nbCo.js} +1 -1
  24. package/client/dist/assets/{RepositoryScopeSelector-DG8KKPrm.js → RepositoryScopeSelector-CBkDsqpz.js} +1 -1
  25. package/client/dist/assets/ReviewPacketPage-BGsesCFL.js +4 -0
  26. package/client/dist/assets/{TemplatePreviewModal-1AVv-hu4.js → TemplatePreviewModal-uGbE-mTU.js} +1 -1
  27. package/client/dist/assets/TicketDetailModalContext-lmkauO0-.js +16 -0
  28. package/client/dist/assets/{Trans-BLljsfHR.js → Trans-92vqs9mz.js} +1 -1
  29. package/client/dist/assets/{agent-BII69Nc0.js → agent-BaG10AN6.js} +1 -1
  30. package/client/dist/assets/{agent-CYFBDwFD.js → agent-CH4aJ884.js} +1 -1
  31. package/client/dist/assets/{agent-CVeMLIu4.js → agent-CLc9fA7v.js} +1 -1
  32. package/client/dist/assets/{agent-B76BhAyE.js → agent-DHh9-AdL.js} +1 -1
  33. package/client/dist/assets/{agent-DSrflYFL.js → agent-DkKnd6Wv.js} +1 -1
  34. package/client/dist/assets/{agent-BIO4q6b0.js → agent-aiVhqFW1.js} +1 -1
  35. package/client/dist/assets/{agent-ButS6kwl.js → agent-dbznD3eO.js} +1 -1
  36. package/client/dist/assets/{agent-D3jkMipn.js → agent-sMVFcPwL.js} +1 -1
  37. package/client/dist/assets/{formatDistanceToNow-CbQZkD8M.js → formatDistanceToNow-Cuoagaiu.js} +1 -1
  38. package/client/dist/assets/{getTimezoneOffsetInMilliseconds-pBVK3rJC.js → getTimezoneOffsetInMilliseconds-BLaUfsdD.js} +1 -1
  39. package/client/dist/assets/index-BAtqmAZR.css +2 -0
  40. package/client/dist/assets/index-Cxdhikml.js +80 -0
  41. package/client/dist/assets/{jira-api-f-sYCIuX.js → jira-api-DqYxW5BY.js} +1 -1
  42. package/client/dist/assets/layers-eDMjG41I.js +1 -0
  43. package/client/dist/assets/{packet-DQ_GnT2b.js → packet-CjS_478C.js} +1 -1
  44. package/client/dist/assets/{packet-r7J14m89.js → packet-D69dKsHN.js} +1 -1
  45. package/client/dist/assets/{packet-DF-iwqqY.js → packet-D9rU6Oxo.js} +1 -1
  46. package/client/dist/assets/{packet-Nb8BLI5h.js → packet-DMDFoamg.js} +1 -1
  47. package/client/dist/assets/{packet-C0pcMd6x.js → packet-Dd2RMs1h.js} +1 -1
  48. package/client/dist/assets/{packet-BsRimSfZ.js → packet-DjC9OibC.js} +1 -1
  49. package/client/dist/assets/packet-DwPtJoDz.js +1 -0
  50. package/client/dist/assets/{packet-C8qzvgrZ.js → packet-ceahDg8_.js} +1 -1
  51. package/client/dist/assets/{project-repositories-DAjJbFhm.js → project-repositories-DbVfkVQJ.js} +1 -1
  52. package/client/dist/assets/rotate-ccw-Wh4id13q.js +1 -0
  53. package/client/dist/assets/specs-Bqvtz2kz.js +1 -0
  54. package/client/dist/assets/specs-CKes-kz6.js +1 -0
  55. package/client/dist/assets/specs-CfXxPDNB.js +1 -0
  56. package/client/dist/assets/specs-CyF9HuTI.js +1 -0
  57. package/client/dist/assets/specs-DMj035Mt.js +1 -0
  58. package/client/dist/assets/specs-DfIdoU5L.js +1 -0
  59. package/client/dist/assets/specs-j5pGLQeU.js +1 -0
  60. package/client/dist/assets/specs-xW5pkkB7.js +1 -0
  61. package/client/dist/assets/{spending-DJBbdJyD.js → spending-ul_XBA3M.js} +1 -1
  62. package/client/dist/assets/{tickets-Bc71ce1C.js → tickets--tiSOCcn.js} +1 -1
  63. package/client/dist/assets/{tickets-CLg7VTJ8.js → tickets-BUJ3DMWU.js} +1 -1
  64. package/client/dist/assets/{tickets-CZNBiHbq.js → tickets-C1WNPODq.js} +1 -1
  65. package/client/dist/assets/{tickets-DlX2mEkD.js → tickets-CDjhWmiE.js} +1 -1
  66. package/client/dist/assets/{tickets-B4rjuSYi.js → tickets-CLgPC3uO.js} +1 -1
  67. package/client/dist/assets/tickets-D41ZMQpf.js +1 -0
  68. package/client/dist/assets/{tickets-CP9U1GJt.js → tickets-DEaQC06P.js} +1 -1
  69. package/client/dist/assets/{tickets-_yKvCt4Y.js → tickets-kcMzbLCp.js} +1 -1
  70. package/client/dist/assets/{useDesktop-BJ8IRBRt.js → useDesktop-BC_4XGnf.js} +1 -1
  71. package/client/dist/assets/{useRuntimeRuns-Ch1KUc7k.js → useRuntimeRuns-B_YsBTsN.js} +1 -1
  72. package/client/dist/assets/{useSharedWebSocket-BN3Lmehi.js → useSharedWebSocket-8NyZpT6v.js} +2 -2
  73. package/client/dist/assets/zap-CUOlvtT1.js +1 -0
  74. package/client/dist/index.html +22 -20
  75. package/docs/creating-specs.md +31 -0
  76. package/docs/guide/de/specs/1-specs-and-the-backlog.md +4 -0
  77. package/docs/guide/en/specs/1-specs-and-the-backlog.md +4 -0
  78. package/docs/guide/es/specs/1-specs-and-the-backlog.md +4 -0
  79. package/docs/guide/fr/specs/1-specs-and-the-backlog.md +4 -0
  80. package/docs/guide/it/specs/1-specs-and-the-backlog.md +4 -0
  81. package/docs/guide/ja/specs/1-specs-and-the-backlog.md +4 -0
  82. package/docs/guide/pt/specs/1-specs-and-the-backlog.md +4 -0
  83. package/docs/guide/zh/specs/1-specs-and-the-backlog.md +4 -0
  84. package/docs/internals/safe-pr-review-flow.md +80 -0
  85. package/docs/internals/spec-addenda.md +166 -0
  86. package/docs/mcp.md +1 -1
  87. package/package.json +1 -1
  88. package/server/dist/agent-context-resolver.js +5 -0
  89. package/server/dist/agent-operator-prompt.js +41 -1
  90. package/server/dist/db.js +22 -0
  91. package/server/dist/jira/jira-materializer.js +3 -0
  92. package/server/dist/loop-run-manager.js +6 -1
  93. package/server/dist/mcp/guide.js +10 -0
  94. package/server/dist/mcp/tools/rails.js +24 -0
  95. package/server/dist/mcp/tools/specs.js +68 -3
  96. package/server/dist/multi-repo-delivery.js +4 -0
  97. package/server/dist/pr-follow-up-scope.js +224 -0
  98. package/server/dist/pr-follow-up.js +75 -0
  99. package/server/dist/project-registry.js +43 -0
  100. package/server/dist/project-router-tickets.js +223 -0
  101. package/server/dist/queue-manager.js +34 -1
  102. package/server/dist/rail-isolated-launch.js +16 -0
  103. package/server/dist/rail-launch-parser.js +12 -0
  104. package/server/dist/rail-pr-decision.js +20 -3
  105. package/server/dist/rail-pr-store.js +40 -3
  106. package/server/dist/rail-pr-ticket-effects.js +21 -0
  107. package/server/dist/rails-router.js +63 -3
  108. package/server/dist/review-packet.js +45 -0
  109. package/server/dist/spec-addenda-core.js +281 -0
  110. package/server/dist/spec-addenda.js +338 -0
  111. package/server/dist/ticket-store.js +5 -1
  112. package/client/dist/assets/ActivityFeedPage-CyOxof-U.js +0 -1
  113. package/client/dist/assets/AgentModeAnalyticsPane-WZpPaNMa.js +0 -2
  114. package/client/dist/assets/AgentModeCodePane-nHYnJUdP.js +0 -2
  115. package/client/dist/assets/AgentsPage-CX90q-nY.js +0 -87
  116. package/client/dist/assets/CodePage-ixqxftCT.js +0 -3
  117. package/client/dist/assets/InteractiveJobComposer-BEok7ntM.js +0 -19
  118. package/client/dist/assets/JobDetailPage-BnS8aHKD.js +0 -1
  119. package/client/dist/assets/JobsPage-BC0mq-MF.js +0 -1
  120. package/client/dist/assets/ReviewPacketPage-DSSi3x16.js +0 -4
  121. package/client/dist/assets/TicketDetailModalContext-D2MEhRwJ.js +0 -14
  122. package/client/dist/assets/index-DEqG4WdV.js +0 -80
  123. package/client/dist/assets/index-UDzndadF.css +0 -2
  124. package/client/dist/assets/packet-BTRySHoQ.js +0 -1
  125. package/client/dist/assets/specs-BcyxaCwf.js +0 -1
  126. package/client/dist/assets/specs-C1q9geAi.js +0 -1
  127. package/client/dist/assets/specs-CLZKysq-.js +0 -1
  128. package/client/dist/assets/specs-DII0C8gw.js +0 -1
  129. package/client/dist/assets/specs-DZMS9o54.js +0 -1
  130. package/client/dist/assets/specs-Dm6zUI-R.js +0 -1
  131. package/client/dist/assets/specs-V3TvhKLX.js +0 -1
  132. package/client/dist/assets/specs-dV3Mq5Mu.js +0 -1
  133. package/client/dist/assets/tickets-W6pBmbrX.js +0 -1
  134. package/client/dist/assets/zap-CHo3aCD1.js +0 -1
@@ -57,6 +57,7 @@ const spec_models_1 = require("./spec-models");
57
57
  const provider_selection_1 = require("./provider-selection");
58
58
  const ticket_store_1 = require("./ticket-store");
59
59
  const explore_draft_title_1 = require("./explore-draft-title");
60
+ const spec_addenda_1 = require("./spec-addenda");
60
61
  const cli_prompt_1 = require("./util/cli-prompt");
61
62
  const transient_children_1 = require("./transient-children");
62
63
  const process_admission_1 = require("./process-admission");
@@ -1535,6 +1536,228 @@ function registerTicketsRoutes(deps) {
1535
1536
  res.status(500).json({ error: 'Failed to create ticket' });
1536
1537
  }
1537
1538
  });
1539
+ // ── Spec addenda (spec-addenda) ─────────────────────────────────────────
1540
+ // Structured iteration notes attached to a spec WITHOUT touching its
1541
+ // description. CRUD here; the launch doors claim them (open → in_flight),
1542
+ // the completion chokepoints settle them, a discarded delivery reopens them.
1543
+ // Never a Jira write-back: addenda are local-only guidance for the run.
1544
+ const ADDENDUM_ID_RE = /^[A-Za-z0-9-]{1,64}$/;
1545
+ const addendaTicketId = (req, res) => {
1546
+ const ticketId = req.params.id;
1547
+ if (!/^\d+$/.test(ticketId)) {
1548
+ res.status(400).json({ error: 'Invalid ticket ID' });
1549
+ return null;
1550
+ }
1551
+ return ticketId;
1552
+ };
1553
+ const addendumId = (req, res) => {
1554
+ const id = req.params.addendumId;
1555
+ if (!ADDENDUM_ID_RE.test(id)) {
1556
+ res.status(400).json({ error: 'Invalid addendum ID' });
1557
+ return null;
1558
+ }
1559
+ return id;
1560
+ };
1561
+ const broadcastTicket = (req, ticket, revision) => {
1562
+ const { broadcast, ticketWatcher } = ctx(req);
1563
+ ticketWatcher.notifyDesktopWrite(revision);
1564
+ const msg = { type: 'ticket_updated', ticket: ticket, projectId: ctx(req).project.id, timestamp: new Date().toISOString() };
1565
+ broadcast(msg);
1566
+ };
1567
+ const addendumEditFailure = (res, failure) => {
1568
+ if (failure === 'not_found') {
1569
+ res.status(404).json({ error: 'addendum_not_found' });
1570
+ return;
1571
+ }
1572
+ if (failure === 'in_flight') {
1573
+ res.status(409).json({ error: 'addendum_in_flight', detail: 'the addendum is frozen inside a running job; wait for it to settle' });
1574
+ return;
1575
+ }
1576
+ res.status(409).json({ error: 'addendum_applied', detail: 'an applied addendum is history; reopen it or add a new one' });
1577
+ };
1578
+ // GET /:projectId/tickets/:id/addenda — list a spec's addenda
1579
+ router.get('/:projectId/tickets/:id/addenda', (req, res) => {
1580
+ const ticketId = addendaTicketId(req, res);
1581
+ if (!ticketId)
1582
+ return;
1583
+ try {
1584
+ const store = (0, ticket_store_1.readStore)(ticketPath(req));
1585
+ const ticket = store.tickets[ticketId];
1586
+ if (!ticket) {
1587
+ res.status(404).json({ error: 'Ticket not found' });
1588
+ return;
1589
+ }
1590
+ res.json({ addenda: (0, spec_addenda_1.readSpecAddenda)(ticket.addenda), revision: store.revision });
1591
+ }
1592
+ catch (err) {
1593
+ console.error('[project-router] addenda list error:', err);
1594
+ res.status(500).json({ error: 'Failed to read addenda' });
1595
+ }
1596
+ });
1597
+ // POST /:projectId/tickets/:id/addenda — add an addendum { kind?, title?, body, originConversationId? }
1598
+ router.post('/:projectId/tickets/:id/addenda', (req, res) => {
1599
+ const ticketId = addendaTicketId(req, res);
1600
+ if (!ticketId)
1601
+ return;
1602
+ let input;
1603
+ try {
1604
+ input = (0, spec_addenda_1.parseSpecAddendumInput)(req.body ?? {});
1605
+ }
1606
+ catch (err) {
1607
+ if (err instanceof spec_addenda_1.SpecAddendumValidationError) {
1608
+ res.status(400).json({ error: 'invalid_addendum', code: err.code, detail: err.message });
1609
+ return;
1610
+ }
1611
+ throw err;
1612
+ }
1613
+ const createdBy = typeof req.body?.createdBy === 'string' && /^[a-z-]{1,32}$/.test(req.body.createdBy) ? req.body.createdBy : 'user';
1614
+ const originConversationId = typeof req.body?.originConversationId === 'string' && /^[A-Za-z0-9-]{1,64}$/.test(req.body.originConversationId) ? req.body.originConversationId : null;
1615
+ try {
1616
+ let created;
1617
+ let updated;
1618
+ let capError = null;
1619
+ const store = (0, ticket_store_1.mutateStore)(ticketPath(req), (s) => {
1620
+ const ticket = s.tickets[ticketId];
1621
+ if (!ticket)
1622
+ return;
1623
+ try {
1624
+ created = (0, spec_addenda_1.appendSpecAddendum)(ticket, (0, spec_addenda_1.buildSpecAddendum)(input, { createdBy, originConversationId }));
1625
+ updated = ticket;
1626
+ }
1627
+ catch (err) {
1628
+ if (err instanceof spec_addenda_1.SpecAddendumValidationError) {
1629
+ capError = err;
1630
+ return;
1631
+ }
1632
+ throw err;
1633
+ }
1634
+ });
1635
+ if (capError) {
1636
+ const e = capError;
1637
+ res.status(400).json({ error: 'invalid_addendum', code: e.code, detail: e.message });
1638
+ return;
1639
+ }
1640
+ if (!updated || !created) {
1641
+ res.status(404).json({ error: 'Ticket not found' });
1642
+ return;
1643
+ }
1644
+ broadcastTicket(req, updated, store.revision);
1645
+ res.status(201).json({ addendum: created, ticket: updated, revision: store.revision });
1646
+ }
1647
+ catch (err) {
1648
+ console.error('[project-router] addendum create error:', err);
1649
+ res.status(500).json({ error: 'Failed to add addendum' });
1650
+ }
1651
+ });
1652
+ // PATCH /:projectId/tickets/:id/addenda/:addendumId — edit content and/or move status (open|dismissed);
1653
+ // `force: true` releases an in-flight claim whose run vanished (user escape hatch)
1654
+ router.patch('/:projectId/tickets/:id/addenda/:addendumId', (req, res) => {
1655
+ const ticketId = addendaTicketId(req, res);
1656
+ if (!ticketId)
1657
+ return;
1658
+ const aid = addendumId(req, res);
1659
+ if (!aid)
1660
+ return;
1661
+ const body = req.body ?? {};
1662
+ const wantsContent = body.kind !== undefined || body.title !== undefined || body.body !== undefined;
1663
+ const status = body.status;
1664
+ if (status !== undefined && status !== 'open' && status !== 'dismissed') {
1665
+ res.status(400).json({ error: 'invalid_addendum', code: 'invalid_status', detail: 'status must be open or dismissed' });
1666
+ return;
1667
+ }
1668
+ if (!wantsContent && status === undefined) {
1669
+ res.status(400).json({ error: 'invalid_addendum', code: 'not_object', detail: 'nothing to change' });
1670
+ return;
1671
+ }
1672
+ try {
1673
+ let result = 'not_found';
1674
+ let updated;
1675
+ let validation = null;
1676
+ const store = (0, ticket_store_1.mutateStore)(ticketPath(req), (s) => {
1677
+ const ticket = s.tickets[ticketId];
1678
+ if (!ticket)
1679
+ return;
1680
+ updated = ticket;
1681
+ if (wantsContent) {
1682
+ const current = (0, spec_addenda_1.readSpecAddenda)(ticket.addenda).find((a) => a.id === aid);
1683
+ if (!current) {
1684
+ result = 'not_found';
1685
+ return;
1686
+ }
1687
+ let input;
1688
+ try {
1689
+ input = (0, spec_addenda_1.parseSpecAddendumInput)({ kind: body.kind ?? current.kind, title: body.title ?? current.title, body: body.body ?? current.body });
1690
+ }
1691
+ catch (err) {
1692
+ if (err instanceof spec_addenda_1.SpecAddendumValidationError) {
1693
+ validation = err;
1694
+ return;
1695
+ }
1696
+ throw err;
1697
+ }
1698
+ result = (0, spec_addenda_1.editSpecAddendum)(ticket, aid, input);
1699
+ if (typeof result === 'string')
1700
+ return;
1701
+ }
1702
+ if (status !== undefined)
1703
+ result = (0, spec_addenda_1.setSpecAddendumStatus)(ticket, aid, status, undefined, { force: body.force === true });
1704
+ });
1705
+ if (validation) {
1706
+ const e = validation;
1707
+ res.status(400).json({ error: 'invalid_addendum', code: e.code, detail: e.message });
1708
+ return;
1709
+ }
1710
+ if (!updated) {
1711
+ res.status(404).json({ error: 'Ticket not found' });
1712
+ return;
1713
+ }
1714
+ if (typeof result === 'string') {
1715
+ addendumEditFailure(res, result);
1716
+ return;
1717
+ }
1718
+ broadcastTicket(req, updated, store.revision);
1719
+ res.json({ addendum: result, ticket: updated, revision: store.revision });
1720
+ }
1721
+ catch (err) {
1722
+ console.error('[project-router] addendum update error:', err);
1723
+ res.status(500).json({ error: 'Failed to update addendum' });
1724
+ }
1725
+ });
1726
+ // DELETE /:projectId/tickets/:id/addenda/:addendumId — remove (never while in flight)
1727
+ router.delete('/:projectId/tickets/:id/addenda/:addendumId', (req, res) => {
1728
+ const ticketId = addendaTicketId(req, res);
1729
+ if (!ticketId)
1730
+ return;
1731
+ const aid = addendumId(req, res);
1732
+ if (!aid)
1733
+ return;
1734
+ try {
1735
+ let result = 'not_found';
1736
+ let updated;
1737
+ const store = (0, ticket_store_1.mutateStore)(ticketPath(req), (s) => {
1738
+ const ticket = s.tickets[ticketId];
1739
+ if (!ticket)
1740
+ return;
1741
+ updated = ticket;
1742
+ result = (0, spec_addenda_1.removeSpecAddendum)(ticket, aid);
1743
+ });
1744
+ if (!updated) {
1745
+ res.status(404).json({ error: 'Ticket not found' });
1746
+ return;
1747
+ }
1748
+ const outcome = result;
1749
+ if (outcome !== true) {
1750
+ addendumEditFailure(res, outcome);
1751
+ return;
1752
+ }
1753
+ broadcastTicket(req, updated, store.revision);
1754
+ res.json({ ok: true, ticket: updated, revision: store.revision });
1755
+ }
1756
+ catch (err) {
1757
+ console.error('[project-router] addendum delete error:', err);
1758
+ res.status(500).json({ error: 'Failed to delete addendum' });
1759
+ }
1760
+ });
1538
1761
  // PATCH /:projectId/tickets/:id — Update ticket fields
1539
1762
  router.patch('/:projectId/tickets/:id', (req, res) => {
1540
1763
  const ticketId = req.params.id;
@@ -33,6 +33,7 @@ const db_1 = require("./db");
33
33
  const interactive_job_session_1 = require("./interactive-job-session");
34
34
  const attachment_manager_1 = require("./attachment-manager");
35
35
  const ticket_store_1 = require("./ticket-store");
36
+ const spec_addenda_1 = require("./spec-addenda");
36
37
  const binary_probe_1 = require("./binary-probe");
37
38
  const workspace_manager_1 = require("./workspace-manager");
38
39
  const claude_trust_1 = require("./claude-trust");
@@ -1481,6 +1482,29 @@ class QueueManager {
1481
1482
  return '';
1482
1483
  }
1483
1484
  }
1485
+ /**
1486
+ * Spec addenda briefing for a job (spec-addenda): claims (idempotently, keyed
1487
+ * on the job id) the open addenda of every ticket the command references and
1488
+ * renders the deterministic briefing. Best-effort: an unreadable store ⇒ ''.
1489
+ */
1490
+ _buildSpecAddendaBriefing(command, jobId) {
1491
+ if (!this._cwd)
1492
+ return '';
1493
+ const ticketIds = this._extractTicketIds(command);
1494
+ if (ticketIds.length === 0)
1495
+ return '';
1496
+ try {
1497
+ const claimed = (0, spec_addenda_1.claimSpecAddendaForRun)(this._resolveTicketsPath(), ticketIds, jobId);
1498
+ if (claimed.store && this._projectId) {
1499
+ (0, spec_addenda_1.broadcastSpecAddendaChange)((msg) => this._broadcast(msg), this._projectId, claimed);
1500
+ }
1501
+ return claimed.briefing;
1502
+ }
1503
+ catch (err) {
1504
+ console.warn(`[queue-manager] failed to build spec addenda briefing: ${err.message}`);
1505
+ return '';
1506
+ }
1507
+ }
1484
1508
  /**
1485
1509
  * Build the Claude prompt for an Freestyle job. Freestyle does NOT invoke
1486
1510
  * a slash command: it sends the resolved pre-prompt followed by the full spec
@@ -2270,9 +2294,18 @@ class QueueManager {
2270
2294
  // prompt. The server route rejects adapters that do not advertise
2271
2295
  // `freestyle` before this point.
2272
2296
  // (`isFreestyle` itself is resolved above, before the systemAppend build.)
2273
- const railPrompt = isFreestyle
2297
+ let railPrompt = isFreestyle
2274
2298
  ? this._buildFreestylePrompt(commandToRun)
2275
2299
  : (0, runtime_1.formatProviderCommand)(adapter, commandToRun, execution.cwd);
2300
+ // Spec addenda (spec-addenda): every ticket-referencing job carries the
2301
+ // specs' open addenda as a briefing APPENDED after the command (so it rides
2302
+ // as command arguments for CLI providers, plain text for freestyle/runner).
2303
+ // Read + claimed at SPAWN time from the store — restart-durable, and the
2304
+ // same path the loop engine takes, so implement/SDD Quick/freestyle/legacy
2305
+ // all see the identical text.
2306
+ const addendaBriefing = this._buildSpecAddendaBriefing(commandToRun, jobId);
2307
+ if (addendaBriefing)
2308
+ railPrompt = `${railPrompt}\n\n${addendaBriefing}`;
2276
2309
  // Per-job model override (consumed once) takes precedence — used by the
2277
2310
  // freestyle model picker so the user can choose haiku/sonnet/opus per launch.
2278
2311
  const modelOverride = this._jobModelSelection.get(jobId);
@@ -88,6 +88,8 @@ const loop_executors_1 = require("./loop-executors");
88
88
  const worktree_overlay_1 = require("./worktree-overlay");
89
89
  const worktree_node_modules_1 = require("./worktree-node-modules");
90
90
  const revision_seed_1 = require("./revision-seed");
91
+ const pr_follow_up_1 = require("./pr-follow-up");
92
+ const spec_addenda_1 = require("./spec-addenda");
91
93
  const workspace_resolution_1 = require("./workspace-resolution");
92
94
  const feature_flags_1 = require("./feature-flags");
93
95
  const file_provenance_1 = require("./file-provenance");
@@ -432,6 +434,12 @@ async function launchIsolatedRail(input, io = {}) {
432
434
  REVISION_REQUEST: buildRevisionSeedForLaunch(ctx.db, input.revision, ticketIds),
433
435
  }
434
436
  : (0, loop_constants_1.loadConstantMap)(ctx.desktopDb);
437
+ // Spec addenda (spec-addenda): the iteration notes the specs carry are
438
+ // planned ONCE at launch entry (frozen on the delivery row below), then
439
+ // claimed per allocated unit with that unit's run id and rendered into the
440
+ // briefing every AI step of the run receives.
441
+ const addendaStorePath = (0, spec_addenda_1.ticketStorePathForProject)(ctx.project);
442
+ const addendaPlan = (0, spec_addenda_1.planSpecAddendaAt)(addendaStorePath, ticketIds);
435
443
  // Capture the PR-delivery mode ONCE at launch entry so a mid-flight env flip
436
444
  // can never split one launch across the two delivery paths.
437
445
  const prMode = Boolean(input.repositoryExecution) || (0, rail_isolation_1.isRailPrDeliveryEnabled)();
@@ -720,6 +728,8 @@ async function launchIsolatedRail(input, io = {}) {
720
728
  : null,
721
729
  specSnapshot: buildSpecSnapshot(ctx, ticketIds),
722
730
  ...(input.revision ? { revisionNote: input.revision.note, revisionOf: input.revision.ofDeliveryId } : {}),
731
+ ...(input.followUp ? { followUp: input.followUp } : {}),
732
+ ...(addendaPlan.length > 0 ? { specAddenda: (0, spec_addenda_1.snapshotSpecAddenda)(addendaPlan) } : {}),
723
733
  }, input.requiredPrContinuation
724
734
  ? { id: input.requiredPrContinuation.deliveryId, decision: input.requiredPrContinuation.decision }
725
735
  // A revision replaces the generation it revises, atomically, so the rail
@@ -1219,6 +1229,10 @@ async function launchIsolatedRail(input, io = {}) {
1219
1229
  requiresTerminalIntent: true,
1220
1230
  });
1221
1231
  const spec = ctx.getTicketSpec(a.ticketId);
1232
+ const claimedAddenda = (0, spec_addenda_1.claimSpecAddendaForRun)(addendaStorePath, a.ticketIds, a.runId, {
1233
+ plan: addendaPlan.filter((e) => a.ticketIds.includes(e.ticketId)),
1234
+ });
1235
+ (0, spec_addenda_1.broadcastSpecAddendaChange)((msg) => ctx.broadcast(msg), ctx.project.id, claimedAddenda);
1222
1236
  const enginePromise = ctx.loopRunManager.run({
1223
1237
  runtimeProviderOverride: input.runtimeProviderOverride,
1224
1238
  runId: a.runId, loopId, loopName, graph: loopGraph, projectId: ctx.project.id,
@@ -1240,6 +1254,8 @@ async function launchIsolatedRail(input, io = {}) {
1240
1254
  deferTerminalOutcome: true,
1241
1255
  constants, provider, model, effort, ...(deciderEngine ? { deciderEngine } : {}),
1242
1256
  profileName: input.profileName,
1257
+ ...(input.followUp ? { followUp: { id: input.followUp.id, version: input.followUp.version, hash: input.followUp.hash, briefing: (0, pr_follow_up_1.renderFollowUpBriefing)(input.followUp, { prNumber: launchContinuation?.prNumber ?? input.explicitPrTarget?.prNumber ?? null, ticketIds }) } } : {}),
1258
+ ...(claimedAddenda.briefing ? { addenda: { ids: claimedAddenda.snapshot.map((e) => e.id), briefing: claimedAddenda.briefing } } : {}),
1243
1259
  });
1244
1260
  runPromises.push(settleAllocatedRun(a, enginePromise));
1245
1261
  try {
@@ -19,6 +19,7 @@ exports.coerceRailLaunchProposal = coerceRailLaunchProposal;
19
19
  exports.extractRailLaunchProposals = extractRailLaunchProposals;
20
20
  exports.looksLikeRailLaunch = looksLikeRailLaunch;
21
21
  const json_tolerant_1 = require("./json-tolerant");
22
+ const pr_follow_up_scope_1 = require("./pr-follow-up-scope");
22
23
  exports.RAIL_LAUNCH_FENCE = 'rail-launch';
23
24
  exports.RAIL_LAUNCH_PROPOSAL_VERSION = 1;
24
25
  exports.RAIL_LAUNCH_MODES = ['implement', 'batch-implement', 'freestyle', 'loop'];
@@ -97,9 +98,20 @@ function coerceRailLaunchProposal(value) {
97
98
  baseBranch: optString(value.baseBranch, 200),
98
99
  railName: optString(value.railName ?? value.name, 80),
99
100
  rationale: optString(value.rationale ?? value.reason ?? value.why, 400),
101
+ followUp: coerceFollowUp(value.followUp),
100
102
  },
101
103
  };
102
104
  }
105
+ function coerceFollowUp(value) {
106
+ if (value === undefined || value === null)
107
+ return null;
108
+ try {
109
+ return (0, pr_follow_up_scope_1.parseFollowUpInput)(value);
110
+ }
111
+ catch {
112
+ return null;
113
+ }
114
+ }
103
115
  function tidy(body) {
104
116
  return body.replace(/\n{3,}/g, '\n\n').trim();
105
117
  }
@@ -162,7 +162,13 @@ async function executePrDecision(deps, input) {
162
162
  return { status: 404, body: { error: 'Unknown prDeliveryId' } };
163
163
  if (row.parent_delivery_id && deps.repositoryChildOf !== row.parent_delivery_id)
164
164
  return { status: 409, body: { error: 'repository_delivery_requires_parent', parentDeliveryId: row.parent_delivery_id } };
165
- if (!row.parent_delivery_id && row.execution_manifest) {
165
+ // A manifest alone does not make a repository GROUP: a launch that failed
166
+ // before allocating a single repository delivery persists the parent with
167
+ // its manifest and zero children (observed: `git worktree add` refused
168
+ // because the PR branch was checked out in the main clone). Routing that
169
+ // parent into the group path 404'd on "no repositories" — which the UI read
170
+ // as "already resolved" — and left the rail pinned at pending_decision.
171
+ if (!row.parent_delivery_id && row.execution_manifest && (0, rail_pr_store_1.hasRepositoryDeliveries)(deps.db, row.id)) {
166
172
  const { executeRepositoryGroupDecision } = await Promise.resolve().then(() => __importStar(require('./multi-repo-delivery')));
167
173
  return executeRepositoryGroupDecision(deps, input);
168
174
  }
@@ -1525,6 +1531,16 @@ async function runDiscard(deps, row, opts = { removeNeedsReview: true }) {
1525
1531
  // borrows the PR/ticket lifecycle. Per-unit ownership below independently
1526
1532
  // prevents deletion of borrowed/pre-existing head refs for fresh replacements.
1527
1533
  const preserveExternalReview = row.is_continuation === 1;
1534
+ // A preparation failure never ran: no worktree, no branch, no commit, no PR.
1535
+ // Discarding it only closes the attempt and frees the rail — the spec keeps
1536
+ // the status it had (typically on_review from a still-open earlier PR), the
1537
+ // backlog/Jira are not told about a delivery that never existed, and no
1538
+ // resource cleanup can be owed. Distinct from implementation_failed, which
1539
+ // did run and did move the ticket.
1540
+ const preparationFailure = row.decision === 'pr_failed' &&
1541
+ row.implementation_outcome === 'failed' &&
1542
+ !row.pr_url && !row.delivery_sha && !preserveExternalReview &&
1543
+ snap.branches.length === 0 && snap.worktreeIds.length === 0;
1528
1544
  const cleanupWarnings = [];
1529
1545
  // A continuation borrows an existing PR/head. Discarding its local iteration
1530
1546
  // may remove Specrails' worktree, but never closes or deletes borrowed review
@@ -1620,7 +1636,7 @@ async function runDiscard(deps, row, opts = { removeNeedsReview: true }) {
1620
1636
  statusCode: cleanupWarnings.length > 0 ? 'cleanup_incomplete' : 'discarded',
1621
1637
  cleanupWarnings,
1622
1638
  };
1623
- const conflict = preserveExternalReview
1639
+ const conflict = preserveExternalReview || preparationFailure
1624
1640
  ? casTransition(deps, row, 'discarded', terminalPatch)
1625
1641
  : casTransitionWithTicketEffect(deps, row, 'discarded', terminalPatch, {
1626
1642
  deliveryId: row.id,
@@ -1634,7 +1650,7 @@ async function runDiscard(deps, row, opts = { removeNeedsReview: true }) {
1634
1650
  if (conflict)
1635
1651
  return conflict;
1636
1652
  pauseChainsOnDiscard(deps, row);
1637
- if (!preserveExternalReview)
1653
+ if (!preserveExternalReview && !preparationFailure)
1638
1654
  applyTerminalTicketEffect(deps, row, 'discarded', cleanupWarnings);
1639
1655
  finalizeTransition(deps, row.id);
1640
1656
  return {
@@ -1643,6 +1659,7 @@ async function runDiscard(deps, row, opts = { removeNeedsReview: true }) {
1643
1659
  ok: true,
1644
1660
  decision: 'discarded',
1645
1661
  ...(cleanupWarnings.length > 0 ? { cleanupWarnings } : {}),
1662
+ ...(preparationFailure ? { preparationFailure: true } : {}),
1646
1663
  ...(preserveExternalReview ? { preservedBorrowedReview: true, preservedExternalReview: true } : {}),
1647
1664
  },
1648
1665
  };
@@ -6,10 +6,12 @@ exports.boundRevisionNote = boundRevisionNote;
6
6
  exports.readSpecSnapshot = readSpecSnapshot;
7
7
  exports.isTerminalPrDecision = isTerminalPrDecision;
8
8
  exports.createPrDelivery = createPrDelivery;
9
+ exports.isPreparationFailureRow = isPreparationFailureRow;
9
10
  exports.createPrDeliveryGeneration = createPrDeliveryGeneration;
10
11
  exports.failPrDeliveryAndRestoreSuperseded = failPrDeliveryAndRestoreSuperseded;
11
12
  exports.getPrDelivery = getPrDelivery;
12
13
  exports.getActivePrDeliveryByRail = getActivePrDeliveryByRail;
14
+ exports.hasRepositoryDeliveries = hasRepositoryDeliveries;
13
15
  exports.listActivePrDeliveries = listActivePrDeliveries;
14
16
  exports.listTerminalPrDeliveries = listTerminalPrDeliveries;
15
17
  exports.getLatestTerminalPrDeliveryTouchingTicketSet = getLatestTerminalPrDeliveryTouchingTicketSet;
@@ -30,6 +32,17 @@ exports.toPrDeliverySnapshot = toPrDeliverySnapshot;
30
32
  exports.toRailPrStateMessage = toRailPrStateMessage;
31
33
  exports.toPrDecisionCardEnvelope = toPrDecisionCardEnvelope;
32
34
  exports.missionPhaseForDecision = missionPhaseForDecision;
35
+ /**
36
+ * CRUD for the `rail_pr_deliveries` ledger (migration 36) — one row per isolated
37
+ * rail LAUNCH tracking the ask-first PR decision lifecycle. This is the single
38
+ * authoritative server-side state both decision surfaces (the rail row on the
39
+ * dashboard and the launching agent-chat conversation) read and write, so the
40
+ * two surfaces never desync and a refresh/restart never loses a pending
41
+ * decision. Pure data access; the PR plumbing lives in rail-pr-delivery /
42
+ * pr-publisher and the launch wiring in rail-isolated-launch.
43
+ */
44
+ const pr_follow_up_1 = require("./pr-follow-up");
45
+ const spec_addenda_core_1 = require("./spec-addenda-core");
33
46
  const ids_1 = require("./ids");
34
47
  const multi_repo_execution_store_1 = require("./multi-repo-execution-store");
35
48
  /** Defensive per-ticket cap; contract-layer specs run long but bounded. */
@@ -88,13 +101,26 @@ function createPrDelivery(db, input) {
88
101
  loop_name, origin_surface, origin_conversation_id,
89
102
  implementation_outcome, delivery_outcome, status_code,
90
103
  is_continuation, supersedes_delivery_id, spec_snapshot,
91
- revision_note, revision_of, parent_delivery_id, repository_id, repository_path
104
+ revision_note, revision_of, parent_delivery_id, repository_id, repository_path, follow_up, spec_addenda
92
105
  ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 'running', 'pending',
93
- 'implementation_running', ?, ?, ?, ?, ?, ?, ?, ?)`).run(id, input.railIndex, input.loopId ?? null, input.railKey, JSON.stringify(input.ticketIds), input.baseBranch, input.loopName, input.originSurface, input.originConversationId ?? null, input.isContinuation ? 1 : 0, input.supersedesDeliveryId ?? null, input.specSnapshot && input.specSnapshot.length > 0
106
+ 'implementation_running', ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(id, input.railIndex, input.loopId ?? null, input.railKey, JSON.stringify(input.ticketIds), input.baseBranch, input.loopName, input.originSurface, input.originConversationId ?? null, input.isContinuation ? 1 : 0, input.supersedesDeliveryId ?? null, input.specSnapshot && input.specSnapshot.length > 0
94
107
  ? JSON.stringify(boundSpecSnapshot(input.specSnapshot))
95
- : null, input.revisionNote ? boundRevisionNote(input.revisionNote) : null, input.revisionOf ?? null, input.parentDeliveryId ?? null, input.repositoryId ?? null, input.repositoryPath ?? null);
108
+ : null, input.revisionNote ? boundRevisionNote(input.revisionNote) : null, input.revisionOf ?? null, input.parentDeliveryId ?? null, input.repositoryId ?? null, input.repositoryPath ?? null, input.followUp ? JSON.stringify(input.followUp) : null, input.specAddenda && input.specAddenda.length > 0 ? JSON.stringify(input.specAddenda) : null);
96
109
  return getPrDelivery(db, id);
97
110
  }
111
+ /**
112
+ * A delivery that failed BEFORE any repository work was allocated (e.g. `git
113
+ * worktree add` refused): it never ran, so it owns no worktree, branch, commit
114
+ * or PR. Shared by the discard path (no ticket/Jira effect) and the launch
115
+ * route (a relaunch closes it instead of reporting `pr_decision_pending`).
116
+ */
117
+ function isPreparationFailureRow(row) {
118
+ if (row.decision !== 'pr_failed' || row.implementation_outcome !== 'failed')
119
+ return false;
120
+ if (row.pr_url || row.delivery_sha || row.is_continuation)
121
+ return false;
122
+ return parseJsonArray(row.branches ?? '[]').length === 0 && parseJsonArray(row.worktree_ids ?? '[]').length === 0;
123
+ }
98
124
  class PrDeliveryGenerationConflict extends Error {
99
125
  currentId;
100
126
  code = 'PR_DELIVERY_GENERATION_CONFLICT';
@@ -203,6 +229,15 @@ function getActivePrDeliveryByRail(db, railIndex) {
203
229
  .get(railIndex);
204
230
  }
205
231
  /** Every non-terminal delivery — hydrates the GET /rails prDeliveries snapshot. */
232
+ /** True when a delivery group has at least one per-repository child row. A
233
+ * parent that carries an execution manifest but no children is a launch that
234
+ * failed BEFORE any repository delivery was allocated (worktree add refused,
235
+ * DB setup error…) — it must be decided as an ordinary single row, not routed
236
+ * to the per-repository group machinery, which has nothing to act on. */
237
+ function hasRepositoryDeliveries(db, parentId) {
238
+ const row = db.prepare('SELECT 1 AS present FROM rail_pr_deliveries WHERE parent_delivery_id = ? LIMIT 1').get(parentId);
239
+ return Boolean(row);
240
+ }
206
241
  function listActivePrDeliveries(db) {
207
242
  return db
208
243
  .prepare(`SELECT * FROM rail_pr_deliveries
@@ -597,6 +632,8 @@ function toPrDeliverySnapshot(row) {
597
632
  runIds: parseJsonArray(row.run_ids ?? '[]'),
598
633
  originSurface: row.origin_surface,
599
634
  originConversationId: row.origin_conversation_id,
635
+ followUp: (0, pr_follow_up_1.readFollowUp)(row.follow_up),
636
+ specAddenda: (0, spec_addenda_core_1.readSpecAddendaSnapshot)(row.spec_addenda),
600
637
  createdAt: row.created_at,
601
638
  updatedAt: row.updated_at,
602
639
  };
@@ -6,6 +6,7 @@ exports.replayPendingRailPrTicketEffects = replayPendingRailPrTicketEffects;
6
6
  exports.replayRailPrTicketEffectsUntilSettled = replayRailPrTicketEffectsUntilSettled;
7
7
  const rail_pr_store_1 = require("./rail-pr-store");
8
8
  const ticket_store_1 = require("./ticket-store");
9
+ const spec_addenda_1 = require("./spec-addenda");
9
10
  const workspace_resolution_1 = require("./workspace-resolution");
10
11
  /** Commit the terminal decision and its external ticket-file intent together. */
11
12
  function transitionClaimedDecisionWithTicketEffect(db, id, expected, next, operationToken, patch, effect) {
@@ -151,6 +152,16 @@ function selectAppliedTicketIds(db, deliveryId, candidateIds, causalOwners, tick
151
152
  }
152
153
  return parseTicketIds(durable.applied_ticket_ids);
153
154
  }
155
+ /** The frozen addenda snapshot a delivery was launched with (null when none / unreadable). */
156
+ function deliveryAddendaSnapshot(db, deliveryId) {
157
+ try {
158
+ const row = db.prepare(`SELECT spec_addenda FROM rail_pr_deliveries WHERE id = ?`).get(deliveryId);
159
+ return (0, spec_addenda_1.readSpecAddendaSnapshot)(row?.spec_addenda ?? null);
160
+ }
161
+ catch {
162
+ return null;
163
+ }
164
+ }
154
165
  /** Apply one pending row idempotently. Only tickets still parked at on_review
155
166
  * move. The selected ids and each boundary phase are durable, so replay after
156
167
  * the JSON write still hands the original ids to Jira; completion is recorded
@@ -173,6 +184,10 @@ function applyRailPrTicketEffect(deps, deliveryId) {
173
184
  try {
174
185
  if (!row.tickets_applied_at) {
175
186
  const now = new Date().toISOString();
187
+ // A discard (→ todo) destroys the work the delivery's addenda were applied
188
+ // by: reopen exactly those (from the delivery's frozen snapshot) so the
189
+ // next launch carries them again. Read before the lock; malformed ⇒ none.
190
+ const reopenAddenda = row.target_status === 'todo' ? deliveryAddendaSnapshot(deps.db, deliveryId) : null;
176
191
  store = (0, ticket_store_1.mutateStore)(resolveTicketFile(deps), (current) => {
177
192
  appliedTicketIds = selectAppliedTicketIds(deps.db, deliveryId, ticketIds, causalOwners, current.tickets);
178
193
  for (const ticketId of appliedTicketIds) {
@@ -183,6 +198,12 @@ function applyRailPrTicketEffect(deps, deliveryId) {
183
198
  ticket.updated_at = now;
184
199
  changedTicketIds.push(ticketId);
185
200
  }
201
+ if (reopenAddenda) {
202
+ for (const ticketId of (0, spec_addenda_1.reopenSpecAddenda)(current, reopenAddenda, now)) {
203
+ if (!changedTicketIds.includes(ticketId))
204
+ changedTicketIds.push(ticketId);
205
+ }
206
+ }
186
207
  });
187
208
  deps.db.prepare(`
188
209
  UPDATE rail_pr_ticket_effects