@jaimevalasek/aioson 1.3.0 → 1.4.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 (213) hide show
  1. package/README.md +19 -2
  2. package/docs/pt/README.md +62 -2
  3. package/docs/pt/advisor-spec.md +5 -5
  4. package/docs/pt/agentes-customizados.md +670 -0
  5. package/docs/pt/agentes.md +111 -13
  6. package/docs/pt/automacao-squads.md +407 -0
  7. package/docs/pt/cenarios.md +3 -3
  8. package/docs/pt/clientes-ai.md +62 -0
  9. package/docs/pt/comandos-cli.md +167 -17
  10. package/docs/pt/deyvin.md +115 -0
  11. package/docs/pt/genome-3.0-spec.md +11 -11
  12. package/docs/pt/inicio-rapido.md +45 -0
  13. package/docs/pt/memoria-contexto.md +255 -0
  14. package/docs/pt/output-strategy-delivery.md +655 -0
  15. package/docs/pt/profiler-system.md +17 -17
  16. package/docs/pt/runtime-observability.md +5 -1
  17. package/docs/pt/skills.md +175 -0
  18. package/docs/pt/{squad-genoma.md → squad-genome.md} +81 -75
  19. package/docs/testing/genome-2.0-rollout.md +1 -1
  20. package/package.json +3 -3
  21. package/src/agents.js +21 -5
  22. package/src/backup-provider.js +303 -0
  23. package/src/cli.js +178 -2
  24. package/src/commands/agents.js +22 -4
  25. package/src/commands/backup.js +533 -0
  26. package/src/commands/cloud.js +17 -17
  27. package/src/commands/context-pack.js +45 -0
  28. package/src/commands/implementation-plan.js +340 -0
  29. package/src/commands/learning.js +134 -0
  30. package/src/commands/live.js +1583 -0
  31. package/src/commands/runtime.js +833 -2
  32. package/src/commands/scan-project.js +288 -24
  33. package/src/commands/setup-context.js +23 -0
  34. package/src/commands/skill.js +558 -0
  35. package/src/commands/squad-agent-create.js +788 -0
  36. package/src/commands/squad-doctor.js +51 -1
  37. package/src/commands/squad-investigate.js +261 -0
  38. package/src/commands/squad-learning.js +209 -0
  39. package/src/commands/squad-pipeline.js +247 -1
  40. package/src/commands/squad-plan.js +329 -0
  41. package/src/commands/squad-status.js +1 -1
  42. package/src/commands/squad-validate.js +57 -1
  43. package/src/commands/test-agents.js +6 -1
  44. package/src/commands/workflow-next.js +8 -1
  45. package/src/commands/workflow-status.js +250 -0
  46. package/src/constants.js +80 -16
  47. package/src/context-memory.js +837 -0
  48. package/src/context-writer.js +2 -0
  49. package/src/delivery-runner.js +319 -0
  50. package/src/genome-files.js +1 -1
  51. package/src/genome-format.js +1 -1
  52. package/src/i18n/messages/en.js +206 -7
  53. package/src/i18n/messages/es.js +123 -6
  54. package/src/i18n/messages/fr.js +122 -5
  55. package/src/i18n/messages/pt-BR.js +205 -12
  56. package/src/installer.js +30 -2
  57. package/src/lib/genomes/compat.js +1 -1
  58. package/src/runtime-store.js +780 -42
  59. package/src/session-handoff.js +77 -0
  60. package/template/.aioson/agents/analyst.md +36 -9
  61. package/template/.aioson/agents/architect.md +20 -5
  62. package/template/.aioson/agents/dev.md +135 -15
  63. package/template/.aioson/agents/deyvin.md +166 -0
  64. package/template/.aioson/agents/discovery-design-doc.md +25 -1
  65. package/template/.aioson/agents/{genoma.md → genome.md} +20 -20
  66. package/template/.aioson/agents/orache.md +371 -0
  67. package/template/.aioson/agents/orchestrator.md +37 -2
  68. package/template/.aioson/agents/pair.md +5 -0
  69. package/template/.aioson/agents/pm.md +17 -5
  70. package/template/.aioson/agents/product.md +58 -22
  71. package/template/.aioson/agents/profiler-enricher.md +1 -1
  72. package/template/.aioson/agents/profiler-forge.md +9 -9
  73. package/template/.aioson/agents/profiler-researcher.md +1 -1
  74. package/template/.aioson/agents/qa.md +17 -5
  75. package/template/.aioson/agents/setup.md +81 -5
  76. package/template/.aioson/agents/squad.md +675 -28
  77. package/template/.aioson/agents/ux-ui.md +277 -34
  78. package/template/.aioson/config.md +175 -0
  79. package/template/.aioson/context/spec.md.template +17 -0
  80. package/template/.aioson/genomes/.gitkeep +0 -0
  81. package/template/.aioson/installed-skills/.gitkeep +0 -0
  82. package/template/.aioson/locales/en/agents/analyst.md +26 -4
  83. package/template/.aioson/locales/en/agents/architect.md +10 -0
  84. package/template/.aioson/locales/en/agents/dev.md +89 -4
  85. package/template/.aioson/locales/en/agents/deyvin.md +129 -0
  86. package/template/.aioson/locales/en/agents/{genoma.md → genome.md} +14 -14
  87. package/template/.aioson/locales/en/agents/orchestrator.md +36 -2
  88. package/template/.aioson/locales/en/agents/pair.md +5 -0
  89. package/template/.aioson/locales/en/agents/pm.md +7 -0
  90. package/template/.aioson/locales/en/agents/product.md +35 -17
  91. package/template/.aioson/locales/en/agents/qa.md +7 -0
  92. package/template/.aioson/locales/en/agents/setup.md +51 -5
  93. package/template/.aioson/locales/en/agents/squad.md +203 -15
  94. package/template/.aioson/locales/en/agents/ux-ui.md +375 -35
  95. package/template/.aioson/locales/es/agents/analyst.md +16 -4
  96. package/template/.aioson/locales/es/agents/architect.md +10 -0
  97. package/template/.aioson/locales/es/agents/dev.md +70 -2
  98. package/template/.aioson/locales/es/agents/deyvin.md +89 -0
  99. package/template/.aioson/locales/es/agents/{genoma.md → genome.md} +13 -13
  100. package/template/.aioson/locales/es/agents/orache.md +103 -0
  101. package/template/.aioson/locales/es/agents/orchestrator.md +36 -2
  102. package/template/.aioson/locales/es/agents/pair.md +5 -0
  103. package/template/.aioson/locales/es/agents/pm.md +7 -0
  104. package/template/.aioson/locales/es/agents/product.md +13 -3
  105. package/template/.aioson/locales/es/agents/qa.md +7 -0
  106. package/template/.aioson/locales/es/agents/setup.md +28 -5
  107. package/template/.aioson/locales/es/agents/squad.md +221 -15
  108. package/template/.aioson/locales/es/agents/ux-ui.md +26 -25
  109. package/template/.aioson/locales/fr/agents/analyst.md +16 -4
  110. package/template/.aioson/locales/fr/agents/architect.md +10 -0
  111. package/template/.aioson/locales/fr/agents/dev.md +70 -2
  112. package/template/.aioson/locales/fr/agents/deyvin.md +89 -0
  113. package/template/.aioson/locales/fr/agents/{genoma.md → genome.md} +7 -7
  114. package/template/.aioson/locales/fr/agents/orache.md +104 -0
  115. package/template/.aioson/locales/fr/agents/orchestrator.md +36 -2
  116. package/template/.aioson/locales/fr/agents/pair.md +5 -0
  117. package/template/.aioson/locales/fr/agents/pm.md +7 -0
  118. package/template/.aioson/locales/fr/agents/product.md +13 -3
  119. package/template/.aioson/locales/fr/agents/qa.md +7 -0
  120. package/template/.aioson/locales/fr/agents/setup.md +28 -5
  121. package/template/.aioson/locales/fr/agents/squad.md +216 -10
  122. package/template/.aioson/locales/fr/agents/ux-ui.md +26 -25
  123. package/template/.aioson/locales/pt-BR/agents/analyst.md +26 -4
  124. package/template/.aioson/locales/pt-BR/agents/architect.md +10 -0
  125. package/template/.aioson/locales/pt-BR/agents/dev.md +93 -4
  126. package/template/.aioson/locales/pt-BR/agents/deyvin.md +129 -0
  127. package/template/.aioson/locales/pt-BR/agents/{genoma.md → genome.md} +49 -49
  128. package/template/.aioson/locales/pt-BR/agents/orache.md +137 -0
  129. package/template/.aioson/locales/pt-BR/agents/orchestrator.md +36 -2
  130. package/template/.aioson/locales/pt-BR/agents/pair.md +5 -0
  131. package/template/.aioson/locales/pt-BR/agents/pm.md +7 -0
  132. package/template/.aioson/locales/pt-BR/agents/product.md +35 -17
  133. package/template/.aioson/locales/pt-BR/agents/qa.md +7 -0
  134. package/template/.aioson/locales/pt-BR/agents/setup.md +51 -5
  135. package/template/.aioson/locales/pt-BR/agents/squad.md +486 -47
  136. package/template/.aioson/locales/pt-BR/agents/ux-ui.md +361 -22
  137. package/template/.aioson/my-agents/.gitkeep +0 -0
  138. package/template/.aioson/rules/.gitkeep +0 -0
  139. package/template/.aioson/rules/squad/.gitkeep +0 -0
  140. package/template/.aioson/rules/squad/README.md +50 -0
  141. package/template/.aioson/schemas/genome-meta.schema.json +1 -1
  142. package/template/.aioson/schemas/genome.schema.json +1 -1
  143. package/template/.aioson/schemas/squad-blueprint.schema.json +11 -0
  144. package/template/.aioson/schemas/squad-manifest.schema.json +257 -1
  145. package/template/.aioson/skills/design/cognitive-core-ui/SKILL.md +157 -0
  146. package/template/.aioson/skills/design/cognitive-core-ui/references/components.md +407 -0
  147. package/template/.aioson/skills/design/cognitive-core-ui/references/dashboards.md +172 -0
  148. package/template/.aioson/skills/design/cognitive-core-ui/references/design-tokens.md +490 -0
  149. package/template/.aioson/skills/design/cognitive-core-ui/references/motion.md +237 -0
  150. package/template/.aioson/skills/design/cognitive-core-ui/references/patterns.md +289 -0
  151. package/template/.aioson/skills/design/cognitive-core-ui/references/websites.md +350 -0
  152. package/template/.aioson/skills/design/interface-design/SKILL.md +47 -0
  153. package/template/.aioson/skills/design/interface-design/references/components-and-states.md +105 -0
  154. package/template/.aioson/skills/design/interface-design/references/design-directions.md +101 -0
  155. package/template/.aioson/skills/design/interface-design/references/handoff-and-quality.md +71 -0
  156. package/template/.aioson/skills/design/interface-design/references/intent-and-domain.md +74 -0
  157. package/template/.aioson/skills/design/interface-design/references/tokens-and-depth.md +173 -0
  158. package/template/.aioson/skills/design/premium-command-center-ui/SKILL.md +62 -0
  159. package/template/.aioson/skills/design/premium-command-center-ui/references/operations.md +74 -0
  160. package/template/.aioson/skills/design/premium-command-center-ui/references/patterns.md +116 -0
  161. package/template/.aioson/skills/design/premium-command-center-ui/references/validation.md +47 -0
  162. package/template/.aioson/skills/design/premium-command-center-ui/references/visual-system.md +215 -0
  163. package/template/.aioson/skills/design-system/SKILL.md +92 -0
  164. package/template/.aioson/skills/design-system/cognitive-core-ui.skill +0 -0
  165. package/template/.aioson/skills/design-system/components/SKILL.md +274 -0
  166. package/template/.aioson/skills/design-system/components/SKILL.md:Zone.Identifier +0 -0
  167. package/template/.aioson/skills/design-system/dashboards/SKILL.md +184 -0
  168. package/template/.aioson/skills/design-system/dashboards/SKILL.md:Zone.Identifier +0 -0
  169. package/template/.aioson/skills/design-system/foundations/SKILL.md +250 -0
  170. package/template/.aioson/skills/design-system/foundations/SKILL.md:Zone.Identifier +0 -0
  171. package/template/.aioson/skills/design-system/motion/SKILL.md +197 -0
  172. package/template/.aioson/skills/design-system/motion/SKILL.md:Zone.Identifier +0 -0
  173. package/template/.aioson/skills/design-system/patterns/SKILL.md +231 -0
  174. package/template/.aioson/skills/design-system/patterns/SKILL.md:Zone.Identifier +0 -0
  175. package/template/.aioson/skills/squad/SKILL.md +58 -0
  176. package/template/.aioson/skills/squad/domains/.gitkeep +0 -0
  177. package/template/.aioson/skills/squad/formats/.gitkeep +0 -0
  178. package/template/.aioson/skills/squad/patterns/.gitkeep +0 -0
  179. package/template/.aioson/skills/squad/references/.gitkeep +0 -0
  180. package/template/.aioson/tasks/implementation-plan.md +288 -0
  181. package/template/.aioson/tasks/squad-create.md +1 -1
  182. package/template/.aioson/tasks/squad-execution-plan.md +279 -0
  183. package/template/.aioson/tasks/squad-export.md +1 -1
  184. package/template/.aioson/tasks/squad-investigate.md +44 -0
  185. package/template/.aioson/tasks/squad-learning-review.md +44 -0
  186. package/template/.aioson/tasks/squad-output-config.md +177 -0
  187. package/template/.aioson/tasks/squad-validate.md +1 -1
  188. package/template/.claude/commands/aioson/agent/deyvin.md +5 -0
  189. package/template/.claude/commands/aioson/agent/discovery-design-doc.md +5 -0
  190. package/template/.claude/commands/aioson/agent/genome.md +5 -0
  191. package/template/.claude/commands/aioson/agent/product.md +5 -0
  192. package/template/.claude/commands/aioson/agent/profiler-enricher.md +5 -0
  193. package/template/.claude/commands/aioson/agent/profiler-forge.md +5 -0
  194. package/template/.claude/commands/aioson/agent/profiler-researcher.md +5 -0
  195. package/template/.claude/commands/aioson/agent/squad.md +5 -0
  196. package/template/.gemini/GEMINI.md +2 -0
  197. package/template/.gemini/commands/aios-deyvin.toml +6 -0
  198. package/template/.gemini/commands/aios-pair.toml +6 -0
  199. package/template/AGENTS.md +34 -6
  200. package/template/CLAUDE.md +31 -4
  201. package/template/OPENCODE.md +6 -2
  202. package/template/squad-searches/.gitkeep +0 -0
  203. package/template/.aioson/skills/static/interface-design.md +0 -372
  204. package/template/.aioson/skills/static/premium-command-center-ui.md +0 -190
  205. /package/template/.aioson/{genomas → docs}/.gitkeep +0 -0
  206. /package/template/.claude/commands/aioson/{analyst.md → agent/analyst.md} +0 -0
  207. /package/template/.claude/commands/aioson/{architect.md → agent/architect.md} +0 -0
  208. /package/template/.claude/commands/aioson/{dev.md → agent/dev.md} +0 -0
  209. /package/template/.claude/commands/aioson/{orchestrator.md → agent/orchestrator.md} +0 -0
  210. /package/template/.claude/commands/aioson/{pm.md → agent/pm.md} +0 -0
  211. /package/template/.claude/commands/aioson/{qa.md → agent/qa.md} +0 -0
  212. /package/template/.claude/commands/aioson/{setup.md → agent/setup.md} +0 -0
  213. /package/template/.claude/commands/aioson/{ux-ui.md → agent/ux-ui.md} +0 -0
@@ -0,0 +1,1583 @@
1
+ 'use strict';
2
+
3
+ const fs = require('node:fs/promises');
4
+ const path = require('node:path');
5
+ const { spawn } = require('node:child_process');
6
+ const {
7
+ resolveRuntimePaths,
8
+ openRuntimeDb,
9
+ runtimeStoreExists,
10
+ startTask,
11
+ updateTask,
12
+ startRun,
13
+ updateRun,
14
+ appendRunEvent,
15
+ readAgentSession,
16
+ writeAgentSession,
17
+ clearAgentSession
18
+ } = require('../runtime-store');
19
+ const { ensureDir, exists } = require('../utils');
20
+ const { SUPPORTED_PROMPT_TOOLS } = require('../prompt-tool');
21
+
22
+ const LIVE_EVENTS_LIMIT = 10;
23
+ const LIVE_MESSAGE_LIMIT = 500;
24
+
25
+ function resolveTargetDir(args) {
26
+ return path.resolve(process.cwd(), args[0] || '.');
27
+ }
28
+
29
+ function requireOption(options, key, t) {
30
+ const value = options[key];
31
+ if (value === undefined || value === null || String(value).trim() === '') {
32
+ throw new Error(t('runtime.option_required', { option: `--${key}` }));
33
+ }
34
+ return String(value).trim();
35
+ }
36
+
37
+ function normalizeAgentHandle(value) {
38
+ const text = String(value || '').trim();
39
+ if (!text) return '';
40
+ return text.startsWith('@') ? text : `@${text}`;
41
+ }
42
+
43
+ function makeDirectSessionKey(agentName) {
44
+ return `direct-session:${Date.now()}:${String(agentName || '').replace(/^@/, '')}`;
45
+ }
46
+
47
+ function parseWatchSeconds(value) {
48
+ if (value === undefined || value === null || value === false) return null;
49
+ if (value === true || value === '') return 2;
50
+
51
+ const parsed = Number(value);
52
+ if (!Number.isFinite(parsed) || parsed <= 0) return 2;
53
+ return parsed;
54
+ }
55
+
56
+ function sleep(ms) {
57
+ return new Promise((resolve) => setTimeout(resolve, ms));
58
+ }
59
+
60
+ async function withRuntimeDb(targetDir, t) {
61
+ const handle = await openRuntimeDb(targetDir, { mustExist: true });
62
+ if (!handle) {
63
+ throw new Error(t('runtime.store_missing', { path: resolveRuntimePaths(targetDir).dbPath }));
64
+ }
65
+ return handle;
66
+ }
67
+
68
+ function resolveLivePaths(runtimeDir, sessionKey) {
69
+ const sessionDir = path.join(runtimeDir, 'live', sessionKey);
70
+ return {
71
+ sessionDir,
72
+ statePath: path.join(sessionDir, 'state.json'),
73
+ eventsPath: path.join(sessionDir, 'events.ndjson'),
74
+ summaryPath: path.join(sessionDir, 'summary.md')
75
+ };
76
+ }
77
+
78
+ function truncateMessage(value, fallback = '') {
79
+ const text = String(value || fallback || '').trim();
80
+ if (!text) return '';
81
+ if (text.length <= LIVE_MESSAGE_LIMIT) return text;
82
+ return `${text.slice(0, LIVE_MESSAGE_LIMIT - 3).trimEnd()}...`;
83
+ }
84
+
85
+ function parseRefs(value) {
86
+ const text = String(value || '').trim();
87
+ if (!text) return [];
88
+ return Array.from(new Set(text.split(',').map((entry) => entry.trim()).filter(Boolean)));
89
+ }
90
+
91
+ function parseJsonOption(value) {
92
+ if (value === undefined || value === null || value === '') return null;
93
+ if (typeof value === 'object' && !Array.isArray(value)) return value;
94
+ try {
95
+ const parsed = JSON.parse(String(value));
96
+ return parsed && typeof parsed === 'object' ? parsed : { value: parsed };
97
+ } catch {
98
+ return { raw: String(value) };
99
+ }
100
+ }
101
+
102
+ function parseToolArgs(value) {
103
+ if (value === undefined || value === null || value === '') return [];
104
+ if (Array.isArray(value)) return value.map((entry) => String(entry));
105
+ const text = String(value).trim();
106
+ if (!text) return [];
107
+
108
+ if (text.startsWith('[')) {
109
+ try {
110
+ const parsed = JSON.parse(text);
111
+ if (Array.isArray(parsed)) {
112
+ return parsed.map((entry) => String(entry));
113
+ }
114
+ } catch {
115
+ // fallback to whitespace split below
116
+ }
117
+ }
118
+
119
+ return text.split(/\s+/).filter(Boolean);
120
+ }
121
+
122
+ function normalizeLiveTool(value, t) {
123
+ const tool = String(value || '').trim().toLowerCase();
124
+ if (SUPPORTED_PROMPT_TOOLS.has(tool)) {
125
+ return tool;
126
+ }
127
+ const supported = Array.from(SUPPORTED_PROMPT_TOOLS).join(', ');
128
+ throw new Error(t ? t('live.unsupported_tool', { tool: value, supported }) : `Unsupported live tool: ${value}. Supported: ${supported}`);
129
+ }
130
+
131
+
132
+ function normalizePlanStepId(value) {
133
+ return String(value || '').trim().replace(/\s+/g, ' ');
134
+ }
135
+
136
+ function collectPlanSteps(markdown) {
137
+ const steps = [];
138
+ const seen = new Set();
139
+
140
+ function pushStep(id, title) {
141
+ const normalizedId = normalizePlanStepId(id);
142
+ const normalizedTitle = String(title || '').trim();
143
+ if (!normalizedId || !normalizedTitle) return;
144
+ const key = normalizedId.toLowerCase();
145
+ if (seen.has(key)) return;
146
+ seen.add(key);
147
+ steps.push({ id: normalizedId, title: normalizedTitle, done: false });
148
+ }
149
+
150
+ const blockPattern = /<!--\s*aioson:steps([\s\S]*?)-->/gi;
151
+ let blockMatch = blockPattern.exec(markdown);
152
+ while (blockMatch) {
153
+ const lines = String(blockMatch[1] || '').split(/\r?\n/);
154
+ for (const line of lines) {
155
+ const trimmed = line.trim();
156
+ if (!trimmed) continue;
157
+ const match = trimmed.match(/^([^:]+):\s*(.+)$/);
158
+ if (match) {
159
+ pushStep(match[1], match[2]);
160
+ }
161
+ }
162
+ blockMatch = blockPattern.exec(markdown);
163
+ }
164
+
165
+ const headingPattern = /^#{3,6}\s+((?:[A-Z]{2,}(?:-[A-Z0-9]+)*-\d+(?:\.\d+)?)|(?:Fase\s+\d+(?:\.\d+)?))\s*[-:]\s+(.+)$/gim;
166
+ let headingMatch = headingPattern.exec(markdown);
167
+ while (headingMatch) {
168
+ pushStep(headingMatch[1], headingMatch[2]);
169
+ headingMatch = headingPattern.exec(markdown);
170
+ }
171
+
172
+ return steps;
173
+ }
174
+
175
+ async function loadPlanReference(targetDir, planRef) {
176
+ if (!planRef) {
177
+ return { planRef: null, planPath: null, planSteps: [] };
178
+ }
179
+
180
+ const planPath = path.isAbsolute(planRef) ? planRef : path.resolve(targetDir, planRef);
181
+ let markdown = '';
182
+ try {
183
+ markdown = await fs.readFile(planPath, 'utf8');
184
+ } catch {
185
+ throw new Error(`Plan file not found: ${planRef}`); // technical message, i18n at caller level
186
+ }
187
+
188
+ return {
189
+ planRef,
190
+ planPath,
191
+ planSteps: collectPlanSteps(markdown)
192
+ };
193
+ }
194
+
195
+
196
+ async function resolveExecutablePath(command) {
197
+ const binary = String(command || '').trim();
198
+ if (!binary) return null;
199
+
200
+ if (path.isAbsolute(binary) || binary.includes(path.sep)) {
201
+ const absolutePath = path.isAbsolute(binary) ? binary : path.resolve(binary);
202
+ return (await exists(absolutePath)) ? absolutePath : null;
203
+ }
204
+
205
+ const pathEntries = String(process.env.PATH || '')
206
+ .split(path.delimiter)
207
+ .map((entry) => entry.trim())
208
+ .filter(Boolean);
209
+
210
+ const extensions = process.platform === 'win32'
211
+ ? Array.from(new Set(['', ...String(process.env.PATHEXT || '.EXE;.CMD;.BAT;.COM').split(';').map((entry) => entry.toLowerCase())]))
212
+ : [''];
213
+
214
+ for (const dir of pathEntries) {
215
+ for (const ext of extensions) {
216
+ const candidate = process.platform === 'win32' && ext && !binary.toLowerCase().endsWith(ext)
217
+ ? path.join(dir, `${binary}${ext}`)
218
+ : path.join(dir, binary);
219
+ if (await exists(candidate)) {
220
+ return candidate;
221
+ }
222
+ }
223
+ }
224
+
225
+ return null;
226
+ }
227
+
228
+ function detectProcessState(pid) {
229
+ if (!pid) return 'not_tracked';
230
+ try {
231
+ process.kill(Number(pid), 0);
232
+ return 'alive';
233
+ } catch (error) {
234
+ if (error && error.code === 'ESRCH') {
235
+ return 'dead';
236
+ }
237
+ return 'unknown';
238
+ }
239
+ }
240
+
241
+ function normalizeLiveStats(stats, fallback = {}) {
242
+ return {
243
+ tasks_completed: Number(stats?.tasks_completed || 0),
244
+ events_total: Number(stats?.events_total || 0),
245
+ plan_steps_done: Number(stats?.plan_steps_done ?? fallback.plan_steps_done ?? 0),
246
+ plan_steps_total: Number(stats?.plan_steps_total ?? fallback.plan_steps_total ?? 0),
247
+ events_by_type: stats?.events_by_type && typeof stats.events_by_type === 'object'
248
+ ? { ...stats.events_by_type }
249
+ : {}
250
+ };
251
+ }
252
+
253
+ function parseTaskMeta(task) {
254
+ if (!task || !task.meta_json) return {};
255
+ try {
256
+ const parsed = JSON.parse(task.meta_json);
257
+ return parsed && typeof parsed === 'object' ? parsed : {};
258
+ } catch {
259
+ return {};
260
+ }
261
+ }
262
+
263
+ function getPlanStats(meta) {
264
+ const steps = Array.isArray(meta?.plan_steps) ? meta.plan_steps : [];
265
+ const done = steps.filter((step) => step && step.done).length;
266
+ return {
267
+ plan_steps_done: done,
268
+ plan_steps_total: steps.length
269
+ };
270
+ }
271
+
272
+ async function readJsonIfExists(filePath) {
273
+ try {
274
+ const raw = await fs.readFile(filePath, 'utf8');
275
+ return JSON.parse(raw);
276
+ } catch {
277
+ return null;
278
+ }
279
+ }
280
+
281
+ async function readLiveState(runtimeDir, sessionKey) {
282
+ if (!sessionKey) return null;
283
+ return readJsonIfExists(resolveLivePaths(runtimeDir, sessionKey).statePath);
284
+ }
285
+
286
+ async function writeLiveState(runtimeDir, sessionKey, state) {
287
+ try {
288
+ const { statePath } = resolveLivePaths(runtimeDir, sessionKey);
289
+ await ensureDir(path.dirname(statePath));
290
+ await fs.writeFile(statePath, JSON.stringify(state, null, 2), 'utf8');
291
+ } catch {
292
+ // filesystem is auxiliary — SQLite is source of truth
293
+ }
294
+ }
295
+
296
+ async function appendLiveEvent(runtimeDir, sessionKey, record) {
297
+ try {
298
+ const { eventsPath } = resolveLivePaths(runtimeDir, sessionKey);
299
+ await ensureDir(path.dirname(eventsPath));
300
+ await fs.appendFile(eventsPath, `${JSON.stringify(record)}\n`, 'utf8');
301
+ } catch {
302
+ // filesystem is auxiliary — SQLite is source of truth
303
+ }
304
+ }
305
+
306
+ async function writeLiveSummary(runtimeDir, sessionKey, markdown) {
307
+ try {
308
+ const { summaryPath } = resolveLivePaths(runtimeDir, sessionKey);
309
+ await ensureDir(path.dirname(summaryPath));
310
+ await fs.writeFile(summaryPath, markdown, 'utf8');
311
+ return summaryPath;
312
+ } catch {
313
+ return null;
314
+ }
315
+ }
316
+
317
+ async function listLiveStates(runtimeDir) {
318
+ const liveRoot = path.join(runtimeDir, 'live');
319
+ if (!(await exists(liveRoot))) {
320
+ return [];
321
+ }
322
+
323
+ const entries = await fs.readdir(liveRoot, { withFileTypes: true });
324
+ const states = [];
325
+ for (const entry of entries) {
326
+ if (!entry.isDirectory()) continue;
327
+ const state = await readJsonIfExists(path.join(liveRoot, entry.name, 'state.json'));
328
+ if (state) {
329
+ states.push(state);
330
+ }
331
+ }
332
+
333
+ states.sort((left, right) => {
334
+ const leftStamp = Date.parse(left.updated_at || left.closed_at || left.started_at || 0);
335
+ const rightStamp = Date.parse(right.updated_at || right.closed_at || right.started_at || 0);
336
+ return rightStamp - leftStamp;
337
+ });
338
+
339
+ return states;
340
+ }
341
+
342
+ function createLiveState(targetDir, run, task, options = {}) {
343
+ const taskMeta = parseTaskMeta(task);
344
+ const planStats = getPlanStats(taskMeta);
345
+ return {
346
+ session_key: run.session_key || task?.session_key || options.sessionKey || null,
347
+ session_task_key: task?.task_key || options.taskKey || null,
348
+ tool_session: options.tool || taskMeta.tool_session || null,
349
+ active_agent: options.activeAgent || run.agent_name || null,
350
+ plan_ref: options.planRef ?? taskMeta.plan_ref ?? null,
351
+ phase: options.phase || (run.status === 'running' || run.status === 'queued' ? 'active' : 'closed'),
352
+ title: options.title || task?.title || run.title || null,
353
+ path: options.projectPath || targetDir,
354
+ child_pid: options.childPid ?? taskMeta.child_pid ?? null,
355
+ started_at: options.startedAt || run.started_at || task?.created_at || null,
356
+ updated_at: options.updatedAt || run.updated_at || task?.updated_at || null,
357
+ closed_at: options.closedAt || (run.status === 'completed' || run.status === 'failed' ? run.finished_at || task?.finished_at || null : null),
358
+ current_task: options.currentTask ?? null,
359
+ current_run_key: options.currentRunKey || run.run_key,
360
+ stats: normalizeLiveStats(options.stats, planStats),
361
+ last_events: Array.isArray(options.lastEvents) ? options.lastEvents.slice(-LIVE_EVENTS_LIMIT) : []
362
+ };
363
+ }
364
+
365
+ function selectLiveRunByKey(db, runKey) {
366
+ if (!runKey) return null;
367
+ return db.prepare(`
368
+ SELECT
369
+ run_key, task_key, agent_name, agent_kind, squad_slug, session_key, source,
370
+ title, status, summary, output_path, started_at, updated_at, finished_at
371
+ FROM agent_runs
372
+ WHERE run_key = ? AND source = 'live'
373
+ LIMIT 1
374
+ `).get(String(runKey));
375
+ }
376
+
377
+ function selectLatestLiveRun(db, options = {}) {
378
+ if (options.sessionKey) {
379
+ return db.prepare(`
380
+ SELECT
381
+ run_key, task_key, agent_name, agent_kind, squad_slug, session_key, source,
382
+ title, status, summary, output_path, started_at, updated_at, finished_at
383
+ FROM agent_runs
384
+ WHERE source = 'live' AND session_key = ?
385
+ ORDER BY updated_at DESC, started_at DESC
386
+ LIMIT 1
387
+ `).get(String(options.sessionKey));
388
+ }
389
+
390
+ if (options.agentName) {
391
+ return db.prepare(`
392
+ SELECT
393
+ run_key, task_key, agent_name, agent_kind, squad_slug, session_key, source,
394
+ title, status, summary, output_path, started_at, updated_at, finished_at
395
+ FROM agent_runs
396
+ WHERE source = 'live' AND agent_name = ?
397
+ ORDER BY updated_at DESC, started_at DESC
398
+ LIMIT 1
399
+ `).get(String(options.agentName));
400
+ }
401
+
402
+ return db.prepare(`
403
+ SELECT
404
+ run_key, task_key, agent_name, agent_kind, squad_slug, session_key, source,
405
+ title, status, summary, output_path, started_at, updated_at, finished_at
406
+ FROM agent_runs
407
+ WHERE source = 'live'
408
+ ORDER BY updated_at DESC, started_at DESC
409
+ LIMIT 1
410
+ `).get();
411
+ }
412
+
413
+ function selectTaskByKey(db, taskKey) {
414
+ if (!taskKey) return null;
415
+ return db.prepare(`
416
+ SELECT
417
+ task_key, squad_slug, session_key, task_kind, parent_task_key,
418
+ title, goal, meta_json, status, created_by, created_at, updated_at, finished_at
419
+ FROM tasks
420
+ WHERE task_key = ?
421
+ LIMIT 1
422
+ `).get(String(taskKey));
423
+ }
424
+
425
+ function mapRecentDbEvent(event) {
426
+ return {
427
+ ts: event.created_at,
428
+ type: event.event_type,
429
+ summary: event.message || '-'
430
+ };
431
+ }
432
+
433
+ async function resolveLiveContext(targetDir, db, runtimeDir, options = {}) {
434
+ let agentName = options.agentName ? normalizeAgentHandle(options.agentName) : null;
435
+ let sessionRef = agentName ? await readAgentSession(runtimeDir, agentName) : null;
436
+ let sessionKey = options.sessionKey ? String(options.sessionKey).trim() : null;
437
+ let state = null;
438
+
439
+ if (!sessionKey && sessionRef?.sessionKey) {
440
+ sessionKey = String(sessionRef.sessionKey).trim();
441
+ }
442
+
443
+ if (!sessionKey && !agentName) {
444
+ const liveStates = await listLiveStates(runtimeDir);
445
+ if (liveStates.length > 0) {
446
+ state = liveStates[0];
447
+ sessionKey = state.session_key || null;
448
+ if (state.active_agent) {
449
+ agentName = normalizeAgentHandle(state.active_agent);
450
+ }
451
+ }
452
+ }
453
+
454
+ if (!state && sessionKey) {
455
+ state = await readLiveState(runtimeDir, sessionKey);
456
+ }
457
+
458
+ if (!agentName && state?.active_agent) {
459
+ agentName = normalizeAgentHandle(state.active_agent);
460
+ }
461
+
462
+ let run = sessionRef?.runKey ? selectLiveRunByKey(db, sessionRef.runKey) : null;
463
+ if (!run && state?.current_run_key) {
464
+ run = selectLiveRunByKey(db, state.current_run_key);
465
+ }
466
+ if (!run && sessionKey) {
467
+ run = selectLatestLiveRun(db, { sessionKey });
468
+ }
469
+ if (!run && agentName) {
470
+ run = selectLatestLiveRun(db, { agentName });
471
+ }
472
+ if (!run && !agentName && !sessionKey) {
473
+ run = selectLatestLiveRun(db);
474
+ }
475
+
476
+ let task = null;
477
+ if (run?.task_key) {
478
+ task = selectTaskByKey(db, run.task_key);
479
+ }
480
+ if (!task && sessionRef?.taskKey) {
481
+ task = selectTaskByKey(db, sessionRef.taskKey);
482
+ }
483
+ if (!task && state?.session_task_key) {
484
+ task = selectTaskByKey(db, state.session_task_key);
485
+ }
486
+
487
+ if (!sessionKey) {
488
+ sessionKey = run?.session_key || task?.session_key || state?.session_key || sessionRef?.sessionKey || null;
489
+ }
490
+
491
+ if (!state && sessionKey) {
492
+ state = await readLiveState(runtimeDir, sessionKey);
493
+ }
494
+
495
+ if (run && !state) {
496
+ state = createLiveState(targetDir, run, task, {
497
+ sessionKey,
498
+ activeAgent: agentName || run.agent_name,
499
+ projectPath: targetDir
500
+ });
501
+ }
502
+
503
+ const recentEvents = run
504
+ ? db.prepare(`
505
+ SELECT event_type, message, created_at
506
+ FROM execution_events
507
+ WHERE run_key = ?
508
+ ORDER BY created_at DESC, id DESC
509
+ LIMIT ?
510
+ `).all(run.run_key, Math.max(1, Math.min(Number(options.limit) || 8, 20))).reverse().map(mapRecentDbEvent)
511
+ : [];
512
+
513
+ const processState = detectProcessState(state?.child_pid);
514
+ const phase = state?.phase || (run && (run.status === 'running' || run.status === 'queued') ? 'active' : run ? 'closed' : 'idle');
515
+ const open = phase === 'active' && Boolean(run && (run.status === 'running' || run.status === 'queued'));
516
+
517
+ return {
518
+ agentName: agentName || state?.active_agent || run?.agent_name || null,
519
+ sessionRef,
520
+ sessionKey,
521
+ run,
522
+ task,
523
+ state,
524
+ recentEvents,
525
+ processState,
526
+ phase,
527
+ open,
528
+ paths: sessionKey ? resolveLivePaths(runtimeDir, sessionKey) : null
529
+ };
530
+ }
531
+
532
+ async function requireActiveLiveContext(targetDir, agentName, t, options = {}) {
533
+ const { db, dbPath, runtimeDir } = await withRuntimeDb(targetDir, t);
534
+ const context = await resolveLiveContext(targetDir, db, runtimeDir, {
535
+ agentName,
536
+ limit: options.limit
537
+ });
538
+
539
+ if (!context.run || context.run.source !== 'live' || !context.sessionKey || !context.task) {
540
+ db.close();
541
+ throw new Error(t('live.no_active_session', { agent: normalizeAgentHandle(agentName) }));
542
+ }
543
+
544
+ if (context.phase !== 'active') {
545
+ db.close();
546
+ throw new Error(t('live.session_not_active', { agent: normalizeAgentHandle(agentName) }));
547
+ }
548
+
549
+ return { db, dbPath, runtimeDir, context };
550
+ }
551
+
552
+ function applyEventToState(state, event, updates = {}) {
553
+ const next = {
554
+ ...state,
555
+ updated_at: event.ts,
556
+ stats: normalizeLiveStats(state?.stats)
557
+ };
558
+
559
+ next.last_events = [...(Array.isArray(state?.last_events) ? state.last_events : []), {
560
+ ts: event.ts,
561
+ type: event.type,
562
+ summary: event.summary
563
+ }].slice(-LIVE_EVENTS_LIMIT);
564
+ next.stats.events_total += 1;
565
+ if (event.type) {
566
+ next.stats.events_by_type[event.type] = (next.stats.events_by_type[event.type] || 0) + 1;
567
+ }
568
+
569
+ if (event.type === 'task_completed') {
570
+ next.stats.tasks_completed += 1;
571
+ }
572
+
573
+ if (Object.prototype.hasOwnProperty.call(updates, 'currentTask')) {
574
+ next.current_task = updates.currentTask;
575
+ }
576
+ if (Object.prototype.hasOwnProperty.call(updates, 'phase')) {
577
+ next.phase = updates.phase;
578
+ }
579
+ if (Object.prototype.hasOwnProperty.call(updates, 'closedAt')) {
580
+ next.closed_at = updates.closedAt;
581
+ }
582
+ if (Object.prototype.hasOwnProperty.call(updates, 'activeAgent')) {
583
+ next.active_agent = updates.activeAgent;
584
+ }
585
+ if (Object.prototype.hasOwnProperty.call(updates, 'currentRunKey')) {
586
+ next.current_run_key = updates.currentRunKey;
587
+ }
588
+ if (Object.prototype.hasOwnProperty.call(updates, 'childPid')) {
589
+ next.child_pid = updates.childPid;
590
+ }
591
+ if (updates.planStats) {
592
+ next.stats.plan_steps_done = Number(updates.planStats.plan_steps_done || 0);
593
+ next.stats.plan_steps_total = Number(updates.planStats.plan_steps_total || 0);
594
+ }
595
+
596
+ return next;
597
+ }
598
+
599
+ function createLiveEventRecord(context, options = {}) {
600
+ return {
601
+ ts: options.ts,
602
+ type: options.type,
603
+ summary: options.summary,
604
+ agent: context.agentName,
605
+ task_key: options.taskKey || context.task?.task_key || null,
606
+ run_key: context.run?.run_key || null,
607
+ session_key: context.sessionKey,
608
+ refs: options.refs || [],
609
+ plan_step: options.planStep || null,
610
+ status: options.status || null,
611
+ meta: options.meta || null
612
+ };
613
+ }
614
+
615
+ function waitForChild(child) {
616
+ return new Promise((resolve, reject) => {
617
+ child.once('error', reject);
618
+ child.once('close', (code, signal) => {
619
+ resolve({ code: Number(code || 0), signal: signal || null });
620
+ });
621
+ });
622
+ }
623
+
624
+ async function runLocalProcess(command, args, options = {}) {
625
+ return new Promise((resolve) => {
626
+ const child = spawn(command, args.map((entry) => String(entry)), {
627
+ cwd: options.cwd || process.cwd(),
628
+ env: { ...process.env, ...(options.env || {}) },
629
+ stdio: ['ignore', 'pipe', 'pipe']
630
+ });
631
+
632
+ let stdout = '';
633
+ let stderr = '';
634
+ child.stdout.on('data', (chunk) => {
635
+ stdout += String(chunk);
636
+ });
637
+ child.stderr.on('data', (chunk) => {
638
+ stderr += String(chunk);
639
+ });
640
+ child.on('error', () => {
641
+ resolve({ code: 1, stdout, stderr });
642
+ });
643
+ child.on('close', (code) => {
644
+ resolve({ code: Number(code || 0), stdout, stderr });
645
+ });
646
+ });
647
+ }
648
+
649
+ async function collectGitSnapshot(targetDir) {
650
+ if (!(await exists(path.join(targetDir, '.git')))) {
651
+ return null;
652
+ }
653
+
654
+ const [branch, commit, diffStat, status] = await Promise.all([
655
+ runLocalProcess('git', ['rev-parse', '--abbrev-ref', 'HEAD'], { cwd: targetDir }),
656
+ runLocalProcess('git', ['rev-parse', '--short', 'HEAD'], { cwd: targetDir }),
657
+ runLocalProcess('git', ['diff', '--stat'], { cwd: targetDir }),
658
+ runLocalProcess('git', ['status', '--short'], { cwd: targetDir })
659
+ ]);
660
+
661
+ if (branch.code !== 0 && commit.code !== 0 && diffStat.code !== 0 && status.code !== 0) {
662
+ return null;
663
+ }
664
+
665
+ return {
666
+ branch: branch.code === 0 ? branch.stdout.trim() : null,
667
+ commit: commit.code === 0 ? commit.stdout.trim() : null,
668
+ diff_stat: diffStat.code === 0 ? diffStat.stdout.trim() : null,
669
+ changed_files: status.code === 0
670
+ ? status.stdout.split(/\r?\n/).map((line) => line.trim()).filter(Boolean).map((line) => line.slice(3).trim() || line)
671
+ : []
672
+ };
673
+ }
674
+
675
+ function formatDuration(startedAt, closedAt) {
676
+ if (!startedAt || !closedAt) return null;
677
+ const ms = Date.parse(closedAt) - Date.parse(startedAt);
678
+ if (!Number.isFinite(ms) || ms < 0) return null;
679
+ const seconds = Math.floor(ms / 1000);
680
+ const hours = Math.floor(seconds / 3600);
681
+ const minutes = Math.floor((seconds % 3600) / 60);
682
+ const secs = seconds % 60;
683
+ if (hours > 0) return `${hours}h ${minutes}m`;
684
+ if (minutes > 0) return `${minutes}m ${secs}s`;
685
+ return `${secs}s`;
686
+ }
687
+
688
+ function renderLiveSummary(snapshot) {
689
+ const duration = formatDuration(snapshot.startedAt, snapshot.closedAt);
690
+ const lines = [
691
+ '# Live Session Summary',
692
+ '',
693
+ `- Session: ${snapshot.sessionKey}`,
694
+ `- Agent: ${snapshot.agent}`,
695
+ `- Tool: ${snapshot.tool || 'unknown'}`,
696
+ `- Status: ${snapshot.status}`,
697
+ `- Started: ${snapshot.startedAt || 'unknown'}`,
698
+ `- Closed: ${snapshot.closedAt || 'unknown'}`,
699
+ ...(duration ? [`- Duration: ${duration}`] : []),
700
+ `- Summary: ${snapshot.summary || 'n/a'}`
701
+ ];
702
+
703
+ if (snapshot.git) {
704
+ lines.push('');
705
+ lines.push('## Git');
706
+ lines.push(`- Branch: ${snapshot.git.branch || 'unknown'}`);
707
+ lines.push(`- Commit: ${snapshot.git.commit || 'unknown'}`);
708
+ if (snapshot.git.diff_stat) {
709
+ lines.push('');
710
+ lines.push('```text');
711
+ lines.push(snapshot.git.diff_stat);
712
+ lines.push('```');
713
+ }
714
+ if (snapshot.git.changed_files.length > 0) {
715
+ lines.push('');
716
+ lines.push('## Changed Files');
717
+ for (const file of snapshot.git.changed_files) {
718
+ lines.push(`- ${file}`);
719
+ }
720
+ }
721
+ }
722
+
723
+ if (Array.isArray(snapshot.recentEvents) && snapshot.recentEvents.length > 0) {
724
+ lines.push('');
725
+ lines.push('## Recent Events');
726
+ for (const event of snapshot.recentEvents) {
727
+ lines.push(`- ${event.ts} | ${event.type} | ${event.summary}`);
728
+ }
729
+ }
730
+
731
+ lines.push('');
732
+ return lines.join('\n');
733
+ }
734
+
735
+ function printLiveStatusSnapshot(snapshot, logger) {
736
+ logger.log(`Live session: ${snapshot.sessionKey || 'none'}`);
737
+ logger.log(`Phase: ${snapshot.phase}`);
738
+ logger.log(`Tool: ${snapshot.tool || '-'}`);
739
+ logger.log(`Active agent: ${snapshot.agent || '-'}`);
740
+ if (snapshot.stats && Number(snapshot.stats.plan_steps_total || 0) > 0) {
741
+ logger.log(`Plan: ${snapshot.stats.plan_steps_done || 0}/${snapshot.stats.plan_steps_total || 0}`);
742
+ }
743
+ logger.log(`Process: ${snapshot.processState}${snapshot.pid ? ` (pid ${snapshot.pid})` : ''}`);
744
+
745
+ if (snapshot.task) {
746
+ logger.log(`Task: ${snapshot.task.task_key} | status: ${snapshot.task.status} | work: ${snapshot.task.title || '-'}`);
747
+ }
748
+ if (snapshot.run) {
749
+ logger.log(`Run: ${snapshot.run.run_key} | status: ${snapshot.run.status} | work: ${snapshot.run.title || snapshot.run.summary || '-'}`);
750
+ }
751
+ if (snapshot.startedAt) {
752
+ logger.log(`Started: ${snapshot.startedAt}`);
753
+ }
754
+ if (snapshot.updatedAt) {
755
+ logger.log(`Updated: ${snapshot.updatedAt}`);
756
+ }
757
+ if (snapshot.closedAt) {
758
+ logger.log(`Closed: ${snapshot.closedAt}`);
759
+ }
760
+ if (snapshot.warning) {
761
+ logger.log(`Warning: ${snapshot.warning}`);
762
+ }
763
+
764
+ if (snapshot.recentEvents.length === 0) {
765
+ logger.log('Recent events: none');
766
+ return;
767
+ }
768
+
769
+ logger.log('Recent events:');
770
+ for (const event of snapshot.recentEvents) {
771
+ logger.log(`- ${event.ts} | ${event.type} | ${event.summary || '-'}`);
772
+ }
773
+ }
774
+
775
+ async function getLiveStatusSnapshot(targetDir, t, options = {}) {
776
+ const { dbPath } = resolveRuntimePaths(targetDir);
777
+
778
+ if (!(await runtimeStoreExists(targetDir))) {
779
+ throw new Error(t('runtime.store_missing', { path: dbPath }));
780
+ }
781
+
782
+ const { db, runtimeDir } = await openRuntimeDb(targetDir, { mustExist: true });
783
+ try {
784
+ const context = await resolveLiveContext(targetDir, db, runtimeDir, {
785
+ agentName: options.agent,
786
+ limit: options.limit
787
+ });
788
+
789
+ if (!context.run && !context.state) {
790
+ return {
791
+ ok: true,
792
+ targetDir,
793
+ dbPath,
794
+ agent: context.agentName,
795
+ tool: null,
796
+ phase: 'idle',
797
+ open: false,
798
+ processState: 'not_tracked',
799
+ pid: null,
800
+ sessionKey: null,
801
+ startedAt: null,
802
+ updatedAt: null,
803
+ closedAt: null,
804
+ title: null,
805
+ currentTask: null,
806
+ run: null,
807
+ task: null,
808
+ stats: normalizeLiveStats(null),
809
+ recentEvents: []
810
+ };
811
+ }
812
+
813
+ const taskMeta = parseTaskMeta(context.task);
814
+ const planStats = getPlanStats(taskMeta);
815
+ const state = context.state || createLiveState(targetDir, context.run, context.task, {
816
+ sessionKey: context.sessionKey,
817
+ activeAgent: context.agentName,
818
+ projectPath: targetDir
819
+ });
820
+ state.stats = normalizeLiveStats(state.stats, planStats);
821
+
822
+ const snapshot = {
823
+ ok: true,
824
+ targetDir,
825
+ dbPath,
826
+ agent: context.agentName,
827
+ tool: state.tool_session || null,
828
+ phase: context.phase,
829
+ open: context.open,
830
+ processState: context.processState,
831
+ pid: state.child_pid || null,
832
+ sessionKey: context.sessionKey,
833
+ startedAt: state.started_at || null,
834
+ updatedAt: state.updated_at || null,
835
+ closedAt: state.closed_at || null,
836
+ title: state.title || null,
837
+ currentTask: state.current_task || null,
838
+ run: context.run,
839
+ task: context.task,
840
+ stats: state.stats,
841
+ recentEvents: Array.isArray(state.last_events) && state.last_events.length > 0 ? state.last_events : context.recentEvents,
842
+ warning: context.processState === 'dead' && context.phase === 'active'
843
+ ? t('live.process_dead_warning')
844
+ : null
845
+ };
846
+
847
+ return snapshot;
848
+ } finally {
849
+ db.close();
850
+ }
851
+ }
852
+
853
+ async function runLiveStart({ args, options = {}, logger, t }) {
854
+ const targetDir = resolveTargetDir(args);
855
+ const agentName = normalizeAgentHandle(requireOption(options, 'agent', t));
856
+ const tool = normalizeLiveTool(requireOption(options, 'tool', t), t);
857
+ const noLaunch = Boolean(options['no-launch']);
858
+
859
+ if (options.json && !noLaunch && !options.attach) {
860
+ throw new Error(t('live.json_requires_no_launch'));
861
+ }
862
+
863
+ const toolBinary = String(options['tool-bin'] || tool).trim();
864
+ const binaryPath = await resolveExecutablePath(toolBinary);
865
+ if (!binaryPath) {
866
+ throw new Error(t('live.tool_binary_not_found', { binary: toolBinary }));
867
+ }
868
+
869
+ const { db, dbPath, runtimeDir } = await openRuntimeDb(targetDir);
870
+
871
+ try {
872
+ const existing = await resolveLiveContext(targetDir, db, runtimeDir, {
873
+ agentName,
874
+ limit: options.limit
875
+ });
876
+
877
+ if (existing.run && existing.run.source === 'live' && existing.open) {
878
+ const state = existing.state || createLiveState(targetDir, existing.run, existing.task, {
879
+ sessionKey: existing.sessionKey,
880
+ activeAgent: existing.agentName,
881
+ projectPath: targetDir
882
+ });
883
+
884
+ const existingTool = state.tool_session || null;
885
+ if (existingTool && existingTool !== tool) {
886
+ throw new Error(t('live.tool_mismatch', { existing: existingTool, requested: tool }));
887
+ }
888
+
889
+ const attach = Boolean(options.attach);
890
+ let attachChild = null;
891
+ let attachResult = null;
892
+
893
+ if (attach && !noLaunch) {
894
+ attachChild = spawn(binaryPath, parseToolArgs(options['tool-args'] || options.toolArgs), {
895
+ cwd: targetDir,
896
+ env: process.env,
897
+ stdio: 'inherit'
898
+ });
899
+ state.child_pid = attachChild.pid || null;
900
+ if (existing.task?.task_key) {
901
+ const taskMeta = parseTaskMeta(existing.task);
902
+ taskMeta.child_pid = state.child_pid;
903
+ updateTask(db, { taskKey: existing.task.task_key, metaJson: taskMeta });
904
+ }
905
+ }
906
+
907
+ await writeLiveState(runtimeDir, existing.sessionKey, state);
908
+
909
+ if (!options.json) {
910
+ logger.log(t('live.session_already_active', { agent: agentName, session: existing.sessionKey, runKey: existing.run.run_key, dbPath }));
911
+ }
912
+
913
+ if (attachChild) {
914
+ attachResult = await waitForChild(attachChild);
915
+ }
916
+
917
+ return {
918
+ ok: true,
919
+ targetDir,
920
+ dbPath,
921
+ agent: existing.agentName,
922
+ tool: state.tool_session || tool,
923
+ taskKey: existing.task?.task_key || existing.sessionRef?.taskKey || null,
924
+ runKey: existing.run.run_key,
925
+ sessionKey: existing.sessionKey,
926
+ pid: state.child_pid || null,
927
+ processState: detectProcessState(state.child_pid),
928
+ reused: true,
929
+ open: true,
930
+ attached: attach,
931
+ childExitCode: attachResult?.code ?? null,
932
+ childSignal: attachResult?.signal ?? null
933
+ };
934
+ }
935
+
936
+ const now = new Date().toISOString();
937
+ const sessionKey = options.session ? String(options.session).trim() : makeDirectSessionKey(agentName);
938
+ const title = options.title ? String(options.title).trim() : `live-${tool}-${Date.now()}`;
939
+ const goal = options.goal ? String(options.goal).trim() : null;
940
+ const planRef = options.plan ? String(options.plan).trim() : null;
941
+ const plan = await loadPlanReference(targetDir, planRef);
942
+ const startMessage = truncateMessage(options.message, `Live session started for ${agentName} with ${tool}`);
943
+ const taskMeta = {
944
+ tool_session: tool,
945
+ plan_ref: plan.planRef,
946
+ path: targetDir,
947
+ child_pid: null
948
+ };
949
+ if (plan.planSteps.length > 0) {
950
+ taskMeta.plan_steps = plan.planSteps;
951
+ }
952
+
953
+ const taskKey = startTask(db, {
954
+ sessionKey,
955
+ title,
956
+ goal,
957
+ status: 'running',
958
+ createdBy: agentName,
959
+ taskKind: 'live_session',
960
+ metaJson: taskMeta
961
+ });
962
+
963
+ const runKey = startRun(db, {
964
+ taskKey,
965
+ agentName,
966
+ agentKind: 'official',
967
+ sessionKey,
968
+ source: 'live',
969
+ title,
970
+ eventType: 'session_started',
971
+ phase: 'live',
972
+ message: startMessage,
973
+ payload: {
974
+ tool_session: tool,
975
+ plan_ref: plan.planRef,
976
+ plan_steps_total: plan.planSteps.length,
977
+ path: targetDir
978
+ }
979
+ });
980
+
981
+ let child = null;
982
+ let childResult = null;
983
+ if (!noLaunch) {
984
+ child = spawn(binaryPath, parseToolArgs(options['tool-args'] || options.toolArgs), {
985
+ cwd: targetDir,
986
+ env: process.env,
987
+ stdio: 'inherit'
988
+ });
989
+ taskMeta.child_pid = child.pid || null;
990
+ updateTask(db, {
991
+ taskKey,
992
+ metaJson: taskMeta
993
+ });
994
+ }
995
+
996
+ await writeAgentSession(runtimeDir, agentName, {
997
+ runKey,
998
+ taskKey,
999
+ sessionKey,
1000
+ startedAt: now,
1001
+ finished: false,
1002
+ source: 'live'
1003
+ });
1004
+
1005
+ const state = createLiveState(targetDir, {
1006
+ run_key: runKey,
1007
+ session_key: sessionKey,
1008
+ agent_name: agentName,
1009
+ title,
1010
+ status: 'running',
1011
+ started_at: now,
1012
+ updated_at: now
1013
+ }, {
1014
+ task_key: taskKey,
1015
+ session_key: sessionKey,
1016
+ title,
1017
+ meta_json: JSON.stringify(taskMeta),
1018
+ created_at: now,
1019
+ updated_at: now
1020
+ }, {
1021
+ tool,
1022
+ planRef: plan.planRef,
1023
+ activeAgent: agentName,
1024
+ currentRunKey: runKey,
1025
+ projectPath: targetDir,
1026
+ childPid: taskMeta.child_pid,
1027
+ stats: {
1028
+ tasks_completed: 0,
1029
+ events_total: 1,
1030
+ plan_steps_done: 0,
1031
+ plan_steps_total: plan.planSteps.length
1032
+ },
1033
+ lastEvents: [{
1034
+ ts: now,
1035
+ type: 'session_started',
1036
+ summary: startMessage
1037
+ }]
1038
+ });
1039
+
1040
+ await writeLiveState(runtimeDir, sessionKey, state);
1041
+ await appendLiveEvent(runtimeDir, sessionKey, {
1042
+ ts: now,
1043
+ type: 'session_started',
1044
+ summary: startMessage,
1045
+ agent: agentName,
1046
+ task_key: taskKey,
1047
+ run_key: runKey,
1048
+ session_key: sessionKey,
1049
+ refs: [],
1050
+ plan_step: null,
1051
+ status: 'running',
1052
+ meta: {
1053
+ tool_session: tool,
1054
+ child_pid: taskMeta.child_pid,
1055
+ path: targetDir,
1056
+ plan_ref: plan.planRef,
1057
+ plan_steps_total: plan.planSteps.length
1058
+ }
1059
+ });
1060
+
1061
+ if (!options.json) {
1062
+ logger.log(t('live.session_started', { agent: agentName, tool, session: sessionKey, dbPath }));
1063
+ }
1064
+
1065
+ if (child) {
1066
+ childResult = await waitForChild(child);
1067
+ }
1068
+
1069
+ return {
1070
+ ok: true,
1071
+ targetDir,
1072
+ dbPath,
1073
+ agent: agentName,
1074
+ tool,
1075
+ taskKey,
1076
+ runKey,
1077
+ sessionKey,
1078
+ pid: taskMeta.child_pid,
1079
+ processState: detectProcessState(taskMeta.child_pid),
1080
+ reused: false,
1081
+ open: true,
1082
+ childExitCode: childResult?.code ?? null,
1083
+ childSignal: childResult?.signal ?? null
1084
+ };
1085
+ } finally {
1086
+ db.close();
1087
+ }
1088
+ }
1089
+
1090
+ async function runRuntimeEmit({ args, options = {}, logger, t }) {
1091
+ const targetDir = resolveTargetDir(args);
1092
+ const agentName = normalizeAgentHandle(requireOption(options, 'agent', t));
1093
+ const eventType = String(options.type || 'note').trim() || 'note';
1094
+
1095
+ const { db, dbPath, runtimeDir, context } = await requireActiveLiveContext(targetDir, agentName, t, {
1096
+ limit: options.limit
1097
+ });
1098
+
1099
+ try {
1100
+ const now = new Date().toISOString();
1101
+ const refs = parseRefs(options.refs);
1102
+ const planStep = options['plan-step'] ? String(options['plan-step']).trim() : null;
1103
+ const summary = truncateMessage(
1104
+ options.summary || options.message || options.title || `${eventType} emitted by ${agentName}`
1105
+ );
1106
+ const meta = parseJsonOption(options.meta);
1107
+ const payload = meta && typeof meta === 'object' ? { ...meta } : {};
1108
+ if (refs.length > 0) payload.refs = refs;
1109
+ if (planStep) payload.plan_step = planStep;
1110
+
1111
+ const state = context.state || createLiveState(targetDir, context.run, context.task, {
1112
+ sessionKey: context.sessionKey,
1113
+ activeAgent: context.agentName,
1114
+ projectPath: targetDir
1115
+ });
1116
+
1117
+ let currentTaskKey = state.current_task || null;
1118
+ let nextCurrentTask = currentTaskKey;
1119
+
1120
+ if (eventType === 'task_started') {
1121
+ if (currentTaskKey) {
1122
+ throw new Error(t('live.micro_task_already_open', { agent: agentName }));
1123
+ }
1124
+
1125
+ currentTaskKey = startTask(db, {
1126
+ sessionKey: context.sessionKey,
1127
+ title: options.title ? String(options.title).trim() : summary,
1128
+ goal: summary,
1129
+ status: 'running',
1130
+ createdBy: agentName,
1131
+ taskKind: 'micro_task',
1132
+ parentTaskKey: context.task.task_key,
1133
+ metaJson: {
1134
+ refs,
1135
+ plan_step: planStep
1136
+ }
1137
+ });
1138
+ nextCurrentTask = currentTaskKey;
1139
+ payload.micro_task_key = currentTaskKey;
1140
+ } else if (eventType === 'task_completed') {
1141
+ if (currentTaskKey) {
1142
+ updateTask(db, {
1143
+ taskKey: currentTaskKey,
1144
+ status: 'completed',
1145
+ goal: summary,
1146
+ metaJson: {
1147
+ refs,
1148
+ plan_step: planStep
1149
+ }
1150
+ });
1151
+ } else {
1152
+ currentTaskKey = startTask(db, {
1153
+ sessionKey: context.sessionKey,
1154
+ title: options.title ? String(options.title).trim() : summary,
1155
+ goal: summary,
1156
+ status: 'completed',
1157
+ createdBy: agentName,
1158
+ taskKind: 'micro_task',
1159
+ parentTaskKey: context.task.task_key,
1160
+ metaJson: {
1161
+ refs,
1162
+ plan_step: planStep,
1163
+ implicit: true
1164
+ }
1165
+ });
1166
+ }
1167
+ nextCurrentTask = null;
1168
+ payload.micro_task_key = currentTaskKey;
1169
+ }
1170
+
1171
+ let planStats = null;
1172
+ if (eventType === 'plan_checkpoint' && planStep) {
1173
+ const sessionMeta = parseTaskMeta(context.task);
1174
+ if (Array.isArray(sessionMeta.plan_steps)) {
1175
+ const normalizedPlanStep = normalizePlanStepId(planStep).toLowerCase();
1176
+ let changed = false;
1177
+ sessionMeta.plan_steps = sessionMeta.plan_steps.map((step) => {
1178
+ if (!step || normalizePlanStepId(step.id).toLowerCase() !== normalizedPlanStep) return step;
1179
+ if (step.done) return step;
1180
+ changed = true;
1181
+ return { ...step, done: true };
1182
+ });
1183
+ if (changed) {
1184
+ updateTask(db, {
1185
+ taskKey: context.task.task_key,
1186
+ metaJson: sessionMeta
1187
+ });
1188
+ planStats = getPlanStats(sessionMeta);
1189
+ }
1190
+ }
1191
+ }
1192
+
1193
+ appendRunEvent(db, {
1194
+ runKey: context.run.run_key,
1195
+ eventType,
1196
+ phase: 'live',
1197
+ status: context.run.status || 'running',
1198
+ message: summary,
1199
+ payload: Object.keys(payload).length > 0 ? payload : null,
1200
+ createdAt: now
1201
+ });
1202
+
1203
+ const eventRecord = createLiveEventRecord(context, {
1204
+ ts: now,
1205
+ type: eventType,
1206
+ summary,
1207
+ refs,
1208
+ planStep,
1209
+ taskKey: currentTaskKey,
1210
+ meta: meta && Object.keys(meta).length > 0 ? meta : null
1211
+ });
1212
+
1213
+ await appendLiveEvent(runtimeDir, context.sessionKey, eventRecord);
1214
+
1215
+ const nextState = applyEventToState(state, eventRecord, {
1216
+ currentTask: nextCurrentTask,
1217
+ planStats,
1218
+ activeAgent: context.agentName,
1219
+ currentRunKey: context.run.run_key
1220
+ });
1221
+ await writeLiveState(runtimeDir, context.sessionKey, nextState);
1222
+
1223
+ if (!options.json) {
1224
+ logger.log(t('live.event_recorded', { agent: agentName, eventType, session: context.sessionKey, dbPath }));
1225
+ }
1226
+
1227
+ return {
1228
+ ok: true,
1229
+ targetDir,
1230
+ dbPath,
1231
+ agent: context.agentName,
1232
+ eventType,
1233
+ sessionKey: context.sessionKey,
1234
+ runKey: context.run.run_key,
1235
+ taskKey: currentTaskKey || context.task.task_key,
1236
+ currentTask: nextCurrentTask,
1237
+ open: true
1238
+ };
1239
+ } finally {
1240
+ db.close();
1241
+ }
1242
+ }
1243
+
1244
+
1245
+ async function runLiveHandoff({ args, options = {}, logger, t }) {
1246
+ const targetDir = resolveTargetDir(args);
1247
+ const agentName = normalizeAgentHandle(requireOption(options, 'agent', t));
1248
+ const nextAgent = normalizeAgentHandle(requireOption(options, 'to', t));
1249
+
1250
+ if (agentName === nextAgent) {
1251
+ throw new Error(t('live.handoff_same_agent'));
1252
+ }
1253
+
1254
+ const reason = truncateMessage(
1255
+ options.reason || options.summary || options.message,
1256
+ `Handoff from ${agentName} to ${nextAgent}`
1257
+ );
1258
+
1259
+ const { db, dbPath, runtimeDir, context } = await requireActiveLiveContext(targetDir, agentName, t, {
1260
+ limit: options.limit
1261
+ });
1262
+
1263
+ try {
1264
+ if (!context.run || context.run.agent_name !== agentName) {
1265
+ throw new Error(t('live.handoff_agent_mismatch', { agent: agentName }));
1266
+ }
1267
+
1268
+ const now = new Date().toISOString();
1269
+ const state = context.state || createLiveState(targetDir, context.run, context.task, {
1270
+ sessionKey: context.sessionKey,
1271
+ activeAgent: context.agentName,
1272
+ projectPath: targetDir
1273
+ });
1274
+
1275
+ const handoffSummary = truncateMessage(`Handoff to ${nextAgent}: ${reason}`);
1276
+ let currentTaskClosed = false;
1277
+ if (state.current_task) {
1278
+ updateTask(db, {
1279
+ taskKey: state.current_task,
1280
+ status: 'completed',
1281
+ goal: truncateMessage(`Closed on handoff to ${nextAgent}: ${reason}`)
1282
+ });
1283
+ currentTaskClosed = true;
1284
+ }
1285
+
1286
+ updateRun(db, {
1287
+ runKey: context.run.run_key,
1288
+ status: 'completed',
1289
+ summary: reason,
1290
+ eventType: 'handoff',
1291
+ phase: 'live',
1292
+ message: handoffSummary,
1293
+ payload: {
1294
+ from: agentName,
1295
+ to: nextAgent,
1296
+ reason,
1297
+ previous_run_key: context.run.run_key,
1298
+ micro_task_key: state.current_task || null,
1299
+ closed_by: 'live:handoff'
1300
+ }
1301
+ });
1302
+
1303
+ const nextRunKey = startRun(db, {
1304
+ taskKey: context.task.task_key,
1305
+ agentName: nextAgent,
1306
+ agentKind: 'official',
1307
+ sessionKey: context.sessionKey,
1308
+ source: 'live',
1309
+ parentRunKey: context.run.run_key,
1310
+ title: nextAgent,
1311
+ phase: 'live',
1312
+ message: truncateMessage(`Live handoff from ${agentName}`),
1313
+ payload: {
1314
+ handoff_from: agentName,
1315
+ reason
1316
+ }
1317
+ });
1318
+
1319
+ await clearAgentSession(runtimeDir, agentName);
1320
+ await writeAgentSession(runtimeDir, nextAgent, {
1321
+ runKey: nextRunKey,
1322
+ taskKey: context.task.task_key,
1323
+ sessionKey: context.sessionKey,
1324
+ startedAt: now,
1325
+ finished: false,
1326
+ source: 'live'
1327
+ });
1328
+
1329
+ const eventRecord = createLiveEventRecord(context, {
1330
+ ts: now,
1331
+ type: 'handoff',
1332
+ summary: handoffSummary,
1333
+ taskKey: context.task.task_key,
1334
+ status: 'completed',
1335
+ meta: {
1336
+ from: agentName,
1337
+ to: nextAgent,
1338
+ reason,
1339
+ previous_run_key: context.run.run_key,
1340
+ current_run_key: nextRunKey,
1341
+ micro_task_key: state.current_task || null
1342
+ }
1343
+ });
1344
+ await appendLiveEvent(runtimeDir, context.sessionKey, eventRecord);
1345
+
1346
+ const nextState = applyEventToState(state, eventRecord, {
1347
+ currentTask: null,
1348
+ activeAgent: nextAgent,
1349
+ currentRunKey: nextRunKey
1350
+ });
1351
+ if (currentTaskClosed) {
1352
+ nextState.stats.tasks_completed += 1;
1353
+ }
1354
+ await writeLiveState(runtimeDir, context.sessionKey, nextState);
1355
+
1356
+ if (!options.json) {
1357
+ logger.log(t('live.handoff_recorded', { from: agentName, to: nextAgent, session: context.sessionKey, dbPath }));
1358
+ }
1359
+
1360
+ return {
1361
+ ok: true,
1362
+ targetDir,
1363
+ dbPath,
1364
+ agent: agentName,
1365
+ nextAgent,
1366
+ taskKey: context.task.task_key,
1367
+ previousRunKey: context.run.run_key,
1368
+ runKey: nextRunKey,
1369
+ sessionKey: context.sessionKey,
1370
+ open: true
1371
+ };
1372
+ } finally {
1373
+ db.close();
1374
+ }
1375
+ }
1376
+
1377
+ async function runLiveStatus({ args, options = {}, logger, t }) {
1378
+ const targetDir = resolveTargetDir(args);
1379
+ const watchSeconds = parseWatchSeconds(options.watch);
1380
+
1381
+ if (watchSeconds && options.json) {
1382
+ throw new Error(t('live.watch_json_conflict'));
1383
+ }
1384
+
1385
+ if (!watchSeconds) {
1386
+ const snapshot = await getLiveStatusSnapshot(targetDir, t, options);
1387
+ if (!options.json) {
1388
+ printLiveStatusSnapshot(snapshot, logger);
1389
+ }
1390
+ return snapshot;
1391
+ }
1392
+
1393
+ let stopped = false;
1394
+ const onSignal = () => { stopped = true; };
1395
+ process.on('SIGINT', onSignal);
1396
+ process.on('SIGTERM', onSignal);
1397
+
1398
+ try {
1399
+ while (!stopped) {
1400
+ const snapshot = await getLiveStatusSnapshot(targetDir, t, options);
1401
+ if (process.stdout && process.stdout.isTTY) {
1402
+ process.stdout.write('\x1Bc');
1403
+ }
1404
+ printLiveStatusSnapshot(snapshot, logger);
1405
+ if (stopped) break;
1406
+ await sleep(Math.round(watchSeconds * 1000));
1407
+ }
1408
+ } finally {
1409
+ process.removeListener('SIGINT', onSignal);
1410
+ process.removeListener('SIGTERM', onSignal);
1411
+ }
1412
+ }
1413
+
1414
+ async function runLiveClose({ args, options = {}, logger, t }) {
1415
+ const targetDir = resolveTargetDir(args);
1416
+ const requestedAgent = options.agent ? normalizeAgentHandle(options.agent) : null;
1417
+ const { db, dbPath, runtimeDir } = await withRuntimeDb(targetDir, t);
1418
+
1419
+ try {
1420
+ const context = await resolveLiveContext(targetDir, db, runtimeDir, {
1421
+ agentName: requestedAgent,
1422
+ limit: options.limit
1423
+ });
1424
+
1425
+ if (!context.run || context.run.source !== 'live' || !context.sessionKey || !context.task) {
1426
+ throw new Error(requestedAgent
1427
+ ? t('live.no_session_for_agent', { agent: requestedAgent })
1428
+ : t('live.no_session_found'));
1429
+ }
1430
+
1431
+ if (context.phase !== 'active') {
1432
+ throw new Error(t('live.session_already_closed', { session: context.sessionKey }));
1433
+ }
1434
+
1435
+ const status = String(options.status || 'completed').trim().toLowerCase() === 'failed' ? 'failed' : 'completed';
1436
+ const now = new Date().toISOString();
1437
+ const summary = truncateMessage(options.summary || options.message || `Live session closed for ${context.agentName}`);
1438
+ const state = context.state || createLiveState(targetDir, context.run, context.task, {
1439
+ sessionKey: context.sessionKey,
1440
+ activeAgent: context.agentName,
1441
+ projectPath: targetDir
1442
+ });
1443
+
1444
+ let currentTaskClosed = false;
1445
+ if (state.current_task) {
1446
+ updateTask(db, {
1447
+ taskKey: state.current_task,
1448
+ status,
1449
+ goal: summary
1450
+ });
1451
+ currentTaskClosed = true;
1452
+ }
1453
+
1454
+ updateRun(db, {
1455
+ runKey: context.run.run_key,
1456
+ status,
1457
+ summary,
1458
+ eventType: 'session_closed',
1459
+ message: summary,
1460
+ payload: {
1461
+ closed_by: 'live:close'
1462
+ }
1463
+ });
1464
+
1465
+ updateTask(db, {
1466
+ taskKey: context.task.task_key,
1467
+ status,
1468
+ goal: summary
1469
+ });
1470
+
1471
+ const eventRecord = createLiveEventRecord(context, {
1472
+ ts: now,
1473
+ type: 'session_closed',
1474
+ summary,
1475
+ taskKey: context.task.task_key,
1476
+ status
1477
+ });
1478
+ await appendLiveEvent(runtimeDir, context.sessionKey, eventRecord);
1479
+
1480
+ const nextState = applyEventToState(state, eventRecord, {
1481
+ currentTask: null,
1482
+ phase: 'closed',
1483
+ closedAt: now,
1484
+ activeAgent: context.agentName,
1485
+ currentRunKey: context.run.run_key
1486
+ });
1487
+ if (currentTaskClosed && status === 'completed') {
1488
+ nextState.stats.tasks_completed += 1;
1489
+ }
1490
+
1491
+ const git = await collectGitSnapshot(targetDir);
1492
+ await writeLiveState(runtimeDir, context.sessionKey, nextState);
1493
+ const summaryPath = await writeLiveSummary(runtimeDir, context.sessionKey, renderLiveSummary({
1494
+ sessionKey: context.sessionKey,
1495
+ agent: context.agentName,
1496
+ tool: nextState.tool_session,
1497
+ status,
1498
+ startedAt: nextState.started_at,
1499
+ closedAt: now,
1500
+ summary,
1501
+ git,
1502
+ recentEvents: nextState.last_events
1503
+ }));
1504
+
1505
+ await clearAgentSession(runtimeDir, context.agentName);
1506
+
1507
+ if (!options.json) {
1508
+ logger.log(t('live.session_closed', { agent: context.agentName, session: context.sessionKey, dbPath }));
1509
+ }
1510
+
1511
+ return {
1512
+ ok: true,
1513
+ targetDir,
1514
+ dbPath,
1515
+ agent: context.agentName,
1516
+ taskKey: context.task.task_key,
1517
+ runKey: context.run.run_key,
1518
+ sessionKey: context.sessionKey,
1519
+ status,
1520
+ closed: true,
1521
+ summaryPath,
1522
+ git
1523
+ };
1524
+ } finally {
1525
+ db.close();
1526
+ }
1527
+ }
1528
+
1529
+ async function runLiveList({ args, options = {}, logger, t }) {
1530
+ const targetDir = resolveTargetDir(args);
1531
+ const { dbPath } = resolveRuntimePaths(targetDir);
1532
+
1533
+ if (!(await runtimeStoreExists(targetDir))) {
1534
+ throw new Error(t('runtime.store_missing', { path: dbPath }));
1535
+ }
1536
+
1537
+ const { db, runtimeDir } = await openRuntimeDb(targetDir, { mustExist: true });
1538
+ db.close();
1539
+ const states = await listLiveStates(runtimeDir);
1540
+
1541
+ if (!options.json) {
1542
+ if (states.length === 0) {
1543
+ logger.log(t('live.list_empty'));
1544
+ } else {
1545
+ logger.log(t('live.list_title', { count: states.length }));
1546
+ for (const state of states) {
1547
+ logger.log(t('live.list_line', {
1548
+ session: state.session_key || '-',
1549
+ agent: state.active_agent || '-',
1550
+ tool: state.tool_session || '-',
1551
+ phase: state.phase || '-',
1552
+ updatedAt: state.updated_at || state.started_at || '-'
1553
+ }));
1554
+ }
1555
+ }
1556
+ }
1557
+
1558
+ return {
1559
+ ok: true,
1560
+ targetDir,
1561
+ dbPath,
1562
+ count: states.length,
1563
+ sessions: states.map((state) => ({
1564
+ sessionKey: state.session_key,
1565
+ agent: state.active_agent,
1566
+ tool: state.tool_session,
1567
+ phase: state.phase,
1568
+ title: state.title,
1569
+ startedAt: state.started_at,
1570
+ updatedAt: state.updated_at,
1571
+ closedAt: state.closed_at
1572
+ }))
1573
+ };
1574
+ }
1575
+
1576
+ module.exports = {
1577
+ runLiveStart,
1578
+ runRuntimeEmit,
1579
+ runLiveHandoff,
1580
+ runLiveStatus,
1581
+ runLiveClose,
1582
+ runLiveList
1583
+ };