@swarmclawai/swarmclaw 1.7.3 → 1.8.1

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 (42) hide show
  1. package/README.md +20 -0
  2. package/next.config.ts +38 -8
  3. package/package.json +2 -2
  4. package/scripts/run-next-build.mjs +51 -3
  5. package/src/app/api/artifacts/route.ts +15 -0
  6. package/src/app/api/clawhub/install/route.ts +4 -4
  7. package/src/app/api/dirs/route.ts +8 -5
  8. package/src/app/api/files/open/route.ts +3 -3
  9. package/src/app/api/files/serve/route.ts +2 -2
  10. package/src/app/api/operations/pulse/route.ts +9 -0
  11. package/src/app/api/runs/[id]/brief/route.ts +12 -0
  12. package/src/app/api/runs/[id]/events/route.ts +4 -13
  13. package/src/app/api/runs/[id]/route.ts +2 -6
  14. package/src/app/api/runs/route.ts +3 -43
  15. package/src/app/api/s/[token]/raw/route.ts +1 -1
  16. package/src/app/home/page.tsx +11 -1
  17. package/src/app/missions/page.tsx +182 -3
  18. package/src/app/s/[token]/page.tsx +173 -48
  19. package/src/cli/index.js +15 -0
  20. package/src/cli/spec.js +13 -0
  21. package/src/components/connectors/connector-list.tsx +36 -20
  22. package/src/components/evidence/evidence-shelf.tsx +97 -0
  23. package/src/components/home/home-launchpad.tsx +52 -2
  24. package/src/components/missions/mission-template-install-dialog.tsx +33 -1
  25. package/src/components/operations/operations-pulse-panel.tsx +184 -0
  26. package/src/components/quality/quality-workspace.tsx +34 -6
  27. package/src/components/runs/run-list.tsx +94 -12
  28. package/src/lib/connectors/connector-readiness.ts +127 -0
  29. package/src/lib/server/artifacts/artifact-resolver.test.ts +98 -0
  30. package/src/lib/server/artifacts/artifact-resolver.ts +241 -0
  31. package/src/lib/server/operations/operation-pulse.test.ts +108 -0
  32. package/src/lib/server/operations/operation-pulse.ts +197 -0
  33. package/src/lib/server/resolve-workspace-path.ts +10 -10
  34. package/src/lib/server/runs/run-brief.test.ts +92 -0
  35. package/src/lib/server/runs/run-brief.ts +107 -0
  36. package/src/lib/server/runs/unified-run-queries.ts +84 -0
  37. package/src/lib/server/sharing/share-resolver.test.ts +129 -0
  38. package/src/lib/server/sharing/share-resolver.ts +48 -3
  39. package/src/types/artifact.ts +28 -0
  40. package/src/types/index.ts +3 -0
  41. package/src/types/operations.ts +39 -0
  42. package/src/types/run-brief.ts +41 -0
@@ -1,8 +1,10 @@
1
1
  'use client'
2
2
 
3
3
  import { useCallback, useEffect, useMemo, useState } from 'react'
4
+ import { useRouter, useSearchParams } from 'next/navigation'
4
5
  import { api } from '@/lib/app/api-client'
5
6
  import { MainContent } from '@/components/layout/main-content'
7
+ import { EvidenceShelf } from '@/components/evidence/evidence-shelf'
6
8
  import { HintTip } from '@/components/shared/hint-tip'
7
9
  import { inputClass } from '@/components/shared/form-styles'
8
10
  import { MissionTemplateGallery } from '@/components/missions/mission-template-gallery'
@@ -11,10 +13,22 @@ import {
11
13
  type InstantiateInput,
12
14
  } from '@/components/missions/mission-template-install-dialog'
13
15
  import { MissionEditSheet, isMissionEditable } from '@/components/missions/mission-edit-sheet'
14
- import type { Mission, MissionReport, MissionEvent, MissionTemplate, Session } from '@/types'
16
+ import type { EvidenceArtifact, Mission, MissionReport, MissionEvent, MissionTemplate, Session } from '@/types'
15
17
  import { toast } from 'sonner'
16
18
 
17
19
  const POLL_MS = 4_000
20
+ const RELEASE_QA_TEMPLATE_ID = 'release-candidate-qa'
21
+
22
+ interface ShareLink {
23
+ id: string
24
+ token: string
25
+ entityType: 'mission' | 'skill' | 'session'
26
+ entityId: string
27
+ label: string | null
28
+ createdAt: number
29
+ expiresAt: number | null
30
+ revokedAt: number | null
31
+ }
18
32
 
19
33
  const STATUS_BADGE: Record<Mission['status'], { label: string; cls: string }> = {
20
34
  draft: { label: 'Draft', cls: 'bg-white/[0.05] text-text-3' },
@@ -398,7 +412,88 @@ interface DetailProps {
398
412
 
399
413
  function MissionDetail({ mission, reports, events, busy, onAction, onForceReport, onEdit }: DetailProps) {
400
414
  const [selectedReport, setSelectedReport] = useState<MissionReport | null>(null)
415
+ const [shareLinks, setShareLinks] = useState<ShareLink[]>([])
416
+ const [artifacts, setArtifacts] = useState<EvidenceArtifact[]>([])
417
+ const [artifactsLoading, setArtifactsLoading] = useState(false)
418
+ const [shareBusy, setShareBusy] = useState<string | null>(null)
401
419
  const wallclockCapMs = mission.budget.maxWallclockSec != null ? mission.budget.maxWallclockSec * 1000 : null
420
+ const activeShare = useMemo(
421
+ () => shareLinks.find((link) => !link.revokedAt && (!link.expiresAt || link.expiresAt > Date.now())) ?? null,
422
+ [shareLinks],
423
+ )
424
+
425
+ const loadShareLinks = useCallback(async () => {
426
+ try {
427
+ const links = await api<ShareLink[]>('GET', `/share?entityType=mission&entityId=${encodeURIComponent(mission.id)}`)
428
+ setShareLinks(Array.isArray(links) ? links : [])
429
+ } catch {
430
+ setShareLinks([])
431
+ }
432
+ }, [mission.id])
433
+
434
+ const loadArtifacts = useCallback(async () => {
435
+ setArtifactsLoading(true)
436
+ try {
437
+ const list = await api<EvidenceArtifact[]>('GET', `/artifacts?missionId=${encodeURIComponent(mission.id)}`)
438
+ setArtifacts(Array.isArray(list) ? list : [])
439
+ } catch {
440
+ setArtifacts([])
441
+ } finally {
442
+ setArtifactsLoading(false)
443
+ }
444
+ }, [mission.id])
445
+
446
+ useEffect(() => {
447
+ void loadShareLinks()
448
+ void loadArtifacts()
449
+ }, [loadArtifacts, loadShareLinks])
450
+
451
+ const shareUrl = activeShare && typeof window !== 'undefined'
452
+ ? `${window.location.origin}/s/${activeShare.token}`
453
+ : ''
454
+
455
+ const createShareLink = useCallback(async () => {
456
+ setShareBusy('create')
457
+ try {
458
+ const link = await api<ShareLink>('POST', '/share', {
459
+ entityType: 'mission',
460
+ entityId: mission.id,
461
+ label: `${mission.title} public report`,
462
+ })
463
+ setShareLinks((prev) => [link, ...prev])
464
+ void loadArtifacts()
465
+ toast.success('Mission share link created')
466
+ } catch (error) {
467
+ toast.error(error instanceof Error ? error.message : 'Unable to create share link')
468
+ } finally {
469
+ setShareBusy(null)
470
+ }
471
+ }, [loadArtifacts, mission.id, mission.title])
472
+
473
+ const revokeShareLink = useCallback(async () => {
474
+ if (!activeShare) return
475
+ setShareBusy(activeShare.id)
476
+ try {
477
+ const revoked = await api<ShareLink>('DELETE', `/share/${activeShare.id}`)
478
+ setShareLinks((prev) => prev.map((link) => (link.id === revoked.id ? revoked : link)))
479
+ void loadArtifacts()
480
+ toast.success('Mission share link revoked')
481
+ } catch (error) {
482
+ toast.error(error instanceof Error ? error.message : 'Unable to revoke share link')
483
+ } finally {
484
+ setShareBusy(null)
485
+ }
486
+ }, [activeShare, loadArtifacts])
487
+
488
+ const copyShareUrl = useCallback(async () => {
489
+ if (!shareUrl) return
490
+ try {
491
+ await navigator.clipboard.writeText(shareUrl)
492
+ toast.success('Share URL copied')
493
+ } catch {
494
+ toast.error('Unable to copy share URL')
495
+ }
496
+ }, [shareUrl])
402
497
 
403
498
  return (
404
499
  <div className="flex flex-col gap-4 p-4">
@@ -428,6 +523,60 @@ function MissionDetail({ mission, reports, events, busy, onAction, onForceReport
428
523
  <MissionControls mission={mission} onAction={onAction} onForceReport={onForceReport} onEdit={onEdit} busy={busy} />
429
524
  </div>
430
525
 
526
+ <div className="rounded-[12px] border border-white/[0.06] bg-white/[0.025] p-4">
527
+ <div className="flex flex-col gap-3 lg:flex-row lg:items-start lg:justify-between">
528
+ <div>
529
+ <div className="text-[11px] font-600 uppercase tracking-wide text-text-3">Public share</div>
530
+ <p className="mt-1 max-w-[620px] text-[12px] leading-relaxed text-text-3/70">
531
+ Publish a revocable mission artifact with status, budgets, milestones, and generated reports. Secrets, credentials, private files, and hidden runtime metadata stay out of the payload.
532
+ </p>
533
+ </div>
534
+ <div className="flex shrink-0 flex-wrap gap-2">
535
+ {activeShare ? (
536
+ <>
537
+ <button
538
+ type="button"
539
+ onClick={() => void copyShareUrl()}
540
+ className="rounded-[9px] border border-emerald-500/25 bg-emerald-500/10 px-2.5 py-1.5 text-[11px] font-700 text-emerald-200 hover:bg-emerald-500/15"
541
+ >
542
+ Copy link
543
+ </button>
544
+ <button
545
+ type="button"
546
+ disabled={!!shareBusy}
547
+ onClick={() => void revokeShareLink()}
548
+ className="rounded-[9px] border border-rose-500/20 bg-rose-500/[0.06] px-2.5 py-1.5 text-[11px] font-700 text-rose-200 hover:bg-rose-500/[0.1] disabled:opacity-40"
549
+ >
550
+ {shareBusy === activeShare.id ? 'Revoking...' : 'Revoke'}
551
+ </button>
552
+ </>
553
+ ) : (
554
+ <button
555
+ type="button"
556
+ disabled={!!shareBusy}
557
+ onClick={() => void createShareLink()}
558
+ className="rounded-[9px] border border-emerald-500/30 bg-emerald-500/10 px-2.5 py-1.5 text-[11px] font-700 text-emerald-200 hover:bg-emerald-500/15 disabled:opacity-40"
559
+ >
560
+ {shareBusy === 'create' ? 'Creating...' : 'Create share link'}
561
+ </button>
562
+ )}
563
+ </div>
564
+ </div>
565
+ {activeShare && (
566
+ <div className="mt-3 rounded-[10px] border border-white/[0.06] bg-black/20 px-3 py-2 text-[11px] text-text-3">
567
+ <span className="font-mono text-text">{shareUrl}</span>
568
+ <span className="ml-2 text-text-3/55">Created {formatTimestamp(activeShare.createdAt)}</span>
569
+ </div>
570
+ )}
571
+ </div>
572
+
573
+ <EvidenceShelf
574
+ artifacts={artifacts}
575
+ loading={artifactsLoading}
576
+ title="Evidence Shelf"
577
+ emptyLabel="No mission reports, public share links, or milestone evidence yet."
578
+ />
579
+
431
580
  {mission.successCriteria.length > 0 && (
432
581
  <div>
433
582
  <div className="text-[11px] font-600 uppercase tracking-wide text-text-3 mb-2">Success criteria</div>
@@ -503,6 +652,8 @@ function MissionDetail({ mission, reports, events, busy, onAction, onForceReport
503
652
  }
504
653
 
505
654
  export default function MissionsPage() {
655
+ const router = useRouter()
656
+ const searchParams = useSearchParams()
506
657
  const [missions, setMissions] = useState<Mission[]>([])
507
658
  const [selectedId, setSelectedId] = useState<string | null>(null)
508
659
  const [reports, setReports] = useState<MissionReport[]>([])
@@ -570,6 +721,24 @@ export default function MissionsPage() {
570
721
  return () => { cancelled = true }
571
722
  }, [])
572
723
 
724
+ useEffect(() => {
725
+ const templateId = searchParams.get('template')?.trim()
726
+ if (!templateId || templates.length === 0) return
727
+ const template = templates.find((item) => item.id === templateId)
728
+ if (!template) return
729
+ setGalleryOpen(false)
730
+ setInstallTemplate(template)
731
+ router.replace('/missions', { scroll: false })
732
+ }, [router, searchParams, templates])
733
+
734
+ useEffect(() => {
735
+ const missionId = searchParams.get('mission')?.trim()
736
+ if (!missionId || missions.length === 0) return
737
+ if (!missions.some((mission) => mission.id === missionId)) return
738
+ setSelectedId(missionId)
739
+ router.replace('/missions', { scroll: false })
740
+ }, [missions, router, searchParams])
741
+
573
742
  const handleAction = useCallback(async (action: string, reason?: string) => {
574
743
  if (!selectedId) return
575
744
  setBusy(true)
@@ -633,10 +802,17 @@ export default function MissionsPage() {
633
802
  <div className="text-[10px] text-text-3">Autonomous goal-driven runs</div>
634
803
  </div>
635
804
  <button
636
- onClick={() => setCreateOpen(true)}
805
+ onClick={() => {
806
+ const template = templates.find((item) => item.id === RELEASE_QA_TEMPLATE_ID)
807
+ if (template) {
808
+ setInstallTemplate(template)
809
+ return
810
+ }
811
+ setCreateOpen(true)
812
+ }}
637
813
  className="text-[11px] font-600 px-2.5 py-1 rounded border border-emerald-500/30 bg-emerald-500/10 text-emerald-300 hover:bg-emerald-500/15"
638
814
  >
639
- + New
815
+ + Mission
640
816
  </button>
641
817
  </div>
642
818
  {templates.length > 0 && (
@@ -736,6 +912,9 @@ export default function MissionsPage() {
736
912
  sessions={sessions}
737
913
  onClose={() => setInstallTemplate(null)}
738
914
  onInstall={handleInstallTemplate}
915
+ onSessionCreated={(session) => {
916
+ setSessions((prev) => [session, ...prev.filter((item) => item.id !== session.id)])
917
+ }}
739
918
  />
740
919
 
741
920
  <MissionEditSheet
@@ -19,65 +19,190 @@ export default async function SharedEntityPage({
19
19
  if (!payload) notFound()
20
20
 
21
21
  return (
22
- <div className="mx-auto max-w-3xl px-6 py-10 font-sans">
23
- <header className="mb-8">
24
- <div className="text-xs uppercase tracking-wider text-neutral-500">
25
- Shared {payload.kind}
26
- </div>
27
- {link.label ? (
28
- <h1 className="mt-1 text-2xl font-semibold">{link.label}</h1>
29
- ) : null}
30
- </header>
31
- {renderBody(payload)}
32
- <footer className="mt-10 border-t border-neutral-200 pt-4 text-xs text-neutral-500">
33
- Public share link. Secrets and credentials are omitted.
34
- </footer>
22
+ <main className="min-h-screen bg-[#080a0f] text-white">
23
+ <div className="mx-auto flex w-full max-w-5xl flex-col gap-8 px-5 py-8 sm:px-8 lg:px-10">
24
+ <header className="border-b border-white/10 pb-6">
25
+ <div className="text-[11px] font-700 uppercase tracking-[0.16em] text-emerald-300/80">
26
+ SwarmClaw shared {payload.kind}
27
+ </div>
28
+ <h1 className="mt-3 max-w-3xl font-display text-[28px] font-700 tracking-normal text-white">
29
+ {link.label || payloadTitle(payload)}
30
+ </h1>
31
+ <p className="mt-3 max-w-2xl text-[13px] leading-relaxed text-white/60">
32
+ Public, revocable share link. Runtime secrets, credentials, private files, and hidden control metadata are omitted.
33
+ </p>
34
+ </header>
35
+ {renderBody(payload)}
36
+ <footer className="border-t border-white/10 pt-4 text-[11px] text-white/45">
37
+ Generated by SwarmClaw. This page only contains the entity fields explicitly allowed by the share resolver.
38
+ </footer>
39
+ </div>
40
+ </main>
41
+ )
42
+ }
43
+
44
+ function payloadTitle(payload: SharedPayload): string {
45
+ if (payload.kind === 'mission') return payload.title
46
+ if (payload.kind === 'skill') return payload.name
47
+ return payload.name
48
+ }
49
+
50
+ function formatUsd(n: number): string {
51
+ return `$${n.toFixed(n < 0.01 ? 4 : 2)}`
52
+ }
53
+
54
+ function formatNumber(n: number): string {
55
+ return Math.round(n).toLocaleString()
56
+ }
57
+
58
+ function formatDuration(ms: number): string {
59
+ if (!ms) return '0s'
60
+ if (ms < 1000) return `${ms}ms`
61
+ const sec = Math.round(ms / 1000)
62
+ if (sec < 60) return `${sec}s`
63
+ const min = Math.round(sec / 60)
64
+ if (min < 60) return `${min}m`
65
+ if (min < 60 * 24) return `${Math.round((min / 60) * 10) / 10}h`
66
+ return `${Math.round((min / 1440) * 10) / 10}d`
67
+ }
68
+
69
+ function Stat({ label, value, hint }: { label: string; value: string; hint?: string }) {
70
+ return (
71
+ <div className="rounded-[12px] border border-white/10 bg-white/[0.035] px-4 py-3">
72
+ <div className="text-[10px] font-700 uppercase tracking-[0.12em] text-white/45">{label}</div>
73
+ <div className="mt-2 font-display text-[22px] font-700 tracking-normal text-white">{value}</div>
74
+ {hint ? <div className="mt-1 text-[11px] leading-relaxed text-white/45">{hint}</div> : null}
75
+ </div>
76
+ )
77
+ }
78
+
79
+ function ReportMarkdown({ content }: { content: string }) {
80
+ const lines = content.split('\n')
81
+
82
+ return (
83
+ <div className="text-[13px] leading-relaxed text-white/72">
84
+ {lines.map((line, index) => renderReportLine(line, index))}
35
85
  </div>
36
86
  )
37
87
  }
38
88
 
89
+ function parseNumberedListItem(line: string): { marker: string; text: string } | null {
90
+ const dotIndex = line.indexOf('. ')
91
+ if (dotIndex <= 0 || dotIndex > 3) return null
92
+ for (let i = 0; i < dotIndex; i += 1) {
93
+ const code = line.charCodeAt(i)
94
+ if (code < 48 || code > 57) return null
95
+ }
96
+ return { marker: line.slice(0, dotIndex + 1), text: line.slice(dotIndex + 2) }
97
+ }
98
+
99
+ function renderReportLine(line: string, index: number) {
100
+ const trimmed = line.trim()
101
+ if (!trimmed) return <div key={index} className="h-2" />
102
+ if (trimmed === '---') return <hr key={index} className="my-4 border-white/10" />
103
+ if (trimmed.startsWith('### ')) {
104
+ return <h5 key={index} className="mb-2 mt-4 text-[13px] font-800 text-white">{trimmed.slice(4)}</h5>
105
+ }
106
+ if (trimmed.startsWith('## ')) {
107
+ return <h4 key={index} className="mb-2 mt-5 text-[13px] font-800 uppercase tracking-[0.1em] text-white/70">{trimmed.slice(3)}</h4>
108
+ }
109
+ if (trimmed.startsWith('# ')) {
110
+ return <h3 key={index} className="mb-3 mt-1 font-display text-[18px] font-700 text-white">{trimmed.slice(2)}</h3>
111
+ }
112
+ if (trimmed.startsWith('- ') || trimmed.startsWith('* ')) {
113
+ return (
114
+ <div key={index} className="my-1 flex gap-2">
115
+ <span className="text-white/40">•</span>
116
+ <span>{trimmed.slice(2)}</span>
117
+ </div>
118
+ )
119
+ }
120
+ const numbered = parseNumberedListItem(trimmed)
121
+ if (numbered) {
122
+ return (
123
+ <div key={index} className="my-1 flex gap-2">
124
+ <span className="min-w-5 text-white/40">{numbered.marker}</span>
125
+ <span>{numbered.text}</span>
126
+ </div>
127
+ )
128
+ }
129
+ return <p key={index} className="my-2">{trimmed}</p>
130
+ }
131
+
39
132
  function renderBody(payload: SharedPayload) {
40
133
  if (payload.kind === 'mission') {
134
+ const statusTone =
135
+ payload.status === 'running'
136
+ ? 'border-emerald-400/25 bg-emerald-400/10 text-emerald-200'
137
+ : payload.status === 'failed' || payload.status === 'budget_exhausted'
138
+ ? 'border-rose-400/25 bg-rose-400/10 text-rose-200'
139
+ : 'border-white/10 bg-white/[0.04] text-white/70'
41
140
  return (
42
- <section>
43
- <h2 className="text-xl font-semibold">{payload.title}</h2>
44
- <p className="mt-3 whitespace-pre-wrap text-neutral-800">{payload.goal}</p>
141
+ <section className="flex flex-col gap-6">
142
+ <div className="flex flex-col gap-4 lg:flex-row lg:items-start lg:justify-between">
143
+ <div className="max-w-3xl">
144
+ <div className={`inline-flex rounded-full border px-2.5 py-1 text-[11px] font-800 uppercase tracking-[0.1em] ${statusTone}`}>
145
+ {payload.status.replaceAll('_', ' ')}
146
+ </div>
147
+ <h2 className="mt-4 font-display text-[24px] font-700 tracking-normal text-white">{payload.title}</h2>
148
+ <p className="mt-3 whitespace-pre-wrap text-[14px] leading-relaxed text-white/68">{payload.goal}</p>
149
+ </div>
150
+ <div className="grid min-w-[280px] grid-cols-2 gap-2">
151
+ <Stat label="Turns" value={formatNumber(payload.usage.turnsRun)} hint={payload.budget.maxTurns != null ? `cap ${formatNumber(payload.budget.maxTurns)}` : 'no cap'} />
152
+ <Stat label="Spend" value={formatUsd(payload.usage.usdSpent)} hint={payload.budget.maxUsd != null ? `cap ${formatUsd(payload.budget.maxUsd)}` : 'no cap'} />
153
+ <Stat label="Tokens" value={formatNumber(payload.usage.tokensUsed)} hint={payload.budget.maxTokens != null ? `cap ${formatNumber(payload.budget.maxTokens)}` : 'no cap'} />
154
+ <Stat label="Wallclock" value={formatDuration(payload.usage.wallclockMsElapsed)} hint={payload.budget.maxWallclockSec != null ? `cap ${formatDuration(payload.budget.maxWallclockSec * 1000)}` : 'no cap'} />
155
+ </div>
156
+ </div>
45
157
  {payload.successCriteria.length > 0 ? (
46
- <>
47
- <h3 className="mt-6 font-semibold">Success criteria</h3>
48
- <ul className="mt-2 list-disc pl-6 text-neutral-800">
158
+ <div className="rounded-[14px] border border-white/10 bg-white/[0.025] p-4">
159
+ <h3 className="text-[12px] font-800 uppercase tracking-[0.12em] text-white/55">Success criteria</h3>
160
+ <ul className="mt-3 grid gap-2 text-[13px] text-white/70 md:grid-cols-2">
49
161
  {payload.successCriteria.map((c, i) => (
50
- <li key={i}>{c}</li>
162
+ <li key={i} className="rounded-[10px] border border-white/10 bg-white/[0.025] px-3 py-2">{c}</li>
51
163
  ))}
52
164
  </ul>
53
- </>
165
+ </div>
166
+ ) : null}
167
+ {payload.latestReport ? (
168
+ <article className="rounded-[14px] border border-emerald-400/20 bg-emerald-400/[0.04] p-4">
169
+ <div className="text-[11px] font-800 uppercase tracking-[0.12em] text-emerald-200/80">Latest report</div>
170
+ <div className="mt-1 text-[15px] font-800 text-white">{payload.latestReport.title}</div>
171
+ <div className="mt-1 text-[11px] text-white/45">{formatTime(payload.latestReport.at)} - {payload.latestReport.format}</div>
172
+ <div className="mt-4 rounded-[12px] border border-white/10 bg-black/20 px-4 py-3">
173
+ <ReportMarkdown content={payload.latestReport.content} />
174
+ </div>
175
+ </article>
54
176
  ) : null}
55
177
  {payload.milestones.length > 0 ? (
56
- <>
57
- <h3 className="mt-6 font-semibold">Milestones</h3>
58
- <ol className="mt-2 space-y-1 text-sm text-neutral-800">
178
+ <div className="rounded-[14px] border border-white/10 bg-white/[0.025] p-4">
179
+ <h3 className="text-[12px] font-800 uppercase tracking-[0.12em] text-white/55">Milestones</h3>
180
+ <ol className="mt-3 space-y-2">
59
181
  {payload.milestones.map((m, i) => (
60
- <li key={i}>
61
- <span className="text-neutral-500">{formatTime(m.at)}:</span> {m.note}
182
+ <li key={i} className="flex gap-3 rounded-[10px] border border-white/10 bg-white/[0.02] px-3 py-2 text-[12px]">
183
+ <span className="shrink-0 text-white/40">{formatTime(m.at)}</span>
184
+ <span className="shrink-0 font-800 text-white/55">{m.kind}</span>
185
+ <span className="text-white/75">{m.summary}</span>
62
186
  </li>
63
187
  ))}
64
188
  </ol>
65
- </>
189
+ </div>
66
190
  ) : null}
67
- {payload.reports.length > 0 ? (
68
- <>
69
- <h3 className="mt-6 font-semibold">Reports</h3>
70
- <div className="mt-3 space-y-4">
71
- {payload.reports.map((r, i) => (
72
- <article key={i} className="rounded border border-neutral-200 p-4">
73
- <div className="mb-2 text-xs text-neutral-500">
74
- {formatTime(r.at)} · {r.format}
191
+ {payload.reports.length > 1 ? (
192
+ <div className="rounded-[14px] border border-white/10 bg-white/[0.025] p-4">
193
+ <h3 className="text-[12px] font-800 uppercase tracking-[0.12em] text-white/55">Earlier reports</h3>
194
+ <div className="mt-3 grid gap-3">
195
+ {payload.reports.slice(1).map((r) => (
196
+ <article key={r.id} className="rounded-[12px] border border-white/10 bg-white/[0.02] p-4">
197
+ <div className="text-[13px] font-800 text-white">{r.title}</div>
198
+ <div className="mt-1 text-[11px] text-white/45">{formatTime(r.at)} - {r.format}</div>
199
+ <div className="mt-3">
200
+ <ReportMarkdown content={r.content} />
75
201
  </div>
76
- <pre className="whitespace-pre-wrap text-sm text-neutral-800">{r.content}</pre>
77
202
  </article>
78
203
  ))}
79
204
  </div>
80
- </>
205
+ </div>
81
206
  ) : null}
82
207
  </section>
83
208
  )
@@ -85,19 +210,19 @@ function renderBody(payload: SharedPayload) {
85
210
 
86
211
  if (payload.kind === 'skill') {
87
212
  return (
88
- <section>
89
- <h2 className="text-xl font-semibold">{payload.name}</h2>
213
+ <section className="rounded-[14px] border border-white/10 bg-white/[0.025] p-5">
214
+ <h2 className="font-display text-[22px] font-700 text-white">{payload.name}</h2>
90
215
  {payload.tags.length > 0 ? (
91
216
  <div className="mt-2 flex flex-wrap gap-2">
92
217
  {payload.tags.map((t) => (
93
- <span key={t} className="rounded bg-neutral-100 px-2 py-0.5 text-xs text-neutral-700">
218
+ <span key={t} className="rounded bg-white/10 px-2 py-0.5 text-xs text-white/70">
94
219
  {t}
95
220
  </span>
96
221
  ))}
97
222
  </div>
98
223
  ) : null}
99
- <p className="mt-4 text-neutral-800">{payload.description}</p>
100
- <pre className="mt-6 whitespace-pre-wrap rounded border border-neutral-200 bg-neutral-50 p-4 text-sm text-neutral-800">
224
+ <p className="mt-4 text-[13px] leading-relaxed text-white/65">{payload.description}</p>
225
+ <pre className="mt-6 whitespace-pre-wrap rounded-[12px] border border-white/10 bg-black/20 p-4 text-sm text-white/70">
101
226
  {payload.content}
102
227
  </pre>
103
228
  </section>
@@ -105,22 +230,22 @@ function renderBody(payload: SharedPayload) {
105
230
  }
106
231
 
107
232
  return (
108
- <section>
109
- <h2 className="text-xl font-semibold">{payload.name}</h2>
233
+ <section className="rounded-[14px] border border-white/10 bg-white/[0.025] p-5">
234
+ <h2 className="font-display text-[22px] font-700 text-white">{payload.name}</h2>
110
235
  {payload.agentName ? (
111
- <div className="mt-1 text-sm text-neutral-500">Agent: {payload.agentName}</div>
236
+ <div className="mt-1 text-sm text-white/50">Agent: {payload.agentName}</div>
112
237
  ) : null}
113
238
  <div className="mt-6 space-y-4">
114
239
  {payload.messages.map((m, i) => (
115
240
  <article
116
241
  key={i}
117
- className="rounded border border-neutral-200 p-4"
242
+ className="rounded-[12px] border border-white/10 bg-white/[0.02] p-4"
118
243
  >
119
- <div className="mb-1 flex items-center justify-between text-xs text-neutral-500">
244
+ <div className="mb-1 flex items-center justify-between text-xs text-white/45">
120
245
  <span className="uppercase">{m.role}</span>
121
246
  {m.at ? <span>{formatTime(m.at)}</span> : null}
122
247
  </div>
123
- <pre className="whitespace-pre-wrap text-sm text-neutral-800">{m.text}</pre>
248
+ <pre className="whitespace-pre-wrap text-sm text-white/70">{m.text}</pre>
124
249
  </article>
125
250
  ))}
126
251
  </div>
package/src/cli/index.js CHANGED
@@ -67,6 +67,13 @@ const COMMAND_GROUPS = [
67
67
  cmd('resolve', 'POST', '/approvals', 'Resolve a human-loop approval', { expectsJsonBody: true }),
68
68
  ],
69
69
  },
70
+ {
71
+ name: 'artifacts',
72
+ description: 'Resolve evidence artifacts for runs, missions, and tasks',
73
+ commands: [
74
+ cmd('list', 'GET', '/artifacts', 'List evidence artifacts (use --query runId=, --query missionId=, or --query taskId=)'),
75
+ ],
76
+ },
70
77
  {
71
78
  name: 'claude-skills',
72
79
  description: 'Read local Claude skills directory metadata',
@@ -198,6 +205,13 @@ const COMMAND_GROUPS = [
198
205
  }),
199
206
  ],
200
207
  },
208
+ {
209
+ name: 'operations',
210
+ description: 'Operator triage and readiness summaries',
211
+ commands: [
212
+ cmd('pulse', 'GET', '/operations/pulse', 'Get Operations Pulse summary (use --query range=24h or --query range=7d)'),
213
+ ],
214
+ },
201
215
  {
202
216
  name: 'documents',
203
217
  description: 'Manage documents',
@@ -544,6 +558,7 @@ const COMMAND_GROUPS = [
544
558
  cmd('list', 'GET', '/runs', 'List runs (use --query sessionId=, --query status=, --query limit=)'),
545
559
  cmd('get', 'GET', '/runs/:id', 'Get run by id'),
546
560
  cmd('events', 'GET', '/runs/:id/events', 'Get run event history by run id'),
561
+ cmd('brief', 'GET', '/runs/:id/brief', 'Get deterministic run brief by run id'),
547
562
  ],
548
563
  },
549
564
  {
package/src/cli/spec.js CHANGED
@@ -184,6 +184,12 @@ const COMMAND_GROUPS = {
184
184
  'task-status': { description: 'Check A2A task status', method: 'GET', path: '/a2a/tasks/:taskId/status', params: ['taskId'] },
185
185
  },
186
186
  },
187
+ artifacts: {
188
+ description: 'Resolve evidence artifacts for runs, missions, and tasks',
189
+ commands: {
190
+ list: { description: 'List evidence artifacts (supports --query runId=,missionId=,taskId=)', method: 'GET', path: '/artifacts' },
191
+ },
192
+ },
187
193
  uploads: {
188
194
  description: 'Manage uploaded artifacts',
189
195
  commands: {
@@ -193,6 +199,12 @@ const COMMAND_GROUPS = {
193
199
  'delete-many': { description: 'Delete uploads by filter/body (filenames, olderThanDays, category, or all)', method: 'DELETE', path: '/uploads' },
194
200
  },
195
201
  },
202
+ operations: {
203
+ description: 'Operator triage and readiness summaries',
204
+ commands: {
205
+ pulse: { description: 'Get Operations Pulse summary (supports --query range=24h|7d)', method: 'GET', path: '/operations/pulse' },
206
+ },
207
+ },
196
208
  files: {
197
209
  description: 'Serve/open local files',
198
210
  commands: {
@@ -526,6 +538,7 @@ const COMMAND_GROUPS = {
526
538
  list: { description: 'List runs (supports --query sessionId=,status=,limit=)', method: 'GET', path: '/runs' },
527
539
  get: { description: 'Get run by id', method: 'GET', path: '/runs/:id', params: ['id'] },
528
540
  events: { description: 'Get run event history by run id', method: 'GET', path: '/runs/:id/events', params: ['id'] },
541
+ brief: { description: 'Get deterministic run brief by run id', method: 'GET', path: '/runs/:id/brief', params: ['id'] },
529
542
  },
530
543
  },
531
544
  webhooks: {