feat(runtime): improve provider readiness diagnostics

This commit is contained in:
777genius 2026-05-19 16:19:38 +03:00
parent 28edecc5e0
commit 85959b6954
53 changed files with 1654 additions and 272 deletions

View file

@ -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>

View file

@ -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 {

View file

@ -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;

View file

@ -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 {

View file

@ -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 {

View file

@ -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;

View file

@ -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;
}

View file

@ -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">

View file

@ -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;

View file

@ -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
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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>

View file

@ -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."
},

View file

@ -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."
},

View file

@ -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."
},

View file

@ -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."
},

View file

@ -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."
},

View file

@ -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 एजेंट टीमों के लिए मुफ़्त ओपन-सोर्स डेस्कटॉप ऐप।"
},

View file

@ -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エージェントチーム用の無料オープンソースデスクトップアプリ。"
},

View file

@ -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."
},

View file

@ -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."
},

View file

@ -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 智能体团队的免费开源桌面应用。"
},

View file

@ -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.

View file

@ -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 |

View file

@ -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
{

View file

@ -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

View file

@ -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).

View file

@ -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 | Достигнут лимит запросов | Дождитесь сброса или смените модель/провайдера |

View file

@ -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
{

View file

@ -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

View file

@ -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;
}

View file

@ -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));
});
});

View file

@ -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');

View file

@ -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,

View file

@ -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;

View file

@ -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;

View file

@ -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(

View file

@ -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);
}

View file

@ -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';

View file

@ -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' });
});
});

View file

@ -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 () => {

View file

@ -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);

View file

@ -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',

View file

@ -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);
});
});

View file

@ -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');