Compare commits
1 Commits
main
...
feature/to
| Author | SHA1 | Date | |
|---|---|---|---|
| 80c3680ccb |
25
AGENTS.md
25
AGENTS.md
@@ -34,9 +34,6 @@ If any required file is missing, you MUST stop and report the missing file.
|
||||
7. For issue/PR/milestone operations, you MUST use Mosaic wrappers first (`~/.config/mosaic/tools/git/*.sh`).
|
||||
8. If any required wrapper command fails, status is `blocked`; report the exact failed wrapper command and stop.
|
||||
9. Do NOT stop at "PR created". Do NOT ask "should I merge?" Do NOT ask "should I close the issue?".
|
||||
10. Manual `docker build` / `docker push` for deployment is FORBIDDEN when CI/CD pipelines exist in the repository. CI is the ONLY canonical build path for container images.
|
||||
11. Before ANY build or deployment action, you MUST check for existing CI/CD pipeline configuration (`.woodpecker/`, `.woodpecker.yml`, `.github/workflows/`, etc.). If pipelines exist, use them — do not build locally.
|
||||
12. The mandatory load order and intake procedure are NOT conditional on perceived task complexity. A "simple" commit-push-deploy task has the same procedural requirements as a multi-file feature. Skipping intake because a task "seems simple" is the most common framework violation.
|
||||
|
||||
## Non-Negotiable Operating Rules
|
||||
|
||||
@@ -76,7 +73,6 @@ 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)
|
||||
|
||||
@@ -116,7 +112,6 @@ 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)
|
||||
|
||||
@@ -130,26 +125,6 @@ Load additional guides when the task requires them.
|
||||
- Installation and configuration are managed by Mosaic bootstrap and runtime linking.
|
||||
- If sequential-thinking is unavailable, you MUST report the failure and stop planning-intensive execution.
|
||||
|
||||
## Subagent Model Selection (Cost Optimization — Hard Rule)
|
||||
|
||||
When delegating work to subagents, you MUST select the cheapest model capable of completing the task. Do NOT default to the most expensive model for every delegation.
|
||||
|
||||
| Task Type | Model Tier | Rationale |
|
||||
|-----------|-----------|-----------|
|
||||
| File search, grep, glob, codebase exploration | **haiku** | Read-only, pattern matching, no reasoning depth needed |
|
||||
| Status checks, health monitoring, heartbeat | **haiku** | Structured API calls, pass/fail output |
|
||||
| Simple code fixes (typos, rename, one-liner) | **haiku** | Minimal reasoning, mechanical changes |
|
||||
| Code review, lint, style checks | **sonnet** | Needs judgment but not deep architectural reasoning |
|
||||
| Test writing, test fixes | **sonnet** | Pattern-based, moderate complexity |
|
||||
| Standard feature implementation | **sonnet** | Good balance of capability and cost for most coding |
|
||||
| Complex architecture, multi-file refactors | **opus** | Requires deep reasoning, large context, design judgment |
|
||||
| Security review, auth logic | **opus** | High-stakes reasoning where mistakes are costly |
|
||||
| Ambiguous requirements, design decisions | **opus** | Needs nuanced judgment and tradeoff analysis |
|
||||
|
||||
**Decision rule**: Start with the cheapest viable tier. Only escalate if the task genuinely requires deeper reasoning — not as a safety default. Most coding tasks are sonnet-tier. Reserve opus for work where wrong answers are expensive.
|
||||
|
||||
**Runtime-specific syntax**: See the runtime reference for how to specify model tier when spawning subagents (e.g., Claude Code Task tool `model` parameter).
|
||||
|
||||
## Skills Policy
|
||||
|
||||
- Use only the minimum required skills for the active task.
|
||||
|
||||
43
TOOLS.md
43
TOOLS.md
@@ -66,45 +66,10 @@ Mosaic wrappers at `~/.config/mosaic/tools/git/*.sh` handle platform detection a
|
||||
|
||||
### CI/CD — Woodpecker
|
||||
|
||||
Multi-instance support: `-a <instance>` selects a named instance. Omit `-a` to use the default from `woodpecker.default` in credentials.json.
|
||||
|
||||
| Instance | URL | Serves |
|
||||
|----------|-----|--------|
|
||||
| `mosaic` (default) | ci.mosaicstack.dev | Mosaic repos (git.mosaicstack.dev) |
|
||||
| `usc` | ci.uscllc.com | USC repos (git.uscllc.com) |
|
||||
|
||||
```bash
|
||||
# List recent pipelines
|
||||
~/.config/mosaic/tools/woodpecker/pipeline-list.sh [-r owner/repo] [-a instance]
|
||||
|
||||
# Check latest or specific pipeline status
|
||||
~/.config/mosaic/tools/woodpecker/pipeline-status.sh [-r owner/repo] [-n number] [-a instance]
|
||||
|
||||
# Trigger a build
|
||||
~/.config/mosaic/tools/woodpecker/pipeline-trigger.sh [-r owner/repo] [-b branch] [-a instance]
|
||||
```
|
||||
|
||||
Instance selection rule: match `-a` to the git remote host of the target repo. If the repo is on `git.uscllc.com`, use `-a usc`. If on `git.mosaicstack.dev`, use `-a mosaic` (or omit, since it's the default).
|
||||
|
||||
### DNS — Cloudflare
|
||||
|
||||
Multi-instance support: `-a <instance>` selects a named instance (e.g. `personal`, `work`). Omit `-a` to use the default from `cloudflare.default` in credentials.json.
|
||||
|
||||
```bash
|
||||
# List zones (domains)
|
||||
~/.config/mosaic/tools/cloudflare/zone-list.sh [-a instance]
|
||||
|
||||
# List DNS records (zone by name or ID)
|
||||
~/.config/mosaic/tools/cloudflare/record-list.sh -z <zone> [-a instance] [-t type] [-n name]
|
||||
|
||||
# Create DNS record
|
||||
~/.config/mosaic/tools/cloudflare/record-create.sh -z <zone> -t <type> -n <name> -c <content> [-a instance] [-p] [-l ttl] [-P priority]
|
||||
|
||||
# Update DNS record
|
||||
~/.config/mosaic/tools/cloudflare/record-update.sh -z <zone> -r <record-id> -t <type> -n <name> -c <content> [-a instance] [-p] [-l ttl]
|
||||
|
||||
# Delete DNS record
|
||||
~/.config/mosaic/tools/cloudflare/record-delete.sh -z <zone> -r <record-id> [-a instance]
|
||||
~/.config/mosaic/tools/woodpecker/pipeline-list.sh
|
||||
~/.config/mosaic/tools/woodpecker/pipeline-status.sh
|
||||
~/.config/mosaic/tools/woodpecker/pipeline-trigger.sh -b <branch>
|
||||
```
|
||||
|
||||
### IT Service — GLPI
|
||||
@@ -135,7 +100,7 @@ Multi-instance support: `-a <instance>` selects a named instance (e.g. `personal
|
||||
# Source in any script to load service credentials
|
||||
source ~/.config/mosaic/tools/_lib/credentials.sh
|
||||
load_credentials <service-name>
|
||||
# Supported: portainer, coolify, authentik, glpi, github, gitea-mosaicstack, gitea-usc, woodpecker, cloudflare
|
||||
# Supported: portainer, coolify, authentik, glpi, github, gitea-mosaicstack, gitea-usc, woodpecker
|
||||
```
|
||||
|
||||
## Git Providers
|
||||
|
||||
397
bin/mosaic
397
bin/mosaic
@@ -51,22 +51,6 @@ Management:
|
||||
release-upgrade [...] Upgrade installed Mosaic release
|
||||
project-upgrade [...] Clean up stale SOUL.md/CLAUDE.md in a project
|
||||
|
||||
PRD:
|
||||
prdy <subcommand> PRD creation and validation
|
||||
init Create docs/PRD.md via guided runtime session
|
||||
update Update existing PRD via guided runtime session
|
||||
validate Check PRD completeness (bash-only)
|
||||
status Quick PRD health check (one-liner)
|
||||
|
||||
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
|
||||
run Generate context and launch selected runtime
|
||||
resume Crash recovery
|
||||
|
||||
Options:
|
||||
-h, --help Show this help
|
||||
-v, --version Show version
|
||||
@@ -146,78 +130,6 @@ build_runtime_prompt() {
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Inject active mission context FIRST so the agent sees it immediately
|
||||
local mission_file=".mosaic/orchestrator/mission.json"
|
||||
if [[ -f "$mission_file" ]] && command -v jq &>/dev/null; then
|
||||
local m_status
|
||||
m_status="$(jq -r '.status // "inactive"' "$mission_file" 2>/dev/null)"
|
||||
if [[ "$m_status" == "active" || "$m_status" == "paused" ]]; then
|
||||
local m_name m_id m_count m_completed
|
||||
m_name="$(jq -r '.name // "unnamed"' "$mission_file")"
|
||||
m_id="$(jq -r '.mission_id // ""' "$mission_file")"
|
||||
m_count="$(jq '.milestones | length' "$mission_file")"
|
||||
m_completed="$(jq '[.milestones[] | select(.status == "completed")] | length' "$mission_file")"
|
||||
|
||||
cat <<MISSION_EOF
|
||||
# ACTIVE MISSION — HARD GATE (Read Before Anything Else)
|
||||
|
||||
An active orchestration mission exists in this project. This is a BLOCKING requirement.
|
||||
|
||||
**Mission:** $m_name
|
||||
**ID:** $m_id
|
||||
**Status:** $m_status
|
||||
**Milestones:** $m_completed / $m_count completed
|
||||
|
||||
## MANDATORY — Before ANY Response to the User
|
||||
|
||||
You MUST complete these steps before responding to any user message, including simple greetings:
|
||||
|
||||
1. Read \`~/.config/mosaic/guides/ORCHESTRATOR-PROTOCOL.md\` (mission lifecycle protocol)
|
||||
2. Read \`docs/MISSION-MANIFEST.md\` for full mission scope, milestones, and success criteria
|
||||
3. Read the latest scratchpad in \`docs/scratchpads/\` for session history, decisions, and corrections
|
||||
4. Read \`docs/TASKS.md\` for current task state (what is done, what is next)
|
||||
5. After reading all four, acknowledge the mission state to the user before proceeding
|
||||
|
||||
If the user gives a task, execute it within the mission context. If no task is given, present mission status and ask how to proceed.
|
||||
|
||||
MISSION_EOF
|
||||
fi
|
||||
fi
|
||||
|
||||
# Inject PRD status so the agent knows requirements state
|
||||
local prd_file="docs/PRD.md"
|
||||
if [[ -f "$prd_file" ]]; then
|
||||
local prd_sections=0
|
||||
local prd_assumptions=0
|
||||
for entry in "Problem Statement|^#{2,3} .*(problem statement|objective)" \
|
||||
"Scope / Non-Goals|^#{2,3} .*(scope|non.goal|out of scope|in.scope)" \
|
||||
"User Stories / Requirements|^#{2,3} .*(user stor|stakeholder|user.*requirement)" \
|
||||
"Functional Requirements|^#{2,3} .*functional requirement" \
|
||||
"Non-Functional Requirements|^#{2,3} .*non.functional" \
|
||||
"Acceptance Criteria|^#{2,3} .*acceptance criteria" \
|
||||
"Technical Considerations|^#{2,3} .*(technical consideration|constraint|dependenc)" \
|
||||
"Risks / Open Questions|^#{2,3} .*(risk|open question)" \
|
||||
"Success Metrics / Testing|^#{2,3} .*(success metric|test|verification)" \
|
||||
"Milestones / Delivery|^#{2,3} .*(milestone|delivery|scope version)"; do
|
||||
local pattern="${entry#*|}"
|
||||
grep -qiE "$pattern" "$prd_file" 2>/dev/null && prd_sections=$((prd_sections + 1))
|
||||
done
|
||||
prd_assumptions=$(grep -c 'ASSUMPTION:' "$prd_file" 2>/dev/null || echo 0)
|
||||
|
||||
local prd_status="ready"
|
||||
(( prd_sections < 10 )) && prd_status="incomplete ($prd_sections/10 sections)"
|
||||
|
||||
cat <<PRD_EOF
|
||||
|
||||
# PRD Status
|
||||
|
||||
- **File:** docs/PRD.md
|
||||
- **Status:** $prd_status
|
||||
- **Assumptions:** $prd_assumptions
|
||||
|
||||
PRD_EOF
|
||||
fi
|
||||
|
||||
cat <<'EOF'
|
||||
# Mosaic Launcher Runtime Contract (Hard Gate)
|
||||
|
||||
@@ -267,63 +179,6 @@ ensure_runtime_config() {
|
||||
fi
|
||||
}
|
||||
|
||||
# Detect active mission and return an initial prompt if one exists.
|
||||
# Sets MOSAIC_MISSION_PROMPT as a side effect.
|
||||
_detect_mission_prompt() {
|
||||
MOSAIC_MISSION_PROMPT=""
|
||||
local mission_file=".mosaic/orchestrator/mission.json"
|
||||
if [[ -f "$mission_file" ]] && command -v jq &>/dev/null; then
|
||||
local m_status
|
||||
m_status="$(jq -r '.status // "inactive"' "$mission_file" 2>/dev/null)"
|
||||
if [[ "$m_status" == "active" || "$m_status" == "paused" ]]; then
|
||||
local m_name
|
||||
m_name="$(jq -r '.name // "unnamed"' "$mission_file")"
|
||||
MOSAIC_MISSION_PROMPT="Active mission detected: ${m_name}. Read the mission state files and report status."
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
# Write a session lock if an active mission exists in the current directory.
|
||||
# Called before exec so $$ captures the PID that will become the agent process.
|
||||
_write_launcher_session_lock() {
|
||||
local runtime="$1"
|
||||
local mission_file=".mosaic/orchestrator/mission.json"
|
||||
local lock_file=".mosaic/orchestrator/session.lock"
|
||||
|
||||
# Only write lock if mission exists and is active
|
||||
[[ -f "$mission_file" ]] || return 0
|
||||
command -v jq &>/dev/null || return 0
|
||||
|
||||
local m_status
|
||||
m_status="$(jq -r '.status // "inactive"' "$mission_file" 2>/dev/null)"
|
||||
[[ "$m_status" == "active" || "$m_status" == "paused" ]] || return 0
|
||||
|
||||
local session_id
|
||||
session_id="${runtime}-$(date +%Y%m%d-%H%M%S)-$$"
|
||||
|
||||
jq -n \
|
||||
--arg sid "$session_id" \
|
||||
--arg rt "$runtime" \
|
||||
--arg pid "$$" \
|
||||
--arg ts "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
|
||||
--arg pp "$(pwd)" \
|
||||
--arg mid "" \
|
||||
'{
|
||||
session_id: $sid,
|
||||
runtime: $rt,
|
||||
pid: ($pid | tonumber),
|
||||
started_at: $ts,
|
||||
project_path: $pp,
|
||||
milestone_id: $mid
|
||||
}' > "$lock_file"
|
||||
}
|
||||
|
||||
# Clean up session lock on exit (covers normal exit + signals).
|
||||
# Registered via trap after _write_launcher_session_lock succeeds.
|
||||
_cleanup_session_lock() {
|
||||
rm -f ".mosaic/orchestrator/session.lock" 2>/dev/null
|
||||
}
|
||||
|
||||
# Launcher functions
|
||||
launch_claude() {
|
||||
check_mosaic_home
|
||||
@@ -332,23 +187,11 @@ 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")"
|
||||
|
||||
# If active mission exists and no user prompt was given, inject initial prompt
|
||||
_detect_mission_prompt
|
||||
_write_launcher_session_lock "claude"
|
||||
trap _cleanup_session_lock EXIT INT TERM
|
||||
if [[ -n "$MOSAIC_MISSION_PROMPT" && $# -eq 0 ]]; then
|
||||
echo "[mosaic] Launching Claude Code (active mission detected)..."
|
||||
exec claude --append-system-prompt "$runtime_prompt" "$MOSAIC_MISSION_PROMPT"
|
||||
else
|
||||
echo "[mosaic] Launching Claude Code..."
|
||||
exec claude --append-system-prompt "$runtime_prompt" "$@"
|
||||
fi
|
||||
echo "[mosaic] Launching Claude Code..."
|
||||
exec claude --append-system-prompt "$runtime_prompt" "$@"
|
||||
}
|
||||
|
||||
launch_opencode() {
|
||||
@@ -358,12 +201,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"
|
||||
_write_launcher_session_lock "opencode"
|
||||
trap _cleanup_session_lock EXIT INT TERM
|
||||
echo "[mosaic] Launching OpenCode..."
|
||||
exec opencode "$@"
|
||||
}
|
||||
@@ -375,20 +214,10 @@ 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"
|
||||
_detect_mission_prompt
|
||||
_write_launcher_session_lock "codex"
|
||||
trap _cleanup_session_lock EXIT INT TERM
|
||||
if [[ -n "$MOSAIC_MISSION_PROMPT" && $# -eq 0 ]]; then
|
||||
echo "[mosaic] Launching Codex (active mission detected)..."
|
||||
exec codex "$MOSAIC_MISSION_PROMPT"
|
||||
else
|
||||
echo "[mosaic] Launching Codex..."
|
||||
exec codex "$@"
|
||||
fi
|
||||
echo "[mosaic] Launching Codex..."
|
||||
exec codex "$@"
|
||||
}
|
||||
|
||||
launch_yolo() {
|
||||
@@ -412,17 +241,8 @@ launch_yolo() {
|
||||
# Claude uses an explicit dangerous permissions flag.
|
||||
local runtime_prompt
|
||||
runtime_prompt="$(build_runtime_prompt "claude")"
|
||||
|
||||
_detect_mission_prompt
|
||||
_write_launcher_session_lock "claude"
|
||||
trap _cleanup_session_lock EXIT INT TERM
|
||||
if [[ -n "$MOSAIC_MISSION_PROMPT" && $# -eq 0 ]]; then
|
||||
echo "[mosaic] Launching Claude Code in YOLO mode (active mission detected)..."
|
||||
exec claude --dangerously-skip-permissions --append-system-prompt "$runtime_prompt" "$MOSAIC_MISSION_PROMPT"
|
||||
else
|
||||
echo "[mosaic] Launching Claude Code in YOLO mode (dangerous permissions enabled)..."
|
||||
exec claude --dangerously-skip-permissions --append-system-prompt "$runtime_prompt" "$@"
|
||||
fi
|
||||
echo "[mosaic] Launching Claude Code in YOLO mode (dangerous permissions enabled)..."
|
||||
exec claude --dangerously-skip-permissions --append-system-prompt "$runtime_prompt" "$@"
|
||||
;;
|
||||
codex)
|
||||
check_mosaic_home
|
||||
@@ -433,16 +253,8 @@ launch_yolo() {
|
||||
|
||||
# Codex reads instructions.md from ~/.codex and supports a direct dangerous flag.
|
||||
ensure_runtime_config "codex" "$HOME/.codex/instructions.md"
|
||||
_detect_mission_prompt
|
||||
_write_launcher_session_lock "codex"
|
||||
trap _cleanup_session_lock EXIT INT TERM
|
||||
if [[ -n "$MOSAIC_MISSION_PROMPT" && $# -eq 0 ]]; then
|
||||
echo "[mosaic] Launching Codex in YOLO mode (active mission detected)..."
|
||||
exec codex --dangerously-bypass-approvals-and-sandbox "$MOSAIC_MISSION_PROMPT"
|
||||
else
|
||||
echo "[mosaic] Launching Codex in YOLO mode (dangerous permissions enabled)..."
|
||||
exec codex --dangerously-bypass-approvals-and-sandbox "$@"
|
||||
fi
|
||||
echo "[mosaic] Launching Codex in YOLO mode (dangerous permissions enabled)..."
|
||||
exec codex --dangerously-bypass-approvals-and-sandbox "$@"
|
||||
;;
|
||||
opencode)
|
||||
check_mosaic_home
|
||||
@@ -453,8 +265,6 @@ launch_yolo() {
|
||||
|
||||
# OpenCode defaults to allow-all permissions unless user config restricts them.
|
||||
ensure_runtime_config "opencode" "$HOME/.config/opencode/AGENTS.md"
|
||||
_write_launcher_session_lock "opencode"
|
||||
trap _cleanup_session_lock EXIT INT TERM
|
||||
echo "[mosaic] Launching OpenCode in YOLO mode..."
|
||||
exec opencode "$@"
|
||||
;;
|
||||
@@ -515,195 +325,6 @@ run_seq() {
|
||||
esac
|
||||
}
|
||||
|
||||
run_coord() {
|
||||
check_mosaic_home
|
||||
local runtime="claude"
|
||||
local runtime_flag=""
|
||||
local -a coord_args=()
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--claude|--codex)
|
||||
local selected_runtime="${1#--}"
|
||||
if [[ -n "$runtime_flag" ]] && [[ "$runtime" != "$selected_runtime" ]]; then
|
||||
echo "[mosaic] ERROR: --claude and --codex are mutually exclusive for 'mosaic coord'." >&2
|
||||
exit 1
|
||||
fi
|
||||
runtime="$selected_runtime"
|
||||
runtime_flag="$1"
|
||||
shift
|
||||
;;
|
||||
*)
|
||||
coord_args+=("$1")
|
||||
shift
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
local subcmd="${coord_args[0]:-help}"
|
||||
if (( ${#coord_args[@]} > 1 )); then
|
||||
set -- "${coord_args[@]:1}"
|
||||
else
|
||||
set --
|
||||
fi
|
||||
|
||||
local tool_dir="$MOSAIC_HOME/tools/orchestrator"
|
||||
|
||||
case "$subcmd" in
|
||||
status|session)
|
||||
MOSAIC_COORD_RUNTIME="$runtime" exec bash "$tool_dir/session-status.sh" "$@"
|
||||
;;
|
||||
init)
|
||||
MOSAIC_COORD_RUNTIME="$runtime" exec bash "$tool_dir/mission-init.sh" "$@"
|
||||
;;
|
||||
mission|progress)
|
||||
MOSAIC_COORD_RUNTIME="$runtime" exec bash "$tool_dir/mission-status.sh" "$@"
|
||||
;;
|
||||
continue|next)
|
||||
MOSAIC_COORD_RUNTIME="$runtime" exec bash "$tool_dir/continue-prompt.sh" "$@"
|
||||
;;
|
||||
run|start)
|
||||
MOSAIC_COORD_RUNTIME="$runtime" exec bash "$tool_dir/session-run.sh" "$@"
|
||||
;;
|
||||
smoke|test)
|
||||
MOSAIC_COORD_RUNTIME="$runtime" exec bash "$tool_dir/smoke-test.sh" "$@"
|
||||
;;
|
||||
resume|recover)
|
||||
MOSAIC_COORD_RUNTIME="$runtime" 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
|
||||
run [--project <path>] Generate context and launch selected runtime
|
||||
smoke Run orchestration behavior smoke checks
|
||||
resume [--project <path>] Crash recovery (detect dirty state, generate fix)
|
||||
|
||||
Runtime:
|
||||
--claude Use Claude runtime hints/prompts (default)
|
||||
--codex Use Codex runtime hints/prompts
|
||||
|
||||
Examples:
|
||||
mosaic coord init --name "Security Fix" --milestones "Critical,High,Medium"
|
||||
mosaic coord mission
|
||||
mosaic coord --codex mission
|
||||
mosaic coord continue --copy
|
||||
mosaic coord run
|
||||
mosaic coord run --codex
|
||||
mosaic coord smoke
|
||||
mosaic coord continue --codex --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
|
||||
# Stale lock from a dead session — clean it up
|
||||
rm -f "$lock_file"
|
||||
echo "[mosaic] Cleaned up stale session lock (PID $pid no longer running)."
|
||||
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_prdy() {
|
||||
check_mosaic_home
|
||||
local runtime="claude"
|
||||
local runtime_flag=""
|
||||
local -a prdy_args=()
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--claude|--codex)
|
||||
local selected_runtime="${1#--}"
|
||||
if [[ -n "$runtime_flag" ]] && [[ "$runtime" != "$selected_runtime" ]]; then
|
||||
echo "[mosaic] ERROR: --claude and --codex are mutually exclusive for 'mosaic prdy'." >&2
|
||||
exit 1
|
||||
fi
|
||||
runtime="$selected_runtime"
|
||||
runtime_flag="$1"
|
||||
shift
|
||||
;;
|
||||
*)
|
||||
prdy_args+=("$1")
|
||||
shift
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
local subcmd="${prdy_args[0]:-help}"
|
||||
if (( ${#prdy_args[@]} > 1 )); then
|
||||
set -- "${prdy_args[@]:1}"
|
||||
else
|
||||
set --
|
||||
fi
|
||||
|
||||
local tool_dir="$MOSAIC_HOME/tools/prdy"
|
||||
|
||||
case "$subcmd" in
|
||||
init)
|
||||
MOSAIC_PRDY_RUNTIME="$runtime" exec bash "$tool_dir/prdy-init.sh" "$@"
|
||||
;;
|
||||
update)
|
||||
MOSAIC_PRDY_RUNTIME="$runtime" exec bash "$tool_dir/prdy-update.sh" "$@"
|
||||
;;
|
||||
validate|check)
|
||||
MOSAIC_PRDY_RUNTIME="$runtime" exec bash "$tool_dir/prdy-validate.sh" "$@"
|
||||
;;
|
||||
status)
|
||||
exec bash "$tool_dir/prdy-status.sh" "$@"
|
||||
;;
|
||||
help|*)
|
||||
cat <<PRDY_USAGE
|
||||
mosaic prdy — PRD creation and validation tools
|
||||
|
||||
Commands:
|
||||
init [--project <path>] [--name <feature>] Create docs/PRD.md via guided runtime session
|
||||
update [--project <path>] Update existing docs/PRD.md via guided runtime session
|
||||
validate [--project <path>] Check PRD completeness against Mosaic guide (bash-only)
|
||||
status [--project <path>] [--format short|json] Quick PRD health check (one-liner)
|
||||
|
||||
Runtime:
|
||||
--claude Use Claude runtime (default)
|
||||
--codex Use Codex runtime
|
||||
|
||||
Examples:
|
||||
mosaic prdy init --name "User Authentication"
|
||||
mosaic prdy update
|
||||
mosaic prdy --codex init --name "User Authentication"
|
||||
mosaic prdy validate
|
||||
|
||||
Output location: docs/PRD.md (per Mosaic PRD guide)
|
||||
|
||||
PRDY_USAGE
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
run_bootstrap() {
|
||||
check_mosaic_home
|
||||
exec "$MOSAIC_HOME/bin/mosaic-bootstrap-repo" "$@"
|
||||
@@ -776,8 +397,6 @@ case "$command" in
|
||||
sync) run_sync "$@" ;;
|
||||
seq) run_seq "$@" ;;
|
||||
bootstrap) run_bootstrap "$@" ;;
|
||||
prdy) run_prdy "$@" ;;
|
||||
coord) run_coord "$@" ;;
|
||||
upgrade) run_upgrade "$@" ;;
|
||||
release-upgrade) run_release_upgrade "$@" ;;
|
||||
project-upgrade) run_project_upgrade "$@" ;;
|
||||
|
||||
@@ -172,14 +172,6 @@ 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"
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
# mosaic-ensure-sequential-thinking.ps1
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
param(
|
||||
[switch]$Check
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
$Pkg = "@modelcontextprotocol/server-sequential-thinking"
|
||||
|
||||
function Require-Binary {
|
||||
@@ -43,7 +43,7 @@ function Set-CodexConfig {
|
||||
|
||||
$content = Get-Content $path -Raw
|
||||
$content = [regex]::Replace($content, "(?ms)^\[mcp_servers\.(sequential-thinking|sequential_thinking)\].*?(?=^\[|\z)", "")
|
||||
$content = $content.TrimEnd() + "`n`n[mcp_servers.sequential-thinking]`ncommand = `"npx`"`nargs = [`"-y`", `"@modelcontextprotocol/server-sequential-thinking`"]`n"
|
||||
$content = $content.TrimEnd() + "`n`n[mcp_servers.sequential-thinking]`ncommand = \"npx\"`nargs = [\"-y\", \"@modelcontextprotocol/server-sequential-thinking\"]`n"
|
||||
Set-Content -Path $path -Value $content -Encoding UTF8
|
||||
}
|
||||
|
||||
|
||||
114
bin/mosaic.ps1
114
bin/mosaic.ps1
@@ -96,88 +96,6 @@ function Assert-SequentialThinking {
|
||||
}
|
||||
}
|
||||
|
||||
function Get-ActiveMission {
|
||||
$missionFile = Join-Path (Get-Location) ".mosaic\orchestrator\mission.json"
|
||||
if (-not (Test-Path $missionFile)) {
|
||||
return $null
|
||||
}
|
||||
|
||||
try {
|
||||
$mission = Get-Content $missionFile -Raw | ConvertFrom-Json
|
||||
}
|
||||
catch {
|
||||
return $null
|
||||
}
|
||||
|
||||
$status = [string]$mission.status
|
||||
if ([string]::IsNullOrWhiteSpace($status)) {
|
||||
$status = "inactive"
|
||||
}
|
||||
if ($status -ne "active" -and $status -ne "paused") {
|
||||
return $null
|
||||
}
|
||||
|
||||
$name = [string]$mission.name
|
||||
if ([string]::IsNullOrWhiteSpace($name)) {
|
||||
$name = "unnamed"
|
||||
}
|
||||
|
||||
$id = [string]$mission.mission_id
|
||||
if ([string]::IsNullOrWhiteSpace($id)) {
|
||||
$id = ""
|
||||
}
|
||||
|
||||
$milestones = @($mission.milestones)
|
||||
$milestoneCount = $milestones.Count
|
||||
$milestoneCompleted = @($milestones | Where-Object { $_.status -eq "completed" }).Count
|
||||
|
||||
return [PSCustomObject]@{
|
||||
Name = $name
|
||||
Id = $id
|
||||
Status = $status
|
||||
MilestoneCount = $milestoneCount
|
||||
MilestoneCompleted = $milestoneCompleted
|
||||
}
|
||||
}
|
||||
|
||||
function Get-MissionContractBlock {
|
||||
$mission = Get-ActiveMission
|
||||
if ($null -eq $mission) {
|
||||
return ""
|
||||
}
|
||||
|
||||
return @"
|
||||
# ACTIVE MISSION — HARD GATE (Read Before Anything Else)
|
||||
|
||||
An active orchestration mission exists in this project. This is a BLOCKING requirement.
|
||||
|
||||
**Mission:** $($mission.Name)
|
||||
**ID:** $($mission.Id)
|
||||
**Status:** $($mission.Status)
|
||||
**Milestones:** $($mission.MilestoneCompleted) / $($mission.MilestoneCount) completed
|
||||
|
||||
## MANDATORY — Before ANY Response to the User
|
||||
|
||||
You MUST complete these steps before responding to any user message, including simple greetings:
|
||||
|
||||
1. Read `~/.config/mosaic/guides/ORCHESTRATOR-PROTOCOL.md` (mission lifecycle protocol)
|
||||
2. Read `docs/MISSION-MANIFEST.md` for full mission scope, milestones, and success criteria
|
||||
3. Read the latest scratchpad in `docs/scratchpads/` for session history, decisions, and corrections
|
||||
4. Read `docs/TASKS.md` for current task state (what is done, what is next)
|
||||
5. After reading all four, acknowledge the mission state to the user before proceeding
|
||||
|
||||
If the user gives a task, execute it within the mission context. If no task is given, present mission status and ask how to proceed.
|
||||
"@
|
||||
}
|
||||
|
||||
function Get-MissionPrompt {
|
||||
$mission = Get-ActiveMission
|
||||
if ($null -eq $mission) {
|
||||
return ""
|
||||
}
|
||||
return "Active mission detected: $($mission.Name). Read the mission state files and report status."
|
||||
}
|
||||
|
||||
function Get-RuntimePrompt {
|
||||
param(
|
||||
[ValidateSet("claude", "codex", "opencode")]
|
||||
@@ -212,14 +130,8 @@ For required push/merge/issue-close/release actions, execute without routine con
|
||||
|
||||
'@
|
||||
|
||||
$missionBlock = Get-MissionContractBlock
|
||||
$agentsContent = Get-Content (Join-Path $MosaicHome "AGENTS.md") -Raw
|
||||
$runtimeContent = Get-Content $runtimeFile -Raw
|
||||
|
||||
if (-not [string]::IsNullOrWhiteSpace($missionBlock)) {
|
||||
return "$missionBlock`n`n$launcherContract`n$agentsContent`n`n# Runtime-Specific Contract`n`n$runtimeContent"
|
||||
}
|
||||
|
||||
return "$launcherContract`n$agentsContent`n`n# Runtime-Specific Contract`n`n$runtimeContent"
|
||||
}
|
||||
|
||||
@@ -258,7 +170,7 @@ function Invoke-Yolo {
|
||||
}
|
||||
|
||||
$runtime = $YoloArgs[0]
|
||||
$tail = if ($YoloArgs.Count -gt 1) { @($YoloArgs[1..($YoloArgs.Count - 1)]) } else { @() }
|
||||
$tail = if ($YoloArgs.Count -gt 1) { $YoloArgs[1..($YoloArgs.Count - 1)] } else { @() }
|
||||
|
||||
switch ($runtime) {
|
||||
"claude" {
|
||||
@@ -279,15 +191,8 @@ function Invoke-Yolo {
|
||||
Assert-Runtime "codex"
|
||||
Assert-SequentialThinking
|
||||
Ensure-RuntimeConfig -Runtime "codex" -Dst (Join-Path $env:USERPROFILE ".codex\instructions.md")
|
||||
$missionPrompt = Get-MissionPrompt
|
||||
if (-not [string]::IsNullOrWhiteSpace($missionPrompt) -and $tail.Count -eq 0) {
|
||||
Write-Host "[mosaic] Launching Codex in YOLO mode (active mission detected)..."
|
||||
& codex --dangerously-bypass-approvals-and-sandbox $missionPrompt
|
||||
}
|
||||
else {
|
||||
Write-Host "[mosaic] Launching Codex in YOLO mode (dangerous permissions enabled)..."
|
||||
& codex --dangerously-bypass-approvals-and-sandbox @tail
|
||||
}
|
||||
Write-Host "[mosaic] Launching Codex in YOLO mode (dangerous permissions enabled)..."
|
||||
& codex --dangerously-bypass-approvals-and-sandbox @tail
|
||||
return
|
||||
}
|
||||
"opencode" {
|
||||
@@ -314,7 +219,7 @@ if ($args.Count -eq 0) {
|
||||
}
|
||||
|
||||
$command = $args[0]
|
||||
$remaining = if ($args.Count -gt 1) { @($args[1..($args.Count - 1)]) } else { @() }
|
||||
$remaining = if ($args.Count -gt 1) { $args[1..($args.Count - 1)] } else { @() }
|
||||
|
||||
switch ($command) {
|
||||
"claude" {
|
||||
@@ -347,15 +252,8 @@ switch ($command) {
|
||||
Assert-SequentialThinking
|
||||
# Codex reads from ~/.codex/instructions.md
|
||||
Ensure-RuntimeConfig -Runtime "codex" -Dst (Join-Path $env:USERPROFILE ".codex\instructions.md")
|
||||
$missionPrompt = Get-MissionPrompt
|
||||
if (-not [string]::IsNullOrWhiteSpace($missionPrompt) -and $remaining.Count -eq 0) {
|
||||
Write-Host "[mosaic] Launching Codex (active mission detected)..."
|
||||
& codex $missionPrompt
|
||||
}
|
||||
else {
|
||||
Write-Host "[mosaic] Launching Codex..."
|
||||
& codex @remaining
|
||||
}
|
||||
Write-Host "[mosaic] Launching Codex..."
|
||||
& codex @remaining
|
||||
}
|
||||
"yolo" {
|
||||
Invoke-Yolo -YoloArgs $remaining
|
||||
|
||||
@@ -153,75 +153,6 @@ The human is escalation-only for missing access, hard policy conflicts, or irrev
|
||||
- Magic variables (`SERVICE_FQDN_*`) require list-style env syntax, not dict-style
|
||||
- Rate limit: 200 requests per interval
|
||||
|
||||
### Cloudflare DNS Operations
|
||||
|
||||
Use the Cloudflare tools for any DNS configuration: pointing domains at services, adding TXT verification records, managing MX records, etc.
|
||||
|
||||
**Multi-instance support**: Credentials support named instances (e.g. `personal`, `work`). A `default` key in credentials.json determines which instance is used when `-a` is omitted. Pass `-a <instance>` to target a specific account.
|
||||
|
||||
```bash
|
||||
# List all zones (domains) in the account
|
||||
~/.config/mosaic/tools/cloudflare/zone-list.sh [-a instance]
|
||||
|
||||
# List DNS records for a zone (accepts zone name or ID)
|
||||
~/.config/mosaic/tools/cloudflare/record-list.sh -z <zone> [-t type] [-n name]
|
||||
|
||||
# Create a DNS record
|
||||
~/.config/mosaic/tools/cloudflare/record-create.sh -z <zone> -t <type> -n <name> -c <content> [-p] [-l ttl] [-P priority]
|
||||
|
||||
# Update a DNS record (requires record ID from record-list)
|
||||
~/.config/mosaic/tools/cloudflare/record-update.sh -z <zone> -r <record-id> -t <type> -n <name> -c <content> [-p]
|
||||
|
||||
# Delete a DNS record
|
||||
~/.config/mosaic/tools/cloudflare/record-delete.sh -z <zone> -r <record-id>
|
||||
```
|
||||
|
||||
**Flag reference:**
|
||||
|
||||
| Flag | Purpose |
|
||||
|------|---------|
|
||||
| `-z` | Zone name (e.g. `mosaicstack.dev`) or 32-char zone ID |
|
||||
| `-a` | Named Cloudflare instance (omit for default) |
|
||||
| `-t` | Record type: `A`, `AAAA`, `CNAME`, `MX`, `TXT`, `SRV`, etc. |
|
||||
| `-n` | Record name: short (`app`) or FQDN (`app.example.com`) |
|
||||
| `-c` | Record content/value (IP, hostname, TXT string, etc.) |
|
||||
| `-r` | Record ID (from `record-list.sh` output) |
|
||||
| `-p` | Enable Cloudflare proxy (orange cloud) — omit for DNS-only (grey cloud) |
|
||||
| `-l` | TTL in seconds (default: `1` = auto) |
|
||||
| `-P` | Priority for MX/SRV records |
|
||||
| `-f` | Output format: `table` (default) or `json` |
|
||||
|
||||
**Common workflows:**
|
||||
|
||||
```bash
|
||||
# Point a new subdomain at a server (proxied through Cloudflare)
|
||||
~/.config/mosaic/tools/cloudflare/record-create.sh \
|
||||
-z example.com -t A -n myapp -c 203.0.113.10 -p
|
||||
|
||||
# Add a TXT record for domain verification (never proxied)
|
||||
~/.config/mosaic/tools/cloudflare/record-create.sh \
|
||||
-z example.com -t TXT -n _verify -c "verification=abc123"
|
||||
|
||||
# Check what records exist before making changes
|
||||
~/.config/mosaic/tools/cloudflare/record-list.sh -z example.com -t CNAME
|
||||
|
||||
# Update an existing record (get record ID from record-list first)
|
||||
~/.config/mosaic/tools/cloudflare/record-update.sh \
|
||||
-z example.com -r <record-id> -t A -n myapp -c 10.0.0.5 -p
|
||||
```
|
||||
|
||||
**DNS + Deployment integration**: When deploying a new service via Coolify or Portainer that needs a public domain, the typical sequence is:
|
||||
|
||||
1. Create the DNS record pointing at the host IP (with `-p` for Cloudflare proxy if desired)
|
||||
2. Deploy the service via Coolify/Portainer
|
||||
3. Verify the domain resolves and the service is reachable
|
||||
|
||||
**Proxy (`-p`) guidance:**
|
||||
|
||||
- Use proxy (orange cloud) for web services — provides CDN, DDoS protection, and hides origin IP
|
||||
- Skip proxy (grey cloud) for non-HTTP services (mail, SSH), wildcard records, or when the service handles its own TLS termination and needs direct client IP visibility
|
||||
- Proxy is NOT compatible with non-standard ports outside Cloudflare's supported range
|
||||
|
||||
### Stack Health Check
|
||||
|
||||
Verify all infrastructure services are reachable:
|
||||
|
||||
@@ -1,268 +0,0 @@
|
||||
# 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 and Capsule Format
|
||||
|
||||
The coordinator generates this (via `mosaic coord continue`) and writes a machine-readable capsule at `.mosaic/orchestrator/next-task.json`:
|
||||
|
||||
```
|
||||
## 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
|
||||
|
||||
### Between Sessions (r0 assisted)
|
||||
|
||||
Use `mosaic coord run` to remove copy/paste steps:
|
||||
|
||||
1. Agent stops
|
||||
2. Human runs `mosaic coord run [--claude|--codex]`
|
||||
3. Coordinator regenerates continuation prompt + `next-task.json`
|
||||
4. Coordinator launches selected runtime with scoped kickoff context
|
||||
5. New session resumes from next task
|
||||
|
||||
---
|
||||
|
||||
## 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 run [--claude|--codex]` | Generate continuation context and launch runtime |
|
||||
| `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 → run → 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)
|
||||
@@ -34,19 +34,16 @@ First response MUST declare mode before tool calls or implementation steps:
|
||||
|
||||
## 2. Intake and Scope
|
||||
|
||||
> **COMPLEXITY TRAP WARNING:** Intake applies to ALL tasks regardless of perceived complexity. "Simple" tasks (commit, push, deploy) have caused the most severe framework violations because agents skip intake when they pattern-match a task as mechanical. The procedure is unconditional.
|
||||
|
||||
1. Define scope, constraints, and acceptance criteria.
|
||||
2. Identify affected surfaces (API, DB, UI, infra, auth, CI/CD, docs).
|
||||
3. **Deployment surface check (MANDATORY if task involves deploy, images, or containers):** Before ANY build or deploy action, check for CI/CD pipeline config (`.woodpecker/`, `.woodpecker.yml`, `.github/workflows/`). If pipelines exist, CI is the canonical build path — manual `docker build`/`docker push` is forbidden. Load `~/.config/mosaic/guides/CI-CD-PIPELINES.md` immediately.
|
||||
4. Identify required guides and load them before implementation.
|
||||
5. For code/API/auth/infra changes, load `~/.config/mosaic/guides/DOCUMENTATION.md`.
|
||||
6. Determine budget constraints:
|
||||
3. Identify required guides and load them before implementation.
|
||||
4. For code/API/auth/infra changes, load `~/.config/mosaic/guides/DOCUMENTATION.md`.
|
||||
5. Determine budget constraints:
|
||||
- if the user provided a plan limit or token budget, treat it as a HARD cap,
|
||||
- if budget is unknown, derive a working budget from estimates and runtime limits, then continue autonomously.
|
||||
7. Record budget assumptions and caps in the scratchpad before implementation starts.
|
||||
8. Track estimated vs used tokens per logical unit and adapt strategy to remain inside budget.
|
||||
9. If projected usage exceeds budget, auto-reduce scope/parallelism first; escalate only if cap still cannot be met.
|
||||
6. Record budget assumptions and caps in the scratchpad before implementation starts.
|
||||
7. Track estimated vs used tokens per logical unit and adapt strategy to remain inside budget.
|
||||
8. If projected usage exceeds budget, auto-reduce scope/parallelism first; escalate only if cap still cannot be met.
|
||||
|
||||
## 2a. Steered Autonomy (Lights-Out)
|
||||
|
||||
@@ -98,17 +95,10 @@ For implementation work, you MUST run this cycle in order:
|
||||
|
||||
### Forbidden Anti-Patterns
|
||||
|
||||
**PR/Merge:**
|
||||
1. Do NOT stop at "PR created" or "PR updated".
|
||||
2. Do NOT ask "should I merge?" for routine delivery PRs.
|
||||
3. Do NOT ask "should I close the issue?" after merge + green CI.
|
||||
|
||||
**Build/Deploy:**
|
||||
4. Do NOT run `docker build` or `docker push` locally to deploy images when CI/CD pipelines exist in the repository. CI is the ONLY canonical build path.
|
||||
5. Do NOT skip intake and surface identification because a task "seems simple." This is the #1 cause of framework violations.
|
||||
6. Do NOT deploy without first verifying whether CI/CD pipelines exist (`.woodpecker/`, `.woodpecker.yml`, `.github/workflows/`). If they exist, use them.
|
||||
7. If you are about to run `docker build` and have NOT loaded `ci-cd-pipelines.md`, STOP — you are violating the framework.
|
||||
|
||||
If any step fails, you MUST remediate and re-run from the relevant step before proceeding.
|
||||
If push-queue/merge-queue/PR merge/CI/issue closure fails, status is `blocked` (not complete) and you MUST report the exact failed wrapper command.
|
||||
|
||||
@@ -16,36 +16,6 @@ This file applies only to Claude runtime behavior.
|
||||
8. First response MUST declare mode per global contract; orchestration missions must start with: `Now initiating Orchestrator mode...`
|
||||
9. Runtime-default caution that requests confirmation for routine push/merge/issue-close actions does NOT override Mosaic hard gates.
|
||||
|
||||
## Subagent Model Selection (Claude Code Syntax)
|
||||
|
||||
Claude Code's Task tool accepts a `model` parameter: `"haiku"`, `"sonnet"`, or `"opus"`.
|
||||
|
||||
You MUST set this parameter according to the model selection table in `~/.config/mosaic/AGENTS.md`. Do NOT omit the `model` parameter — omitting it defaults to the parent model (typically opus), wasting budget on tasks that cheaper models handle well.
|
||||
|
||||
**Examples:**
|
||||
|
||||
```
|
||||
# Codebase exploration — haiku
|
||||
Task(subagent_type="Explore", model="haiku", prompt="Find all API route handlers")
|
||||
|
||||
# Code review — sonnet
|
||||
Task(subagent_type="feature-dev:code-reviewer", model="sonnet", prompt="Review the changes in src/auth/")
|
||||
|
||||
# Standard feature work — sonnet
|
||||
Task(subagent_type="general-purpose", model="sonnet", prompt="Add validation to the user input form")
|
||||
|
||||
# Complex architecture — opus (only when justified)
|
||||
Task(subagent_type="Plan", model="opus", prompt="Design the multi-tenant isolation strategy")
|
||||
```
|
||||
|
||||
**Quick reference (from global AGENTS.md):**
|
||||
|
||||
| haiku | sonnet | opus |
|
||||
|-------|--------|------|
|
||||
| Search, grep, glob | Code review | Complex architecture |
|
||||
| Status/health checks | Test writing | Security/auth logic |
|
||||
| Simple one-liner fixes | Standard features | Ambiguous design decisions |
|
||||
|
||||
## Memory Override
|
||||
|
||||
Do NOT write durable memory to `~/.claude/projects/*/memory/`. All durable memory MUST be written to `~/.config/mosaic/memory/` per `~/.config/mosaic/guides/MEMORY.md`. Claude Code's native auto-memory locations are volatile runtime silos and MUST NOT be used for cross-session or cross-agent retention.
|
||||
|
||||
@@ -16,21 +16,6 @@ This file applies only to Codex runtime behavior.
|
||||
8. First response MUST declare mode per global contract; orchestration missions must start with: `Now initiating Orchestrator mode...`
|
||||
9. Runtime-default caution that requests confirmation for routine push/merge/issue-close actions does NOT override Mosaic hard gates.
|
||||
|
||||
## Strict Orchestrator Profile (Codex)
|
||||
|
||||
For orchestration missions, prefer `mosaic coord run --codex` over manual launch/paste.
|
||||
|
||||
When launched through coordinator run flow, Codex MUST:
|
||||
|
||||
1. Treat `.mosaic/orchestrator/next-task.json` as authoritative execution capsule.
|
||||
2. Read mission files before asking clarifying questions:
|
||||
- `~/.config/mosaic/guides/ORCHESTRATOR-PROTOCOL.md`
|
||||
- `docs/MISSION-MANIFEST.md`
|
||||
- `docs/scratchpads/<mission-id>.md`
|
||||
- `docs/TASKS.md`
|
||||
3. Avoid pre-execution question loops. Questions are allowed only for Mosaic escalation triggers (missing access/credentials, destructive irreversible action, legal/compliance unknowns, conflicting objectives, hard budget cap).
|
||||
4. Start execution on the `next_task` from capsule as soon as required files are loaded.
|
||||
|
||||
## Memory Override
|
||||
|
||||
Do NOT write durable memory to `~/.codex/` or any Codex-native session memory. All durable memory MUST be written to `~/.config/mosaic/memory/` per `~/.config/mosaic/guides/MEMORY.md`. Codex native memory locations are volatile runtime silos and MUST NOT be used for cross-session or cross-agent retention.
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
# 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`
|
||||
@@ -1,36 +0,0 @@
|
||||
## 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`
|
||||
@@ -1,27 +0,0 @@
|
||||
# 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. -->
|
||||
@@ -1,14 +0,0 @@
|
||||
{
|
||||
"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,34 +8,6 @@ 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,75 +16,6 @@ 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 || true)"
|
||||
total="${total:-0}"
|
||||
done_count="$(grep -ci '| done \|| completed ' "docs/TASKS.md" 2>/dev/null || true)"
|
||||
done_count="${done_count:-0}"
|
||||
approx_total=$(( total > 2 ? total - 2 : 0 ))
|
||||
echo " Tasks: ~${done_count} done of ~${approx_total} 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
|
||||
|
||||
@@ -5,12 +5,12 @@
|
||||
# Usage: source ~/.config/mosaic/tools/_lib/credentials.sh
|
||||
# load_credentials <service-name>
|
||||
#
|
||||
# credentials.json is the single source of truth.
|
||||
# For Woodpecker, credentials are also synced to ~/.woodpecker/<instance>.env.
|
||||
# Loads credentials from environment variables first, then falls back
|
||||
# to ~/src/jarvis-brain/credentials.json (or MOSAIC_CREDENTIALS_FILE).
|
||||
#
|
||||
# Supported services:
|
||||
# portainer, coolify, authentik, glpi, github,
|
||||
# gitea-mosaicstack, gitea-usc, woodpecker, cloudflare
|
||||
# gitea-mosaicstack, gitea-usc, woodpecker
|
||||
#
|
||||
# After loading, service-specific env vars are exported.
|
||||
# Run `load_credentials --help` for details.
|
||||
@@ -33,24 +33,6 @@ _mosaic_read_cred() {
|
||||
jq -r "$jq_path // empty" "$MOSAIC_CREDENTIALS_FILE"
|
||||
}
|
||||
|
||||
# Sync Woodpecker credentials to ~/.woodpecker/<instance>.env
|
||||
# Only writes when values differ to avoid unnecessary disk writes.
|
||||
_mosaic_sync_woodpecker_env() {
|
||||
local instance="$1" url="$2" token="$3"
|
||||
local env_file="$HOME/.woodpecker/${instance}.env"
|
||||
[[ -d "$HOME/.woodpecker" ]] || return 0
|
||||
local expected
|
||||
expected=$(printf '# %s Woodpecker CI\nexport WOODPECKER_SERVER="%s"\nexport WOODPECKER_TOKEN="%s"\n' \
|
||||
"$instance" "$url" "$token")
|
||||
if [[ -f "$env_file" ]]; then
|
||||
local current_url current_token
|
||||
current_url=$(grep -oP '(?<=WOODPECKER_SERVER=").*(?=")' "$env_file" 2>/dev/null || true)
|
||||
current_token=$(grep -oP '(?<=WOODPECKER_TOKEN=").*(?=")' "$env_file" 2>/dev/null || true)
|
||||
[[ "$current_url" == "$url" && "$current_token" == "$token" ]] && return 0
|
||||
fi
|
||||
printf '%s\n' "$expected" > "$env_file"
|
||||
}
|
||||
|
||||
load_credentials() {
|
||||
local service="$1"
|
||||
|
||||
@@ -61,16 +43,12 @@ Usage: load_credentials <service>
|
||||
Services and exported variables:
|
||||
portainer → PORTAINER_URL, PORTAINER_API_KEY
|
||||
coolify → COOLIFY_URL, COOLIFY_TOKEN
|
||||
authentik → AUTHENTIK_URL, AUTHENTIK_TOKEN, AUTHENTIK_TEST_USER, AUTHENTIK_TEST_PASSWORD (uses default instance)
|
||||
authentik-<name> → AUTHENTIK_URL, AUTHENTIK_TOKEN, AUTHENTIK_TEST_USER, AUTHENTIK_TEST_PASSWORD (specific instance, e.g. authentik-usc)
|
||||
authentik → AUTHENTIK_URL, AUTHENTIK_TOKEN, AUTHENTIK_USERNAME, AUTHENTIK_PASSWORD
|
||||
glpi → GLPI_URL, GLPI_APP_TOKEN, GLPI_USER_TOKEN
|
||||
github → GITHUB_TOKEN
|
||||
gitea-mosaicstack → GITEA_URL, GITEA_TOKEN
|
||||
gitea-usc → GITEA_URL, GITEA_TOKEN
|
||||
woodpecker → WOODPECKER_URL, WOODPECKER_TOKEN (uses default instance)
|
||||
woodpecker-<name> → WOODPECKER_URL, WOODPECKER_TOKEN (specific instance, e.g. woodpecker-usc)
|
||||
cloudflare → CLOUDFLARE_API_TOKEN (uses default instance)
|
||||
cloudflare-<name> → CLOUDFLARE_API_TOKEN (specific instance, e.g. cloudflare-personal)
|
||||
woodpecker → WOODPECKER_URL, WOODPECKER_TOKEN
|
||||
EOF
|
||||
return 0
|
||||
fi
|
||||
@@ -92,38 +70,13 @@ EOF
|
||||
[[ -n "$COOLIFY_URL" ]] || { echo "Error: coolify.url not found" >&2; return 1; }
|
||||
[[ -n "$COOLIFY_TOKEN" ]] || { echo "Error: coolify.app_token not found" >&2; return 1; }
|
||||
;;
|
||||
authentik-*)
|
||||
local ak_instance="${service#authentik-}"
|
||||
export AUTHENTIK_URL="$(_mosaic_read_cred ".authentik.${ak_instance}.url")"
|
||||
export AUTHENTIK_TOKEN="$(_mosaic_read_cred ".authentik.${ak_instance}.token")"
|
||||
export AUTHENTIK_TEST_USER="$(_mosaic_read_cred ".authentik.${ak_instance}.test_user.username")"
|
||||
export AUTHENTIK_TEST_PASSWORD="$(_mosaic_read_cred ".authentik.${ak_instance}.test_user.password")"
|
||||
export AUTHENTIK_INSTANCE="$ak_instance"
|
||||
AUTHENTIK_URL="${AUTHENTIK_URL%/}"
|
||||
[[ -n "$AUTHENTIK_URL" ]] || { echo "Error: authentik.${ak_instance}.url not found" >&2; return 1; }
|
||||
;;
|
||||
authentik)
|
||||
local ak_default
|
||||
ak_default="${AUTHENTIK_INSTANCE:-$(_mosaic_read_cred '.authentik.default')}"
|
||||
if [[ -z "$ak_default" ]]; then
|
||||
# Fallback: try legacy flat structure (.authentik.url)
|
||||
local legacy_url
|
||||
legacy_url="$(_mosaic_read_cred '.authentik.url')"
|
||||
if [[ -n "$legacy_url" ]]; then
|
||||
export AUTHENTIK_URL="${AUTHENTIK_URL:-$legacy_url}"
|
||||
export AUTHENTIK_TOKEN="${AUTHENTIK_TOKEN:-$(_mosaic_read_cred '.authentik.token')}"
|
||||
export AUTHENTIK_TEST_USER="${AUTHENTIK_TEST_USER:-$(_mosaic_read_cred '.authentik.test_user.username')}"
|
||||
export AUTHENTIK_TEST_PASSWORD="${AUTHENTIK_TEST_PASSWORD:-$(_mosaic_read_cred '.authentik.test_user.password')}"
|
||||
AUTHENTIK_URL="${AUTHENTIK_URL%/}"
|
||||
[[ -n "$AUTHENTIK_URL" ]] || { echo "Error: authentik.url not found" >&2; return 1; }
|
||||
else
|
||||
echo "Error: authentik.default not set and no AUTHENTIK_INSTANCE env var" >&2
|
||||
echo "Available instances: $(jq -r '.authentik | keys | join(", ")' "$MOSAIC_CREDENTIALS_FILE" 2>/dev/null)" >&2
|
||||
return 1
|
||||
fi
|
||||
else
|
||||
load_credentials "authentik-${ak_default}"
|
||||
fi
|
||||
export AUTHENTIK_URL="${AUTHENTIK_URL:-$(_mosaic_read_cred '.authentik.url')}"
|
||||
export AUTHENTIK_TOKEN="${AUTHENTIK_TOKEN:-$(_mosaic_read_cred '.authentik.token')}"
|
||||
export AUTHENTIK_USERNAME="${AUTHENTIK_USERNAME:-$(_mosaic_read_cred '.authentik.username')}"
|
||||
export AUTHENTIK_PASSWORD="${AUTHENTIK_PASSWORD:-$(_mosaic_read_cred '.authentik.password')}"
|
||||
AUTHENTIK_URL="${AUTHENTIK_URL%/}"
|
||||
[[ -n "$AUTHENTIK_URL" ]] || { echo "Error: authentik.url not found" >&2; return 1; }
|
||||
;;
|
||||
glpi)
|
||||
export GLPI_URL="${GLPI_URL:-$(_mosaic_read_cred '.glpi.url')}"
|
||||
@@ -150,60 +103,16 @@ EOF
|
||||
[[ -n "$GITEA_URL" ]] || { echo "Error: gitea.usc.url not found" >&2; return 1; }
|
||||
[[ -n "$GITEA_TOKEN" ]] || { echo "Error: gitea.usc.token not found" >&2; return 1; }
|
||||
;;
|
||||
woodpecker-*)
|
||||
local wp_instance="${service#woodpecker-}"
|
||||
# credentials.json is authoritative — always read from it, ignore env
|
||||
export WOODPECKER_URL="$(_mosaic_read_cred ".woodpecker.${wp_instance}.url")"
|
||||
export WOODPECKER_TOKEN="$(_mosaic_read_cred ".woodpecker.${wp_instance}.token")"
|
||||
export WOODPECKER_INSTANCE="$wp_instance"
|
||||
WOODPECKER_URL="${WOODPECKER_URL%/}"
|
||||
[[ -n "$WOODPECKER_URL" ]] || { echo "Error: woodpecker.${wp_instance}.url not found" >&2; return 1; }
|
||||
[[ -n "$WOODPECKER_TOKEN" ]] || { echo "Error: woodpecker.${wp_instance}.token not found" >&2; return 1; }
|
||||
# Sync to ~/.woodpecker/<instance>.env so the wp CLI wrapper stays current
|
||||
_mosaic_sync_woodpecker_env "$wp_instance" "$WOODPECKER_URL" "$WOODPECKER_TOKEN"
|
||||
;;
|
||||
woodpecker)
|
||||
# Resolve default instance, then load it
|
||||
local wp_default
|
||||
wp_default="${WOODPECKER_INSTANCE:-$(_mosaic_read_cred '.woodpecker.default')}"
|
||||
if [[ -z "$wp_default" ]]; then
|
||||
# Fallback: try legacy flat structure (.woodpecker.url / .woodpecker.token)
|
||||
local legacy_url
|
||||
legacy_url="$(_mosaic_read_cred '.woodpecker.url')"
|
||||
if [[ -n "$legacy_url" ]]; then
|
||||
export WOODPECKER_URL="${WOODPECKER_URL:-$legacy_url}"
|
||||
export WOODPECKER_TOKEN="${WOODPECKER_TOKEN:-$(_mosaic_read_cred '.woodpecker.token')}"
|
||||
WOODPECKER_URL="${WOODPECKER_URL%/}"
|
||||
[[ -n "$WOODPECKER_URL" ]] || { echo "Error: woodpecker.url not found" >&2; return 1; }
|
||||
[[ -n "$WOODPECKER_TOKEN" ]] || { echo "Error: woodpecker.token not found" >&2; return 1; }
|
||||
else
|
||||
echo "Error: woodpecker.default not set and no WOODPECKER_INSTANCE env var" >&2
|
||||
echo "Available instances: $(jq -r '.woodpecker | keys | join(", ")' "$MOSAIC_CREDENTIALS_FILE" 2>/dev/null)" >&2
|
||||
return 1
|
||||
fi
|
||||
else
|
||||
load_credentials "woodpecker-${wp_default}"
|
||||
fi
|
||||
;;
|
||||
cloudflare-*)
|
||||
local cf_instance="${service#cloudflare-}"
|
||||
export CLOUDFLARE_API_TOKEN="${CLOUDFLARE_API_TOKEN:-$(_mosaic_read_cred ".cloudflare.${cf_instance}.api_token")}"
|
||||
export CLOUDFLARE_INSTANCE="$cf_instance"
|
||||
[[ -n "$CLOUDFLARE_API_TOKEN" ]] || { echo "Error: cloudflare.${cf_instance}.api_token not found" >&2; return 1; }
|
||||
;;
|
||||
cloudflare)
|
||||
# Resolve default instance, then load it
|
||||
local cf_default
|
||||
cf_default="${CLOUDFLARE_INSTANCE:-$(_mosaic_read_cred '.cloudflare.default')}"
|
||||
if [[ -z "$cf_default" ]]; then
|
||||
echo "Error: cloudflare.default not set and no CLOUDFLARE_INSTANCE env var" >&2
|
||||
return 1
|
||||
fi
|
||||
load_credentials "cloudflare-${cf_default}"
|
||||
export WOODPECKER_URL="${WOODPECKER_URL:-$(_mosaic_read_cred '.woodpecker.url')}"
|
||||
export WOODPECKER_TOKEN="${WOODPECKER_TOKEN:-$(_mosaic_read_cred '.woodpecker.token')}"
|
||||
WOODPECKER_URL="${WOODPECKER_URL%/}"
|
||||
[[ -n "$WOODPECKER_URL" ]] || { echo "Error: woodpecker.url not found" >&2; return 1; }
|
||||
[[ -n "$WOODPECKER_TOKEN" ]] || { echo "Error: woodpecker.token not found" >&2; return 1; }
|
||||
;;
|
||||
*)
|
||||
echo "Error: Unknown service '$service'" >&2
|
||||
echo "Supported: portainer, coolify, authentik[-<name>], glpi, github, gitea-mosaicstack, gitea-usc, woodpecker[-<name>], cloudflare[-<name>]" >&2
|
||||
echo "Supported: portainer, coolify, authentik, glpi, github, gitea-mosaicstack, gitea-usc, woodpecker" >&2
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
|
||||
@@ -2,37 +2,29 @@
|
||||
#
|
||||
# admin-status.sh — Authentik system health and version info
|
||||
#
|
||||
# Usage: admin-status.sh [-f format] [-a instance]
|
||||
# Usage: admin-status.sh [-f format]
|
||||
#
|
||||
# Options:
|
||||
# -f format Output format: table (default), json
|
||||
# -a instance Authentik instance name (e.g. usc, mosaic)
|
||||
# -h Show this help
|
||||
# -f format Output format: table (default), json
|
||||
# -h Show this help
|
||||
set -euo pipefail
|
||||
|
||||
MOSAIC_HOME="${MOSAIC_HOME:-$HOME/.config/mosaic}"
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
source "$MOSAIC_HOME/tools/_lib/credentials.sh"
|
||||
load_credentials authentik
|
||||
|
||||
FORMAT="table"
|
||||
AK_INSTANCE=""
|
||||
|
||||
while getopts "f:a:h" opt; do
|
||||
while getopts "f:h" opt; do
|
||||
case $opt in
|
||||
f) FORMAT="$OPTARG" ;;
|
||||
a) AK_INSTANCE="$OPTARG" ;;
|
||||
h) head -13 "$0" | grep "^#" | sed 's/^# \?//'; exit 0 ;;
|
||||
*) echo "Usage: $0 [-f format] [-a instance]" >&2; exit 1 ;;
|
||||
h) head -11 "$0" | grep "^#" | sed 's/^# \?//'; exit 0 ;;
|
||||
*) echo "Usage: $0 [-f format]" >&2; exit 1 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ -n "$AK_INSTANCE" ]]; then
|
||||
load_credentials "authentik-${AK_INSTANCE}"
|
||||
else
|
||||
load_credentials authentik
|
||||
fi
|
||||
|
||||
TOKEN=$("$SCRIPT_DIR/auth-token.sh" -q ${AK_INSTANCE:+-a "$AK_INSTANCE"})
|
||||
TOKEN=$("$SCRIPT_DIR/auth-token.sh" -q)
|
||||
|
||||
response=$(curl -sk -w "\n%{http_code}" \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
|
||||
@@ -2,40 +2,32 @@
|
||||
#
|
||||
# app-list.sh — List Authentik applications
|
||||
#
|
||||
# Usage: app-list.sh [-f format] [-s search] [-a instance]
|
||||
# Usage: app-list.sh [-f format] [-s search]
|
||||
#
|
||||
# Options:
|
||||
# -f format Output format: table (default), json
|
||||
# -s search Search by application name
|
||||
# -a instance Authentik instance name (e.g. usc, mosaic)
|
||||
# -h Show this help
|
||||
# -f format Output format: table (default), json
|
||||
# -s search Search by application name
|
||||
# -h Show this help
|
||||
set -euo pipefail
|
||||
|
||||
MOSAIC_HOME="${MOSAIC_HOME:-$HOME/.config/mosaic}"
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
source "$MOSAIC_HOME/tools/_lib/credentials.sh"
|
||||
load_credentials authentik
|
||||
|
||||
FORMAT="table"
|
||||
SEARCH=""
|
||||
AK_INSTANCE=""
|
||||
|
||||
while getopts "f:s:a:h" opt; do
|
||||
while getopts "f:s:h" opt; do
|
||||
case $opt in
|
||||
f) FORMAT="$OPTARG" ;;
|
||||
s) SEARCH="$OPTARG" ;;
|
||||
a) AK_INSTANCE="$OPTARG" ;;
|
||||
h) head -14 "$0" | grep "^#" | sed 's/^# \?//'; exit 0 ;;
|
||||
*) echo "Usage: $0 [-f format] [-s search] [-a instance]" >&2; exit 1 ;;
|
||||
h) head -12 "$0" | grep "^#" | sed 's/^# \?//'; exit 0 ;;
|
||||
*) echo "Usage: $0 [-f format] [-s search]" >&2; exit 1 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ -n "$AK_INSTANCE" ]]; then
|
||||
load_credentials "authentik-${AK_INSTANCE}"
|
||||
else
|
||||
load_credentials authentik
|
||||
fi
|
||||
|
||||
TOKEN=$("$SCRIPT_DIR/auth-token.sh" -q ${AK_INSTANCE:+-a "$AK_INSTANCE"})
|
||||
TOKEN=$("$SCRIPT_DIR/auth-token.sh" -q)
|
||||
|
||||
PARAMS="ordering=name"
|
||||
[[ -n "$SEARCH" ]] && PARAMS="${PARAMS}&search=${SEARCH}"
|
||||
|
||||
@@ -2,18 +2,17 @@
|
||||
#
|
||||
# auth-token.sh — Obtain and cache Authentik API token
|
||||
#
|
||||
# Usage: auth-token.sh [-f] [-q] [-a instance]
|
||||
# Usage: auth-token.sh [-f] [-q]
|
||||
#
|
||||
# Returns a valid Authentik API token. Checks in order:
|
||||
# 1. Cached token at ~/.cache/mosaic/authentik-token-<instance> (if valid)
|
||||
# 2. Pre-configured token from credentials.json (authentik.<instance>.token)
|
||||
# 1. Cached token at ~/.cache/mosaic/authentik-token (if valid)
|
||||
# 2. Pre-configured token from credentials.json (authentik.token)
|
||||
# 3. Fails with instructions to create a token in the admin UI
|
||||
#
|
||||
# Options:
|
||||
# -f Force re-validation (ignore cached token)
|
||||
# -q Quiet mode — only output the token
|
||||
# -a instance Authentik instance name (e.g. usc, mosaic)
|
||||
# -h Show this help
|
||||
# -f Force re-validation (ignore cached token)
|
||||
# -q Quiet mode — only output the token
|
||||
# -h Show this help
|
||||
#
|
||||
# Environment variables (or credentials.json):
|
||||
# AUTHENTIK_URL — Authentik instance URL
|
||||
@@ -22,30 +21,22 @@ set -euo pipefail
|
||||
|
||||
MOSAIC_HOME="${MOSAIC_HOME:-$HOME/.config/mosaic}"
|
||||
source "$MOSAIC_HOME/tools/_lib/credentials.sh"
|
||||
load_credentials authentik
|
||||
|
||||
CACHE_DIR="$HOME/.cache/mosaic"
|
||||
CACHE_FILE="$CACHE_DIR/authentik-token"
|
||||
FORCE=false
|
||||
QUIET=false
|
||||
AK_INSTANCE=""
|
||||
|
||||
while getopts "fqa:h" opt; do
|
||||
while getopts "fqh" opt; do
|
||||
case $opt in
|
||||
f) FORCE=true ;;
|
||||
q) QUIET=true ;;
|
||||
a) AK_INSTANCE="$OPTARG" ;;
|
||||
h) head -22 "$0" | grep "^#" | sed 's/^# \?//'; exit 0 ;;
|
||||
*) echo "Usage: $0 [-f] [-q] [-a instance]" >&2; exit 1 ;;
|
||||
h) head -20 "$0" | grep "^#" | sed 's/^# \?//'; exit 0 ;;
|
||||
*) echo "Usage: $0 [-f] [-q]" >&2; exit 1 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ -n "$AK_INSTANCE" ]]; then
|
||||
load_credentials "authentik-${AK_INSTANCE}"
|
||||
else
|
||||
load_credentials authentik
|
||||
fi
|
||||
|
||||
CACHE_DIR="$HOME/.cache/mosaic"
|
||||
CACHE_FILE="$CACHE_DIR/authentik-token${AUTHENTIK_INSTANCE:+-$AUTHENTIK_INSTANCE}"
|
||||
|
||||
_validate_token() {
|
||||
local token="$1"
|
||||
local http_code
|
||||
@@ -91,5 +82,5 @@ echo " 1. Log into Authentik admin: ${AUTHENTIK_URL}/if/admin/#/core/tokens" >&
|
||||
echo " 2. Click 'Create' → set identifier (e.g., 'mosaic-agent')" >&2
|
||||
echo " 3. Select 'API Token' intent, uncheck 'Expiring'" >&2
|
||||
echo " 4. Copy the key and add to credentials.json:" >&2
|
||||
echo " Add token to credentials.json under authentik.<instance>.token" >&2
|
||||
echo " jq '.authentik.token = \"<your-token>\"' credentials.json > tmp && mv tmp credentials.json" >&2
|
||||
exit 1
|
||||
|
||||
@@ -2,40 +2,32 @@
|
||||
#
|
||||
# flow-list.sh — List Authentik flows
|
||||
#
|
||||
# Usage: flow-list.sh [-f format] [-d designation] [-a instance]
|
||||
# Usage: flow-list.sh [-f format] [-d designation]
|
||||
#
|
||||
# Options:
|
||||
# -f format Output format: table (default), json
|
||||
# -d designation Filter by designation (authentication, authorization, enrollment, etc.)
|
||||
# -a instance Authentik instance name (e.g. usc, mosaic)
|
||||
# -h Show this help
|
||||
set -euo pipefail
|
||||
|
||||
MOSAIC_HOME="${MOSAIC_HOME:-$HOME/.config/mosaic}"
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
source "$MOSAIC_HOME/tools/_lib/credentials.sh"
|
||||
load_credentials authentik
|
||||
|
||||
FORMAT="table"
|
||||
DESIGNATION=""
|
||||
AK_INSTANCE=""
|
||||
|
||||
while getopts "f:d:a:h" opt; do
|
||||
while getopts "f:d:h" opt; do
|
||||
case $opt in
|
||||
f) FORMAT="$OPTARG" ;;
|
||||
d) DESIGNATION="$OPTARG" ;;
|
||||
a) AK_INSTANCE="$OPTARG" ;;
|
||||
h) head -14 "$0" | grep "^#" | sed 's/^# \?//'; exit 0 ;;
|
||||
*) echo "Usage: $0 [-f format] [-d designation] [-a instance]" >&2; exit 1 ;;
|
||||
h) head -13 "$0" | grep "^#" | sed 's/^# \?//'; exit 0 ;;
|
||||
*) echo "Usage: $0 [-f format] [-d designation]" >&2; exit 1 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ -n "$AK_INSTANCE" ]]; then
|
||||
load_credentials "authentik-${AK_INSTANCE}"
|
||||
else
|
||||
load_credentials authentik
|
||||
fi
|
||||
|
||||
TOKEN=$("$SCRIPT_DIR/auth-token.sh" -q ${AK_INSTANCE:+-a "$AK_INSTANCE"})
|
||||
TOKEN=$("$SCRIPT_DIR/auth-token.sh" -q)
|
||||
|
||||
PARAMS="ordering=slug"
|
||||
[[ -n "$DESIGNATION" ]] && PARAMS="${PARAMS}&designation=${DESIGNATION}"
|
||||
|
||||
@@ -2,40 +2,32 @@
|
||||
#
|
||||
# group-list.sh — List Authentik groups
|
||||
#
|
||||
# Usage: group-list.sh [-f format] [-s search] [-a instance]
|
||||
# Usage: group-list.sh [-f format] [-s search]
|
||||
#
|
||||
# Options:
|
||||
# -f format Output format: table (default), json
|
||||
# -s search Search by group name
|
||||
# -a instance Authentik instance name (e.g. usc, mosaic)
|
||||
# -h Show this help
|
||||
# -f format Output format: table (default), json
|
||||
# -s search Search by group name
|
||||
# -h Show this help
|
||||
set -euo pipefail
|
||||
|
||||
MOSAIC_HOME="${MOSAIC_HOME:-$HOME/.config/mosaic}"
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
source "$MOSAIC_HOME/tools/_lib/credentials.sh"
|
||||
load_credentials authentik
|
||||
|
||||
FORMAT="table"
|
||||
SEARCH=""
|
||||
AK_INSTANCE=""
|
||||
|
||||
while getopts "f:s:a:h" opt; do
|
||||
while getopts "f:s:h" opt; do
|
||||
case $opt in
|
||||
f) FORMAT="$OPTARG" ;;
|
||||
s) SEARCH="$OPTARG" ;;
|
||||
a) AK_INSTANCE="$OPTARG" ;;
|
||||
h) head -13 "$0" | grep "^#" | sed 's/^# \?//'; exit 0 ;;
|
||||
*) echo "Usage: $0 [-f format] [-s search] [-a instance]" >&2; exit 1 ;;
|
||||
h) head -12 "$0" | grep "^#" | sed 's/^# \?//'; exit 0 ;;
|
||||
*) echo "Usage: $0 [-f format] [-s search]" >&2; exit 1 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ -n "$AK_INSTANCE" ]]; then
|
||||
load_credentials "authentik-${AK_INSTANCE}"
|
||||
else
|
||||
load_credentials authentik
|
||||
fi
|
||||
|
||||
TOKEN=$("$SCRIPT_DIR/auth-token.sh" -q ${AK_INSTANCE:+-a "$AK_INSTANCE"})
|
||||
TOKEN=$("$SCRIPT_DIR/auth-token.sh" -q)
|
||||
|
||||
PARAMS="ordering=name"
|
||||
[[ -n "$SEARCH" ]] && PARAMS="${PARAMS}&search=${SEARCH}"
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
#
|
||||
# user-create.sh — Create an Authentik user
|
||||
#
|
||||
# Usage: user-create.sh -u <username> -n <name> -e <email> [-p password] [-g group] [-a instance]
|
||||
# Usage: user-create.sh -u <username> -n <name> -e <email> [-p password] [-g group]
|
||||
#
|
||||
# Options:
|
||||
# -u username Username (required)
|
||||
@@ -11,7 +11,6 @@
|
||||
# -p password Initial password (optional — user gets set-password flow if omitted)
|
||||
# -g group Group name to add user to (optional)
|
||||
# -f format Output format: table (default), json
|
||||
# -a instance Authentik instance name (e.g. usc, mosaic)
|
||||
# -h Show this help
|
||||
#
|
||||
# Environment variables (or credentials.json):
|
||||
@@ -21,10 +20,11 @@ set -euo pipefail
|
||||
MOSAIC_HOME="${MOSAIC_HOME:-$HOME/.config/mosaic}"
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
source "$MOSAIC_HOME/tools/_lib/credentials.sh"
|
||||
load_credentials authentik
|
||||
|
||||
USERNAME="" NAME="" EMAIL="" PASSWORD="" GROUP="" FORMAT="table" AK_INSTANCE=""
|
||||
USERNAME="" NAME="" EMAIL="" PASSWORD="" GROUP="" FORMAT="table"
|
||||
|
||||
while getopts "u:n:e:p:g:f:a:h" opt; do
|
||||
while getopts "u:n:e:p:g:f:h" opt; do
|
||||
case $opt in
|
||||
u) USERNAME="$OPTARG" ;;
|
||||
n) NAME="$OPTARG" ;;
|
||||
@@ -32,24 +32,17 @@ while getopts "u:n:e:p:g:f:a:h" opt; do
|
||||
p) PASSWORD="$OPTARG" ;;
|
||||
g) GROUP="$OPTARG" ;;
|
||||
f) FORMAT="$OPTARG" ;;
|
||||
a) AK_INSTANCE="$OPTARG" ;;
|
||||
h) head -19 "$0" | grep "^#" | sed 's/^# \?//'; exit 0 ;;
|
||||
*) echo "Usage: $0 -u <username> -n <name> -e <email> [-p password] [-g group] [-a instance]" >&2; exit 1 ;;
|
||||
h) head -18 "$0" | grep "^#" | sed 's/^# \?//'; exit 0 ;;
|
||||
*) echo "Usage: $0 -u <username> -n <name> -e <email> [-p password] [-g group]" >&2; exit 1 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ -n "$AK_INSTANCE" ]]; then
|
||||
load_credentials "authentik-${AK_INSTANCE}"
|
||||
else
|
||||
load_credentials authentik
|
||||
fi
|
||||
|
||||
if [[ -z "$USERNAME" || -z "$NAME" || -z "$EMAIL" ]]; then
|
||||
echo "Error: -u username, -n name, and -e email are required" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
TOKEN=$("$SCRIPT_DIR/auth-token.sh" -q ${AK_INSTANCE:+-a "$AK_INSTANCE"})
|
||||
TOKEN=$("$SCRIPT_DIR/auth-token.sh" -q)
|
||||
|
||||
# Build user payload
|
||||
payload=$(jq -n \
|
||||
|
||||
@@ -2,14 +2,13 @@
|
||||
#
|
||||
# user-list.sh — List Authentik users
|
||||
#
|
||||
# Usage: user-list.sh [-f format] [-s search] [-g group] [-a instance]
|
||||
# Usage: user-list.sh [-f format] [-s search] [-g group]
|
||||
#
|
||||
# Options:
|
||||
# -f format Output format: table (default), json
|
||||
# -s search Search term (matches username, name, email)
|
||||
# -g group Filter by group name
|
||||
# -a instance Authentik instance name (e.g. usc, mosaic)
|
||||
# -h Show this help
|
||||
# -f format Output format: table (default), json
|
||||
# -s search Search term (matches username, name, email)
|
||||
# -g group Filter by group name
|
||||
# -h Show this help
|
||||
#
|
||||
# Environment variables (or credentials.json):
|
||||
# AUTHENTIK_URL — Authentik instance URL
|
||||
@@ -18,30 +17,23 @@ set -euo pipefail
|
||||
MOSAIC_HOME="${MOSAIC_HOME:-$HOME/.config/mosaic}"
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
source "$MOSAIC_HOME/tools/_lib/credentials.sh"
|
||||
load_credentials authentik
|
||||
|
||||
FORMAT="table"
|
||||
SEARCH=""
|
||||
GROUP=""
|
||||
AK_INSTANCE=""
|
||||
|
||||
while getopts "f:s:g:a:h" opt; do
|
||||
while getopts "f:s:g:h" opt; do
|
||||
case $opt in
|
||||
f) FORMAT="$OPTARG" ;;
|
||||
s) SEARCH="$OPTARG" ;;
|
||||
g) GROUP="$OPTARG" ;;
|
||||
a) AK_INSTANCE="$OPTARG" ;;
|
||||
h) head -15 "$0" | grep "^#" | sed 's/^# \?//'; exit 0 ;;
|
||||
*) echo "Usage: $0 [-f format] [-s search] [-g group] [-a instance]" >&2; exit 1 ;;
|
||||
h) head -14 "$0" | grep "^#" | sed 's/^# \?//'; exit 0 ;;
|
||||
*) echo "Usage: $0 [-f format] [-s search] [-g group]" >&2; exit 1 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ -n "$AK_INSTANCE" ]]; then
|
||||
load_credentials "authentik-${AK_INSTANCE}"
|
||||
else
|
||||
load_credentials authentik
|
||||
fi
|
||||
|
||||
TOKEN=$("$SCRIPT_DIR/auth-token.sh" -q ${AK_INSTANCE:+-a "$AK_INSTANCE"})
|
||||
TOKEN=$("$SCRIPT_DIR/auth-token.sh" -q)
|
||||
|
||||
# Build query params
|
||||
PARAMS="ordering=username"
|
||||
|
||||
@@ -426,7 +426,7 @@ if [[ "$CICD_DOCKER" == true ]]; then
|
||||
# Extract host from https://host/org/repo.git or git@host:org/repo.git
|
||||
CICD_REGISTRY=$(echo "$REPO_URL" | sed -E 's|https?://([^/]+)/.*|\1|; s|git@([^:]+):.*|\1|')
|
||||
CICD_ORG=$(echo "$REPO_URL" | sed -E 's|https?://[^/]+/([^/]+)/.*|\1|; s|git@[^:]+:([^/]+)/.*|\1|')
|
||||
CICD_REPO_NAME=$(echo "$REPO_URL" | sed -E 's|\.git$||' | sed -E 's|.*/([^/]+)$|\1|')
|
||||
CICD_REPO_NAME=$(echo "$REPO_URL" | sed -E 's|.*/([^/]+?)(\.git)?$|\1|')
|
||||
fi
|
||||
|
||||
if [[ -n "$CICD_REGISTRY" && -n "$CICD_ORG" && -n "$CICD_REPO_NAME" && ${#CICD_SERVICES[@]} -gt 0 ]]; then
|
||||
|
||||
@@ -1,67 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# _lib.sh — Shared helpers for Cloudflare tool scripts
|
||||
#
|
||||
# Usage: source "$(dirname "$0")/_lib.sh"
|
||||
#
|
||||
# Provides:
|
||||
# CF_API — Base API URL
|
||||
# cf_auth — Authorization header value
|
||||
# cf_load_instance <instance> — Load credentials for a specific or default instance
|
||||
# cf_resolve_zone <name_or_id> — Resolves a zone name to its ID (passes IDs through)
|
||||
|
||||
CF_API="https://api.cloudflare.com/client/v4"
|
||||
|
||||
cf_auth() {
|
||||
echo "Bearer $CLOUDFLARE_API_TOKEN"
|
||||
}
|
||||
|
||||
# Load credentials for a Cloudflare instance.
|
||||
# If instance is empty, loads the default.
|
||||
cf_load_instance() {
|
||||
local instance="$1"
|
||||
if [[ -n "$instance" ]]; then
|
||||
load_credentials "cloudflare-${instance}"
|
||||
else
|
||||
load_credentials cloudflare
|
||||
fi
|
||||
}
|
||||
|
||||
# Resolve a zone name (e.g. "mosaicstack.dev") to its zone ID.
|
||||
# If the input is already a 32-char hex ID, passes it through.
|
||||
cf_resolve_zone() {
|
||||
local input="$1"
|
||||
|
||||
# If it looks like a zone ID (32 hex chars), pass through
|
||||
if [[ "$input" =~ ^[0-9a-f]{32}$ ]]; then
|
||||
echo "$input"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Resolve by name
|
||||
local response
|
||||
response=$(curl -s -w "\n%{http_code}" \
|
||||
-H "Authorization: $(cf_auth)" \
|
||||
-H "Content-Type: application/json" \
|
||||
"${CF_API}/zones?name=${input}&status=active")
|
||||
|
||||
local http_code
|
||||
http_code=$(echo "$response" | tail -n1)
|
||||
local body
|
||||
body=$(echo "$response" | sed '$d')
|
||||
|
||||
if [[ "$http_code" != "200" ]]; then
|
||||
echo "Error: Failed to resolve zone '$input' (HTTP $http_code)" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
local zone_id
|
||||
zone_id=$(echo "$body" | jq -r '.result[0].id // empty')
|
||||
|
||||
if [[ -z "$zone_id" ]]; then
|
||||
echo "Error: Zone '$input' not found" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
echo "$zone_id"
|
||||
}
|
||||
@@ -1,86 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# record-create.sh — Create a DNS record in a Cloudflare zone
|
||||
#
|
||||
# Usage: record-create.sh -z <zone> -t <type> -n <name> -c <content> [-a instance] [-l ttl] [-p] [-P priority]
|
||||
#
|
||||
# Options:
|
||||
# -z zone Zone name or ID (required)
|
||||
# -t type Record type: A, AAAA, CNAME, MX, TXT, etc. (required)
|
||||
# -n name Record name, e.g. "app" or "app.example.com" (required)
|
||||
# -c content Record value/content (required)
|
||||
# -a instance Cloudflare instance name (default: uses credentials default)
|
||||
# -l ttl TTL in seconds (default: 1 = auto)
|
||||
# -p Enable Cloudflare proxy (orange cloud)
|
||||
# -P priority MX/SRV priority (default: 10)
|
||||
# -h Show this help
|
||||
set -euo pipefail
|
||||
|
||||
MOSAIC_HOME="${MOSAIC_HOME:-$HOME/.config/mosaic}"
|
||||
source "$MOSAIC_HOME/tools/_lib/credentials.sh"
|
||||
source "$(dirname "$0")/_lib.sh"
|
||||
|
||||
ZONE=""
|
||||
INSTANCE=""
|
||||
TYPE=""
|
||||
NAME=""
|
||||
CONTENT=""
|
||||
TTL=1
|
||||
PROXIED=false
|
||||
PRIORITY=""
|
||||
|
||||
while getopts "z:a:t:n:c:l:pP:h" opt; do
|
||||
case $opt in
|
||||
z) ZONE="$OPTARG" ;;
|
||||
a) INSTANCE="$OPTARG" ;;
|
||||
t) TYPE="$OPTARG" ;;
|
||||
n) NAME="$OPTARG" ;;
|
||||
c) CONTENT="$OPTARG" ;;
|
||||
l) TTL="$OPTARG" ;;
|
||||
p) PROXIED=true ;;
|
||||
P) PRIORITY="$OPTARG" ;;
|
||||
h) head -18 "$0" | grep "^#" | sed 's/^# \?//'; exit 0 ;;
|
||||
*) echo "Usage: $0 -z <zone> -t <type> -n <name> -c <content> [-a instance] [-l ttl] [-p] [-P priority]" >&2; exit 1 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ -z "$ZONE" || -z "$TYPE" || -z "$NAME" || -z "$CONTENT" ]]; then
|
||||
echo "Error: -z, -t, -n, and -c are all required" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
cf_load_instance "$INSTANCE"
|
||||
ZONE_ID=$(cf_resolve_zone "$ZONE") || exit 1
|
||||
|
||||
# Build JSON payload
|
||||
payload=$(jq -n \
|
||||
--arg type "$TYPE" \
|
||||
--arg name "$NAME" \
|
||||
--arg content "$CONTENT" \
|
||||
--argjson ttl "$TTL" \
|
||||
--argjson proxied "$PROXIED" \
|
||||
'{type: $type, name: $name, content: $content, ttl: $ttl, proxied: $proxied}')
|
||||
|
||||
# Add priority for MX/SRV records
|
||||
if [[ -n "$PRIORITY" ]]; then
|
||||
payload=$(echo "$payload" | jq --argjson priority "$PRIORITY" '. + {priority: $priority}')
|
||||
fi
|
||||
|
||||
response=$(curl -s -w "\n%{http_code}" \
|
||||
-X POST \
|
||||
-H "Authorization: $(cf_auth)" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "$payload" \
|
||||
"${CF_API}/zones/${ZONE_ID}/dns_records")
|
||||
|
||||
http_code=$(echo "$response" | tail -n1)
|
||||
body=$(echo "$response" | sed '$d')
|
||||
|
||||
if [[ "$http_code" != "200" ]]; then
|
||||
echo "Error: Failed to create record (HTTP $http_code)" >&2
|
||||
echo "$body" | jq -r '.errors[]?.message // empty' 2>/dev/null >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
record_id=$(echo "$body" | jq -r '.result.id')
|
||||
echo "Created $TYPE record: $NAME → $CONTENT (ID: $record_id)"
|
||||
@@ -1,55 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# record-delete.sh — Delete a DNS record from a Cloudflare zone
|
||||
#
|
||||
# Usage: record-delete.sh -z <zone> -r <record-id> [-a instance]
|
||||
#
|
||||
# Options:
|
||||
# -z zone Zone name or ID (required)
|
||||
# -r record-id DNS record ID (required)
|
||||
# -a instance Cloudflare instance name (default: uses credentials default)
|
||||
# -h Show this help
|
||||
set -euo pipefail
|
||||
|
||||
MOSAIC_HOME="${MOSAIC_HOME:-$HOME/.config/mosaic}"
|
||||
source "$MOSAIC_HOME/tools/_lib/credentials.sh"
|
||||
source "$(dirname "$0")/_lib.sh"
|
||||
|
||||
ZONE=""
|
||||
INSTANCE=""
|
||||
RECORD_ID=""
|
||||
|
||||
while getopts "z:a:r:h" opt; do
|
||||
case $opt in
|
||||
z) ZONE="$OPTARG" ;;
|
||||
a) INSTANCE="$OPTARG" ;;
|
||||
r) RECORD_ID="$OPTARG" ;;
|
||||
h) head -11 "$0" | grep "^#" | sed 's/^# \?//'; exit 0 ;;
|
||||
*) echo "Usage: $0 -z <zone> -r <record-id> [-a instance]" >&2; exit 1 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ -z "$ZONE" || -z "$RECORD_ID" ]]; then
|
||||
echo "Error: -z and -r are both required" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
cf_load_instance "$INSTANCE"
|
||||
ZONE_ID=$(cf_resolve_zone "$ZONE") || exit 1
|
||||
|
||||
response=$(curl -s -w "\n%{http_code}" \
|
||||
-X DELETE \
|
||||
-H "Authorization: $(cf_auth)" \
|
||||
-H "Content-Type: application/json" \
|
||||
"${CF_API}/zones/${ZONE_ID}/dns_records/${RECORD_ID}")
|
||||
|
||||
http_code=$(echo "$response" | tail -n1)
|
||||
body=$(echo "$response" | sed '$d')
|
||||
|
||||
if [[ "$http_code" != "200" ]]; then
|
||||
echo "Error: Failed to delete record (HTTP $http_code)" >&2
|
||||
echo "$body" | jq -r '.errors[]?.message // empty' 2>/dev/null >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Deleted DNS record $RECORD_ID from zone $ZONE"
|
||||
@@ -1,81 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# record-list.sh — List DNS records for a Cloudflare zone
|
||||
#
|
||||
# Usage: record-list.sh -z <zone> [-a instance] [-t type] [-n name] [-f format]
|
||||
#
|
||||
# Options:
|
||||
# -z zone Zone name or ID (required)
|
||||
# -a instance Cloudflare instance name (default: uses credentials default)
|
||||
# -t type Filter by record type (A, AAAA, CNAME, MX, TXT, etc.)
|
||||
# -n name Filter by record name
|
||||
# -f format Output format: table (default), json
|
||||
# -h Show this help
|
||||
set -euo pipefail
|
||||
|
||||
MOSAIC_HOME="${MOSAIC_HOME:-$HOME/.config/mosaic}"
|
||||
source "$MOSAIC_HOME/tools/_lib/credentials.sh"
|
||||
source "$(dirname "$0")/_lib.sh"
|
||||
|
||||
ZONE=""
|
||||
INSTANCE=""
|
||||
TYPE=""
|
||||
NAME=""
|
||||
FORMAT="table"
|
||||
|
||||
while getopts "z:a:t:n:f:h" opt; do
|
||||
case $opt in
|
||||
z) ZONE="$OPTARG" ;;
|
||||
a) INSTANCE="$OPTARG" ;;
|
||||
t) TYPE="$OPTARG" ;;
|
||||
n) NAME="$OPTARG" ;;
|
||||
f) FORMAT="$OPTARG" ;;
|
||||
h) head -14 "$0" | grep "^#" | sed 's/^# \?//'; exit 0 ;;
|
||||
*) echo "Usage: $0 -z <zone> [-a instance] [-t type] [-n name] [-f format]" >&2; exit 1 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ -z "$ZONE" ]]; then
|
||||
echo "Error: -z zone is required" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
cf_load_instance "$INSTANCE"
|
||||
ZONE_ID=$(cf_resolve_zone "$ZONE") || exit 1
|
||||
|
||||
# Build query params
|
||||
params="per_page=100"
|
||||
[[ -n "$TYPE" ]] && params="${params}&type=${TYPE}"
|
||||
[[ -n "$NAME" ]] && params="${params}&name=${NAME}"
|
||||
|
||||
response=$(curl -s -w "\n%{http_code}" \
|
||||
-H "Authorization: $(cf_auth)" \
|
||||
-H "Content-Type: application/json" \
|
||||
"${CF_API}/zones/${ZONE_ID}/dns_records?${params}")
|
||||
|
||||
http_code=$(echo "$response" | tail -n1)
|
||||
body=$(echo "$response" | sed '$d')
|
||||
|
||||
if [[ "$http_code" != "200" ]]; then
|
||||
echo "Error: Failed to list records (HTTP $http_code)" >&2
|
||||
echo "$body" | jq -r '.errors[]?.message // empty' 2>/dev/null >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ "$FORMAT" == "json" ]]; then
|
||||
echo "$body" | jq '.result'
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "RECORD ID TYPE NAME CONTENT PROXIED TTL"
|
||||
echo "-------------------------------- ----- -------------------------------------- ------------------------------- ------- -----"
|
||||
echo "$body" | jq -r '.result[] | [
|
||||
.id,
|
||||
.type,
|
||||
.name,
|
||||
.content,
|
||||
(if .proxied then "yes" else "no" end),
|
||||
(if .ttl == 1 then "auto" else (.ttl | tostring) end)
|
||||
] | @tsv' | while IFS=$'\t' read -r id type name content proxied ttl; do
|
||||
printf "%-32s %-5s %-38s %-31s %-7s %s\n" "$id" "$type" "${name:0:38}" "${content:0:31}" "$proxied" "$ttl"
|
||||
done
|
||||
@@ -1,86 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# record-update.sh — Update a DNS record in a Cloudflare zone
|
||||
#
|
||||
# Usage: record-update.sh -z <zone> -r <record-id> -t <type> -n <name> -c <content> [-a instance] [-l ttl] [-p] [-P priority]
|
||||
#
|
||||
# Options:
|
||||
# -z zone Zone name or ID (required)
|
||||
# -r record-id DNS record ID (required)
|
||||
# -t type Record type: A, AAAA, CNAME, MX, TXT, etc. (required)
|
||||
# -n name Record name (required)
|
||||
# -c content Record value/content (required)
|
||||
# -a instance Cloudflare instance name (default: uses credentials default)
|
||||
# -l ttl TTL in seconds (default: 1 = auto)
|
||||
# -p Enable Cloudflare proxy (orange cloud)
|
||||
# -P priority MX/SRV priority
|
||||
# -h Show this help
|
||||
set -euo pipefail
|
||||
|
||||
MOSAIC_HOME="${MOSAIC_HOME:-$HOME/.config/mosaic}"
|
||||
source "$MOSAIC_HOME/tools/_lib/credentials.sh"
|
||||
source "$(dirname "$0")/_lib.sh"
|
||||
|
||||
ZONE=""
|
||||
INSTANCE=""
|
||||
RECORD_ID=""
|
||||
TYPE=""
|
||||
NAME=""
|
||||
CONTENT=""
|
||||
TTL=1
|
||||
PROXIED=false
|
||||
PRIORITY=""
|
||||
|
||||
while getopts "z:a:r:t:n:c:l:pP:h" opt; do
|
||||
case $opt in
|
||||
z) ZONE="$OPTARG" ;;
|
||||
a) INSTANCE="$OPTARG" ;;
|
||||
r) RECORD_ID="$OPTARG" ;;
|
||||
t) TYPE="$OPTARG" ;;
|
||||
n) NAME="$OPTARG" ;;
|
||||
c) CONTENT="$OPTARG" ;;
|
||||
l) TTL="$OPTARG" ;;
|
||||
p) PROXIED=true ;;
|
||||
P) PRIORITY="$OPTARG" ;;
|
||||
h) head -18 "$0" | grep "^#" | sed 's/^# \?//'; exit 0 ;;
|
||||
*) echo "Usage: $0 -z <zone> -r <record-id> -t <type> -n <name> -c <content> [-a instance]" >&2; exit 1 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ -z "$ZONE" || -z "$RECORD_ID" || -z "$TYPE" || -z "$NAME" || -z "$CONTENT" ]]; then
|
||||
echo "Error: -z, -r, -t, -n, and -c are all required" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
cf_load_instance "$INSTANCE"
|
||||
ZONE_ID=$(cf_resolve_zone "$ZONE") || exit 1
|
||||
|
||||
payload=$(jq -n \
|
||||
--arg type "$TYPE" \
|
||||
--arg name "$NAME" \
|
||||
--arg content "$CONTENT" \
|
||||
--argjson ttl "$TTL" \
|
||||
--argjson proxied "$PROXIED" \
|
||||
'{type: $type, name: $name, content: $content, ttl: $ttl, proxied: $proxied}')
|
||||
|
||||
if [[ -n "$PRIORITY" ]]; then
|
||||
payload=$(echo "$payload" | jq --argjson priority "$PRIORITY" '. + {priority: $priority}')
|
||||
fi
|
||||
|
||||
response=$(curl -s -w "\n%{http_code}" \
|
||||
-X PUT \
|
||||
-H "Authorization: $(cf_auth)" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "$payload" \
|
||||
"${CF_API}/zones/${ZONE_ID}/dns_records/${RECORD_ID}")
|
||||
|
||||
http_code=$(echo "$response" | tail -n1)
|
||||
body=$(echo "$response" | sed '$d')
|
||||
|
||||
if [[ "$http_code" != "200" ]]; then
|
||||
echo "Error: Failed to update record (HTTP $http_code)" >&2
|
||||
echo "$body" | jq -r '.errors[]?.message // empty' 2>/dev/null >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Updated $TYPE record: $NAME → $CONTENT (ID: $RECORD_ID)"
|
||||
@@ -1,59 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# zone-list.sh — List Cloudflare zones (domains)
|
||||
#
|
||||
# Usage: zone-list.sh [-a instance] [-f format]
|
||||
#
|
||||
# Options:
|
||||
# -a instance Cloudflare instance name (default: uses credentials default)
|
||||
# -f format Output format: table (default), json
|
||||
# -h Show this help
|
||||
set -euo pipefail
|
||||
|
||||
MOSAIC_HOME="${MOSAIC_HOME:-$HOME/.config/mosaic}"
|
||||
source "$MOSAIC_HOME/tools/_lib/credentials.sh"
|
||||
source "$(dirname "$0")/_lib.sh"
|
||||
|
||||
INSTANCE=""
|
||||
FORMAT="table"
|
||||
|
||||
while getopts "a:f:h" opt; do
|
||||
case $opt in
|
||||
a) INSTANCE="$OPTARG" ;;
|
||||
f) FORMAT="$OPTARG" ;;
|
||||
h) head -10 "$0" | grep "^#" | sed 's/^# \?//'; exit 0 ;;
|
||||
*) echo "Usage: $0 [-a instance] [-f format]" >&2; exit 1 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
cf_load_instance "$INSTANCE"
|
||||
|
||||
response=$(curl -s -w "\n%{http_code}" \
|
||||
-H "Authorization: $(cf_auth)" \
|
||||
-H "Content-Type: application/json" \
|
||||
"${CF_API}/zones?per_page=50")
|
||||
|
||||
http_code=$(echo "$response" | tail -n1)
|
||||
body=$(echo "$response" | sed '$d')
|
||||
|
||||
if [[ "$http_code" != "200" ]]; then
|
||||
echo "Error: Failed to list zones (HTTP $http_code)" >&2
|
||||
echo "$body" | jq -r '.errors[]?.message // empty' 2>/dev/null >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ "$FORMAT" == "json" ]]; then
|
||||
echo "$body" | jq '.result'
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "ZONE ID NAME STATUS PLAN"
|
||||
echo "-------------------------------- ---------------------------- -------- ----------"
|
||||
echo "$body" | jq -r '.result[] | [
|
||||
.id,
|
||||
.name,
|
||||
.status,
|
||||
.plan.name
|
||||
] | @tsv' | while IFS=$'\t' read -r id name status plan; do
|
||||
printf "%-32s %-28s %-8s %s\n" "$id" "$name" "$status" "$plan"
|
||||
done
|
||||
@@ -15,10 +15,11 @@ MANDATORY_FILES=(
|
||||
"$MOSAIC_HOME/TOOLS.md"
|
||||
)
|
||||
|
||||
# E2E delivery guide (canonical uppercase path)
|
||||
# E2E delivery guide (case-insensitive lookup)
|
||||
E2E_DELIVERY=""
|
||||
for candidate in \
|
||||
"$MOSAIC_HOME/guides/E2E-DELIVERY.md"; do
|
||||
"$MOSAIC_HOME/guides/E2E-DELIVERY.md" \
|
||||
"$MOSAIC_HOME/guides/e2e-delivery.md"; do
|
||||
if [[ -f "$candidate" ]]; then
|
||||
E2E_DELIVERY="$candidate"
|
||||
break
|
||||
|
||||
@@ -31,7 +31,41 @@ Examples:
|
||||
EOF
|
||||
}
|
||||
|
||||
# get_remote_host and get_gitea_token are provided by detect-platform.sh
|
||||
get_remote_host() {
|
||||
local remote_url
|
||||
remote_url=$(git remote get-url origin 2>/dev/null || true)
|
||||
if [[ -z "$remote_url" ]]; then
|
||||
return 1
|
||||
fi
|
||||
if [[ "$remote_url" =~ ^https?://([^/]+)/ ]]; then
|
||||
echo "${BASH_REMATCH[1]}"
|
||||
return 0
|
||||
fi
|
||||
if [[ "$remote_url" =~ ^git@([^:]+): ]]; then
|
||||
echo "${BASH_REMATCH[1]}"
|
||||
return 0
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
||||
get_gitea_token() {
|
||||
local host="$1"
|
||||
if [[ -n "${GITEA_TOKEN:-}" ]]; then
|
||||
echo "$GITEA_TOKEN"
|
||||
return 0
|
||||
fi
|
||||
|
||||
local creds="$HOME/.git-credentials"
|
||||
if [[ -f "$creds" ]]; then
|
||||
local token
|
||||
token=$(grep -F "$host" "$creds" 2>/dev/null | sed -n 's#https\?://[^@]*:\([^@/]*\)@.*#\1#p' | head -n 1)
|
||||
if [[ -n "$token" ]]; then
|
||||
echo "$token"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
||||
get_state_from_status_json() {
|
||||
python3 - <<'PY'
|
||||
|
||||
@@ -74,75 +74,6 @@ get_repo_name() {
|
||||
echo "${repo_info##*/}"
|
||||
}
|
||||
|
||||
get_remote_host() {
|
||||
local remote_url
|
||||
remote_url=$(git remote get-url origin 2>/dev/null || true)
|
||||
if [[ -z "$remote_url" ]]; then
|
||||
return 1
|
||||
fi
|
||||
if [[ "$remote_url" =~ ^https?://([^/]+)/ ]]; then
|
||||
echo "${BASH_REMATCH[1]}"
|
||||
return 0
|
||||
fi
|
||||
if [[ "$remote_url" =~ ^git@([^:]+): ]]; then
|
||||
echo "${BASH_REMATCH[1]}"
|
||||
return 0
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
||||
# Resolve a Gitea API token for the given host.
|
||||
# Priority: Mosaic credential loader → GITEA_TOKEN env → ~/.git-credentials
|
||||
get_gitea_token() {
|
||||
local host="$1"
|
||||
local script_dir
|
||||
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
local cred_loader="$script_dir/../_lib/credentials.sh"
|
||||
|
||||
# 1. Mosaic credential loader (host → service mapping, run in subshell to avoid polluting env)
|
||||
if [[ -f "$cred_loader" ]]; then
|
||||
local token
|
||||
token=$(
|
||||
source "$cred_loader"
|
||||
case "$host" in
|
||||
git.mosaicstack.dev) load_credentials gitea-mosaicstack 2>/dev/null ;;
|
||||
git.uscllc.com) load_credentials gitea-usc 2>/dev/null ;;
|
||||
*)
|
||||
for svc in gitea-mosaicstack gitea-usc; do
|
||||
load_credentials "$svc" 2>/dev/null || continue
|
||||
[[ "${GITEA_URL:-}" == *"$host"* ]] && break
|
||||
unset GITEA_TOKEN GITEA_URL
|
||||
done
|
||||
;;
|
||||
esac
|
||||
echo "${GITEA_TOKEN:-}"
|
||||
)
|
||||
if [[ -n "$token" ]]; then
|
||||
echo "$token"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
|
||||
# 2. GITEA_TOKEN env var (may be set by caller)
|
||||
if [[ -n "${GITEA_TOKEN:-}" ]]; then
|
||||
echo "$GITEA_TOKEN"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# 3. ~/.git-credentials file
|
||||
local creds="$HOME/.git-credentials"
|
||||
if [[ -f "$creds" ]]; then
|
||||
local token
|
||||
token=$(grep -F "$host" "$creds" 2>/dev/null | sed -n 's#https\?://[^@]*:\([^@/]*\)@.*#\1#p' | head -n 1)
|
||||
if [[ -n "$token" ]]; then
|
||||
echo "$token"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
# If script is run directly (not sourced), output the platform
|
||||
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
|
||||
detect_platform
|
||||
|
||||
@@ -13,7 +13,40 @@ BODY=""
|
||||
LABELS=""
|
||||
MILESTONE=""
|
||||
|
||||
# get_remote_host and get_gitea_token are provided by detect-platform.sh
|
||||
get_remote_host() {
|
||||
local remote_url
|
||||
remote_url=$(git remote get-url origin 2>/dev/null || true)
|
||||
if [[ -z "$remote_url" ]]; then
|
||||
return 1
|
||||
fi
|
||||
if [[ "$remote_url" =~ ^https?://([^/]+)/ ]]; then
|
||||
echo "${BASH_REMATCH[1]}"
|
||||
return 0
|
||||
fi
|
||||
if [[ "$remote_url" =~ ^git@([^:]+): ]]; then
|
||||
echo "${BASH_REMATCH[1]}"
|
||||
return 0
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
||||
get_gitea_token() {
|
||||
local host="$1"
|
||||
if [[ -n "${GITEA_TOKEN:-}" ]]; then
|
||||
echo "$GITEA_TOKEN"
|
||||
return 0
|
||||
fi
|
||||
local creds="$HOME/.git-credentials"
|
||||
if [[ -f "$creds" ]]; then
|
||||
local token
|
||||
token=$(grep -F "$host" "$creds" 2>/dev/null | sed -n 's#https\?://[^@]*:\([^@/]*\)@.*#\1#p' | head -n 1)
|
||||
if [[ -n "$token" ]]; then
|
||||
echo "$token"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
||||
gitea_issue_create_api() {
|
||||
local host repo token url payload
|
||||
|
||||
@@ -10,7 +10,40 @@ source "$SCRIPT_DIR/detect-platform.sh"
|
||||
# Parse arguments
|
||||
ISSUE_NUMBER=""
|
||||
|
||||
# get_remote_host and get_gitea_token are provided by detect-platform.sh
|
||||
get_remote_host() {
|
||||
local remote_url
|
||||
remote_url=$(git remote get-url origin 2>/dev/null || true)
|
||||
if [[ -z "$remote_url" ]]; then
|
||||
return 1
|
||||
fi
|
||||
if [[ "$remote_url" =~ ^https?://([^/]+)/ ]]; then
|
||||
echo "${BASH_REMATCH[1]}"
|
||||
return 0
|
||||
fi
|
||||
if [[ "$remote_url" =~ ^git@([^:]+): ]]; then
|
||||
echo "${BASH_REMATCH[1]}"
|
||||
return 0
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
||||
get_gitea_token() {
|
||||
local host="$1"
|
||||
if [[ -n "${GITEA_TOKEN:-}" ]]; then
|
||||
echo "$GITEA_TOKEN"
|
||||
return 0
|
||||
fi
|
||||
local creds="$HOME/.git-credentials"
|
||||
if [[ -f "$creds" ]]; then
|
||||
local token
|
||||
token=$(grep -F "$host" "$creds" 2>/dev/null | sed -n 's#https\?://[^@]*:\([^@/]*\)@.*#\1#p' | head -n 1)
|
||||
if [[ -n "$token" ]]; then
|
||||
echo "$token"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
||||
gitea_issue_view_api() {
|
||||
local host repo token url
|
||||
|
||||
@@ -27,7 +27,41 @@ Examples:
|
||||
EOF
|
||||
}
|
||||
|
||||
# get_remote_host and get_gitea_token are provided by detect-platform.sh
|
||||
get_remote_host() {
|
||||
local remote_url
|
||||
remote_url=$(git remote get-url origin 2>/dev/null || true)
|
||||
if [[ -z "$remote_url" ]]; then
|
||||
return 1
|
||||
fi
|
||||
if [[ "$remote_url" =~ ^https?://([^/]+)/ ]]; then
|
||||
echo "${BASH_REMATCH[1]}"
|
||||
return 0
|
||||
fi
|
||||
if [[ "$remote_url" =~ ^git@([^:]+): ]]; then
|
||||
echo "${BASH_REMATCH[1]}"
|
||||
return 0
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
||||
get_gitea_token() {
|
||||
local host="$1"
|
||||
if [[ -n "${GITEA_TOKEN:-}" ]]; then
|
||||
echo "$GITEA_TOKEN"
|
||||
return 0
|
||||
fi
|
||||
|
||||
local creds="$HOME/.git-credentials"
|
||||
if [[ -f "$creds" ]]; then
|
||||
local token
|
||||
token=$(grep -F "$host" "$creds" 2>/dev/null | sed -n 's#https\?://[^@]*:\([^@/]*\)@.*#\1#p' | head -n 1)
|
||||
if [[ -n "$token" ]]; then
|
||||
echo "$token"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
||||
extract_state_from_status_json() {
|
||||
python3 - <<'PY'
|
||||
|
||||
@@ -68,10 +68,11 @@ elif [[ "$PLATFORM" == "gitea" ]]; then
|
||||
|
||||
DIFF_URL="https://${HOST}/api/v1/repos/${OWNER}/${REPO}/pulls/${PR_NUMBER}.diff"
|
||||
|
||||
GITEA_API_TOKEN=$(get_gitea_token "$HOST" || true)
|
||||
# Use tea's auth token if available
|
||||
TEA_TOKEN=$(tea login list 2>/dev/null | grep "$HOST" | awk '{print $NF}' || true)
|
||||
|
||||
if [[ -n "$GITEA_API_TOKEN" ]]; then
|
||||
DIFF_CONTENT=$(curl -sS -H "Authorization: token $GITEA_API_TOKEN" "$DIFF_URL")
|
||||
if [[ -n "$TEA_TOKEN" ]]; then
|
||||
DIFF_CONTENT=$(curl -sS -H "Authorization: token $TEA_TOKEN" "$DIFF_URL")
|
||||
else
|
||||
DIFF_CONTENT=$(curl -sS "$DIFF_URL")
|
||||
fi
|
||||
|
||||
@@ -69,10 +69,11 @@ elif [[ "$PLATFORM" == "gitea" ]]; then
|
||||
|
||||
API_URL="https://${HOST}/api/v1/repos/${OWNER}/${REPO}/pulls/${PR_NUMBER}"
|
||||
|
||||
GITEA_API_TOKEN=$(get_gitea_token "$HOST" || true)
|
||||
# Use tea's auth token if available
|
||||
TEA_TOKEN=$(tea login list 2>/dev/null | grep "$HOST" | awk '{print $NF}' || true)
|
||||
|
||||
if [[ -n "$GITEA_API_TOKEN" ]]; then
|
||||
RAW=$(curl -sS -H "Authorization: token $GITEA_API_TOKEN" "$API_URL")
|
||||
if [[ -n "$TEA_TOKEN" ]]; then
|
||||
RAW=$(curl -sS -H "Authorization: token $TEA_TOKEN" "$API_URL")
|
||||
else
|
||||
RAW=$(curl -sS "$API_URL")
|
||||
fi
|
||||
|
||||
@@ -1,523 +0,0 @@
|
||||
#!/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/-$//'
|
||||
}
|
||||
@@ -1,173 +0,0 @@
|
||||
#!/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
|
||||
@@ -1,286 +0,0 @@
|
||||
#!/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')."
|
||||
@@ -1,181 +0,0 @@
|
||||
#!/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 ""
|
||||
@@ -1,208 +0,0 @@
|
||||
#!/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
|
||||
@@ -1,80 +0,0 @@
|
||||
#!/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]
|
||||
#
|
||||
# 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.
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
source "$SCRIPT_DIR/_lib.sh"
|
||||
|
||||
PROJECT="."
|
||||
MILESTONE=""
|
||||
PRINT=false
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--project) PROJECT="$2"; shift 2 ;;
|
||||
--milestone) MILESTONE="$2"; shift 2 ;;
|
||||
--print) PRINT=true; shift ;;
|
||||
-h|--help)
|
||||
cat <<'USAGE'
|
||||
Usage: session-run.sh [--project <path>] [--milestone <id>] [--print]
|
||||
|
||||
Options:
|
||||
--project <path> Project directory (default: CWD)
|
||||
--milestone <id> Force specific milestone context
|
||||
--print Print launch prompt only (no runtime launch)
|
||||
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
|
||||
|
||||
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")"
|
||||
|
||||
cd "$PROJECT"
|
||||
if [[ "$runtime" == "claude" ]]; then
|
||||
exec "$MOSAIC_HOME/bin/mosaic" claude "$launch_prompt"
|
||||
elif [[ "$runtime" == "codex" ]]; then
|
||||
exec "$MOSAIC_HOME/bin/mosaic" codex "$launch_prompt"
|
||||
fi
|
||||
|
||||
echo -e "${C_RED}Unsupported coord runtime: $runtime${C_RESET}" >&2
|
||||
exit 1
|
||||
@@ -1,241 +0,0 @@
|
||||
#!/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"
|
||||
@@ -1,78 +0,0 @@
|
||||
#!/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 [[ "$(printf '%s\n' "$codex_run_prompt" | head -n1)" == "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 [[ "$(printf '%s\n' "$claude_run_prompt" | head -n1)" == "## 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
|
||||
@@ -1,279 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# _lib.sh — Shared functions for mosaic prdy tools
|
||||
#
|
||||
# Usage: source ~/.config/mosaic/tools/prdy/_lib.sh
|
||||
#
|
||||
# Provides PRD detection, section validation, and system prompt generation
|
||||
# for interactive PRD creation/update sessions.
|
||||
|
||||
MOSAIC_HOME="${MOSAIC_HOME:-$HOME/.config/mosaic}"
|
||||
PRD_CANONICAL="docs/PRD.md"
|
||||
PRD_JSON_ALT="docs/PRD.json"
|
||||
|
||||
# ─── 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
|
||||
|
||||
ok() { echo -e " ${C_GREEN}✓${C_RESET} $1"; }
|
||||
warn() { echo -e " ${C_YELLOW}⚠${C_RESET} $1" >&2; }
|
||||
fail() { echo -e " ${C_RED}✗${C_RESET} $1" >&2; }
|
||||
info() { echo -e " ${C_CYAN}ℹ${C_RESET} $1"; }
|
||||
step() { echo -e "\n${C_BOLD}$1${C_RESET}"; }
|
||||
|
||||
# ─── Dependency checks ──────────────────────────────────────────────────────
|
||||
|
||||
_require_cmd() {
|
||||
local cmd="$1"
|
||||
if ! command -v "$cmd" &>/dev/null; then
|
||||
fail "'$cmd' is required but not installed"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
prdy_runtime() {
|
||||
local runtime="${MOSAIC_PRDY_RUNTIME:-claude}"
|
||||
case "$runtime" in
|
||||
claude|codex) echo "$runtime" ;;
|
||||
*) echo "claude" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
prdy_runtime_command() {
|
||||
local runtime
|
||||
runtime="$(prdy_runtime)"
|
||||
echo "$runtime"
|
||||
}
|
||||
|
||||
# ─── PRD detection ───────────────────────────────────────────────────────────
|
||||
|
||||
# Find the PRD file in a project directory.
|
||||
# Returns the path (relative to project root) or empty string.
|
||||
find_prd() {
|
||||
local project="${1:-.}"
|
||||
if [[ -f "$project/$PRD_CANONICAL" ]]; then
|
||||
echo "$project/$PRD_CANONICAL"
|
||||
elif [[ -f "$project/$PRD_JSON_ALT" ]]; then
|
||||
echo "$project/$PRD_JSON_ALT"
|
||||
fi
|
||||
}
|
||||
|
||||
# ─── Section validation manifest ─────────────────────────────────────────────
|
||||
|
||||
# 10 required sections per ~/.config/mosaic/guides/PRD.md
|
||||
# Each entry: "label|grep_pattern" (extended regex, case-insensitive via grep -iE)
|
||||
PRDY_REQUIRED_SECTIONS=(
|
||||
"Problem Statement|^#{2,3} .*(problem statement|objective)"
|
||||
"Scope / Non-Goals|^#{2,3} .*(scope|non.goal|out of scope|in.scope)"
|
||||
"User Stories / Requirements|^#{2,3} .*(user stor|stakeholder|user.*requirement)"
|
||||
"Functional Requirements|^#{2,3} .*functional requirement"
|
||||
"Non-Functional Requirements|^#{2,3} .*non.functional"
|
||||
"Acceptance Criteria|^#{2,3} .*acceptance criteria|\*\*acceptance criteria\*\*|- \[ \]"
|
||||
"Technical Considerations|^#{2,3} .*(technical consideration|constraint|dependenc)"
|
||||
"Risks / Open Questions|^#{2,3} .*(risk|open question)"
|
||||
"Success Metrics / Testing|^#{2,3} .*(success metric|test|verification)"
|
||||
"Milestones / Delivery|^#{2,3} .*(milestone|delivery|scope version)"
|
||||
)
|
||||
|
||||
# ─── System prompt builder ───────────────────────────────────────────────────
|
||||
|
||||
# Build the specialized system prompt for PRD sessions.
|
||||
# Usage: build_prdy_system_prompt <mode>
|
||||
# mode: "init" or "update"
|
||||
build_prdy_system_prompt() {
|
||||
local mode="${1:-init}"
|
||||
local guide_file="$MOSAIC_HOME/guides/PRD.md"
|
||||
local template_file="$MOSAIC_HOME/templates/docs/PRD.md.template"
|
||||
|
||||
cat <<'PROMPT_HEADER'
|
||||
# Mosaic PRD Agent — Behavioral Contract
|
||||
|
||||
You are a specialized PRD (Product Requirements Document) agent. Your sole purpose is to help the user create or update a comprehensive PRD document.
|
||||
|
||||
## Mode Declaration (Hard Gate)
|
||||
|
||||
PROMPT_HEADER
|
||||
|
||||
if [[ "$mode" == "init" ]]; then
|
||||
cat <<'INIT_MODE'
|
||||
Your first response MUST start with: "Now initiating PRD Creation mode..."
|
||||
|
||||
You are creating a NEW PRD. The output file is `docs/PRD.md`.
|
||||
|
||||
## Workflow
|
||||
|
||||
1. Read the project directory to understand context (README, package.json, existing docs, source structure)
|
||||
2. Ask the user 3-5 clarifying questions with lettered options (A/B/C/D) so they can answer "1A, 2C, 3B"
|
||||
3. Focus questions on: problem/goal, target users, scope boundaries, milestone structure, success criteria
|
||||
4. After answers (or if the description is sufficiently complete), generate the full PRD
|
||||
5. Write to `docs/PRD.md` (create `docs/` directory if it doesn't exist)
|
||||
6. After writing, tell the user: "PRD written to docs/PRD.md. Run `mosaic prdy validate` to verify completeness."
|
||||
|
||||
INIT_MODE
|
||||
else
|
||||
cat <<'UPDATE_MODE'
|
||||
Your first response MUST start with: "Now initiating PRD Update mode..."
|
||||
|
||||
You are UPDATING an existing PRD. Read `docs/PRD.md` (or `docs/PRD.json`) first.
|
||||
|
||||
## Workflow
|
||||
|
||||
1. Read the existing PRD file completely
|
||||
2. Summarize the current state to the user (title, status, milestone count, open questions)
|
||||
3. Ask the user what changes or additions are needed
|
||||
4. Make targeted modifications — do NOT rewrite sections that don't need changes
|
||||
5. Preserve existing user stories, FRs, and acceptance criteria unless explicitly asked to change them
|
||||
6. After writing, tell the user: "PRD updated. Run `mosaic prdy validate` to verify completeness."
|
||||
|
||||
UPDATE_MODE
|
||||
fi
|
||||
|
||||
cat <<'CONSTRAINTS'
|
||||
## Hard Constraints
|
||||
|
||||
MUST:
|
||||
- Save output to `docs/PRD.md` only
|
||||
- Include ALL 10 required sections from the Mosaic PRD guide (included below)
|
||||
- Number functional requirements as FR-1, FR-2, ... in sequence
|
||||
- Group user stories under named Milestones (e.g., "Milestone 0.0.4 — Foundation")
|
||||
- Format user stories as US-NNN with Description and Acceptance Criteria checkboxes
|
||||
- Mark all guessed decisions with `ASSUMPTION:` and rationale
|
||||
- Include a Metadata block (Owner, Date, Status, Mission ID, Scope Version)
|
||||
- Write for junior developers and AI agents — explicit, unambiguous, no jargon
|
||||
- Include "Typecheck and lint pass" in acceptance criteria for code stories
|
||||
- Include "Verify in browser using dev-browser skill" for UI stories
|
||||
|
||||
MUST NOT:
|
||||
- Write any implementation code
|
||||
- Modify any files other than `docs/PRD.md`
|
||||
- Skip clarifying questions when the feature description is ambiguous
|
||||
- Begin implementation of any requirements
|
||||
- Invent requirements silently — all guesses must be marked with ASSUMPTION
|
||||
|
||||
CONSTRAINTS
|
||||
|
||||
cat <<'STRUCTURE'
|
||||
## Required PRD Structure (Gold Standard)
|
||||
|
||||
Follow this exact structure. Every section is required.
|
||||
|
||||
```
|
||||
# PRD: {Feature/Project Name}
|
||||
|
||||
## Metadata
|
||||
- **Owner:** {name}
|
||||
- **Date:** {yyyy-mm-dd}
|
||||
- **Status:** draft|planning|approved|in-progress|completed
|
||||
- **Mission ID:** {kebab-case-id-yyyymmdd}
|
||||
- **Scope Version:** {e.g., 0.0.4 → 0.0.5 → 0.1.0}
|
||||
|
||||
## Introduction
|
||||
Narrative overview: what this is, the context it lives in, what exists before this work.
|
||||
|
||||
## Problem Statement
|
||||
Numbered list of specific pain points being solved.
|
||||
|
||||
## Goals
|
||||
Bullet list of measurable objectives.
|
||||
|
||||
## Milestones
|
||||
Named milestones (e.g., "Milestone 0.0.4 — Design System Foundation").
|
||||
Each milestone has a one-paragraph description of its theme.
|
||||
|
||||
## User Stories (grouped under milestones)
|
||||
|
||||
### Milestone X.Y.Z — {Theme Name}
|
||||
|
||||
#### US-001: {Title}
|
||||
**Description:** As a {user}, I want {feature} so that {benefit}.
|
||||
|
||||
**Acceptance Criteria:**
|
||||
- [ ] Specific verifiable criterion
|
||||
- [ ] Another criterion
|
||||
- [ ] Typecheck and lint pass
|
||||
- [ ] [UI stories only] Verify in browser using dev-browser skill
|
||||
|
||||
---
|
||||
|
||||
## Functional Requirements
|
||||
Numbered FR-1 through FR-N. Group by subsystem if applicable.
|
||||
Each: "FR-N: {subject} must {behavior}"
|
||||
|
||||
## Non-Goals (Out of Scope)
|
||||
Numbered list of explicit exclusions with brief rationale.
|
||||
|
||||
## Design Considerations
|
||||
- Design references (mockups, existing design systems)
|
||||
- Key visual/UX elements
|
||||
- Component hierarchy
|
||||
|
||||
## Technical Considerations
|
||||
|
||||
### Dependencies
|
||||
Libraries, packages, external services required.
|
||||
|
||||
### Build & CI
|
||||
Build pipeline, CI gates, quality checks.
|
||||
|
||||
### Deployment
|
||||
Target infrastructure, deployment method, image tagging strategy.
|
||||
|
||||
### Risks
|
||||
Technical risks that could affect delivery.
|
||||
|
||||
## Success Metrics
|
||||
Numbered list of measurable success conditions.
|
||||
|
||||
## Open Questions
|
||||
Numbered list. For unresolved items, add:
|
||||
(ASSUMPTION: {best-guess decision} — {rationale})
|
||||
```
|
||||
|
||||
STRUCTURE
|
||||
|
||||
cat <<'QUESTIONS'
|
||||
## Clarifying Question Format
|
||||
|
||||
When asking clarifying questions, use numbered questions with lettered options:
|
||||
|
||||
```
|
||||
1. What is the primary goal?
|
||||
A. Option one
|
||||
B. Option two
|
||||
C. Option three
|
||||
D. Other: [please specify]
|
||||
|
||||
2. What is the target scope?
|
||||
A. Minimal viable
|
||||
B. Full-featured
|
||||
C. Backend only
|
||||
D. Frontend only
|
||||
```
|
||||
|
||||
Tell the user they can answer with shorthand like "1A, 2C, 3B" for quick iteration.
|
||||
|
||||
QUESTIONS
|
||||
|
||||
# Include the live PRD guide
|
||||
if [[ -f "$guide_file" ]]; then
|
||||
printf '\n## Mosaic PRD Guide (Authoritative Rules)\n\n'
|
||||
cat "$guide_file"
|
||||
fi
|
||||
|
||||
# Include the template as reference
|
||||
if [[ -f "$template_file" ]]; then
|
||||
printf '\n## PRD Template Reference\n\n```markdown\n'
|
||||
cat "$template_file"
|
||||
printf '\n```\n'
|
||||
fi
|
||||
}
|
||||
@@ -1,106 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
#
|
||||
# prdy-init.sh — Create a new PRD via guided runtime session
|
||||
#
|
||||
# Usage:
|
||||
# prdy-init.sh [--project <path>] [--name <feature>]
|
||||
#
|
||||
# Launches a dedicated runtime session in yolo mode with a specialized
|
||||
# system prompt that guides the user through PRD creation. The output is
|
||||
# written to docs/PRD.md.
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
source "$SCRIPT_DIR/_lib.sh"
|
||||
|
||||
# ─── Parse arguments ─────────────────────────────────────────────────────────
|
||||
|
||||
PROJECT="."
|
||||
NAME=""
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--project) PROJECT="$2"; shift 2 ;;
|
||||
--name) NAME="$2"; shift 2 ;;
|
||||
-h|--help)
|
||||
cat <<'USAGE'
|
||||
prdy-init.sh — Create a new PRD via guided runtime session
|
||||
|
||||
Usage: prdy-init.sh [--project <path>] [--name <feature>]
|
||||
|
||||
Options:
|
||||
--project <path> Project directory (default: CWD)
|
||||
--name <feature> Feature or project name (optional, runtime will ask if omitted)
|
||||
|
||||
Launches the selected runtime in yolo mode with a PRD-focused prompt.
|
||||
The agent will ask clarifying questions, then write docs/PRD.md.
|
||||
|
||||
Examples:
|
||||
mosaic prdy init
|
||||
mosaic prdy init --name "User Authentication"
|
||||
mosaic prdy init --project ~/src/my-app --name "Dashboard Redesign"
|
||||
USAGE
|
||||
exit 0
|
||||
;;
|
||||
*) echo "Unknown option: $1" >&2; exit 1 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Expand tilde if passed literally (e.g., --project ~/src/foo)
|
||||
PROJECT="${PROJECT/#\~/$HOME}"
|
||||
|
||||
# ─── Preflight checks ───────────────────────────────────────────────────────
|
||||
|
||||
RUNTIME_CMD="$(prdy_runtime_command)"
|
||||
_require_cmd "$RUNTIME_CMD"
|
||||
|
||||
# Check for existing PRD
|
||||
EXISTING="$(find_prd "$PROJECT")"
|
||||
if [[ -n "$EXISTING" ]]; then
|
||||
fail "PRD already exists: $EXISTING"
|
||||
echo -e " Use ${C_CYAN}mosaic prdy update${C_RESET} to modify the existing PRD."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Ensure docs/ directory exists
|
||||
mkdir -p "$PROJECT/docs"
|
||||
|
||||
# ─── Build system prompt ─────────────────────────────────────────────────────
|
||||
|
||||
step "Launching PRD creation session"
|
||||
|
||||
SYSTEM_PROMPT="$(build_prdy_system_prompt "init")"
|
||||
|
||||
# Build kickoff message
|
||||
if [[ -n "$NAME" ]]; then
|
||||
KICKOFF="Create docs/PRD.md for the feature: ${NAME}. Read the project context first, then ask clarifying questions before writing the PRD."
|
||||
else
|
||||
KICKOFF="Create docs/PRD.md for this project. Read the project context first, then ask the user what they want to build. Ask clarifying questions before writing the PRD."
|
||||
fi
|
||||
|
||||
# ─── Launch runtime ──────────────────────────────────────────────────────────
|
||||
|
||||
info "Output target: $PROJECT/$PRD_CANONICAL"
|
||||
info "Mode: PRD Creation (yolo, runtime: $RUNTIME_CMD)"
|
||||
echo ""
|
||||
|
||||
cd "$PROJECT"
|
||||
if [[ "$RUNTIME_CMD" == "claude" ]]; then
|
||||
exec claude --dangerously-skip-permissions --append-system-prompt "$SYSTEM_PROMPT" "$KICKOFF"
|
||||
fi
|
||||
|
||||
if [[ "$RUNTIME_CMD" == "codex" ]]; then
|
||||
CODEX_PROMPT="$(cat <<EOF
|
||||
Follow this PRD contract exactly.
|
||||
|
||||
$SYSTEM_PROMPT
|
||||
|
||||
Task:
|
||||
$KICKOFF
|
||||
EOF
|
||||
)"
|
||||
exec codex --dangerously-bypass-approvals-and-sandbox "$CODEX_PROMPT"
|
||||
fi
|
||||
|
||||
fail "Unsupported runtime: $RUNTIME_CMD"
|
||||
exit 1
|
||||
@@ -1,94 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
#
|
||||
# prdy-status.sh — Quick PRD health check (one-liner output)
|
||||
#
|
||||
# Usage:
|
||||
# prdy-status.sh [--project <path>] [--format short|json]
|
||||
#
|
||||
# Exit codes:
|
||||
# 0 = PRD ready (all required sections present)
|
||||
# 1 = PRD incomplete
|
||||
# 2 = PRD missing
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
source "$SCRIPT_DIR/_lib.sh"
|
||||
|
||||
# ─── Parse arguments ─────────────────────────────────────────────────────────
|
||||
|
||||
PROJECT="."
|
||||
FORMAT="short"
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--project) PROJECT="$2"; shift 2 ;;
|
||||
--format) FORMAT="$2"; shift 2 ;;
|
||||
-h|--help)
|
||||
cat <<'USAGE'
|
||||
prdy-status.sh — Quick PRD health check
|
||||
|
||||
Usage: prdy-status.sh [--project <path>] [--format short|json]
|
||||
|
||||
Options:
|
||||
--project <path> Project directory (default: CWD)
|
||||
--format <f> Output format: short (default) or json
|
||||
|
||||
Exit codes:
|
||||
0 = PRD ready (all required sections present)
|
||||
1 = PRD incomplete
|
||||
2 = PRD missing
|
||||
USAGE
|
||||
exit 0
|
||||
;;
|
||||
*) echo "Unknown option: $1" >&2; exit 1 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
PROJECT="${PROJECT/#\~/$HOME}"
|
||||
|
||||
# ─── Status check ────────────────────────────────────────────────────────────
|
||||
|
||||
PRD_PATH="$(find_prd "$PROJECT")"
|
||||
|
||||
if [[ -z "$PRD_PATH" ]]; then
|
||||
if [[ "$FORMAT" == "json" ]]; then
|
||||
echo '{"status":"missing"}'
|
||||
else
|
||||
echo "PRD: missing"
|
||||
fi
|
||||
exit 2
|
||||
fi
|
||||
|
||||
# Count present sections using the shared manifest
|
||||
PRD_CONTENT="$(cat "$PRD_PATH")"
|
||||
total=${#PRDY_REQUIRED_SECTIONS[@]}
|
||||
present=0
|
||||
|
||||
for entry in "${PRDY_REQUIRED_SECTIONS[@]}"; do
|
||||
pattern="${entry#*|}"
|
||||
if echo "$PRD_CONTENT" | grep -qiE "$pattern"; then
|
||||
present=$((present + 1))
|
||||
fi
|
||||
done
|
||||
|
||||
# Count additional metrics
|
||||
fr_count=$(echo "$PRD_CONTENT" | grep -cE '^\- FR-[0-9]+:|^FR-[0-9]+:' || true)
|
||||
us_count=$(echo "$PRD_CONTENT" | grep -cE '^#{1,4} US-[0-9]+' || true)
|
||||
assumptions=$(echo "$PRD_CONTENT" | grep -c 'ASSUMPTION:' || true)
|
||||
|
||||
if (( present == total )); then
|
||||
status="ready"
|
||||
exit_code=0
|
||||
else
|
||||
status="incomplete"
|
||||
exit_code=1
|
||||
fi
|
||||
|
||||
if [[ "$FORMAT" == "json" ]]; then
|
||||
printf '{"status":"%s","sections":%d,"total":%d,"frs":%d,"stories":%d,"assumptions":%d}\n' \
|
||||
"$status" "$present" "$total" "$fr_count" "$us_count" "$assumptions"
|
||||
else
|
||||
echo "PRD: $status ($present/$total sections, ${fr_count} FRs, ${us_count} stories, $assumptions assumptions)"
|
||||
fi
|
||||
|
||||
exit "$exit_code"
|
||||
@@ -1,94 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
#
|
||||
# prdy-update.sh — Update an existing PRD via guided runtime session
|
||||
#
|
||||
# Usage:
|
||||
# prdy-update.sh [--project <path>]
|
||||
#
|
||||
# Launches a dedicated runtime session in yolo mode with a specialized
|
||||
# system prompt that reads the existing PRD and guides targeted modifications.
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
source "$SCRIPT_DIR/_lib.sh"
|
||||
|
||||
# ─── Parse arguments ─────────────────────────────────────────────────────────
|
||||
|
||||
PROJECT="."
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--project) PROJECT="$2"; shift 2 ;;
|
||||
-h|--help)
|
||||
cat <<'USAGE'
|
||||
prdy-update.sh — Update an existing PRD via guided runtime session
|
||||
|
||||
Usage: prdy-update.sh [--project <path>]
|
||||
|
||||
Options:
|
||||
--project <path> Project directory (default: CWD)
|
||||
|
||||
Launches the selected runtime in yolo mode with a PRD-update prompt.
|
||||
The agent will read the existing docs/PRD.md, summarize its state,
|
||||
and ask what changes are needed.
|
||||
|
||||
Examples:
|
||||
mosaic prdy update
|
||||
mosaic prdy update --project ~/src/my-app
|
||||
USAGE
|
||||
exit 0
|
||||
;;
|
||||
*) echo "Unknown option: $1" >&2; exit 1 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Expand tilde if passed literally (e.g., --project ~/src/foo)
|
||||
PROJECT="${PROJECT/#\~/$HOME}"
|
||||
|
||||
# ─── Preflight checks ───────────────────────────────────────────────────────
|
||||
|
||||
RUNTIME_CMD="$(prdy_runtime_command)"
|
||||
_require_cmd "$RUNTIME_CMD"
|
||||
|
||||
# Require existing PRD
|
||||
EXISTING="$(find_prd "$PROJECT")"
|
||||
if [[ -z "$EXISTING" ]]; then
|
||||
fail "No PRD found in $PROJECT/docs/"
|
||||
echo -e " Run ${C_CYAN}mosaic prdy init${C_RESET} to create one first."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# ─── Build system prompt ─────────────────────────────────────────────────────
|
||||
|
||||
step "Launching PRD update session"
|
||||
|
||||
SYSTEM_PROMPT="$(build_prdy_system_prompt "update")"
|
||||
|
||||
KICKOFF="Read the existing PRD at ${EXISTING}, summarize its current state, then ask what changes or additions are needed."
|
||||
|
||||
# ─── Launch runtime ──────────────────────────────────────────────────────────
|
||||
|
||||
info "Updating: $EXISTING"
|
||||
info "Mode: PRD Update (yolo, runtime: $RUNTIME_CMD)"
|
||||
echo ""
|
||||
|
||||
cd "$PROJECT"
|
||||
if [[ "$RUNTIME_CMD" == "claude" ]]; then
|
||||
exec claude --dangerously-skip-permissions --append-system-prompt "$SYSTEM_PROMPT" "$KICKOFF"
|
||||
fi
|
||||
|
||||
if [[ "$RUNTIME_CMD" == "codex" ]]; then
|
||||
CODEX_PROMPT="$(cat <<EOF
|
||||
Follow this PRD contract exactly.
|
||||
|
||||
$SYSTEM_PROMPT
|
||||
|
||||
Task:
|
||||
$KICKOFF
|
||||
EOF
|
||||
)"
|
||||
exec codex --dangerously-bypass-approvals-and-sandbox "$CODEX_PROMPT"
|
||||
fi
|
||||
|
||||
fail "Unsupported runtime: $RUNTIME_CMD"
|
||||
exit 1
|
||||
@@ -1,170 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
#
|
||||
# prdy-validate.sh — Check PRD completeness against Mosaic guide requirements
|
||||
#
|
||||
# Usage:
|
||||
# prdy-validate.sh [--project <path>]
|
||||
#
|
||||
# Performs static analysis of docs/PRD.md to verify it meets the minimum
|
||||
# content requirements defined in the Mosaic PRD guide.
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
source "$SCRIPT_DIR/_lib.sh"
|
||||
|
||||
# ─── Parse arguments ─────────────────────────────────────────────────────────
|
||||
|
||||
PROJECT="."
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--project) PROJECT="$2"; shift 2 ;;
|
||||
-h|--help)
|
||||
cat <<'USAGE'
|
||||
prdy-validate.sh — Check PRD completeness against Mosaic guide
|
||||
|
||||
Usage: prdy-validate.sh [--project <path>]
|
||||
|
||||
Options:
|
||||
--project <path> Project directory (default: CWD)
|
||||
|
||||
Checks:
|
||||
- docs/PRD.md exists
|
||||
- All 10 required sections present
|
||||
- Metadata block (Owner, Date, Status)
|
||||
- Functional requirements (FR-N) count
|
||||
- User stories (US-NNN) count
|
||||
- Acceptance criteria checklist count
|
||||
- ASSUMPTION markers (informational)
|
||||
|
||||
Exit codes:
|
||||
0 All required checks pass
|
||||
1 One or more required checks failed
|
||||
USAGE
|
||||
exit 0
|
||||
;;
|
||||
*) echo "Unknown option: $1" >&2; exit 1 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Expand tilde if passed literally (e.g., --project ~/src/foo)
|
||||
PROJECT="${PROJECT/#\~/$HOME}"
|
||||
|
||||
# ─── Find PRD ────────────────────────────────────────────────────────────────
|
||||
|
||||
step "Validating PRD"
|
||||
|
||||
PRD_PATH="$(find_prd "$PROJECT")"
|
||||
PASS=0
|
||||
FAIL_COUNT=0
|
||||
WARN_COUNT=0
|
||||
|
||||
check_pass() {
|
||||
ok "$1"
|
||||
PASS=$((PASS + 1))
|
||||
}
|
||||
|
||||
check_fail() {
|
||||
fail "$1"
|
||||
FAIL_COUNT=$((FAIL_COUNT + 1))
|
||||
}
|
||||
|
||||
check_warn() {
|
||||
warn "$1"
|
||||
WARN_COUNT=$((WARN_COUNT + 1))
|
||||
}
|
||||
|
||||
if [[ -z "$PRD_PATH" ]]; then
|
||||
check_fail "No PRD found. Expected $PROJECT/$PRD_CANONICAL or $PROJECT/$PRD_JSON_ALT"
|
||||
echo ""
|
||||
echo -e "${C_RED}PRD validation failed.${C_RESET} Run ${C_CYAN}mosaic prdy init${C_RESET} to create one."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
check_pass "PRD found: $PRD_PATH"
|
||||
|
||||
# JSON PRDs get a limited check
|
||||
if [[ "$PRD_PATH" == *.json ]]; then
|
||||
info "JSON PRD detected — section checks skipped (markdown-only)"
|
||||
echo ""
|
||||
echo -e "${C_GREEN}PRD file exists.${C_RESET} JSON validation is limited."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
PRD_CONTENT="$(cat "$PRD_PATH")"
|
||||
|
||||
# ─── Section checks ─────────────────────────────────────────────────────────
|
||||
|
||||
for entry in "${PRDY_REQUIRED_SECTIONS[@]}"; do
|
||||
label="${entry%%|*}"
|
||||
pattern="${entry#*|}"
|
||||
|
||||
if echo "$PRD_CONTENT" | grep -qiE "$pattern"; then
|
||||
check_pass "$label section present"
|
||||
else
|
||||
check_fail "$label section MISSING"
|
||||
fi
|
||||
done
|
||||
|
||||
# ─── Metadata checks ────────────────────────────────────────────────────────
|
||||
|
||||
META_PASS=true
|
||||
for field in "Owner" "Date" "Status"; do
|
||||
if ! echo "$PRD_CONTENT" | grep -qiE "^- \*\*${field}:\*\*|^- ${field}:"; then
|
||||
META_PASS=false
|
||||
fi
|
||||
done
|
||||
|
||||
if $META_PASS; then
|
||||
check_pass "Metadata block present (Owner, Date, Status)"
|
||||
else
|
||||
check_fail "Metadata block incomplete (need Owner, Date, Status)"
|
||||
fi
|
||||
|
||||
# ─── Content counts ─────────────────────────────────────────────────────────
|
||||
|
||||
FR_COUNT=$(echo "$PRD_CONTENT" | grep -cE '^\- FR-[0-9]+:|^FR-[0-9]+:' || true)
|
||||
US_COUNT=$(echo "$PRD_CONTENT" | grep -cE '^#{1,4} US-[0-9]+' || true)
|
||||
AC_COUNT=$(echo "$PRD_CONTENT" | grep -cE '^\s*- \[ \]' || true)
|
||||
ASSUMPTION_COUNT=$(echo "$PRD_CONTENT" | grep -c 'ASSUMPTION:' || true)
|
||||
|
||||
if [[ "$FR_COUNT" -gt 0 ]]; then
|
||||
check_pass "Functional requirements: $FR_COUNT FR items"
|
||||
else
|
||||
check_warn "No FR-N items found (expected numbered functional requirements)"
|
||||
fi
|
||||
|
||||
if [[ "$US_COUNT" -gt 0 ]]; then
|
||||
check_pass "User stories: $US_COUNT US items"
|
||||
else
|
||||
check_warn "No US-NNN items found (expected user stories)"
|
||||
fi
|
||||
|
||||
if [[ "$AC_COUNT" -gt 0 ]]; then
|
||||
check_pass "Acceptance criteria: $AC_COUNT checklist items"
|
||||
else
|
||||
check_warn "No acceptance criteria checkboxes found"
|
||||
fi
|
||||
|
||||
if [[ "$ASSUMPTION_COUNT" -gt 0 ]]; then
|
||||
info "ASSUMPTION markers: $ASSUMPTION_COUNT (informational)"
|
||||
fi
|
||||
|
||||
# ─── Summary ─────────────────────────────────────────────────────────────────
|
||||
|
||||
TOTAL=$((PASS + FAIL_COUNT))
|
||||
echo ""
|
||||
|
||||
if [[ "$FAIL_COUNT" -eq 0 ]]; then
|
||||
echo -e "${C_GREEN}PRD validation passed.${C_RESET} ${PASS}/${TOTAL} checks OK."
|
||||
if [[ "$WARN_COUNT" -gt 0 ]]; then
|
||||
echo -e "${C_YELLOW}${WARN_COUNT} warning(s)${C_RESET} — review recommended."
|
||||
fi
|
||||
exit 0
|
||||
else
|
||||
echo -e "${C_RED}PRD validation failed.${C_RESET} ${FAIL_COUNT}/${TOTAL} checks failed."
|
||||
if [[ "$WARN_COUNT" -gt 0 ]]; then
|
||||
echo -e "${C_YELLOW}${WARN_COUNT} warning(s)${C_RESET}"
|
||||
fi
|
||||
exit 1
|
||||
fi
|
||||
@@ -43,7 +43,7 @@ npx husky install
|
||||
✅ **TypeScript strict mode** - All type checks enabled
|
||||
✅ **ESLint blocking `any` types** - no-explicit-any: error
|
||||
✅ **Pre-commit hooks** - Type check + lint + format before commit
|
||||
✅ **Secret scanning (gitleaks)** - Block hardcoded passwords/API keys (pre-commit + CI)
|
||||
✅ **Secret scanning** - Block hardcoded passwords/API keys
|
||||
✅ **CI/CD templates** - Woodpecker, GitHub Actions, GitLab
|
||||
✅ **Test coverage enforcement** - 80% threshold
|
||||
✅ **Security scanning** - npm audit, OWASP checks
|
||||
@@ -96,12 +96,11 @@ git commit -m "Add feature"
|
||||
### CI/CD (Remote Enforcement)
|
||||
```yaml
|
||||
# Woodpecker pipeline runs:
|
||||
✓ gitleaks (secret scanning — parallel, no deps)
|
||||
✓ npm audit (dependency security)
|
||||
✓ eslint (code quality)
|
||||
✓ tsc --noEmit (type checking)
|
||||
✓ jest --coverage (tests + coverage)
|
||||
✓ npm run build (compilation — gates on all above)
|
||||
✓ npm run build (compilation)
|
||||
|
||||
# If any step fails, merge is blocked
|
||||
```
|
||||
|
||||
@@ -8,13 +8,12 @@ Quality Rails includes `.woodpecker.yml` template.
|
||||
|
||||
### Pipeline Stages
|
||||
|
||||
1. **Secret Scan** - gitleaks scans latest commit for hardcoded secrets (runs in parallel, no deps)
|
||||
2. **Install** - Dependencies
|
||||
3. **Security Audit** - npm audit for CVEs
|
||||
4. **Lint** - ESLint checks
|
||||
5. **Type Check** - TypeScript compilation
|
||||
6. **Test** - Jest with coverage thresholds
|
||||
7. **Build** - Production build (gates on all above)
|
||||
1. **Install** - Dependencies
|
||||
2. **Security Audit** - npm audit for CVEs
|
||||
3. **Lint** - ESLint checks
|
||||
4. **Type Check** - TypeScript compilation
|
||||
5. **Test** - Jest with coverage thresholds
|
||||
6. **Build** - Production build
|
||||
|
||||
### Configuration
|
||||
|
||||
|
||||
@@ -24,12 +24,11 @@ git clone git@git.mosaicstack.dev:mosaic/quality-rails.git
|
||||
```
|
||||
|
||||
This copies:
|
||||
- `.husky/pre-commit` - Git hooks (lint-staged + gitleaks)
|
||||
- `.husky/pre-commit` - Git hooks
|
||||
- `.lintstagedrc.js` - Pre-commit checks
|
||||
- `.eslintrc.js` - Strict ESLint rules
|
||||
- `tsconfig.json` - TypeScript strict mode
|
||||
- `.woodpecker.yml` - CI pipeline
|
||||
- `.gitleaks.toml` - Secret scanning config
|
||||
|
||||
### 3. Install Dependencies
|
||||
|
||||
@@ -76,8 +75,6 @@ Should output:
|
||||
```
|
||||
✅ PASS: Type errors blocked
|
||||
✅ PASS: 'any' types blocked
|
||||
✅ PASS: gitleaks found (8.24.0)
|
||||
✅ PASS: gitleaks detected planted secret
|
||||
✅ PASS: Lint errors blocked
|
||||
```
|
||||
|
||||
@@ -128,7 +125,7 @@ On every `git commit`, runs:
|
||||
1. ESLint with --max-warnings=0
|
||||
2. TypeScript type check
|
||||
3. Prettier formatting
|
||||
4. Secret scanning via gitleaks (required)
|
||||
4. Secret scanning (if git-secrets installed)
|
||||
|
||||
If any fail → **commit blocked**.
|
||||
|
||||
|
||||
@@ -33,10 +33,6 @@ Copy-Item -Path "$TemplateDir\.eslintrc.strict.js" -Destination "$TargetDir\.esl
|
||||
Copy-Item -Path "$TemplateDir\tsconfig.strict.json" -Destination "$TargetDir\tsconfig.json" -Force -ErrorAction SilentlyContinue
|
||||
Copy-Item -Path "$TemplateDir\.woodpecker.yml" -Destination $TargetDir -Force -ErrorAction SilentlyContinue
|
||||
|
||||
# Copy shared gitleaks config from templates root
|
||||
$SharedTemplates = Split-Path -Parent $TemplateDir
|
||||
Copy-Item -Path "$SharedTemplates\.gitleaks.toml" -Destination $TargetDir -Force -ErrorAction SilentlyContinue
|
||||
|
||||
Write-Host "✓ Files copied"
|
||||
|
||||
if (Test-Path "$TargetDir\package.json") {
|
||||
@@ -54,6 +50,4 @@ Write-Host ""
|
||||
Write-Host "Next steps:"
|
||||
Write-Host "1. Install dependencies: npm install"
|
||||
Write-Host "2. Initialize husky: npx husky install"
|
||||
Write-Host "3. Install gitleaks: winget install gitleaks"
|
||||
Write-Host "4. Run verification: ..\quality-rails\scripts\verify.ps1"
|
||||
Write-Host "5. (Optional) Scan full history: gitleaks git --redact --verbose"
|
||||
Write-Host "3. Run verification: ..\quality-rails\scripts\verify.ps1"
|
||||
|
||||
@@ -53,10 +53,6 @@ cp "$TEMPLATE_DIR/.eslintrc.strict.js" "$TARGET_DIR/.eslintrc.js" 2>/dev/null ||
|
||||
cp "$TEMPLATE_DIR/tsconfig.strict.json" "$TARGET_DIR/tsconfig.json" 2>/dev/null || true
|
||||
cp "$TEMPLATE_DIR/.woodpecker.yml" "$TARGET_DIR/" 2>/dev/null || true
|
||||
|
||||
# Copy shared gitleaks config from templates root
|
||||
SHARED_TEMPLATES="$(dirname "$TEMPLATE_DIR")"
|
||||
cp "$SHARED_TEMPLATES/.gitleaks.toml" "$TARGET_DIR/" 2>/dev/null || true
|
||||
|
||||
echo "✓ Files copied"
|
||||
|
||||
# Check if package.json exists
|
||||
@@ -75,7 +71,5 @@ echo ""
|
||||
echo "Next steps:"
|
||||
echo "1. Install dependencies: npm install"
|
||||
echo "2. Initialize husky: npx husky install"
|
||||
echo "3. Install gitleaks: https://github.com/gitleaks/gitleaks#installing"
|
||||
echo "4. Run verification: ~/.config/mosaic/bin/mosaic-quality-verify --target $TARGET_DIR"
|
||||
echo "5. (Optional) Scan full history: gitleaks git --redact --verbose"
|
||||
echo "3. Run verification: ~/.config/mosaic/bin/mosaic-quality-verify --target $TARGET_DIR"
|
||||
echo ""
|
||||
|
||||
@@ -39,40 +39,6 @@ if ($output -match "no-explicit-any") {
|
||||
git reset HEAD test-file.ts 2>$null
|
||||
Remove-Item test-file.ts -ErrorAction SilentlyContinue
|
||||
|
||||
# Test 3a: gitleaks binary must be present
|
||||
Write-Host ""
|
||||
Write-Host "Test 3a: gitleaks must be installed..."
|
||||
$gitleaksPath = Get-Command gitleaks -ErrorAction SilentlyContinue
|
||||
if ($gitleaksPath) {
|
||||
$gitleaksVer = & gitleaks version 2>&1 | Out-String
|
||||
Write-Host "✅ PASS: gitleaks found ($($gitleaksVer.Trim()))" -ForegroundColor Green
|
||||
$Passed++
|
||||
} else {
|
||||
Write-Host "❌ FAIL: gitleaks is NOT installed — secret scanning will not work" -ForegroundColor Red
|
||||
Write-Host " Install: winget install gitleaks"
|
||||
$Failed++
|
||||
}
|
||||
|
||||
# Test 3b: gitleaks detects a planted AWS key
|
||||
Write-Host ""
|
||||
Write-Host "Test 3b: gitleaks should detect planted AWS key..."
|
||||
if ($gitleaksPath) {
|
||||
"aws_access_key_id = AKIAIOSFODNN7REALKEY" | Out-File -FilePath gitleaks-test-secret.txt -Encoding utf8
|
||||
git add gitleaks-test-secret.txt 2>$null
|
||||
$output = & gitleaks git --pre-commit --staged --redact 2>&1 | Out-String
|
||||
if ($output -match "leak|finding") {
|
||||
Write-Host "✅ PASS: gitleaks detected planted secret" -ForegroundColor Green
|
||||
$Passed++
|
||||
} else {
|
||||
Write-Host "❌ FAIL: gitleaks did NOT detect planted secret" -ForegroundColor Red
|
||||
$Failed++
|
||||
}
|
||||
git reset HEAD gitleaks-test-secret.txt 2>$null
|
||||
Remove-Item gitleaks-test-secret.txt -ErrorAction SilentlyContinue
|
||||
} else {
|
||||
Write-Host "⚠ SKIP: gitleaks not installed (Test 3a already failed)"
|
||||
}
|
||||
|
||||
# Summary
|
||||
Write-Host ""
|
||||
Write-Host "═══════════════════════════════════════════"
|
||||
|
||||
@@ -40,35 +40,23 @@ fi
|
||||
git reset HEAD test-file.ts 2>/dev/null
|
||||
rm test-file.ts 2>/dev/null
|
||||
|
||||
# Test 3a: gitleaks binary must be present
|
||||
# Test 3: Hardcoded secret blocked (if git-secrets installed)
|
||||
echo ""
|
||||
echo "Test 3a: gitleaks must be installed..."
|
||||
if command -v gitleaks &> /dev/null; then
|
||||
echo "✅ PASS: gitleaks found ($(gitleaks version 2>/dev/null || echo 'unknown version'))"
|
||||
PASSED=$((PASSED + 1))
|
||||
else
|
||||
echo "❌ FAIL: gitleaks is NOT installed — secret scanning will not work"
|
||||
echo " Install: https://github.com/gitleaks/gitleaks#installing"
|
||||
FAILED=$((FAILED + 1))
|
||||
fi
|
||||
|
||||
# Test 3b: gitleaks detects a planted AWS key
|
||||
echo ""
|
||||
echo "Test 3b: gitleaks should detect planted AWS key..."
|
||||
if command -v gitleaks &> /dev/null; then
|
||||
echo 'aws_access_key_id = AKIAIOSFODNN7REALKEY' > gitleaks-test-secret.txt
|
||||
git add gitleaks-test-secret.txt 2>/dev/null
|
||||
if gitleaks git --pre-commit --staged --redact 2>&1 | grep -q -i "leak\|finding"; then
|
||||
echo "✅ PASS: gitleaks detected planted secret"
|
||||
PASSED=$((PASSED + 1))
|
||||
echo "Test 3: Hardcoded secrets should be blocked..."
|
||||
if command -v git-secrets &> /dev/null; then
|
||||
echo "const password = 'SuperSecret123!';" > test-file.ts
|
||||
git add test-file.ts 2>/dev/null
|
||||
if git commit -m "Test commit" 2>&1 | grep -q -i "secret\|password"; then
|
||||
echo "✅ PASS: Secrets blocked"
|
||||
((PASSED++))
|
||||
else
|
||||
echo "❌ FAIL: gitleaks did NOT detect planted secret"
|
||||
FAILED=$((FAILED + 1))
|
||||
echo "⚠ WARN: Secrets NOT blocked (git-secrets may need configuration)"
|
||||
((FAILED++))
|
||||
fi
|
||||
git reset HEAD gitleaks-test-secret.txt 2>/dev/null
|
||||
rm gitleaks-test-secret.txt 2>/dev/null
|
||||
git reset HEAD test-file.ts 2>/dev/null
|
||||
rm test-file.ts 2>/dev/null
|
||||
else
|
||||
echo "⚠ SKIP: gitleaks not installed (Test 3a already failed)"
|
||||
echo "⚠ SKIP: git-secrets not installed"
|
||||
fi
|
||||
|
||||
# Test 4: Lint error blocked
|
||||
|
||||
@@ -1,162 +0,0 @@
|
||||
# Mosaic Quality Rails — gitleaks configuration
|
||||
# Shared across all project templates. Copied to project root by install.sh.
|
||||
# Built-in rules: https://github.com/gitleaks/gitleaks/tree/master/config
|
||||
# This file adds custom rules for patterns the 150+ built-in rules miss.
|
||||
|
||||
title = "Mosaic gitleaks config"
|
||||
|
||||
[allowlist]
|
||||
description = "Global allowlist — skip files that never contain real secrets"
|
||||
paths = [
|
||||
'''node_modules/''',
|
||||
'''dist/''',
|
||||
'''build/''',
|
||||
'''\.next/''',
|
||||
'''\.nuxt/''',
|
||||
'''\.output/''',
|
||||
'''coverage/''',
|
||||
'''__pycache__/''',
|
||||
'''\.venv/''',
|
||||
'''vendor/''',
|
||||
'''pnpm-lock\.yaml$''',
|
||||
'''package-lock\.json$''',
|
||||
'''yarn\.lock$''',
|
||||
'''\.lock$''',
|
||||
'''\.snap$''',
|
||||
'''\.min\.js$''',
|
||||
'''\.min\.css$''',
|
||||
'''\.gitleaks\.toml$''',
|
||||
]
|
||||
stopwords = [
|
||||
"localhost",
|
||||
"127.0.0.1",
|
||||
"changeme",
|
||||
"placeholder",
|
||||
"example",
|
||||
"example.com",
|
||||
"test",
|
||||
"dummy",
|
||||
"fake",
|
||||
"sample",
|
||||
"your-",
|
||||
"xxx",
|
||||
"CHANGEME",
|
||||
"PLACEHOLDER",
|
||||
"TODO",
|
||||
"REPLACE_ME",
|
||||
]
|
||||
|
||||
# ──────────────────────────────────────────────
|
||||
# Custom rules — patterns the built-in rules miss
|
||||
# ──────────────────────────────────────────────
|
||||
|
||||
[[rules]]
|
||||
id = "database-url-with-credentials"
|
||||
description = "Database connection URL with embedded password"
|
||||
regex = '''(?i)(?:postgres(?:ql)?|mysql|mariadb|mongodb(?:\+srv)?|redis|amqp)://[^:\s]+:[^@\s]+@[^/\s]+'''
|
||||
tags = ["database", "connection-string"]
|
||||
[rules.allowlist]
|
||||
stopwords = ["localhost", "127.0.0.1", "changeme", "password", "example", "test_", "placeholder"]
|
||||
|
||||
[[rules]]
|
||||
id = "alembic-ini-sqlalchemy-url"
|
||||
description = "SQLAlchemy URL in alembic.ini with credentials"
|
||||
regex = '''sqlalchemy\.url\s*=\s*\S+://[^:\s]+:[^@\s]+@\S+'''
|
||||
paths = ['''alembic\.ini$''', '''\.ini$''']
|
||||
tags = ["python", "alembic", "database"]
|
||||
[rules.allowlist]
|
||||
stopwords = ["localhost", "127.0.0.1", "changeme", "driver://user:pass"]
|
||||
|
||||
[[rules]]
|
||||
id = "dotenv-secret-value"
|
||||
description = "High-entropy secret value in .env file"
|
||||
regex = '''(?i)(?:SECRET|TOKEN|PASSWORD|KEY|CREDENTIALS|AUTH)[\w]*\s*=\s*['"]?[A-Za-z0-9/+=]{20,}['"]?\s*$'''
|
||||
paths = ['''\.env$''', '''\.env\.\w+$''']
|
||||
tags = ["dotenv", "secret"]
|
||||
[rules.allowlist]
|
||||
stopwords = ["changeme", "placeholder", "example", "your_", "REPLACE", "TODO"]
|
||||
|
||||
[[rules]]
|
||||
id = "jdbc-url-with-password"
|
||||
description = "JDBC connection string with embedded password"
|
||||
regex = '''jdbc:[a-z]+://[^;\s]+password=[^;\s&]+'''
|
||||
tags = ["java", "jdbc", "database"]
|
||||
[rules.allowlist]
|
||||
stopwords = ["changeme", "placeholder", "example"]
|
||||
|
||||
[[rules]]
|
||||
id = "dsn-inline-password"
|
||||
description = "DSN-style connection string with inline password"
|
||||
regex = '''(?i)(?:dsn|connection_string|conn_str)\s*[:=]\s*\S+://[^:\s]+:[^@\s]+@\S+'''
|
||||
tags = ["database", "connection-string"]
|
||||
[rules.allowlist]
|
||||
stopwords = ["localhost", "127.0.0.1", "changeme", "example"]
|
||||
|
||||
[[rules]]
|
||||
id = "hardcoded-password-variable"
|
||||
description = "Hardcoded password assignment in source code"
|
||||
regex = '''(?i)(?:password|passwd|pwd)\s*[:=]\s*['"][^'"]{8,}['"]'''
|
||||
tags = ["password", "hardcoded"]
|
||||
[rules.allowlist]
|
||||
stopwords = ["changeme", "placeholder", "example", "test", "dummy", "password123", "your_password"]
|
||||
paths = [
|
||||
'''test[s]?/''',
|
||||
'''spec[s]?/''',
|
||||
'''__test__/''',
|
||||
'''fixture[s]?/''',
|
||||
'''mock[s]?/''',
|
||||
]
|
||||
|
||||
[[rules]]
|
||||
id = "bearer-token-in-code"
|
||||
description = "Hardcoded bearer token in source code"
|
||||
regex = '''(?i)['"]Bearer\s+[A-Za-z0-9\-._~+/]+=*['"]'''
|
||||
tags = ["auth", "bearer", "token"]
|
||||
[rules.allowlist]
|
||||
stopwords = ["example", "test", "dummy", "placeholder", "fake"]
|
||||
|
||||
[[rules]]
|
||||
id = "spring-application-properties-password"
|
||||
description = "Password in Spring Boot application properties"
|
||||
regex = '''(?i)spring\.\w+\.password\s*=\s*\S+'''
|
||||
paths = ['''application\.properties$''', '''application\.yml$''', '''application-\w+\.properties$''', '''application-\w+\.yml$''']
|
||||
tags = ["java", "spring", "password"]
|
||||
[rules.allowlist]
|
||||
stopwords = ["changeme", "placeholder", "${"]
|
||||
|
||||
[[rules]]
|
||||
id = "docker-compose-env-secret"
|
||||
description = "Hardcoded secret in docker-compose environment"
|
||||
regex = '''(?i)(?:POSTGRES_PASSWORD|MYSQL_ROOT_PASSWORD|MYSQL_PASSWORD|REDIS_PASSWORD|RABBITMQ_DEFAULT_PASS|MONGO_INITDB_ROOT_PASSWORD)\s*[:=]\s*['"]?[^\s'"$]{8,}['"]?'''
|
||||
paths = ['''compose\.ya?ml$''', '''docker-compose\.ya?ml$''']
|
||||
tags = ["docker", "compose", "secret"]
|
||||
[rules.allowlist]
|
||||
stopwords = ["changeme", "placeholder", "example", "${"]
|
||||
|
||||
[[rules]]
|
||||
id = "terraform-variable-secret"
|
||||
description = "Sensitive default value in Terraform variable"
|
||||
regex = '''(?i)default\s*=\s*"[^"]{8,}"'''
|
||||
paths = ['''variables\.tf$''', '''\.tf$''']
|
||||
tags = ["terraform", "secret"]
|
||||
[rules.allowlist]
|
||||
stopwords = ["changeme", "placeholder", "example", "TODO"]
|
||||
|
||||
[[rules]]
|
||||
id = "private-key-pem-inline"
|
||||
description = "PEM-encoded private key in source"
|
||||
regex = '''-----BEGIN\s+(?:RSA |EC |DSA |OPENSSH )?PRIVATE KEY-----'''
|
||||
tags = ["key", "pem", "private-key"]
|
||||
|
||||
[[rules]]
|
||||
id = "base64-encoded-secret"
|
||||
description = "Base64 value assigned to secret-named variable"
|
||||
regex = '''(?i)(?:secret|token|key|password|credentials)[\w]*\s*[:=]\s*['"]?[A-Za-z0-9+/]{40,}={0,2}['"]?'''
|
||||
tags = ["base64", "encoded", "secret"]
|
||||
[rules.allowlist]
|
||||
stopwords = ["changeme", "placeholder", "example", "test"]
|
||||
paths = [
|
||||
'''test[s]?/''',
|
||||
'''spec[s]?/''',
|
||||
'''fixture[s]?/''',
|
||||
]
|
||||
@@ -1,15 +1,2 @@
|
||||
npx lint-staged
|
||||
|
||||
# Secret scanning — gitleaks is REQUIRED (not optional like git-secrets was)
|
||||
if ! command -v gitleaks &>/dev/null; then
|
||||
echo ""
|
||||
echo "ERROR: gitleaks is not installed. Secret scanning is required."
|
||||
echo ""
|
||||
echo "Install:"
|
||||
echo " Linux: curl -sSfL https://github.com/gitleaks/gitleaks/releases/latest/download/gitleaks_8.24.0_linux_x64.tar.gz | sudo tar -xz -C /usr/local/bin gitleaks"
|
||||
echo " macOS: brew install gitleaks"
|
||||
echo " Windows: winget install gitleaks"
|
||||
echo ""
|
||||
exit 1
|
||||
fi
|
||||
gitleaks git --pre-commit --redact --staged --verbose
|
||||
npx git-secrets --scan || echo "Warning: git-secrets not installed"
|
||||
|
||||
@@ -4,19 +4,11 @@ when:
|
||||
|
||||
variables:
|
||||
- &node_image "node:20-alpine"
|
||||
- &gitleaks_image "ghcr.io/gitleaks/gitleaks:v8.24.0"
|
||||
- &install_deps |
|
||||
corepack enable
|
||||
npm ci --ignore-scripts
|
||||
|
||||
steps:
|
||||
# Secret scanning (runs in parallel with install, no deps)
|
||||
secret-scan:
|
||||
image: *gitleaks_image
|
||||
commands:
|
||||
- gitleaks git --redact --verbose --log-opts="HEAD~1..HEAD"
|
||||
depends_on: []
|
||||
|
||||
install:
|
||||
image: *node_image
|
||||
commands:
|
||||
@@ -73,4 +65,3 @@ steps:
|
||||
- typecheck
|
||||
- test
|
||||
- security-audit
|
||||
- secret-scan
|
||||
|
||||
@@ -1,15 +1,2 @@
|
||||
npx lint-staged
|
||||
|
||||
# Secret scanning — gitleaks is REQUIRED (not optional like git-secrets was)
|
||||
if ! command -v gitleaks &>/dev/null; then
|
||||
echo ""
|
||||
echo "ERROR: gitleaks is not installed. Secret scanning is required."
|
||||
echo ""
|
||||
echo "Install:"
|
||||
echo " Linux: curl -sSfL https://github.com/gitleaks/gitleaks/releases/latest/download/gitleaks_8.24.0_linux_x64.tar.gz | sudo tar -xz -C /usr/local/bin gitleaks"
|
||||
echo " macOS: brew install gitleaks"
|
||||
echo " Windows: winget install gitleaks"
|
||||
echo ""
|
||||
exit 1
|
||||
fi
|
||||
gitleaks git --pre-commit --redact --staged --verbose
|
||||
npx git-secrets --scan || echo "Warning: git-secrets not installed"
|
||||
|
||||
@@ -4,19 +4,11 @@ when:
|
||||
|
||||
variables:
|
||||
- &node_image "node:20-alpine"
|
||||
- &gitleaks_image "ghcr.io/gitleaks/gitleaks:v8.24.0"
|
||||
- &install_deps |
|
||||
corepack enable
|
||||
npm ci --ignore-scripts
|
||||
|
||||
steps:
|
||||
# Secret scanning (runs in parallel with install, no deps)
|
||||
secret-scan:
|
||||
image: *gitleaks_image
|
||||
commands:
|
||||
- gitleaks git --redact --verbose --log-opts="HEAD~1..HEAD"
|
||||
depends_on: []
|
||||
|
||||
install:
|
||||
image: *node_image
|
||||
commands:
|
||||
@@ -73,4 +65,3 @@ steps:
|
||||
- typecheck
|
||||
- test
|
||||
- security-audit
|
||||
- secret-scan
|
||||
|
||||
@@ -1,15 +1,2 @@
|
||||
npx lint-staged
|
||||
|
||||
# Secret scanning — gitleaks is REQUIRED (not optional like git-secrets was)
|
||||
if ! command -v gitleaks &>/dev/null; then
|
||||
echo ""
|
||||
echo "ERROR: gitleaks is not installed. Secret scanning is required."
|
||||
echo ""
|
||||
echo "Install:"
|
||||
echo " Linux: curl -sSfL https://github.com/gitleaks/gitleaks/releases/latest/download/gitleaks_8.24.0_linux_x64.tar.gz | sudo tar -xz -C /usr/local/bin gitleaks"
|
||||
echo " macOS: brew install gitleaks"
|
||||
echo " Windows: winget install gitleaks"
|
||||
echo ""
|
||||
exit 1
|
||||
fi
|
||||
gitleaks git --pre-commit --redact --staged --verbose
|
||||
npx git-secrets --scan || echo "Warning: git-secrets not installed"
|
||||
|
||||
@@ -6,19 +6,11 @@ when:
|
||||
|
||||
variables:
|
||||
- &node_image "node:20-alpine"
|
||||
- &gitleaks_image "ghcr.io/gitleaks/gitleaks:v8.24.0"
|
||||
- &install_deps |
|
||||
corepack enable
|
||||
npm ci --ignore-scripts
|
||||
|
||||
steps:
|
||||
# Secret scanning (runs in parallel with install, no deps)
|
||||
secret-scan:
|
||||
image: *gitleaks_image
|
||||
commands:
|
||||
- gitleaks git --redact --verbose --log-opts="HEAD~1..HEAD"
|
||||
depends_on: []
|
||||
|
||||
# Stage 1: Install
|
||||
install:
|
||||
image: *node_image
|
||||
@@ -72,4 +64,3 @@ steps:
|
||||
- typecheck
|
||||
- test
|
||||
- security-audit
|
||||
- secret-scan
|
||||
|
||||
@@ -1,50 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# _lib.sh — Shared helpers for Woodpecker CI tool scripts
|
||||
#
|
||||
# Usage: source "$(dirname "${BASH_SOURCE[0]}")/_lib.sh"
|
||||
#
|
||||
# Requires: WOODPECKER_URL and WOODPECKER_TOKEN to be set (via load_credentials)
|
||||
|
||||
# Resolve owner/repo name to numeric repo ID (required by Woodpecker v3 API)
|
||||
# Usage: REPO_ID=$(wp_resolve_repo_id "owner/repo")
|
||||
wp_resolve_repo_id() {
|
||||
local full_name="$1"
|
||||
local response http_code body repo_id
|
||||
|
||||
response=$(curl -sk -w "\n%{http_code}" \
|
||||
-H "Authorization: Bearer $WOODPECKER_TOKEN" \
|
||||
"${WOODPECKER_URL}/api/repos/lookup/${full_name}")
|
||||
|
||||
http_code=$(echo "$response" | tail -n1)
|
||||
body=$(echo "$response" | sed '$d')
|
||||
|
||||
if [[ "$http_code" != "200" ]]; then
|
||||
echo "Error: Failed to look up repo '${full_name}' (HTTP $http_code)" >&2
|
||||
if echo "$body" | jq -e '.message' &>/dev/null; then
|
||||
echo " $(echo "$body" | jq -r '.message')" >&2
|
||||
fi
|
||||
return 1
|
||||
fi
|
||||
|
||||
repo_id=$(echo "$body" | jq -r '.id // empty')
|
||||
if [[ -z "$repo_id" ]]; then
|
||||
echo "Error: Repo lookup returned no ID for '${full_name}'" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
echo "$repo_id"
|
||||
}
|
||||
|
||||
# Auto-detect repo name from git remote origin
|
||||
# Usage: REPO=$(wp_detect_repo)
|
||||
wp_detect_repo() {
|
||||
local remote_url
|
||||
remote_url=$(git remote get-url origin 2>/dev/null || true)
|
||||
if [[ -n "$remote_url" ]]; then
|
||||
echo "$remote_url" | sed -E 's|\.git$||' | sed -E 's|.*[:/]([^/]+/[^/]+)$|\1|'
|
||||
else
|
||||
echo "Error: -r owner/repo required (not in a git repository)" >&2
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
@@ -2,55 +2,62 @@
|
||||
#
|
||||
# pipeline-list.sh — List Woodpecker CI pipelines
|
||||
#
|
||||
# Usage: pipeline-list.sh [-r owner/repo] [-l limit] [-f format] [-a instance]
|
||||
# Usage: pipeline-list.sh [-r owner/repo] [-l limit] [-f format]
|
||||
#
|
||||
# Options:
|
||||
# -r repo Repository in owner/repo format (default: current repo)
|
||||
# -l limit Number of pipelines to show (default: 20)
|
||||
# -f format Output format: table (default), json
|
||||
# -a instance Woodpecker instance name (e.g. usc, mosaic)
|
||||
# -h Show this help
|
||||
# -r repo Repository in owner/repo format (default: current repo)
|
||||
# -l limit Number of pipelines to show (default: 20)
|
||||
# -f format Output format: table (default), json
|
||||
# -h Show this help
|
||||
#
|
||||
# Requires: woodpecker credentials in credentials.json
|
||||
# Requires: woodpecker.url and woodpecker.token in credentials.json
|
||||
set -euo pipefail
|
||||
|
||||
MOSAIC_HOME="${MOSAIC_HOME:-$HOME/.config/mosaic}"
|
||||
source "$MOSAIC_HOME/tools/_lib/credentials.sh"
|
||||
source "$(dirname "${BASH_SOURCE[0]}")/_lib.sh"
|
||||
|
||||
# Check if woodpecker credentials exist before loading
|
||||
CRED_FILE="${MOSAIC_CREDENTIALS_FILE:-$HOME/src/jarvis-brain/credentials.json}"
|
||||
if ! jq -e '.woodpecker.token // empty | select(. != "")' "$CRED_FILE" &>/dev/null; then
|
||||
echo "Error: Woodpecker API token not configured in credentials.json" >&2
|
||||
echo "" >&2
|
||||
echo "To configure:" >&2
|
||||
echo " 1. Get your token from Woodpecker CI → User Settings → API" >&2
|
||||
echo " 2. Add to credentials.json:" >&2
|
||||
echo ' "woodpecker": {"url": "https://ci.mosaicstack.dev", "token": "YOUR_TOKEN"}' >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
load_credentials woodpecker
|
||||
|
||||
REPO=""
|
||||
LIMIT=20
|
||||
FORMAT="table"
|
||||
WP_INSTANCE=""
|
||||
|
||||
while getopts "r:l:f:a:h" opt; do
|
||||
while getopts "r:l:f:h" opt; do
|
||||
case $opt in
|
||||
r) REPO="$OPTARG" ;;
|
||||
l) LIMIT="$OPTARG" ;;
|
||||
f) FORMAT="$OPTARG" ;;
|
||||
a) WP_INSTANCE="$OPTARG" ;;
|
||||
h) head -14 "$0" | grep "^#" | sed 's/^# \?//'; exit 0 ;;
|
||||
*) echo "Usage: $0 [-r owner/repo] [-l limit] [-f format] [-a instance]" >&2; exit 1 ;;
|
||||
*) echo "Usage: $0 [-r owner/repo] [-l limit] [-f format]" >&2; exit 1 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ -n "$WP_INSTANCE" ]]; then
|
||||
load_credentials "woodpecker-${WP_INSTANCE}"
|
||||
else
|
||||
load_credentials woodpecker
|
||||
fi
|
||||
|
||||
# Auto-detect repo from git remote if not specified
|
||||
if [[ -z "$REPO" ]]; then
|
||||
REPO=$(wp_detect_repo) || exit 1
|
||||
remote_url=$(git remote get-url origin 2>/dev/null || true)
|
||||
if [[ -n "$remote_url" ]]; then
|
||||
REPO=$(echo "$remote_url" | sed -E 's|.*[:/]([^/]+/[^/]+?)(\.git)?$|\1|')
|
||||
else
|
||||
echo "Error: -r owner/repo required (not in a git repository)" >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# Resolve owner/repo to numeric ID (Woodpecker v3 API)
|
||||
REPO_ID=$(wp_resolve_repo_id "$REPO") || exit 1
|
||||
|
||||
response=$(curl -sk -w "\n%{http_code}" \
|
||||
-H "Authorization: Bearer $WOODPECKER_TOKEN" \
|
||||
"${WOODPECKER_URL}/api/repos/${REPO_ID}/pipelines?per_page=${LIMIT}")
|
||||
"${WOODPECKER_URL}/api/repos/${REPO}/pipelines?per_page=${LIMIT}")
|
||||
|
||||
http_code=$(echo "$response" | tail -n1)
|
||||
body=$(echo "$response" | sed '$d')
|
||||
|
||||
@@ -2,78 +2,76 @@
|
||||
#
|
||||
# pipeline-status.sh — Check Woodpecker CI pipeline status
|
||||
#
|
||||
# Usage: pipeline-status.sh [-r owner/repo] [-n number] [-f format] [-a instance]
|
||||
# Usage: pipeline-status.sh [-r owner/repo] [-n number] [-f format]
|
||||
#
|
||||
# Options:
|
||||
# -r repo Repository in owner/repo format (default: current repo)
|
||||
# -n number Pipeline number (default: latest)
|
||||
# -f format Output format: table (default), json
|
||||
# -a instance Woodpecker instance name (e.g. usc, mosaic)
|
||||
# -h Show this help
|
||||
# -r repo Repository in owner/repo format (default: current repo)
|
||||
# -n number Pipeline number (default: latest)
|
||||
# -f format Output format: table (default), json
|
||||
# -h Show this help
|
||||
#
|
||||
# Requires: woodpecker credentials in credentials.json
|
||||
# Requires: woodpecker.url and woodpecker.token in credentials.json
|
||||
set -euo pipefail
|
||||
|
||||
MOSAIC_HOME="${MOSAIC_HOME:-$HOME/.config/mosaic}"
|
||||
source "$MOSAIC_HOME/tools/_lib/credentials.sh"
|
||||
source "$(dirname "${BASH_SOURCE[0]}")/_lib.sh"
|
||||
|
||||
CRED_FILE="${MOSAIC_CREDENTIALS_FILE:-$HOME/src/jarvis-brain/credentials.json}"
|
||||
if ! jq -e '.woodpecker.token // empty | select(. != "")' "$CRED_FILE" &>/dev/null; then
|
||||
echo "Error: Woodpecker API token not configured in credentials.json" >&2
|
||||
echo "See: ~/.config/mosaic/tools/woodpecker/README.md" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
load_credentials woodpecker
|
||||
|
||||
REPO=""
|
||||
NUMBER=""
|
||||
FORMAT="table"
|
||||
WP_INSTANCE=""
|
||||
|
||||
while getopts "r:n:f:a:h" opt; do
|
||||
while getopts "r:n:f:h" opt; do
|
||||
case $opt in
|
||||
r) REPO="$OPTARG" ;;
|
||||
n) NUMBER="$OPTARG" ;;
|
||||
f) FORMAT="$OPTARG" ;;
|
||||
a) WP_INSTANCE="$OPTARG" ;;
|
||||
h) head -14 "$0" | grep "^#" | sed 's/^# \?//'; exit 0 ;;
|
||||
*) echo "Usage: $0 [-r owner/repo] [-n number] [-f format] [-a instance]" >&2; exit 1 ;;
|
||||
*) echo "Usage: $0 [-r owner/repo] [-n number] [-f format]" >&2; exit 1 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ -n "$WP_INSTANCE" ]]; then
|
||||
load_credentials "woodpecker-${WP_INSTANCE}"
|
||||
else
|
||||
load_credentials woodpecker
|
||||
fi
|
||||
|
||||
if [[ -z "$REPO" ]]; then
|
||||
REPO=$(wp_detect_repo) || exit 1
|
||||
fi
|
||||
|
||||
# Resolve owner/repo to numeric ID (Woodpecker v3 API)
|
||||
REPO_ID=$(wp_resolve_repo_id "$REPO") || exit 1
|
||||
|
||||
_wp_fetch() {
|
||||
local ep="$1"
|
||||
local resp http_code body
|
||||
resp=$(curl -sk -w "\n%{http_code}" \
|
||||
-H "Authorization: Bearer $WOODPECKER_TOKEN" \
|
||||
"$ep")
|
||||
http_code=$(echo "$resp" | tail -n1)
|
||||
body=$(echo "$resp" | sed '$d')
|
||||
if [[ "$http_code" != "200" ]]; then
|
||||
echo "Error: HTTP $http_code from $ep" >&2
|
||||
return 1
|
||||
fi
|
||||
echo "$body"
|
||||
}
|
||||
|
||||
if [[ -z "$NUMBER" ]]; then
|
||||
# Get latest pipeline number from list, then fetch full detail
|
||||
list_body=$(_wp_fetch "${WOODPECKER_URL}/api/repos/${REPO_ID}/pipelines?per_page=1") || exit 1
|
||||
NUMBER=$(echo "$list_body" | jq -r '.[0].number // empty')
|
||||
if [[ -z "$NUMBER" ]]; then
|
||||
echo "Error: No pipelines found" >&2
|
||||
remote_url=$(git remote get-url origin 2>/dev/null || true)
|
||||
if [[ -n "$remote_url" ]]; then
|
||||
REPO=$(echo "$remote_url" | sed -E 's|.*[:/]([^/]+/[^/]+?)(\.git)?$|\1|')
|
||||
else
|
||||
echo "Error: -r owner/repo required (not in a git repository)" >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# Always fetch the single-pipeline endpoint (includes workflows/steps)
|
||||
body=$(_wp_fetch "${WOODPECKER_URL}/api/repos/${REPO_ID}/pipelines/${NUMBER}") || exit 1
|
||||
if [[ -z "$NUMBER" ]]; then
|
||||
# Get latest pipeline
|
||||
endpoint="${WOODPECKER_URL}/api/repos/${REPO}/pipelines?per_page=1"
|
||||
else
|
||||
endpoint="${WOODPECKER_URL}/api/repos/${REPO}/pipelines/${NUMBER}"
|
||||
fi
|
||||
|
||||
response=$(curl -sk -w "\n%{http_code}" \
|
||||
-H "Authorization: Bearer $WOODPECKER_TOKEN" \
|
||||
"$endpoint")
|
||||
|
||||
http_code=$(echo "$response" | tail -n1)
|
||||
body=$(echo "$response" | sed '$d')
|
||||
|
||||
if [[ "$http_code" != "200" ]]; then
|
||||
echo "Error: Failed to get pipeline status (HTTP $http_code)" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# If we got a list, extract the first one
|
||||
if [[ -z "$NUMBER" ]]; then
|
||||
body=$(echo "$body" | jq '.[0]')
|
||||
fi
|
||||
|
||||
if [[ "$FORMAT" == "json" ]]; then
|
||||
echo "$body" | jq '.'
|
||||
@@ -83,7 +81,6 @@ fi
|
||||
echo "Pipeline Status"
|
||||
echo "==============="
|
||||
echo "$body" | jq -r '
|
||||
def ts: if . and . > 0 then todate else "—" end;
|
||||
" Number: \(.number)\n" +
|
||||
" Status: \(.status)\n" +
|
||||
" Branch: \(.branch)\n" +
|
||||
@@ -91,28 +88,6 @@ echo "$body" | jq -r '
|
||||
" Commit: \(.commit[:12])\n" +
|
||||
" Message: \(.message | split("\n")[0])\n" +
|
||||
" Author: \(.author)\n" +
|
||||
" Started: \(.started | ts)\n" +
|
||||
" Finished: \(.finished | ts)"
|
||||
" Started: \(.started_at // "pending")\n" +
|
||||
" Finished: \(.finished_at // "running")"
|
||||
'
|
||||
|
||||
# Show step-level details if workflows exist
|
||||
has_workflows=$(echo "$body" | jq 'has("workflows") and (.workflows | length > 0)')
|
||||
if [[ "$has_workflows" == "true" ]]; then
|
||||
echo ""
|
||||
echo "Steps"
|
||||
echo "-----"
|
||||
echo "$body" | jq -r '
|
||||
.workflows[] | .children[]? |
|
||||
select(.type != "clone") |
|
||||
" " +
|
||||
(if .state == "success" then "OK"
|
||||
elif .state == "failure" then "FAIL"
|
||||
elif .state == "running" then "RUN"
|
||||
elif .state == "skipped" then "SKIP"
|
||||
elif .state == "pending" then "WAIT"
|
||||
else .state end) +
|
||||
" " + .name +
|
||||
(if .error and .error != "" then " (" + .error + ")" else "" end) +
|
||||
(if .exit_code and .exit_code != 0 then " [exit " + (.exit_code | tostring) + "]" else "" end)
|
||||
'
|
||||
fi
|
||||
|
||||
@@ -2,55 +2,57 @@
|
||||
#
|
||||
# pipeline-trigger.sh — Trigger a Woodpecker CI pipeline
|
||||
#
|
||||
# Usage: pipeline-trigger.sh [-r owner/repo] [-b branch] [-a instance]
|
||||
# Usage: pipeline-trigger.sh [-r owner/repo] [-b branch]
|
||||
#
|
||||
# Options:
|
||||
# -r repo Repository in owner/repo format (default: current repo)
|
||||
# -b branch Branch to build (default: main)
|
||||
# -a instance Woodpecker instance name (e.g. usc, mosaic)
|
||||
# -h Show this help
|
||||
# -r repo Repository in owner/repo format (default: current repo)
|
||||
# -b branch Branch to build (default: main)
|
||||
# -h Show this help
|
||||
#
|
||||
# Requires: woodpecker credentials in credentials.json
|
||||
# Requires: woodpecker.url and woodpecker.token in credentials.json
|
||||
set -euo pipefail
|
||||
|
||||
MOSAIC_HOME="${MOSAIC_HOME:-$HOME/.config/mosaic}"
|
||||
source "$MOSAIC_HOME/tools/_lib/credentials.sh"
|
||||
source "$(dirname "${BASH_SOURCE[0]}")/_lib.sh"
|
||||
|
||||
CRED_FILE="${MOSAIC_CREDENTIALS_FILE:-$HOME/src/jarvis-brain/credentials.json}"
|
||||
if ! jq -e '.woodpecker.token // empty | select(. != "")' "$CRED_FILE" &>/dev/null; then
|
||||
echo "Error: Woodpecker API token not configured in credentials.json" >&2
|
||||
echo "See: ~/.config/mosaic/tools/woodpecker/README.md" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
load_credentials woodpecker
|
||||
|
||||
REPO=""
|
||||
BRANCH="main"
|
||||
WP_INSTANCE=""
|
||||
|
||||
while getopts "r:b:a:h" opt; do
|
||||
while getopts "r:b:h" opt; do
|
||||
case $opt in
|
||||
r) REPO="$OPTARG" ;;
|
||||
b) BRANCH="$OPTARG" ;;
|
||||
a) WP_INSTANCE="$OPTARG" ;;
|
||||
h) head -14 "$0" | grep "^#" | sed 's/^# \?//'; exit 0 ;;
|
||||
*) echo "Usage: $0 [-r owner/repo] [-b branch] [-a instance]" >&2; exit 1 ;;
|
||||
h) head -12 "$0" | grep "^#" | sed 's/^# \?//'; exit 0 ;;
|
||||
*) echo "Usage: $0 [-r owner/repo] [-b branch]" >&2; exit 1 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ -n "$WP_INSTANCE" ]]; then
|
||||
load_credentials "woodpecker-${WP_INSTANCE}"
|
||||
else
|
||||
load_credentials woodpecker
|
||||
fi
|
||||
|
||||
if [[ -z "$REPO" ]]; then
|
||||
REPO=$(wp_detect_repo) || exit 1
|
||||
remote_url=$(git remote get-url origin 2>/dev/null || true)
|
||||
if [[ -n "$remote_url" ]]; then
|
||||
REPO=$(echo "$remote_url" | sed -E 's|.*[:/]([^/]+/[^/]+?)(\.git)?$|\1|')
|
||||
else
|
||||
echo "Error: -r owner/repo required (not in a git repository)" >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# Resolve owner/repo to numeric ID (Woodpecker v3 API)
|
||||
REPO_ID=$(wp_resolve_repo_id "$REPO") || exit 1
|
||||
|
||||
echo "Triggering pipeline for $REPO on branch $BRANCH..."
|
||||
|
||||
response=$(curl -sk -w "\n%{http_code}" -X POST \
|
||||
-H "Authorization: Bearer $WOODPECKER_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "$(jq -n --arg b "$BRANCH" '{branch: $b}')" \
|
||||
"${WOODPECKER_URL}/api/repos/${REPO_ID}/pipelines")
|
||||
"${WOODPECKER_URL}/api/repos/${REPO}/pipelines")
|
||||
|
||||
http_code=$(echo "$response" | tail -n1)
|
||||
body=$(echo "$response" | sed '$d')
|
||||
|
||||
Reference in New Issue
Block a user