feat: r0 coordinator tooling for orchestrator protocol
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 <noreply@anthropic.com>
This commit is contained in:
@@ -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)
|
||||
|
||||
|
||||
85
bin/mosaic
85
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 <subcommand> 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 <<COORD_USAGE
|
||||
mosaic coord — r0 manual coordinator tools
|
||||
|
||||
Commands:
|
||||
init --name <name> [opts] Initialize a new mission
|
||||
mission [--project <path>] Show mission progress dashboard
|
||||
status [--project <path>] Check agent session health
|
||||
continue [--project <path>] Generate continuation prompt for next session
|
||||
resume [--project <path>] 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 "$@" ;;
|
||||
|
||||
@@ -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"
|
||||
|
||||
257
guides/ORCHESTRATOR-PROTOCOL.md
Normal file
257
guides/ORCHESTRATOR-PROTOCOL.md
Normal file
@@ -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)
|
||||
53
templates/docs/MISSION-MANIFEST.md.template
Normal file
53
templates/docs/MISSION-MANIFEST.md.template
Normal file
@@ -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`
|
||||
36
templates/docs/continuation-prompt.md.template
Normal file
36
templates/docs/continuation-prompt.md.template
Normal file
@@ -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`
|
||||
27
templates/docs/mission-scratchpad.md.template
Normal file
27
templates/docs/mission-scratchpad.md.template
Normal file
@@ -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
|
||||
|
||||
<!-- Record key decisions made during planning. Format: decision + rationale. -->
|
||||
|
||||
## Session Log
|
||||
|
||||
| Session | Date | Milestone | Tasks Done | Outcome |
|
||||
|---------|------|-----------|------------|---------|
|
||||
|
||||
## Open Questions
|
||||
|
||||
<!-- Unresolved items that need human input or cross-session investigation. -->
|
||||
|
||||
## Corrections
|
||||
|
||||
<!-- Record any corrections to earlier decisions or assumptions. -->
|
||||
14
templates/repo/.mosaic/orchestrator/mission.json
Normal file
14
templates/repo/.mosaic/orchestrator/mission.json
Normal file
@@ -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": []
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
386
tools/orchestrator/_lib.sh
Executable file
386
tools/orchestrator/_lib.sh
Executable file
@@ -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/-$//'
|
||||
}
|
||||
153
tools/orchestrator/continue-prompt.sh
Executable file
153
tools/orchestrator/continue-prompt.sh
Executable file
@@ -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 <path>] [--milestone <id>] [--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 <path>] [--milestone <id>] [--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 <<EOF
|
||||
## 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_ms_name:-—} (${current_ms_id:-—})
|
||||
- **Next task:** ${next_task:-—}
|
||||
- **Progress:** ${tasks_done}/${tasks_total} tasks (${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:-next-pending}**
|
||||
7. Follow Two-Phase Completion Protocol
|
||||
8. You are the SOLE writer of \`docs/TASKS.md\`
|
||||
EOF
|
||||
)"
|
||||
|
||||
# ─── Output ──────────────────────────────────────────────────────────────────
|
||||
|
||||
if [[ "$COPY" == true ]]; then
|
||||
if command -v wl-copy &>/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
|
||||
283
tools/orchestrator/mission-init.sh
Executable file
283
tools/orchestrator/mission-init.sh
Executable file
@@ -0,0 +1,283 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
#
|
||||
# mission-init.sh — Initialize a new orchestration mission
|
||||
#
|
||||
# Usage:
|
||||
# mission-init.sh --name <name> [options]
|
||||
#
|
||||
# Options:
|
||||
# --name <name> Mission name (required)
|
||||
# --project <path> Project directory (default: CWD)
|
||||
# --prefix <prefix> Task ID prefix (e.g., MS)
|
||||
# --milestones <comma-list> Milestone names, comma-separated
|
||||
# --quality-gates <command> Quality gate command string
|
||||
# --version <semver> Milestone version (default: 0.0.1)
|
||||
# --description <text> 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 <name> [options]
|
||||
|
||||
Options:
|
||||
--name <name> Mission name (required)
|
||||
--project <path> Project directory (default: CWD)
|
||||
--prefix <prefix> Task ID prefix (e.g., MS)
|
||||
--milestones <comma-list> Milestone names, comma-separated
|
||||
--quality-gates <command> Quality gate command string
|
||||
--version <semver> Milestone version (default: 0.0.1)
|
||||
--description <text> 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" <<EOF
|
||||
# Mission Manifest — $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:** $DESCRIPTION
|
||||
**Phase:** Intake
|
||||
**Current Milestone:** —
|
||||
**Progress:** 0 / $MILESTONE_COUNT milestones
|
||||
**Status:** active
|
||||
**Last Updated:** $(date -u +"%Y-%m-%d %H:%M UTC")
|
||||
|
||||
## Success Criteria
|
||||
|
||||
<!-- Define measurable success criteria here -->
|
||||
|
||||
## 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" <<EOF
|
||||
# Mission Scratchpad — $NAME
|
||||
|
||||
> 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" <<EOF
|
||||
# Tasks — $NAME
|
||||
|
||||
> 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'"
|
||||
181
tools/orchestrator/mission-status.sh
Executable file
181
tools/orchestrator/mission-status.sh
Executable file
@@ -0,0 +1,181 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
#
|
||||
# mission-status.sh — Show mission progress dashboard
|
||||
#
|
||||
# Usage:
|
||||
# mission-status.sh [--project <path>] [--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 <path>] [--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<filled; i++ )); do bar+="="; done
|
||||
if (( empty > 0 && filled > 0 )); then
|
||||
bar+=">"
|
||||
empty=$(( empty - 1 ))
|
||||
fi
|
||||
for (( i=0; i<empty; i++ )); do bar+="."; done
|
||||
|
||||
printf "[%s]" "$bar"
|
||||
}
|
||||
|
||||
# ─── Table / Markdown output ────────────────────────────────────────────────
|
||||
|
||||
# Header
|
||||
echo ""
|
||||
echo "=================================================="
|
||||
echo -e " ${C_BOLD}Mission: $mission_name${C_RESET}"
|
||||
echo -e " Status: ${C_CYAN}$mission_status${C_RESET} Version: $version"
|
||||
echo -e " Started: ${created_at:0:10} Sessions: $session_count"
|
||||
echo "=================================================="
|
||||
echo ""
|
||||
|
||||
# Milestones
|
||||
echo -e "${C_BOLD}Milestones:${C_RESET}"
|
||||
for i in $(seq 0 $(( milestone_count - 1 ))); do
|
||||
ms_id="$(echo "$mission" | jq -r ".milestones[$i].id")"
|
||||
ms_name="$(echo "$mission" | jq -r ".milestones[$i].name")"
|
||||
ms_status="$(echo "$mission" | jq -r ".milestones[$i].status")"
|
||||
ms_issue="$(echo "$mission" | jq -r ".milestones[$i].issue_ref // \"\"")"
|
||||
|
||||
case "$ms_status" in
|
||||
completed) icon="${C_GREEN}[x]${C_RESET}" ;;
|
||||
in-progress) icon="${C_YELLOW}[>]${C_RESET}" ;;
|
||||
blocked) icon="${C_RED}[!]${C_RESET}" ;;
|
||||
*) icon="${C_DIM}[ ]${C_RESET}" ;;
|
||||
esac
|
||||
|
||||
issue_str=""
|
||||
[[ -n "$ms_issue" ]] && issue_str="$ms_issue"
|
||||
|
||||
printf " %b %-40s %s\n" "$icon" "$ms_name" "$issue_str"
|
||||
done
|
||||
echo ""
|
||||
|
||||
# Tasks progress
|
||||
pct=0
|
||||
(( tasks_total > 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 ""
|
||||
208
tools/orchestrator/session-resume.sh
Executable file
208
tools/orchestrator/session-resume.sh
Executable file
@@ -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 <path>] [--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 <path>] [--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 <<EOF
|
||||
## Crash Recovery Mission
|
||||
|
||||
Recovering **${mission_name:-Unknown Mission}** from crashed session ${lock_sid:-unknown}.
|
||||
|
||||
### WARNING: Dirty State Detected
|
||||
The previous session left uncommitted changes. Before continuing:
|
||||
1. Run \`git diff\` to review uncommitted changes
|
||||
2. Decide: commit (if good) or discard (if broken)
|
||||
3. Then proceed with the mission
|
||||
|
||||
## Setup
|
||||
|
||||
- **Project:** ${project_path:-$PROJECT}
|
||||
- **State:** docs/TASKS.md (${tasks_done}/${tasks_total} tasks complete)
|
||||
- **Manifest:** docs/MISSION-MANIFEST.md
|
||||
- **Scratchpad:** docs/scratchpads/${mission_id:-mission}.md
|
||||
- **Protocol:** ~/.config/mosaic/guides/ORCHESTRATOR.md
|
||||
- **Quality gates:** ${quality_gates:-—}
|
||||
|
||||
## Resume Point
|
||||
|
||||
- **Next task:** ${next_task:-check TASKS.md}
|
||||
|
||||
## Instructions
|
||||
|
||||
1. Read \`docs/MISSION-MANIFEST.md\` for mission scope
|
||||
2. Read \`docs/scratchpads/${mission_id:-mission}.md\` for session history
|
||||
3. Review and resolve any uncommitted changes first
|
||||
4. Read \`docs/TASKS.md\` for current task state
|
||||
5. Continue execution from the next pending task
|
||||
6. You are the SOLE writer of \`docs/TASKS.md\`
|
||||
EOF
|
||||
fi
|
||||
157
tools/orchestrator/session-status.sh
Executable file
157
tools/orchestrator/session-status.sh
Executable file
@@ -0,0 +1,157 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
#
|
||||
# session-status.sh — Check agent session health
|
||||
#
|
||||
# Usage:
|
||||
# session-status.sh [--project <path>] [--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 <path>] [--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"
|
||||
Reference in New Issue
Block a user