@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.
- package/README.md +20 -0
- package/next.config.ts +38 -8
- package/package.json +2 -2
- package/scripts/run-next-build.mjs +51 -3
- package/src/app/api/artifacts/route.ts +15 -0
- package/src/app/api/clawhub/install/route.ts +4 -4
- package/src/app/api/dirs/route.ts +8 -5
- package/src/app/api/files/open/route.ts +3 -3
- package/src/app/api/files/serve/route.ts +2 -2
- package/src/app/api/operations/pulse/route.ts +9 -0
- package/src/app/api/runs/[id]/brief/route.ts +12 -0
- package/src/app/api/runs/[id]/events/route.ts +4 -13
- package/src/app/api/runs/[id]/route.ts +2 -6
- package/src/app/api/runs/route.ts +3 -43
- package/src/app/api/s/[token]/raw/route.ts +1 -1
- package/src/app/home/page.tsx +11 -1
- package/src/app/missions/page.tsx +182 -3
- package/src/app/s/[token]/page.tsx +173 -48
- package/src/cli/index.js +15 -0
- package/src/cli/spec.js +13 -0
- package/src/components/connectors/connector-list.tsx +36 -20
- package/src/components/evidence/evidence-shelf.tsx +97 -0
- package/src/components/home/home-launchpad.tsx +52 -2
- package/src/components/missions/mission-template-install-dialog.tsx +33 -1
- package/src/components/operations/operations-pulse-panel.tsx +184 -0
- package/src/components/quality/quality-workspace.tsx +34 -6
- package/src/components/runs/run-list.tsx +94 -12
- package/src/lib/connectors/connector-readiness.ts +127 -0
- package/src/lib/server/artifacts/artifact-resolver.test.ts +98 -0
- package/src/lib/server/artifacts/artifact-resolver.ts +241 -0
- package/src/lib/server/operations/operation-pulse.test.ts +108 -0
- package/src/lib/server/operations/operation-pulse.ts +197 -0
- package/src/lib/server/resolve-workspace-path.ts +10 -10
- package/src/lib/server/runs/run-brief.test.ts +92 -0
- package/src/lib/server/runs/run-brief.ts +107 -0
- package/src/lib/server/runs/unified-run-queries.ts +84 -0
- package/src/lib/server/sharing/share-resolver.test.ts +129 -0
- package/src/lib/server/sharing/share-resolver.ts +48 -3
- package/src/types/artifact.ts +28 -0
- package/src/types/index.ts +3 -0
- package/src/types/operations.ts +39 -0
- 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={() =>
|
|
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
|
-
+
|
|
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
|
-
<
|
|
23
|
-
<
|
|
24
|
-
<
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
<h1 className="mt-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
-
<
|
|
44
|
-
|
|
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="
|
|
48
|
-
<ul className="mt-
|
|
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="
|
|
58
|
-
<ol className="mt-
|
|
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-
|
|
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 >
|
|
68
|
-
|
|
69
|
-
<h3 className="
|
|
70
|
-
<div className="mt-3
|
|
71
|
-
{payload.reports.map((r
|
|
72
|
-
<article key={
|
|
73
|
-
<div className="
|
|
74
|
-
|
|
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-
|
|
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-
|
|
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-
|
|
100
|
-
<pre className="mt-6 whitespace-pre-wrap rounded border border-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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: {
|