Compare commits

25 Commits

Author SHA1 Message Date
Jason Woltje
9fbfdcee6d fix(woodpecker): add step-level details and fix timestamps in pipeline-status
- Show individual step names with OK/FAIL/RUN/SKIP/WAIT status
- Show error messages and exit codes for failed steps
- Convert epoch timestamps to ISO 8601
- Always fetch full pipeline detail (list endpoint lacks workflows)
- Fix started_at/finished_at field names (API uses started/finished)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 12:05:30 -06:00
Jason Woltje
21afb58b33 feat: multi-instance Authentik credentials with test_user support
Add -a <instance> flag to all Authentik wrapper scripts, matching the
existing multi-instance pattern used by Woodpecker and Cloudflare.

credentials.json now supports per-instance Authentik config:
  authentik.<instance>.url      — instance URL
  authentik.<instance>.token    — API token (admin wrappers)
  authentik.<instance>.test_user — username/password (Playwright/agent tests)
  authentik.default             — default instance name

Legacy flat structure (authentik.url) still works as fallback.
Token cache is now per-instance (~/.cache/mosaic/authentik-token-<name>).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-24 17:46:15 -06:00
09786ee6e0 fix: use Mosaic credential loader for Gitea API token resolution (#7) 2026-02-24 22:11:44 +00:00
1fd67b9ec0 docs: update quality rails docs for gitleaks migration (#6) 2026-02-24 21:33:25 +00:00
38223c8ec2 feat: add gitleaks secret scanning to quality rails (#5) 2026-02-24 20:46:50 +00:00
Jason Woltje
8de2f7439a fix: make credentials.json authoritative for Woodpecker, auto-sync to .env
- Woodpecker tokens from credentials.json now always override env vars,
  preventing stale .bashrc or env leakage from silently winning
- After loading, credentials are synced to ~/.woodpecker/<instance>.env
  so the wp CLI wrapper stays current automatically
- Sync only writes when values differ to avoid unnecessary disk I/O

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-24 13:03:29 -06:00
Jason Woltje
98b9bc3c93 docs: document Woodpecker multi-instance usage and instance-to-repo mapping
Agents had no guidance on which Woodpecker instance serves which repos,
leading to repeated 401 failures and workaround attempts.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-24 12:54:44 -06:00
b1403703b1 feat: add prdy-status command and PRD status injection into system prompt
- Add prdy-status.sh for quick one-liner PRD health check (short/json output)
- Inject PRD section count and assumption count into agent system prompt
  so the agent knows PRD state at session start without running validate
- Add status subcommand to mosaic prdy routing and help text

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-23 18:33:33 -06:00
Jason Woltje
abead17e0e feat: add multi-runtime support (coord run, prdy --codex) and next-task capsule
- coord/prdy subcommands now accept --claude/--codex runtime flags
- New `mosaic coord run` generates continuation context and launches
  selected runtime, replacing manual copy/paste workflow
- Next-task capsule (.mosaic/orchestrator/next-task.json) provides
  machine-readable execution context for deterministic session launches
- Codex strict orchestrator profile added to runtime/codex/RUNTIME.md
- Orchestrator protocol updated with between-session run flow
- New smoke-test.sh for orchestration behavior verification

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-23 18:27:09 -06:00
Jason Woltje
fbf74c2736 fix: strip .git suffix in repo detection (POSIX ERE non-greedy bug)
POSIX ERE doesn't support non-greedy +? quantifier, so the pattern
([^/]+?)(\.git)?$ matched .git as part of the repo name instead of
stripping it. Split into two sed passes: strip .git first, then
extract owner/repo.

Fixes wp_detect_repo() and init-project.sh CICD_REPO_NAME.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-23 13:08:47 -06:00
Jason Woltje
364d6c2278 fix: use Woodpecker v3 numeric repo IDs in API calls
Woodpecker v3 requires numeric repo IDs in API endpoints, not
owner/repo path segments. The old paths hit the SPA frontend
catch-all and return HTML, which downstream tools misinterpret
as auth failure (401).

- Add tools/woodpecker/_lib.sh with wp_resolve_repo_id() helper
  that calls /api/repos/lookup/{owner}/{repo} to get numeric ID
- Update all 3 pipeline scripts to resolve repo ID before API calls

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-23 12:54:03 -06:00
Jason Woltje
93efbcdafe fix: align codex mission resume and uppercase guide refs 2026-02-23 12:29:37 -06:00
Jason Woltje
def9c2fd7a feat: add Woodpecker multi-instance credential support
Add named instance support matching the existing cloudflare pattern:
- credentials.sh: woodpecker-<name> loads .woodpecker.<name>.{url,token}
- credentials.sh: bare woodpecker resolves via .woodpecker.default or
  WOODPECKER_INSTANCE env, with legacy flat-key fallback
- All 3 pipeline tools accept -a <instance> flag to select instance

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-23 11:40:51 -06:00
Jason Woltje
87501ea952 fix: rename guide files to match AGENTS.md references (case-sensitive)
e2e-delivery.md → E2E-DELIVERY.md
orchestrator.md → ORCHESTRATOR.md
ci-cd-pipelines.md → CI-CD-PIPELINES.md

Agents on case-sensitive filesystems couldn't find these guides because
AGENTS.md referenced uppercase names but the files were lowercase.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-23 11:30:33 -06:00
Jason Woltje
7a5f28c8b5 feat: write session lock from all launcher paths
All launch paths (claude, codex, opencode, yolo variants) now write a
session.lock before exec'ing, so `mosaic coord status` can detect
running agent sessions. Stale locks from dead sessions are cleaned up
automatically on next launch.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-23 11:14:57 -06:00
Jason Woltje
405bc4c797 fix: show mission context when no active session in coord status
Previously `mosaic coord status` only said "No active session" with no
indication of whether a mission existed. Now shows mission name, status,
milestones/tasks progress, and actionable next steps.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-23 11:08:16 -06:00
Jason Woltje
c9bf578396 feat: add mosaic prdy command for PRD creation and validation
Adds `mosaic prdy {init|update|validate}` subcommand:
- init: launches yolo Claude session with PRD-focused system prompt
- update: launches session to modify existing docs/PRD.md
- validate: bash-only completeness checker (15 checks against PRD guide)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-23 11:04:35 -06:00
c1f4830bf5 feat: add subagent model selection guidance for cost optimization
Global AGENTS.md: task-type-to-model-tier mapping table with decision
rule — haiku for search/status, sonnet for standard coding/review,
opus only for complex architecture and security.

Claude RUNTIME.md: Task tool model parameter syntax with examples
and quick reference table.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-23 07:37:44 -06:00
e5c4bf25b3 feat: add Cloudflare DNS tool suite with multi-instance support
- zone-list, record-list, record-create, record-update, record-delete
- Named instance support (-a flag) with configurable default
- Zone name-to-ID auto-resolution in shared _lib.sh
- Updated credentials loader with cloudflare/cloudflare-<name> services
- TOOLS.md and INFRASTRUCTURE.md guide documentation

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 21:31:52 -06:00
a9623e9219 fix: add hard gates for manual docker build bypass and intake skipping
Post-mortem from website agent session that manually built/pushed Docker
images instead of using existing Woodpecker CI pipelines. Root cause:
agent skipped E2E intake because the task "felt simple."

AGENTS.md hard gates 10-12:
- Manual docker build/push FORBIDDEN when CI pipelines exist
- MUST check for pipeline config before any build/deploy action
- Load order and intake are NOT conditional on task complexity

E2E-DELIVERY.md:
- Complexity trap warning on intake section
- Mandatory deployment surface check (step 3) with pipeline discovery
- Expanded forbidden anti-patterns with Build/Deploy section

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 18:18:36 -06:00
5d666bdca9 fix: move mission context to top of system prompt + auto-inject initial prompt
Mission context was buried at the end of a 21K char system prompt and the
agent ignored it. Two fixes:
1. Mission block now emits FIRST in build_runtime_prompt() so it's the most
   prominent instruction the agent sees
2. When an active mission exists and no user prompt is given, auto-inject
   an initial user message triggering the agent to read mission state files

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 17:52:44 -06:00
221afe94d9 feat: inject active mission context into agent system prompt
The session-start hook approach didn't work — Claude Code's TUI
overwrites stdout before the agent sees it, and the hook only fires
when the agent calls it as a tool.

Instead, inject mission context directly into the composed system
prompt via build_runtime_prompt(). When mission.json is active in
CWD, the agent gets mission name, ID, milestone progress, and
mandatory first-action instructions in its initial context.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 17:44:36 -06:00
612796d8e0 fix: prevent task count line break in session-start template
grep -c returns empty on no match, causing arithmetic to break
across lines. Use ${var:-0} fallback pattern.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 17:42:41 -06:00
5ba531e2d0 feat: r0 coordinator tooling for orchestrator protocol
Implements the manual coordinator workflow for multi-session agent
orchestration. Agents stop after one milestone (confirmed limitation);
these tools let the human coordinator check status, generate continuation
prompts, and chain sessions together.

New:
- tools/orchestrator/ — 5 scripts + shared library (_lib.sh)
  - mission-init.sh: initialize mission with milestones and state files
  - mission-status.sh: dashboard showing milestones, tasks, sessions
  - session-status.sh: check if agent is running/stale/dead
  - continue-prompt.sh: generate paste-ready continuation prompt
  - session-resume.sh: crash recovery with dirty state detection
- guides/ORCHESTRATOR-PROTOCOL.md: agent-facing mission lifecycle guide
- templates/docs/: mission manifest, scratchpad, continuation templates
- templates/repo/.mosaic/orchestrator/mission.json: state file template

Modified:
- bin/mosaic: add 'coord' subcommand + resume advisory on launch
- AGENTS.md: conditional loading for protocol guide + rule 37
- bin/mosaic-doctor: checks for new coordinator files
- session hooks: mission detection on start, cleanup on end

Usage: mosaic coord init|mission|status|continue|resume

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 17:22:50 -06:00
a8e580e1a3 feat: rename rails/ to tools/ and add service tool suites (#4)
Co-authored-by: Jason Woltje <jason@diversecanvas.com>
Co-committed-by: Jason Woltje <jason@diversecanvas.com>
2026-02-22 17:52:23 +00:00
193 changed files with 7317 additions and 496 deletions

1
.gitignore vendored
View File

@@ -1 +1,2 @@
node_modules/
rails

View File

@@ -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.

View File

@@ -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.

View File

@@ -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

View File

@@ -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
View File

@@ -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

View File

@@ -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).

View File

@@ -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 "$@" ;;

View File

@@ -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

View File

@@ -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"

View File

@@ -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")

View File

@@ -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
}

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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**

View File

@@ -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

View File

@@ -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/`)

View File

@@ -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:

View File

@@ -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

View File

@@ -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.

View File

@@ -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

View File

@@ -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)

View 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)

View File

@@ -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.

View File

@@ -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.

View File

@@ -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"

View File

@@ -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

View File

@@ -1,2 +0,0 @@
npx lint-staged
npx git-secrets --scan || echo "Warning: git-secrets not installed"

View File

@@ -1,2 +0,0 @@
npx lint-staged
npx git-secrets --scan || echo "Warning: git-secrets not installed"

View File

@@ -1,2 +0,0 @@
npx lint-staged
npx git-secrets --scan || echo "Warning: git-secrets not installed"

View File

@@ -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.

View File

@@ -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
}
]

View File

@@ -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.

View File

@@ -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.

View File

@@ -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.

View File

@@ -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 \

View File

@@ -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.

View 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`

View 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`

View 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. -->

View 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": []
}

View File

@@ -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

View File

@@ -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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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

View File

@@ -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

View File

@@ -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

View File

@@ -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
View 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"
}

View 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)"

View 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
View 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

View 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
View 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

View File

@@ -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/

View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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

View File

@@ -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
View 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

View File

@@ -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

View File

@@ -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