feat(runtime): improve provider readiness diagnostics
This commit is contained in:
parent
28edecc5e0
commit
85959b6954
53 changed files with 1654 additions and 272 deletions
14
README.md
14
README.md
|
|
@ -23,7 +23,7 @@
|
|||
</p>
|
||||
|
||||
<p align="center">
|
||||
<sub>Free desktop app for AI agent teams. Auto-detects Claude/Codex/OpenCode (200+ models). Use the provider access you already have - subscriptions or API keys. Not just coding agents.</sub>
|
||||
<sub>Free desktop app for AI agent teams. Start with a free model with no auth - no signup, API key, or card - or connect Claude/Codex/OpenCode provider access for more models. Not just coding agents.</sub>
|
||||
</p>
|
||||
|
||||
<img width="1304" height="820" alt="image" src="https://github.com/user-attachments/assets/dea53a01-68b3-4c36-bcf6-e4d1ad4cdb31" />
|
||||
|
|
@ -109,7 +109,7 @@ If you want the FRESHEST version, clone the repo and run it from the `dev` branc
|
|||
|
||||
An orchestration layer for AI agent teams across Claude, Codex, and OpenCode.
|
||||
|
||||
- **Claude + Codex + OpenCode orchestration** — auto-detect available Claude/Codex/OpenCode runtimes and use the provider access you already have - subscriptions or API keys
|
||||
- **Claude + Codex + OpenCode orchestration** — start with a free model with no auth immediately, or auto-detect available Claude/Codex/OpenCode runtimes and use the provider access you already have - subscriptions or API keys
|
||||
- **Assemble your team** — create agent teams with different roles that work autonomously in parallel
|
||||
- **Agents talk to each other** — they communicate, create and manage their own tasks, review, leave comments
|
||||
- **Cross-team communication** — agents can fully communicate across different teams; you can configure or prompt them to collaborate and message each other between teams
|
||||
|
|
@ -137,7 +137,7 @@ An orchestration layer for AI agent teams across Claude, Codex, and OpenCode.
|
|||
|
||||
- **Recent tasks across projects** — browse the latest completed tasks from all your projects in one place
|
||||
|
||||
- **Zero-setup onboarding** — built-in runtime detection and provider authentication
|
||||
- **Zero-setup onboarding** — start with the free model with no auth, then connect paid/account providers only when you need them
|
||||
|
||||
- **Built-in code editor** — edit project files with Git support without leaving the app
|
||||
|
||||
|
|
@ -197,7 +197,7 @@ For feature architecture and implementation guidance:
|
|||
| **Teammate launch status** | ✅ Know who started, who is stuck, and who replied | ⚠️ Session health, less clear message status | ⚠️ Run status, not live teammate status | ❌ | ⚠️ CLI mailbox, no visual status |
|
||||
| **Org chart / governance** | ⚠️ Roles + approvals, no org chart | ⚠️ Roles + escalation | ✅ Org chart + board governance | ⚠️ Team admin only | ❌ |
|
||||
| **Budget controls** | ⚠️ Cost/token visibility, no hard caps | ⚠️ Cost tiers + digest, no hard caps | ✅ Per-agent budgets + hard stops | ⚠️ Usage + BG spend limits | ⚠️ `/usage` + workspace limits |
|
||||
| **Price** | **Free OSS UI**, provider access needed | Free OSS, runtime plans needed | Free OSS, self-hosted + infra | Free + paid usage | Claude plan or API usage |
|
||||
| **Price** | **Free OSS UI + free model with no auth**, paid providers optional | Free OSS, runtime plans needed | Free OSS, self-hosted + infra | Free + paid usage | Claude plan or API usage |
|
||||
|
||||
Fact sources checked on May 18, 2026: [detailed research notes](docs/research/gastown-paperclip-comparison-2026-05-16.md), [Gastown README](https://github.com/gastownhall/gastown), [Gastown provider guide](https://github.com/gastownhall/gastown/blob/main/docs/agent-provider-integration.md), [Gastown scheduler](https://github.com/gastownhall/gastown/blob/main/docs/design/scheduler.md), [Gastown dashboard source](https://github.com/gastownhall/gastown/blob/main/internal/web/templates/convoy.html), [Gastown release](https://github.com/gastownhall/gastown/releases/tag/v1.1.0), [Paperclip README](https://github.com/paperclipai/paperclip), [Paperclip adapters](https://github.com/paperclipai/paperclip/blob/master/docs/adapters/overview.md), [Paperclip heartbeat protocol](https://github.com/paperclipai/paperclip/blob/master/docs/guides/agent-developer/heartbeat-protocol.md), [Paperclip org chart](https://paperclip.inc/docs/guides/board-operator/org-structure/), [Paperclip OrgChart source](https://github.com/paperclipai/paperclip/blob/master/ui/src/pages/OrgChart.tsx), [Paperclip budgets](https://github.com/paperclipai/paperclip/blob/master/docs/guides/board-operator/costs-and-budgets.md), [Paperclip runtime services](https://github.com/paperclipai/paperclip/blob/master/docs/guides/board-operator/execution-workspaces-and-runtime-services.md), [Paperclip Kanban source](https://github.com/paperclipai/paperclip/blob/master/ui/src/components/KanbanBoard.tsx), [Paperclip work products](https://github.com/paperclipai/paperclip/blob/master/packages/shared/src/validators/work-product.ts), [Paperclip release](https://github.com/paperclipai/paperclip/releases/tag/v2026.517.0), [Cursor Background Agents](https://docs.cursor.com/en/background-agents), [Cursor Diffs & Review](https://docs.cursor.com/en/agent/review), [Cursor Bugbot](https://docs.cursor.com/en/bugbot), [Cursor pricing](https://docs.cursor.com/en/account/usage), [Claude Code agent teams](https://code.claude.com/docs/en/agent-teams), [Claude Code subagents](https://code.claude.com/docs/en/sub-agents), [Claude Code workflows](https://code.claude.com/docs/en/common-workflows), [Claude Code costs](https://code.claude.com/docs/en/costs), [Claude pricing](https://claude.com/pricing).
|
||||
|
||||
|
|
@ -206,7 +206,7 @@ Fact sources checked on May 18, 2026: [detailed research notes](docs/research/ga
|
|||
## Quick start
|
||||
|
||||
1. **Download** the app for your platform (see [Installation](#installation))
|
||||
2. **Launch the desktop app** - On first run, the setup wizard will detect the runtime and guide provider authentication
|
||||
2. **Launch the desktop app** - start with the free model with no auth, or let the setup wizard detect runtimes and guide provider authentication
|
||||
3. **Create a team** — Pick a project, define roles, write a provisioning prompt
|
||||
4. **Watch** — Agents spawn, create tasks, and work. You see it all on the kanban board
|
||||
|
||||
|
|
@ -220,7 +220,7 @@ Use the desktop app as the primary product. The browser/web path is not needed f
|
|||
<details>
|
||||
<summary><strong>Do I need to install a runtime before using this app?</strong></summary>
|
||||
<br />
|
||||
No. The app guides runtime detection/setup and provider authentication from the UI - just launch and follow the setup wizard.
|
||||
No. You can start with the free model with no auth right away. If you want Claude, Codex, OpenCode/OpenRouter, or other provider-backed models, the app guides runtime detection/setup and provider authentication from the UI.
|
||||
</details>
|
||||
|
||||
<details>
|
||||
|
|
@ -238,7 +238,7 @@ Yes. Agents send direct messages, create shared tasks, and leave comments - all
|
|||
<details>
|
||||
<summary><strong>Is it free?</strong></summary>
|
||||
<br />
|
||||
Yes, free and open source. The app has no paid tier of its own. To run agents, you only need access to a supported provider/runtime, such as Anthropic or Codex.
|
||||
Yes. The app is free and open source, and you can start with a free model with no auth - no registration, API keys, or credit card. If you want more models, connect the provider access you already have, such as Claude, Codex, OpenCode/OpenRouter, or other supported runtimes.
|
||||
</details>
|
||||
|
||||
<details>
|
||||
|
|
|
|||
|
|
@ -24,17 +24,18 @@
|
|||
radial-gradient(circle at 86% 70%, rgba(255, 43, 255, 0.14), transparent 34%),
|
||||
linear-gradient(180deg, var(--cyber-bg-0), var(--cyber-bg-1) 58%, var(--cyber-bg-0));
|
||||
--cyber-monterey-bg:
|
||||
radial-gradient(circle at 76% 22%, rgba(138, 47, 255, 0.76), transparent 30%),
|
||||
radial-gradient(circle at 18% 76%, rgba(37, 8, 128, 0.72), transparent 36%),
|
||||
linear-gradient(180deg, #180061 0%, #3200a2 46%, #130042 100%);
|
||||
radial-gradient(circle at 76% 22%, rgba(47, 125, 255, 0.22), transparent 30%),
|
||||
radial-gradient(circle at 18% 76%, rgba(37, 8, 128, 0.22), transparent 36%),
|
||||
radial-gradient(circle at 88% 68%, rgba(255, 43, 255, 0.1), transparent 34%),
|
||||
linear-gradient(180deg, #030614 0%, #07122a 46%, #02050d 100%);
|
||||
--cyber-monterey-before-bg:
|
||||
radial-gradient(circle at 18% 34%, rgba(2, 5, 13, 0.62), rgba(2, 5, 13, 0.14) 34%, transparent 62%),
|
||||
linear-gradient(90deg, rgba(2, 5, 13, 0.48) 0%, rgba(2, 5, 13, 0.17) 42%, rgba(2, 5, 13, 0.04) 64%, rgba(2, 5, 13, 0.3) 100%);
|
||||
--cyber-monterey-after-bg:
|
||||
linear-gradient(180deg, rgba(2, 5, 13, 0.92) 0%, rgba(2, 5, 13, 0.62) 15%, rgba(2, 5, 13, 0.08) 44%, rgba(2, 5, 13, 0.68) 100%),
|
||||
radial-gradient(circle at 64% 42%, transparent 0 26%, rgba(2, 5, 13, 0.24) 70%, rgba(2, 5, 13, 0.54) 100%);
|
||||
--cyber-monterey-canvas-opacity: 1;
|
||||
--cyber-monterey-canvas-filter: blur(4px) saturate(1.22) brightness(1.08) contrast(1.08);
|
||||
--cyber-monterey-canvas-opacity: 0.42;
|
||||
--cyber-monterey-canvas-filter: blur(4px) saturate(0.78) brightness(0.62) contrast(1.08);
|
||||
--cyber-monterey-canvas-blend: normal;
|
||||
--cyber-background-bg:
|
||||
radial-gradient(circle at 72% 28%, rgba(0, 234, 255, 0.1), transparent 30%),
|
||||
|
|
@ -454,7 +455,7 @@
|
|||
flex-direction: row;
|
||||
flex-wrap: nowrap;
|
||||
gap: 0.18em;
|
||||
font-size: clamp(2.55rem, 5.1vw, 5.7rem);
|
||||
font-size: clamp(2.45rem, 4.8vw, 5.35rem);
|
||||
line-height: 1;
|
||||
font-weight: 900;
|
||||
letter-spacing: 0;
|
||||
|
|
@ -1468,7 +1469,7 @@
|
|||
}
|
||||
|
||||
.cyber-hero__title {
|
||||
font-size: clamp(3rem, 4.6vw, 4.8rem);
|
||||
font-size: clamp(2.8rem, 4.2vw, 4.45rem);
|
||||
}
|
||||
|
||||
.cyber-scene {
|
||||
|
|
@ -1609,7 +1610,8 @@
|
|||
}
|
||||
|
||||
.cyber-hero__title {
|
||||
font-size: clamp(2.55rem, 13vw, 4rem);
|
||||
gap: 0.12em;
|
||||
font-size: clamp(2rem, 9.4vw, 3.1rem);
|
||||
}
|
||||
|
||||
.cyber-hero__slogan {
|
||||
|
|
|
|||
|
|
@ -6,12 +6,12 @@ const { baseURL } = useRuntimeConfig().app;
|
|||
<NuxtLink to="/" class="app-logo" :prefetch="false">
|
||||
<img
|
||||
:src="`${baseURL}logo-192.png`"
|
||||
alt="Agent Teams"
|
||||
alt="Agent Teams AI"
|
||||
class="app-logo__img"
|
||||
width="36"
|
||||
height="36"
|
||||
>
|
||||
<span class="app-logo__text">Agent Teams</span>
|
||||
<span class="app-logo__text">Agent Teams AI</span>
|
||||
</NuxtLink>
|
||||
</template>
|
||||
|
||||
|
|
@ -46,12 +46,13 @@ const { baseURL } = useRuntimeConfig().app;
|
|||
-webkit-background-clip: text;
|
||||
background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
text-shadow: 0 0 16px rgba(0, 240, 255, 0.22);
|
||||
}
|
||||
</style>
|
||||
|
||||
<style>
|
||||
.v-theme--light .app-logo__text {
|
||||
background: linear-gradient(135deg, #1e293b, #0891b2);
|
||||
background: linear-gradient(135deg, #ffffff 0%, #dff8ff 44%, #43efff 100%);
|
||||
-webkit-background-clip: text;
|
||||
background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
|
|
|
|||
|
|
@ -43,16 +43,25 @@ const docsHref = computed(() => {
|
|||
|
||||
<style scoped>
|
||||
.app-footer {
|
||||
--footer-bg:
|
||||
linear-gradient(180deg, rgba(3, 10, 22, 0.96) 0%, rgba(2, 6, 16, 0.98) 100%);
|
||||
--footer-wall-border: rgba(0, 234, 255, 0.28);
|
||||
--footer-wall-highlight: rgba(255, 255, 255, 0.06);
|
||||
|
||||
position: relative;
|
||||
border-top: 1px solid var(--at-c-border);
|
||||
padding: 20px 0;
|
||||
border-top: 1px solid var(--footer-wall-border);
|
||||
padding: 28px 0 22px;
|
||||
isolation: isolate;
|
||||
background: var(--footer-bg);
|
||||
box-shadow:
|
||||
0 -28px 70px rgba(0, 0, 0, 0.34),
|
||||
0 -1px 0 var(--footer-wall-highlight) inset;
|
||||
}
|
||||
|
||||
.app-footer__robot-stage {
|
||||
position: absolute;
|
||||
right: clamp(24px, 7vw, 112px);
|
||||
bottom: calc(100% - 5px);
|
||||
bottom: calc(100% - 18px);
|
||||
z-index: 2;
|
||||
width: clamp(178px, 16vw, 236px);
|
||||
pointer-events: none;
|
||||
|
|
@ -92,7 +101,7 @@ const docsHref = computed(() => {
|
|||
|
||||
.app-footer__copy {
|
||||
font-size: 13px;
|
||||
opacity: 0.5;
|
||||
color: rgba(244, 247, 255, 0.72);
|
||||
font-family: var(--at-font-mono);
|
||||
}
|
||||
|
||||
|
|
@ -106,7 +115,7 @@ const docsHref = computed(() => {
|
|||
color: var(--at-c-cyan);
|
||||
text-decoration: none;
|
||||
font-size: 13px;
|
||||
opacity: 0.7;
|
||||
opacity: 0.9;
|
||||
transition: opacity 0.2s ease;
|
||||
font-family: var(--at-font-mono);
|
||||
}
|
||||
|
|
@ -122,11 +131,19 @@ const docsHref = computed(() => {
|
|||
}
|
||||
|
||||
.v-theme--light .app-footer {
|
||||
border-top-color: var(--at-c-border);
|
||||
--footer-bg:
|
||||
linear-gradient(180deg, rgba(230, 240, 247, 0.98) 0%, rgba(218, 229, 238, 0.98) 100%);
|
||||
--footer-wall-border: rgba(8, 88, 112, 0.24);
|
||||
--footer-wall-highlight: rgba(255, 255, 255, 0.82);
|
||||
|
||||
border-top-color: var(--footer-wall-border);
|
||||
box-shadow:
|
||||
0 -32px 74px rgba(62, 84, 104, 0.2),
|
||||
0 -1px 0 rgba(255, 255, 255, 0.92) inset;
|
||||
}
|
||||
|
||||
.v-theme--light .app-footer__copy {
|
||||
opacity: 0.72;
|
||||
color: rgba(42, 50, 61, 0.74);
|
||||
}
|
||||
|
||||
.v-theme--light .app-footer__link {
|
||||
|
|
|
|||
|
|
@ -183,12 +183,12 @@ const navItems = computed(() => [
|
|||
--header-cyan: var(--cyber-cyan);
|
||||
--header-violet: var(--cyber-violet);
|
||||
--header-magenta: var(--cyber-magenta);
|
||||
--header-height: 128px;
|
||||
--header-height: 126px;
|
||||
--header-panel-height: 86px;
|
||||
--header-action-size: clamp(54px, 3.25vw, 66px);
|
||||
--header-github-width: clamp(150px, 9.7vw, 204px);
|
||||
--header-brand-icon: clamp(58px, 4.1vw, 76px);
|
||||
--header-brand-text: clamp(24px, 1.55vw, 34px);
|
||||
--header-brand-icon: clamp(52px, 3.7vw, 68px);
|
||||
--header-brand-text: clamp(23px, 1.42vw, 32px);
|
||||
|
||||
position: fixed;
|
||||
top: 0;
|
||||
|
|
@ -299,7 +299,7 @@ const navItems = computed(() => [
|
|||
width: 100%;
|
||||
height: var(--header-panel-height);
|
||||
min-width: 0;
|
||||
padding: 0 64px 0 clamp(38px, 4.2vw, 82px);
|
||||
padding: 0 46px 0 clamp(24px, 2vw, 38px);
|
||||
background: transparent;
|
||||
border: 0;
|
||||
overflow: hidden;
|
||||
|
|
@ -548,12 +548,12 @@ const navItems = computed(() => [
|
|||
|
||||
@media (max-width: 1439px) {
|
||||
.app-header {
|
||||
--header-height: 104px;
|
||||
--header-height: 112px;
|
||||
--header-panel-height: 72px;
|
||||
--header-action-size: 54px;
|
||||
--header-github-width: 124px;
|
||||
--header-brand-icon: 48px;
|
||||
--header-brand-text: 16px;
|
||||
--header-brand-icon: 44px;
|
||||
--header-brand-text: 15px;
|
||||
}
|
||||
|
||||
.app-header__inner {
|
||||
|
|
@ -563,8 +563,8 @@ const navItems = computed(() => [
|
|||
|
||||
.app-header__brand-frame {
|
||||
height: 72px;
|
||||
padding-left: 38px;
|
||||
padding-right: 38px;
|
||||
padding-left: 24px;
|
||||
padding-right: 28px;
|
||||
}
|
||||
|
||||
.app-header__brand-frame :deep(.app-logo__text) {
|
||||
|
|
@ -598,7 +598,7 @@ const navItems = computed(() => [
|
|||
@media (max-width: 1120px) {
|
||||
.app-header {
|
||||
--header-github-width: 104px;
|
||||
--header-brand-text: 14px;
|
||||
--header-brand-text: 13px;
|
||||
}
|
||||
|
||||
.app-header__inner {
|
||||
|
|
@ -606,8 +606,8 @@ const navItems = computed(() => [
|
|||
}
|
||||
|
||||
.app-header__brand-frame {
|
||||
padding-left: 26px;
|
||||
padding-right: 34px;
|
||||
padding-left: 18px;
|
||||
padding-right: 24px;
|
||||
}
|
||||
|
||||
.app-header__nav {
|
||||
|
|
@ -628,11 +628,11 @@ const navItems = computed(() => [
|
|||
|
||||
@media (max-width: 1279px) and (min-width: 768px) {
|
||||
.app-header {
|
||||
--header-height: 88px;
|
||||
--header-height: 104px;
|
||||
--header-panel-height: 64px;
|
||||
--header-action-size: clamp(40px, 5vw, 48px);
|
||||
--header-brand-icon: clamp(38px, 4.8vw, 44px);
|
||||
--header-brand-text: clamp(11px, 1.35vw, 14px);
|
||||
--header-brand-icon: clamp(34px, 4.6vw, 42px);
|
||||
--header-brand-text: clamp(10px, 1.2vw, 12px);
|
||||
}
|
||||
|
||||
.app-header__inner {
|
||||
|
|
@ -642,8 +642,8 @@ const navItems = computed(() => [
|
|||
|
||||
.app-header__brand-frame {
|
||||
height: 64px;
|
||||
padding-left: clamp(18px, 2.4vw, 30px);
|
||||
padding-right: clamp(22px, 3vw, 34px);
|
||||
padding-left: clamp(12px, 1.7vw, 18px);
|
||||
padding-right: clamp(16px, 2vw, 22px);
|
||||
}
|
||||
|
||||
.app-header__brand-frame :deep(.app-logo) {
|
||||
|
|
@ -658,7 +658,7 @@ const navItems = computed(() => [
|
|||
--nav-pad-start: clamp(14px, 3.4cqw, 28px);
|
||||
--nav-pad-end: clamp(18px, 4cqw, 34px);
|
||||
|
||||
top: 12px;
|
||||
top: calc((var(--header-height) - var(--header-panel-height)) / 2);
|
||||
left: calc(456 / 2048 * 100%);
|
||||
right: calc((2048 - 1568) / 2048 * 100%);
|
||||
height: 64px;
|
||||
|
|
@ -715,6 +715,8 @@ const navItems = computed(() => [
|
|||
@media (max-width: 767px) {
|
||||
.app-header {
|
||||
height: 64px;
|
||||
--header-brand-icon: 34px;
|
||||
--header-brand-text: 12px;
|
||||
}
|
||||
|
||||
.app-header__inner {
|
||||
|
|
|
|||
|
|
@ -224,7 +224,7 @@ const rows = computed<ComparisonRow[]>(() => [
|
|||
},
|
||||
{
|
||||
feature: t('comparison.features.price'),
|
||||
us: { status: 'free', note: 'OSS, provider access needed' },
|
||||
us: { status: 'free', note: 'OSS + free model with no auth, paid providers optional' },
|
||||
gastown: { status: 'free', note: 'OSS, runtime plans needed' },
|
||||
paperclip: { status: 'free', note: 'OSS, self-hosted + infra' },
|
||||
cursor: { status: 'text', note: 'Free + paid usage' },
|
||||
|
|
@ -511,7 +511,7 @@ function getStatusIcon(status: string): string {
|
|||
.comparison-table__robot {
|
||||
position: absolute;
|
||||
right: clamp(28px, 7vw, 96px);
|
||||
bottom: calc(100% - 5px);
|
||||
bottom: calc(100% - 20px);
|
||||
z-index: 4;
|
||||
width: clamp(82px, 7.2vw, 124px);
|
||||
height: auto;
|
||||
|
|
|
|||
|
|
@ -892,6 +892,11 @@ const releaseDate = computed(() => {
|
|||
color: #64748b;
|
||||
}
|
||||
|
||||
.v-theme--light .download-section__btn {
|
||||
color: #f8fbff;
|
||||
text-shadow: 0 1px 8px rgba(15, 23, 42, 0.34);
|
||||
}
|
||||
|
||||
.v-theme--light .download-section__release-info {
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -131,9 +131,10 @@ onUnmounted(() => {
|
|||
<v-container class="cyber-hero__container">
|
||||
<div class="cyber-hero__layout">
|
||||
<div class="cyber-hero__copy">
|
||||
<h1 class="cyber-hero__title" aria-label="Agent Teams">
|
||||
<h1 class="cyber-hero__title" aria-label="Agent Teams AI">
|
||||
<span>Agent</span>
|
||||
<span class="cyber-hero__title-accent">Teams</span>
|
||||
<span class="cyber-hero__title-accent">AI</span>
|
||||
</h1>
|
||||
|
||||
<p class="cyber-hero__slogan cyber-panel">
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<script setup lang="ts">
|
||||
import { mdiOpenSourceInitiative } from '@mdi/js'
|
||||
import { mdiCheckCircleOutline, mdiOpenSourceInitiative } from '@mdi/js'
|
||||
import { useLandingContent } from '~/composables/useLandingContent'
|
||||
|
||||
const { content } = useLandingContent()
|
||||
|
|
@ -43,6 +43,23 @@ function onGetStarted() {
|
|||
<span v-if="plan.period" class="pricing-card__period">/ {{ plan.period }}</span>
|
||||
</div>
|
||||
<p class="pricing-card__description">{{ plan.description }}</p>
|
||||
<div class="pricing-card__callout">
|
||||
{{ t('pricing.freeModelCallout') }}
|
||||
</div>
|
||||
<ul v-if="plan.features.length" class="pricing-card__features">
|
||||
<li
|
||||
v-for="feature in plan.features"
|
||||
:key="feature"
|
||||
class="pricing-card__feature"
|
||||
>
|
||||
<v-icon
|
||||
size="16"
|
||||
class="pricing-card__feature-icon"
|
||||
:icon="mdiCheckCircleOutline"
|
||||
/>
|
||||
<span>{{ feature }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<button
|
||||
|
|
@ -177,6 +194,41 @@ function onGetStarted() {
|
|||
margin: 0;
|
||||
}
|
||||
|
||||
.pricing-card__callout {
|
||||
margin-top: 16px;
|
||||
padding: 10px 12px;
|
||||
border: 1px solid rgba(57, 255, 20, 0.22);
|
||||
border-radius: 10px;
|
||||
background: rgba(57, 255, 20, 0.08);
|
||||
color: #d9ffe0;
|
||||
font-size: 0.86rem;
|
||||
font-weight: 700;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.pricing-card__features {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 18px 0 0;
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.pricing-card__feature {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
color: #aab4d4;
|
||||
font-size: 0.84rem;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.pricing-card__feature-icon {
|
||||
color: #39ff14;
|
||||
flex-shrink: 0;
|
||||
margin-top: 1px;
|
||||
}
|
||||
|
||||
.pricing-card__btn--primary {
|
||||
margin-top: 24px;
|
||||
width: 100%;
|
||||
|
|
@ -243,6 +295,20 @@ function onGetStarted() {
|
|||
color: #475569;
|
||||
}
|
||||
|
||||
.v-theme--light .pricing-card__callout {
|
||||
border-color: rgba(18, 161, 80, 0.22);
|
||||
background: rgba(18, 161, 80, 0.08);
|
||||
color: #116b3b;
|
||||
}
|
||||
|
||||
.v-theme--light .pricing-card__feature {
|
||||
color: #475569;
|
||||
}
|
||||
|
||||
.v-theme--light .pricing-card__feature-icon {
|
||||
color: #12a150;
|
||||
}
|
||||
|
||||
.v-theme--light .pricing-section__note {
|
||||
color: #56617c;
|
||||
opacity: 1;
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@
|
|||
{
|
||||
"id": "isFree",
|
||||
"question": "هل هو مجاني فعلاً؟",
|
||||
"answer": "نعم. التطبيق مجاني ومفتوح المصدر. التطبيق نفسه لا يملك خطة مدفوعة. لتشغيل الوكلاء تحتاج فقط إلى وصول إلى مزود أو runtime مدعوم مثل Anthropic أو Codex."
|
||||
"answer": "نعم. التطبيق مجاني ومفتوح المصدر، ويمكنك البدء بنموذج مجاني بدون مصادقة - بدون تسجيل أو مفتاح API أو بطاقة ائتمان. إذا أردت نماذج أكثر، وصّل Claude أو Codex أو OpenCode/OpenRouter أو مزوداً مدعوماً آخر."
|
||||
},
|
||||
{
|
||||
"id": "platforms",
|
||||
|
|
@ -64,7 +64,7 @@
|
|||
{
|
||||
"id": "requirements",
|
||||
"question": "ماذا أحتاج للبدء؟",
|
||||
"answer": "فقط ثبّت التطبيق - وسيرشدك من الواجهة لاكتشاف الـ runtime وتسجيل دخول المزود. البدء بدون إعداد يجعلك تعمل في دقائق."
|
||||
"answer": "فقط ثبّت التطبيق - ابدأ بالنموذج المجاني بدون مصادقة، ثم وصّل نماذج provider-backed من الواجهة فقط عندما تحتاجها."
|
||||
}
|
||||
],
|
||||
"download": {
|
||||
|
|
@ -85,16 +85,16 @@
|
|||
"name": "مجاني",
|
||||
"price": "$0",
|
||||
"period": "",
|
||||
"description": "كل شيء متضمن. بدون حدود، بدون مفاتيح API، بدون بطاقة ائتمان.",
|
||||
"description": "ابدأ بالنموذج المجاني بدون مصادقة المضمّن. بدون تسجيل، بدون مفتاح API، بدون بطاقة.",
|
||||
"features": [
|
||||
"نموذج مجاني بدون مصادقة لأول تشغيل",
|
||||
"لا تحتاج حساباً أو تسجيل دخول مزود للتجربة",
|
||||
"وصول اختياري إلى Claude وCodex وOpenCode",
|
||||
"فرق وكلاء غير محدودة",
|
||||
"لوحة كانبان بتحديثات فورية",
|
||||
"مراجعة كود مع عرض diff",
|
||||
"تواصل بين الفرق",
|
||||
"وضع فردي وجماعي",
|
||||
"مراقبة عمليات مباشرة",
|
||||
"محرر كود مدمج",
|
||||
"تكامل MCP"
|
||||
"وضع فردي وجماعي"
|
||||
],
|
||||
"highlighted": true
|
||||
}
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@
|
|||
{
|
||||
"id": "isFree",
|
||||
"question": "Ist es wirklich kostenlos?",
|
||||
"answer": "Ja. Die App ist kostenlos und Open Source. Die App selbst hat kein Bezahlmodell. Um Agenten auszuführen, brauchen Sie nur Zugriff auf einen unterstützten Provider bzw. Runtime wie Anthropic oder Codex."
|
||||
"answer": "Ja. Die App ist kostenlos und Open Source, und Sie können mit einem kostenlosen Modell ohne Authentifizierung starten - ohne Registrierung, API-Schlüssel oder Kreditkarte. Für mehr Modelle verbinden Sie Claude, Codex, OpenCode/OpenRouter oder einen anderen unterstützten Provider."
|
||||
},
|
||||
{
|
||||
"id": "platforms",
|
||||
|
|
@ -64,7 +64,7 @@
|
|||
{
|
||||
"id": "requirements",
|
||||
"question": "Was brauche ich zum Start?",
|
||||
"answer": "Einfach die App installieren - sie führt Sie in der UI durch Runtime-Erkennung und Provider-Authentifizierung. Zero-Setup-Onboarding bringt Sie in Minuten zum Laufen."
|
||||
"answer": "Einfach die App installieren - starten Sie mit dem kostenlosen Modell ohne Authentifizierung und verbinden Sie provider-backed Modelle in der UI erst, wenn Sie sie brauchen."
|
||||
}
|
||||
],
|
||||
"download": {
|
||||
|
|
@ -85,16 +85,16 @@
|
|||
"name": "Kostenlos",
|
||||
"price": "0€",
|
||||
"period": "",
|
||||
"description": "Alles inklusive. Keine Limits, keine API-Schlüssel, keine Kreditkarte.",
|
||||
"description": "Starten Sie mit dem enthaltenen kostenlosen Modell ohne Authentifizierung. Keine Registrierung, kein API-Schlüssel, keine Kreditkarte.",
|
||||
"features": [
|
||||
"Kostenloses Modell ohne Authentifizierung für erste Läufe",
|
||||
"Kein Konto und kein Provider-Login zum Testen erforderlich",
|
||||
"Optionaler Zugriff auf Claude, Codex und OpenCode",
|
||||
"Unbegrenzte Agenten-Teams",
|
||||
"Kanban-Board mit Echtzeit-Updates",
|
||||
"Code-Review mit Diff-Ansicht",
|
||||
"Teamübergreifende Kommunikation",
|
||||
"Solo- und Team-Modi",
|
||||
"Live-Prozessüberwachung",
|
||||
"Integrierter Code-Editor",
|
||||
"MCP-Integration"
|
||||
"Solo- und Team-Modi"
|
||||
],
|
||||
"highlighted": true
|
||||
}
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@
|
|||
{
|
||||
"id": "isFree",
|
||||
"question": "Is it really free?",
|
||||
"answer": "Yes. The app itself is free and open source. The app has no paid tier of its own. To run agents, you only need access to a supported provider/runtime, such as Anthropic or Codex."
|
||||
"answer": "Yes. The app is free and open source, and you can start with a free model with no auth - no registration, API keys, or credit card. If you want more models, connect Claude, Codex, OpenCode/OpenRouter, or another supported provider."
|
||||
},
|
||||
{
|
||||
"id": "platforms",
|
||||
|
|
@ -64,7 +64,7 @@
|
|||
{
|
||||
"id": "requirements",
|
||||
"question": "What do I need to get started?",
|
||||
"answer": "Just install the app - it guides runtime detection and provider authentication from the UI. Zero-setup onboarding gets you running in minutes."
|
||||
"answer": "Just install the app - start with the free model with no auth, then connect provider-backed models from the UI only when you need them."
|
||||
}
|
||||
],
|
||||
"download": {
|
||||
|
|
@ -85,16 +85,16 @@
|
|||
"name": "Free",
|
||||
"price": "$0",
|
||||
"period": "",
|
||||
"description": "Everything included. No limits, no API keys, no credit card.",
|
||||
"description": "Start with the included free model with no auth. No signup, no API key, no credit card.",
|
||||
"features": [
|
||||
"Free model with no auth for first runs",
|
||||
"No account or provider login required to try",
|
||||
"Optional Claude, Codex, and OpenCode provider access",
|
||||
"Unlimited agent teams",
|
||||
"Kanban board with real-time updates",
|
||||
"Code review with diff view",
|
||||
"Cross-team communication",
|
||||
"Solo & team modes",
|
||||
"Live process monitoring",
|
||||
"Built-in code editor",
|
||||
"MCP integration"
|
||||
"Solo & team modes"
|
||||
],
|
||||
"highlighted": true
|
||||
}
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@
|
|||
{
|
||||
"id": "isFree",
|
||||
"question": "¿Es realmente gratis?",
|
||||
"answer": "Sí. La app es gratuita y de código abierto. La app no tiene un plan de pago propio. Para ejecutar agentes solo necesitas acceso a un proveedor/runtime compatible, como Anthropic o Codex."
|
||||
"answer": "Sí. La app es gratuita y de código abierto, y puedes empezar con un modelo gratuito sin autenticación - sin registro, claves API ni tarjeta. Si quieres más modelos, conecta Claude, Codex, OpenCode/OpenRouter u otro proveedor compatible."
|
||||
},
|
||||
{
|
||||
"id": "platforms",
|
||||
|
|
@ -64,7 +64,7 @@
|
|||
{
|
||||
"id": "requirements",
|
||||
"question": "¿Qué necesito para empezar?",
|
||||
"answer": "Solo instala la app - te guía en la detección del runtime y la autenticación del proveedor desde la interfaz. El onboarding sin configuración te pone en marcha en minutos."
|
||||
"answer": "Solo instala la app - empieza con el modelo gratuito sin autenticación, y conecta modelos con proveedor desde la interfaz solo cuando los necesites."
|
||||
}
|
||||
],
|
||||
"download": {
|
||||
|
|
@ -85,16 +85,16 @@
|
|||
"name": "Gratis",
|
||||
"price": "$0",
|
||||
"period": "",
|
||||
"description": "Todo incluido. Sin límites, sin claves API, sin tarjeta de crédito.",
|
||||
"description": "Empieza con el modelo gratuito sin autenticación incluido. Sin registro, sin clave API, sin tarjeta.",
|
||||
"features": [
|
||||
"Modelo gratuito sin autenticación para los primeros usos",
|
||||
"No necesitas cuenta ni login de proveedor para probar",
|
||||
"Acceso opcional a Claude, Codex y OpenCode",
|
||||
"Equipos de agentes ilimitados",
|
||||
"Tablero kanban con actualizaciones en tiempo real",
|
||||
"Revisión de código con vista diff",
|
||||
"Comunicación entre equipos",
|
||||
"Modo solo y equipo",
|
||||
"Monitorización de procesos en vivo",
|
||||
"Editor de código integrado",
|
||||
"Integración MCP"
|
||||
"Modo solo y equipo"
|
||||
],
|
||||
"highlighted": true
|
||||
}
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@
|
|||
{
|
||||
"id": "isFree",
|
||||
"question": "C'est vraiment gratuit ?",
|
||||
"answer": "Oui. L'application est gratuite et open source. L'application n'a pas d'offre payante. Pour exécuter des agents, vous avez seulement besoin d'un accès à un provider/runtime pris en charge, comme Anthropic ou Codex."
|
||||
"answer": "Oui. L'application est gratuite et open source, et vous pouvez commencer avec un modèle gratuit sans authentification - sans inscription, clé API ni carte bancaire. Pour plus de modèles, connectez Claude, Codex, OpenCode/OpenRouter ou un autre provider pris en charge."
|
||||
},
|
||||
{
|
||||
"id": "platforms",
|
||||
|
|
@ -64,7 +64,7 @@
|
|||
{
|
||||
"id": "requirements",
|
||||
"question": "De quoi ai-je besoin pour commencer ?",
|
||||
"answer": "Installez simplement l'application - elle vous guide pour la détection du runtime et l'authentification du provider depuis l'interface. L'onboarding zéro-configuration vous fait démarrer en quelques minutes."
|
||||
"answer": "Installez simplement l'application - commencez avec le modèle gratuit sans authentification, puis connectez les modèles provider-backed depuis l'interface seulement si nécessaire."
|
||||
}
|
||||
],
|
||||
"download": {
|
||||
|
|
@ -85,16 +85,16 @@
|
|||
"name": "Gratuit",
|
||||
"price": "0€",
|
||||
"period": "",
|
||||
"description": "Tout inclus. Sans limites, sans clés API, sans carte bancaire.",
|
||||
"description": "Commencez avec le modèle gratuit sans authentification inclus. Sans inscription, sans clé API, sans carte bancaire.",
|
||||
"features": [
|
||||
"Modèle gratuit sans authentification pour les premiers essais",
|
||||
"Aucun compte ni login provider requis pour tester",
|
||||
"Accès optionnel à Claude, Codex et OpenCode",
|
||||
"Équipes d'agents illimitées",
|
||||
"Kanban avec mises à jour en temps réel",
|
||||
"Revue de code avec vue diff",
|
||||
"Communication inter-équipes",
|
||||
"Modes solo et équipe",
|
||||
"Surveillance des processus en direct",
|
||||
"Éditeur de code intégré",
|
||||
"Intégration MCP"
|
||||
"Modes solo et équipe"
|
||||
],
|
||||
"highlighted": true
|
||||
}
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@
|
|||
{
|
||||
"id": "isFree",
|
||||
"question": "क्या यह सच में मुफ़्त है?",
|
||||
"answer": "हाँ। ऐप मुफ़्त और ओपन सोर्स है। ऐप का अपना कोई paid plan नहीं है। agents चलाने के लिए आपको सिर्फ़ किसी supported provider/runtime, जैसे Anthropic या Codex, का access चाहिए।"
|
||||
"answer": "हाँ। ऐप मुफ़्त और ओपन सोर्स है, और आप बिना registration, API key या credit card के मुफ़्त no-auth model से शुरू कर सकते हैं। अगर और models चाहिए, तो Claude, Codex, OpenCode/OpenRouter या कोई दूसरा supported provider जोड़ें।"
|
||||
},
|
||||
{
|
||||
"id": "platforms",
|
||||
|
|
@ -64,7 +64,7 @@
|
|||
{
|
||||
"id": "requirements",
|
||||
"question": "शुरू करने के लिए क्या चाहिए?",
|
||||
"answer": "बस ऐप इंस्टॉल करें - यह UI से runtime detection और provider authentication में गाइड करता है। zero-setup onboarding आपको कुछ ही मिनटों में शुरू करा देता है।"
|
||||
"answer": "बस ऐप इंस्टॉल करें - मुफ़्त no-auth model से शुरू करें, फिर provider-backed models को UI से तभी जोड़ें जब ज़रूरत हो।"
|
||||
}
|
||||
],
|
||||
"download": {
|
||||
|
|
@ -85,16 +85,16 @@
|
|||
"name": "मुफ़्त",
|
||||
"price": "$0",
|
||||
"period": "",
|
||||
"description": "सब कुछ शामिल। कोई लिमिट नहीं, कोई API कुंजी नहीं, कोई क्रेडिट कार्ड नहीं।",
|
||||
"description": "शामिल मुफ़्त no-auth model से शुरू करें। कोई registration नहीं, कोई API key नहीं, कोई credit card नहीं।",
|
||||
"features": [
|
||||
"पहले runs के लिए मुफ़्त no-auth model",
|
||||
"Try करने के लिए account या provider login नहीं चाहिए",
|
||||
"Claude, Codex और OpenCode provider access optional",
|
||||
"असीमित एजेंट टीमें",
|
||||
"रियल-टाइम अपडेट के साथ कानबन बोर्ड",
|
||||
"diff व्यू के साथ कोड रिव्यू",
|
||||
"क्रॉस-टीम कम्युनिकेशन",
|
||||
"सोलो और टीम मोड",
|
||||
"लाइव प्रोसेस मॉनिटरिंग",
|
||||
"बिल्ट-इन कोड एडिटर",
|
||||
"MCP इंटीग्रेशन"
|
||||
"सोलो और टीम मोड"
|
||||
],
|
||||
"highlighted": true
|
||||
}
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@
|
|||
{
|
||||
"id": "isFree",
|
||||
"question": "本当に無料ですか?",
|
||||
"answer": "はい。アプリは無料のオープンソースです。アプリ自体に有料プランはありません。エージェントを実行するには、Anthropic や Codex など対応する provider/runtime へのアクセスだけが必要です。"
|
||||
"answer": "はい。アプリは無料のオープンソースで、登録、APIキー、クレジットカードなしで認証なしの無料モデルから始められます。さらに多くのモデルが必要な場合は、Claude、Codex、OpenCode/OpenRouter など対応 provider を接続できます。"
|
||||
},
|
||||
{
|
||||
"id": "platforms",
|
||||
|
|
@ -64,7 +64,7 @@
|
|||
{
|
||||
"id": "requirements",
|
||||
"question": "始めるには何が必要ですか?",
|
||||
"answer": "アプリをインストールするだけです - UI 上で runtime の検出と provider 認証を案内します。ゼロ設定のオンボーディングで数分で開始できます。"
|
||||
"answer": "アプリをインストールするだけです - 認証なしの無料モデルから始め、provider-backed モデルは必要になった時だけ UI から接続できます。"
|
||||
}
|
||||
],
|
||||
"download": {
|
||||
|
|
@ -85,16 +85,16 @@
|
|||
"name": "無料",
|
||||
"price": "¥0",
|
||||
"period": "",
|
||||
"description": "すべて含まれています。制限なし、APIキー不要、クレジットカード不要。",
|
||||
"description": "含まれている認証なしの無料モデルから開始できます。登録なし、APIキー不要、クレジットカード不要。",
|
||||
"features": [
|
||||
"最初の実行に使える認証なしの無料モデル",
|
||||
"試用にアカウントや provider ログインは不要",
|
||||
"Claude、Codex、OpenCode provider 接続は任意",
|
||||
"無制限のエージェントチーム",
|
||||
"リアルタイム更新のカンバンボード",
|
||||
"Diffビュー付きコードレビュー",
|
||||
"チーム間コミュニケーション",
|
||||
"ソロ&チームモード",
|
||||
"ライブプロセス監視",
|
||||
"組み込みコードエディタ",
|
||||
"MCP統合"
|
||||
"ソロ&チームモード"
|
||||
],
|
||||
"highlighted": true
|
||||
}
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@
|
|||
{
|
||||
"id": "isFree",
|
||||
"question": "É realmente grátis?",
|
||||
"answer": "Sim. O app é gratuito e open source. O app não tem plano pago próprio. Para rodar agentes, você só precisa de acesso a um provider/runtime compatível, como Anthropic ou Codex."
|
||||
"answer": "Sim. O app é gratuito e open source, e você pode começar com um modelo gratuito sem autenticação - sem cadastro, chave de API ou cartão. Para mais modelos, conecte Claude, Codex, OpenCode/OpenRouter ou outro provedor compatível."
|
||||
},
|
||||
{
|
||||
"id": "platforms",
|
||||
|
|
@ -64,7 +64,7 @@
|
|||
{
|
||||
"id": "requirements",
|
||||
"question": "O que preciso para começar?",
|
||||
"answer": "Basta instalar o app - ele guia a detecção do runtime e a autenticação do provider pela interface. O onboarding sem configuração coloca você em ação em minutos."
|
||||
"answer": "Basta instalar o app - comece com o modelo gratuito sem autenticação, depois conecte modelos com provedor pela interface só quando precisar."
|
||||
}
|
||||
],
|
||||
"download": {
|
||||
|
|
@ -85,16 +85,16 @@
|
|||
"name": "Grátis",
|
||||
"price": "$0",
|
||||
"period": "",
|
||||
"description": "Tudo incluído. Sem limites, sem chaves de API, sem cartão de crédito.",
|
||||
"description": "Comece com o modelo gratuito sem autenticação incluído. Sem cadastro, sem chave de API, sem cartão.",
|
||||
"features": [
|
||||
"Modelo gratuito sem autenticação para os primeiros usos",
|
||||
"Não precisa de conta nem login de provedor para testar",
|
||||
"Acesso opcional a Claude, Codex e OpenCode",
|
||||
"Equipes de agentes ilimitadas",
|
||||
"Quadro kanban com atualizações em tempo real",
|
||||
"Revisão de código com visualização diff",
|
||||
"Comunicação entre equipes",
|
||||
"Modo solo e equipe",
|
||||
"Monitoramento de processos ao vivo",
|
||||
"Editor de código integrado",
|
||||
"Integração MCP"
|
||||
"Modo solo e equipe"
|
||||
],
|
||||
"highlighted": true
|
||||
}
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@
|
|||
{
|
||||
"id": "isFree",
|
||||
"question": "Это действительно бесплатно?",
|
||||
"answer": "Да. Само приложение полностью бесплатное и с открытым кодом. У него нет собственного платного тарифа. Для запуска агентов нужен только доступ к поддерживаемому провайдеру/рантайму, например Anthropic или Codex."
|
||||
"answer": "Да. Приложение бесплатное и open source, и можно начать с бесплатной модели без авторизации - без регистрации, API-ключей и карты. Если нужны дополнительные модели, подключите Claude, Codex, OpenCode/OpenRouter или другого поддерживаемого провайдера."
|
||||
},
|
||||
{
|
||||
"id": "platforms",
|
||||
|
|
@ -64,7 +64,7 @@
|
|||
{
|
||||
"id": "requirements",
|
||||
"question": "Что нужно для начала?",
|
||||
"answer": "Просто установите приложение - оно проведёт вас через определение рантайма и аутентификацию провайдера прямо в интерфейсе. Онбординг без настройки запустит вас за минуты."
|
||||
"answer": "Просто установите приложение - начните с бесплатной модели без авторизации, а provider-backed модели подключайте из интерфейса только когда они понадобятся."
|
||||
}
|
||||
],
|
||||
"download": {
|
||||
|
|
@ -85,16 +85,16 @@
|
|||
"name": "Бесплатно",
|
||||
"price": "$0",
|
||||
"period": "",
|
||||
"description": "Всё включено. Без лимитов, без API-ключей, без кредитной карты.",
|
||||
"description": "Начните со встроенной бесплатной модели без авторизации. Без регистрации, API-ключей и карты.",
|
||||
"features": [
|
||||
"Бесплатная модель без авторизации для первого запуска",
|
||||
"Без аккаунта и логина провайдера для пробы",
|
||||
"Опциональный доступ к Claude, Codex и OpenCode",
|
||||
"Безлимитные команды агентов",
|
||||
"Канбан-доска с обновлениями в реальном времени",
|
||||
"Код-ревью с diff-просмотром",
|
||||
"Межкомандная коммуникация",
|
||||
"Соло и командный режимы",
|
||||
"Мониторинг живых процессов",
|
||||
"Встроенный редактор кода",
|
||||
"Интеграция MCP"
|
||||
"Соло и командный режимы"
|
||||
],
|
||||
"highlighted": true
|
||||
}
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@
|
|||
{
|
||||
"id": "isFree",
|
||||
"question": "真的免费吗?",
|
||||
"answer": "是的。应用本身免费且开源。应用没有自己的付费方案。要运行智能体,你只需要接入受支持的 provider/runtime,例如 Anthropic 或 Codex。"
|
||||
"answer": "是的。应用免费且开源,你可以从无需认证的免费模型开始,无需注册、API 密钥或信用卡。如果需要更多模型,可以连接 Claude、Codex、OpenCode/OpenRouter 或其他受支持的提供商。"
|
||||
},
|
||||
{
|
||||
"id": "platforms",
|
||||
|
|
@ -64,7 +64,7 @@
|
|||
{
|
||||
"id": "requirements",
|
||||
"question": "开始需要什么?",
|
||||
"answer": "只需安装应用 - 它会在界面中引导你完成 runtime 检测和 provider 认证。零配置上手,几分钟即可运行。"
|
||||
"answer": "只需安装应用 - 先使用无需认证的免费模型,需要更多 provider-backed 模型时再从界面连接。"
|
||||
}
|
||||
],
|
||||
"download": {
|
||||
|
|
@ -85,16 +85,16 @@
|
|||
"name": "免费",
|
||||
"price": "$0",
|
||||
"period": "",
|
||||
"description": "全部功能。无限制,无需 API 密钥,无需信用卡。",
|
||||
"description": "从内置无需认证的免费模型开始。无需注册、API 密钥或信用卡。",
|
||||
"features": [
|
||||
"用于首次运行的无需认证的免费模型",
|
||||
"试用无需账号或提供商登录",
|
||||
"可选连接 Claude、Codex 和 OpenCode 提供商",
|
||||
"无限智能体团队",
|
||||
"实时更新的看板",
|
||||
"带 diff 查看的代码审查",
|
||||
"跨团队通信",
|
||||
"单人和团队模式",
|
||||
"实时进程监控",
|
||||
"内置代码编辑器",
|
||||
"MCP 集成"
|
||||
"单人和团队模式"
|
||||
],
|
||||
"highlighted": true
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,5 +12,19 @@
|
|||
.app-layout__main {
|
||||
flex: 1;
|
||||
padding-top: 64px;
|
||||
background:
|
||||
radial-gradient(circle at 78% 18%, rgba(0, 234, 255, 0.08), transparent 32%),
|
||||
radial-gradient(circle at 18% 72%, rgba(47, 125, 255, 0.07), transparent 38%),
|
||||
linear-gradient(180deg, #02050d 0%, #050814 58%, #02050d 100%);
|
||||
}
|
||||
|
||||
.v-theme--light .app-layout__main {
|
||||
background:
|
||||
radial-gradient(circle at 52% 0%, rgba(255, 43, 255, 0.16), transparent 30%),
|
||||
radial-gradient(circle at 64% 0%, rgba(0, 234, 255, 0.14), transparent 28%),
|
||||
radial-gradient(circle at 78% 24%, rgba(113, 185, 255, 0.28), transparent 31%),
|
||||
radial-gradient(circle at 22% 72%, rgba(221, 170, 255, 0.24), transparent 38%),
|
||||
radial-gradient(circle at 8% 32%, rgba(101, 218, 255, 0.18), transparent 34%),
|
||||
linear-gradient(180deg, #edf8ff 0%, #eaf7fb 48%, #fbf7ff 100%);
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -43,11 +43,12 @@
|
|||
"sectionSubtitle": "أدوات قوية تجعل التعاون متعدد الوكلاء يعمل فعلاً."
|
||||
},
|
||||
"pricing": {
|
||||
"sectionTitle": "مجاني 100%. بدون شروط.",
|
||||
"sectionSubtitle": "مفتوح المصدر، بدون مفاتيح API، بدون إعدادات. فقط ثبّت وابدأ.",
|
||||
"sectionTitle": "التثبيت مجاني. نموذج مجاني مضمّن.",
|
||||
"sectionSubtitle": "ابدأ فوراً بنموذج مجاني بدون مصادقة - بدون حساب أو مفتاح API أو بطاقة. وصّل Claude أو Codex أو OpenCode/OpenRouter أو مزودين آخرين فقط عندما تحتاج نماذج إضافية.",
|
||||
"getStarted": "حمّل الآن",
|
||||
"popular": "مجاني",
|
||||
"note": "مفتوح المصدر. بدون مفاتيح API. بدون إعدادات. يعمل محلياً بالكامل."
|
||||
"freeModelCallout": "نموذج مجاني بدون مصادقة مضمّن",
|
||||
"note": "لا يملك Agent Teams أي خطة مدفوعة. النموذج المجاني بدون مصادقة يتيح التجربة فوراً؛ استخدام المزودين المدفوعين اختياري ويتبع المزود الذي تختاره."
|
||||
},
|
||||
"testimonials": {
|
||||
"sectionTitle": "ماذا يقول المطورون",
|
||||
|
|
@ -112,7 +113,7 @@
|
|||
},
|
||||
"meta": {
|
||||
"homeTitle": "Agent Teams - تنسيق وكلاء الذكاء الاصطناعي للمطورين",
|
||||
"homeDescription": "تطبيق سطح مكتب مجاني ومفتوح المصدر لتشكيل فرق وكلاء الذكاء الاصطناعي. لوحة كانبان، مراجعة الكود، تواصل بين الفرق. يعمل محلياً بالكامل.",
|
||||
"homeDescription": "تطبيق سطح مكتب مجاني ومفتوح المصدر لفرق وكلاء الذكاء الاصطناعي. ابدأ بنموذج مجاني بدون مصادقة، ثم وصّل Claude أو Codex أو OpenCode عند الحاجة.",
|
||||
"downloadTitle": "تنزيل Agent Teams لنظام macOS وWindows وLinux",
|
||||
"downloadDescription": "نزّل Agent Teams لنظام macOS وWindows وLinux. تطبيق سطح مكتب مجاني ومفتوح المصدر لفرق وكلاء Claude وCodex وOpenCode."
|
||||
},
|
||||
|
|
|
|||
|
|
@ -43,11 +43,12 @@
|
|||
"sectionSubtitle": "Leistungsstarke Tools für effektive Multi-Agenten-Zusammenarbeit."
|
||||
},
|
||||
"pricing": {
|
||||
"sectionTitle": "100% Kostenlos. Ohne Haken.",
|
||||
"sectionSubtitle": "Open Source, keine API-Schlüssel, keine Konfiguration. Einfach installieren und loslegen.",
|
||||
"sectionTitle": "Kostenlos installieren. Kostenloses Modell inklusive.",
|
||||
"sectionSubtitle": "Starten Sie sofort mit einem kostenlosen Modell ohne Authentifizierung - ohne Konto, API-Schlüssel oder Kreditkarte. Verbinden Sie Claude, Codex, OpenCode/OpenRouter oder andere Provider nur, wenn Sie mehr Modelle möchten.",
|
||||
"getStarted": "Herunterladen",
|
||||
"popular": "Kostenlos",
|
||||
"note": "Open Source. Keine API-Schlüssel. Keine Konfiguration. Läuft vollständig lokal."
|
||||
"freeModelCallout": "Kostenloses Modell ohne Authentifizierung inklusive",
|
||||
"note": "Agent Teams hat keinen eigenen Bezahlplan. Mit dem kostenlosen Modell ohne Authentifizierung können Sie sofort testen; kostenpflichtige Provider-Nutzung ist optional und hängt vom gewählten Provider ab."
|
||||
},
|
||||
"testimonials": {
|
||||
"sectionTitle": "Was Entwickler sagen",
|
||||
|
|
@ -112,7 +113,7 @@
|
|||
},
|
||||
"meta": {
|
||||
"homeTitle": "Agent Teams - KI-Agenten-Orchestrierung für Entwickler",
|
||||
"homeDescription": "Kostenlose Open-Source-Desktop-App für KI-Agenten-Teams. Kanban-Board, Code-Review, teamübergreifende Kommunikation. Läuft vollständig lokal.",
|
||||
"homeDescription": "Kostenlose Open-Source-Desktop-App für KI-Agenten-Teams. Starten Sie mit einem kostenlosen Modell ohne Authentifizierung und verbinden Sie Claude, Codex oder OpenCode bei Bedarf.",
|
||||
"downloadTitle": "Agent Teams für macOS, Windows und Linux herunterladen",
|
||||
"downloadDescription": "Laden Sie Agent Teams für macOS, Windows und Linux herunter. Kostenlose Open-Source-Desktop-App für Claude-, Codex- und OpenCode-Agententeams."
|
||||
},
|
||||
|
|
|
|||
|
|
@ -43,11 +43,12 @@
|
|||
"sectionSubtitle": "Powerful tools that make multi-agent collaboration actually work."
|
||||
},
|
||||
"pricing": {
|
||||
"sectionTitle": "100% Free. No strings attached.",
|
||||
"sectionSubtitle": "Open source, no API keys, no configuration. Just install and go.",
|
||||
"sectionTitle": "Free to install. Free model included.",
|
||||
"sectionSubtitle": "Start immediately with a free model with no auth - no account, API key, or credit card. Connect Claude, Codex, OpenCode/OpenRouter, or other provider access only when you want more models.",
|
||||
"getStarted": "Download Now",
|
||||
"popular": "Free",
|
||||
"note": "Open source. No API keys. No configuration. Runs entirely locally."
|
||||
"freeModelCallout": "Free model with no auth included",
|
||||
"note": "Agent Teams has no paid tier. The free model with no auth lets you try it right away; paid provider usage is optional and controlled by the provider you choose."
|
||||
},
|
||||
"testimonials": {
|
||||
"sectionTitle": "What developers say",
|
||||
|
|
@ -112,7 +113,7 @@
|
|||
},
|
||||
"meta": {
|
||||
"homeTitle": "Agent Teams - AI Agent Orchestration for Developers",
|
||||
"homeDescription": "Free, open-source desktop app for assembling AI agent teams. Kanban board, code review, cross-team communication. Runs entirely locally.",
|
||||
"homeDescription": "Free, open-source desktop app for AI agent teams. Start with a free model with no auth, then connect Claude, Codex, or OpenCode when you need more models.",
|
||||
"downloadTitle": "Download Agent Teams for macOS, Windows, and Linux",
|
||||
"downloadDescription": "Download Agent Teams for macOS, Windows, and Linux. Free open-source desktop app for Claude, Codex, and OpenCode agent teams."
|
||||
},
|
||||
|
|
|
|||
|
|
@ -43,11 +43,12 @@
|
|||
"sectionSubtitle": "Herramientas potentes que hacen que la colaboración multi-agente realmente funcione."
|
||||
},
|
||||
"pricing": {
|
||||
"sectionTitle": "100% Gratis. Sin letra pequeña.",
|
||||
"sectionSubtitle": "Código abierto, sin claves API, sin configuración. Instala y empieza.",
|
||||
"sectionTitle": "Gratis para instalar. Modelo gratis incluido.",
|
||||
"sectionSubtitle": "Empieza al instante con un modelo gratuito sin autenticación - sin cuenta, clave API ni tarjeta. Conecta Claude, Codex, OpenCode/OpenRouter u otros proveedores solo si quieres más modelos.",
|
||||
"getStarted": "Descargar ahora",
|
||||
"popular": "Gratis",
|
||||
"note": "Código abierto. Sin claves API. Sin configuración. Funciona completamente en local."
|
||||
"freeModelCallout": "Modelo gratuito sin autenticación incluido",
|
||||
"note": "Agent Teams no tiene plan de pago propio. El modelo gratuito sin autenticación te permite probarlo de inmediato; el uso de proveedores de pago es opcional y depende del proveedor que elijas."
|
||||
},
|
||||
"testimonials": {
|
||||
"sectionTitle": "Lo que dicen los desarrolladores",
|
||||
|
|
@ -112,7 +113,7 @@
|
|||
},
|
||||
"meta": {
|
||||
"homeTitle": "Agent Teams - Orquestación de agentes IA para desarrolladores",
|
||||
"homeDescription": "App de escritorio gratuita y de código abierto para montar equipos de agentes IA. Tablero kanban, revisión de código, comunicación entre equipos. Funciona completamente en local.",
|
||||
"homeDescription": "App de escritorio gratuita y open source para equipos de agentes IA. Empieza con un modelo gratuito sin autenticación, y conecta Claude, Codex u OpenCode cuando necesites más modelos.",
|
||||
"downloadTitle": "Descargar Agent Teams para macOS, Windows y Linux",
|
||||
"downloadDescription": "Descarga Agent Teams para macOS, Windows y Linux. App de escritorio gratis y open source para equipos de agentes Claude, Codex y OpenCode."
|
||||
},
|
||||
|
|
|
|||
|
|
@ -43,11 +43,12 @@
|
|||
"sectionSubtitle": "Des outils puissants pour une collaboration multi-agents efficace."
|
||||
},
|
||||
"pricing": {
|
||||
"sectionTitle": "100% Gratuit. Sans conditions.",
|
||||
"sectionSubtitle": "Open source, sans clé API, sans configuration. Installez et c'est parti.",
|
||||
"sectionTitle": "Installation gratuite. Modèle gratuit inclus.",
|
||||
"sectionSubtitle": "Commencez immédiatement avec un modèle gratuit sans authentification - sans compte, clé API ni carte bancaire. Connectez Claude, Codex, OpenCode/OpenRouter ou un autre provider seulement si vous voulez plus de modèles.",
|
||||
"getStarted": "Télécharger",
|
||||
"popular": "Gratuit",
|
||||
"note": "Open source. Sans clé API. Sans configuration. Fonctionne entièrement en local."
|
||||
"freeModelCallout": "Modèle gratuit sans authentification inclus",
|
||||
"note": "Agent Teams n'a pas d'offre payante. Le modèle gratuit sans authentification permet d'essayer tout de suite; l'usage de providers payants est optionnel et dépend du provider choisi."
|
||||
},
|
||||
"testimonials": {
|
||||
"sectionTitle": "Ce que disent les développeurs",
|
||||
|
|
@ -112,7 +113,7 @@
|
|||
},
|
||||
"meta": {
|
||||
"homeTitle": "Agent Teams - Orchestration d'agents IA pour développeurs",
|
||||
"homeDescription": "Application desktop gratuite et open source pour gérer des équipes d'agents IA. Tableau kanban, revue de code, communication inter-équipes. Fonctionne entièrement en local.",
|
||||
"homeDescription": "Application desktop gratuite et open source pour équipes d'agents IA. Commencez avec un modèle gratuit sans authentification, puis connectez Claude, Codex ou OpenCode si besoin.",
|
||||
"downloadTitle": "Télécharger Agent Teams pour macOS, Windows et Linux",
|
||||
"downloadDescription": "Téléchargez Agent Teams pour macOS, Windows et Linux. Application desktop gratuite et open source pour équipes d'agents Claude, Codex et OpenCode."
|
||||
},
|
||||
|
|
|
|||
|
|
@ -43,11 +43,12 @@
|
|||
"sectionSubtitle": "शक्तिशाली उपकरण जो मल्टी-एजेंट सहयोग को वास्तव में काम करते हैं।"
|
||||
},
|
||||
"pricing": {
|
||||
"sectionTitle": "100% मुफ़्त। कोई शर्त नहीं।",
|
||||
"sectionSubtitle": "ओपन सोर्स, कोई API कुंजी नहीं, कोई कॉन्फ़िगरेशन नहीं। बस इंस्टॉल करें और शुरू करें।",
|
||||
"sectionTitle": "इंस्टॉल मुफ़्त। मुफ़्त मॉडल शामिल।",
|
||||
"sectionSubtitle": "मुफ़्त no-auth model से तुरंत शुरू करें - कोई account, API key या credit card नहीं। Claude, Codex, OpenCode/OpenRouter या अन्य providers तभी जोड़ें जब आपको और models चाहिए।",
|
||||
"getStarted": "अभी डाउनलोड करें",
|
||||
"popular": "मुफ़्त",
|
||||
"note": "ओपन सोर्स। कोई API कुंजी नहीं। कोई कॉन्फ़िगरेशन नहीं। पूरी तरह लोकल चलता है।"
|
||||
"freeModelCallout": "मुफ़्त no-auth model शामिल",
|
||||
"note": "Agent Teams का अपना कोई paid plan नहीं है। मुफ़्त no-auth model से आप तुरंत try कर सकते हैं; paid provider usage optional है और चुने गए provider पर निर्भर है।"
|
||||
},
|
||||
"testimonials": {
|
||||
"sectionTitle": "डेवलपर्स क्या कहते हैं",
|
||||
|
|
@ -112,7 +113,7 @@
|
|||
},
|
||||
"meta": {
|
||||
"homeTitle": "Agent Teams - डेवलपर्स के लिए AI एजेंट ऑर्केस्ट्रेशन",
|
||||
"homeDescription": "AI एजेंट टीमें बनाने के लिए मुफ़्त, ओपन-सोर्स डेस्कटॉप ऐप। कानबन बोर्ड, कोड रिव्यू, क्रॉस-टीम संचार। पूरी तरह लोकल चलता है।",
|
||||
"homeDescription": "AI agent teams के लिए मुफ़्त open-source desktop app। मुफ़्त no-auth model से शुरू करें, फिर ज़रूरत पर Claude, Codex या OpenCode जोड़ें।",
|
||||
"downloadTitle": "macOS, Windows और Linux के लिए Agent Teams डाउनलोड करें",
|
||||
"downloadDescription": "macOS, Windows और Linux के लिए Agent Teams डाउनलोड करें। Claude, Codex और OpenCode एजेंट टीमों के लिए मुफ़्त ओपन-सोर्स डेस्कटॉप ऐप।"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -43,11 +43,12 @@
|
|||
"sectionSubtitle": "マルチエージェント連携を実現する強力なツール。"
|
||||
},
|
||||
"pricing": {
|
||||
"sectionTitle": "100%無料。制約なし。",
|
||||
"sectionSubtitle": "オープンソース、APIキー不要、設定不要。インストールするだけ。",
|
||||
"sectionTitle": "インストール無料。無料モデル付き。",
|
||||
"sectionSubtitle": "認証なしの無料モデルですぐに開始できます - アカウント、APIキー、クレジットカードは不要。追加モデルが必要な時だけ Claude、Codex、OpenCode/OpenRouter などを接続できます。",
|
||||
"getStarted": "ダウンロード",
|
||||
"popular": "無料",
|
||||
"note": "オープンソース。APIキー不要。設定不要。完全にローカルで動作。"
|
||||
"freeModelCallout": "認証なしの無料モデルが含まれています",
|
||||
"note": "Agent Teams 自体に有料プランはありません。認証なしの無料モデルですぐに試せます。有料 provider の利用は任意で、選択した provider の条件に従います。"
|
||||
},
|
||||
"testimonials": {
|
||||
"sectionTitle": "開発者の声",
|
||||
|
|
@ -112,7 +113,7 @@
|
|||
},
|
||||
"meta": {
|
||||
"homeTitle": "Agent Teams - 開発者向けAIエージェントオーケストレーション",
|
||||
"homeDescription": "AIエージェントチームを管理する無料のオープンソースデスクトップアプリ。カンバンボード、コードレビュー、チーム間通信。完全にローカルで動作。",
|
||||
"homeDescription": "AIエージェントチーム向けの無料オープンソースデスクトップアプリ。認証なしの無料モデルから始め、必要に応じて Claude、Codex、OpenCode を接続できます。",
|
||||
"downloadTitle": "macOS、Windows、Linux向けAgent Teamsをダウンロード",
|
||||
"downloadDescription": "macOS、Windows、Linux向けAgent Teamsをダウンロード。Claude、Codex、OpenCodeエージェントチーム用の無料オープンソースデスクトップアプリ。"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -43,11 +43,12 @@
|
|||
"sectionSubtitle": "Ferramentas poderosas que fazem a colaboração multi-agente realmente funcionar."
|
||||
},
|
||||
"pricing": {
|
||||
"sectionTitle": "100% Grátis. Sem pegadinhas.",
|
||||
"sectionSubtitle": "Código aberto, sem chaves de API, sem configuração. Instale e comece.",
|
||||
"sectionTitle": "Grátis para instalar. Modelo grátis incluído.",
|
||||
"sectionSubtitle": "Comece na hora com um modelo gratuito sem autenticação - sem conta, chave de API ou cartão. Conecte Claude, Codex, OpenCode/OpenRouter ou outros provedores só quando quiser mais modelos.",
|
||||
"getStarted": "Baixar agora",
|
||||
"popular": "Grátis",
|
||||
"note": "Código aberto. Sem chaves de API. Sem configuração. Roda totalmente local."
|
||||
"freeModelCallout": "Modelo gratuito sem autenticação incluído",
|
||||
"note": "Agent Teams não tem plano pago próprio. O modelo gratuito sem autenticação permite testar imediatamente; uso de provedores pagos é opcional e depende do provedor escolhido."
|
||||
},
|
||||
"testimonials": {
|
||||
"sectionTitle": "O que os desenvolvedores dizem",
|
||||
|
|
@ -112,7 +113,7 @@
|
|||
},
|
||||
"meta": {
|
||||
"homeTitle": "Agent Teams - Orquestração de agentes IA para desenvolvedores",
|
||||
"homeDescription": "App desktop gratuito e open source para montar equipes de agentes IA. Quadro kanban, revisão de código, comunicação entre equipes. Roda totalmente local.",
|
||||
"homeDescription": "App desktop gratuito e open source para equipes de agentes IA. Comece com um modelo gratuito sem autenticação, depois conecte Claude, Codex ou OpenCode quando precisar.",
|
||||
"downloadTitle": "Baixar Agent Teams para macOS, Windows e Linux",
|
||||
"downloadDescription": "Baixe o Agent Teams para macOS, Windows e Linux. App desktop gratuito e open source para equipes de agentes Claude, Codex e OpenCode."
|
||||
},
|
||||
|
|
|
|||
|
|
@ -43,11 +43,12 @@
|
|||
"sectionSubtitle": "Мощные инструменты, которые делают мультиагентную совместную работу реальностью."
|
||||
},
|
||||
"pricing": {
|
||||
"sectionTitle": "100% Бесплатно. Без подвоха.",
|
||||
"sectionSubtitle": "Открытый код, без API-ключей, без конфигурации. Просто установите и работайте.",
|
||||
"sectionTitle": "Бесплатно установить. Бесплатная модель уже внутри.",
|
||||
"sectionSubtitle": "Можно сразу начать с бесплатной модели без авторизации - без регистрации, API-ключей и карты. Claude, Codex, OpenCode/OpenRouter и другие провайдеры подключайте только если нужны дополнительные модели.",
|
||||
"getStarted": "Скачать",
|
||||
"popular": "Бесплатно",
|
||||
"note": "Открытый код. Без API-ключей. Без конфигурации. Работает полностью локально."
|
||||
"freeModelCallout": "Бесплатная модель без авторизации включена",
|
||||
"note": "У Agent Teams нет платного тарифа. Бесплатная модель без авторизации даёт попробовать сразу; платные провайдеры опциональны и зависят от выбранного сервиса."
|
||||
},
|
||||
"testimonials": {
|
||||
"sectionTitle": "Что говорят разработчики",
|
||||
|
|
@ -112,7 +113,7 @@
|
|||
},
|
||||
"meta": {
|
||||
"homeTitle": "Agent Teams - Оркестрация ИИ-агентов для разработчиков",
|
||||
"homeDescription": "Бесплатное десктопное приложение с открытым кодом для управления командами ИИ-агентов. Канбан-доска, код-ревью, межкомандная коммуникация. Работает полностью локально.",
|
||||
"homeDescription": "Бесплатное open-source desktop-приложение для команд ИИ-агентов. Начните с бесплатной модели без авторизации, а Claude, Codex или OpenCode подключайте когда нужны дополнительные модели.",
|
||||
"downloadTitle": "Скачать Agent Teams для macOS, Windows и Linux",
|
||||
"downloadDescription": "Скачайте Agent Teams для macOS, Windows и Linux. Бесплатное open-source приложение для команд агентов Claude, Codex и OpenCode."
|
||||
},
|
||||
|
|
|
|||
|
|
@ -43,11 +43,12 @@
|
|||
"sectionSubtitle": "强大的工具,让多智能体协作真正有效。"
|
||||
},
|
||||
"pricing": {
|
||||
"sectionTitle": "100% 免费,没有附加条件。",
|
||||
"sectionSubtitle": "开源,无需 API 密钥,无需配置。安装即用。",
|
||||
"sectionTitle": "免费安装。内置免费模型。",
|
||||
"sectionSubtitle": "可立即使用无需认证的免费模型 - 无需账号、API 密钥或信用卡。只有在需要更多模型时,才连接 Claude、Codex、OpenCode/OpenRouter 或其他提供商。",
|
||||
"getStarted": "立即下载",
|
||||
"popular": "免费",
|
||||
"note": "开源。无需 API 密钥。无需配置。完全本地运行。"
|
||||
"freeModelCallout": "已包含无需认证的免费模型",
|
||||
"note": "Agent Teams 没有自己的付费套餐。无需认证的免费模型可让你立即试用;付费提供商的使用是可选的,并由你选择的提供商控制。"
|
||||
},
|
||||
"testimonials": {
|
||||
"sectionTitle": "开发者怎么说",
|
||||
|
|
@ -112,7 +113,7 @@
|
|||
},
|
||||
"meta": {
|
||||
"homeTitle": "Agent Teams - 面向开发者的 AI 智能体编排",
|
||||
"homeDescription": "免费开源桌面应用,用于组建 AI 智能体团队。看板、代码审查、跨团队通信。完全本地运行。",
|
||||
"homeDescription": "面向 AI 智能体团队的免费开源桌面应用。先使用无需认证的免费模型,需要更多模型时再连接 Claude、Codex 或 OpenCode。",
|
||||
"downloadTitle": "下载适用于 macOS、Windows 和 Linux 的 Agent Teams",
|
||||
"downloadDescription": "下载适用于 macOS、Windows 和 Linux 的 Agent Teams。面向 Claude、Codex 和 OpenCode 智能体团队的免费开源桌面应用。"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ Agent Teams is distributed as a desktop app for macOS, Windows, and Linux.
|
|||
|
||||
::: tip Shortest path
|
||||
1. Download the build for your platform below
|
||||
2. Launch the app — it detects runtimes and guides provider auth from the UI
|
||||
2. Launch the app - start with the free model with no auth or connect provider auth from the UI
|
||||
3. Start the [quickstart](/guide/quickstart) to create your first team
|
||||
|
||||
Desktop app startup: run `pnpm dev` for the Electron app. Do not start the browser/web dev mode for normal use.
|
||||
|
|
@ -30,16 +30,16 @@ Unsigned or newly published open-source apps can trigger SmartScreen. If you tru
|
|||
|
||||
## Requirements
|
||||
|
||||
The packaged app is designed for zero-setup onboarding. It guides you through runtime detection and provider authentication from the UI — no manual CLI configuration needed.
|
||||
The packaged app is designed for zero-setup onboarding. You can start with the free model with no auth - no registration, API keys, or credit card. If you want more models, the app guides runtime detection and provider authentication from the UI.
|
||||
|
||||
To use agent runtimes, you need access to at least one provider:
|
||||
For paid or account-backed models, connect at least one provider:
|
||||
|
||||
| Provider | Access method |
|
||||
| ------------------ | ------------------------------------------------- |
|
||||
| Claude (Anthropic) | Claude Code CLI login or API key |
|
||||
| Codex (OpenAI) | Codex CLI login or API key |
|
||||
| Gemini (Google) | Google ADC, Gemini CLI, or API key |
|
||||
| OpenCode | API key for a supported backend (e.g. OpenRouter) |
|
||||
| OpenCode | Included free model with no auth, or API key for a supported backend (e.g. OpenRouter) |
|
||||
|
||||
::: info
|
||||
Gemini is available as a supported provider path. See [Providers and runtimes](/reference/providers-runtimes) for auth options and current status across all providers.
|
||||
|
|
|
|||
|
|
@ -47,7 +47,7 @@ For project conventions and architecture guidance, refer to these canonical file
|
|||
|
||||
## 1. Run from source or download
|
||||
|
||||
**Download the packaged app** for macOS, Windows, or Linux from the <a href="/download/" target="_self">download page</a> — no prerequisites needed. The app guides runtime detection and provider authentication from the UI.
|
||||
**Download the packaged app** for macOS, Windows, or Linux from the <a href="/download/" target="_self">download page</a> - no prerequisites needed. Start with the free model with no auth, or connect provider auth from the UI when you want more models.
|
||||
|
||||
**Or run from source** for development:
|
||||
|
||||
|
|
@ -84,7 +84,7 @@ The setup flow auto-detects installed runtimes on your machine. A common first s
|
|||
| -------- | ----------------------------------------------- |
|
||||
| Claude | Claude Code users and existing Anthropic access |
|
||||
| Codex | Codex-native workflows and OpenAI access |
|
||||
| OpenCode | Multi-model teams and many provider backends |
|
||||
| OpenCode | Free model with no auth, multi-model teams, and many provider backends |
|
||||
|
||||
::: info
|
||||
Gemini is available as a supported provider path. See [Providers and runtimes](/reference/providers-runtimes) for auth options and current provider status.
|
||||
|
|
@ -92,7 +92,7 @@ Gemini is available as a supported provider path. See [Providers and runtimes](/
|
|||
|
||||
See [Runtime setup](/guide/runtime-setup) for detailed configuration per provider.
|
||||
|
||||
To verify the selected runtime outside the app, check the binary and test auth:
|
||||
To verify a paid or account-backed runtime outside the app, check the binary and test auth:
|
||||
|
||||
```bash
|
||||
# Check that the runtime is installed and on PATH
|
||||
|
|
@ -101,7 +101,7 @@ command -v codex && codex --version
|
|||
command -v opencode && opencode --version
|
||||
```
|
||||
|
||||
If the command fails, fix the runtime installation or `PATH` first. Team prompts cannot work around a missing binary or missing provider auth.
|
||||
If the command fails, fix the runtime installation or `PATH` first. Team prompts cannot work around a missing binary or missing provider auth for models that require it.
|
||||
|
||||
::: tip
|
||||
If the binary is found but the app reports "not logged in", the environment may differ between your terminal and the app. See the [auth diagnostic log](/guide/troubleshooting#auth-diagnostic-log) to compare them.
|
||||
|
|
@ -162,7 +162,7 @@ Before approving the first task, check three things:
|
|||
| Symptom | Likely cause | Check |
|
||||
| --- | --- | --- |
|
||||
| App does not detect a runtime | Binary not on `PATH`, or app and terminal see different environments | Run `command -v <runtime>` in a terminal, then use the same terminal env to launch the app |
|
||||
| Team launch hangs | Missing provider auth, wrong model string, or runtime binary not found | See [Troubleshooting](/guide/troubleshooting#team-does-not-launch) |
|
||||
| Team launch hangs | Missing provider auth for a paid/account model, wrong model string, or runtime binary not found | See [Troubleshooting](/guide/troubleshooting#team-does-not-launch) |
|
||||
| OpenCode lane stuck on `registered` | Lane evidence not committed yet, or model string mismatch | Inspect `~/.claude/teams/<team>/.opencode-runtime/lanes/` |
|
||||
| Agent replies missing | Runtime delivery retry, parsing, or task attribution issue | Open task logs and check the delivery ledger |
|
||||
| Provider returns 429s | Rate limit reached | Wait for reset or switch model/provider |
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ Agent Teams is a coordination layer. The actual model work runs through supporte
|
|||
| --- | --- |
|
||||
| Already use Claude Code or have Anthropic access | **Claude** - familiar auth, minimal setup |
|
||||
| Use Codex or OpenAI-based workflows | **Codex** - native integration |
|
||||
| Want to try Agent Teams without signup or API keys | **OpenCode** - use the included free model with no auth |
|
||||
| Want multi-model routing or broad provider coverage | **OpenCode** - most flexible, one config for many backends |
|
||||
| Are not sure which runtime fits | **OpenCode** - covers the most provider options and lets you switch later |
|
||||
|
||||
|
|
@ -23,7 +24,7 @@ Start with one runtime and one teammate. Confirm one launch works before expandi
|
|||
Before launching a team, make sure:
|
||||
|
||||
- The runtime binary is installed and on your `PATH`.
|
||||
- Your provider account has active access to the model you intend to use.
|
||||
- Your provider account has active access to the model you intend to use, unless you start with the included free OpenCode model with no auth.
|
||||
- The project path exists and is readable.
|
||||
- The app and your terminal use the same home/config environment when you test auth manually.
|
||||
|
||||
|
|
@ -55,10 +56,10 @@ Gemini is available as a supported provider path with Google ADC (`gcloud auth`)
|
|||
|
||||
## Provider access
|
||||
|
||||
Agent Teams has no paid tier of its own. You bring the provider access you already have: subscriptions, local runtime auth, or API keys depending on the path you choose.
|
||||
Agent Teams has no paid tier of its own. You can start with the included free OpenCode model with no auth - no registration, API keys, or credit card. For additional models, bring the provider access you already have: subscriptions, local runtime auth, or API keys depending on the path you choose.
|
||||
|
||||
- **Claude** and **Codex** paths rely on their respective CLI auth tools.
|
||||
- **OpenCode** needs provider-specific API keys in a config file (e.g., `openrouter`, `openai`, `anthropic`).
|
||||
- **OpenCode** can run the included free model with no auth first. Other OpenCode models may need provider-specific API keys in a config file (e.g., `openrouter`, `openai`, `anthropic`).
|
||||
|
||||
## Auth configuration
|
||||
|
||||
|
|
@ -96,7 +97,7 @@ Codex-native launches use Codex account state and model catalog data when availa
|
|||
|
||||
### OpenCode
|
||||
|
||||
Create or edit `~/.opencode/config.json` (or the equivalent path on your platform) with the provider key you want:
|
||||
To use the included free model with no auth, select it in the app and launch without provider signup. To use other OpenCode backends, create or edit `~/.opencode/config.json` (or the equivalent path on your platform) with the provider key you want:
|
||||
|
||||
```json
|
||||
{
|
||||
|
|
|
|||
|
|
@ -88,7 +88,7 @@ Recommended patterns:
|
|||
|
||||
## Provider costs
|
||||
|
||||
Agent Teams is free and open source. Provider usage is governed by the runtime/provider you select: subscription limits, API keys, account auth, rate limits, and provider policies all remain external to the app.
|
||||
Agent Teams is free and open source. You can start with the included free model with no auth - no registration, API keys, or credit card. Paid or account-backed provider usage is governed by the runtime/provider you select: subscription limits, API keys, account auth, rate limits, and provider policies all remain external to the app.
|
||||
|
||||
## Capability checks
|
||||
|
||||
|
|
|
|||
|
|
@ -23,16 +23,16 @@ Agent Teams распространяется как desktop-приложение
|
|||
|
||||
## Требования
|
||||
|
||||
Пакетная сборка рассчитана на zero-setup onboarding. Приложение само помогает с runtime detection и provider authentication — ручная настройка CLI не нужна.
|
||||
Пакетная сборка рассчитана на zero-setup onboarding. Можно начать с бесплатной модели без авторизации - без регистрации, API-ключей и карты. Если нужны дополнительные модели, приложение само помогает с runtime detection и provider authentication.
|
||||
|
||||
Для работы агентных рантаймов нужен доступ хотя бы к одному провайдеру:
|
||||
Для платных или account-backed моделей подключите хотя бы один провайдер:
|
||||
|
||||
| Провайдер | Способ доступа |
|
||||
| ------------------ | ---------------------------------------------------------- |
|
||||
| Claude (Anthropic) | Claude Code CLI login или API key |
|
||||
| Codex (OpenAI) | Codex CLI login или API key |
|
||||
| Gemini (Google) | Google ADC, Gemini CLI или API key |
|
||||
| OpenCode | API key для поддерживаемого бэкенда (например, OpenRouter) |
|
||||
| OpenCode | Встроенная бесплатная модель без авторизации или API key для поддерживаемого бэкенда (например, OpenRouter) |
|
||||
|
||||
::: info
|
||||
Gemini — поддерживаемый провайдер. Варианты auth смотрите в разделе [Провайдеры и рантаймы](/ru/reference/providers-runtimes).
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ lang: ru-RU
|
|||
|
||||
- **macOS, Windows или Linux** машина
|
||||
- **Git-репозиторий** в качестве проекта (рекомендуется для diff review и worktree isolation)
|
||||
- Доступ хотя бы к одному провайдеру: Anthropic (Claude), OpenAI (Codex), OpenRouter (OpenCode) или Google (Gemini)
|
||||
- Бесплатная модель без авторизации для первого запуска или доступ к провайдеру, если нужны дополнительные модели: Anthropic (Claude), OpenAI (Codex), OpenRouter (OpenCode) или Google (Gemini)
|
||||
- Node.js 20+ и pnpm 10+ при запуске из исходников
|
||||
|
||||
Подробности и ссылки для скачивания — в разделе [Установка](/ru/guide/installation).
|
||||
|
|
@ -24,7 +24,7 @@ lang: ru-RU
|
|||
Скачайте последний релиз под вашу платформу на <a href="/ru/download/" target="_self">странице загрузок</a> или в [GitHub releases](https://github.com/777genius/agent-teams-ai/releases).
|
||||
|
||||
::: tip
|
||||
Приложение бесплатное и с открытым кодом. Выбранный runtime может требовать доступ к провайдеру — подробности в разделе [Установка](/ru/guide/installation).
|
||||
Приложение бесплатное и с открытым кодом. Можно начать с бесплатной модели без авторизации - без регистрации; дополнительные runtime/provider paths могут требовать доступ к провайдеру. Подробности в разделе [Установка](/ru/guide/installation).
|
||||
:::
|
||||
|
||||
::: info
|
||||
|
|
@ -55,7 +55,7 @@ git status --short
|
|||
| -------- | ------------------------------------------------------------------- |
|
||||
| Claude | Если вы уже используете Claude Code или у вас есть Anthropic access |
|
||||
| Codex | Для Codex-native workflows и OpenAI access |
|
||||
| OpenCode | Для multi-model команд и большого числа provider backends |
|
||||
| OpenCode | Бесплатная модель без авторизации, multi-model команды и много provider backends |
|
||||
|
||||
::: info
|
||||
Gemini — поддерживаемый провайдер. Варианты auth смотрите в разделе [Провайдеры и рантаймы](/ru/reference/providers-runtimes).
|
||||
|
|
@ -63,7 +63,7 @@ Gemini — поддерживаемый провайдер. Варианты aut
|
|||
|
||||
Подробная настройка каждого провайдера — в разделе [Настройка рантайма](/ru/guide/runtime-setup).
|
||||
|
||||
Чтобы проверить выбранный runtime вне приложения, запустите соответствующую команду версии:
|
||||
Чтобы проверить платный или account-backed runtime вне приложения, запустите соответствующую команду версии:
|
||||
|
||||
```bash
|
||||
claude --version
|
||||
|
|
@ -71,7 +71,7 @@ codex --version
|
|||
opencode --version
|
||||
```
|
||||
|
||||
Если команда падает в терминале, сначала исправьте runtime installation или `PATH`. Team prompts не смогут компенсировать отсутствующий binary или provider auth.
|
||||
Если команда падает в терминале, сначала исправьте runtime installation или `PATH`. Team prompts не смогут компенсировать отсутствующий binary или provider auth для моделей, которым он нужен.
|
||||
|
||||
Также можно проверить, что бинарник доступен в `PATH`:
|
||||
|
||||
|
|
@ -134,7 +134,7 @@ Lead создаёт задачи, назначает работу и коорд
|
|||
| Симптом | Вероятная причина | Что проверить |
|
||||
| --- | --- | --- |
|
||||
| Приложение не видит runtime | Бинарник не в `PATH` или разные окружения у приложения и терминала | Запустите `command -v <runtime>` в терминале |
|
||||
| Запуск команды зависает | Нет provider auth, неверная модель или runtime не найден | Раздел [Диагностика](/ru/guide/troubleshooting#team-does-not-launch) |
|
||||
| Запуск команды зависает | Нет provider auth для платной/account модели, неверная модель или runtime не найден | Раздел [Диагностика](/ru/guide/troubleshooting#team-does-not-launch) |
|
||||
| OpenCode lane в статусе `registered` | Lane evidence ещё не зафиксирован или несовпадение модели | Проверьте `~/.claude/teams/<team>/.opencode-runtime/lanes/` |
|
||||
| Ответы агента не приходят | Runtime delivery retry, parsing или task attribution | Откройте task logs и проверьте delivery ledger |
|
||||
| Провайдер возвращает 429 | Достигнут лимит запросов | Дождитесь сброса или смените модель/провайдера |
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ Agent Teams - координационный слой. Работа моделе
|
|||
Перед запуском команды убедитесь, что:
|
||||
|
||||
- Runtime binary установлен и находится в `PATH`.
|
||||
- Ваш аккаунт провайдера имеет доступ к выбранной модели.
|
||||
- Ваш аккаунт провайдера имеет доступ к выбранной модели, если вы не начинаете со встроенной OpenCode-модели без авторизации.
|
||||
- Путь к проекту существует и доступен для чтения.
|
||||
- Приложение и терминал используют одинаковое home/config окружение, когда вы вручную проверяете auth.
|
||||
|
||||
|
|
@ -45,10 +45,10 @@ Gemini — поддерживаемый провайдер с Google ADC (`gclou
|
|||
|
||||
## Доступ к провайдеру
|
||||
|
||||
У Agent Teams нет своего платного тарифа. Вы используете доступ к провайдеру, который у вас уже есть: подписка, локальная авторизация рантайма или API-ключи в зависимости от выбранного пути.
|
||||
У Agent Teams нет своего платного тарифа. Можно начать со встроенной OpenCode-модели без авторизации - без регистрации, API-ключей и карты. Для дополнительных моделей используйте доступ к провайдеру, который у вас уже есть: подписка, локальная авторизация рантайма или API-ключи в зависимости от выбранного пути.
|
||||
|
||||
- Для **Claude** и **Codex** используется auth соответствующего CLI.
|
||||
- Для **OpenCode** требуются provider-specific API keys в файле конфигурации (например, `openrouter`, `openai`, `anthropic`).
|
||||
- **OpenCode** может сначала работать через встроенную бесплатную модель без авторизации. Другие OpenCode-модели могут требовать provider-specific API keys в файле конфигурации (например, `openrouter`, `openai`, `anthropic`).
|
||||
|
||||
## Настройка авторизации
|
||||
|
||||
|
|
@ -86,7 +86,7 @@ Codex-native launches используют Codex account state и model catalog
|
|||
|
||||
### OpenCode
|
||||
|
||||
Создайте или отредактируйте `~/.opencode/config.json` (или эквивалентный путь на вашей платформе):
|
||||
Для встроенной бесплатной модели без авторизации достаточно выбрать её в приложении и запустить без регистрации у провайдера. Для других OpenCode backend создайте или отредактируйте `~/.opencode/config.json` (или эквивалентный путь на вашей платформе):
|
||||
|
||||
```json
|
||||
{
|
||||
|
|
|
|||
|
|
@ -89,7 +89,7 @@ Contributor-facing границы и canonical implementation guidance смот
|
|||
|
||||
## Стоимость providers
|
||||
|
||||
Agent Teams бесплатен и open source. Provider usage зависит от выбранного runtime/provider: subscription limits, API keys, account auth, rate limits и provider policies остаются внешними для приложения.
|
||||
Agent Teams бесплатен и open source. Можно начать со встроенной бесплатной модели без авторизации - без регистрации, API-ключей и карты. Платный или account-backed provider usage зависит от выбранного runtime/provider: subscription limits, API keys, account auth, rate limits и provider policies остаются внешними для приложения.
|
||||
|
||||
## Capability checks
|
||||
|
||||
|
|
|
|||
|
|
@ -9,13 +9,16 @@ import { buildMergedCliPath } from '@main/utils/cliPathMerge';
|
|||
import { getCachedShellEnv } from '@main/utils/shellEnv';
|
||||
|
||||
const CACHE_VERIFY_TTL_MS = 30_000;
|
||||
const STALE_POSITIVE_CACHE_TTL_MS = 5 * 60_000;
|
||||
const VERSION_CACHE_TTL_MS = 30_000;
|
||||
const BINARY_LAUNCH_VERIFY_TIMEOUT_MS = 3_000;
|
||||
|
||||
let cachedBinaryPath: string | null | undefined;
|
||||
let cacheVerifiedAt = 0;
|
||||
let cacheLaunchVerifiedAt = 0;
|
||||
let resolveInFlight: Promise<string | null> | null = null;
|
||||
let cachedMissHadShellEnv = false;
|
||||
let cachedPositiveIsStale = false;
|
||||
const versionCache = new Map<string, { version: string | null; observedAt: number }>();
|
||||
|
||||
async function fileExists(filePath: string): Promise<boolean> {
|
||||
|
|
@ -117,29 +120,54 @@ async function verifyBinary(candidate: string): Promise<string | null> {
|
|||
return null;
|
||||
}
|
||||
|
||||
async function canReuseStalePositiveBinary(
|
||||
candidate: string | null,
|
||||
launchVerifiedAt: number
|
||||
): Promise<boolean> {
|
||||
if (
|
||||
!candidate ||
|
||||
launchVerifiedAt <= 0 ||
|
||||
Date.now() - launchVerifiedAt > STALE_POSITIVE_CACHE_TTL_MS
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return fileExists(candidate);
|
||||
}
|
||||
|
||||
export class CodexBinaryResolver {
|
||||
static clearCache(): void {
|
||||
cachedBinaryPath = undefined;
|
||||
cacheVerifiedAt = 0;
|
||||
cacheLaunchVerifiedAt = 0;
|
||||
resolveInFlight = null;
|
||||
cachedMissHadShellEnv = false;
|
||||
cachedPositiveIsStale = false;
|
||||
versionCache.clear();
|
||||
}
|
||||
|
||||
static async resolve(): Promise<string | null> {
|
||||
let stalePositiveBinaryPath: string | null = null;
|
||||
let stalePositiveLaunchVerifiedAt = 0;
|
||||
|
||||
if (cachedBinaryPath !== undefined) {
|
||||
if (cachedBinaryPath === null) {
|
||||
if (!cachedMissHadShellEnv && getCachedShellEnv() !== null) {
|
||||
cachedBinaryPath = undefined;
|
||||
cacheVerifiedAt = 0;
|
||||
cacheLaunchVerifiedAt = 0;
|
||||
cachedMissHadShellEnv = false;
|
||||
cachedPositiveIsStale = false;
|
||||
} else {
|
||||
const verifiedAppManagedBinaryPath =
|
||||
await resolveVerifiedAppManagedCodexRuntimeBinaryPath();
|
||||
if (verifiedAppManagedBinaryPath) {
|
||||
const now = Date.now();
|
||||
cachedBinaryPath = verifiedAppManagedBinaryPath;
|
||||
cacheVerifiedAt = Date.now();
|
||||
cacheVerifiedAt = now;
|
||||
cacheLaunchVerifiedAt = now;
|
||||
cachedMissHadShellEnv = false;
|
||||
cachedPositiveIsStale = false;
|
||||
return verifiedAppManagedBinaryPath;
|
||||
}
|
||||
if (Date.now() - cacheVerifiedAt <= CACHE_VERIFY_TTL_MS) {
|
||||
|
|
@ -147,22 +175,36 @@ export class CodexBinaryResolver {
|
|||
}
|
||||
cachedBinaryPath = undefined;
|
||||
cacheVerifiedAt = 0;
|
||||
cacheLaunchVerifiedAt = 0;
|
||||
cachedMissHadShellEnv = false;
|
||||
cachedPositiveIsStale = false;
|
||||
}
|
||||
} else {
|
||||
if (Date.now() - cacheVerifiedAt <= CACHE_VERIFY_TTL_MS) {
|
||||
const now = Date.now();
|
||||
const stalePositiveIsStillAllowed =
|
||||
!cachedPositiveIsStale || now - cacheLaunchVerifiedAt <= STALE_POSITIVE_CACHE_TTL_MS;
|
||||
if (now - cacheVerifiedAt <= CACHE_VERIFY_TTL_MS && stalePositiveIsStillAllowed) {
|
||||
return cachedBinaryPath;
|
||||
}
|
||||
|
||||
const verified = await verifyBinary(cachedBinaryPath);
|
||||
const cachedPositiveBinaryPath = cachedBinaryPath;
|
||||
const cachedPositiveLaunchVerifiedAt = cacheLaunchVerifiedAt;
|
||||
const verified = await verifyBinary(cachedPositiveBinaryPath);
|
||||
if (verified) {
|
||||
cacheVerifiedAt = Date.now();
|
||||
const verifiedAt = Date.now();
|
||||
cacheVerifiedAt = verifiedAt;
|
||||
cacheLaunchVerifiedAt = verifiedAt;
|
||||
cachedMissHadShellEnv = false;
|
||||
cachedPositiveIsStale = false;
|
||||
return verified;
|
||||
}
|
||||
|
||||
stalePositiveBinaryPath = cachedPositiveBinaryPath;
|
||||
stalePositiveLaunchVerifiedAt = cachedPositiveLaunchVerifiedAt;
|
||||
cachedBinaryPath = undefined;
|
||||
cacheVerifiedAt = 0;
|
||||
cacheLaunchVerifiedAt = 0;
|
||||
cachedPositiveIsStale = false;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -172,7 +214,20 @@ export class CodexBinaryResolver {
|
|||
});
|
||||
}
|
||||
|
||||
return resolveInFlight;
|
||||
const resolved = await resolveInFlight;
|
||||
if (
|
||||
!resolved &&
|
||||
(await canReuseStalePositiveBinary(stalePositiveBinaryPath, stalePositiveLaunchVerifiedAt))
|
||||
) {
|
||||
cachedBinaryPath = stalePositiveBinaryPath;
|
||||
cacheVerifiedAt = Date.now();
|
||||
cacheLaunchVerifiedAt = stalePositiveLaunchVerifiedAt;
|
||||
cachedMissHadShellEnv = false;
|
||||
cachedPositiveIsStale = true;
|
||||
return stalePositiveBinaryPath;
|
||||
}
|
||||
|
||||
return resolved;
|
||||
}
|
||||
|
||||
private static async runResolve(): Promise<string | null> {
|
||||
|
|
@ -187,16 +242,21 @@ export class CodexBinaryResolver {
|
|||
for (const candidate of candidates) {
|
||||
const resolved = await verifyBinary(candidate);
|
||||
if (resolved) {
|
||||
const now = Date.now();
|
||||
cachedBinaryPath = resolved;
|
||||
cacheVerifiedAt = Date.now();
|
||||
cacheVerifiedAt = now;
|
||||
cacheLaunchVerifiedAt = now;
|
||||
cachedMissHadShellEnv = false;
|
||||
cachedPositiveIsStale = false;
|
||||
return resolved;
|
||||
}
|
||||
}
|
||||
|
||||
cachedBinaryPath = null;
|
||||
cacheVerifiedAt = Date.now();
|
||||
cacheLaunchVerifiedAt = 0;
|
||||
cachedMissHadShellEnv = getCachedShellEnv() !== null;
|
||||
cachedPositiveIsStale = false;
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,134 @@
|
|||
// @vitest-environment node
|
||||
import { chmod, mkdtemp, rm, unlink, writeFile } from 'node:fs/promises';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
vi.mock('@main/utils/cliPathMerge', () => ({
|
||||
buildMergedCliPath: () => process.env.PATH ?? '',
|
||||
}));
|
||||
|
||||
vi.mock('@main/utils/shellEnv', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('@main/utils/shellEnv')>();
|
||||
return {
|
||||
...actual,
|
||||
getCachedShellEnv: () => null,
|
||||
};
|
||||
});
|
||||
|
||||
const originalPath = process.env.PATH;
|
||||
const originalCodexCliPath = process.env.CODEX_CLI_PATH;
|
||||
const originalFakeFailFile = process.env.CODEX_FAKE_CODEX_FAIL_FILE;
|
||||
const describePosix = process.platform === 'win32' ? describe.skip : describe;
|
||||
const LIVE_CODEX_BINARY_SMOKE = process.env.LIVE_CODEX_BINARY_RESOLVER_SMOKE === '1';
|
||||
const describeLive = LIVE_CODEX_BINARY_SMOKE ? describe : describe.skip;
|
||||
const BASE_TIME_MS = 1_767_225_600_000;
|
||||
|
||||
let tempDirs: string[] = [];
|
||||
|
||||
async function clearResolverCache(): Promise<void> {
|
||||
const { CodexBinaryResolver } = await import('../CodexBinaryResolver');
|
||||
CodexBinaryResolver.clearCache();
|
||||
}
|
||||
|
||||
async function createFakeCodexBinary(): Promise<{
|
||||
binaryPath: string;
|
||||
failMarkerPath: string;
|
||||
}> {
|
||||
const tempDir = await mkdtemp(path.join(os.tmpdir(), 'codex-binary-resolver-real-'));
|
||||
tempDirs.push(tempDir);
|
||||
const binaryPath = path.join(tempDir, 'codex');
|
||||
const failMarkerPath = path.join(tempDir, 'fail');
|
||||
await writeFile(
|
||||
binaryPath,
|
||||
[
|
||||
'#!/bin/sh',
|
||||
'if [ -n "$CODEX_FAKE_CODEX_FAIL_FILE" ] && [ -f "$CODEX_FAKE_CODEX_FAIL_FILE" ]; then',
|
||||
' echo "fake codex failure" >&2',
|
||||
' exit 42',
|
||||
'fi',
|
||||
'if [ "$1" = "--version" ]; then',
|
||||
' echo "codex-cli 99.0.0"',
|
||||
' exit 0',
|
||||
'fi',
|
||||
'echo "unexpected args: $*" >&2',
|
||||
'exit 2',
|
||||
'',
|
||||
].join('\n'),
|
||||
'utf8'
|
||||
);
|
||||
await chmod(binaryPath, 0o755);
|
||||
process.env.PATH = tempDir;
|
||||
return { binaryPath, failMarkerPath };
|
||||
}
|
||||
|
||||
afterEach(async () => {
|
||||
vi.restoreAllMocks();
|
||||
process.env.PATH = originalPath;
|
||||
process.env.CODEX_CLI_PATH = originalCodexCliPath;
|
||||
process.env.CODEX_FAKE_CODEX_FAIL_FILE = originalFakeFailFile;
|
||||
await clearResolverCache();
|
||||
await Promise.all(tempDirs.map((tempDir) => rm(tempDir, { recursive: true, force: true })));
|
||||
tempDirs = [];
|
||||
});
|
||||
|
||||
describePosix('CodexBinaryResolver real filesystem/process smoke', () => {
|
||||
it('resolves an explicit executable through real fs access and execFile', async () => {
|
||||
const { binaryPath, failMarkerPath } = await createFakeCodexBinary();
|
||||
process.env.CODEX_CLI_PATH = binaryPath;
|
||||
process.env.CODEX_FAKE_CODEX_FAIL_FILE = failMarkerPath;
|
||||
const { CodexBinaryResolver } = await import('../CodexBinaryResolver');
|
||||
CodexBinaryResolver.clearCache();
|
||||
|
||||
await expect(CodexBinaryResolver.resolve()).resolves.toBe(binaryPath);
|
||||
await expect(CodexBinaryResolver.resolveVersion(binaryPath)).resolves.toBe('99.0.0');
|
||||
});
|
||||
|
||||
it('keeps a recent real executable during transient launch failure, then expires it', async () => {
|
||||
const { binaryPath, failMarkerPath } = await createFakeCodexBinary();
|
||||
process.env.CODEX_CLI_PATH = binaryPath;
|
||||
process.env.CODEX_FAKE_CODEX_FAIL_FILE = failMarkerPath;
|
||||
const nowSpy = vi.spyOn(Date, 'now');
|
||||
const { CodexBinaryResolver } = await import('../CodexBinaryResolver');
|
||||
CodexBinaryResolver.clearCache();
|
||||
|
||||
nowSpy.mockReturnValue(BASE_TIME_MS);
|
||||
await expect(CodexBinaryResolver.resolve()).resolves.toBe(binaryPath);
|
||||
|
||||
await writeFile(failMarkerPath, 'fail', 'utf8');
|
||||
nowSpy.mockReturnValue(BASE_TIME_MS + 30_001);
|
||||
await expect(CodexBinaryResolver.resolve()).resolves.toBe(binaryPath);
|
||||
|
||||
nowSpy.mockReturnValue(BASE_TIME_MS + 300_001);
|
||||
await expect(CodexBinaryResolver.resolve()).resolves.toBeNull();
|
||||
});
|
||||
|
||||
it('does not keep a recent real executable after it is removed', async () => {
|
||||
const { binaryPath, failMarkerPath } = await createFakeCodexBinary();
|
||||
process.env.CODEX_CLI_PATH = binaryPath;
|
||||
process.env.CODEX_FAKE_CODEX_FAIL_FILE = failMarkerPath;
|
||||
const nowSpy = vi.spyOn(Date, 'now');
|
||||
const { CodexBinaryResolver } = await import('../CodexBinaryResolver');
|
||||
CodexBinaryResolver.clearCache();
|
||||
|
||||
nowSpy.mockReturnValue(BASE_TIME_MS);
|
||||
await expect(CodexBinaryResolver.resolve()).resolves.toBe(binaryPath);
|
||||
|
||||
await unlink(binaryPath);
|
||||
nowSpy.mockReturnValue(BASE_TIME_MS + 30_001);
|
||||
await expect(CodexBinaryResolver.resolve()).resolves.toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describeLive('CodexBinaryResolver live local Codex smoke', () => {
|
||||
it('resolves and versions the current local Codex binary', async () => {
|
||||
const { CodexBinaryResolver } = await import('../CodexBinaryResolver');
|
||||
CodexBinaryResolver.clearCache();
|
||||
|
||||
const binaryPath = await CodexBinaryResolver.resolve();
|
||||
expect(binaryPath).toEqual(expect.any(String));
|
||||
const version = await CodexBinaryResolver.resolveVersion(binaryPath);
|
||||
expect(version).toEqual(expect.any(String));
|
||||
});
|
||||
});
|
||||
|
|
@ -327,6 +327,137 @@ describe('CodexBinaryResolver', () => {
|
|||
expect(buildEnrichedEnvMock).toHaveBeenCalledWith(codexShim);
|
||||
});
|
||||
|
||||
it('reuses a recent known-good binary when revalidation transiently fails', async () => {
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(new Date('2026-01-01T00:00:00.000Z'));
|
||||
setPlatform('darwin');
|
||||
process.env.PATH = '/usr/local/bin:/usr/bin:/bin';
|
||||
const codexShim = path.posix.join('/usr/local/bin', 'codex');
|
||||
let canLaunch = true;
|
||||
|
||||
accessMock.mockImplementation((filePath) => {
|
||||
if (filePath === codexShim) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
return Promise.reject(Object.assign(new Error('ENOENT'), { code: 'ENOENT' }));
|
||||
});
|
||||
execCliMock.mockImplementation(() => {
|
||||
if (canLaunch) {
|
||||
return Promise.resolve({ stdout: 'codex-cli 0.130.0', stderr: '' });
|
||||
}
|
||||
return Promise.reject(new Error('codex --version timed out'));
|
||||
});
|
||||
|
||||
const { CodexBinaryResolver } = await import('../CodexBinaryResolver');
|
||||
CodexBinaryResolver.clearCache();
|
||||
|
||||
await expect(CodexBinaryResolver.resolve()).resolves.toBe(codexShim);
|
||||
|
||||
canLaunch = false;
|
||||
vi.advanceTimersByTime(30_001);
|
||||
|
||||
await expect(CodexBinaryResolver.resolve()).resolves.toBe(codexShim);
|
||||
});
|
||||
|
||||
it('expires stale known-good reuse from the last real launch verification', async () => {
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(new Date('2026-01-01T00:00:00.000Z'));
|
||||
setPlatform('darwin');
|
||||
process.env.PATH = '/usr/local/bin:/usr/bin:/bin';
|
||||
const codexShim = path.posix.join('/usr/local/bin', 'codex');
|
||||
let canLaunch = true;
|
||||
|
||||
accessMock.mockImplementation((filePath) => {
|
||||
if (filePath === codexShim) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
return Promise.reject(Object.assign(new Error('ENOENT'), { code: 'ENOENT' }));
|
||||
});
|
||||
execCliMock.mockImplementation(() => {
|
||||
if (canLaunch) {
|
||||
return Promise.resolve({ stdout: 'codex-cli 0.130.0', stderr: '' });
|
||||
}
|
||||
return Promise.reject(new Error('codex --version timed out'));
|
||||
});
|
||||
|
||||
const { CodexBinaryResolver } = await import('../CodexBinaryResolver');
|
||||
CodexBinaryResolver.clearCache();
|
||||
|
||||
await expect(CodexBinaryResolver.resolve()).resolves.toBe(codexShim);
|
||||
|
||||
canLaunch = false;
|
||||
vi.advanceTimersByTime(290_000);
|
||||
|
||||
await expect(CodexBinaryResolver.resolve()).resolves.toBe(codexShim);
|
||||
|
||||
vi.advanceTimersByTime(10_001);
|
||||
|
||||
await expect(CodexBinaryResolver.resolve()).resolves.toBeNull();
|
||||
});
|
||||
|
||||
it('prefers a newly resolved binary over stale known-good reuse', async () => {
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(new Date('2026-01-01T00:00:00.000Z'));
|
||||
setPlatform('darwin');
|
||||
const oldCodexShim = path.posix.join('/old/bin', 'codex');
|
||||
const newCodexShim = path.posix.join('/new/bin', 'codex');
|
||||
process.env.PATH = '/old/bin:/usr/bin:/bin';
|
||||
let oldCanLaunch = true;
|
||||
|
||||
accessMock.mockImplementation((filePath) => {
|
||||
if (filePath === oldCodexShim || filePath === newCodexShim) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
return Promise.reject(Object.assign(new Error('ENOENT'), { code: 'ENOENT' }));
|
||||
});
|
||||
execCliMock.mockImplementation((binaryPath) => {
|
||||
if (binaryPath === oldCodexShim) {
|
||||
if (oldCanLaunch) {
|
||||
return Promise.resolve({ stdout: 'codex-cli 0.130.0', stderr: '' });
|
||||
}
|
||||
return Promise.reject(new Error('old codex --version timed out'));
|
||||
}
|
||||
return Promise.resolve({ stdout: 'codex-cli 0.131.0', stderr: '' });
|
||||
});
|
||||
|
||||
const { CodexBinaryResolver } = await import('../CodexBinaryResolver');
|
||||
CodexBinaryResolver.clearCache();
|
||||
|
||||
await expect(CodexBinaryResolver.resolve()).resolves.toBe(oldCodexShim);
|
||||
|
||||
oldCanLaunch = false;
|
||||
process.env.PATH = '/new/bin:/usr/bin:/bin';
|
||||
vi.advanceTimersByTime(30_001);
|
||||
|
||||
await expect(CodexBinaryResolver.resolve()).resolves.toBe(newCodexShim);
|
||||
});
|
||||
|
||||
it('does not reuse a recent known-good binary after the file disappears', async () => {
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(new Date('2026-01-01T00:00:00.000Z'));
|
||||
setPlatform('darwin');
|
||||
process.env.PATH = '/usr/local/bin:/usr/bin:/bin';
|
||||
const codexShim = path.posix.join('/usr/local/bin', 'codex');
|
||||
let filePresent = true;
|
||||
|
||||
accessMock.mockImplementation((filePath) => {
|
||||
if (filePath === codexShim && filePresent) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
return Promise.reject(Object.assign(new Error('ENOENT'), { code: 'ENOENT' }));
|
||||
});
|
||||
|
||||
const { CodexBinaryResolver } = await import('../CodexBinaryResolver');
|
||||
CodexBinaryResolver.clearCache();
|
||||
|
||||
await expect(CodexBinaryResolver.resolve()).resolves.toBe(codexShim);
|
||||
|
||||
filePresent = false;
|
||||
vi.advanceTimersByTime(30_001);
|
||||
|
||||
await expect(CodexBinaryResolver.resolve()).resolves.toBeNull();
|
||||
});
|
||||
|
||||
it('uses enriched env for Codex version probes', async () => {
|
||||
setPlatform('darwin');
|
||||
const codexShim = path.posix.join('/usr/local/bin', 'codex');
|
||||
|
|
|
|||
|
|
@ -498,20 +498,12 @@ export class TeamMemberRuntimeAdvisoryService {
|
|||
getOpenCodeRuntimeDeliveryRecordTimeMs(right) -
|
||||
getOpenCodeRuntimeDeliveryRecordTimeMs(left)
|
||||
);
|
||||
const latestSuccess = ordered.find(isTerminalSuccessfulOpenCodeDeliveryRecord);
|
||||
const latestError = ordered.find((record) => {
|
||||
return this.isOpenCodeDeliveryAdvisoryCandidate(record, now);
|
||||
});
|
||||
if (!latestError) {
|
||||
return null;
|
||||
}
|
||||
if (
|
||||
latestSuccess &&
|
||||
getOpenCodeRuntimeDeliveryRecordTimeMs(latestSuccess) >
|
||||
getOpenCodeRuntimeDeliveryRecordTimeMs(latestError)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const decision = decideOpenCodeRuntimeDeliveryAdvisory({
|
||||
record: latestError,
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import type {
|
|||
} from '@shared/types';
|
||||
|
||||
export const OPENCODE_RUNTIME_DELIVERY_GENERIC_PROOF_GRACE_MS = 120_000;
|
||||
const OPENCODE_RUNTIME_DELIVERY_PROOF_TIMESTAMP_SKEW_MS = 5_000;
|
||||
|
||||
export interface OpenCodeRuntimeDeliveryProofSnapshot {
|
||||
latestSuccessAt?: number;
|
||||
|
|
@ -156,14 +157,20 @@ export function hasSupersedingOpenCodeRuntimeDeliveryProof(input: {
|
|||
if (!proof) {
|
||||
return false;
|
||||
}
|
||||
const recordTime = getOpenCodeRuntimeDeliveryRecordTimeMs(input.record);
|
||||
if (typeof proof.latestSuccessAt === 'number' && proof.latestSuccessAt > recordTime) {
|
||||
const promptTime = getOpenCodeRuntimeDeliveryPromptTimeMs(input.record);
|
||||
const isPromptTimeEligible = (proofAt: number): boolean => {
|
||||
if (!Number.isFinite(proofAt) || proofAt <= 0) {
|
||||
return false;
|
||||
}
|
||||
if (!Number.isFinite(promptTime) || promptTime <= 0) {
|
||||
return true;
|
||||
}
|
||||
return proofAt + OPENCODE_RUNTIME_DELIVERY_PROOF_TIMESTAMP_SKEW_MS >= promptTime;
|
||||
};
|
||||
if (typeof proof.visibleReplyAt === 'number' && isPromptTimeEligible(proof.visibleReplyAt)) {
|
||||
return true;
|
||||
}
|
||||
if (typeof proof.visibleReplyAt === 'number' && proof.visibleReplyAt > 0) {
|
||||
return true;
|
||||
}
|
||||
if (typeof proof.taskProgressAt === 'number' && proof.taskProgressAt > 0) {
|
||||
if (typeof proof.taskProgressAt === 'number' && isPromptTimeEligible(proof.taskProgressAt)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
|
|
|
|||
|
|
@ -101,6 +101,10 @@ export interface OpenCodeTeamLaunchReadinessServiceOptions {
|
|||
|
||||
const OPENCODE_RUNTIME_BINARY_UNREACHABLE_DIAGNOSTIC =
|
||||
'OpenCode runtime binary is not installed or not reachable by launch preflight.';
|
||||
const OPENCODE_UNAUTHENTICATED_FREE_MODEL_DIAGNOSTIC =
|
||||
'No connected OpenCode provider found. Proceeding with a free OpenCode model route that does not require provider authentication.';
|
||||
const OPENCODE_UNAUTHENTICATED_PAID_MODEL_DIAGNOSTIC =
|
||||
'No connected OpenCode provider found. Choose a free OpenCode model such as Big Pickle, or connect a provider in OpenCode for provider-backed models.';
|
||||
|
||||
export class OpenCodeTeamLaunchReadinessService {
|
||||
constructor(
|
||||
|
|
@ -132,18 +136,14 @@ export class OpenCodeTeamLaunchReadinessService {
|
|||
});
|
||||
}
|
||||
|
||||
if (!inventory.authenticated || inventory.connectedProviders.length === 0) {
|
||||
return readiness({
|
||||
state: 'not_authenticated',
|
||||
inventory,
|
||||
modelId: input.selectedModel,
|
||||
diagnostics: appendDiagnostics(inventory.diagnostics, [
|
||||
'No connected OpenCode providers found',
|
||||
]),
|
||||
});
|
||||
}
|
||||
|
||||
const modelId = input.selectedModel ?? inventory.models[0] ?? null;
|
||||
const explicitModelId = input.selectedModel?.trim() || null;
|
||||
const hasConnectedProvider =
|
||||
inventory.authenticated && inventory.connectedProviders.length > 0;
|
||||
const modelId =
|
||||
explicitModelId ??
|
||||
(!hasConnectedProvider
|
||||
? (inventory.models.find(isFreeOpenCodeModelRoute) ?? inventory.models[0] ?? null)
|
||||
: (inventory.models[0] ?? null));
|
||||
if (!modelId) {
|
||||
return readiness({
|
||||
state: 'model_unavailable',
|
||||
|
|
@ -153,6 +153,20 @@ export class OpenCodeTeamLaunchReadinessService {
|
|||
});
|
||||
}
|
||||
|
||||
const usingFreeModelWithoutProvider =
|
||||
!hasConnectedProvider && isFreeOpenCodeModelRoute(modelId);
|
||||
|
||||
if (!hasConnectedProvider && !usingFreeModelWithoutProvider) {
|
||||
return readiness({
|
||||
state: 'not_authenticated',
|
||||
inventory,
|
||||
modelId,
|
||||
diagnostics: appendDiagnostics(inventory.diagnostics, [
|
||||
OPENCODE_UNAUTHENTICATED_PAID_MODEL_DIAGNOSTIC,
|
||||
]),
|
||||
});
|
||||
}
|
||||
|
||||
const capabilities = await this.capabilities.detect({
|
||||
projectPath: input.projectPath,
|
||||
inventory,
|
||||
|
|
@ -241,7 +255,11 @@ export class OpenCodeTeamLaunchReadinessService {
|
|||
runtimeStoreReadiness,
|
||||
supportLevel: support.supportLevel,
|
||||
launchAllowed: true,
|
||||
diagnostics: inventory.diagnostics,
|
||||
diagnostics: usingFreeModelWithoutProvider
|
||||
? appendDiagnostics(inventory.diagnostics, [
|
||||
OPENCODE_UNAUTHENTICATED_FREE_MODEL_DIAGNOSTIC,
|
||||
])
|
||||
: inventory.diagnostics,
|
||||
});
|
||||
} catch (error) {
|
||||
return readiness({
|
||||
|
|
@ -254,6 +272,16 @@ export class OpenCodeTeamLaunchReadinessService {
|
|||
}
|
||||
}
|
||||
|
||||
function isFreeOpenCodeModelRoute(modelId: string): boolean {
|
||||
const normalized = modelId.trim().toLowerCase();
|
||||
return (
|
||||
normalized === 'opencode/big-pickle' ||
|
||||
normalized.includes(':free') ||
|
||||
normalized.endsWith('-free') ||
|
||||
normalized.endsWith('/free')
|
||||
);
|
||||
}
|
||||
|
||||
function readiness(input: {
|
||||
state: OpenCodeTeamLaunchReadinessState;
|
||||
inventory: OpenCodeRuntimeInventory | null;
|
||||
|
|
|
|||
|
|
@ -18,14 +18,14 @@ const REVIEW_TOUCH_TOOLS = new Set(['review_start', 'task_add_comment']);
|
|||
|
||||
const ONE_MINUTE_MS = 60_000;
|
||||
const WORK_THRESHOLDS_MS: Record<TaskStallSignal, number> = {
|
||||
turn_ended_after_touch: 8 * ONE_MINUTE_MS,
|
||||
touch_then_other_turns: 10 * ONE_MINUTE_MS,
|
||||
mid_turn_after_touch: 20 * ONE_MINUTE_MS,
|
||||
turn_ended_after_touch: 4 * ONE_MINUTE_MS,
|
||||
touch_then_other_turns: 5 * ONE_MINUTE_MS,
|
||||
mid_turn_after_touch: 10 * ONE_MINUTE_MS,
|
||||
};
|
||||
const REVIEW_THRESHOLDS_MS: Record<TaskStallSignal, number> = {
|
||||
turn_ended_after_touch: 10 * ONE_MINUTE_MS,
|
||||
touch_then_other_turns: 10 * ONE_MINUTE_MS,
|
||||
mid_turn_after_touch: 25 * ONE_MINUTE_MS,
|
||||
turn_ended_after_touch: 5 * ONE_MINUTE_MS,
|
||||
touch_then_other_turns: 6 * ONE_MINUTE_MS,
|
||||
mid_turn_after_touch: 12 * ONE_MINUTE_MS,
|
||||
};
|
||||
|
||||
function skip(
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ export function isTeamTaskStallAlertsEnabled(): boolean {
|
|||
}
|
||||
|
||||
export function getTeamTaskStallScanIntervalMs(): number {
|
||||
return readInt(process.env.CLAUDE_TEAM_TASK_STALL_SCAN_INTERVAL_MS, 60_000);
|
||||
return readInt(process.env.CLAUDE_TEAM_TASK_STALL_SCAN_INTERVAL_MS, 30_000);
|
||||
}
|
||||
|
||||
export function getTeamTaskStallStartupGraceMs(): number {
|
||||
|
|
@ -52,10 +52,10 @@ export function getTeamTaskStallStartupGraceMs(): number {
|
|||
}
|
||||
|
||||
export function getTeamTaskStallActivationGraceMs(): number {
|
||||
return readInt(process.env.CLAUDE_TEAM_TASK_STALL_ACTIVATION_GRACE_MS, 120_000);
|
||||
return readInt(process.env.CLAUDE_TEAM_TASK_STALL_ACTIVATION_GRACE_MS, 60_000);
|
||||
}
|
||||
|
||||
export function getOpenCodeWeakStartStallThresholdMs(): number {
|
||||
// Shorter OpenCode threshold for "started work" comments that do not contain concrete progress.
|
||||
return readInt(process.env.CLAUDE_TEAM_OPENCODE_WEAK_START_STALL_THRESHOLD_MS, 120_000);
|
||||
return readInt(process.env.CLAUDE_TEAM_OPENCODE_WEAK_START_STALL_THRESHOLD_MS, 100_000);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -328,6 +328,38 @@ function getOpenCodeModelPricingInfo(
|
|||
};
|
||||
}
|
||||
|
||||
function isFreeOpenCodeModelRoute(model: string): boolean {
|
||||
const normalized = model.trim().toLowerCase();
|
||||
return (
|
||||
normalized === 'opencode/big-pickle' ||
|
||||
normalized.includes(':free') ||
|
||||
normalized.endsWith('-free') ||
|
||||
normalized.endsWith('/free')
|
||||
);
|
||||
}
|
||||
|
||||
function hasFreeOpenCodeModelRoute(providerStatus: CliProviderStatus | null | undefined): boolean {
|
||||
if (providerStatus?.providerId !== 'opencode') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (providerStatus.models.some(isFreeOpenCodeModelRoute)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return (
|
||||
providerStatus.modelCatalog?.models.some((model) => {
|
||||
const badgeLabel = model.badgeLabel?.trim().toLowerCase();
|
||||
return (
|
||||
model.metadata?.free === true ||
|
||||
badgeLabel === 'free' ||
|
||||
isFreeOpenCodeModelRoute(model.launchModel) ||
|
||||
isFreeOpenCodeModelRoute(model.id)
|
||||
);
|
||||
}) ?? false
|
||||
);
|
||||
}
|
||||
|
||||
const OPENCODE_UI_DISABLED_REASON = 'OpenCode team launch is not ready.';
|
||||
export const OPENCODE_ONE_SHOT_DISABLED_REASON =
|
||||
'OpenCode team launch is available for normal teams, but scheduled one-shot prompts still run through claude -p. Choose Anthropic or Codex for one-shot schedules.';
|
||||
|
|
@ -343,7 +375,7 @@ function getOpenCodeReadinessBadgeLabel(
|
|||
return 'Install';
|
||||
}
|
||||
if (!providerStatus.authenticated) {
|
||||
return 'Auth';
|
||||
return 'Free';
|
||||
}
|
||||
return 'Setup';
|
||||
}
|
||||
|
|
@ -353,10 +385,26 @@ function getOpenCodeReadinessSummary(providerStatus: CliProviderStatus | null |
|
|||
return 'OpenCode status: checking runtime';
|
||||
}
|
||||
|
||||
const runtimeReady = providerStatus.supported;
|
||||
const hasFreeModelRoute = hasFreeOpenCodeModelRoute(providerStatus);
|
||||
let readinessSummary = 'team launch blocked';
|
||||
if (runtimeReady) {
|
||||
if (!providerStatus.authenticated) {
|
||||
readinessSummary = hasFreeModelRoute
|
||||
? 'provider connection optional'
|
||||
: 'provider-backed models need setup';
|
||||
} else if (providerStatus.capabilities.teamLaunch) {
|
||||
readinessSummary = 'team launch ready';
|
||||
}
|
||||
}
|
||||
const parts = [
|
||||
providerStatus.supported ? 'runtime detected' : 'runtime missing',
|
||||
providerStatus.authenticated ? 'provider connected' : 'provider not connected',
|
||||
providerStatus.capabilities.teamLaunch ? 'team launch ready' : 'team launch blocked',
|
||||
runtimeReady ? 'runtime detected' : 'runtime missing',
|
||||
runtimeReady && !providerStatus.authenticated && hasFreeModelRoute
|
||||
? 'free models available without auth'
|
||||
: providerStatus.authenticated
|
||||
? 'provider connected'
|
||||
: 'provider not connected',
|
||||
readinessSummary,
|
||||
];
|
||||
return `OpenCode status: ${parts.join(' · ')}`;
|
||||
}
|
||||
|
|
@ -369,7 +417,10 @@ function getOpenCodeReadinessMessage(providerStatus: CliProviderStatus | null |
|
|||
return 'OpenCode is not installed, not found, or the detected runtime is not supported. Install or update OpenCode, then refresh provider status.';
|
||||
}
|
||||
if (!providerStatus.authenticated) {
|
||||
return 'OpenCode is detected, but it does not have a connected provider. Connect a provider in OpenCode, then refresh provider status.';
|
||||
if (hasFreeOpenCodeModelRoute(providerStatus)) {
|
||||
return 'OpenCode is detected. You can use free OpenCode models such as Big Pickle without connecting a provider. Connect a provider only when you want provider-backed models.';
|
||||
}
|
||||
return 'OpenCode is detected, but no free OpenCode model is listed yet. Refresh provider status, or connect a provider in OpenCode for provider-backed models.';
|
||||
}
|
||||
if (!providerStatus.capabilities.teamLaunch) {
|
||||
return 'OpenCode is installed and authenticated, but Agent Teams launch readiness is blocked.';
|
||||
|
|
@ -702,14 +753,7 @@ export const TeamModelSelector: React.FC<TeamModelSelectorProps> = ({
|
|||
'OpenCode runtime is not installed.'
|
||||
);
|
||||
}
|
||||
if (!providerStatus.authenticated) {
|
||||
return (
|
||||
providerStatus.detailMessage ??
|
||||
providerStatus.statusMessage ??
|
||||
'OpenCode has no connected provider.'
|
||||
);
|
||||
}
|
||||
if (!providerStatus.capabilities.teamLaunch) {
|
||||
if (providerStatus.authenticated && !providerStatus.capabilities.teamLaunch) {
|
||||
return (
|
||||
providerStatus.detailMessage ??
|
||||
providerStatus.statusMessage ??
|
||||
|
|
@ -1104,17 +1148,30 @@ export const TeamModelSelector: React.FC<TeamModelSelectorProps> = ({
|
|||
reason: activeProviderDisabledReason,
|
||||
actionLabel: null,
|
||||
}
|
||||
: canActivateInspectedOpenCode
|
||||
: effectiveProviderId === 'opencode' &&
|
||||
runtimeProviderStatus?.supported === true &&
|
||||
runtimeProviderStatus.authenticated === false
|
||||
? {
|
||||
tone: 'ready' as const,
|
||||
title: 'OpenCode is ready',
|
||||
tone: 'warning' as const,
|
||||
title: hasFreeOpenCodeModelRoute(runtimeProviderStatus)
|
||||
? 'OpenCode free models are available'
|
||||
: 'OpenCode provider is not connected',
|
||||
summary: getOpenCodeReadinessSummary(runtimeProviderStatus),
|
||||
message:
|
||||
'OpenCode passed provider readiness. Select it to use OpenCode models for this team.',
|
||||
message: getOpenCodeReadinessMessage(runtimeProviderStatus),
|
||||
reason: null,
|
||||
actionLabel: 'Use OpenCode',
|
||||
actionLabel: null,
|
||||
}
|
||||
: null;
|
||||
: canActivateInspectedOpenCode
|
||||
? {
|
||||
tone: 'ready' as const,
|
||||
title: 'OpenCode is ready',
|
||||
summary: getOpenCodeReadinessSummary(runtimeProviderStatus),
|
||||
message:
|
||||
'OpenCode passed provider readiness. Select it to use OpenCode models for this team.',
|
||||
reason: null,
|
||||
actionLabel: 'Use OpenCode',
|
||||
}
|
||||
: null;
|
||||
const activeProviderNotice = providerNoticeById?.[effectiveProviderId] ?? null;
|
||||
const getModelAdvisoryBadgeLabel = (reason: string | null): string =>
|
||||
reason?.toLowerCase().includes('ping not confirmed') ? 'Ping not confirmed' : 'Note';
|
||||
|
|
|
|||
|
|
@ -173,4 +173,56 @@ describe('OpenCodeRuntimeDeliveryAdvisoryPolicy', () => {
|
|||
})
|
||||
).toMatchObject({ action: 'suppress' });
|
||||
});
|
||||
|
||||
it('does not suppress terminal failures with stale visible proof before the prompt window', () => {
|
||||
const record = makeRecord({});
|
||||
|
||||
expect(
|
||||
decideOpenCodeRuntimeDeliveryAdvisory({
|
||||
record,
|
||||
proof: {
|
||||
visibleReplyAt: Date.parse(record.inboxTimestamp) - 6_000,
|
||||
visibleReplyMessageId: 'old-reply',
|
||||
visibleReplyInbox: 'user',
|
||||
},
|
||||
now: Date.parse(record.failedAt!) + OPENCODE_RUNTIME_DELIVERY_GENERIC_PROOF_GRACE_MS + 1,
|
||||
})
|
||||
).toMatchObject({
|
||||
action: 'surface',
|
||||
severity: 'error',
|
||||
});
|
||||
});
|
||||
|
||||
it('does not suppress terminal failures with only unrelated later delivery success', () => {
|
||||
const record = makeRecord({});
|
||||
|
||||
expect(
|
||||
decideOpenCodeRuntimeDeliveryAdvisory({
|
||||
record,
|
||||
proof: {
|
||||
latestSuccessAt: Date.parse(record.failedAt!) + 60_000,
|
||||
},
|
||||
now: Date.parse(record.failedAt!) + OPENCODE_RUNTIME_DELIVERY_GENERIC_PROOF_GRACE_MS + 1,
|
||||
})
|
||||
).toMatchObject({
|
||||
action: 'surface',
|
||||
severity: 'error',
|
||||
});
|
||||
});
|
||||
|
||||
it('accepts visible proof inside the prompt timestamp skew window', () => {
|
||||
const record = makeRecord({});
|
||||
|
||||
expect(
|
||||
decideOpenCodeRuntimeDeliveryAdvisory({
|
||||
record,
|
||||
proof: {
|
||||
visibleReplyAt: Date.parse(record.inboxTimestamp) - 4_000,
|
||||
visibleReplyMessageId: 'nearby-reply',
|
||||
visibleReplyInbox: 'user',
|
||||
},
|
||||
now: Date.parse(record.failedAt!) + OPENCODE_RUNTIME_DELIVERY_GENERIC_PROOF_GRACE_MS + 1,
|
||||
})
|
||||
).toMatchObject({ action: 'suppress' });
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -3,13 +3,13 @@ import { describe, expect, it, vi } from 'vitest';
|
|||
import { createEmptyEndpointMap } from '../../../../src/main/services/team/opencode/capabilities/OpenCodeApiCapabilities';
|
||||
import { REQUIRED_AGENT_TEAMS_RUNTIME_TOOLS } from '../../../../src/main/services/team/opencode/mcp/OpenCodeMcpToolAvailability';
|
||||
import {
|
||||
OpenCodeTeamLaunchReadinessService,
|
||||
type OpenCodeApiCapabilityPort,
|
||||
type OpenCodeModelExecutionProbePort,
|
||||
type OpenCodeMcpToolProofPort,
|
||||
type OpenCodeModelExecutionProbePort,
|
||||
type OpenCodeRuntimeInventory,
|
||||
type OpenCodeRuntimeInventoryPort,
|
||||
type OpenCodeRuntimeStoreReadinessPort,
|
||||
OpenCodeTeamLaunchReadinessService,
|
||||
} from '../../../../src/main/services/team/opencode/readiness/OpenCodeTeamLaunchReadiness';
|
||||
|
||||
import type {
|
||||
|
|
@ -38,7 +38,66 @@ describe('OpenCodeTeamLaunchReadinessService', () => {
|
|||
expect(ports.mcpTools.prove).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('blocks unauthenticated OpenCode even when the binary is installed', async () => {
|
||||
it('allows unauthenticated OpenCode when the selected model is a free route', async () => {
|
||||
const ports = createPorts({
|
||||
inventory: {
|
||||
authenticated: false,
|
||||
connectedProviders: [],
|
||||
models: ['opencode/big-pickle'],
|
||||
},
|
||||
});
|
||||
|
||||
await expect(
|
||||
service(ports).check(readinessInput({ selectedModel: 'opencode/big-pickle' }))
|
||||
).resolves.toMatchObject({
|
||||
state: 'ready',
|
||||
launchAllowed: true,
|
||||
modelId: 'opencode/big-pickle',
|
||||
diagnostics: [
|
||||
'No connected OpenCode provider found. Proceeding with a free OpenCode model route that does not require provider authentication.',
|
||||
],
|
||||
});
|
||||
expect(ports.capabilities.detect).toHaveBeenCalled();
|
||||
expect(ports.mcpTools.prove).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('uses the first free OpenCode model for unauthenticated default selection', async () => {
|
||||
const ports = createPorts({
|
||||
inventory: {
|
||||
authenticated: false,
|
||||
connectedProviders: [],
|
||||
models: ['openai/gpt-5.4-mini', 'opencode/big-pickle'],
|
||||
},
|
||||
});
|
||||
|
||||
await expect(service(ports).check(readinessInput({ selectedModel: null }))).resolves.toMatchObject({
|
||||
state: 'ready',
|
||||
launchAllowed: true,
|
||||
modelId: 'opencode/big-pickle',
|
||||
});
|
||||
expect(ports.capabilities.detect).toHaveBeenCalled();
|
||||
expect(ports.mcpTools.prove).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('does not replace an explicit unauthenticated provider-backed model with a free route', async () => {
|
||||
const ports = createPorts({
|
||||
inventory: {
|
||||
authenticated: false,
|
||||
connectedProviders: [],
|
||||
models: ['opencode/big-pickle', 'openai/gpt-5.4-mini'],
|
||||
},
|
||||
});
|
||||
|
||||
await expect(service(ports).check(readinessInput())).resolves.toMatchObject({
|
||||
state: 'not_authenticated',
|
||||
launchAllowed: false,
|
||||
modelId: 'openai/gpt-5.4-mini',
|
||||
});
|
||||
expect(ports.capabilities.detect).not.toHaveBeenCalled();
|
||||
expect(ports.mcpTools.prove).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('blocks unauthenticated OpenCode when the selected model needs a provider', async () => {
|
||||
const ports = createPorts({
|
||||
inventory: { authenticated: false, connectedProviders: [] },
|
||||
});
|
||||
|
|
@ -47,8 +106,12 @@ describe('OpenCodeTeamLaunchReadinessService', () => {
|
|||
state: 'not_authenticated',
|
||||
launchAllowed: false,
|
||||
opencodeVersion: '1.14.19',
|
||||
diagnostics: ['No connected OpenCode providers found'],
|
||||
diagnostics: [
|
||||
'No connected OpenCode provider found. Choose a free OpenCode model such as Big Pickle, or connect a provider in OpenCode for provider-backed models.',
|
||||
],
|
||||
});
|
||||
expect(ports.capabilities.detect).not.toHaveBeenCalled();
|
||||
expect(ports.mcpTools.prove).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('blocks unsupported versions before MCP and model probes', async () => {
|
||||
|
|
|
|||
|
|
@ -1,13 +1,19 @@
|
|||
import * as fs from 'fs/promises';
|
||||
import * as os from 'os';
|
||||
import * as path from 'path';
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import * as fs from 'fs/promises';
|
||||
|
||||
import { TeamMemberRuntimeAdvisoryService } from '../../../../src/main/services/team/TeamMemberRuntimeAdvisoryService';
|
||||
import { setClaudeBasePathOverride } from '../../../../src/main/utils/pathDecoder';
|
||||
|
||||
import type { MemberRuntimeAdvisory, ResolvedTeamMember } from '../../../../src/shared/types/team';
|
||||
import type { OpenCodePromptDeliveryLedgerRecord } from '../../../../src/main/services/team/opencode/delivery/OpenCodePromptDeliveryLedger';
|
||||
import type {
|
||||
InboxMessage,
|
||||
MemberRuntimeAdvisory,
|
||||
ResolvedTeamMember,
|
||||
TaskRef,
|
||||
TeamTask,
|
||||
} from '../../../../src/shared/types/team';
|
||||
|
||||
interface Deferred<T> {
|
||||
promise: Promise<T>;
|
||||
|
|
@ -15,6 +21,11 @@ interface Deferred<T> {
|
|||
reject: (reason?: unknown) => void;
|
||||
}
|
||||
|
||||
interface TeamMemberRuntimeAdvisoryServiceTestAccess {
|
||||
extractApiRetryAdvisory(line: string): MemberRuntimeAdvisory | null;
|
||||
extractApiErrorAdvisory(line: string, observedAtMs: number): MemberRuntimeAdvisory | null;
|
||||
}
|
||||
|
||||
function createDeferred<T>(): Deferred<T> {
|
||||
let resolve!: (value: T) => void;
|
||||
let reject!: (reason?: unknown) => void;
|
||||
|
|
@ -43,6 +54,12 @@ function buildRetryingAdvisory(label: string): MemberRuntimeAdvisory {
|
|||
};
|
||||
}
|
||||
|
||||
function serviceTestAccess(
|
||||
service: TeamMemberRuntimeAdvisoryService
|
||||
): TeamMemberRuntimeAdvisoryServiceTestAccess {
|
||||
return service as unknown as TeamMemberRuntimeAdvisoryServiceTestAccess;
|
||||
}
|
||||
|
||||
function createStubbedServiceHarness() {
|
||||
const logsFinder = {
|
||||
findMemberLogs: vi.fn(async (_teamName: string, memberName: string) => [
|
||||
|
|
@ -67,6 +84,119 @@ function createStubbedServiceHarness() {
|
|||
return { service, logsFinder, advisoryByFilePath, readRecentApiRetryAdvisory };
|
||||
}
|
||||
|
||||
function buildOpenCodeDeliveryRecord(
|
||||
overrides: Partial<OpenCodePromptDeliveryLedgerRecord>
|
||||
): OpenCodePromptDeliveryLedgerRecord {
|
||||
const now = '2026-05-19T12:19:04.252Z';
|
||||
return {
|
||||
id: 'opencode-prompt:test',
|
||||
teamName: 'relay-release',
|
||||
memberName: 'tom',
|
||||
laneId: 'secondary:opencode:tom',
|
||||
runId: 'run-1',
|
||||
runtimeSessionId: 'session-1',
|
||||
inboxMessageId: 'assignment-1',
|
||||
inboxTimestamp: '2026-05-19T12:14:56.227Z',
|
||||
source: 'watcher',
|
||||
messageKind: null,
|
||||
replyRecipient: 'team-lead',
|
||||
actionMode: null,
|
||||
taskRefs: [],
|
||||
payloadHash: 'sha256:test',
|
||||
status: 'failed_terminal',
|
||||
responseState: 'reconcile_failed',
|
||||
attempts: 3,
|
||||
maxAttempts: 3,
|
||||
acceptanceUnknown: false,
|
||||
nextAttemptAt: null,
|
||||
lastAttemptAt: '2026-05-19T12:19:04.203Z',
|
||||
lastObservedAt: '2026-05-19T12:18:44.306Z',
|
||||
acceptedAt: '2026-05-19T12:15:47.042Z',
|
||||
respondedAt: '2026-05-19T12:16:09.712Z',
|
||||
failedAt: now,
|
||||
inboxReadCommittedAt: null,
|
||||
inboxReadCommitError: null,
|
||||
prePromptCursor: 'msg_before',
|
||||
postPromptCursor: null,
|
||||
deliveredUserMessageId: 'msg_user',
|
||||
observedAssistantMessageId: 'msg_assistant',
|
||||
observedAssistantPreview: null,
|
||||
observedToolCallNames: [],
|
||||
observedVisibleMessageId: null,
|
||||
visibleReplyMessageId: null,
|
||||
visibleReplyInbox: null,
|
||||
visibleReplyCorrelation: null,
|
||||
lastReason: 'OpenCode bridge command timed out',
|
||||
diagnostics: [
|
||||
'OpenCode prompt_async accepted; response observation will continue through durable app-side ledger reconciliation.',
|
||||
'project_behavior_changed',
|
||||
'opencode_session_stale_observe_scheduled_after_accepted_prompt',
|
||||
'OpenCode bridge command timed out',
|
||||
],
|
||||
createdAt: '2026-05-19T12:14:56.474Z',
|
||||
updatedAt: now,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
async function writeOpenCodeDeliveryFixture(input: {
|
||||
baseDir: string;
|
||||
teamName: string;
|
||||
laneId: string;
|
||||
records: OpenCodePromptDeliveryLedgerRecord[];
|
||||
inboxes?: Record<string, InboxMessage[]>;
|
||||
tasks?: TeamTask[];
|
||||
}): Promise<void> {
|
||||
const teamDir = path.join(input.baseDir, 'teams', input.teamName);
|
||||
const laneDir = path.join(
|
||||
teamDir,
|
||||
'.opencode-runtime',
|
||||
'lanes',
|
||||
encodeURIComponent(input.laneId)
|
||||
);
|
||||
await fs.mkdir(laneDir, { recursive: true });
|
||||
await fs.writeFile(
|
||||
path.join(teamDir, '.opencode-runtime', 'lanes.json'),
|
||||
JSON.stringify({
|
||||
version: 1,
|
||||
updatedAt: input.records[0]?.updatedAt ?? new Date().toISOString(),
|
||||
lanes: {
|
||||
[input.laneId]: {
|
||||
laneId: input.laneId,
|
||||
state: 'active',
|
||||
updatedAt: input.records[0]?.updatedAt ?? new Date().toISOString(),
|
||||
},
|
||||
},
|
||||
}),
|
||||
'utf8'
|
||||
);
|
||||
await fs.writeFile(
|
||||
path.join(laneDir, 'opencode-prompt-delivery-ledger.json'),
|
||||
JSON.stringify({
|
||||
schemaVersion: 1,
|
||||
updatedAt: input.records[0]?.updatedAt ?? new Date().toISOString(),
|
||||
data: input.records,
|
||||
}),
|
||||
'utf8'
|
||||
);
|
||||
|
||||
if (input.inboxes) {
|
||||
const inboxDir = path.join(teamDir, 'inboxes');
|
||||
await fs.mkdir(inboxDir, { recursive: true });
|
||||
for (const [inboxName, messages] of Object.entries(input.inboxes)) {
|
||||
await fs.writeFile(path.join(inboxDir, `${inboxName}.json`), JSON.stringify(messages), 'utf8');
|
||||
}
|
||||
}
|
||||
|
||||
if (input.tasks) {
|
||||
const tasksDir = path.join(input.baseDir, 'tasks', input.teamName);
|
||||
await fs.mkdir(tasksDir, { recursive: true });
|
||||
for (const task of input.tasks) {
|
||||
await fs.writeFile(path.join(tasksDir, `${task.id}.json`), JSON.stringify(task), 'utf8');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
describe('TeamMemberRuntimeAdvisoryService', () => {
|
||||
let tmpDir: string | null = null;
|
||||
|
||||
|
|
@ -175,7 +305,7 @@ describe('TeamMemberRuntimeAdvisoryService', () => {
|
|||
['backend_error', 'Unexpected backend blew up during request processing.'],
|
||||
] as const)('classifies %s retry causes from api_error messages', async (expected, message) => {
|
||||
const service = new TeamMemberRuntimeAdvisoryService({} as never);
|
||||
const advisory = (service as any).extractApiRetryAdvisory(
|
||||
const advisory = serviceTestAccess(service).extractApiRetryAdvisory(
|
||||
JSON.stringify({
|
||||
type: 'system',
|
||||
subtype: 'api_error',
|
||||
|
|
@ -196,7 +326,7 @@ describe('TeamMemberRuntimeAdvisoryService', () => {
|
|||
|
||||
it('classifies missing api_error message text as unknown', () => {
|
||||
const service = new TeamMemberRuntimeAdvisoryService({} as never);
|
||||
const advisory = (service as any).extractApiRetryAdvisory(
|
||||
const advisory = serviceTestAccess(service).extractApiRetryAdvisory(
|
||||
JSON.stringify({
|
||||
type: 'system',
|
||||
subtype: 'api_error',
|
||||
|
|
@ -211,7 +341,7 @@ describe('TeamMemberRuntimeAdvisoryService', () => {
|
|||
it('keeps terminal API errors visible after retries stop', () => {
|
||||
const service = new TeamMemberRuntimeAdvisoryService({} as never);
|
||||
const observedAt = '2099-04-09T10:00:00.000Z';
|
||||
const advisory = (service as any).extractApiErrorAdvisory(
|
||||
const advisory = serviceTestAccess(service).extractApiErrorAdvisory(
|
||||
JSON.stringify({
|
||||
type: 'assistant',
|
||||
timestamp: observedAt,
|
||||
|
|
@ -241,7 +371,7 @@ describe('TeamMemberRuntimeAdvisoryService', () => {
|
|||
it('treats Claude Code account access failures as auth errors', () => {
|
||||
const service = new TeamMemberRuntimeAdvisoryService({} as never);
|
||||
const observedAt = '2099-04-09T10:00:00.000Z';
|
||||
const advisory = (service as any).extractApiErrorAdvisory(
|
||||
const advisory = serviceTestAccess(service).extractApiErrorAdvisory(
|
||||
JSON.stringify({
|
||||
type: 'assistant',
|
||||
timestamp: observedAt,
|
||||
|
|
@ -558,6 +688,288 @@ describe('TeamMemberRuntimeAdvisoryService', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('suppresses stale OpenCode reconcile advisories after a later relayed runtime reply exists', async () => {
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(new Date('2026-05-19T12:26:30.000Z'));
|
||||
tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'claude-team-advisory-'));
|
||||
setClaudeBasePathOverride(tmpDir);
|
||||
|
||||
const teamName = 'relay-release';
|
||||
const laneId = 'secondary:opencode:tom';
|
||||
const taskRef: TaskRef = {
|
||||
teamName,
|
||||
taskId: 'fb72209d-ea5b-45e0-9380-fe2e8235206e',
|
||||
displayId: 'fb72209d',
|
||||
};
|
||||
await writeOpenCodeDeliveryFixture({
|
||||
baseDir: tmpDir,
|
||||
teamName,
|
||||
laneId,
|
||||
records: [
|
||||
buildOpenCodeDeliveryRecord({
|
||||
teamName,
|
||||
laneId,
|
||||
taskRefs: [taskRef],
|
||||
}),
|
||||
],
|
||||
inboxes: {
|
||||
'team-lead': [
|
||||
{
|
||||
from: 'tom',
|
||||
to: 'team-lead',
|
||||
text: '#fb72209d done. API docs regenerated, diff empty.',
|
||||
timestamp: '2026-05-19T12:25:56.384Z',
|
||||
read: true,
|
||||
relayOfMessageId: 'assignment-1',
|
||||
source: 'runtime_delivery',
|
||||
messageId: 'visible-reply-1',
|
||||
taskRefs: [taskRef],
|
||||
summary: '#fb72209d done',
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
const service = new TeamMemberRuntimeAdvisoryService({
|
||||
findMemberLogs: vi.fn(async () => []),
|
||||
});
|
||||
const advisory = await service.getMemberAdvisory(teamName, 'tom');
|
||||
|
||||
expect(advisory).toBeNull();
|
||||
});
|
||||
|
||||
it('keeps stale OpenCode reconcile advisories visible until persisted proof exists', async () => {
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(new Date('2026-05-19T12:26:30.000Z'));
|
||||
tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'claude-team-advisory-'));
|
||||
setClaudeBasePathOverride(tmpDir);
|
||||
|
||||
const teamName = 'relay-release';
|
||||
const laneId = 'secondary:opencode:tom';
|
||||
await writeOpenCodeDeliveryFixture({
|
||||
baseDir: tmpDir,
|
||||
teamName,
|
||||
laneId,
|
||||
records: [buildOpenCodeDeliveryRecord({ teamName, laneId })],
|
||||
});
|
||||
|
||||
const service = new TeamMemberRuntimeAdvisoryService({
|
||||
findMemberLogs: vi.fn(async () => []),
|
||||
});
|
||||
const advisory = await service.getMemberAdvisory(teamName, 'tom');
|
||||
|
||||
expect(advisory).toMatchObject({
|
||||
kind: 'api_error',
|
||||
reasonCode: 'backend_error',
|
||||
});
|
||||
expect(advisory?.message).toContain(
|
||||
'opencode_session_stale_observe_scheduled_after_accepted_prompt'
|
||||
);
|
||||
});
|
||||
|
||||
it('keeps stale OpenCode advisories visible after unrelated later delivery success', async () => {
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(new Date('2026-05-19T12:30:00.000Z'));
|
||||
tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'claude-team-advisory-'));
|
||||
setClaudeBasePathOverride(tmpDir);
|
||||
|
||||
const teamName = 'relay-release';
|
||||
const laneId = 'secondary:opencode:tom';
|
||||
await writeOpenCodeDeliveryFixture({
|
||||
baseDir: tmpDir,
|
||||
teamName,
|
||||
laneId,
|
||||
records: [
|
||||
buildOpenCodeDeliveryRecord({ teamName, laneId }),
|
||||
buildOpenCodeDeliveryRecord({
|
||||
id: 'opencode-prompt:later-success',
|
||||
teamName,
|
||||
laneId,
|
||||
inboxMessageId: 'later-assignment',
|
||||
inboxTimestamp: '2026-05-19T12:24:00.000Z',
|
||||
status: 'responded',
|
||||
responseState: 'responded_visible_message',
|
||||
taskRefs: [
|
||||
{
|
||||
teamName,
|
||||
taskId: 'different-task',
|
||||
displayId: 'different',
|
||||
},
|
||||
],
|
||||
failedAt: null,
|
||||
respondedAt: '2026-05-19T12:25:30.000Z',
|
||||
lastObservedAt: '2026-05-19T12:25:30.000Z',
|
||||
updatedAt: '2026-05-19T12:25:45.000Z',
|
||||
inboxReadCommittedAt: '2026-05-19T12:25:45.000Z',
|
||||
visibleReplyMessageId: 'later-visible-reply',
|
||||
visibleReplyInbox: 'team-lead',
|
||||
visibleReplyCorrelation: 'relayOfMessageId',
|
||||
lastReason: null,
|
||||
diagnostics: [],
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
const service = new TeamMemberRuntimeAdvisoryService({
|
||||
findMemberLogs: vi.fn(async () => []),
|
||||
});
|
||||
const advisory = await service.getMemberAdvisory(teamName, 'tom');
|
||||
|
||||
expect(advisory).toMatchObject({
|
||||
kind: 'api_error',
|
||||
reasonCode: 'backend_error',
|
||||
});
|
||||
});
|
||||
|
||||
it('does not suppress stale OpenCode advisories for same-member replies without relay or task proof', async () => {
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(new Date('2026-05-19T12:26:30.000Z'));
|
||||
tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'claude-team-advisory-'));
|
||||
setClaudeBasePathOverride(tmpDir);
|
||||
|
||||
const teamName = 'relay-release';
|
||||
const laneId = 'secondary:opencode:tom';
|
||||
const taskRef: TaskRef = {
|
||||
teamName,
|
||||
taskId: 'fb72209d-ea5b-45e0-9380-fe2e8235206e',
|
||||
displayId: 'fb72209d',
|
||||
};
|
||||
await writeOpenCodeDeliveryFixture({
|
||||
baseDir: tmpDir,
|
||||
teamName,
|
||||
laneId,
|
||||
records: [
|
||||
buildOpenCodeDeliveryRecord({
|
||||
teamName,
|
||||
laneId,
|
||||
taskRefs: [taskRef],
|
||||
}),
|
||||
],
|
||||
inboxes: {
|
||||
'team-lead': [
|
||||
{
|
||||
from: 'tom',
|
||||
to: 'team-lead',
|
||||
text: 'Done on a different prompt.',
|
||||
timestamp: '2026-05-19T12:25:56.384Z',
|
||||
read: true,
|
||||
source: 'runtime_delivery',
|
||||
messageId: 'unrelated-reply',
|
||||
summary: 'Done',
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
const service = new TeamMemberRuntimeAdvisoryService({
|
||||
findMemberLogs: vi.fn(async () => []),
|
||||
});
|
||||
const advisory = await service.getMemberAdvisory(teamName, 'tom');
|
||||
|
||||
expect(advisory).toMatchObject({
|
||||
kind: 'api_error',
|
||||
reasonCode: 'backend_error',
|
||||
});
|
||||
});
|
||||
|
||||
it('does not suppress stale OpenCode advisories for task progress from another member', async () => {
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(new Date('2026-05-19T12:26:30.000Z'));
|
||||
tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'claude-team-advisory-'));
|
||||
setClaudeBasePathOverride(tmpDir);
|
||||
|
||||
const teamName = 'relay-release';
|
||||
const laneId = 'secondary:opencode:tom';
|
||||
const taskId = 'fb72209d-ea5b-45e0-9380-fe2e8235206e';
|
||||
const taskRef: TaskRef = { teamName, taskId, displayId: 'fb72209d' };
|
||||
await writeOpenCodeDeliveryFixture({
|
||||
baseDir: tmpDir,
|
||||
teamName,
|
||||
laneId,
|
||||
records: [
|
||||
buildOpenCodeDeliveryRecord({
|
||||
teamName,
|
||||
laneId,
|
||||
taskRefs: [taskRef],
|
||||
}),
|
||||
],
|
||||
tasks: [
|
||||
{
|
||||
id: taskId,
|
||||
displayId: 'fb72209d',
|
||||
subject: 'API docs',
|
||||
owner: 'tom',
|
||||
status: 'completed',
|
||||
updatedAt: '2026-05-19T12:25:56.384Z',
|
||||
comments: [
|
||||
{
|
||||
id: 'other-member-comment',
|
||||
author: 'alice',
|
||||
text: 'I verified this task.',
|
||||
createdAt: '2026-05-19T12:25:56.384Z',
|
||||
type: 'regular',
|
||||
},
|
||||
],
|
||||
historyEvents: [
|
||||
{
|
||||
id: 'other-member-status',
|
||||
type: 'status_changed',
|
||||
from: 'in_progress',
|
||||
to: 'completed',
|
||||
actor: 'alice',
|
||||
timestamp: '2026-05-19T12:25:56.384Z',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const service = new TeamMemberRuntimeAdvisoryService({
|
||||
findMemberLogs: vi.fn(async () => []),
|
||||
});
|
||||
const advisory = await service.getMemberAdvisory(teamName, 'tom');
|
||||
|
||||
expect(advisory).toMatchObject({
|
||||
kind: 'api_error',
|
||||
reasonCode: 'backend_error',
|
||||
});
|
||||
});
|
||||
|
||||
it('does not surface advisory for responded OpenCode records with committed visible proof', async () => {
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(new Date('2026-05-19T12:28:00.000Z'));
|
||||
tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'claude-team-advisory-'));
|
||||
setClaudeBasePathOverride(tmpDir);
|
||||
|
||||
const teamName = 'relay-release';
|
||||
const laneId = 'secondary:opencode:tom';
|
||||
await writeOpenCodeDeliveryFixture({
|
||||
baseDir: tmpDir,
|
||||
teamName,
|
||||
laneId,
|
||||
records: [
|
||||
buildOpenCodeDeliveryRecord({
|
||||
teamName,
|
||||
laneId,
|
||||
status: 'responded',
|
||||
responseState: 'responded_visible_message',
|
||||
inboxReadCommittedAt: '2026-05-19T12:27:04.858Z',
|
||||
visibleReplyMessageId: 'visible-reply-1',
|
||||
visibleReplyInbox: 'team-lead',
|
||||
visibleReplyCorrelation: 'relayOfMessageId',
|
||||
updatedAt: '2026-05-19T12:27:04.858Z',
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
const service = new TeamMemberRuntimeAdvisoryService({
|
||||
findMemberLogs: vi.fn(async () => []),
|
||||
});
|
||||
const advisory = await service.getMemberAdvisory(teamName, 'tom');
|
||||
|
||||
expect(advisory).toBeNull();
|
||||
});
|
||||
|
||||
it('suppresses stale OpenCode prompt delivery advisories after a visible runtime reply exists', async () => {
|
||||
tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'claude-team-advisory-'));
|
||||
setClaudeBasePathOverride(tmpDir);
|
||||
|
|
|
|||
|
|
@ -186,6 +186,84 @@ describe('TeamTaskStallPolicy', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it.each([
|
||||
['turn_ended_after_touch', 4],
|
||||
['touch_then_other_turns', 5],
|
||||
['mid_turn_after_touch', 10],
|
||||
] as const)('uses the aggressive work threshold for %s', (signal, thresholdMinutes) => {
|
||||
const task: TeamTask = {
|
||||
id: 'task-work-threshold',
|
||||
displayId: 'abcd4444',
|
||||
subject: 'Work threshold',
|
||||
owner: 'alice',
|
||||
status: 'in_progress',
|
||||
workIntervals: [{ startedAt: '2026-04-19T11:50:00.000Z' }],
|
||||
};
|
||||
const turnEndRow = createExactRow({
|
||||
sourceOrder: 2,
|
||||
messageUuid: 'msg-turn-end',
|
||||
systemSubtype: 'turn_duration',
|
||||
parsedMessage: createParsedMessage({
|
||||
uuid: 'msg-turn-end',
|
||||
type: 'system',
|
||||
}),
|
||||
});
|
||||
const laterAssistantRow = createExactRow({
|
||||
sourceOrder: 3,
|
||||
messageUuid: 'msg-later',
|
||||
parsedMessage: createParsedMessage({
|
||||
uuid: 'msg-later',
|
||||
type: 'assistant',
|
||||
}),
|
||||
});
|
||||
const postTouchRows =
|
||||
signal === 'touch_then_other_turns'
|
||||
? [turnEndRow, laterAssistantRow]
|
||||
: signal === 'mid_turn_after_touch'
|
||||
? [laterAssistantRow]
|
||||
: [turnEndRow];
|
||||
const snapshot = createSnapshot({
|
||||
activeTasks: [task],
|
||||
allTasksById: new Map([[task.id, task]]),
|
||||
inProgressTasks: [task],
|
||||
recordsByTaskId: new Map([[task.id, [createRecord()]]]),
|
||||
exactRowsByFilePath: new Map([
|
||||
[
|
||||
'/tmp/session.jsonl',
|
||||
[
|
||||
createExactRow({
|
||||
messageUuid: 'msg-touch',
|
||||
toolUseIds: ['tool-1'],
|
||||
}),
|
||||
...postTouchRows,
|
||||
],
|
||||
],
|
||||
]),
|
||||
});
|
||||
const touchAtMs = Date.parse('2026-04-19T12:00:00.000Z');
|
||||
|
||||
expect(
|
||||
policy.evaluateWork({
|
||||
now: new Date(touchAtMs + thresholdMinutes * 60_000 - 1),
|
||||
task,
|
||||
snapshot,
|
||||
})
|
||||
).toMatchObject({
|
||||
status: 'skip',
|
||||
skipReason: 'below_threshold',
|
||||
});
|
||||
expect(
|
||||
policy.evaluateWork({
|
||||
now: new Date(touchAtMs + thresholdMinutes * 60_000),
|
||||
task,
|
||||
snapshot,
|
||||
})
|
||||
).toMatchObject({
|
||||
status: 'alert',
|
||||
signal,
|
||||
});
|
||||
});
|
||||
|
||||
it('alerts OpenCode-owned tasks faster after weak start-only task comments', () => {
|
||||
const task: TeamTask = {
|
||||
id: 'task-open-weak',
|
||||
|
|
@ -259,13 +337,26 @@ describe('TeamTaskStallPolicy', () => {
|
|||
]),
|
||||
});
|
||||
|
||||
const evaluation = policy.evaluateWork({
|
||||
now: new Date('2026-04-19T12:07:00.000Z'),
|
||||
task,
|
||||
snapshot,
|
||||
});
|
||||
const touchAtMs = Date.parse('2026-04-19T12:00:00.000Z');
|
||||
|
||||
expect(evaluation).toMatchObject({
|
||||
expect(
|
||||
policy.evaluateWork({
|
||||
now: new Date(touchAtMs + 100_000 - 1),
|
||||
task,
|
||||
snapshot,
|
||||
})
|
||||
).toMatchObject({
|
||||
status: 'skip',
|
||||
taskId: 'task-open-weak',
|
||||
skipReason: 'below_threshold',
|
||||
});
|
||||
expect(
|
||||
policy.evaluateWork({
|
||||
now: new Date(touchAtMs + 100_000),
|
||||
task,
|
||||
snapshot,
|
||||
})
|
||||
).toMatchObject({
|
||||
status: 'alert',
|
||||
taskId: 'task-open-weak',
|
||||
progressSignal: 'weak_start_only',
|
||||
|
|
@ -273,7 +364,7 @@ describe('TeamTaskStallPolicy', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('keeps existing thresholds for weak comments from non-OpenCode owners', () => {
|
||||
it('uses normal work thresholds for weak comments from non-OpenCode owners', () => {
|
||||
const task: TeamTask = {
|
||||
id: 'task-codex-weak',
|
||||
displayId: 'feed2222',
|
||||
|
|
@ -347,7 +438,7 @@ describe('TeamTaskStallPolicy', () => {
|
|||
});
|
||||
|
||||
const evaluation = policy.evaluateWork({
|
||||
now: new Date('2026-04-19T12:07:00.000Z'),
|
||||
now: new Date('2026-04-19T12:03:00.000Z'),
|
||||
task,
|
||||
snapshot,
|
||||
});
|
||||
|
|
@ -433,7 +524,7 @@ describe('TeamTaskStallPolicy', () => {
|
|||
});
|
||||
|
||||
const evaluation = policy.evaluateWork({
|
||||
now: new Date('2026-04-19T12:07:00.000Z'),
|
||||
now: new Date('2026-04-19T12:03:00.000Z'),
|
||||
task,
|
||||
snapshot,
|
||||
});
|
||||
|
|
@ -751,6 +842,123 @@ describe('TeamTaskStallPolicy', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it.each([
|
||||
['turn_ended_after_touch', 5],
|
||||
['touch_then_other_turns', 6],
|
||||
['mid_turn_after_touch', 12],
|
||||
] as const)('uses the aggressive review threshold for %s', (signal, thresholdMinutes) => {
|
||||
const task: TeamTask = {
|
||||
id: 'task-review-threshold',
|
||||
displayId: 'c0ffee55',
|
||||
subject: 'Review threshold',
|
||||
status: 'completed',
|
||||
reviewState: 'review',
|
||||
historyEvents: [
|
||||
{
|
||||
id: 'evt-review-started',
|
||||
type: 'review_started',
|
||||
timestamp: '2026-04-19T12:00:00.000Z',
|
||||
from: 'review',
|
||||
to: 'review',
|
||||
actor: 'bob',
|
||||
},
|
||||
],
|
||||
};
|
||||
const turnEndRow = createExactRow({
|
||||
filePath: '/tmp/review-threshold.jsonl',
|
||||
sourceOrder: 2,
|
||||
messageUuid: 'msg-review-threshold-end',
|
||||
systemSubtype: 'turn_duration',
|
||||
parsedMessage: createParsedMessage({
|
||||
uuid: 'msg-review-threshold-end',
|
||||
type: 'system',
|
||||
}),
|
||||
});
|
||||
const laterAssistantRow = createExactRow({
|
||||
filePath: '/tmp/review-threshold.jsonl',
|
||||
sourceOrder: 3,
|
||||
messageUuid: 'msg-review-threshold-later',
|
||||
parsedMessage: createParsedMessage({
|
||||
uuid: 'msg-review-threshold-later',
|
||||
type: 'assistant',
|
||||
}),
|
||||
});
|
||||
const postTouchRows =
|
||||
signal === 'touch_then_other_turns'
|
||||
? [turnEndRow, laterAssistantRow]
|
||||
: signal === 'mid_turn_after_touch'
|
||||
? [laterAssistantRow]
|
||||
: [turnEndRow];
|
||||
const record = createRecord({
|
||||
timestamp: '2026-04-19T12:00:00.000Z',
|
||||
actor: {
|
||||
memberName: 'bob',
|
||||
role: 'member',
|
||||
sessionId: 'session-b',
|
||||
isSidechain: true,
|
||||
},
|
||||
actorContext: {
|
||||
relation: 'same_task',
|
||||
activePhase: 'review',
|
||||
},
|
||||
action: {
|
||||
canonicalToolName: 'review_start',
|
||||
category: 'review',
|
||||
toolUseId: 'tool-review-threshold',
|
||||
},
|
||||
source: {
|
||||
messageUuid: 'msg-review-threshold',
|
||||
filePath: '/tmp/review-threshold.jsonl',
|
||||
toolUseId: 'tool-review-threshold',
|
||||
sourceOrder: 1,
|
||||
},
|
||||
});
|
||||
const snapshot = createSnapshot({
|
||||
activeTasks: [task],
|
||||
allTasksById: new Map([[task.id, task]]),
|
||||
reviewOpenTasks: [task],
|
||||
resolvedReviewersByTaskId: new Map([
|
||||
[task.id, { reviewer: 'bob', source: 'history_review_started_actor' }],
|
||||
]),
|
||||
recordsByTaskId: new Map([[task.id, [record]]]),
|
||||
exactRowsByFilePath: new Map([
|
||||
[
|
||||
'/tmp/review-threshold.jsonl',
|
||||
[
|
||||
createExactRow({
|
||||
filePath: '/tmp/review-threshold.jsonl',
|
||||
messageUuid: 'msg-review-threshold',
|
||||
toolUseIds: ['tool-review-threshold'],
|
||||
}),
|
||||
...postTouchRows,
|
||||
],
|
||||
],
|
||||
]),
|
||||
});
|
||||
const touchAtMs = Date.parse('2026-04-19T12:00:00.000Z');
|
||||
|
||||
expect(
|
||||
policy.evaluateReview({
|
||||
now: new Date(touchAtMs + thresholdMinutes * 60_000 - 1),
|
||||
task,
|
||||
snapshot,
|
||||
})
|
||||
).toMatchObject({
|
||||
status: 'skip',
|
||||
skipReason: 'below_threshold',
|
||||
});
|
||||
expect(
|
||||
policy.evaluateReview({
|
||||
now: new Date(touchAtMs + thresholdMinutes * 60_000),
|
||||
task,
|
||||
snapshot,
|
||||
})
|
||||
).toMatchObject({
|
||||
status: 'alert',
|
||||
signal,
|
||||
});
|
||||
});
|
||||
|
||||
it('alerts for started-review stall when review_started actor is missing but same-task reviewer touch exists after the review start', () => {
|
||||
const task: TeamTask = {
|
||||
id: 'task-d',
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import {
|
||||
getTeamTaskStallActivationGraceMs,
|
||||
getOpenCodeWeakStartStallThresholdMs,
|
||||
getTeamTaskStallActivationGraceMs,
|
||||
getTeamTaskStallScanIntervalMs,
|
||||
getTeamTaskStallStartupGraceMs,
|
||||
isOpenCodeTaskStallRemediationEnabled,
|
||||
|
|
@ -21,10 +21,10 @@ describe('stallMonitor feature gates', () => {
|
|||
expect(isOpenCodeTaskStallRemediationEnabled()).toBe(true);
|
||||
expect(isTeamTaskStallScannerEnabled()).toBe(true);
|
||||
expect(isTeamTaskStallAlertsEnabled()).toBe(true);
|
||||
expect(getTeamTaskStallScanIntervalMs()).toBe(60_000);
|
||||
expect(getTeamTaskStallScanIntervalMs()).toBe(30_000);
|
||||
expect(getTeamTaskStallStartupGraceMs()).toBe(180_000);
|
||||
expect(getTeamTaskStallActivationGraceMs()).toBe(120_000);
|
||||
expect(getOpenCodeWeakStartStallThresholdMs()).toBe(120_000);
|
||||
expect(getTeamTaskStallActivationGraceMs()).toBe(60_000);
|
||||
expect(getOpenCodeWeakStartStallThresholdMs()).toBe(100_000);
|
||||
});
|
||||
|
||||
it('parses truthy and falsy environment values', () => {
|
||||
|
|
@ -75,6 +75,6 @@ describe('stallMonitor feature gates', () => {
|
|||
expect(isOpenCodeTaskStallRemediationEnabled()).toBe(true);
|
||||
expect(isTeamTaskStallScannerEnabled()).toBe(true);
|
||||
expect(isTeamTaskStallAlertsEnabled()).toBe(true);
|
||||
expect(getOpenCodeWeakStartStallThresholdMs()).toBe(120_000);
|
||||
expect(getOpenCodeWeakStartStallThresholdMs()).toBe(100_000);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1548,6 +1548,125 @@ describe('TeamModelSelector disabled Codex models', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('allows selecting unauthenticated OpenCode when free models are available', async () => {
|
||||
vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true);
|
||||
storeState.cliStatus = {
|
||||
providers: [
|
||||
{
|
||||
providerId: 'opencode',
|
||||
supported: true,
|
||||
authenticated: false,
|
||||
statusMessage: 'Provider not connected',
|
||||
detailMessage: null,
|
||||
capabilities: { teamLaunch: false },
|
||||
models: ['opencode/big-pickle'],
|
||||
modelVerificationState: 'idle',
|
||||
modelAvailability: [],
|
||||
},
|
||||
],
|
||||
};
|
||||
const host = document.createElement('div');
|
||||
document.body.appendChild(host);
|
||||
const root = createRoot(host);
|
||||
const onProviderChange = vi.fn();
|
||||
|
||||
const ControlledSelector = (): React.JSX.Element => {
|
||||
const [provider, setProvider] = React.useState<'anthropic' | 'opencode'>('anthropic');
|
||||
return React.createElement(TeamModelSelector, {
|
||||
providerId: provider,
|
||||
onProviderChange: (nextProvider) => {
|
||||
onProviderChange(nextProvider);
|
||||
if (nextProvider === 'anthropic' || nextProvider === 'opencode') {
|
||||
setProvider(nextProvider);
|
||||
}
|
||||
},
|
||||
value: '',
|
||||
onValueChange: () => undefined,
|
||||
});
|
||||
};
|
||||
|
||||
await act(async () => {
|
||||
root.render(React.createElement(ControlledSelector));
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
const openCodeButton = Array.from(host.querySelectorAll('button')).find((button) =>
|
||||
button.textContent?.includes('OpenCode')
|
||||
);
|
||||
expect(openCodeButton?.hasAttribute('disabled')).toBe(false);
|
||||
expect(openCodeButton?.getAttribute('aria-disabled')).toBeNull();
|
||||
expect(openCodeButton?.textContent).not.toContain('Auth');
|
||||
|
||||
await act(async () => {
|
||||
openCodeButton?.dispatchEvent(new MouseEvent('click', { bubbles: true }));
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
expect(onProviderChange).toHaveBeenCalledWith('opencode');
|
||||
expect(host.textContent).toContain('OpenCode free models are available');
|
||||
expect(host.textContent).toContain('provider connection optional');
|
||||
expect(host.textContent).toContain(
|
||||
'You can use free OpenCode models such as Big Pickle without connecting a provider.'
|
||||
);
|
||||
expect(host.textContent).not.toContain('OpenCode is not ready for team launch');
|
||||
expect(host.textContent).not.toContain('team launch available');
|
||||
expect(host.textContent).toContain('big-pickle');
|
||||
|
||||
await act(async () => {
|
||||
root.unmount();
|
||||
await Promise.resolve();
|
||||
});
|
||||
});
|
||||
|
||||
it('keeps unauthenticated OpenCode selectable but does not promise free models when none are listed', async () => {
|
||||
vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true);
|
||||
storeState.cliStatus = {
|
||||
providers: [
|
||||
{
|
||||
providerId: 'opencode',
|
||||
supported: true,
|
||||
authenticated: false,
|
||||
statusMessage: 'Provider not connected',
|
||||
detailMessage: null,
|
||||
capabilities: { teamLaunch: false },
|
||||
models: ['openai/gpt-5.4-mini'],
|
||||
modelVerificationState: 'idle',
|
||||
modelAvailability: [],
|
||||
},
|
||||
],
|
||||
};
|
||||
const host = document.createElement('div');
|
||||
document.body.appendChild(host);
|
||||
const root = createRoot(host);
|
||||
|
||||
await act(async () => {
|
||||
root.render(
|
||||
React.createElement(TeamModelSelector, {
|
||||
providerId: 'opencode',
|
||||
onProviderChange: () => undefined,
|
||||
value: '',
|
||||
onValueChange: () => undefined,
|
||||
})
|
||||
);
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
const openCodeButton = Array.from(host.querySelectorAll('button')).find((button) =>
|
||||
button.textContent?.includes('OpenCode')
|
||||
);
|
||||
expect(openCodeButton?.hasAttribute('disabled')).toBe(false);
|
||||
expect(host.textContent).toContain('OpenCode provider is not connected');
|
||||
expect(host.textContent).toContain('no free OpenCode model is listed yet');
|
||||
expect(host.textContent).toContain('provider-backed models need setup');
|
||||
expect(host.textContent).not.toContain('team launch available');
|
||||
expect(host.textContent).not.toContain('OpenCode free models are available');
|
||||
|
||||
await act(async () => {
|
||||
root.unmount();
|
||||
await Promise.resolve();
|
||||
});
|
||||
});
|
||||
|
||||
it('does not normalize the selected model while viewing OpenCode readiness diagnostics', async () => {
|
||||
vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true);
|
||||
const host = document.createElement('div');
|
||||
|
|
|
|||
Loading…
Reference in a new issue