@straiffi/archon 1.4.12 → 1.6.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 (130) hide show
  1. package/dist/client/assets/{GitHubActionsDialog-DcW7fUgz.js → GitHubActionsDialog-oMY61JXF.js} +1 -1
  2. package/dist/client/assets/{TestsDialog-xNEttQOx.js → TestsDialog-elElGi9L.js} +3 -4
  3. package/dist/client/assets/{architectureDiagram-3BPJPVTR-DHADo63Y.js → architectureDiagram-3BPJPVTR-BNcdEMha.js} +1 -1
  4. package/dist/client/assets/{blockDiagram-GPEHLZMM-D_LqGLuD.js → blockDiagram-GPEHLZMM-xHTKb0-z.js} +1 -1
  5. package/dist/client/assets/{c4Diagram-AAUBKEIU-mIqv7fCU.js → c4Diagram-AAUBKEIU-kMngbYRg.js} +1 -1
  6. package/dist/client/assets/channel-DAW1OrrC.js +1 -0
  7. package/dist/client/assets/{chunk-2J33WTMH-DP-b68f-.js → chunk-2J33WTMH-BVZnJcYa.js} +1 -1
  8. package/dist/client/assets/{chunk-3OPIFGDE-D4GWQbqc.js → chunk-3OPIFGDE-Dq_H_MkG.js} +1 -1
  9. package/dist/client/assets/{chunk-5ZQYHXKU-HftAkT9j.js → chunk-5ZQYHXKU-_TwHLQwE.js} +1 -1
  10. package/dist/client/assets/{chunk-727SXJPM-CvB0JQZP.js → chunk-727SXJPM-CKkS_93w.js} +1 -1
  11. package/dist/client/assets/{chunk-AQP2D5EJ-DSB28EAb.js → chunk-AQP2D5EJ-BHSSO0SM.js} +1 -1
  12. package/dist/client/assets/{chunk-CSCIHK7Q-DuSgBocb.js → chunk-CSCIHK7Q-CgzsiMAL.js} +1 -1
  13. package/dist/client/assets/{chunk-KSCS5N6A-cm_ppJ7v.js → chunk-KSCS5N6A-DM-rnicK.js} +1 -1
  14. package/dist/client/assets/{chunk-L5ZTLDWV-BzDHIV9v.js → chunk-L5ZTLDWV-CnMgoXZs.js} +1 -1
  15. package/dist/client/assets/{chunk-LZXEDZCA-BruMwvko.js → chunk-LZXEDZCA-CCr2z1iU.js} +2 -2
  16. package/dist/client/assets/{chunk-ND2GUHAM-DrnvodkL.js → chunk-ND2GUHAM-DtA6gjMn.js} +1 -1
  17. package/dist/client/assets/{chunk-NZK2D7GU-QVsa0LuF.js → chunk-NZK2D7GU-_8fmSSaf.js} +1 -1
  18. package/dist/client/assets/{chunk-O5CBEL6O-DJFhq2gr.js → chunk-O5CBEL6O-B84elqwU.js} +1 -1
  19. package/dist/client/assets/{chunk-WU5MYG2G-ieLjE2uQ.js → chunk-WU5MYG2G-DBvmvPPt.js} +1 -1
  20. package/dist/client/assets/classDiagram-4FO5ZUOK-BmFh-n6n.js +1 -0
  21. package/dist/client/assets/classDiagram-v2-Q7XG4LA2-tA1aAbh4.js +1 -0
  22. package/dist/client/assets/{dagre-BM42HDAG-CFRh82aT.js → dagre-BM42HDAG-C-QssoNU.js} +1 -1
  23. package/dist/client/assets/{diagram-2AECGRRQ-CPk1WJvx.js → diagram-2AECGRRQ-8RpG5DGb.js} +1 -1
  24. package/dist/client/assets/{diagram-5GNKFQAL-DHMJ735r.js → diagram-5GNKFQAL-Dk2raJWq.js} +1 -1
  25. package/dist/client/assets/{diagram-KO2AKTUF-Cq9d2BWb.js → diagram-KO2AKTUF-7iZuIIef.js} +1 -1
  26. package/dist/client/assets/{diagram-LMA3HP47-CgWJTR_b.js → diagram-LMA3HP47-JLaaaEE6.js} +1 -1
  27. package/dist/client/assets/{diagram-OG6HWLK6-HoFYGw81.js → diagram-OG6HWLK6-jeL3-Rd3.js} +1 -1
  28. package/dist/client/assets/{erDiagram-TEJ5UH35-B7yuPZck.js → erDiagram-TEJ5UH35-B6uFrMrQ.js} +1 -1
  29. package/dist/client/assets/{flowDiagram-I6XJVG4X-DaXXRHQj.js → flowDiagram-I6XJVG4X-BFWnhmx2.js} +1 -1
  30. package/dist/client/assets/{ganttDiagram-6RSMTGT7-pKsFmtHB.js → ganttDiagram-6RSMTGT7-nxON-689.js} +1 -1
  31. package/dist/client/assets/{gitGraphDiagram-PVQCEYII-Dwx5kc1M.js → gitGraphDiagram-PVQCEYII-DEAsC0GZ.js} +1 -1
  32. package/dist/client/assets/index-BzDXl4AF.js +265 -0
  33. package/dist/client/assets/index-DSap5tSX.css +2 -0
  34. package/dist/client/assets/{infoDiagram-5YYISTIA-6Taupp-l.js → infoDiagram-5YYISTIA-CAZHR2I8.js} +1 -1
  35. package/dist/client/assets/{ishikawaDiagram-YF4QCWOH-BBKjiFtW.js → ishikawaDiagram-YF4QCWOH-DEe4SNzl.js} +1 -1
  36. package/dist/client/assets/{journeyDiagram-JHISSGLW-BQQ1EKJ3.js → journeyDiagram-JHISSGLW-CG2kTAj9.js} +1 -1
  37. package/dist/client/assets/{kanban-definition-UN3LZRKU-BvsroFzw.js → kanban-definition-UN3LZRKU-DUWutAjA.js} +1 -1
  38. package/dist/client/assets/{line-C6BHsie7.js → line-B1SFN8mz.js} +1 -1
  39. package/dist/client/assets/{mermaid-parser.core-Cb4G8u3H.js → mermaid-parser.core-C-dyz_nb.js} +1 -1
  40. package/dist/client/assets/{mermaid.core-NCO8Z4wR.js → mermaid.core-nm4BMZuF.js} +3 -3
  41. package/dist/client/assets/{mindmap-definition-RKZ34NQL-DYzSjAq3.js → mindmap-definition-RKZ34NQL-DZEd-RCz.js} +1 -1
  42. package/dist/client/assets/{pieDiagram-4H26LBE5-CWMyFFuU.js → pieDiagram-4H26LBE5-WXxrzUwX.js} +1 -1
  43. package/dist/client/assets/{quadrantDiagram-W4KKPZXB-D1ifIucc.js → quadrantDiagram-W4KKPZXB-BClH2kQx.js} +1 -1
  44. package/dist/client/assets/{requirementDiagram-4Y6WPE33-DW68fC21.js → requirementDiagram-4Y6WPE33-ByolUxfP.js} +1 -1
  45. package/dist/client/assets/{sankeyDiagram-5OEKKPKP-DnnV47BL.js → sankeyDiagram-5OEKKPKP-B2vcb3U1.js} +1 -1
  46. package/dist/client/assets/{sequenceDiagram-3UESZ5HK-Dw-9ZBU_.js → sequenceDiagram-3UESZ5HK-J8Nt0GXG.js} +1 -1
  47. package/dist/client/assets/{stateDiagram-AJRCARHV-C7dkf8nk.js → stateDiagram-AJRCARHV-CrgZ4Uwa.js} +1 -1
  48. package/dist/client/assets/stateDiagram-v2-BHNVJYJU-CUEBQOsD.js +1 -0
  49. package/dist/client/assets/{timeline-definition-PNZ67QCA-BmVfK_16.js → timeline-definition-PNZ67QCA-CXebl6LI.js} +1 -1
  50. package/dist/client/assets/{vennDiagram-CIIHVFJN-B9j3QbmD.js → vennDiagram-CIIHVFJN-DLBqI39h.js} +1 -1
  51. package/dist/client/assets/{wardleyDiagram-YWT4CUSO-C520auMy.js → wardleyDiagram-YWT4CUSO-ku4fViV9.js} +1 -1
  52. package/dist/client/assets/{xychartDiagram-2RQKCTM6-CPqUJuqs.js → xychartDiagram-2RQKCTM6-5Jh-bBeb.js} +1 -1
  53. package/dist/client/index.html +2 -2
  54. package/dist/server/db.js +291 -3
  55. package/dist/server/db.js.map +1 -1
  56. package/dist/server/index.js +339 -97
  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/git.js +18 -0
  67. package/dist/server/lib/git.js.map +1 -1
  68. package/dist/server/lib/mobileAccess.js +45 -2
  69. package/dist/server/lib/mobileAccess.js.map +1 -1
  70. package/dist/server/lib/models.js +65 -48
  71. package/dist/server/lib/models.js.map +1 -1
  72. package/dist/server/lib/opencodeTaskSessions.js +216 -0
  73. package/dist/server/lib/opencodeTaskSessions.js.map +1 -0
  74. package/dist/server/lib/projectMemory.js +1073 -276
  75. package/dist/server/lib/projectMemory.js.map +1 -1
  76. package/dist/server/lib/projectMemoryCursor.js +27 -0
  77. package/dist/server/lib/projectMemoryCursor.js.map +1 -0
  78. package/dist/server/lib/projectMemoryEvents.js +24 -0
  79. package/dist/server/lib/projectMemoryEvents.js.map +1 -0
  80. package/dist/server/lib/projectMemoryLimits.js +61 -0
  81. package/dist/server/lib/projectMemoryLimits.js.map +1 -0
  82. package/dist/server/lib/projectMemoryPrompt.js +299 -110
  83. package/dist/server/lib/projectMemoryPrompt.js.map +1 -1
  84. package/dist/server/lib/projectMemoryScan.js +461 -493
  85. package/dist/server/lib/projectMemoryScan.js.map +1 -1
  86. package/dist/server/lib/projectMemorySelections.js +97 -0
  87. package/dist/server/lib/projectMemorySelections.js.map +1 -0
  88. package/dist/server/lib/projectMemorySuggestions.js +19 -4
  89. package/dist/server/lib/projectMemorySuggestions.js.map +1 -1
  90. package/dist/server/lib/projectMemoryTransfer.js +635 -83
  91. package/dist/server/lib/projectMemoryTransfer.js.map +1 -1
  92. package/dist/server/lib/projectPathGlob.js +40 -0
  93. package/dist/server/lib/projectPathGlob.js.map +1 -0
  94. package/dist/server/lib/projects.js +50 -0
  95. package/dist/server/lib/projects.js.map +1 -1
  96. package/dist/server/lib/promptSkills.js +5 -0
  97. package/dist/server/lib/promptSkills.js.map +1 -1
  98. package/dist/server/lib/specMessages.js +9 -6
  99. package/dist/server/lib/specMessages.js.map +1 -1
  100. package/dist/server/lib/specReviewContext.js +2 -1
  101. package/dist/server/lib/specReviewContext.js.map +1 -1
  102. package/dist/server/lib/ticketMessages.js +22 -7
  103. package/dist/server/lib/ticketMessages.js.map +1 -1
  104. package/dist/server/lib/tickets.js +6 -0
  105. package/dist/server/lib/tickets.js.map +1 -1
  106. package/dist/server/lib/transcriptRuns.js +85 -0
  107. package/dist/server/lib/transcriptRuns.js.map +1 -0
  108. package/dist/server/workers/build.js +18 -4
  109. package/dist/server/workers/build.js.map +1 -1
  110. package/dist/server/workers/chat.js +24 -7
  111. package/dist/server/workers/chat.js.map +1 -1
  112. package/dist/server/workers/followUp.js +27 -10
  113. package/dist/server/workers/followUp.js.map +1 -1
  114. package/dist/server/workers/plan.js +405 -69
  115. package/dist/server/workers/plan.js.map +1 -1
  116. package/dist/server/workers/planFollowUp.js +30 -7
  117. package/dist/server/workers/planFollowUp.js.map +1 -1
  118. package/dist/server/workers/review.js +76 -20
  119. package/dist/server/workers/review.js.map +1 -1
  120. package/dist/server/workers/specReview.js +10 -0
  121. package/dist/server/workers/specReview.js.map +1 -1
  122. package/dist/server/workers/specSession.js +37 -3
  123. package/dist/server/workers/specSession.js.map +1 -1
  124. package/package.json +1 -1
  125. package/dist/client/assets/channel-D7PWuiXU.js +0 -1
  126. package/dist/client/assets/classDiagram-4FO5ZUOK-BNIT931s.js +0 -1
  127. package/dist/client/assets/classDiagram-v2-Q7XG4LA2-TRtc0GGS.js +0 -1
  128. package/dist/client/assets/index-5Eun897r.js +0 -260
  129. package/dist/client/assets/index-CWWEw76C.css +0 -2
  130. 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,59 @@ 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, createAgentRun, finalizeAgentRun, getNextAgentRunIndex } from '../lib/agentRuns.js';
28
+ const createPlanningInvocation = (options, label, role) => {
29
+ const parentType = options.ticketId ? 'ticket' : options.projectId ? 'planning' : null;
30
+ const parentId = options.ticketId ?? options.projectId ?? null;
31
+ if (!parentType || !parentId) {
32
+ return null;
33
+ }
34
+ return {
35
+ parentType,
36
+ parentId,
37
+ runId: createAgentRun({
38
+ parentType,
39
+ parentId,
40
+ runIndex: getNextAgentRunIndex(parentType, parentId),
41
+ label,
42
+ role,
43
+ }),
44
+ };
45
+ };
46
+ const finalizePlanningInvocation = (invocation, status, response) => {
47
+ if (invocation) {
48
+ finalizeAgentRun(invocation.runId, status, response);
49
+ }
50
+ };
51
+ const createPlannerRecoveryTracker = () => {
52
+ let sawError = false;
53
+ let latestEvent = null;
54
+ let finalAnswerFragments = [];
55
+ return {
56
+ consume: (events) => {
57
+ let shouldResetResponse = false;
58
+ for (const event of events ?? []) {
59
+ latestEvent = event.type;
60
+ if (event.type === 'error') {
61
+ sawError = true;
62
+ finalAnswerFragments = [];
63
+ shouldResetResponse = true;
64
+ continue;
65
+ }
66
+ if (sawError) {
67
+ finalAnswerFragments.push(event.text);
68
+ }
69
+ }
70
+ return shouldResetResponse;
71
+ },
72
+ hasRecoveredFinalAnswer: () => (sawError
73
+ && latestEvent === 'final_answer'
74
+ && finalAnswerFragments.length > 0),
75
+ hasUnrecoveredError: () => latestEvent === 'error',
76
+ getRecoveredResponse: () => finalAnswerFragments.join('\n\n'),
77
+ };
78
+ };
25
79
  const extractLeadingJsonObject = (line) => {
26
80
  if (!line.startsWith('{')) {
27
81
  return null;
@@ -63,6 +117,43 @@ const extractLeadingJsonObject = (line) => {
63
117
  return null;
64
118
  };
65
119
  const normalizeToolName = (name) => name.toLowerCase().replace(/[^a-z0-9]/g, '');
120
+ const isPlannerSubagentTool = (name) => {
121
+ const normalized = normalizeToolName(name);
122
+ return normalized === 'task' || normalized === 'subagent' || normalized === 'agent';
123
+ };
124
+ const getPlannerSubagentDescription = (input, fallback) => {
125
+ if (input) {
126
+ for (const key of ['description', 'title', 'task', 'subagent_type', 'prompt']) {
127
+ const value = input[key];
128
+ if (typeof value === 'string' && value.trim()) {
129
+ const firstLine = value.trim().split('\n')[0] ?? '';
130
+ return firstLine.length > 120 ? `${firstLine.slice(0, 117)}...` : firstLine;
131
+ }
132
+ }
133
+ }
134
+ return fallback;
135
+ };
136
+ const stringifyPlannerToolResult = (content) => {
137
+ if (typeof content === 'string') {
138
+ return content.trim() || null;
139
+ }
140
+ if (Array.isArray(content)) {
141
+ const text = content
142
+ .map(item => item && typeof item === 'object' && typeof item.text === 'string'
143
+ ? item.text.trim()
144
+ : '')
145
+ .filter(Boolean)
146
+ .join('\n\n');
147
+ return text || null;
148
+ }
149
+ return content === null || content === undefined ? null : JSON.stringify(content);
150
+ };
151
+ const emptyParsedPlannerStreamLine = () => ({
152
+ logFragment: null,
153
+ responseFragment: null,
154
+ subagentEvents: [],
155
+ providerSessionId: null,
156
+ });
66
157
  const toPlannerInputPath = (input) => {
67
158
  if (typeof input?.filePath === 'string') {
68
159
  return input.filePath.trim();
@@ -117,26 +208,6 @@ const formatPlannerWriteToolCall = (input) => {
117
208
  lines.push('[[/archon-apply-patch]]');
118
209
  return lines.join('\n');
119
210
  };
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
211
  const formatPlannerToolCall = (name, input) => {
141
212
  const normalized = normalizeToolName(name);
142
213
  if (normalized === 'edit') {
@@ -144,32 +215,46 @@ const formatPlannerToolCall = (name, input) => {
144
215
  if (formatted !== null) {
145
216
  return formatted;
146
217
  }
147
- return formatPlannerGenericToolCall(name, input);
218
+ return formatGenericToolCall(name, input);
148
219
  }
149
220
  if (normalized === 'write') {
150
221
  const formatted = formatPlannerWriteToolCall(input);
151
222
  if (formatted !== null) {
152
223
  return formatted;
153
224
  }
154
- return formatPlannerGenericToolCall(name, input);
225
+ return formatGenericToolCall(name, input);
155
226
  }
156
- return formatPlannerGenericToolCall(name, input);
227
+ return formatGenericToolCall(name, input);
157
228
  };
158
229
  const parseClaudePlannerStreamJsonLine = (line) => {
159
230
  const trimmedLine = line.trim();
160
231
  if (!trimmedLine) {
161
- return { logFragment: null, responseFragment: null };
232
+ return emptyParsedPlannerStreamLine();
162
233
  }
163
234
  let event;
164
235
  try {
165
236
  event = JSON.parse(trimmedLine);
166
237
  }
167
238
  catch {
168
- return { logFragment: trimmedLine, responseFragment: trimmedLine };
239
+ const envelope = parseOpencodeTaskEnvelope(trimmedLine);
240
+ if (envelope) {
241
+ return {
242
+ ...emptyParsedPlannerStreamLine(),
243
+ subagentEvents: [{
244
+ kind: 'complete',
245
+ callId: null,
246
+ sessionId: envelope.childSessionId,
247
+ status: envelope.state?.toLowerCase() === 'error' || envelope.state?.toLowerCase() === 'failed' ? 'error' : 'done',
248
+ ...(envelope.result ? { text: envelope.result } : {}),
249
+ }],
250
+ };
251
+ }
252
+ return { ...emptyParsedPlannerStreamLine(), logFragment: trimmedLine, responseFragment: trimmedLine };
169
253
  }
170
254
  if (event.type === 'assistant' && Array.isArray(event.message?.content)) {
171
255
  const logParts = [];
172
256
  const responseParts = [];
257
+ const subagentEvents = [];
173
258
  for (const block of event.message.content) {
174
259
  if (block.type === 'text' && block.text) {
175
260
  const text = block.text.trim();
@@ -180,15 +265,61 @@ const parseClaudePlannerStreamJsonLine = (line) => {
180
265
  continue;
181
266
  }
182
267
  if (block.type === 'tool_use' && block.name) {
268
+ if (isPlannerSubagentTool(block.name)) {
269
+ const description = getPlannerSubagentDescription(block.input, block.name);
270
+ logParts.push(`Subagent: ${description}`);
271
+ subagentEvents.push({
272
+ kind: 'start',
273
+ callId: block.id ?? null,
274
+ label: `Subagent: ${description}`,
275
+ });
276
+ continue;
277
+ }
183
278
  logParts.push(formatPlannerToolCall(block.name, block.input));
184
279
  }
185
280
  }
186
281
  return {
187
282
  logFragment: logParts.length > 0 ? logParts.join('\n\n') : null,
188
283
  responseFragment: responseParts.length > 0 ? responseParts.join('\n\n') : null,
284
+ subagentEvents,
285
+ providerSessionId: null,
189
286
  };
190
287
  }
191
- return { logFragment: null, responseFragment: null };
288
+ if (event.type === 'stream_event' && event.event?.type === 'content_block_start') {
289
+ const block = event.event.content_block;
290
+ if (block?.type === 'tool_use' && block.name && isPlannerSubagentTool(block.name)) {
291
+ const description = getPlannerSubagentDescription(block.input, block.name);
292
+ return {
293
+ ...emptyParsedPlannerStreamLine(),
294
+ logFragment: `Subagent: ${description}`,
295
+ subagentEvents: [{
296
+ kind: 'start',
297
+ callId: block.id ?? null,
298
+ label: `Subagent: ${description}`,
299
+ }],
300
+ };
301
+ }
302
+ }
303
+ if (event.type === 'user' && Array.isArray(event.message?.content)) {
304
+ const subagentEvents = event.message.content.flatMap(block => {
305
+ if (block.type !== 'tool_result' || !block.tool_use_id) {
306
+ return [];
307
+ }
308
+ const rawText = stringifyPlannerToolResult(block.content);
309
+ const envelope = parseOpencodeTaskEnvelope(rawText);
310
+ const text = envelope?.result ?? rawText;
311
+ const state = envelope?.state?.toLowerCase();
312
+ return [{
313
+ kind: 'complete',
314
+ callId: block.tool_use_id,
315
+ status: block.is_error || state === 'error' || state === 'failed' ? 'error' : 'done',
316
+ ...(text ? { text } : {}),
317
+ ...(envelope ? { sessionId: envelope.childSessionId } : {}),
318
+ }];
319
+ });
320
+ return { ...emptyParsedPlannerStreamLine(), subagentEvents };
321
+ }
322
+ return emptyParsedPlannerStreamLine();
192
323
  };
193
324
  const formatPlannerOpencodeToolUse = (event) => {
194
325
  const toolName = event.part?.tool;
@@ -204,25 +335,48 @@ const formatPlannerStepTitle = (event) => {
204
335
  const parseOpencodePlannerJsonLine = (line) => {
205
336
  const trimmedLine = line.trim();
206
337
  if (!trimmedLine) {
207
- return { logFragment: null, responseFragment: null };
338
+ return emptyParsedPlannerStreamLine();
208
339
  }
209
340
  const jsonLine = extractLeadingJsonObject(trimmedLine);
210
341
  if (!jsonLine) {
211
- return { logFragment: trimmedLine, responseFragment: trimmedLine };
342
+ const envelope = parseOpencodeTaskEnvelope(trimmedLine);
343
+ if (envelope) {
344
+ return {
345
+ ...emptyParsedPlannerStreamLine(),
346
+ subagentEvents: [{
347
+ kind: 'complete',
348
+ callId: null,
349
+ sessionId: envelope.childSessionId,
350
+ status: envelope.state?.toLowerCase() === 'error' || envelope.state?.toLowerCase() === 'failed' ? 'error' : 'done',
351
+ ...(envelope.result ? { text: envelope.result } : {}),
352
+ }],
353
+ };
354
+ }
355
+ return { ...emptyParsedPlannerStreamLine(), logFragment: trimmedLine, responseFragment: trimmedLine };
212
356
  }
213
357
  let event;
214
358
  try {
215
359
  event = JSON.parse(jsonLine);
216
360
  }
217
361
  catch {
218
- return { logFragment: trimmedLine, responseFragment: trimmedLine };
362
+ return { ...emptyParsedPlannerStreamLine(), logFragment: trimmedLine, responseFragment: trimmedLine };
219
363
  }
364
+ const providerSessionId = event.sessionID?.trim()
365
+ || event.sessionId?.trim()
366
+ || event.part?.sessionID?.trim()
367
+ || event.part?.sessionId?.trim()
368
+ || null;
220
369
  if (event.type === 'text' && event.part?.type === 'text' && event.part.text) {
221
370
  const text = event.part.text.trim();
222
371
  const phase = event.part.metadata?.openai?.phase;
223
372
  return {
224
373
  logFragment: text || null,
225
374
  responseFragment: text && (!phase || phase === 'final_answer') ? text : null,
375
+ subagentEvents: [],
376
+ providerSessionId,
377
+ recoveryEvents: text && phase === 'final_answer'
378
+ ? [{ type: 'final_answer', text }]
379
+ : [],
226
380
  };
227
381
  }
228
382
  if (event.type === 'reasoning' && event.part?.type === 'reasoning') {
@@ -230,21 +384,88 @@ const parseOpencodePlannerJsonLine = (line) => {
230
384
  return {
231
385
  logFragment: text || null,
232
386
  responseFragment: null,
387
+ subagentEvents: [],
388
+ providerSessionId,
233
389
  };
234
390
  }
235
- if (event.type === 'tool_use') {
391
+ if (event.type === 'tool' || event.type === 'tool_use' || event.type === 'tool_result') {
392
+ const isToolInvocation = event.type === 'tool' || event.type === 'tool_use';
393
+ const toolName = event.part?.tool;
394
+ if (toolName && isPlannerSubagentTool(toolName)) {
395
+ const description = getPlannerSubagentDescription(event.part?.state?.input, toolName);
396
+ const rawOutput = event.part?.state?.output?.trim();
397
+ const envelope = parseOpencodeTaskEnvelope(rawOutput);
398
+ const output = envelope?.result ?? rawOutput;
399
+ const taskStatus = event.part?.state?.status?.toLowerCase() ?? envelope?.state?.toLowerCase();
400
+ const childSessionId = envelope?.childSessionId
401
+ ?? (typeof event.part?.state?.metadata?.sessionID === 'string' ? event.part.state.metadata.sessionID : null)
402
+ ?? (typeof event.part?.state?.metadata?.sessionId === 'string' ? event.part.state.metadata.sessionId : null);
403
+ return {
404
+ logFragment: isToolInvocation ? `Subagent: ${description}` : null,
405
+ responseFragment: null,
406
+ subagentEvents: [
407
+ ...(isToolInvocation
408
+ ? [{
409
+ kind: 'start',
410
+ callId: event.part?.callID ?? null,
411
+ label: `Subagent: ${description}`,
412
+ sessionId: childSessionId,
413
+ }]
414
+ : []),
415
+ ...(output
416
+ ? [{
417
+ kind: taskStatus === 'running' || taskStatus === 'pending' ? 'fragment' : 'complete',
418
+ callId: event.part?.callID ?? null,
419
+ text: output,
420
+ sessionId: childSessionId,
421
+ status: taskStatus === 'error' || taskStatus === 'failed' ? 'error' : 'done',
422
+ }]
423
+ : []),
424
+ ],
425
+ providerSessionId,
426
+ };
427
+ }
428
+ if (!isToolInvocation) {
429
+ return { ...emptyParsedPlannerStreamLine(), providerSessionId };
430
+ }
236
431
  return {
237
432
  logFragment: formatPlannerOpencodeToolUse(event),
238
433
  responseFragment: null,
434
+ subagentEvents: [],
435
+ providerSessionId,
239
436
  };
240
437
  }
241
438
  if (event.type === 'step_start') {
242
439
  return {
243
440
  logFragment: formatPlannerStepTitle(event),
244
441
  responseFragment: null,
442
+ subagentEvents: [],
443
+ providerSessionId,
444
+ };
445
+ }
446
+ if (event.type === 'error') {
447
+ const errorText = typeof event.error === 'string'
448
+ ? event.error.trim()
449
+ : event.error?.data?.message?.trim()
450
+ || event.error?.message?.trim()
451
+ || (event.error ? JSON.stringify(event.error) : '');
452
+ const text = event.part?.text?.trim() || errorText;
453
+ return {
454
+ logFragment: text ? `**Error:** ${text}` : null,
455
+ responseFragment: null,
456
+ subagentEvents: [],
457
+ providerSessionId,
458
+ recoveryEvents: [{ type: 'error' }],
245
459
  };
246
460
  }
247
- return { logFragment: null, responseFragment: null };
461
+ return { ...emptyParsedPlannerStreamLine(), providerSessionId };
462
+ };
463
+ const attachPlannerTranscriptRuns = (error, transcriptRuns) => {
464
+ const resolvedError = (error instanceof Error ? error : new Error(String(error)));
465
+ if (transcriptRuns) {
466
+ resolvedError.transcriptRuns = transcriptRuns;
467
+ }
468
+ return resolvedError;
248
469
  };
249
470
  const stripSystemReminderBlocks = (output) => {
250
471
  return output.replace(/<system-reminder>[\s\S]*?<\/system-reminder>/g, '').trim();
@@ -268,10 +489,10 @@ const PLANNING_QUESTIONS_RULES = [
268
489
  '- Return an empty `open_questions` array when the ticket is already clear enough to implement.',
269
490
  ].join('\n');
270
491
  const MEMORY_SUGGESTION_RULES = [
492
+ '- Prioritize explicit durable decisions established by the user.',
271
493
  ...PROJECT_MEMORY_SUGGESTION_COMMON_RULE_LINES,
272
494
  '- Do not suggest planner assumptions as project memory.',
273
- '- Use the exact shape `{ "kind": "decision", "title": "...", "scope": "optional scope", "decision": "...", "rationale": "optional why", "implications": ["optional implication"] }` for decisions.',
274
- '- Use the exact shape `{ "kind": "convention", "title": "...", "scope": "relevant area", "instruction": "...", "rationale": "optional why", "stages": ["build"], "priority": "normal" }` for conventions.',
495
+ '- Include structured applicability, evidence, and a create/update/supersede recommendation using the common suggestion contract.',
275
496
  ].join('\n');
276
497
  const getPlanningMemorySuggestionRules = (memoryEnabled) => {
277
498
  return memoryEnabled ? MEMORY_SUGGESTION_RULES : '';
@@ -439,6 +660,7 @@ const buildSingleTicketPrompt = (title, description, linkedResearchContext = '',
439
660
 
440
661
  This is a planning-only task.
441
662
  - You may inspect the repository and use read-only tools if helpful.
663
+ ${buildPlanningSubagentGuidancePromptBlock()}
442
664
  - Do not edit files or make changes.
443
665
  - Return a short natural-language planning update first, then the final JSON object on the last line.
444
666
 
@@ -497,6 +719,7 @@ This is pass 1 of 2. Your job is to decide the best ticket split and dependency
497
719
 
498
720
  This is a planning-only task.
499
721
  - You may inspect the repository and use read-only tools if helpful.
722
+ ${buildPlanningSubagentGuidancePromptBlock()}
500
723
  - Do not edit files or make changes.
501
724
  - Return only the final JSON object.
502
725
 
@@ -548,7 +771,7 @@ ${projectMemoryBlock ? `${projectMemoryBlock}
548
771
 
549
772
  ` : ''}
550
773
  ${linkedResearchContext}`;
551
- const buildPlanningAgentExpansionPrompt = (description, outline, linkedResearchContext = '', projectMemoryBlock = '', memoryEnabled = false) => `You are helping expand a previously decomposed software development initiative into implementation-ready tickets for a kanban board.
774
+ const buildPlanningAgentExpansionPrompt = (description, outline, linkedResearchContext = '', projectMemoryBlock = '') => `You are helping expand a previously decomposed software development initiative into implementation-ready tickets for a kanban board.
552
775
 
553
776
  Each generated ticket will later be implemented by a separate AI agent, so every ticket description must be self-contained and implementation-ready without needing any further context or clarification.
554
777
 
@@ -556,6 +779,7 @@ This is pass 2 of 2. The ticket split and dependency graph are already decided.
556
779
 
557
780
  This is a planning-only task.
558
781
  - You may inspect the repository and use read-only tools if helpful.
782
+ ${buildPlanningSubagentGuidancePromptBlock()}
559
783
  - Do not edit files or make changes.
560
784
  - Return only the final JSON object.
561
785
 
@@ -566,7 +790,7 @@ Return ONLY a JSON object with this exact shape:
566
790
  "key": "short-kebab-key",
567
791
  "title": "Ticket title",
568
792
  "description": "Detailed markdown description",
569
- "open_questions": ["Optional question"]${memoryEnabled ? ',\n "memory_suggestions": []' : ''}
793
+ "open_questions": ["Optional question"]
570
794
  }
571
795
  ],
572
796
  "dependencies": [
@@ -604,8 +828,7 @@ ${IMPLEMENTATION_BODY_DEDUPLICATION_RULES}
604
828
  - Include those resources verbatim in the final ticket description instead of replacing them with vague summaries.
605
829
  - Clarification handling rules:
606
830
  ${PLANNING_QUESTIONS_RULES}
607
- - Memory suggestion rules:${getPlanningMemorySuggestionRules(memoryEnabled) ? `
608
- ${getPlanningMemorySuggestionRules(memoryEnabled)}` : ' Omit memory suggestions entirely when project memory is disabled.'}
831
+ - Do not return \`memory_suggestions\`. Multi-ticket expansion has no durable source record to own them.
609
832
 
610
833
  Example of the expected depth and structure for a single expanded ticket:
611
834
  ${EXPANDED_TICKET_EXAMPLE}
@@ -740,11 +963,19 @@ export const runPlannerPrompt = (tool, model, variant, prompt, cwd, options = {}
740
963
  let resolvedSessionId = sessionTracking?.resumeSessionId ?? null;
741
964
  let lastStdoutAt = Date.now();
742
965
  let watchdogTimer = null;
966
+ const subagentTracker = options.agentRunParent
967
+ ? createSubagentRunTracker({
968
+ ...options.agentRunParent,
969
+ io: options.agentRunParent.io,
970
+ })
971
+ : null;
972
+ let opencodeSubagentMonitor = null;
743
973
  const resolveOnce = (value) => {
744
974
  if (settled) {
745
975
  return;
746
976
  }
747
977
  settled = true;
978
+ opencodeSubagentMonitor?.stop();
748
979
  resolve(value);
749
980
  };
750
981
  const rejectOnce = (error) => {
@@ -752,6 +983,7 @@ export const runPlannerPrompt = (tool, model, variant, prompt, cwd, options = {}
752
983
  return;
753
984
  }
754
985
  settled = true;
986
+ opencodeSubagentMonitor?.stop();
755
987
  reject(error);
756
988
  };
757
989
  const clearWatchdogTimer = () => {
@@ -779,6 +1011,9 @@ export const runPlannerPrompt = (tool, model, variant, prompt, cwd, options = {}
779
1011
  }
780
1012
  return hasResolvedSessionId;
781
1013
  };
1014
+ if (tool === 'opencode' && (sessionTracking || subagentTracker)) {
1015
+ spawnTimestamp = Date.now();
1016
+ }
782
1017
  const child = spawn(cmd, args, {
783
1018
  stdio: ['ignore', 'pipe', 'pipe'],
784
1019
  env: { ...process.env, ...(cwd ? { PWD: cwd } : {}) },
@@ -789,7 +1024,6 @@ export const runPlannerPrompt = (tool, model, variant, prompt, cwd, options = {}
789
1024
  updatePlanningSessionId(sessionTracking.ticketId, sessionId, sessionTracking.io);
790
1025
  }
791
1026
  if (sessionTracking && tool === 'opencode') {
792
- spawnTimestamp = Date.now();
793
1027
  if (!sessionTracking.resumeSessionId) {
794
1028
  sessionLookupTimer = setTimeout(() => {
795
1029
  attemptOpencodeSessionLookup();
@@ -797,10 +1031,25 @@ export const runPlannerPrompt = (tool, model, variant, prompt, cwd, options = {}
797
1031
  }, 1500);
798
1032
  }
799
1033
  }
1034
+ if (subagentTracker && tool === 'opencode') {
1035
+ spawnTimestamp = spawnTimestamp ?? Date.now();
1036
+ opencodeSubagentMonitor = createOpencodeSubagentMonitor({
1037
+ initialParentSessionId: sessionTracking?.resumeSessionId ?? null,
1038
+ spawnTimestamp,
1039
+ cwd: cwd ?? process.cwd(),
1040
+ tracker: subagentTracker,
1041
+ discoverParentSession: false,
1042
+ onParentSessionId: discoveredSessionId => {
1043
+ resolvedSessionId = resolvedSessionId ?? discoveredSessionId;
1044
+ },
1045
+ });
1046
+ opencodeSubagentMonitor.start();
1047
+ }
800
1048
  const stdoutChunks = [];
801
1049
  const stderrChunks = [];
802
1050
  const logLines = [];
803
1051
  const responseLines = [];
1052
+ const recoveryTracker = createPlannerRecoveryTracker();
804
1053
  let usageSnapshot = EMPTY_CHAT_USAGE_SNAPSHOT;
805
1054
  let remainder = '';
806
1055
  let insideHiddenBlock = false;
@@ -839,9 +1088,18 @@ export const runPlannerPrompt = (tool, model, variant, prompt, cwd, options = {}
839
1088
  catch { /* best-effort timing */ }
840
1089
  }
841
1090
  }
842
- const { logFragment, responseFragment } = tool === 'claude'
1091
+ const parsedLine = tool === 'claude'
843
1092
  ? parseClaudePlannerStreamJsonLine(line)
844
1093
  : parseOpencodePlannerJsonLine(line);
1094
+ const { logFragment, responseFragment, subagentEvents, providerSessionId } = parsedLine;
1095
+ if (tool === 'opencode' && recoveryTracker.consume(parsedLine.recoveryEvents)) {
1096
+ responseLines.length = 0;
1097
+ firstFinalAnswerTime = null;
1098
+ }
1099
+ subagentTracker?.handle(subagentEvents);
1100
+ if (providerSessionId) {
1101
+ opencodeSubagentMonitor?.setParentSessionId(providerSessionId);
1102
+ }
845
1103
  if (isHiddenOpenTag.test(line)) {
846
1104
  insideHiddenBlock = true;
847
1105
  }
@@ -863,6 +1121,7 @@ export const runPlannerPrompt = (tool, model, variant, prompt, cwd, options = {}
863
1121
  child.stderr?.on('data', (chunk) => stderrChunks.push(chunk));
864
1122
  child.on('close', code => {
865
1123
  clearWatchdogTimer();
1124
+ opencodeSubagentMonitor?.refresh();
866
1125
  const resolvedSessionOnClose = attemptOpencodeSessionLookup();
867
1126
  if (sessionLookupTimer && (resolvedSessionOnClose || hasResolvedSessionId)) {
868
1127
  clearTimeout(sessionLookupTimer);
@@ -872,9 +1131,18 @@ export const runPlannerPrompt = (tool, model, variant, prompt, cwd, options = {}
872
1131
  if (tool === 'claude') {
873
1132
  usageSnapshot = mergeUsageFromJsonLine(usageSnapshot, remainder);
874
1133
  }
875
- const { logFragment, responseFragment } = tool === 'claude'
1134
+ const parsedLine = tool === 'claude'
876
1135
  ? parseClaudePlannerStreamJsonLine(remainder)
877
1136
  : parseOpencodePlannerJsonLine(remainder);
1137
+ const { logFragment, responseFragment, subagentEvents, providerSessionId } = parsedLine;
1138
+ if (tool === 'opencode' && recoveryTracker.consume(parsedLine.recoveryEvents)) {
1139
+ responseLines.length = 0;
1140
+ firstFinalAnswerTime = null;
1141
+ }
1142
+ subagentTracker?.handle(subagentEvents);
1143
+ if (providerSessionId) {
1144
+ opencodeSubagentMonitor?.setParentSessionId(providerSessionId);
1145
+ }
878
1146
  if (logFragment !== null) {
879
1147
  logLines.push(logFragment);
880
1148
  }
@@ -884,9 +1152,12 @@ export const runPlannerPrompt = (tool, model, variant, prompt, cwd, options = {}
884
1152
  }
885
1153
  }
886
1154
  const rawOutput = Buffer.concat(stdoutChunks).toString().trim();
887
- const output = tool === 'claude' && !rawOutput.startsWith('{')
888
- ? rawOutput
889
- : responseLines.join('\n\n').trim() || rawOutput;
1155
+ const recoveredResponse = recoveryTracker.getRecoveredResponse();
1156
+ const output = tool === 'opencode' && recoveryTracker.hasRecoveredFinalAnswer()
1157
+ ? recoveredResponse
1158
+ : tool === 'claude' && !rawOutput.startsWith('{')
1159
+ ? rawOutput
1160
+ : responseLines.join('\n\n').trim() || rawOutput;
890
1161
  const errorOutput = Buffer.concat(stderrChunks).toString().trim();
891
1162
  const timing = (() => {
892
1163
  const totalMs = Date.now() - startTime;
@@ -898,17 +1169,22 @@ export const runPlannerPrompt = (tool, model, variant, prompt, cwd, options = {}
898
1169
  return { totalMs };
899
1170
  })();
900
1171
  if (hardTimeoutForced) {
901
- rejectOnce(new Error(output || errorOutput || `Planner exceeded the ${PLANNER_HARD_TIMEOUT_MS} ms hard timeout.`));
1172
+ const transcriptRuns = subagentTracker?.finalize('error', logLines.join('\n\n') || errorOutput || null, output || null) ?? null;
1173
+ rejectOnce(attachPlannerTranscriptRuns(new Error(output || errorOutput || `Planner exceeded the ${PLANNER_HARD_TIMEOUT_MS} ms hard timeout.`), transcriptRuns));
902
1174
  return;
903
1175
  }
904
- if ((code === 0 || completionForced) && output) {
905
- resolveOnce({ output, responseTiming: timing, usageSnapshot });
1176
+ if (!recoveryTracker.hasUnrecoveredError()
1177
+ && (code === 0 || completionForced || recoveryTracker.hasRecoveredFinalAnswer())
1178
+ && output) {
1179
+ const transcriptRuns = subagentTracker?.finalize('done', logLines.join('\n\n') || null, output) ?? null;
1180
+ resolveOnce({ output, responseTiming: timing, usageSnapshot, transcriptRuns });
906
1181
  return;
907
1182
  }
908
1183
  const fallbackError = code === 0
909
1184
  ? 'Planner did not return a usable plan.'
910
1185
  : `Process exited with code ${code}`;
911
- rejectOnce(new Error(output || errorOutput || fallbackError));
1186
+ const transcriptRuns = subagentTracker?.finalize('error', logLines.join('\n\n') || errorOutput || null, output || null) ?? null;
1187
+ rejectOnce(attachPlannerTranscriptRuns(new Error(output || errorOutput || fallbackError), transcriptRuns));
912
1188
  });
913
1189
  child.on('error', err => {
914
1190
  clearWatchdogTimer();
@@ -916,7 +1192,8 @@ export const runPlannerPrompt = (tool, model, variant, prompt, cwd, options = {}
916
1192
  clearTimeout(sessionLookupTimer);
917
1193
  sessionLookupTimer = null;
918
1194
  }
919
- rejectOnce(err);
1195
+ const transcriptRuns = subagentTracker?.finalize('error', err.message, null) ?? null;
1196
+ rejectOnce(attachPlannerTranscriptRuns(err, transcriptRuns));
920
1197
  });
921
1198
  if (tool === 'opencode') {
922
1199
  const runWatchdog = () => {
@@ -1227,14 +1504,17 @@ export const generatePlanText = (title, description, options = {}) => {
1227
1504
  const tool = options.tool ?? config.tool ?? 'opencode';
1228
1505
  const project = options.projectId ? getProjectById(options.projectId) : null;
1229
1506
  return (async () => {
1507
+ const invocation = createPlanningInvocation(options, 'Ticket Planning', 'planning');
1230
1508
  const linkedResearch = options.linkedResearch ?? await prepareLinkedPlanningResearch(`${title}\n\n${description}`, options);
1231
1509
  const linkedResearchContext = project ? buildLinkedResearchContext(project, linkedResearch) : '';
1232
1510
  const projectMemoryBlock = getProjectMemoryPromptBlock(project?.id, {
1511
+ flow: 'plan',
1233
1512
  stage: 'plan',
1234
1513
  title,
1235
1514
  description,
1515
+ ...(invocation ? { invocation: { parentType: invocation.parentType, parentId: invocation.parentId, agentRunId: invocation.runId } } : {}),
1236
1516
  });
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), {
1517
+ 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
1518
  onProgress: options.onProgress,
1239
1519
  ...(options.ticketId && options.io
1240
1520
  ? {
@@ -1243,51 +1523,87 @@ export const generatePlanText = (title, description, options = {}) => {
1243
1523
  io: options.io,
1244
1524
  resumeSessionId: options.planningSessionId,
1245
1525
  },
1526
+ agentRunParent: {
1527
+ parentType: 'ticket',
1528
+ parentId: options.ticketId,
1529
+ io: options.io,
1530
+ },
1246
1531
  }
1247
1532
  : {}),
1248
1533
  });
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.');
1534
+ try {
1535
+ const expanded = extractTaggedJsonObjectWithLeadingText(plannerOutput, PLAN_OUTPUT_TAG)
1536
+ ?? extractFirstJsonObjectWithLeadingText(plannerOutput);
1537
+ const expandedTicket = expanded.parsed.tickets.find(ticket => ticket.key === 'main-ticket');
1538
+ if (!expandedTicket?.description?.trim()) {
1539
+ throw new Error('Planner did not return a usable plan.');
1540
+ }
1541
+ const result = {
1542
+ ...normalizePlanningDraftTicket(expandedTicket),
1543
+ key: 'main-ticket',
1544
+ title: title.trim(),
1545
+ description: expandedTicket.description.trim(),
1546
+ response: expanded.before.trim(),
1547
+ responseTiming: plannerTiming,
1548
+ usageSnapshot,
1549
+ transcriptRuns,
1550
+ };
1551
+ finalizePlanningInvocation(invocation, 'done', plannerOutput);
1552
+ return result;
1553
+ }
1554
+ catch (error) {
1555
+ finalizePlanningInvocation(invocation, 'error', plannerOutput);
1556
+ throw attachPlannerTranscriptRuns(error, transcriptRuns ?? null);
1254
1557
  }
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
1558
  })();
1265
1559
  };
1266
1560
  export const generatePlanningTicketOutline = async (description, options = {}) => {
1267
1561
  const tool = options.tool ?? config.tool ?? 'opencode';
1268
1562
  const project = options.projectId ? getProjectById(options.projectId) : null;
1563
+ const invocation = createPlanningInvocation(options, 'Planning Outline', 'planning-outline');
1269
1564
  const linkedResearch = options.linkedResearch ?? await prepareLinkedPlanningResearch(description, options);
1270
1565
  const linkedResearchContext = project ? buildLinkedResearchContext(project, linkedResearch) : '';
1271
1566
  const projectMemoryBlock = getProjectMemoryPromptBlock(project?.id, {
1567
+ flow: 'plan',
1272
1568
  stage: 'plan',
1273
1569
  title: description,
1274
1570
  description,
1571
+ ...(invocation ? { invocation: { parentType: invocation.parentType, parentId: invocation.parentId, agentRunId: invocation.runId } } : {}),
1275
1572
  });
1276
1573
  const { output } = await runPlannerPrompt(tool, options.model ?? null, options.variant ?? null, prependSkillPromptBlock(buildPlanningAgentOutlinePrompt(description, linkedResearchContext, projectMemoryBlock), options.skills ?? []), resolvePlannerCwd(project, options.branch));
1277
- return extractFirstJsonObject(output);
1574
+ try {
1575
+ const result = extractFirstJsonObject(output);
1576
+ finalizePlanningInvocation(invocation, 'done', output);
1577
+ return result;
1578
+ }
1579
+ catch (error) {
1580
+ finalizePlanningInvocation(invocation, 'error', output);
1581
+ throw error;
1582
+ }
1278
1583
  };
1279
1584
  export const expandPlanningTicketOutline = async (description, outline, options = {}) => {
1280
1585
  const tool = options.tool ?? config.tool ?? 'opencode';
1281
1586
  const project = options.projectId ? getProjectById(options.projectId) : null;
1587
+ const invocation = createPlanningInvocation(options, 'Planning Expansion', 'planning-expansion');
1282
1588
  const linkedResearch = options.linkedResearch ?? await prepareLinkedPlanningResearch(description, options);
1283
1589
  const linkedResearchContext = project ? buildLinkedResearchContext(project, linkedResearch) : '';
1284
1590
  const projectMemoryBlock = getProjectMemoryPromptBlock(project?.id, {
1591
+ flow: 'plan',
1285
1592
  stage: 'plan',
1286
1593
  title: description,
1287
1594
  description,
1595
+ ...(invocation ? { invocation: { parentType: invocation.parentType, parentId: invocation.parentId, agentRunId: invocation.runId } } : {}),
1288
1596
  });
1289
- const { output } = await runPlannerPrompt(tool, options.model ?? null, options.variant ?? null, prependSkillPromptBlock(buildPlanningAgentExpansionPrompt(description, outline, linkedResearchContext, projectMemoryBlock, Boolean(project?.memory_enabled)), options.skills ?? []), resolvePlannerCwd(project, options.branch));
1290
- return extractFirstJsonObject(output);
1597
+ const { output } = await runPlannerPrompt(tool, options.model ?? null, options.variant ?? null, prependSkillPromptBlock(buildPlanningAgentExpansionPrompt(description, outline, linkedResearchContext, projectMemoryBlock), options.skills ?? []), resolvePlannerCwd(project, options.branch));
1598
+ try {
1599
+ const result = extractFirstJsonObject(output);
1600
+ finalizePlanningInvocation(invocation, 'done', output);
1601
+ return result;
1602
+ }
1603
+ catch (error) {
1604
+ finalizePlanningInvocation(invocation, 'error', output);
1605
+ throw error;
1606
+ }
1291
1607
  };
1292
1608
  const emitTicketUpdate = (ticketId, io) => {
1293
1609
  const updated = getTicket(ticketId);
@@ -1308,6 +1624,7 @@ export const startPlan = (ticket, io, options = {}) => {
1308
1624
  registerBundleActivity(ticket, 'plan');
1309
1625
  beginTicketThinkingRun(ticket.id);
1310
1626
  db.prepare('UPDATE tickets SET agent_status = ?, agent_log = NULL, streaming_response = NULL, updated_at = CURRENT_TIMESTAMP WHERE id = ?').run('running', ticket.id);
1627
+ clearAndEmitAgentRunsForParent(io, 'ticket', ticket.id);
1311
1628
  emitTicketUpdate(ticket.id, io);
1312
1629
  try {
1313
1630
  const project = getEffectiveProject(ticket);
@@ -1356,7 +1673,7 @@ export const startPlan = (ticket, io, options = {}) => {
1356
1673
  const assistantMessage = buildPlanningAssistantMessage({
1357
1674
  response: planned.response,
1358
1675
  openQuestions: planned.open_questions,
1359
- });
1676
+ }) || (planned.transcriptRuns?.length ? planned.description : '');
1360
1677
  if (assistantMessage) {
1361
1678
  const messageId = createTicketMessage({
1362
1679
  ticketId: ticket.id,
@@ -1364,14 +1681,19 @@ export const startPlan = (ticket, io, options = {}) => {
1364
1681
  kind: 'planning_response',
1365
1682
  content: assistantMessage,
1366
1683
  responseTiming: planned.responseTiming,
1684
+ transcriptRuns: planned.transcriptRuns,
1367
1685
  });
1368
1686
  if (messageId) {
1369
1687
  bindTicketThinkingMessage(ticket.id, messageId);
1370
1688
  }
1371
1689
  }
1690
+ if (planned.transcriptRuns) {
1691
+ clearAndEmitAgentRunsForParent(io, 'ticket', ticket.id);
1692
+ }
1372
1693
  emitTicketUpdate(ticket.id, io);
1373
1694
  }
1374
1695
  catch (err) {
1696
+ const transcriptRuns = getAgentErrorTranscriptRuns(err);
1375
1697
  const message = err instanceof PlanningReferenceError
1376
1698
  ? err.message
1377
1699
  : err instanceof JiraApiError
@@ -1381,6 +1703,20 @@ export const startPlan = (ticket, io, options = {}) => {
1381
1703
  : err instanceof Error
1382
1704
  ? err.message
1383
1705
  : String(err);
1706
+ const errorContent = formatAgentErrorForTranscript(err);
1707
+ let errorMessageId = null;
1708
+ if (errorContent) {
1709
+ errorMessageId = createTicketMessage({
1710
+ ticketId: ticket.id,
1711
+ role: 'assistant',
1712
+ kind: 'error',
1713
+ content: errorContent,
1714
+ transcriptRuns,
1715
+ });
1716
+ }
1717
+ if (transcriptRuns && errorMessageId) {
1718
+ clearAndEmitAgentRunsForParent(io, 'ticket', ticket.id);
1719
+ }
1384
1720
  db.prepare('UPDATE tickets SET agent_status = ?, agent_log = ?, streaming_response = NULL, updated_at = CURRENT_TIMESTAMP WHERE id = ?').run('error', message, ticket.id);
1385
1721
  emitTicketUpdate(ticket.id, io);
1386
1722
  }