Compare commits

...

2 Commits

Author SHA1 Message Date
429acd2fb5 Add Mission Control integration with per-project toggle
All checks were successful
Build App / build-macos (push) Successful in 2m49s
Build App / build-windows (push) Successful in 3m32s
Build App / build-linux (push) Successful in 4m29s
Build Container / build-container (push) Successful in 56s
Build App / sync-to-github (push) Successful in 9s
When enabled, the entrypoint clones mission-control into ~/mission-control
(persisted on the home volume) and symlinks it to /workspace/mission-control.
Flight Control global and project instructions are programmatically appended
to CLAUDE.md. Container recreation is triggered on toggle change.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 19:32:04 -08:00
c853f2676d Add tooltip hint for double-click to rename project name
All checks were successful
Build App / build-macos (push) Successful in 3m29s
Build App / build-windows (push) Successful in 3m55s
Build App / build-linux (push) Successful in 4m43s
Build App / sync-to-github (push) Successful in 9s
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 18:27:24 -08:00
5 changed files with 141 additions and 4 deletions

View File

@@ -40,6 +40,54 @@ After tasks run, check notifications with `triple-c-scheduler notifications` and
### Timezone ### Timezone
Scheduled times use the container's configured timezone (check with `date`). If no timezone is configured, UTC is used."#; Scheduled times use the container's configured timezone (check with `date`). If no timezone is configured, UTC is used."#;
const MISSION_CONTROL_GLOBAL_INSTRUCTIONS: &str = r#"## Mission Control
The `/workspace/mission-control/` directory contains **Flight Control** — an AI-first development methodology for structured project management. Use it for all project work.
### How It Works
- **Mission Control is a tool, not a project.** It provides skills and methodology for managing other projects.
- All Flight Control skills live in `/workspace/mission-control/.claude/skills/`
- The projects registry at `/workspace/mission-control/projects.md` lists all active projects
### When to Use
When working on any project that has a `.flightops/` directory, follow the Flight Control methodology:
1. Read the project's `.flightops/ARTIFACTS.md` to understand artifact storage
2. Read `.flightops/FLIGHT_OPERATIONS.md` for the implementation workflow
3. Use Mission Control skills for planning and execution
### Available Skills
| Skill | When to Use |
|-------|-------------|
| `/init-project` | Setting up a new project for Flight Control |
| `/mission` | Defining new work outcomes (days-to-weeks scope) |
| `/flight` | Creating technical specs from missions (hours-to-days scope) |
| `/leg` | Generating implementation steps from flights (minutes-to-hours scope) |
| `/agentic-workflow` | Executing legs with multi-agent workflow (implement, review, commit) |
| `/flight-debrief` | Post-flight analysis after a flight lands |
| `/mission-debrief` | Post-mission retrospective after completion |
| `/daily-briefing` | Cross-project status report |
### Key Rules
- **Planning skills produce artifacts only** — never modify source code directly
- **Phase gates require human confirmation** — missions before flights, flights before legs
- **Legs are immutable once in-flight** — create new ones instead of modifying
- **`/agentic-workflow` orchestrates implementation** — it spawns separate Developer and Reviewer agents
- **Artifacts live in the target project** — not in mission-control"#;
const MISSION_CONTROL_PROJECT_INSTRUCTIONS: &str = r#"## Flight Operations
This project uses [Flight Control](https://github.com/msieurthenardier/mission-control) for structured development.
**Before any mission/flight/leg work, read these files in order:**
1. `.flightops/README.md` — What the flightops directory contains
2. `.flightops/FLIGHT_OPERATIONS.md` — **The workflow you MUST follow**
3. `.flightops/ARTIFACTS.md` — Where all artifacts are stored
4. `.flightops/agent-crews/` — Project crew definitions for each phase (read the relevant crew file)"#;
/// Build the full CLAUDE_INSTRUCTIONS value by merging global + project /// Build the full CLAUDE_INSTRUCTIONS value by merging global + project
/// instructions, appending port mapping docs, and appending scheduler docs. /// instructions, appending port mapping docs, and appending scheduler docs.
/// Used by both create_container() and container_needs_recreation() to ensure /// Used by both create_container() and container_needs_recreation() to ensure
@@ -48,8 +96,13 @@ fn build_claude_instructions(
global_instructions: Option<&str>, global_instructions: Option<&str>,
project_instructions: Option<&str>, project_instructions: Option<&str>,
port_mappings: &[PortMapping], port_mappings: &[PortMapping],
mission_control_enabled: bool,
) -> Option<String> { ) -> Option<String> {
let mut combined = merge_claude_instructions(global_instructions, project_instructions); let mut combined = merge_claude_instructions(
global_instructions,
project_instructions,
mission_control_enabled,
);
if !port_mappings.is_empty() { if !port_mappings.is_empty() {
let mut port_lines: Vec<String> = Vec::new(); let mut port_lines: Vec<String> = Vec::new();
@@ -116,14 +169,37 @@ fn merge_custom_env_vars(global: &[EnvVar], project: &[EnvVar]) -> Vec<EnvVar> {
} }
/// Merge global and per-project Claude instructions into a single string. /// Merge global and per-project Claude instructions into a single string.
/// When mission_control_enabled is true, appends Mission Control global
/// instructions after global and project instructions after project.
fn merge_claude_instructions( fn merge_claude_instructions(
global_instructions: Option<&str>, global_instructions: Option<&str>,
project_instructions: Option<&str>, project_instructions: Option<&str>,
mission_control_enabled: bool,
) -> Option<String> { ) -> Option<String> {
match (global_instructions, project_instructions) { // Build the global portion (user global + optional MC global)
let global_part = if mission_control_enabled {
match global_instructions {
Some(g) => Some(format!("{}\n\n{}", g, MISSION_CONTROL_GLOBAL_INSTRUCTIONS)),
None => Some(MISSION_CONTROL_GLOBAL_INSTRUCTIONS.to_string()),
}
} else {
global_instructions.map(|g| g.to_string())
};
// Build the project portion (user project + optional MC project)
let project_part = if mission_control_enabled {
match project_instructions {
Some(p) => Some(format!("{}\n\n{}", p, MISSION_CONTROL_PROJECT_INSTRUCTIONS)),
None => Some(MISSION_CONTROL_PROJECT_INSTRUCTIONS.to_string()),
}
} else {
project_instructions.map(|p| p.to_string())
};
match (global_part, project_part) {
(Some(g), Some(p)) => Some(format!("{}\n\n{}", g, p)), (Some(g), Some(p)) => Some(format!("{}\n\n{}", g, p)),
(Some(g), None) => Some(g.to_string()), (Some(g), None) => Some(g),
(None, Some(p)) => Some(p.to_string()), (None, Some(p)) => Some(p),
(None, None) => None, (None, None) => None,
} }
} }
@@ -426,11 +502,17 @@ pub async fn create_container(
} }
} }
// Mission Control env var
if project.mission_control_enabled {
env_vars.push("MISSION_CONTROL_ENABLED=1".to_string());
}
// Claude instructions (global + per-project, plus port mapping info + scheduler docs) // Claude instructions (global + per-project, plus port mapping info + scheduler docs)
let combined_instructions = build_claude_instructions( let combined_instructions = build_claude_instructions(
global_claude_instructions, global_claude_instructions,
project.claude_instructions.as_deref(), project.claude_instructions.as_deref(),
&project.port_mappings, &project.port_mappings,
project.mission_control_enabled,
); );
if let Some(ref instructions) = combined_instructions { if let Some(ref instructions) = combined_instructions {
@@ -567,6 +649,7 @@ pub async fn create_container(
labels.insert("triple-c.image".to_string(), image_name.to_string()); labels.insert("triple-c.image".to_string(), image_name.to_string());
labels.insert("triple-c.timezone".to_string(), timezone.unwrap_or("").to_string()); labels.insert("triple-c.timezone".to_string(), timezone.unwrap_or("").to_string());
labels.insert("triple-c.mcp-fingerprint".to_string(), compute_mcp_fingerprint(mcp_servers)); labels.insert("triple-c.mcp-fingerprint".to_string(), compute_mcp_fingerprint(mcp_servers));
labels.insert("triple-c.mission-control".to_string(), project.mission_control_enabled.to_string());
let host_config = HostConfig { let host_config = HostConfig {
mounts: Some(mounts), mounts: Some(mounts),
@@ -885,11 +968,20 @@ pub async fn container_needs_recreation(
return Ok(true); return Ok(true);
} }
// ── Mission Control ────────────────────────────────────────────────────
let expected_mc = project.mission_control_enabled.to_string();
let container_mc = get_label("triple-c.mission-control").unwrap_or_else(|| "false".to_string());
if container_mc != expected_mc {
log::info!("Mission Control mismatch (container={:?}, expected={:?})", container_mc, expected_mc);
return Ok(true);
}
// ── Claude instructions ─────────────────────────────────────────────── // ── Claude instructions ───────────────────────────────────────────────
let expected_instructions = build_claude_instructions( let expected_instructions = build_claude_instructions(
global_claude_instructions, global_claude_instructions,
project.claude_instructions.as_deref(), project.claude_instructions.as_deref(),
&project.port_mappings, &project.port_mappings,
project.mission_control_enabled,
); );
let container_instructions = get_env("CLAUDE_INSTRUCTIONS"); let container_instructions = get_env("CLAUDE_INSTRUCTIONS");
if container_instructions.as_deref() != expected_instructions.as_deref() { if container_instructions.as_deref() != expected_instructions.as_deref() {

View File

@@ -34,6 +34,8 @@ pub struct Project {
pub auth_mode: AuthMode, pub auth_mode: AuthMode,
pub bedrock_config: Option<BedrockConfig>, pub bedrock_config: Option<BedrockConfig>,
pub allow_docker_access: bool, pub allow_docker_access: bool,
#[serde(default)]
pub mission_control_enabled: bool,
pub ssh_key_path: Option<String>, pub ssh_key_path: Option<String>,
#[serde(skip_serializing, default)] #[serde(skip_serializing, default)]
pub git_token: Option<String>, pub git_token: Option<String>,
@@ -125,6 +127,7 @@ impl Project {
auth_mode: AuthMode::default(), auth_mode: AuthMode::default(),
bedrock_config: None, bedrock_config: None,
allow_docker_access: false, allow_docker_access: false,
mission_control_enabled: false,
ssh_key_path: None, ssh_key_path: None,
git_token: None, git_token: None,
git_user_name: None, git_user_name: None,

View File

@@ -342,6 +342,7 @@ export default function ProjectCard({ project }: Props) {
) : ( ) : (
<span <span
className="text-sm font-medium truncate flex-1 cursor-text" className="text-sm font-medium truncate flex-1 cursor-text"
title="Double-click to rename"
onDoubleClick={(e) => { e.stopPropagation(); setIsEditingName(true); }} onDoubleClick={(e) => { e.stopPropagation(); setIsEditingName(true); }}
> >
{project.name} {project.name}
@@ -631,6 +632,28 @@ export default function ProjectCard({ project }: Props) {
</button> </button>
</div> </div>
{/* Mission Control toggle */}
<div className="flex items-center gap-2">
<label className="text-xs text-[var(--text-secondary)]">Mission Control</label>
<button
onClick={async () => {
try {
await update({ ...project, mission_control_enabled: !project.mission_control_enabled });
} catch (err) {
console.error("Failed to update Mission Control setting:", err);
}
}}
disabled={!isStopped}
className={`px-2 py-0.5 text-xs rounded transition-colors disabled:opacity-50 ${
project.mission_control_enabled
? "bg-[var(--success)] text-white"
: "bg-[var(--bg-primary)] border border-[var(--border-color)] text-[var(--text-secondary)]"
}`}
>
{project.mission_control_enabled ? "ON" : "OFF"}
</button>
</div>
{/* Environment Variables */} {/* Environment Variables */}
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<label className="text-xs text-[var(--text-secondary)]"> <label className="text-xs text-[var(--text-secondary)]">

View File

@@ -23,6 +23,7 @@ export interface Project {
auth_mode: AuthMode; auth_mode: AuthMode;
bedrock_config: BedrockConfig | null; bedrock_config: BedrockConfig | null;
allow_docker_access: boolean; allow_docker_access: boolean;
mission_control_enabled: boolean;
ssh_key_path: string | null; ssh_key_path: string | null;
git_token: string | null; git_token: string | null;
git_user_name: string | null; git_user_name: string | null;

View File

@@ -116,6 +116,24 @@ if [ -n "$CLAUDE_INSTRUCTIONS" ]; then
unset CLAUDE_INSTRUCTIONS unset CLAUDE_INSTRUCTIONS
fi fi
# ── Mission Control setup ───────────────────────────────────────────────────
if [ "$MISSION_CONTROL_ENABLED" = "1" ]; then
MC_HOME="/home/claude/mission-control"
MC_LINK="/workspace/mission-control"
if [ ! -d "$MC_HOME/.git" ]; then
echo "entrypoint: cloning mission-control..."
su -s /bin/bash claude -c \
'git clone https://github.com/msieurthenardier/mission-control.git /home/claude/mission-control' \
|| echo "entrypoint: warning — failed to clone mission-control"
else
echo "entrypoint: mission-control already present, skipping clone"
fi
# Symlink into workspace so Claude sees it at /workspace/mission-control
ln -sfn "$MC_HOME" "$MC_LINK"
chown -h claude:claude "$MC_LINK"
unset MISSION_CONTROL_ENABLED
fi
# ── MCP server configuration ──────────────────────────────────────────────── # ── MCP server configuration ────────────────────────────────────────────────
# Merge MCP server config into ~/.claude.json (preserves existing keys like # Merge MCP server config into ~/.claude.json (preserves existing keys like
# OAuth tokens). Creates the file if it doesn't exist. # OAuth tokens). Creates the file if it doesn't exist.