add worktree & up task、teammate etc

This commit is contained in:
CrazyBoyM
2026-02-24 01:44:44 +08:00
parent c6a27ef1d7
commit aea8844bac
54 changed files with 2404 additions and 210 deletions

View File

@@ -17,6 +17,7 @@ import s08Annotations from "@/data/annotations/s08.json";
import s09Annotations from "@/data/annotations/s09.json";
import s10Annotations from "@/data/annotations/s10.json";
import s11Annotations from "@/data/annotations/s11.json";
import s12Annotations from "@/data/annotations/s12.json";
interface Decision {
id: string;
@@ -44,6 +45,7 @@ const ANNOTATIONS: Record<string, AnnotationFile> = {
s09: s09Annotations as AnnotationFile,
s10: s10Annotations as AnnotationFile,
s11: s11Annotations as AnnotationFile,
s12: s12Annotations as AnnotationFile,
};
interface DesignDecisionsProps {

View File

@@ -20,6 +20,7 @@ const scenarioModules: Record<string, () => Promise<{ default: Scenario }>> = {
s09: () => import("@/data/scenarios/s09.json") as Promise<{ default: Scenario }>,
s10: () => import("@/data/scenarios/s10.json") as Promise<{ default: Scenario }>,
s11: () => import("@/data/scenarios/s11.json") as Promise<{ default: Scenario }>,
s12: () => import("@/data/scenarios/s12.json") as Promise<{ default: Scenario }>,
};
interface AgentLoopSimulatorProps {

View File

@@ -18,6 +18,7 @@ const visualizations: Record<
s09: lazy(() => import("./s09-agent-teams")),
s10: lazy(() => import("./s10-team-protocols")),
s11: lazy(() => import("./s11-autonomous-agents")),
s12: lazy(() => import("./s12-worktree-task-isolation")),
};
export function SessionVisualization({ version }: { version: string }) {

View File

@@ -62,7 +62,7 @@ const STEPS = [
{
title: "Clean Context",
description:
"The parent gets a clean summary without context bloat. This is process isolation for LLMs.",
"The parent gets a clean summary without context bloat. This is fresh-context isolation via messages[].",
},
];

View File

@@ -0,0 +1,278 @@
"use client";
import { motion } from "framer-motion";
import { useSteppedVisualization } from "@/hooks/useSteppedVisualization";
import { StepControls } from "@/components/visualizations/shared/step-controls";
type TaskStatus = "pending" | "in_progress" | "completed";
interface TaskRow {
id: number;
subject: string;
status: TaskStatus;
worktree: string;
}
interface WorktreeRow {
name: string;
branch: string;
task: string;
state: "none" | "active" | "kept" | "removed";
}
interface Lane {
name: string;
files: string[];
highlight?: boolean;
}
interface StepState {
title: string;
desc: string;
tasks: TaskRow[];
worktrees: WorktreeRow[];
lanes: Lane[];
op: string;
}
const STEPS: StepState[] = [
{
title: "Single Workspace Pain",
desc: "Two tasks are active, but both edits would hit one directory and collide.",
op: "task_create x2",
tasks: [
{ id: 1, subject: "Auth refactor", status: "in_progress", worktree: "" },
{ id: 2, subject: "UI login polish", status: "in_progress", worktree: "" },
],
worktrees: [],
lanes: [
{ name: "main", files: ["auth/service.py", "ui/Login.tsx"], highlight: true },
{ name: "wt/auth-refactor", files: [] },
{ name: "wt/ui-login", files: [] },
],
},
{
title: "Allocate Lane for Task 1",
desc: "Create a worktree lane and associate it with task 1 for clear ownership.",
op: "worktree_create(name='auth-refactor', task_id=1)",
tasks: [
{ id: 1, subject: "Auth refactor", status: "in_progress", worktree: "auth-refactor" },
{ id: 2, subject: "UI login polish", status: "in_progress", worktree: "" },
],
worktrees: [
{ name: "auth-refactor", branch: "wt/auth-refactor", task: "#1", state: "active" },
],
lanes: [
{ name: "main", files: ["ui/Login.tsx"] },
{ name: "wt/auth-refactor", files: ["auth/service.py"], highlight: true },
{ name: "wt/ui-login", files: [] },
],
},
{
title: "Allocate Lane for Task 2",
desc: "Lane creation and task association can be separate. Here task 2 binds after lane creation.",
op: "worktree_create(name='ui-login')\ntask_bind_worktree(task_id=2, worktree='ui-login')",
tasks: [
{ id: 1, subject: "Auth refactor", status: "in_progress", worktree: "auth-refactor" },
{ id: 2, subject: "UI login polish", status: "in_progress", worktree: "ui-login" },
],
worktrees: [
{ name: "auth-refactor", branch: "wt/auth-refactor", task: "#1", state: "active" },
{ name: "ui-login", branch: "wt/ui-login", task: "#2", state: "active" },
],
lanes: [
{ name: "main", files: [] },
{ name: "wt/auth-refactor", files: ["auth/service.py"] },
{ name: "wt/ui-login", files: ["ui/Login.tsx"], highlight: true },
],
},
{
title: "Run Commands in Isolated Lanes",
desc: "Each command routes by selected lane directory, not by the shared root.",
op: "worktree_run('auth-refactor', 'pytest tests/auth -q')",
tasks: [
{ id: 1, subject: "Auth refactor", status: "in_progress", worktree: "auth-refactor" },
{ id: 2, subject: "UI login polish", status: "in_progress", worktree: "ui-login" },
],
worktrees: [
{ name: "auth-refactor", branch: "wt/auth-refactor", task: "#1", state: "active" },
{ name: "ui-login", branch: "wt/ui-login", task: "#2", state: "active" },
],
lanes: [
{ name: "main", files: [] },
{ name: "wt/auth-refactor", files: ["auth/service.py", "tests/auth/test_login.py"], highlight: true },
{ name: "wt/ui-login", files: ["ui/Login.tsx", "ui/Login.css"] },
],
},
{
title: "Keep One Lane, Close Another",
desc: "Closeout can mix decisions: keep ui-login active for follow-up, remove auth-refactor and complete task 1.",
op: "worktree_keep('ui-login')\nworktree_remove('auth-refactor', complete_task=true)\nworktree_events(limit=10)",
tasks: [
{ id: 1, subject: "Auth refactor", status: "completed", worktree: "" },
{ id: 2, subject: "UI login polish", status: "in_progress", worktree: "ui-login" },
],
worktrees: [
{ name: "auth-refactor", branch: "wt/auth-refactor", task: "#1", state: "removed" },
{ name: "ui-login", branch: "wt/ui-login", task: "#2", state: "kept" },
],
lanes: [
{ name: "main", files: [] },
{ name: "wt/auth-refactor", files: [] },
{ name: "wt/ui-login", files: ["ui/Login.tsx"], highlight: true },
],
},
{
title: "Isolation + Coordination + Events",
desc: "The board tracks shared truth, worktree lanes isolate execution, and events provide auditable side-channel traces.",
op: "task_list + worktree_list + worktree_events",
tasks: [
{ id: 1, subject: "Auth refactor", status: "completed", worktree: "" },
{ id: 2, subject: "UI login polish", status: "in_progress", worktree: "ui-login" },
],
worktrees: [
{ name: "auth-refactor", branch: "wt/auth-refactor", task: "#1", state: "removed" },
{ name: "ui-login", branch: "wt/ui-login", task: "#2", state: "kept" },
],
lanes: [
{ name: "main", files: [] },
{ name: "wt/auth-refactor", files: [] },
{ name: "wt/ui-login", files: ["ui/Login.tsx"], highlight: true },
],
},
];
function statusClass(status: TaskStatus): string {
if (status === "completed") return "bg-emerald-100 text-emerald-700 dark:bg-emerald-900/30 dark:text-emerald-300";
if (status === "in_progress") return "bg-amber-100 text-amber-700 dark:bg-amber-900/30 dark:text-amber-300";
return "bg-zinc-100 text-zinc-700 dark:bg-zinc-800 dark:text-zinc-300";
}
function worktreeClass(state: WorktreeRow["state"]): string {
if (state === "active") return "border-emerald-300 bg-emerald-50 dark:border-emerald-800 dark:bg-emerald-900/20";
if (state === "kept") return "border-sky-300 bg-sky-50 dark:border-sky-800 dark:bg-sky-900/20";
if (state === "removed") return "border-zinc-200 bg-zinc-100 opacity-70 dark:border-zinc-700 dark:bg-zinc-800";
return "border-zinc-200 bg-white dark:border-zinc-700 dark:bg-zinc-900";
}
export default function WorktreeTaskIsolation({ title }: { title?: string }) {
const vis = useSteppedVisualization({ totalSteps: STEPS.length, autoPlayInterval: 2600 });
const step = STEPS[vis.currentStep];
return (
<section className="min-h-[500px] space-y-4">
<h2 className="text-xl font-semibold text-zinc-900 dark:text-zinc-100">
{title || "Worktree Task Isolation"}
</h2>
<div className="rounded-lg border border-zinc-200 bg-white p-4 dark:border-zinc-700 dark:bg-zinc-900">
<div className="mb-3 rounded-md border border-blue-200 bg-blue-50 px-3 py-2 font-mono text-xs text-blue-700 dark:border-blue-900 dark:bg-blue-950/30 dark:text-blue-300">
{step.op}
</div>
<div className="grid gap-3 lg:grid-cols-3">
<div className="rounded-md border border-zinc-200 dark:border-zinc-700">
<div className="border-b border-zinc-200 bg-zinc-50 px-3 py-2 text-xs font-semibold uppercase tracking-wide text-zinc-600 dark:border-zinc-700 dark:bg-zinc-800 dark:text-zinc-300">
Task Board (.tasks)
</div>
<div className="space-y-2 p-2">
{step.tasks.map((task) => (
<motion.div
key={`${task.id}-${task.status}-${task.worktree}`}
initial={{ opacity: 0, y: 6 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.25 }}
className="rounded border border-zinc-200 p-2 text-xs dark:border-zinc-700"
>
<div className="flex items-center justify-between gap-2">
<span className="font-mono text-zinc-500 dark:text-zinc-400">#{task.id}</span>
<span className={`rounded px-1.5 py-0.5 text-[10px] font-semibold ${statusClass(task.status)}`}>
{task.status}
</span>
</div>
<div className="mt-1 font-medium text-zinc-800 dark:text-zinc-100">{task.subject}</div>
<div className="mt-1 font-mono text-[10px] text-zinc-500 dark:text-zinc-400">
worktree: {task.worktree || "-"}
</div>
</motion.div>
))}
</div>
</div>
<div className="rounded-md border border-zinc-200 dark:border-zinc-700">
<div className="border-b border-zinc-200 bg-zinc-50 px-3 py-2 text-xs font-semibold uppercase tracking-wide text-zinc-600 dark:border-zinc-700 dark:bg-zinc-800 dark:text-zinc-300">
Worktree Index (.worktrees/index.json)
</div>
<div className="space-y-2 p-2">
{step.worktrees.length === 0 && (
<div className="rounded border border-dashed border-zinc-300 px-3 py-4 text-center text-xs text-zinc-500 dark:border-zinc-700 dark:text-zinc-400">
no worktrees yet
</div>
)}
{step.worktrees.map((wt) => (
<motion.div
key={`${wt.name}-${wt.state}`}
initial={{ opacity: 0, y: 6 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.25 }}
className={`rounded border p-2 text-xs ${worktreeClass(wt.state)}`}
>
<div className="font-mono text-[11px] font-semibold text-zinc-800 dark:text-zinc-100">{wt.name}</div>
<div className="font-mono text-[10px] text-zinc-500 dark:text-zinc-400">{wt.branch}</div>
<div className="mt-1 text-[10px] text-zinc-600 dark:text-zinc-300">task: {wt.task}</div>
</motion.div>
))}
</div>
</div>
<div className="rounded-md border border-zinc-200 dark:border-zinc-700">
<div className="border-b border-zinc-200 bg-zinc-50 px-3 py-2 text-xs font-semibold uppercase tracking-wide text-zinc-600 dark:border-zinc-700 dark:bg-zinc-800 dark:text-zinc-300">
Execution Lanes
</div>
<div className="space-y-2 p-2">
{step.lanes.map((lane) => (
<motion.div
key={`${lane.name}-${lane.files.join(",")}`}
initial={{ opacity: 0, x: -4 }}
animate={{ opacity: 1, x: 0 }}
transition={{ duration: 0.25 }}
className={`rounded border p-2 text-xs ${
lane.highlight
? "border-blue-300 bg-blue-50 dark:border-blue-800 dark:bg-blue-900/20"
: "border-zinc-200 bg-white dark:border-zinc-700 dark:bg-zinc-900"
}`}
>
<div className="font-mono text-[11px] font-semibold text-zinc-800 dark:text-zinc-100">{lane.name}</div>
<div className="mt-1 space-y-1 font-mono text-[10px] text-zinc-500 dark:text-zinc-400">
{lane.files.length === 0 ? (
<div>(no changes)</div>
) : (
lane.files.map((f) => <div key={f}>{f}</div>)
)}
</div>
</motion.div>
))}
</div>
</div>
</div>
<div className="mt-4 rounded-md border border-zinc-200 bg-zinc-50 px-3 py-2 text-sm dark:border-zinc-700 dark:bg-zinc-800/60">
<div className="font-medium text-zinc-800 dark:text-zinc-100">{step.title}</div>
<div className="text-zinc-600 dark:text-zinc-300">{step.desc}</div>
</div>
</div>
<StepControls
currentStep={vis.currentStep}
totalSteps={vis.totalSteps}
onPrev={vis.prev}
onNext={vis.next}
onReset={vis.reset}
isPlaying={vis.isPlaying}
onToggleAutoPlay={vis.toggleAutoPlay}
stepTitle={step.title}
stepDescription={step.desc}
/>
</section>
);
}

View File

@@ -30,17 +30,59 @@
}
},
{
"id": "task-replaces-todo",
"title": "TaskManager Replaces TodoWrite",
"description": "TaskManager is the multi-agent evolution of TodoWrite. Same core concept (a list of items with statuses) but with critical additions: file persistence (survives crashes), dependency tracking (blocks/blockedBy), ownership (which agent is working on what), and multi-process safety. TodoWrite was designed for a single agent tracking its own work in memory. TaskManager is designed for a team of agents coordinating through the filesystem. The API is intentionally similar so the conceptual upgrade path is clear.",
"alternatives": "Keeping TodoWrite for single-agent use and adding TaskManager only for multi-agent scenarios would avoid breaking the single-agent experience. But maintaining two systems with overlapping functionality increases complexity. TaskManager is a strict superset of TodoWrite -- a single agent using TaskManager just ignores the multi-agent features.",
"id": "task-default-todo-coexistence",
"title": "Task as Course Default, Todo Still Useful",
"description": "TaskManager extends the Todo mental model and becomes the default workflow from s07 onward in this course. This 'default' is a course sequencing choice, not a universal runtime default claim. Both track work items with statuses, but TaskManager adds file persistence (survives crashes), dependency tracking (blocks/blockedBy), ownership fields, and multi-process coordination. Todo remains useful for short, linear, one-shot tracking where heavyweight coordination is unnecessary.",
"alternatives": "Using only Todo keeps the model minimal but weak for long-running or collaborative work. Using only Task everywhere maximizes consistency but can feel heavy for tiny one-off tasks. Reminder signals are omission-reduction hints, not implicit mode switches; Task/Todo choice should still come from workflow intent and available tools.",
"zh": {
"title": "TaskManager 取代 TodoWrite",
"description": "TaskManager TodoWrite 的多代理进化版。核心概念相同(带状态的项目列表),但增加了关键能力:文件持久化(崩溃后存活、依赖追踪blocks/blockedBy所有权(哪个 agent 在处理什么、以及多进程安全。TodoWrite 为单 agent 在内存中追踪自身工作而设计。TaskManager 为代理团队通过文件系统协调而设计。API 刻意保持相似,使概念升级路径清晰。"
"title": "Task 为课程主线Todo 仍有适用场景",
"description": "TaskManager 延续了 Todo 的心智模型,并在本课程 s07 之后成为默认主线。两者都管理带状态的任务项,但 TaskManager 增加了文件持久化(崩溃后可恢复、依赖追踪blocks/blockedByowner 字段与多进程协作能力。Todo 仍适合短、线性、一次性的轻量跟踪。"
},
"ja": {
"title": "TaskManager が TodoWrite を置き換え",
"description": "TaskManager は TodoWrite のマルチエージェント進化版です。コア概念は同じ(ステータス付きの項目リスト)ですが、重要な追加があります:ファイル永続化(クラッシュ後も存続、依存関係追跡blocks/blockedBy所有権どのエージェントが何を担当しているか、マルチプロセス安全性。TodoWrite は単一エージェントがメモリ内で自身の作業を追跡するために設計されました。TaskManager はエージェントチームがファイルシステムを通じて連携するために設計されています。API は意図的に類似させ、概念的なアップグレードパスを明確にしています。"
"title": "Task を主線にしつつ Todo も併存",
"description": "TaskManager は Todo のメンタルモデルを拡張し、本コースでは s07 以降のデフォルトになる。どちらもステータス付き作業項目を扱うが、TaskManager にはファイル永続化(クラッシュ耐性、依存関係追跡blocks/blockedByowner、マルチプロセス協調がある。Todo は短く直線的な単発作業では引き続き有効。"
}
},
{
"id": "task-write-discipline",
"title": "Durability Needs Write Discipline",
"description": "File persistence reduces context loss, but it does not remove concurrent-write risks by itself. Before writing task state, reload the JSON, validate expected status/dependency fields, and then save atomically. This prevents one agent from silently overwriting another agent's transition.",
"alternatives": "Blind overwrite writes are simpler but can corrupt coordination state under parallel execution. A database with optimistic locking would enforce stronger safety, but the course keeps file-based state for zero-dependency teaching.",
"zh": {
"title": "持久化仍需要写入纪律",
"description": "文件持久化能降低上下文丢失,但不会自动消除并发写入风险。写任务状态前应先重读 JSON、校验 `status/blockedBy` 是否符合预期,再原子写回,避免不同 agent 悄悄覆盖彼此状态。"
},
"ja": {
"title": "耐久性には書き込み規律が必要",
"description": "ファイル永続化だけでは並行書き込み競合は防げない。更新前に JSON を再読込し、`status/blockedBy` を検証して原子的に保存することで、他エージェントの遷移上書きを防ぐ。"
}
},
{
"id": "reminder-advisory-not-switch",
"title": "Reminder Is Advisory, Not a Mode Switch",
"description": "Reminder signals should be treated as omission-reduction hints, not as control-plane switches. Choosing Task vs Todo should come from workflow intent and currently available tools, while reminders only nudge usage when tracking appears stale.",
"alternatives": "Treating reminders as implicit mode selectors looks convenient, but it hides decision boundaries and makes behavior harder to reason about during long sessions.",
"zh": {
"title": "Reminder 是提示,不是模式开关",
"description": "Reminder 信号用于降低遗漏不应当被当作控制面的模式切换器。Task/Todo 的选择应由工作流意图与可用工具决定,提醒只在追踪滞后时提供轻量提示。"
},
"ja": {
"title": "Reminder は助言でありモード切替ではない",
"description": "Reminder は取りこぼしを減らすための助言であり、制御面のモード切替として扱わない。Task/Todo の選択はワークフロー意図と利用可能ツールで決め、Reminder は追跡が滞ったときに軽く促す。"
}
},
{
"id": "todo-task-fast-matrix",
"title": "Todo/Task Fast Decision Matrix",
"description": "Use Todo for short one-session linear checklists. Use Task for cross-session work, dependencies, or teammate coordination. If uncertain, start with Task because downscoping is cheaper than migrating state mid-run.",
"alternatives": "Always using Todo keeps the model minimal but breaks durability and collaboration. Always using Task maximizes consistency but may feel heavy for tiny one-shot notes.",
"zh": {
"title": "Todo/Task 快速判定矩阵",
"description": "短时单会话线性清单用 Todo跨会话、依赖、多人协作用 Task拿不准时先用 Task因为后续降级比半途迁移状态更便宜。"
},
"ja": {
"title": "Todo/Task クイック判定マトリクス",
"description": "短い単一セッションの直線タスクは Todo、セッション跨ぎや依存・協調がある作業は Task。迷うなら Task 開始が安全で、後で簡略化する方が途中移行より低コスト。"
}
}
]

View File

@@ -0,0 +1,103 @@
{
"version": "s12",
"decisions": [
{
"id": "shared-board-isolated-lanes",
"title": "Shared Task Board + Isolated Execution Lanes",
"description": "The task board remains shared and centralized in `.tasks/`, while file edits happen in per-task worktree directories. This separation preserves global visibility (who owns what, what is done) without forcing everyone to edit inside one mutable directory. Coordination stays simple because there is one board, and execution stays safe because each lane is isolated.",
"alternatives": "A single shared workspace is simpler but causes edit collisions and mixed git state. Fully independent task stores per lane avoid collisions but lose team-level visibility and make planning harder.",
"zh": {
"title": "共享任务板 + 隔离执行通道",
"description": "任务板继续集中在 `.tasks/`,而文件改动发生在按任务划分的 worktree 目录中。这样既保留了全局可见性(谁在做什么、完成到哪),又避免所有人同时写同一目录导致冲突。协调层简单(一个任务板),执行层安全(多条隔离通道)。"
},
"ja": {
"title": "共有タスクボード + 分離実行レーン",
"description": "タスクボードは `.tasks/` に集約しつつ、実際の編集はタスクごとの worktree ディレクトリで行う。これにより全体の可視性担当と進捗を維持しながら、単一ディレクトリでの衝突を回避できる。調整は1つのボードで単純化され、実行はレーン分離で安全になる。"
}
},
{
"id": "index-file-lifecycle",
"title": "Explicit Worktree Lifecycle Index",
"description": "`.worktrees/index.json` records each worktree's name, path, branch, task_id, and status. This makes lifecycle state inspectable and recoverable even after context compression or process restarts. The index also provides a deterministic source for list/status/remove operations.",
"alternatives": "Relying only on `git worktree list` removes local bookkeeping but loses task binding metadata and custom lifecycle states. Keeping all state only in memory is simpler in code but breaks recoverability.",
"zh": {
"title": "显式 worktree 生命周期索引",
"description": "`.worktrees/index.json` 记录每个 worktree 的名称、路径、分支、task_id 与状态。即使上下文压缩或进程重启,这些生命周期状态仍可检查和恢复。它也为 list/status/remove 提供了确定性的本地数据源。"
},
"ja": {
"title": "明示的な worktree ライフサイクル索引",
"description": "`.worktrees/index.json` に name/path/branch/task_id/status を記録することで、コンテキスト圧縮やプロセス再起動後も状態を追跡できる。list/status/remove の挙動もこの索引を基準に決定できる。"
}
},
{
"id": "lane-cwd-routing-and-reentry-guard",
"title": "Lane-Scoped CWD Routing + Re-entry Guard",
"description": "This course runtime uses lane-scoped cwd routing (`worktree_run(name, command)`). Other runtimes may choose session-level cwd switches. The design goal is predictable lane context with a re-entry guard when already inside an active worktree context.",
"alternatives": "Global cwd mutation is easy to implement but can leak context across parallel work. Allowing silent re-entry makes lifecycle ownership ambiguous and complicates teardown behavior.",
"zh": {
"title": "按通道 cwd 路由 + 禁止重入",
"description": "本课程运行时采用按通道 `cwd` 路由(`worktree_run(name, command)`)。其他运行时也可能选择会话级 cwd 切换。设计目标是让并行通道可预测,并在已处于 active worktree 上下文时通过重入保护避免二次进入。"
},
"ja": {
"title": "レーン単位 cwd ルーティング + 再入防止",
"description": "本コース実装では `worktree_run(name, command)` によるレーン単位 cwd ルーティングを採用する。実装によってはセッション単位で cwd を切り替える場合もある。狙いは並列レーンの予測可能性を保ち、active な worktree 文脈での再入を防ぐこと。"
}
},
{
"id": "event-stream-observability",
"title": "Append-Only Lifecycle Event Stream",
"description": "Lifecycle events are appended to `.worktrees/events.jsonl` (`worktree.create.*`, `worktree.remove.*`, `task.completed`). This turns hidden transitions into queryable records and makes failures explicit (`*.failed`) instead of silent.",
"alternatives": "Relying only on console logs is lighter but fragile during long sessions and hard to audit. A full event bus infrastructure is powerful but heavier than needed for this teaching baseline.",
"zh": {
"title": "追加式生命周期事件流",
"description": "生命周期事件写入 `.worktrees/events.jsonl`(如 `worktree.create.*`、`worktree.remove.*`、`task.completed`)。这样状态迁移可查询、可追踪,失败也会以 `*.failed` 显式暴露,而不是静默丢失。"
},
"ja": {
"title": "追記型ライフサイクルイベント",
"description": "ライフサイクルイベントを `.worktrees/events.jsonl` に追記する(`worktree.create.*`、`worktree.remove.*`、`task.completed` など)。遷移が可観測になり、失敗も `*.failed` として明示できる。"
}
},
{
"id": "hook-style-extension",
"title": "Hook-Style Extensions via Event Triplets",
"description": "Treat `before/after/failed` lifecycle emissions as extension points. Keep source-of-truth state writes in task/worktree files, and run side effects (audit, notification, policy checks) in event consumers.",
"alternatives": "Embedding every side effect directly in create/remove logic couples concerns tightly and makes failure handling harder. Moving source-of-truth to event replay is also risky without strict idempotency/repair semantics.",
"zh": {
"title": "通过三段事件实现 Hook 风格扩展",
"description": "把 `before/after/failed` 生命周期事件当作扩展插槽。真实状态写入仍留在 task/worktree 文件,审计、通知、策略检查等副作用交给事件消费者。"
},
"ja": {
"title": "三段イベントによる Hook 風拡張",
"description": "`before/after/failed` ライフサイクルイベントを拡張ポイントとして使う。正準状態は task/worktree ファイルに残し、副作用(監査・通知・ポリシーチェック)はイベント購読側で処理する。"
}
},
{
"id": "task-worktree-closeout",
"title": "Close Task and Workspace Together",
"description": "`worktree_remove(..., complete_task=true)` allows a single closeout step: remove the isolated directory and mark the bound task completed. In this course model, closeout remains an explicit tool-driven transition (`worktree_keep` / `worktree_remove`) rather than hidden automatic cleanup. This reduces dangling state where a task says done but its temporary lane remains active (or the reverse).",
"alternatives": "Keeping closeout fully manual gives flexibility but increases operational drift. Fully automatic removal on every completion risks deleting a workspace before final review.",
"zh": {
"title": "任务与工作区一起收尾",
"description": "`worktree_remove(..., complete_task=true)` 允许在一个动作里完成收尾:删除隔离目录并把绑定任务标记为 completed。在本课程模型里收尾保持为显式工具驱动迁移`worktree_keep` / `worktree_remove`),而不是隐藏的自动清理。这样可减少状态悬挂(任务已完成但临时工作区仍活跃,或反过来)。"
},
"ja": {
"title": "タスクとワークスペースを同時にクローズ",
"description": "`worktree_remove(..., complete_task=true)` により、分離ディレクトリ削除とタスク完了更新を1ステップで実行できる。本コースのモデルでは、クローズ処理は `worktree_keep` / `worktree_remove` の明示ツール遷移として扱い、暗黙の自動清掃にはしない。完了済みタスクに未回収レーンが残る、といったズレを減らせる。"
}
},
{
"id": "event-stream-side-channel",
"title": "Event Stream Is Observability Side-Channel",
"description": "Lifecycle events improve auditability, but the source of truth remains task/worktree state files. Events should be read as transition traces, not as a replacement state machine.",
"alternatives": "Using logs alone hides structured transitions; using events as the only state source risks drift when replay/repair semantics are undefined.",
"zh": {
"title": "事件流是观测旁路,不是状态机替身",
"description": "生命周期事件提升可审计性,但真实状态源仍是任务/工作区状态文件。事件更适合做迁移轨迹,而不是替代主状态机。"
},
"ja": {
"title": "イベントは観測サイドチャネルであり状態機械の代替ではない",
"description": "ライフサイクルイベントは監査性を高めるが、真の状態源は task/worktree 状態ファイルのまま。イベントは遷移トレースとして扱い、主状態機械の代替にしない。"
}
}
]
}

View File

@@ -271,6 +271,43 @@ export const EXECUTION_FLOWS: Record<string, FlowDefinition> = {
{ from: "poll", to: "inbox" },
],
},
s12: {
nodes: [
{ id: "start", label: "User Input", type: "start", x: COL_CENTER, y: 30 },
{ id: "llm", label: "LLM Call", type: "process", x: COL_CENTER, y: 110 },
{ id: "tool_check", label: "tool_use?", type: "decision", x: COL_CENTER, y: 190 },
{ id: "is_wt", label: "worktree tool?", type: "decision", x: COL_LEFT, y: 280 },
{ id: "task", label: "Task Board\\n(.tasks)", type: "process", x: 60, y: 360 },
{ id: "wt_create", label: "Allocate / Enter\\nWorktree", type: "subprocess", x: 60, y: 440 },
{ id: "wt_run", label: "Run in\\nIsolated Dir", type: "subprocess", x: COL_LEFT + 80, y: 360 },
{ id: "wt_close", label: "Closeout:\\nworktree_keep / remove", type: "process", x: COL_LEFT + 80, y: 440 },
{ id: "events", label: "Emit Lifecycle Events\\n(side-channel)", type: "process", x: COL_RIGHT, y: 420 },
{ id: "events_read", label: "Optional Read\\nworktree_events", type: "subprocess", x: COL_RIGHT, y: 520 },
{ id: "append", label: "Append Result", type: "process", x: COL_CENTER, y: 530 },
{ id: "end", label: "Output", type: "end", x: COL_RIGHT, y: 280 },
],
edges: [
{ from: "start", to: "llm" },
{ from: "llm", to: "tool_check" },
{ from: "tool_check", to: "is_wt", label: "yes" },
{ from: "tool_check", to: "end", label: "no" },
{ from: "is_wt", to: "task", label: "task ops" },
{ from: "is_wt", to: "wt_create", label: "create/bind" },
{ from: "is_wt", to: "wt_run", label: "run/status" },
{ from: "task", to: "wt_create", label: "allocate lane" },
{ from: "wt_create", to: "wt_run" },
{ from: "task", to: "append", label: "task result" },
{ from: "wt_create", to: "events", label: "emit create" },
{ from: "wt_create", to: "append", label: "create result" },
{ from: "wt_run", to: "wt_close" },
{ from: "wt_run", to: "append", label: "run/status result" },
{ from: "wt_close", to: "events", label: "emit closeout" },
{ from: "wt_close", to: "append", label: "closeout result" },
{ from: "events", to: "events_read", label: "optional query" },
{ from: "events_read", to: "append", label: "events result" },
{ from: "append", to: "llm" },
],
},
};
export function getFlowForVersion(version: string): FlowDefinition | null {

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,51 @@
{
"version": "s12",
"title": "Worktree + Task Isolation",
"description": "Use a shared task board with optional worktree lanes for clean parallel execution",
"steps": [
{
"type": "user_message",
"content": "Implement auth refactor and login UI updates in parallel",
"annotation": "Two active tasks in one workspace would collide"
},
{
"type": "tool_call",
"content": "task_create(subject: \"Auth refactor\")\ntask_create(subject: \"Login UI polish\")",
"toolName": "task_manager",
"annotation": "Shared board remains the coordination source of truth"
},
{
"type": "tool_call",
"content": "worktree_create(name: \"auth-refactor\", task_id: 1)\nworktree_create(name: \"ui-login\")\ntask_bind_worktree(task_id: 2, worktree: \"ui-login\")",
"toolName": "worktree_manager",
"annotation": "Lane allocation and task association are composable; task 2 binds after lane creation"
},
{
"type": "system_event",
"content": "worktree.create.before/after emitted\n.tasks/task_1.json -> { status: \"in_progress\", worktree: \"auth-refactor\" }\n.tasks/task_2.json -> { status: \"in_progress\", worktree: \"ui-login\" }\n.worktrees/index.json updated",
"annotation": "Control-plane state remains canonical; hook-style consumers can react to lifecycle events without owning canonical state writes"
},
{
"type": "tool_call",
"content": "worktree_run(name: \"auth-refactor\", command: \"pytest tests/auth -q\")\nworktree_run(name: \"ui-login\", command: \"npm test -- login\")",
"toolName": "worktree_run",
"annotation": "In this teaching runtime, commands route by lane-scoped cwd; other runtimes may use session-level directory switches. The invariant is explicit execution context."
},
{
"type": "tool_call",
"content": "worktree_keep(name: \"ui-login\")\nworktree_remove(name: \"auth-refactor\", complete_task: true)\nworktree_events(limit: 10)",
"toolName": "worktree_manager",
"annotation": "Closeout is explicit tool-driven state transition: mix keep/remove decisions and query lifecycle events in one pass"
},
{
"type": "system_event",
"content": "worktree.keep emitted for ui-login\nworktree.remove.before/after emitted for auth-refactor\ntask.completed emitted for #1\n.worktrees/events.jsonl appended",
"annotation": "Lifecycle transitions become explicit records while task/worktree files remain source-of-truth"
},
{
"type": "assistant_text",
"content": "Task board handles coordination, worktrees handle isolation. Parallel tracks stay clean and auditable.",
"annotation": "Coordinate in one board, isolate by lane only where needed, and run optional policy/audit side effects from lifecycle events"
}
]
}

View File

@@ -1,10 +1,10 @@
{
"meta": { "title": "Learn Claude Code", "description": "Build an AI coding agent from scratch, one concept at a time" },
"meta": { "title": "Learn Claude Code", "description": "Build a nano Claude Code-like agent from 0 to 1, one mechanism at a time" },
"nav": { "home": "Home", "timeline": "Timeline", "compare": "Compare", "layers": "Layers", "github": "GitHub" },
"home": { "hero_title": "Learn Claude Code", "hero_subtitle": "Build an AI coding agent from scratch, one concept at a time", "start": "Start Learning", "core_pattern": "The Core Pattern", "core_pattern_desc": "Every AI coding agent shares the same loop: call the model, execute tools, feed results back. Everything else is details.", "learning_path": "Learning Path", "learning_path_desc": "11 progressive sessions, from a simple loop to full autonomous teams", "layers_title": "Architectural Layers", "layers_desc": "Five orthogonal concerns that compose into a complete agent", "loc": "LOC", "learn_more": "Learn More", "versions_in_layer": "versions", "message_flow": "Message Growth", "message_flow_desc": "Watch the messages array grow as the agent loop executes" },
"home": { "hero_title": "Learn Claude Code", "hero_subtitle": "Build a nano Claude Code-like agent from 0 to 1, one mechanism at a time", "start": "Start Learning", "core_pattern": "The Core Pattern", "core_pattern_desc": "Every AI coding agent shares the same loop: call the model, execute tools, feed results back. Production systems add policy, permissions, and lifecycle layers on top.", "learning_path": "Learning Path", "learning_path_desc": "12 progressive sessions, from a simple loop to isolated autonomous execution", "layers_title": "Architectural Layers", "layers_desc": "Five orthogonal concerns that compose into a complete agent", "loc": "LOC", "learn_more": "Learn More", "versions_in_layer": "versions", "message_flow": "Message Growth", "message_flow_desc": "Watch the messages array grow as the agent loop executes" },
"version": { "loc": "lines of code", "tools": "tools", "new": "New", "prev": "Previous", "next": "Next", "view_source": "View Source", "view_diff": "View Diff", "design_decisions": "Design Decisions", "whats_new": "What's New", "tutorial": "Tutorial", "simulator": "Agent Loop Simulator", "execution_flow": "Execution Flow", "architecture": "Architecture", "concept_viz": "Concept Visualization", "alternatives": "Alternatives Considered", "tab_learn": "Learn", "tab_simulate": "Simulate", "tab_code": "Code", "tab_deep_dive": "Deep Dive" },
"sim": { "play": "Play", "pause": "Pause", "step": "Step", "reset": "Reset", "speed": "Speed", "step_of": "of" },
"timeline": { "title": "Learning Path", "subtitle": "s01 to s11: Progressive Agent Design", "layer_legend": "Layer Legend", "loc_growth": "LOC Growth", "learn_more": "Learn More" },
"timeline": { "title": "Learning Path", "subtitle": "s01 to s12: Progressive Agent Design", "layer_legend": "Layer Legend", "loc_growth": "LOC Growth", "learn_more": "Learn More" },
"layers": {
"title": "Architectural Layers",
"subtitle": "Five orthogonal concerns that compose into a complete agent",
@@ -49,7 +49,8 @@
"s08": "Background Tasks",
"s09": "Agent Teams",
"s10": "Team Protocols",
"s11": "Autonomous Agents"
"s11": "Autonomous Agents",
"s12": "Worktree + Task Isolation"
},
"layer_labels": {
"tools": "Tools & Execution",
@@ -69,6 +70,7 @@
"s08": "Background Task Lanes",
"s09": "Agent Team Mailboxes",
"s10": "FSM Team Protocols",
"s11": "Autonomous Agent Cycle"
"s11": "Autonomous Agent Cycle",
"s12": "Worktree Task Isolation"
}
}

View File

@@ -1,10 +1,10 @@
{
"meta": { "title": "Learn Claude Code", "description": "AIコーディングエージェントをゼロから構築、一つずつ概念を追加" },
"meta": { "title": "Learn Claude Code", "description": "0 から 1 へ nano Claude Code-like agent を構築し、毎回 1 つの仕組みを追加" },
"nav": { "home": "ホーム", "timeline": "学習パス", "compare": "バージョン比較", "layers": "アーキテクチャ層", "github": "GitHub" },
"home": { "hero_title": "Learn Claude Code", "hero_subtitle": "AIコーディングエージェントをゼロから構築、一つずつ概念を追加", "start": "学習を始める", "core_pattern": "コアパターン", "core_pattern_desc": "てのAIコーディングエージェントは同じループを共有モデルを呼び出し、ツールを実行し、結果をフィードバック。他は全て詳細。", "learning_path": "学習パス", "learning_path_desc": "11の段階的セッション、シンプルなループから完全自律チームまで", "layers_title": "アーキテクチャ層", "layers_desc": "5つの直交する関心事が完全なエージェントを構成", "loc": "行", "learn_more": "詳細を見る", "versions_in_layer": "バージョン", "message_flow": "メッセージの増加", "message_flow_desc": "エージェントループ実行時のメッセージ配列の成長を観察" },
"home": { "hero_title": "Learn Claude Code", "hero_subtitle": "0 から 1 へ nano Claude Code-like agent を構築し、毎回 1 つの仕組みを追加", "start": "学習を始める", "core_pattern": "コアパターン", "core_pattern_desc": "すべての AI コーディングエージェントは同じループを共有する:モデルを呼び出し、ツールを実行し、結果を返す。実運用ではこの上にポリシー、権限、ライフサイクル層が重なる。", "learning_path": "学習パス", "learning_path_desc": "12の段階的セッション、シンプルなループから分離された自律実行まで", "layers_title": "アーキテクチャ層", "layers_desc": "5つの直交する関心事が完全なエージェントを構成", "loc": "行", "learn_more": "詳細を見る", "versions_in_layer": "バージョン", "message_flow": "メッセージの増加", "message_flow_desc": "エージェントループ実行時のメッセージ配列の成長を観察" },
"version": { "loc": "行のコード", "tools": "ツール", "new": "新規", "prev": "前のバージョン", "next": "次のバージョン", "view_source": "ソースを見る", "view_diff": "差分を見る", "design_decisions": "設計判断", "whats_new": "新機能", "tutorial": "チュートリアル", "simulator": "エージェントループシミュレーター", "execution_flow": "実行フロー", "architecture": "アーキテクチャ", "concept_viz": "コンセプト可視化", "alternatives": "検討された代替案", "tab_learn": "学習", "tab_simulate": "シミュレーション", "tab_code": "ソースコード", "tab_deep_dive": "詳細分析" },
"sim": { "play": "再生", "pause": "一時停止", "step": "ステップ", "reset": "リセット", "speed": "速度", "step_of": "/" },
"timeline": { "title": "学習パス", "subtitle": "s01からs11へ:段階的エージェント設計", "layer_legend": "レイヤー凡例", "loc_growth": "コード量の推移", "learn_more": "詳細を見る" },
"timeline": { "title": "学習パス", "subtitle": "s01からs12へ:段階的エージェント設計", "layer_legend": "レイヤー凡例", "loc_growth": "コード量の推移", "learn_more": "詳細を見る" },
"layers": {
"title": "アーキテクチャ層",
"subtitle": "5つの直交する関心事が完全なエージェントを構成",
@@ -49,7 +49,8 @@
"s08": "バックグラウンドタスク",
"s09": "エージェントチーム",
"s10": "チームプロトコル",
"s11": "自律エージェント"
"s11": "自律エージェント",
"s12": "Worktree + タスク分離"
},
"layer_labels": {
"tools": "ツールと実行",
@@ -69,6 +70,7 @@
"s08": "バックグラウンドタスクレーン",
"s09": "エージェントチーム メールボックス",
"s10": "FSM チームプロトコル",
"s11": "自律エージェントサイクル"
"s11": "自律エージェントサイクル",
"s12": "Worktree タスク分離"
}
}

View File

@@ -1,10 +1,10 @@
{
"meta": { "title": "Learn Claude Code", "description": "从零构建 AI 编程 Agent每次只加一个概念" },
"meta": { "title": "Learn Claude Code", "description": "从 0 到 1 构建 nano Claude Code-like agent每次只加一个机制" },
"nav": { "home": "首页", "timeline": "学习路径", "compare": "版本对比", "layers": "架构层", "github": "GitHub" },
"home": { "hero_title": "Learn Claude Code", "hero_subtitle": "从零构建 AI 编程 Agent每次只加一个概念", "start": "开始学习", "core_pattern": "核心模式", "core_pattern_desc": "所有 AI 编程 Agent 共享同一个循环:调用模型、执行工具、回传结果。其他都是细节。", "learning_path": "学习路径", "learning_path_desc": "11 个渐进式课程,从简单循环到完整自主团队", "layers_title": "架构层次", "layers_desc": "五个正交关注点组合成完整的 Agent", "loc": "行", "learn_more": "了解更多", "versions_in_layer": "个版本", "message_flow": "消息增长", "message_flow_desc": "观察 Agent 循环执行时消息数组的增长" },
"home": { "hero_title": "Learn Claude Code", "hero_subtitle": "从 0 到 1 构建 nano Claude Code-like agent每次只加一个机制", "start": "开始学习", "core_pattern": "核心模式", "core_pattern_desc": "所有 AI 编程 Agent 共享同一个循环:调用模型、执行工具、回传结果。生产级系统会在其上叠加策略、权限和生命周期层。", "learning_path": "学习路径", "learning_path_desc": "12 个渐进式课程,从简单循环到隔离化自治执行", "layers_title": "架构层次", "layers_desc": "五个正交关注点组合成完整的 Agent", "loc": "行", "learn_more": "了解更多", "versions_in_layer": "个版本", "message_flow": "消息增长", "message_flow_desc": "观察 Agent 循环执行时消息数组的增长" },
"version": { "loc": "行代码", "tools": "个工具", "new": "新增", "prev": "上一版", "next": "下一版", "view_source": "查看源码", "view_diff": "查看变更", "design_decisions": "设计决策", "whats_new": "新增内容", "tutorial": "教程", "simulator": "Agent 循环模拟器", "execution_flow": "执行流程", "architecture": "架构", "concept_viz": "概念可视化", "alternatives": "替代方案", "tab_learn": "学习", "tab_simulate": "模拟", "tab_code": "源码", "tab_deep_dive": "深入探索" },
"sim": { "play": "播放", "pause": "暂停", "step": "单步", "reset": "重置", "speed": "速度", "step_of": "/" },
"timeline": { "title": "学习路径", "subtitle": "s01 到 s11:渐进式 Agent 设计", "layer_legend": "层次图例", "loc_growth": "代码量增长", "learn_more": "了解更多" },
"timeline": { "title": "学习路径", "subtitle": "s01 到 s12:渐进式 Agent 设计", "layer_legend": "层次图例", "loc_growth": "代码量增长", "learn_more": "了解更多" },
"layers": {
"title": "架构层次",
"subtitle": "五个正交关注点组合成完整的 Agent",
@@ -49,7 +49,8 @@
"s08": "后台任务",
"s09": "Agent 团队",
"s10": "团队协议",
"s11": "自主 Agent"
"s11": "自主 Agent",
"s12": "Worktree + 任务隔离"
},
"layer_labels": {
"tools": "工具与执行",
@@ -69,6 +70,7 @@
"s08": "后台任务通道",
"s09": "Agent 团队邮箱",
"s10": "FSM 团队协议",
"s11": "自主 Agent 循环"
"s11": "自主 Agent 循环",
"s12": "Worktree 任务隔离"
}
}

View File

@@ -1,5 +1,5 @@
export const VERSION_ORDER = [
"s01", "s02", "s03", "s04", "s05", "s06", "s07", "s08", "s09", "s10", "s11"
"s01", "s02", "s03", "s04", "s05", "s06", "s07", "s08", "s09", "s10", "s11", "s12"
] as const;
export const LEARNING_PATH = VERSION_ORDER;
@@ -14,10 +14,10 @@ export const VERSION_META: Record<string, {
layer: "tools" | "planning" | "memory" | "concurrency" | "collaboration";
prevVersion: string | null;
}> = {
s01: { title: "The Agent Loop", subtitle: "Bash is All You Need", coreAddition: "Single-tool agent loop", keyInsight: "The entire agent is a while loop + one tool", layer: "tools", prevVersion: null },
s02: { title: "Tools", subtitle: "The Loop Didn't Change", coreAddition: "Tool dispatch map", keyInsight: "Adding tools means adding handlers, the loop stays the same", layer: "tools", prevVersion: "s01" },
s01: { title: "The Agent Loop", subtitle: "Bash is All You Need", coreAddition: "Single-tool agent loop", keyInsight: "The minimal agent kernel is a while loop + one tool", layer: "tools", prevVersion: null },
s02: { title: "Tools", subtitle: "The Loop Didn't Change", coreAddition: "Tool dispatch map", keyInsight: "Adding tools means adding handlers, not rewriting the loop", layer: "tools", prevVersion: "s01" },
s03: { title: "TodoWrite", subtitle: "Plan Before You Act", coreAddition: "TodoManager + nag reminder", keyInsight: "Visible plans improve task completion and accountability", layer: "planning", prevVersion: "s02" },
s04: { title: "Subagents", subtitle: "Fresh Context via Task Tool", coreAddition: "Subagent spawn with isolated messages[]", keyInsight: "Process isolation = context isolation", layer: "planning", prevVersion: "s03" },
s04: { title: "Subagents", subtitle: "Process Isolation = Context Isolation", coreAddition: "Subagent spawn with isolated messages[]", keyInsight: "Process isolation gives context isolation for free", layer: "planning", prevVersion: "s03" },
s05: { title: "Skills", subtitle: "SKILL.md + tool_result Injection", coreAddition: "SkillLoader + two-layer injection", keyInsight: "Skills inject via tool_result, not system prompt", layer: "planning", prevVersion: "s04" },
s06: { title: "Compact", subtitle: "Strategic Forgetting", coreAddition: "micro-compact + auto-compact + archival", keyInsight: "Forgetting old context enables infinite-length sessions", layer: "memory", prevVersion: "s05" },
s07: { title: "Tasks", subtitle: "Persistent CRUD with Dependencies", coreAddition: "TaskManager with file-based state + dependency graph", keyInsight: "File-based state survives context compression", layer: "planning", prevVersion: "s06" },
@@ -25,6 +25,7 @@ export const VERSION_META: Record<string, {
s09: { title: "Agent Teams", subtitle: "Teammates + Mailboxes", coreAddition: "TeammateManager + file-based mailbox", keyInsight: "Persistent teammates with async mailbox inboxes", layer: "collaboration", prevVersion: "s08" },
s10: { title: "Team Protocols", subtitle: "Shutdown + Plan Approval", coreAddition: "request_id correlation for two protocols", keyInsight: "Same request-response pattern, two applications", layer: "collaboration", prevVersion: "s09" },
s11: { title: "Autonomous Agents", subtitle: "Idle Cycle + Auto-Claim", coreAddition: "Task board polling + timeout-based self-governance", keyInsight: "Polling + timeout makes teammates self-organizing", layer: "collaboration", prevVersion: "s10" },
s12: { title: "Worktree + Task Isolation", subtitle: "Isolate by Directory", coreAddition: "Composable worktree lifecycle + event stream over a shared task board", keyInsight: "Task board coordinates ownership, worktrees isolate execution, and events make lifecycle auditable", layer: "collaboration", prevVersion: "s11" },
};
export const LAYERS = [
@@ -32,5 +33,5 @@ export const LAYERS = [
{ id: "planning" as const, label: "Planning & Coordination", color: "#10B981", versions: ["s03", "s04", "s05", "s07"] },
{ id: "memory" as const, label: "Memory Management", color: "#8B5CF6", versions: ["s06"] },
{ id: "concurrency" as const, label: "Concurrency", color: "#F59E0B", versions: ["s08"] },
{ id: "collaboration" as const, label: "Collaboration", color: "#EF4444", versions: ["s09", "s10", "s11"] },
{ id: "collaboration" as const, label: "Collaboration", color: "#EF4444", versions: ["s09", "s10", "s11", "s12"] },
] as const;