Skip to content

Commit 5a06149

Browse files
committed
feat: release mission command v1.8.0
1 parent 747ac67 commit 5a06149

17 files changed

Lines changed: 713 additions & 125 deletions

README.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -399,6 +399,16 @@ Operational docs: https://swarmclaw.ai/docs/observability
399399

400400
## Releases
401401

402+
### v1.8.0 Highlights
403+
404+
Mission Command release: a bigger operator update that makes autonomous missions easier to launch, inspect, and share.
405+
406+
- **Mission Command launchpad.** The home launchpad now opens concrete mission starters for Release QA, Launch Sprint, Cost Audit, and Connector Smoke Test instead of dropping users into a generic mission list.
407+
- **Deep-linked mission templates.** `/missions?template=<id>` opens the right starter template directly, and the template installer can create a mission-driver chat when no sessions exist yet.
408+
- **Quality Center handoffs.** `/quality?tab=evals|approvals|runs` is shareable, and the Quality overview/Eval Lab can start a Release QA mission from current operator evidence.
409+
- **Public mission reports.** Missions can mint, copy, and revoke public share links from the detail view. Shared pages render status, budgets, milestones, and generated reports using the existing allowlisted share resolver.
410+
- **Safer share payloads.** Mission milestones now expose `summary` correctly in public HTML and raw markdown shares, with regression coverage in `npm run test:runtime`.
411+
402412
### v1.7.3 Highlights
403413

404414
Desktop packaging fix for Linux AppImage and deb builds.

electron-builder.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ extraMetadata:
1212
afterPack: ./scripts/electron-after-pack.cjs
1313

1414
asar: true
15+
npmRebuild: false
1516

1617
files:
1718
- electron-dist/**/*

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@swarmclawai/swarmclaw",
3-
"version": "1.7.3",
3+
"version": "1.8.0",
44
"description": "Build and run autonomous AI agents with OpenClaw, Hermes, multiple model providers, orchestration, delegation, memory, skills, schedules, and chat connectors.",
55
"main": "electron-dist/main.js",
66
"license": "MIT",
@@ -87,7 +87,7 @@
8787
"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",
8888
"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",
8989
"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/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/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",
9191
"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",
9292
"test:e2e": "node --import tsx scripts/browser-e2e-smoke.ts",
9393
"test:mcp:conformance": "node --import tsx ./scripts/mcp-conformance-check.ts",

scripts/browser-e2e-smoke.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@ const ROOT = process.cwd()
99
const DEFAULT_ACCESS_KEY = 'swarmclaw-e2e-access-key'
1010
const DEFAULT_CREDENTIAL_SECRET = 'swarmclaw-e2e-credential-secret'
1111
const HEALTH_TIMEOUT_MS = 90_000
12-
const PAGE_TIMEOUT_MS = 45_000
12+
const DEFAULT_PAGE_TIMEOUT_MS = 240_000
13+
const PAGE_TIMEOUT_MS = readPositiveInt(process.env.SWARMCLAW_E2E_PAGE_TIMEOUT_MS) ?? DEFAULT_PAGE_TIMEOUT_MS
1314

1415
interface StartedServer {
1516
baseUrl: string
@@ -61,6 +62,12 @@ function readEnvFileKey(filePath: string, key: string): string | null {
6162
return null
6263
}
6364

65+
function readPositiveInt(value: string | undefined): number | null {
66+
if (!value) return null
67+
const parsed = Number.parseInt(value, 10)
68+
return Number.isFinite(parsed) && parsed > 0 ? parsed : null
69+
}
70+
6471
function resolveAccessKey(): string {
6572
return process.env.SWARMCLAW_E2E_ACCESS_KEY
6673
|| process.env.ACCESS_KEY

scripts/build-electron.mjs

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,6 @@ import { spawnSync } from 'node:child_process'
66
import { fileURLToPath } from 'node:url'
77

88
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
9-
const nativeModules = [
10-
'better-sqlite3',
11-
'@mongodb-js/zstd',
12-
'node-liblzma',
13-
'utf-8-validate',
14-
]
15-
169
const args = new Set(process.argv.slice(2))
1710
const skipNext = args.has('--skip-next')
1811
const publishAlways = args.has('--publish')
@@ -46,11 +39,6 @@ function runWithStatus(cmd, cmdArgs, env = {}) {
4639
return 0
4740
}
4841

49-
function restoreHostNativeModules() {
50-
console.log('[build-electron] restoring host native modules…')
51-
run('npm', ['rebuild', ...nativeModules, '--silent'])
52-
}
53-
5442
function copyDir(src, dest) {
5543
fs.mkdirSync(dest, { recursive: true })
5644
for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
@@ -104,7 +92,6 @@ if (publishAlways) {
10492
builderArgs.push('--publish', 'never')
10593
}
10694
const builderStatus = runWithStatus('npx', ['--no-install', 'electron-builder', ...builderArgs])
107-
restoreHostNativeModules()
10895
if (builderStatus !== 0) process.exit(builderStatus)
10996

11097
console.log('[build-electron] done. Artifacts in release/')

scripts/electron-after-pack.cjs

Lines changed: 48 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -7,39 +7,58 @@ const { spawnSync } = require('node:child_process')
77

88
const ARCH_NAMES = { 0: 'ia32', 1: 'x64', 2: 'armv7l', 3: 'arm64', 4: 'universal' }
99

10-
// Native modules that live in .next/standalone/node_modules and load inside the
11-
// Electron child process. Root node_modules are already rebuilt for the target
12-
// Electron ABI by electron-builder's @electron/rebuild pass before this hook
13-
// fires, so copying those .node files into the packaged standalone is the most
14-
// reliable way to keep the two trees in sync per architecture.
15-
const NATIVE_MODULES = [
10+
// Native modules that load inside the Electron child process. Rebuild these in
11+
// the packaged standalone tree for the target Electron ABI/arch; do not let
12+
// electron-builder rebuild every native dependency in root node_modules because
13+
// transitive N-API packages can force brittle source builds that the app does
14+
// not load from the desktop standalone path.
15+
const STANDALONE_REBUILD_MODULES = [
1616
'better-sqlite3',
17-
'@mongodb-js/zstd',
18-
'node-liblzma',
1917
'utf-8-validate',
2018
]
21-
const REQUIRED_SYNC_MODULES = new Set(['better-sqlite3'])
2219

23-
function copyIfExists(src, dest) {
24-
if (!fs.existsSync(src)) return false
25-
fs.mkdirSync(path.dirname(dest), { recursive: true })
26-
fs.copyFileSync(src, dest)
27-
return true
20+
function readElectronVersion(projectDir) {
21+
const electronPkg = path.join(projectDir, 'node_modules', 'electron', 'package.json')
22+
const raw = fs.readFileSync(electronPkg, 'utf8')
23+
return JSON.parse(raw).version
2824
}
2925

30-
function syncNativeBuildDir(rootPkgDir, standalonePkgDir) {
31-
if (!fs.existsSync(rootPkgDir) || !fs.existsSync(standalonePkgDir)) return false
32-
const rootBuild = path.join(rootPkgDir, 'build', 'Release')
33-
const standaloneBuild = path.join(standalonePkgDir, 'build', 'Release')
34-
if (!fs.existsSync(rootBuild)) return false
35-
fs.mkdirSync(standaloneBuild, { recursive: true })
36-
let synced = false
37-
for (const entry of fs.readdirSync(rootBuild)) {
38-
if (!entry.endsWith('.node')) continue
39-
copyIfExists(path.join(rootBuild, entry), path.join(standaloneBuild, entry))
40-
synced = true
26+
function rebuildStandaloneNativeModules(projectDir, standaloneDir, archName) {
27+
const modules = STANDALONE_REBUILD_MODULES.filter((moduleName) => fs.existsSync(path.join(standaloneDir, 'node_modules', moduleName)))
28+
if (modules.length === 0) return
29+
30+
const electronRebuild = path.join(
31+
projectDir,
32+
'node_modules',
33+
'.bin',
34+
process.platform === 'win32' ? 'electron-rebuild.cmd' : 'electron-rebuild',
35+
)
36+
const electronVersion = readElectronVersion(projectDir)
37+
const cacheDir = path.join(projectDir, '.tmp-electron-rebuild-cache')
38+
const result = spawnSync(
39+
electronRebuild,
40+
[
41+
'--version', electronVersion,
42+
'--module-dir', standaloneDir,
43+
'--only', modules.join(','),
44+
'--arch', archName,
45+
'--sequential',
46+
'--force',
47+
'--disable-pre-gyp-copy',
48+
],
49+
{
50+
cwd: projectDir,
51+
stdio: 'inherit',
52+
shell: process.platform === 'win32',
53+
env: {
54+
...process.env,
55+
npm_config_cache: process.env.npm_config_cache || cacheDir,
56+
},
57+
},
58+
)
59+
if (result.status !== 0) {
60+
throw new Error(`afterPack: electron-rebuild failed with status ${result.status}`)
4161
}
42-
return synced
4362
}
4463

4564
function resolveResourcesDir(context) {
@@ -74,21 +93,12 @@ function signMacApp(context) {
7493

7594
exports.default = async function afterPack(context) {
7695
const projectDir = context.packager.info.projectDir
77-
const standaloneNodeModules = path.join(resolveResourcesDir(context), '.next', 'standalone', 'node_modules')
78-
const rootNodeModules = path.join(projectDir, 'node_modules')
96+
const standaloneDir = path.join(resolveResourcesDir(context), '.next', 'standalone')
7997
const archName = ARCH_NAMES[context.arch]
8098
if (!archName) throw new Error(`afterPack: unknown arch ${context.arch}`)
8199

82-
console.log(`[after-pack] syncing native modules into standalone for arch=${archName}`)
83-
for (const moduleName of NATIVE_MODULES) {
84-
const rootPkg = path.join(rootNodeModules, moduleName)
85-
const standalonePkg = path.join(standaloneNodeModules, moduleName)
86-
const synced = syncNativeBuildDir(rootPkg, standalonePkg)
87-
console.log(`[after-pack] ${moduleName}: ${synced ? 'synced' : 'skipped'}`)
88-
if (!synced && REQUIRED_SYNC_MODULES.has(moduleName) && fs.existsSync(standalonePkg)) {
89-
throw new Error(`afterPack: failed to sync required native module ${moduleName}`)
90-
}
91-
}
100+
console.log(`[after-pack] rebuilding required standalone native modules for arch=${archName}`)
101+
rebuildStandaloneNativeModules(projectDir, standaloneDir, archName)
92102

93103
if (context.electronPlatformName === 'darwin') signMacApp(context)
94104
}

scripts/electron-after-pack.test.mjs

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,17 +11,34 @@ const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'
1111
const { default: afterPack } = require(path.join(repoRoot, 'scripts', 'electron-after-pack.cjs'))
1212

1313
describe('electron afterPack hook', () => {
14-
it('syncs rebuilt native modules into Linux standalone resources', async () => {
14+
it('rebuilds required native modules inside Linux standalone resources', async () => {
1515
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'swarmclaw-after-pack-'))
1616
const projectDir = path.join(tempDir, 'project')
1717
const appOutDir = path.join(tempDir, 'dist', 'linux-unpacked')
18-
const rootNative = path.join(projectDir, 'node_modules', 'better-sqlite3', 'build', 'Release', 'better_sqlite3.node')
18+
const electronPkg = path.join(projectDir, 'node_modules', 'electron', 'package.json')
19+
const rebuildBin = path.join(projectDir, 'node_modules', '.bin', 'electron-rebuild')
1920
const standalonePkg = path.join(appOutDir, 'resources', '.next', 'standalone', 'node_modules', 'better-sqlite3')
2021
const standaloneNative = path.join(standalonePkg, 'build', 'Release', 'better_sqlite3.node')
2122

22-
fs.mkdirSync(path.dirname(rootNative), { recursive: true })
23+
fs.mkdirSync(path.dirname(electronPkg), { recursive: true })
24+
fs.writeFileSync(electronPkg, JSON.stringify({ version: '33.4.11' }))
25+
fs.mkdirSync(path.dirname(rebuildBin), { recursive: true })
26+
fs.writeFileSync(rebuildBin, `#!/bin/sh
27+
set -eu
28+
module_dir=""
29+
arch=""
30+
while [ "$#" -gt 0 ]; do
31+
case "$1" in
32+
--module-dir) module_dir="$2"; shift 2 ;;
33+
--arch) arch="$2"; shift 2 ;;
34+
*) shift ;;
35+
esac
36+
done
37+
mkdir -p "$module_dir/node_modules/better-sqlite3/build/Release"
38+
printf "electron-abi-build-%s" "$arch" > "$module_dir/node_modules/better-sqlite3/build/Release/better_sqlite3.node"
39+
`)
40+
fs.chmodSync(rebuildBin, 0o755)
2341
fs.mkdirSync(standalonePkg, { recursive: true })
24-
fs.writeFileSync(rootNative, 'electron-abi-build')
2542
fs.mkdirSync(path.dirname(standaloneNative), { recursive: true })
2643
fs.writeFileSync(standaloneNative, 'host-node-build')
2744

@@ -36,7 +53,7 @@ describe('electron afterPack hook', () => {
3653
},
3754
})
3855

39-
assert.equal(fs.readFileSync(standaloneNative, 'utf8'), 'electron-abi-build')
56+
assert.equal(fs.readFileSync(standaloneNative, 'utf8'), 'electron-abi-build-x64')
4057
} finally {
4158
fs.rmSync(tempDir, { recursive: true, force: true })
4259
}

src/app/api/s/[token]/raw/route.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ export async function GET(_req: Request, ctx: { params: Promise<{ token: string
4949
if (payload.milestones.length > 0) {
5050
lines.push('## Milestones', '')
5151
for (const m of payload.milestones) {
52-
lines.push(`- ${new Date(m.at).toISOString().slice(0, 19).replace('T', ' ')}: ${m.note}`)
52+
lines.push(`- ${new Date(m.at).toISOString().slice(0, 19).replace('T', ' ')}: ${m.summary}`)
5353
}
5454
lines.push('')
5555
}

src/app/home/page.tsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -266,6 +266,10 @@ export default function HomePage() {
266266
router.push(DEFAULT_BUILDER_ROUTE)
267267
}
268268

269+
const openMissionTemplate = (templateId: string) => {
270+
router.push(`/missions?template=${encodeURIComponent(templateId)}`)
271+
}
272+
269273
if (homeMode === 'launchpad') {
270274
return (
271275
<MainContent>
@@ -286,7 +290,10 @@ export default function HomePage() {
286290
onRunEvalSuite={() => navigateTo('quality')}
287291
onReviewApprovals={() => navigateTo('quality')}
288292
onInspectFailedRuns={() => navigateTo('quality')}
289-
onStartReleaseQaMission={() => navigateTo('missions')}
293+
onStartReleaseQaMission={() => openMissionTemplate('release-candidate-qa')}
294+
onStartLaunchSprintMission={() => openMissionTemplate('launch-week-growth-sprint')}
295+
onStartCostAuditMission={() => openMissionTemplate('agent-cost-audit')}
296+
onStartConnectorSmokeMission={() => openMissionTemplate('connector-smoke-test')}
290297
/>
291298
</div>
292299
</MainContent>

0 commit comments

Comments
 (0)