@straiffi/archon 1.2.34 → 1.3.1

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 (158) hide show
  1. package/dist/client/assets/TestsDialog-DelI4dOJ.js +5 -0
  2. package/dist/client/assets/arc-4YfDy8uK.js +1 -0
  3. package/dist/client/assets/architecture-7EHR7CIX-B0ldQ5d_.js +1 -0
  4. package/dist/client/assets/architectureDiagram-3BPJPVTR-Cn7GlwQg.js +36 -0
  5. package/dist/client/assets/array-DOVTz2Mq.js +1 -0
  6. package/dist/client/assets/blockDiagram-GPEHLZMM-Dbt3i35a.js +132 -0
  7. package/dist/client/assets/c4Diagram-AAUBKEIU-BCNsqu0u.js +10 -0
  8. package/dist/client/assets/channel-BmkYY3c1.js +1 -0
  9. package/dist/client/assets/chunk-2J33WTMH-B9c8AzpT.js +1 -0
  10. package/dist/client/assets/chunk-3OPIFGDE-D-qpTyaE.js +62 -0
  11. package/dist/client/assets/chunk-4BX2VUAB-B3mVWXS4.js +1 -0
  12. package/dist/client/assets/chunk-4EGX6M5U-DTB6Um9X.js +1 -0
  13. package/dist/client/assets/chunk-55IACEB6-BB-0ajVZ.js +1 -0
  14. package/dist/client/assets/chunk-5DO6E6H7-DtX6GYFY.js +1 -0
  15. package/dist/client/assets/chunk-5ZQYHXKU-bBV3HPCk.js +2 -0
  16. package/dist/client/assets/chunk-727SXJPM-InkxsEhY.js +206 -0
  17. package/dist/client/assets/chunk-AQP2D5EJ-DZZL694W.js +231 -0
  18. package/dist/client/assets/chunk-BR22UD5L-CkJbcXZG.js +1 -0
  19. package/dist/client/assets/chunk-BSJP7CBP-X4eyoSqE.js +1 -0
  20. package/dist/client/assets/chunk-CSCIHK7Q-BuAgybUd.js +124 -0
  21. package/dist/client/assets/chunk-FHYWG6QK-Cb5GzaCb.js +1 -0
  22. package/dist/client/assets/chunk-FMBD7UC4-mGY2D2YB.js +15 -0
  23. package/dist/client/assets/chunk-KSCS5N6A-B9fgatSq.js +10 -0
  24. package/dist/client/assets/chunk-L5ZTLDWV-L-7z2xOe.js +1 -0
  25. package/dist/client/assets/chunk-LZXEDZCA-hrScZnrx.js +2 -0
  26. package/dist/client/assets/chunk-MPE355IW-WT7jTCZh.js +1 -0
  27. package/dist/client/assets/chunk-MZUSXYTE-CClXfOSj.js +1 -0
  28. package/dist/client/assets/chunk-N66VUXT2-Bpe4yprw.js +1 -0
  29. package/dist/client/assets/chunk-ND2GUHAM-d2HjiRzD.js +1 -0
  30. package/dist/client/assets/chunk-NNHCCRGN-C4ci-Gd6.js +159 -0
  31. package/dist/client/assets/chunk-NZK2D7GU-C6cn05ZM.js +1 -0
  32. package/dist/client/assets/chunk-O5CBEL6O-BAjbtmB_.js +70 -0
  33. package/dist/client/assets/chunk-PUPMXCY4-Dh45WL5s.js +1 -0
  34. package/dist/client/assets/chunk-QZHKN3VN-U_l6-b5-.js +1 -0
  35. package/dist/client/assets/chunk-UIBZB4QT-Cp2xSKlH.js +1 -0
  36. package/dist/client/assets/chunk-WCWK7LTN-BOt5FFIu.js +1 -0
  37. package/dist/client/assets/chunk-WU5MYG2G-By4sIjXN.js +1 -0
  38. package/dist/client/assets/chunk-XPW4576I-YiT9fJFY.js +32 -0
  39. package/dist/client/assets/classDiagram-4FO5ZUOK-B9B9lIAo.js +1 -0
  40. package/dist/client/assets/classDiagram-v2-Q7XG4LA2-DXR72HYe.js +1 -0
  41. package/dist/client/assets/cose-bilkent-S5V4N54A-CtC2zeMr.js +1 -0
  42. package/dist/client/assets/cytoscape.esm-DnmRtSj9.js +321 -0
  43. package/dist/client/assets/dagre-5AiD43Uu.js +1 -0
  44. package/dist/client/assets/dagre-BM42HDAG-_iu0KXos.js +4 -0
  45. package/dist/client/assets/defaultLocale-Ck2Xxk-C.js +1 -0
  46. package/dist/client/assets/diagram-2AECGRRQ-DyBAakP6.js +43 -0
  47. package/dist/client/assets/diagram-5GNKFQAL-xD8YOWp-.js +10 -0
  48. package/dist/client/assets/diagram-KO2AKTUF-BQDiusQc.js +3 -0
  49. package/dist/client/assets/diagram-LMA3HP47-BRu1DXcK.js +24 -0
  50. package/dist/client/assets/diagram-OG6HWLK6-C3A1wt8S.js +24 -0
  51. package/dist/client/assets/dist-BQjeO--f.js +1 -0
  52. package/dist/client/assets/erDiagram-TEJ5UH35-C_fWil4q.js +85 -0
  53. package/dist/client/assets/eventmodeling-FCH6USID-BczyucFk.js +1 -0
  54. package/dist/client/assets/flowDiagram-I6XJVG4X-CKsgXWad.js +162 -0
  55. package/dist/client/assets/ganttDiagram-6RSMTGT7-DRhmLmdi.js +292 -0
  56. package/dist/client/assets/gitGraph-WXDBUCRP-x5mlGyzr.js +1 -0
  57. package/dist/client/assets/gitGraphDiagram-PVQCEYII-C34Vr5C1.js +106 -0
  58. package/dist/client/assets/graphlib-CMVU3iJd.js +1 -0
  59. package/dist/client/assets/index-DENrWubs.js +260 -0
  60. package/dist/client/assets/index-iElL-crz.css +2 -0
  61. package/dist/client/assets/info-J43DQDTF-BsM_Qb70.js +1 -0
  62. package/dist/client/assets/infoDiagram-5YYISTIA-Bkbr_hQH.js +2 -0
  63. package/dist/client/assets/init-Bft5Ffpj.js +1 -0
  64. package/dist/client/assets/ishikawaDiagram-YF4QCWOH-CEXsy2hU.js +70 -0
  65. package/dist/client/assets/journeyDiagram-JHISSGLW-RuKnL83f.js +139 -0
  66. package/dist/client/assets/kanban-definition-UN3LZRKU-DrlhCmkA.js +89 -0
  67. package/dist/client/assets/katex-Bpz5kWH9.js +257 -0
  68. package/dist/client/assets/line-CGdxyPh9.js +1 -0
  69. package/dist/client/assets/linear-BYVhmsFj.js +1 -0
  70. package/dist/client/assets/mermaid-parser.core-CbzywbCW.js +4 -0
  71. package/dist/client/assets/mermaid.core-09FBpi2R.js +9 -0
  72. package/dist/client/assets/mindmap-definition-RKZ34NQL-CXrsI5b1.js +96 -0
  73. package/dist/client/assets/ordinal-DIg8h6NI.js +1 -0
  74. package/dist/client/assets/packet-YPE3B663-DeVIMHV5.js +1 -0
  75. package/dist/client/assets/path-DfRbCp9y.js +1 -0
  76. package/dist/client/assets/pie-LRSECV5Y-ByRF4_cj.js +1 -0
  77. package/dist/client/assets/pieDiagram-4H26LBE5-HbOpep9N.js +30 -0
  78. package/dist/client/assets/quadrantDiagram-W4KKPZXB-BWw-ZWMn.js +7 -0
  79. package/dist/client/assets/radar-GUYGQ44K-XEreg8aR.js +1 -0
  80. package/dist/client/assets/requirementDiagram-4Y6WPE33-CpscsQdI.js +84 -0
  81. package/dist/client/assets/rough.esm-DCvqWubr.js +1 -0
  82. package/dist/client/assets/sankeyDiagram-5OEKKPKP-BoV5tyAf.js +40 -0
  83. package/dist/client/assets/sequenceDiagram-3UESZ5HK-CBcDRtjR.js +162 -0
  84. package/dist/client/assets/src-BcaL9nCM.js +1 -0
  85. package/dist/client/assets/stateDiagram-AJRCARHV-Df9lcs70.js +1 -0
  86. package/dist/client/assets/stateDiagram-v2-BHNVJYJU-FIWIctC4.js +1 -0
  87. package/dist/client/assets/timeline-definition-PNZ67QCA-BFv1VICg.js +120 -0
  88. package/dist/client/assets/treeView-BLDUP644-D85P2Ji3.js +1 -0
  89. package/dist/client/assets/treemap-LRROVOQU-BnXG5utQ.js +1 -0
  90. package/dist/client/assets/vennDiagram-CIIHVFJN-DPRtn-XH.js +34 -0
  91. package/dist/client/assets/wardley-L42UT6IY-Ct9qHtJP.js +1 -0
  92. package/dist/client/assets/wardleyDiagram-YWT4CUSO-CDiint0P.js +78 -0
  93. package/dist/client/assets/xychartDiagram-2RQKCTM6-ELyFdgmG.js +7 -0
  94. package/dist/client/index.html +3 -4
  95. package/dist/server/db.js +92 -0
  96. package/dist/server/db.js.map +1 -1
  97. package/dist/server/index.js +396 -123
  98. package/dist/server/index.js.map +1 -1
  99. package/dist/server/lib/agent.js +462 -0
  100. package/dist/server/lib/agent.js.map +1 -1
  101. package/dist/server/lib/attachments.js +146 -0
  102. package/dist/server/lib/attachments.js.map +1 -1
  103. package/dist/server/lib/buildFlow.js +2 -1
  104. package/dist/server/lib/buildFlow.js.map +1 -1
  105. package/dist/server/lib/chatOperations.js +2 -0
  106. package/dist/server/lib/chatOperations.js.map +1 -1
  107. package/dist/server/lib/chats.js +9 -4
  108. package/dist/server/lib/chats.js.map +1 -1
  109. package/dist/server/lib/externalSource.js +32 -0
  110. package/dist/server/lib/externalSource.js.map +1 -0
  111. package/dist/server/lib/httpError.js +9 -0
  112. package/dist/server/lib/httpError.js.map +1 -0
  113. package/dist/server/lib/mobileAccess.js +3 -0
  114. package/dist/server/lib/mobileAccess.js.map +1 -1
  115. package/dist/server/lib/specArtifact.js +353 -0
  116. package/dist/server/lib/specArtifact.js.map +1 -0
  117. package/dist/server/lib/specDraft.js +33 -0
  118. package/dist/server/lib/specDraft.js.map +1 -0
  119. package/dist/server/lib/specExport.js +70 -0
  120. package/dist/server/lib/specExport.js.map +1 -0
  121. package/dist/server/lib/specFormatting.js +26 -0
  122. package/dist/server/lib/specFormatting.js.map +1 -0
  123. package/dist/server/lib/specMessages.js +79 -0
  124. package/dist/server/lib/specMessages.js.map +1 -0
  125. package/dist/server/lib/specOperations.js +171 -0
  126. package/dist/server/lib/specOperations.js.map +1 -0
  127. package/dist/server/lib/specSections.js +82 -0
  128. package/dist/server/lib/specSections.js.map +1 -0
  129. package/dist/server/lib/specs.js +347 -0
  130. package/dist/server/lib/specs.js.map +1 -0
  131. package/dist/server/lib/thinkingState.js +26 -0
  132. package/dist/server/lib/thinkingState.js.map +1 -1
  133. package/dist/server/lib/ticketBatch.js +55 -0
  134. package/dist/server/lib/ticketBatch.js.map +1 -0
  135. package/dist/server/lib/ticketDescriptionContract.js +52 -0
  136. package/dist/server/lib/ticketDescriptionContract.js.map +1 -0
  137. package/dist/server/lib/tickets.js +7 -3
  138. package/dist/server/lib/tickets.js.map +1 -1
  139. package/dist/server/workers/build.js +10 -1
  140. package/dist/server/workers/build.js.map +1 -1
  141. package/dist/server/workers/chat.js +10 -1
  142. package/dist/server/workers/chat.js.map +1 -1
  143. package/dist/server/workers/followUp.js +10 -1
  144. package/dist/server/workers/followUp.js.map +1 -1
  145. package/dist/server/workers/plan.js +10 -32
  146. package/dist/server/workers/plan.js.map +1 -1
  147. package/dist/server/workers/planFollowUp.js +10 -0
  148. package/dist/server/workers/planFollowUp.js.map +1 -1
  149. package/dist/server/workers/review.js +10 -1
  150. package/dist/server/workers/review.js.map +1 -1
  151. package/dist/server/workers/specSession.js +520 -0
  152. package/dist/server/workers/specSession.js.map +1 -0
  153. package/package.json +1 -1
  154. package/dist/client/assets/TestsDialog-1JPVtcz7.js +0 -5
  155. package/dist/client/assets/dropdown-menu-BULYW006.js +0 -43
  156. package/dist/client/assets/index-DGoXllNX.js +0 -150
  157. package/dist/client/assets/index-DoNUd8CK.css +0 -2
  158. /package/dist/client/assets/{rolldown-runtime-BYbx6iT9.js → chunk-BNv3lrIs.js} +0 -0
@@ -9,8 +9,10 @@ import cors from 'cors';
9
9
  import { adjectives, animals, uniqueNamesGenerator } from 'unique-names-generator';
10
10
  import db from './db.js';
11
11
  import { deleteChatSession, getChatSession, listChatSessions, updateChatSessionMode } from './lib/chats.js';
12
- import { createChatAttachment, deleteChatAttachment, getChatAttachmentById, getChatAttachmentForSession, getChatAttachmentLinkedMessageId, MAX_CHAT_ATTACHMENTS_PER_TURN, MAX_CHAT_ATTACHMENT_SIZE_BYTES, readChatAttachmentContent, validateImageAttachment, } from './lib/attachments.js';
12
+ import { deleteSpecSession, getCreatedTicketIdsForSnapshot, getSpecSession, getSpecSnapshot, listSpecSessions } from './lib/specs.js';
13
+ import { createChatAttachment, createSpecAttachment, deleteChatAttachment, deleteSpecSessionAttachment, getChatAttachmentById, getChatAttachmentForSession, getChatAttachmentLinkedMessageId, getSpecAttachmentForSession, getSpecAttachmentLinkedMessageId, MAX_CHAT_ATTACHMENTS_PER_TURN, MAX_CHAT_ATTACHMENT_SIZE_BYTES, MAX_SPEC_ATTACHMENTS_PER_TURN, MAX_SPEC_ATTACHMENT_SIZE_BYTES, readChatAttachmentContent, readSpecAttachmentContent, validateImageAttachment, } from './lib/attachments.js';
13
14
  import { createProjectChatSession, resolveChatMode, resolveChatTool, stopProjectChatSessionResponse, submitProjectChatSessionMessage, } from './lib/chatOperations.js';
15
+ import { createProjectSpecSession, deleteProjectSpecSession, stopProjectSpecSessionResponse, submitProjectSpecSessionMessage, updateProjectSpecSessionMode, } from './lib/specOperations.js';
14
16
  import { resolveChatSessionTarget } from './lib/chatTargets.js';
15
17
  import { createChatMessage, getChatMessage, updateChatMessageContent } from './lib/chatMessages.js';
16
18
  import { attachChatTicketProposalsToMessageContent } from './lib/chatTicketProposals.js';
@@ -23,7 +25,7 @@ import { browseRepoPath } from './lib/directoryPicker.js';
23
25
  import { resolveDesktopServerHost } from './lib/desktopServerHost.js';
24
26
  import { emitMobileRealtime, getMobileAccessStatus, regenerateMobilePairingChallenge, revokeMobileSession, setMobileAccessStatusChangeListener, startMobileAccess, stopMobileAccess, } from './lib/mobileAccess.js';
25
27
  import { validateDesktopMobileAccessRequest } from './lib/mobileAccessSecurity.js';
26
- import { refreshChatSessionUsageSnapshot, stopAllBuildAgents } from './lib/agent.js';
28
+ import { refreshChatSessionUsageSnapshot, refreshSpecSessionUsageSnapshot, stopAllBuildAgents } from './lib/agent.js';
27
29
  import { runLightweightPrompt } from './lib/lightweightPrompt.js';
28
30
  import { startBuildChain, stopBuildChain } from './lib/buildChains.js';
29
31
  import { enqueueBundledBuild, getDependencyOrderedBundleTicketIds, removeTicketFromBuildSequence, stopAllActiveBuildSequences, } from './lib/buildSequences.js';
@@ -58,19 +60,17 @@ import { getActiveBundleStageChangeHead, undoTicketStage } from './lib/ticketUnd
58
60
  import { closeAllTerminalSessions, createProjectTerminalSession, createTerminalSessionForWorkspace, destroyTerminalSessionById, registerTerminalSocketHandlers, } from './lib/terminal.js';
59
61
  import { countBundleTickets, createTicketRecord, createBundle, deleteBundle, ensureProjectRootBundle, autoParkTicketIfStale, autoParkSettledBundleTicket, isBundleSettledForAutoPark, clearAutoParkDismissalIfNeeded, assignTicketDoneOrder, assignTicketLaneOrder, getBundle, getBundleByName, getBundleByBranch, getBundleRepresentativeTicketContext, isProjectRootBundle, getTicket, isBundledTicket, listTicketsInRunContext, listBundles, listBoardTicketEnrichment, listTickets, resolveTicketBranch, resolveTicketTool, } from './lib/tickets.js';
60
62
  import { createTicketMessage, upsertTicketDescriptionMessage } from './lib/ticketMessages.js';
63
+ import { HttpError } from './lib/httpError.js';
64
+ import { parseIncomingExternalSource } from './lib/externalSource.js';
65
+ import { materializeTicketsWithDependencies } from './lib/ticketBatch.js';
66
+ import { IMPLEMENTATION_READY_DESCRIPTION_SECTION_HEADINGS } from './lib/ticketDescriptionContract.js';
67
+ import { buildSpecImplementationDraft } from './lib/specDraft.js';
68
+ import { exportSpecToDocs, checkSpecExportCollision } from './lib/specExport.js';
61
69
  import { createTicketDependency, deleteTicketDependenciesForTicket, deleteTicketDependency, getTicketDependency, isTicketRunSetupReady, listTicketDependencies, validateCreateTicketDependency, } from './lib/ticketDependencies.js';
62
70
  import { startBuild } from './workers/build.js';
63
71
  import { startFollowUp } from './workers/followUp.js';
64
- import { expandPlanningTicketOutline, generatePlanningTicketOutline, generatePlanText, IMPLEMENTATION_READY_DESCRIPTION_SECTION_HEADINGS, prepareLinkedPlanningResearch, startPlan, } from './workers/plan.js';
72
+ import { expandPlanningTicketOutline, generatePlanningTicketOutline, generatePlanText, prepareLinkedPlanningResearch, startPlan, } from './workers/plan.js';
65
73
  import { startReview } from './workers/review.js';
66
- class HttpError extends Error {
67
- status;
68
- constructor(status, message) {
69
- super(message);
70
- this.name = 'HttpError';
71
- this.status = status;
72
- }
73
- }
74
74
  const getErrorMessage = (error) => {
75
75
  return error instanceof Error ? error.message : String(error);
76
76
  };
@@ -296,6 +296,24 @@ const getChatAttachmentUploadErrorMessage = (error) => {
296
296
  }
297
297
  return 'Unable to upload the selected images right now.';
298
298
  };
299
+ const specAttachmentUpload = multer({
300
+ storage: multer.memoryStorage(),
301
+ limits: {
302
+ files: MAX_SPEC_ATTACHMENTS_PER_TURN,
303
+ fileSize: MAX_SPEC_ATTACHMENT_SIZE_BYTES,
304
+ },
305
+ });
306
+ const getSpecAttachmentUploadErrorMessage = (error) => {
307
+ if (error instanceof multer.MulterError) {
308
+ if (error.code === 'LIMIT_FILE_COUNT') {
309
+ return 'You can attach up to 4 images per turn.';
310
+ }
311
+ if (error.code === 'LIMIT_FILE_SIZE') {
312
+ return 'Images must be 10 MB or smaller.';
313
+ }
314
+ }
315
+ return 'Unable to upload the selected images right now.';
316
+ };
299
317
  const normalizeGeneratedCommitMessage = (value) => {
300
318
  const firstLine = value
301
319
  .replace(/\r/g, '\n')
@@ -396,33 +414,6 @@ const listBundleReviewTicketContextsStatement = db.prepare(`
396
414
  AND tickets.worktree_bundle_id = ?
397
415
  ORDER BY tickets.updated_at DESC, tickets.id DESC
398
416
  `);
399
- const parseIncomingExternalSource = (value) => {
400
- if (!value || typeof value !== 'object' || Array.isArray(value)) {
401
- return null;
402
- }
403
- const payload = value;
404
- const provider = asTrimmedString(payload.provider);
405
- const externalId = asTrimmedString(payload.external_id);
406
- const externalKey = asTrimmedString(payload.external_key);
407
- const externalUrl = asTrimmedString(payload.external_url);
408
- if (provider === '' && externalId === '' && externalKey === '' && externalUrl === '') {
409
- return null;
410
- }
411
- if ((provider !== 'jira' && provider !== 'github') || !externalId || !externalKey) {
412
- throw new HttpError(400, 'external_source must contain a supported provider, external_id, and external_key');
413
- }
414
- const metadata = payload.metadata;
415
- if (metadata != null && (typeof metadata !== 'object' || Array.isArray(metadata))) {
416
- throw new HttpError(400, 'external_source metadata must be an object when provided');
417
- }
418
- return {
419
- provider: provider,
420
- external_id: externalId,
421
- external_key: externalKey,
422
- external_url: externalUrl || null,
423
- metadata: metadata ?? null,
424
- };
425
- };
426
417
  const slugify = (text) => {
427
418
  const slug = String(text ?? '')
428
419
  .replace(/[^a-zA-Z0-9\s-]/g, '')
@@ -574,6 +565,13 @@ const getProjectChatSession = (projectId, chatSessionId) => {
574
565
  }
575
566
  return chatSession;
576
567
  };
568
+ const getProjectSpecSession = (projectId, specSessionId) => {
569
+ const specSession = getSpecSession(specSessionId, { includeMessages: true });
570
+ if (!specSession || specSession.project_id !== projectId) {
571
+ return null;
572
+ }
573
+ return specSession;
574
+ };
577
575
  const resolveTicketDiffWorkspace = (ticket) => {
578
576
  const project = getEffectiveProject(ticket);
579
577
  if (!project) {
@@ -1910,35 +1908,25 @@ const validateExpandedPlanningAgentDraft = (outline, draft) => {
1910
1908
  return draft;
1911
1909
  };
1912
1910
  const createPlanningAgentTickets = db.transaction(({ draft, initiativePrompt, projectId, model, variant, skills, tool, worktreeBundleId, }) => {
1913
- const createdTickets = [];
1914
- const createdDependencies = [];
1915
- const ticketIdsByKey = new Map();
1916
- const createdTicketDefinitions = new Map();
1917
- for (const ticket of draft.tickets) {
1918
- const planningContext = {
1919
- source_kind: 'multi',
1920
- initiative_prompt: initiativePrompt,
1921
- related_ticket_ids: [],
1922
- open_questions: ticket.open_questions,
1923
- };
1924
- const createdTicket = createTicketRecord({
1925
- title: ticket.title,
1926
- description: ticket.description,
1911
+ const { createdTickets, createdDependencies, ticketIdsByKey } = materializeTicketsWithDependencies({
1912
+ options: {
1913
+ projectId,
1927
1914
  model,
1928
1915
  variant,
1929
1916
  skills,
1930
1917
  tool: tool ?? 'opencode',
1931
- agentStatus: 'done',
1932
- projectId,
1933
1918
  worktreeBundleId,
1934
- planningContext,
1935
- });
1936
- if (!createdTicket) {
1937
- throw new HttpError(500, 'Unable to create the generated tickets.');
1919
+ agentStatus: 'done',
1920
+ },
1921
+ tickets: draft.tickets,
1922
+ dependencies: draft.dependencies,
1923
+ });
1924
+ const createdTicketDefinitions = new Map();
1925
+ for (const ticket of draft.tickets) {
1926
+ const createdId = ticketIdsByKey.get(ticket.key);
1927
+ if (createdId) {
1928
+ createdTicketDefinitions.set(createdId, ticket);
1938
1929
  }
1939
- createdTickets.push(createdTicket);
1940
- ticketIdsByKey.set(ticket.key, createdTicket.id);
1941
- createdTicketDefinitions.set(createdTicket.id, ticket);
1942
1930
  }
1943
1931
  const createdTicketIds = createdTickets.map(ticket => ticket.id);
1944
1932
  for (const createdTicket of createdTickets) {
@@ -1961,72 +1949,28 @@ const createPlanningAgentTickets = db.transaction(({ draft, initiativePrompt, pr
1961
1949
  });
1962
1950
  }
1963
1951
  }
1964
- for (const dependency of draft.dependencies) {
1965
- const blockerTicketId = ticketIdsByKey.get(dependency.blocker_key);
1966
- const dependentTicketId = ticketIdsByKey.get(dependency.dependent_key);
1967
- if (!blockerTicketId || !dependentTicketId) {
1968
- throw new HttpError(500, 'Planning agent returned a dependency for an unknown generated ticket.');
1969
- }
1970
- const validation = validateCreateTicketDependency({
1971
- blockerTicketId,
1972
- dependentTicketId,
1973
- projectId,
1974
- });
1975
- if (!validation.ok) {
1976
- throw new HttpError(validation.status, validation.error);
1977
- }
1978
- const createdDependency = createTicketDependency(blockerTicketId, dependentTicketId);
1979
- if (!createdDependency) {
1980
- throw new HttpError(500, 'Unable to create the generated ticket dependency.');
1981
- }
1982
- createdDependencies.push(createdDependency);
1983
- }
1984
1952
  return { tickets: createdTickets, dependencies: createdDependencies };
1985
1953
  });
1986
1954
  const createChatProposedTickets = db.transaction(({ project, chatSession, proposals, }) => {
1987
- const createdTickets = [];
1988
- const createdDependencies = [];
1989
- const ticketIdsByKey = new Map();
1990
- for (const proposedTicket of proposals.tickets) {
1991
- const bundle = createAutoBundleForName(proposedTicket.title, project.id);
1992
- const createdTicket = createTicketRecord({
1993
- title: proposedTicket.title,
1994
- description: proposedTicket.description,
1955
+ const bundle = createAutoPlanningBundle(project.id);
1956
+ const { createdTickets, createdDependencies, ticketIdsByKey } = materializeTicketsWithDependencies({
1957
+ options: {
1958
+ projectId: project.id,
1995
1959
  model: chatSession.model,
1996
1960
  variant: chatSession.variant,
1997
1961
  skills: chatSession.skills,
1998
- tool: chatSession.tool,
1999
- projectId: project.id,
1962
+ tool: null,
2000
1963
  worktreeBundleId: bundle.id,
2001
- });
2002
- if (!createdTicket) {
2003
- throw new HttpError(500, 'Unable to create the proposed tickets.');
2004
- }
2005
- createdTickets.push({ key: proposedTicket.key, ticket: createdTicket });
2006
- ticketIdsByKey.set(proposedTicket.key, createdTicket.id);
2007
- }
2008
- for (const dependency of proposals.dependencies) {
2009
- const blockerTicketId = ticketIdsByKey.get(dependency.blocker_key);
2010
- const dependentTicketId = ticketIdsByKey.get(dependency.dependent_key);
2011
- if (!blockerTicketId || !dependentTicketId) {
2012
- throw new HttpError(500, 'Chat ticket proposal returned a dependency for an unknown ticket key.');
2013
- }
2014
- const validation = validateCreateTicketDependency({
2015
- blockerTicketId,
2016
- dependentTicketId,
2017
- projectId: project.id,
2018
- });
2019
- if (!validation.ok) {
2020
- throw new HttpError(validation.status, validation.error);
2021
- }
2022
- const createdDependency = createTicketDependency(blockerTicketId, dependentTicketId);
2023
- if (!createdDependency) {
2024
- throw new HttpError(500, 'Unable to create the proposed ticket dependency.');
2025
- }
2026
- createdDependencies.push(createdDependency);
2027
- }
1964
+ },
1965
+ tickets: proposals.tickets,
1966
+ dependencies: proposals.dependencies,
1967
+ });
1968
+ const createdTicketsWithKeys = createdTickets.map(ticket => {
1969
+ const key = [...ticketIdsByKey.entries()].find(([, id]) => id === ticket.id)?.[0] ?? '';
1970
+ return { key, ticket };
1971
+ });
2028
1972
  return {
2029
- createdTickets,
1973
+ createdTickets: createdTicketsWithKeys,
2030
1974
  createdDependencies,
2031
1975
  };
2032
1976
  });
@@ -2441,6 +2385,335 @@ app.post('/projects/:id/chat-sessions/:sessionId/messages/:messageId/create-tick
2441
2385
  return res.status(500).json({ error: getErrorMessage(error) || 'Unable to create tickets from the chat proposal.' });
2442
2386
  }
2443
2387
  });
2388
+ app.get('/projects/:id/spec-sessions', (req, res) => {
2389
+ const project = getProjectById(req.params.id);
2390
+ if (!project) {
2391
+ return res.status(404).json({ error: 'Not found' });
2392
+ }
2393
+ return res.json(listSpecSessions(project.id));
2394
+ });
2395
+ app.post('/projects/:id/spec-sessions', (req, res) => {
2396
+ const result = createProjectSpecSession({
2397
+ projectId: req.params.id,
2398
+ payload: req.body,
2399
+ broadcaster: io,
2400
+ });
2401
+ if (!result.ok) {
2402
+ return res.status(result.status).json({ error: result.error });
2403
+ }
2404
+ return res.status(result.status).json(result.value);
2405
+ });
2406
+ app.get('/projects/:id/spec-sessions/:sessionId', (req, res) => {
2407
+ const project = getProjectById(req.params.id);
2408
+ if (!project) {
2409
+ return res.status(404).json({ error: 'Not found' });
2410
+ }
2411
+ const specSession = getProjectSpecSession(project.id, req.params.sessionId);
2412
+ if (!specSession) {
2413
+ return res.status(404).json({ error: 'Not found' });
2414
+ }
2415
+ const refreshedSpecSession = refreshSpecSessionUsageSnapshot(specSession.id, { includeMessages: true });
2416
+ if (!refreshedSpecSession || refreshedSpecSession.project_id !== project.id) {
2417
+ return res.json(specSession);
2418
+ }
2419
+ return res.json(refreshedSpecSession);
2420
+ });
2421
+ app.patch('/projects/:id/spec-sessions/:sessionId', (req, res) => {
2422
+ const result = updateProjectSpecSessionMode({
2423
+ projectId: req.params.id,
2424
+ specSessionId: req.params.sessionId,
2425
+ payload: req.body,
2426
+ broadcaster: io,
2427
+ });
2428
+ if (!result.ok) {
2429
+ return res.status(result.status).json({ error: result.error });
2430
+ }
2431
+ return res.status(result.status).json(result.value);
2432
+ });
2433
+ app.post('/projects/:id/spec-sessions/:sessionId/messages', (req, res) => {
2434
+ const result = submitProjectSpecSessionMessage({
2435
+ projectId: req.params.id,
2436
+ specSessionId: req.params.sessionId,
2437
+ payload: req.body,
2438
+ broadcaster: io,
2439
+ });
2440
+ if (!result.ok) {
2441
+ return res.status(result.status).json({ error: result.error });
2442
+ }
2443
+ return res.status(result.status).json(result.value);
2444
+ });
2445
+ app.post('/projects/:id/spec-sessions/:sessionId/attachments', (req, res) => {
2446
+ const project = getProjectById(req.params.id);
2447
+ if (!project) {
2448
+ return res.status(404).json({ error: 'Not found' });
2449
+ }
2450
+ const specSession = getProjectSpecSession(project.id, req.params.sessionId);
2451
+ if (!specSession) {
2452
+ return res.status(404).json({ error: 'Not found' });
2453
+ }
2454
+ if (specSession.agent_status === 'running') {
2455
+ return res.status(409).json({ error: 'Wait for the current spec response to finish before uploading images.' });
2456
+ }
2457
+ return specAttachmentUpload.array('files', MAX_SPEC_ATTACHMENTS_PER_TURN)(req, res, error => {
2458
+ if (error) {
2459
+ return res.status(400).json({ error: getSpecAttachmentUploadErrorMessage(error) });
2460
+ }
2461
+ const files = (req.files ?? []);
2462
+ if (files.length === 0) {
2463
+ return res.status(400).json({ error: 'At least one image is required.' });
2464
+ }
2465
+ for (const file of files) {
2466
+ const validationError = validateImageAttachment({
2467
+ mimeType: file.mimetype,
2468
+ originalName: file.originalname,
2469
+ });
2470
+ if (validationError) {
2471
+ return res.status(400).json({ error: validationError });
2472
+ }
2473
+ }
2474
+ const createdAttachmentIds = [];
2475
+ try {
2476
+ const attachments = files.map(file => {
2477
+ const attachment = createSpecAttachment({
2478
+ specSessionId: specSession.id,
2479
+ originalName: file.originalname,
2480
+ mimeType: file.mimetype,
2481
+ buffer: file.buffer,
2482
+ projectId: project.id,
2483
+ });
2484
+ createdAttachmentIds.push(attachment.id);
2485
+ return attachment;
2486
+ });
2487
+ return res.status(201).json({ attachments });
2488
+ }
2489
+ catch (uploadError) {
2490
+ for (const attachmentId of createdAttachmentIds) {
2491
+ const attachment = getChatAttachmentById(attachmentId);
2492
+ if (!attachment) {
2493
+ continue;
2494
+ }
2495
+ deleteSpecSessionAttachment(attachment);
2496
+ }
2497
+ return res.status(500).json({ error: uploadError instanceof Error ? uploadError.message : 'Unable to upload the selected images right now.' });
2498
+ }
2499
+ });
2500
+ });
2501
+ app.delete('/projects/:id/spec-sessions/:sessionId/attachments/:attachmentId', (req, res) => {
2502
+ const project = getProjectById(req.params.id);
2503
+ if (!project) {
2504
+ return res.status(404).json({ error: 'Not found' });
2505
+ }
2506
+ const specSession = getProjectSpecSession(project.id, req.params.sessionId);
2507
+ if (!specSession) {
2508
+ return res.status(404).json({ error: 'Not found' });
2509
+ }
2510
+ const attachment = getSpecAttachmentForSession(req.params.attachmentId, specSession.id);
2511
+ if (!attachment) {
2512
+ return res.status(404).json({ error: 'Attachment not found' });
2513
+ }
2514
+ if (getSpecAttachmentLinkedMessageId(attachment.id)) {
2515
+ return res.status(409).json({ error: 'Sent message attachments cannot be removed.' });
2516
+ }
2517
+ deleteSpecSessionAttachment(attachment);
2518
+ return res.json({ ok: true });
2519
+ });
2520
+ app.get('/projects/:id/spec-sessions/:sessionId/attachments/:attachmentId/content', (req, res) => {
2521
+ const project = getProjectById(req.params.id);
2522
+ if (!project) {
2523
+ return res.status(404).json({ error: 'Not found' });
2524
+ }
2525
+ const specSession = getProjectSpecSession(project.id, req.params.sessionId);
2526
+ if (!specSession) {
2527
+ return res.status(404).json({ error: 'Not found' });
2528
+ }
2529
+ const attachment = getSpecAttachmentForSession(req.params.attachmentId, specSession.id);
2530
+ if (!attachment) {
2531
+ return res.status(404).json({ error: 'Attachment not found' });
2532
+ }
2533
+ try {
2534
+ const content = readSpecAttachmentContent(attachment);
2535
+ res.type(attachment.mime_type);
2536
+ return res.send(content);
2537
+ }
2538
+ catch {
2539
+ return res.status(404).json({ error: 'Attachment content not found' });
2540
+ }
2541
+ });
2542
+ app.post('/projects/:id/spec-sessions/:sessionId/stop', async (req, res) => {
2543
+ const result = await stopProjectSpecSessionResponse({
2544
+ projectId: req.params.id,
2545
+ specSessionId: req.params.sessionId,
2546
+ });
2547
+ if (!result.ok) {
2548
+ return res.status(result.status).json({ error: result.error });
2549
+ }
2550
+ return res.status(result.status).json(result.value);
2551
+ });
2552
+ app.delete('/projects/:id/spec-sessions/:sessionId', (req, res) => {
2553
+ const project = getProjectById(req.params.id);
2554
+ if (!project) {
2555
+ return res.status(404).json({ error: 'Not found' });
2556
+ }
2557
+ const specSession = getProjectSpecSession(project.id, req.params.sessionId);
2558
+ if (!specSession) {
2559
+ return res.status(404).json({ error: 'Not found' });
2560
+ }
2561
+ if (specSession.agent_status === 'running') {
2562
+ return res.status(409).json({ error: 'Wait for the current spec response to finish before deleting this session.' });
2563
+ }
2564
+ deleteProjectSpecSession({
2565
+ projectId: project.id,
2566
+ specSessionId: specSession.id,
2567
+ broadcaster: io,
2568
+ });
2569
+ return res.json({ ok: true });
2570
+ });
2571
+ app.get('/projects/:id/spec-sessions/:sessionId/snapshots/:snapshotId', (req, res) => {
2572
+ const project = getProjectById(req.params.id);
2573
+ if (!project) {
2574
+ return res.status(404).json({ error: 'Not found' });
2575
+ }
2576
+ const specSession = getProjectSpecSession(project.id, req.params.sessionId);
2577
+ if (!specSession) {
2578
+ return res.status(404).json({ error: 'Not found' });
2579
+ }
2580
+ const snapshot = getSpecSnapshot(req.params.snapshotId);
2581
+ if (!snapshot) {
2582
+ return res.status(404).json({ error: 'Not found' });
2583
+ }
2584
+ if (snapshot.spec_session_id !== specSession.id) {
2585
+ return res.status(404).json({ error: 'Not found' });
2586
+ }
2587
+ return res.json({
2588
+ ...snapshot,
2589
+ created_ticket_ids: getCreatedTicketIdsForSnapshot(snapshot.id, project.id),
2590
+ });
2591
+ });
2592
+ app.post('/projects/:id/spec-sessions/:sessionId/snapshots/:snapshotId/create-tickets', (req, res) => {
2593
+ const project = getProjectById(req.params.id);
2594
+ if (!project) {
2595
+ return res.status(404).json({ error: 'Not found' });
2596
+ }
2597
+ const specSession = getProjectSpecSession(project.id, req.params.sessionId);
2598
+ if (!specSession) {
2599
+ return res.status(404).json({ error: 'Not found' });
2600
+ }
2601
+ const snapshot = getSpecSnapshot(req.params.snapshotId);
2602
+ if (!snapshot || snapshot.spec_session_id !== specSession.id) {
2603
+ return res.status(404).json({ error: 'Not found' });
2604
+ }
2605
+ if (snapshot.tickets.length === 0) {
2606
+ return res.status(409).json({ error: 'This spec snapshot has no proposed tickets to create.' });
2607
+ }
2608
+ const existingTicketIds = getCreatedTicketIdsForSnapshot(snapshot.id, project.id);
2609
+ if (existingTicketIds.length > 0) {
2610
+ return res.status(409).json({
2611
+ error: 'Tickets have already been created from this spec snapshot.',
2612
+ existing_ticket_ids: existingTicketIds,
2613
+ });
2614
+ }
2615
+ const ticketsWithoutTestingGuidance = snapshot.tickets.filter(ticket => !hasTestingGuidance(ticket.description));
2616
+ if (ticketsWithoutTestingGuidance.length > 0) {
2617
+ const ticketKeys = ticketsWithoutTestingGuidance.map(ticket => ticket.key).join(', ');
2618
+ return res.status(422).json({
2619
+ error: `Spec tickets must include testing or verification guidance in their descriptions before they can be created on the board. Missing guidance in: ${ticketKeys}`,
2620
+ });
2621
+ }
2622
+ try {
2623
+ const bundleName = specSession.external_source?.external_key ?? '';
2624
+ const bundle = bundleName
2625
+ ? createAutoBundleForName(bundleName, project.id)
2626
+ : createAutoPlanningBundle(project.id);
2627
+ const { createdTickets, createdDependencies } = materializeTicketsWithDependencies({
2628
+ options: {
2629
+ projectId: project.id,
2630
+ model: specSession.model,
2631
+ variant: specSession.variant,
2632
+ skills: specSession.skills,
2633
+ tool: null,
2634
+ worktreeBundleId: bundle.id,
2635
+ originSpecSessionId: specSession.id,
2636
+ originSpecSnapshotId: snapshot.id,
2637
+ },
2638
+ tickets: snapshot.tickets,
2639
+ dependencies: snapshot.dependencies,
2640
+ });
2641
+ for (const createdTicket of createdTickets) {
2642
+ io.emit('ticket:created', createdTicket);
2643
+ }
2644
+ for (const createdDependency of createdDependencies) {
2645
+ io.emit('ticket-dependency:created', createdDependency);
2646
+ }
2647
+ return res.status(201).json({
2648
+ tickets: createdTickets,
2649
+ dependencies: createdDependencies,
2650
+ });
2651
+ }
2652
+ catch (error) {
2653
+ if (error instanceof HttpError) {
2654
+ return res.status(error.status).json({ error: error.message });
2655
+ }
2656
+ return res.status(500).json({ error: getErrorMessage(error) || 'Unable to create tickets from the spec snapshot.' });
2657
+ }
2658
+ });
2659
+ app.post('/projects/:id/spec-sessions/:sessionId/snapshots/:snapshotId/implement-in-chat', (req, res) => {
2660
+ const project = getProjectById(req.params.id);
2661
+ if (!project) {
2662
+ return res.status(404).json({ error: 'Not found' });
2663
+ }
2664
+ const specSession = getProjectSpecSession(project.id, req.params.sessionId);
2665
+ if (!specSession) {
2666
+ return res.status(404).json({ error: 'Not found' });
2667
+ }
2668
+ const snapshot = getSpecSnapshot(req.params.snapshotId);
2669
+ if (!snapshot || snapshot.spec_session_id !== specSession.id) {
2670
+ return res.status(404).json({ error: 'Not found' });
2671
+ }
2672
+ const draftText = buildSpecImplementationDraft({ snapshot, specSession });
2673
+ return res.status(200).json({
2674
+ draft_text: draftText,
2675
+ spec_session_id: specSession.id,
2676
+ spec_snapshot_id: snapshot.id,
2677
+ });
2678
+ });
2679
+ app.post('/projects/:id/spec-sessions/:sessionId/snapshots/:snapshotId/export', (req, res) => {
2680
+ const project = getProjectById(req.params.id);
2681
+ if (!project) {
2682
+ return res.status(404).json({ error: 'Not found' });
2683
+ }
2684
+ const specSession = getProjectSpecSession(project.id, req.params.sessionId);
2685
+ if (!specSession) {
2686
+ return res.status(404).json({ error: 'Not found' });
2687
+ }
2688
+ const snapshot = getSpecSnapshot(req.params.snapshotId);
2689
+ if (!snapshot || snapshot.spec_session_id !== specSession.id) {
2690
+ return res.status(404).json({ error: 'Not found' });
2691
+ }
2692
+ try {
2693
+ const collision = checkSpecExportCollision({
2694
+ specSession,
2695
+ repoPath: project.repo_path,
2696
+ });
2697
+ if (collision) {
2698
+ return res.status(409).json({
2699
+ error: 'A spec with this title has already been exported.',
2700
+ conflicting_path: collision,
2701
+ });
2702
+ }
2703
+ const result = exportSpecToDocs({
2704
+ snapshot,
2705
+ specSession,
2706
+ repoPath: project.repo_path,
2707
+ });
2708
+ return res.status(201).json({
2709
+ path: result.filePath,
2710
+ content: result.content,
2711
+ });
2712
+ }
2713
+ catch (error) {
2714
+ return res.status(500).json({ error: getErrorMessage(error) || 'Unable to export the spec snapshot.' });
2715
+ }
2716
+ });
2444
2717
  app.get('/tickets', async (req, res) => {
2445
2718
  const requestedProjectId = getRequestedProjectId(req);
2446
2719
  const detail = req.query.detail === 'board-enrichment'
@@ -4635,9 +4908,9 @@ app.post('/tickets', (req, res) => {
4635
4908
  if (!project) {
4636
4909
  return res.status(400).json({ error: 'project_id must reference an existing project' });
4637
4910
  }
4638
- const tool = resolveChatTool(req.body.tool ?? config.tool);
4639
- const model = tool === 'opencode' && req.body.model != null ? normalizeModelId(req.body.model) || null : null;
4640
- const variant = tool === 'opencode' && typeof req.body.variant === 'string' && req.body.variant.trim()
4911
+ const requestTool = resolveChatTool(req.body.tool ?? config.tool);
4912
+ const model = requestTool === 'opencode' && req.body.model != null ? normalizeModelId(req.body.model) || null : null;
4913
+ const variant = requestTool === 'opencode' && typeof req.body.variant === 'string' && req.body.variant.trim()
4641
4914
  ? req.body.variant.trim()
4642
4915
  : null;
4643
4916
  const skills = resolveRequestedSkills(req.body.skills);
@@ -4665,7 +4938,7 @@ app.post('/tickets', (req, res) => {
4665
4938
  model,
4666
4939
  variant,
4667
4940
  skills,
4668
- tool,
4941
+ tool: null,
4669
4942
  projectId: project_id,
4670
4943
  worktreeBundleId: resolvedBundleId,
4671
4944
  externalSource,