@straiffi/archon 1.7.1 → 1.7.3
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-DTEenQas.js +1 -0
- package/dist/client/assets/GitHubActionsDialog-BKWHonUS.js +1 -0
- package/dist/client/assets/TestsDialog-cBaoffCs.js +4 -0
- package/dist/client/assets/{architectureDiagram-3BPJPVTR-CJgwCv5O.js → architectureDiagram-3BPJPVTR-BzzGotac.js} +1 -1
- package/dist/client/assets/{blockDiagram-GPEHLZMM--xjes8FA.js → blockDiagram-GPEHLZMM-DIcqLHKg.js} +1 -1
- package/dist/client/assets/{c4Diagram-AAUBKEIU-DHnp82NG.js → c4Diagram-AAUBKEIU-Hc1Sqmhs.js} +1 -1
- package/dist/client/assets/channel-Cym17waa.js +1 -0
- package/dist/client/assets/{chunk-2J33WTMH-DAOyJCKy.js → chunk-2J33WTMH-CWSwjsgk.js} +1 -1
- package/dist/client/assets/{chunk-3OPIFGDE-C9YI_tk4.js → chunk-3OPIFGDE-BnX3DljN.js} +1 -1
- package/dist/client/assets/{chunk-5ZQYHXKU-BW9khkin.js → chunk-5ZQYHXKU-EyyJlI-d.js} +1 -1
- package/dist/client/assets/{chunk-727SXJPM-DL4t5Hpi.js → chunk-727SXJPM-Ci6CfxWj.js} +1 -1
- package/dist/client/assets/{chunk-AQP2D5EJ-C4VMKa_O.js → chunk-AQP2D5EJ-RKZNsJTm.js} +1 -1
- package/dist/client/assets/{chunk-CSCIHK7Q-DHP7zrZK.js → chunk-CSCIHK7Q-DBf0Euoj.js} +1 -1
- package/dist/client/assets/{chunk-KSCS5N6A-CqZWek3t.js → chunk-KSCS5N6A-BmLQI_2S.js} +1 -1
- package/dist/client/assets/{chunk-L5ZTLDWV-D3q55pCI.js → chunk-L5ZTLDWV-DKwVAb8G.js} +1 -1
- package/dist/client/assets/{chunk-LZXEDZCA-UDY_Lo6v.js → chunk-LZXEDZCA-axotKkbz.js} +2 -2
- package/dist/client/assets/{chunk-ND2GUHAM-ZCiOxkYV.js → chunk-ND2GUHAM-BBlJ4nXC.js} +1 -1
- package/dist/client/assets/{chunk-NZK2D7GU-CuryoBtd.js → chunk-NZK2D7GU-BKJGoZZt.js} +1 -1
- package/dist/client/assets/{chunk-O5CBEL6O-PnPsz2TB.js → chunk-O5CBEL6O-C-Ghigo1.js} +1 -1
- package/dist/client/assets/{chunk-WU5MYG2G-BzOopX-U.js → chunk-WU5MYG2G-CCzVWYcd.js} +1 -1
- package/dist/client/assets/classDiagram-4FO5ZUOK-BcigvvIC.js +1 -0
- package/dist/client/assets/classDiagram-v2-Q7XG4LA2-3AdSuE0N.js +1 -0
- package/dist/client/assets/{dagre-BM42HDAG-5LBwQ83I.js → dagre-BM42HDAG-rwIdPcjI.js} +1 -1
- package/dist/client/assets/{diagram-2AECGRRQ-_4pckiGR.js → diagram-2AECGRRQ-DJWxOQsg.js} +1 -1
- package/dist/client/assets/{diagram-5GNKFQAL-CqElv5rJ.js → diagram-5GNKFQAL-C-mQeFtd.js} +1 -1
- package/dist/client/assets/{diagram-KO2AKTUF-DKv4zS4F.js → diagram-KO2AKTUF-BAu6u4sR.js} +1 -1
- package/dist/client/assets/{diagram-LMA3HP47-iLmTH4UP.js → diagram-LMA3HP47-U6CbvBn0.js} +1 -1
- package/dist/client/assets/{diagram-OG6HWLK6-DpFYIa0o.js → diagram-OG6HWLK6-DNTwFcs1.js} +1 -1
- package/dist/client/assets/{erDiagram-TEJ5UH35-DnfbvbKs.js → erDiagram-TEJ5UH35-DaHX6Xw5.js} +1 -1
- package/dist/client/assets/{flowDiagram-I6XJVG4X-C2MN70Qv.js → flowDiagram-I6XJVG4X-B9hKqL2l.js} +1 -1
- package/dist/client/assets/{ganttDiagram-6RSMTGT7-yonpliOJ.js → ganttDiagram-6RSMTGT7-BQ1HPHVO.js} +1 -1
- package/dist/client/assets/{gitGraphDiagram-PVQCEYII-CaKDWx7y.js → gitGraphDiagram-PVQCEYII-BBRcsmMK.js} +1 -1
- package/dist/client/assets/index-CEMaJCPS.js +273 -0
- package/dist/client/assets/index-lWftq65U.css +2 -0
- package/dist/client/assets/{infoDiagram-5YYISTIA-82-8njnx.js → infoDiagram-5YYISTIA-DUoNiSGC.js} +1 -1
- package/dist/client/assets/{ishikawaDiagram-YF4QCWOH-BMiyLWwK.js → ishikawaDiagram-YF4QCWOH-DJN1y2w8.js} +1 -1
- package/dist/client/assets/{journeyDiagram-JHISSGLW-C1qBnR37.js → journeyDiagram-JHISSGLW-BxLLMT70.js} +1 -1
- package/dist/client/assets/{kanban-definition-UN3LZRKU-zByEuCzn.js → kanban-definition-UN3LZRKU-BdCPistJ.js} +1 -1
- package/dist/client/assets/{line-EpLqyzw7.js → line-4mIgXsZd.js} +1 -1
- package/dist/client/assets/{mermaid-parser.core-o124REPN.js → mermaid-parser.core-Pu1QGWJ-.js} +1 -1
- package/dist/client/assets/{mermaid.core-DgiAn2-k.js → mermaid.core-DnH9DrgZ.js} +3 -3
- package/dist/client/assets/{mindmap-definition-RKZ34NQL-UK6V0Bxq.js → mindmap-definition-RKZ34NQL-DqE4ba3C.js} +1 -1
- package/dist/client/assets/{pieDiagram-4H26LBE5-B2s9oGFl.js → pieDiagram-4H26LBE5-Crjy9ejf.js} +1 -1
- package/dist/client/assets/{quadrantDiagram-W4KKPZXB-d5H7-zcc.js → quadrantDiagram-W4KKPZXB-BUSZ2VB7.js} +1 -1
- package/dist/client/assets/{requirementDiagram-4Y6WPE33-DlwN9Vcv.js → requirementDiagram-4Y6WPE33-CBhFKEoV.js} +1 -1
- package/dist/client/assets/{sankeyDiagram-5OEKKPKP-9dGyAxu5.js → sankeyDiagram-5OEKKPKP-CgdbY41v.js} +1 -1
- package/dist/client/assets/{sequenceDiagram-3UESZ5HK-DKwRjEym.js → sequenceDiagram-3UESZ5HK-hkp_H7AE.js} +1 -1
- package/dist/client/assets/{stateDiagram-AJRCARHV-D_6cuqsX.js → stateDiagram-AJRCARHV-C6x45RWp.js} +1 -1
- package/dist/client/assets/stateDiagram-v2-BHNVJYJU-DVzPjRPN.js +1 -0
- package/dist/client/assets/{timeline-definition-PNZ67QCA-ChqD4j8I.js → timeline-definition-PNZ67QCA-CjnZWttQ.js} +1 -1
- package/dist/client/assets/{vennDiagram-CIIHVFJN-CSAZk0IW.js → vennDiagram-CIIHVFJN-DU5ZyYju.js} +1 -1
- package/dist/client/assets/{wardleyDiagram-YWT4CUSO-BUE6IF4q.js → wardleyDiagram-YWT4CUSO-CaCEnCB0.js} +1 -1
- package/dist/client/assets/{xychartDiagram-2RQKCTM6-MOkC53m_.js → xychartDiagram-2RQKCTM6-Ce3xa37U.js} +1 -1
- package/dist/client/index.html +2 -2
- package/dist/server/db.js +471 -39
- package/dist/server/db.js.map +1 -1
- package/dist/server/index.js +1267 -213
- package/dist/server/index.js.map +1 -1
- package/dist/server/lib/agent.js +281 -76
- 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/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 +223 -0
- package/dist/server/lib/buildOpenQuestionOperations.js.map +1 -0
- package/dist/server/lib/buildOpenQuestions.js +52 -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 +82 -9
- package/dist/server/lib/bundleReviewOperations.js.map +1 -1
- package/dist/server/lib/chatOperations.js +31 -0
- 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/executionRunRetention.js +141 -0
- package/dist/server/lib/executionRunRetention.js.map +1 -0
- package/dist/server/lib/executionRuns.js +1264 -0
- package/dist/server/lib/executionRuns.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 +31 -0
- package/dist/server/lib/specOperations.js.map +1 -1
- package/dist/server/lib/specReviewContext.js +44 -14
- 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 +44 -7
- 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 +164 -19
- 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 +214 -20
- package/dist/server/lib/ticketFollowUpOperations.js.map +1 -1
- package/dist/server/lib/ticketOpenQuestions.js +30 -14
- package/dist/server/lib/ticketOpenQuestions.js.map +1 -1
- package/dist/server/lib/ticketRunOperations.js +75 -7
- 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 +46 -29
- 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 +57 -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 +261 -130
- package/dist/server/workers/chat.js.map +1 -1
- package/dist/server/workers/followUp.js +720 -195
- package/dist/server/workers/followUp.js.map +1 -1
- package/dist/server/workers/plan.js +527 -188
- package/dist/server/workers/plan.js.map +1 -1
- package/dist/server/workers/planFollowUp.js +231 -69
- package/dist/server/workers/planFollowUp.js.map +1 -1
- package/dist/server/workers/review.js +796 -239
- package/dist/server/workers/review.js.map +1 -1
- package/dist/server/workers/specReview.js +211 -45
- package/dist/server/workers/specReview.js.map +1 -1
- package/dist/server/workers/specSession.js +425 -252
- package/dist/server/workers/specSession.js.map +1 -1
- package/package.json +1 -1
- package/dist/client/assets/GitHubActionsDialog-BrUq-zO1.js +0 -1
- package/dist/client/assets/TestsDialog-D9N4GD77.js +0 -4
- package/dist/client/assets/TicketChangesModal-bYHJSO6d.js +0 -1
- package/dist/client/assets/channel-B4kCvLCt.js +0 -1
- package/dist/client/assets/classDiagram-4FO5ZUOK-C-eRRNNj.js +0 -1
- package/dist/client/assets/classDiagram-v2-Q7XG4LA2-B3vxoc73.js +0 -1
- package/dist/client/assets/index-9q1GXkUw.js +0 -270
- package/dist/client/assets/index-BxTc5qQT.css +0 -2
- package/dist/client/assets/stateDiagram-v2-BHNVJYJU-BkMKz90T.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, 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, }) => {
|
|
316
375
|
const specSession = getSpecSession(specSessionId, { includeMessages: true });
|
|
317
376
|
if (!specSession) {
|
|
318
377
|
return false;
|
|
@@ -337,6 +396,16 @@ export const startSpecSessionTurn = ({ specSessionId, instruction, attachments =
|
|
|
337
396
|
kind: 'prompt',
|
|
338
397
|
content: trimmedInstruction,
|
|
339
398
|
});
|
|
399
|
+
addExecutionRunLink({
|
|
400
|
+
projectId: executionContext.projectId,
|
|
401
|
+
runId: executionContext.runId,
|
|
402
|
+
link: {
|
|
403
|
+
entityType: 'spec_message',
|
|
404
|
+
entityId: userMessageId,
|
|
405
|
+
relationship: 'trigger',
|
|
406
|
+
expectedParent: { entityType: 'spec_session', entityId: specSessionId },
|
|
407
|
+
},
|
|
408
|
+
});
|
|
340
409
|
if (attachments.length > 0) {
|
|
341
410
|
linkSpecAttachmentsToMessage({
|
|
342
411
|
specMessageId: userMessageId,
|
|
@@ -363,7 +432,6 @@ export const startSpecSessionTurn = ({ specSessionId, instruction, attachments =
|
|
|
363
432
|
}
|
|
364
433
|
beginSpecSessionThinkingRun(specSessionId);
|
|
365
434
|
updateSpecSessionStatus(specSessionId, 'running', null, null);
|
|
366
|
-
clearAndEmitAgentRunsForParent(io, 'spec', specSessionId);
|
|
367
435
|
// Register a cancellation token before any async work so an interrupt during
|
|
368
436
|
// the pre-response window (prepareLinkedPlanningResearch) is accepted rather
|
|
369
437
|
// than returning a 409. runSpecAgent will overwrite this entry with the real
|
|
@@ -371,279 +439,384 @@ export const startSpecSessionTurn = ({ specSessionId, instruction, attachments =
|
|
|
371
439
|
// stopRequested below and finalizes to 'stopped' without starting the agent.
|
|
372
440
|
const specCancellation = registerSpecProcessPlaceholder(specSessionId);
|
|
373
441
|
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
|
-
|
|
442
|
+
let queuedJob;
|
|
443
|
+
try {
|
|
444
|
+
queuedJob = enqueue(jobKey, async () => {
|
|
445
|
+
let stagedAgentAttachmentsDirectoryPath = null;
|
|
446
|
+
let mainRunId = null;
|
|
447
|
+
try {
|
|
448
|
+
transitionExecutionRun({
|
|
449
|
+
projectId: executionContext.projectId,
|
|
450
|
+
runId: executionContext.runId,
|
|
451
|
+
status: 'running',
|
|
452
|
+
domainStatus: 'running',
|
|
453
|
+
eventType: 'spec_turn_started',
|
|
454
|
+
}, io);
|
|
455
|
+
const mainRunIndex = getNextAgentRunIndex(executionContext.runId);
|
|
456
|
+
mainRunId = createAgentRun({
|
|
457
|
+
executionRunId: executionContext.runId,
|
|
458
|
+
parentType: 'spec',
|
|
459
|
+
parentId: specSessionId,
|
|
460
|
+
label: 'Main agent',
|
|
461
|
+
role: 'main',
|
|
462
|
+
});
|
|
463
|
+
const latestSession = getSpecSession(specSessionId);
|
|
464
|
+
if (!latestSession) {
|
|
465
|
+
throw new Error('Spec session no longer exists');
|
|
466
|
+
}
|
|
467
|
+
// The prior snapshot (if any) is read once and reused both to guide the
|
|
468
|
+
// prompt (don't redraw established artifacts) and to carry its mermaid/
|
|
469
|
+
// mockup forward when the current turn omits them. No new snapshot is
|
|
470
|
+
// created mid-run, so this reference stays valid through the done path.
|
|
471
|
+
const previousSnapshot = latestSession.latest_snapshot_id
|
|
472
|
+
? getSpecSnapshot(latestSession.latest_snapshot_id)
|
|
473
|
+
: null;
|
|
474
|
+
const promptContext = latestSession.title === getDefaultSpecSessionTitle()
|
|
475
|
+
? null
|
|
476
|
+
: latestSession.title;
|
|
477
|
+
const projectMemory = getProjectMemoryPromptBlock(project.id, {
|
|
478
|
+
flow: 'spec',
|
|
479
|
+
stage: 'plan',
|
|
480
|
+
title: trimmedInstruction || 'Attached images only',
|
|
481
|
+
description: promptContext,
|
|
482
|
+
currentInstruction: trimmedInstruction,
|
|
483
|
+
invocation: { parentType: 'spec', parentId: specSessionId, agentRunId: mainRunId },
|
|
484
|
+
});
|
|
485
|
+
const cwd = project.repo_path;
|
|
486
|
+
const linkedResearch = trimmedInstruction
|
|
487
|
+
? await prepareLinkedPlanningResearch(trimmedInstruction, {
|
|
488
|
+
projectId: project.id,
|
|
489
|
+
tool: resolvedTool,
|
|
490
|
+
model: resolvedModel,
|
|
491
|
+
variant: resolvedVariant,
|
|
492
|
+
skills: resolvedSkills,
|
|
493
|
+
executionContext,
|
|
494
|
+
cancellationState: specCancellation,
|
|
495
|
+
agentRunParent: { parentType: 'spec', parentId: specSessionId, io },
|
|
496
|
+
})
|
|
497
|
+
: [];
|
|
498
|
+
// Honor an interrupt that arrived during the pre-response window. There
|
|
499
|
+
// is no child to kill and no partial response to save; transition straight
|
|
500
|
+
// to 'stopped' (worker-owned, mirroring how Chat finalizes after a stop).
|
|
501
|
+
if (specCancellation.stopRequested) {
|
|
502
|
+
untrackSpecProcessById(specSessionId);
|
|
503
|
+
finalizeAgentRun(mainRunId, 'stopped', null);
|
|
504
|
+
updateSpecSessionStatus(specSessionId, 'stopped', null, null);
|
|
505
|
+
emitUpdatedSpecSession(specSessionId, io);
|
|
506
|
+
transitionExecutionRun({
|
|
507
|
+
projectId: executionContext.projectId,
|
|
508
|
+
runId: executionContext.runId,
|
|
509
|
+
status: 'cancelled',
|
|
510
|
+
domainStatus: 'stopped',
|
|
511
|
+
eventType: 'spec_turn_cancelled',
|
|
512
|
+
}, io);
|
|
513
|
+
emitAgentRunsUpdated(io, 'spec', specSessionId);
|
|
514
|
+
return;
|
|
515
|
+
}
|
|
516
|
+
const linkedResearchContext = buildLinkedResearchContext(project, linkedResearch);
|
|
517
|
+
const stagedAgentAttachments = resolvedTool === 'opencode'
|
|
518
|
+
? stageSpecAttachmentsForAgent({
|
|
519
|
+
specSessionId,
|
|
520
|
+
cwd,
|
|
521
|
+
attachments,
|
|
522
|
+
})
|
|
523
|
+
: { directoryPath: null, files: [] };
|
|
524
|
+
stagedAgentAttachmentsDirectoryPath = stagedAgentAttachments.directoryPath;
|
|
525
|
+
const specPrompt = buildSpecPrompt({
|
|
526
|
+
projectName: project.name,
|
|
527
|
+
projectPath: cwd,
|
|
528
|
+
instruction: trimmedInstruction,
|
|
529
|
+
hasAttachments: attachments.length > 0,
|
|
530
|
+
projectMemory,
|
|
531
|
+
memoryEnabled: Boolean(project.memory_enabled),
|
|
532
|
+
previousSnapshot,
|
|
533
|
+
linkedResearchContext,
|
|
534
|
+
mode: resolveSpecModeValue(latestSession.mode),
|
|
535
|
+
});
|
|
536
|
+
const specRunResult = await runSpecAgent({
|
|
537
|
+
specSessionId,
|
|
403
538
|
tool: resolvedTool,
|
|
539
|
+
prompt: specPrompt,
|
|
540
|
+
cwd,
|
|
541
|
+
files: stagedAgentAttachments.files,
|
|
542
|
+
io,
|
|
404
543
|
model: resolvedModel,
|
|
405
544
|
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
|
-
|
|
545
|
+
resumeSessionId: resolvedTool === 'claude'
|
|
546
|
+
? latestSession.claude_session_id
|
|
547
|
+
: latestSession.opencode_session_id,
|
|
548
|
+
executionContext,
|
|
549
|
+
mainRun: { id: mainRunId, label: 'Main agent', role: 'main', runIndex: mainRunIndex },
|
|
550
|
+
});
|
|
551
|
+
if (specRunResult.completionStatus === 'stopped') {
|
|
552
|
+
const interruptedSession = getSpecSession(specSessionId);
|
|
553
|
+
const interruptedResponse = interruptedSession?.streaming_response?.trim()
|
|
554
|
+
|| specRunResult.finalResponse?.trim()
|
|
555
|
+
|| getTranscriptContentFallback(specRunResult.transcriptRuns)
|
|
556
|
+
|| '';
|
|
557
|
+
if (resolvedTool === 'opencode' && interruptedSession?.title === getDefaultSpecSessionTitle()) {
|
|
558
|
+
updateSpecSessionTitle(specSessionId, deriveSpecSessionTitle(trimmedInstruction));
|
|
559
|
+
}
|
|
560
|
+
if (interruptedResponse) {
|
|
561
|
+
const messageId = createSpecMessage({
|
|
562
|
+
specSessionId,
|
|
563
|
+
role: 'assistant',
|
|
564
|
+
kind: 'final_response',
|
|
565
|
+
content: interruptedResponse,
|
|
566
|
+
responseTiming: specRunResult.responseTiming,
|
|
567
|
+
transcriptRuns: specRunResult.transcriptRuns,
|
|
568
|
+
});
|
|
569
|
+
if (messageId) {
|
|
570
|
+
bindSpecSessionThinkingMessage(specSessionId, messageId);
|
|
571
|
+
addExecutionRunLink({
|
|
572
|
+
projectId: executionContext.projectId,
|
|
573
|
+
runId: executionContext.runId,
|
|
574
|
+
link: {
|
|
575
|
+
entityType: 'spec_message',
|
|
576
|
+
entityId: messageId,
|
|
577
|
+
relationship: 'result',
|
|
578
|
+
expectedParent: { entityType: 'spec_session', entityId: specSessionId },
|
|
579
|
+
},
|
|
580
|
+
});
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
updateSpecSessionUsageSnapshot(specSessionId, specRunResult.usageSnapshot);
|
|
584
|
+
updateSpecSessionStatus(specSessionId, 'stopped', interruptedSession?.agent_log ?? null, null);
|
|
585
|
+
emitUpdatedSpecSession(specSessionId, io);
|
|
586
|
+
finalizeAgentRun(mainRunId, 'stopped', interruptedResponse || null);
|
|
587
|
+
transitionExecutionRun({
|
|
588
|
+
projectId: executionContext.projectId,
|
|
589
|
+
runId: executionContext.runId,
|
|
590
|
+
status: 'cancelled',
|
|
591
|
+
domainStatus: 'stopped',
|
|
592
|
+
eventType: 'spec_turn_cancelled',
|
|
593
|
+
}, io);
|
|
594
|
+
emitAgentRunsUpdated(io, 'spec', specSessionId);
|
|
595
|
+
return;
|
|
596
|
+
}
|
|
597
|
+
// Done path: process the completed response.
|
|
598
|
+
const latestCompletedSession = getSpecSession(specSessionId);
|
|
599
|
+
const streamedAssistantMessage = latestCompletedSession?.streaming_response?.trim() ?? '';
|
|
600
|
+
const rawResponse = typeof specRunResult.finalResponse === 'string'
|
|
601
|
+
? specRunResult.finalResponse
|
|
602
|
+
: '';
|
|
603
|
+
// Resolve the session mode at the moment the turn completed. NULL/legacy
|
|
604
|
+
// values normalize to 'standard'. The worker's per-mode output branch is
|
|
605
|
+
// decided here, not from a re-read after the artifact path, so an
|
|
606
|
+
// auto-finalize mode flip does not change how THIS turn's output parses.
|
|
607
|
+
const sessionMode = resolveSpecModeValue(latestCompletedSession?.mode);
|
|
608
|
+
// Grill mode: try the grill-question extractor first. If a payload is
|
|
609
|
+
// returned, store it as a 'grill-question' assistant message and skip the
|
|
610
|
+
// snapshot. If no payload is returned, fall through to the standard
|
|
611
|
+
// artifact path (the agent signaled it has no more questions and is
|
|
612
|
+
// emitting the full plan — this is the auto-finalize path).
|
|
613
|
+
if (sessionMode === 'grill' && rawResponse) {
|
|
614
|
+
const grillResult = extractGrillQuestionFromOutput(rawResponse);
|
|
615
|
+
if (grillResult.grillQuestion) {
|
|
616
|
+
const cleanedGrillText = grillResult.cleanedOutput.trim()
|
|
617
|
+
|| grillResult.grillQuestion.question
|
|
618
|
+
|| streamedAssistantMessage.trim();
|
|
619
|
+
const grillMessageId = createSpecMessage({
|
|
620
|
+
specSessionId,
|
|
621
|
+
role: 'assistant',
|
|
622
|
+
kind: 'grill-question',
|
|
623
|
+
content: cleanedGrillText,
|
|
624
|
+
metadata: { grill_question: grillResult.grillQuestion },
|
|
625
|
+
responseTiming: specRunResult.responseTiming,
|
|
626
|
+
transcriptRuns: specRunResult.transcriptRuns,
|
|
627
|
+
});
|
|
628
|
+
if (grillMessageId) {
|
|
629
|
+
bindSpecSessionThinkingMessage(specSessionId, grillMessageId);
|
|
630
|
+
addExecutionRunLink({
|
|
631
|
+
projectId: executionContext.projectId,
|
|
632
|
+
runId: executionContext.runId,
|
|
633
|
+
link: {
|
|
634
|
+
entityType: 'spec_message',
|
|
635
|
+
entityId: grillMessageId,
|
|
636
|
+
relationship: 'result',
|
|
637
|
+
expectedParent: { entityType: 'spec_session', entityId: specSessionId },
|
|
638
|
+
},
|
|
639
|
+
});
|
|
640
|
+
}
|
|
641
|
+
if (resolvedTool === 'opencode' && latestCompletedSession?.title === getDefaultSpecSessionTitle()) {
|
|
642
|
+
updateSpecSessionTitle(specSessionId, deriveSpecSessionTitle(trimmedInstruction));
|
|
643
|
+
}
|
|
644
|
+
updateSpecSessionUsageSnapshot(specSessionId, specRunResult.usageSnapshot);
|
|
645
|
+
updateSpecSessionStatus(specSessionId, 'done', getSpecSession(specSessionId)?.agent_log ?? null, null);
|
|
646
|
+
emitUpdatedSpecSession(specSessionId, io);
|
|
647
|
+
finalizeAgentRun(mainRunId, 'done', cleanedGrillText);
|
|
648
|
+
transitionExecutionRun({
|
|
649
|
+
projectId: executionContext.projectId,
|
|
650
|
+
runId: executionContext.runId,
|
|
651
|
+
status: 'completed',
|
|
652
|
+
domainStatus: 'done',
|
|
653
|
+
eventType: 'spec_turn_completed',
|
|
654
|
+
}, io);
|
|
655
|
+
emitAgentRunsUpdated(io, 'spec', specSessionId);
|
|
656
|
+
return;
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
const artifactResult = rawResponse
|
|
660
|
+
? extractSpecArtifactFromOutput(rawResponse)
|
|
661
|
+
: { cleanedOutput: '', artifact: null, error: null };
|
|
662
|
+
const parsedResponse = extractProjectMemorySuggestionsFromOutput(artifactResult.cleanedOutput);
|
|
663
|
+
if (project.memory_enabled && parsedResponse.suggestions && parsedResponse.suggestions.length > 0) {
|
|
664
|
+
persistSpecSessionProjectMemorySuggestions(project.id, specSessionId, parsedResponse.suggestions);
|
|
665
|
+
}
|
|
666
|
+
const cleanedPlanText = stripMermaidBlocks(parsedResponse.cleanedOutput).trim()
|
|
667
|
+
|| stripMermaidBlocks(streamedAssistantMessage).trim();
|
|
668
|
+
let assistantMessage = cleanedPlanText
|
|
455
669
|
|| getTranscriptContentFallback(specRunResult.transcriptRuns)
|
|
456
|
-
|| '';
|
|
457
|
-
|
|
458
|
-
|
|
670
|
+
|| (attachments.length > 0 ? ATTACHMENT_ONLY_EMPTY_RESPONSE_FALLBACK : '');
|
|
671
|
+
// Create snapshot from artifact if present. Presence is populated
|
|
672
|
+
// alongside artifact by the extractor, so the two are non-null together.
|
|
673
|
+
if (artifactResult.artifact && artifactResult.presence) {
|
|
674
|
+
// A block re-emitted this turn overrides the previous snapshot's value;
|
|
675
|
+
// an omitted block is carried forward so established artifacts don't
|
|
676
|
+
// disappear when a follow-up turn omits the diagram/mockup.
|
|
677
|
+
const { mermaid: mermaidBlock, mockup: mockupBlock, fileRelationGraph: fileRelationGraphBlock } = resolveCarriedForwardArtifacts({
|
|
678
|
+
rawResponse,
|
|
679
|
+
previousMermaid: previousSnapshot?.mermaid ?? null,
|
|
680
|
+
previousMockup: previousSnapshot?.mockup ?? null,
|
|
681
|
+
previousFileRelationGraph: previousSnapshot?.file_relation_graph ?? null,
|
|
682
|
+
});
|
|
683
|
+
// Optional structured array fields use the same carry-forward contract:
|
|
684
|
+
// a field the LLM emitted this turn (including an explicit empty array)
|
|
685
|
+
// overrides the prior snapshot; a field the LLM omitted is carried
|
|
686
|
+
// forward unchanged so follow-up turns never silently drop information.
|
|
687
|
+
const carriedFields = resolveCarriedForwardStructuredFields({
|
|
688
|
+
artifact: artifactResult.artifact,
|
|
689
|
+
presence: artifactResult.presence,
|
|
690
|
+
previous: previousSnapshot
|
|
691
|
+
? {
|
|
692
|
+
assumptions: previousSnapshot.assumptions,
|
|
693
|
+
openQuestions: previousSnapshot.open_questions,
|
|
694
|
+
likelyFiles: previousSnapshot.likely_files,
|
|
695
|
+
dependencies: previousSnapshot.dependencies,
|
|
696
|
+
}
|
|
697
|
+
: null,
|
|
698
|
+
});
|
|
699
|
+
const snapshotId = createSpecSnapshot({
|
|
700
|
+
specSessionId,
|
|
701
|
+
summary: artifactResult.artifact.summary,
|
|
702
|
+
assumptions: carriedFields.assumptions,
|
|
703
|
+
openQuestions: carriedFields.openQuestions,
|
|
704
|
+
likelyFiles: carriedFields.likelyFiles,
|
|
705
|
+
tickets: artifactResult.artifact.tickets,
|
|
706
|
+
dependencies: carriedFields.dependencies,
|
|
707
|
+
mermaid: mermaidBlock,
|
|
708
|
+
mockup: mockupBlock,
|
|
709
|
+
fileRelationGraph: fileRelationGraphBlock,
|
|
710
|
+
planText: cleanedPlanText,
|
|
711
|
+
});
|
|
712
|
+
updateSpecSessionLatestSnapshot(specSessionId, snapshotId);
|
|
713
|
+
addExecutionRunLink({
|
|
714
|
+
projectId: executionContext.projectId,
|
|
715
|
+
runId: executionContext.runId,
|
|
716
|
+
link: {
|
|
717
|
+
entityType: 'spec_snapshot',
|
|
718
|
+
entityId: snapshotId,
|
|
719
|
+
relationship: 'result',
|
|
720
|
+
expectedParent: { entityType: 'spec_session', entityId: specSessionId },
|
|
721
|
+
},
|
|
722
|
+
});
|
|
723
|
+
// Auto-finalize: when an artifact is emitted during grill mode, the
|
|
724
|
+
// agent has signaled that it has no remaining questions. Flip the
|
|
725
|
+
// session mode back to 'standard' so subsequent turns run the standard
|
|
726
|
+
// prompt. This runs inside the same queued job that wrote the snapshot,
|
|
727
|
+
// so it cannot race with a concurrent user-initiated mode change.
|
|
728
|
+
if (sessionMode === 'grill') {
|
|
729
|
+
updateSpecSessionMode(specSessionId, 'standard');
|
|
730
|
+
}
|
|
459
731
|
}
|
|
460
|
-
if
|
|
732
|
+
// Append artifact error note to the visible message if extraction failed.
|
|
733
|
+
// Suppress the "no block found" note — it is normal for Q&A turns where the
|
|
734
|
+
// agent intentionally omitted the artifact because the specification did not change.
|
|
735
|
+
if (artifactResult.error && assistantMessage && artifactResult.error !== 'No spec artifact block found in planner response') {
|
|
736
|
+
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.]`;
|
|
737
|
+
}
|
|
738
|
+
if (assistantMessage) {
|
|
461
739
|
const messageId = createSpecMessage({
|
|
462
740
|
specSessionId,
|
|
463
741
|
role: 'assistant',
|
|
464
742
|
kind: 'final_response',
|
|
465
|
-
content:
|
|
743
|
+
content: assistantMessage,
|
|
466
744
|
responseTiming: specRunResult.responseTiming,
|
|
467
745
|
transcriptRuns: specRunResult.transcriptRuns,
|
|
468
746
|
});
|
|
469
747
|
if (messageId) {
|
|
470
748
|
bindSpecSessionThinkingMessage(specSessionId, messageId);
|
|
749
|
+
addExecutionRunLink({
|
|
750
|
+
projectId: executionContext.projectId,
|
|
751
|
+
runId: executionContext.runId,
|
|
752
|
+
link: {
|
|
753
|
+
entityType: 'spec_message',
|
|
754
|
+
entityId: messageId,
|
|
755
|
+
relationship: 'result',
|
|
756
|
+
expectedParent: { entityType: 'spec_session', entityId: specSessionId },
|
|
757
|
+
},
|
|
758
|
+
});
|
|
471
759
|
}
|
|
472
760
|
}
|
|
473
|
-
if (
|
|
474
|
-
|
|
761
|
+
if (resolvedTool === 'opencode' && latestCompletedSession?.title === getDefaultSpecSessionTitle()) {
|
|
762
|
+
updateSpecSessionTitle(specSessionId, deriveSpecSessionTitle(trimmedInstruction));
|
|
475
763
|
}
|
|
476
764
|
updateSpecSessionUsageSnapshot(specSessionId, specRunResult.usageSnapshot);
|
|
477
|
-
updateSpecSessionStatus(specSessionId, '
|
|
765
|
+
updateSpecSessionStatus(specSessionId, 'done', getSpecSession(specSessionId)?.agent_log ?? null, null);
|
|
478
766
|
emitUpdatedSpecSession(specSessionId, io);
|
|
479
|
-
|
|
767
|
+
finalizeAgentRun(mainRunId, 'done', assistantMessage || rawResponse || null);
|
|
768
|
+
transitionExecutionRun({
|
|
769
|
+
projectId: executionContext.projectId,
|
|
770
|
+
runId: executionContext.runId,
|
|
771
|
+
status: 'completed',
|
|
772
|
+
domainStatus: 'done',
|
|
773
|
+
eventType: 'spec_turn_completed',
|
|
774
|
+
}, io);
|
|
775
|
+
emitAgentRunsUpdated(io, 'spec', specSessionId);
|
|
480
776
|
}
|
|
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);
|
|
777
|
+
catch (error) {
|
|
778
|
+
if (specCancellation.stopRequested) {
|
|
779
|
+
untrackSpecProcessById(specSessionId);
|
|
780
|
+
if (mainRunId) {
|
|
781
|
+
finalizeAgentRun(mainRunId, 'stopped', null);
|
|
517
782
|
}
|
|
518
|
-
|
|
519
|
-
updateSpecSessionTitle(specSessionId, deriveSpecSessionTitle(trimmedInstruction));
|
|
520
|
-
}
|
|
521
|
-
updateSpecSessionUsageSnapshot(specSessionId, specRunResult.usageSnapshot);
|
|
522
|
-
updateSpecSessionStatus(specSessionId, 'done', getSpecSession(specSessionId)?.agent_log ?? null, null);
|
|
783
|
+
updateSpecSessionStatus(specSessionId, 'stopped', null, null);
|
|
523
784
|
emitUpdatedSpecSession(specSessionId, io);
|
|
785
|
+
transitionExecutionRun({
|
|
786
|
+
projectId: executionContext.projectId,
|
|
787
|
+
runId: executionContext.runId,
|
|
788
|
+
status: 'cancelled',
|
|
789
|
+
domainStatus: 'stopped',
|
|
790
|
+
eventType: 'spec_turn_cancelled',
|
|
791
|
+
}, io);
|
|
792
|
+
emitAgentRunsUpdated(io, 'spec', specSessionId);
|
|
524
793
|
return;
|
|
525
794
|
}
|
|
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);
|
|
795
|
+
const latestSession = getSpecSession(specSessionId);
|
|
796
|
+
if (resolvedTool === 'opencode' && latestSession?.title === getDefaultSpecSessionTitle()) {
|
|
797
|
+
updateSpecSessionTitle(specSessionId, deriveSpecSessionTitle(trimmedInstruction));
|
|
607
798
|
}
|
|
799
|
+
failSpecTurn({ specSessionId, executionContext, mainRunId, error, io });
|
|
608
800
|
}
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
}
|
|
612
|
-
if (resolvedTool === 'opencode' && latestCompletedSession?.title === getDefaultSpecSessionTitle()) {
|
|
613
|
-
updateSpecSessionTitle(specSessionId, deriveSpecSessionTitle(trimmedInstruction));
|
|
801
|
+
finally {
|
|
802
|
+
deleteStagedSpecAttachments(stagedAgentAttachmentsDirectoryPath);
|
|
614
803
|
}
|
|
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);
|
|
804
|
+
});
|
|
805
|
+
}
|
|
806
|
+
catch (error) {
|
|
807
|
+
untrackSpecProcessById(specSessionId);
|
|
808
|
+
const failureMessage = getErrorMessage(error);
|
|
809
|
+
updateSpecSessionStatus(specSessionId, 'error', failureMessage, null);
|
|
810
|
+
emitUpdatedSpecSession(specSessionId, io);
|
|
811
|
+
throw new TurnStartError('queue_failed', failureMessage);
|
|
812
|
+
}
|
|
813
|
+
queuedJob.catch(error => {
|
|
814
|
+
try {
|
|
815
|
+
failSpecTurn({ specSessionId, executionContext, mainRunId: null, error, io });
|
|
640
816
|
}
|
|
641
|
-
|
|
642
|
-
|
|
817
|
+
catch {
|
|
818
|
+
// Startup reconciliation remains the final repair boundary for database failures.
|
|
643
819
|
}
|
|
644
|
-
}).catch(error => {
|
|
645
|
-
updateSpecSessionStatus(specSessionId, 'error', getErrorMessage(error), null);
|
|
646
|
-
emitUpdatedSpecSession(specSessionId, io);
|
|
647
820
|
});
|
|
648
821
|
return true;
|
|
649
822
|
};
|