chore: consolidate new foundation and archive v1 (#1495)
This commit is contained in:
@@ -0,0 +1,92 @@
|
||||
# orchestrator/ tools
|
||||
|
||||
Helper scripts for r0 coordinator / orchestrator sessions — mission lifecycle,
|
||||
session health, continuation, and board maintenance. See
|
||||
`framework/guides/ORCHESTRATOR-PROTOCOL.md` for the surrounding process.
|
||||
|
||||
| Script | Purpose |
|
||||
| -------------------- | ----------------------------------------------------------------------------------------------- |
|
||||
| `mission-init.sh` | Initialize a new orchestration mission (manifest, scratchpad, TASKS.md). |
|
||||
| `mission-status.sh` | Show the mission progress dashboard. |
|
||||
| `session-run.sh` | Generate continuation context and launch the target runtime. |
|
||||
| `session-resume.sh` | Crash recovery for dead orchestrator sessions. |
|
||||
| `session-status.sh` | Check agent session health. |
|
||||
| `continue-prompt.sh` | Generate the continuation prompt for the next session. |
|
||||
| `board-roll.sh` | Keep a LIVE orchestration board under its byte cap by rolling the oldest entries to its LEDGER. |
|
||||
| `smoke-test.sh` | Behavior smoke checks for the coord continue/run workflows. |
|
||||
| `test-board-roll.sh` | Regression harness for `board-roll.sh`. |
|
||||
| `_lib.sh` | Shared functions sourced by the above (state files, TASKS.md parsing, locks). |
|
||||
|
||||
## board-roll.sh
|
||||
|
||||
Coordinator boards (`MOS-ORCHESTRATION-BOARD-LIVE.md`, `MS-LEAD-BOARD-LIVE.md`)
|
||||
follow a **"< 8 KB LIVE"** discipline: the LIVE board is the only file loaded on
|
||||
resume, so it must stay small, and history lives in an append-only LEDGER. When a
|
||||
board write would push LIVE over its cap, coordinators otherwise hand-trim and
|
||||
retry every time — an observed 38 ABORT-OVER-CAP cycles in one 24 h window.
|
||||
`board-roll.sh` automates that trim mechanically and reversibly: the audit trail
|
||||
is moved to the LEDGER instead of being hand-deleted.
|
||||
|
||||
### Contract (conservative — it never guesses what is safe to move)
|
||||
|
||||
The LIVE board opts in by wrapping its aging archival ticks in an explicit roll
|
||||
zone. Everything **outside** the markers (title, protocol blockquote, curated
|
||||
always-current `##` sections) is pinned and never touched:
|
||||
|
||||
```markdown
|
||||
# MOS ORCHESTRATION BOARD — LIVE state
|
||||
|
||||
> protocol blockquote … (pinned)
|
||||
|
||||
## 🟦 Curated always-current section (pinned)
|
||||
|
||||
…
|
||||
|
||||
<!-- BOARD-ROLL:START -->
|
||||
|
||||
### 2026-07-22 (mid²²) — newest tick, stays longest
|
||||
|
||||
…
|
||||
|
||||
### 2026-07-20 (dawn) — oldest tick, rolled first
|
||||
|
||||
…
|
||||
|
||||
<!-- BOARD-ROLL:END -->
|
||||
```
|
||||
|
||||
Inside the zone, entries are delimited by a heading marker (default `### `) and
|
||||
are assumed **newest-first (top) → oldest-last (bottom)**. `board-roll.sh` moves
|
||||
whole oldest (bottom-most) entry blocks out of the zone and appends them verbatim
|
||||
to the LEDGER, one at a time, until LIVE is back under the cap or the zone is
|
||||
empty. If the board has no markers, it exits `3` and changes nothing — adding the
|
||||
markers is a deliberate opt-in by the board owner.
|
||||
|
||||
### Usage
|
||||
|
||||
```bash
|
||||
board-roll.sh --live <LIVE.md> --ledger <LEDGER.md> [options]
|
||||
|
||||
--live <path> LIVE board file (required)
|
||||
--ledger <path> append-only LEDGER file (required; created if absent)
|
||||
--cap <bytes> size ceiling for LIVE (default 8192)
|
||||
--marker <prefix> entry-heading prefix inside the roll zone (default "### ")
|
||||
--dry-run report what would move; change nothing
|
||||
-h, --help show help and exit 0
|
||||
```
|
||||
|
||||
Only **one** roll zone is supported. If a board carries more than one
|
||||
`BOARD-ROLL:START`/`END` pair, `board-roll.sh` refuses (exit `3`, zero changes)
|
||||
rather than span first-START..last-END and relocate the curated content between
|
||||
the zones — consolidate the ticks into a single zone instead.
|
||||
|
||||
Exit codes: `0` LIVE under cap (already, or after rolling) — on `--dry-run`, a
|
||||
plan exists or nothing to do · `2` usage / argument / IO error · `3` cannot meet
|
||||
the cap (no markers, **more than one marker pair**, or the pinned sections alone
|
||||
exceed the cap and need a manual trim).
|
||||
|
||||
Writes are atomic (temp file + `mv`, LEDGER first) so a failure never leaves a
|
||||
board half-written; line endings are normalized to LF on rewrite. `--dry-run`
|
||||
first is recommended when wiring it into a board update protocol.
|
||||
|
||||
Run the regression suite with `bash test-board-roll.sh`.
|
||||
+523
@@ -0,0 +1,523 @@
|
||||
#!/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"
|
||||
NEXT_TASK_FILE="next-task.json"
|
||||
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
|
||||
}
|
||||
|
||||
coord_runtime() {
|
||||
local runtime="${MOSAIC_COORD_RUNTIME:-claude}"
|
||||
case "$runtime" in
|
||||
claude|codex) echo "$runtime" ;;
|
||||
*) echo "claude" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
coord_launch_command() {
|
||||
local runtime
|
||||
runtime="$(coord_runtime)"
|
||||
echo "mosaic $runtime"
|
||||
}
|
||||
|
||||
coord_run_command() {
|
||||
local runtime
|
||||
runtime="$(coord_runtime)"
|
||||
if [[ "$runtime" == "claude" ]]; then
|
||||
echo "mosaic coord run"
|
||||
else
|
||||
echo "mosaic coord run --$runtime"
|
||||
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"
|
||||
}
|
||||
|
||||
next_task_capsule_path() {
|
||||
local project="${1:-.}"
|
||||
echo "$(orch_dir "$project")/$NEXT_TASK_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"
|
||||
}
|
||||
|
||||
# ─── Next-task capsule helpers ───────────────────────────────────────────────
|
||||
|
||||
write_next_task_capsule() {
|
||||
local project="${1:-.}"
|
||||
local runtime="${2:-claude}"
|
||||
local mission_id="${3:-}"
|
||||
local mission_name="${4:-}"
|
||||
local project_path="${5:-}"
|
||||
local quality_gates="${6:-}"
|
||||
local current_ms_id="${7:-}"
|
||||
local current_ms_name="${8:-}"
|
||||
local next_task="${9:-}"
|
||||
local tasks_done="${10:-0}"
|
||||
local tasks_total="${11:-0}"
|
||||
local pct="${12:-0}"
|
||||
local current_branch="${13:-}"
|
||||
|
||||
_require_jq || return 1
|
||||
mkdir -p "$(orch_dir "$project")"
|
||||
|
||||
local payload
|
||||
payload="$(jq -n \
|
||||
--arg generated_at "$(iso_now)" \
|
||||
--arg runtime "$runtime" \
|
||||
--arg mission_id "$mission_id" \
|
||||
--arg mission_name "$mission_name" \
|
||||
--arg project_path "$project_path" \
|
||||
--arg quality_gates "$quality_gates" \
|
||||
--arg current_ms_id "$current_ms_id" \
|
||||
--arg current_ms_name "$current_ms_name" \
|
||||
--arg next_task "$next_task" \
|
||||
--arg current_branch "$current_branch" \
|
||||
--arg tasks_done "$tasks_done" \
|
||||
--arg tasks_total "$tasks_total" \
|
||||
--arg pct "$pct" \
|
||||
'{
|
||||
generated_at: $generated_at,
|
||||
runtime: $runtime,
|
||||
mission_id: $mission_id,
|
||||
mission_name: $mission_name,
|
||||
project_path: $project_path,
|
||||
quality_gates: $quality_gates,
|
||||
current_milestone: {
|
||||
id: $current_ms_id,
|
||||
name: $current_ms_name
|
||||
},
|
||||
next_task: $next_task,
|
||||
progress: {
|
||||
tasks_done: ($tasks_done | tonumber),
|
||||
tasks_total: ($tasks_total | tonumber),
|
||||
pct: ($pct | tonumber)
|
||||
},
|
||||
current_branch: $current_branch
|
||||
}')"
|
||||
|
||||
write_json "$(next_task_capsule_path "$project")" "$payload"
|
||||
}
|
||||
|
||||
build_codex_strict_kickoff() {
|
||||
local project="${1:-.}"
|
||||
local continuation_prompt="${2:-}"
|
||||
|
||||
_require_jq || return 1
|
||||
|
||||
local capsule_path
|
||||
capsule_path="$(next_task_capsule_path "$project")"
|
||||
local capsule='{}'
|
||||
if [[ -f "$capsule_path" ]]; then
|
||||
capsule="$(cat "$capsule_path")"
|
||||
fi
|
||||
|
||||
local mission_id next_task project_path quality_gates
|
||||
mission_id="$(echo "$capsule" | jq -r '.mission_id // "unknown"')"
|
||||
next_task="$(echo "$capsule" | jq -r '.next_task // "none"')"
|
||||
project_path="$(echo "$capsule" | jq -r '.project_path // "."')"
|
||||
quality_gates="$(echo "$capsule" | jq -r '.quality_gates // "none"')"
|
||||
|
||||
cat <<EOF
|
||||
Now initiating Orchestrator mode...
|
||||
|
||||
STRICT EXECUTION PROFILE FOR CODEX (HARD GATE)
|
||||
- Do NOT ask clarifying questions before your first tool actions unless a Mosaic escalation trigger is hit.
|
||||
- Your first actions must be reading mission state files in order.
|
||||
- Treat the next-task capsule as authoritative execution input.
|
||||
|
||||
REQUIRED FIRST ACTIONS (IN ORDER)
|
||||
1. Read ~/.config/mosaic/guides/ORCHESTRATOR-PROTOCOL.md
|
||||
2. Read docs/MISSION-MANIFEST.md
|
||||
3. Read docs/scratchpads/${mission_id}.md
|
||||
4. Read docs/TASKS.md
|
||||
5. Begin execution on next task: ${next_task}
|
||||
|
||||
WORKING CONTEXT
|
||||
- Project: ${project_path}
|
||||
- Quality gates: ${quality_gates}
|
||||
- Capsule file: .mosaic/orchestrator/next-task.json
|
||||
|
||||
Task capsule (JSON):
|
||||
\`\`\`json
|
||||
${capsule}
|
||||
\`\`\`
|
||||
|
||||
Continuation prompt:
|
||||
${continuation_prompt}
|
||||
EOF
|
||||
}
|
||||
|
||||
# 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/-$//'
|
||||
}
|
||||
@@ -0,0 +1,277 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# board-roll.sh — keep a LIVE orchestration board under its byte cap by rolling
|
||||
# the oldest archival entries out to its append-only LEDGER.
|
||||
#
|
||||
# WHY: coordinator boards (MOS-ORCHESTRATION-BOARD-LIVE.md, MS-LEAD-BOARD-LIVE.md)
|
||||
# enforce a "< 8 KB LIVE" discipline via a self-guard that ABORTs the board write
|
||||
# when the file exceeds the cap. In practice the LIVE board keeps bumping the cap,
|
||||
# so coordinators hand-trim + retry every time (observed: 38 ABORT-OVER-CAP cycles
|
||||
# in a 24h window on one coordinator). This automates that trim, mechanically and
|
||||
# reversibly, so the audit trail is preserved in the LEDGER instead of hand-deleted.
|
||||
#
|
||||
# CONTRACT (conservative by design — it NEVER guesses what is safe to move):
|
||||
# The LIVE board must declare an explicit ROLL ZONE with HTML-comment markers:
|
||||
#
|
||||
# <!-- BOARD-ROLL:START -->
|
||||
# ### 2026-07-22 (newest tick — stays longest)
|
||||
# ...
|
||||
# ### 2026-07-19 (oldest tick — rolled first)
|
||||
# ...
|
||||
# <!-- BOARD-ROLL:END -->
|
||||
#
|
||||
# Everything OUTSIDE the markers (title, protocol blockquote, curated always-current
|
||||
# `##` sections) is PINNED and never touched. Inside the zone, entries are delimited
|
||||
# by a heading marker (default `### `) and are assumed newest-first (top) → oldest-last
|
||||
# (bottom), matching board convention. board-roll moves whole oldest (bottom-most)
|
||||
# entry blocks out of the zone and APPENDS them verbatim to the LEDGER, one block at a
|
||||
# time, until the LIVE file is back under the cap or the zone is empty.
|
||||
#
|
||||
# If no markers are present, it exits 3 without changing anything (safe default —
|
||||
# adding the markers is a deliberate opt-in by the board owner).
|
||||
#
|
||||
# USAGE:
|
||||
# board-roll.sh --live <LIVE.md> --ledger <LEDGER.md> [options]
|
||||
#
|
||||
# OPTIONS:
|
||||
# --live <path> LIVE board file (required)
|
||||
# --ledger <path> append-only LEDGER file (required; created if absent)
|
||||
# --cap <bytes> size ceiling for LIVE (default 8192)
|
||||
# --marker <prefix> entry-heading prefix inside the roll zone (default "### ")
|
||||
# --dry-run report what would move + resulting size; change nothing
|
||||
# -h, --help print usage and exit 0
|
||||
#
|
||||
# EXIT CODES:
|
||||
# 0 LIVE is under cap (already, or after rolling); on --dry-run, 0 = a plan exists
|
||||
# (or nothing to do)
|
||||
# 2 usage / argument / IO error (bad flag, missing file, unwritable target)
|
||||
# 3 cannot satisfy the cap: no roll markers present, MORE THAN ONE marker pair
|
||||
# (multiple zones are refused, not guessed), OR the zone was emptied and LIVE
|
||||
# is still over cap (curated pinned sections need a manual trim)
|
||||
#
|
||||
# NOTE: line endings are normalized to LF on rewrite (boards are LF markdown); a
|
||||
# trailing newline is always ensured. Writes are atomic (temp file + mv) so a
|
||||
# failure never leaves LIVE or LEDGER half-written.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
START_MARK='<!-- BOARD-ROLL:START -->'
|
||||
END_MARK='<!-- BOARD-ROLL:END -->'
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage: board-roll.sh --live <LIVE.md> --ledger <LEDGER.md> [options]
|
||||
|
||||
Roll the oldest entries out of a LIVE orchestration board into its LEDGER
|
||||
until the LIVE file is under a byte cap. Conservative: only content inside
|
||||
explicit <!-- BOARD-ROLL:START -->/<!-- BOARD-ROLL:END --> markers is moved.
|
||||
|
||||
Options:
|
||||
--live <path> LIVE board file (required)
|
||||
--ledger <path> append-only LEDGER file (required; created if absent)
|
||||
--cap <bytes> size ceiling for LIVE (default 8192)
|
||||
--marker <prefix> entry-heading prefix inside the roll zone (default "### ")
|
||||
--dry-run report what would move; change nothing
|
||||
-h, --help show this help and exit 0
|
||||
|
||||
Exit: 0 under cap (or dry-run plan) · 2 usage/IO error · 3 cannot meet cap
|
||||
(no markers, or pinned sections alone exceed the cap).
|
||||
EOF
|
||||
}
|
||||
|
||||
die() { echo "board-roll: $*" >&2; exit 2; }
|
||||
|
||||
LIVE=""; LEDGER=""; CAP=8192; MARKER='### '; DRYRUN=0
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--live) LIVE="${2:-}"; shift 2 || die "--live needs a value" ;;
|
||||
--ledger) LEDGER="${2:-}"; shift 2 || die "--ledger needs a value" ;;
|
||||
--cap) CAP="${2:-}"; shift 2 || die "--cap needs a value" ;;
|
||||
--marker) MARKER="${2:-}"; shift 2 || die "--marker needs a value" ;;
|
||||
--dry-run) DRYRUN=1; shift ;;
|
||||
-h|--help) usage; exit 0 ;;
|
||||
*) usage >&2; die "unknown option: $1" ;;
|
||||
esac
|
||||
done
|
||||
|
||||
[[ -n "$LIVE" ]] || { usage >&2; die "--live is required"; }
|
||||
[[ -n "$LEDGER" ]] || { usage >&2; die "--ledger is required"; }
|
||||
[[ -f "$LIVE" ]] || die "LIVE file not found: $LIVE"
|
||||
[[ "$CAP" =~ ^[0-9]+$ ]] || die "--cap must be a non-negative integer, got: $CAP"
|
||||
|
||||
# --- read LIVE into a line array (newlines stripped; re-added on write) ---------
|
||||
mapfile -t LINES < "$LIVE"
|
||||
|
||||
# byte size of an array rendered as LF-terminated text
|
||||
render_size() {
|
||||
if [[ $# -eq 0 ]]; then printf 0; return; fi
|
||||
printf '%s\n' "$@" | wc -c
|
||||
}
|
||||
|
||||
orig_size=$(render_size "${LINES[@]}")
|
||||
|
||||
# --- already under cap → nothing to do -----------------------------------------
|
||||
if (( orig_size < CAP )); then
|
||||
echo "board-roll: LIVE is ${orig_size}B (< cap ${CAP}B) — nothing to roll."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# --- locate the roll-zone markers ----------------------------------------------
|
||||
# Exactly ONE marker pair is supported. If a board carries more than one START or
|
||||
# END marker we REFUSE (exit 3, zero changes) rather than guess: a naive
|
||||
# first-START..last-END span would swallow the curated content and the intermediate
|
||||
# markers sitting between two intended zones and silently relocate that pinned text
|
||||
# to the LEDGER — the exact data-loss this tool exists to prevent. Refusing matches
|
||||
# the "no markers = exit 3" conservative posture.
|
||||
start_idx=-1; end_idx=-1; start_count=0; end_count=0
|
||||
for i in "${!LINES[@]}"; do
|
||||
if [[ "${LINES[$i]}" == "$START_MARK" ]]; then
|
||||
if (( start_count == 0 )); then start_idx=$i; fi
|
||||
start_count=$(( start_count + 1 ))
|
||||
fi
|
||||
if [[ "${LINES[$i]}" == "$END_MARK" ]]; then
|
||||
end_idx=$i
|
||||
end_count=$(( end_count + 1 ))
|
||||
fi
|
||||
done
|
||||
if (( start_count > 1 || end_count > 1 )); then
|
||||
echo "board-roll: LIVE is ${orig_size}B (>= cap ${CAP}B) but has ${start_count} START / ${end_count} END" >&2
|
||||
echo " markers — only a SINGLE '$START_MARK' … '$END_MARK' roll zone is supported." >&2
|
||||
echo " Multiple zones are refused (not guessed) so content between zones is never relocated." >&2
|
||||
echo " Consolidate the archival ticks into one zone, or trim manually." >&2
|
||||
exit 3
|
||||
fi
|
||||
if (( start_idx < 0 || end_idx < 0 || end_idx <= start_idx )); then
|
||||
echo "board-roll: LIVE is ${orig_size}B (>= cap ${CAP}B) but no usable roll zone" >&2
|
||||
echo " (need '$START_MARK' then '$END_MARK'). Add the markers around the" >&2
|
||||
echo " archival tick section to opt this board into automatic rolling." >&2
|
||||
exit 3
|
||||
fi
|
||||
|
||||
# preamble = lines [0 .. start_idx] (inclusive of START marker)
|
||||
# zone = lines (start_idx .. end_idx) (exclusive of both markers)
|
||||
# footer = lines [end_idx .. end] (inclusive of END marker)
|
||||
preamble=(); zone=(); footer=()
|
||||
for i in "${!LINES[@]}"; do
|
||||
if (( i <= start_idx )); then preamble+=("${LINES[$i]}")
|
||||
elif (( i < end_idx )); then zone+=("${LINES[$i]}")
|
||||
else footer+=("${LINES[$i]}")
|
||||
fi
|
||||
done
|
||||
|
||||
# --- split the zone into a fixed head + entry blocks ----------------------------
|
||||
# zone_head = any zone lines before the first entry marker (kept, never rolled).
|
||||
# blocks[k] = newline-joined text of entry k (marker line .. line before next marker).
|
||||
zone_head=(); declare -a block_start=()
|
||||
first_block=-1
|
||||
for i in "${!zone[@]}"; do
|
||||
if [[ "${zone[$i]}" == "$MARKER"* ]]; then
|
||||
[[ $first_block -eq -1 ]] && first_block=$i
|
||||
block_start+=("$i")
|
||||
fi
|
||||
done
|
||||
if (( first_block == -1 )); then
|
||||
echo "board-roll: LIVE is ${orig_size}B (>= cap ${CAP}B) but the roll zone has no" >&2
|
||||
echo " '${MARKER}' entries to move. Trim the pinned sections manually." >&2
|
||||
exit 3
|
||||
fi
|
||||
for (( i=0; i<first_block; i++ )); do zone_head+=("${zone[$i]}"); done
|
||||
|
||||
nblocks=${#block_start[@]}
|
||||
# block k spans zone[ block_start[k] .. (block_start[k+1]-1 or end-of-zone) ]
|
||||
block_text() { # $1 = block index → prints the block's lines, LF-joined (no trailing)
|
||||
local k=$1 s e
|
||||
s=${block_start[$k]}
|
||||
if (( k+1 < nblocks )); then e=$(( block_start[$((k+1))] - 1 )); else e=$(( ${#zone[@]} - 1 )); fi
|
||||
local out=()
|
||||
for (( j=s; j<=e; j++ )); do out+=("${zone[$j]}"); done
|
||||
printf '%s\n' "${out[@]}"
|
||||
}
|
||||
|
||||
# --- greedily roll oldest (bottom-most) blocks until under cap ------------------
|
||||
# keep = number of newest blocks retained; start with all, drop from the bottom.
|
||||
keep=$nblocks # blocks [keep .. nblocks-1] are the oldest set that gets moved
|
||||
current_size=$orig_size
|
||||
build_live_size() { # size of LIVE if we keep blocks [0 .. keep-1]
|
||||
local acc=("${preamble[@]}" "${zone_head[@]}")
|
||||
local k s e j
|
||||
for (( k=0; k<keep; k++ )); do
|
||||
s=${block_start[$k]}
|
||||
if (( k+1 < nblocks )); then e=$(( block_start[$((k+1))] - 1 )); else e=$(( ${#zone[@]} - 1 )); fi
|
||||
for (( j=s; j<=e; j++ )); do acc+=("${zone[$j]}"); done
|
||||
done
|
||||
acc+=("${footer[@]}")
|
||||
render_size "${acc[@]}"
|
||||
}
|
||||
while (( current_size >= CAP && keep > 0 )); do
|
||||
keep=$(( keep - 1 ))
|
||||
current_size=$(build_live_size)
|
||||
done
|
||||
|
||||
moved_count=$(( nblocks - keep ))
|
||||
if (( moved_count == 0 )); then
|
||||
# zone had entries but none movable brought us under (shouldn't happen: keep hits 0)
|
||||
echo "board-roll: could not reduce LIVE below cap (${current_size}B >= ${CAP}B)." >&2
|
||||
exit 3
|
||||
fi
|
||||
|
||||
# --- dry-run report -------------------------------------------------------------
|
||||
plan_headers() {
|
||||
local k
|
||||
for (( k=keep; k<nblocks; k++ )); do
|
||||
# first line of each moved block
|
||||
printf ' %s\n' "${zone[${block_start[$k]}]}"
|
||||
done
|
||||
}
|
||||
if (( DRYRUN )); then
|
||||
echo "board-roll: DRY RUN"
|
||||
echo " LIVE now: ${orig_size}B (cap ${CAP}B) — over by $(( orig_size - CAP ))B"
|
||||
echo " would roll: ${moved_count} of ${nblocks} entr$([[ $moved_count -eq 1 ]] && echo y || echo ies) (oldest first):"
|
||||
plan_headers
|
||||
echo " LIVE after: ${current_size}B"
|
||||
if (( current_size >= CAP )); then
|
||||
echo " WARNING: still >= cap after emptying the zone; pinned sections need a manual trim." >&2
|
||||
exit 3
|
||||
fi
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# --- commit the roll atomically -------------------------------------------------
|
||||
live_tmp="$(mktemp "${LIVE}.roll.XXXXXX")" || die "cannot create temp next to LIVE"
|
||||
ledger_tmp=""
|
||||
# shellcheck disable=SC2329 # invoked indirectly via `trap cleanup EXIT`
|
||||
cleanup() { rm -f "$live_tmp" "$ledger_tmp" 2>/dev/null || true; }
|
||||
trap cleanup EXIT
|
||||
|
||||
# new LIVE = preamble + zone_head + kept blocks + footer
|
||||
{
|
||||
printf '%s\n' "${preamble[@]}" "${zone_head[@]}"
|
||||
for (( k=0; k<keep; k++ )); do block_text "$k"; done
|
||||
printf '%s\n' "${footer[@]}"
|
||||
} > "$live_tmp"
|
||||
|
||||
# LEDGER gets the moved blocks appended verbatim, in original top→bottom order,
|
||||
# under a provenance separator. LEDGER is append-only, so we only ever add at EOF.
|
||||
ledger_tmp="$(mktemp "${LEDGER}.roll.XXXXXX")" || die "cannot create temp next to LEDGER"
|
||||
if [[ -f "$LEDGER" ]]; then cat "$LEDGER" > "$ledger_tmp"; fi
|
||||
# ensure a trailing newline on existing content before appending
|
||||
if [[ -s "$ledger_tmp" && -n "$(tail -c1 "$ledger_tmp")" ]]; then printf '\n' >> "$ledger_tmp"; fi
|
||||
{
|
||||
printf '\n<!-- board-roll: %d entr%s rolled from %s -->\n' \
|
||||
"$moved_count" "$([[ $moved_count -eq 1 ]] && echo y || echo ies)" "$(basename "$LIVE")"
|
||||
for (( k=keep; k<nblocks; k++ )); do block_text "$k"; done
|
||||
} >> "$ledger_tmp"
|
||||
|
||||
# atomic swap (both, LEDGER first so a crash never drops content that left LIVE)
|
||||
mv "$ledger_tmp" "$LEDGER"; ledger_tmp=""
|
||||
mv "$live_tmp" "$LIVE"; live_tmp=""
|
||||
trap - EXIT
|
||||
|
||||
# read the real on-disk size back (truthful, not the predicted value)
|
||||
final_size=$(wc -c < "$LIVE")
|
||||
echo "board-roll: rolled ${moved_count} entr$([[ $moved_count -eq 1 ]] && echo y || echo ies) to $(basename "$LEDGER"); LIVE ${orig_size}B → ${final_size}B (cap ${CAP}B)."
|
||||
if (( final_size >= CAP )); then
|
||||
echo "board-roll: still >= cap after rolling all zone entries; pinned sections need a manual trim." >&2
|
||||
exit 3
|
||||
fi
|
||||
exit 0
|
||||
@@ -0,0 +1,173 @@
|
||||
#!/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"
|
||||
target_runtime="$(coord_runtime)"
|
||||
launch_cmd="$(coord_launch_command)"
|
||||
|
||||
# ─── 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
|
||||
|
||||
# Write machine-readable next-task capsule for deterministic runtime launches.
|
||||
write_next_task_capsule \
|
||||
"$PROJECT" \
|
||||
"$target_runtime" \
|
||||
"$mission_id" \
|
||||
"$mission_name" \
|
||||
"$project_path" \
|
||||
"$quality_gates" \
|
||||
"$current_ms_id" \
|
||||
"$current_ms_name" \
|
||||
"$next_task" \
|
||||
"$tasks_done" \
|
||||
"$tasks_total" \
|
||||
"$pct" \
|
||||
"$current_branch"
|
||||
|
||||
# ─── 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
|
||||
- **Target runtime:** $target_runtime
|
||||
|
||||
## 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. Launch runtime with \`$launch_cmd\`
|
||||
7. Continue execution from task **${next_task:-next-pending}**
|
||||
8. Follow Two-Phase Completion Protocol
|
||||
9. 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
|
||||
@@ -0,0 +1,286 @@
|
||||
#!/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 ──────────────────────────────────────────────────────────────────
|
||||
|
||||
runtime_cmd="$(coord_launch_command)"
|
||||
run_cmd="$(coord_run_command)"
|
||||
|
||||
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: Resume with '$run_cmd' (or launch directly with '$runtime_cmd')."
|
||||
@@ -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 ""
|
||||
@@ -0,0 +1,210 @@
|
||||
#!/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:"
|
||||
mapfile -t dirty_lines <<<"$dirty_files"
|
||||
file_count="${#dirty_lines[@]}"
|
||||
display_count=$((file_count < 20 ? file_count : 20))
|
||||
for ((i = 0; i < display_count; i++)); do
|
||||
echo " ${dirty_lines[$i]}"
|
||||
done
|
||||
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
|
||||
@@ -0,0 +1,91 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
#
|
||||
# session-run.sh — Generate continuation context and launch target runtime.
|
||||
#
|
||||
# Usage:
|
||||
# session-run.sh [--project <path>] [--milestone <id>] [--print] [--yolo]
|
||||
#
|
||||
# Behavior:
|
||||
# - Builds continuation prompt + next-task capsule.
|
||||
# - Launches selected runtime (default: claude, override via MOSAIC_COORD_RUNTIME).
|
||||
# - For codex, injects strict orchestration kickoff to reduce clarification loops.
|
||||
# - --yolo launches the runtime in dangerous/skip-permissions mode.
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
source "$SCRIPT_DIR/_lib.sh"
|
||||
|
||||
PROJECT="."
|
||||
MILESTONE=""
|
||||
PRINT=false
|
||||
YOLO=false
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--project) PROJECT="$2"; shift 2 ;;
|
||||
--milestone) MILESTONE="$2"; shift 2 ;;
|
||||
--print) PRINT=true; shift ;;
|
||||
--yolo) YOLO=true; shift ;;
|
||||
-h|--help)
|
||||
cat <<'USAGE'
|
||||
Usage: session-run.sh [--project <path>] [--milestone <id>] [--print] [--yolo]
|
||||
|
||||
Options:
|
||||
--project <path> Project directory (default: CWD)
|
||||
--milestone <id> Force specific milestone context
|
||||
--print Print launch prompt only (no runtime launch)
|
||||
--yolo Launch runtime in dangerous/skip-permissions mode
|
||||
USAGE
|
||||
exit 0
|
||||
;;
|
||||
*) echo "Unknown option: $1" >&2; exit 1 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
PROJECT="${PROJECT/#\~/$HOME}"
|
||||
PROJECT="$(cd "$PROJECT" && pwd)"
|
||||
|
||||
_require_jq
|
||||
require_mission "$PROJECT"
|
||||
|
||||
runtime="$(coord_runtime)"
|
||||
launch_cmd="$(coord_launch_command)"
|
||||
|
||||
continue_cmd=(bash "$SCRIPT_DIR/continue-prompt.sh" --project "$PROJECT")
|
||||
if [[ -n "$MILESTONE" ]]; then
|
||||
continue_cmd+=(--milestone "$MILESTONE")
|
||||
fi
|
||||
|
||||
continuation_prompt="$(MOSAIC_COORD_RUNTIME="$runtime" "${continue_cmd[@]}")"
|
||||
|
||||
if [[ "$runtime" == "codex" ]]; then
|
||||
launch_prompt="$(build_codex_strict_kickoff "$PROJECT" "$continuation_prompt")"
|
||||
else
|
||||
launch_prompt="$continuation_prompt"
|
||||
fi
|
||||
|
||||
if [[ "$PRINT" == true ]]; then
|
||||
echo "$launch_prompt"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [[ "$YOLO" == true ]]; then
|
||||
launch_cmd="mosaic yolo $runtime"
|
||||
fi
|
||||
|
||||
echo -e "${C_CYAN}Launching orchestration runtime: ${launch_cmd}${C_RESET}"
|
||||
echo -e "${C_CYAN}Project:${C_RESET} $PROJECT"
|
||||
echo -e "${C_CYAN}Capsule:${C_RESET} $(next_task_capsule_path "$PROJECT")"
|
||||
[[ "$YOLO" == true ]] && echo -e "${C_YELLOW}[YOLO] Dangerous permissions mode enabled.${C_RESET}"
|
||||
|
||||
cd "$PROJECT"
|
||||
if [[ "$YOLO" == true ]]; then
|
||||
exec mosaic yolo "$runtime" "$launch_prompt"
|
||||
elif [[ "$runtime" == "claude" ]]; then
|
||||
exec mosaic claude "$launch_prompt"
|
||||
elif [[ "$runtime" == "codex" ]]; then
|
||||
exec mosaic codex "$launch_prompt"
|
||||
fi
|
||||
|
||||
echo -e "${C_RED}Unsupported coord runtime: $runtime${C_RESET}" >&2
|
||||
exit 1
|
||||
@@ -0,0 +1,241 @@
|
||||
#!/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
|
||||
runtime_cmd="$(coord_launch_command)"
|
||||
run_cmd="$(coord_run_command)"
|
||||
|
||||
# ─── Check session lock ─────────────────────────────────────────────────────
|
||||
|
||||
lock_data=""
|
||||
if ! lock_data="$(session_lock_read "$PROJECT")"; then
|
||||
# No active session — but check if a mission exists
|
||||
mp="$(mission_path "$PROJECT")"
|
||||
if [[ -f "$mp" ]]; then
|
||||
m_status="$(jq -r '.status // "inactive"' "$mp")"
|
||||
m_name="$(jq -r '.name // "unnamed"' "$mp")"
|
||||
m_id="$(jq -r '.mission_id // ""' "$mp")"
|
||||
m_total="$(jq '.milestones | length' "$mp")"
|
||||
m_done="$(jq '[.milestones[] | select(.status == "completed")] | length' "$mp")"
|
||||
m_current="$(jq -r '[.milestones[] | select(.status == "active" or .status == "pending")][0].name // "none"' "$mp")"
|
||||
|
||||
# Task counts if TASKS.md exists
|
||||
task_json="$(count_tasks_md "$PROJECT")"
|
||||
t_total="$(echo "$task_json" | jq '.total')"
|
||||
t_done="$(echo "$task_json" | jq '.done')"
|
||||
t_pending="$(echo "$task_json" | jq '.pending')"
|
||||
t_inprog="$(echo "$task_json" | jq '.in_progress')"
|
||||
|
||||
if [[ "$FORMAT" == "json" ]]; then
|
||||
jq -n \
|
||||
--arg status "no-session" \
|
||||
--arg mission_status "$m_status" \
|
||||
--arg mission_name "$m_name" \
|
||||
--arg mission_id "$m_id" \
|
||||
--argjson milestones_total "$m_total" \
|
||||
--argjson milestones_done "$m_done" \
|
||||
--argjson tasks_total "$t_total" \
|
||||
--argjson tasks_done "$t_done" \
|
||||
'{
|
||||
status: $status,
|
||||
mission: {
|
||||
status: $mission_status,
|
||||
name: $mission_name,
|
||||
id: $mission_id,
|
||||
milestones_total: $milestones_total,
|
||||
milestones_done: $milestones_done,
|
||||
tasks_total: $tasks_total,
|
||||
tasks_done: $tasks_done
|
||||
}
|
||||
}'
|
||||
else
|
||||
echo ""
|
||||
echo -e " ${C_DIM}No active agent session.${C_RESET}"
|
||||
echo ""
|
||||
|
||||
# Mission info
|
||||
case "$m_status" in
|
||||
active) ms_color="${C_GREEN}ACTIVE${C_RESET}" ;;
|
||||
paused) ms_color="${C_YELLOW}PAUSED${C_RESET}" ;;
|
||||
completed) ms_color="${C_CYAN}COMPLETED${C_RESET}" ;;
|
||||
*) ms_color="${C_DIM}${m_status}${C_RESET}" ;;
|
||||
esac
|
||||
|
||||
echo -e " ${C_BOLD}Mission:${C_RESET} $m_name"
|
||||
echo -e " ${C_CYAN}Status:${C_RESET} $ms_color"
|
||||
echo -e " ${C_CYAN}ID:${C_RESET} $m_id"
|
||||
echo -e " ${C_CYAN}Milestones:${C_RESET} $m_done / $m_total completed"
|
||||
[[ "$m_current" != "none" ]] && echo -e " ${C_CYAN}Current:${C_RESET} $m_current"
|
||||
|
||||
if (( t_total > 0 )); then
|
||||
echo -e " ${C_CYAN}Tasks:${C_RESET} $t_done / $t_total done ($t_pending pending, $t_inprog in-progress)"
|
||||
fi
|
||||
echo ""
|
||||
|
||||
if [[ "$m_status" == "active" || "$m_status" == "paused" ]]; then
|
||||
echo -e " ${C_BOLD}Next steps:${C_RESET}"
|
||||
echo " $run_cmd Auto-generate context and launch"
|
||||
echo " mosaic coord continue Generate continuation prompt"
|
||||
echo " $runtime_cmd Launch agent session"
|
||||
elif [[ "$m_status" == "completed" ]]; then
|
||||
echo -e " ${C_DIM}Mission completed. Start a new one with: mosaic coord init${C_RESET}"
|
||||
else
|
||||
echo -e " ${C_DIM}Initialize with: mosaic coord init --name \"Mission Name\"${C_RESET}"
|
||||
fi
|
||||
echo ""
|
||||
fi
|
||||
else
|
||||
if [[ "$FORMAT" == "json" ]]; then
|
||||
echo '{"status":"no-session","mission":null}'
|
||||
else
|
||||
echo ""
|
||||
echo -e " ${C_DIM}No active session.${C_RESET}"
|
||||
echo -e " ${C_DIM}No mission found.${C_RESET}"
|
||||
echo ""
|
||||
echo " Initialize with: mosaic coord init --name \"Mission Name\""
|
||||
echo ""
|
||||
fi
|
||||
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"
|
||||
@@ -0,0 +1,78 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
#
|
||||
# smoke-test.sh — Behavior smoke checks for coord continue/run workflows.
|
||||
#
|
||||
# Usage:
|
||||
# smoke-test.sh
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
source "$SCRIPT_DIR/_lib.sh"
|
||||
|
||||
PASS=0
|
||||
FAIL=0
|
||||
|
||||
pass_case() {
|
||||
echo "PASS: $1"
|
||||
PASS=$((PASS + 1))
|
||||
}
|
||||
|
||||
fail_case() {
|
||||
echo "FAIL: $1" >&2
|
||||
FAIL=$((FAIL + 1))
|
||||
}
|
||||
|
||||
tmp_project="$(mktemp -d)"
|
||||
trap 'rm -rf "$tmp_project"' EXIT
|
||||
|
||||
mkdir -p "$tmp_project/.mosaic/orchestrator" "$tmp_project/docs/scratchpads"
|
||||
|
||||
cat > "$tmp_project/.mosaic/orchestrator/mission.json" <<'JSON'
|
||||
{
|
||||
"mission_id": "smoke-mission-20260223",
|
||||
"name": "Smoke Mission",
|
||||
"status": "active",
|
||||
"project_path": "SMOKE_PROJECT",
|
||||
"quality_gates": "pnpm lint && pnpm test",
|
||||
"milestones": [
|
||||
{ "id": "M1", "name": "Milestone One", "status": "pending" }
|
||||
],
|
||||
"sessions": []
|
||||
}
|
||||
JSON
|
||||
|
||||
cat > "$tmp_project/docs/MISSION-MANIFEST.md" <<'MD'
|
||||
# Mission Manifest
|
||||
MD
|
||||
|
||||
cat > "$tmp_project/docs/scratchpads/smoke-mission-20260223.md" <<'MD'
|
||||
# Scratchpad
|
||||
MD
|
||||
|
||||
cat > "$tmp_project/docs/TASKS.md" <<'MD'
|
||||
| id | status | milestone | description | pr | notes |
|
||||
|----|--------|-----------|-------------|----|-------|
|
||||
| T-001 | pending | M1 | Smoke task | | |
|
||||
MD
|
||||
|
||||
codex_continue_output="$(MOSAIC_COORD_RUNTIME=codex bash "$SCRIPT_DIR/continue-prompt.sh" --project "$tmp_project")"
|
||||
capsule_file="$tmp_project/.mosaic/orchestrator/next-task.json"
|
||||
|
||||
if [[ -f "$capsule_file" ]]; then pass_case "continue writes next-task capsule"; else fail_case "continue writes next-task capsule"; fi
|
||||
if jq -e '.runtime == "codex"' "$capsule_file" >/dev/null 2>&1; then pass_case "capsule runtime is codex"; else fail_case "capsule runtime is codex"; fi
|
||||
if jq -e '.next_task == "T-001"' "$capsule_file" >/dev/null 2>&1; then pass_case "capsule next_task is T-001"; else fail_case "capsule next_task is T-001"; fi
|
||||
if grep -Fq 'Target runtime:** codex' <<< "$codex_continue_output"; then pass_case "continue prompt contains target runtime codex"; else fail_case "continue prompt contains target runtime codex"; fi
|
||||
|
||||
codex_run_prompt="$(MOSAIC_COORD_RUNTIME=codex bash "$SCRIPT_DIR/session-run.sh" --project "$tmp_project" --print)"
|
||||
if [[ "${codex_run_prompt%%$'\n'*}" == "Now initiating Orchestrator mode..." ]]; then pass_case "codex run prompt first line is mode declaration"; else fail_case "codex run prompt first line is mode declaration"; fi
|
||||
if grep -Fq 'Do NOT ask clarifying questions before your first tool actions' <<< "$codex_run_prompt"; then pass_case "codex run prompt includes no-questions hard gate"; else fail_case "codex run prompt includes no-questions hard gate"; fi
|
||||
if grep -Fq '"next_task": "T-001"' <<< "$codex_run_prompt"; then pass_case "codex run prompt embeds capsule json"; else fail_case "codex run prompt embeds capsule json"; fi
|
||||
|
||||
claude_run_prompt="$(MOSAIC_COORD_RUNTIME=claude bash "$SCRIPT_DIR/session-run.sh" --project "$tmp_project" --print)"
|
||||
if [[ "${claude_run_prompt%%$'\n'*}" == "## Continuation Mission" ]]; then pass_case "claude run prompt remains continuation prompt format"; else fail_case "claude run prompt remains continuation prompt format"; fi
|
||||
|
||||
echo ""
|
||||
echo "Smoke test summary: pass=$PASS fail=$FAIL"
|
||||
if (( FAIL > 0 )); then
|
||||
exit 1
|
||||
fi
|
||||
@@ -0,0 +1,156 @@
|
||||
#!/usr/bin/env bash
|
||||
# Regression harness for board-roll.sh — rolling oldest LIVE-board entries to LEDGER.
|
||||
#
|
||||
# Asserts:
|
||||
# 1. Under cap → no-op, exit 0, files unchanged.
|
||||
# 2. Over cap, no roll markers → exit 3, LIVE unchanged (never guesses).
|
||||
# 3. Over cap, markers present → rolls the fewest oldest entries to get under cap,
|
||||
# LIVE ends under cap, pinned preamble/footer + newest entries preserved.
|
||||
# 4. Rolled blocks land in the LEDGER verbatim, oldest set in original order.
|
||||
# 5. --dry-run changes nothing and reports a plan.
|
||||
# 6. Zone emptied but pinned sections alone exceed cap → exit 3.
|
||||
# 7. --help exits 0 and prints usage; an unknown flag exits nonzero (#701 discipline).
|
||||
# 8. More than one marker pair → exit 3, unchanged; curated content between the two
|
||||
# zones is never relocated to the LEDGER (rev0 #868 regression).
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
SUT="$SCRIPT_DIR/board-roll.sh"
|
||||
|
||||
fail=0
|
||||
note() { echo "FAIL: $*" >&2; fail=1; }
|
||||
|
||||
WORK="$(mktemp -d)"
|
||||
trap 'rm -rf "$WORK"' EXIT
|
||||
|
||||
# builds a LIVE board: pinned preamble + roll zone with N dated entries (newest first),
|
||||
# each entry padded to be individually large so the cap math is predictable.
|
||||
make_board() { # $1 file $2 n_entries $3 with_markers(1/0) $4 pad_bytes
|
||||
local f=$1 n=$2 markers=$3 pad=$4 i padtxt
|
||||
padtxt=$(head -c "$pad" < /dev/zero | tr '\0' 'x')
|
||||
{
|
||||
echo "# BOARD — LIVE"
|
||||
echo "> pinned protocol blockquote, never rolled."
|
||||
echo
|
||||
echo "## Curated always-current section (pinned)"
|
||||
echo "- this stays no matter what"
|
||||
echo
|
||||
[[ "$markers" == 1 ]] && echo '<!-- BOARD-ROLL:START -->'
|
||||
# newest first (i=n .. 1); oldest (i=1) ends at the bottom
|
||||
for (( i=n; i>=1; i-- )); do
|
||||
echo "### 2026-07-$(printf '%02d' $i) tick number $i"
|
||||
echo "- detail $i $padtxt"
|
||||
echo
|
||||
done
|
||||
[[ "$markers" == 1 ]] && echo '<!-- BOARD-ROLL:END -->'
|
||||
} > "$f"
|
||||
return 0
|
||||
}
|
||||
|
||||
# ── 1. under cap → no-op ───────────────────────────────────────────────────────
|
||||
L="$WORK/live1.md"; G="$WORK/ledger1.md"; : > "$G"
|
||||
make_board "$L" 2 1 10
|
||||
before=$(cat "$L")
|
||||
if ! out=$(bash "$SUT" --live "$L" --ledger "$G" --cap 100000 2>&1); then
|
||||
note "under-cap should exit 0 (got nonzero): $out"
|
||||
fi
|
||||
[[ "$(cat "$L")" == "$before" ]] || note "under-cap modified LIVE"
|
||||
[[ -s "$G" ]] && note "under-cap wrote to LEDGER"
|
||||
|
||||
# ── 2. over cap, no markers → exit 3, unchanged ────────────────────────────────
|
||||
L="$WORK/live2.md"; G="$WORK/ledger2.md"; : > "$G"
|
||||
make_board "$L" 6 0 400
|
||||
before=$(cat "$L")
|
||||
set +e; bash "$SUT" --live "$L" --ledger "$G" --cap 800 >/dev/null 2>&1; rc=$?; set -e
|
||||
[[ "$rc" -eq 3 ]] || note "no-markers over-cap should exit 3 (got $rc)"
|
||||
[[ "$(cat "$L")" == "$before" ]] || note "no-markers run modified LIVE (must never guess)"
|
||||
|
||||
# ── 3+4. over cap with markers → rolls oldest, LIVE under cap, LEDGER gets them ─
|
||||
L="$WORK/live3.md"; G="$WORK/ledger3.md"; echo "# LEDGER" > "$G"
|
||||
make_board "$L" 6 1 400 # 6 entries, each ~>400B
|
||||
big=$(wc -c < "$L")
|
||||
[[ "$big" -ge 2000 ]] || note "fixture too small to test rolling ($big B)"
|
||||
if ! out=$(bash "$SUT" --live "$L" --ledger "$G" --cap 2000 2>&1); then
|
||||
note "marker roll should exit 0 when it can get under cap: $out"
|
||||
fi
|
||||
after=$(wc -c < "$L")
|
||||
[[ "$after" -lt 2000 ]] || note "LIVE still >= cap after roll ($after B)"
|
||||
# pinned content survives
|
||||
grep -q "Curated always-current section" "$L" || note "roll dropped pinned section"
|
||||
grep -q 'BOARD-ROLL:START' "$L" || note "roll dropped START marker"
|
||||
grep -q 'BOARD-ROLL:END' "$L" || note "roll dropped END marker"
|
||||
# newest entry (07-06) stays; oldest (07-01) is the first to leave
|
||||
grep -q "### 2026-07-06 tick number 6" "$L" || note "roll dropped the newest entry"
|
||||
grep -q "### 2026-07-01 tick number 1" "$L" && note "oldest entry not rolled out of LIVE"
|
||||
# oldest went to LEDGER
|
||||
grep -q "### 2026-07-01 tick number 1" "$G" || note "oldest entry not appended to LEDGER"
|
||||
grep -q "board-roll:.*rolled from live3.md" "$G" || note "LEDGER missing provenance separator"
|
||||
# a rolled entry must not be duplicated (present in exactly one of LIVE/LEDGER)
|
||||
if grep -q "### 2026-07-01 tick number 1" "$L"; then note "rolled entry duplicated in LIVE"; fi
|
||||
# LEDGER original content preserved
|
||||
grep -q "^# LEDGER" "$G" || note "roll clobbered existing LEDGER content"
|
||||
|
||||
# ── 5. --dry-run changes nothing ───────────────────────────────────────────────
|
||||
L="$WORK/live5.md"; G="$WORK/ledger5.md"; echo "# LEDGER" > "$G"
|
||||
make_board "$L" 6 1 400
|
||||
before_l=$(cat "$L"); before_g=$(cat "$G")
|
||||
out=$(bash "$SUT" --live "$L" --ledger "$G" --cap 2000 --dry-run 2>&1) || note "dry-run exited nonzero: $out"
|
||||
grep -qi "dry run" <<<"$out" || note "dry-run did not announce itself"
|
||||
grep -q "would roll" <<<"$out" || note "dry-run did not report a plan"
|
||||
[[ "$(cat "$L")" == "$before_l" ]] || note "dry-run modified LIVE"
|
||||
[[ "$(cat "$G")" == "$before_g" ]] || note "dry-run modified LEDGER"
|
||||
|
||||
# ── 6. zone emptied, pinned alone over cap → exit 3 ────────────────────────────
|
||||
# cap 120 is below the pinned preamble+footer size (~180B), so even after rolling
|
||||
# every zone entry the LIVE file stays over cap → must report the unsatisfiable case.
|
||||
L="$WORK/live6.md"; G="$WORK/ledger6.md"; echo "# LEDGER" > "$G"
|
||||
make_board "$L" 3 1 50
|
||||
set +e; bash "$SUT" --live "$L" --ledger "$G" --cap 120 >/dev/null 2>&1; rc=$?; set -e
|
||||
[[ "$rc" -eq 3 ]] || note "unsatisfiable cap should exit 3 (got $rc)"
|
||||
|
||||
# ── 7. help exits 0, unknown flag exits nonzero (#701) ─────────────────────────
|
||||
if ! out=$(bash "$SUT" --help 2>&1); then note "--help exited nonzero"; fi
|
||||
[[ "$out" == Usage:* ]] || note "--help did not print usage"
|
||||
bash "$SUT" -h >/dev/null 2>&1 || note "-h exited nonzero"
|
||||
if bash "$SUT" --not-a-real-flag >/dev/null 2>&1; then note "unknown flag was accepted"; fi
|
||||
if bash "$SUT" --live "$WORK/live3.md" >/dev/null 2>&1; then note "missing --ledger was accepted"; fi
|
||||
|
||||
# ── 8. multiple marker pairs → exit 3, unchanged (no cross-zone relocation) ─────
|
||||
# Two separately-marked zones with a curated pinned section BETWEEN them. A naive
|
||||
# first-START..last-END span would sweep that curated section (and the intermediate
|
||||
# markers) into the LEDGER. board-roll must refuse (exit 3) and touch nothing.
|
||||
L="$WORK/live8.md"; G="$WORK/ledger8.md"; echo "# LEDGER" > "$G"
|
||||
pad8=$(head -c 300 < /dev/zero | tr '\0' 'x')
|
||||
{
|
||||
echo "# BOARD — LIVE"
|
||||
echo "> pinned protocol blockquote"
|
||||
echo
|
||||
echo '<!-- BOARD-ROLL:START -->'
|
||||
echo "### 2026-07-10 zone-A newest"
|
||||
echo "- detail A2 $pad8"
|
||||
echo "### 2026-07-09 zone-A oldest"
|
||||
echo "- detail A1 $pad8"
|
||||
echo '<!-- BOARD-ROLL:END -->'
|
||||
echo
|
||||
echo "## Curated-between-zones (pinned — must never move)"
|
||||
echo "- CANARY-BETWEEN keep me"
|
||||
echo
|
||||
echo '<!-- BOARD-ROLL:START -->'
|
||||
echo "### 2026-07-08 zone-B newest"
|
||||
echo "- detail B2 $pad8"
|
||||
echo "### 2026-07-07 zone-B oldest"
|
||||
echo "- detail B1 $pad8"
|
||||
echo '<!-- BOARD-ROLL:END -->'
|
||||
} > "$L"
|
||||
before8=$(cat "$L")
|
||||
set +e; bash "$SUT" --live "$L" --ledger "$G" --cap 80 >/dev/null 2>&1; rc=$?; set -e
|
||||
[[ "$rc" -eq 3 ]] || note "multi-pair board should exit 3 (got $rc)"
|
||||
[[ "$(cat "$L")" == "$before8" ]] || note "multi-pair run modified LIVE (must never guess across zones)"
|
||||
grep -q "CANARY-BETWEEN keep me" "$L" || note "multi-pair run relocated curated between-zones content"
|
||||
grep -q "CANARY-BETWEEN" "$G" && note "curated between-zones content leaked into LEDGER"
|
||||
|
||||
if [[ "$fail" -eq 0 ]]; then
|
||||
echo "board-roll regression passed (8 groups)"
|
||||
fi
|
||||
exit "$fail"
|
||||
Reference in New Issue
Block a user