@straiffi/archon 1.7.2 → 1.7.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/client/assets/ActivityView-BgMz3NNT.js +1 -0
- package/dist/client/assets/GitHubActionsDialog-CNqWDS3E.js +1 -0
- package/dist/client/assets/TestsDialog-Cll7xJe0.js +4 -0
- package/dist/client/assets/{architectureDiagram-3BPJPVTR-Do9nbC8v.js → architectureDiagram-3BPJPVTR-BL5dYiul.js} +1 -1
- package/dist/client/assets/{blockDiagram-GPEHLZMM-DGGOLHF8.js → blockDiagram-GPEHLZMM-Bi5x2v80.js} +1 -1
- package/dist/client/assets/{c4Diagram-AAUBKEIU-Bz7tQybs.js → c4Diagram-AAUBKEIU-esEm54Ay.js} +1 -1
- package/dist/client/assets/channel-CqNHF3FD.js +1 -0
- package/dist/client/assets/{chunk-2J33WTMH-CJO-GmWJ.js → chunk-2J33WTMH-DACfY8xM.js} +1 -1
- package/dist/client/assets/{chunk-3OPIFGDE-DPAVagpC.js → chunk-3OPIFGDE-DZNUiaUs.js} +1 -1
- package/dist/client/assets/{chunk-5ZQYHXKU-Cof75Mu7.js → chunk-5ZQYHXKU-ClDfkMAp.js} +1 -1
- package/dist/client/assets/{chunk-727SXJPM-COuPqQ8_.js → chunk-727SXJPM-CCwA3GWT.js} +1 -1
- package/dist/client/assets/{chunk-AQP2D5EJ-CFj5KabW.js → chunk-AQP2D5EJ-Byl4DRZ9.js} +1 -1
- package/dist/client/assets/{chunk-CSCIHK7Q-C4M6w5cT.js → chunk-CSCIHK7Q-CXymZQ7A.js} +1 -1
- package/dist/client/assets/{chunk-KSCS5N6A-ByaiwVue.js → chunk-KSCS5N6A-VV3D8g6a.js} +1 -1
- package/dist/client/assets/{chunk-L5ZTLDWV-Cx5jeot9.js → chunk-L5ZTLDWV-BL9N5GRI.js} +1 -1
- package/dist/client/assets/{chunk-LZXEDZCA-DmGtztud.js → chunk-LZXEDZCA-DKNN-zno.js} +2 -2
- package/dist/client/assets/{chunk-ND2GUHAM-i3yuspqL.js → chunk-ND2GUHAM-CHCpQ1yr.js} +1 -1
- package/dist/client/assets/{chunk-NZK2D7GU-CQ4E_jP4.js → chunk-NZK2D7GU-B0Dm74mt.js} +1 -1
- package/dist/client/assets/{chunk-O5CBEL6O-D2g4VDCd.js → chunk-O5CBEL6O-DAa5z2xG.js} +1 -1
- package/dist/client/assets/{chunk-WU5MYG2G-exIu-zQ2.js → chunk-WU5MYG2G-DrVJWKVR.js} +1 -1
- package/dist/client/assets/classDiagram-4FO5ZUOK-BjEgQwKg.js +1 -0
- package/dist/client/assets/classDiagram-v2-Q7XG4LA2-gGomH-F_.js +1 -0
- package/dist/client/assets/{dagre-BM42HDAG-DvF75yLZ.js → dagre-BM42HDAG-B0fsl8mE.js} +1 -1
- package/dist/client/assets/{diagram-2AECGRRQ-DFv0HxLL.js → diagram-2AECGRRQ-Cx_VEbSS.js} +1 -1
- package/dist/client/assets/{diagram-5GNKFQAL-C_LRir0-.js → diagram-5GNKFQAL-Cv7VVn5r.js} +1 -1
- package/dist/client/assets/{diagram-KO2AKTUF-DGZxRDRy.js → diagram-KO2AKTUF-C8Sl5czV.js} +1 -1
- package/dist/client/assets/{diagram-LMA3HP47-UXt0uti4.js → diagram-LMA3HP47-C3dZaLz2.js} +1 -1
- package/dist/client/assets/{diagram-OG6HWLK6-CFbZS4n5.js → diagram-OG6HWLK6-BAU9DtTr.js} +1 -1
- package/dist/client/assets/{erDiagram-TEJ5UH35-CfAwZDdf.js → erDiagram-TEJ5UH35-DpNc-eBD.js} +1 -1
- package/dist/client/assets/{flowDiagram-I6XJVG4X-Cdgi5z3n.js → flowDiagram-I6XJVG4X-DGwTAT1S.js} +1 -1
- package/dist/client/assets/{ganttDiagram-6RSMTGT7-DOrkFkhu.js → ganttDiagram-6RSMTGT7-MPZhEgzJ.js} +1 -1
- package/dist/client/assets/{gitGraphDiagram-PVQCEYII-cEox0peD.js → gitGraphDiagram-PVQCEYII-CY0tHL5N.js} +1 -1
- package/dist/client/assets/index-CJRgmWwY.js +273 -0
- package/dist/client/assets/index-mRkJWu70.css +2 -0
- package/dist/client/assets/{infoDiagram-5YYISTIA-gm51u2Nn.js → infoDiagram-5YYISTIA-BBZeKjf4.js} +1 -1
- package/dist/client/assets/{ishikawaDiagram-YF4QCWOH-CjnoOIja.js → ishikawaDiagram-YF4QCWOH-CM_iFrES.js} +1 -1
- package/dist/client/assets/{journeyDiagram-JHISSGLW-Dlg4DsiO.js → journeyDiagram-JHISSGLW-Do_lY94X.js} +1 -1
- package/dist/client/assets/{kanban-definition-UN3LZRKU-BU0XZaSd.js → kanban-definition-UN3LZRKU-brxg3j37.js} +1 -1
- package/dist/client/assets/{line-Bert22-_.js → line-PHHbm9rj.js} +1 -1
- package/dist/client/assets/{mermaid-parser.core-BvQSGDmL.js → mermaid-parser.core-CLxAkW69.js} +1 -1
- package/dist/client/assets/{mermaid.core--qSAHJRj.js → mermaid.core-CebvSmx9.js} +3 -3
- package/dist/client/assets/{mindmap-definition-RKZ34NQL-BgFIvWWD.js → mindmap-definition-RKZ34NQL-CX_-ZaJF.js} +1 -1
- package/dist/client/assets/{pieDiagram-4H26LBE5-DGcbXD0M.js → pieDiagram-4H26LBE5-BJuHvql_.js} +1 -1
- package/dist/client/assets/{quadrantDiagram-W4KKPZXB-ClkBUeia.js → quadrantDiagram-W4KKPZXB-DiJll7l9.js} +1 -1
- package/dist/client/assets/{requirementDiagram-4Y6WPE33-B8CwGToU.js → requirementDiagram-4Y6WPE33-Bg-tXgcj.js} +1 -1
- package/dist/client/assets/{sankeyDiagram-5OEKKPKP-DTk3jL6B.js → sankeyDiagram-5OEKKPKP-BoK2egfZ.js} +1 -1
- package/dist/client/assets/{sequenceDiagram-3UESZ5HK-BQVrcnKf.js → sequenceDiagram-3UESZ5HK-DcrDWNzv.js} +1 -1
- package/dist/client/assets/{stateDiagram-AJRCARHV-DD4yMZPB.js → stateDiagram-AJRCARHV-B995qt1S.js} +1 -1
- package/dist/client/assets/stateDiagram-v2-BHNVJYJU-Dn8P7Sa1.js +1 -0
- package/dist/client/assets/{timeline-definition-PNZ67QCA-BnCBVqcd.js → timeline-definition-PNZ67QCA-DqSVAhqL.js} +1 -1
- package/dist/client/assets/{vennDiagram-CIIHVFJN-BDmLMNXS.js → vennDiagram-CIIHVFJN-D9wVraVY.js} +1 -1
- package/dist/client/assets/{wardleyDiagram-YWT4CUSO-DSLuQiEv.js → wardleyDiagram-YWT4CUSO-CLff9zVd.js} +1 -1
- package/dist/client/assets/{xychartDiagram-2RQKCTM6-DAq_LA4r.js → xychartDiagram-2RQKCTM6-BuHUMMH2.js} +1 -1
- package/dist/client/index.html +2 -2
- package/dist/server/db.js +472 -39
- package/dist/server/db.js.map +1 -1
- package/dist/server/index.js +1311 -212
- package/dist/server/index.js.map +1 -1
- package/dist/server/lib/agent.js +228 -71
- package/dist/server/lib/agent.js.map +1 -1
- package/dist/server/lib/agentRuns.js +205 -38
- package/dist/server/lib/agentRuns.js.map +1 -1
- package/dist/server/lib/attachments.js +24 -0
- package/dist/server/lib/attachments.js.map +1 -1
- package/dist/server/lib/automationProgression.js +464 -0
- package/dist/server/lib/automationProgression.js.map +1 -0
- package/dist/server/lib/automationReconciliation.js +498 -0
- package/dist/server/lib/automationReconciliation.js.map +1 -0
- package/dist/server/lib/automationStageDetails.js +232 -0
- package/dist/server/lib/automationStageDetails.js.map +1 -0
- package/dist/server/lib/buildOpenQuestionOperations.js +238 -0
- package/dist/server/lib/buildOpenQuestionOperations.js.map +1 -0
- package/dist/server/lib/buildOpenQuestions.js +44 -0
- package/dist/server/lib/buildOpenQuestions.js.map +1 -1
- package/dist/server/lib/buildSequences.js +130 -46
- package/dist/server/lib/buildSequences.js.map +1 -1
- package/dist/server/lib/bundleReviewOperations.js +91 -9
- package/dist/server/lib/bundleReviewOperations.js.map +1 -1
- package/dist/server/lib/chatOperations.js +67 -12
- package/dist/server/lib/chatOperations.js.map +1 -1
- package/dist/server/lib/executionReconciliation.js +518 -0
- package/dist/server/lib/executionReconciliation.js.map +1 -0
- package/dist/server/lib/executionRetry.js +694 -0
- package/dist/server/lib/executionRetry.js.map +1 -0
- package/dist/server/lib/executionRetryContext.js +489 -0
- package/dist/server/lib/executionRetryContext.js.map +1 -0
- package/dist/server/lib/executionRetryPredicates.js +74 -0
- package/dist/server/lib/executionRetryPredicates.js.map +1 -0
- package/dist/server/lib/executionRunRetention.js +141 -0
- package/dist/server/lib/executionRunRetention.js.map +1 -0
- package/dist/server/lib/executionRuns.js +1367 -0
- package/dist/server/lib/executionRuns.js.map +1 -0
- package/dist/server/lib/executionTimeline.js +45 -0
- package/dist/server/lib/executionTimeline.js.map +1 -0
- package/dist/server/lib/git.js +385 -6
- package/dist/server/lib/git.js.map +1 -1
- package/dist/server/lib/integrations/github.js +43 -0
- package/dist/server/lib/integrations/github.js.map +1 -1
- package/dist/server/lib/integrations/githubActions.js +1 -0
- package/dist/server/lib/integrations/githubActions.js.map +1 -1
- package/dist/server/lib/integrations/githubActionsSessions.js +337 -114
- package/dist/server/lib/integrations/githubActionsSessions.js.map +1 -1
- package/dist/server/lib/projectMemorySelections.js +7 -3
- package/dist/server/lib/projectMemorySelections.js.map +1 -1
- package/dist/server/lib/shutdown.js +2 -2
- package/dist/server/lib/shutdown.js.map +1 -1
- package/dist/server/lib/specArtifact.js +3 -0
- package/dist/server/lib/specArtifact.js.map +1 -1
- package/dist/server/lib/specFormatting.js +1 -1
- package/dist/server/lib/specFormatting.js.map +1 -1
- package/dist/server/lib/specOperations.js +78 -12
- package/dist/server/lib/specOperations.js.map +1 -1
- package/dist/server/lib/specReviewContext.js +54 -18
- package/dist/server/lib/specReviewContext.js.map +1 -1
- package/dist/server/lib/specReviewDraft.js +3 -2
- package/dist/server/lib/specReviewDraft.js.map +1 -1
- package/dist/server/lib/specReviewOperations.js +74 -9
- package/dist/server/lib/specReviewOperations.js.map +1 -1
- package/dist/server/lib/specReviewValidation.js +217 -0
- package/dist/server/lib/specReviewValidation.js.map +1 -0
- package/dist/server/lib/specReviews.js +30 -2
- package/dist/server/lib/specReviews.js.map +1 -1
- package/dist/server/lib/specs.js +18 -1
- package/dist/server/lib/specs.js.map +1 -1
- package/dist/server/lib/stageSnapshots.js +197 -0
- package/dist/server/lib/stageSnapshots.js.map +1 -0
- package/dist/server/lib/testSessions.js +160 -18
- package/dist/server/lib/testSessions.js.map +1 -1
- package/dist/server/lib/ticketAutomation.js +80 -4
- package/dist/server/lib/ticketAutomation.js.map +1 -1
- package/dist/server/lib/ticketAutomationCommit.js +466 -91
- package/dist/server/lib/ticketAutomationCommit.js.map +1 -1
- package/dist/server/lib/ticketFollowUpOperations.js +270 -29
- package/dist/server/lib/ticketFollowUpOperations.js.map +1 -1
- package/dist/server/lib/ticketMessages.js +16 -0
- package/dist/server/lib/ticketMessages.js.map +1 -1
- package/dist/server/lib/ticketRunOperations.js +51 -6
- package/dist/server/lib/ticketRunOperations.js.map +1 -1
- package/dist/server/lib/ticketUndo.js +77 -168
- package/dist/server/lib/ticketUndo.js.map +1 -1
- package/dist/server/lib/ticketWorkflowOperations.js +132 -28
- package/dist/server/lib/ticketWorkflowOperations.js.map +1 -1
- package/dist/server/lib/tickets.js +41 -27
- package/dist/server/lib/tickets.js.map +1 -1
- package/dist/server/lib/transcriptRuns.js +3 -3
- package/dist/server/lib/transcriptRuns.js.map +1 -1
- package/dist/server/lib/turnExecutionErrors.js +12 -0
- package/dist/server/lib/turnExecutionErrors.js.map +1 -0
- package/dist/server/lib/types.js +58 -1
- package/dist/server/lib/types.js.map +1 -1
- package/dist/server/workers/build.js +596 -184
- package/dist/server/workers/build.js.map +1 -1
- package/dist/server/workers/chat.js +269 -132
- package/dist/server/workers/chat.js.map +1 -1
- package/dist/server/workers/followUp.js +731 -203
- package/dist/server/workers/followUp.js.map +1 -1
- package/dist/server/workers/plan.js +546 -188
- package/dist/server/workers/plan.js.map +1 -1
- package/dist/server/workers/planFollowUp.js +240 -75
- package/dist/server/workers/planFollowUp.js.map +1 -1
- package/dist/server/workers/review.js +790 -297
- package/dist/server/workers/review.js.map +1 -1
- package/dist/server/workers/specReview.js +212 -46
- package/dist/server/workers/specReview.js.map +1 -1
- package/dist/server/workers/specSession.js +434 -254
- package/dist/server/workers/specSession.js.map +1 -1
- package/package.json +1 -1
- package/dist/client/assets/GitHubActionsDialog-DAD5REmY.js +0 -1
- package/dist/client/assets/TestsDialog-B5NVrroq.js +0 -4
- package/dist/client/assets/TicketChangesModal-By-_gTGd.js +0 -1
- package/dist/client/assets/channel-Dd5Len0X.js +0 -1
- package/dist/client/assets/classDiagram-4FO5ZUOK-B7KUg6r8.js +0 -1
- package/dist/client/assets/classDiagram-v2-Q7XG4LA2-CQm-z5c4.js +0 -1
- package/dist/client/assets/index-BHq6-Eir.js +0 -270
- package/dist/client/assets/index-BxTc5qQT.css +0 -2
- package/dist/client/assets/stateDiagram-v2-BHNVJYJU-C8yO6POT.js +0 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { formatAgentErrorForTranscript, getAgentErrorTranscriptRuns, registerSpecProcessPlaceholder, runSpecAgent, untrackSpecProcessById } from '../lib/agent.js';
|
|
2
|
-
import {
|
|
2
|
+
import { createAgentRun, emitAgentRunsUpdated, finalizeAgentRun, getNextAgentRunIndex, terminalizeAgentRunsForExecutionRun } from '../lib/agentRuns.js';
|
|
3
3
|
import { deleteStagedSpecAttachments, linkSpecAttachmentsToMessage, stageSpecAttachmentsForAgent } from '../lib/attachments.js';
|
|
4
4
|
import { createSpecMessage } from '../lib/specMessages.js';
|
|
5
5
|
import { beginSpecSessionThinkingRun, bindSpecSessionThinkingMessage } from '../lib/thinkingState.js';
|
|
@@ -12,6 +12,8 @@ import { persistSpecSessionProjectMemorySuggestions } from '../lib/projectMemory
|
|
|
12
12
|
import { extractProjectMemorySuggestionsFromOutput, getProjectMemorySuggestionPromptRules, } from '../lib/projectMemorySuggestions.js';
|
|
13
13
|
import { enqueue, isRunning } from '../lib/queue.js';
|
|
14
14
|
import { getProjectById } from '../lib/projects.js';
|
|
15
|
+
import { addExecutionRunLink, transitionExecutionRun } from '../lib/executionRuns.js';
|
|
16
|
+
import { TurnStartError } from '../lib/turnExecutionErrors.js';
|
|
15
17
|
import { buildLinkedResearchContext, prepareLinkedPlanningResearch } from './plan.js';
|
|
16
18
|
import { extractGrillQuestionFromOutput, extractSpecArtifactFromOutput, resolveCarriedForwardArtifacts, resolveCarriedForwardStructuredFields, stripMermaidBlocks } from '../lib/specArtifact.js';
|
|
17
19
|
import { IMPLEMENTATION_BODY_DEDUPLICATION_RULES, IMPLEMENTATION_DETAIL_RULES, IMPLEMENTATION_READY_DESCRIPTION_REQUIREMENTS, IMPLEMENTATION_READY_DESCRIPTION_SECTION_TEMPLATE, } from '../lib/ticketDescriptionContract.js';
|
|
@@ -39,7 +41,7 @@ const formatPreviousSpecStateBlock = (snapshot) => {
|
|
|
39
41
|
if (snapshot.likely_files.length > 0) {
|
|
40
42
|
lines.push('Likely files:');
|
|
41
43
|
for (const file of snapshot.likely_files) {
|
|
42
|
-
lines.push(`- ${file.path} — ${file.reason}`);
|
|
44
|
+
lines.push(`- [${file.is_new ? 'new' : 'existing'}] ${file.path} — ${file.reason}`);
|
|
43
45
|
}
|
|
44
46
|
}
|
|
45
47
|
if (snapshot.assumptions.length > 0) {
|
|
@@ -124,7 +126,7 @@ const buildStandardModeTail = ({ existingMermaid, existingMockup, existingFileRe
|
|
|
124
126
|
'',
|
|
125
127
|
'4. A machine-readable artifact block with the spec structured data, using exactly these markers:',
|
|
126
128
|
' <archon-spec-artifact>',
|
|
127
|
-
' {"summary":"short plan summary","likely_files":[{"path":"src/foo.ts","reason":"Add bar helper"},{"path":"src/
|
|
129
|
+
' {"summary":"short plan summary","likely_files":[{"path":"src/foo.ts","reason":"Add bar helper","is_new":false},{"path":"src/newHelper.ts","reason":"Provide the new shared helper","is_new":true}],"tickets":[{"key":"add-foo-bar","title":"Add bar helper to foo module","description":"## Problem Statement\\nThe foo module is missing the `bar` helper, blocking downstream consumers from validating inputs consistently.\\n\\n## Acceptance Criteria\\n- [ ] A `bar(input)` function is exported from `src/foo.ts`.\\n- [ ] Invalid inputs throw a typed error with a clear message.\\n- [ ] All existing callers of foo continue to work unchanged.\\n\\n## Implementation Approach\\n1. Add `bar` to `src/foo.ts` following the existing `baz` helper signature pattern.\\n2. Reuse the project existing validation utilities instead of inlining checks.\\n3. Export `bar` from the module barrel file.\\n\\n## Edge Cases and Error Handling\\n- Empty string inputs should throw the same typed error as other invalid inputs.\\n- Preserve current behavior for `undefined`/`null` (do not coerce).\\n\\n## Constraints and Things to Avoid\\n- Do not introduce a new dependency.\\n- Do not change the public signature of existing exports."},{"key":"use-bar-in-consumer","title":"Call bar from consumer module","description":"## Problem Statement\\nThe consumer module duplicates input validation that `bar` now provides.\\n\\n## Acceptance Criteria\\n- [ ] Consumer calls `bar` instead of its own inline validation.\\n- [ ] Existing consumer tests still pass.\\n\\n## Implementation Approach\\n1. Replace the inline check in consumer with a call to `bar`.\\n2. Remove the now-unused validation helper from consumer.\\n\\n## Edge Cases and Error Handling\\n- Consumer must handle the typed errors thrown by `bar`.\\n\\n## Constraints and Things to Avoid\\n- Do not change the consumer public API."}],"dependencies":[{"blocker_key":"add-foo-bar","dependent_key":"use-bar-in-consumer"}]}',
|
|
128
130
|
' </archon-spec-artifact>',
|
|
129
131
|
'',
|
|
130
132
|
'The artifact represents the complete current state of the spec. On follow-up turns, re-emit every field',
|
|
@@ -135,9 +137,11 @@ const buildStandardModeTail = ({ existingMermaid, existingMockup, existingFileRe
|
|
|
135
137
|
' what this turn did, what was emitted, or what changed; the summary describes the plan itself.',
|
|
136
138
|
'- `assumptions` — array of assumptions that affect the plan.',
|
|
137
139
|
'- `open_questions` — array of unresolved questions needing clarification.',
|
|
138
|
-
'- `likely_files` — array of {"path": "...", "reason": "..."} objects for files you
|
|
139
|
-
'
|
|
140
|
-
'
|
|
140
|
+
'- `likely_files` — array of {"path": "...", "reason": "...", "is_new": boolean} objects for files you',
|
|
141
|
+
' expect to create or modify. Inspect the repository before classifying each path: set `is_new` to true',
|
|
142
|
+
' only when the path does not exist and the implementation will create it; use false for an existing file',
|
|
143
|
+
' that will be modified. If your human-readable text includes a table of files to change, include the same',
|
|
144
|
+
' files here. The `reason` should describe what change is needed, not just repeat the path.',
|
|
141
145
|
'- `tickets` (required) — proposed board tickets with stable kebab-case keys (always at least one).',
|
|
142
146
|
' Each ticket will later be implemented by a separate build agent that has no access to this planning',
|
|
143
147
|
' conversation, so every ticket `description` must be self-contained and implementation-ready.',
|
|
@@ -312,7 +316,62 @@ const emitUpdatedSpecSession = (specSessionId, io) => {
|
|
|
312
316
|
io.emit('spec-session:updated', specSession);
|
|
313
317
|
}
|
|
314
318
|
};
|
|
315
|
-
|
|
319
|
+
const failSpecTurn = ({ specSessionId, executionContext, existingTriggerMessageId, providerContinuation, conversationContext, priorSnapshotId, turnMode, mainRunId, error, io, }) => {
|
|
320
|
+
const failureMessage = getErrorMessage(error);
|
|
321
|
+
untrackSpecProcessById(specSessionId);
|
|
322
|
+
try {
|
|
323
|
+
if (mainRunId) {
|
|
324
|
+
finalizeAgentRun(mainRunId, 'error', null, failureMessage);
|
|
325
|
+
}
|
|
326
|
+
terminalizeAgentRunsForExecutionRun(executionContext.runId, 'error', failureMessage);
|
|
327
|
+
}
|
|
328
|
+
catch {
|
|
329
|
+
// Preserve the top-level execution even if detail finalization fails.
|
|
330
|
+
}
|
|
331
|
+
try {
|
|
332
|
+
const errorContent = formatAgentErrorForTranscript(error);
|
|
333
|
+
if (errorContent) {
|
|
334
|
+
const errorMessageId = createSpecMessage({
|
|
335
|
+
specSessionId,
|
|
336
|
+
role: 'assistant',
|
|
337
|
+
kind: 'error',
|
|
338
|
+
content: errorContent,
|
|
339
|
+
transcriptRuns: getAgentErrorTranscriptRuns(error),
|
|
340
|
+
});
|
|
341
|
+
bindSpecSessionThinkingMessage(specSessionId, errorMessageId);
|
|
342
|
+
addExecutionRunLink({
|
|
343
|
+
projectId: executionContext.projectId,
|
|
344
|
+
runId: executionContext.runId,
|
|
345
|
+
link: {
|
|
346
|
+
entityType: 'spec_message',
|
|
347
|
+
entityId: errorMessageId,
|
|
348
|
+
relationship: 'result',
|
|
349
|
+
expectedParent: { entityType: 'spec_session', entityId: specSessionId },
|
|
350
|
+
},
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
catch {
|
|
355
|
+
// Terminal state must not depend on secondary error-message persistence.
|
|
356
|
+
}
|
|
357
|
+
try {
|
|
358
|
+
updateSpecSessionStatus(specSessionId, 'error', failureMessage, null);
|
|
359
|
+
emitUpdatedSpecSession(specSessionId, io);
|
|
360
|
+
}
|
|
361
|
+
catch {
|
|
362
|
+
// Preserve the top-level execution even if domain projection fails.
|
|
363
|
+
}
|
|
364
|
+
transitionExecutionRun({
|
|
365
|
+
projectId: executionContext.projectId,
|
|
366
|
+
runId: executionContext.runId,
|
|
367
|
+
status: 'failed',
|
|
368
|
+
domainStatus: 'error',
|
|
369
|
+
failure: { code: 'spec_turn_failed', message: failureMessage },
|
|
370
|
+
eventType: 'spec_turn_failed',
|
|
371
|
+
}, io);
|
|
372
|
+
emitAgentRunsUpdated(io, 'spec', specSessionId);
|
|
373
|
+
};
|
|
374
|
+
export const startSpecSessionTurn = ({ specSessionId, instruction, attachments = [], tool, model, variant, skills = [], io, executionContext, existingTriggerMessageId, providerContinuation, conversationContext, priorSnapshotId, turnMode, }) => {
|
|
316
375
|
const specSession = getSpecSession(specSessionId, { includeMessages: true });
|
|
317
376
|
if (!specSession) {
|
|
318
377
|
return false;
|
|
@@ -331,13 +390,25 @@ export const startSpecSessionTurn = ({ specSessionId, instruction, attachments =
|
|
|
331
390
|
if (specSession.agent_status === 'running' || isRunning(jobKey)) {
|
|
332
391
|
return false;
|
|
333
392
|
}
|
|
334
|
-
const userMessageId = createSpecMessage({
|
|
393
|
+
const userMessageId = existingTriggerMessageId ?? createSpecMessage({
|
|
335
394
|
specSessionId,
|
|
336
395
|
role: 'user',
|
|
337
396
|
kind: 'prompt',
|
|
338
397
|
content: trimmedInstruction,
|
|
339
398
|
});
|
|
340
|
-
if (
|
|
399
|
+
if (!existingTriggerMessageId) {
|
|
400
|
+
addExecutionRunLink({
|
|
401
|
+
projectId: executionContext.projectId,
|
|
402
|
+
runId: executionContext.runId,
|
|
403
|
+
link: {
|
|
404
|
+
entityType: 'spec_message',
|
|
405
|
+
entityId: userMessageId,
|
|
406
|
+
relationship: 'trigger',
|
|
407
|
+
expectedParent: { entityType: 'spec_session', entityId: specSessionId },
|
|
408
|
+
},
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
if (!existingTriggerMessageId && attachments.length > 0) {
|
|
341
412
|
linkSpecAttachmentsToMessage({
|
|
342
413
|
specMessageId: userMessageId,
|
|
343
414
|
attachments,
|
|
@@ -363,7 +434,6 @@ export const startSpecSessionTurn = ({ specSessionId, instruction, attachments =
|
|
|
363
434
|
}
|
|
364
435
|
beginSpecSessionThinkingRun(specSessionId);
|
|
365
436
|
updateSpecSessionStatus(specSessionId, 'running', null, null);
|
|
366
|
-
clearAndEmitAgentRunsForParent(io, 'spec', specSessionId);
|
|
367
437
|
// Register a cancellation token before any async work so an interrupt during
|
|
368
438
|
// the pre-response window (prepareLinkedPlanningResearch) is accepted rather
|
|
369
439
|
// than returning a 409. runSpecAgent will overwrite this entry with the real
|
|
@@ -371,279 +441,389 @@ export const startSpecSessionTurn = ({ specSessionId, instruction, attachments =
|
|
|
371
441
|
// stopRequested below and finalizes to 'stopped' without starting the agent.
|
|
372
442
|
const specCancellation = registerSpecProcessPlaceholder(specSessionId);
|
|
373
443
|
emitUpdatedSpecSession(specSessionId, io);
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
444
|
+
let queuedJob;
|
|
445
|
+
try {
|
|
446
|
+
queuedJob = enqueue(jobKey, async () => {
|
|
447
|
+
let stagedAgentAttachmentsDirectoryPath = null;
|
|
448
|
+
let mainRunId = null;
|
|
449
|
+
try {
|
|
450
|
+
transitionExecutionRun({
|
|
451
|
+
projectId: executionContext.projectId,
|
|
452
|
+
runId: executionContext.runId,
|
|
453
|
+
status: 'running',
|
|
454
|
+
domainStatus: 'running',
|
|
455
|
+
eventType: 'spec_turn_started',
|
|
456
|
+
}, io);
|
|
457
|
+
const mainRunIndex = getNextAgentRunIndex(executionContext.runId);
|
|
458
|
+
mainRunId = createAgentRun({
|
|
459
|
+
executionRunId: executionContext.runId,
|
|
460
|
+
parentType: 'spec',
|
|
461
|
+
parentId: specSessionId,
|
|
462
|
+
label: 'Main agent',
|
|
463
|
+
role: 'main',
|
|
464
|
+
});
|
|
465
|
+
const latestSession = getSpecSession(specSessionId);
|
|
466
|
+
if (!latestSession) {
|
|
467
|
+
throw new Error('Spec session no longer exists');
|
|
468
|
+
}
|
|
469
|
+
// The prior snapshot (if any) is read once and reused both to guide the
|
|
470
|
+
// prompt (don't redraw established artifacts) and to carry its mermaid/
|
|
471
|
+
// mockup forward when the current turn omits them. No new snapshot is
|
|
472
|
+
// created mid-run, so this reference stays valid through the done path.
|
|
473
|
+
const resolvedPriorSnapshotId = priorSnapshotId === undefined ? latestSession.latest_snapshot_id : priorSnapshotId;
|
|
474
|
+
const previousSnapshot = resolvedPriorSnapshotId
|
|
475
|
+
? getSpecSnapshot(resolvedPriorSnapshotId)
|
|
476
|
+
: null;
|
|
477
|
+
const promptContext = latestSession.title === getDefaultSpecSessionTitle()
|
|
478
|
+
? null
|
|
479
|
+
: latestSession.title;
|
|
480
|
+
const projectMemory = getProjectMemoryPromptBlock(project.id, {
|
|
481
|
+
flow: 'spec',
|
|
482
|
+
stage: 'plan',
|
|
483
|
+
title: trimmedInstruction || 'Attached images only',
|
|
484
|
+
description: promptContext,
|
|
485
|
+
currentInstruction: trimmedInstruction,
|
|
486
|
+
invocation: { parentType: 'spec', parentId: specSessionId, agentRunId: mainRunId },
|
|
487
|
+
});
|
|
488
|
+
const cwd = project.repo_path;
|
|
489
|
+
const linkedResearch = trimmedInstruction
|
|
490
|
+
? await prepareLinkedPlanningResearch(trimmedInstruction, {
|
|
491
|
+
projectId: project.id,
|
|
492
|
+
tool: resolvedTool,
|
|
493
|
+
model: resolvedModel,
|
|
494
|
+
variant: resolvedVariant,
|
|
495
|
+
skills: resolvedSkills,
|
|
496
|
+
executionContext,
|
|
497
|
+
cancellationState: specCancellation,
|
|
498
|
+
agentRunParent: { parentType: 'spec', parentId: specSessionId, io },
|
|
499
|
+
})
|
|
500
|
+
: [];
|
|
501
|
+
// Honor an interrupt that arrived during the pre-response window. There
|
|
502
|
+
// is no child to kill and no partial response to save; transition straight
|
|
503
|
+
// to 'stopped' (worker-owned, mirroring how Chat finalizes after a stop).
|
|
504
|
+
if (specCancellation.stopRequested) {
|
|
505
|
+
untrackSpecProcessById(specSessionId);
|
|
506
|
+
finalizeAgentRun(mainRunId, 'stopped', null);
|
|
507
|
+
updateSpecSessionStatus(specSessionId, 'stopped', null, null);
|
|
508
|
+
emitUpdatedSpecSession(specSessionId, io);
|
|
509
|
+
transitionExecutionRun({
|
|
510
|
+
projectId: executionContext.projectId,
|
|
511
|
+
runId: executionContext.runId,
|
|
512
|
+
status: 'cancelled',
|
|
513
|
+
domainStatus: 'stopped',
|
|
514
|
+
eventType: 'spec_turn_cancelled',
|
|
515
|
+
}, io);
|
|
516
|
+
emitAgentRunsUpdated(io, 'spec', specSessionId);
|
|
517
|
+
return;
|
|
518
|
+
}
|
|
519
|
+
const linkedResearchContext = buildLinkedResearchContext(project, linkedResearch);
|
|
520
|
+
const stagedAgentAttachments = resolvedTool === 'opencode'
|
|
521
|
+
? stageSpecAttachmentsForAgent({
|
|
522
|
+
specSessionId,
|
|
523
|
+
cwd,
|
|
524
|
+
attachments,
|
|
525
|
+
})
|
|
526
|
+
: { directoryPath: null, files: [] };
|
|
527
|
+
stagedAgentAttachmentsDirectoryPath = stagedAgentAttachments.directoryPath;
|
|
528
|
+
const specPrompt = buildSpecPrompt({
|
|
529
|
+
projectName: project.name,
|
|
530
|
+
projectPath: cwd,
|
|
531
|
+
instruction: conversationContext
|
|
532
|
+
? `<conversation_before_retry>\n${conversationContext}\n</conversation_before_retry>\n\n${trimmedInstruction}`
|
|
533
|
+
: trimmedInstruction,
|
|
534
|
+
hasAttachments: attachments.length > 0,
|
|
535
|
+
projectMemory,
|
|
536
|
+
memoryEnabled: Boolean(project.memory_enabled),
|
|
537
|
+
previousSnapshot,
|
|
538
|
+
linkedResearchContext,
|
|
539
|
+
mode: turnMode ?? resolveSpecModeValue(latestSession.mode),
|
|
540
|
+
});
|
|
541
|
+
const specRunResult = await runSpecAgent({
|
|
542
|
+
specSessionId,
|
|
403
543
|
tool: resolvedTool,
|
|
544
|
+
prompt: specPrompt,
|
|
545
|
+
cwd,
|
|
546
|
+
files: stagedAgentAttachments.files,
|
|
547
|
+
io,
|
|
404
548
|
model: resolvedModel,
|
|
405
549
|
variant: resolvedVariant,
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
550
|
+
resumeSessionId: providerContinuation === 'fresh'
|
|
551
|
+
? null
|
|
552
|
+
: resolvedTool === 'claude'
|
|
553
|
+
? latestSession.claude_session_id
|
|
554
|
+
: latestSession.opencode_session_id,
|
|
555
|
+
executionContext,
|
|
556
|
+
mainRun: { id: mainRunId, label: 'Main agent', role: 'main', runIndex: mainRunIndex },
|
|
557
|
+
});
|
|
558
|
+
if (specRunResult.completionStatus === 'stopped') {
|
|
559
|
+
const interruptedSession = getSpecSession(specSessionId);
|
|
560
|
+
const interruptedResponse = interruptedSession?.streaming_response?.trim()
|
|
561
|
+
|| specRunResult.finalResponse?.trim()
|
|
562
|
+
|| getTranscriptContentFallback(specRunResult.transcriptRuns)
|
|
563
|
+
|| '';
|
|
564
|
+
if (resolvedTool === 'opencode' && interruptedSession?.title === getDefaultSpecSessionTitle()) {
|
|
565
|
+
updateSpecSessionTitle(specSessionId, deriveSpecSessionTitle(trimmedInstruction));
|
|
566
|
+
}
|
|
567
|
+
if (interruptedResponse) {
|
|
568
|
+
const messageId = createSpecMessage({
|
|
569
|
+
specSessionId,
|
|
570
|
+
role: 'assistant',
|
|
571
|
+
kind: 'final_response',
|
|
572
|
+
content: interruptedResponse,
|
|
573
|
+
responseTiming: specRunResult.responseTiming,
|
|
574
|
+
transcriptRuns: specRunResult.transcriptRuns,
|
|
575
|
+
});
|
|
576
|
+
if (messageId) {
|
|
577
|
+
bindSpecSessionThinkingMessage(specSessionId, messageId);
|
|
578
|
+
addExecutionRunLink({
|
|
579
|
+
projectId: executionContext.projectId,
|
|
580
|
+
runId: executionContext.runId,
|
|
581
|
+
link: {
|
|
582
|
+
entityType: 'spec_message',
|
|
583
|
+
entityId: messageId,
|
|
584
|
+
relationship: 'result',
|
|
585
|
+
expectedParent: { entityType: 'spec_session', entityId: specSessionId },
|
|
586
|
+
},
|
|
587
|
+
});
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
updateSpecSessionUsageSnapshot(specSessionId, specRunResult.usageSnapshot);
|
|
591
|
+
updateSpecSessionStatus(specSessionId, 'stopped', interruptedSession?.agent_log ?? null, null);
|
|
592
|
+
emitUpdatedSpecSession(specSessionId, io);
|
|
593
|
+
finalizeAgentRun(mainRunId, 'stopped', interruptedResponse || null);
|
|
594
|
+
transitionExecutionRun({
|
|
595
|
+
projectId: executionContext.projectId,
|
|
596
|
+
runId: executionContext.runId,
|
|
597
|
+
status: 'cancelled',
|
|
598
|
+
domainStatus: 'stopped',
|
|
599
|
+
eventType: 'spec_turn_cancelled',
|
|
600
|
+
}, io);
|
|
601
|
+
emitAgentRunsUpdated(io, 'spec', specSessionId);
|
|
602
|
+
return;
|
|
603
|
+
}
|
|
604
|
+
// Done path: process the completed response.
|
|
605
|
+
const latestCompletedSession = getSpecSession(specSessionId);
|
|
606
|
+
const streamedAssistantMessage = latestCompletedSession?.streaming_response?.trim() ?? '';
|
|
607
|
+
const rawResponse = typeof specRunResult.finalResponse === 'string'
|
|
608
|
+
? specRunResult.finalResponse
|
|
609
|
+
: '';
|
|
610
|
+
// Resolve the session mode at the moment the turn completed. NULL/legacy
|
|
611
|
+
// values normalize to 'standard'. The worker's per-mode output branch is
|
|
612
|
+
// decided here, not from a re-read after the artifact path, so an
|
|
613
|
+
// auto-finalize mode flip does not change how THIS turn's output parses.
|
|
614
|
+
const sessionMode = turnMode ?? resolveSpecModeValue(latestCompletedSession?.mode);
|
|
615
|
+
// Grill mode: try the grill-question extractor first. If a payload is
|
|
616
|
+
// returned, store it as a 'grill-question' assistant message and skip the
|
|
617
|
+
// snapshot. If no payload is returned, fall through to the standard
|
|
618
|
+
// artifact path (the agent signaled it has no more questions and is
|
|
619
|
+
// emitting the full plan — this is the auto-finalize path).
|
|
620
|
+
if (sessionMode === 'grill' && rawResponse) {
|
|
621
|
+
const grillResult = extractGrillQuestionFromOutput(rawResponse);
|
|
622
|
+
if (grillResult.grillQuestion) {
|
|
623
|
+
const cleanedGrillText = grillResult.cleanedOutput.trim()
|
|
624
|
+
|| grillResult.grillQuestion.question
|
|
625
|
+
|| streamedAssistantMessage.trim();
|
|
626
|
+
const grillMessageId = createSpecMessage({
|
|
627
|
+
specSessionId,
|
|
628
|
+
role: 'assistant',
|
|
629
|
+
kind: 'grill-question',
|
|
630
|
+
content: cleanedGrillText,
|
|
631
|
+
metadata: { grill_question: grillResult.grillQuestion },
|
|
632
|
+
responseTiming: specRunResult.responseTiming,
|
|
633
|
+
transcriptRuns: specRunResult.transcriptRuns,
|
|
634
|
+
});
|
|
635
|
+
if (grillMessageId) {
|
|
636
|
+
bindSpecSessionThinkingMessage(specSessionId, grillMessageId);
|
|
637
|
+
addExecutionRunLink({
|
|
638
|
+
projectId: executionContext.projectId,
|
|
639
|
+
runId: executionContext.runId,
|
|
640
|
+
link: {
|
|
641
|
+
entityType: 'spec_message',
|
|
642
|
+
entityId: grillMessageId,
|
|
643
|
+
relationship: 'result',
|
|
644
|
+
expectedParent: { entityType: 'spec_session', entityId: specSessionId },
|
|
645
|
+
},
|
|
646
|
+
});
|
|
647
|
+
}
|
|
648
|
+
if (resolvedTool === 'opencode' && latestCompletedSession?.title === getDefaultSpecSessionTitle()) {
|
|
649
|
+
updateSpecSessionTitle(specSessionId, deriveSpecSessionTitle(trimmedInstruction));
|
|
650
|
+
}
|
|
651
|
+
updateSpecSessionUsageSnapshot(specSessionId, specRunResult.usageSnapshot);
|
|
652
|
+
updateSpecSessionStatus(specSessionId, 'done', getSpecSession(specSessionId)?.agent_log ?? null, null);
|
|
653
|
+
emitUpdatedSpecSession(specSessionId, io);
|
|
654
|
+
finalizeAgentRun(mainRunId, 'done', cleanedGrillText);
|
|
655
|
+
transitionExecutionRun({
|
|
656
|
+
projectId: executionContext.projectId,
|
|
657
|
+
runId: executionContext.runId,
|
|
658
|
+
status: 'completed',
|
|
659
|
+
domainStatus: 'done',
|
|
660
|
+
eventType: 'spec_turn_completed',
|
|
661
|
+
}, io);
|
|
662
|
+
emitAgentRunsUpdated(io, 'spec', specSessionId);
|
|
663
|
+
return;
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
const artifactResult = rawResponse
|
|
667
|
+
? extractSpecArtifactFromOutput(rawResponse)
|
|
668
|
+
: { cleanedOutput: '', artifact: null, error: null };
|
|
669
|
+
const parsedResponse = extractProjectMemorySuggestionsFromOutput(artifactResult.cleanedOutput);
|
|
670
|
+
if (project.memory_enabled && parsedResponse.suggestions && parsedResponse.suggestions.length > 0) {
|
|
671
|
+
persistSpecSessionProjectMemorySuggestions(project.id, specSessionId, parsedResponse.suggestions);
|
|
672
|
+
}
|
|
673
|
+
const cleanedPlanText = stripMermaidBlocks(parsedResponse.cleanedOutput).trim()
|
|
674
|
+
|| stripMermaidBlocks(streamedAssistantMessage).trim();
|
|
675
|
+
let assistantMessage = cleanedPlanText
|
|
455
676
|
|| getTranscriptContentFallback(specRunResult.transcriptRuns)
|
|
456
|
-
|| '';
|
|
457
|
-
|
|
458
|
-
|
|
677
|
+
|| (attachments.length > 0 ? ATTACHMENT_ONLY_EMPTY_RESPONSE_FALLBACK : '');
|
|
678
|
+
// Create snapshot from artifact if present. Presence is populated
|
|
679
|
+
// alongside artifact by the extractor, so the two are non-null together.
|
|
680
|
+
if (artifactResult.artifact && artifactResult.presence) {
|
|
681
|
+
// A block re-emitted this turn overrides the previous snapshot's value;
|
|
682
|
+
// an omitted block is carried forward so established artifacts don't
|
|
683
|
+
// disappear when a follow-up turn omits the diagram/mockup.
|
|
684
|
+
const { mermaid: mermaidBlock, mockup: mockupBlock, fileRelationGraph: fileRelationGraphBlock } = resolveCarriedForwardArtifacts({
|
|
685
|
+
rawResponse,
|
|
686
|
+
previousMermaid: previousSnapshot?.mermaid ?? null,
|
|
687
|
+
previousMockup: previousSnapshot?.mockup ?? null,
|
|
688
|
+
previousFileRelationGraph: previousSnapshot?.file_relation_graph ?? null,
|
|
689
|
+
});
|
|
690
|
+
// Optional structured array fields use the same carry-forward contract:
|
|
691
|
+
// a field the LLM emitted this turn (including an explicit empty array)
|
|
692
|
+
// overrides the prior snapshot; a field the LLM omitted is carried
|
|
693
|
+
// forward unchanged so follow-up turns never silently drop information.
|
|
694
|
+
const carriedFields = resolveCarriedForwardStructuredFields({
|
|
695
|
+
artifact: artifactResult.artifact,
|
|
696
|
+
presence: artifactResult.presence,
|
|
697
|
+
previous: previousSnapshot
|
|
698
|
+
? {
|
|
699
|
+
assumptions: previousSnapshot.assumptions,
|
|
700
|
+
openQuestions: previousSnapshot.open_questions,
|
|
701
|
+
likelyFiles: previousSnapshot.likely_files,
|
|
702
|
+
dependencies: previousSnapshot.dependencies,
|
|
703
|
+
}
|
|
704
|
+
: null,
|
|
705
|
+
});
|
|
706
|
+
const snapshotId = createSpecSnapshot({
|
|
707
|
+
specSessionId,
|
|
708
|
+
summary: artifactResult.artifact.summary,
|
|
709
|
+
assumptions: carriedFields.assumptions,
|
|
710
|
+
openQuestions: carriedFields.openQuestions,
|
|
711
|
+
likelyFiles: carriedFields.likelyFiles,
|
|
712
|
+
tickets: artifactResult.artifact.tickets,
|
|
713
|
+
dependencies: carriedFields.dependencies,
|
|
714
|
+
mermaid: mermaidBlock,
|
|
715
|
+
mockup: mockupBlock,
|
|
716
|
+
fileRelationGraph: fileRelationGraphBlock,
|
|
717
|
+
planText: cleanedPlanText,
|
|
718
|
+
});
|
|
719
|
+
updateSpecSessionLatestSnapshot(specSessionId, snapshotId);
|
|
720
|
+
addExecutionRunLink({
|
|
721
|
+
projectId: executionContext.projectId,
|
|
722
|
+
runId: executionContext.runId,
|
|
723
|
+
link: {
|
|
724
|
+
entityType: 'spec_snapshot',
|
|
725
|
+
entityId: snapshotId,
|
|
726
|
+
relationship: 'result',
|
|
727
|
+
expectedParent: { entityType: 'spec_session', entityId: specSessionId },
|
|
728
|
+
},
|
|
729
|
+
});
|
|
730
|
+
// Auto-finalize: when an artifact is emitted during grill mode, the
|
|
731
|
+
// agent has signaled that it has no remaining questions. Flip the
|
|
732
|
+
// session mode back to 'standard' so subsequent turns run the standard
|
|
733
|
+
// prompt. This runs inside the same queued job that wrote the snapshot,
|
|
734
|
+
// so it cannot race with a concurrent user-initiated mode change.
|
|
735
|
+
if (sessionMode === 'grill') {
|
|
736
|
+
updateSpecSessionMode(specSessionId, 'standard');
|
|
737
|
+
}
|
|
459
738
|
}
|
|
460
|
-
if
|
|
739
|
+
// Append artifact error note to the visible message if extraction failed.
|
|
740
|
+
// Suppress the "no block found" note — it is normal for Q&A turns where the
|
|
741
|
+
// agent intentionally omitted the artifact because the specification did not change.
|
|
742
|
+
if (artifactResult.error && assistantMessage && artifactResult.error !== 'No spec artifact block found in planner response') {
|
|
743
|
+
assistantMessage += `\n\n[Note: ${artifactResult.error}. No specification snapshot was saved. You can try refining the spec or asking the planner to regenerate the artifact.]`;
|
|
744
|
+
}
|
|
745
|
+
if (assistantMessage) {
|
|
461
746
|
const messageId = createSpecMessage({
|
|
462
747
|
specSessionId,
|
|
463
748
|
role: 'assistant',
|
|
464
749
|
kind: 'final_response',
|
|
465
|
-
content:
|
|
750
|
+
content: assistantMessage,
|
|
466
751
|
responseTiming: specRunResult.responseTiming,
|
|
467
752
|
transcriptRuns: specRunResult.transcriptRuns,
|
|
468
753
|
});
|
|
469
754
|
if (messageId) {
|
|
470
755
|
bindSpecSessionThinkingMessage(specSessionId, messageId);
|
|
756
|
+
addExecutionRunLink({
|
|
757
|
+
projectId: executionContext.projectId,
|
|
758
|
+
runId: executionContext.runId,
|
|
759
|
+
link: {
|
|
760
|
+
entityType: 'spec_message',
|
|
761
|
+
entityId: messageId,
|
|
762
|
+
relationship: 'result',
|
|
763
|
+
expectedParent: { entityType: 'spec_session', entityId: specSessionId },
|
|
764
|
+
},
|
|
765
|
+
});
|
|
471
766
|
}
|
|
472
767
|
}
|
|
473
|
-
if (
|
|
474
|
-
|
|
768
|
+
if (resolvedTool === 'opencode' && latestCompletedSession?.title === getDefaultSpecSessionTitle()) {
|
|
769
|
+
updateSpecSessionTitle(specSessionId, deriveSpecSessionTitle(trimmedInstruction));
|
|
475
770
|
}
|
|
476
771
|
updateSpecSessionUsageSnapshot(specSessionId, specRunResult.usageSnapshot);
|
|
477
|
-
updateSpecSessionStatus(specSessionId, '
|
|
772
|
+
updateSpecSessionStatus(specSessionId, 'done', getSpecSession(specSessionId)?.agent_log ?? null, null);
|
|
478
773
|
emitUpdatedSpecSession(specSessionId, io);
|
|
479
|
-
|
|
774
|
+
finalizeAgentRun(mainRunId, 'done', assistantMessage || rawResponse || null);
|
|
775
|
+
transitionExecutionRun({
|
|
776
|
+
projectId: executionContext.projectId,
|
|
777
|
+
runId: executionContext.runId,
|
|
778
|
+
status: 'completed',
|
|
779
|
+
domainStatus: 'done',
|
|
780
|
+
eventType: 'spec_turn_completed',
|
|
781
|
+
}, io);
|
|
782
|
+
emitAgentRunsUpdated(io, 'spec', specSessionId);
|
|
480
783
|
}
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
: '';
|
|
487
|
-
// Resolve the session mode at the moment the turn completed. NULL/legacy
|
|
488
|
-
// values normalize to 'standard'. The worker's per-mode output branch is
|
|
489
|
-
// decided here, not from a re-read after the artifact path, so an
|
|
490
|
-
// auto-finalize mode flip does not change how THIS turn's output parses.
|
|
491
|
-
const sessionMode = resolveSpecModeValue(latestCompletedSession?.mode);
|
|
492
|
-
// Grill mode: try the grill-question extractor first. If a payload is
|
|
493
|
-
// returned, store it as a 'grill-question' assistant message and skip the
|
|
494
|
-
// snapshot. If no payload is returned, fall through to the standard
|
|
495
|
-
// artifact path (the agent signaled it has no more questions and is
|
|
496
|
-
// emitting the full plan — this is the auto-finalize path).
|
|
497
|
-
if (sessionMode === 'grill' && rawResponse) {
|
|
498
|
-
const grillResult = extractGrillQuestionFromOutput(rawResponse);
|
|
499
|
-
if (grillResult.grillQuestion) {
|
|
500
|
-
const cleanedGrillText = grillResult.cleanedOutput.trim()
|
|
501
|
-
|| grillResult.grillQuestion.question
|
|
502
|
-
|| streamedAssistantMessage.trim();
|
|
503
|
-
const grillMessageId = createSpecMessage({
|
|
504
|
-
specSessionId,
|
|
505
|
-
role: 'assistant',
|
|
506
|
-
kind: 'grill-question',
|
|
507
|
-
content: cleanedGrillText,
|
|
508
|
-
metadata: { grill_question: grillResult.grillQuestion },
|
|
509
|
-
responseTiming: specRunResult.responseTiming,
|
|
510
|
-
transcriptRuns: specRunResult.transcriptRuns,
|
|
511
|
-
});
|
|
512
|
-
if (grillMessageId) {
|
|
513
|
-
bindSpecSessionThinkingMessage(specSessionId, grillMessageId);
|
|
514
|
-
}
|
|
515
|
-
if (specRunResult.transcriptRuns) {
|
|
516
|
-
clearAndEmitAgentRunsForParent(io, 'spec', specSessionId);
|
|
784
|
+
catch (error) {
|
|
785
|
+
if (specCancellation.stopRequested) {
|
|
786
|
+
untrackSpecProcessById(specSessionId);
|
|
787
|
+
if (mainRunId) {
|
|
788
|
+
finalizeAgentRun(mainRunId, 'stopped', null);
|
|
517
789
|
}
|
|
518
|
-
|
|
519
|
-
updateSpecSessionTitle(specSessionId, deriveSpecSessionTitle(trimmedInstruction));
|
|
520
|
-
}
|
|
521
|
-
updateSpecSessionUsageSnapshot(specSessionId, specRunResult.usageSnapshot);
|
|
522
|
-
updateSpecSessionStatus(specSessionId, 'done', getSpecSession(specSessionId)?.agent_log ?? null, null);
|
|
790
|
+
updateSpecSessionStatus(specSessionId, 'stopped', null, null);
|
|
523
791
|
emitUpdatedSpecSession(specSessionId, io);
|
|
792
|
+
transitionExecutionRun({
|
|
793
|
+
projectId: executionContext.projectId,
|
|
794
|
+
runId: executionContext.runId,
|
|
795
|
+
status: 'cancelled',
|
|
796
|
+
domainStatus: 'stopped',
|
|
797
|
+
eventType: 'spec_turn_cancelled',
|
|
798
|
+
}, io);
|
|
799
|
+
emitAgentRunsUpdated(io, 'spec', specSessionId);
|
|
524
800
|
return;
|
|
525
801
|
}
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
: { cleanedOutput: '', artifact: null, error: null };
|
|
530
|
-
const parsedResponse = extractProjectMemorySuggestionsFromOutput(artifactResult.cleanedOutput);
|
|
531
|
-
if (project.memory_enabled && parsedResponse.suggestions && parsedResponse.suggestions.length > 0) {
|
|
532
|
-
persistSpecSessionProjectMemorySuggestions(project.id, specSessionId, parsedResponse.suggestions);
|
|
533
|
-
}
|
|
534
|
-
const cleanedPlanText = stripMermaidBlocks(parsedResponse.cleanedOutput).trim()
|
|
535
|
-
|| stripMermaidBlocks(streamedAssistantMessage).trim();
|
|
536
|
-
let assistantMessage = cleanedPlanText
|
|
537
|
-
|| getTranscriptContentFallback(specRunResult.transcriptRuns)
|
|
538
|
-
|| (attachments.length > 0 ? ATTACHMENT_ONLY_EMPTY_RESPONSE_FALLBACK : '');
|
|
539
|
-
// Create snapshot from artifact if present. Presence is populated
|
|
540
|
-
// alongside artifact by the extractor, so the two are non-null together.
|
|
541
|
-
if (artifactResult.artifact && artifactResult.presence) {
|
|
542
|
-
// A block re-emitted this turn overrides the previous snapshot's value;
|
|
543
|
-
// an omitted block is carried forward so established artifacts don't
|
|
544
|
-
// disappear when a follow-up turn omits the diagram/mockup.
|
|
545
|
-
const { mermaid: mermaidBlock, mockup: mockupBlock, fileRelationGraph: fileRelationGraphBlock } = resolveCarriedForwardArtifacts({
|
|
546
|
-
rawResponse,
|
|
547
|
-
previousMermaid: previousSnapshot?.mermaid ?? null,
|
|
548
|
-
previousMockup: previousSnapshot?.mockup ?? null,
|
|
549
|
-
previousFileRelationGraph: previousSnapshot?.file_relation_graph ?? null,
|
|
550
|
-
});
|
|
551
|
-
// Optional structured array fields use the same carry-forward contract:
|
|
552
|
-
// a field the LLM emitted this turn (including an explicit empty array)
|
|
553
|
-
// overrides the prior snapshot; a field the LLM omitted is carried
|
|
554
|
-
// forward unchanged so follow-up turns never silently drop information.
|
|
555
|
-
const carriedFields = resolveCarriedForwardStructuredFields({
|
|
556
|
-
artifact: artifactResult.artifact,
|
|
557
|
-
presence: artifactResult.presence,
|
|
558
|
-
previous: previousSnapshot
|
|
559
|
-
? {
|
|
560
|
-
assumptions: previousSnapshot.assumptions,
|
|
561
|
-
openQuestions: previousSnapshot.open_questions,
|
|
562
|
-
likelyFiles: previousSnapshot.likely_files,
|
|
563
|
-
dependencies: previousSnapshot.dependencies,
|
|
564
|
-
}
|
|
565
|
-
: null,
|
|
566
|
-
});
|
|
567
|
-
const snapshotId = createSpecSnapshot({
|
|
568
|
-
specSessionId,
|
|
569
|
-
summary: artifactResult.artifact.summary,
|
|
570
|
-
assumptions: carriedFields.assumptions,
|
|
571
|
-
openQuestions: carriedFields.openQuestions,
|
|
572
|
-
likelyFiles: carriedFields.likelyFiles,
|
|
573
|
-
tickets: artifactResult.artifact.tickets,
|
|
574
|
-
dependencies: carriedFields.dependencies,
|
|
575
|
-
mermaid: mermaidBlock,
|
|
576
|
-
mockup: mockupBlock,
|
|
577
|
-
fileRelationGraph: fileRelationGraphBlock,
|
|
578
|
-
planText: cleanedPlanText,
|
|
579
|
-
});
|
|
580
|
-
updateSpecSessionLatestSnapshot(specSessionId, snapshotId);
|
|
581
|
-
// Auto-finalize: when an artifact is emitted during grill mode, the
|
|
582
|
-
// agent has signaled that it has no remaining questions. Flip the
|
|
583
|
-
// session mode back to 'standard' so subsequent turns run the standard
|
|
584
|
-
// prompt. This runs inside the same queued job that wrote the snapshot,
|
|
585
|
-
// so it cannot race with a concurrent user-initiated mode change.
|
|
586
|
-
if (sessionMode === 'grill') {
|
|
587
|
-
updateSpecSessionMode(specSessionId, 'standard');
|
|
588
|
-
}
|
|
589
|
-
}
|
|
590
|
-
// Append artifact error note to the visible message if extraction failed.
|
|
591
|
-
// Suppress the "no block found" note — it is normal for Q&A turns where the
|
|
592
|
-
// agent intentionally omitted the artifact because the specification did not change.
|
|
593
|
-
if (artifactResult.error && assistantMessage && artifactResult.error !== 'No spec artifact block found in planner response') {
|
|
594
|
-
assistantMessage += `\n\n[Note: ${artifactResult.error}. No specification snapshot was saved. You can try refining the spec or asking the planner to regenerate the artifact.]`;
|
|
595
|
-
}
|
|
596
|
-
if (assistantMessage) {
|
|
597
|
-
const messageId = createSpecMessage({
|
|
598
|
-
specSessionId,
|
|
599
|
-
role: 'assistant',
|
|
600
|
-
kind: 'final_response',
|
|
601
|
-
content: assistantMessage,
|
|
602
|
-
responseTiming: specRunResult.responseTiming,
|
|
603
|
-
transcriptRuns: specRunResult.transcriptRuns,
|
|
604
|
-
});
|
|
605
|
-
if (messageId) {
|
|
606
|
-
bindSpecSessionThinkingMessage(specSessionId, messageId);
|
|
802
|
+
const latestSession = getSpecSession(specSessionId);
|
|
803
|
+
if (resolvedTool === 'opencode' && latestSession?.title === getDefaultSpecSessionTitle()) {
|
|
804
|
+
updateSpecSessionTitle(specSessionId, deriveSpecSessionTitle(trimmedInstruction));
|
|
607
805
|
}
|
|
806
|
+
failSpecTurn({ specSessionId, executionContext, mainRunId, error, io });
|
|
608
807
|
}
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
}
|
|
612
|
-
if (resolvedTool === 'opencode' && latestCompletedSession?.title === getDefaultSpecSessionTitle()) {
|
|
613
|
-
updateSpecSessionTitle(specSessionId, deriveSpecSessionTitle(trimmedInstruction));
|
|
808
|
+
finally {
|
|
809
|
+
deleteStagedSpecAttachments(stagedAgentAttachmentsDirectoryPath);
|
|
614
810
|
}
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
createSpecMessage({
|
|
628
|
-
specSessionId,
|
|
629
|
-
role: 'assistant',
|
|
630
|
-
kind: 'error',
|
|
631
|
-
content: errorContent,
|
|
632
|
-
transcriptRuns,
|
|
633
|
-
});
|
|
634
|
-
}
|
|
635
|
-
if (transcriptRuns) {
|
|
636
|
-
clearAndEmitAgentRunsForParent(io, 'spec', specSessionId);
|
|
637
|
-
}
|
|
638
|
-
updateSpecSessionStatus(specSessionId, 'error', getErrorMessage(error), null);
|
|
639
|
-
emitUpdatedSpecSession(specSessionId, io);
|
|
811
|
+
});
|
|
812
|
+
}
|
|
813
|
+
catch (error) {
|
|
814
|
+
untrackSpecProcessById(specSessionId);
|
|
815
|
+
const failureMessage = getErrorMessage(error);
|
|
816
|
+
updateSpecSessionStatus(specSessionId, 'error', failureMessage, null);
|
|
817
|
+
emitUpdatedSpecSession(specSessionId, io);
|
|
818
|
+
throw new TurnStartError('queue_failed', failureMessage);
|
|
819
|
+
}
|
|
820
|
+
queuedJob.catch(error => {
|
|
821
|
+
try {
|
|
822
|
+
failSpecTurn({ specSessionId, executionContext, mainRunId: null, error, io });
|
|
640
823
|
}
|
|
641
|
-
|
|
642
|
-
|
|
824
|
+
catch {
|
|
825
|
+
// Startup reconciliation remains the final repair boundary for database failures.
|
|
643
826
|
}
|
|
644
|
-
}).catch(error => {
|
|
645
|
-
updateSpecSessionStatus(specSessionId, 'error', getErrorMessage(error), null);
|
|
646
|
-
emitUpdatedSpecSession(specSessionId, io);
|
|
647
827
|
});
|
|
648
828
|
return true;
|
|
649
829
|
};
|