Compare commits

31 Commits

Author SHA1 Message Date
Jason Woltje
8c960eee9d feat: integrate excalidraw MCP into bootstrap and runtime setup
- install.sh: run mosaic-ensure-excalidraw post-install (non-fatal)
- runtime-setup.ts: configure excalidraw MCP during wizard setup
- bin/mosaic-ensure-excalidraw: install deps + register MCP with Claude
- runtime/mcp/EXCALIDRAW.json: MCP server config template
- tools/excalidraw/: headless .excalidraw → SVG export server

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-04 17:59:33 -06:00
f380d232e6 docs: add MCP registration section with Claude Code config gotchas
Documents the correct MCP registration flow: claude mcp add --scope user
writes to ~/.claude.json (not settings.json, which silently ignores
mcpServers). Covers scope semantics, http vs sse transport requirement
for FastMCP, sequential-thinking hard requirement, and OpenBrain setup.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-02 21:34:37 -06:00
8b441c17b7 fix: MCP servers belong in ~/.claude.json not settings.json
Claude Code reads mcpServers from ~/.claude.json (global state file),
NOT from ~/.claude/settings.json. The settings.json mcpServers key is
silently ignored for MCP loading. Using claude mcp add --scope user
writes to the correct file.

Also document correct registration commands and scope semantics in
RUNTIME.md so agents and users know how to add MCPs that actually load.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-02 21:31:22 -06:00
2a91f6c202 feat: hard-gate agent memory to OpenBrain via PreToolUse hook
Agents consistently ignore written instructions about memory routing
and default to writing local MEMORY.md files regardless of rules in
RUNTIME.md, CLAUDE.md, or MEMORY.md itself. Instructions alone are
insufficient — a technical gate is required.

Changes:
- Add tools/qa/prevent-memory-write.sh — PreToolUse hook that blocks
  Write/Edit/MultiEdit to ~/.claude/projects/*/memory/*.md (exit 2)
- Register hook in runtime/claude/settings.json PreToolUse array
- Update runtime/claude/RUNTIME.md: replace soft "Memory Override"
  note with hard-gate policy, what-goes-where table, and rationale
- Rewrite guides/MEMORY.md: OpenBrain as primary layer, blocked silos
  table, project continuity files, how-the-hook-works section

The correct behavior is now the only possible behavior for Claude Code.
All agent learnings route to OpenBrain where every harness can read them.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-02 21:15:28 -06:00
97ee66770a docs: add full CRUD API patterns to OpenBrain section
REST endpoints (GET/PATCH/DELETE /v1/thoughts/{id}, bulk DELETE/GET
with filters) and updated MCP tools list to include get, update,
delete, delete_where, list_thoughts — all live in v0.1.0.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-02 20:53:21 -06:00
30ce4cecc7 feat: add openbrain + turbo-cache credential support and OpenBrain tool docs
- credentials.sh: add turbo-cache and openbrain cases (load_credentials openbrain
  exports OPENBRAIN_URL + OPENBRAIN_TOKEN from credentials.json .openbrain.*)
- credentials.sh: update --help text and error messages to list new services
- TOOLS.md: mark Coolify as DEPRECATED (superseded by Portainer Docker Swarm)
- TOOLS.md: update Shared Credential Loader service list (turbo-cache, openbrain)
- TOOLS.md: add OpenBrain section — primary shared memory layer, REST API patterns,
  Python client usage, MCP note, and mandatory usage table

credentials.sh is always overwritten on reinstall (not in PRESERVE_PATHS), so all
agents that run install.sh will automatically get openbrain credential support.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-02 20:10:40 -06:00
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
204 changed files with 8161 additions and 529 deletions

1
.gitignore vendored
View File

@@ -1 +1,2 @@
node_modules/ 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. 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. 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. 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`. 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/rails/git/*.sh`). 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. 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?". 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 ## 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. 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. 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`. 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. 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. 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. 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...` 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. 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. 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) ## 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` | | QA and test strategy | `~/.config/mosaic/guides/QA-TESTING.md` |
| Secrets and vault usage | `~/.config/mosaic/guides/VAULT-SECRETS.md` | | Secrets and vault usage | `~/.config/mosaic/guides/VAULT-SECRETS.md` |
| Orchestrator estimation heuristics | `~/.config/mosaic/guides/ORCHESTRATOR-LEARNINGS.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) ## 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. - 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. - 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 ## Skills Policy
- Use only the minimum required skills for the active task. - Use only the minimum required skills for the active task.

View File

@@ -24,19 +24,19 @@ Scope:
### MF-001 (QA rails path correction) ### MF-001 (QA rails path correction)
Updated: Updated:
- `rails/qa/qa-hook-wrapper.sh` - `tools/qa/qa-hook-wrapper.sh`
- `rails/qa/qa-hook-stdin.sh` - `tools/qa/qa-hook-stdin.sh`
- `rails/qa/qa-hook-handler.sh` - `tools/qa/qa-hook-handler.sh`
- `rails/qa/remediation-hook-handler.sh` - `tools/qa/remediation-hook-handler.sh`
- `rails/qa/qa-queue-monitor.sh` - `tools/qa/qa-queue-monitor.sh`
Change: 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) ### MF-002 + MF-003 (conditional loading/context detection)
Updated: Updated:
- `rails/bootstrap/agent-lint.sh` - `tools/bootstrap/agent-lint.sh`
- `rails/bootstrap/agent-upgrade.sh` - `tools/bootstrap/agent-upgrade.sh`
- `templates/agent/SPEC.md` - `templates/agent/SPEC.md`
Change: Change:
@@ -58,7 +58,7 @@ Updated:
- `skills/pr-reviewer/SKILL.md` - `skills/pr-reviewer/SKILL.md`
Change: 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/...`. - Replaced `~/.claude/skills/...` with `~/.config/mosaic/skills/...`.
### MF-006 (worktree skill docs hierarchy) ### MF-006 (worktree skill docs hierarchy)
@@ -109,7 +109,7 @@ These are required to support existing Claude runtime integration while keeping
Executed checks: Executed checks:
- `rg -n "~/.claude|\\.claude/|agent-guides" ~/src/agent-skills -S` - `rg -n "~/.claude|\\.claude/|agent-guides" ~/src/agent-skills -S`
- Result: no matches after remediation. - 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. - Result: no invalid old-style QA rail paths remain.
- Installed runtime validation: - 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.) ├── bin/ ← CLI tools (mosaic, mosaic-init, mosaic-doctor, etc.)
├── dist/ ← Bundled wizard (mosaic-wizard.mjs) ├── dist/ ← Bundled wizard (mosaic-wizard.mjs)
├── guides/ ← Operational guides ├── 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 ├── runtime/ ← Runtime adapters + runtime-specific references
│ ├── claude/CLAUDE.md │ ├── claude/CLAUDE.md
│ ├── claude/RUNTIME.md │ ├── claude/RUNTIME.md
@@ -228,17 +228,57 @@ Re-sync manually:
~/.config/mosaic/bin/mosaic-link-runtime-assets ~/.config/mosaic/bin/mosaic-link-runtime-assets
``` ```
## sequential-thinking MCP Requirement ## MCP Registration
sequential-thinking MCP is a hard requirement for Mosaic Stack. ### How MCPs Are Configured in Claude Code
Use: **MCPs must be registered via `claude mcp add` — not by hand-editing `~/.claude/settings.json`.**
`settings.json` controls hooks, model, plugins, and allowed commands. The `mcpServers` key in
`settings.json` is silently ignored by Claude Code's MCP loader. The correct file is `~/.claude.json`,
which is managed by the `claude mcp` CLI.
```bash
# Register a stdio MCP (user scope = all projects, persists across sessions)
claude mcp add --scope user <name> -- npx -y <package>
# Register an HTTP MCP (e.g. OpenBrain)
claude mcp add --scope user --transport http <name> <url> \
--header "Authorization: Bearer <token>"
# List registered MCPs
claude mcp list
```
**Scope options:**
- `--scope user` — writes to `~/.claude.json`, available in all projects (recommended for shared tools)
- `--scope project` — writes to `.claude/settings.json` in the project root, committed to the repo
- `--scope local` — default, machine-local only, not committed
**Transport for HTTP MCPs must be `http`** — not `sse`. `type: "sse"` is a deprecated protocol
that silently fails to connect against FastMCP streamable HTTP servers.
### sequential-thinking MCP (Hard Requirement)
sequential-thinking MCP is required for Mosaic Stack. The installer registers it automatically.
To verify or re-register manually:
```bash ```bash
~/.config/mosaic/bin/mosaic-ensure-sequential-thinking ~/.config/mosaic/bin/mosaic-ensure-sequential-thinking
~/.config/mosaic/bin/mosaic-ensure-sequential-thinking --check ~/.config/mosaic/bin/mosaic-ensure-sequential-thinking --check
``` ```
### OpenBrain Semantic Memory (Recommended)
OpenBrain is the shared cross-agent memory layer. Register once per machine:
```bash
claude mcp add --scope user --transport http openbrain https://your-openbrain-host/mcp \
--header "Authorization: Bearer YOUR_TOKEN"
```
See [mosaic/openbrain](https://git.mosaicstack.dev/mosaic/openbrain) for setup and API docs.
## Bootstrap Any Repo ## Bootstrap Any Repo
Attach any repository to the Mosaic standards layer: Attach any repository to the Mosaic standards layer:

View File

@@ -12,16 +12,16 @@ Master/slave model:
2. Load project-local `AGENTS.md` next. 2. Load project-local `AGENTS.md` next.
3. Respect repository-specific tooling and workflows. 3. Respect repository-specific tooling and workflows.
4. Use lifecycle scripts when available (`scripts/agent/*.sh`). 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 ## Non-Negotiables
- Data files are authoritative; generated views are derived artifacts. - Data files are authoritative; generated views are derived artifacts.
- Pull before edits when collaborating in shared repos. - Pull before edits when collaborating in shared repos.
- Run validation checks before claiming completion. - Run validation checks before claiming completion.
- Apply quality rails from `~/.config/mosaic/rails/` when relevant (review, QA, git workflow). - Apply quality tools from `~/.config/mosaic/tools/` 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 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/rails/orchestrator-matrix/` with repo-local `.mosaic/orchestrator/` state. - 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. - Avoid hardcoded secrets and token leakage in remotes/commits.
- Do not perform destructive git/file actions without explicit instruction. - 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. - 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. before task execution.
Runtime-compatible guides and rails are hosted at: Runtime-compatible guides and tools are hosted at:
- `~/.config/mosaic/guides/` - `~/.config/mosaic/guides/`
- `~/.config/mosaic/rails/` - `~/.config/mosaic/tools/`
- `~/.config/mosaic/profiles/` (runtime-neutral domain/workflow/stack presets) - `~/.config/mosaic/profiles/` (runtime-neutral domain/workflow/stack presets)
- `~/.config/mosaic/runtime/` (runtime-specific overlays) - `~/.config/mosaic/runtime/` (runtime-specific overlays)
- `~/.config/mosaic/skills-local/` (local private skills shared across runtimes) - `~/.config/mosaic/skills-local/` (local private skills shared across runtimes)

212
TOOLS.md
View File

@@ -3,36 +3,213 @@
Centralized reference for tools, credentials, and CLI patterns available across all projects. Centralized reference for tools, credentials, and CLI patterns available across all projects.
Project-specific tooling belongs in the project's `AGENTS.md`, not here. 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 ```bash
# Issues # Issues
~/.config/mosaic/rails/git/issue-create.sh ~/.config/mosaic/tools/git/issue-create.sh
~/.config/mosaic/rails/git/issue-close.sh ~/.config/mosaic/tools/git/issue-close.sh
# PRs # PRs
~/.config/mosaic/rails/git/pr-create.sh ~/.config/mosaic/tools/git/pr-create.sh
~/.config/mosaic/rails/git/pr-merge.sh ~/.config/mosaic/tools/git/pr-merge.sh
# Milestones # Milestones
~/.config/mosaic/rails/git/milestone-create.sh ~/.config/mosaic/tools/git/milestone-create.sh
# CI queue guard (required before push/merge) # 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 ```bash
# Code quality review ~/.config/mosaic/tools/codex/codex-code-review.sh --uncommitted
~/.config/mosaic/rails/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 (DEPRECATED)
> Coolify has been superseded by Portainer Docker Swarm in this stack.
> Tools remain for reference but should not be used for new deployments.
```bash
# DEPRECATED — do not use for new deployments
~/.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, turbo-cache, openbrain
```
### OpenBrain — Semantic Memory (PRIMARY)
Self-hosted semantic brain backed by pgvector. Primary shared memory layer for all agents across all sessions and harnesses. Stores and retrieves decisions, context, project state, and observations via semantic search.
**Credentials:** `load_credentials openbrain` → exports `OPENBRAIN_URL`, `OPENBRAIN_TOKEN`
Configure in your credentials.json:
```json
"openbrain": {
"url": "https://<your-openbrain-host>",
"api_key": "<your-api-key>"
}
```
**REST API** (any language, any harness):
```bash
source ~/.config/mosaic/tools/_lib/credentials.sh && load_credentials openbrain
# --- Read ---
curl -s -H "Authorization: Bearer $OPENBRAIN_TOKEN" "$OPENBRAIN_URL/v1/thoughts/recent?limit=5"
curl -s -H "Authorization: Bearer $OPENBRAIN_TOKEN" "$OPENBRAIN_URL/v1/thoughts/{id}"
curl -s -H "Authorization: Bearer $OPENBRAIN_TOKEN" \
"$OPENBRAIN_URL/v1/thoughts?source=agent-name&metadata_id=my-entity&limit=10"
# --- Search ---
curl -s -X POST -H "Authorization: Bearer $OPENBRAIN_TOKEN" -H "Content-Type: application/json" \
-d '{"query": "your search", "limit": 5}' "$OPENBRAIN_URL/v1/search"
# --- Capture ---
curl -s -X POST -H "Authorization: Bearer $OPENBRAIN_TOKEN" -H "Content-Type: application/json" \
-d '{"content": "...", "source": "agent-name", "metadata": {}}' "$OPENBRAIN_URL/v1/thoughts"
# --- Update (re-embeds if content changes) ---
curl -s -X PATCH -H "Authorization: Bearer $OPENBRAIN_TOKEN" -H "Content-Type: application/json" \
-d '{"content": "updated text", "metadata": {"key": "val"}}' "$OPENBRAIN_URL/v1/thoughts/{id}"
# --- Delete single ---
curl -s -X DELETE -H "Authorization: Bearer $OPENBRAIN_TOKEN" "$OPENBRAIN_URL/v1/thoughts/{id}"
# --- Bulk delete by filter (source and/or metadata_id required) ---
curl -s -X DELETE -H "Authorization: Bearer $OPENBRAIN_TOKEN" \
"$OPENBRAIN_URL/v1/thoughts?source=agent-name&metadata_id=my-entity"
# --- Stats ---
curl -s -H "Authorization: Bearer $OPENBRAIN_TOKEN" "$OPENBRAIN_URL/v1/stats"
```
**Python client** (if jarvis-brain is available on PYTHONPATH):
```bash
python tools/openbrain_client.py search "topic"
python tools/openbrain_client.py capture "decision or observation" --source agent-name
python tools/openbrain_client.py recent --limit 5
python tools/openbrain_client.py stats
```
**MCP (Claude Code sessions):** When connected, all CRUD tools are available natively:
`capture`, `search`, `recent`, `stats`, `get`, `update`, `delete`, `delete_where`, `list_thoughts`
**When to use openbrain (required for all agents):**
| Trigger | Action |
|---------|--------|
| Session start | Search/recent to load prior context |
| Significant decision made | Capture with rationale |
| Blocker or gotcha discovered | Capture immediately |
| Task or milestone completed | Capture summary |
| Cross-agent handoff | Capture current state |
## Git Providers ## Git Providers
| Instance | URL | CLI | Purpose | | Instance | URL | CLI | Purpose |
@@ -42,16 +219,13 @@ Mosaic wrappers at `~/.config/mosaic/rails/git/*.sh` handle platform detection a
## Credentials ## Credentials
**Location:** (configure your credential file path) **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.** **Never expose actual values. Never commit credential files.**
## CLI Gotchas ## CLI Gotchas
(Add platform-specific CLI gotchas as you discover them. Examples: TTY requirements, default list limits, API fallback patterns.) (Add platform-specific CLI gotchas as you discover them.)
## Custom Tools
(Add any machine-specific tools, scripts, or workflows here.)
## Safety Defaults ## Safety Defaults

View File

@@ -14,4 +14,4 @@ Use wrapper commands from `~/.config/mosaic/bin/` for lifecycle rituals.
## Migration Note ## Migration Note
Project-local `.claude/commands/*.md` should call `scripts/agent/*.sh` so behavior stays runtime-neutral. 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 release-upgrade [...] Upgrade installed Mosaic release
project-upgrade [...] Clean up stale SOUL.md/CLAUDE.md in a project 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: Options:
-h, --help Show this help -h, --help Show this help
-v, --version Show version -v, --version Show version
@@ -130,6 +146,78 @@ build_runtime_prompt() {
exit 1 exit 1
fi 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' cat <<'EOF'
# Mosaic Launcher Runtime Contract (Hard Gate) # Mosaic Launcher Runtime Contract (Hard Gate)
@@ -179,6 +267,63 @@ ensure_runtime_config() {
fi 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 # Launcher functions
launch_claude() { launch_claude() {
check_mosaic_home check_mosaic_home
@@ -187,11 +332,23 @@ launch_claude() {
check_runtime "claude" check_runtime "claude"
check_sequential_thinking "claude" check_sequential_thinking "claude"
_check_resumable_session
# Claude supports --append-system-prompt for direct injection # Claude supports --append-system-prompt for direct injection
local runtime_prompt local runtime_prompt
runtime_prompt="$(build_runtime_prompt "claude")" 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() { launch_opencode() {
@@ -201,8 +358,12 @@ launch_opencode() {
check_runtime "opencode" check_runtime "opencode"
check_sequential_thinking "opencode" check_sequential_thinking "opencode"
_check_resumable_session
# OpenCode reads from ~/.config/opencode/AGENTS.md # OpenCode reads from ~/.config/opencode/AGENTS.md
ensure_runtime_config "opencode" "$HOME/.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..." echo "[mosaic] Launching OpenCode..."
exec opencode "$@" exec opencode "$@"
} }
@@ -214,10 +375,20 @@ launch_codex() {
check_runtime "codex" check_runtime "codex"
check_sequential_thinking "codex" check_sequential_thinking "codex"
_check_resumable_session
# Codex reads from ~/.codex/instructions.md # Codex reads from ~/.codex/instructions.md
ensure_runtime_config "codex" "$HOME/.codex/instructions.md" ensure_runtime_config "codex" "$HOME/.codex/instructions.md"
echo "[mosaic] Launching Codex..." _detect_mission_prompt
exec codex "$@" _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() { launch_yolo() {
@@ -241,8 +412,17 @@ launch_yolo() {
# Claude uses an explicit dangerous permissions flag. # Claude uses an explicit dangerous permissions flag.
local runtime_prompt local runtime_prompt
runtime_prompt="$(build_runtime_prompt "claude")" 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) codex)
check_mosaic_home check_mosaic_home
@@ -253,8 +433,16 @@ launch_yolo() {
# Codex reads instructions.md from ~/.codex and supports a direct dangerous flag. # Codex reads instructions.md from ~/.codex and supports a direct dangerous flag.
ensure_runtime_config "codex" "$HOME/.codex/instructions.md" ensure_runtime_config "codex" "$HOME/.codex/instructions.md"
echo "[mosaic] Launching Codex in YOLO mode (dangerous permissions enabled)..." _detect_mission_prompt
exec codex --dangerously-bypass-approvals-and-sandbox "$@" _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) opencode)
check_mosaic_home check_mosaic_home
@@ -265,6 +453,8 @@ launch_yolo() {
# OpenCode defaults to allow-all permissions unless user config restricts them. # OpenCode defaults to allow-all permissions unless user config restricts them.
ensure_runtime_config "opencode" "$HOME/.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 in YOLO mode..." echo "[mosaic] Launching OpenCode in YOLO mode..."
exec opencode "$@" exec opencode "$@"
;; ;;
@@ -325,6 +515,195 @@ run_seq() {
esac 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() { run_bootstrap() {
check_mosaic_home check_mosaic_home
exec "$MOSAIC_HOME/bin/mosaic-bootstrap-repo" "$@" exec "$MOSAIC_HOME/bin/mosaic-bootstrap-repo" "$@"
@@ -397,6 +776,8 @@ case "$command" in
sync) run_sync "$@" ;; sync) run_sync "$@" ;;
seq) run_seq "$@" ;; seq) run_seq "$@" ;;
bootstrap) run_bootstrap "$@" ;; bootstrap) run_bootstrap "$@" ;;
prdy) run_prdy "$@" ;;
coord) run_coord "$@" ;;
upgrade) run_upgrade "$@" ;; upgrade) run_upgrade "$@" ;;
release-upgrade) run_release_upgrade "$@" ;; release-upgrade) run_release_upgrade "$@" ;;
project-upgrade) run_project_upgrade "$@" ;; project-upgrade) run_project_upgrade "$@" ;;

View File

@@ -90,10 +90,10 @@ bash scripts/agent/critical.sh
bash scripts/agent/session-end.sh bash scripts/agent/session-end.sh
``` ```
## Shared Rails ## Shared Tools
- Quality and orchestration guides: `~/.config/mosaic/guides/` - Quality and orchestration guides: `~/.config/mosaic/guides/`
- Shared automation rails: `~/.config/mosaic/rails/` - Shared automation tools: `~/.config/mosaic/tools/`
## Repo-Specific Notes ## Repo-Specific Notes
@@ -108,7 +108,7 @@ fi
echo "[mosaic] Repo bootstrap complete: $TARGET_DIR" echo "[mosaic] Repo bootstrap complete: $TARGET_DIR"
echo "[mosaic] Next: edit $TARGET_DIR/.mosaic/repo-hooks.sh with project workflows" 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 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" 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/^enabled:.*/enabled: true/" "$TARGET_DIR/.mosaic/quality-rails.yml"
sed -i "s/^template:.*/template: \"$QUALITY_TEMPLATE\"/" "$TARGET_DIR/.mosaic/quality-rails.yml" sed -i "s/^template:.*/template: \"$QUALITY_TEMPLATE\"/" "$TARGET_DIR/.mosaic/quality-rails.yml"
fi fi
echo "[mosaic] Applied quality rails template: $QUALITY_TEMPLATE" echo "[mosaic] Applied quality tools template: $QUALITY_TEMPLATE"
else 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
fi fi

View File

@@ -149,9 +149,9 @@ expect_file "$MOSAIC_HOME/STANDARDS.md"
expect_file "$MOSAIC_HOME/USER.md" expect_file "$MOSAIC_HOME/USER.md"
expect_file "$MOSAIC_HOME/TOOLS.md" expect_file "$MOSAIC_HOME/TOOLS.md"
expect_dir "$MOSAIC_HOME/guides" expect_dir "$MOSAIC_HOME/guides"
expect_dir "$MOSAIC_HOME/rails" expect_dir "$MOSAIC_HOME/tools"
expect_dir "$MOSAIC_HOME/rails/quality" expect_dir "$MOSAIC_HOME/tools/quality"
expect_dir "$MOSAIC_HOME/rails/orchestrator-matrix" expect_dir "$MOSAIC_HOME/tools/orchestrator-matrix"
expect_dir "$MOSAIC_HOME/profiles" expect_dir "$MOSAIC_HOME/profiles"
expect_dir "$MOSAIC_HOME/templates/agent" expect_dir "$MOSAIC_HOME/templates/agent"
expect_dir "$MOSAIC_HOME/skills" 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-publish"
expect_file "$MOSAIC_HOME/bin/mosaic-orchestrator-matrix-consume" expect_file "$MOSAIC_HOME/bin/mosaic-orchestrator-matrix-consume"
expect_file "$MOSAIC_HOME/bin/mosaic-orchestrator-matrix-cycle" expect_file "$MOSAIC_HOME/bin/mosaic-orchestrator-matrix-cycle"
expect_file "$MOSAIC_HOME/rails/git/ci-queue-wait.sh" expect_file "$MOSAIC_HOME/tools/git/ci-queue-wait.sh"
expect_file "$MOSAIC_HOME/rails/git/pr-ci-wait.sh" expect_file "$MOSAIC_HOME/tools/git/pr-ci-wait.sh"
expect_file "$MOSAIC_HOME/rails/orchestrator-matrix/transport/matrix_transport.py" expect_file "$MOSAIC_HOME/tools/orchestrator-matrix/transport/matrix_transport.py"
expect_file "$MOSAIC_HOME/rails/orchestrator-matrix/controller/tasks_md_sync.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/mcp/SEQUENTIAL-THINKING.json"
expect_file "$MOSAIC_HOME/runtime/claude/RUNTIME.md" expect_file "$MOSAIC_HOME/runtime/claude/RUNTIME.md"
expect_file "$MOSAIC_HOME/runtime/codex/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 # Canonical Mosaic checks
Expect-File (Join-Path $MosaicHome "STANDARDS.md") Expect-File (Join-Path $MosaicHome "STANDARDS.md")
Expect-Dir (Join-Path $MosaicHome "guides") Expect-Dir (Join-Path $MosaicHome "guides")
Expect-Dir (Join-Path $MosaicHome "rails") Expect-Dir (Join-Path $MosaicHome "tools")
Expect-Dir (Join-Path $MosaicHome "rails\quality") Expect-Dir (Join-Path $MosaicHome "tools\quality")
Expect-Dir (Join-Path $MosaicHome "rails\orchestrator-matrix") Expect-Dir (Join-Path $MosaicHome "tools\orchestrator-matrix")
Expect-Dir (Join-Path $MosaicHome "profiles") Expect-Dir (Join-Path $MosaicHome "profiles")
Expect-Dir (Join-Path $MosaicHome "templates\agent") Expect-Dir (Join-Path $MosaicHome "templates\agent")
Expect-Dir (Join-Path $MosaicHome "skills") 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-publish")
Expect-File (Join-Path $MosaicHome "bin\mosaic-orchestrator-matrix-consume") 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 "bin\mosaic-orchestrator-matrix-cycle")
Expect-File (Join-Path $MosaicHome "rails\git\ci-queue-wait.ps1") Expect-File (Join-Path $MosaicHome "tools\git\ci-queue-wait.ps1")
Expect-File (Join-Path $MosaicHome "rails\git\ci-queue-wait.sh") Expect-File (Join-Path $MosaicHome "tools\git\ci-queue-wait.sh")
Expect-File (Join-Path $MosaicHome "rails\git\pr-ci-wait.sh") Expect-File (Join-Path $MosaicHome "tools\git\pr-ci-wait.sh")
Expect-File (Join-Path $MosaicHome "rails\orchestrator-matrix\transport\matrix_transport.py") Expect-File (Join-Path $MosaicHome "tools\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\orchestrator-matrix\controller\tasks_md_sync.py")
Expect-File (Join-Path $MosaicHome "runtime\mcp\SEQUENTIAL-THINKING.json") 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\claude\RUNTIME.md")
Expect-File (Join-Path $MosaicHome "runtime\codex\RUNTIME.md") Expect-File (Join-Path $MosaicHome "runtime\codex\RUNTIME.md")

119
bin/mosaic-ensure-excalidraw Executable file
View File

@@ -0,0 +1,119 @@
#!/usr/bin/env bash
set -euo pipefail
MOSAIC_HOME="${MOSAIC_HOME:-$HOME/.config/mosaic}"
TOOLS_DIR="$MOSAIC_HOME/tools/excalidraw"
MODE="apply"
SCOPE="user"
err() { echo "[mosaic-excalidraw] ERROR: $*" >&2; }
log() { echo "[mosaic-excalidraw] $*"; }
while [[ $# -gt 0 ]]; do
case "$1" in
--check) MODE="check"; shift ;;
--scope)
if [[ $# -lt 2 ]]; then
err "--scope requires a value: user|local"
exit 2
fi
SCOPE="$2"
shift 2
;;
*)
err "Unknown argument: $1"
exit 2
;;
esac
done
require_binary() {
local name="$1"
if ! command -v "$name" >/dev/null 2>&1; then
err "Required binary missing: $name"
return 1
fi
}
check_software() {
require_binary node
require_binary npm
}
check_tool_dir() {
[[ -d "$TOOLS_DIR" ]] || { err "Tool dir not found: $TOOLS_DIR"; return 1; }
[[ -f "$TOOLS_DIR/package.json" ]] || { err "package.json not found in $TOOLS_DIR"; return 1; }
[[ -f "$TOOLS_DIR/launch.sh" ]] || { err "launch.sh not found in $TOOLS_DIR"; return 1; }
}
check_npm_deps() {
[[ -d "$TOOLS_DIR/node_modules/@modelcontextprotocol" ]] || return 1
[[ -d "$TOOLS_DIR/node_modules/@excalidraw" ]] || return 1
[[ -d "$TOOLS_DIR/node_modules/jsdom" ]] || return 1
}
install_npm_deps() {
if check_npm_deps; then
return 0
fi
log "Installing npm deps in $TOOLS_DIR..."
(cd "$TOOLS_DIR" && npm install --silent) || {
err "npm install failed in $TOOLS_DIR"
return 1
}
}
check_claude_config() {
python3 - <<'PY'
import json
from pathlib import Path
p = Path.home() / ".claude.json"
if not p.exists():
raise SystemExit(1)
try:
data = json.loads(p.read_text(encoding="utf-8"))
except Exception:
raise SystemExit(1)
mcp = data.get("mcpServers")
if not isinstance(mcp, dict):
raise SystemExit(1)
entry = mcp.get("excalidraw")
if not isinstance(entry, dict):
raise SystemExit(1)
cmd = entry.get("command", "")
if not cmd.endswith("launch.sh"):
raise SystemExit(1)
PY
}
apply_claude_config() {
require_binary claude
local launch_sh="$TOOLS_DIR/launch.sh"
claude mcp add --scope user excalidraw -- "$launch_sh"
}
# ── Check mode ────────────────────────────────────────────────────────────────
if [[ "$MODE" == "check" ]]; then
check_software
check_tool_dir
if ! check_npm_deps; then
err "npm deps not installed in $TOOLS_DIR (run without --check to install)"
exit 1
fi
if ! check_claude_config; then
err "excalidraw not registered in ~/.claude.json"
exit 1
fi
log "excalidraw MCP is configured and available"
exit 0
fi
# ── Apply mode ────────────────────────────────────────────────────────────────
check_software
check_tool_dir
install_npm_deps
apply_claude_config
log "excalidraw MCP configured (scope: $SCOPE)"

View File

@@ -1,10 +1,10 @@
# mosaic-ensure-sequential-thinking.ps1 # mosaic-ensure-sequential-thinking.ps1
$ErrorActionPreference = "Stop"
param( param(
[switch]$Check [switch]$Check
) )
$ErrorActionPreference = "Stop"
$Pkg = "@modelcontextprotocol/server-sequential-thinking" $Pkg = "@modelcontextprotocol/server-sequential-thinking"
function Require-Binary { function Require-Binary {
@@ -43,7 +43,7 @@ function Set-CodexConfig {
$content = Get-Content $path -Raw $content = Get-Content $path -Raw
$content = [regex]::Replace($content, "(?ms)^\[mcp_servers\.(sequential-thinking|sequential_thinking)\].*?(?=^\[|\z)", "") $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 Set-Content -Path $path -Value $content -Encoding UTF8
} }

View File

@@ -2,7 +2,7 @@
set -euo pipefail set -euo pipefail
MOSAIC_HOME="${MOSAIC_HOME:-$HOME/.config/mosaic}" 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 if [[ ! -f "$BRIDGE" ]]; then
echo "[mosaic-orch-matrix] missing transport bridge: $BRIDGE" >&2 echo "[mosaic-orch-matrix] missing transport bridge: $BRIDGE" >&2

View File

@@ -2,7 +2,7 @@
set -euo pipefail set -euo pipefail
MOSAIC_HOME="${MOSAIC_HOME:-$HOME/.config/mosaic}" 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 if [[ ! -f "$BRIDGE" ]]; then
echo "[mosaic-orch-matrix] missing transport bridge: $BRIDGE" >&2 echo "[mosaic-orch-matrix] missing transport bridge: $BRIDGE" >&2

View File

@@ -2,7 +2,7 @@
set -euo pipefail set -euo pipefail
MOSAIC_HOME="${MOSAIC_HOME:-$HOME/.config/mosaic}" 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 if [[ ! -f "$CTRL" ]]; then
echo "[mosaic-orchestrator] missing controller: $CTRL" >&2 echo "[mosaic-orchestrator] missing controller: $CTRL" >&2

View File

@@ -2,7 +2,7 @@
set -euo pipefail set -euo pipefail
MOSAIC_HOME="${MOSAIC_HOME:-$HOME/.config/mosaic}" 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 if [[ ! -f "$SYNC" ]]; then
echo "[mosaic-orchestrator-sync] missing sync script: $SYNC" >&2 echo "[mosaic-orchestrator-sync] missing sync script: $SYNC" >&2

View File

@@ -9,7 +9,7 @@ usage() {
cat <<USAGE cat <<USAGE
Usage: $(basename "$0") --template <name> [--target <dir>] Usage: $(basename "$0") --template <name> [--target <dir>]
Apply Mosaic quality rails templates into a project. Apply Mosaic quality tools templates into a project.
Templates: Templates:
typescript-node typescript-node
@@ -55,7 +55,7 @@ if [[ ! -d "$TARGET_DIR" ]]; then
exit 1 exit 1
fi fi
SCRIPT="$MOSAIC_HOME/rails/quality/scripts/install.sh" SCRIPT="$MOSAIC_HOME/tools/quality/scripts/install.sh"
if [[ ! -x "$SCRIPT" ]]; then if [[ ! -x "$SCRIPT" ]]; then
echo "[mosaic-quality] Missing install script: $SCRIPT" >&2 echo "[mosaic-quality] Missing install script: $SCRIPT" >&2
exit 1 exit 1

View File

@@ -39,7 +39,7 @@ if [[ ! -d "$TARGET_DIR" ]]; then
exit 1 exit 1
fi fi
SCRIPT="$MOSAIC_HOME/rails/quality/scripts/verify.sh" SCRIPT="$MOSAIC_HOME/tools/quality/scripts/verify.sh"
if [[ ! -x "$SCRIPT" ]]; then if [[ ! -x "$SCRIPT" ]]; then
echo "[mosaic-quality] Missing verify script: $SCRIPT" >&2 echo "[mosaic-quality] Missing verify script: $SCRIPT" >&2
exit 1 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 { function Get-RuntimePrompt {
param( param(
[ValidateSet("claude", "codex", "opencode")] [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 $agentsContent = Get-Content (Join-Path $MosaicHome "AGENTS.md") -Raw
$runtimeContent = Get-Content $runtimeFile -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" return "$launcherContract`n$agentsContent`n`n# Runtime-Specific Contract`n`n$runtimeContent"
} }
@@ -170,7 +258,7 @@ function Invoke-Yolo {
} }
$runtime = $YoloArgs[0] $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) { switch ($runtime) {
"claude" { "claude" {
@@ -191,8 +279,15 @@ function Invoke-Yolo {
Assert-Runtime "codex" Assert-Runtime "codex"
Assert-SequentialThinking Assert-SequentialThinking
Ensure-RuntimeConfig -Runtime "codex" -Dst (Join-Path $env:USERPROFILE ".codex\instructions.md") Ensure-RuntimeConfig -Runtime "codex" -Dst (Join-Path $env:USERPROFILE ".codex\instructions.md")
Write-Host "[mosaic] Launching Codex in YOLO mode (dangerous permissions enabled)..." $missionPrompt = Get-MissionPrompt
& codex --dangerously-bypass-approvals-and-sandbox @tail 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 return
} }
"opencode" { "opencode" {
@@ -219,7 +314,7 @@ if ($args.Count -eq 0) {
} }
$command = $args[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) { switch ($command) {
"claude" { "claude" {
@@ -252,8 +347,15 @@ switch ($command) {
Assert-SequentialThinking Assert-SequentialThinking
# Codex reads from ~/.codex/instructions.md # Codex reads from ~/.codex/instructions.md
Ensure-RuntimeConfig -Runtime "codex" -Dst (Join-Path $env:USERPROFILE ".codex\instructions.md") Ensure-RuntimeConfig -Runtime "codex" -Dst (Join-Path $env:USERPROFILE ".codex\instructions.md")
Write-Host "[mosaic] Launching Codex..." $missionPrompt = Get-MissionPrompt
& codex @remaining 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" { "yolo" {
Invoke-Yolo -YoloArgs $remaining Invoke-Yolo -YoloArgs $remaining

View File

@@ -1,7 +1,7 @@
# Authentication & Authorization Guide # Authentication & Authorization Guide
## Before Starting ## 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 2. Review existing auth implementation in codebase
3. Review Vault secrets structure: `docs/vault-secrets-structure.md` 3. Review Vault secrets structure: `docs/vault-secrets-structure.md`
@@ -115,6 +115,41 @@ class TestAuthentication:
pass 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 ## Common Vulnerabilities to Avoid
1. **Broken Authentication** 1. **Broken Authentication**

View File

@@ -1,7 +1,7 @@
# Backend Development Guide # Backend Development Guide
## Before Starting ## 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` 2. Create scratchpad: `docs/scratchpads/{issue-number}-{short-name}.md`
3. Review API contracts and database schema 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 ```bash
# Automated bootstrap (recommended) # Automated bootstrap (recommended)
~/.config/mosaic/rails/bootstrap/init-project.sh \ ~/.config/mosaic/tools/bootstrap/init-project.sh \
--name "my-project" \ --name "my-project" \
--type "nestjs-nextjs" \ --type "nestjs-nextjs" \
--repo "https://git.mosaicstack.dev/owner/repo" --repo "https://git.mosaicstack.dev/owner/repo"
@@ -240,10 +240,10 @@ Documentation root hygiene (HARD RULE):
```bash ```bash
# Use the init script # 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 # 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 ### 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. Reserve `0.1.0` for the MVP release milestone.
```bash ```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: # 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 ```bash
# Copy Codex review pipeline # Copy Codex review pipeline
mkdir -p .woodpecker/schemas mkdir -p .woodpecker/schemas
cp ~/.config/mosaic/rails/codex/woodpecker/codex-review.yml .woodpecker/ cp ~/.config/mosaic/tools/codex/woodpecker/codex-review.yml .woodpecker/
cp ~/.config/mosaic/rails/codex/schemas/*.json .woodpecker/schemas/ cp ~/.config/mosaic/tools/codex/schemas/*.json .woodpecker/schemas/
# Add codex_api_key secret to Woodpecker CI dashboard # Add codex_api_key secret to Woodpecker CI dashboard
``` ```
@@ -366,7 +366,7 @@ fi
# (execute the command block under "Quality Gates") # (execute the command block under "Quality Gates")
# Test Codex review (if configured) # 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 # Verify sequential-thinking MCP remains configured
~/.config/mosaic/bin/mosaic-ensure-sequential-thinking --check ~/.config/mosaic/bin/mosaic-ensure-sequential-thinking --check
@@ -434,7 +434,7 @@ fi
Full project bootstrap with interactive and flag-based modes: Full project bootstrap with interactive and flag-based modes:
```bash ```bash
~/.config/mosaic/rails/bootstrap/init-project.sh \ ~/.config/mosaic/tools/bootstrap/init-project.sh \
--name "My Project" \ --name "My Project" \
--type "nestjs-nextjs" \ --type "nestjs-nextjs" \
--repo "https://git.mosaicstack.dev/owner/repo" \ --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: Initialize standard labels and the first pre-MVP milestone:
```bash ```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) - [ ] `.env.example` exists (if project uses env vars)
- [ ] CI/CD pipeline configured (if using Woodpecker/GitHub Actions) - [ ] CI/CD pipeline configured (if using Woodpecker/GitHub Actions)
- [ ] Python publish path configured in CI (if project ships Python packages) - [ ] 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. 1. Merge PR to `main` (squash) via Mosaic wrapper.
2. Monitor CI to terminal status: 2. Monitor CI to terminal status:
```bash ```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. 3. Require green status before claiming completion.
4. If CI fails, create remediation task(s) and continue until green. 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: Before pushing a branch or merging a PR, guard against overlapping project pipelines:
```bash ```bash
~/.config/mosaic/rails/git/ci-queue-wait.sh --purpose push -B main ~/.config/mosaic/tools/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 merge -B main
``` ```
Behavior: Behavior:

View File

@@ -12,7 +12,7 @@ Merge strategy enforcement (HARD RULE):
- PR target for delivery is `main`. - PR target for delivery is `main`.
- Direct pushes to `main` are prohibited. - Direct pushes to `main` are prohibited.
- Merge to `main` MUST be squash-only. - 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 ## Review Checklist
@@ -101,7 +101,7 @@ Use `~/.config/mosaic/templates/docs/DOCUMENTATION-CHECKLIST.md` whenever code/A
### Getting Context ### Getting Context
```bash ```bash
# List the issue being addressed # 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 # View the changes
git diff main...HEAD 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. 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`. 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`. 3. Detect provider first via `~/.config/mosaic/tools/git/detect-platform.sh`.
4. For issue/PR/milestone operations, use Mosaic wrappers first (`~/.config/mosaic/rails/git/*.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. 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`). 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`). 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 ## 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. 1. Define scope, constraints, and acceptance criteria.
2. Identify affected surfaces (API, DB, UI, infra, auth, CI/CD, docs). 2. Identify affected surfaces (API, DB, UI, infra, auth, CI/CD, docs).
3. Identify required guides and load them before implementation. 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. For code/API/auth/infra changes, load `~/.config/mosaic/guides/DOCUMENTATION.md`. 4. Identify required guides and load them before implementation.
5. Determine budget constraints: 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 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. - 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. 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. 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. 9. If projected usage exceeds budget, auto-reduce scope/parallelism first; escalate only if cap still cannot be met.
## 2a. Steered Autonomy (Lights-Out) ## 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. 5. `remediate` - fix all findings and any test failures.
6. `review` - re-review remediated changes until blockers are cleared. 6. `review` - re-review remediated changes until blockers are cleared.
7. `commit` - commit only when the logical unit passes tests and review. 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. 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. 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`. 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/rails/git/pr-ci-wait.sh` for PR-based workflow). 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). 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). 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. 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) ### Post-PR Hard Gate (Execute Sequentially, No Exceptions)
1. `~/.config/mosaic/rails/git/ci-queue-wait.sh --purpose merge -B main` 1. `~/.config/mosaic/tools/git/ci-queue-wait.sh --purpose merge -B main`
2. `~/.config/mosaic/rails/git/pr-merge.sh -n <PR_NUMBER> -m squash` 2. `~/.config/mosaic/tools/git/pr-merge.sh -n <PR_NUMBER> -m squash`
3. `~/.config/mosaic/rails/git/pr-ci-wait.sh -n <PR_NUMBER>` 3. `~/.config/mosaic/tools/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) 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. 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. 6. Do not ask the human to perform routine merge/close operations.
7. Do not claim completion before step 4 succeeds. 7. Do not claim completion before step 4 succeeds.
### Forbidden Anti-Patterns ### Forbidden Anti-Patterns
**PR/Merge:**
1. Do NOT stop at "PR created" or "PR updated". 1. Do NOT stop at "PR created" or "PR updated".
2. Do NOT ask "should I merge?" for routine delivery PRs. 2. Do NOT ask "should I merge?" for routine delivery PRs.
3. Do NOT ask "should I close the issue?" after merge + green CI. 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 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. 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 # Frontend Development Guide
## Before Starting ## 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` 2. Create scratchpad: `docs/scratchpads/{issue-number}-{short-name}.md`
3. Review existing components and patterns in the codebase 3. Review existing components and patterns in the codebase

View File

@@ -1,7 +1,7 @@
# Infrastructure & DevOps Guide # Infrastructure & DevOps Guide
## Before Starting ## 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` 2. Create scratchpad: `docs/scratchpads/{issue-number}-{short-name}.md`
3. Review existing infrastructure configuration 3. Review existing infrastructure configuration
@@ -97,10 +97,10 @@ readinessProbe:
periodSeconds: 3 periodSeconds: 3
``` ```
## CI/CD Pipelines ## CI/CD Pipelines
### Pipeline Stages ### Pipeline Stages
1. **Lint**: Code style and static analysis 1. **Lint**: Code style and static analysis
2. **Test**: Unit and integration tests 2. **Test**: Unit and integration tests
3. **Build**: Compile and package 3. **Build**: Compile and package
4. **Scan**: Security and vulnerability scanning 4. **Scan**: Security and vulnerability scanning
@@ -109,65 +109,165 @@ readinessProbe:
### Pipeline Security ### Pipeline Security
- Use secrets management (not hardcoded) - Use secrets management (not hardcoded)
- Pin action/image versions - Pin action/image versions
- Implement approval gates for production - Implement approval gates for production
- Audit pipeline access - Audit pipeline access
## Steered-Autonomous Deployment (Hard Rule) ## Steered-Autonomous Deployment (Hard Rule)
In lights-out mode, the agent owns deployment end-to-end when deployment is in scope. 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. The human is escalation-only for missing access, hard policy conflicts, or irreversible risk.
### Deployment Target Selection ### Deployment Target Selection
1. Use explicit target from `docs/PRD.md` / `docs/PRD.json` or `docs/DEPLOYMENT.md`. 1. Use explicit target from `docs/PRD.md` / `docs/PRD.json` or `docs/DEPLOYMENT.md`.
2. If unspecified, infer from existing project config/integration. 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. 3. If multiple targets exist, choose the target already wired in CI/CD and document rationale.
### Supported Targets ### Supported Targets
- **Portainer**: Deploy via configured stack webhook/API, then verify service health and container status. - **Portainer**: Deploy via `~/.config/mosaic/tools/portainer/stack-redeploy.sh`, then verify with `stack-status.sh`.
- **Coolify**: Trigger deployment via Coolify API/webhook, then verify deployment status and endpoint health. - **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. - **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. - **Other SaaS providers**: Use provider CLI/API/runbook with the same validation and rollback gates.
### Image Tagging and Promotion (Hard Rule) ### Coolify API Operations
For containerized deployments: ```bash
# List projects and services
1. Build immutable image tags: `sha-<shortsha>` and `v{base-version}-rc.{build}`. ~/.config/mosaic/tools/coolify/project-list.sh
2. Use mutable environment tags only as pointers: `testing`, optional `staging`, and `prod`. ~/.config/mosaic/tools/coolify/service-list.sh
3. Deploy by immutable digest, not by mutable tag alone.
4. Promote the exact tested digest between environments (no rebuild between testing and prod). # Check service status
5. Do not use `latest` or `dev` as deployment references. ~/.config/mosaic/tools/coolify/service-status.sh -u <uuid>
Blue-green is the default strategy for production promotion. # Set env vars (takes effect on next deploy)
Canary is allowed only when automated SLO/error-rate gates and auto-rollback triggers are implemented. ~/.config/mosaic/tools/coolify/env-set.sh -u <uuid> -k KEY -v VALUE
### Post-Deploy Validation (REQUIRED) # Deploy
~/.config/mosaic/tools/coolify/deploy.sh -u <uuid>
1. Health endpoints return expected status. ```
2. Critical smoke tests pass in target environment.
3. Running version and digest match the promoted release candidate. **Known Coolify Limitations:**
4. Observability signals (errors/latency) are within expected thresholds. - FQDN updates on compose sub-apps not supported via API (DB workaround required)
- Compose files must be base64-encoded in `docker_compose_raw` field
### Rollback Rule - Magic variables (`SERVICE_FQDN_*`) require list-style env syntax, not dict-style
- Rate limit: 200 requests per interval
If post-deploy validation fails:
### Cloudflare DNS Operations
1. Execute rollback/redeploy-safe path immediately.
2. Mark deployment as blocked in `docs/TASKS.md`. Use the Cloudflare tools for any DNS configuration: pointing domains at services, adding TXT verification records, managing MX records, etc.
3. Record failure evidence and next remediation step in scratchpad and release notes.
**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.
### Registry Retention and Cleanup
```bash
Cleanup MUST be automated. # List all zones (domains) in the account
~/.config/mosaic/tools/cloudflare/zone-list.sh [-a instance]
- Keep all final release tags (`vX.Y.Z`) indefinitely.
- Keep active environment digests (`prod`, `testing`, and active blue/green slots). # List DNS records for a zone (accepts zone name or ID)
- Keep recent RC tags (`vX.Y.Z-rc.N`) based on retention window. ~/.config/mosaic/tools/cloudflare/record-list.sh -z <zone> [-t type] [-n name]
- Remove stale `sha-*` and RC tags outside retention window if they are not actively deployed.
# Create a DNS record
## Monitoring & Logging ~/.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 ### Logging Standards
- Use structured logging (JSON) - Use structured logging (JSON)

View File

@@ -1,27 +1,50 @@
# Memory and Retention Rules # Memory and Retention Rules
## Primary Memory Layer: OpenBrain
**OpenBrain is the canonical shared memory for all Mosaic agents across all harnesses and sessions.**
Use the `capture` MCP tool (or REST `POST /v1/thoughts`) to store:
- Discovered gotchas and workarounds
- Architectural decisions and rationale
- Project state and context for handoffs
- Anything a future agent should know
Use `search` or `recent` at session start to load prior context before acting.
This is not optional. An agent that uses local file-based memory instead of OpenBrain is a broken agent — its knowledge is invisible to every other agent on the platform.
## Hard Rules ## Hard Rules
1. You MUST store learned operational memory in `~/.config/mosaic/memory`. 1. Agent learnings MUST go to OpenBrain — not to any file-based memory location.
2. You MUST NOT store durable memory in runtime-native memory silos. 2. You MUST NOT write to runtime-native memory silos (they are write-blocked by hook).
3. You MUST write concise, reusable learnings that help future agents. 3. Active execution state belongs in project `docs/` — not in memory files.
4. You MUST track active execution state in project documentation, not ad-hoc text files. 4. `~/.config/mosaic/memory/` is for mosaic framework technical notes only, not project knowledge.
## Runtime-Native Memory Silos (FORBIDDEN for Durable Memory) ## Runtime-Native Memory Silos (WRITE-BLOCKED)
| Runtime | Native silo (forbidden for durable memory) | Required durable location | These locations are blocked by PreToolUse hooks. Attempting to write there fails at the tool level.
|---|---|---|
| Claude Code | `~/.claude/projects/*/memory/` | `~/.config/mosaic/memory/` + project `docs/` |
| Codex | Runtime session/native memory under `~/.codex/` | `~/.config/mosaic/memory/` + project `docs/` |
| OpenCode | Runtime session/native memory under `~/.config/opencode/` | `~/.config/mosaic/memory/` + project `docs/` |
Treat runtime-native memory as volatile implementation detail. Do not rely on it for long-term project continuity. | Runtime | Blocked silo | Use instead |
|---------|-------------|-------------|
| Claude Code | `~/.claude/projects/*/memory/*.md` | OpenBrain `capture` |
| Codex | Runtime session memory | OpenBrain `capture` |
| OpenCode | Runtime session memory | OpenBrain `capture` |
MEMORY.md files may only contain behavioral guardrails that must be injected at load-path — not knowledge.
## Project Continuity Files (MANDATORY) ## Project Continuity Files (MANDATORY)
| File | Purpose | Location | | File | Purpose | Location |
|---|---|---| |---|---|---|
| `docs/PRD.md` or `docs/PRD.json` | Source of requirements for planning, coding, testing, and review | Project `docs/` | | `docs/PRD.md` or `docs/PRD.json` | Source of requirements | Project `docs/` |
| `docs/TASKS.md` | Canonical tracking for tasks, milestones, issues, status, and blockers | Project `docs/` | | `docs/TASKS.md` | Task tracking, milestones, issues, status | Project `docs/` |
| `docs/scratchpads/<task>.md` | Task-specific working memory and verification evidence | Project `docs/scratchpads/` | | `docs/scratchpads/<task>.md` | Task-specific working memory | Project `docs/scratchpads/` |
| `AGENTS.md` | Reusable local patterns and gotchas | Any working directory | | `AGENTS.md` | Project-local patterns and conventions | Project root |
## How the Block Works
`~/.config/mosaic/tools/qa/prevent-memory-write.sh` is registered as a `PreToolUse` hook in
`~/.claude/settings.json`. It intercepts Write/Edit/MultiEdit calls and rejects any targeting
`~/.claude/projects/*/memory/*.md` before the tool executes. Exit code 2 blocks the call and
the agent sees a message directing it to OpenBrain instead.

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: 1. Gitea (preferred when available) via Mosaic helper:
```bash ```bash
~/.config/mosaic/rails/git/issue-create.sh \ ~/.config/mosaic/tools/git/issue-create.sh \
-t "Phase 1: Critical Security Fixes" \ -t "Phase 1: Critical Security Fixes" \
-b "$(cat <<'EOF' -b "$(cat <<'EOF'
## Findings ## Findings
@@ -412,15 +412,15 @@ git push
and checklist completed (`~/.config/mosaic/templates/docs/DOCUMENTATION-CHECKLIST.md`) when applicable. and checklist completed (`~/.config/mosaic/templates/docs/DOCUMENTATION-CHECKLIST.md`) when applicable.
13. **PR + CI + Issue Closure Gate** (HARD RULE for source-code tasks): 13. **PR + CI + Issue Closure Gate** (HARD RULE for source-code tasks):
- Before merging, run queue guard: - 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): - 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: - 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: - 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: - 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. - 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 stop at "PR created" or "PR merged pending CI".
- Do NOT claim completion before CI is green and issue/internal ref is closed. - 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} cd {project_path}
# Code quality review # 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 # 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 ### 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`. 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. 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"` 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}` 11. Push: `git push origin {branch}`
12. Report result as JSON (see format below) 12. Report result as JSON (see format below)
## Git Scripts ## Git Scripts
For issue/PR/milestone operations, use scripts (NOT raw tea/gh): For issue/PR/milestone operations, use scripts (NOT raw tea/gh):
- `~/.config/mosaic/rails/git/issue-view.sh -i {N}` - `~/.config/mosaic/tools/git/issue-view.sh -i {N}`
- `~/.config/mosaic/rails/git/pr-create.sh -t "Title" -b "Desc" -B main` - `~/.config/mosaic/tools/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/tools/git/ci-queue-wait.sh --purpose push|merge -B main`
- `~/.config/mosaic/rails/git/pr-merge.sh -n {PR_NUMBER} -m squash` - `~/.config/mosaic/tools/git/pr-merge.sh -n {PR_NUMBER} -m squash`
- `~/.config/mosaic/rails/git/pr-ci-wait.sh -n {PR_NUMBER}` - `~/.config/mosaic/tools/git/pr-ci-wait.sh -n {PR_NUMBER}`
- `~/.config/mosaic/rails/git/issue-close.sh -i {N}` - `~/.config/mosaic/tools/git/issue-close.sh -i {N}`
Standard git commands (pull, commit, push, checkout) are fine. 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**: 5. **Close milestone in provider**:
- Gitea/GitHub: - Gitea/GitHub:
```bash ```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). - GitLab: close milestone via provider workflow (CLI or web UI).
If provider tooling is unavailable, record milestone closure status in `docs/TASKS.md` notes. If provider tooling is unavailable, record milestone closure status in `docs/TASKS.md` notes.

View File

@@ -2,7 +2,7 @@
## Before Starting ## 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` 2. Create scratchpad: `docs/scratchpads/{issue-number}-{short-name}.md`
3. Review `docs/PRD.md` or `docs/PRD.json` as the requirements source. 3. Review `docs/PRD.md` or `docs/PRD.json` as the requirements source.
4. Review acceptance criteria and affected change surfaces. 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"/bin/*
chmod +x "$TARGET_DIR"/install.sh 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" ok "Framework installed to $TARGET_DIR"
step "Post-install tasks" step "Post-install tasks"
@@ -166,6 +177,12 @@ else
fi fi
fi fi
if "$TARGET_DIR/bin/mosaic-ensure-excalidraw" >/dev/null 2>&1; then
ok "excalidraw MCP configured"
else
warn "excalidraw MCP setup failed (non-fatal) — run 'mosaic-ensure-excalidraw' to retry"
fi
if [[ "${MOSAIC_SKIP_SKILLS_SYNC:-0}" == "1" ]]; then if [[ "${MOSAIC_SKIP_SKILLS_SYNC:-0}" == "1" ]]; then
ok "Skills sync skipped (MOSAIC_SKIP_SKILLS_SYNC=1)" ok "Skills sync skipped (MOSAIC_SKIP_SKILLS_SYNC=1)"
else else

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,15 +11,92 @@ This file applies only to Claude runtime behavior.
3. Treat sequential-thinking MCP as required. 3. Treat sequential-thinking MCP as required.
4. If runtime config conflicts with global rules, global rules win. 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`. 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. 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...` 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. 9. Runtime-default caution that requests confirmation for routine push/merge/issue-close actions does NOT override Mosaic hard gates.
## Memory Override ## Subagent Model Selection (Claude Code Syntax)
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. Claude Code's Task tool accepts a `model` parameter: `"haiku"`, `"sonnet"`, or `"opus"`.
## MCP Requirement 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.
Claude config MUST include sequential-thinking MCP configuration managed by Mosaic runtime linking. **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 Policy (Hard Gate)
**OpenBrain is the primary cross-agent memory layer.** All agent learnings, gotchas, decisions, and project state MUST be captured to OpenBrain via the `capture` MCP tool or REST API.
`~/.claude/projects/*/memory/MEMORY.md` files are **write-blocked by PreToolUse hook** (`prevent-memory-write.sh`). Any attempt to write agent learnings there will be rejected with an error directing you to OpenBrain.
### What belongs where
| Content | Location |
|---------|----------|
| Discoveries, gotchas, decisions, observations | OpenBrain `capture` — searchable by all agents |
| Active task state | `docs/TASKS.md` or `docs/scratchpads/` |
| Behavioral guardrails that MUST be in load-path | `MEMORY.md` (read-mostly; write only for genuine behavioral overrides) |
| Mosaic framework technical notes | `~/.config/mosaic/memory/` |
### Using OpenBrain
At session start, load prior context:
```
search("topic or project name") # semantic search
recent(limit=5) # what's been happening
```
When you discover something:
```
capture("The thing you learned", source="project/context", metadata={"type": "gotcha", ...})
```
### Why the hook exists
Instructions in RUNTIME.md, CLAUDE.md, and MEMORY.md are insufficient — agents default to writing local MEMORY.md regardless of written rules. The PreToolUse hook is a hard technical gate that makes the correct behavior the only possible behavior.
## MCP Configuration
**MCPs are configured in `~/.claude.json` — NOT `~/.claude/settings.json`.**
`settings.json` controls hooks, model, plugins, and allowed commands.
`~/.claude.json` is the global Claude Code state file where `mcpServers` lives.
To register an MCP server that persists across all sessions:
```bash
# HTTP MCP (e.g. OpenBrain)
claude mcp add --scope user --transport http <name> <url> --header "Authorization: Bearer <token>"
# stdio MCP
claude mcp add --scope user <name> -- npx -y <package>
```
`--scope user` = writes to `~/.claude.json` (global, all projects).
`--scope project` = writes to `.claude/settings.json` in project root.
`--scope local` = default, local-only (not committed).
Do NOT add `mcpServers` to `~/.claude/settings.json` — that key is ignored for MCP loading.

View File

@@ -1,13 +1,25 @@
{ {
"model": "opus", "model": "opus",
"hooks": { "hooks": {
"PreToolUse": [
{
"matcher": "Write|Edit|MultiEdit",
"hooks": [
{
"type": "command",
"command": "~/.config/mosaic/tools/qa/prevent-memory-write.sh",
"timeout": 10
}
]
}
],
"PostToolUse": [ "PostToolUse": [
{ {
"matcher": "Edit|MultiEdit|Write", "matcher": "Edit|MultiEdit|Write",
"hooks": [ "hooks": [
{ {
"type": "command", "type": "command",
"command": "~/.config/mosaic/rails/qa/qa-hook-stdin.sh", "command": "~/.config/mosaic/tools/qa/qa-hook-stdin.sh",
"timeout": 60 "timeout": 60
} }
] ]
@@ -224,14 +236,5 @@
"cpan", "cpan",
"nohup" "nohup"
], ],
"enableAllMcpTools": true, "enableAllMcpTools": true
"mcpServers": {
"sequential-thinking": {
"command": "npx",
"args": [
"-y",
"@modelcontextprotocol/server-sequential-thinking"
]
}
}
} }

View File

@@ -11,11 +11,26 @@ This file applies only to Codex runtime behavior.
3. Treat sequential-thinking MCP as required. 3. Treat sequential-thinking MCP as required.
4. If runtime config conflicts with global rules, global rules win. 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`. 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. 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...` 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. 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 ## 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. 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

@@ -0,0 +1,7 @@
{
"name": "excalidraw",
"launch": "${MOSAIC_TOOLS}/excalidraw/launch.sh",
"enabled": true,
"required": false,
"description": "Headless .excalidraw → SVG export and diagram generation via @excalidraw/excalidraw"
}

View File

@@ -11,7 +11,7 @@ This file applies only to OpenCode runtime behavior.
3. Treat sequential-thinking MCP as required. 3. Treat sequential-thinking MCP as required.
4. If runtime config conflicts with global rules, global rules win. 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`. 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. 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...` 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. 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 ## 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. - Do not edit generated project views directly when the repo defines canonical data sources.
- Pull/rebase before edits in shared repositories. - Pull/rebase before edits in shared repositories.
- Run project verification commands before claiming completion. - 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: If the project's `.woodpecker.yml` doesn't already have a `kaniko_setup` anchor in its `variables:` section, add it:
```bash ```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: 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: Use the generator script with the user's answers:
```bash ```bash
~/.config/mosaic/rails/cicd/generate-docker-steps.sh \ ~/.config/mosaic/tools/cicd/generate-docker-steps.sh \
--registry REGISTRY \ --registry REGISTRY \
--org ORG \ --org ORG \
--repo REPO \ --repo REPO \

View File

@@ -1,3 +1,7 @@
import { spawnSync } from 'node:child_process';
import { existsSync } from 'node:fs';
import { join } from 'node:path';
import { homedir } from 'node:os';
import type { WizardPrompter } from '../prompter/interface.js'; import type { WizardPrompter } from '../prompter/interface.js';
import type { WizardState, RuntimeName } from '../types.js'; import type { WizardState, RuntimeName } from '../types.js';
import { detectRuntime, type RuntimeInfo } from '../runtime/detector.js'; import { detectRuntime, type RuntimeInfo } from '../runtime/detector.js';
@@ -66,5 +70,20 @@ export async function runtimeSetupStage(
`MCP setup failed: ${err instanceof Error ? err.message : String(err)}. Run 'mosaic seq fix' later.`, `MCP setup failed: ${err instanceof Error ? err.message : String(err)}. Run 'mosaic seq fix' later.`,
); );
} }
// Configure excalidraw MCP (non-fatal — optional tool)
const mosaicHome = process.env['MOSAIC_HOME'] ?? join(homedir(), '.config', 'mosaic');
const ensureExcalidraw = join(mosaicHome, 'bin', 'mosaic-ensure-excalidraw');
if (existsSync(ensureExcalidraw)) {
const spin3 = p.spinner();
spin3.update('Configuring excalidraw MCP...');
const res = spawnSync(ensureExcalidraw, [], { encoding: 'utf8' });
if (res.status === 0) {
spin3.stop('excalidraw MCP configured');
} else {
spin3.stop('excalidraw MCP setup failed (non-fatal)');
p.warn("Run 'mosaic-ensure-excalidraw' manually if needed.");
}
}
} }
} }

View File

@@ -5,10 +5,10 @@ Run independent reviews:
```bash ```bash
# Code quality review (Codex) # 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) # 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. **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 ensure_repo_root
load_repo_hooks 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 if declare -F mosaic_hook_session_end >/dev/null 2>&1; then
run_step "Run repo end hook" mosaic_hook_session_end run_step "Run repo end hook" mosaic_hook_session_end
else else

View File

@@ -16,6 +16,75 @@ if git rev-parse --is-inside-work-tree >/dev/null 2>&1 && has_remote; then
fi fi
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 if declare -F mosaic_hook_session_start >/dev/null 2>&1; then
run_step "Run repo start hook" mosaic_hook_session_start run_step "Run repo start hook" mosaic_hook_session_start
else else

284
tools/_lib/credentials.sh Executable file
View File

@@ -0,0 +1,284 @@
#!/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,
# turbo-cache, openbrain
#
# 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)
turbo-cache → TURBO_API, TURBO_TOKEN, TURBO_TEAM
openbrain → OPENBRAIN_URL, OPENBRAIN_TOKEN
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}"
;;
turbo-cache)
export TURBO_API="${TURBO_API:-$(_mosaic_read_cred '.turbo_cache.api_url')}"
export TURBO_TOKEN="${TURBO_TOKEN:-$(_mosaic_read_cred '.turbo_cache.token')}"
export TURBO_TEAM="${TURBO_TEAM:-$(_mosaic_read_cred '.turbo_cache.team')}"
[[ -n "$TURBO_API" ]] || { echo "Error: turbo_cache.api_url not found" >&2; return 1; }
[[ -n "$TURBO_TOKEN" ]] || { echo "Error: turbo_cache.token not found" >&2; return 1; }
[[ -n "$TURBO_TEAM" ]] || { echo "Error: turbo_cache.team not found" >&2; return 1; }
;;
openbrain)
export OPENBRAIN_URL="${OPENBRAIN_URL:-$(_mosaic_read_cred '.openbrain.url')}"
export OPENBRAIN_TOKEN="${OPENBRAIN_TOKEN:-$(_mosaic_read_cred '.openbrain.api_key')}"
OPENBRAIN_URL="${OPENBRAIN_URL%/}"
[[ -n "$OPENBRAIN_URL" ]] || { echo "Error: openbrain.url not found" >&2; return 1; }
[[ -n "$OPENBRAIN_TOKEN" ]] || { echo "Error: openbrain.api_key not found" >&2; return 1; }
;;
*)
echo "Error: Unknown service '$service'" >&2
echo "Supported: portainer, coolify, authentik[-<name>], glpi, github, gitea-mosaicstack, gitea-usc, woodpecker[-<name>], cloudflare[-<name>], turbo-cache, openbrain" >&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 $FIX_HINT && ! $JSON_OUTPUT; then
if [[ "$has_runtime" == "MISS" || "$has_agents" == "MISS" ]]; 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 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
fi fi

View File

@@ -9,7 +9,7 @@ set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
TEMPLATE_DIR="$HOME/.config/mosaic/templates/agent" 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" SEQUENTIAL_MCP_SCRIPT="$HOME/.config/mosaic/bin/mosaic-ensure-sequential-thinking"
# Defaults # Defaults
@@ -403,7 +403,7 @@ echo "Created docs/scratchpads/, docs/reports/*, docs/tasks/, docs/releases/, do
# Set up CI/CD pipeline # Set up CI/CD pipeline
if [[ "$SKIP_CI" != true ]]; then 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 if [[ -d "$CODEX_DIR/woodpecker" ]]; then
mkdir -p .woodpecker/schemas mkdir -p .woodpecker/schemas
cp "$CODEX_DIR/woodpecker/codex-review.yml" .woodpecker/ cp "$CODEX_DIR/woodpecker/codex-review.yml" .woodpecker/
@@ -416,7 +416,7 @@ fi
# Generate Docker build/push/link pipeline steps # Generate Docker build/push/link pipeline steps
if [[ "$CICD_DOCKER" == true ]]; then 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 if [[ -x "$CICD_SCRIPT" ]]; then
# Parse org and repo from git remote # Parse org and repo from git remote
CICD_REGISTRY="" 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 # 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_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_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 fi
if [[ -n "$CICD_REGISTRY" && -n "$CICD_ORG" && -n "$CICD_REPO_NAME" && ${#CICD_SERVICES[@]} -gt 0 ]]; then if [[ -n "$CICD_REGISTRY" && -n "$CICD_ORG" && -n "$CICD_REPO_NAME" && ${#CICD_SERVICES[@]} -gt 0 ]]; then

View File

@@ -7,7 +7,7 @@
set -e set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" 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" source "$GIT_SCRIPT_DIR/detect-platform.sh"
SKIP_MILESTONE=false 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 ```bash
# Code review # Code review
~/.config/mosaic/rails/codex/codex-code-review.sh --uncommitted ~/.config/mosaic/tools/codex/codex-code-review.sh --uncommitted
# Security review # Security review
~/.config/mosaic/rails/codex/codex-security-review.sh --uncommitted ~/.config/mosaic/tools/codex/codex-security-review.sh --uncommitted
``` ```
### Review a Pull Request ### Review a Pull Request
```bash ```bash
# Review and post findings as a PR comment # 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 # 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 ### Review Against Base Branch
```bash ```bash
# Code review changes vs main # 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 # 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 ### Review a Specific Commit
```bash ```bash
~/.config/mosaic/rails/codex/codex-code-review.sh -c abc123f ~/.config/mosaic/tools/codex/codex-code-review.sh -c abc123f
~/.config/mosaic/rails/codex/codex-security-review.sh -c abc123f ~/.config/mosaic/tools/codex/codex-security-review.sh -c abc123f
``` ```
### Save Results to File ### Save Results to File
```bash ```bash
# Save JSON output # Save JSON output
~/.config/mosaic/rails/codex/codex-code-review.sh --uncommitted -o review-results.json ~/.config/mosaic/tools/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-security-review.sh --uncommitted -o security-results.json
``` ```
## Options ## Options
@@ -113,12 +113,12 @@ Automated PR reviews in CI pipelines.
1. **Copy the pipeline template to your repo:** 1. **Copy the pipeline template to your repo:**
```bash ```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:** 2. **Copy the schemas directory:**
```bash ```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:** 3. **Add Codex API key to Woodpecker:**
@@ -203,7 +203,7 @@ Automated PR reviews in CI pipelines.
## Platform Support ## 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 - Auto-detects platform from git remote
- Posts PR comments using `gh` (GitHub) or `tea` (Gitea) - Posts PR comments using `gh` (GitHub) or `tea` (Gitea)
- Unified interface across both platforms - 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 ## See Also
- `~/.config/mosaic/guides/CODE-REVIEW.md` — Manual code review checklist - `~/.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/ - 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

1
tools/excalidraw/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
node_modules/

5
tools/excalidraw/launch.sh Executable file
View File

@@ -0,0 +1,5 @@
#!/usr/bin/env bash
# Launcher for Excalidraw MCP stdio server.
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
exec node --loader "$SCRIPT_DIR/loader.mjs" "$SCRIPT_DIR/server.mjs"

View File

@@ -0,0 +1,76 @@
/**
* Custom ESM loader to fix missing .js extensions in @excalidraw/excalidraw deps.
*
* Problems patched:
* 1. excalidraw imports 'roughjs/bin/rough' (and other roughjs/* paths) without .js
* 2. roughjs/* files import sibling modules as './canvas' (relative, no .js)
* 3. JSON files need { type: 'json' } import attribute in Node.js v22+
*
* Usage: node --loader ./loader.mjs server.mjs [args...]
*/
import { fileURLToPath, pathToFileURL } from 'url';
import { dirname, resolve as pathResolve } from 'path';
const __dirname = dirname(fileURLToPath(import.meta.url));
// Modules that have incompatible ESM format — redirect to local stubs
const STUBS = {
'@excalidraw/laser-pointer': pathToFileURL(pathResolve(__dirname, 'stubs/laser-pointer.mjs')).href,
};
export async function resolve(specifier, context, nextResolve) {
// 0. Module stubs (incompatible ESM format packages)
if (STUBS[specifier]) {
return { url: STUBS[specifier], shortCircuit: true };
}
// 1. Bare roughjs/* specifiers without .js extension
if (/^roughjs\/bin\/[a-z-]+$/.test(specifier)) {
return nextResolve(`${specifier}.js`, context);
}
// 2. Relative imports without extension (e.g. './canvas' from roughjs/bin/rough.js)
// These come in as relative paths that resolve to extensionless file URLs.
if (specifier.startsWith('./') || specifier.startsWith('../')) {
// Try resolving first; if it fails with a missing-extension error, add .js
try {
return await nextResolve(specifier, context);
} catch (err) {
if (err.code === 'ERR_MODULE_NOT_FOUND') {
// Try appending .js
try {
return await nextResolve(`${specifier}.js`, context);
} catch {
// Fall through to original error
}
}
throw err;
}
}
// 3. JSON imports need type: 'json' attribute
if (specifier.endsWith('.json')) {
const resolved = await nextResolve(specifier, context);
if (!resolved.importAttributes?.type) {
return {
...resolved,
importAttributes: { ...resolved.importAttributes, type: 'json' },
};
}
return resolved;
}
return nextResolve(specifier, context);
}
export async function load(url, context, nextLoad) {
// Ensure JSON files are loaded with json format
if (url.endsWith('.json')) {
return nextLoad(url, {
...context,
importAttributes: { ...context.importAttributes, type: 'json' },
});
}
return nextLoad(url, context);
}

View File

@@ -0,0 +1,11 @@
{
"name": "excalidraw-mcp",
"version": "1.0.0",
"type": "module",
"private": true,
"dependencies": {
"@modelcontextprotocol/sdk": "^1.12.0",
"@excalidraw/excalidraw": "^0.18.0",
"jsdom": "^25.0.1"
}
}

323
tools/excalidraw/server.mjs Normal file
View File

@@ -0,0 +1,323 @@
#!/usr/bin/env node
/**
* Excalidraw MCP stdio server
* Provides headless .excalidraw → SVG export via @excalidraw/excalidraw.
* Optional: diagram generation via EXCALIDRAW_GEN_PATH (excalidraw_gen.py).
*/
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod/v3";
import { readFileSync, writeFileSync, existsSync } from 'fs';
import { resolve } from 'path';
import { spawnSync } from 'child_process';
import { JSDOM } from 'jsdom';
// ---------------------------------------------------------------------------
// 1. DOM environment — must be established BEFORE importing excalidraw
// ---------------------------------------------------------------------------
const dom = new JSDOM('<!DOCTYPE html><html><body></body></html>', {
url: 'http://localhost/',
pretendToBeVisual: true,
});
const { window } = dom;
// Helper: define a global, overriding read-only getters (e.g. navigator in Node v22)
function defineGlobal(key, value) {
if (value === undefined) return;
try {
Object.defineProperty(global, key, {
value,
writable: true,
configurable: true,
});
} catch {
// Already defined and non-configurable — skip
}
}
// Core DOM globals
defineGlobal('window', window);
defineGlobal('document', window.document);
defineGlobal('navigator', window.navigator);
defineGlobal('location', window.location);
defineGlobal('history', window.history);
defineGlobal('screen', window.screen);
// Element / event interfaces
for (const key of [
'Node', 'Element', 'HTMLElement', 'SVGElement', 'SVGSVGElement',
'HTMLCanvasElement', 'HTMLImageElement', 'Image',
'Event', 'CustomEvent', 'MouseEvent', 'PointerEvent',
'KeyboardEvent', 'TouchEvent', 'WheelEvent', 'InputEvent',
'MutationObserver', 'ResizeObserver', 'IntersectionObserver',
'XMLHttpRequest', 'XMLSerializer',
'DOMParser', 'Range',
'getComputedStyle', 'matchMedia',
]) {
defineGlobal(key, window[key]);
}
// Animation frame stubs (jsdom doesn't implement them)
global.requestAnimationFrame = (fn) => setTimeout(() => fn(Date.now()), 0);
global.cancelAnimationFrame = (id) => clearTimeout(id);
// CSS Font Loading API stub — jsdom doesn't implement FontFace
class FontFaceStub {
constructor(family, source, _descriptors) {
this.family = family;
this.source = source;
this.status = 'loaded';
this.loaded = Promise.resolve(this);
}
load() { return Promise.resolve(this); }
}
defineGlobal('FontFace', FontFaceStub);
// FontFaceSet stub for document.fonts
const fontFaceSet = {
add: () => {},
delete: () => {},
has: () => false,
clear: () => {},
load: () => Promise.resolve([]),
check: () => true,
ready: Promise.resolve(),
status: 'loaded',
forEach: () => {},
[Symbol.iterator]: function*() {},
};
Object.defineProperty(window.document, 'fonts', {
value: fontFaceSet,
writable: true,
configurable: true,
});
// Canvas stub — excalidraw's exportToSvg doesn't need real canvas rendering,
// but the class must exist for isinstance checks.
if (!global.HTMLCanvasElement) {
defineGlobal('HTMLCanvasElement', window.HTMLCanvasElement ?? class HTMLCanvasElement {});
}
// Device pixel ratio
global.devicePixelRatio = 1;
// ---------------------------------------------------------------------------
// 1b. Stub canvas getContext — excalidraw calls this at module init time.
// jsdom throws "Not implemented" by default; we return a no-op 2D stub.
// ---------------------------------------------------------------------------
const _canvasCtx = {
canvas: { width: 800, height: 600 },
fillRect: () => {}, clearRect: () => {}, strokeRect: () => {},
getImageData: (x, y, w, h) => ({ data: new Uint8ClampedArray(w * h * 4), width: w, height: h }),
putImageData: () => {}, createImageData: () => ({ data: new Uint8ClampedArray(0) }),
setTransform: () => {}, resetTransform: () => {}, transform: () => {},
drawImage: () => {}, save: () => {}, restore: () => {},
scale: () => {}, rotate: () => {}, translate: () => {},
beginPath: () => {}, closePath: () => {}, moveTo: () => {}, lineTo: () => {},
bezierCurveTo: () => {}, quadraticCurveTo: () => {},
arc: () => {}, arcTo: () => {}, ellipse: () => {}, rect: () => {},
fill: () => {}, stroke: () => {}, clip: () => {},
fillText: () => {}, strokeText: () => {},
measureText: (t) => ({ width: t.length * 8, actualBoundingBoxAscent: 12, actualBoundingBoxDescent: 3, fontBoundingBoxAscent: 14, fontBoundingBoxDescent: 4 }),
createLinearGradient: () => ({ addColorStop: () => {} }),
createRadialGradient: () => ({ addColorStop: () => {} }),
createPattern: () => null,
setLineDash: () => {}, getLineDash: () => [],
isPointInPath: () => false, isPointInStroke: () => false,
getContextAttributes: () => ({ alpha: true, desynchronized: false }),
font: '10px sans-serif', fillStyle: '#000', strokeStyle: '#000',
lineWidth: 1, lineCap: 'butt', lineJoin: 'miter',
textAlign: 'start', textBaseline: 'alphabetic',
globalAlpha: 1, globalCompositeOperation: 'source-over',
shadowOffsetX: 0, shadowOffsetY: 0, shadowBlur: 0, shadowColor: 'transparent',
miterLimit: 10, lineDashOffset: 0, filter: 'none', imageSmoothingEnabled: true,
};
// Patch before excalidraw import so module-level canvas calls get the stub
if (window.HTMLCanvasElement) {
window.HTMLCanvasElement.prototype.getContext = function (type) {
if (type === '2d') return _canvasCtx;
return null;
};
}
// ---------------------------------------------------------------------------
// 2. Load excalidraw (dynamic import so globals are set first)
// ---------------------------------------------------------------------------
let exportToSvg;
try {
const excalidraw = await import('@excalidraw/excalidraw');
exportToSvg = excalidraw.exportToSvg;
if (!exportToSvg) throw new Error('exportToSvg not found in package exports');
} catch (err) {
process.stderr.write(`FATAL: Failed to load @excalidraw/excalidraw: ${err.message}\n`);
process.exit(1);
}
// ---------------------------------------------------------------------------
// 3. SVG export helper
// ---------------------------------------------------------------------------
async function renderToSvg(elements, appState, files) {
const svgEl = await exportToSvg({
elements: elements ?? [],
appState: {
exportWithDarkMode: false,
exportBackground: true,
viewBackgroundColor: '#ffffff',
...appState,
},
files: files ?? {},
});
const serializer = new window.XMLSerializer();
return serializer.serializeToString(svgEl);
}
// ---------------------------------------------------------------------------
// 4. Gen subprocess helper (optional — requires EXCALIDRAW_GEN_PATH)
// ---------------------------------------------------------------------------
function requireGenPath() {
const p = process.env.EXCALIDRAW_GEN_PATH;
if (!p) {
return null;
}
return p;
}
function spawnGen(args) {
const genPath = requireGenPath();
if (!genPath) {
return {
ok: false,
text: 'EXCALIDRAW_GEN_PATH is not set. Set it to the path of excalidraw_gen.py to use diagram generation.',
};
}
const result = spawnSync('python3', [genPath, ...args], { encoding: 'utf8' });
if (result.error) return { ok: false, text: `spawn error: ${result.error.message}` };
if (result.status !== 0) return { ok: false, text: result.stderr || 'subprocess failed' };
return { ok: true, text: result.stdout.trim() };
}
// ---------------------------------------------------------------------------
// 5. MCP Server
// ---------------------------------------------------------------------------
const server = new McpServer({
name: "excalidraw",
version: "1.0.0",
});
// --- Tool: excalidraw_to_svg ---
server.tool(
"excalidraw_to_svg",
"Convert Excalidraw elements JSON to SVG string",
{
elements: z.string().describe("JSON string of Excalidraw elements array"),
app_state: z.string().optional().describe("JSON string of appState overrides"),
},
async ({ elements, app_state }) => {
let parsed;
try {
parsed = JSON.parse(elements);
} catch (err) {
throw new Error(`Invalid elements JSON: ${err.message}`);
}
const appState = app_state ? JSON.parse(app_state) : {};
const svg = await renderToSvg(parsed, appState, {});
return { content: [{ type: "text", text: svg }] };
}
);
// --- Tool: excalidraw_file_to_svg ---
server.tool(
"excalidraw_file_to_svg",
"Convert an .excalidraw file to SVG (writes .svg alongside the input file)",
{
file_path: z.string().describe("Absolute or relative path to .excalidraw file"),
},
async ({ file_path }) => {
const absPath = resolve(file_path);
if (!existsSync(absPath)) {
throw new Error(`File not found: ${absPath}`);
}
const raw = JSON.parse(readFileSync(absPath, 'utf8'));
const svg = await renderToSvg(raw.elements, raw.appState, raw.files);
const outPath = absPath.replace(/\.excalidraw$/, '.svg');
writeFileSync(outPath, svg, 'utf8');
return {
content: [{ type: "text", text: `SVG written to: ${outPath}\n\n${svg}` }],
};
}
);
// --- Tool: list_diagrams ---
server.tool(
"list_diagrams",
"List available diagram templates from the DIAGRAMS registry (requires EXCALIDRAW_GEN_PATH)",
{},
async () => {
const res = spawnGen(['--list']);
return { content: [{ type: "text", text: res.text }] };
}
);
// --- Tool: generate_diagram ---
server.tool(
"generate_diagram",
"Generate an .excalidraw file from a named diagram template (requires EXCALIDRAW_GEN_PATH)",
{
name: z.string().describe("Diagram template name (from list_diagrams)"),
output_path: z.string().optional().describe("Output path for the .excalidraw file"),
},
async ({ name, output_path }) => {
const args = [name];
if (output_path) args.push('--output', output_path);
const res = spawnGen(args);
if (!res.ok) throw new Error(res.text);
return { content: [{ type: "text", text: res.text }] };
}
);
// --- Tool: generate_and_export ---
server.tool(
"generate_and_export",
"Generate an .excalidraw file and immediately export it to SVG (requires EXCALIDRAW_GEN_PATH)",
{
name: z.string().describe("Diagram template name (from list_diagrams)"),
output_path: z.string().optional().describe("Output path for the .excalidraw file (SVG written alongside)"),
},
async ({ name, output_path }) => {
const genArgs = [name];
if (output_path) genArgs.push('--output', output_path);
const genRes = spawnGen(genArgs);
if (!genRes.ok) throw new Error(genRes.text);
const excalidrawPath = genRes.text;
if (!existsSync(excalidrawPath)) {
throw new Error(`Generated file not found: ${excalidrawPath}`);
}
const raw = JSON.parse(readFileSync(excalidrawPath, 'utf8'));
const svg = await renderToSvg(raw.elements, raw.appState, raw.files);
const svgPath = excalidrawPath.replace(/\.excalidraw$/, '.svg');
writeFileSync(svgPath, svg, 'utf8');
return {
content: [{ type: "text", text: `Generated: ${excalidrawPath}\nExported SVG: ${svgPath}` }],
};
}
);
// --- Start ---
const transport = new StdioServerTransport();
await server.connect(transport);

View File

@@ -0,0 +1,7 @@
/**
* Stub for @excalidraw/laser-pointer
* The real package uses a Parcel bundle format that Node.js ESM can't consume.
* For headless SVG export, the laser pointer feature is not needed.
*/
export class LaserPointer {}
export default { LaserPointer };

View File

@@ -31,41 +31,7 @@ Examples:
EOF EOF
} }
get_remote_host() { # get_remote_host and get_gitea_token are provided by detect-platform.sh
local remote_url
remote_url=$(git remote get-url origin 2>/dev/null || true)
if [[ -z "$remote_url" ]]; then
return 1
fi
if [[ "$remote_url" =~ ^https?://([^/]+)/ ]]; then
echo "${BASH_REMATCH[1]}"
return 0
fi
if [[ "$remote_url" =~ ^git@([^:]+): ]]; then
echo "${BASH_REMATCH[1]}"
return 0
fi
return 1
}
get_gitea_token() {
local host="$1"
if [[ -n "${GITEA_TOKEN:-}" ]]; then
echo "$GITEA_TOKEN"
return 0
fi
local creds="$HOME/.git-credentials"
if [[ -f "$creds" ]]; then
local token
token=$(grep -F "$host" "$creds" 2>/dev/null | sed -n 's#https\?://[^@]*:\([^@/]*\)@.*#\1#p' | head -n 1)
if [[ -n "$token" ]]; then
echo "$token"
return 0
fi
fi
return 1
}
get_state_from_status_json() { get_state_from_status_json() {
python3 - <<'PY' 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

Some files were not shown because too many files have changed in this diff Show More