@lmctl-ai/lmctl 0.2.38 → 0.2.39

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 (202) hide show
  1. package/README.md +16 -0
  2. package/bin/lmbee +2 -0
  3. package/bin/lmscript +3 -0
  4. package/build-info.json +1 -0
  5. package/dist/cli/index.js +485 -460
  6. package/dist/cli/schema.sql +0 -169
  7. package/dist/cli/script.js +246 -246
  8. package/dist/lmbee.js +960 -0
  9. package/dist/lmscript.js +388 -0
  10. package/dist/public/app.js +379 -0
  11. package/dist/public/index.html +78 -0
  12. package/dist/public/styles.css +160 -0
  13. package/dist/schema.sql +455 -0
  14. package/dist/vendor-webui/index.html +18 -0
  15. package/dist/vendor-webui/local.js +231 -0
  16. package/dist/vendor-webui/styles.css +1852 -0
  17. package/package.json +11 -7
  18. package/dist/cli/batch.d.ts +0 -2
  19. package/dist/cli/busy-evidence.d.ts +0 -27
  20. package/dist/cli/chat-cycle.d.ts +0 -59
  21. package/dist/cli/chat-exec.d.ts +0 -91
  22. package/dist/cli/chat-policy.d.ts +0 -22
  23. package/dist/cli/chat.d.ts +0 -14
  24. package/dist/cli/code-resolver.d.ts +0 -8
  25. package/dist/cli/common.d.ts +0 -57
  26. package/dist/cli/context.d.ts +0 -12
  27. package/dist/cli/cost.d.ts +0 -2
  28. package/dist/cli/export.d.ts +0 -2
  29. package/dist/cli/hello.d.ts +0 -10
  30. package/dist/cli/index.d.ts +0 -3
  31. package/dist/cli/infra.d.ts +0 -78
  32. package/dist/cli/init.d.ts +0 -8
  33. package/dist/cli/intent-input.d.ts +0 -16
  34. package/dist/cli/intent-match.d.ts +0 -30
  35. package/dist/cli/intent-sessions.d.ts +0 -24
  36. package/dist/cli/intent-team.d.ts +0 -25
  37. package/dist/cli/intent.d.ts +0 -22
  38. package/dist/cli/ls.d.ts +0 -2
  39. package/dist/cli/mailbox-cli.d.ts +0 -6
  40. package/dist/cli/managed.d.ts +0 -62
  41. package/dist/cli/mcp-install.d.ts +0 -23
  42. package/dist/cli/mcp.d.ts +0 -2
  43. package/dist/cli/model.d.ts +0 -21
  44. package/dist/cli/paths.d.ts +0 -50
  45. package/dist/cli/plan-wizard.d.ts +0 -42
  46. package/dist/cli/plan.d.ts +0 -20
  47. package/dist/cli/queue-delivery.d.ts +0 -122
  48. package/dist/cli/quickrun.d.ts +0 -14
  49. package/dist/cli/ratelimit.d.ts +0 -2
  50. package/dist/cli/remote-fetch.d.ts +0 -7
  51. package/dist/cli/run-queries.d.ts +0 -82
  52. package/dist/cli/running.d.ts +0 -5
  53. package/dist/cli/save-script.d.ts +0 -41
  54. package/dist/cli/script.d.ts +0 -6
  55. package/dist/cli/seed-flight.d.ts +0 -2
  56. package/dist/cli/self-identity.d.ts +0 -25
  57. package/dist/cli/session-idle.d.ts +0 -118
  58. package/dist/cli/status.d.ts +0 -22
  59. package/dist/cli/tail.d.ts +0 -18
  60. package/dist/cli/target-resolver.d.ts +0 -139
  61. package/dist/cli/team-status.d.ts +0 -91
  62. package/dist/cli/teamfile-compat.d.ts +0 -116
  63. package/dist/cli/teamfile-store.d.ts +0 -18
  64. package/dist/cli/teamfile-template.d.ts +0 -47
  65. package/dist/cli/template-env.d.ts +0 -25
  66. package/dist/cli/terminal-lock.d.ts +0 -25
  67. package/dist/cli/terminal.d.ts +0 -23
  68. package/dist/cli/time-filter.d.ts +0 -10
  69. package/dist/cli/update-notifier.d.ts +0 -11
  70. package/dist/cli/version-util.d.ts +0 -23
  71. package/dist/config/seed_templates.d.ts +0 -1
  72. package/dist/db/backfill-event-log.d.ts +0 -26
  73. package/dist/db/delegation.d.ts +0 -37
  74. package/dist/db/escalation_vocab.d.ts +0 -45
  75. package/dist/db/event-log.d.ts +0 -40
  76. package/dist/db/index.d.ts +0 -1213
  77. package/dist/db/inflight-acquire.d.ts +0 -70
  78. package/dist/db/invocation-event-projections.d.ts +0 -65
  79. package/dist/db/ledger-identity.test.d.ts +0 -1
  80. package/dist/db/live-event-projections.d.ts +0 -92
  81. package/dist/db/migrate.d.ts +0 -51
  82. package/dist/db/open-readonly.d.ts +0 -2
  83. package/dist/db/process-liveness.d.ts +0 -37
  84. package/dist/db/sqlite.d.ts +0 -53
  85. package/dist/db/sqlite.test.d.ts +0 -1
  86. package/dist/db/terminal-lock.d.ts +0 -14
  87. package/dist/db/tracked-invocation-reconcile.d.ts +0 -52
  88. package/dist/db/tracked-invocation.d.ts +0 -116
  89. package/dist/mcp/dispatch.d.ts +0 -45
  90. package/dist/mcp/mediation.d.ts +0 -128
  91. package/dist/mcp/path-warning.d.ts +0 -7
  92. package/dist/mcp/server.d.ts +0 -63
  93. package/dist/mcp/tools.d.ts +0 -24
  94. package/dist/providers/adapter.d.ts +0 -271
  95. package/dist/providers/adapters/agy-adapter.d.ts +0 -18
  96. package/dist/providers/adapters/claude-adapter.d.ts +0 -20
  97. package/dist/providers/adapters/codex-adapter.d.ts +0 -22
  98. package/dist/providers/adapters/copilot-adapter.d.ts +0 -16
  99. package/dist/providers/adapters/dsh-adapter.d.ts +0 -89
  100. package/dist/providers/adapters/gemini-adapter.d.ts +0 -44
  101. package/dist/providers/adapters/hermes-adapter.d.ts +0 -71
  102. package/dist/providers/adapters/index.d.ts +0 -35
  103. package/dist/providers/adapters/kimi-adapter.d.ts +0 -16
  104. package/dist/providers/adapters/lmplayer-adapter.d.ts +0 -14
  105. package/dist/providers/adapters/opencode-adapter.d.ts +0 -11
  106. package/dist/providers/adapters/pi-adapter.d.ts +0 -17
  107. package/dist/providers/adapters/print-turn.d.ts +0 -29
  108. package/dist/providers/adapters/qwen-adapter.d.ts +0 -11
  109. package/dist/providers/agy.d.ts +0 -32
  110. package/dist/providers/base-adapter.d.ts +0 -110
  111. package/dist/providers/base.d.ts +0 -12
  112. package/dist/providers/binary-check.d.ts +0 -102
  113. package/dist/providers/bundle.d.ts +0 -124
  114. package/dist/providers/claude-mock.d.ts +0 -90
  115. package/dist/providers/claude.d.ts +0 -21
  116. package/dist/providers/codex-config.d.ts +0 -15
  117. package/dist/providers/codex.d.ts +0 -25
  118. package/dist/providers/config/init-config.d.ts +0 -59
  119. package/dist/providers/config/init-edit.d.ts +0 -3
  120. package/dist/providers/config/init-file.d.ts +0 -20
  121. package/dist/providers/config/init-write.d.ts +0 -2
  122. package/dist/providers/config/model-selection-lint.d.ts +0 -70
  123. package/dist/providers/config/team-config.d.ts +0 -184
  124. package/dist/providers/config/types.d.ts +0 -146
  125. package/dist/providers/config/user-config.d.ts +0 -88
  126. package/dist/providers/copilot.d.ts +0 -21
  127. package/dist/providers/debug.d.ts +0 -128
  128. package/dist/providers/dsh-sidecar.d.ts +0 -65
  129. package/dist/providers/dsh-zstd.d.ts +0 -27
  130. package/dist/providers/dsh.d.ts +0 -37
  131. package/dist/providers/frontmatter.d.ts +0 -46
  132. package/dist/providers/gemini-session-reader.d.ts +0 -151
  133. package/dist/providers/gemini.d.ts +0 -39
  134. package/dist/providers/hermes.d.ts +0 -90
  135. package/dist/providers/index.d.ts +0 -423
  136. package/dist/providers/kimi.d.ts +0 -37
  137. package/dist/providers/model-catalog.d.ts +0 -145
  138. package/dist/providers/model-suggestions.d.ts +0 -4
  139. package/dist/providers/one-shot-timeout.d.ts +0 -1
  140. package/dist/providers/opencode-config.d.ts +0 -62
  141. package/dist/providers/opencode.d.ts +0 -40
  142. package/dist/providers/parsers/acp.d.ts +0 -71
  143. package/dist/providers/parsers/codex-app-server.d.ts +0 -60
  144. package/dist/providers/parsers/stream-json.d.ts +0 -57
  145. package/dist/providers/passive-readers/agy.d.ts +0 -155
  146. package/dist/providers/passive-readers/copilot.d.ts +0 -173
  147. package/dist/providers/passive-readers/dsh.d.ts +0 -34
  148. package/dist/providers/passive-readers/kimi.d.ts +0 -44
  149. package/dist/providers/passive-readers/opencode.d.ts +0 -89
  150. package/dist/providers/passive-readers/qwen.d.ts +0 -151
  151. package/dist/providers/passive-readers/shared.d.ts +0 -138
  152. package/dist/providers/passive-session-reader.d.ts +0 -188
  153. package/dist/providers/path-utils.d.ts +0 -1
  154. package/dist/providers/pi.d.ts +0 -32
  155. package/dist/providers/pricing.d.ts +0 -107
  156. package/dist/providers/qwen.d.ts +0 -22
  157. package/dist/providers/ratelimit/agy.d.ts +0 -22
  158. package/dist/providers/ratelimit/claude-auth.d.ts +0 -13
  159. package/dist/providers/ratelimit/claude.d.ts +0 -6
  160. package/dist/providers/ratelimit/codex.d.ts +0 -7
  161. package/dist/providers/ratelimit/kimi-auth.d.ts +0 -15
  162. package/dist/providers/ratelimit/kimi.d.ts +0 -6
  163. package/dist/providers/ratelimit/schema.d.ts +0 -28
  164. package/dist/providers/ratelimit-readings.d.ts +0 -19
  165. package/dist/providers/registry.d.ts +0 -31
  166. package/dist/providers/resume.d.ts +0 -43
  167. package/dist/providers/seed-instruction.d.ts +0 -3
  168. package/dist/providers/self-session-env.d.ts +0 -21
  169. package/dist/providers/session-limit.d.ts +0 -9
  170. package/dist/providers/strip-ansi.d.ts +0 -5
  171. package/dist/providers/transport-errors.d.ts +0 -94
  172. package/dist/providers/transport-lifecycle.d.ts +0 -63
  173. package/dist/providers/transport-wrappers.d.ts +0 -110
  174. package/dist/providers/transports/acp-profiles/config-option-step.d.ts +0 -24
  175. package/dist/providers/transports/acp-profiles/copilot.d.ts +0 -12
  176. package/dist/providers/transports/acp-profiles/gemini.d.ts +0 -22
  177. package/dist/providers/transports/acp-profiles/hermes.d.ts +0 -27
  178. package/dist/providers/transports/acp-profiles/index.d.ts +0 -19
  179. package/dist/providers/transports/acp-profiles/kimi.d.ts +0 -24
  180. package/dist/providers/transports/acp-profiles/opencode.d.ts +0 -26
  181. package/dist/providers/transports/acp-profiles/types.d.ts +0 -258
  182. package/dist/providers/transports/acp-rpc.d.ts +0 -18
  183. package/dist/providers/transports/acp.d.ts +0 -217
  184. package/dist/providers/transports/codex-app-server.d.ts +0 -190
  185. package/dist/providers/transports/hold-open.d.ts +0 -120
  186. package/dist/providers/transports/manager.d.ts +0 -21
  187. package/dist/providers/transports/stream-json.d.ts +0 -162
  188. package/dist/providers/transports/types.d.ts +0 -56
  189. package/dist/providers/types.d.ts +0 -154
  190. package/dist/providers/yolo-contract.d.ts +0 -131
  191. package/dist/script/adapter.d.ts +0 -107
  192. package/dist/script/batch-script.d.ts +0 -7
  193. package/dist/script/index.d.ts +0 -3
  194. package/dist/script/init-environment.d.ts +0 -8
  195. package/dist/script/lmbee-module.d.ts +0 -2
  196. package/dist/script/lmscript.d.ts +0 -647
  197. package/dist/shared/policy.d.ts +0 -74
  198. package/dist/shared-lib/db.d.ts +0 -25
  199. package/dist/shared-lib/passive-session-reader.d.ts +0 -2
  200. package/dist/shared-lib/provider-parse.d.ts +0 -6
  201. package/dist/templates/basedir.d.ts +0 -52
  202. package/dist/uri/lmctl_uri.d.ts +0 -38
@@ -0,0 +1,379 @@
1
+ const $ = (selector) => document.querySelector(selector);
2
+
3
+ function escapeHtml(value) {
4
+ return String(value ?? '').replace(/[&<>"']/g, (ch) => ({
5
+ '&': '&amp;',
6
+ '<': '&lt;',
7
+ '>': '&gt;',
8
+ '"': '&quot;',
9
+ "'": '&#39;',
10
+ }[ch]));
11
+ }
12
+
13
+ async function fetchJson(url, options) {
14
+ const res = await fetch(url, options);
15
+ const body = await res.json().catch(() => ({}));
16
+ if (!res.ok) throw new Error(body.error || `request failed: ${res.status}`);
17
+ return body;
18
+ }
19
+
20
+ function renderTable(headers, rows) {
21
+ if (rows.length === 0) return '<p class="muted">(none)</p>';
22
+ const headHtml = headers.map((h) => `<th>${escapeHtml(h)}</th>`).join('');
23
+ const bodyHtml = rows
24
+ .map((row) => `<tr>${row.map((cell) => `<td>${cell}</td>`).join('')}</tr>`)
25
+ .join('');
26
+ return `<table><thead><tr>${headHtml}</tr></thead><tbody>${bodyHtml}</tbody></table>`;
27
+ }
28
+
29
+ function statusClass(status) {
30
+ if (!status) return '';
31
+ if (status === 'BLOCKED') return 'status-BLOCKED';
32
+ if (status === 'DONE') return 'status-DONE';
33
+ return '';
34
+ }
35
+
36
+ // Phase 2c (2026-08-31, tools.lmctl's design cue): `/api/liveness`'s `remoteOutbound` is `null` for a
37
+ // RemoteTeam this site has never sent anything -- genuinely nothing observed yet, not an error.
38
+ function remoteOutboundLabel(remoteOutbound) {
39
+ if (!remoteOutbound) return 'remote -- delivery only';
40
+ const ts = remoteOutbound.repliedTs ?? remoteOutbound.ackedTs ?? remoteOutbound.sentTs;
41
+ if (remoteOutbound.state === 'replied') return `remote -- replied ${ts}`;
42
+ if (remoteOutbound.state === 'acked') return `remote -- acked ${ts}`;
43
+ return `remote -- sent ${ts}, no ack yet`;
44
+ }
45
+
46
+ // Populated by renderBoard, read by the enqueue/session-link flows so they know each team's lead
47
+ // alias without a second round-trip.
48
+ let teamsCache = [];
49
+
50
+ async function renderBoard() {
51
+ const el = $('#board-table');
52
+ try {
53
+ const [teamsBody, stateBody, liveBody, stalledBody] = await Promise.all([
54
+ fetchJson('api/teams'),
55
+ fetchJson('api/state'),
56
+ fetchJson('api/liveness').catch(() => ({ teams: [] })),
57
+ fetchJson('api/stalled').catch(() => ({ stalled: [], remoteStalled: [] })),
58
+ ]);
59
+ teamsCache = teamsBody.teams;
60
+ const stateByTeam = new Map(stateBody.teams.map((t) => [t.team, t.state]));
61
+ const progressByTeam = new Map(stateBody.teams.map((t) => [t.team, t.progress]));
62
+ const liveByTeam = new Map(liveBody.teams.map((t) => [t.team, t]));
63
+ const stalledSet = new Set(stalledBody.stalled);
64
+ const remoteStalledSet = new Set(stalledBody.remoteStalled ?? []);
65
+
66
+ const rows = teamsBody.teams.map((team) => {
67
+ // 2026-08-31, Phase 2b3: a RemoteTeam (channel=lmmail) has no leadAlias/promptName and no `lctl`
68
+ // liveness concept. Phase 2c (tools.lmctl's design cue): once this site has actually SENT the
69
+ // team something, `/api/liveness`'s `remoteOutbound` gives real ack/reply observation --
70
+ // "remote -- delivery+observe" with the actual state, not just the static placeholder. Before
71
+ // that (never sent anything yet), there's genuinely nothing to observe -- stays "delivery only",
72
+ // honest about the absence of evidence rather than implying observation that hasn't happened.
73
+ const isRemote = team.channel === 'lmmail';
74
+ const state = stateByTeam.get(team.name);
75
+ const progress = progressByTeam.get(team.name);
76
+ const live = liveByTeam.get(team.name);
77
+ const liveLabel = isRemote
78
+ ? remoteOutboundLabel(live?.remoteOutbound)
79
+ : live
80
+ ? live.running
81
+ ? 'Running'
82
+ : live.deadReclaimable
83
+ ? 'Dead(reclaimable)'
84
+ : 'Idle'
85
+ : '?';
86
+ const status = state?.status ?? '?';
87
+ // Objective, git/file-write-evidence-based signal (2026-08-30 status-engine redesign) -- `status`
88
+ // above is self-report only, kept as a hint; this is the ground truth anti-churn actually uses.
89
+ const progressLabel = progress?.confirmed_done_ts ? 'DONE(confirmed)' : (progress?.progress_status ?? '?');
90
+ // Surfaced for the LLM/operator to decide on -- the cron itself never auto-nudges a stalled
91
+ // team (see cron/scheduler.ts); this badge is the whole point of that surfacing. Phase 2c: a
92
+ // remote team's stalled signal is a DIFFERENT classification mechanism (ack/reply observation,
93
+ // not lctl/progress-scan) -- labeled distinctly so it's never confused with the local one.
94
+ const attention = stalledSet.has(team.name)
95
+ ? '<span class="status-BLOCKED">STALLED</span>'
96
+ : remoteStalledSet.has(team.name)
97
+ ? '<span class="status-BLOCKED">STALLED (remote)</span>'
98
+ : '';
99
+ return [
100
+ isRemote
101
+ ? escapeHtml(team.name)
102
+ : `<a href="#" class="team-link" data-team="${escapeHtml(team.name)}" data-alias="${escapeHtml(team.leadAlias)}">${escapeHtml(team.name)}</a>`,
103
+ isRemote ? '(remote)' : escapeHtml(team.leadAlias),
104
+ isRemote ? '--' : escapeHtml(team.promptName),
105
+ `<span class="${statusClass(status)}">${escapeHtml(status)}</span>`,
106
+ escapeHtml(progressLabel),
107
+ escapeHtml(state?.changed_ts ?? '-'),
108
+ escapeHtml(liveLabel),
109
+ attention,
110
+ ];
111
+ });
112
+ el.innerHTML = renderTable(
113
+ ['Team', 'Lead', 'Prompt', 'Status(self-report)', 'Progress', 'Changed', 'Live', 'Attention'],
114
+ rows,
115
+ );
116
+ el.querySelectorAll('.team-link').forEach((link) => {
117
+ link.addEventListener('click', (event) => {
118
+ event.preventDefault();
119
+ $('#session-form [name="team"]').value = link.dataset.team;
120
+ $('#session-form [name="alias"]').value = link.dataset.alias;
121
+ $('#session-form').requestSubmit();
122
+ });
123
+ });
124
+ } catch (err) {
125
+ el.innerHTML = `<p class="error">${escapeHtml(err.message)}</p>`;
126
+ }
127
+ }
128
+
129
+ async function renderQueue() {
130
+ const el = $('#queue-table');
131
+ try {
132
+ const body = await fetchJson('api/queue');
133
+ const rows = body.queue.map((row) => [
134
+ String(row.id),
135
+ escapeHtml(row.from_id),
136
+ escapeHtml(row.to_id),
137
+ escapeHtml(row.status),
138
+ escapeHtml(row.created_ts),
139
+ escapeHtml(row.sent_ts ?? row.retrieved_ts ?? '-'),
140
+ ]);
141
+ el.innerHTML = renderTable(['ID', 'From', 'To', 'Status', 'Created', 'Sent/Retrieved'], rows);
142
+ } catch (err) {
143
+ el.innerHTML = `<p class="error">${escapeHtml(err.message)}</p>`;
144
+ }
145
+ }
146
+
147
+ async function renderHistory() {
148
+ const el = $('#history-table');
149
+ const team = $('#history-team-filter').value.trim();
150
+ try {
151
+ const query = team ? `?team=${encodeURIComponent(team)}` : '';
152
+ const body = await fetchJson(`api/history${query}`);
153
+ const rows = body.events.map((row) => [
154
+ String(row.id),
155
+ escapeHtml(row.ts),
156
+ escapeHtml(row.team),
157
+ `<span class="${statusClass(row.status)}">${escapeHtml(row.status)}</span>`,
158
+ escapeHtml(row.note ?? ''),
159
+ ]);
160
+ el.innerHTML = renderTable(['ID', 'TS', 'Team', 'Status', 'Note'], rows);
161
+ } catch (err) {
162
+ el.innerHTML = `<p class="error">${escapeHtml(err.message)}</p>`;
163
+ }
164
+ }
165
+
166
+ async function renderSources() {
167
+ const el = $('#sources-table');
168
+ try {
169
+ const body = await fetchJson('api/sources');
170
+ const rows = [
171
+ ['-', escapeHtml(body.defaultTeamsConfPath), '(default, always scanned)', ''],
172
+ ...body.sources.map((s) => [
173
+ String(s.id),
174
+ escapeHtml(s.path),
175
+ escapeHtml(s.label ?? ''),
176
+ `<button data-remove-path="${escapeHtml(s.path)}">Remove</button>`,
177
+ ]),
178
+ ];
179
+ el.innerHTML = renderTable(['ID', 'Path', 'Label', ''], rows);
180
+ el.querySelectorAll('[data-remove-path]').forEach((button) => {
181
+ button.addEventListener('click', async () => {
182
+ await fetchJson(`api/sources?path=${encodeURIComponent(button.dataset.removePath)}`, {
183
+ method: 'DELETE',
184
+ });
185
+ renderSources();
186
+ });
187
+ });
188
+ } catch (err) {
189
+ el.innerHTML = `<p class="error">${escapeHtml(err.message)}</p>`;
190
+ }
191
+ }
192
+
193
+ async function refreshAll() {
194
+ $('#generated-at').textContent = `updated ${new Date().toISOString()}`;
195
+ await Promise.all([renderBoard(), renderQueue(), renderHistory(), renderSources()]);
196
+ }
197
+
198
+ $('#refresh-all').addEventListener('click', refreshAll);
199
+ $('#history-team-filter').addEventListener('change', renderHistory);
200
+
201
+ $('#send-form').addEventListener('submit', async (event) => {
202
+ event.preventDefault();
203
+ const form = new FormData(event.target);
204
+ const resultEl = $('#send-result');
205
+ resultEl.textContent = 'sending...';
206
+ try {
207
+ const body = await fetchJson('api/send', {
208
+ method: 'POST',
209
+ headers: { 'content-type': 'application/json' },
210
+ body: JSON.stringify({ team: form.get('team'), prompt: form.get('prompt') }),
211
+ });
212
+ resultEl.textContent = JSON.stringify(body, null, 2);
213
+ renderBoard();
214
+ renderHistory();
215
+ } catch (err) {
216
+ resultEl.textContent = `error: ${err.message}`;
217
+ }
218
+ });
219
+
220
+ $('#enqueue-form').addEventListener('submit', async (event) => {
221
+ event.preventDefault();
222
+ const form = new FormData(event.target);
223
+ const team = form.get('to');
224
+ try {
225
+ await fetchJson('api/enqueue', {
226
+ method: 'POST',
227
+ headers: { 'content-type': 'application/json' },
228
+ body: JSON.stringify({ to: team, prompt: form.get('prompt'), from: 'operator' }),
229
+ });
230
+ event.target.reset();
231
+ renderQueue();
232
+
233
+ const teamInfo = teamsCache.find((t) => t.name === team);
234
+ const alias = teamInfo?.leadAlias ?? 'Lead';
235
+ $('#session-form [name="team"]').value = team;
236
+ $('#session-form [name="alias"]').value = alias;
237
+ pollForReply(team, alias);
238
+ } catch (err) {
239
+ alert(`enqueue failed: ${err.message}`);
240
+ }
241
+ });
242
+
243
+ $('#deliver-btn').addEventListener('click', async () => {
244
+ try {
245
+ const body = await fetchJson('api/queue/deliver', { method: 'POST' });
246
+ renderQueue();
247
+ renderBoard();
248
+ alert(`dispatched ${body.delivered} pending prompt(s)`);
249
+ } catch (err) {
250
+ alert(`deliver failed: ${err.message}`);
251
+ }
252
+ });
253
+
254
+ $('#source-form').addEventListener('submit', async (event) => {
255
+ event.preventDefault();
256
+ const form = new FormData(event.target);
257
+ try {
258
+ await fetchJson('api/sources', {
259
+ method: 'POST',
260
+ headers: { 'content-type': 'application/json' },
261
+ body: JSON.stringify({ path: form.get('path'), label: form.get('label') || undefined }),
262
+ });
263
+ event.target.reset();
264
+ renderSources();
265
+ renderBoard();
266
+ } catch (err) {
267
+ alert(`add source failed: ${err.message}`);
268
+ }
269
+ });
270
+
271
+ async function loadSessionSummary(team, alias) {
272
+ // Best-effort: `lmctl session` loads a team's ENTIRE session before slicing, which can exceed our
273
+ // exec maxBuffer for a long-running team (live-confirmed against math.lmctl) -- a failure here must
274
+ // never block the transcript view (loadTranscript, below), which is the reliable one.
275
+ const el = $('#session-detail');
276
+ el.innerHTML = '<p class="muted">loading...</p>';
277
+ try {
278
+ const body = await fetchJson(
279
+ `api/session/${encodeURIComponent(team)}/${encodeURIComponent(alias)}?limit=10`,
280
+ );
281
+ const tokens = body.tokens?.aggregate ?? {};
282
+ const summaryRows = [
283
+ ['Provider', escapeHtml(body.provider ?? '?')],
284
+ ['Session ID', escapeHtml(body.sessionId ?? '?')],
285
+ ['Modified', escapeHtml(body.modifiedAt ?? '?')],
286
+ ['Handled', `${body.handled} (${escapeHtml(body.handledSignal ?? '?')})`],
287
+ ['Tokens (aggregate)', `in ${tokens.input ?? 0} / out ${tokens.output ?? 0}`],
288
+ ['Tool calls', String((body.toolCalls ?? []).length)],
289
+ ['Tracked invocations', String((body.invocations ?? []).length)],
290
+ ['Last prompt', escapeHtml((body.lastPrompt ?? '').slice(0, 200))],
291
+ ];
292
+ el.innerHTML = renderTable(['Field', 'Value'], summaryRows);
293
+ } catch (err) {
294
+ el.innerHTML = `<p class="error">${escapeHtml(err.message)}</p>`;
295
+ }
296
+ }
297
+
298
+ function renderTranscriptTurns(turns, newFromIndex) {
299
+ if (turns.length === 0) return '<p class="muted">(no turns)</p>';
300
+ return (
301
+ '<div class="transcript">' +
302
+ turns
303
+ .map((turn, i) => {
304
+ const cls = i >= newFromIndex ? 'transcript-turn turn-new' : 'transcript-turn';
305
+ return (
306
+ `<div class="${cls}">` +
307
+ `<div class="turn-head"><span>${escapeHtml(turn.role)}</span><span>${escapeHtml(turn.ts ?? '')}</span></div>` +
308
+ `<div class="turn-text">${escapeHtml(turn.text)}</div>` +
309
+ '</div>'
310
+ );
311
+ })
312
+ .join('') +
313
+ '</div>'
314
+ );
315
+ }
316
+
317
+ /** Fetches + renders the real request/response transcript for one team member. Returns the fetched
318
+ * turns (or null on error) so callers -- e.g. the enqueue reply-poller -- can detect new turns. */
319
+ async function loadTranscript(team, alias, lines, newFromIndex = Infinity) {
320
+ const el = $('#transcript-view');
321
+ try {
322
+ const body = await fetchJson(
323
+ `api/transcript?team=${encodeURIComponent(team)}&alias=${encodeURIComponent(alias)}&lines=${lines || 40}`,
324
+ );
325
+ el.innerHTML = renderTranscriptTurns(body.turns, newFromIndex);
326
+ return body.turns;
327
+ } catch (err) {
328
+ el.innerHTML = `<p class="error">${escapeHtml(err.message)}</p>`;
329
+ return null;
330
+ }
331
+ }
332
+
333
+ let activeReplyPoll = null;
334
+
335
+ /** After an operator enqueue, watch that team's transcript for the reply to land (delivered by the
336
+ * in-server cron, then answered by the target's own LLM turn) and highlight it on the page --
337
+ * the "operator can drive a team from the web app" loop the mission brief asks for (function 3). */
338
+ function pollForReply(team, alias) {
339
+ if (activeReplyPoll) clearInterval(activeReplyPoll.timer);
340
+ const statusEl = $('#enqueue-poll-status');
341
+ const POLL_MS = 5000;
342
+ const MAX_ATTEMPTS = 60; // ~5 minutes
343
+
344
+ loadTranscript(team, alias, 40).then((baseline) => {
345
+ if (baseline === null) return; // error already rendered by loadTranscript
346
+ const baselineLength = baseline.length;
347
+ let attempts = 0;
348
+ statusEl.textContent = `queued to ${team} -- watching for a new reply...`;
349
+ const timer = setInterval(async () => {
350
+ attempts++;
351
+ const turns = await loadTranscript(team, alias, 40, baselineLength);
352
+ if (turns === null) return;
353
+ if (turns.length > baselineLength) {
354
+ clearInterval(timer);
355
+ activeReplyPoll = null;
356
+ statusEl.textContent = `new reply from ${team} landed (see the transcript below).`;
357
+ } else if (attempts >= MAX_ATTEMPTS) {
358
+ clearInterval(timer);
359
+ activeReplyPoll = null;
360
+ statusEl.textContent =
361
+ `still waiting on ${team} after 5 minutes -- stopped polling (it may still be running; ` +
362
+ `click "Inspect" in Active session review to check manually).`;
363
+ }
364
+ }, POLL_MS);
365
+ activeReplyPoll = { team, timer };
366
+ });
367
+ }
368
+
369
+ $('#session-form').addEventListener('submit', async (event) => {
370
+ event.preventDefault();
371
+ const form = new FormData(event.target);
372
+ const team = form.get('team');
373
+ const alias = form.get('alias');
374
+ const lines = Number(form.get('lines')) || 40;
375
+ $('#transcript-status').textContent = '';
376
+ await Promise.all([loadSessionSummary(team, alias), loadTranscript(team, alias, lines)]);
377
+ });
378
+
379
+ refreshAll();
@@ -0,0 +1,78 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
6
+ <title>lmauto</title>
7
+ <link rel="stylesheet" href="styles.css" />
8
+ </head>
9
+ <body>
10
+ <div class="page-shell">
11
+ <header class="toolbar">
12
+ <h1>lmauto</h1>
13
+ <button id="refresh-all">Refresh</button>
14
+ <span id="generated-at" class="muted"></span>
15
+ </header>
16
+
17
+ <main class="layout">
18
+ <section class="panel" id="panel-board">
19
+ <h2>Team board (S1&harr;S2)</h2>
20
+ <div id="board-table"></div>
21
+ </section>
22
+
23
+ <section class="panel" id="panel-send">
24
+ <h2>Send as operator</h2>
25
+ <form id="send-form">
26
+ <label>Team <input type="text" name="team" required /></label>
27
+ <label>Prompt <textarea name="prompt" rows="4" required></textarea></label>
28
+ <button type="submit">Send</button>
29
+ </form>
30
+ <pre id="send-result" class="result"></pre>
31
+ </section>
32
+
33
+ <section class="panel" id="panel-queue">
34
+ <h2>Queue &mdash; drive a team (e.g. <code>math</code>, the meta-lead) via the outbox</h2>
35
+ <form id="enqueue-form">
36
+ <label>To <input type="text" name="to" required /></label>
37
+ <label>Prompt <textarea name="prompt" rows="2" required></textarea></label>
38
+ <button type="submit">Enqueue</button>
39
+ <button type="button" id="deliver-btn">Deliver pending</button>
40
+ </form>
41
+ <p id="enqueue-poll-status" class="muted"></p>
42
+ <div id="queue-table"></div>
43
+ </section>
44
+
45
+ <section class="panel" id="panel-history">
46
+ <h2>History</h2>
47
+ <label>Team filter <input type="text" id="history-team-filter" placeholder="(all)" /></label>
48
+ <div id="history-table"></div>
49
+ </section>
50
+
51
+ <section class="panel" id="panel-sources">
52
+ <h2>Registry sources</h2>
53
+ <form id="source-form">
54
+ <label>Path <input type="text" name="path" required /></label>
55
+ <label>Label <input type="text" name="label" /></label>
56
+ <button type="submit">Add source</button>
57
+ </form>
58
+ <div id="sources-table"></div>
59
+ </section>
60
+
61
+ <section class="panel" id="panel-session">
62
+ <h2>Active session review</h2>
63
+ <form id="session-form">
64
+ <label>Team <input type="text" name="team" required /></label>
65
+ <label>Alias <input type="text" name="alias" value="Lead" required /></label>
66
+ <label>Lines <input type="number" name="lines" value="40" min="1" style="width: 5em" /></label>
67
+ <button type="submit">Inspect</button>
68
+ </form>
69
+ <div id="session-detail"></div>
70
+ <h3>Transcript (prompts + replies, real request/response history)</h3>
71
+ <p id="transcript-status" class="muted"></p>
72
+ <div id="transcript-view"></div>
73
+ </section>
74
+ </main>
75
+ </div>
76
+ <script src="app.js"></script>
77
+ </body>
78
+ </html>
@@ -0,0 +1,160 @@
1
+ :root {
2
+ color-scheme: light dark;
3
+ --border: #d0d0d0;
4
+ --muted: #666;
5
+ --bg-panel: rgba(127, 127, 127, 0.06);
6
+ }
7
+
8
+ * {
9
+ box-sizing: border-box;
10
+ }
11
+
12
+ body {
13
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
14
+ margin: 0;
15
+ padding: 1rem;
16
+ line-height: 1.4;
17
+ }
18
+
19
+ .page-shell {
20
+ max-width: 1400px;
21
+ margin: 0 auto;
22
+ }
23
+
24
+ .toolbar {
25
+ display: flex;
26
+ align-items: center;
27
+ gap: 1rem;
28
+ margin-bottom: 1rem;
29
+ }
30
+
31
+ .toolbar h1 {
32
+ margin: 0;
33
+ font-size: 1.25rem;
34
+ }
35
+
36
+ .muted {
37
+ color: var(--muted);
38
+ font-size: 0.85rem;
39
+ }
40
+
41
+ .layout {
42
+ display: grid;
43
+ grid-template-columns: repeat(auto-fit, minmax(360px, 1fr));
44
+ gap: 1rem;
45
+ }
46
+
47
+ .panel {
48
+ border: 1px solid var(--border);
49
+ border-radius: 8px;
50
+ padding: 1rem;
51
+ background: var(--bg-panel);
52
+ }
53
+
54
+ .panel h2 {
55
+ margin-top: 0;
56
+ font-size: 1rem;
57
+ }
58
+
59
+ table {
60
+ border-collapse: collapse;
61
+ width: 100%;
62
+ font-size: 0.85rem;
63
+ }
64
+
65
+ th,
66
+ td {
67
+ border-bottom: 1px solid var(--border);
68
+ padding: 0.35rem 0.5rem;
69
+ text-align: left;
70
+ vertical-align: top;
71
+ }
72
+
73
+ form {
74
+ display: flex;
75
+ flex-direction: column;
76
+ gap: 0.5rem;
77
+ margin-bottom: 0.75rem;
78
+ }
79
+
80
+ form label {
81
+ display: flex;
82
+ flex-direction: column;
83
+ gap: 0.25rem;
84
+ font-size: 0.85rem;
85
+ }
86
+
87
+ input,
88
+ textarea,
89
+ button {
90
+ font: inherit;
91
+ padding: 0.35rem 0.5rem;
92
+ border: 1px solid var(--border);
93
+ border-radius: 4px;
94
+ }
95
+
96
+ button {
97
+ cursor: pointer;
98
+ width: fit-content;
99
+ }
100
+
101
+ .result {
102
+ white-space: pre-wrap;
103
+ background: rgba(127, 127, 127, 0.1);
104
+ padding: 0.5rem;
105
+ border-radius: 4px;
106
+ font-size: 0.8rem;
107
+ max-height: 200px;
108
+ overflow: auto;
109
+ }
110
+
111
+ .error {
112
+ color: #c0392b;
113
+ }
114
+
115
+ .status-Running {
116
+ color: #2c7be5;
117
+ }
118
+
119
+ .status-BLOCKED {
120
+ color: #c0392b;
121
+ font-weight: bold;
122
+ }
123
+
124
+ .status-DONE {
125
+ color: #1a9850;
126
+ }
127
+
128
+ .transcript {
129
+ max-height: 420px;
130
+ overflow: auto;
131
+ display: flex;
132
+ flex-direction: column;
133
+ gap: 0.5rem;
134
+ }
135
+
136
+ .transcript-turn {
137
+ border: 1px solid var(--border);
138
+ border-radius: 4px;
139
+ padding: 0.4rem 0.6rem;
140
+ font-size: 0.82rem;
141
+ }
142
+
143
+ .transcript-turn.turn-new {
144
+ border-color: #1a9850;
145
+ background: rgba(26, 152, 80, 0.08);
146
+ }
147
+
148
+ .transcript-turn .turn-head {
149
+ display: flex;
150
+ justify-content: space-between;
151
+ gap: 0.5rem;
152
+ font-size: 0.75rem;
153
+ color: var(--muted);
154
+ margin-bottom: 0.25rem;
155
+ }
156
+
157
+ .transcript-turn .turn-text {
158
+ white-space: pre-wrap;
159
+ word-break: break-word;
160
+ }