@straiffi/archon 1.4.12 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (100) hide show
  1. package/dist/client/assets/{GitHubActionsDialog-DcW7fUgz.js → GitHubActionsDialog-CQMqcAiI.js} +1 -1
  2. package/dist/client/assets/{TestsDialog-xNEttQOx.js → TestsDialog-iGukzvw2.js} +1 -1
  3. package/dist/client/assets/{architectureDiagram-3BPJPVTR-DHADo63Y.js → architectureDiagram-3BPJPVTR-BG4rc0pD.js} +1 -1
  4. package/dist/client/assets/{blockDiagram-GPEHLZMM-D_LqGLuD.js → blockDiagram-GPEHLZMM-BifkewDx.js} +1 -1
  5. package/dist/client/assets/{c4Diagram-AAUBKEIU-mIqv7fCU.js → c4Diagram-AAUBKEIU-DEYVUDCs.js} +1 -1
  6. package/dist/client/assets/channel-gy-JdvF3.js +1 -0
  7. package/dist/client/assets/{chunk-2J33WTMH-DP-b68f-.js → chunk-2J33WTMH-DD50YIcA.js} +1 -1
  8. package/dist/client/assets/{chunk-3OPIFGDE-D4GWQbqc.js → chunk-3OPIFGDE-DLbwwbdv.js} +1 -1
  9. package/dist/client/assets/{chunk-5ZQYHXKU-HftAkT9j.js → chunk-5ZQYHXKU-BW-r3gXJ.js} +1 -1
  10. package/dist/client/assets/{chunk-727SXJPM-CvB0JQZP.js → chunk-727SXJPM-B5C_ENX7.js} +1 -1
  11. package/dist/client/assets/{chunk-AQP2D5EJ-DSB28EAb.js → chunk-AQP2D5EJ-BUT3wmDg.js} +1 -1
  12. package/dist/client/assets/{chunk-CSCIHK7Q-DuSgBocb.js → chunk-CSCIHK7Q-BO7wWVh_.js} +1 -1
  13. package/dist/client/assets/{chunk-KSCS5N6A-cm_ppJ7v.js → chunk-KSCS5N6A-DUNFfKB2.js} +1 -1
  14. package/dist/client/assets/{chunk-L5ZTLDWV-BzDHIV9v.js → chunk-L5ZTLDWV-SYJQkU8_.js} +1 -1
  15. package/dist/client/assets/{chunk-LZXEDZCA-BruMwvko.js → chunk-LZXEDZCA-DSVJIylK.js} +2 -2
  16. package/dist/client/assets/{chunk-ND2GUHAM-DrnvodkL.js → chunk-ND2GUHAM-CxL5ue9A.js} +1 -1
  17. package/dist/client/assets/{chunk-NZK2D7GU-QVsa0LuF.js → chunk-NZK2D7GU-BwxeYeqR.js} +1 -1
  18. package/dist/client/assets/{chunk-O5CBEL6O-DJFhq2gr.js → chunk-O5CBEL6O-Covi8YcB.js} +1 -1
  19. package/dist/client/assets/{chunk-WU5MYG2G-ieLjE2uQ.js → chunk-WU5MYG2G-DlQoHSB3.js} +1 -1
  20. package/dist/client/assets/classDiagram-4FO5ZUOK-B1Xs78Ge.js +1 -0
  21. package/dist/client/assets/classDiagram-v2-Q7XG4LA2-BU_Soq1k.js +1 -0
  22. package/dist/client/assets/{dagre-BM42HDAG-CFRh82aT.js → dagre-BM42HDAG-CuNExSrh.js} +1 -1
  23. package/dist/client/assets/{diagram-2AECGRRQ-CPk1WJvx.js → diagram-2AECGRRQ-Bk7EYe6Z.js} +1 -1
  24. package/dist/client/assets/{diagram-5GNKFQAL-DHMJ735r.js → diagram-5GNKFQAL-koHJCfAk.js} +1 -1
  25. package/dist/client/assets/{diagram-KO2AKTUF-Cq9d2BWb.js → diagram-KO2AKTUF-DI1Ecx54.js} +1 -1
  26. package/dist/client/assets/{diagram-LMA3HP47-CgWJTR_b.js → diagram-LMA3HP47-CmFk1MNf.js} +1 -1
  27. package/dist/client/assets/{diagram-OG6HWLK6-HoFYGw81.js → diagram-OG6HWLK6-YTiDn39n.js} +1 -1
  28. package/dist/client/assets/{erDiagram-TEJ5UH35-B7yuPZck.js → erDiagram-TEJ5UH35-DyWU43DX.js} +1 -1
  29. package/dist/client/assets/{flowDiagram-I6XJVG4X-DaXXRHQj.js → flowDiagram-I6XJVG4X-DgbU15ZJ.js} +1 -1
  30. package/dist/client/assets/{ganttDiagram-6RSMTGT7-pKsFmtHB.js → ganttDiagram-6RSMTGT7-CT39xvdk.js} +1 -1
  31. package/dist/client/assets/{gitGraphDiagram-PVQCEYII-Dwx5kc1M.js → gitGraphDiagram-PVQCEYII-DfK_1xRj.js} +1 -1
  32. package/dist/client/assets/index-BCqJfUGA.css +2 -0
  33. package/dist/client/assets/index-C_XaTJyb.js +261 -0
  34. package/dist/client/assets/{infoDiagram-5YYISTIA-6Taupp-l.js → infoDiagram-5YYISTIA-C7z_7194.js} +1 -1
  35. package/dist/client/assets/{ishikawaDiagram-YF4QCWOH-BBKjiFtW.js → ishikawaDiagram-YF4QCWOH-DhJ7h17n.js} +1 -1
  36. package/dist/client/assets/{journeyDiagram-JHISSGLW-BQQ1EKJ3.js → journeyDiagram-JHISSGLW-CtkhoMAA.js} +1 -1
  37. package/dist/client/assets/{kanban-definition-UN3LZRKU-BvsroFzw.js → kanban-definition-UN3LZRKU-Bcas7cg9.js} +1 -1
  38. package/dist/client/assets/{line-C6BHsie7.js → line-WO_ReaCl.js} +1 -1
  39. package/dist/client/assets/{mermaid-parser.core-Cb4G8u3H.js → mermaid-parser.core-WmRuf19A.js} +1 -1
  40. package/dist/client/assets/{mermaid.core-NCO8Z4wR.js → mermaid.core-BMdju4xN.js} +3 -3
  41. package/dist/client/assets/{mindmap-definition-RKZ34NQL-DYzSjAq3.js → mindmap-definition-RKZ34NQL-DLxsUQmn.js} +1 -1
  42. package/dist/client/assets/{pieDiagram-4H26LBE5-CWMyFFuU.js → pieDiagram-4H26LBE5-D-oaWpEg.js} +1 -1
  43. package/dist/client/assets/{quadrantDiagram-W4KKPZXB-D1ifIucc.js → quadrantDiagram-W4KKPZXB-B9krq8I-.js} +1 -1
  44. package/dist/client/assets/{requirementDiagram-4Y6WPE33-DW68fC21.js → requirementDiagram-4Y6WPE33-m2VnH3vv.js} +1 -1
  45. package/dist/client/assets/{sankeyDiagram-5OEKKPKP-DnnV47BL.js → sankeyDiagram-5OEKKPKP-bCF9Ot9-.js} +1 -1
  46. package/dist/client/assets/{sequenceDiagram-3UESZ5HK-Dw-9ZBU_.js → sequenceDiagram-3UESZ5HK-C-4zDJcn.js} +1 -1
  47. package/dist/client/assets/{stateDiagram-AJRCARHV-C7dkf8nk.js → stateDiagram-AJRCARHV-CksbJK59.js} +1 -1
  48. package/dist/client/assets/stateDiagram-v2-BHNVJYJU-BvbN4CmJ.js +1 -0
  49. package/dist/client/assets/{timeline-definition-PNZ67QCA-BmVfK_16.js → timeline-definition-PNZ67QCA-DDxpdMfo.js} +1 -1
  50. package/dist/client/assets/{vennDiagram-CIIHVFJN-B9j3QbmD.js → vennDiagram-CIIHVFJN-BfQM7_zD.js} +1 -1
  51. package/dist/client/assets/{wardleyDiagram-YWT4CUSO-C520auMy.js → wardleyDiagram-YWT4CUSO-CuixXlAX.js} +1 -1
  52. package/dist/client/assets/{xychartDiagram-2RQKCTM6-CPqUJuqs.js → xychartDiagram-2RQKCTM6-53YB-tf6.js} +1 -1
  53. package/dist/client/index.html +2 -2
  54. package/dist/server/db.js +3 -0
  55. package/dist/server/db.js.map +1 -1
  56. package/dist/server/index.js +16 -0
  57. package/dist/server/index.js.map +1 -1
  58. package/dist/server/lib/agent.js +853 -161
  59. package/dist/server/lib/agent.js.map +1 -1
  60. package/dist/server/lib/agentRuns.js +22 -0
  61. package/dist/server/lib/agentRuns.js.map +1 -1
  62. package/dist/server/lib/chatMessages.js +24 -8
  63. package/dist/server/lib/chatMessages.js.map +1 -1
  64. package/dist/server/lib/genericToolCall.js +151 -0
  65. package/dist/server/lib/genericToolCall.js.map +1 -0
  66. package/dist/server/lib/mobileAccess.js +45 -2
  67. package/dist/server/lib/mobileAccess.js.map +1 -1
  68. package/dist/server/lib/models.js +89 -36
  69. package/dist/server/lib/models.js.map +1 -1
  70. package/dist/server/lib/opencodeTaskSessions.js +216 -0
  71. package/dist/server/lib/opencodeTaskSessions.js.map +1 -0
  72. package/dist/server/lib/promptSkills.js +5 -0
  73. package/dist/server/lib/promptSkills.js.map +1 -1
  74. package/dist/server/lib/specMessages.js +9 -6
  75. package/dist/server/lib/specMessages.js.map +1 -1
  76. package/dist/server/lib/ticketMessages.js +22 -7
  77. package/dist/server/lib/ticketMessages.js.map +1 -1
  78. package/dist/server/lib/transcriptRuns.js +85 -0
  79. package/dist/server/lib/transcriptRuns.js.map +1 -0
  80. package/dist/server/workers/build.js +12 -2
  81. package/dist/server/workers/build.js.map +1 -1
  82. package/dist/server/workers/chat.js +15 -1
  83. package/dist/server/workers/chat.js.map +1 -1
  84. package/dist/server/workers/followUp.js +12 -2
  85. package/dist/server/workers/followUp.js.map +1 -1
  86. package/dist/server/workers/plan.js +346 -60
  87. package/dist/server/workers/plan.js.map +1 -1
  88. package/dist/server/workers/planFollowUp.js +27 -7
  89. package/dist/server/workers/planFollowUp.js.map +1 -1
  90. package/dist/server/workers/review.js +50 -9
  91. package/dist/server/workers/review.js.map +1 -1
  92. package/dist/server/workers/specSession.js +32 -1
  93. package/dist/server/workers/specSession.js.map +1 -1
  94. package/package.json +1 -1
  95. package/dist/client/assets/channel-D7PWuiXU.js +0 -1
  96. package/dist/client/assets/classDiagram-4FO5ZUOK-BNIT931s.js +0 -1
  97. package/dist/client/assets/classDiagram-v2-Q7XG4LA2-TRtc0GGS.js +0 -1
  98. package/dist/client/assets/index-5Eun897r.js +0 -260
  99. package/dist/client/assets/index-CWWEw76C.css +0 -2
  100. package/dist/client/assets/stateDiagram-v2-BHNVJYJU-BFmoroJA.js +0 -1
@@ -5,14 +5,16 @@ import db from '../db.js';
5
5
  import config from '../lib/config.js';
6
6
  import { JiraApiError } from '../lib/integrations/jira.js';
7
7
  import { GitHubApiError } from '../lib/integrations/github.js';
8
+ import { formatGenericToolCall } from '../lib/genericToolCall.js';
8
9
  import { PlanningReferenceError, resolveExternalPlanningDescription } from '../lib/integrations/planning.js';
9
10
  import { resolveExistingWorktreePath } from '../lib/git.js';
10
11
  import { normalizeModelId } from '../lib/models.js';
12
+ import { parseOpencodeTaskEnvelope } from '../lib/opencodeTaskSessions.js';
11
13
  import { CLAUDE_NO_PLAN_FILE_DIRECTIVE, PLAN_MODE_QUESTION_TOOL_NAME } from '../lib/planModeQuestions.js';
12
14
  import { persistProjectMemorySuggestions } from '../lib/projectMemory.js';
13
15
  import { getProjectMemoryPromptBlock } from '../lib/projectMemoryPrompt.js';
14
16
  import { normalizeProjectMemorySuggestions, PROJECT_MEMORY_SUGGESTION_COMMON_RULE_LINES } from '../lib/projectMemorySuggestions.js';
15
- import { prependSkillPromptBlock } from '../lib/promptSkills.js';
17
+ import { buildPlanningSubagentGuidancePromptBlock, prependSkillPromptBlock } from '../lib/promptSkills.js';
16
18
  import { registerBundleActivity, unregisterBundleActivity } from '../lib/bundleActivity.js';
17
19
  import { enqueue, isRunning } from '../lib/queue.js';
18
20
  import { getEffectiveProject, getProjectById, listLinkedProjects } from '../lib/projects.js';
@@ -21,7 +23,36 @@ import { IMPLEMENTATION_BODY_DEDUPLICATION_RULES, IMPLEMENTATION_DETAIL_RULES, I
21
23
  import { beginTicketThinkingRun, bindTicketThinkingMessage } from '../lib/thinkingState.js';
22
24
  import { createTicketMessage, upsertTicketDescriptionMessage } from '../lib/ticketMessages.js';
23
25
  import { getTicket, resolveTicketBranch, resolveTicketTool, resolveTicketWorkerKey } from '../lib/tickets.js';
24
- import { storeTicketUsageSnapshot, storeTicketUsageSnapshotDirect, mergeUsageFromJsonLine, EMPTY_CHAT_USAGE_SNAPSHOT } from '../lib/agent.js';
26
+ import { createOpencodeSubagentMonitor, createSubagentRunTracker, formatAgentErrorForTranscript, getAgentErrorTranscriptRuns, storeTicketUsageSnapshot, storeTicketUsageSnapshotDirect, mergeUsageFromJsonLine, EMPTY_CHAT_USAGE_SNAPSHOT } from '../lib/agent.js';
27
+ import { clearAndEmitAgentRunsForParent } from '../lib/agentRuns.js';
28
+ const createPlannerRecoveryTracker = () => {
29
+ let sawError = false;
30
+ let latestEvent = null;
31
+ let finalAnswerFragments = [];
32
+ return {
33
+ consume: (events) => {
34
+ let shouldResetResponse = false;
35
+ for (const event of events ?? []) {
36
+ latestEvent = event.type;
37
+ if (event.type === 'error') {
38
+ sawError = true;
39
+ finalAnswerFragments = [];
40
+ shouldResetResponse = true;
41
+ continue;
42
+ }
43
+ if (sawError) {
44
+ finalAnswerFragments.push(event.text);
45
+ }
46
+ }
47
+ return shouldResetResponse;
48
+ },
49
+ hasRecoveredFinalAnswer: () => (sawError
50
+ && latestEvent === 'final_answer'
51
+ && finalAnswerFragments.length > 0),
52
+ hasUnrecoveredError: () => latestEvent === 'error',
53
+ getRecoveredResponse: () => finalAnswerFragments.join('\n\n'),
54
+ };
55
+ };
25
56
  const extractLeadingJsonObject = (line) => {
26
57
  if (!line.startsWith('{')) {
27
58
  return null;
@@ -63,6 +94,43 @@ const extractLeadingJsonObject = (line) => {
63
94
  return null;
64
95
  };
65
96
  const normalizeToolName = (name) => name.toLowerCase().replace(/[^a-z0-9]/g, '');
97
+ const isPlannerSubagentTool = (name) => {
98
+ const normalized = normalizeToolName(name);
99
+ return normalized === 'task' || normalized === 'subagent' || normalized === 'agent';
100
+ };
101
+ const getPlannerSubagentDescription = (input, fallback) => {
102
+ if (input) {
103
+ for (const key of ['description', 'title', 'task', 'subagent_type', 'prompt']) {
104
+ const value = input[key];
105
+ if (typeof value === 'string' && value.trim()) {
106
+ const firstLine = value.trim().split('\n')[0] ?? '';
107
+ return firstLine.length > 120 ? `${firstLine.slice(0, 117)}...` : firstLine;
108
+ }
109
+ }
110
+ }
111
+ return fallback;
112
+ };
113
+ const stringifyPlannerToolResult = (content) => {
114
+ if (typeof content === 'string') {
115
+ return content.trim() || null;
116
+ }
117
+ if (Array.isArray(content)) {
118
+ const text = content
119
+ .map(item => item && typeof item === 'object' && typeof item.text === 'string'
120
+ ? item.text.trim()
121
+ : '')
122
+ .filter(Boolean)
123
+ .join('\n\n');
124
+ return text || null;
125
+ }
126
+ return content === null || content === undefined ? null : JSON.stringify(content);
127
+ };
128
+ const emptyParsedPlannerStreamLine = () => ({
129
+ logFragment: null,
130
+ responseFragment: null,
131
+ subagentEvents: [],
132
+ providerSessionId: null,
133
+ });
66
134
  const toPlannerInputPath = (input) => {
67
135
  if (typeof input?.filePath === 'string') {
68
136
  return input.filePath.trim();
@@ -117,26 +185,6 @@ const formatPlannerWriteToolCall = (input) => {
117
185
  lines.push('[[/archon-apply-patch]]');
118
186
  return lines.join('\n');
119
187
  };
120
- const formatPlannerGenericToolCall = (name, input) => {
121
- const explicitVal = {
122
- Read: input?.filePath ?? input?.file_path,
123
- Bash: input?.command,
124
- Glob: input?.pattern,
125
- Grep: input?.pattern,
126
- WebFetch: input?.url,
127
- WebSearch: input?.query,
128
- Task: input?.description,
129
- NotebookEdit: input?.notebook_path,
130
- }[name];
131
- const value = typeof explicitVal === 'string' && explicitVal.length > 0
132
- ? explicitVal
133
- : Object.values(input ?? {}).find(entry => typeof entry === 'string');
134
- if (typeof value !== 'string' || value.length === 0) {
135
- return `_${name}_`;
136
- }
137
- const display = value.length > 100 ? value.slice(0, 100) + '…' : value;
138
- return `_${name}_ \`${display}\``;
139
- };
140
188
  const formatPlannerToolCall = (name, input) => {
141
189
  const normalized = normalizeToolName(name);
142
190
  if (normalized === 'edit') {
@@ -144,32 +192,46 @@ const formatPlannerToolCall = (name, input) => {
144
192
  if (formatted !== null) {
145
193
  return formatted;
146
194
  }
147
- return formatPlannerGenericToolCall(name, input);
195
+ return formatGenericToolCall(name, input);
148
196
  }
149
197
  if (normalized === 'write') {
150
198
  const formatted = formatPlannerWriteToolCall(input);
151
199
  if (formatted !== null) {
152
200
  return formatted;
153
201
  }
154
- return formatPlannerGenericToolCall(name, input);
202
+ return formatGenericToolCall(name, input);
155
203
  }
156
- return formatPlannerGenericToolCall(name, input);
204
+ return formatGenericToolCall(name, input);
157
205
  };
158
206
  const parseClaudePlannerStreamJsonLine = (line) => {
159
207
  const trimmedLine = line.trim();
160
208
  if (!trimmedLine) {
161
- return { logFragment: null, responseFragment: null };
209
+ return emptyParsedPlannerStreamLine();
162
210
  }
163
211
  let event;
164
212
  try {
165
213
  event = JSON.parse(trimmedLine);
166
214
  }
167
215
  catch {
168
- return { logFragment: trimmedLine, responseFragment: trimmedLine };
216
+ const envelope = parseOpencodeTaskEnvelope(trimmedLine);
217
+ if (envelope) {
218
+ return {
219
+ ...emptyParsedPlannerStreamLine(),
220
+ subagentEvents: [{
221
+ kind: 'complete',
222
+ callId: null,
223
+ sessionId: envelope.childSessionId,
224
+ status: envelope.state?.toLowerCase() === 'error' || envelope.state?.toLowerCase() === 'failed' ? 'error' : 'done',
225
+ ...(envelope.result ? { text: envelope.result } : {}),
226
+ }],
227
+ };
228
+ }
229
+ return { ...emptyParsedPlannerStreamLine(), logFragment: trimmedLine, responseFragment: trimmedLine };
169
230
  }
170
231
  if (event.type === 'assistant' && Array.isArray(event.message?.content)) {
171
232
  const logParts = [];
172
233
  const responseParts = [];
234
+ const subagentEvents = [];
173
235
  for (const block of event.message.content) {
174
236
  if (block.type === 'text' && block.text) {
175
237
  const text = block.text.trim();
@@ -180,15 +242,61 @@ const parseClaudePlannerStreamJsonLine = (line) => {
180
242
  continue;
181
243
  }
182
244
  if (block.type === 'tool_use' && block.name) {
245
+ if (isPlannerSubagentTool(block.name)) {
246
+ const description = getPlannerSubagentDescription(block.input, block.name);
247
+ logParts.push(`Subagent: ${description}`);
248
+ subagentEvents.push({
249
+ kind: 'start',
250
+ callId: block.id ?? null,
251
+ label: `Subagent: ${description}`,
252
+ });
253
+ continue;
254
+ }
183
255
  logParts.push(formatPlannerToolCall(block.name, block.input));
184
256
  }
185
257
  }
186
258
  return {
187
259
  logFragment: logParts.length > 0 ? logParts.join('\n\n') : null,
188
260
  responseFragment: responseParts.length > 0 ? responseParts.join('\n\n') : null,
261
+ subagentEvents,
262
+ providerSessionId: null,
189
263
  };
190
264
  }
191
- return { logFragment: null, responseFragment: null };
265
+ if (event.type === 'stream_event' && event.event?.type === 'content_block_start') {
266
+ const block = event.event.content_block;
267
+ if (block?.type === 'tool_use' && block.name && isPlannerSubagentTool(block.name)) {
268
+ const description = getPlannerSubagentDescription(block.input, block.name);
269
+ return {
270
+ ...emptyParsedPlannerStreamLine(),
271
+ logFragment: `Subagent: ${description}`,
272
+ subagentEvents: [{
273
+ kind: 'start',
274
+ callId: block.id ?? null,
275
+ label: `Subagent: ${description}`,
276
+ }],
277
+ };
278
+ }
279
+ }
280
+ if (event.type === 'user' && Array.isArray(event.message?.content)) {
281
+ const subagentEvents = event.message.content.flatMap(block => {
282
+ if (block.type !== 'tool_result' || !block.tool_use_id) {
283
+ return [];
284
+ }
285
+ const rawText = stringifyPlannerToolResult(block.content);
286
+ const envelope = parseOpencodeTaskEnvelope(rawText);
287
+ const text = envelope?.result ?? rawText;
288
+ const state = envelope?.state?.toLowerCase();
289
+ return [{
290
+ kind: 'complete',
291
+ callId: block.tool_use_id,
292
+ status: block.is_error || state === 'error' || state === 'failed' ? 'error' : 'done',
293
+ ...(text ? { text } : {}),
294
+ ...(envelope ? { sessionId: envelope.childSessionId } : {}),
295
+ }];
296
+ });
297
+ return { ...emptyParsedPlannerStreamLine(), subagentEvents };
298
+ }
299
+ return emptyParsedPlannerStreamLine();
192
300
  };
193
301
  const formatPlannerOpencodeToolUse = (event) => {
194
302
  const toolName = event.part?.tool;
@@ -204,25 +312,48 @@ const formatPlannerStepTitle = (event) => {
204
312
  const parseOpencodePlannerJsonLine = (line) => {
205
313
  const trimmedLine = line.trim();
206
314
  if (!trimmedLine) {
207
- return { logFragment: null, responseFragment: null };
315
+ return emptyParsedPlannerStreamLine();
208
316
  }
209
317
  const jsonLine = extractLeadingJsonObject(trimmedLine);
210
318
  if (!jsonLine) {
211
- return { logFragment: trimmedLine, responseFragment: trimmedLine };
319
+ const envelope = parseOpencodeTaskEnvelope(trimmedLine);
320
+ if (envelope) {
321
+ return {
322
+ ...emptyParsedPlannerStreamLine(),
323
+ subagentEvents: [{
324
+ kind: 'complete',
325
+ callId: null,
326
+ sessionId: envelope.childSessionId,
327
+ status: envelope.state?.toLowerCase() === 'error' || envelope.state?.toLowerCase() === 'failed' ? 'error' : 'done',
328
+ ...(envelope.result ? { text: envelope.result } : {}),
329
+ }],
330
+ };
331
+ }
332
+ return { ...emptyParsedPlannerStreamLine(), logFragment: trimmedLine, responseFragment: trimmedLine };
212
333
  }
213
334
  let event;
214
335
  try {
215
336
  event = JSON.parse(jsonLine);
216
337
  }
217
338
  catch {
218
- return { logFragment: trimmedLine, responseFragment: trimmedLine };
339
+ return { ...emptyParsedPlannerStreamLine(), logFragment: trimmedLine, responseFragment: trimmedLine };
219
340
  }
341
+ const providerSessionId = event.sessionID?.trim()
342
+ || event.sessionId?.trim()
343
+ || event.part?.sessionID?.trim()
344
+ || event.part?.sessionId?.trim()
345
+ || null;
220
346
  if (event.type === 'text' && event.part?.type === 'text' && event.part.text) {
221
347
  const text = event.part.text.trim();
222
348
  const phase = event.part.metadata?.openai?.phase;
223
349
  return {
224
350
  logFragment: text || null,
225
351
  responseFragment: text && (!phase || phase === 'final_answer') ? text : null,
352
+ subagentEvents: [],
353
+ providerSessionId,
354
+ recoveryEvents: text && phase === 'final_answer'
355
+ ? [{ type: 'final_answer', text }]
356
+ : [],
226
357
  };
227
358
  }
228
359
  if (event.type === 'reasoning' && event.part?.type === 'reasoning') {
@@ -230,21 +361,88 @@ const parseOpencodePlannerJsonLine = (line) => {
230
361
  return {
231
362
  logFragment: text || null,
232
363
  responseFragment: null,
364
+ subagentEvents: [],
365
+ providerSessionId,
233
366
  };
234
367
  }
235
- if (event.type === 'tool_use') {
368
+ if (event.type === 'tool' || event.type === 'tool_use' || event.type === 'tool_result') {
369
+ const isToolInvocation = event.type === 'tool' || event.type === 'tool_use';
370
+ const toolName = event.part?.tool;
371
+ if (toolName && isPlannerSubagentTool(toolName)) {
372
+ const description = getPlannerSubagentDescription(event.part?.state?.input, toolName);
373
+ const rawOutput = event.part?.state?.output?.trim();
374
+ const envelope = parseOpencodeTaskEnvelope(rawOutput);
375
+ const output = envelope?.result ?? rawOutput;
376
+ const taskStatus = event.part?.state?.status?.toLowerCase() ?? envelope?.state?.toLowerCase();
377
+ const childSessionId = envelope?.childSessionId
378
+ ?? (typeof event.part?.state?.metadata?.sessionID === 'string' ? event.part.state.metadata.sessionID : null)
379
+ ?? (typeof event.part?.state?.metadata?.sessionId === 'string' ? event.part.state.metadata.sessionId : null);
380
+ return {
381
+ logFragment: isToolInvocation ? `Subagent: ${description}` : null,
382
+ responseFragment: null,
383
+ subagentEvents: [
384
+ ...(isToolInvocation
385
+ ? [{
386
+ kind: 'start',
387
+ callId: event.part?.callID ?? null,
388
+ label: `Subagent: ${description}`,
389
+ sessionId: childSessionId,
390
+ }]
391
+ : []),
392
+ ...(output
393
+ ? [{
394
+ kind: taskStatus === 'running' || taskStatus === 'pending' ? 'fragment' : 'complete',
395
+ callId: event.part?.callID ?? null,
396
+ text: output,
397
+ sessionId: childSessionId,
398
+ status: taskStatus === 'error' || taskStatus === 'failed' ? 'error' : 'done',
399
+ }]
400
+ : []),
401
+ ],
402
+ providerSessionId,
403
+ };
404
+ }
405
+ if (!isToolInvocation) {
406
+ return { ...emptyParsedPlannerStreamLine(), providerSessionId };
407
+ }
236
408
  return {
237
409
  logFragment: formatPlannerOpencodeToolUse(event),
238
410
  responseFragment: null,
411
+ subagentEvents: [],
412
+ providerSessionId,
239
413
  };
240
414
  }
241
415
  if (event.type === 'step_start') {
242
416
  return {
243
417
  logFragment: formatPlannerStepTitle(event),
244
418
  responseFragment: null,
419
+ subagentEvents: [],
420
+ providerSessionId,
245
421
  };
246
422
  }
247
- return { logFragment: null, responseFragment: null };
423
+ if (event.type === 'error') {
424
+ const errorText = typeof event.error === 'string'
425
+ ? event.error.trim()
426
+ : event.error?.data?.message?.trim()
427
+ || event.error?.message?.trim()
428
+ || (event.error ? JSON.stringify(event.error) : '');
429
+ const text = event.part?.text?.trim() || errorText;
430
+ return {
431
+ logFragment: text ? `**Error:** ${text}` : null,
432
+ responseFragment: null,
433
+ subagentEvents: [],
434
+ providerSessionId,
435
+ recoveryEvents: [{ type: 'error' }],
436
+ };
437
+ }
438
+ return { ...emptyParsedPlannerStreamLine(), providerSessionId };
439
+ };
440
+ const attachPlannerTranscriptRuns = (error, transcriptRuns) => {
441
+ const resolvedError = (error instanceof Error ? error : new Error(String(error)));
442
+ if (transcriptRuns) {
443
+ resolvedError.transcriptRuns = transcriptRuns;
444
+ }
445
+ return resolvedError;
248
446
  };
249
447
  const stripSystemReminderBlocks = (output) => {
250
448
  return output.replace(/<system-reminder>[\s\S]*?<\/system-reminder>/g, '').trim();
@@ -439,6 +637,7 @@ const buildSingleTicketPrompt = (title, description, linkedResearchContext = '',
439
637
 
440
638
  This is a planning-only task.
441
639
  - You may inspect the repository and use read-only tools if helpful.
640
+ ${buildPlanningSubagentGuidancePromptBlock()}
442
641
  - Do not edit files or make changes.
443
642
  - Return a short natural-language planning update first, then the final JSON object on the last line.
444
643
 
@@ -497,6 +696,7 @@ This is pass 1 of 2. Your job is to decide the best ticket split and dependency
497
696
 
498
697
  This is a planning-only task.
499
698
  - You may inspect the repository and use read-only tools if helpful.
699
+ ${buildPlanningSubagentGuidancePromptBlock()}
500
700
  - Do not edit files or make changes.
501
701
  - Return only the final JSON object.
502
702
 
@@ -556,6 +756,7 @@ This is pass 2 of 2. The ticket split and dependency graph are already decided.
556
756
 
557
757
  This is a planning-only task.
558
758
  - You may inspect the repository and use read-only tools if helpful.
759
+ ${buildPlanningSubagentGuidancePromptBlock()}
559
760
  - Do not edit files or make changes.
560
761
  - Return only the final JSON object.
561
762
 
@@ -740,11 +941,19 @@ export const runPlannerPrompt = (tool, model, variant, prompt, cwd, options = {}
740
941
  let resolvedSessionId = sessionTracking?.resumeSessionId ?? null;
741
942
  let lastStdoutAt = Date.now();
742
943
  let watchdogTimer = null;
944
+ const subagentTracker = options.agentRunParent
945
+ ? createSubagentRunTracker({
946
+ ...options.agentRunParent,
947
+ io: options.agentRunParent.io,
948
+ })
949
+ : null;
950
+ let opencodeSubagentMonitor = null;
743
951
  const resolveOnce = (value) => {
744
952
  if (settled) {
745
953
  return;
746
954
  }
747
955
  settled = true;
956
+ opencodeSubagentMonitor?.stop();
748
957
  resolve(value);
749
958
  };
750
959
  const rejectOnce = (error) => {
@@ -752,6 +961,7 @@ export const runPlannerPrompt = (tool, model, variant, prompt, cwd, options = {}
752
961
  return;
753
962
  }
754
963
  settled = true;
964
+ opencodeSubagentMonitor?.stop();
755
965
  reject(error);
756
966
  };
757
967
  const clearWatchdogTimer = () => {
@@ -779,6 +989,9 @@ export const runPlannerPrompt = (tool, model, variant, prompt, cwd, options = {}
779
989
  }
780
990
  return hasResolvedSessionId;
781
991
  };
992
+ if (tool === 'opencode' && (sessionTracking || subagentTracker)) {
993
+ spawnTimestamp = Date.now();
994
+ }
782
995
  const child = spawn(cmd, args, {
783
996
  stdio: ['ignore', 'pipe', 'pipe'],
784
997
  env: { ...process.env, ...(cwd ? { PWD: cwd } : {}) },
@@ -789,7 +1002,6 @@ export const runPlannerPrompt = (tool, model, variant, prompt, cwd, options = {}
789
1002
  updatePlanningSessionId(sessionTracking.ticketId, sessionId, sessionTracking.io);
790
1003
  }
791
1004
  if (sessionTracking && tool === 'opencode') {
792
- spawnTimestamp = Date.now();
793
1005
  if (!sessionTracking.resumeSessionId) {
794
1006
  sessionLookupTimer = setTimeout(() => {
795
1007
  attemptOpencodeSessionLookup();
@@ -797,10 +1009,25 @@ export const runPlannerPrompt = (tool, model, variant, prompt, cwd, options = {}
797
1009
  }, 1500);
798
1010
  }
799
1011
  }
1012
+ if (subagentTracker && tool === 'opencode') {
1013
+ spawnTimestamp = spawnTimestamp ?? Date.now();
1014
+ opencodeSubagentMonitor = createOpencodeSubagentMonitor({
1015
+ initialParentSessionId: sessionTracking?.resumeSessionId ?? null,
1016
+ spawnTimestamp,
1017
+ cwd: cwd ?? process.cwd(),
1018
+ tracker: subagentTracker,
1019
+ discoverParentSession: false,
1020
+ onParentSessionId: discoveredSessionId => {
1021
+ resolvedSessionId = resolvedSessionId ?? discoveredSessionId;
1022
+ },
1023
+ });
1024
+ opencodeSubagentMonitor.start();
1025
+ }
800
1026
  const stdoutChunks = [];
801
1027
  const stderrChunks = [];
802
1028
  const logLines = [];
803
1029
  const responseLines = [];
1030
+ const recoveryTracker = createPlannerRecoveryTracker();
804
1031
  let usageSnapshot = EMPTY_CHAT_USAGE_SNAPSHOT;
805
1032
  let remainder = '';
806
1033
  let insideHiddenBlock = false;
@@ -839,9 +1066,18 @@ export const runPlannerPrompt = (tool, model, variant, prompt, cwd, options = {}
839
1066
  catch { /* best-effort timing */ }
840
1067
  }
841
1068
  }
842
- const { logFragment, responseFragment } = tool === 'claude'
1069
+ const parsedLine = tool === 'claude'
843
1070
  ? parseClaudePlannerStreamJsonLine(line)
844
1071
  : parseOpencodePlannerJsonLine(line);
1072
+ const { logFragment, responseFragment, subagentEvents, providerSessionId } = parsedLine;
1073
+ if (tool === 'opencode' && recoveryTracker.consume(parsedLine.recoveryEvents)) {
1074
+ responseLines.length = 0;
1075
+ firstFinalAnswerTime = null;
1076
+ }
1077
+ subagentTracker?.handle(subagentEvents);
1078
+ if (providerSessionId) {
1079
+ opencodeSubagentMonitor?.setParentSessionId(providerSessionId);
1080
+ }
845
1081
  if (isHiddenOpenTag.test(line)) {
846
1082
  insideHiddenBlock = true;
847
1083
  }
@@ -863,6 +1099,7 @@ export const runPlannerPrompt = (tool, model, variant, prompt, cwd, options = {}
863
1099
  child.stderr?.on('data', (chunk) => stderrChunks.push(chunk));
864
1100
  child.on('close', code => {
865
1101
  clearWatchdogTimer();
1102
+ opencodeSubagentMonitor?.refresh();
866
1103
  const resolvedSessionOnClose = attemptOpencodeSessionLookup();
867
1104
  if (sessionLookupTimer && (resolvedSessionOnClose || hasResolvedSessionId)) {
868
1105
  clearTimeout(sessionLookupTimer);
@@ -872,9 +1109,18 @@ export const runPlannerPrompt = (tool, model, variant, prompt, cwd, options = {}
872
1109
  if (tool === 'claude') {
873
1110
  usageSnapshot = mergeUsageFromJsonLine(usageSnapshot, remainder);
874
1111
  }
875
- const { logFragment, responseFragment } = tool === 'claude'
1112
+ const parsedLine = tool === 'claude'
876
1113
  ? parseClaudePlannerStreamJsonLine(remainder)
877
1114
  : parseOpencodePlannerJsonLine(remainder);
1115
+ const { logFragment, responseFragment, subagentEvents, providerSessionId } = parsedLine;
1116
+ if (tool === 'opencode' && recoveryTracker.consume(parsedLine.recoveryEvents)) {
1117
+ responseLines.length = 0;
1118
+ firstFinalAnswerTime = null;
1119
+ }
1120
+ subagentTracker?.handle(subagentEvents);
1121
+ if (providerSessionId) {
1122
+ opencodeSubagentMonitor?.setParentSessionId(providerSessionId);
1123
+ }
878
1124
  if (logFragment !== null) {
879
1125
  logLines.push(logFragment);
880
1126
  }
@@ -884,9 +1130,12 @@ export const runPlannerPrompt = (tool, model, variant, prompt, cwd, options = {}
884
1130
  }
885
1131
  }
886
1132
  const rawOutput = Buffer.concat(stdoutChunks).toString().trim();
887
- const output = tool === 'claude' && !rawOutput.startsWith('{')
888
- ? rawOutput
889
- : responseLines.join('\n\n').trim() || rawOutput;
1133
+ const recoveredResponse = recoveryTracker.getRecoveredResponse();
1134
+ const output = tool === 'opencode' && recoveryTracker.hasRecoveredFinalAnswer()
1135
+ ? recoveredResponse
1136
+ : tool === 'claude' && !rawOutput.startsWith('{')
1137
+ ? rawOutput
1138
+ : responseLines.join('\n\n').trim() || rawOutput;
890
1139
  const errorOutput = Buffer.concat(stderrChunks).toString().trim();
891
1140
  const timing = (() => {
892
1141
  const totalMs = Date.now() - startTime;
@@ -898,17 +1147,22 @@ export const runPlannerPrompt = (tool, model, variant, prompt, cwd, options = {}
898
1147
  return { totalMs };
899
1148
  })();
900
1149
  if (hardTimeoutForced) {
901
- rejectOnce(new Error(output || errorOutput || `Planner exceeded the ${PLANNER_HARD_TIMEOUT_MS} ms hard timeout.`));
1150
+ const transcriptRuns = subagentTracker?.finalize('error', logLines.join('\n\n') || errorOutput || null, output || null) ?? null;
1151
+ rejectOnce(attachPlannerTranscriptRuns(new Error(output || errorOutput || `Planner exceeded the ${PLANNER_HARD_TIMEOUT_MS} ms hard timeout.`), transcriptRuns));
902
1152
  return;
903
1153
  }
904
- if ((code === 0 || completionForced) && output) {
905
- resolveOnce({ output, responseTiming: timing, usageSnapshot });
1154
+ if (!recoveryTracker.hasUnrecoveredError()
1155
+ && (code === 0 || completionForced || recoveryTracker.hasRecoveredFinalAnswer())
1156
+ && output) {
1157
+ const transcriptRuns = subagentTracker?.finalize('done', logLines.join('\n\n') || null, output) ?? null;
1158
+ resolveOnce({ output, responseTiming: timing, usageSnapshot, transcriptRuns });
906
1159
  return;
907
1160
  }
908
1161
  const fallbackError = code === 0
909
1162
  ? 'Planner did not return a usable plan.'
910
1163
  : `Process exited with code ${code}`;
911
- rejectOnce(new Error(output || errorOutput || fallbackError));
1164
+ const transcriptRuns = subagentTracker?.finalize('error', logLines.join('\n\n') || errorOutput || null, output || null) ?? null;
1165
+ rejectOnce(attachPlannerTranscriptRuns(new Error(output || errorOutput || fallbackError), transcriptRuns));
912
1166
  });
913
1167
  child.on('error', err => {
914
1168
  clearWatchdogTimer();
@@ -916,7 +1170,8 @@ export const runPlannerPrompt = (tool, model, variant, prompt, cwd, options = {}
916
1170
  clearTimeout(sessionLookupTimer);
917
1171
  sessionLookupTimer = null;
918
1172
  }
919
- rejectOnce(err);
1173
+ const transcriptRuns = subagentTracker?.finalize('error', err.message, null) ?? null;
1174
+ rejectOnce(attachPlannerTranscriptRuns(err, transcriptRuns));
920
1175
  });
921
1176
  if (tool === 'opencode') {
922
1177
  const runWatchdog = () => {
@@ -1234,7 +1489,7 @@ export const generatePlanText = (title, description, options = {}) => {
1234
1489
  title,
1235
1490
  description,
1236
1491
  });
1237
- const { output: plannerOutput, responseTiming: plannerTiming, usageSnapshot } = await runPlannerPrompt(tool, options.model ?? null, options.variant ?? null, prependSkillPromptBlock(buildSingleTicketPrompt(title, description, linkedResearchContext, projectMemoryBlock, Boolean(project?.memory_enabled)), options.skills ?? []), resolvePlannerCwd(project, options.branch), {
1492
+ const { output: plannerOutput, responseTiming: plannerTiming, usageSnapshot, transcriptRuns } = await runPlannerPrompt(tool, options.model ?? null, options.variant ?? null, prependSkillPromptBlock(buildSingleTicketPrompt(title, description, linkedResearchContext, projectMemoryBlock, Boolean(project?.memory_enabled)), options.skills ?? []), resolvePlannerCwd(project, options.branch), {
1238
1493
  onProgress: options.onProgress,
1239
1494
  ...(options.ticketId && options.io
1240
1495
  ? {
@@ -1243,24 +1498,35 @@ export const generatePlanText = (title, description, options = {}) => {
1243
1498
  io: options.io,
1244
1499
  resumeSessionId: options.planningSessionId,
1245
1500
  },
1501
+ agentRunParent: {
1502
+ parentType: 'ticket',
1503
+ parentId: options.ticketId,
1504
+ io: options.io,
1505
+ },
1246
1506
  }
1247
1507
  : {}),
1248
1508
  });
1249
- const expanded = extractTaggedJsonObjectWithLeadingText(plannerOutput, PLAN_OUTPUT_TAG)
1250
- ?? extractFirstJsonObjectWithLeadingText(plannerOutput);
1251
- const expandedTicket = expanded.parsed.tickets.find(ticket => ticket.key === 'main-ticket');
1252
- if (!expandedTicket?.description?.trim()) {
1253
- throw new Error('Planner did not return a usable plan.');
1509
+ try {
1510
+ const expanded = extractTaggedJsonObjectWithLeadingText(plannerOutput, PLAN_OUTPUT_TAG)
1511
+ ?? extractFirstJsonObjectWithLeadingText(plannerOutput);
1512
+ const expandedTicket = expanded.parsed.tickets.find(ticket => ticket.key === 'main-ticket');
1513
+ if (!expandedTicket?.description?.trim()) {
1514
+ throw new Error('Planner did not return a usable plan.');
1515
+ }
1516
+ return {
1517
+ ...normalizePlanningDraftTicket(expandedTicket),
1518
+ key: 'main-ticket',
1519
+ title: title.trim(),
1520
+ description: expandedTicket.description.trim(),
1521
+ response: expanded.before.trim(),
1522
+ responseTiming: plannerTiming,
1523
+ usageSnapshot,
1524
+ transcriptRuns,
1525
+ };
1526
+ }
1527
+ catch (error) {
1528
+ throw attachPlannerTranscriptRuns(error, transcriptRuns ?? null);
1254
1529
  }
1255
- return {
1256
- ...normalizePlanningDraftTicket(expandedTicket),
1257
- key: 'main-ticket',
1258
- title: title.trim(),
1259
- description: expandedTicket.description.trim(),
1260
- response: expanded.before.trim(),
1261
- responseTiming: plannerTiming,
1262
- usageSnapshot,
1263
- };
1264
1530
  })();
1265
1531
  };
1266
1532
  export const generatePlanningTicketOutline = async (description, options = {}) => {
@@ -1308,6 +1574,7 @@ export const startPlan = (ticket, io, options = {}) => {
1308
1574
  registerBundleActivity(ticket, 'plan');
1309
1575
  beginTicketThinkingRun(ticket.id);
1310
1576
  db.prepare('UPDATE tickets SET agent_status = ?, agent_log = NULL, streaming_response = NULL, updated_at = CURRENT_TIMESTAMP WHERE id = ?').run('running', ticket.id);
1577
+ clearAndEmitAgentRunsForParent(io, 'ticket', ticket.id);
1311
1578
  emitTicketUpdate(ticket.id, io);
1312
1579
  try {
1313
1580
  const project = getEffectiveProject(ticket);
@@ -1356,7 +1623,7 @@ export const startPlan = (ticket, io, options = {}) => {
1356
1623
  const assistantMessage = buildPlanningAssistantMessage({
1357
1624
  response: planned.response,
1358
1625
  openQuestions: planned.open_questions,
1359
- });
1626
+ }) || (planned.transcriptRuns?.length ? planned.description : '');
1360
1627
  if (assistantMessage) {
1361
1628
  const messageId = createTicketMessage({
1362
1629
  ticketId: ticket.id,
@@ -1364,14 +1631,19 @@ export const startPlan = (ticket, io, options = {}) => {
1364
1631
  kind: 'planning_response',
1365
1632
  content: assistantMessage,
1366
1633
  responseTiming: planned.responseTiming,
1634
+ transcriptRuns: planned.transcriptRuns,
1367
1635
  });
1368
1636
  if (messageId) {
1369
1637
  bindTicketThinkingMessage(ticket.id, messageId);
1370
1638
  }
1371
1639
  }
1640
+ if (planned.transcriptRuns) {
1641
+ clearAndEmitAgentRunsForParent(io, 'ticket', ticket.id);
1642
+ }
1372
1643
  emitTicketUpdate(ticket.id, io);
1373
1644
  }
1374
1645
  catch (err) {
1646
+ const transcriptRuns = getAgentErrorTranscriptRuns(err);
1375
1647
  const message = err instanceof PlanningReferenceError
1376
1648
  ? err.message
1377
1649
  : err instanceof JiraApiError
@@ -1381,6 +1653,20 @@ export const startPlan = (ticket, io, options = {}) => {
1381
1653
  : err instanceof Error
1382
1654
  ? err.message
1383
1655
  : String(err);
1656
+ const errorContent = formatAgentErrorForTranscript(err);
1657
+ let errorMessageId = null;
1658
+ if (errorContent) {
1659
+ errorMessageId = createTicketMessage({
1660
+ ticketId: ticket.id,
1661
+ role: 'assistant',
1662
+ kind: 'error',
1663
+ content: errorContent,
1664
+ transcriptRuns,
1665
+ });
1666
+ }
1667
+ if (transcriptRuns && errorMessageId) {
1668
+ clearAndEmitAgentRunsForParent(io, 'ticket', ticket.id);
1669
+ }
1384
1670
  db.prepare('UPDATE tickets SET agent_status = ?, agent_log = ?, streaming_response = NULL, updated_at = CURRENT_TIMESTAMP WHERE id = ?').run('error', message, ticket.id);
1385
1671
  emitTicketUpdate(ticket.id, io);
1386
1672
  }