Compare commits
25 Commits
feature/to
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9fbfdcee6d | ||
|
|
21afb58b33 | ||
| 09786ee6e0 | |||
| 1fd67b9ec0 | |||
| 38223c8ec2 | |||
|
|
8de2f7439a | ||
|
|
98b9bc3c93 | ||
| b1403703b1 | |||
|
|
abead17e0e | ||
|
|
fbf74c2736 | ||
|
|
364d6c2278 | ||
|
|
93efbcdafe | ||
|
|
def9c2fd7a | ||
|
|
87501ea952 | ||
|
|
7a5f28c8b5 | ||
|
|
405bc4c797 | ||
|
|
c9bf578396 | ||
| c1f4830bf5 | |||
| e5c4bf25b3 | |||
| a9623e9219 | |||
| 5d666bdca9 | |||
| 221afe94d9 | |||
| 612796d8e0 | |||
| 5ba531e2d0 | |||
| a8e580e1a3 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1 +1,2 @@
|
||||
node_modules/
|
||||
rails
|
||||
|
||||
33
AGENTS.md
33
AGENTS.md
@@ -30,10 +30,13 @@ If any required file is missing, you MUST stop and report the missing file.
|
||||
3. Routine repository operations are NOT escalation triggers. Use escalation triggers only from this contract.
|
||||
4. For source-code delivery, completion is forbidden at PR-open stage.
|
||||
5. Completion requires merged PR to `main` + terminal green CI + linked issue/internal task closed.
|
||||
6. Before push or merge, you MUST run queue guard: `~/.config/mosaic/rails/git/ci-queue-wait.sh --purpose push|merge`.
|
||||
7. For issue/PR/milestone operations, you MUST use Mosaic wrappers first (`~/.config/mosaic/rails/git/*.sh`).
|
||||
6. Before push or merge, you MUST run queue guard: `~/.config/mosaic/tools/git/ci-queue-wait.sh --purpose push|merge`.
|
||||
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
|
||||
|
||||
@@ -63,7 +66,7 @@ If any required file is missing, you MUST stop and report the missing file.
|
||||
24. Deployment ownership is REQUIRED when deployment is in scope and target access is configured.
|
||||
25. For container deployments, you MUST use immutable image tags (`sha-*`, `vX.Y.Z-rc.N`) with digest-first promotion; `latest` is forbidden as a deployment reference.
|
||||
26. If an external git provider is available (Gitea/GitHub/GitLab), you MUST create or update issue(s) and link them in `docs/TASKS.md` before coding; if unavailable, use `TASKS:<id>` internal refs in `docs/TASKS.md`.
|
||||
27. For provider operations (issue/PR/milestone), you MUST detect platform first and use `~/.config/mosaic/rails/git/*.sh` wrappers before any raw provider CLI/API calls.
|
||||
27. For provider operations (issue/PR/milestone), you MUST detect platform first and use `~/.config/mosaic/tools/git/*.sh` wrappers before any raw provider CLI/API calls.
|
||||
28. Direct `gh`/`tea`/`glab` commands are forbidden as first choice when a Mosaic wrapper exists; use raw commands only as documented fallback.
|
||||
29. If the mission is orchestration-oriented (contains "orchestrate", issue/milestone coordination, or multi-task execution), you MUST load and follow `~/.config/mosaic/guides/ORCHESTRATOR.md` before taking action.
|
||||
30. At session start, you MUST declare the operating mode in your first response before any tool calls or implementation steps.
|
||||
@@ -72,7 +75,8 @@ If any required file is missing, you MUST stop and report the missing file.
|
||||
33. For explicit review-only missions, the first line MUST be exactly: `Now initiating Review mode...`
|
||||
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/rails/git/ci-queue-wait.sh --purpose push|merge`.
|
||||
36. Before push or PR merge, you MUST run CI queue guard and wait if the project has running/queued pipelines: `~/.config/mosaic/tools/git/ci-queue-wait.sh --purpose push|merge`.
|
||||
37. When an active mission is detected at session start (MISSION-MANIFEST.md, TASKS.md, or scratchpads/ present), you MUST load `~/.config/mosaic/guides/ORCHESTRATOR-PROTOCOL.md` and follow the Session Resume Protocol before taking any action.
|
||||
|
||||
## Mode Declaration Protocol (Hard Rule)
|
||||
|
||||
@@ -112,6 +116,7 @@ Load additional guides when the task requires them.
|
||||
| QA and test strategy | `~/.config/mosaic/guides/QA-TESTING.md` |
|
||||
| Secrets and vault usage | `~/.config/mosaic/guides/VAULT-SECRETS.md` |
|
||||
| Orchestrator estimation heuristics | `~/.config/mosaic/guides/ORCHESTRATOR-LEARNINGS.md` |
|
||||
| Mission lifecycle / multi-session orchestration | `~/.config/mosaic/guides/ORCHESTRATOR-PROTOCOL.md` |
|
||||
|
||||
## Embedded Delivery Cycle (Hard Rule)
|
||||
|
||||
@@ -125,6 +130,26 @@ 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.
|
||||
|
||||
@@ -24,19 +24,19 @@ Scope:
|
||||
|
||||
### MF-001 (QA rails path correction)
|
||||
Updated:
|
||||
- `rails/qa/qa-hook-wrapper.sh`
|
||||
- `rails/qa/qa-hook-stdin.sh`
|
||||
- `rails/qa/qa-hook-handler.sh`
|
||||
- `rails/qa/remediation-hook-handler.sh`
|
||||
- `rails/qa/qa-queue-monitor.sh`
|
||||
- `tools/qa/qa-hook-wrapper.sh`
|
||||
- `tools/qa/qa-hook-stdin.sh`
|
||||
- `tools/qa/qa-hook-handler.sh`
|
||||
- `tools/qa/remediation-hook-handler.sh`
|
||||
- `tools/qa/qa-queue-monitor.sh`
|
||||
|
||||
Change:
|
||||
- Standardized handler paths to `~/.config/mosaic/rails/qa/...`.
|
||||
- Standardized handler paths to `~/.config/mosaic/tools/qa/...`.
|
||||
|
||||
### MF-002 + MF-003 (conditional loading/context detection)
|
||||
Updated:
|
||||
- `rails/bootstrap/agent-lint.sh`
|
||||
- `rails/bootstrap/agent-upgrade.sh`
|
||||
- `tools/bootstrap/agent-lint.sh`
|
||||
- `tools/bootstrap/agent-upgrade.sh`
|
||||
- `templates/agent/SPEC.md`
|
||||
|
||||
Change:
|
||||
@@ -58,7 +58,7 @@ Updated:
|
||||
- `skills/pr-reviewer/SKILL.md`
|
||||
|
||||
Change:
|
||||
- Replaced all `~/.claude/scripts/git/...` with `~/.config/mosaic/rails/git/...`.
|
||||
- Replaced all `~/.claude/scripts/git/...` with `~/.config/mosaic/tools/git/...`.
|
||||
- Replaced `~/.claude/skills/...` with `~/.config/mosaic/skills/...`.
|
||||
|
||||
### MF-006 (worktree skill docs hierarchy)
|
||||
@@ -109,7 +109,7 @@ These are required to support existing Claude runtime integration while keeping
|
||||
Executed checks:
|
||||
- `rg -n "~/.claude|\\.claude/|agent-guides" ~/src/agent-skills -S`
|
||||
- Result: no matches after remediation.
|
||||
- `rg -n "~/.config/mosaic/rails/(qa-hook|remediation-hook|qa-queue-monitor)" ~/src/mosaic-bootstrap -S`
|
||||
- `rg -n "~/.config/mosaic/tools/(qa-hook|remediation-hook|qa-queue-monitor)" ~/src/mosaic-bootstrap -S`
|
||||
- Result: no invalid old-style QA rail paths remain.
|
||||
- Installed runtime validation:
|
||||
- `~/.config/mosaic` contains `rails/git`, `rails/portainer`, `rails/cicd`, `skills`, and `bin` tooling.
|
||||
- `~/.config/mosaic` contains `tools/git`, `tools/portainer`, `tools/cicd`, `skills`, and `bin` tooling.
|
||||
|
||||
@@ -103,7 +103,7 @@ You can still launch runtimes directly (`claude`, `codex`, etc.) — thin runtim
|
||||
├── bin/ ← CLI tools (mosaic, mosaic-init, mosaic-doctor, etc.)
|
||||
├── dist/ ← Bundled wizard (mosaic-wizard.mjs)
|
||||
├── guides/ ← Operational guides
|
||||
├── rails/ ← Quality rails, git scripts, portainer scripts
|
||||
├── tools/ ← Tool suites: git, portainer, authentik, coolify, codex, etc.
|
||||
├── runtime/ ← Runtime adapters + runtime-specific references
|
||||
│ ├── claude/CLAUDE.md
|
||||
│ ├── claude/RUNTIME.md
|
||||
|
||||
12
STANDARDS.md
12
STANDARDS.md
@@ -12,16 +12,16 @@ Master/slave model:
|
||||
2. Load project-local `AGENTS.md` next.
|
||||
3. Respect repository-specific tooling and workflows.
|
||||
4. Use lifecycle scripts when available (`scripts/agent/*.sh`).
|
||||
5. Use shared rails/guides from `~/.config/mosaic` as canonical references.
|
||||
5. Use shared tools/guides from `~/.config/mosaic` as canonical references.
|
||||
|
||||
## Non-Negotiables
|
||||
|
||||
- Data files are authoritative; generated views are derived artifacts.
|
||||
- Pull before edits when collaborating in shared repos.
|
||||
- Run validation checks before claiming completion.
|
||||
- Apply quality rails from `~/.config/mosaic/rails/` when relevant (review, QA, git workflow).
|
||||
- For project-level mechanical enforcement templates, use `~/.config/mosaic/rails/quality/` via `~/.config/mosaic/bin/mosaic-quality-apply`.
|
||||
- For runtime-agnostic delegation/orchestration, use `~/.config/mosaic/rails/orchestrator-matrix/` with repo-local `.mosaic/orchestrator/` state.
|
||||
- Apply quality tools from `~/.config/mosaic/tools/` when relevant (review, QA, git workflow).
|
||||
- For project-level mechanical enforcement templates, use `~/.config/mosaic/tools/quality/` via `~/.config/mosaic/bin/mosaic-quality-apply`.
|
||||
- For runtime-agnostic delegation/orchestration, use `~/.config/mosaic/tools/orchestrator-matrix/` with repo-local `.mosaic/orchestrator/` state.
|
||||
- Avoid hardcoded secrets and token leakage in remotes/commits.
|
||||
- Do not perform destructive git/file actions without explicit instruction.
|
||||
- Browser automation (Playwright, Cypress, Puppeteer) MUST run in headless mode. Never launch a visible browser — it collides with the user's display and active session.
|
||||
@@ -50,10 +50,10 @@ All runtime adapters should inject:
|
||||
|
||||
before task execution.
|
||||
|
||||
Runtime-compatible guides and rails are hosted at:
|
||||
Runtime-compatible guides and tools are hosted at:
|
||||
|
||||
- `~/.config/mosaic/guides/`
|
||||
- `~/.config/mosaic/rails/`
|
||||
- `~/.config/mosaic/tools/`
|
||||
- `~/.config/mosaic/profiles/` (runtime-neutral domain/workflow/stack presets)
|
||||
- `~/.config/mosaic/runtime/` (runtime-specific overlays)
|
||||
- `~/.config/mosaic/skills-local/` (local private skills shared across runtimes)
|
||||
|
||||
138
TOOLS.md
138
TOOLS.md
@@ -3,34 +3,139 @@
|
||||
Centralized reference for tools, credentials, and CLI patterns available across all projects.
|
||||
Project-specific tooling belongs in the project's `AGENTS.md`, not here.
|
||||
|
||||
## Mosaic Git Wrappers (Use First)
|
||||
All tool suites are located at `~/.config/mosaic/tools/`.
|
||||
|
||||
Mosaic wrappers at `~/.config/mosaic/rails/git/*.sh` handle platform detection and edge cases. Always use these before raw CLI commands.
|
||||
## Tool Suites
|
||||
|
||||
### Git Wrappers (Use First)
|
||||
|
||||
Mosaic wrappers at `~/.config/mosaic/tools/git/*.sh` handle platform detection and edge cases. Always use these before raw CLI commands.
|
||||
|
||||
```bash
|
||||
# Issues
|
||||
~/.config/mosaic/rails/git/issue-create.sh
|
||||
~/.config/mosaic/rails/git/issue-close.sh
|
||||
~/.config/mosaic/tools/git/issue-create.sh
|
||||
~/.config/mosaic/tools/git/issue-close.sh
|
||||
|
||||
# PRs
|
||||
~/.config/mosaic/rails/git/pr-create.sh
|
||||
~/.config/mosaic/rails/git/pr-merge.sh
|
||||
~/.config/mosaic/tools/git/pr-create.sh
|
||||
~/.config/mosaic/tools/git/pr-merge.sh
|
||||
|
||||
# Milestones
|
||||
~/.config/mosaic/rails/git/milestone-create.sh
|
||||
~/.config/mosaic/tools/git/milestone-create.sh
|
||||
|
||||
# CI queue guard (required before push/merge)
|
||||
~/.config/mosaic/rails/git/ci-queue-wait.sh --purpose push|merge
|
||||
~/.config/mosaic/tools/git/ci-queue-wait.sh --purpose push|merge
|
||||
```
|
||||
|
||||
## Code Review (Codex)
|
||||
### Code Review (Codex)
|
||||
|
||||
```bash
|
||||
# Code quality review
|
||||
~/.config/mosaic/rails/codex/codex-code-review.sh --uncommitted
|
||||
~/.config/mosaic/tools/codex/codex-code-review.sh --uncommitted
|
||||
~/.config/mosaic/tools/codex/codex-security-review.sh --uncommitted
|
||||
```
|
||||
|
||||
# Security review
|
||||
~/.config/mosaic/rails/codex/codex-security-review.sh --uncommitted
|
||||
### Infrastructure — Portainer
|
||||
|
||||
```bash
|
||||
~/.config/mosaic/tools/portainer/stack-status.sh -n <stack-name>
|
||||
~/.config/mosaic/tools/portainer/stack-redeploy.sh -n <stack-name>
|
||||
~/.config/mosaic/tools/portainer/stack-list.sh
|
||||
~/.config/mosaic/tools/portainer/endpoint-list.sh
|
||||
```
|
||||
|
||||
### Infrastructure — Coolify
|
||||
|
||||
```bash
|
||||
~/.config/mosaic/tools/coolify/project-list.sh
|
||||
~/.config/mosaic/tools/coolify/service-list.sh
|
||||
~/.config/mosaic/tools/coolify/service-status.sh -u <uuid>
|
||||
~/.config/mosaic/tools/coolify/deploy.sh -u <uuid>
|
||||
~/.config/mosaic/tools/coolify/env-set.sh -u <uuid> -k KEY -v VALUE
|
||||
```
|
||||
|
||||
### Identity — Authentik
|
||||
|
||||
```bash
|
||||
~/.config/mosaic/tools/authentik/user-list.sh
|
||||
~/.config/mosaic/tools/authentik/user-create.sh -u <username> -n <name> -e <email>
|
||||
~/.config/mosaic/tools/authentik/group-list.sh
|
||||
~/.config/mosaic/tools/authentik/app-list.sh
|
||||
~/.config/mosaic/tools/authentik/flow-list.sh
|
||||
~/.config/mosaic/tools/authentik/admin-status.sh
|
||||
```
|
||||
|
||||
### 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]
|
||||
```
|
||||
|
||||
### IT Service — GLPI
|
||||
|
||||
```bash
|
||||
~/.config/mosaic/tools/glpi/ticket-list.sh
|
||||
~/.config/mosaic/tools/glpi/ticket-create.sh -t <title> -c <content>
|
||||
~/.config/mosaic/tools/glpi/computer-list.sh
|
||||
~/.config/mosaic/tools/glpi/user-list.sh
|
||||
```
|
||||
|
||||
### Health Check
|
||||
|
||||
```bash
|
||||
# Check all configured services
|
||||
~/.config/mosaic/tools/health/stack-health.sh
|
||||
|
||||
# Check a specific service
|
||||
~/.config/mosaic/tools/health/stack-health.sh -s portainer
|
||||
|
||||
# JSON output for automation
|
||||
~/.config/mosaic/tools/health/stack-health.sh -f json
|
||||
```
|
||||
|
||||
### Shared Credential Loader
|
||||
|
||||
```bash
|
||||
# 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
|
||||
```
|
||||
|
||||
## Git Providers
|
||||
@@ -42,16 +147,13 @@ Mosaic wrappers at `~/.config/mosaic/rails/git/*.sh` handle platform detection a
|
||||
## Credentials
|
||||
|
||||
**Location:** (configure your credential file path)
|
||||
**Loader:** `source ~/.config/mosaic/tools/_lib/credentials.sh && load_credentials <service>`
|
||||
|
||||
**Never expose actual values. Never commit credential files.**
|
||||
|
||||
## CLI Gotchas
|
||||
|
||||
(Add platform-specific CLI gotchas as you discover them. Examples: TTY requirements, default list limits, API fallback patterns.)
|
||||
|
||||
## Custom Tools
|
||||
|
||||
(Add any machine-specific tools, scripts, or workflows here.)
|
||||
(Add platform-specific CLI gotchas as you discover them.)
|
||||
|
||||
## Safety Defaults
|
||||
|
||||
|
||||
@@ -14,4 +14,4 @@ Use wrapper commands from `~/.config/mosaic/bin/` for lifecycle rituals.
|
||||
## Migration Note
|
||||
|
||||
Project-local `.claude/commands/*.md` should call `scripts/agent/*.sh` so behavior stays runtime-neutral.
|
||||
Guides and rails should resolve to `~/.config/mosaic/guides` and `~/.config/mosaic/rails` (linked into `~/.claude` for compatibility).
|
||||
Guides and tools should resolve to `~/.config/mosaic/guides` and `~/.config/mosaic/tools` (linked into `~/.claude` for compatibility).
|
||||
|
||||
397
bin/mosaic
397
bin/mosaic
@@ -51,6 +51,22 @@ 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
|
||||
@@ -130,6 +146,78 @@ 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)
|
||||
|
||||
@@ -179,6 +267,63 @@ 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
|
||||
@@ -187,11 +332,23 @@ 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")"
|
||||
echo "[mosaic] Launching Claude Code..."
|
||||
exec claude --append-system-prompt "$runtime_prompt" "$@"
|
||||
|
||||
# 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
|
||||
}
|
||||
|
||||
launch_opencode() {
|
||||
@@ -201,8 +358,12 @@ 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 "$@"
|
||||
}
|
||||
@@ -214,10 +375,20 @@ launch_codex() {
|
||||
check_runtime "codex"
|
||||
check_sequential_thinking "codex"
|
||||
|
||||
_check_resumable_session
|
||||
|
||||
# Codex reads from ~/.codex/instructions.md
|
||||
ensure_runtime_config "codex" "$HOME/.codex/instructions.md"
|
||||
echo "[mosaic] Launching Codex..."
|
||||
exec codex "$@"
|
||||
_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
|
||||
}
|
||||
|
||||
launch_yolo() {
|
||||
@@ -241,8 +412,17 @@ launch_yolo() {
|
||||
# Claude uses an explicit dangerous permissions flag.
|
||||
local runtime_prompt
|
||||
runtime_prompt="$(build_runtime_prompt "claude")"
|
||||
echo "[mosaic] Launching Claude Code in YOLO mode (dangerous permissions enabled)..."
|
||||
exec claude --dangerously-skip-permissions --append-system-prompt "$runtime_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 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
|
||||
;;
|
||||
codex)
|
||||
check_mosaic_home
|
||||
@@ -253,8 +433,16 @@ launch_yolo() {
|
||||
|
||||
# Codex reads instructions.md from ~/.codex and supports a direct dangerous flag.
|
||||
ensure_runtime_config "codex" "$HOME/.codex/instructions.md"
|
||||
echo "[mosaic] Launching Codex in YOLO mode (dangerous permissions enabled)..."
|
||||
exec codex --dangerously-bypass-approvals-and-sandbox "$@"
|
||||
_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
|
||||
;;
|
||||
opencode)
|
||||
check_mosaic_home
|
||||
@@ -265,6 +453,8 @@ 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 "$@"
|
||||
;;
|
||||
@@ -325,6 +515,195 @@ 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" "$@"
|
||||
@@ -397,6 +776,8 @@ 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 "$@" ;;
|
||||
|
||||
@@ -90,10 +90,10 @@ bash scripts/agent/critical.sh
|
||||
bash scripts/agent/session-end.sh
|
||||
```
|
||||
|
||||
## Shared Rails
|
||||
## Shared Tools
|
||||
|
||||
- Quality and orchestration guides: `~/.config/mosaic/guides/`
|
||||
- Shared automation rails: `~/.config/mosaic/rails/`
|
||||
- Shared automation tools: `~/.config/mosaic/tools/`
|
||||
|
||||
## Repo-Specific Notes
|
||||
|
||||
@@ -108,7 +108,7 @@ fi
|
||||
|
||||
echo "[mosaic] Repo bootstrap complete: $TARGET_DIR"
|
||||
echo "[mosaic] Next: edit $TARGET_DIR/.mosaic/repo-hooks.sh with project workflows"
|
||||
echo "[mosaic] Optional: apply quality rails via ~/.config/mosaic/bin/mosaic-quality-apply --template <template> --target $TARGET_DIR"
|
||||
echo "[mosaic] Optional: apply quality tools via ~/.config/mosaic/bin/mosaic-quality-apply --template <template> --target $TARGET_DIR"
|
||||
echo "[mosaic] Optional: run orchestrator rail via ~/.config/mosaic/bin/mosaic-orchestrator-drain"
|
||||
echo "[mosaic] Optional: run detached orchestrator via bash $TARGET_DIR/scripts/agent/orchestrator-daemon.sh start"
|
||||
|
||||
@@ -119,8 +119,8 @@ if [[ -n "$QUALITY_TEMPLATE" ]]; then
|
||||
sed -i "s/^enabled:.*/enabled: true/" "$TARGET_DIR/.mosaic/quality-rails.yml"
|
||||
sed -i "s/^template:.*/template: \"$QUALITY_TEMPLATE\"/" "$TARGET_DIR/.mosaic/quality-rails.yml"
|
||||
fi
|
||||
echo "[mosaic] Applied quality rails template: $QUALITY_TEMPLATE"
|
||||
echo "[mosaic] Applied quality tools template: $QUALITY_TEMPLATE"
|
||||
else
|
||||
echo "[mosaic] WARN: mosaic-quality-apply not found; skipping quality rails apply" >&2
|
||||
echo "[mosaic] WARN: mosaic-quality-apply not found; skipping quality tools apply" >&2
|
||||
fi
|
||||
fi
|
||||
|
||||
@@ -149,9 +149,9 @@ expect_file "$MOSAIC_HOME/STANDARDS.md"
|
||||
expect_file "$MOSAIC_HOME/USER.md"
|
||||
expect_file "$MOSAIC_HOME/TOOLS.md"
|
||||
expect_dir "$MOSAIC_HOME/guides"
|
||||
expect_dir "$MOSAIC_HOME/rails"
|
||||
expect_dir "$MOSAIC_HOME/rails/quality"
|
||||
expect_dir "$MOSAIC_HOME/rails/orchestrator-matrix"
|
||||
expect_dir "$MOSAIC_HOME/tools"
|
||||
expect_dir "$MOSAIC_HOME/tools/quality"
|
||||
expect_dir "$MOSAIC_HOME/tools/orchestrator-matrix"
|
||||
expect_dir "$MOSAIC_HOME/profiles"
|
||||
expect_dir "$MOSAIC_HOME/templates/agent"
|
||||
expect_dir "$MOSAIC_HOME/skills"
|
||||
@@ -168,10 +168,18 @@ expect_file "$MOSAIC_HOME/bin/mosaic-orchestrator-drain"
|
||||
expect_file "$MOSAIC_HOME/bin/mosaic-orchestrator-matrix-publish"
|
||||
expect_file "$MOSAIC_HOME/bin/mosaic-orchestrator-matrix-consume"
|
||||
expect_file "$MOSAIC_HOME/bin/mosaic-orchestrator-matrix-cycle"
|
||||
expect_file "$MOSAIC_HOME/rails/git/ci-queue-wait.sh"
|
||||
expect_file "$MOSAIC_HOME/rails/git/pr-ci-wait.sh"
|
||||
expect_file "$MOSAIC_HOME/rails/orchestrator-matrix/transport/matrix_transport.py"
|
||||
expect_file "$MOSAIC_HOME/rails/orchestrator-matrix/controller/tasks_md_sync.py"
|
||||
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"
|
||||
|
||||
@@ -138,9 +138,9 @@ Write-Host "[mosaic-doctor] Mosaic home: $MosaicHome"
|
||||
# Canonical Mosaic checks
|
||||
Expect-File (Join-Path $MosaicHome "STANDARDS.md")
|
||||
Expect-Dir (Join-Path $MosaicHome "guides")
|
||||
Expect-Dir (Join-Path $MosaicHome "rails")
|
||||
Expect-Dir (Join-Path $MosaicHome "rails\quality")
|
||||
Expect-Dir (Join-Path $MosaicHome "rails\orchestrator-matrix")
|
||||
Expect-Dir (Join-Path $MosaicHome "tools")
|
||||
Expect-Dir (Join-Path $MosaicHome "tools\quality")
|
||||
Expect-Dir (Join-Path $MosaicHome "tools\orchestrator-matrix")
|
||||
Expect-Dir (Join-Path $MosaicHome "profiles")
|
||||
Expect-Dir (Join-Path $MosaicHome "templates\agent")
|
||||
Expect-Dir (Join-Path $MosaicHome "skills")
|
||||
@@ -157,11 +157,11 @@ Expect-File (Join-Path $MosaicHome "bin\mosaic-orchestrator-drain")
|
||||
Expect-File (Join-Path $MosaicHome "bin\mosaic-orchestrator-matrix-publish")
|
||||
Expect-File (Join-Path $MosaicHome "bin\mosaic-orchestrator-matrix-consume")
|
||||
Expect-File (Join-Path $MosaicHome "bin\mosaic-orchestrator-matrix-cycle")
|
||||
Expect-File (Join-Path $MosaicHome "rails\git\ci-queue-wait.ps1")
|
||||
Expect-File (Join-Path $MosaicHome "rails\git\ci-queue-wait.sh")
|
||||
Expect-File (Join-Path $MosaicHome "rails\git\pr-ci-wait.sh")
|
||||
Expect-File (Join-Path $MosaicHome "rails\orchestrator-matrix\transport\matrix_transport.py")
|
||||
Expect-File (Join-Path $MosaicHome "rails\orchestrator-matrix\controller\tasks_md_sync.py")
|
||||
Expect-File (Join-Path $MosaicHome "tools\git\ci-queue-wait.ps1")
|
||||
Expect-File (Join-Path $MosaicHome "tools\git\ci-queue-wait.sh")
|
||||
Expect-File (Join-Path $MosaicHome "tools\git\pr-ci-wait.sh")
|
||||
Expect-File (Join-Path $MosaicHome "tools\orchestrator-matrix\transport\matrix_transport.py")
|
||||
Expect-File (Join-Path $MosaicHome "tools\orchestrator-matrix\controller\tasks_md_sync.py")
|
||||
Expect-File (Join-Path $MosaicHome "runtime\mcp\SEQUENTIAL-THINKING.json")
|
||||
Expect-File (Join-Path $MosaicHome "runtime\claude\RUNTIME.md")
|
||||
Expect-File (Join-Path $MosaicHome "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
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
set -euo pipefail
|
||||
|
||||
MOSAIC_HOME="${MOSAIC_HOME:-$HOME/.config/mosaic}"
|
||||
BRIDGE="$MOSAIC_HOME/rails/orchestrator-matrix/transport/matrix_transport.py"
|
||||
BRIDGE="$MOSAIC_HOME/tools/orchestrator-matrix/transport/matrix_transport.py"
|
||||
|
||||
if [[ ! -f "$BRIDGE" ]]; then
|
||||
echo "[mosaic-orch-matrix] missing transport bridge: $BRIDGE" >&2
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
set -euo pipefail
|
||||
|
||||
MOSAIC_HOME="${MOSAIC_HOME:-$HOME/.config/mosaic}"
|
||||
BRIDGE="$MOSAIC_HOME/rails/orchestrator-matrix/transport/matrix_transport.py"
|
||||
BRIDGE="$MOSAIC_HOME/tools/orchestrator-matrix/transport/matrix_transport.py"
|
||||
|
||||
if [[ ! -f "$BRIDGE" ]]; then
|
||||
echo "[mosaic-orch-matrix] missing transport bridge: $BRIDGE" >&2
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
set -euo pipefail
|
||||
|
||||
MOSAIC_HOME="${MOSAIC_HOME:-$HOME/.config/mosaic}"
|
||||
CTRL="$MOSAIC_HOME/rails/orchestrator-matrix/controller/mosaic_orchestrator.py"
|
||||
CTRL="$MOSAIC_HOME/tools/orchestrator-matrix/controller/mosaic_orchestrator.py"
|
||||
|
||||
if [[ ! -f "$CTRL" ]]; then
|
||||
echo "[mosaic-orchestrator] missing controller: $CTRL" >&2
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
set -euo pipefail
|
||||
|
||||
MOSAIC_HOME="${MOSAIC_HOME:-$HOME/.config/mosaic}"
|
||||
SYNC="$MOSAIC_HOME/rails/orchestrator-matrix/controller/tasks_md_sync.py"
|
||||
SYNC="$MOSAIC_HOME/tools/orchestrator-matrix/controller/tasks_md_sync.py"
|
||||
|
||||
if [[ ! -f "$SYNC" ]]; then
|
||||
echo "[mosaic-orchestrator-sync] missing sync script: $SYNC" >&2
|
||||
|
||||
@@ -9,7 +9,7 @@ usage() {
|
||||
cat <<USAGE
|
||||
Usage: $(basename "$0") --template <name> [--target <dir>]
|
||||
|
||||
Apply Mosaic quality rails templates into a project.
|
||||
Apply Mosaic quality tools templates into a project.
|
||||
|
||||
Templates:
|
||||
typescript-node
|
||||
@@ -55,7 +55,7 @@ if [[ ! -d "$TARGET_DIR" ]]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
SCRIPT="$MOSAIC_HOME/rails/quality/scripts/install.sh"
|
||||
SCRIPT="$MOSAIC_HOME/tools/quality/scripts/install.sh"
|
||||
if [[ ! -x "$SCRIPT" ]]; then
|
||||
echo "[mosaic-quality] Missing install script: $SCRIPT" >&2
|
||||
exit 1
|
||||
|
||||
@@ -39,7 +39,7 @@ if [[ ! -d "$TARGET_DIR" ]]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
SCRIPT="$MOSAIC_HOME/rails/quality/scripts/verify.sh"
|
||||
SCRIPT="$MOSAIC_HOME/tools/quality/scripts/verify.sh"
|
||||
if [[ ! -x "$SCRIPT" ]]; then
|
||||
echo "[mosaic-quality] Missing verify script: $SCRIPT" >&2
|
||||
exit 1
|
||||
|
||||
114
bin/mosaic.ps1
114
bin/mosaic.ps1
@@ -96,6 +96,88 @@ 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")]
|
||||
@@ -130,8 +212,14 @@ 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"
|
||||
}
|
||||
|
||||
@@ -170,7 +258,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" {
|
||||
@@ -191,8 +279,15 @@ function Invoke-Yolo {
|
||||
Assert-Runtime "codex"
|
||||
Assert-SequentialThinking
|
||||
Ensure-RuntimeConfig -Runtime "codex" -Dst (Join-Path $env:USERPROFILE ".codex\instructions.md")
|
||||
Write-Host "[mosaic] Launching Codex in YOLO mode (dangerous permissions enabled)..."
|
||||
& codex --dangerously-bypass-approvals-and-sandbox @tail
|
||||
$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
|
||||
}
|
||||
return
|
||||
}
|
||||
"opencode" {
|
||||
@@ -219,7 +314,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" {
|
||||
@@ -252,8 +347,15 @@ switch ($command) {
|
||||
Assert-SequentialThinking
|
||||
# Codex reads from ~/.codex/instructions.md
|
||||
Ensure-RuntimeConfig -Runtime "codex" -Dst (Join-Path $env:USERPROFILE ".codex\instructions.md")
|
||||
Write-Host "[mosaic] Launching Codex..."
|
||||
& codex @remaining
|
||||
$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
|
||||
}
|
||||
}
|
||||
"yolo" {
|
||||
Invoke-Yolo -YoloArgs $remaining
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# Authentication & Authorization Guide
|
||||
|
||||
## Before Starting
|
||||
1. Check assigned issue: `~/.config/mosaic/rails/git/issue-list.sh -a @me`
|
||||
1. Check assigned issue: `~/.config/mosaic/tools/git/issue-list.sh -a @me`
|
||||
2. Review existing auth implementation in codebase
|
||||
3. Review Vault secrets structure: `docs/vault-secrets-structure.md`
|
||||
|
||||
@@ -115,6 +115,41 @@ class TestAuthentication:
|
||||
pass
|
||||
```
|
||||
|
||||
## Authentik SSO Administration
|
||||
|
||||
Authentik is the identity provider for the Mosaic Stack. Use the Authentik tool suite for administration.
|
||||
|
||||
### Tool Suite
|
||||
|
||||
```bash
|
||||
# System health
|
||||
~/.config/mosaic/tools/authentik/admin-status.sh
|
||||
|
||||
# User management
|
||||
~/.config/mosaic/tools/authentik/user-list.sh
|
||||
~/.config/mosaic/tools/authentik/user-create.sh -u <username> -n <name> -e <email>
|
||||
|
||||
# Group and app management
|
||||
~/.config/mosaic/tools/authentik/group-list.sh
|
||||
~/.config/mosaic/tools/authentik/app-list.sh
|
||||
~/.config/mosaic/tools/authentik/flow-list.sh
|
||||
```
|
||||
|
||||
### Registering an OAuth Application
|
||||
|
||||
1. Create an OAuth2 provider in Authentik admin (Applications > Providers)
|
||||
2. Create an application linked to the provider (Applications > Applications)
|
||||
3. Configure redirect URIs for the application
|
||||
4. Store client_id and client_secret in Vault: `secret-{env}/{service}/oauth/authentik/`
|
||||
5. Verify with: `~/.config/mosaic/tools/authentik/app-list.sh`
|
||||
|
||||
### API Reference
|
||||
|
||||
- Base URL: `https://auth.diversecanvas.com`
|
||||
- API prefix: `/api/v3/`
|
||||
- OpenAPI schema: `/api/v3/schema/`
|
||||
- Auth: Bearer token (obtained via `auth-token.sh`)
|
||||
|
||||
## Common Vulnerabilities to Avoid
|
||||
|
||||
1. **Broken Authentication**
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# Backend Development Guide
|
||||
|
||||
## Before Starting
|
||||
1. Check assigned issue: `~/.config/mosaic/rails/git/issue-list.sh -a @me`
|
||||
1. Check assigned issue: `~/.config/mosaic/tools/git/issue-list.sh -a @me`
|
||||
2. Create scratchpad: `docs/scratchpads/{issue-number}-{short-name}.md`
|
||||
3. Review API contracts and database schema
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ This guide covers how to bootstrap a project so AI agents (Claude, Codex, etc.)
|
||||
|
||||
```bash
|
||||
# Automated bootstrap (recommended)
|
||||
~/.config/mosaic/rails/bootstrap/init-project.sh \
|
||||
~/.config/mosaic/tools/bootstrap/init-project.sh \
|
||||
--name "my-project" \
|
||||
--type "nestjs-nextjs" \
|
||||
--repo "https://git.mosaicstack.dev/owner/repo"
|
||||
@@ -240,10 +240,10 @@ Documentation root hygiene (HARD RULE):
|
||||
|
||||
```bash
|
||||
# Use the init script
|
||||
~/.config/mosaic/rails/bootstrap/init-repo-labels.sh
|
||||
~/.config/mosaic/tools/bootstrap/init-repo-labels.sh
|
||||
|
||||
# Or manually create standard labels
|
||||
~/.config/mosaic/rails/git/issue-create.sh # (labels are created on first use)
|
||||
~/.config/mosaic/tools/git/issue-create.sh # (labels are created on first use)
|
||||
```
|
||||
|
||||
### Standard Labels
|
||||
@@ -264,10 +264,10 @@ Create the first pre-MVP milestone at `0.0.1`.
|
||||
Reserve `0.1.0` for the MVP release milestone.
|
||||
|
||||
```bash
|
||||
~/.config/mosaic/rails/git/milestone-create.sh -t "0.0.1" -d "Pre-MVP - Foundation Sprint"
|
||||
~/.config/mosaic/tools/git/milestone-create.sh -t "0.0.1" -d "Pre-MVP - Foundation Sprint"
|
||||
|
||||
# Create when MVP scope is complete and release-ready:
|
||||
~/.config/mosaic/rails/git/milestone-create.sh -t "0.1.0" -d "MVP - Minimum Viable Product"
|
||||
~/.config/mosaic/tools/git/milestone-create.sh -t "0.1.0" -d "MVP - Minimum Viable Product"
|
||||
```
|
||||
|
||||
---
|
||||
@@ -293,8 +293,8 @@ This enforces one merge strategy across human and agent workflows.
|
||||
```bash
|
||||
# Copy Codex review pipeline
|
||||
mkdir -p .woodpecker/schemas
|
||||
cp ~/.config/mosaic/rails/codex/woodpecker/codex-review.yml .woodpecker/
|
||||
cp ~/.config/mosaic/rails/codex/schemas/*.json .woodpecker/schemas/
|
||||
cp ~/.config/mosaic/tools/codex/woodpecker/codex-review.yml .woodpecker/
|
||||
cp ~/.config/mosaic/tools/codex/schemas/*.json .woodpecker/schemas/
|
||||
|
||||
# Add codex_api_key secret to Woodpecker CI dashboard
|
||||
```
|
||||
@@ -366,7 +366,7 @@ fi
|
||||
# (execute the command block under "Quality Gates")
|
||||
|
||||
# Test Codex review (if configured)
|
||||
~/.config/mosaic/rails/codex/codex-code-review.sh --help
|
||||
~/.config/mosaic/tools/codex/codex-code-review.sh --help
|
||||
|
||||
# Verify sequential-thinking MCP remains configured
|
||||
~/.config/mosaic/bin/mosaic-ensure-sequential-thinking --check
|
||||
@@ -434,7 +434,7 @@ fi
|
||||
Full project bootstrap with interactive and flag-based modes:
|
||||
|
||||
```bash
|
||||
~/.config/mosaic/rails/bootstrap/init-project.sh \
|
||||
~/.config/mosaic/tools/bootstrap/init-project.sh \
|
||||
--name "My Project" \
|
||||
--type "nestjs-nextjs" \
|
||||
--repo "https://git.mosaicstack.dev/owner/repo" \
|
||||
@@ -447,7 +447,7 @@ Full project bootstrap with interactive and flag-based modes:
|
||||
Initialize standard labels and the first pre-MVP milestone:
|
||||
|
||||
```bash
|
||||
~/.config/mosaic/rails/bootstrap/init-repo-labels.sh
|
||||
~/.config/mosaic/tools/bootstrap/init-repo-labels.sh
|
||||
```
|
||||
|
||||
---
|
||||
@@ -483,4 +483,4 @@ After bootstrapping, verify:
|
||||
- [ ] `.env.example` exists (if project uses env vars)
|
||||
- [ ] CI/CD pipeline configured (if using Woodpecker/GitHub Actions)
|
||||
- [ ] Python publish path configured in CI (if project ships Python packages)
|
||||
- [ ] Codex review scripts accessible (`~/.config/mosaic/rails/codex/`)
|
||||
- [ ] Codex review scripts accessible (`~/.config/mosaic/tools/codex/`)
|
||||
|
||||
@@ -870,7 +870,7 @@ Required sequence:
|
||||
1. Merge PR to `main` (squash) via Mosaic wrapper.
|
||||
2. Monitor CI to terminal status:
|
||||
```bash
|
||||
~/.config/mosaic/rails/git/pr-ci-wait.sh -n <PR_NUMBER>
|
||||
~/.config/mosaic/tools/git/pr-ci-wait.sh -n <PR_NUMBER>
|
||||
```
|
||||
3. Require green status before claiming completion.
|
||||
4. If CI fails, create remediation task(s) and continue until green.
|
||||
@@ -885,8 +885,8 @@ Woodpecker note:
|
||||
Before pushing a branch or merging a PR, guard against overlapping project pipelines:
|
||||
|
||||
```bash
|
||||
~/.config/mosaic/rails/git/ci-queue-wait.sh --purpose push -B main
|
||||
~/.config/mosaic/rails/git/ci-queue-wait.sh --purpose merge -B main
|
||||
~/.config/mosaic/tools/git/ci-queue-wait.sh --purpose push -B main
|
||||
~/.config/mosaic/tools/git/ci-queue-wait.sh --purpose merge -B main
|
||||
```
|
||||
|
||||
Behavior:
|
||||
@@ -12,7 +12,7 @@ Merge strategy enforcement (HARD RULE):
|
||||
- PR target for delivery is `main`.
|
||||
- Direct pushes to `main` are prohibited.
|
||||
- Merge to `main` MUST be squash-only.
|
||||
- Use `~/.config/mosaic/rails/git/pr-merge.sh -n {PR_NUMBER} -m squash` (or PowerShell equivalent).
|
||||
- Use `~/.config/mosaic/tools/git/pr-merge.sh -n {PR_NUMBER} -m squash` (or PowerShell equivalent).
|
||||
|
||||
## Review Checklist
|
||||
|
||||
@@ -101,7 +101,7 @@ Use `~/.config/mosaic/templates/docs/DOCUMENTATION-CHECKLIST.md` whenever code/A
|
||||
### Getting Context
|
||||
```bash
|
||||
# List the issue being addressed
|
||||
~/.config/mosaic/rails/git/issue-list.sh -i {issue-number}
|
||||
~/.config/mosaic/tools/git/issue-list.sh -i {issue-number}
|
||||
|
||||
# View the changes
|
||||
git diff main...HEAD
|
||||
|
||||
@@ -25,8 +25,8 @@ First response MUST declare mode before tool calls or implementation steps:
|
||||
|
||||
1. For non-trivial work, `docs/TASKS.md` MUST exist before coding.
|
||||
2. If `docs/TASKS.md` is missing, create it from `~/.config/mosaic/templates/docs/TASKS.md.template`.
|
||||
3. Detect provider first via `~/.config/mosaic/rails/git/detect-platform.sh`.
|
||||
4. For issue/PR/milestone operations, use Mosaic wrappers first (`~/.config/mosaic/rails/git/*.sh`).
|
||||
3. Detect provider first via `~/.config/mosaic/tools/git/detect-platform.sh`.
|
||||
4. For issue/PR/milestone operations, use Mosaic wrappers first (`~/.config/mosaic/tools/git/*.sh`).
|
||||
5. If external git provider is available (Gitea/GitHub/GitLab), create or update issue(s) before coding.
|
||||
6. Record provider issue reference(s) in `docs/TASKS.md` (example: `#123`).
|
||||
7. If no external provider is available, use internal task refs in `docs/TASKS.md` (example: `TASKS:T1`).
|
||||
@@ -34,16 +34,19 @@ 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. 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:
|
||||
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:
|
||||
- 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.
|
||||
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.
|
||||
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.
|
||||
|
||||
## 2a. Steered Autonomy (Lights-Out)
|
||||
|
||||
@@ -73,11 +76,11 @@ For implementation work, you MUST run this cycle in order:
|
||||
5. `remediate` - fix all findings and any test failures.
|
||||
6. `review` - re-review remediated changes until blockers are cleared.
|
||||
7. `commit` - commit only when the logical unit passes tests and review.
|
||||
8. `pre-push queue guard` - before pushing, wait for running/queued project pipelines to clear: `~/.config/mosaic/rails/git/ci-queue-wait.sh --purpose push`.
|
||||
8. `pre-push queue guard` - before pushing, wait for running/queued project pipelines to clear: `~/.config/mosaic/tools/git/ci-queue-wait.sh --purpose push`.
|
||||
9. `push` - push immediately after queue guard passes.
|
||||
10. `PR integration` - if external git provider is available, create/update PR to `main` and merge with required strategy via Mosaic wrappers.
|
||||
11. `pre-merge queue guard` - before merging PR, wait for running/queued project pipelines to clear: `~/.config/mosaic/rails/git/ci-queue-wait.sh --purpose merge`.
|
||||
12. `CI/pipeline verification` - wait for terminal CI status and require green before completion (`~/.config/mosaic/rails/git/pr-ci-wait.sh` for PR-based workflow).
|
||||
11. `pre-merge queue guard` - before merging PR, wait for running/queued project pipelines to clear: `~/.config/mosaic/tools/git/ci-queue-wait.sh --purpose merge`.
|
||||
12. `CI/pipeline verification` - wait for terminal CI status and require green before completion (`~/.config/mosaic/tools/git/pr-ci-wait.sh` for PR-based workflow).
|
||||
13. `issue closure` - close linked external issue (or close internal `docs/TASKS.md` task ref when provider is unavailable).
|
||||
14. `greenfield situational test` - validate required user flows in a clean environment/startup path (post-merge for trunk workflow changes).
|
||||
15. `deploy + post-deploy validation` - when deployment is in scope, deploy to configured target and run post-deploy health/smoke checks.
|
||||
@@ -85,20 +88,27 @@ For implementation work, you MUST run this cycle in order:
|
||||
|
||||
### Post-PR Hard Gate (Execute Sequentially, No Exceptions)
|
||||
|
||||
1. `~/.config/mosaic/rails/git/ci-queue-wait.sh --purpose merge -B main`
|
||||
2. `~/.config/mosaic/rails/git/pr-merge.sh -n <PR_NUMBER> -m squash`
|
||||
3. `~/.config/mosaic/rails/git/pr-ci-wait.sh -n <PR_NUMBER>`
|
||||
4. `~/.config/mosaic/rails/git/issue-close.sh -i <ISSUE_NUMBER>` (or close internal `docs/TASKS.md` ref when no provider exists)
|
||||
1. `~/.config/mosaic/tools/git/ci-queue-wait.sh --purpose merge -B main`
|
||||
2. `~/.config/mosaic/tools/git/pr-merge.sh -n <PR_NUMBER> -m squash`
|
||||
3. `~/.config/mosaic/tools/git/pr-ci-wait.sh -n <PR_NUMBER>`
|
||||
4. `~/.config/mosaic/tools/git/issue-close.sh -i <ISSUE_NUMBER>` (or close internal `docs/TASKS.md` ref when no provider exists)
|
||||
5. If any step fails: set status `blocked`, report the exact failed wrapper command, and stop.
|
||||
6. Do not ask the human to perform routine merge/close operations.
|
||||
7. Do not claim completion before step 4 succeeds.
|
||||
|
||||
### 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.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# Frontend Development Guide
|
||||
|
||||
## Before Starting
|
||||
1. Check assigned issue in git repo: `~/.config/mosaic/rails/git/issue-list.sh -a @me`
|
||||
1. Check assigned issue in git repo: `~/.config/mosaic/tools/git/issue-list.sh -a @me`
|
||||
2. Create scratchpad: `docs/scratchpads/{issue-number}-{short-name}.md`
|
||||
3. Review existing components and patterns in the codebase
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# Infrastructure & DevOps Guide
|
||||
|
||||
## Before Starting
|
||||
1. Check assigned issue: `~/.config/mosaic/rails/git/issue-list.sh -a @me`
|
||||
1. Check assigned issue: `~/.config/mosaic/tools/git/issue-list.sh -a @me`
|
||||
2. Create scratchpad: `docs/scratchpads/{issue-number}-{short-name}.md`
|
||||
3. Review existing infrastructure configuration
|
||||
|
||||
@@ -97,10 +97,10 @@ readinessProbe:
|
||||
periodSeconds: 3
|
||||
```
|
||||
|
||||
## CI/CD Pipelines
|
||||
|
||||
### Pipeline Stages
|
||||
1. **Lint**: Code style and static analysis
|
||||
## CI/CD Pipelines
|
||||
|
||||
### Pipeline Stages
|
||||
1. **Lint**: Code style and static analysis
|
||||
2. **Test**: Unit and integration tests
|
||||
3. **Build**: Compile and package
|
||||
4. **Scan**: Security and vulnerability scanning
|
||||
@@ -109,65 +109,165 @@ readinessProbe:
|
||||
### Pipeline Security
|
||||
- Use secrets management (not hardcoded)
|
||||
- Pin action/image versions
|
||||
- Implement approval gates for production
|
||||
- Audit pipeline access
|
||||
|
||||
## Steered-Autonomous Deployment (Hard Rule)
|
||||
|
||||
In lights-out mode, the agent owns deployment end-to-end when deployment is in scope.
|
||||
The human is escalation-only for missing access, hard policy conflicts, or irreversible risk.
|
||||
|
||||
### Deployment Target Selection
|
||||
|
||||
1. Use explicit target from `docs/PRD.md` / `docs/PRD.json` or `docs/DEPLOYMENT.md`.
|
||||
2. If unspecified, infer from existing project config/integration.
|
||||
3. If multiple targets exist, choose the target already wired in CI/CD and document rationale.
|
||||
|
||||
### Supported Targets
|
||||
|
||||
- **Portainer**: Deploy via configured stack webhook/API, then verify service health and container status.
|
||||
- **Coolify**: Trigger deployment via Coolify API/webhook, then verify deployment status and endpoint health.
|
||||
- **Vercel**: Deploy via `vercel` CLI or connected Git integration, then verify preview/production URL health.
|
||||
- **Other SaaS providers**: Use provider CLI/API/runbook with the same validation and rollback gates.
|
||||
|
||||
### Image Tagging and Promotion (Hard Rule)
|
||||
|
||||
For containerized deployments:
|
||||
|
||||
1. Build immutable image tags: `sha-<shortsha>` and `v{base-version}-rc.{build}`.
|
||||
2. Use mutable environment tags only as pointers: `testing`, optional `staging`, and `prod`.
|
||||
3. Deploy by immutable digest, not by mutable tag alone.
|
||||
4. Promote the exact tested digest between environments (no rebuild between testing and prod).
|
||||
5. Do not use `latest` or `dev` as deployment references.
|
||||
|
||||
Blue-green is the default strategy for production promotion.
|
||||
Canary is allowed only when automated SLO/error-rate gates and auto-rollback triggers are implemented.
|
||||
|
||||
### Post-Deploy Validation (REQUIRED)
|
||||
|
||||
1. Health endpoints return expected status.
|
||||
2. Critical smoke tests pass in target environment.
|
||||
3. Running version and digest match the promoted release candidate.
|
||||
4. Observability signals (errors/latency) are within expected thresholds.
|
||||
|
||||
### Rollback Rule
|
||||
|
||||
If post-deploy validation fails:
|
||||
|
||||
1. Execute rollback/redeploy-safe path immediately.
|
||||
2. Mark deployment as blocked in `docs/TASKS.md`.
|
||||
3. Record failure evidence and next remediation step in scratchpad and release notes.
|
||||
|
||||
### Registry Retention and Cleanup
|
||||
|
||||
Cleanup MUST be automated.
|
||||
|
||||
- Keep all final release tags (`vX.Y.Z`) indefinitely.
|
||||
- Keep active environment digests (`prod`, `testing`, and active blue/green slots).
|
||||
- Keep recent RC tags (`vX.Y.Z-rc.N`) based on retention window.
|
||||
- Remove stale `sha-*` and RC tags outside retention window if they are not actively deployed.
|
||||
|
||||
## Monitoring & Logging
|
||||
- Implement approval gates for production
|
||||
- Audit pipeline access
|
||||
|
||||
## Steered-Autonomous Deployment (Hard Rule)
|
||||
|
||||
In lights-out mode, the agent owns deployment end-to-end when deployment is in scope.
|
||||
The human is escalation-only for missing access, hard policy conflicts, or irreversible risk.
|
||||
|
||||
### Deployment Target Selection
|
||||
|
||||
1. Use explicit target from `docs/PRD.md` / `docs/PRD.json` or `docs/DEPLOYMENT.md`.
|
||||
2. If unspecified, infer from existing project config/integration.
|
||||
3. If multiple targets exist, choose the target already wired in CI/CD and document rationale.
|
||||
|
||||
### Supported Targets
|
||||
|
||||
- **Portainer**: Deploy via `~/.config/mosaic/tools/portainer/stack-redeploy.sh`, then verify with `stack-status.sh`.
|
||||
- **Coolify**: Deploy via `~/.config/mosaic/tools/coolify/deploy.sh -u <uuid>`, then verify with `service-status.sh`.
|
||||
- **Vercel**: Deploy via `vercel` CLI or connected Git integration, then verify preview/production URL health.
|
||||
- **Other SaaS providers**: Use provider CLI/API/runbook with the same validation and rollback gates.
|
||||
|
||||
### Coolify API Operations
|
||||
|
||||
```bash
|
||||
# List projects and services
|
||||
~/.config/mosaic/tools/coolify/project-list.sh
|
||||
~/.config/mosaic/tools/coolify/service-list.sh
|
||||
|
||||
# Check service status
|
||||
~/.config/mosaic/tools/coolify/service-status.sh -u <uuid>
|
||||
|
||||
# Set env vars (takes effect on next deploy)
|
||||
~/.config/mosaic/tools/coolify/env-set.sh -u <uuid> -k KEY -v VALUE
|
||||
|
||||
# Deploy
|
||||
~/.config/mosaic/tools/coolify/deploy.sh -u <uuid>
|
||||
```
|
||||
|
||||
**Known Coolify Limitations:**
|
||||
- FQDN updates on compose sub-apps not supported via API (DB workaround required)
|
||||
- Compose files must be base64-encoded in `docker_compose_raw` field
|
||||
- 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:
|
||||
|
||||
```bash
|
||||
~/.config/mosaic/tools/health/stack-health.sh
|
||||
```
|
||||
|
||||
### Image Tagging and Promotion (Hard Rule)
|
||||
|
||||
For containerized deployments:
|
||||
|
||||
1. Build immutable image tags: `sha-<shortsha>` and `v{base-version}-rc.{build}`.
|
||||
2. Use mutable environment tags only as pointers: `testing`, optional `staging`, and `prod`.
|
||||
3. Deploy by immutable digest, not by mutable tag alone.
|
||||
4. Promote the exact tested digest between environments (no rebuild between testing and prod).
|
||||
5. Do not use `latest` or `dev` as deployment references.
|
||||
|
||||
Blue-green is the default strategy for production promotion.
|
||||
Canary is allowed only when automated SLO/error-rate gates and auto-rollback triggers are implemented.
|
||||
|
||||
### Post-Deploy Validation (REQUIRED)
|
||||
|
||||
1. Health endpoints return expected status.
|
||||
2. Critical smoke tests pass in target environment.
|
||||
3. Running version and digest match the promoted release candidate.
|
||||
4. Observability signals (errors/latency) are within expected thresholds.
|
||||
|
||||
### Rollback Rule
|
||||
|
||||
If post-deploy validation fails:
|
||||
|
||||
1. Execute rollback/redeploy-safe path immediately.
|
||||
2. Mark deployment as blocked in `docs/TASKS.md`.
|
||||
3. Record failure evidence and next remediation step in scratchpad and release notes.
|
||||
|
||||
### Registry Retention and Cleanup
|
||||
|
||||
Cleanup MUST be automated.
|
||||
|
||||
- Keep all final release tags (`vX.Y.Z`) indefinitely.
|
||||
- Keep active environment digests (`prod`, `testing`, and active blue/green slots).
|
||||
- Keep recent RC tags (`vX.Y.Z-rc.N`) based on retention window.
|
||||
- Remove stale `sha-*` and RC tags outside retention window if they are not actively deployed.
|
||||
|
||||
## Monitoring & Logging
|
||||
|
||||
### Logging Standards
|
||||
- Use structured logging (JSON)
|
||||
|
||||
268
guides/ORCHESTRATOR-PROTOCOL.md
Normal file
268
guides/ORCHESTRATOR-PROTOCOL.md
Normal file
@@ -0,0 +1,268 @@
|
||||
# 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)
|
||||
@@ -272,7 +272,7 @@ Provider options:
|
||||
1. Gitea (preferred when available) via Mosaic helper:
|
||||
|
||||
```bash
|
||||
~/.config/mosaic/rails/git/issue-create.sh \
|
||||
~/.config/mosaic/tools/git/issue-create.sh \
|
||||
-t "Phase 1: Critical Security Fixes" \
|
||||
-b "$(cat <<'EOF'
|
||||
## Findings
|
||||
@@ -412,15 +412,15 @@ git push
|
||||
and checklist completed (`~/.config/mosaic/templates/docs/DOCUMENTATION-CHECKLIST.md`) when applicable.
|
||||
13. **PR + CI + Issue Closure Gate** (HARD RULE for source-code tasks):
|
||||
- Before merging, run queue guard:
|
||||
`~/.config/mosaic/rails/git/ci-queue-wait.sh --purpose merge -B main`
|
||||
`~/.config/mosaic/tools/git/ci-queue-wait.sh --purpose merge -B main`
|
||||
- Ensure PR exists for the task branch (create/update via wrappers if needed):
|
||||
`~/.config/mosaic/rails/git/pr-create.sh ... -B main`
|
||||
`~/.config/mosaic/tools/git/pr-create.sh ... -B main`
|
||||
- Merge via wrapper:
|
||||
`~/.config/mosaic/rails/git/pr-merge.sh -n {PR_NUMBER} -m squash`
|
||||
`~/.config/mosaic/tools/git/pr-merge.sh -n {PR_NUMBER} -m squash`
|
||||
- Wait for terminal CI status:
|
||||
`~/.config/mosaic/rails/git/pr-ci-wait.sh -n {PR_NUMBER}`
|
||||
`~/.config/mosaic/tools/git/pr-ci-wait.sh -n {PR_NUMBER}`
|
||||
- Close linked issue after merge + green CI:
|
||||
`~/.config/mosaic/rails/git/issue-close.sh -i {ISSUE_NUMBER}`
|
||||
`~/.config/mosaic/tools/git/issue-close.sh -i {ISSUE_NUMBER}`
|
||||
- If any wrapper command fails, mark task `blocked`, record the exact failed wrapper command, report blocker, and STOP.
|
||||
- Do NOT stop at "PR created" or "PR merged pending CI".
|
||||
- Do NOT claim completion before CI is green and issue/internal ref is closed.
|
||||
@@ -463,10 +463,10 @@ Run review when the worker's result includes code changes (commits). Skip for ta
|
||||
cd {project_path}
|
||||
|
||||
# Code quality review
|
||||
~/.config/mosaic/rails/codex/codex-code-review.sh -b {base_branch} -o /tmp/review-{task_id}.json
|
||||
~/.config/mosaic/tools/codex/codex-code-review.sh -b {base_branch} -o /tmp/review-{task_id}.json
|
||||
|
||||
# Security review
|
||||
~/.config/mosaic/rails/codex/codex-security-review.sh -b {base_branch} -o /tmp/security-{task_id}.json
|
||||
~/.config/mosaic/tools/codex/codex-security-review.sh -b {base_branch} -o /tmp/security-{task_id}.json
|
||||
```
|
||||
|
||||
### Step 2: Parse Review Results
|
||||
@@ -599,19 +599,19 @@ Construct this from the task row and pass to worker via Task tool:
|
||||
7. If task is bug fix/security/auth/critical business logic, apply REQUIRED TDD discipline per `~/.config/mosaic/guides/QA-TESTING.md`.
|
||||
8. If gates or required situational tests fail: Fix and retry. Do NOT report success with failures.
|
||||
9. Commit: `git commit -m "fix({finding_id}): brief description"`
|
||||
10. Before push, run queue guard: `~/.config/mosaic/rails/git/ci-queue-wait.sh --purpose push -B main`
|
||||
10. Before push, run queue guard: `~/.config/mosaic/tools/git/ci-queue-wait.sh --purpose push -B main`
|
||||
11. Push: `git push origin {branch}`
|
||||
12. Report result as JSON (see format below)
|
||||
|
||||
## Git Scripts
|
||||
|
||||
For issue/PR/milestone operations, use scripts (NOT raw tea/gh):
|
||||
- `~/.config/mosaic/rails/git/issue-view.sh -i {N}`
|
||||
- `~/.config/mosaic/rails/git/pr-create.sh -t "Title" -b "Desc" -B main`
|
||||
- `~/.config/mosaic/rails/git/ci-queue-wait.sh --purpose push|merge -B main`
|
||||
- `~/.config/mosaic/rails/git/pr-merge.sh -n {PR_NUMBER} -m squash`
|
||||
- `~/.config/mosaic/rails/git/pr-ci-wait.sh -n {PR_NUMBER}`
|
||||
- `~/.config/mosaic/rails/git/issue-close.sh -i {N}`
|
||||
- `~/.config/mosaic/tools/git/issue-view.sh -i {N}`
|
||||
- `~/.config/mosaic/tools/git/pr-create.sh -t "Title" -b "Desc" -B main`
|
||||
- `~/.config/mosaic/tools/git/ci-queue-wait.sh --purpose push|merge -B main`
|
||||
- `~/.config/mosaic/tools/git/pr-merge.sh -n {PR_NUMBER} -m squash`
|
||||
- `~/.config/mosaic/tools/git/pr-ci-wait.sh -n {PR_NUMBER}`
|
||||
- `~/.config/mosaic/tools/git/issue-close.sh -i {N}`
|
||||
|
||||
Standard git commands (pull, commit, push, checkout) are fine.
|
||||
|
||||
@@ -1035,7 +1035,7 @@ When all tasks in `docs/TASKS.md` are `done` (or triaged as `deferred`), you MUS
|
||||
5. **Close milestone in provider**:
|
||||
- Gitea/GitHub:
|
||||
```bash
|
||||
~/.config/mosaic/rails/git/milestone-close.sh -t "{milestone-name}"
|
||||
~/.config/mosaic/tools/git/milestone-close.sh -t "{milestone-name}"
|
||||
```
|
||||
- GitLab: close milestone via provider workflow (CLI or web UI).
|
||||
If provider tooling is unavailable, record milestone closure status in `docs/TASKS.md` notes.
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
## Before Starting
|
||||
|
||||
1. Check assigned issue: `~/.config/mosaic/rails/git/issue-list.sh -a @me`
|
||||
1. Check assigned issue: `~/.config/mosaic/tools/git/issue-list.sh -a @me`
|
||||
2. Create scratchpad: `docs/scratchpads/{issue-number}-{short-name}.md`
|
||||
3. Review `docs/PRD.md` or `docs/PRD.json` as the requirements source.
|
||||
4. Review acceptance criteria and affected change surfaces.
|
||||
|
||||
11
install.sh
11
install.sh
@@ -144,6 +144,17 @@ mkdir -p "$TARGET_DIR/memory"
|
||||
chmod +x "$TARGET_DIR"/bin/*
|
||||
chmod +x "$TARGET_DIR"/install.sh
|
||||
|
||||
# Ensure tool scripts are executable
|
||||
find "$TARGET_DIR/tools" -name "*.sh" -exec chmod +x {} + 2>/dev/null || true
|
||||
|
||||
# Create backward-compat symlink: rails/ → tools/
|
||||
if [[ -d "$TARGET_DIR/tools" ]]; then
|
||||
if [[ -d "$TARGET_DIR/rails" ]] && [[ ! -L "$TARGET_DIR/rails" ]]; then
|
||||
rm -rf "$TARGET_DIR/rails"
|
||||
fi
|
||||
ln -sfn "tools" "$TARGET_DIR/rails"
|
||||
fi
|
||||
|
||||
ok "Framework installed to $TARGET_DIR"
|
||||
|
||||
step "Post-install tasks"
|
||||
|
||||
@@ -1,80 +0,0 @@
|
||||
#!/bin/bash
|
||||
# detect-platform.sh - Detect git platform (Gitea or GitHub) for current repo
|
||||
# Usage: source detect-platform.sh && detect_platform
|
||||
# or: ./detect-platform.sh (prints platform name)
|
||||
|
||||
detect_platform() {
|
||||
local remote_url
|
||||
remote_url=$(git remote get-url origin 2>/dev/null)
|
||||
|
||||
if [[ -z "$remote_url" ]]; then
|
||||
echo "error: not a git repository or no origin remote" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Check for GitHub
|
||||
if [[ "$remote_url" == *"github.com"* ]]; then
|
||||
PLATFORM="github"
|
||||
export PLATFORM
|
||||
echo "github"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Check for common Gitea indicators
|
||||
# Gitea URLs typically don't contain github.com, gitlab.com, bitbucket.org
|
||||
if [[ "$remote_url" != *"gitlab.com"* ]] && \
|
||||
[[ "$remote_url" != *"bitbucket.org"* ]]; then
|
||||
# Assume Gitea for self-hosted repos
|
||||
PLATFORM="gitea"
|
||||
export PLATFORM
|
||||
echo "gitea"
|
||||
return 0
|
||||
fi
|
||||
|
||||
PLATFORM="unknown"
|
||||
export PLATFORM
|
||||
echo "unknown"
|
||||
return 1
|
||||
}
|
||||
|
||||
get_repo_info() {
|
||||
local remote_url
|
||||
remote_url=$(git remote get-url origin 2>/dev/null)
|
||||
|
||||
if [[ -z "$remote_url" ]]; then
|
||||
echo "error: not a git repository or no origin remote" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Extract owner/repo from URL
|
||||
# Handles: git@host:owner/repo.git, https://host/owner/repo.git, https://host/owner/repo
|
||||
local repo_path
|
||||
if [[ "$remote_url" == git@* ]]; then
|
||||
repo_path="${remote_url#*:}"
|
||||
else
|
||||
repo_path="${remote_url#*://}"
|
||||
repo_path="${repo_path#*/}"
|
||||
fi
|
||||
|
||||
# Remove .git suffix if present
|
||||
repo_path="${repo_path%.git}"
|
||||
|
||||
echo "$repo_path"
|
||||
}
|
||||
|
||||
get_repo_owner() {
|
||||
local repo_info
|
||||
repo_info=$(get_repo_info)
|
||||
echo "${repo_info%%/*}"
|
||||
}
|
||||
|
||||
get_repo_name() {
|
||||
local repo_info
|
||||
repo_info=$(get_repo_info)
|
||||
echo "${repo_info##*/}"
|
||||
}
|
||||
|
||||
# If script is run directly (not sourced), output the platform
|
||||
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
|
||||
detect_platform
|
||||
fi
|
||||
@@ -1,2 +0,0 @@
|
||||
npx lint-staged
|
||||
npx git-secrets --scan || echo "Warning: git-secrets not installed"
|
||||
@@ -1,2 +0,0 @@
|
||||
npx lint-staged
|
||||
npx git-secrets --scan || echo "Warning: git-secrets not installed"
|
||||
@@ -1,2 +0,0 @@
|
||||
npx lint-staged
|
||||
npx git-secrets --scan || echo "Warning: git-secrets not installed"
|
||||
@@ -11,11 +11,41 @@ This file applies only to Claude runtime behavior.
|
||||
3. Treat sequential-thinking MCP as required.
|
||||
4. If runtime config conflicts with global rules, global rules win.
|
||||
5. Documentation rules are inherited from `~/.config/mosaic/AGENTS.md` and `~/.config/mosaic/guides/DOCUMENTATION.md`.
|
||||
6. For issue/PR/milestone actions, run Mosaic git wrappers first (`~/.config/mosaic/rails/git/*.sh`) and do not call raw `gh`/`tea`/`glab` first.
|
||||
6. For issue/PR/milestone actions, run Mosaic git wrappers first (`~/.config/mosaic/tools/git/*.sh`) and do not call raw `gh`/`tea`/`glab` first.
|
||||
7. For orchestration-oriented missions, load `~/.config/mosaic/guides/ORCHESTRATOR.md` before acting.
|
||||
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.
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "~/.config/mosaic/rails/qa/qa-hook-stdin.sh",
|
||||
"command": "~/.config/mosaic/tools/qa/qa-hook-stdin.sh",
|
||||
"timeout": 60
|
||||
}
|
||||
]
|
||||
|
||||
@@ -11,11 +11,26 @@ This file applies only to Codex runtime behavior.
|
||||
3. Treat sequential-thinking MCP as required.
|
||||
4. If runtime config conflicts with global rules, global rules win.
|
||||
5. Documentation rules are inherited from `~/.config/mosaic/AGENTS.md` and `~/.config/mosaic/guides/DOCUMENTATION.md`.
|
||||
6. For issue/PR/milestone actions, run Mosaic git wrappers first (`~/.config/mosaic/rails/git/*.sh`) and do not call raw `gh`/`tea`/`glab` first.
|
||||
6. For issue/PR/milestone actions, run Mosaic git wrappers first (`~/.config/mosaic/tools/git/*.sh`) and do not call raw `gh`/`tea`/`glab` first.
|
||||
7. For orchestration-oriented missions, load `~/.config/mosaic/guides/ORCHESTRATOR.md` before acting.
|
||||
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.
|
||||
|
||||
@@ -11,7 +11,7 @@ This file applies only to OpenCode runtime behavior.
|
||||
3. Treat sequential-thinking MCP as required.
|
||||
4. If runtime config conflicts with global rules, global rules win.
|
||||
5. Documentation rules are inherited from `~/.config/mosaic/AGENTS.md` and `~/.config/mosaic/guides/DOCUMENTATION.md`.
|
||||
6. For issue/PR/milestone actions, run Mosaic git wrappers first (`~/.config/mosaic/rails/git/*.sh`) and do not call raw `gh`/`tea`/`glab` first.
|
||||
6. For issue/PR/milestone actions, run Mosaic git wrappers first (`~/.config/mosaic/tools/git/*.sh`) and do not call raw `gh`/`tea`/`glab` first.
|
||||
7. For orchestration-oriented missions, load `~/.config/mosaic/guides/ORCHESTRATOR.md` before acting.
|
||||
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.
|
||||
|
||||
@@ -25,7 +25,7 @@ If wrappers are available, you may use:
|
||||
|
||||
## Enforcement Rules
|
||||
|
||||
- Treat `~/.config/mosaic` as canonical for shared guides, rails, profiles, and skills.
|
||||
- Treat `~/.config/mosaic` as canonical for shared guides, tools, profiles, and skills.
|
||||
- Do not edit generated project views directly when the repo defines canonical data sources.
|
||||
- Pull/rebase before edits in shared repositories.
|
||||
- Run project verification commands before claiming completion.
|
||||
|
||||
@@ -140,7 +140,7 @@ Ask these questions with lettered options (user can respond "1A, 2B, 3C"):
|
||||
If the project's `.woodpecker.yml` doesn't already have a `kaniko_setup` anchor in its `variables:` section, add it:
|
||||
|
||||
```bash
|
||||
~/.config/mosaic/rails/cicd/generate-docker-steps.sh --kaniko-setup-only --registry REGISTRY_HOST
|
||||
~/.config/mosaic/tools/cicd/generate-docker-steps.sh --kaniko-setup-only --registry REGISTRY_HOST
|
||||
```
|
||||
|
||||
This outputs:
|
||||
@@ -158,7 +158,7 @@ Add this to the existing `variables:` block at the top of `.woodpecker.yml`.
|
||||
Use the generator script with the user's answers:
|
||||
|
||||
```bash
|
||||
~/.config/mosaic/rails/cicd/generate-docker-steps.sh \
|
||||
~/.config/mosaic/tools/cicd/generate-docker-steps.sh \
|
||||
--registry REGISTRY \
|
||||
--org ORG \
|
||||
--repo REPO \
|
||||
|
||||
@@ -5,10 +5,10 @@ Run independent reviews:
|
||||
|
||||
```bash
|
||||
# Code quality review (Codex)
|
||||
~/.config/mosaic/rails/codex/codex-code-review.sh --uncommitted
|
||||
~/.config/mosaic/tools/codex/codex-code-review.sh --uncommitted
|
||||
|
||||
# Security review (Codex)
|
||||
~/.config/mosaic/rails/codex/codex-security-review.sh --uncommitted
|
||||
~/.config/mosaic/tools/codex/codex-security-review.sh --uncommitted
|
||||
```
|
||||
|
||||
**Fallback:** If Codex is unavailable, use Claude's built-in review skills.
|
||||
|
||||
53
templates/docs/MISSION-MANIFEST.md.template
Normal file
53
templates/docs/MISSION-MANIFEST.md.template
Normal file
@@ -0,0 +1,53 @@
|
||||
# Mission Manifest — ${MISSION_NAME}
|
||||
|
||||
> Persistent document tracking full mission scope, status, and session history.
|
||||
> Updated by the orchestrator at each phase transition and milestone completion.
|
||||
|
||||
## Mission
|
||||
|
||||
**ID:** ${MISSION_ID}
|
||||
**Statement:** ${MISSION_STATEMENT}
|
||||
**Phase:** Intake
|
||||
**Current Milestone:** —
|
||||
**Progress:** 0 / ${MILESTONE_COUNT} milestones
|
||||
**Status:** not-started
|
||||
**Last Updated:** ${CREATED_AT}
|
||||
|
||||
## Success Criteria
|
||||
|
||||
${SUCCESS_CRITERIA}
|
||||
|
||||
## Milestones
|
||||
|
||||
| # | ID | Name | Status | Branch | Issue | Started | Completed |
|
||||
|---|-----|------|--------|--------|-------|---------|-----------|
|
||||
${MILESTONES_TABLE}
|
||||
|
||||
## Deployment
|
||||
|
||||
| Target | URL | Method |
|
||||
|--------|-----|--------|
|
||||
${DEPLOYMENT_TABLE}
|
||||
|
||||
## Coordination
|
||||
|
||||
- **Primary Agent:** ${PRIMARY_RUNTIME}
|
||||
- **Sibling Agents:** ${SIBLING_AGENTS}
|
||||
- **Shared Contracts:** ${SHARED_CONTRACTS}
|
||||
|
||||
## Token Budget
|
||||
|
||||
| Metric | Value |
|
||||
|--------|-------|
|
||||
| Budget | ${TOKEN_BUDGET} |
|
||||
| Used | 0 |
|
||||
| Mode | normal |
|
||||
|
||||
## Session History
|
||||
|
||||
| Session | Runtime | Started | Duration | Ended Reason | Last Task |
|
||||
|---------|---------|---------|----------|--------------|-----------|
|
||||
|
||||
## Scratchpad
|
||||
|
||||
Path: `docs/scratchpads/${MISSION_ID}.md`
|
||||
36
templates/docs/continuation-prompt.md.template
Normal file
36
templates/docs/continuation-prompt.md.template
Normal file
@@ -0,0 +1,36 @@
|
||||
## Continuation Mission
|
||||
|
||||
Continue **${MISSION_NAME}** from existing state.
|
||||
|
||||
## Setup
|
||||
|
||||
- **Project:** ${PROJECT_PATH}
|
||||
- **State:** docs/TASKS.md (already populated — ${TASKS_DONE}/${TASKS_TOTAL} tasks complete)
|
||||
- **Manifest:** docs/MISSION-MANIFEST.md
|
||||
- **Scratchpad:** docs/scratchpads/${MISSION_ID}.md
|
||||
- **Protocol:** ~/.config/mosaic/guides/ORCHESTRATOR.md
|
||||
- **Quality gates:** ${QUALITY_GATES}
|
||||
|
||||
## Resume Point
|
||||
|
||||
- **Current milestone:** ${CURRENT_MILESTONE_NAME} (${CURRENT_MILESTONE_ID})
|
||||
- **Next task:** ${NEXT_TASK_ID}
|
||||
- **Progress:** ${TASKS_DONE}/${TASKS_TOTAL} tasks (${PROGRESS_PCT}%)
|
||||
- **Branch:** ${CURRENT_BRANCH}
|
||||
|
||||
## Previous Session Context
|
||||
|
||||
- **Session:** ${PREV_SESSION_ID} (${PREV_RUNTIME}, ${PREV_DURATION})
|
||||
- **Ended:** ${PREV_ENDED_REASON}
|
||||
- **Last completed task:** ${PREV_LAST_TASK}
|
||||
|
||||
## Instructions
|
||||
|
||||
1. Read `~/.config/mosaic/guides/ORCHESTRATOR.md` for full protocol
|
||||
2. Read `docs/MISSION-MANIFEST.md` for mission scope and status
|
||||
3. Read `docs/scratchpads/${MISSION_ID}.md` for session history and decisions
|
||||
4. Read `docs/TASKS.md` for current task state
|
||||
5. `git pull --rebase` to sync latest changes
|
||||
6. Continue execution from task **${NEXT_TASK_ID}**
|
||||
7. Follow Two-Phase Completion Protocol
|
||||
8. You are the SOLE writer of `docs/TASKS.md`
|
||||
27
templates/docs/mission-scratchpad.md.template
Normal file
27
templates/docs/mission-scratchpad.md.template
Normal file
@@ -0,0 +1,27 @@
|
||||
# Mission Scratchpad — ${MISSION_NAME}
|
||||
|
||||
> Append-only log. NEVER delete entries. NEVER overwrite sections.
|
||||
> This is the orchestrator's working memory across sessions.
|
||||
|
||||
## Original Mission Prompt
|
||||
|
||||
```
|
||||
${MISSION_PROMPT}
|
||||
```
|
||||
|
||||
## Planning Decisions
|
||||
|
||||
<!-- Record key decisions made during planning. Format: decision + rationale. -->
|
||||
|
||||
## Session Log
|
||||
|
||||
| Session | Date | Milestone | Tasks Done | Outcome |
|
||||
|---------|------|-----------|------------|---------|
|
||||
|
||||
## Open Questions
|
||||
|
||||
<!-- Unresolved items that need human input or cross-session investigation. -->
|
||||
|
||||
## Corrections
|
||||
|
||||
<!-- Record any corrections to earlier decisions or assumptions. -->
|
||||
14
templates/repo/.mosaic/orchestrator/mission.json
Normal file
14
templates/repo/.mosaic/orchestrator/mission.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"schema_version": 1,
|
||||
"mission_id": "",
|
||||
"name": "",
|
||||
"description": "",
|
||||
"project_path": "",
|
||||
"created_at": "",
|
||||
"status": "inactive",
|
||||
"task_prefix": "",
|
||||
"quality_gates": "",
|
||||
"milestone_version": "0.0.1",
|
||||
"milestones": [],
|
||||
"sessions": []
|
||||
}
|
||||
@@ -8,6 +8,34 @@ source "$SCRIPT_DIR/common.sh"
|
||||
ensure_repo_root
|
||||
load_repo_hooks
|
||||
|
||||
# ─── Mission session cleanup (ORCHESTRATOR-PROTOCOL) ────────────────────────
|
||||
ORCH_DIR=".mosaic/orchestrator"
|
||||
MISSION_JSON="$ORCH_DIR/mission.json"
|
||||
SESSION_LOCK="$ORCH_DIR/session.lock"
|
||||
COORD_LIB="$HOME/.config/mosaic/tools/orchestrator/_lib.sh"
|
||||
|
||||
if [[ -f "$SESSION_LOCK" ]] && [[ -f "$COORD_LIB" ]] && command -v jq &>/dev/null; then
|
||||
# shellcheck source=/dev/null
|
||||
source "$COORD_LIB"
|
||||
|
||||
sess_id="$(jq -r '.session_id // ""' "$SESSION_LOCK")"
|
||||
if [[ -n "$sess_id" && -f "$MISSION_JSON" ]]; then
|
||||
# Update mission.json: mark session ended
|
||||
updated="$(jq \
|
||||
--arg sid "$sess_id" \
|
||||
--arg ts "$(iso_now)" \
|
||||
--arg reason "completed" \
|
||||
'(.sessions[] | select(.session_id == $sid)) |= . + {
|
||||
ended_at: $ts,
|
||||
ended_reason: $reason
|
||||
}' "$MISSION_JSON")"
|
||||
echo "$updated" > "$MISSION_JSON.tmp" && mv "$MISSION_JSON.tmp" "$MISSION_JSON"
|
||||
echo "[agent-framework] Session $sess_id recorded in mission state"
|
||||
fi
|
||||
|
||||
session_lock_clear "."
|
||||
fi
|
||||
|
||||
if declare -F mosaic_hook_session_end >/dev/null 2>&1; then
|
||||
run_step "Run repo end hook" mosaic_hook_session_end
|
||||
else
|
||||
|
||||
@@ -16,6 +16,75 @@ 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
|
||||
|
||||
266
tools/_lib/credentials.sh
Executable file
266
tools/_lib/credentials.sh
Executable file
@@ -0,0 +1,266 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# credentials.sh — Shared credential loader for Mosaic tool suites
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
# Supported services:
|
||||
# portainer, coolify, authentik, glpi, github,
|
||||
# gitea-mosaicstack, gitea-usc, woodpecker, cloudflare
|
||||
#
|
||||
# After loading, service-specific env vars are exported.
|
||||
# Run `load_credentials --help` for details.
|
||||
|
||||
MOSAIC_CREDENTIALS_FILE="${MOSAIC_CREDENTIALS_FILE:-$HOME/src/jarvis-brain/credentials.json}"
|
||||
|
||||
_mosaic_require_jq() {
|
||||
if ! command -v jq &>/dev/null; then
|
||||
echo "Error: jq is required but not installed" >&2
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
_mosaic_read_cred() {
|
||||
local jq_path="$1"
|
||||
if [[ ! -f "$MOSAIC_CREDENTIALS_FILE" ]]; then
|
||||
echo "Error: Credentials file not found: $MOSAIC_CREDENTIALS_FILE" >&2
|
||||
return 1
|
||||
fi
|
||||
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"
|
||||
|
||||
if [[ -z "$service" || "$service" == "--help" ]]; then
|
||||
cat <<'EOF'
|
||||
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)
|
||||
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)
|
||||
EOF
|
||||
return 0
|
||||
fi
|
||||
|
||||
_mosaic_require_jq || return 1
|
||||
|
||||
case "$service" in
|
||||
portainer)
|
||||
export PORTAINER_URL="${PORTAINER_URL:-$(_mosaic_read_cred '.portainer.url')}"
|
||||
export PORTAINER_API_KEY="${PORTAINER_API_KEY:-$(_mosaic_read_cred '.portainer.api_key')}"
|
||||
PORTAINER_URL="${PORTAINER_URL%/}"
|
||||
[[ -n "$PORTAINER_URL" ]] || { echo "Error: portainer.url not found" >&2; return 1; }
|
||||
[[ -n "$PORTAINER_API_KEY" ]] || { echo "Error: portainer.api_key not found" >&2; return 1; }
|
||||
;;
|
||||
coolify)
|
||||
export COOLIFY_URL="${COOLIFY_URL:-$(_mosaic_read_cred '.coolify.url')}"
|
||||
export COOLIFY_TOKEN="${COOLIFY_TOKEN:-$(_mosaic_read_cred '.coolify.app_token')}"
|
||||
COOLIFY_URL="${COOLIFY_URL%/}"
|
||||
[[ -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
|
||||
;;
|
||||
glpi)
|
||||
export GLPI_URL="${GLPI_URL:-$(_mosaic_read_cred '.glpi.url')}"
|
||||
export GLPI_APP_TOKEN="${GLPI_APP_TOKEN:-$(_mosaic_read_cred '.glpi.app_token')}"
|
||||
export GLPI_USER_TOKEN="${GLPI_USER_TOKEN:-$(_mosaic_read_cred '.glpi.user_token')}"
|
||||
GLPI_URL="${GLPI_URL%/}"
|
||||
[[ -n "$GLPI_URL" ]] || { echo "Error: glpi.url not found" >&2; return 1; }
|
||||
;;
|
||||
github)
|
||||
export GITHUB_TOKEN="${GITHUB_TOKEN:-$(_mosaic_read_cred '.github.token')}"
|
||||
[[ -n "$GITHUB_TOKEN" ]] || { echo "Error: github.token not found" >&2; return 1; }
|
||||
;;
|
||||
gitea-mosaicstack)
|
||||
export GITEA_URL="${GITEA_URL:-$(_mosaic_read_cred '.gitea.mosaicstack.url')}"
|
||||
export GITEA_TOKEN="${GITEA_TOKEN:-$(_mosaic_read_cred '.gitea.mosaicstack.token')}"
|
||||
GITEA_URL="${GITEA_URL%/}"
|
||||
[[ -n "$GITEA_URL" ]] || { echo "Error: gitea.mosaicstack.url not found" >&2; return 1; }
|
||||
[[ -n "$GITEA_TOKEN" ]] || { echo "Error: gitea.mosaicstack.token not found" >&2; return 1; }
|
||||
;;
|
||||
gitea-usc)
|
||||
export GITEA_URL="${GITEA_URL:-$(_mosaic_read_cred '.gitea.usc.url')}"
|
||||
export GITEA_TOKEN="${GITEA_TOKEN:-$(_mosaic_read_cred '.gitea.usc.token')}"
|
||||
GITEA_URL="${GITEA_URL%/}"
|
||||
[[ -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}"
|
||||
;;
|
||||
*)
|
||||
echo "Error: Unknown service '$service'" >&2
|
||||
echo "Supported: portainer, coolify, authentik[-<name>], glpi, github, gitea-mosaicstack, gitea-usc, woodpecker[-<name>], cloudflare[-<name>]" >&2
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
# Common HTTP helper — makes a curl request and separates body from status code
|
||||
# Usage: mosaic_http GET "/api/v1/endpoint" "Authorization: Bearer $TOKEN" [base_url]
|
||||
# Returns: body on stdout, sets MOSAIC_HTTP_CODE
|
||||
mosaic_http() {
|
||||
local method="$1"
|
||||
local endpoint="$2"
|
||||
local auth_header="$3"
|
||||
local base_url="${4:-}"
|
||||
|
||||
local response
|
||||
response=$(curl -sk -w "\n%{http_code}" -X "$method" \
|
||||
-H "$auth_header" \
|
||||
-H "Content-Type: application/json" \
|
||||
"${base_url}${endpoint}")
|
||||
|
||||
MOSAIC_HTTP_CODE=$(echo "$response" | tail -n1)
|
||||
echo "$response" | sed '$d'
|
||||
}
|
||||
|
||||
# POST variant with body
|
||||
# Usage: mosaic_http_post "/api/v1/endpoint" "Authorization: Bearer $TOKEN" '{"key":"val"}' [base_url]
|
||||
mosaic_http_post() {
|
||||
local endpoint="$1"
|
||||
local auth_header="$2"
|
||||
local data="$3"
|
||||
local base_url="${4:-}"
|
||||
|
||||
local response
|
||||
response=$(curl -sk -w "\n%{http_code}" -X POST \
|
||||
-H "$auth_header" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "$data" \
|
||||
"${base_url}${endpoint}")
|
||||
|
||||
MOSAIC_HTTP_CODE=$(echo "$response" | tail -n1)
|
||||
echo "$response" | sed '$d'
|
||||
}
|
||||
|
||||
# PATCH variant with body
|
||||
mosaic_http_patch() {
|
||||
local endpoint="$1"
|
||||
local auth_header="$2"
|
||||
local data="$3"
|
||||
local base_url="${4:-}"
|
||||
|
||||
local response
|
||||
response=$(curl -sk -w "\n%{http_code}" -X PATCH \
|
||||
-H "$auth_header" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "$data" \
|
||||
"${base_url}${endpoint}")
|
||||
|
||||
MOSAIC_HTTP_CODE=$(echo "$response" | tail -n1)
|
||||
echo "$response" | sed '$d'
|
||||
}
|
||||
59
tools/authentik/README.md
Normal file
59
tools/authentik/README.md
Normal file
@@ -0,0 +1,59 @@
|
||||
# Authentik Tool Suite
|
||||
|
||||
Manage Authentik identity provider (SSO, users, groups, applications, flows) via CLI.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- `jq` installed
|
||||
- Authentik credentials in `~/src/jarvis-brain/credentials.json` (or `$MOSAIC_CREDENTIALS_FILE`)
|
||||
- Required fields: `authentik.url`, `authentik.username`, `authentik.password`
|
||||
|
||||
## Authentication
|
||||
|
||||
Scripts use `auth-token.sh` to auto-authenticate via username/password and cache the API token at `~/.cache/mosaic/authentik-token`. The token is validated on each use and refreshed automatically when expired.
|
||||
|
||||
For better security, create a long-lived API token in Authentik admin (Directory > Tokens) and set `$AUTHENTIK_TOKEN` in your environment — the scripts will use it directly.
|
||||
|
||||
## Scripts
|
||||
|
||||
| Script | Purpose |
|
||||
|--------|---------|
|
||||
| `auth-token.sh` | Authenticate and cache API token |
|
||||
| `user-list.sh` | List users (search, filter by group) |
|
||||
| `user-create.sh` | Create user with optional group assignment |
|
||||
| `group-list.sh` | List groups |
|
||||
| `app-list.sh` | List OAuth/SAML applications |
|
||||
| `flow-list.sh` | List authentication flows |
|
||||
| `admin-status.sh` | System health and version info |
|
||||
|
||||
## Common Options
|
||||
|
||||
All scripts support:
|
||||
- `-f json` — JSON output (default: table)
|
||||
- `-h` — Show help
|
||||
|
||||
## API Reference
|
||||
|
||||
- Base URL: `https://auth.diversecanvas.com`
|
||||
- API prefix: `/api/v3/`
|
||||
- OpenAPI schema: `/api/v3/schema/`
|
||||
- Auth: Bearer token in `Authorization` header
|
||||
|
||||
## Examples
|
||||
|
||||
```bash
|
||||
# List all users
|
||||
~/.config/mosaic/tools/authentik/user-list.sh
|
||||
|
||||
# Search for a user
|
||||
~/.config/mosaic/tools/authentik/user-list.sh -s "jason"
|
||||
|
||||
# Create a user in the admins group
|
||||
~/.config/mosaic/tools/authentik/user-create.sh -u newuser -n "New User" -e new@example.com -g admins
|
||||
|
||||
# List OAuth applications as JSON
|
||||
~/.config/mosaic/tools/authentik/app-list.sh -f json
|
||||
|
||||
# Check system health
|
||||
~/.config/mosaic/tools/authentik/admin-status.sh
|
||||
```
|
||||
63
tools/authentik/admin-status.sh
Executable file
63
tools/authentik/admin-status.sh
Executable file
@@ -0,0 +1,63 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# admin-status.sh — Authentik system health and version info
|
||||
#
|
||||
# Usage: admin-status.sh [-f format] [-a instance]
|
||||
#
|
||||
# Options:
|
||||
# -f format Output format: table (default), json
|
||||
# -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"
|
||||
|
||||
FORMAT="table"
|
||||
AK_INSTANCE=""
|
||||
|
||||
while getopts "f:a: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 ;;
|
||||
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"})
|
||||
|
||||
response=$(curl -sk -w "\n%{http_code}" \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
"${AUTHENTIK_URL}/api/v3/admin/system/")
|
||||
|
||||
http_code=$(echo "$response" | tail -n1)
|
||||
body=$(echo "$response" | sed '$d')
|
||||
|
||||
if [[ "$http_code" != "200" ]]; then
|
||||
echo "Error: Failed to get system status (HTTP $http_code)" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ "$FORMAT" == "json" ]]; then
|
||||
echo "$body" | jq '.'
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "Authentik System Status"
|
||||
echo "======================="
|
||||
echo "$body" | jq -r '
|
||||
" URL: \(.http_host // "unknown")\n" +
|
||||
" Version: \(.runtime.authentik_version // "unknown")\n" +
|
||||
" Python: \(.runtime.python_version // "unknown")\n" +
|
||||
" Workers: \(.runtime.gunicorn_workers // "unknown")\n" +
|
||||
" Build Hash: \(.runtime.build_hash // "unknown")\n" +
|
||||
" Embedded Outpost: \(.embedded_outpost_host // "unknown")"
|
||||
'
|
||||
70
tools/authentik/app-list.sh
Executable file
70
tools/authentik/app-list.sh
Executable file
@@ -0,0 +1,70 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# app-list.sh — List Authentik applications
|
||||
#
|
||||
# Usage: app-list.sh [-f format] [-s search] [-a instance]
|
||||
#
|
||||
# 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
|
||||
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"
|
||||
|
||||
FORMAT="table"
|
||||
SEARCH=""
|
||||
AK_INSTANCE=""
|
||||
|
||||
while getopts "f:s:a: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 ;;
|
||||
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"})
|
||||
|
||||
PARAMS="ordering=name"
|
||||
[[ -n "$SEARCH" ]] && PARAMS="${PARAMS}&search=${SEARCH}"
|
||||
|
||||
response=$(curl -sk -w "\n%{http_code}" \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
"${AUTHENTIK_URL}/api/v3/core/applications/?${PARAMS}")
|
||||
|
||||
http_code=$(echo "$response" | tail -n1)
|
||||
body=$(echo "$response" | sed '$d')
|
||||
|
||||
if [[ "$http_code" != "200" ]]; then
|
||||
echo "Error: Failed to list applications (HTTP $http_code)" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ "$FORMAT" == "json" ]]; then
|
||||
echo "$body" | jq '.results'
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "NAME SLUG PROVIDER LAUNCH URL"
|
||||
echo "---------------------------- ---------------------------- ----------------- ----------------------------------------"
|
||||
echo "$body" | jq -r '.results[] | [
|
||||
.name,
|
||||
.slug,
|
||||
(.provider_obj.name // "none"),
|
||||
(.launch_url // "—")
|
||||
] | @tsv' | while IFS=$'\t' read -r name slug provider launch_url; do
|
||||
printf "%-28s %-28s %-17s %s\n" \
|
||||
"${name:0:28}" "${slug:0:28}" "${provider:0:17}" "$launch_url"
|
||||
done
|
||||
95
tools/authentik/auth-token.sh
Executable file
95
tools/authentik/auth-token.sh
Executable file
@@ -0,0 +1,95 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# auth-token.sh — Obtain and cache Authentik API token
|
||||
#
|
||||
# Usage: auth-token.sh [-f] [-q] [-a instance]
|
||||
#
|
||||
# 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)
|
||||
# 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
|
||||
#
|
||||
# Environment variables (or credentials.json):
|
||||
# AUTHENTIK_URL — Authentik instance URL
|
||||
# AUTHENTIK_TOKEN — Pre-configured API token (recommended)
|
||||
set -euo pipefail
|
||||
|
||||
MOSAIC_HOME="${MOSAIC_HOME:-$HOME/.config/mosaic}"
|
||||
source "$MOSAIC_HOME/tools/_lib/credentials.sh"
|
||||
|
||||
FORCE=false
|
||||
QUIET=false
|
||||
AK_INSTANCE=""
|
||||
|
||||
while getopts "fqa:h" 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 ;;
|
||||
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
|
||||
http_code=$(curl -sk -o /dev/null -w "%{http_code}" \
|
||||
--connect-timeout 5 --max-time 10 \
|
||||
-H "Authorization: Bearer $token" \
|
||||
"${AUTHENTIK_URL}/api/v3/core/users/me/")
|
||||
[[ "$http_code" == "200" ]]
|
||||
}
|
||||
|
||||
# 1. Check cached token
|
||||
if [[ "$FORCE" == "false" ]] && [[ -f "$CACHE_FILE" ]]; then
|
||||
cached_token=$(cat "$CACHE_FILE")
|
||||
if [[ -n "$cached_token" ]] && _validate_token "$cached_token"; then
|
||||
[[ "$QUIET" == "false" ]] && echo "Using cached token (valid)" >&2
|
||||
echo "$cached_token"
|
||||
exit 0
|
||||
fi
|
||||
[[ "$QUIET" == "false" ]] && echo "Cached token invalid, checking credentials..." >&2
|
||||
fi
|
||||
|
||||
# 2. Use pre-configured token from credentials.json
|
||||
if [[ -n "${AUTHENTIK_TOKEN:-}" ]]; then
|
||||
if _validate_token "$AUTHENTIK_TOKEN"; then
|
||||
# Cache it for faster future access
|
||||
mkdir -p "$CACHE_DIR"
|
||||
echo "$AUTHENTIK_TOKEN" > "$CACHE_FILE"
|
||||
chmod 600 "$CACHE_FILE"
|
||||
[[ "$QUIET" == "false" ]] && echo "Token validated and cached at $CACHE_FILE" >&2
|
||||
echo "$AUTHENTIK_TOKEN"
|
||||
exit 0
|
||||
else
|
||||
echo "Error: Pre-configured AUTHENTIK_TOKEN is invalid (API returned non-200)" >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# 3. No token available
|
||||
echo "Error: No Authentik API token configured" >&2
|
||||
echo "" >&2
|
||||
echo "To create one:" >&2
|
||||
echo " 1. Log into Authentik admin: ${AUTHENTIK_URL}/if/admin/#/core/tokens" >&2
|
||||
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
|
||||
exit 1
|
||||
70
tools/authentik/flow-list.sh
Executable file
70
tools/authentik/flow-list.sh
Executable file
@@ -0,0 +1,70 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# flow-list.sh — List Authentik flows
|
||||
#
|
||||
# Usage: flow-list.sh [-f format] [-d designation] [-a instance]
|
||||
#
|
||||
# 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"
|
||||
|
||||
FORMAT="table"
|
||||
DESIGNATION=""
|
||||
AK_INSTANCE=""
|
||||
|
||||
while getopts "f:d:a: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 ;;
|
||||
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"})
|
||||
|
||||
PARAMS="ordering=slug"
|
||||
[[ -n "$DESIGNATION" ]] && PARAMS="${PARAMS}&designation=${DESIGNATION}"
|
||||
|
||||
response=$(curl -sk -w "\n%{http_code}" \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
"${AUTHENTIK_URL}/api/v3/flows/instances/?${PARAMS}")
|
||||
|
||||
http_code=$(echo "$response" | tail -n1)
|
||||
body=$(echo "$response" | sed '$d')
|
||||
|
||||
if [[ "$http_code" != "200" ]]; then
|
||||
echo "Error: Failed to list flows (HTTP $http_code)" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ "$FORMAT" == "json" ]]; then
|
||||
echo "$body" | jq '.results'
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "NAME SLUG DESIGNATION TITLE"
|
||||
echo "---------------------------- ---------------------------- ---------------- ----------------------------"
|
||||
echo "$body" | jq -r '.results[] | [
|
||||
.name,
|
||||
.slug,
|
||||
.designation,
|
||||
(.title // "—")
|
||||
] | @tsv' | while IFS=$'\t' read -r name slug designation title; do
|
||||
printf "%-28s %-28s %-16s %s\n" \
|
||||
"${name:0:28}" "${slug:0:28}" "$designation" "${title:0:28}"
|
||||
done
|
||||
69
tools/authentik/group-list.sh
Executable file
69
tools/authentik/group-list.sh
Executable file
@@ -0,0 +1,69 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# group-list.sh — List Authentik groups
|
||||
#
|
||||
# Usage: group-list.sh [-f format] [-s search] [-a instance]
|
||||
#
|
||||
# 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
|
||||
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"
|
||||
|
||||
FORMAT="table"
|
||||
SEARCH=""
|
||||
AK_INSTANCE=""
|
||||
|
||||
while getopts "f:s:a: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 ;;
|
||||
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"})
|
||||
|
||||
PARAMS="ordering=name"
|
||||
[[ -n "$SEARCH" ]] && PARAMS="${PARAMS}&search=${SEARCH}"
|
||||
|
||||
response=$(curl -sk -w "\n%{http_code}" \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
"${AUTHENTIK_URL}/api/v3/core/groups/?${PARAMS}")
|
||||
|
||||
http_code=$(echo "$response" | tail -n1)
|
||||
body=$(echo "$response" | sed '$d')
|
||||
|
||||
if [[ "$http_code" != "200" ]]; then
|
||||
echo "Error: Failed to list groups (HTTP $http_code)" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ "$FORMAT" == "json" ]]; then
|
||||
echo "$body" | jq '.results'
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "NAME PK MEMBERS SUPERUSER"
|
||||
echo "---------------------------- ------------------------------------ ------- ---------"
|
||||
echo "$body" | jq -r '.results[] | [
|
||||
.name,
|
||||
.pk,
|
||||
(.users | length | tostring),
|
||||
(if .is_superuser then "yes" else "no" end)
|
||||
] | @tsv' | while IFS=$'\t' read -r name pk members superuser; do
|
||||
printf "%-28s %-36s %-7s %s\n" "${name:0:28}" "$pk" "$members" "$superuser"
|
||||
done
|
||||
100
tools/authentik/user-create.sh
Executable file
100
tools/authentik/user-create.sh
Executable file
@@ -0,0 +1,100 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# user-create.sh — Create an Authentik user
|
||||
#
|
||||
# Usage: user-create.sh -u <username> -n <name> -e <email> [-p password] [-g group] [-a instance]
|
||||
#
|
||||
# Options:
|
||||
# -u username Username (required)
|
||||
# -n name Display name (required)
|
||||
# -e email Email address (required)
|
||||
# -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):
|
||||
# AUTHENTIK_URL — Authentik instance URL
|
||||
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"
|
||||
|
||||
USERNAME="" NAME="" EMAIL="" PASSWORD="" GROUP="" FORMAT="table" AK_INSTANCE=""
|
||||
|
||||
while getopts "u:n:e:p:g:f:a:h" opt; do
|
||||
case $opt in
|
||||
u) USERNAME="$OPTARG" ;;
|
||||
n) NAME="$OPTARG" ;;
|
||||
e) EMAIL="$OPTARG" ;;
|
||||
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 ;;
|
||||
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"})
|
||||
|
||||
# Build user payload
|
||||
payload=$(jq -n \
|
||||
--arg username "$USERNAME" \
|
||||
--arg name "$NAME" \
|
||||
--arg email "$EMAIL" \
|
||||
'{username: $username, name: $name, email: $email, is_active: true}')
|
||||
|
||||
# Add password if provided
|
||||
if [[ -n "$PASSWORD" ]]; then
|
||||
payload=$(echo "$payload" | jq --arg pw "$PASSWORD" '. + {password: $pw}')
|
||||
fi
|
||||
|
||||
# Add to group if provided
|
||||
if [[ -n "$GROUP" ]]; then
|
||||
# Look up group PK by name
|
||||
group_response=$(curl -sk \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
"${AUTHENTIK_URL}/api/v3/core/groups/?search=${GROUP}")
|
||||
group_pk=$(echo "$group_response" | jq -r ".results[] | select(.name == \"$GROUP\") | .pk" | head -1)
|
||||
if [[ -n "$group_pk" ]]; then
|
||||
payload=$(echo "$payload" | jq --arg gk "$group_pk" '. + {groups: [$gk]}')
|
||||
else
|
||||
echo "Warning: Group '$GROUP' not found — creating user without group" >&2
|
||||
fi
|
||||
fi
|
||||
|
||||
response=$(curl -sk -w "\n%{http_code}" -X POST \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "$payload" \
|
||||
"${AUTHENTIK_URL}/api/v3/core/users/")
|
||||
|
||||
http_code=$(echo "$response" | tail -n1)
|
||||
body=$(echo "$response" | sed '$d')
|
||||
|
||||
if [[ "$http_code" != "201" ]]; then
|
||||
echo "Error: Failed to create user (HTTP $http_code)" >&2
|
||||
echo "$body" | jq -r '.' 2>/dev/null >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ "$FORMAT" == "json" ]]; then
|
||||
echo "$body" | jq '.'
|
||||
else
|
||||
echo "User created successfully:"
|
||||
echo "$body" | jq -r '" Username: \(.username)\n Name: \(.name)\n Email: \(.email)\n PK: \(.pk)"'
|
||||
fi
|
||||
80
tools/authentik/user-list.sh
Executable file
80
tools/authentik/user-list.sh
Executable file
@@ -0,0 +1,80 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# user-list.sh — List Authentik users
|
||||
#
|
||||
# Usage: user-list.sh [-f format] [-s search] [-g group] [-a instance]
|
||||
#
|
||||
# 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
|
||||
#
|
||||
# Environment variables (or credentials.json):
|
||||
# AUTHENTIK_URL — Authentik instance URL
|
||||
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"
|
||||
|
||||
FORMAT="table"
|
||||
SEARCH=""
|
||||
GROUP=""
|
||||
AK_INSTANCE=""
|
||||
|
||||
while getopts "f:s:g:a: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 ;;
|
||||
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"})
|
||||
|
||||
# Build query params
|
||||
PARAMS="ordering=username"
|
||||
[[ -n "$SEARCH" ]] && PARAMS="${PARAMS}&search=${SEARCH}"
|
||||
[[ -n "$GROUP" ]] && PARAMS="${PARAMS}&groups_by_name=${GROUP}"
|
||||
|
||||
response=$(curl -sk -w "\n%{http_code}" \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
"${AUTHENTIK_URL}/api/v3/core/users/?${PARAMS}")
|
||||
|
||||
http_code=$(echo "$response" | tail -n1)
|
||||
body=$(echo "$response" | sed '$d')
|
||||
|
||||
if [[ "$http_code" != "200" ]]; then
|
||||
echo "Error: Failed to list users (HTTP $http_code)" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ "$FORMAT" == "json" ]]; then
|
||||
echo "$body" | jq '.results'
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Table output
|
||||
echo "USERNAME NAME EMAIL ACTIVE LAST LOGIN"
|
||||
echo "-------------------- ---------------------------- ---------------------------- ------ ----------"
|
||||
echo "$body" | jq -r '.results[] | [
|
||||
.username,
|
||||
.name,
|
||||
.email,
|
||||
(if .is_active then "yes" else "no" end),
|
||||
(.last_login // "never" | split("T")[0])
|
||||
] | @tsv' | while IFS=$'\t' read -r username name email active last_login; do
|
||||
printf "%-20s %-28s %-28s %-6s %s\n" \
|
||||
"${username:0:20}" "${name:0:28}" "${email:0:28}" "$active" "$last_login"
|
||||
done
|
||||
@@ -230,9 +230,9 @@ JSONEOF
|
||||
|
||||
if $FIX_HINT && ! $JSON_OUTPUT; then
|
||||
if [[ "$has_runtime" == "MISS" || "$has_agents" == "MISS" ]]; then
|
||||
echo " ${DIM}Fix: ~/.config/mosaic/rails/bootstrap/init-project.sh --name \"$name\" --type auto${NC}"
|
||||
echo " ${DIM}Fix: ~/.config/mosaic/tools/bootstrap/init-project.sh --name \"$name\" --type auto${NC}"
|
||||
elif [[ "$has_guides" == "MISS" ]]; then
|
||||
echo " ${DIM}Fix: ~/.config/mosaic/rails/bootstrap/agent-upgrade.sh $dir --section conditional-loading${NC}"
|
||||
echo " ${DIM}Fix: ~/.config/mosaic/tools/bootstrap/agent-upgrade.sh $dir --section conditional-loading${NC}"
|
||||
fi
|
||||
fi
|
||||
|
||||
@@ -9,7 +9,7 @@ set -e
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
TEMPLATE_DIR="$HOME/.config/mosaic/templates/agent"
|
||||
GIT_SCRIPT_DIR="$HOME/.config/mosaic/rails/git"
|
||||
GIT_SCRIPT_DIR="$HOME/.config/mosaic/tools/git"
|
||||
SEQUENTIAL_MCP_SCRIPT="$HOME/.config/mosaic/bin/mosaic-ensure-sequential-thinking"
|
||||
|
||||
# Defaults
|
||||
@@ -403,7 +403,7 @@ echo "Created docs/scratchpads/, docs/reports/*, docs/tasks/, docs/releases/, do
|
||||
|
||||
# Set up CI/CD pipeline
|
||||
if [[ "$SKIP_CI" != true ]]; then
|
||||
CODEX_DIR="$HOME/.config/mosaic/rails/codex"
|
||||
CODEX_DIR="$HOME/.config/mosaic/tools/codex"
|
||||
if [[ -d "$CODEX_DIR/woodpecker" ]]; then
|
||||
mkdir -p .woodpecker/schemas
|
||||
cp "$CODEX_DIR/woodpecker/codex-review.yml" .woodpecker/
|
||||
@@ -416,7 +416,7 @@ fi
|
||||
|
||||
# Generate Docker build/push/link pipeline steps
|
||||
if [[ "$CICD_DOCKER" == true ]]; then
|
||||
CICD_SCRIPT="$HOME/.config/mosaic/rails/cicd/generate-docker-steps.sh"
|
||||
CICD_SCRIPT="$HOME/.config/mosaic/tools/cicd/generate-docker-steps.sh"
|
||||
if [[ -x "$CICD_SCRIPT" ]]; then
|
||||
# Parse org and repo from git remote
|
||||
CICD_REGISTRY=""
|
||||
@@ -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)?$|\1|')
|
||||
CICD_REPO_NAME=$(echo "$REPO_URL" | sed -E 's|\.git$||' | sed -E 's|.*/([^/]+)$|\1|')
|
||||
fi
|
||||
|
||||
if [[ -n "$CICD_REGISTRY" && -n "$CICD_ORG" && -n "$CICD_REPO_NAME" && ${#CICD_SERVICES[@]} -gt 0 ]]; then
|
||||
@@ -7,7 +7,7 @@
|
||||
set -e
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
GIT_SCRIPT_DIR="$HOME/.config/mosaic/rails/git"
|
||||
GIT_SCRIPT_DIR="$HOME/.config/mosaic/tools/git"
|
||||
source "$GIT_SCRIPT_DIR/detect-platform.sh"
|
||||
|
||||
SKIP_MILESTONE=false
|
||||
67
tools/cloudflare/_lib.sh
Executable file
67
tools/cloudflare/_lib.sh
Executable file
@@ -0,0 +1,67 @@
|
||||
#!/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"
|
||||
}
|
||||
86
tools/cloudflare/record-create.sh
Executable file
86
tools/cloudflare/record-create.sh
Executable file
@@ -0,0 +1,86 @@
|
||||
#!/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)"
|
||||
55
tools/cloudflare/record-delete.sh
Executable file
55
tools/cloudflare/record-delete.sh
Executable file
@@ -0,0 +1,55 @@
|
||||
#!/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"
|
||||
81
tools/cloudflare/record-list.sh
Executable file
81
tools/cloudflare/record-list.sh
Executable file
@@ -0,0 +1,81 @@
|
||||
#!/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
|
||||
86
tools/cloudflare/record-update.sh
Executable file
86
tools/cloudflare/record-update.sh
Executable file
@@ -0,0 +1,86 @@
|
||||
#!/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)"
|
||||
59
tools/cloudflare/zone-list.sh
Executable file
59
tools/cloudflare/zone-list.sh
Executable file
@@ -0,0 +1,59 @@
|
||||
#!/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
|
||||
@@ -50,45 +50,45 @@ Security vulnerability review focusing on:
|
||||
|
||||
```bash
|
||||
# Code review
|
||||
~/.config/mosaic/rails/codex/codex-code-review.sh --uncommitted
|
||||
~/.config/mosaic/tools/codex/codex-code-review.sh --uncommitted
|
||||
|
||||
# Security review
|
||||
~/.config/mosaic/rails/codex/codex-security-review.sh --uncommitted
|
||||
~/.config/mosaic/tools/codex/codex-security-review.sh --uncommitted
|
||||
```
|
||||
|
||||
### Review a Pull Request
|
||||
|
||||
```bash
|
||||
# Review and post findings as a PR comment
|
||||
~/.config/mosaic/rails/codex/codex-code-review.sh -n 42
|
||||
~/.config/mosaic/tools/codex/codex-code-review.sh -n 42
|
||||
|
||||
# Security review and post to PR
|
||||
~/.config/mosaic/rails/codex/codex-security-review.sh -n 42
|
||||
~/.config/mosaic/tools/codex/codex-security-review.sh -n 42
|
||||
```
|
||||
|
||||
### Review Against Base Branch
|
||||
|
||||
```bash
|
||||
# Code review changes vs main
|
||||
~/.config/mosaic/rails/codex/codex-code-review.sh -b main
|
||||
~/.config/mosaic/tools/codex/codex-code-review.sh -b main
|
||||
|
||||
# Security review changes vs develop
|
||||
~/.config/mosaic/rails/codex/codex-security-review.sh -b develop
|
||||
~/.config/mosaic/tools/codex/codex-security-review.sh -b develop
|
||||
```
|
||||
|
||||
### Review a Specific Commit
|
||||
|
||||
```bash
|
||||
~/.config/mosaic/rails/codex/codex-code-review.sh -c abc123f
|
||||
~/.config/mosaic/rails/codex/codex-security-review.sh -c abc123f
|
||||
~/.config/mosaic/tools/codex/codex-code-review.sh -c abc123f
|
||||
~/.config/mosaic/tools/codex/codex-security-review.sh -c abc123f
|
||||
```
|
||||
|
||||
### Save Results to File
|
||||
|
||||
```bash
|
||||
# Save JSON output
|
||||
~/.config/mosaic/rails/codex/codex-code-review.sh --uncommitted -o review-results.json
|
||||
~/.config/mosaic/rails/codex/codex-security-review.sh --uncommitted -o security-results.json
|
||||
~/.config/mosaic/tools/codex/codex-code-review.sh --uncommitted -o review-results.json
|
||||
~/.config/mosaic/tools/codex/codex-security-review.sh --uncommitted -o security-results.json
|
||||
```
|
||||
|
||||
## Options
|
||||
@@ -113,12 +113,12 @@ Automated PR reviews in CI pipelines.
|
||||
|
||||
1. **Copy the pipeline template to your repo:**
|
||||
```bash
|
||||
cp ~/.config/mosaic/rails/codex/woodpecker/codex-review.yml your-repo/.woodpecker/
|
||||
cp ~/.config/mosaic/tools/codex/woodpecker/codex-review.yml your-repo/.woodpecker/
|
||||
```
|
||||
|
||||
2. **Copy the schemas directory:**
|
||||
```bash
|
||||
cp -r ~/.config/mosaic/rails/codex/schemas your-repo/.woodpecker/
|
||||
cp -r ~/.config/mosaic/tools/codex/schemas your-repo/.woodpecker/
|
||||
```
|
||||
|
||||
3. **Add Codex API key to Woodpecker:**
|
||||
@@ -203,7 +203,7 @@ Automated PR reviews in CI pipelines.
|
||||
|
||||
## Platform Support
|
||||
|
||||
Works with both **GitHub** and **Gitea** via the shared `~/.config/mosaic/rails/git/` infrastructure:
|
||||
Works with both **GitHub** and **Gitea** via the shared `~/.config/mosaic/tools/git/` infrastructure:
|
||||
- Auto-detects platform from git remote
|
||||
- Posts PR comments using `gh` (GitHub) or `tea` (Gitea)
|
||||
- Unified interface across both platforms
|
||||
@@ -261,5 +261,5 @@ For best results, use `gpt-5.2-codex` or newer for strongest review accuracy.
|
||||
## See Also
|
||||
|
||||
- `~/.config/mosaic/guides/CODE-REVIEW.md` — Manual code review checklist
|
||||
- `~/.config/mosaic/rails/git/` — Git helper scripts (issue/PR management)
|
||||
- `~/.config/mosaic/tools/git/` — Git helper scripts (issue/PR management)
|
||||
- OpenAI Codex CLI docs: https://developers.openai.com/codex/cli/
|
||||
64
tools/context/mosaic-context-loader.sh
Executable file
64
tools/context/mosaic-context-loader.sh
Executable file
@@ -0,0 +1,64 @@
|
||||
#!/usr/bin/env bash
|
||||
# mosaic-context-loader.sh — SessionStart hook for Claude Code
|
||||
# Injects mandatory Mosaic config files into agent context at session init.
|
||||
# Stdout from this script is added to Claude's context before processing.
|
||||
set -euo pipefail
|
||||
|
||||
MOSAIC_HOME="${MOSAIC_HOME:-$HOME/.config/mosaic}"
|
||||
|
||||
# Mandatory load order (per AGENTS.md contract)
|
||||
MANDATORY_FILES=(
|
||||
"$MOSAIC_HOME/SOUL.md"
|
||||
"$MOSAIC_HOME/USER.md"
|
||||
"$MOSAIC_HOME/STANDARDS.md"
|
||||
"$MOSAIC_HOME/AGENTS.md"
|
||||
"$MOSAIC_HOME/TOOLS.md"
|
||||
)
|
||||
|
||||
# E2E delivery guide (canonical uppercase path)
|
||||
E2E_DELIVERY=""
|
||||
for candidate in \
|
||||
"$MOSAIC_HOME/guides/E2E-DELIVERY.md"; do
|
||||
if [[ -f "$candidate" ]]; then
|
||||
E2E_DELIVERY="$candidate"
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
# Runtime-specific reference
|
||||
RUNTIME_FILE="$MOSAIC_HOME/runtime/claude/RUNTIME.md"
|
||||
|
||||
# Project-local AGENTS.md (cwd at session start)
|
||||
PROJECT_AGENTS=""
|
||||
if [[ -f "./AGENTS.md" ]]; then
|
||||
PROJECT_AGENTS="./AGENTS.md"
|
||||
fi
|
||||
|
||||
emit_file() {
|
||||
local filepath="$1"
|
||||
local label="${2:-$(basename "$filepath")}"
|
||||
if [[ -f "$filepath" ]]; then
|
||||
echo "=== MOSAIC: $label ==="
|
||||
cat "$filepath"
|
||||
echo ""
|
||||
fi
|
||||
}
|
||||
|
||||
echo "=== MOSAIC CONTEXT INJECTION (SessionStart) ==="
|
||||
echo ""
|
||||
|
||||
for f in "${MANDATORY_FILES[@]}"; do
|
||||
emit_file "$f"
|
||||
done
|
||||
|
||||
if [[ -n "$E2E_DELIVERY" ]]; then
|
||||
emit_file "$E2E_DELIVERY" "E2E-DELIVERY.md"
|
||||
fi
|
||||
|
||||
if [[ -n "$PROJECT_AGENTS" ]]; then
|
||||
emit_file "$PROJECT_AGENTS" "Project AGENTS.md ($(pwd))"
|
||||
fi
|
||||
|
||||
emit_file "$RUNTIME_FILE" "Claude RUNTIME.md"
|
||||
|
||||
echo "=== END MOSAIC CONTEXT INJECTION ==="
|
||||
65
tools/coolify/README.md
Normal file
65
tools/coolify/README.md
Normal file
@@ -0,0 +1,65 @@
|
||||
# Coolify Tool Suite
|
||||
|
||||
Manage Coolify container deployment platform (projects, services, deployments, environment variables).
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- `jq` and `curl` installed
|
||||
- Coolify credentials in `~/src/jarvis-brain/credentials.json` (or `$MOSAIC_CREDENTIALS_FILE`)
|
||||
- Required fields: `coolify.url`, `coolify.app_token`
|
||||
|
||||
## Scripts
|
||||
|
||||
| Script | Purpose |
|
||||
|--------|---------|
|
||||
| `team-list.sh` | List teams |
|
||||
| `project-list.sh` | List projects |
|
||||
| `service-list.sh` | List all services |
|
||||
| `service-status.sh` | Get service details and status |
|
||||
| `deploy.sh` | Trigger service deployment |
|
||||
| `env-set.sh` | Set environment variable on a service |
|
||||
|
||||
## Common Options
|
||||
|
||||
- `-f json` — JSON output (default: table)
|
||||
- `-u uuid` — Service UUID (for service-specific operations)
|
||||
- `-h` — Show help
|
||||
|
||||
## API Reference
|
||||
|
||||
- Base URL: `http://10.1.1.44:8000`
|
||||
- API prefix: `/api/v1/`
|
||||
- Auth: Bearer token in `Authorization` header
|
||||
- Rate limit: 200 requests per interval
|
||||
|
||||
## Known Limitations
|
||||
|
||||
- **FQDN updates on compose sub-apps not supported via API.** Workaround: update directly in Coolify's PostgreSQL DB (`coolify-db` container, `service_applications` table).
|
||||
- **Compose must be base64-encoded** in `docker_compose_raw` field when creating services via API.
|
||||
- **Don't send `type` with `docker_compose_raw`** — API rejects payloads with both fields.
|
||||
|
||||
## Coolify Magic Variables
|
||||
|
||||
Coolify reads special env vars from compose files:
|
||||
- `SERVICE_FQDN_{NAME}_{PORT}` — assigns a domain to a compose service
|
||||
- `SERVICE_URL_{NAME}_{PORT}` — internal URL reference
|
||||
- Must use list-style env syntax (`- SERVICE_FQDN_API_3001`), NOT dict-style.
|
||||
|
||||
## Examples
|
||||
|
||||
```bash
|
||||
# List all projects
|
||||
~/.config/mosaic/tools/coolify/project-list.sh
|
||||
|
||||
# List services as JSON
|
||||
~/.config/mosaic/tools/coolify/service-list.sh -f json
|
||||
|
||||
# Check service status
|
||||
~/.config/mosaic/tools/coolify/service-status.sh -u <uuid>
|
||||
|
||||
# Set an env var
|
||||
~/.config/mosaic/tools/coolify/env-set.sh -u <uuid> -k DATABASE_URL -v "postgres://..."
|
||||
|
||||
# Deploy a service
|
||||
~/.config/mosaic/tools/coolify/deploy.sh -u <uuid>
|
||||
```
|
||||
61
tools/coolify/deploy.sh
Executable file
61
tools/coolify/deploy.sh
Executable file
@@ -0,0 +1,61 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# deploy.sh — Trigger Coolify service deployment
|
||||
#
|
||||
# Usage: deploy.sh -u <uuid> [-f]
|
||||
#
|
||||
# Options:
|
||||
# -u uuid Service UUID (required)
|
||||
# -f Force restart (stop then start)
|
||||
# -h Show this help
|
||||
set -euo pipefail
|
||||
|
||||
MOSAIC_HOME="${MOSAIC_HOME:-$HOME/.config/mosaic}"
|
||||
source "$MOSAIC_HOME/tools/_lib/credentials.sh"
|
||||
load_credentials coolify
|
||||
|
||||
UUID=""
|
||||
FORCE=false
|
||||
|
||||
while getopts "u:fh" opt; do
|
||||
case $opt in
|
||||
u) UUID="$OPTARG" ;;
|
||||
f) FORCE=true ;;
|
||||
h) head -11 "$0" | grep "^#" | sed 's/^# \?//'; exit 0 ;;
|
||||
*) echo "Usage: $0 -u <uuid> [-f]" >&2; exit 1 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ -z "$UUID" ]]; then
|
||||
echo "Error: -u uuid is required" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ "$FORCE" == "true" ]]; then
|
||||
echo "Stopping service $UUID..."
|
||||
curl -s -o /dev/null -w "" \
|
||||
-X POST \
|
||||
-H "Authorization: Bearer $COOLIFY_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
"${COOLIFY_URL}/api/v1/services/${UUID}/stop"
|
||||
sleep 2
|
||||
fi
|
||||
|
||||
echo "Starting service $UUID..."
|
||||
response=$(curl -s -w "\n%{http_code}" \
|
||||
-X POST \
|
||||
-H "Authorization: Bearer $COOLIFY_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
"${COOLIFY_URL}/api/v1/services/${UUID}/start")
|
||||
|
||||
http_code=$(echo "$response" | tail -n1)
|
||||
body=$(echo "$response" | sed '$d')
|
||||
|
||||
if [[ "$http_code" != "200" && "$http_code" != "201" && "$http_code" != "202" ]]; then
|
||||
echo "Error: Deployment failed (HTTP $http_code)" >&2
|
||||
echo "$body" | jq -r '.' 2>/dev/null >&2 || echo "$body" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Deployment triggered successfully for service $UUID"
|
||||
echo "$body" | jq -r '.message // empty' 2>/dev/null || true
|
||||
65
tools/coolify/env-set.sh
Executable file
65
tools/coolify/env-set.sh
Executable file
@@ -0,0 +1,65 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# env-set.sh — Set environment variable on a Coolify service
|
||||
#
|
||||
# Usage: env-set.sh -u <uuid> -k <key> -v <value> [--preview]
|
||||
#
|
||||
# Options:
|
||||
# -u uuid Service UUID (required)
|
||||
# -k key Environment variable name (required)
|
||||
# -v value Environment variable value (required)
|
||||
# --preview Set as preview-only variable
|
||||
# -h Show this help
|
||||
#
|
||||
# Note: Changes take effect on next deploy/restart.
|
||||
set -euo pipefail
|
||||
|
||||
MOSAIC_HOME="${MOSAIC_HOME:-$HOME/.config/mosaic}"
|
||||
source "$MOSAIC_HOME/tools/_lib/credentials.sh"
|
||||
load_credentials coolify
|
||||
|
||||
UUID=""
|
||||
KEY=""
|
||||
VALUE=""
|
||||
IS_PREVIEW="false"
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
-u) UUID="$2"; shift 2 ;;
|
||||
-k) KEY="$2"; shift 2 ;;
|
||||
-v) VALUE="$2"; shift 2 ;;
|
||||
--preview) IS_PREVIEW="true"; shift ;;
|
||||
-h) head -15 "$0" | grep "^#" | sed 's/^# \?//'; exit 0 ;;
|
||||
*) echo "Usage: $0 -u <uuid> -k <key> -v <value> [--preview]" >&2; exit 1 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ -z "$UUID" || -z "$KEY" || -z "$VALUE" ]]; then
|
||||
echo "Error: -u uuid, -k key, and -v value are required" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
payload=$(jq -n \
|
||||
--arg key "$KEY" \
|
||||
--arg value "$VALUE" \
|
||||
--argjson preview "$IS_PREVIEW" \
|
||||
'{key: $key, value: $value, is_preview: $preview}')
|
||||
|
||||
response=$(curl -s -w "\n%{http_code}" \
|
||||
-X PATCH \
|
||||
-H "Authorization: Bearer $COOLIFY_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "$payload" \
|
||||
"${COOLIFY_URL}/api/v1/services/${UUID}/envs")
|
||||
|
||||
http_code=$(echo "$response" | tail -n1)
|
||||
body=$(echo "$response" | sed '$d')
|
||||
|
||||
if [[ "$http_code" != "200" && "$http_code" != "201" ]]; then
|
||||
echo "Error: Failed to set environment variable (HTTP $http_code)" >&2
|
||||
echo "$body" | jq -r '.' 2>/dev/null >&2 || echo "$body" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Set $KEY on service $UUID"
|
||||
echo "Note: Redeploy the service to apply the change"
|
||||
52
tools/coolify/project-list.sh
Executable file
52
tools/coolify/project-list.sh
Executable file
@@ -0,0 +1,52 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# project-list.sh — List Coolify projects
|
||||
#
|
||||
# Usage: project-list.sh [-f format]
|
||||
#
|
||||
# Options:
|
||||
# -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"
|
||||
load_credentials coolify
|
||||
|
||||
FORMAT="table"
|
||||
|
||||
while getopts "f:h" opt; do
|
||||
case $opt in
|
||||
f) FORMAT="$OPTARG" ;;
|
||||
h) head -10 "$0" | grep "^#" | sed 's/^# \?//'; exit 0 ;;
|
||||
*) echo "Usage: $0 [-f format]" >&2; exit 1 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
response=$(curl -s -w "\n%{http_code}" \
|
||||
-H "Authorization: Bearer $COOLIFY_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
"${COOLIFY_URL}/api/v1/projects")
|
||||
|
||||
http_code=$(echo "$response" | tail -n1)
|
||||
body=$(echo "$response" | sed '$d')
|
||||
|
||||
if [[ "$http_code" != "200" ]]; then
|
||||
echo "Error: Failed to list projects (HTTP $http_code)" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ "$FORMAT" == "json" ]]; then
|
||||
echo "$body" | jq '.'
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "UUID NAME DESCRIPTION"
|
||||
echo "------------------------------------ ---------------------------- ----------------------------------------"
|
||||
echo "$body" | jq -r '.[] | [
|
||||
.uuid,
|
||||
.name,
|
||||
(.description // "—")
|
||||
] | @tsv' | while IFS=$'\t' read -r uuid name desc; do
|
||||
printf "%-36s %-28s %s\n" "$uuid" "${name:0:28}" "${desc:0:40}"
|
||||
done
|
||||
53
tools/coolify/service-list.sh
Executable file
53
tools/coolify/service-list.sh
Executable file
@@ -0,0 +1,53 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# service-list.sh — List Coolify services
|
||||
#
|
||||
# Usage: service-list.sh [-f format]
|
||||
#
|
||||
# Options:
|
||||
# -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"
|
||||
load_credentials coolify
|
||||
|
||||
FORMAT="table"
|
||||
|
||||
while getopts "f:h" opt; do
|
||||
case $opt in
|
||||
f) FORMAT="$OPTARG" ;;
|
||||
h) head -10 "$0" | grep "^#" | sed 's/^# \?//'; exit 0 ;;
|
||||
*) echo "Usage: $0 [-f format]" >&2; exit 1 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
response=$(curl -s -w "\n%{http_code}" \
|
||||
-H "Authorization: Bearer $COOLIFY_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
"${COOLIFY_URL}/api/v1/services")
|
||||
|
||||
http_code=$(echo "$response" | tail -n1)
|
||||
body=$(echo "$response" | sed '$d')
|
||||
|
||||
if [[ "$http_code" != "200" ]]; then
|
||||
echo "Error: Failed to list services (HTTP $http_code)" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ "$FORMAT" == "json" ]]; then
|
||||
echo "$body" | jq '.'
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "UUID NAME TYPE STATUS"
|
||||
echo "------------------------------------ ---------------------------- ------------ ----------"
|
||||
echo "$body" | jq -r '.[] | [
|
||||
.uuid,
|
||||
.name,
|
||||
(.type // "unknown"),
|
||||
(.status // "unknown")
|
||||
] | @tsv' | while IFS=$'\t' read -r uuid name type status; do
|
||||
printf "%-36s %-28s %-12s %s\n" "$uuid" "${name:0:28}" "${type:0:12}" "$status"
|
||||
done
|
||||
62
tools/coolify/service-status.sh
Executable file
62
tools/coolify/service-status.sh
Executable file
@@ -0,0 +1,62 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# service-status.sh — Get Coolify service status and details
|
||||
#
|
||||
# Usage: service-status.sh -u <uuid> [-f format]
|
||||
#
|
||||
# Options:
|
||||
# -u uuid Service UUID (required)
|
||||
# -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"
|
||||
load_credentials coolify
|
||||
|
||||
UUID=""
|
||||
FORMAT="table"
|
||||
|
||||
while getopts "u:f:h" opt; do
|
||||
case $opt in
|
||||
u) UUID="$OPTARG" ;;
|
||||
f) FORMAT="$OPTARG" ;;
|
||||
h) head -12 "$0" | grep "^#" | sed 's/^# \?//'; exit 0 ;;
|
||||
*) echo "Usage: $0 -u <uuid> [-f format]" >&2; exit 1 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ -z "$UUID" ]]; then
|
||||
echo "Error: -u uuid is required" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
response=$(curl -s -w "\n%{http_code}" \
|
||||
-H "Authorization: Bearer $COOLIFY_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
"${COOLIFY_URL}/api/v1/services/${UUID}")
|
||||
|
||||
http_code=$(echo "$response" | tail -n1)
|
||||
body=$(echo "$response" | sed '$d')
|
||||
|
||||
if [[ "$http_code" != "200" ]]; then
|
||||
echo "Error: Failed to get service status (HTTP $http_code)" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ "$FORMAT" == "json" ]]; then
|
||||
echo "$body" | jq '.'
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "Service Details"
|
||||
echo "==============="
|
||||
echo "$body" | jq -r '
|
||||
" UUID: \(.uuid)\n" +
|
||||
" Name: \(.name)\n" +
|
||||
" Type: \(.type // "unknown")\n" +
|
||||
" Status: \(.status // "unknown")\n" +
|
||||
" FQDN: \(.fqdn // "none")\n" +
|
||||
" Created: \(.created_at // "unknown")\n" +
|
||||
" Updated: \(.updated_at // "unknown")"
|
||||
'
|
||||
52
tools/coolify/team-list.sh
Executable file
52
tools/coolify/team-list.sh
Executable file
@@ -0,0 +1,52 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# team-list.sh — List Coolify teams
|
||||
#
|
||||
# Usage: team-list.sh [-f format]
|
||||
#
|
||||
# Options:
|
||||
# -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"
|
||||
load_credentials coolify
|
||||
|
||||
FORMAT="table"
|
||||
|
||||
while getopts "f:h" opt; do
|
||||
case $opt in
|
||||
f) FORMAT="$OPTARG" ;;
|
||||
h) head -10 "$0" | grep "^#" | sed 's/^# \?//'; exit 0 ;;
|
||||
*) echo "Usage: $0 [-f format]" >&2; exit 1 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
response=$(curl -s -w "\n%{http_code}" \
|
||||
-H "Authorization: Bearer $COOLIFY_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
"${COOLIFY_URL}/api/v1/teams")
|
||||
|
||||
http_code=$(echo "$response" | tail -n1)
|
||||
body=$(echo "$response" | sed '$d')
|
||||
|
||||
if [[ "$http_code" != "200" ]]; then
|
||||
echo "Error: Failed to list teams (HTTP $http_code)" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ "$FORMAT" == "json" ]]; then
|
||||
echo "$body" | jq '.'
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "ID NAME DESCRIPTION"
|
||||
echo "---- ---------------------------- ----------------------------------------"
|
||||
echo "$body" | jq -r '.[] | [
|
||||
(.id | tostring),
|
||||
.name,
|
||||
(.description // "—")
|
||||
] | @tsv' | while IFS=$'\t' read -r id name desc; do
|
||||
printf "%-4s %-28s %s\n" "$id" "${name:0:28}" "${desc:0:40}"
|
||||
done
|
||||
@@ -31,41 +31,7 @@ Examples:
|
||||
EOF
|
||||
}
|
||||
|
||||
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_remote_host and get_gitea_token are provided by detect-platform.sh
|
||||
|
||||
get_state_from_status_json() {
|
||||
python3 - <<'PY'
|
||||
149
tools/git/detect-platform.sh
Executable file
149
tools/git/detect-platform.sh
Executable file
@@ -0,0 +1,149 @@
|
||||
#!/bin/bash
|
||||
# detect-platform.sh - Detect git platform (Gitea or GitHub) for current repo
|
||||
# Usage: source detect-platform.sh && detect_platform
|
||||
# or: ./detect-platform.sh (prints platform name)
|
||||
|
||||
detect_platform() {
|
||||
local remote_url
|
||||
remote_url=$(git remote get-url origin 2>/dev/null)
|
||||
|
||||
if [[ -z "$remote_url" ]]; then
|
||||
echo "error: not a git repository or no origin remote" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Check for GitHub
|
||||
if [[ "$remote_url" == *"github.com"* ]]; then
|
||||
PLATFORM="github"
|
||||
export PLATFORM
|
||||
echo "github"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Check for common Gitea indicators
|
||||
# Gitea URLs typically don't contain github.com, gitlab.com, bitbucket.org
|
||||
if [[ "$remote_url" != *"gitlab.com"* ]] && \
|
||||
[[ "$remote_url" != *"bitbucket.org"* ]]; then
|
||||
# Assume Gitea for self-hosted repos
|
||||
PLATFORM="gitea"
|
||||
export PLATFORM
|
||||
echo "gitea"
|
||||
return 0
|
||||
fi
|
||||
|
||||
PLATFORM="unknown"
|
||||
export PLATFORM
|
||||
echo "unknown"
|
||||
return 1
|
||||
}
|
||||
|
||||
get_repo_info() {
|
||||
local remote_url
|
||||
remote_url=$(git remote get-url origin 2>/dev/null)
|
||||
|
||||
if [[ -z "$remote_url" ]]; then
|
||||
echo "error: not a git repository or no origin remote" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Extract owner/repo from URL
|
||||
# Handles: git@host:owner/repo.git, https://host/owner/repo.git, https://host/owner/repo
|
||||
local repo_path
|
||||
if [[ "$remote_url" == git@* ]]; then
|
||||
repo_path="${remote_url#*:}"
|
||||
else
|
||||
repo_path="${remote_url#*://}"
|
||||
repo_path="${repo_path#*/}"
|
||||
fi
|
||||
|
||||
# Remove .git suffix if present
|
||||
repo_path="${repo_path%.git}"
|
||||
|
||||
echo "$repo_path"
|
||||
}
|
||||
|
||||
get_repo_owner() {
|
||||
local repo_info
|
||||
repo_info=$(get_repo_info)
|
||||
echo "${repo_info%%/*}"
|
||||
}
|
||||
|
||||
get_repo_name() {
|
||||
local repo_info
|
||||
repo_info=$(get_repo_info)
|
||||
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
|
||||
fi
|
||||
@@ -13,40 +13,7 @@ BODY=""
|
||||
LABELS=""
|
||||
MILESTONE=""
|
||||
|
||||
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_remote_host and get_gitea_token are provided by detect-platform.sh
|
||||
|
||||
gitea_issue_create_api() {
|
||||
local host repo token url payload
|
||||
@@ -10,40 +10,7 @@ source "$SCRIPT_DIR/detect-platform.sh"
|
||||
# Parse arguments
|
||||
ISSUE_NUMBER=""
|
||||
|
||||
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_remote_host and get_gitea_token are provided by detect-platform.sh
|
||||
|
||||
gitea_issue_view_api() {
|
||||
local host repo token url
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user