@teamclaws/teamclaw 2026.3.26-2 → 2026.4.2-2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +52 -8
- package/cli.mjs +538 -224
- package/index.ts +76 -27
- package/openclaw.plugin.json +53 -28
- package/package.json +5 -2
- package/skills/teamclaw/SKILL.md +213 -0
- package/skills/teamclaw/references/api-quick-ref.md +117 -0
- package/skills/teamclaw-setup/SKILL.md +81 -0
- package/skills/teamclaw-setup/references/install-modes.md +136 -0
- package/skills/teamclaw-setup/references/validation-checklist.md +73 -0
- package/src/config.ts +44 -16
- package/src/controller/controller-capacity.ts +2 -2
- package/src/controller/controller-service.ts +193 -47
- package/src/controller/controller-tools.ts +102 -2
- package/src/controller/delivery-report.ts +563 -0
- package/src/controller/http-server.ts +1907 -172
- package/src/controller/kickoff-orchestrator.ts +292 -0
- package/src/controller/managed-gateway-process.ts +330 -0
- package/src/controller/orchestration-manifest.ts +69 -1
- package/src/controller/preview-manager.ts +676 -0
- package/src/controller/prompt-injector.ts +116 -67
- package/src/controller/role-inference.ts +41 -0
- package/src/controller/websocket.ts +3 -1
- package/src/controller/worker-provisioning.ts +429 -74
- package/src/discovery.ts +1 -1
- package/src/git-collaboration.ts +198 -47
- package/src/identity.ts +12 -2
- package/src/interaction-contracts.ts +179 -3
- package/src/networking.ts +99 -0
- package/src/openclaw-workspace.ts +478 -11
- package/src/prompt-policy.ts +381 -0
- package/src/roles.ts +37 -36
- package/src/state.ts +40 -1
- package/src/task-executor.ts +282 -78
- package/src/types.ts +150 -7
- package/src/ui/app.js +1403 -175
- package/src/ui/assets/teamclaw-app-icon.png +0 -0
- package/src/ui/index.html +122 -40
- package/src/ui/style.css +829 -143
- package/src/worker/http-handler.ts +40 -4
- package/src/worker/prompt-injector.ts +9 -38
- package/src/worker/skill-installer.ts +2 -2
- package/src/worker/tools.ts +31 -5
- package/src/worker/worker-service.ts +49 -8
- package/src/workspace-browser.ts +20 -7
- package/src/controller/local-worker-manager.ts +0 -533
|
@@ -0,0 +1,563 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Delivery Report — auto-generated project completion report for TeamClaw sessions.
|
|
3
|
+
*
|
|
4
|
+
* When all tasks in a controller session finish, this module aggregates
|
|
5
|
+
* task results, deliverables, previews, and timeline into a self-contained
|
|
6
|
+
* HTML page that can be shared via URL or pushed to notification channels.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type {
|
|
10
|
+
TeamState,
|
|
11
|
+
TaskInfo,
|
|
12
|
+
ControllerRunInfo,
|
|
13
|
+
WorkerTaskResultDeliverable,
|
|
14
|
+
} from "../types.js";
|
|
15
|
+
|
|
16
|
+
// ── Report data model ────────────────────────────────────────────────
|
|
17
|
+
|
|
18
|
+
export type DeliveryReportPhase = {
|
|
19
|
+
taskId: string;
|
|
20
|
+
title: string;
|
|
21
|
+
role: string;
|
|
22
|
+
status: string;
|
|
23
|
+
durationMs: number;
|
|
24
|
+
summary: string;
|
|
25
|
+
keyPoints: string[];
|
|
26
|
+
error?: string;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export type DeliveryReportDeliverable = {
|
|
30
|
+
taskId: string;
|
|
31
|
+
kind: string;
|
|
32
|
+
path: string;
|
|
33
|
+
summary: string;
|
|
34
|
+
artifactType?: string;
|
|
35
|
+
previewUrl?: string;
|
|
36
|
+
previewError?: string;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export type DeliveryReport = {
|
|
40
|
+
id: string;
|
|
41
|
+
sessionKey: string;
|
|
42
|
+
generatedAt: number;
|
|
43
|
+
|
|
44
|
+
// Header
|
|
45
|
+
projectName: string;
|
|
46
|
+
requirementSummary: string;
|
|
47
|
+
status: "completed" | "partial" | "failed";
|
|
48
|
+
totalDurationMs: number;
|
|
49
|
+
|
|
50
|
+
// Pipeline
|
|
51
|
+
phases: DeliveryReportPhase[];
|
|
52
|
+
|
|
53
|
+
// Deliverables
|
|
54
|
+
deliverables: DeliveryReportDeliverable[];
|
|
55
|
+
|
|
56
|
+
// Highlights
|
|
57
|
+
keyPoints: string[];
|
|
58
|
+
blockers: string[];
|
|
59
|
+
followUps: string[];
|
|
60
|
+
notes: string;
|
|
61
|
+
|
|
62
|
+
// Meta
|
|
63
|
+
runCount: number;
|
|
64
|
+
taskCount: number;
|
|
65
|
+
rolesUsed: string[];
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
// ── Report generation ────────────────────────────────────────────────
|
|
69
|
+
|
|
70
|
+
export function generateDeliveryReport(
|
|
71
|
+
sessionKey: string,
|
|
72
|
+
state: TeamState,
|
|
73
|
+
normalizeSessionKey: (key: unknown) => string,
|
|
74
|
+
): DeliveryReport | null {
|
|
75
|
+
const normalizedKey = normalizeSessionKey(sessionKey);
|
|
76
|
+
|
|
77
|
+
// Collect all controller runs for this session
|
|
78
|
+
const runs = Object.values(state.controllerRuns)
|
|
79
|
+
.filter((r) => normalizeSessionKey(r.sessionKey) === normalizedKey)
|
|
80
|
+
.sort((a, b) => a.createdAt - b.createdAt);
|
|
81
|
+
|
|
82
|
+
if (runs.length === 0) return null;
|
|
83
|
+
|
|
84
|
+
// Collect all task IDs across all runs
|
|
85
|
+
const allTaskIds = Array.from(new Set(runs.flatMap((r) => r.createdTaskIds)));
|
|
86
|
+
const tasks = allTaskIds
|
|
87
|
+
.map((id) => state.tasks[id])
|
|
88
|
+
.filter((t): t is TaskInfo => !!t)
|
|
89
|
+
.sort((a, b) => a.createdAt - b.createdAt);
|
|
90
|
+
|
|
91
|
+
if (tasks.length === 0) return null;
|
|
92
|
+
|
|
93
|
+
// Determine overall status
|
|
94
|
+
const failedTasks = tasks.filter((t) => t.status === "failed");
|
|
95
|
+
const completedTasks = tasks.filter((t) => t.status === "completed");
|
|
96
|
+
const activeTasks = tasks.filter((t) =>
|
|
97
|
+
t.status === "in_progress" || t.status === "pending" || t.status === "assigned",
|
|
98
|
+
);
|
|
99
|
+
let status: DeliveryReport["status"] = "completed";
|
|
100
|
+
if (failedTasks.length > 0 && completedTasks.length === 0) status = "failed";
|
|
101
|
+
else if (activeTasks.length > 0 || failedTasks.length > 0) status = "partial";
|
|
102
|
+
|
|
103
|
+
// Find the best project name and summary
|
|
104
|
+
const latestManifest = [...runs].reverse().find((r) => r.manifest)?.manifest;
|
|
105
|
+
const firstManifest = runs.find((r) => r.manifest)?.manifest;
|
|
106
|
+
const projectName =
|
|
107
|
+
latestManifest?.projectName || firstManifest?.projectName || runs[0].projectDir || "Untitled Project";
|
|
108
|
+
const requirementSummary =
|
|
109
|
+
firstManifest?.requirementSummary || runs[0].request || "";
|
|
110
|
+
|
|
111
|
+
// Timeline
|
|
112
|
+
const sessionStart = runs[0].createdAt;
|
|
113
|
+
const lastCompletion = Math.max(...tasks.map((t) => t.completedAt ?? t.updatedAt));
|
|
114
|
+
const totalDurationMs = lastCompletion - sessionStart;
|
|
115
|
+
|
|
116
|
+
// Build phases
|
|
117
|
+
const phases: DeliveryReportPhase[] = tasks.map((task) => ({
|
|
118
|
+
taskId: task.id,
|
|
119
|
+
title: task.title,
|
|
120
|
+
role: task.assignedRole ?? "unknown",
|
|
121
|
+
status: task.status,
|
|
122
|
+
durationMs: (task.completedAt ?? task.updatedAt) - task.createdAt,
|
|
123
|
+
summary: task.resultContract?.summary ?? task.result?.slice(0, 200) ?? "",
|
|
124
|
+
keyPoints: task.resultContract?.keyPoints ?? [],
|
|
125
|
+
error: task.error,
|
|
126
|
+
}));
|
|
127
|
+
|
|
128
|
+
// Collect deliverables
|
|
129
|
+
const deliverables: DeliveryReportDeliverable[] = [];
|
|
130
|
+
for (const task of tasks) {
|
|
131
|
+
if (!task.resultContract?.deliverables) continue;
|
|
132
|
+
for (const [di, d] of task.resultContract.deliverables.entries()) {
|
|
133
|
+
const preview = resolvePreviewInfo(d, task.id, di, state);
|
|
134
|
+
deliverables.push({
|
|
135
|
+
taskId: task.id,
|
|
136
|
+
kind: d.kind,
|
|
137
|
+
path: d.value,
|
|
138
|
+
summary: d.summary ?? "",
|
|
139
|
+
artifactType: d.artifactType,
|
|
140
|
+
previewUrl: preview.url,
|
|
141
|
+
previewError: preview.error,
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Aggregate highlights
|
|
147
|
+
const keyPoints = tasks.flatMap((t) => t.resultContract?.keyPoints ?? []);
|
|
148
|
+
const blockers = tasks.flatMap((t) => t.resultContract?.blockers ?? []);
|
|
149
|
+
const followUps = tasks.flatMap((t) =>
|
|
150
|
+
(t.resultContract?.followUps ?? []).map(
|
|
151
|
+
(f) => `${f.type}${f.targetRole ? ` (${f.targetRole})` : ""}: ${f.reason}`,
|
|
152
|
+
),
|
|
153
|
+
);
|
|
154
|
+
const notes = latestManifest?.notes ?? "";
|
|
155
|
+
|
|
156
|
+
const rolesUsed = Array.from(new Set(tasks.map((t) => t.assignedRole).filter(Boolean) as string[]));
|
|
157
|
+
|
|
158
|
+
return {
|
|
159
|
+
id: `report-${normalizedKey}`,
|
|
160
|
+
sessionKey: normalizedKey,
|
|
161
|
+
generatedAt: Date.now(),
|
|
162
|
+
projectName,
|
|
163
|
+
requirementSummary,
|
|
164
|
+
status,
|
|
165
|
+
totalDurationMs,
|
|
166
|
+
phases,
|
|
167
|
+
deliverables,
|
|
168
|
+
keyPoints,
|
|
169
|
+
blockers,
|
|
170
|
+
followUps,
|
|
171
|
+
notes,
|
|
172
|
+
runCount: runs.length,
|
|
173
|
+
taskCount: tasks.length,
|
|
174
|
+
rolesUsed,
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function isPreviewableArtifactType(d: WorkerTaskResultDeliverable): boolean {
|
|
179
|
+
return d.artifactType === "web-app" || d.artifactType === "static-site" || d.artifactType === "rest-api";
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function resolvePreviewInfo(
|
|
183
|
+
deliverable: WorkerTaskResultDeliverable,
|
|
184
|
+
taskId: string,
|
|
185
|
+
deliverableIndex: number,
|
|
186
|
+
state: TeamState,
|
|
187
|
+
): { url?: string; error?: string } {
|
|
188
|
+
if (deliverable.liveUrl) return { url: deliverable.liveUrl };
|
|
189
|
+
|
|
190
|
+
// Non-previewable deliverables (plain files, notes, commands) should never
|
|
191
|
+
// show preview errors — they don't have previews to fail.
|
|
192
|
+
if (!isPreviewableArtifactType(deliverable)) return {};
|
|
193
|
+
|
|
194
|
+
// Find preview record for this specific deliverable
|
|
195
|
+
const previewId = `preview-${taskId}-${deliverableIndex}`;
|
|
196
|
+
const exact = (state.previews ?? {})[previewId];
|
|
197
|
+
if (exact) {
|
|
198
|
+
if (exact.status === "healthy") {
|
|
199
|
+
const url = deliverable.artifactType === "rest-api" && exact.previewReadyPath && exact.previewReadyPath !== "/"
|
|
200
|
+
? exact.liveUrl.replace(/\/$/, "") + exact.previewReadyPath
|
|
201
|
+
: exact.liveUrl;
|
|
202
|
+
return { url };
|
|
203
|
+
}
|
|
204
|
+
if (exact.status === "failed") return { error: exact.lastError ?? "Preview failed" };
|
|
205
|
+
if (exact.status === "stopped") return { error: "Preview stopped" };
|
|
206
|
+
return { error: "Preview is still starting…" };
|
|
207
|
+
}
|
|
208
|
+
// Fallback: find any healthy preview for this task
|
|
209
|
+
const previews = Object.values(state.previews ?? {});
|
|
210
|
+
const match = previews.find((p) => p.taskId === taskId && p.status === "healthy");
|
|
211
|
+
if (match) {
|
|
212
|
+
const url = deliverable.artifactType === "rest-api" && match.previewReadyPath && match.previewReadyPath !== "/"
|
|
213
|
+
? match.liveUrl.replace(/\/$/, "") + match.previewReadyPath
|
|
214
|
+
: match.liveUrl;
|
|
215
|
+
return { url };
|
|
216
|
+
}
|
|
217
|
+
const failed = previews.find((p) => p.taskId === taskId && p.status === "failed");
|
|
218
|
+
if (failed) return { error: failed.lastError ?? "Preview failed" };
|
|
219
|
+
return {};
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// ── Session completion detection ─────────────────────────────────────
|
|
223
|
+
|
|
224
|
+
export function isSessionComplete(
|
|
225
|
+
sessionKey: string,
|
|
226
|
+
state: TeamState,
|
|
227
|
+
normalizeSessionKey: (key: unknown) => string,
|
|
228
|
+
): boolean {
|
|
229
|
+
const normalizedKey = normalizeSessionKey(sessionKey);
|
|
230
|
+
const runs = Object.values(state.controllerRuns)
|
|
231
|
+
.filter((r) => normalizeSessionKey(r.sessionKey) === normalizedKey)
|
|
232
|
+
.sort((a, b) => b.createdAt - a.createdAt);
|
|
233
|
+
|
|
234
|
+
if (runs.length === 0) return false;
|
|
235
|
+
|
|
236
|
+
// Collect all tasks for this session
|
|
237
|
+
const taskIds = new Set(runs.flatMap((r) => r.createdTaskIds));
|
|
238
|
+
if (taskIds.size === 0) return false;
|
|
239
|
+
|
|
240
|
+
// Check if any task is still active
|
|
241
|
+
for (const taskId of taskIds) {
|
|
242
|
+
const task = state.tasks[taskId];
|
|
243
|
+
if (!task) continue;
|
|
244
|
+
if (task.status !== "completed" && task.status !== "failed") {
|
|
245
|
+
return false;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Check if the latest run still has active deferred tasks
|
|
250
|
+
const latestWithManifest = runs.find((r) => r.manifest);
|
|
251
|
+
if (latestWithManifest?.manifest?.deferredTasks?.length) {
|
|
252
|
+
// There are deferred tasks — a follow-up run should advance them.
|
|
253
|
+
// Only consider complete if the latest run also says requirementFullyComplete.
|
|
254
|
+
if (!latestWithManifest.manifest.requirementFullyComplete) {
|
|
255
|
+
return false;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Check if any run is still actively executing
|
|
260
|
+
const activeRun = runs.find((r) => r.status === "pending" || r.status === "running");
|
|
261
|
+
if (activeRun) return false;
|
|
262
|
+
|
|
263
|
+
return true;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// ── HTML rendering ───────────────────────────────────────────────────
|
|
267
|
+
|
|
268
|
+
function escapeHtml(text: string): string {
|
|
269
|
+
return text
|
|
270
|
+
.replace(/&/g, "&")
|
|
271
|
+
.replace(/</g, "<")
|
|
272
|
+
.replace(/>/g, ">")
|
|
273
|
+
.replace(/"/g, """);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const IMAGE_EXTENSIONS = new Set([".png", ".jpg", ".jpeg", ".gif", ".svg", ".webp", ".ico", ".bmp"]);
|
|
277
|
+
|
|
278
|
+
function isImagePath(filePath: string): boolean {
|
|
279
|
+
const ext = filePath.slice(filePath.lastIndexOf(".")).toLowerCase();
|
|
280
|
+
return IMAGE_EXTENSIONS.has(ext);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function formatDuration(ms: number): string {
|
|
284
|
+
if (ms < 1000) return "<1s";
|
|
285
|
+
const totalSec = Math.floor(ms / 1000);
|
|
286
|
+
if (totalSec < 60) return `${totalSec}s`;
|
|
287
|
+
const min = Math.floor(totalSec / 60);
|
|
288
|
+
const sec = totalSec % 60;
|
|
289
|
+
if (min < 60) return sec > 0 ? `${min}m ${sec}s` : `${min}m`;
|
|
290
|
+
const hr = Math.floor(min / 60);
|
|
291
|
+
const remMin = min % 60;
|
|
292
|
+
return remMin > 0 ? `${hr}h ${remMin}m` : `${hr}h`;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
const STATUS_EMOJI: Record<string, string> = {
|
|
296
|
+
completed: "✅",
|
|
297
|
+
failed: "❌",
|
|
298
|
+
partial: "⚠️",
|
|
299
|
+
in_progress: "🔄",
|
|
300
|
+
pending: "⏳",
|
|
301
|
+
assigned: "📋",
|
|
302
|
+
blocked: "🚫",
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
export function renderReportHtml(report: DeliveryReport): string {
|
|
306
|
+
const statusEmoji = STATUS_EMOJI[report.status] ?? "❓";
|
|
307
|
+
const statusLabel =
|
|
308
|
+
report.status === "completed"
|
|
309
|
+
? "Completed"
|
|
310
|
+
: report.status === "failed"
|
|
311
|
+
? "Failed"
|
|
312
|
+
: "Partial";
|
|
313
|
+
|
|
314
|
+
const phasesHtml = report.phases
|
|
315
|
+
.map((phase) => {
|
|
316
|
+
const emoji = STATUS_EMOJI[phase.status] ?? "❓";
|
|
317
|
+
const duration = formatDuration(phase.durationMs);
|
|
318
|
+
const errorHtml = phase.error
|
|
319
|
+
? `<div class="phase-error">Error: ${escapeHtml(phase.error.slice(0, 200))}</div>`
|
|
320
|
+
: "";
|
|
321
|
+
const keyPointsHtml =
|
|
322
|
+
phase.keyPoints.length > 0
|
|
323
|
+
? `<ul class="phase-keypoints">${phase.keyPoints.map((kp) => `<li>${escapeHtml(kp)}</li>`).join("")}</ul>`
|
|
324
|
+
: "";
|
|
325
|
+
return `
|
|
326
|
+
<div class="phase-card phase-${phase.status}">
|
|
327
|
+
<div class="phase-header">
|
|
328
|
+
<span class="phase-emoji">${emoji}</span>
|
|
329
|
+
<span class="phase-title">${escapeHtml(phase.title)}</span>
|
|
330
|
+
<span class="phase-role">${escapeHtml(phase.role)}</span>
|
|
331
|
+
<span class="phase-duration">${duration}</span>
|
|
332
|
+
</div>
|
|
333
|
+
<div class="phase-summary">${escapeHtml(phase.summary)}</div>
|
|
334
|
+
${errorHtml}
|
|
335
|
+
${keyPointsHtml}
|
|
336
|
+
</div>`;
|
|
337
|
+
})
|
|
338
|
+
.join("\n");
|
|
339
|
+
|
|
340
|
+
const deliverablesHtml = report.deliverables
|
|
341
|
+
.map((d) => {
|
|
342
|
+
const kindIcon =
|
|
343
|
+
d.artifactType === "rest-api"
|
|
344
|
+
? "🔌"
|
|
345
|
+
: d.artifactType === "web-app" || d.artifactType === "static-site"
|
|
346
|
+
? "🌐"
|
|
347
|
+
: d.artifactType === "document"
|
|
348
|
+
? "📝"
|
|
349
|
+
: d.kind === "directory"
|
|
350
|
+
? "📁"
|
|
351
|
+
: d.kind === "command"
|
|
352
|
+
? "💻"
|
|
353
|
+
: isImagePath(d.path)
|
|
354
|
+
? "🖼️"
|
|
355
|
+
: "📄";
|
|
356
|
+
let previewHtml = "";
|
|
357
|
+
if (d.previewUrl) {
|
|
358
|
+
const previewLabel = d.artifactType === "rest-api"
|
|
359
|
+
? "📖 Open API Documentation"
|
|
360
|
+
: "🔗 Open Live Preview";
|
|
361
|
+
previewHtml = `<div class="deliverable-preview">
|
|
362
|
+
<a href="${escapeHtml(d.previewUrl)}" target="_blank" class="preview-link">${previewLabel}</a>
|
|
363
|
+
<iframe src="${escapeHtml(d.previewUrl)}" class="preview-iframe" loading="lazy" sandbox="allow-scripts allow-same-origin allow-forms"></iframe>
|
|
364
|
+
</div>`;
|
|
365
|
+
} else if (d.previewError) {
|
|
366
|
+
previewHtml = `<div class="preview-error">⚠️ ${escapeHtml(d.previewError)}</div>`;
|
|
367
|
+
} else if (d.kind === "command") {
|
|
368
|
+
previewHtml = `<div class="deliverable-run-hint">💡 Run: <code>${escapeHtml(d.path)}</code></div>`;
|
|
369
|
+
} else if (d.artifactType === "document") {
|
|
370
|
+
previewHtml = `<div class="deliverable-doc-hint">📖 Document — view in workspace at <code>${escapeHtml(d.path)}</code></div>`;
|
|
371
|
+
} else if (isImagePath(d.path)) {
|
|
372
|
+
// Serve image via workspace file endpoint if available
|
|
373
|
+
previewHtml = `<div class="deliverable-image-hint">🖼️ Image file — open from workspace</div>`;
|
|
374
|
+
}
|
|
375
|
+
return `
|
|
376
|
+
<div class="deliverable-card">
|
|
377
|
+
<div class="deliverable-header">
|
|
378
|
+
<span class="deliverable-icon">${kindIcon}</span>
|
|
379
|
+
<span class="deliverable-path">${escapeHtml(d.path)}</span>
|
|
380
|
+
</div>
|
|
381
|
+
<div class="deliverable-summary">${escapeHtml(d.summary)}</div>
|
|
382
|
+
${previewHtml}
|
|
383
|
+
</div>`;
|
|
384
|
+
})
|
|
385
|
+
.join("\n");
|
|
386
|
+
|
|
387
|
+
const keyPointsHtml =
|
|
388
|
+
report.keyPoints.length > 0
|
|
389
|
+
? `<section class="section">
|
|
390
|
+
<h2>💡 Key Points</h2>
|
|
391
|
+
<ul>${report.keyPoints.map((kp) => `<li>${escapeHtml(kp)}</li>`).join("")}</ul>
|
|
392
|
+
</section>`
|
|
393
|
+
: "";
|
|
394
|
+
|
|
395
|
+
const blockersHtml =
|
|
396
|
+
report.blockers.length > 0
|
|
397
|
+
? `<section class="section section-warning">
|
|
398
|
+
<h2>🚧 Blockers</h2>
|
|
399
|
+
<ul>${report.blockers.map((b) => `<li>${escapeHtml(b)}</li>`).join("")}</ul>
|
|
400
|
+
</section>`
|
|
401
|
+
: "";
|
|
402
|
+
|
|
403
|
+
const followUpsHtml =
|
|
404
|
+
report.followUps.length > 0
|
|
405
|
+
? `<section class="section">
|
|
406
|
+
<h2>📌 Follow-ups</h2>
|
|
407
|
+
<ul>${report.followUps.map((f) => `<li>${escapeHtml(f)}</li>`).join("")}</ul>
|
|
408
|
+
</section>`
|
|
409
|
+
: "";
|
|
410
|
+
|
|
411
|
+
const notesHtml = report.notes
|
|
412
|
+
? `<section class="section"><h2>📝 Notes</h2><p>${escapeHtml(report.notes)}</p></section>`
|
|
413
|
+
: "";
|
|
414
|
+
|
|
415
|
+
return `<!DOCTYPE html>
|
|
416
|
+
<html lang="en">
|
|
417
|
+
<head>
|
|
418
|
+
<meta charset="UTF-8">
|
|
419
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
420
|
+
<title>TeamClaw Report — ${escapeHtml(report.projectName)}</title>
|
|
421
|
+
<style>
|
|
422
|
+
:root {
|
|
423
|
+
--bg: #f8f9fa; --card: #fff; --border: #e2e8f0; --text: #1a202c;
|
|
424
|
+
--muted: #718096; --accent: #3182ce; --success: #38a169; --danger: #e53e3e;
|
|
425
|
+
--warning: #d69e2e; --radius: 10px; --shadow: 0 1px 3px rgba(0,0,0,.08);
|
|
426
|
+
}
|
|
427
|
+
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
428
|
+
body {
|
|
429
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
|
430
|
+
background: var(--bg); color: var(--text); line-height: 1.6;
|
|
431
|
+
max-width: 900px; margin: 0 auto; padding: 24px 16px;
|
|
432
|
+
}
|
|
433
|
+
.header {
|
|
434
|
+
background: linear-gradient(135deg, #2d3748, #1a365d);
|
|
435
|
+
color: #fff; border-radius: var(--radius); padding: 32px; margin-bottom: 24px;
|
|
436
|
+
}
|
|
437
|
+
.header h1 { font-size: 1.5rem; font-weight: 700; margin-bottom: 8px; }
|
|
438
|
+
.header-meta { display: flex; gap: 16px; flex-wrap: wrap; font-size: .9rem; opacity: .85; }
|
|
439
|
+
.header-meta span { display: flex; align-items: center; gap: 4px; }
|
|
440
|
+
.requirement {
|
|
441
|
+
background: rgba(255,255,255,.1); border-radius: 6px;
|
|
442
|
+
padding: 12px; margin-top: 16px; font-size: .9rem; line-height: 1.5;
|
|
443
|
+
}
|
|
444
|
+
.section { background: var(--card); border: 1px solid var(--border); border-radius: var(--radius);
|
|
445
|
+
padding: 20px; margin-bottom: 16px; box-shadow: var(--shadow); }
|
|
446
|
+
.section h2 { font-size: 1.1rem; margin-bottom: 12px; }
|
|
447
|
+
.section ul { padding-left: 20px; }
|
|
448
|
+
.section li { margin-bottom: 4px; font-size: .9rem; }
|
|
449
|
+
.section-warning { border-left: 4px solid var(--warning); }
|
|
450
|
+
|
|
451
|
+
/* Pipeline */
|
|
452
|
+
.pipeline { display: flex; flex-direction: column; gap: 8px; }
|
|
453
|
+
.phase-card {
|
|
454
|
+
background: var(--card); border: 1px solid var(--border); border-radius: 8px;
|
|
455
|
+
padding: 14px 16px; box-shadow: var(--shadow); position: relative;
|
|
456
|
+
}
|
|
457
|
+
.phase-card.phase-completed { border-left: 4px solid var(--success); }
|
|
458
|
+
.phase-card.phase-failed { border-left: 4px solid var(--danger); }
|
|
459
|
+
.phase-card.phase-in_progress { border-left: 4px solid var(--accent); }
|
|
460
|
+
.phase-header {
|
|
461
|
+
display: flex; align-items: center; gap: 8px; flex-wrap: wrap; margin-bottom: 6px;
|
|
462
|
+
}
|
|
463
|
+
.phase-title { font-weight: 600; flex: 1; }
|
|
464
|
+
.phase-role {
|
|
465
|
+
background: #edf2f7; color: var(--muted); font-size: .75rem; padding: 2px 8px;
|
|
466
|
+
border-radius: 12px; text-transform: uppercase; letter-spacing: .5px;
|
|
467
|
+
}
|
|
468
|
+
.phase-duration { font-size: .8rem; color: var(--muted); }
|
|
469
|
+
.phase-summary { font-size: .85rem; color: #4a5568; }
|
|
470
|
+
.phase-error { font-size: .85rem; color: var(--danger); margin-top: 6px; }
|
|
471
|
+
.phase-keypoints { font-size: .8rem; color: var(--muted); margin-top: 6px; padding-left: 18px; }
|
|
472
|
+
|
|
473
|
+
/* Deliverables */
|
|
474
|
+
.deliverable-card {
|
|
475
|
+
background: var(--card); border: 1px solid var(--border); border-radius: 8px;
|
|
476
|
+
padding: 16px; margin-bottom: 12px; box-shadow: var(--shadow);
|
|
477
|
+
}
|
|
478
|
+
.deliverable-header { display: flex; align-items: center; gap: 8px; margin-bottom: 6px; }
|
|
479
|
+
.deliverable-icon { font-size: 1.3rem; }
|
|
480
|
+
.deliverable-path { font-family: "SF Mono", Monaco, monospace; font-size: .85rem; color: var(--accent); }
|
|
481
|
+
.deliverable-summary { font-size: .85rem; color: #4a5568; }
|
|
482
|
+
.deliverable-preview { margin-top: 12px; }
|
|
483
|
+
.preview-link {
|
|
484
|
+
display: inline-block; margin-bottom: 8px; font-size: .85rem;
|
|
485
|
+
color: var(--accent); text-decoration: none; font-weight: 500;
|
|
486
|
+
}
|
|
487
|
+
.preview-link:hover { text-decoration: underline; }
|
|
488
|
+
.preview-iframe {
|
|
489
|
+
width: 100%; height: 400px; border: 1px solid var(--border);
|
|
490
|
+
border-radius: 6px; background: #fff;
|
|
491
|
+
}
|
|
492
|
+
.preview-error {
|
|
493
|
+
margin-top: 8px; padding: 10px 14px; background: #fff5f5; border: 1px solid #fed7d7;
|
|
494
|
+
border-radius: 6px; font-size: .85rem; color: var(--danger);
|
|
495
|
+
}
|
|
496
|
+
.deliverable-run-hint {
|
|
497
|
+
margin-top: 8px; padding: 8px 12px; background: #ebf8ff; border: 1px solid #bee3f8;
|
|
498
|
+
border-radius: 6px; font-size: .85rem; color: #2a4365;
|
|
499
|
+
}
|
|
500
|
+
.deliverable-run-hint code {
|
|
501
|
+
background: #e2e8f0; padding: 1px 6px; border-radius: 4px; font-size: .82rem;
|
|
502
|
+
}
|
|
503
|
+
.deliverable-doc-hint {
|
|
504
|
+
margin-top: 8px; padding: 8px 12px; background: #f0fff4; border: 1px solid #c6f6d5;
|
|
505
|
+
border-radius: 6px; font-size: .85rem; color: #22543d;
|
|
506
|
+
}
|
|
507
|
+
.deliverable-doc-hint code, .deliverable-image-hint code {
|
|
508
|
+
background: #e2e8f0; padding: 1px 6px; border-radius: 4px; font-size: .82rem;
|
|
509
|
+
}
|
|
510
|
+
.deliverable-image-hint {
|
|
511
|
+
margin-top: 8px; padding: 8px 12px; background: #faf5ff; border: 1px solid #e9d8fd;
|
|
512
|
+
border-radius: 6px; font-size: .85rem; color: #44337a;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
.footer {
|
|
516
|
+
text-align: center; font-size: .8rem; color: var(--muted);
|
|
517
|
+
padding: 16px 0; margin-top: 8px;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
@media (max-width: 600px) {
|
|
521
|
+
body { padding: 12px 8px; }
|
|
522
|
+
.header { padding: 20px 16px; }
|
|
523
|
+
.header h1 { font-size: 1.2rem; }
|
|
524
|
+
.preview-iframe { height: 280px; }
|
|
525
|
+
}
|
|
526
|
+
</style>
|
|
527
|
+
</head>
|
|
528
|
+
<body>
|
|
529
|
+
<div class="header">
|
|
530
|
+
<h1>${statusEmoji} ${escapeHtml(report.projectName)}</h1>
|
|
531
|
+
<div class="header-meta">
|
|
532
|
+
<span>Status: <strong>${statusLabel}</strong></span>
|
|
533
|
+
<span>⏱️ ${formatDuration(report.totalDurationMs)}</span>
|
|
534
|
+
<span>📋 ${report.taskCount} task${report.taskCount !== 1 ? "s" : ""}</span>
|
|
535
|
+
<span>👥 ${report.rolesUsed.join(", ") || "—"}</span>
|
|
536
|
+
</div>
|
|
537
|
+
<div class="requirement">${escapeHtml(report.requirementSummary)}</div>
|
|
538
|
+
</div>
|
|
539
|
+
|
|
540
|
+
<section class="section">
|
|
541
|
+
<h2>📋 Task Pipeline</h2>
|
|
542
|
+
<div class="pipeline">
|
|
543
|
+
${phasesHtml}
|
|
544
|
+
</div>
|
|
545
|
+
</section>
|
|
546
|
+
|
|
547
|
+
${report.deliverables.length > 0 ? `
|
|
548
|
+
<section class="section">
|
|
549
|
+
<h2>📦 Deliverables</h2>
|
|
550
|
+
${deliverablesHtml}
|
|
551
|
+
</section>` : ""}
|
|
552
|
+
|
|
553
|
+
${keyPointsHtml}
|
|
554
|
+
${blockersHtml}
|
|
555
|
+
${followUpsHtml}
|
|
556
|
+
${notesHtml}
|
|
557
|
+
|
|
558
|
+
<div class="footer">
|
|
559
|
+
Generated by TeamClaw · ${new Date(report.generatedAt).toLocaleString()}
|
|
560
|
+
</div>
|
|
561
|
+
</body>
|
|
562
|
+
</html>`;
|
|
563
|
+
}
|