@swarmclawai/swarmclaw 1.8.0 → 1.8.11

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 (47) hide show
  1. package/README.md +18 -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/home/page.tsx +3 -0
  16. package/src/app/missions/page.tsx +37 -4
  17. package/src/cli/index.js +15 -0
  18. package/src/cli/spec.js +13 -0
  19. package/src/components/connectors/connector-list.tsx +36 -20
  20. package/src/components/evidence/evidence-shelf.tsx +97 -0
  21. package/src/components/home/home-launchpad.tsx +3 -0
  22. package/src/components/operations/operations-pulse-panel.tsx +184 -0
  23. package/src/components/quality/quality-workspace.tsx +3 -0
  24. package/src/components/runs/run-list.tsx +94 -12
  25. package/src/lib/connectors/connector-readiness.ts +127 -0
  26. package/src/lib/providers/deepseek-reasoning-chat-openai.ts +305 -0
  27. package/src/lib/providers/openai.test.ts +73 -1
  28. package/src/lib/providers/openai.ts +19 -2
  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/build-llm.test.ts +13 -0
  32. package/src/lib/server/build-llm.ts +7 -0
  33. package/src/lib/server/chat-execution/chat-turn-finalization.ts +2 -0
  34. package/src/lib/server/chat-execution/chat-turn-partial-persistence.ts +12 -0
  35. package/src/lib/server/chat-execution/iteration-event-handler.ts +11 -1
  36. package/src/lib/server/chat-execution/stream-agent-chat.ts +11 -2
  37. package/src/lib/server/operations/operation-pulse.test.ts +108 -0
  38. package/src/lib/server/operations/operation-pulse.ts +197 -0
  39. package/src/lib/server/resolve-workspace-path.ts +10 -10
  40. package/src/lib/server/runs/run-brief.test.ts +92 -0
  41. package/src/lib/server/runs/run-brief.ts +107 -0
  42. package/src/lib/server/runs/unified-run-queries.ts +84 -0
  43. package/src/types/artifact.ts +28 -0
  44. package/src/types/index.ts +3 -0
  45. package/src/types/message.ts +2 -0
  46. package/src/types/operations.ts +39 -0
  47. package/src/types/run-brief.ts +41 -0
package/README.md CHANGED
@@ -399,6 +399,24 @@ Operational docs: https://swarmclaw.ai/docs/observability
399
399
 
400
400
  ## Releases
401
401
 
402
+ ### v1.8.11 Highlights
403
+
404
+ DeepSeek tool-use hotfix for issue [#67](https://github.com/swarmclawai/swarmclaw/issues/67).
405
+
406
+ - **DeepSeek reasoning replay.** Stored assistant turns now keep provider-native `reasoning_content` separately from visible text and send it back to DeepSeek on follow-up tool-use turns.
407
+ - **Streaming parity.** Direct OpenAI-compatible streams and LangGraph agent streams both preserve `reasoning_content` while continuing to show reasoning through SwarmClaw's existing thinking surface.
408
+ - **Regression coverage.** Added tests for DeepSeek history replay and the LangChain bridge selection path.
409
+
410
+ ### v1.8.1 Highlights
411
+
412
+ Operator evidence release: a focused follow-up that makes release and mission review easier to scan.
413
+
414
+ - **Operations Pulse.** Home and Quality now share a live triage panel that rolls missions, runs, approvals, connector readiness, and budget pressure into one next-action queue.
415
+ - **Run Briefs.** Run detail sheets now open with a deterministic brief: objective, owner, timeline, warnings, usage, and evidence before the raw replay log.
416
+ - **Evidence Shelf.** Runs and missions now expose linked artifacts, task outputs, protocol outputs, mission reports, public share links, and knowledge citations through a shared artifact resolver.
417
+ - **Connector readiness.** Connector cards now show credential, route, pairing, gateway, connection, and doctor hints so setup gaps are visible before a platform bridge is started.
418
+ - **API surface.** Added `GET /api/operations/pulse`, `GET /api/runs/:id/brief`, and `GET /api/artifacts` for external operator dashboards and release tooling.
419
+
402
420
  ### v1.8.0 Highlights
403
421
 
404
422
  Mission Command release: a bigger operator update that makes autonomous missions easier to launch, inspect, and share.
package/next.config.ts CHANGED
@@ -6,9 +6,39 @@ import path from "path";
6
6
  import { fileURLToPath } from "url";
7
7
 
8
8
  const PROJECT_ROOT = path.dirname(fileURLToPath(import.meta.url))
9
- const RUNTIME_STATE_GLOBS = [
10
- 'data/**/*',
11
- '.tmp-swarmclaw-build/**/*',
9
+ const OUTPUT_TRACE_EXCLUDE_GLOBS = [
10
+ './.env*',
11
+ './.git/**/*',
12
+ './.next/cache/**/*',
13
+ './.tmp-swarmclaw-build/**/*',
14
+ './AGENTS.md',
15
+ './CLAUDE.md',
16
+ './CONTRIBUTING.md',
17
+ './Dockerfile',
18
+ './Dockerfile.*',
19
+ './README.md',
20
+ './SWARMDOCK.md',
21
+ './artifacts/**/*',
22
+ './components.json',
23
+ './coverage/**/*',
24
+ './data/**/*',
25
+ './daemon.log',
26
+ './docker-compose.yml',
27
+ './electron-builder.yml',
28
+ './electron-dist/**/*',
29
+ './eslint.config.mjs',
30
+ './fly.toml',
31
+ './install.sh',
32
+ './next.config.ts',
33
+ './package-lock.json',
34
+ './postcss.config.mjs',
35
+ './railway.json',
36
+ './release/**/*',
37
+ './render.yaml',
38
+ './research.md',
39
+ './swarmclaw-skill.md',
40
+ './test-results/**/*',
41
+ './tsconfig.json',
12
42
  ]
13
43
 
14
44
  function getGitSha(): string {
@@ -50,11 +80,11 @@ function getAllowedDevOrigins(): string[] {
50
80
  const nextConfig: NextConfig = {
51
81
  output: 'standalone',
52
82
  outputFileTracingExcludes: {
53
- '/*': RUNTIME_STATE_GLOBS,
54
- '/api/**': RUNTIME_STATE_GLOBS,
55
- instrumentation: RUNTIME_STATE_GLOBS,
56
- '/instrumentation': RUNTIME_STATE_GLOBS,
57
- 'next-server': RUNTIME_STATE_GLOBS,
83
+ '/*': OUTPUT_TRACE_EXCLUDE_GLOBS,
84
+ '/api/**': OUTPUT_TRACE_EXCLUDE_GLOBS,
85
+ instrumentation: OUTPUT_TRACE_EXCLUDE_GLOBS,
86
+ '/instrumentation': OUTPUT_TRACE_EXCLUDE_GLOBS,
87
+ 'next-server': OUTPUT_TRACE_EXCLUDE_GLOBS,
58
88
  },
59
89
  turbopack: {
60
90
  // Pin workspace root to the project directory so a stale lockfile
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@swarmclawai/swarmclaw",
3
- "version": "1.8.0",
3
+ "version": "1.8.11",
4
4
  "description": "Build and run autonomous AI agents with OpenClaw, Hermes, multiple model providers, orchestration, delegation, memory, skills, schedules, and chat connectors.",
5
5
  "main": "electron-dist/main.js",
6
6
  "license": "MIT",
@@ -87,7 +87,7 @@
87
87
  "test:cli": "node --test src/cli/*.test.js bin/*.test.js scripts/electron-after-pack.test.mjs scripts/postinstall.test.mjs scripts/run-next-build.test.mjs scripts/run-next-typegen.test.mjs",
88
88
  "test:setup": "tsx --test src/app/api/setup/check-provider/route.test.ts src/lib/server/provider-model-discovery.test.ts src/components/auth/setup-wizard/utils.test.ts src/components/auth/setup-wizard/types.test.ts src/hooks/setup-done-detection.test.ts src/lib/setup-defaults.test.ts src/lib/server/storage-auth.test.ts src/lib/server/storage-auth-docker.test.ts",
89
89
  "test:openclaw": "tsx --test src/lib/openclaw/openclaw-agent-id.test.ts src/lib/openclaw/openclaw-endpoint.test.ts src/lib/server/agents/agent-runtime-config.test.ts src/lib/server/build-llm.test.ts src/lib/server/connectors/connector-routing.test.ts src/lib/server/connectors/openclaw.test.ts src/lib/server/connectors/swarmdock.test.ts src/lib/server/gateway/protocol.test.ts src/lib/server/llm-response-cache.test.ts src/lib/server/mcp-conformance.test.ts src/lib/server/openclaw/agent-resolver.test.ts src/lib/server/openclaw/deploy.test.ts src/lib/server/openclaw/skills-normalize.test.ts src/lib/server/session-tools/openclaw-nodes.test.ts src/lib/server/session-tools/swarmdock.test.ts src/lib/server/tasks/task-quality-gate.test.ts src/lib/server/tasks/task-validation.test.ts src/lib/server/tool-capability-policy.test.ts src/lib/providers/openai.test.ts src/lib/providers/openclaw-exports.test.ts src/app/api/openclaw/dashboard-url/route.test.ts",
90
- "test:runtime": "tsx --test src/lib/a2a/agent-card.test.ts src/lib/strip-internal-metadata.test.ts src/lib/provider-sets.test.ts src/lib/providers/opencode-cli.test.ts src/lib/providers/cli-provider-metadata.test.ts src/lib/providers/cli-utils.test.ts src/lib/providers/generic-cli.test.ts src/lib/server/cli-provider-readiness.test.ts src/lib/server/provider-health.test.ts src/lib/server/mcp-gateway-runtime.test.ts src/lib/server/mcp-connection-pool.test.ts src/lib/server/knowledge-sources.test.ts src/lib/server/chat-execution/chat-execution-grounding.test.ts src/lib/server/chat-execution/chat-turn-preparation.test.ts src/lib/server/chat-execution/iteration-timers.test.ts src/lib/server/chat-execution/post-stream-finalization.test.ts src/lib/server/chats/clear-undo-snapshots.test.ts src/lib/server/connectors/email.test.ts src/lib/server/protocols/protocol-service.test.ts src/lib/server/runtime/run-ledger.test.ts src/lib/server/observability/otel-config.test.ts src/lib/server/safe-parse-body.test.ts src/lib/server/missions/mission-templates.test.ts src/lib/server/sharing/share-link-repository.test.ts src/lib/server/sharing/share-resolver.test.ts src/lib/app/view-constants.test.ts src/lib/quality/quality-summary.test.ts src/app/api/approvals/route.test.ts src/app/api/agents/agents-route.test.ts src/app/api/tasks/tasks-route.test.ts src/app/api/chats/chat-route.test.ts src/app/api/chats/clear-route.test.ts src/app/api/chats/compact-route.test.ts src/app/api/chats/context-status-route.test.ts src/app/api/connectors/connector-doctor-route.test.ts src/app/api/healthz/route.test.ts src/app/api/logs/route.test.ts src/app/api/providers/[id]/route.test.ts src/app/api/tts/route.test.ts",
90
+ "test:runtime": "tsx --test src/lib/a2a/agent-card.test.ts src/lib/strip-internal-metadata.test.ts src/lib/provider-sets.test.ts src/lib/providers/opencode-cli.test.ts src/lib/providers/cli-provider-metadata.test.ts src/lib/providers/cli-utils.test.ts src/lib/providers/generic-cli.test.ts src/lib/server/cli-provider-readiness.test.ts src/lib/server/provider-health.test.ts src/lib/server/mcp-gateway-runtime.test.ts src/lib/server/mcp-connection-pool.test.ts src/lib/server/knowledge-sources.test.ts src/lib/server/chat-execution/chat-execution-grounding.test.ts src/lib/server/chat-execution/chat-turn-preparation.test.ts src/lib/server/chat-execution/iteration-timers.test.ts src/lib/server/chat-execution/post-stream-finalization.test.ts src/lib/server/chats/clear-undo-snapshots.test.ts src/lib/server/connectors/email.test.ts src/lib/server/protocols/protocol-service.test.ts src/lib/server/runtime/run-ledger.test.ts src/lib/server/runs/run-brief.test.ts src/lib/server/operations/operation-pulse.test.ts src/lib/server/artifacts/artifact-resolver.test.ts src/lib/server/observability/otel-config.test.ts src/lib/server/safe-parse-body.test.ts src/lib/server/missions/mission-templates.test.ts src/lib/server/sharing/share-link-repository.test.ts src/lib/server/sharing/share-resolver.test.ts src/lib/app/view-constants.test.ts src/lib/quality/quality-summary.test.ts src/app/api/approvals/route.test.ts src/app/api/agents/agents-route.test.ts src/app/api/tasks/tasks-route.test.ts src/app/api/chats/chat-route.test.ts src/app/api/chats/clear-route.test.ts src/app/api/chats/compact-route.test.ts src/app/api/chats/context-status-route.test.ts src/app/api/connectors/connector-doctor-route.test.ts src/app/api/healthz/route.test.ts src/app/api/logs/route.test.ts src/app/api/providers/[id]/route.test.ts src/app/api/tts/route.test.ts",
91
91
  "test:builder": "tsx --test src/features/protocols/builder/utils/nodes-to-template.test.ts src/features/protocols/builder/utils/template-to-nodes.test.ts src/features/protocols/builder/validators/dag-validator.test.ts",
92
92
  "test:e2e": "node --import tsx scripts/browser-e2e-smoke.ts",
93
93
  "test:mcp:conformance": "node --import tsx ./scripts/mcp-conformance-check.ts",
@@ -11,13 +11,15 @@ import { ensureBuildBootstrapPaths } from './build-bootstrap-env.mjs'
11
11
 
12
12
  const require = createRequire(import.meta.url)
13
13
 
14
- export const DEFAULT_MAX_OLD_SPACE_SIZE_MB = '12288'
14
+ export const DEFAULT_MAX_OLD_SPACE_SIZE_MB = '24576'
15
15
  export const MIN_MAX_OLD_SPACE_SIZE_MB = 1024
16
16
  export const FALLBACK_MIN_MAX_OLD_SPACE_SIZE_MB = 512
17
17
  export const RESERVED_BUILD_MEMORY_MB = 768
18
- export const MAX_OLD_SPACE_RATIO = 0.75
18
+ export const MAX_OLD_SPACE_RATIO = 0.85
19
19
  export const LOW_MEMORY_RATIO = 0.6
20
20
  export const BUILD_MAX_OLD_SPACE_SIZE_ENV = 'SWARMCLAW_BUILD_MAX_OLD_SPACE_SIZE_MB'
21
+ export const BUILD_BUNDLER_ENV = 'SWARMCLAW_BUILD_BUNDLER'
22
+ export const DEFAULT_BUILD_BUNDLER = 'turbopack'
21
23
  export const CGROUP_MEMORY_LIMIT_PATHS = [
22
24
  '/sys/fs/cgroup/memory.max',
23
25
  '/sys/fs/cgroup/memory/memory.limit_in_bytes',
@@ -40,6 +42,15 @@ export const REQUIRED_STANDALONE_BROWSER_PACKAGES = [
40
42
  'playwright',
41
43
  'playwright-core',
42
44
  ]
45
+ export const STANDALONE_LOCAL_STATE_ENTRIES = [
46
+ '.git',
47
+ '.tmp-swarmclaw-build',
48
+ 'artifacts',
49
+ 'coverage',
50
+ 'data',
51
+ 'release',
52
+ 'test-results',
53
+ ]
43
54
 
44
55
  function parsePositiveInteger(value) {
45
56
  const parsed = Number.parseInt(String(value ?? '').trim(), 10)
@@ -133,6 +144,18 @@ export function buildNextBuildEnv(
133
144
  }
134
145
  }
135
146
 
147
+ export function resolveNextBuildBundlerFlag(args = [], env = process.env) {
148
+ if (args.includes('--webpack') || args.includes('--turbopack')) return null
149
+
150
+ const requested = String(env[BUILD_BUNDLER_ENV] || DEFAULT_BUILD_BUNDLER).trim().toLowerCase()
151
+ if (requested === 'webpack') return '--webpack'
152
+ if (requested === 'turbopack') return '--turbopack'
153
+
154
+ throw new Error(
155
+ `${BUILD_BUNDLER_ENV} must be "turbopack" or "webpack"; received ${JSON.stringify(requested)}.`,
156
+ )
157
+ }
158
+
136
159
  export function hasTraceCopyWarning(output = '') {
137
160
  return output.includes(TRACE_COPY_WARNING)
138
161
  }
@@ -239,6 +262,27 @@ export function repairStandaloneNextMetadata(cwd = process.cwd()) {
239
262
  return true
240
263
  }
241
264
 
265
+ export function pruneStandaloneLocalState(cwd = process.cwd()) {
266
+ const standaloneDir = path.join(cwd, '.next', 'standalone')
267
+ if (!fs.existsSync(standaloneDir)) return false
268
+
269
+ let pruned = false
270
+ for (const entry of STANDALONE_LOCAL_STATE_ENTRIES) {
271
+ const target = path.join(standaloneDir, entry)
272
+ if (!fs.existsSync(target)) continue
273
+ fs.rmSync(target, { recursive: true, force: true })
274
+ pruned = true
275
+ }
276
+
277
+ for (const entry of fs.readdirSync(standaloneDir, { withFileTypes: true })) {
278
+ if (entry.name !== '.env' && !entry.name.startsWith('.env.')) continue
279
+ fs.rmSync(path.join(standaloneDir, entry.name), { recursive: entry.isDirectory(), force: true })
280
+ pruned = true
281
+ }
282
+
283
+ return pruned
284
+ }
285
+
242
286
  export function runNextBuild(
243
287
  args = process.argv.slice(2),
244
288
  env = process.env,
@@ -246,7 +290,8 @@ export function runNextBuild(
246
290
  maxOldSpaceSizeMb = resolveNextBuildMaxOldSpaceSizeMb(env),
247
291
  ) {
248
292
  const nextBin = require.resolve('next/dist/bin/next')
249
- return spawnSync(process.execPath, [nextBin, 'build', '--webpack', ...args], {
293
+ const bundlerFlag = resolveNextBuildBundlerFlag(args, env)
294
+ return spawnSync(process.execPath, [nextBin, 'build', ...(bundlerFlag ? [bundlerFlag] : []), ...args], {
250
295
  stdio: 'pipe',
251
296
  encoding: 'utf-8',
252
297
  env: buildNextBuildEnv(env, maxOldSpaceSizeMb, cwd),
@@ -277,6 +322,9 @@ function main() {
277
322
  if (result.status === 0 && repairStandaloneBrowserMcpRuntime(process.cwd())) {
278
323
  console.error('Copied Playwright MCP runtime packages into standalone build output.')
279
324
  }
325
+ if (result.status === 0 && pruneStandaloneLocalState(process.cwd())) {
326
+ console.error('Pruned local state and release artifacts from standalone build output.')
327
+ }
280
328
  process.exit(result.status)
281
329
  }
282
330
  if (result.signal) {
@@ -0,0 +1,15 @@
1
+ import { NextResponse } from 'next/server'
2
+ import { listEvidenceArtifacts } from '@/lib/server/artifacts/artifact-resolver'
3
+
4
+ export const dynamic = 'force-dynamic'
5
+
6
+ export async function GET(req: Request) {
7
+ const { searchParams } = new URL(req.url)
8
+ const runId = searchParams.get('runId')
9
+ const missionId = searchParams.get('missionId')
10
+ const taskId = searchParams.get('taskId')
11
+ if (!runId && !missionId && !taskId) {
12
+ return NextResponse.json({ error: 'runId, missionId, or taskId is required' }, { status: 400 })
13
+ }
14
+ return NextResponse.json(listEvidenceArtifacts({ runId, missionId, taskId }))
15
+ }
@@ -48,7 +48,7 @@ async function writeClawHubBundleToWorkspace(bundle: ClawHubSkillBundle): Promis
48
48
  .filter((entry): entry is { file: ClawHubSkillBundle['files'][number], path: string } => Boolean(entry.path))
49
49
 
50
50
  const workspaceSkillsDir = resolveWorkspaceSkillsDir()
51
- const targetDir = path.join(workspaceSkillsDir, sanitizeSkillDirName(bundle.slug))
51
+ const targetDir = path.join(/*turbopackIgnore: true*/ workspaceSkillsDir, sanitizeSkillDirName(bundle.slug))
52
52
  const normalizedPaths = stripSharedTopLevelDir(normalizedEntries.map((entry) => entry.path))
53
53
 
54
54
  await fs.rm(targetDir, { recursive: true, force: true })
@@ -57,12 +57,12 @@ async function writeClawHubBundleToWorkspace(bundle: ClawHubSkillBundle): Promis
57
57
  for (let index = 0; index < normalizedEntries.length; index += 1) {
58
58
  const relativePath = normalizedPaths[index]
59
59
  if (!relativePath) continue
60
- const destination = path.join(targetDir, relativePath)
60
+ const destination = path.join(/*turbopackIgnore: true*/ targetDir, relativePath)
61
61
  if (!destination.startsWith(targetDir + path.sep) && destination !== targetDir) {
62
62
  throw new Error(`Refusing to write bundle file outside the target directory: ${relativePath}`)
63
63
  }
64
- await fs.mkdir(path.dirname(destination), { recursive: true })
65
- await fs.writeFile(destination, normalizedEntries[index].file.content)
64
+ await fs.mkdir(/*turbopackIgnore: true*/ path.dirname(destination), { recursive: true })
65
+ await fs.writeFile(/*turbopackIgnore: true*/ destination, normalizedEntries[index].file.content)
66
66
  }
67
67
 
68
68
  clearDiscoveredSkillsCache()
@@ -9,18 +9,21 @@ export async function GET(req: NextRequest) {
9
9
 
10
10
  // Resolve ~ to home dir
11
11
  const resolved = targetDir.startsWith('~')
12
- ? path.join(os.homedir(), targetDir.slice(1))
13
- : path.resolve(targetDir)
12
+ ? path.join(/*turbopackIgnore: true*/ os.homedir(), targetDir.slice(1))
13
+ : path.resolve(/*turbopackIgnore: true*/ targetDir)
14
14
 
15
15
  let dirs: Array<{ name: string; path: string }> = []
16
16
  try {
17
- dirs = fs.readdirSync(resolved)
17
+ dirs = fs.readdirSync(/*turbopackIgnore: true*/ resolved)
18
18
  .filter(d => {
19
19
  if (d.startsWith('.')) return false
20
- try { return fs.statSync(path.join(resolved, d)).isDirectory() } catch { return false }
20
+ try {
21
+ const childPath = path.join(/*turbopackIgnore: true*/ resolved, d)
22
+ return fs.statSync(/*turbopackIgnore: true*/ childPath).isDirectory()
23
+ } catch { return false }
21
24
  })
22
25
  .sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase()))
23
- .map(d => ({ name: d, path: path.join(resolved, d) }))
26
+ .map(d => ({ name: d, path: path.join(/*turbopackIgnore: true*/ resolved, d) }))
24
27
  } catch {}
25
28
 
26
29
  const parentPath = resolved === '/' ? null : path.dirname(resolved)
@@ -19,7 +19,7 @@ export async function POST(req: Request) {
19
19
  return NextResponse.json({ error: 'Path does not exist' }, { status: 404 })
20
20
  }
21
21
 
22
- const isDir = fs.statSync(resolved).isDirectory()
22
+ const isDir = fs.statSync(/*turbopackIgnore: true*/ resolved).isDirectory()
23
23
  const platform = process.platform
24
24
 
25
25
  let command: string
@@ -32,11 +32,11 @@ export async function POST(req: Request) {
32
32
  args = isDir ? [resolved] : [`/select,${resolved}`]
33
33
  } else {
34
34
  command = 'xdg-open'
35
- args = [isDir ? resolved : path.dirname(resolved)]
35
+ args = [isDir ? resolved : path.dirname(/*turbopackIgnore: true*/ resolved)]
36
36
  }
37
37
 
38
38
  return new Promise<NextResponse>((resolve) => {
39
- const child = spawn(command, args, { stdio: 'ignore' })
39
+ const child = spawn(/*turbopackIgnore: true*/ command, args, { stdio: 'ignore' })
40
40
  child.once('error', (err) => {
41
41
  resolve(NextResponse.json({ error: err.message }, { status: 500 }))
42
42
  })
@@ -57,7 +57,7 @@ export async function GET(req: Request) {
57
57
  return NextResponse.json({ error: 'Access denied' }, { status: 403 })
58
58
  }
59
59
 
60
- const stat = fs.statSync(resolved)
60
+ const stat = fs.statSync(/*turbopackIgnore: true*/ resolved)
61
61
  if (!stat.isFile()) {
62
62
  return NextResponse.json({ error: 'Not a file' }, { status: 400 })
63
63
  }
@@ -67,7 +67,7 @@ export async function GET(req: Request) {
67
67
 
68
68
  const ext = path.extname(resolved).toLowerCase()
69
69
  const contentType = MIME_MAP[ext] || 'application/octet-stream'
70
- const content = fs.readFileSync(resolved)
70
+ const content = fs.readFileSync(/*turbopackIgnore: true*/ resolved)
71
71
 
72
72
  return new NextResponse(content, {
73
73
  headers: {
@@ -0,0 +1,9 @@
1
+ import { NextResponse } from 'next/server'
2
+ import { getOperationPulse, normalizeOperationPulseRange } from '@/lib/server/operations/operation-pulse'
3
+
4
+ export const dynamic = 'force-dynamic'
5
+
6
+ export async function GET(req: Request) {
7
+ const { searchParams } = new URL(req.url)
8
+ return NextResponse.json(getOperationPulse(normalizeOperationPulseRange(searchParams.get('range'))))
9
+ }
@@ -0,0 +1,12 @@
1
+ import { NextResponse } from 'next/server'
2
+ import { buildRunBrief } from '@/lib/server/runs/run-brief'
3
+ import { getUnifiedRunById, listUnifiedRunEvents } from '@/lib/server/runs/unified-run-queries'
4
+
5
+ export const dynamic = 'force-dynamic'
6
+
7
+ export async function GET(_req: Request, { params }: { params: Promise<{ id: string }> }) {
8
+ const { id } = await params
9
+ const run = getUnifiedRunById(id)
10
+ if (!run) return NextResponse.json({ error: 'Run not found' }, { status: 404 })
11
+ return NextResponse.json(buildRunBrief(run, listUnifiedRunEvents(id, 300)))
12
+ }
@@ -1,7 +1,5 @@
1
1
  import { NextResponse } from 'next/server'
2
- import { getRunById, listRunEvents } from '@/lib/server/runtime/session-run-manager'
3
- import { listProtocolRunEventsForRun, loadProtocolRunById } from '@/lib/server/protocols/protocol-queries'
4
- import { protocolEventToRunEventRecord } from '@/lib/server/runs/unified-run-records'
2
+ import { getUnifiedRunById, listUnifiedRunEvents } from '@/lib/server/runs/unified-run-queries'
5
3
 
6
4
  export const dynamic = 'force-dynamic'
7
5
 
@@ -13,16 +11,9 @@ function parseLimit(value: string | null): number | undefined {
13
11
 
14
12
  export async function GET(req: Request, { params }: { params: Promise<{ id: string }> }) {
15
13
  const { id } = await params
16
- const run = getRunById(id)
17
- if (run) {
18
- const url = new URL(req.url)
19
- const limit = parseLimit(url.searchParams.get('limit'))
20
- return NextResponse.json(listRunEvents(id, limit))
21
- }
22
- const protocolRun = loadProtocolRunById(id)
23
- if (!protocolRun) return NextResponse.json({ error: 'Run not found' }, { status: 404 })
14
+ const run = getUnifiedRunById(id)
15
+ if (!run) return NextResponse.json({ error: 'Run not found' }, { status: 404 })
24
16
  const url = new URL(req.url)
25
17
  const limit = parseLimit(url.searchParams.get('limit'))
26
- const events = listProtocolRunEventsForRun(id, limit || 200).map((event) => protocolEventToRunEventRecord(protocolRun, event))
27
- return NextResponse.json(events)
18
+ return NextResponse.json(listUnifiedRunEvents(id, limit || 200))
28
19
  }
@@ -1,13 +1,9 @@
1
1
  import { NextResponse } from 'next/server'
2
- import { getRunById } from '@/lib/server/runtime/session-run-manager'
3
- import { loadProtocolRunById } from '@/lib/server/protocols/protocol-queries'
4
- import { protocolRunToSessionRunRecord } from '@/lib/server/runs/unified-run-records'
2
+ import { getUnifiedRunById } from '@/lib/server/runs/unified-run-queries'
5
3
 
6
4
  export async function GET(_req: Request, { params }: { params: Promise<{ id: string }> }) {
7
5
  const { id } = await params
8
- const run = getRunById(id)
6
+ const run = getUnifiedRunById(id)
9
7
  if (run) return NextResponse.json(run)
10
- const protocolRun = loadProtocolRunById(id)
11
- if (protocolRun) return NextResponse.json(protocolRunToSessionRunRecord(protocolRun))
12
8
  return NextResponse.json({ error: 'Run not found' }, { status: 404 })
13
9
  }
@@ -1,28 +1,9 @@
1
1
  import { NextResponse } from 'next/server'
2
- import { listRuns } from '@/lib/server/runtime/session-run-manager'
3
- import { listProtocolRuns } from '@/lib/server/protocols/protocol-queries'
4
- import { protocolRunToSessionRunRecord } from '@/lib/server/runs/unified-run-records'
5
- import type { ProtocolRunStatus, SessionRunStatus } from '@/types'
2
+ import { listUnifiedRuns } from '@/lib/server/runs/unified-run-queries'
3
+ import type { SessionRunStatus } from '@/types'
6
4
 
7
5
  export const dynamic = 'force-dynamic'
8
6
 
9
- function protocolStatusesForRunStatus(status?: SessionRunStatus): ProtocolRunStatus[] {
10
- switch (status) {
11
- case 'queued':
12
- return ['draft']
13
- case 'running':
14
- return ['running', 'waiting', 'paused']
15
- case 'completed':
16
- return ['completed']
17
- case 'failed':
18
- return ['failed']
19
- case 'cancelled':
20
- return ['cancelled', 'archived']
21
- default:
22
- return []
23
- }
24
- }
25
-
26
7
  export async function GET(req: Request) {
27
8
  const { searchParams } = new URL(req.url)
28
9
  const sessionId = searchParams.get('sessionId') || undefined
@@ -30,26 +11,5 @@ export async function GET(req: Request) {
30
11
  const limitRaw = searchParams.get('limit')
31
12
  const limit = limitRaw ? Number.parseInt(limitRaw, 10) : undefined
32
13
 
33
- const sessionRuns = listRuns({ sessionId, status, limit })
34
- const fetchLimit = limit || 200
35
- const scopedProtocolRuns = status
36
- ? protocolStatusesForRunStatus(status).flatMap((protocolStatus) => listProtocolRuns({
37
- includeSystemOwned: true,
38
- sessionId,
39
- status: protocolStatus,
40
- limit: fetchLimit,
41
- }))
42
- : listProtocolRuns({
43
- includeSystemOwned: true,
44
- sessionId,
45
- limit: fetchLimit,
46
- })
47
- const protocolRuns = Array.from(new Map(scopedProtocolRuns.map((run) => [run.id, run])).values())
48
- .filter((run) => run.status !== 'archived')
49
- .map(protocolRunToSessionRunRecord)
50
- .filter((run) => !status || run.status === status)
51
- const runs = [...sessionRuns, ...protocolRuns]
52
- .sort((left, right) => (right.queuedAt || 0) - (left.queuedAt || 0))
53
- .slice(0, fetchLimit)
54
- return NextResponse.json(runs)
14
+ return NextResponse.json(listUnifiedRuns({ sessionId, status, limit }))
55
15
  }
@@ -7,6 +7,7 @@ import { useAppStore } from '@/stores/use-app-store'
7
7
  import { useChatStore } from '@/stores/use-chat-store'
8
8
  import { AgentAvatar } from '@/components/agents/agent-avatar'
9
9
  import { HomeLaunchpad } from '@/components/home/home-launchpad'
10
+ import { OperationsPulsePanel } from '@/components/operations/operations-pulse-panel'
10
11
  import { useMountedRef } from '@/hooks/use-mounted-ref'
11
12
  import { useNow } from '@/hooks/use-now'
12
13
  import { api } from '@/lib/app/api-client'
@@ -314,6 +315,8 @@ export default function HomePage() {
314
315
  </p>
315
316
  </div>
316
317
 
318
+ <OperationsPulsePanel className="mb-8" compact />
319
+
317
320
  {/* Quick actions / triage */}
318
321
  <section className="mb-8" style={{ animation: 'fade-up 0.6s var(--ease-spring) 0.15s both' }}>
319
322
  <SectionHeader
@@ -4,6 +4,7 @@ import { useCallback, useEffect, useMemo, useState } from 'react'
4
4
  import { useRouter, useSearchParams } from 'next/navigation'
5
5
  import { api } from '@/lib/app/api-client'
6
6
  import { MainContent } from '@/components/layout/main-content'
7
+ import { EvidenceShelf } from '@/components/evidence/evidence-shelf'
7
8
  import { HintTip } from '@/components/shared/hint-tip'
8
9
  import { inputClass } from '@/components/shared/form-styles'
9
10
  import { MissionTemplateGallery } from '@/components/missions/mission-template-gallery'
@@ -12,7 +13,7 @@ import {
12
13
  type InstantiateInput,
13
14
  } from '@/components/missions/mission-template-install-dialog'
14
15
  import { MissionEditSheet, isMissionEditable } from '@/components/missions/mission-edit-sheet'
15
- import type { Mission, MissionReport, MissionEvent, MissionTemplate, Session } from '@/types'
16
+ import type { EvidenceArtifact, Mission, MissionReport, MissionEvent, MissionTemplate, Session } from '@/types'
16
17
  import { toast } from 'sonner'
17
18
 
18
19
  const POLL_MS = 4_000
@@ -412,6 +413,8 @@ interface DetailProps {
412
413
  function MissionDetail({ mission, reports, events, busy, onAction, onForceReport, onEdit }: DetailProps) {
413
414
  const [selectedReport, setSelectedReport] = useState<MissionReport | null>(null)
414
415
  const [shareLinks, setShareLinks] = useState<ShareLink[]>([])
416
+ const [artifacts, setArtifacts] = useState<EvidenceArtifact[]>([])
417
+ const [artifactsLoading, setArtifactsLoading] = useState(false)
415
418
  const [shareBusy, setShareBusy] = useState<string | null>(null)
416
419
  const wallclockCapMs = mission.budget.maxWallclockSec != null ? mission.budget.maxWallclockSec * 1000 : null
417
420
  const activeShare = useMemo(
@@ -428,9 +431,22 @@ function MissionDetail({ mission, reports, events, busy, onAction, onForceReport
428
431
  }
429
432
  }, [mission.id])
430
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
+
431
446
  useEffect(() => {
432
447
  void loadShareLinks()
433
- }, [loadShareLinks])
448
+ void loadArtifacts()
449
+ }, [loadArtifacts, loadShareLinks])
434
450
 
435
451
  const shareUrl = activeShare && typeof window !== 'undefined'
436
452
  ? `${window.location.origin}/s/${activeShare.token}`
@@ -445,13 +461,14 @@ function MissionDetail({ mission, reports, events, busy, onAction, onForceReport
445
461
  label: `${mission.title} public report`,
446
462
  })
447
463
  setShareLinks((prev) => [link, ...prev])
464
+ void loadArtifacts()
448
465
  toast.success('Mission share link created')
449
466
  } catch (error) {
450
467
  toast.error(error instanceof Error ? error.message : 'Unable to create share link')
451
468
  } finally {
452
469
  setShareBusy(null)
453
470
  }
454
- }, [mission.id, mission.title])
471
+ }, [loadArtifacts, mission.id, mission.title])
455
472
 
456
473
  const revokeShareLink = useCallback(async () => {
457
474
  if (!activeShare) return
@@ -459,13 +476,14 @@ function MissionDetail({ mission, reports, events, busy, onAction, onForceReport
459
476
  try {
460
477
  const revoked = await api<ShareLink>('DELETE', `/share/${activeShare.id}`)
461
478
  setShareLinks((prev) => prev.map((link) => (link.id === revoked.id ? revoked : link)))
479
+ void loadArtifacts()
462
480
  toast.success('Mission share link revoked')
463
481
  } catch (error) {
464
482
  toast.error(error instanceof Error ? error.message : 'Unable to revoke share link')
465
483
  } finally {
466
484
  setShareBusy(null)
467
485
  }
468
- }, [activeShare])
486
+ }, [activeShare, loadArtifacts])
469
487
 
470
488
  const copyShareUrl = useCallback(async () => {
471
489
  if (!shareUrl) return
@@ -552,6 +570,13 @@ function MissionDetail({ mission, reports, events, busy, onAction, onForceReport
552
570
  )}
553
571
  </div>
554
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
+
555
580
  {mission.successCriteria.length > 0 && (
556
581
  <div>
557
582
  <div className="text-[11px] font-600 uppercase tracking-wide text-text-3 mb-2">Success criteria</div>
@@ -706,6 +731,14 @@ export default function MissionsPage() {
706
731
  router.replace('/missions', { scroll: false })
707
732
  }, [router, searchParams, templates])
708
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
+
709
742
  const handleAction = useCallback(async (action: string, reason?: string) => {
710
743
  if (!selectedId) return
711
744
  setBusy(true)
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
  {