From 5ba531e2d0894387a73f1fa95298b3e01ffaff77 Mon Sep 17 00:00:00 2001 From: Jason Woltje Date: Sun, 22 Feb 2026 17:22:50 -0600 Subject: [PATCH] feat: r0 coordinator tooling for orchestrator protocol MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implements the manual coordinator workflow for multi-session agent orchestration. Agents stop after one milestone (confirmed limitation); these tools let the human coordinator check status, generate continuation prompts, and chain sessions together. New: - tools/orchestrator/ — 5 scripts + shared library (_lib.sh) - mission-init.sh: initialize mission with milestones and state files - mission-status.sh: dashboard showing milestones, tasks, sessions - session-status.sh: check if agent is running/stale/dead - continue-prompt.sh: generate paste-ready continuation prompt - session-resume.sh: crash recovery with dirty state detection - guides/ORCHESTRATOR-PROTOCOL.md: agent-facing mission lifecycle guide - templates/docs/: mission manifest, scratchpad, continuation templates - templates/repo/.mosaic/orchestrator/mission.json: state file template Modified: - bin/mosaic: add 'coord' subcommand + resume advisory on launch - AGENTS.md: conditional loading for protocol guide + rule 37 - bin/mosaic-doctor: checks for new coordinator files - session hooks: mission detection on start, cleanup on end Usage: mosaic coord init|mission|status|continue|resume Co-Authored-By: Claude Opus 4.6 --- AGENTS.md | 2 + bin/mosaic | 85 ++++ bin/mosaic-doctor | 8 + guides/ORCHESTRATOR-PROTOCOL.md | 257 ++++++++++++ templates/docs/MISSION-MANIFEST.md.template | 53 +++ .../docs/continuation-prompt.md.template | 36 ++ templates/docs/mission-scratchpad.md.template | 27 ++ .../repo/.mosaic/orchestrator/mission.json | 14 + templates/repo/scripts/agent/session-end.sh | 28 ++ templates/repo/scripts/agent/session-start.sh | 66 +++ tools/orchestrator/_lib.sh | 386 ++++++++++++++++++ tools/orchestrator/continue-prompt.sh | 153 +++++++ tools/orchestrator/mission-init.sh | 283 +++++++++++++ tools/orchestrator/mission-status.sh | 181 ++++++++ tools/orchestrator/session-resume.sh | 208 ++++++++++ tools/orchestrator/session-status.sh | 157 +++++++ 16 files changed, 1944 insertions(+) create mode 100644 guides/ORCHESTRATOR-PROTOCOL.md create mode 100644 templates/docs/MISSION-MANIFEST.md.template create mode 100644 templates/docs/continuation-prompt.md.template create mode 100644 templates/docs/mission-scratchpad.md.template create mode 100644 templates/repo/.mosaic/orchestrator/mission.json create mode 100755 tools/orchestrator/_lib.sh create mode 100755 tools/orchestrator/continue-prompt.sh create mode 100755 tools/orchestrator/mission-init.sh create mode 100755 tools/orchestrator/mission-status.sh create mode 100755 tools/orchestrator/session-resume.sh create mode 100755 tools/orchestrator/session-status.sh diff --git a/AGENTS.md b/AGENTS.md index 9fe3330..60c34bd 100755 --- a/AGENTS.md +++ b/AGENTS.md @@ -73,6 +73,7 @@ If any required file is missing, you MUST stop and report the missing file. 34. For source-code delivery through PR workflow, completion is forbidden until the PR is merged to `main`, CI/pipeline status is terminal green, and linked issue/internal task is closed. 35. If merge/CI/issue-closure operations fail, you MUST report a blocker with the exact failed wrapper command and stop instead of declaring completion. 36. Before push or PR merge, you MUST run CI queue guard and wait if the project has running/queued pipelines: `~/.config/mosaic/tools/git/ci-queue-wait.sh --purpose push|merge`. +37. When an active mission is detected at session start (MISSION-MANIFEST.md, TASKS.md, or scratchpads/ present), you MUST load `~/.config/mosaic/guides/ORCHESTRATOR-PROTOCOL.md` and follow the Session Resume Protocol before taking any action. ## Mode Declaration Protocol (Hard Rule) @@ -112,6 +113,7 @@ Load additional guides when the task requires them. | QA and test strategy | `~/.config/mosaic/guides/QA-TESTING.md` | | Secrets and vault usage | `~/.config/mosaic/guides/VAULT-SECRETS.md` | | Orchestrator estimation heuristics | `~/.config/mosaic/guides/ORCHESTRATOR-LEARNINGS.md` | +| Mission lifecycle / multi-session orchestration | `~/.config/mosaic/guides/ORCHESTRATOR-PROTOCOL.md` | ## Embedded Delivery Cycle (Hard Rule) diff --git a/bin/mosaic b/bin/mosaic index b112e5d..a86c948 100755 --- a/bin/mosaic +++ b/bin/mosaic @@ -51,6 +51,14 @@ Management: release-upgrade [...] Upgrade installed Mosaic release project-upgrade [...] Clean up stale SOUL.md/CLAUDE.md in a project +Coordinator (r0): + coord Manual coordinator tools + init Initialize a new mission + mission Show mission progress dashboard + status Check agent session health + continue Generate continuation prompt + resume Crash recovery + Options: -h, --help Show this help -v, --version Show version @@ -187,6 +195,8 @@ launch_claude() { check_runtime "claude" check_sequential_thinking "claude" + _check_resumable_session + # Claude supports --append-system-prompt for direct injection local runtime_prompt runtime_prompt="$(build_runtime_prompt "claude")" @@ -201,6 +211,8 @@ launch_opencode() { check_runtime "opencode" check_sequential_thinking "opencode" + _check_resumable_session + # OpenCode reads from ~/.config/opencode/AGENTS.md ensure_runtime_config "opencode" "$HOME/.config/opencode/AGENTS.md" echo "[mosaic] Launching OpenCode..." @@ -214,6 +226,8 @@ launch_codex() { check_runtime "codex" check_sequential_thinking "codex" + _check_resumable_session + # Codex reads from ~/.codex/instructions.md ensure_runtime_config "codex" "$HOME/.codex/instructions.md" echo "[mosaic] Launching Codex..." @@ -325,6 +339,76 @@ run_seq() { esac } +run_coord() { + check_mosaic_home + local subcmd="${1:-help}" + shift || true + + local tool_dir="$MOSAIC_HOME/tools/orchestrator" + + case "$subcmd" in + status|session) + exec bash "$tool_dir/session-status.sh" "$@" + ;; + init) + exec bash "$tool_dir/mission-init.sh" "$@" + ;; + mission|progress) + exec bash "$tool_dir/mission-status.sh" "$@" + ;; + continue|next) + exec bash "$tool_dir/continue-prompt.sh" "$@" + ;; + resume|recover) + exec bash "$tool_dir/session-resume.sh" "$@" + ;; + help|*) + cat < [opts] Initialize a new mission + mission [--project ] Show mission progress dashboard + status [--project ] Check agent session health + continue [--project ] Generate continuation prompt for next session + resume [--project ] Crash recovery (detect dirty state, generate fix) + +Examples: + mosaic coord init --name "Security Fix" --milestones "Critical,High,Medium" + mosaic coord mission + mosaic coord continue --copy + +COORD_USAGE + ;; + esac +} + +# Resume advisory — prints warning if active mission or stale session detected +_check_resumable_session() { + local mission_file=".mosaic/orchestrator/mission.json" + local lock_file=".mosaic/orchestrator/session.lock" + + command -v jq &>/dev/null || return 0 + + if [[ -f "$lock_file" ]]; then + local pid + pid="$(jq -r '.pid // 0' "$lock_file" 2>/dev/null)" + if [[ -n "$pid" ]] && [[ "$pid" != "0" ]] && ! kill -0 "$pid" 2>/dev/null; then + echo "[mosaic] Previous orchestration session detected (crashed)." + echo "[mosaic] Run: mosaic coord resume" + echo "" + fi + elif [[ -f "$mission_file" ]]; then + local status + status="$(jq -r '.status // "inactive"' "$mission_file" 2>/dev/null)" + if [[ "$status" == "active" ]]; then + echo "[mosaic] Active mission detected. Generate continuation prompt with:" + echo "[mosaic] mosaic coord continue" + echo "" + fi + fi +} + run_bootstrap() { check_mosaic_home exec "$MOSAIC_HOME/bin/mosaic-bootstrap-repo" "$@" @@ -397,6 +481,7 @@ case "$command" in sync) run_sync "$@" ;; seq) run_seq "$@" ;; bootstrap) run_bootstrap "$@" ;; + coord) run_coord "$@" ;; upgrade) run_upgrade "$@" ;; release-upgrade) run_release_upgrade "$@" ;; project-upgrade) run_project_upgrade "$@" ;; diff --git a/bin/mosaic-doctor b/bin/mosaic-doctor index 6e75ece..4e1348d 100755 --- a/bin/mosaic-doctor +++ b/bin/mosaic-doctor @@ -172,6 +172,14 @@ expect_file "$MOSAIC_HOME/tools/git/ci-queue-wait.sh" expect_file "$MOSAIC_HOME/tools/git/pr-ci-wait.sh" expect_file "$MOSAIC_HOME/tools/orchestrator-matrix/transport/matrix_transport.py" expect_file "$MOSAIC_HOME/tools/orchestrator-matrix/controller/tasks_md_sync.py" +expect_file "$MOSAIC_HOME/guides/ORCHESTRATOR-PROTOCOL.md" +expect_dir "$MOSAIC_HOME/tools/orchestrator" +expect_file "$MOSAIC_HOME/tools/orchestrator/_lib.sh" +expect_file "$MOSAIC_HOME/tools/orchestrator/mission-init.sh" +expect_file "$MOSAIC_HOME/tools/orchestrator/mission-status.sh" +expect_file "$MOSAIC_HOME/tools/orchestrator/continue-prompt.sh" +expect_file "$MOSAIC_HOME/tools/orchestrator/session-status.sh" +expect_file "$MOSAIC_HOME/tools/orchestrator/session-resume.sh" expect_file "$MOSAIC_HOME/runtime/mcp/SEQUENTIAL-THINKING.json" expect_file "$MOSAIC_HOME/runtime/claude/RUNTIME.md" expect_file "$MOSAIC_HOME/runtime/codex/RUNTIME.md" diff --git a/guides/ORCHESTRATOR-PROTOCOL.md b/guides/ORCHESTRATOR-PROTOCOL.md new file mode 100644 index 0000000..12a39c1 --- /dev/null +++ b/guides/ORCHESTRATOR-PROTOCOL.md @@ -0,0 +1,257 @@ +# Orchestrator Protocol — Mission Lifecycle Guide + +> **Operational guide for agent sessions.** Distilled from the full specification at +> `jarvis-brain/docs/protocols/ORCHESTRATOR-PROTOCOL.md` (1,066 lines). +> +> Load this guide when: active mission detected, multi-milestone orchestration, mission continuation. +> Load `ORCHESTRATOR.md` for per-session execution protocol (planning, coding, review, commit cycle). + +--- + +## 1. Relationship to ORCHESTRATOR.md + +| Concern | Guide | +|---------|-------| +| How to execute within a session (plan, code, test, review, commit) | `ORCHESTRATOR.md` | +| How to manage a mission across sessions (resume, continue, handoff) | **This guide** | +| Both guides are active simultaneously during orchestration missions. | + +--- + +## 2. Mission Manifest + +**Location:** `docs/MISSION-MANIFEST.md` +**Owner:** Orchestrator (sole writer) +**Template:** `~/.config/mosaic/templates/docs/MISSION-MANIFEST.md.template` + +The manifest is the persistent document tracking full mission scope, status, milestones, and session history. It survives session death. + +### Update Rules + +- Update **Phase** when transitioning (Intake → Planning → Execution → Continuation → Completion) +- Update **Current Milestone** when starting a new milestone +- Update **Progress** after each milestone completion +- Append to **Session History** at session start and end +- Update **Status** to `completed` only when ALL success criteria are verified + +### Hard Rule + +The manifest is the source of truth for mission scope. If the manifest says a milestone is done, it is done. If it says remaining, it remains. + +--- + +## 3. Scratchpad Protocol + +**Location:** `docs/scratchpads/{mission-id}.md` +**Template:** `~/.config/mosaic/templates/docs/mission-scratchpad.md.template` + +### Rules + +1. **First action** — Before ANY planning or coding, write the mission prompt to the scratchpad +2. **Append-only** — NEVER delete or overwrite previous entries +3. **Session log** — Record session start, tasks done, and outcome at session end +4. **Decisions** — Record all planning decisions with rationale +5. **Corrections** — Record course corrections from human or coordinator +6. **Never deleted** — Scratchpads survive mission completion (archival reference) + +--- + +## 4. TASKS.md as Control Plane + +**Location:** `docs/TASKS.md` +**Owner:** Orchestrator (sole writer). Workers read but NEVER modify. + +### Table Schema + +```markdown +| id | status | milestone | description | pr | notes | +``` + +### Status Values + +`not-started` → `in-progress` → `done` (or `blocked` / `failed`) + +### Planning Tasks Are First-Class + +Include explicit planning tasks (e.g., `PLAN-001: Break down milestone into tasks`). These count toward progress. + +### Post-Merge Tasks Are Explicit + +Include verification tasks after merge: CI check, deployment verification, Playwright test. Don't assume they happen automatically. + +--- + +## 5. Session Resume Protocol + +When starting a session and an active mission is detected, follow this checklist: + +### Detection (5-point check) + +1. `docs/MISSION-MANIFEST.md` exists → read Phase, Current Milestone, Progress +2. `docs/scratchpads/*.md` exists → read latest scratchpad for decisions and corrections +3. `docs/TASKS.md` exists → read task state (what's done, what's next) +4. Git state → current branch, open PRs, recent commits +5. Provider state → open issues, milestone status (if accessible) + +### Resume Procedure + +1. Read the mission manifest FIRST +2. Read the scratchpad for session history and corrections +3. Read TASKS.md for current task state +4. Identify the next `not-started` or `in-progress` task +5. Continue execution from that task +6. Update Session History in the manifest + +### Dirty State Recovery + +| State | Recovery | +|-------|----------| +| Dirty git working tree | Stash changes, log stash ref in scratchpad, resume clean | +| Open PR in bad state | Check PR status, close if broken, re-create if needed | +| Half-created issues | Audit issues against TASKS.md, reconcile | +| Tasks marked in-progress | Check if work was committed; if so, mark done; if not, restart task | + +### Hard Rule + +Session state is NEVER automatically deleted. The coordinator (human or automated) must explicitly request cleanup. + +--- + +## 6. Mission Continuation + +When a milestone completes and more milestones remain: + +### Agent Handoff (at ~55-60% context) + +If context usage is high, produce a handoff message: + +1. Update TASKS.md with final task statuses +2. Update mission manifest with session results +3. Append session summary to scratchpad +4. Commit all state files +5. The coordinator will generate a continuation prompt for the next session + +### Continuation Prompt Format + +The coordinator generates this (via `mosaic coord continue`): + +``` +## Continuation Mission +Continue **{mission}** from existing state. +- Read docs/MISSION-MANIFEST.md for scope and status +- Read docs/scratchpads/{id}.md for decisions +- Read docs/TASKS.md for current state +- Continue from task {next-task-id} +``` + +### Between Sessions (r0 manual) + +1. Agent stops (expected — this is the confirmed stamina limitation) +2. Human runs `mosaic coord mission` to check status +3. Human runs `mosaic coord continue` to generate continuation prompt +4. Human launches new session and pastes the prompt +5. New agent reads manifest, scratchpad, TASKS.md and continues + +--- + +## 7. Failure Taxonomy Quick Reference + +| Code | Type | Recovery | +|------|------|----------| +| F1 | Premature Stop | Continuation prompt → new session (most common) | +| F2 | Context Exhaustion | Handoff message → new session | +| F3 | Session Crash | Check git state → `mosaic coord resume` → new session | +| F4 | Error Spiral | Kill session, mark task blocked, skip to next | +| F5 | Quality Gate Failure | Create QA remediation task | +| F6 | Infrastructure Failure | Pause, retry when service recovers | +| F7 | False Completion | Append correction to scratchpad, relaunch | +| F8 | Scope Drift | Kill session, relaunch with scratchpad ref | +| F9 | Subagent Failure | Orchestrator retries or creates remediation | +| F10 | Deadlock | Escalate to human | + +### F1: Premature Stop — Detailed Recovery + +This is the confirmed, most common failure. Every session will eventually trigger F1. + +1. Session ends with tasks remaining in TASKS.md +2. Run `mosaic coord mission` — verify milestone status +3. If milestone complete: verify CI green, deployed, issues closed +4. Run `mosaic coord continue` — generates scoped continuation prompt +5. Launch new session, paste prompt +6. New session reads state and continues from next pending task + +--- + +## 8. r0 Manual Coordinator Process + +In r0, the Coordinator is Jason + shell scripts. No daemon. No automation. + +### Commands + +| Command | Purpose | +|---------|---------| +| `mosaic coord init --name "..." --milestones "..."` | Initialize a new mission | +| `mosaic coord mission` | Show mission progress dashboard | +| `mosaic coord status` | Check if agent session is still running | +| `mosaic coord continue` | Generate continuation prompt for next session | +| `mosaic coord resume` | Crash recovery (detect dirty state, generate fix) | +| `mosaic coord resume --clean-lock` | Clear stale session lock after review | + +### Typical Workflow + +``` +init → launch agent → [agent works] → agent stops → +status → mission → continue → launch agent → repeat +``` + +--- + +## 9. Operational Checklist + +### Pre-Mission + +- [ ] Mission initialized: `mosaic coord init` +- [ ] docs/MISSION-MANIFEST.md exists with scope and milestones +- [ ] docs/TASKS.md scaffolded +- [ ] docs/scratchpads/{id}.md scaffolded +- [ ] Success criteria defined in manifest + +### Session Start + +- [ ] Read manifest → know phase, milestone, progress +- [ ] Read scratchpad → know decisions, corrections, history +- [ ] Read TASKS.md → know what's done and what's next +- [ ] Write session start to scratchpad +- [ ] Update Session History in manifest + +### Planning Gate (Hard Gate — No Coding Until Complete) + +- [ ] Milestones created in provider (Gitea/GitHub) +- [ ] Issues created for all milestone tasks +- [ ] TASKS.md populated with all planned tasks (including planning + verification tasks) +- [ ] All planning artifacts committed and pushed + +### Per-Task + +- [ ] Update task status to `in-progress` in TASKS.md +- [ ] Execute task following ORCHESTRATOR.md cycle +- [ ] Update task status to `done` (or `blocked`/`failed`) +- [ ] Commit, push + +### Milestone Completion + +- [ ] All milestone tasks in TASKS.md are `done` +- [ ] CI/pipeline green +- [ ] PR merged to `main` +- [ ] Issues closed +- [ ] Update manifest: milestone status → completed +- [ ] Update scratchpad: session log entry +- [ ] If deployment target: verify accessible + +### Mission Completion + +- [ ] ALL milestones completed +- [ ] ALL success criteria verified with evidence +- [ ] manifest status → completed +- [ ] Final scratchpad entry with completion evidence +- [ ] Release tag created and pushed (if applicable) diff --git a/templates/docs/MISSION-MANIFEST.md.template b/templates/docs/MISSION-MANIFEST.md.template new file mode 100644 index 0000000..6d69f14 --- /dev/null +++ b/templates/docs/MISSION-MANIFEST.md.template @@ -0,0 +1,53 @@ +# Mission Manifest — ${MISSION_NAME} + +> Persistent document tracking full mission scope, status, and session history. +> Updated by the orchestrator at each phase transition and milestone completion. + +## Mission + +**ID:** ${MISSION_ID} +**Statement:** ${MISSION_STATEMENT} +**Phase:** Intake +**Current Milestone:** — +**Progress:** 0 / ${MILESTONE_COUNT} milestones +**Status:** not-started +**Last Updated:** ${CREATED_AT} + +## Success Criteria + +${SUCCESS_CRITERIA} + +## Milestones + +| # | ID | Name | Status | Branch | Issue | Started | Completed | +|---|-----|------|--------|--------|-------|---------|-----------| +${MILESTONES_TABLE} + +## Deployment + +| Target | URL | Method | +|--------|-----|--------| +${DEPLOYMENT_TABLE} + +## Coordination + +- **Primary Agent:** ${PRIMARY_RUNTIME} +- **Sibling Agents:** ${SIBLING_AGENTS} +- **Shared Contracts:** ${SHARED_CONTRACTS} + +## Token Budget + +| Metric | Value | +|--------|-------| +| Budget | ${TOKEN_BUDGET} | +| Used | 0 | +| Mode | normal | + +## Session History + +| Session | Runtime | Started | Duration | Ended Reason | Last Task | +|---------|---------|---------|----------|--------------|-----------| + +## Scratchpad + +Path: `docs/scratchpads/${MISSION_ID}.md` diff --git a/templates/docs/continuation-prompt.md.template b/templates/docs/continuation-prompt.md.template new file mode 100644 index 0000000..d190618 --- /dev/null +++ b/templates/docs/continuation-prompt.md.template @@ -0,0 +1,36 @@ +## Continuation Mission + +Continue **${MISSION_NAME}** from existing state. + +## Setup + +- **Project:** ${PROJECT_PATH} +- **State:** docs/TASKS.md (already populated — ${TASKS_DONE}/${TASKS_TOTAL} tasks complete) +- **Manifest:** docs/MISSION-MANIFEST.md +- **Scratchpad:** docs/scratchpads/${MISSION_ID}.md +- **Protocol:** ~/.config/mosaic/guides/ORCHESTRATOR.md +- **Quality gates:** ${QUALITY_GATES} + +## Resume Point + +- **Current milestone:** ${CURRENT_MILESTONE_NAME} (${CURRENT_MILESTONE_ID}) +- **Next task:** ${NEXT_TASK_ID} +- **Progress:** ${TASKS_DONE}/${TASKS_TOTAL} tasks (${PROGRESS_PCT}%) +- **Branch:** ${CURRENT_BRANCH} + +## Previous Session Context + +- **Session:** ${PREV_SESSION_ID} (${PREV_RUNTIME}, ${PREV_DURATION}) +- **Ended:** ${PREV_ENDED_REASON} +- **Last completed task:** ${PREV_LAST_TASK} + +## Instructions + +1. Read `~/.config/mosaic/guides/ORCHESTRATOR.md` for full protocol +2. Read `docs/MISSION-MANIFEST.md` for mission scope and status +3. Read `docs/scratchpads/${MISSION_ID}.md` for session history and decisions +4. Read `docs/TASKS.md` for current task state +5. `git pull --rebase` to sync latest changes +6. Continue execution from task **${NEXT_TASK_ID}** +7. Follow Two-Phase Completion Protocol +8. You are the SOLE writer of `docs/TASKS.md` diff --git a/templates/docs/mission-scratchpad.md.template b/templates/docs/mission-scratchpad.md.template new file mode 100644 index 0000000..b91e367 --- /dev/null +++ b/templates/docs/mission-scratchpad.md.template @@ -0,0 +1,27 @@ +# Mission Scratchpad — ${MISSION_NAME} + +> Append-only log. NEVER delete entries. NEVER overwrite sections. +> This is the orchestrator's working memory across sessions. + +## Original Mission Prompt + +``` +${MISSION_PROMPT} +``` + +## Planning Decisions + + + +## Session Log + +| Session | Date | Milestone | Tasks Done | Outcome | +|---------|------|-----------|------------|---------| + +## Open Questions + + + +## Corrections + + diff --git a/templates/repo/.mosaic/orchestrator/mission.json b/templates/repo/.mosaic/orchestrator/mission.json new file mode 100644 index 0000000..8ab98fc --- /dev/null +++ b/templates/repo/.mosaic/orchestrator/mission.json @@ -0,0 +1,14 @@ +{ + "schema_version": 1, + "mission_id": "", + "name": "", + "description": "", + "project_path": "", + "created_at": "", + "status": "inactive", + "task_prefix": "", + "quality_gates": "", + "milestone_version": "0.0.1", + "milestones": [], + "sessions": [] +} diff --git a/templates/repo/scripts/agent/session-end.sh b/templates/repo/scripts/agent/session-end.sh index 3b3bbc2..7605db6 100755 --- a/templates/repo/scripts/agent/session-end.sh +++ b/templates/repo/scripts/agent/session-end.sh @@ -8,6 +8,34 @@ source "$SCRIPT_DIR/common.sh" ensure_repo_root load_repo_hooks +# ─── Mission session cleanup (ORCHESTRATOR-PROTOCOL) ──────────────────────── +ORCH_DIR=".mosaic/orchestrator" +MISSION_JSON="$ORCH_DIR/mission.json" +SESSION_LOCK="$ORCH_DIR/session.lock" +COORD_LIB="$HOME/.config/mosaic/tools/orchestrator/_lib.sh" + +if [[ -f "$SESSION_LOCK" ]] && [[ -f "$COORD_LIB" ]] && command -v jq &>/dev/null; then + # shellcheck source=/dev/null + source "$COORD_LIB" + + sess_id="$(jq -r '.session_id // ""' "$SESSION_LOCK")" + if [[ -n "$sess_id" && -f "$MISSION_JSON" ]]; then + # Update mission.json: mark session ended + updated="$(jq \ + --arg sid "$sess_id" \ + --arg ts "$(iso_now)" \ + --arg reason "completed" \ + '(.sessions[] | select(.session_id == $sid)) |= . + { + ended_at: $ts, + ended_reason: $reason + }' "$MISSION_JSON")" + echo "$updated" > "$MISSION_JSON.tmp" && mv "$MISSION_JSON.tmp" "$MISSION_JSON" + echo "[agent-framework] Session $sess_id recorded in mission state" + fi + + session_lock_clear "." +fi + if declare -F mosaic_hook_session_end >/dev/null 2>&1; then run_step "Run repo end hook" mosaic_hook_session_end else diff --git a/templates/repo/scripts/agent/session-start.sh b/templates/repo/scripts/agent/session-start.sh index 89e8cd1..b268573 100755 --- a/templates/repo/scripts/agent/session-start.sh +++ b/templates/repo/scripts/agent/session-start.sh @@ -16,6 +16,72 @@ if git rev-parse --is-inside-work-tree >/dev/null 2>&1 && has_remote; then fi fi +# ─── Mission state detection (ORCHESTRATOR-PROTOCOL) ──────────────────────── +ORCH_DIR=".mosaic/orchestrator" +MISSION_JSON="$ORCH_DIR/mission.json" +COORD_LIB="$HOME/.config/mosaic/tools/orchestrator/_lib.sh" + +if [[ -f "$MISSION_JSON" ]] && command -v jq &>/dev/null; then + mission_status="$(jq -r '.status // "inactive"' "$MISSION_JSON")" + + if [[ "$mission_status" == "active" || "$mission_status" == "paused" ]]; then + mission_name="$(jq -r '.name // "unnamed"' "$MISSION_JSON")" + echo "" + echo "=========================================" + echo "ACTIVE MISSION DETECTED" + echo "=========================================" + echo " Mission: $mission_name" + + # Extract key fields from manifest if present + manifest="docs/MISSION-MANIFEST.md" + if [[ -f "$manifest" ]]; then + phase="$(grep -m1 '^\*\*Phase:\*\*' "$manifest" 2>/dev/null | sed 's/.*\*\*Phase:\*\* //' || true)" + milestone="$(grep -m1 '^\*\*Current Milestone:\*\*' "$manifest" 2>/dev/null | sed 's/.*\*\*Current Milestone:\*\* //' || true)" + progress="$(grep -m1 '^\*\*Progress:\*\*' "$manifest" 2>/dev/null | sed 's/.*\*\*Progress:\*\* //' || true)" + [[ -n "$phase" ]] && echo " Phase: $phase" + [[ -n "$milestone" ]] && echo " Milestone: $milestone" + [[ -n "$progress" ]] && echo " Progress: $progress" + fi + + # Task counts + if [[ -f "docs/TASKS.md" ]]; then + total="$(grep -c '^|' "docs/TASKS.md" 2>/dev/null || echo 0)" + done_count="$(grep -ci '| done \|| completed ' "docs/TASKS.md" 2>/dev/null || echo 0)" + echo " Tasks: ~$done_count done of ~$((total - 2)) total" + fi + + # Scratchpad + if [[ -d "docs/scratchpads" ]]; then + latest_sp="$(ls -t docs/scratchpads/*.md 2>/dev/null | head -1 || true)" + [[ -n "$latest_sp" ]] && echo " Scratchpad: $latest_sp" + fi + + echo "" + echo " Resume: Read manifest + scratchpad before taking action." + echo " Protocol: ~/.config/mosaic/guides/ORCHESTRATOR-PROTOCOL.md" + echo "=========================================" + echo "" + + # Register session if coordinator lib is available + if [[ -f "$COORD_LIB" ]]; then + # shellcheck source=/dev/null + source "$COORD_LIB" + sess_id="$(next_session_id ".")" + runtime="${MOSAIC_RUNTIME:-unknown}" + session_lock_write "." "$sess_id" "$runtime" "$$" + + # Append session to mission.json + updated="$(jq \ + --arg sid "$sess_id" \ + --arg rt "$runtime" \ + --arg ts "$(iso_now)" \ + '.sessions += [{"session_id":$sid,"runtime":$rt,"started_at":$ts,"ended_at":"","ended_reason":"","milestone_at_end":"","tasks_completed":[],"last_task_id":""}]' \ + "$MISSION_JSON")" + echo "$updated" > "$MISSION_JSON.tmp" && mv "$MISSION_JSON.tmp" "$MISSION_JSON" + fi + fi +fi + if declare -F mosaic_hook_session_start >/dev/null 2>&1; then run_step "Run repo start hook" mosaic_hook_session_start else diff --git a/tools/orchestrator/_lib.sh b/tools/orchestrator/_lib.sh new file mode 100755 index 0000000..69b50cf --- /dev/null +++ b/tools/orchestrator/_lib.sh @@ -0,0 +1,386 @@ +#!/usr/bin/env bash +# +# _lib.sh — Shared functions for r0 coordinator scripts +# +# Usage: source ~/.config/mosaic/tools/orchestrator/_lib.sh +# +# Provides state file access, TASKS.md parsing, session lock management, +# process health checks, and formatting utilities. + +MOSAIC_HOME="${MOSAIC_HOME:-$HOME/.config/mosaic}" +ORCH_SUBDIR=".mosaic/orchestrator" +MISSION_FILE="mission.json" +SESSION_LOCK_FILE="session.lock" +MANIFEST_FILE="docs/MISSION-MANIFEST.md" +TASKS_MD="docs/TASKS.md" +SCRATCHPAD_DIR="docs/scratchpads" + +# Thresholds (seconds) +STALE_THRESHOLD=300 # 5 minutes +DEAD_THRESHOLD=1800 # 30 minutes + +# ─── Color support ─────────────────────────────────────────────────────────── + +if [[ -t 1 ]]; then + C_GREEN='\033[0;32m' + C_RED='\033[0;31m' + C_YELLOW='\033[0;33m' + C_CYAN='\033[0;36m' + C_BOLD='\033[1m' + C_DIM='\033[2m' + C_RESET='\033[0m' +else + C_GREEN='' C_RED='' C_YELLOW='' C_CYAN='' C_BOLD='' C_DIM='' C_RESET='' +fi + +# ─── Dependency checks ────────────────────────────────────────────────────── + +_require_jq() { + if ! command -v jq &>/dev/null; then + echo -e "${C_RED}Error: jq is required but not installed${C_RESET}" >&2 + return 1 + fi +} + +# ─── Project / state file access ──────────────────────────────────────────── + +# Return the orchestrator directory for a project +orch_dir() { + local project="${1:-.}" + echo "$project/$ORCH_SUBDIR" +} + +# Return the mission.json path for a project +mission_path() { + local project="${1:-.}" + echo "$(orch_dir "$project")/$MISSION_FILE" +} + +# Exit with error if mission.json is missing or inactive +require_mission() { + local project="${1:-.}" + local mp + mp="$(mission_path "$project")" + + if [[ ! -f "$mp" ]]; then + echo -e "${C_RED}No mission found at $mp${C_RESET}" >&2 + echo "Initialize one with: mosaic coord init --name \"Mission Name\"" >&2 + return 1 + fi + + _require_jq || return 1 + + local status + status="$(jq -r '.status // "inactive"' "$mp")" + if [[ "$status" == "inactive" ]]; then + echo -e "${C_YELLOW}Mission exists but is inactive. Initialize with: mosaic coord init${C_RESET}" >&2 + return 1 + fi +} + +# Cat mission.json (caller pipes to jq) +load_mission() { + local project="${1:-.}" + cat "$(mission_path "$project")" +} + +# ─── Atomic JSON write ────────────────────────────────────────────────────── + +write_json() { + local path="$1" + local content="$2" + local tmp + tmp="$(mktemp "${path}.tmp.XXXXXX")" + echo "$content" > "$tmp" + mv "$tmp" "$path" +} + +# ─── TASKS.md parsing ─────────────────────────────────────────────────────── + +# Parse TASKS.md pipe-delimited table and output JSON counts +count_tasks_md() { + local project="${1:-.}" + local tasks_file="$project/$TASKS_MD" + + if [[ ! -f "$tasks_file" ]]; then + echo '{"total":0,"done":0,"in_progress":0,"pending":0,"failed":0,"blocked":0}' + return + fi + + awk -F'|' ' + /^\|.*[Ii][Dd].*[Ss]tatus/ { header=1; next } + header && /^\|.*---/ { data=1; next } + data && /^\|/ { + gsub(/^[ \t]+|[ \t]+$/, "", $3) + status = tolower($3) + total++ + if (status == "done" || status == "completed") done++ + else if (status == "in-progress" || status == "in_progress") inprog++ + else if (status == "not-started" || status == "pending" || status == "todo") pending++ + else if (status == "failed") failed++ + else if (status == "blocked") blocked++ + } + data && !/^\|/ && total > 0 { exit } + END { + printf "{\"total\":%d,\"done\":%d,\"in_progress\":%d,\"pending\":%d,\"failed\":%d,\"blocked\":%d}\n", + total, done, inprog, pending, failed, blocked + } + ' "$tasks_file" +} + +# Return the ID of the first not-started/pending task +find_next_task() { + local project="${1:-.}" + local tasks_file="$project/$TASKS_MD" + + if [[ ! -f "$tasks_file" ]]; then + echo "" + return + fi + + awk -F'|' ' + /^\|.*[Ii][Dd].*[Ss]tatus/ { header=1; next } + header && /^\|.*---/ { data=1; next } + data && /^\|/ { + gsub(/^[ \t]+|[ \t]+$/, "", $2) + gsub(/^[ \t]+|[ \t]+$/, "", $3) + status = tolower($3) + if (status == "not-started" || status == "pending" || status == "todo") { + print $2 + exit + } + } + ' "$tasks_file" +} + +# ─── Session lock management ──────────────────────────────────────────────── + +session_lock_path() { + local project="${1:-.}" + echo "$(orch_dir "$project")/$SESSION_LOCK_FILE" +} + +session_lock_read() { + local project="${1:-.}" + local lp + lp="$(session_lock_path "$project")" + if [[ -f "$lp" ]]; then + cat "$lp" + return 0 + fi + return 1 +} + +session_lock_write() { + local project="${1:-.}" + local session_id="$2" + local runtime="$3" + local pid="$4" + local milestone_id="${5:-}" + + local lp + lp="$(session_lock_path "$project")" + + _require_jq || return 1 + + local json + json=$(jq -n \ + --arg sid "$session_id" \ + --arg rt "$runtime" \ + --arg pid "$pid" \ + --arg ts "$(iso_now)" \ + --arg pp "$(cd "$project" && pwd)" \ + --arg mid "$milestone_id" \ + '{ + session_id: $sid, + runtime: $rt, + pid: ($pid | tonumber), + started_at: $ts, + project_path: $pp, + milestone_id: $mid + }') + + write_json "$lp" "$json" +} + +session_lock_clear() { + local project="${1:-.}" + local lp + lp="$(session_lock_path "$project")" + rm -f "$lp" +} + +# ─── Process health checks ────────────────────────────────────────────────── + +is_pid_alive() { + local pid="$1" + kill -0 "$pid" 2>/dev/null +} + +detect_agent_runtime() { + local pid="$1" + local cmdline + if [[ -f "/proc/$pid/cmdline" ]]; then + cmdline="$(tr '\0' ' ' < "/proc/$pid/cmdline")" + if [[ "$cmdline" == *claude* ]]; then + echo "claude" + elif [[ "$cmdline" == *codex* ]]; then + echo "codex" + elif [[ "$cmdline" == *opencode* ]]; then + echo "opencode" + else + echo "unknown" + fi + else + echo "unknown" + fi +} + +# ─── Time / formatting utilities ──────────────────────────────────────────── + +iso_now() { + date -u +"%Y-%m-%dT%H:%M:%SZ" +} + +epoch_now() { + date +%s +} + +# Convert ISO timestamp to epoch seconds +iso_to_epoch() { + local ts="$1" + date -d "$ts" +%s 2>/dev/null || echo 0 +} + +# Return most recent modification time (epoch) of key project files +last_activity_time() { + local project="${1:-.}" + local latest=0 + local ts + + for f in \ + "$project/$TASKS_MD" \ + "$project/$(orch_dir "$project")/$MISSION_FILE" \ + "$(orch_dir "$project")/state.json"; do + if [[ -f "$f" ]]; then + ts="$(stat -c %Y "$f" 2>/dev/null || echo 0)" + (( ts > latest )) && latest=$ts + fi + done + + # Also check git log for last commit time + if git -C "$project" rev-parse --is-inside-work-tree &>/dev/null; then + ts="$(git -C "$project" log -1 --format=%ct 2>/dev/null || echo 0)" + (( ts > latest )) && latest=$ts + fi + + echo "$latest" +} + +# Format seconds-ago into human-readable string +format_ago() { + local epoch="$1" + local now + now="$(epoch_now)" + local diff=$(( now - epoch )) + + if (( diff < 60 )); then + echo "${diff}s ago" + elif (( diff < 3600 )); then + echo "$(( diff / 60 ))m ago" + elif (( diff < 86400 )); then + echo "$(( diff / 3600 ))h $(( (diff % 3600) / 60 ))m ago" + else + echo "$(( diff / 86400 ))d ago" + fi +} + +# Format seconds into duration string +format_duration() { + local secs="$1" + if (( secs < 60 )); then + echo "${secs}s" + elif (( secs < 3600 )); then + echo "$(( secs / 60 ))m $(( secs % 60 ))s" + else + echo "$(( secs / 3600 ))h $(( (secs % 3600) / 60 ))m" + fi +} + +# ─── Session ID generation ────────────────────────────────────────────────── + +next_session_id() { + local project="${1:-.}" + local mp + mp="$(mission_path "$project")" + + if [[ ! -f "$mp" ]]; then + echo "sess-001" + return + fi + + _require_jq || { echo "sess-001"; return; } + + local count + count="$(jq '.sessions | length' "$mp")" + printf "sess-%03d" "$(( count + 1 ))" +} + +# ─── Milestone helpers ─────────────────────────────────────────────────────── + +# Get current milestone (first in-progress, or first pending) +current_milestone_id() { + local project="${1:-.}" + _require_jq || return 1 + + local mp + mp="$(mission_path "$project")" + [[ -f "$mp" ]] || return 1 + + local mid + mid="$(jq -r '[.milestones[] | select(.status == "in-progress")][0].id // empty' "$mp")" + if [[ -z "$mid" ]]; then + mid="$(jq -r '[.milestones[] | select(.status == "pending")][0].id // empty' "$mp")" + fi + echo "$mid" +} + +# Get milestone name by ID +milestone_name() { + local project="${1:-.}" + local mid="$2" + _require_jq || return 1 + + local mp + mp="$(mission_path "$project")" + [[ -f "$mp" ]] || return 1 + + jq -r --arg id "$mid" '.milestones[] | select(.id == $id) | .name // empty' "$mp" +} + +# Get next milestone after the given one +next_milestone_id() { + local project="${1:-.}" + local current_id="$2" + _require_jq || return 1 + + local mp + mp="$(mission_path "$project")" + [[ -f "$mp" ]] || return 1 + + jq -r --arg cid "$current_id" ' + .milestones as $ms | + ($ms | to_entries | map(select(.value.id == $cid)) | .[0].key // -1) as $idx | + if $idx >= 0 and ($idx + 1) < ($ms | length) then + $ms[$idx + 1].id + else + empty + end + ' "$mp" +} + +# ─── Slugify ───────────────────────────────────────────────────────────────── + +slugify() { + echo "$1" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]/-/g; s/--*/-/g; s/^-//; s/-$//' +} diff --git a/tools/orchestrator/continue-prompt.sh b/tools/orchestrator/continue-prompt.sh new file mode 100755 index 0000000..bc1221c --- /dev/null +++ b/tools/orchestrator/continue-prompt.sh @@ -0,0 +1,153 @@ +#!/usr/bin/env bash +set -euo pipefail +# +# continue-prompt.sh — Generate continuation prompt for next orchestrator session +# +# Usage: +# continue-prompt.sh [--project ] [--milestone ] [--copy] + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "$SCRIPT_DIR/_lib.sh" + +# ─── Parse arguments ───────────────────────────────────────────────────────── + +PROJECT="." +MILESTONE="" +COPY=false + +while [[ $# -gt 0 ]]; do + case "$1" in + --project) PROJECT="$2"; shift 2 ;; + --milestone) MILESTONE="$2"; shift 2 ;; + --copy) COPY=true; shift ;; + -h|--help) + echo "Usage: continue-prompt.sh [--project ] [--milestone ] [--copy]" + exit 0 + ;; + *) echo "Unknown option: $1" >&2; exit 1 ;; + esac +done + +_require_jq +require_mission "$PROJECT" + +# ─── Load mission data ────────────────────────────────────────────────────── + +mission="$(load_mission "$PROJECT")" +mission_name="$(echo "$mission" | jq -r '.name')" +mission_id="$(echo "$mission" | jq -r '.mission_id')" +quality_gates="$(echo "$mission" | jq -r '.quality_gates // "—"')" +project_path="$(echo "$mission" | jq -r '.project_path')" + +# Determine current milestone +if [[ -n "$MILESTONE" ]]; then + current_ms_id="$MILESTONE" +else + current_ms_id="$(current_milestone_id "$PROJECT")" +fi + +current_ms_name="" +if [[ -n "$current_ms_id" ]]; then + current_ms_name="$(milestone_name "$PROJECT" "$current_ms_id")" +fi + +# Task counts +task_counts="$(count_tasks_md "$PROJECT")" +tasks_total="$(echo "$task_counts" | jq '.total')" +tasks_done="$(echo "$task_counts" | jq '.done')" +pct=0 +(( tasks_total > 0 )) && pct=$(( (tasks_done * 100) / tasks_total )) + +# Next task +next_task="$(find_next_task "$PROJECT")" + +# Current branch +current_branch="" +if git -C "$PROJECT" rev-parse --is-inside-work-tree &>/dev/null; then + current_branch="$(git -C "$PROJECT" branch --show-current 2>/dev/null || echo "—")" +fi + +# Previous session info +session_count="$(echo "$mission" | jq '.sessions | length')" +prev_session_id="—" +prev_runtime="—" +prev_duration="—" +prev_ended_reason="—" +prev_last_task="—" + +if (( session_count > 0 )); then + last_idx=$(( session_count - 1 )) + prev_session_id="$(echo "$mission" | jq -r ".sessions[$last_idx].session_id // \"—\"")" + prev_runtime="$(echo "$mission" | jq -r ".sessions[$last_idx].runtime // \"—\"")" + prev_ended_reason="$(echo "$mission" | jq -r ".sessions[$last_idx].ended_reason // \"—\"")" + prev_last_task="$(echo "$mission" | jq -r ".sessions[$last_idx].last_task_id // \"—\"")" + + s_start="$(echo "$mission" | jq -r ".sessions[$last_idx].started_at // \"\"")" + s_end="$(echo "$mission" | jq -r ".sessions[$last_idx].ended_at // \"\"")" + if [[ -n "$s_start" && -n "$s_end" && "$s_end" != "" ]]; then + s_epoch="$(iso_to_epoch "$s_start")" + e_epoch="$(iso_to_epoch "$s_end")" + if (( e_epoch > 0 && s_epoch > 0 )); then + prev_duration="$(format_duration $(( e_epoch - s_epoch )))" + fi + fi +fi + +# ─── Generate prompt ──────────────────────────────────────────────────────── + +prompt="$(cat </dev/null; then + echo "$prompt" | wl-copy + echo -e "${C_GREEN}Continuation prompt copied to clipboard (wl-copy)${C_RESET}" >&2 + elif command -v xclip &>/dev/null; then + echo "$prompt" | xclip -selection clipboard + echo -e "${C_GREEN}Continuation prompt copied to clipboard (xclip)${C_RESET}" >&2 + else + echo -e "${C_YELLOW}No clipboard tool found (wl-copy or xclip). Printing to stdout.${C_RESET}" >&2 + echo "$prompt" + fi +else + echo "$prompt" +fi diff --git a/tools/orchestrator/mission-init.sh b/tools/orchestrator/mission-init.sh new file mode 100755 index 0000000..68cfe6b --- /dev/null +++ b/tools/orchestrator/mission-init.sh @@ -0,0 +1,283 @@ +#!/usr/bin/env bash +set -euo pipefail +# +# mission-init.sh — Initialize a new orchestration mission +# +# Usage: +# mission-init.sh --name [options] +# +# Options: +# --name Mission name (required) +# --project Project directory (default: CWD) +# --prefix Task ID prefix (e.g., MS) +# --milestones Milestone names, comma-separated +# --quality-gates Quality gate command string +# --version Milestone version (default: 0.0.1) +# --description Mission description +# --force Overwrite existing active mission + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "$SCRIPT_DIR/_lib.sh" + +# ─── Parse arguments ───────────────────────────────────────────────────────── + +NAME="" +PROJECT="." +PREFIX="" +MILESTONES="" +QUALITY_GATES="" +VERSION="0.0.1" +DESCRIPTION="" +FORCE=false + +while [[ $# -gt 0 ]]; do + case "$1" in + --name) NAME="$2"; shift 2 ;; + --project) PROJECT="$2"; shift 2 ;; + --prefix) PREFIX="$2"; shift 2 ;; + --milestones) MILESTONES="$2"; shift 2 ;; + --quality-gates) QUALITY_GATES="$2"; shift 2 ;; + --version) VERSION="$2"; shift 2 ;; + --description) DESCRIPTION="$2"; shift 2 ;; + --force) FORCE=true; shift ;; + -h|--help) + cat <<'USAGE' +mission-init.sh — Initialize a new orchestration mission + +Usage: mission-init.sh --name [options] + +Options: + --name Mission name (required) + --project Project directory (default: CWD) + --prefix Task ID prefix (e.g., MS) + --milestones Milestone names, comma-separated + --quality-gates Quality gate command string + --version Milestone version (default: 0.0.1) + --description Mission description + --force Overwrite existing active mission + +Example: + mosaic coord init \ + --name "Security Remediation" \ + --prefix SEC \ + --milestones "Critical Fixes,High Priority,Code Quality" \ + --quality-gates "pnpm lint && pnpm typecheck && pnpm test" +USAGE + exit 0 + ;; + *) echo "Unknown option: $1" >&2; exit 1 ;; + esac +done + +if [[ -z "$NAME" ]]; then + echo -e "${C_RED}Error: --name is required${C_RESET}" >&2 + exit 1 +fi + +_require_jq + +# ─── Validate project ─────────────────────────────────────────────────────── + +od="$(orch_dir "$PROJECT")" +if [[ ! -d "$od" ]]; then + echo -e "${C_RED}Error: $od not found. Run 'mosaic bootstrap' first.${C_RESET}" >&2 + exit 1 +fi + +# Check for existing active mission +mp="$(mission_path "$PROJECT")" +if [[ -f "$mp" ]]; then + existing_status="$(jq -r '.status // "inactive"' "$mp")" + if [[ "$existing_status" == "active" || "$existing_status" == "paused" ]] && [[ "$FORCE" != true ]]; then + existing_name="$(jq -r '.name // "unnamed"' "$mp")" + echo -e "${C_YELLOW}Active mission exists: $existing_name (status: $existing_status)${C_RESET}" >&2 + echo "Use --force to overwrite." >&2 + exit 1 + fi +fi + +# ─── Generate mission ID ──────────────────────────────────────────────────── + +MISSION_ID="$(slugify "$NAME")-$(date +%Y%m%d)" + +# ─── Build milestones array ───────────────────────────────────────────────── + +milestones_json="[]" +if [[ -n "$MILESTONES" ]]; then + IFS=',' read -ra ms_array <<< "$MILESTONES" + for i in "${!ms_array[@]}"; do + ms_name="$(echo "${ms_array[$i]}" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')" + ms_id="phase-$(( i + 1 ))" + ms_branch="$(slugify "$ms_name")" + milestones_json="$(echo "$milestones_json" | jq \ + --arg id "$ms_id" \ + --arg name "$ms_name" \ + --arg branch "$ms_branch" \ + '. + [{ + "id": $id, + "name": $name, + "status": "pending", + "branch": $branch, + "issue_ref": "", + "started_at": "", + "completed_at": "" + }]')" + done +fi + +MILESTONE_COUNT="$(echo "$milestones_json" | jq 'length')" + +# ─── Write mission.json ───────────────────────────────────────────────────── + +mission_json="$(jq -n \ + --arg mid "$MISSION_ID" \ + --arg name "$NAME" \ + --arg desc "$DESCRIPTION" \ + --arg pp "$(cd "$PROJECT" && pwd)" \ + --arg ts "$(iso_now)" \ + --arg prefix "$PREFIX" \ + --arg qg "$QUALITY_GATES" \ + --arg ver "$VERSION" \ + --argjson milestones "$milestones_json" \ + '{ + schema_version: 1, + mission_id: $mid, + name: $name, + description: $desc, + project_path: $pp, + created_at: $ts, + status: "active", + task_prefix: $prefix, + quality_gates: $qg, + milestone_version: $ver, + milestones: $milestones, + sessions: [] + }')" + +write_json "$mp" "$mission_json" + +# ─── Scaffold MISSION-MANIFEST.md ─────────────────────────────────────────── + +manifest_path="$PROJECT/$MANIFEST_FILE" +mkdir -p "$(dirname "$manifest_path")" + +if [[ ! -f "$manifest_path" ]] || [[ "$FORCE" == true ]]; then + # Build milestones table rows + ms_table="" + for i in $(seq 0 $(( MILESTONE_COUNT - 1 ))); do + ms_id="$(echo "$milestones_json" | jq -r ".[$i].id")" + ms_name="$(echo "$milestones_json" | jq -r ".[$i].name")" + ms_table+="| $(( i + 1 )) | $ms_id | $ms_name | pending | — | — | — | — |"$'\n' + done + + cat > "$manifest_path" < Persistent document tracking full mission scope, status, and session history. +> Updated by the orchestrator at each phase transition and milestone completion. + +## Mission + +**ID:** $MISSION_ID +**Statement:** $DESCRIPTION +**Phase:** Intake +**Current Milestone:** — +**Progress:** 0 / $MILESTONE_COUNT milestones +**Status:** active +**Last Updated:** $(date -u +"%Y-%m-%d %H:%M UTC") + +## Success Criteria + + + +## Milestones + +| # | ID | Name | Status | Branch | Issue | Started | Completed | +|---|-----|------|--------|--------|-------|---------|-----------| +$ms_table +## Deployment + +| Target | URL | Method | +|--------|-----|--------| +| — | — | — | + +## Token Budget + +| Metric | Value | +|--------|-------| +| Budget | — | +| Used | 0 | +| Mode | normal | + +## Session History + +| Session | Runtime | Started | Duration | Ended Reason | Last Task | +|---------|---------|---------|----------|--------------|-----------| + +## Scratchpad + +Path: \`docs/scratchpads/$MISSION_ID.md\` +EOF +fi + +# ─── Scaffold scratchpad ──────────────────────────────────────────────────── + +sp_dir="$PROJECT/$SCRATCHPAD_DIR" +sp_file="$sp_dir/$MISSION_ID.md" +mkdir -p "$sp_dir" + +if [[ ! -f "$sp_file" ]]; then + cat > "$sp_file" < Append-only log. NEVER delete entries. NEVER overwrite sections. +> This is the orchestrator's working memory across sessions. + +## Original Mission Prompt + +\`\`\` +(Paste the mission prompt here on first session) +\`\`\` + +## Planning Decisions + +## Session Log + +| Session | Date | Milestone | Tasks Done | Outcome | +|---------|------|-----------|------------|---------| + +## Open Questions + +## Corrections +EOF +fi + +# ─── Scaffold TASKS.md if absent ──────────────────────────────────────────── + +tasks_path="$PROJECT/$TASKS_MD" +mkdir -p "$(dirname "$tasks_path")" + +if [[ ! -f "$tasks_path" ]]; then + cat > "$tasks_path" < Single-writer: orchestrator only. Workers read but never modify. + +| id | status | milestone | description | pr | notes | +|----|--------|-----------|-------------|----|-------| +EOF +fi + +# ─── Report ────────────────────────────────────────────────────────────────── + +echo "" +echo -e "${C_GREEN}${C_BOLD}Mission initialized: $NAME${C_RESET}" +echo "" +echo -e " ${C_CYAN}Mission ID:${C_RESET} $MISSION_ID" +echo -e " ${C_CYAN}Milestones:${C_RESET} $MILESTONE_COUNT" +echo -e " ${C_CYAN}State:${C_RESET} $(mission_path "$PROJECT")" +echo -e " ${C_CYAN}Manifest:${C_RESET} $manifest_path" +echo -e " ${C_CYAN}Scratchpad:${C_RESET} $sp_file" +echo -e " ${C_CYAN}Tasks:${C_RESET} $tasks_path" +echo "" +echo "Next: Launch an agent session with 'mosaic claude' or generate a prompt with 'mosaic coord continue'" diff --git a/tools/orchestrator/mission-status.sh b/tools/orchestrator/mission-status.sh new file mode 100755 index 0000000..2fd17aa --- /dev/null +++ b/tools/orchestrator/mission-status.sh @@ -0,0 +1,181 @@ +#!/usr/bin/env bash +set -euo pipefail +# +# mission-status.sh — Show mission progress dashboard +# +# Usage: +# mission-status.sh [--project ] [--format table|json|markdown] + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "$SCRIPT_DIR/_lib.sh" + +# ─── Parse arguments ───────────────────────────────────────────────────────── + +PROJECT="." +FORMAT="table" + +while [[ $# -gt 0 ]]; do + case "$1" in + --project) PROJECT="$2"; shift 2 ;; + --format) FORMAT="$2"; shift 2 ;; + -h|--help) + echo "Usage: mission-status.sh [--project ] [--format table|json|markdown]" + exit 0 + ;; + *) echo "Unknown option: $1" >&2; exit 1 ;; + esac +done + +_require_jq +require_mission "$PROJECT" + +# ─── Load data ─────────────────────────────────────────────────────────────── + +mission="$(load_mission "$PROJECT")" +mission_name="$(echo "$mission" | jq -r '.name')" +mission_id="$(echo "$mission" | jq -r '.mission_id')" +mission_status="$(echo "$mission" | jq -r '.status')" +version="$(echo "$mission" | jq -r '.milestone_version // "—"')" +created_at="$(echo "$mission" | jq -r '.created_at // "—"')" +session_count="$(echo "$mission" | jq '.sessions | length')" +milestone_count="$(echo "$mission" | jq '.milestones | length')" +completed_milestones="$(echo "$mission" | jq '[.milestones[] | select(.status == "completed")] | length')" + +# Task counts +task_counts="$(count_tasks_md "$PROJECT")" +tasks_total="$(echo "$task_counts" | jq '.total')" +tasks_done="$(echo "$task_counts" | jq '.done')" +tasks_inprog="$(echo "$task_counts" | jq '.in_progress')" +tasks_pending="$(echo "$task_counts" | jq '.pending')" +tasks_blocked="$(echo "$task_counts" | jq '.blocked')" +tasks_failed="$(echo "$task_counts" | jq '.failed')" + +# Next task +next_task="$(find_next_task "$PROJECT")" + +# ─── JSON output ───────────────────────────────────────────────────────────── + +if [[ "$FORMAT" == "json" ]]; then + echo "$mission" | jq \ + --argjson tasks "$task_counts" \ + --arg next "$next_task" \ + '. + {task_counts: $tasks, next_task: $next}' + exit 0 +fi + +# ─── Progress bar ──────────────────────────────────────────────────────────── + +progress_bar() { + local done=$1 + local total=$2 + local width=30 + + if (( total == 0 )); then + printf "[%${width}s]" "" + return + fi + + local filled=$(( (done * width) / total )) + local empty=$(( width - filled )) + local bar="" + + for (( i=0; i 0 && filled > 0 )); then + bar+=">" + empty=$(( empty - 1 )) + fi + for (( i=0; i 0 )) && pct=$(( (tasks_done * 100) / tasks_total )) + +echo -e "${C_BOLD}Tasks:${C_RESET} $(progress_bar "$tasks_done" "$tasks_total") ${tasks_done}/${tasks_total} (${pct}%)" +echo -e " done: ${C_GREEN}$tasks_done${C_RESET} in-progress: ${C_YELLOW}$tasks_inprog${C_RESET} pending: $tasks_pending blocked: ${C_RED}$tasks_blocked${C_RESET} failed: ${C_RED}$tasks_failed${C_RESET}" +echo "" + +# Session history (last 5) +if (( session_count > 0 )); then + echo -e "${C_BOLD}Recent Sessions:${C_RESET}" + start_idx=$(( session_count > 5 ? session_count - 5 : 0 )) + for i in $(seq "$start_idx" $(( session_count - 1 ))); do + s_id="$(echo "$mission" | jq -r ".sessions[$i].session_id")" + s_rt="$(echo "$mission" | jq -r ".sessions[$i].runtime // \"—\"")" + s_start="$(echo "$mission" | jq -r ".sessions[$i].started_at // \"\"")" + s_end="$(echo "$mission" | jq -r ".sessions[$i].ended_at // \"\"")" + s_reason="$(echo "$mission" | jq -r ".sessions[$i].ended_reason // \"—\"")" + s_last="$(echo "$mission" | jq -r ".sessions[$i].last_task_id // \"—\"")" + + duration_str="—" + if [[ -n "$s_start" && -n "$s_end" && "$s_end" != "" ]]; then + s_epoch="$(iso_to_epoch "$s_start")" + e_epoch="$(iso_to_epoch "$s_end")" + if (( e_epoch > 0 && s_epoch > 0 )); then + duration_str="$(format_duration $(( e_epoch - s_epoch )))" + fi + fi + + printf " %-10s %-8s %-10s %-18s → %s\n" "$s_id" "$s_rt" "$duration_str" "$s_reason" "$s_last" + done + echo "" +fi + +# Current session check +lock_data="" +if lock_data="$(session_lock_read "$PROJECT" 2>/dev/null)"; then + lock_pid="$(echo "$lock_data" | jq -r '.pid // 0')" + lock_rt="$(echo "$lock_data" | jq -r '.runtime // "unknown"')" + lock_start="$(echo "$lock_data" | jq -r '.started_at // ""')" + + if is_pid_alive "$lock_pid"; then + dur=0 + if [[ -n "$lock_start" ]]; then + dur=$(( $(epoch_now) - $(iso_to_epoch "$lock_start") )) + fi + echo -e "${C_GREEN}Current: running ($lock_rt, PID $lock_pid, $(format_duration "$dur"))${C_RESET}" + else + echo -e "${C_RED}Stale session lock: $lock_rt (PID $lock_pid, not running)${C_RESET}" + echo " Run: mosaic coord resume --clean-lock" + fi +else + echo -e "${C_DIM}No active session.${C_RESET}" +fi + +[[ -n "$next_task" ]] && echo -e "Next unblocked task: ${C_CYAN}$next_task${C_RESET}" +echo "" diff --git a/tools/orchestrator/session-resume.sh b/tools/orchestrator/session-resume.sh new file mode 100755 index 0000000..12acf33 --- /dev/null +++ b/tools/orchestrator/session-resume.sh @@ -0,0 +1,208 @@ +#!/usr/bin/env bash +set -euo pipefail +# +# session-resume.sh — Crash recovery for dead orchestrator sessions +# +# Usage: +# session-resume.sh [--project ] [--clean-lock] + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "$SCRIPT_DIR/_lib.sh" + +# ─── Parse arguments ───────────────────────────────────────────────────────── + +PROJECT="." +CLEAN_LOCK=false + +while [[ $# -gt 0 ]]; do + case "$1" in + --project) PROJECT="$2"; shift 2 ;; + --clean-lock) CLEAN_LOCK=true; shift ;; + -h|--help) + echo "Usage: session-resume.sh [--project ] [--clean-lock]" + exit 0 + ;; + *) echo "Unknown option: $1" >&2; exit 1 ;; + esac +done + +_require_jq + +# ─── Check session lock ───────────────────────────────────────────────────── + +lock_data="" +has_lock=false +if lock_data="$(session_lock_read "$PROJECT" 2>/dev/null)"; then + has_lock=true +fi + +if [[ "$has_lock" == true ]]; then + lock_pid="$(echo "$lock_data" | jq -r '.pid // 0')" + lock_sid="$(echo "$lock_data" | jq -r '.session_id // "unknown"')" + lock_rt="$(echo "$lock_data" | jq -r '.runtime // "unknown"')" + lock_start="$(echo "$lock_data" | jq -r '.started_at // ""')" + lock_milestone="$(echo "$lock_data" | jq -r '.milestone_id // ""')" + + if is_pid_alive "$lock_pid"; then + echo -e "${C_YELLOW}Session $lock_sid is still running (PID $lock_pid).${C_RESET}" + echo "Use 'mosaic coord status' to check session health." + exit 0 + fi + + # Session is dead + echo "" + echo -e "${C_RED}${C_BOLD}CRASH RECOVERY — Session $lock_sid ($lock_rt)${C_RESET}" + echo "===========================================" + echo "" + + if [[ -n "$lock_start" ]]; then + echo -e " ${C_CYAN}Session started:${C_RESET} $lock_start" + fi + echo -e " ${C_CYAN}Session died:${C_RESET} PID $lock_pid is not running" + [[ -n "$lock_milestone" ]] && echo -e " ${C_CYAN}Active milestone:${C_RESET} $lock_milestone" + echo "" + +else + # No lock — check mission.json for last session info + if [[ -f "$(mission_path "$PROJECT")" ]]; then + mission="$(load_mission "$PROJECT")" + session_count="$(echo "$mission" | jq '.sessions | length')" + if (( session_count > 0 )); then + last_idx=$(( session_count - 1 )) + last_sid="$(echo "$mission" | jq -r ".sessions[$last_idx].session_id")" + last_reason="$(echo "$mission" | jq -r ".sessions[$last_idx].ended_reason // \"unknown\"")" + echo -e "${C_DIM}No session lock found. Last session: $last_sid (ended: $last_reason)${C_RESET}" + echo "Use 'mosaic coord continue' to generate a continuation prompt." + exit 0 + fi + fi + echo -e "${C_DIM}No session state found.${C_RESET}" + exit 4 +fi + +# ─── Detect dirty state ───────────────────────────────────────────────────── + +echo -e "${C_BOLD}Dirty State:${C_RESET}" + +dirty_files="" +if git -C "$PROJECT" rev-parse --is-inside-work-tree &>/dev/null; then + dirty_files="$(git -C "$PROJECT" status --porcelain 2>/dev/null || true)" +fi + +if [[ -n "$dirty_files" ]]; then + echo " Modified files:" + echo "$dirty_files" | head -20 | while IFS= read -r line; do + echo " $line" + done + file_count="$(echo "$dirty_files" | wc -l)" + if (( file_count > 20 )); then + echo " ... and $(( file_count - 20 )) more" + fi +else + echo -e " ${C_GREEN}Working tree is clean.${C_RESET}" +fi + +# Check for in-progress tasks +inprog_count=0 +task_counts="$(count_tasks_md "$PROJECT")" +inprog_count="$(echo "$task_counts" | jq '.in_progress')" +if (( inprog_count > 0 )); then + echo -e " ${C_YELLOW}$inprog_count task(s) still marked in-progress in TASKS.md${C_RESET}" +fi + +echo "" + +# ─── Recovery actions ──────────────────────────────────────────────────────── + +echo -e "${C_BOLD}Recovery Actions:${C_RESET}" +if [[ -n "$dirty_files" ]]; then + echo " 1. Review changes: git diff" + echo " 2. If good: git add -A && git commit -m \"wip: partial work from crashed session\"" + echo " 3. If bad: git checkout ." +fi +echo " 4. Clean lock: mosaic coord resume --clean-lock" +echo " 5. Generate prompt: mosaic coord continue" +echo "" + +# ─── Clean lock if requested ───────────────────────────────────────────────── + +if [[ "$CLEAN_LOCK" == true ]]; then + echo -e "${C_CYAN}Cleaning session lock...${C_RESET}" + + # Update mission.json with crash info + mp="$(mission_path "$PROJECT")" + if [[ -f "$mp" && "$has_lock" == true ]]; then + updated="$(jq \ + --arg sid "$lock_sid" \ + --arg ts "$(iso_now)" \ + '(.sessions[] | select(.session_id == $sid)) |= . + { + ended_at: $ts, + ended_reason: "crashed" + }' "$mp")" + write_json "$mp" "$updated" + echo " Updated mission.json: session $lock_sid marked as crashed" + fi + + session_lock_clear "$PROJECT" + echo " Cleared session.lock" + echo "" + echo -e "${C_GREEN}Lock cleared. Generate continuation prompt with: mosaic coord continue${C_RESET}" +fi + +# ─── Generate resume prompt ───────────────────────────────────────────────── + +if [[ "$CLEAN_LOCK" != true ]]; then + echo "---" + echo "" + echo -e "${C_BOLD}Resume Prompt (paste to new session):${C_RESET}" + echo "" + + mission_name="" + mission_id="" + if [[ -f "$(mission_path "$PROJECT")" ]]; then + mission="$(load_mission "$PROJECT")" + mission_name="$(echo "$mission" | jq -r '.name')" + mission_id="$(echo "$mission" | jq -r '.mission_id')" + quality_gates="$(echo "$mission" | jq -r '.quality_gates // "—"')" + project_path="$(echo "$mission" | jq -r '.project_path')" + fi + + task_counts="$(count_tasks_md "$PROJECT")" + tasks_done="$(echo "$task_counts" | jq '.done')" + tasks_total="$(echo "$task_counts" | jq '.total')" + next_task="$(find_next_task "$PROJECT")" + + cat <] [--format table|json] +# +# Exit codes: +# 0 = running +# 2 = stale (recently died) +# 3 = dead (no longer running) +# 4 = no session + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "$SCRIPT_DIR/_lib.sh" + +# ─── Parse arguments ───────────────────────────────────────────────────────── + +PROJECT="." +FORMAT="table" + +while [[ $# -gt 0 ]]; do + case "$1" in + --project) PROJECT="$2"; shift 2 ;; + --format) FORMAT="$2"; shift 2 ;; + -h|--help) + echo "Usage: session-status.sh [--project ] [--format table|json]" + exit 0 + ;; + *) echo "Unknown option: $1" >&2; exit 1 ;; + esac +done + +_require_jq + +# ─── Check session lock ───────────────────────────────────────────────────── + +lock_data="" +if ! lock_data="$(session_lock_read "$PROJECT")"; then + if [[ "$FORMAT" == "json" ]]; then + echo '{"status":"no-session"}' + else + echo -e "${C_DIM}No active session.${C_RESET}" + fi + exit 4 +fi + +# Parse lock +session_id="$(echo "$lock_data" | jq -r '.session_id // "unknown"')" +runtime="$(echo "$lock_data" | jq -r '.runtime // "unknown"')" +pid="$(echo "$lock_data" | jq -r '.pid // 0')" +started_at="$(echo "$lock_data" | jq -r '.started_at // ""')" +milestone_id="$(echo "$lock_data" | jq -r '.milestone_id // ""')" + +# ─── Determine status ─────────────────────────────────────────────────────── + +status="unknown" +exit_code=1 + +if is_pid_alive "$pid"; then + status="running" + exit_code=0 +else + # PID is dead — check how recently + last_act="$(last_activity_time "$PROJECT")" + now="$(epoch_now)" + age=$(( now - last_act )) + + if (( age < STALE_THRESHOLD )); then + status="stale" + exit_code=2 + elif (( age < DEAD_THRESHOLD )); then + status="stale" + exit_code=2 + else + status="dead" + exit_code=3 + fi +fi + +# ─── Gather supplementary info ────────────────────────────────────────────── + +duration_secs=0 +if [[ -n "$started_at" ]]; then + start_epoch="$(iso_to_epoch "$started_at")" + now="$(epoch_now)" + duration_secs=$(( now - start_epoch )) +fi + +last_act="$(last_activity_time "$PROJECT")" + +# Current milestone from mission.json +current_ms="" +if [[ -f "$(mission_path "$PROJECT")" ]]; then + current_ms="$(current_milestone_id "$PROJECT")" + if [[ -n "$current_ms" ]]; then + ms_name="$(milestone_name "$PROJECT" "$current_ms")" + [[ -n "$ms_name" ]] && current_ms="$current_ms ($ms_name)" + fi +fi + +# Next task from TASKS.md +next_task="$(find_next_task "$PROJECT")" + +# ─── Output ────────────────────────────────────────────────────────────────── + +if [[ "$FORMAT" == "json" ]]; then + jq -n \ + --arg status "$status" \ + --arg session_id "$session_id" \ + --arg runtime "$runtime" \ + --arg pid "$pid" \ + --arg started_at "$started_at" \ + --arg duration "$duration_secs" \ + --arg milestone "$current_ms" \ + --arg next_task "$next_task" \ + --arg last_activity "$last_act" \ + '{ + status: $status, + session_id: $session_id, + runtime: $runtime, + pid: ($pid | tonumber), + started_at: $started_at, + duration_seconds: ($duration | tonumber), + milestone: $milestone, + next_task: $next_task, + last_activity_epoch: ($last_activity | tonumber) + }' +else + # Color the status + case "$status" in + running) status_color="${C_GREEN}RUNNING${C_RESET}" ;; + stale) status_color="${C_YELLOW}STALE${C_RESET}" ;; + dead) status_color="${C_RED}DEAD${C_RESET}" ;; + *) status_color="$status" ;; + esac + + echo "" + echo -e " Session Status: $status_color ($runtime)" + echo -e " ${C_CYAN}Session ID:${C_RESET} $session_id" + echo -e " ${C_CYAN}Started:${C_RESET} $started_at ($(format_duration "$duration_secs"))" + echo -e " ${C_CYAN}PID:${C_RESET} $pid" + [[ -n "$current_ms" ]] && echo -e " ${C_CYAN}Milestone:${C_RESET} $current_ms" + [[ -n "$next_task" ]] && echo -e " ${C_CYAN}Next task:${C_RESET} $next_task" + echo -e " ${C_CYAN}Last activity:${C_RESET} $(format_ago "$last_act")" + echo "" + + if [[ "$status" == "stale" || "$status" == "dead" ]]; then + echo -e " ${C_YELLOW}Session is no longer running.${C_RESET}" + echo " Recovery: mosaic coord resume" + echo " Continue: mosaic coord continue" + echo "" + fi +fi + +exit "$exit_code"