Compare commits
31 Commits
feature/to
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8c960eee9d | ||
| f380d232e6 | |||
| 8b441c17b7 | |||
| 2a91f6c202 | |||
| 97ee66770a | |||
| 30ce4cecc7 | |||
|
|
9fbfdcee6d | ||
|
|
21afb58b33 | ||
| 09786ee6e0 | |||
| 1fd67b9ec0 | |||
| 38223c8ec2 | |||
|
|
8de2f7439a | ||
|
|
98b9bc3c93 | ||
| b1403703b1 | |||
|
|
abead17e0e | ||
|
|
fbf74c2736 | ||
|
|
364d6c2278 | ||
|
|
93efbcdafe | ||
|
|
def9c2fd7a | ||
|
|
87501ea952 | ||
|
|
7a5f28c8b5 | ||
|
|
405bc4c797 | ||
|
|
c9bf578396 | ||
| c1f4830bf5 | |||
| e5c4bf25b3 | |||
| a9623e9219 | |||
| 5d666bdca9 | |||
| 221afe94d9 | |||
| 612796d8e0 | |||
| 5ba531e2d0 | |||
| a8e580e1a3 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1 +1,2 @@
|
|||||||
node_modules/
|
node_modules/
|
||||||
|
rails
|
||||||
|
|||||||
33
AGENTS.md
33
AGENTS.md
@@ -30,10 +30,13 @@ If any required file is missing, you MUST stop and report the missing file.
|
|||||||
3. Routine repository operations are NOT escalation triggers. Use escalation triggers only from this contract.
|
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.
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
48
README.md
48
README.md
@@ -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:
|
||||||
|
|||||||
12
STANDARDS.md
12
STANDARDS.md
@@ -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
212
TOOLS.md
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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).
|
||||||
|
|||||||
397
bin/mosaic
397
bin/mosaic
@@ -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 "$@" ;;
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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
119
bin/mosaic-ensure-excalidraw
Executable 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)"
|
||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
114
bin/mosaic.ps1
114
bin/mosaic.ps1
@@ -96,6 +96,88 @@ function Assert-SequentialThinking {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function Get-ActiveMission {
|
||||||
|
$missionFile = Join-Path (Get-Location) ".mosaic\orchestrator\mission.json"
|
||||||
|
if (-not (Test-Path $missionFile)) {
|
||||||
|
return $null
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$mission = Get-Content $missionFile -Raw | ConvertFrom-Json
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
return $null
|
||||||
|
}
|
||||||
|
|
||||||
|
$status = [string]$mission.status
|
||||||
|
if ([string]::IsNullOrWhiteSpace($status)) {
|
||||||
|
$status = "inactive"
|
||||||
|
}
|
||||||
|
if ($status -ne "active" -and $status -ne "paused") {
|
||||||
|
return $null
|
||||||
|
}
|
||||||
|
|
||||||
|
$name = [string]$mission.name
|
||||||
|
if ([string]::IsNullOrWhiteSpace($name)) {
|
||||||
|
$name = "unnamed"
|
||||||
|
}
|
||||||
|
|
||||||
|
$id = [string]$mission.mission_id
|
||||||
|
if ([string]::IsNullOrWhiteSpace($id)) {
|
||||||
|
$id = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
$milestones = @($mission.milestones)
|
||||||
|
$milestoneCount = $milestones.Count
|
||||||
|
$milestoneCompleted = @($milestones | Where-Object { $_.status -eq "completed" }).Count
|
||||||
|
|
||||||
|
return [PSCustomObject]@{
|
||||||
|
Name = $name
|
||||||
|
Id = $id
|
||||||
|
Status = $status
|
||||||
|
MilestoneCount = $milestoneCount
|
||||||
|
MilestoneCompleted = $milestoneCompleted
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-MissionContractBlock {
|
||||||
|
$mission = Get-ActiveMission
|
||||||
|
if ($null -eq $mission) {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
return @"
|
||||||
|
# ACTIVE MISSION — HARD GATE (Read Before Anything Else)
|
||||||
|
|
||||||
|
An active orchestration mission exists in this project. This is a BLOCKING requirement.
|
||||||
|
|
||||||
|
**Mission:** $($mission.Name)
|
||||||
|
**ID:** $($mission.Id)
|
||||||
|
**Status:** $($mission.Status)
|
||||||
|
**Milestones:** $($mission.MilestoneCompleted) / $($mission.MilestoneCount) completed
|
||||||
|
|
||||||
|
## MANDATORY — Before ANY Response to the User
|
||||||
|
|
||||||
|
You MUST complete these steps before responding to any user message, including simple greetings:
|
||||||
|
|
||||||
|
1. Read `~/.config/mosaic/guides/ORCHESTRATOR-PROTOCOL.md` (mission lifecycle protocol)
|
||||||
|
2. Read `docs/MISSION-MANIFEST.md` for full mission scope, milestones, and success criteria
|
||||||
|
3. Read the latest scratchpad in `docs/scratchpads/` for session history, decisions, and corrections
|
||||||
|
4. Read `docs/TASKS.md` for current task state (what is done, what is next)
|
||||||
|
5. After reading all four, acknowledge the mission state to the user before proceeding
|
||||||
|
|
||||||
|
If the user gives a task, execute it within the mission context. If no task is given, present mission status and ask how to proceed.
|
||||||
|
"@
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-MissionPrompt {
|
||||||
|
$mission = Get-ActiveMission
|
||||||
|
if ($null -eq $mission) {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return "Active mission detected: $($mission.Name). Read the mission state files and report status."
|
||||||
|
}
|
||||||
|
|
||||||
function Get-RuntimePrompt {
|
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
|
||||||
|
|||||||
@@ -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**
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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/`)
|
||||||
|
|||||||
@@ -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:
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|
||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
268
guides/ORCHESTRATOR-PROTOCOL.md
Normal file
268
guides/ORCHESTRATOR-PROTOCOL.md
Normal file
@@ -0,0 +1,268 @@
|
|||||||
|
# Orchestrator Protocol — Mission Lifecycle Guide
|
||||||
|
|
||||||
|
> **Operational guide for agent sessions.** Distilled from the full specification at
|
||||||
|
> `jarvis-brain/docs/protocols/ORCHESTRATOR-PROTOCOL.md` (1,066 lines).
|
||||||
|
>
|
||||||
|
> Load this guide when: active mission detected, multi-milestone orchestration, mission continuation.
|
||||||
|
> Load `ORCHESTRATOR.md` for per-session execution protocol (planning, coding, review, commit cycle).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Relationship to ORCHESTRATOR.md
|
||||||
|
|
||||||
|
| Concern | Guide |
|
||||||
|
|---------|-------|
|
||||||
|
| How to execute within a session (plan, code, test, review, commit) | `ORCHESTRATOR.md` |
|
||||||
|
| How to manage a mission across sessions (resume, continue, handoff) | **This guide** |
|
||||||
|
| Both guides are active simultaneously during orchestration missions. |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Mission Manifest
|
||||||
|
|
||||||
|
**Location:** `docs/MISSION-MANIFEST.md`
|
||||||
|
**Owner:** Orchestrator (sole writer)
|
||||||
|
**Template:** `~/.config/mosaic/templates/docs/MISSION-MANIFEST.md.template`
|
||||||
|
|
||||||
|
The manifest is the persistent document tracking full mission scope, status, milestones, and session history. It survives session death.
|
||||||
|
|
||||||
|
### Update Rules
|
||||||
|
|
||||||
|
- Update **Phase** when transitioning (Intake → Planning → Execution → Continuation → Completion)
|
||||||
|
- Update **Current Milestone** when starting a new milestone
|
||||||
|
- Update **Progress** after each milestone completion
|
||||||
|
- Append to **Session History** at session start and end
|
||||||
|
- Update **Status** to `completed` only when ALL success criteria are verified
|
||||||
|
|
||||||
|
### Hard Rule
|
||||||
|
|
||||||
|
The manifest is the source of truth for mission scope. If the manifest says a milestone is done, it is done. If it says remaining, it remains.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Scratchpad Protocol
|
||||||
|
|
||||||
|
**Location:** `docs/scratchpads/{mission-id}.md`
|
||||||
|
**Template:** `~/.config/mosaic/templates/docs/mission-scratchpad.md.template`
|
||||||
|
|
||||||
|
### Rules
|
||||||
|
|
||||||
|
1. **First action** — Before ANY planning or coding, write the mission prompt to the scratchpad
|
||||||
|
2. **Append-only** — NEVER delete or overwrite previous entries
|
||||||
|
3. **Session log** — Record session start, tasks done, and outcome at session end
|
||||||
|
4. **Decisions** — Record all planning decisions with rationale
|
||||||
|
5. **Corrections** — Record course corrections from human or coordinator
|
||||||
|
6. **Never deleted** — Scratchpads survive mission completion (archival reference)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. TASKS.md as Control Plane
|
||||||
|
|
||||||
|
**Location:** `docs/TASKS.md`
|
||||||
|
**Owner:** Orchestrator (sole writer). Workers read but NEVER modify.
|
||||||
|
|
||||||
|
### Table Schema
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
| id | status | milestone | description | pr | notes |
|
||||||
|
```
|
||||||
|
|
||||||
|
### Status Values
|
||||||
|
|
||||||
|
`not-started` → `in-progress` → `done` (or `blocked` / `failed`)
|
||||||
|
|
||||||
|
### Planning Tasks Are First-Class
|
||||||
|
|
||||||
|
Include explicit planning tasks (e.g., `PLAN-001: Break down milestone into tasks`). These count toward progress.
|
||||||
|
|
||||||
|
### Post-Merge Tasks Are Explicit
|
||||||
|
|
||||||
|
Include verification tasks after merge: CI check, deployment verification, Playwright test. Don't assume they happen automatically.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Session Resume Protocol
|
||||||
|
|
||||||
|
When starting a session and an active mission is detected, follow this checklist:
|
||||||
|
|
||||||
|
### Detection (5-point check)
|
||||||
|
|
||||||
|
1. `docs/MISSION-MANIFEST.md` exists → read Phase, Current Milestone, Progress
|
||||||
|
2. `docs/scratchpads/*.md` exists → read latest scratchpad for decisions and corrections
|
||||||
|
3. `docs/TASKS.md` exists → read task state (what's done, what's next)
|
||||||
|
4. Git state → current branch, open PRs, recent commits
|
||||||
|
5. Provider state → open issues, milestone status (if accessible)
|
||||||
|
|
||||||
|
### Resume Procedure
|
||||||
|
|
||||||
|
1. Read the mission manifest FIRST
|
||||||
|
2. Read the scratchpad for session history and corrections
|
||||||
|
3. Read TASKS.md for current task state
|
||||||
|
4. Identify the next `not-started` or `in-progress` task
|
||||||
|
5. Continue execution from that task
|
||||||
|
6. Update Session History in the manifest
|
||||||
|
|
||||||
|
### Dirty State Recovery
|
||||||
|
|
||||||
|
| State | Recovery |
|
||||||
|
|-------|----------|
|
||||||
|
| Dirty git working tree | Stash changes, log stash ref in scratchpad, resume clean |
|
||||||
|
| Open PR in bad state | Check PR status, close if broken, re-create if needed |
|
||||||
|
| Half-created issues | Audit issues against TASKS.md, reconcile |
|
||||||
|
| Tasks marked in-progress | Check if work was committed; if so, mark done; if not, restart task |
|
||||||
|
|
||||||
|
### Hard Rule
|
||||||
|
|
||||||
|
Session state is NEVER automatically deleted. The coordinator (human or automated) must explicitly request cleanup.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Mission Continuation
|
||||||
|
|
||||||
|
When a milestone completes and more milestones remain:
|
||||||
|
|
||||||
|
### Agent Handoff (at ~55-60% context)
|
||||||
|
|
||||||
|
If context usage is high, produce a handoff message:
|
||||||
|
|
||||||
|
1. Update TASKS.md with final task statuses
|
||||||
|
2. Update mission manifest with session results
|
||||||
|
3. Append session summary to scratchpad
|
||||||
|
4. Commit all state files
|
||||||
|
5. The coordinator will generate a continuation prompt for the next session
|
||||||
|
|
||||||
|
### Continuation Prompt and Capsule Format
|
||||||
|
|
||||||
|
The coordinator generates this (via `mosaic coord continue`) and writes a machine-readable capsule at `.mosaic/orchestrator/next-task.json`:
|
||||||
|
|
||||||
|
```
|
||||||
|
## Continuation Mission
|
||||||
|
Continue **{mission}** from existing state.
|
||||||
|
- Read docs/MISSION-MANIFEST.md for scope and status
|
||||||
|
- Read docs/scratchpads/{id}.md for decisions
|
||||||
|
- Read docs/TASKS.md for current state
|
||||||
|
- Continue from task {next-task-id}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Between Sessions (r0 manual)
|
||||||
|
|
||||||
|
1. Agent stops (expected — this is the confirmed stamina limitation)
|
||||||
|
2. Human runs `mosaic coord mission` to check status
|
||||||
|
3. Human runs `mosaic coord continue` to generate continuation prompt
|
||||||
|
4. Human launches new session and pastes the prompt
|
||||||
|
5. New agent reads manifest, scratchpad, TASKS.md and continues
|
||||||
|
|
||||||
|
### Between Sessions (r0 assisted)
|
||||||
|
|
||||||
|
Use `mosaic coord run` to remove copy/paste steps:
|
||||||
|
|
||||||
|
1. Agent stops
|
||||||
|
2. Human runs `mosaic coord run [--claude|--codex]`
|
||||||
|
3. Coordinator regenerates continuation prompt + `next-task.json`
|
||||||
|
4. Coordinator launches selected runtime with scoped kickoff context
|
||||||
|
5. New session resumes from next task
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Failure Taxonomy Quick Reference
|
||||||
|
|
||||||
|
| Code | Type | Recovery |
|
||||||
|
|------|------|----------|
|
||||||
|
| F1 | Premature Stop | Continuation prompt → new session (most common) |
|
||||||
|
| F2 | Context Exhaustion | Handoff message → new session |
|
||||||
|
| F3 | Session Crash | Check git state → `mosaic coord resume` → new session |
|
||||||
|
| F4 | Error Spiral | Kill session, mark task blocked, skip to next |
|
||||||
|
| F5 | Quality Gate Failure | Create QA remediation task |
|
||||||
|
| F6 | Infrastructure Failure | Pause, retry when service recovers |
|
||||||
|
| F7 | False Completion | Append correction to scratchpad, relaunch |
|
||||||
|
| F8 | Scope Drift | Kill session, relaunch with scratchpad ref |
|
||||||
|
| F9 | Subagent Failure | Orchestrator retries or creates remediation |
|
||||||
|
| F10 | Deadlock | Escalate to human |
|
||||||
|
|
||||||
|
### F1: Premature Stop — Detailed Recovery
|
||||||
|
|
||||||
|
This is the confirmed, most common failure. Every session will eventually trigger F1.
|
||||||
|
|
||||||
|
1. Session ends with tasks remaining in TASKS.md
|
||||||
|
2. Run `mosaic coord mission` — verify milestone status
|
||||||
|
3. If milestone complete: verify CI green, deployed, issues closed
|
||||||
|
4. Run `mosaic coord continue` — generates scoped continuation prompt
|
||||||
|
5. Launch new session, paste prompt
|
||||||
|
6. New session reads state and continues from next pending task
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. r0 Manual Coordinator Process
|
||||||
|
|
||||||
|
In r0, the Coordinator is Jason + shell scripts. No daemon. No automation.
|
||||||
|
|
||||||
|
### Commands
|
||||||
|
|
||||||
|
| Command | Purpose |
|
||||||
|
|---------|---------|
|
||||||
|
| `mosaic coord init --name "..." --milestones "..."` | Initialize a new mission |
|
||||||
|
| `mosaic coord mission` | Show mission progress dashboard |
|
||||||
|
| `mosaic coord status` | Check if agent session is still running |
|
||||||
|
| `mosaic coord continue` | Generate continuation prompt for next session |
|
||||||
|
| `mosaic coord run [--claude|--codex]` | Generate continuation context and launch runtime |
|
||||||
|
| `mosaic coord resume` | Crash recovery (detect dirty state, generate fix) |
|
||||||
|
| `mosaic coord resume --clean-lock` | Clear stale session lock after review |
|
||||||
|
|
||||||
|
### Typical Workflow
|
||||||
|
|
||||||
|
```
|
||||||
|
init → launch agent → [agent works] → agent stops →
|
||||||
|
status → mission → run → repeat
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. Operational Checklist
|
||||||
|
|
||||||
|
### Pre-Mission
|
||||||
|
|
||||||
|
- [ ] Mission initialized: `mosaic coord init`
|
||||||
|
- [ ] docs/MISSION-MANIFEST.md exists with scope and milestones
|
||||||
|
- [ ] docs/TASKS.md scaffolded
|
||||||
|
- [ ] docs/scratchpads/{id}.md scaffolded
|
||||||
|
- [ ] Success criteria defined in manifest
|
||||||
|
|
||||||
|
### Session Start
|
||||||
|
|
||||||
|
- [ ] Read manifest → know phase, milestone, progress
|
||||||
|
- [ ] Read scratchpad → know decisions, corrections, history
|
||||||
|
- [ ] Read TASKS.md → know what's done and what's next
|
||||||
|
- [ ] Write session start to scratchpad
|
||||||
|
- [ ] Update Session History in manifest
|
||||||
|
|
||||||
|
### Planning Gate (Hard Gate — No Coding Until Complete)
|
||||||
|
|
||||||
|
- [ ] Milestones created in provider (Gitea/GitHub)
|
||||||
|
- [ ] Issues created for all milestone tasks
|
||||||
|
- [ ] TASKS.md populated with all planned tasks (including planning + verification tasks)
|
||||||
|
- [ ] All planning artifacts committed and pushed
|
||||||
|
|
||||||
|
### Per-Task
|
||||||
|
|
||||||
|
- [ ] Update task status to `in-progress` in TASKS.md
|
||||||
|
- [ ] Execute task following ORCHESTRATOR.md cycle
|
||||||
|
- [ ] Update task status to `done` (or `blocked`/`failed`)
|
||||||
|
- [ ] Commit, push
|
||||||
|
|
||||||
|
### Milestone Completion
|
||||||
|
|
||||||
|
- [ ] All milestone tasks in TASKS.md are `done`
|
||||||
|
- [ ] CI/pipeline green
|
||||||
|
- [ ] PR merged to `main`
|
||||||
|
- [ ] Issues closed
|
||||||
|
- [ ] Update manifest: milestone status → completed
|
||||||
|
- [ ] Update scratchpad: session log entry
|
||||||
|
- [ ] If deployment target: verify accessible
|
||||||
|
|
||||||
|
### Mission Completion
|
||||||
|
|
||||||
|
- [ ] ALL milestones completed
|
||||||
|
- [ ] ALL success criteria verified with evidence
|
||||||
|
- [ ] manifest status → completed
|
||||||
|
- [ ] Final scratchpad entry with completion evidence
|
||||||
|
- [ ] Release tag created and pushed (if applicable)
|
||||||
@@ -272,7 +272,7 @@ Provider options:
|
|||||||
1. Gitea (preferred when available) via Mosaic helper:
|
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.
|
||||||
@@ -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.
|
||||||
|
|||||||
17
install.sh
17
install.sh
@@ -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
|
||||||
|
|||||||
@@ -1,80 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
# detect-platform.sh - Detect git platform (Gitea or GitHub) for current repo
|
|
||||||
# Usage: source detect-platform.sh && detect_platform
|
|
||||||
# or: ./detect-platform.sh (prints platform name)
|
|
||||||
|
|
||||||
detect_platform() {
|
|
||||||
local remote_url
|
|
||||||
remote_url=$(git remote get-url origin 2>/dev/null)
|
|
||||||
|
|
||||||
if [[ -z "$remote_url" ]]; then
|
|
||||||
echo "error: not a git repository or no origin remote" >&2
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Check for GitHub
|
|
||||||
if [[ "$remote_url" == *"github.com"* ]]; then
|
|
||||||
PLATFORM="github"
|
|
||||||
export PLATFORM
|
|
||||||
echo "github"
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Check for common Gitea indicators
|
|
||||||
# Gitea URLs typically don't contain github.com, gitlab.com, bitbucket.org
|
|
||||||
if [[ "$remote_url" != *"gitlab.com"* ]] && \
|
|
||||||
[[ "$remote_url" != *"bitbucket.org"* ]]; then
|
|
||||||
# Assume Gitea for self-hosted repos
|
|
||||||
PLATFORM="gitea"
|
|
||||||
export PLATFORM
|
|
||||||
echo "gitea"
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
PLATFORM="unknown"
|
|
||||||
export PLATFORM
|
|
||||||
echo "unknown"
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
get_repo_info() {
|
|
||||||
local remote_url
|
|
||||||
remote_url=$(git remote get-url origin 2>/dev/null)
|
|
||||||
|
|
||||||
if [[ -z "$remote_url" ]]; then
|
|
||||||
echo "error: not a git repository or no origin remote" >&2
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Extract owner/repo from URL
|
|
||||||
# Handles: git@host:owner/repo.git, https://host/owner/repo.git, https://host/owner/repo
|
|
||||||
local repo_path
|
|
||||||
if [[ "$remote_url" == git@* ]]; then
|
|
||||||
repo_path="${remote_url#*:}"
|
|
||||||
else
|
|
||||||
repo_path="${remote_url#*://}"
|
|
||||||
repo_path="${repo_path#*/}"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Remove .git suffix if present
|
|
||||||
repo_path="${repo_path%.git}"
|
|
||||||
|
|
||||||
echo "$repo_path"
|
|
||||||
}
|
|
||||||
|
|
||||||
get_repo_owner() {
|
|
||||||
local repo_info
|
|
||||||
repo_info=$(get_repo_info)
|
|
||||||
echo "${repo_info%%/*}"
|
|
||||||
}
|
|
||||||
|
|
||||||
get_repo_name() {
|
|
||||||
local repo_info
|
|
||||||
repo_info=$(get_repo_info)
|
|
||||||
echo "${repo_info##*/}"
|
|
||||||
}
|
|
||||||
|
|
||||||
# If script is run directly (not sourced), output the platform
|
|
||||||
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
|
|
||||||
detect_platform
|
|
||||||
fi
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
npx lint-staged
|
|
||||||
npx git-secrets --scan || echo "Warning: git-secrets not installed"
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
npx lint-staged
|
|
||||||
npx git-secrets --scan || echo "Warning: git-secrets not installed"
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
npx lint-staged
|
|
||||||
npx git-secrets --scan || echo "Warning: git-secrets not installed"
|
|
||||||
@@ -11,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.
|
||||||
|
|||||||
@@ -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"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
7
runtime/mcp/EXCALIDRAW.json
Normal file
7
runtime/mcp/EXCALIDRAW.json
Normal 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"
|
||||||
|
}
|
||||||
@@ -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.
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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 \
|
||||||
|
|||||||
@@ -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.");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
53
templates/docs/MISSION-MANIFEST.md.template
Normal file
53
templates/docs/MISSION-MANIFEST.md.template
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
# Mission Manifest — ${MISSION_NAME}
|
||||||
|
|
||||||
|
> Persistent document tracking full mission scope, status, and session history.
|
||||||
|
> Updated by the orchestrator at each phase transition and milestone completion.
|
||||||
|
|
||||||
|
## Mission
|
||||||
|
|
||||||
|
**ID:** ${MISSION_ID}
|
||||||
|
**Statement:** ${MISSION_STATEMENT}
|
||||||
|
**Phase:** Intake
|
||||||
|
**Current Milestone:** —
|
||||||
|
**Progress:** 0 / ${MILESTONE_COUNT} milestones
|
||||||
|
**Status:** not-started
|
||||||
|
**Last Updated:** ${CREATED_AT}
|
||||||
|
|
||||||
|
## Success Criteria
|
||||||
|
|
||||||
|
${SUCCESS_CRITERIA}
|
||||||
|
|
||||||
|
## Milestones
|
||||||
|
|
||||||
|
| # | ID | Name | Status | Branch | Issue | Started | Completed |
|
||||||
|
|---|-----|------|--------|--------|-------|---------|-----------|
|
||||||
|
${MILESTONES_TABLE}
|
||||||
|
|
||||||
|
## Deployment
|
||||||
|
|
||||||
|
| Target | URL | Method |
|
||||||
|
|--------|-----|--------|
|
||||||
|
${DEPLOYMENT_TABLE}
|
||||||
|
|
||||||
|
## Coordination
|
||||||
|
|
||||||
|
- **Primary Agent:** ${PRIMARY_RUNTIME}
|
||||||
|
- **Sibling Agents:** ${SIBLING_AGENTS}
|
||||||
|
- **Shared Contracts:** ${SHARED_CONTRACTS}
|
||||||
|
|
||||||
|
## Token Budget
|
||||||
|
|
||||||
|
| Metric | Value |
|
||||||
|
|--------|-------|
|
||||||
|
| Budget | ${TOKEN_BUDGET} |
|
||||||
|
| Used | 0 |
|
||||||
|
| Mode | normal |
|
||||||
|
|
||||||
|
## Session History
|
||||||
|
|
||||||
|
| Session | Runtime | Started | Duration | Ended Reason | Last Task |
|
||||||
|
|---------|---------|---------|----------|--------------|-----------|
|
||||||
|
|
||||||
|
## Scratchpad
|
||||||
|
|
||||||
|
Path: `docs/scratchpads/${MISSION_ID}.md`
|
||||||
36
templates/docs/continuation-prompt.md.template
Normal file
36
templates/docs/continuation-prompt.md.template
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
## Continuation Mission
|
||||||
|
|
||||||
|
Continue **${MISSION_NAME}** from existing state.
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
- **Project:** ${PROJECT_PATH}
|
||||||
|
- **State:** docs/TASKS.md (already populated — ${TASKS_DONE}/${TASKS_TOTAL} tasks complete)
|
||||||
|
- **Manifest:** docs/MISSION-MANIFEST.md
|
||||||
|
- **Scratchpad:** docs/scratchpads/${MISSION_ID}.md
|
||||||
|
- **Protocol:** ~/.config/mosaic/guides/ORCHESTRATOR.md
|
||||||
|
- **Quality gates:** ${QUALITY_GATES}
|
||||||
|
|
||||||
|
## Resume Point
|
||||||
|
|
||||||
|
- **Current milestone:** ${CURRENT_MILESTONE_NAME} (${CURRENT_MILESTONE_ID})
|
||||||
|
- **Next task:** ${NEXT_TASK_ID}
|
||||||
|
- **Progress:** ${TASKS_DONE}/${TASKS_TOTAL} tasks (${PROGRESS_PCT}%)
|
||||||
|
- **Branch:** ${CURRENT_BRANCH}
|
||||||
|
|
||||||
|
## Previous Session Context
|
||||||
|
|
||||||
|
- **Session:** ${PREV_SESSION_ID} (${PREV_RUNTIME}, ${PREV_DURATION})
|
||||||
|
- **Ended:** ${PREV_ENDED_REASON}
|
||||||
|
- **Last completed task:** ${PREV_LAST_TASK}
|
||||||
|
|
||||||
|
## Instructions
|
||||||
|
|
||||||
|
1. Read `~/.config/mosaic/guides/ORCHESTRATOR.md` for full protocol
|
||||||
|
2. Read `docs/MISSION-MANIFEST.md` for mission scope and status
|
||||||
|
3. Read `docs/scratchpads/${MISSION_ID}.md` for session history and decisions
|
||||||
|
4. Read `docs/TASKS.md` for current task state
|
||||||
|
5. `git pull --rebase` to sync latest changes
|
||||||
|
6. Continue execution from task **${NEXT_TASK_ID}**
|
||||||
|
7. Follow Two-Phase Completion Protocol
|
||||||
|
8. You are the SOLE writer of `docs/TASKS.md`
|
||||||
27
templates/docs/mission-scratchpad.md.template
Normal file
27
templates/docs/mission-scratchpad.md.template
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
# Mission Scratchpad — ${MISSION_NAME}
|
||||||
|
|
||||||
|
> Append-only log. NEVER delete entries. NEVER overwrite sections.
|
||||||
|
> This is the orchestrator's working memory across sessions.
|
||||||
|
|
||||||
|
## Original Mission Prompt
|
||||||
|
|
||||||
|
```
|
||||||
|
${MISSION_PROMPT}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Planning Decisions
|
||||||
|
|
||||||
|
<!-- Record key decisions made during planning. Format: decision + rationale. -->
|
||||||
|
|
||||||
|
## Session Log
|
||||||
|
|
||||||
|
| Session | Date | Milestone | Tasks Done | Outcome |
|
||||||
|
|---------|------|-----------|------------|---------|
|
||||||
|
|
||||||
|
## Open Questions
|
||||||
|
|
||||||
|
<!-- Unresolved items that need human input or cross-session investigation. -->
|
||||||
|
|
||||||
|
## Corrections
|
||||||
|
|
||||||
|
<!-- Record any corrections to earlier decisions or assumptions. -->
|
||||||
14
templates/repo/.mosaic/orchestrator/mission.json
Normal file
14
templates/repo/.mosaic/orchestrator/mission.json
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"schema_version": 1,
|
||||||
|
"mission_id": "",
|
||||||
|
"name": "",
|
||||||
|
"description": "",
|
||||||
|
"project_path": "",
|
||||||
|
"created_at": "",
|
||||||
|
"status": "inactive",
|
||||||
|
"task_prefix": "",
|
||||||
|
"quality_gates": "",
|
||||||
|
"milestone_version": "0.0.1",
|
||||||
|
"milestones": [],
|
||||||
|
"sessions": []
|
||||||
|
}
|
||||||
@@ -8,6 +8,34 @@ source "$SCRIPT_DIR/common.sh"
|
|||||||
ensure_repo_root
|
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
|
||||||
|
|||||||
@@ -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
284
tools/_lib/credentials.sh
Executable 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
59
tools/authentik/README.md
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
# Authentik Tool Suite
|
||||||
|
|
||||||
|
Manage Authentik identity provider (SSO, users, groups, applications, flows) via CLI.
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- `jq` installed
|
||||||
|
- Authentik credentials in `~/src/jarvis-brain/credentials.json` (or `$MOSAIC_CREDENTIALS_FILE`)
|
||||||
|
- Required fields: `authentik.url`, `authentik.username`, `authentik.password`
|
||||||
|
|
||||||
|
## Authentication
|
||||||
|
|
||||||
|
Scripts use `auth-token.sh` to auto-authenticate via username/password and cache the API token at `~/.cache/mosaic/authentik-token`. The token is validated on each use and refreshed automatically when expired.
|
||||||
|
|
||||||
|
For better security, create a long-lived API token in Authentik admin (Directory > Tokens) and set `$AUTHENTIK_TOKEN` in your environment — the scripts will use it directly.
|
||||||
|
|
||||||
|
## Scripts
|
||||||
|
|
||||||
|
| Script | Purpose |
|
||||||
|
|--------|---------|
|
||||||
|
| `auth-token.sh` | Authenticate and cache API token |
|
||||||
|
| `user-list.sh` | List users (search, filter by group) |
|
||||||
|
| `user-create.sh` | Create user with optional group assignment |
|
||||||
|
| `group-list.sh` | List groups |
|
||||||
|
| `app-list.sh` | List OAuth/SAML applications |
|
||||||
|
| `flow-list.sh` | List authentication flows |
|
||||||
|
| `admin-status.sh` | System health and version info |
|
||||||
|
|
||||||
|
## Common Options
|
||||||
|
|
||||||
|
All scripts support:
|
||||||
|
- `-f json` — JSON output (default: table)
|
||||||
|
- `-h` — Show help
|
||||||
|
|
||||||
|
## API Reference
|
||||||
|
|
||||||
|
- Base URL: `https://auth.diversecanvas.com`
|
||||||
|
- API prefix: `/api/v3/`
|
||||||
|
- OpenAPI schema: `/api/v3/schema/`
|
||||||
|
- Auth: Bearer token in `Authorization` header
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# List all users
|
||||||
|
~/.config/mosaic/tools/authentik/user-list.sh
|
||||||
|
|
||||||
|
# Search for a user
|
||||||
|
~/.config/mosaic/tools/authentik/user-list.sh -s "jason"
|
||||||
|
|
||||||
|
# Create a user in the admins group
|
||||||
|
~/.config/mosaic/tools/authentik/user-create.sh -u newuser -n "New User" -e new@example.com -g admins
|
||||||
|
|
||||||
|
# List OAuth applications as JSON
|
||||||
|
~/.config/mosaic/tools/authentik/app-list.sh -f json
|
||||||
|
|
||||||
|
# Check system health
|
||||||
|
~/.config/mosaic/tools/authentik/admin-status.sh
|
||||||
|
```
|
||||||
63
tools/authentik/admin-status.sh
Executable file
63
tools/authentik/admin-status.sh
Executable file
@@ -0,0 +1,63 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
#
|
||||||
|
# admin-status.sh — Authentik system health and version info
|
||||||
|
#
|
||||||
|
# Usage: admin-status.sh [-f format] [-a instance]
|
||||||
|
#
|
||||||
|
# Options:
|
||||||
|
# -f format Output format: table (default), json
|
||||||
|
# -a instance Authentik instance name (e.g. usc, mosaic)
|
||||||
|
# -h Show this help
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
MOSAIC_HOME="${MOSAIC_HOME:-$HOME/.config/mosaic}"
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
source "$MOSAIC_HOME/tools/_lib/credentials.sh"
|
||||||
|
|
||||||
|
FORMAT="table"
|
||||||
|
AK_INSTANCE=""
|
||||||
|
|
||||||
|
while getopts "f:a:h" opt; do
|
||||||
|
case $opt in
|
||||||
|
f) FORMAT="$OPTARG" ;;
|
||||||
|
a) AK_INSTANCE="$OPTARG" ;;
|
||||||
|
h) head -13 "$0" | grep "^#" | sed 's/^# \?//'; exit 0 ;;
|
||||||
|
*) echo "Usage: $0 [-f format] [-a instance]" >&2; exit 1 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ -n "$AK_INSTANCE" ]]; then
|
||||||
|
load_credentials "authentik-${AK_INSTANCE}"
|
||||||
|
else
|
||||||
|
load_credentials authentik
|
||||||
|
fi
|
||||||
|
|
||||||
|
TOKEN=$("$SCRIPT_DIR/auth-token.sh" -q ${AK_INSTANCE:+-a "$AK_INSTANCE"})
|
||||||
|
|
||||||
|
response=$(curl -sk -w "\n%{http_code}" \
|
||||||
|
-H "Authorization: Bearer $TOKEN" \
|
||||||
|
"${AUTHENTIK_URL}/api/v3/admin/system/")
|
||||||
|
|
||||||
|
http_code=$(echo "$response" | tail -n1)
|
||||||
|
body=$(echo "$response" | sed '$d')
|
||||||
|
|
||||||
|
if [[ "$http_code" != "200" ]]; then
|
||||||
|
echo "Error: Failed to get system status (HTTP $http_code)" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$FORMAT" == "json" ]]; then
|
||||||
|
echo "$body" | jq '.'
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Authentik System Status"
|
||||||
|
echo "======================="
|
||||||
|
echo "$body" | jq -r '
|
||||||
|
" URL: \(.http_host // "unknown")\n" +
|
||||||
|
" Version: \(.runtime.authentik_version // "unknown")\n" +
|
||||||
|
" Python: \(.runtime.python_version // "unknown")\n" +
|
||||||
|
" Workers: \(.runtime.gunicorn_workers // "unknown")\n" +
|
||||||
|
" Build Hash: \(.runtime.build_hash // "unknown")\n" +
|
||||||
|
" Embedded Outpost: \(.embedded_outpost_host // "unknown")"
|
||||||
|
'
|
||||||
70
tools/authentik/app-list.sh
Executable file
70
tools/authentik/app-list.sh
Executable file
@@ -0,0 +1,70 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
#
|
||||||
|
# app-list.sh — List Authentik applications
|
||||||
|
#
|
||||||
|
# Usage: app-list.sh [-f format] [-s search] [-a instance]
|
||||||
|
#
|
||||||
|
# Options:
|
||||||
|
# -f format Output format: table (default), json
|
||||||
|
# -s search Search by application name
|
||||||
|
# -a instance Authentik instance name (e.g. usc, mosaic)
|
||||||
|
# -h Show this help
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
MOSAIC_HOME="${MOSAIC_HOME:-$HOME/.config/mosaic}"
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
source "$MOSAIC_HOME/tools/_lib/credentials.sh"
|
||||||
|
|
||||||
|
FORMAT="table"
|
||||||
|
SEARCH=""
|
||||||
|
AK_INSTANCE=""
|
||||||
|
|
||||||
|
while getopts "f:s:a:h" opt; do
|
||||||
|
case $opt in
|
||||||
|
f) FORMAT="$OPTARG" ;;
|
||||||
|
s) SEARCH="$OPTARG" ;;
|
||||||
|
a) AK_INSTANCE="$OPTARG" ;;
|
||||||
|
h) head -14 "$0" | grep "^#" | sed 's/^# \?//'; exit 0 ;;
|
||||||
|
*) echo "Usage: $0 [-f format] [-s search] [-a instance]" >&2; exit 1 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ -n "$AK_INSTANCE" ]]; then
|
||||||
|
load_credentials "authentik-${AK_INSTANCE}"
|
||||||
|
else
|
||||||
|
load_credentials authentik
|
||||||
|
fi
|
||||||
|
|
||||||
|
TOKEN=$("$SCRIPT_DIR/auth-token.sh" -q ${AK_INSTANCE:+-a "$AK_INSTANCE"})
|
||||||
|
|
||||||
|
PARAMS="ordering=name"
|
||||||
|
[[ -n "$SEARCH" ]] && PARAMS="${PARAMS}&search=${SEARCH}"
|
||||||
|
|
||||||
|
response=$(curl -sk -w "\n%{http_code}" \
|
||||||
|
-H "Authorization: Bearer $TOKEN" \
|
||||||
|
"${AUTHENTIK_URL}/api/v3/core/applications/?${PARAMS}")
|
||||||
|
|
||||||
|
http_code=$(echo "$response" | tail -n1)
|
||||||
|
body=$(echo "$response" | sed '$d')
|
||||||
|
|
||||||
|
if [[ "$http_code" != "200" ]]; then
|
||||||
|
echo "Error: Failed to list applications (HTTP $http_code)" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$FORMAT" == "json" ]]; then
|
||||||
|
echo "$body" | jq '.results'
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "NAME SLUG PROVIDER LAUNCH URL"
|
||||||
|
echo "---------------------------- ---------------------------- ----------------- ----------------------------------------"
|
||||||
|
echo "$body" | jq -r '.results[] | [
|
||||||
|
.name,
|
||||||
|
.slug,
|
||||||
|
(.provider_obj.name // "none"),
|
||||||
|
(.launch_url // "—")
|
||||||
|
] | @tsv' | while IFS=$'\t' read -r name slug provider launch_url; do
|
||||||
|
printf "%-28s %-28s %-17s %s\n" \
|
||||||
|
"${name:0:28}" "${slug:0:28}" "${provider:0:17}" "$launch_url"
|
||||||
|
done
|
||||||
95
tools/authentik/auth-token.sh
Executable file
95
tools/authentik/auth-token.sh
Executable file
@@ -0,0 +1,95 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
#
|
||||||
|
# auth-token.sh — Obtain and cache Authentik API token
|
||||||
|
#
|
||||||
|
# Usage: auth-token.sh [-f] [-q] [-a instance]
|
||||||
|
#
|
||||||
|
# Returns a valid Authentik API token. Checks in order:
|
||||||
|
# 1. Cached token at ~/.cache/mosaic/authentik-token-<instance> (if valid)
|
||||||
|
# 2. Pre-configured token from credentials.json (authentik.<instance>.token)
|
||||||
|
# 3. Fails with instructions to create a token in the admin UI
|
||||||
|
#
|
||||||
|
# Options:
|
||||||
|
# -f Force re-validation (ignore cached token)
|
||||||
|
# -q Quiet mode — only output the token
|
||||||
|
# -a instance Authentik instance name (e.g. usc, mosaic)
|
||||||
|
# -h Show this help
|
||||||
|
#
|
||||||
|
# Environment variables (or credentials.json):
|
||||||
|
# AUTHENTIK_URL — Authentik instance URL
|
||||||
|
# AUTHENTIK_TOKEN — Pre-configured API token (recommended)
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
MOSAIC_HOME="${MOSAIC_HOME:-$HOME/.config/mosaic}"
|
||||||
|
source "$MOSAIC_HOME/tools/_lib/credentials.sh"
|
||||||
|
|
||||||
|
FORCE=false
|
||||||
|
QUIET=false
|
||||||
|
AK_INSTANCE=""
|
||||||
|
|
||||||
|
while getopts "fqa:h" opt; do
|
||||||
|
case $opt in
|
||||||
|
f) FORCE=true ;;
|
||||||
|
q) QUIET=true ;;
|
||||||
|
a) AK_INSTANCE="$OPTARG" ;;
|
||||||
|
h) head -22 "$0" | grep "^#" | sed 's/^# \?//'; exit 0 ;;
|
||||||
|
*) echo "Usage: $0 [-f] [-q] [-a instance]" >&2; exit 1 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ -n "$AK_INSTANCE" ]]; then
|
||||||
|
load_credentials "authentik-${AK_INSTANCE}"
|
||||||
|
else
|
||||||
|
load_credentials authentik
|
||||||
|
fi
|
||||||
|
|
||||||
|
CACHE_DIR="$HOME/.cache/mosaic"
|
||||||
|
CACHE_FILE="$CACHE_DIR/authentik-token${AUTHENTIK_INSTANCE:+-$AUTHENTIK_INSTANCE}"
|
||||||
|
|
||||||
|
_validate_token() {
|
||||||
|
local token="$1"
|
||||||
|
local http_code
|
||||||
|
http_code=$(curl -sk -o /dev/null -w "%{http_code}" \
|
||||||
|
--connect-timeout 5 --max-time 10 \
|
||||||
|
-H "Authorization: Bearer $token" \
|
||||||
|
"${AUTHENTIK_URL}/api/v3/core/users/me/")
|
||||||
|
[[ "$http_code" == "200" ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
# 1. Check cached token
|
||||||
|
if [[ "$FORCE" == "false" ]] && [[ -f "$CACHE_FILE" ]]; then
|
||||||
|
cached_token=$(cat "$CACHE_FILE")
|
||||||
|
if [[ -n "$cached_token" ]] && _validate_token "$cached_token"; then
|
||||||
|
[[ "$QUIET" == "false" ]] && echo "Using cached token (valid)" >&2
|
||||||
|
echo "$cached_token"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
[[ "$QUIET" == "false" ]] && echo "Cached token invalid, checking credentials..." >&2
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 2. Use pre-configured token from credentials.json
|
||||||
|
if [[ -n "${AUTHENTIK_TOKEN:-}" ]]; then
|
||||||
|
if _validate_token "$AUTHENTIK_TOKEN"; then
|
||||||
|
# Cache it for faster future access
|
||||||
|
mkdir -p "$CACHE_DIR"
|
||||||
|
echo "$AUTHENTIK_TOKEN" > "$CACHE_FILE"
|
||||||
|
chmod 600 "$CACHE_FILE"
|
||||||
|
[[ "$QUIET" == "false" ]] && echo "Token validated and cached at $CACHE_FILE" >&2
|
||||||
|
echo "$AUTHENTIK_TOKEN"
|
||||||
|
exit 0
|
||||||
|
else
|
||||||
|
echo "Error: Pre-configured AUTHENTIK_TOKEN is invalid (API returned non-200)" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 3. No token available
|
||||||
|
echo "Error: No Authentik API token configured" >&2
|
||||||
|
echo "" >&2
|
||||||
|
echo "To create one:" >&2
|
||||||
|
echo " 1. Log into Authentik admin: ${AUTHENTIK_URL}/if/admin/#/core/tokens" >&2
|
||||||
|
echo " 2. Click 'Create' → set identifier (e.g., 'mosaic-agent')" >&2
|
||||||
|
echo " 3. Select 'API Token' intent, uncheck 'Expiring'" >&2
|
||||||
|
echo " 4. Copy the key and add to credentials.json:" >&2
|
||||||
|
echo " Add token to credentials.json under authentik.<instance>.token" >&2
|
||||||
|
exit 1
|
||||||
70
tools/authentik/flow-list.sh
Executable file
70
tools/authentik/flow-list.sh
Executable file
@@ -0,0 +1,70 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
#
|
||||||
|
# flow-list.sh — List Authentik flows
|
||||||
|
#
|
||||||
|
# Usage: flow-list.sh [-f format] [-d designation] [-a instance]
|
||||||
|
#
|
||||||
|
# Options:
|
||||||
|
# -f format Output format: table (default), json
|
||||||
|
# -d designation Filter by designation (authentication, authorization, enrollment, etc.)
|
||||||
|
# -a instance Authentik instance name (e.g. usc, mosaic)
|
||||||
|
# -h Show this help
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
MOSAIC_HOME="${MOSAIC_HOME:-$HOME/.config/mosaic}"
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
source "$MOSAIC_HOME/tools/_lib/credentials.sh"
|
||||||
|
|
||||||
|
FORMAT="table"
|
||||||
|
DESIGNATION=""
|
||||||
|
AK_INSTANCE=""
|
||||||
|
|
||||||
|
while getopts "f:d:a:h" opt; do
|
||||||
|
case $opt in
|
||||||
|
f) FORMAT="$OPTARG" ;;
|
||||||
|
d) DESIGNATION="$OPTARG" ;;
|
||||||
|
a) AK_INSTANCE="$OPTARG" ;;
|
||||||
|
h) head -14 "$0" | grep "^#" | sed 's/^# \?//'; exit 0 ;;
|
||||||
|
*) echo "Usage: $0 [-f format] [-d designation] [-a instance]" >&2; exit 1 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ -n "$AK_INSTANCE" ]]; then
|
||||||
|
load_credentials "authentik-${AK_INSTANCE}"
|
||||||
|
else
|
||||||
|
load_credentials authentik
|
||||||
|
fi
|
||||||
|
|
||||||
|
TOKEN=$("$SCRIPT_DIR/auth-token.sh" -q ${AK_INSTANCE:+-a "$AK_INSTANCE"})
|
||||||
|
|
||||||
|
PARAMS="ordering=slug"
|
||||||
|
[[ -n "$DESIGNATION" ]] && PARAMS="${PARAMS}&designation=${DESIGNATION}"
|
||||||
|
|
||||||
|
response=$(curl -sk -w "\n%{http_code}" \
|
||||||
|
-H "Authorization: Bearer $TOKEN" \
|
||||||
|
"${AUTHENTIK_URL}/api/v3/flows/instances/?${PARAMS}")
|
||||||
|
|
||||||
|
http_code=$(echo "$response" | tail -n1)
|
||||||
|
body=$(echo "$response" | sed '$d')
|
||||||
|
|
||||||
|
if [[ "$http_code" != "200" ]]; then
|
||||||
|
echo "Error: Failed to list flows (HTTP $http_code)" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$FORMAT" == "json" ]]; then
|
||||||
|
echo "$body" | jq '.results'
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "NAME SLUG DESIGNATION TITLE"
|
||||||
|
echo "---------------------------- ---------------------------- ---------------- ----------------------------"
|
||||||
|
echo "$body" | jq -r '.results[] | [
|
||||||
|
.name,
|
||||||
|
.slug,
|
||||||
|
.designation,
|
||||||
|
(.title // "—")
|
||||||
|
] | @tsv' | while IFS=$'\t' read -r name slug designation title; do
|
||||||
|
printf "%-28s %-28s %-16s %s\n" \
|
||||||
|
"${name:0:28}" "${slug:0:28}" "$designation" "${title:0:28}"
|
||||||
|
done
|
||||||
69
tools/authentik/group-list.sh
Executable file
69
tools/authentik/group-list.sh
Executable file
@@ -0,0 +1,69 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
#
|
||||||
|
# group-list.sh — List Authentik groups
|
||||||
|
#
|
||||||
|
# Usage: group-list.sh [-f format] [-s search] [-a instance]
|
||||||
|
#
|
||||||
|
# Options:
|
||||||
|
# -f format Output format: table (default), json
|
||||||
|
# -s search Search by group name
|
||||||
|
# -a instance Authentik instance name (e.g. usc, mosaic)
|
||||||
|
# -h Show this help
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
MOSAIC_HOME="${MOSAIC_HOME:-$HOME/.config/mosaic}"
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
source "$MOSAIC_HOME/tools/_lib/credentials.sh"
|
||||||
|
|
||||||
|
FORMAT="table"
|
||||||
|
SEARCH=""
|
||||||
|
AK_INSTANCE=""
|
||||||
|
|
||||||
|
while getopts "f:s:a:h" opt; do
|
||||||
|
case $opt in
|
||||||
|
f) FORMAT="$OPTARG" ;;
|
||||||
|
s) SEARCH="$OPTARG" ;;
|
||||||
|
a) AK_INSTANCE="$OPTARG" ;;
|
||||||
|
h) head -13 "$0" | grep "^#" | sed 's/^# \?//'; exit 0 ;;
|
||||||
|
*) echo "Usage: $0 [-f format] [-s search] [-a instance]" >&2; exit 1 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ -n "$AK_INSTANCE" ]]; then
|
||||||
|
load_credentials "authentik-${AK_INSTANCE}"
|
||||||
|
else
|
||||||
|
load_credentials authentik
|
||||||
|
fi
|
||||||
|
|
||||||
|
TOKEN=$("$SCRIPT_DIR/auth-token.sh" -q ${AK_INSTANCE:+-a "$AK_INSTANCE"})
|
||||||
|
|
||||||
|
PARAMS="ordering=name"
|
||||||
|
[[ -n "$SEARCH" ]] && PARAMS="${PARAMS}&search=${SEARCH}"
|
||||||
|
|
||||||
|
response=$(curl -sk -w "\n%{http_code}" \
|
||||||
|
-H "Authorization: Bearer $TOKEN" \
|
||||||
|
"${AUTHENTIK_URL}/api/v3/core/groups/?${PARAMS}")
|
||||||
|
|
||||||
|
http_code=$(echo "$response" | tail -n1)
|
||||||
|
body=$(echo "$response" | sed '$d')
|
||||||
|
|
||||||
|
if [[ "$http_code" != "200" ]]; then
|
||||||
|
echo "Error: Failed to list groups (HTTP $http_code)" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$FORMAT" == "json" ]]; then
|
||||||
|
echo "$body" | jq '.results'
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "NAME PK MEMBERS SUPERUSER"
|
||||||
|
echo "---------------------------- ------------------------------------ ------- ---------"
|
||||||
|
echo "$body" | jq -r '.results[] | [
|
||||||
|
.name,
|
||||||
|
.pk,
|
||||||
|
(.users | length | tostring),
|
||||||
|
(if .is_superuser then "yes" else "no" end)
|
||||||
|
] | @tsv' | while IFS=$'\t' read -r name pk members superuser; do
|
||||||
|
printf "%-28s %-36s %-7s %s\n" "${name:0:28}" "$pk" "$members" "$superuser"
|
||||||
|
done
|
||||||
100
tools/authentik/user-create.sh
Executable file
100
tools/authentik/user-create.sh
Executable file
@@ -0,0 +1,100 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
#
|
||||||
|
# user-create.sh — Create an Authentik user
|
||||||
|
#
|
||||||
|
# Usage: user-create.sh -u <username> -n <name> -e <email> [-p password] [-g group] [-a instance]
|
||||||
|
#
|
||||||
|
# Options:
|
||||||
|
# -u username Username (required)
|
||||||
|
# -n name Display name (required)
|
||||||
|
# -e email Email address (required)
|
||||||
|
# -p password Initial password (optional — user gets set-password flow if omitted)
|
||||||
|
# -g group Group name to add user to (optional)
|
||||||
|
# -f format Output format: table (default), json
|
||||||
|
# -a instance Authentik instance name (e.g. usc, mosaic)
|
||||||
|
# -h Show this help
|
||||||
|
#
|
||||||
|
# Environment variables (or credentials.json):
|
||||||
|
# AUTHENTIK_URL — Authentik instance URL
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
MOSAIC_HOME="${MOSAIC_HOME:-$HOME/.config/mosaic}"
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
source "$MOSAIC_HOME/tools/_lib/credentials.sh"
|
||||||
|
|
||||||
|
USERNAME="" NAME="" EMAIL="" PASSWORD="" GROUP="" FORMAT="table" AK_INSTANCE=""
|
||||||
|
|
||||||
|
while getopts "u:n:e:p:g:f:a:h" opt; do
|
||||||
|
case $opt in
|
||||||
|
u) USERNAME="$OPTARG" ;;
|
||||||
|
n) NAME="$OPTARG" ;;
|
||||||
|
e) EMAIL="$OPTARG" ;;
|
||||||
|
p) PASSWORD="$OPTARG" ;;
|
||||||
|
g) GROUP="$OPTARG" ;;
|
||||||
|
f) FORMAT="$OPTARG" ;;
|
||||||
|
a) AK_INSTANCE="$OPTARG" ;;
|
||||||
|
h) head -19 "$0" | grep "^#" | sed 's/^# \?//'; exit 0 ;;
|
||||||
|
*) echo "Usage: $0 -u <username> -n <name> -e <email> [-p password] [-g group] [-a instance]" >&2; exit 1 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ -n "$AK_INSTANCE" ]]; then
|
||||||
|
load_credentials "authentik-${AK_INSTANCE}"
|
||||||
|
else
|
||||||
|
load_credentials authentik
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -z "$USERNAME" || -z "$NAME" || -z "$EMAIL" ]]; then
|
||||||
|
echo "Error: -u username, -n name, and -e email are required" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
TOKEN=$("$SCRIPT_DIR/auth-token.sh" -q ${AK_INSTANCE:+-a "$AK_INSTANCE"})
|
||||||
|
|
||||||
|
# Build user payload
|
||||||
|
payload=$(jq -n \
|
||||||
|
--arg username "$USERNAME" \
|
||||||
|
--arg name "$NAME" \
|
||||||
|
--arg email "$EMAIL" \
|
||||||
|
'{username: $username, name: $name, email: $email, is_active: true}')
|
||||||
|
|
||||||
|
# Add password if provided
|
||||||
|
if [[ -n "$PASSWORD" ]]; then
|
||||||
|
payload=$(echo "$payload" | jq --arg pw "$PASSWORD" '. + {password: $pw}')
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Add to group if provided
|
||||||
|
if [[ -n "$GROUP" ]]; then
|
||||||
|
# Look up group PK by name
|
||||||
|
group_response=$(curl -sk \
|
||||||
|
-H "Authorization: Bearer $TOKEN" \
|
||||||
|
"${AUTHENTIK_URL}/api/v3/core/groups/?search=${GROUP}")
|
||||||
|
group_pk=$(echo "$group_response" | jq -r ".results[] | select(.name == \"$GROUP\") | .pk" | head -1)
|
||||||
|
if [[ -n "$group_pk" ]]; then
|
||||||
|
payload=$(echo "$payload" | jq --arg gk "$group_pk" '. + {groups: [$gk]}')
|
||||||
|
else
|
||||||
|
echo "Warning: Group '$GROUP' not found — creating user without group" >&2
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
response=$(curl -sk -w "\n%{http_code}" -X POST \
|
||||||
|
-H "Authorization: Bearer $TOKEN" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "$payload" \
|
||||||
|
"${AUTHENTIK_URL}/api/v3/core/users/")
|
||||||
|
|
||||||
|
http_code=$(echo "$response" | tail -n1)
|
||||||
|
body=$(echo "$response" | sed '$d')
|
||||||
|
|
||||||
|
if [[ "$http_code" != "201" ]]; then
|
||||||
|
echo "Error: Failed to create user (HTTP $http_code)" >&2
|
||||||
|
echo "$body" | jq -r '.' 2>/dev/null >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$FORMAT" == "json" ]]; then
|
||||||
|
echo "$body" | jq '.'
|
||||||
|
else
|
||||||
|
echo "User created successfully:"
|
||||||
|
echo "$body" | jq -r '" Username: \(.username)\n Name: \(.name)\n Email: \(.email)\n PK: \(.pk)"'
|
||||||
|
fi
|
||||||
80
tools/authentik/user-list.sh
Executable file
80
tools/authentik/user-list.sh
Executable file
@@ -0,0 +1,80 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
#
|
||||||
|
# user-list.sh — List Authentik users
|
||||||
|
#
|
||||||
|
# Usage: user-list.sh [-f format] [-s search] [-g group] [-a instance]
|
||||||
|
#
|
||||||
|
# Options:
|
||||||
|
# -f format Output format: table (default), json
|
||||||
|
# -s search Search term (matches username, name, email)
|
||||||
|
# -g group Filter by group name
|
||||||
|
# -a instance Authentik instance name (e.g. usc, mosaic)
|
||||||
|
# -h Show this help
|
||||||
|
#
|
||||||
|
# Environment variables (or credentials.json):
|
||||||
|
# AUTHENTIK_URL — Authentik instance URL
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
MOSAIC_HOME="${MOSAIC_HOME:-$HOME/.config/mosaic}"
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
source "$MOSAIC_HOME/tools/_lib/credentials.sh"
|
||||||
|
|
||||||
|
FORMAT="table"
|
||||||
|
SEARCH=""
|
||||||
|
GROUP=""
|
||||||
|
AK_INSTANCE=""
|
||||||
|
|
||||||
|
while getopts "f:s:g:a:h" opt; do
|
||||||
|
case $opt in
|
||||||
|
f) FORMAT="$OPTARG" ;;
|
||||||
|
s) SEARCH="$OPTARG" ;;
|
||||||
|
g) GROUP="$OPTARG" ;;
|
||||||
|
a) AK_INSTANCE="$OPTARG" ;;
|
||||||
|
h) head -15 "$0" | grep "^#" | sed 's/^# \?//'; exit 0 ;;
|
||||||
|
*) echo "Usage: $0 [-f format] [-s search] [-g group] [-a instance]" >&2; exit 1 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ -n "$AK_INSTANCE" ]]; then
|
||||||
|
load_credentials "authentik-${AK_INSTANCE}"
|
||||||
|
else
|
||||||
|
load_credentials authentik
|
||||||
|
fi
|
||||||
|
|
||||||
|
TOKEN=$("$SCRIPT_DIR/auth-token.sh" -q ${AK_INSTANCE:+-a "$AK_INSTANCE"})
|
||||||
|
|
||||||
|
# Build query params
|
||||||
|
PARAMS="ordering=username"
|
||||||
|
[[ -n "$SEARCH" ]] && PARAMS="${PARAMS}&search=${SEARCH}"
|
||||||
|
[[ -n "$GROUP" ]] && PARAMS="${PARAMS}&groups_by_name=${GROUP}"
|
||||||
|
|
||||||
|
response=$(curl -sk -w "\n%{http_code}" \
|
||||||
|
-H "Authorization: Bearer $TOKEN" \
|
||||||
|
"${AUTHENTIK_URL}/api/v3/core/users/?${PARAMS}")
|
||||||
|
|
||||||
|
http_code=$(echo "$response" | tail -n1)
|
||||||
|
body=$(echo "$response" | sed '$d')
|
||||||
|
|
||||||
|
if [[ "$http_code" != "200" ]]; then
|
||||||
|
echo "Error: Failed to list users (HTTP $http_code)" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$FORMAT" == "json" ]]; then
|
||||||
|
echo "$body" | jq '.results'
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Table output
|
||||||
|
echo "USERNAME NAME EMAIL ACTIVE LAST LOGIN"
|
||||||
|
echo "-------------------- ---------------------------- ---------------------------- ------ ----------"
|
||||||
|
echo "$body" | jq -r '.results[] | [
|
||||||
|
.username,
|
||||||
|
.name,
|
||||||
|
.email,
|
||||||
|
(if .is_active then "yes" else "no" end),
|
||||||
|
(.last_login // "never" | split("T")[0])
|
||||||
|
] | @tsv' | while IFS=$'\t' read -r username name email active last_login; do
|
||||||
|
printf "%-20s %-28s %-28s %-6s %s\n" \
|
||||||
|
"${username:0:20}" "${name:0:28}" "${email:0:28}" "$active" "$last_login"
|
||||||
|
done
|
||||||
@@ -230,9 +230,9 @@ JSONEOF
|
|||||||
|
|
||||||
if $FIX_HINT && ! $JSON_OUTPUT; then
|
if $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
|
||||||
|
|
||||||
@@ -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
|
||||||
@@ -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
67
tools/cloudflare/_lib.sh
Executable file
@@ -0,0 +1,67 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
#
|
||||||
|
# _lib.sh — Shared helpers for Cloudflare tool scripts
|
||||||
|
#
|
||||||
|
# Usage: source "$(dirname "$0")/_lib.sh"
|
||||||
|
#
|
||||||
|
# Provides:
|
||||||
|
# CF_API — Base API URL
|
||||||
|
# cf_auth — Authorization header value
|
||||||
|
# cf_load_instance <instance> — Load credentials for a specific or default instance
|
||||||
|
# cf_resolve_zone <name_or_id> — Resolves a zone name to its ID (passes IDs through)
|
||||||
|
|
||||||
|
CF_API="https://api.cloudflare.com/client/v4"
|
||||||
|
|
||||||
|
cf_auth() {
|
||||||
|
echo "Bearer $CLOUDFLARE_API_TOKEN"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Load credentials for a Cloudflare instance.
|
||||||
|
# If instance is empty, loads the default.
|
||||||
|
cf_load_instance() {
|
||||||
|
local instance="$1"
|
||||||
|
if [[ -n "$instance" ]]; then
|
||||||
|
load_credentials "cloudflare-${instance}"
|
||||||
|
else
|
||||||
|
load_credentials cloudflare
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Resolve a zone name (e.g. "mosaicstack.dev") to its zone ID.
|
||||||
|
# If the input is already a 32-char hex ID, passes it through.
|
||||||
|
cf_resolve_zone() {
|
||||||
|
local input="$1"
|
||||||
|
|
||||||
|
# If it looks like a zone ID (32 hex chars), pass through
|
||||||
|
if [[ "$input" =~ ^[0-9a-f]{32}$ ]]; then
|
||||||
|
echo "$input"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Resolve by name
|
||||||
|
local response
|
||||||
|
response=$(curl -s -w "\n%{http_code}" \
|
||||||
|
-H "Authorization: $(cf_auth)" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
"${CF_API}/zones?name=${input}&status=active")
|
||||||
|
|
||||||
|
local http_code
|
||||||
|
http_code=$(echo "$response" | tail -n1)
|
||||||
|
local body
|
||||||
|
body=$(echo "$response" | sed '$d')
|
||||||
|
|
||||||
|
if [[ "$http_code" != "200" ]]; then
|
||||||
|
echo "Error: Failed to resolve zone '$input' (HTTP $http_code)" >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
local zone_id
|
||||||
|
zone_id=$(echo "$body" | jq -r '.result[0].id // empty')
|
||||||
|
|
||||||
|
if [[ -z "$zone_id" ]]; then
|
||||||
|
echo "Error: Zone '$input' not found" >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "$zone_id"
|
||||||
|
}
|
||||||
86
tools/cloudflare/record-create.sh
Executable file
86
tools/cloudflare/record-create.sh
Executable file
@@ -0,0 +1,86 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
#
|
||||||
|
# record-create.sh — Create a DNS record in a Cloudflare zone
|
||||||
|
#
|
||||||
|
# Usage: record-create.sh -z <zone> -t <type> -n <name> -c <content> [-a instance] [-l ttl] [-p] [-P priority]
|
||||||
|
#
|
||||||
|
# Options:
|
||||||
|
# -z zone Zone name or ID (required)
|
||||||
|
# -t type Record type: A, AAAA, CNAME, MX, TXT, etc. (required)
|
||||||
|
# -n name Record name, e.g. "app" or "app.example.com" (required)
|
||||||
|
# -c content Record value/content (required)
|
||||||
|
# -a instance Cloudflare instance name (default: uses credentials default)
|
||||||
|
# -l ttl TTL in seconds (default: 1 = auto)
|
||||||
|
# -p Enable Cloudflare proxy (orange cloud)
|
||||||
|
# -P priority MX/SRV priority (default: 10)
|
||||||
|
# -h Show this help
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
MOSAIC_HOME="${MOSAIC_HOME:-$HOME/.config/mosaic}"
|
||||||
|
source "$MOSAIC_HOME/tools/_lib/credentials.sh"
|
||||||
|
source "$(dirname "$0")/_lib.sh"
|
||||||
|
|
||||||
|
ZONE=""
|
||||||
|
INSTANCE=""
|
||||||
|
TYPE=""
|
||||||
|
NAME=""
|
||||||
|
CONTENT=""
|
||||||
|
TTL=1
|
||||||
|
PROXIED=false
|
||||||
|
PRIORITY=""
|
||||||
|
|
||||||
|
while getopts "z:a:t:n:c:l:pP:h" opt; do
|
||||||
|
case $opt in
|
||||||
|
z) ZONE="$OPTARG" ;;
|
||||||
|
a) INSTANCE="$OPTARG" ;;
|
||||||
|
t) TYPE="$OPTARG" ;;
|
||||||
|
n) NAME="$OPTARG" ;;
|
||||||
|
c) CONTENT="$OPTARG" ;;
|
||||||
|
l) TTL="$OPTARG" ;;
|
||||||
|
p) PROXIED=true ;;
|
||||||
|
P) PRIORITY="$OPTARG" ;;
|
||||||
|
h) head -18 "$0" | grep "^#" | sed 's/^# \?//'; exit 0 ;;
|
||||||
|
*) echo "Usage: $0 -z <zone> -t <type> -n <name> -c <content> [-a instance] [-l ttl] [-p] [-P priority]" >&2; exit 1 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ -z "$ZONE" || -z "$TYPE" || -z "$NAME" || -z "$CONTENT" ]]; then
|
||||||
|
echo "Error: -z, -t, -n, and -c are all required" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
cf_load_instance "$INSTANCE"
|
||||||
|
ZONE_ID=$(cf_resolve_zone "$ZONE") || exit 1
|
||||||
|
|
||||||
|
# Build JSON payload
|
||||||
|
payload=$(jq -n \
|
||||||
|
--arg type "$TYPE" \
|
||||||
|
--arg name "$NAME" \
|
||||||
|
--arg content "$CONTENT" \
|
||||||
|
--argjson ttl "$TTL" \
|
||||||
|
--argjson proxied "$PROXIED" \
|
||||||
|
'{type: $type, name: $name, content: $content, ttl: $ttl, proxied: $proxied}')
|
||||||
|
|
||||||
|
# Add priority for MX/SRV records
|
||||||
|
if [[ -n "$PRIORITY" ]]; then
|
||||||
|
payload=$(echo "$payload" | jq --argjson priority "$PRIORITY" '. + {priority: $priority}')
|
||||||
|
fi
|
||||||
|
|
||||||
|
response=$(curl -s -w "\n%{http_code}" \
|
||||||
|
-X POST \
|
||||||
|
-H "Authorization: $(cf_auth)" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "$payload" \
|
||||||
|
"${CF_API}/zones/${ZONE_ID}/dns_records")
|
||||||
|
|
||||||
|
http_code=$(echo "$response" | tail -n1)
|
||||||
|
body=$(echo "$response" | sed '$d')
|
||||||
|
|
||||||
|
if [[ "$http_code" != "200" ]]; then
|
||||||
|
echo "Error: Failed to create record (HTTP $http_code)" >&2
|
||||||
|
echo "$body" | jq -r '.errors[]?.message // empty' 2>/dev/null >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
record_id=$(echo "$body" | jq -r '.result.id')
|
||||||
|
echo "Created $TYPE record: $NAME → $CONTENT (ID: $record_id)"
|
||||||
55
tools/cloudflare/record-delete.sh
Executable file
55
tools/cloudflare/record-delete.sh
Executable file
@@ -0,0 +1,55 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
#
|
||||||
|
# record-delete.sh — Delete a DNS record from a Cloudflare zone
|
||||||
|
#
|
||||||
|
# Usage: record-delete.sh -z <zone> -r <record-id> [-a instance]
|
||||||
|
#
|
||||||
|
# Options:
|
||||||
|
# -z zone Zone name or ID (required)
|
||||||
|
# -r record-id DNS record ID (required)
|
||||||
|
# -a instance Cloudflare instance name (default: uses credentials default)
|
||||||
|
# -h Show this help
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
MOSAIC_HOME="${MOSAIC_HOME:-$HOME/.config/mosaic}"
|
||||||
|
source "$MOSAIC_HOME/tools/_lib/credentials.sh"
|
||||||
|
source "$(dirname "$0")/_lib.sh"
|
||||||
|
|
||||||
|
ZONE=""
|
||||||
|
INSTANCE=""
|
||||||
|
RECORD_ID=""
|
||||||
|
|
||||||
|
while getopts "z:a:r:h" opt; do
|
||||||
|
case $opt in
|
||||||
|
z) ZONE="$OPTARG" ;;
|
||||||
|
a) INSTANCE="$OPTARG" ;;
|
||||||
|
r) RECORD_ID="$OPTARG" ;;
|
||||||
|
h) head -11 "$0" | grep "^#" | sed 's/^# \?//'; exit 0 ;;
|
||||||
|
*) echo "Usage: $0 -z <zone> -r <record-id> [-a instance]" >&2; exit 1 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ -z "$ZONE" || -z "$RECORD_ID" ]]; then
|
||||||
|
echo "Error: -z and -r are both required" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
cf_load_instance "$INSTANCE"
|
||||||
|
ZONE_ID=$(cf_resolve_zone "$ZONE") || exit 1
|
||||||
|
|
||||||
|
response=$(curl -s -w "\n%{http_code}" \
|
||||||
|
-X DELETE \
|
||||||
|
-H "Authorization: $(cf_auth)" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
"${CF_API}/zones/${ZONE_ID}/dns_records/${RECORD_ID}")
|
||||||
|
|
||||||
|
http_code=$(echo "$response" | tail -n1)
|
||||||
|
body=$(echo "$response" | sed '$d')
|
||||||
|
|
||||||
|
if [[ "$http_code" != "200" ]]; then
|
||||||
|
echo "Error: Failed to delete record (HTTP $http_code)" >&2
|
||||||
|
echo "$body" | jq -r '.errors[]?.message // empty' 2>/dev/null >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Deleted DNS record $RECORD_ID from zone $ZONE"
|
||||||
81
tools/cloudflare/record-list.sh
Executable file
81
tools/cloudflare/record-list.sh
Executable file
@@ -0,0 +1,81 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
#
|
||||||
|
# record-list.sh — List DNS records for a Cloudflare zone
|
||||||
|
#
|
||||||
|
# Usage: record-list.sh -z <zone> [-a instance] [-t type] [-n name] [-f format]
|
||||||
|
#
|
||||||
|
# Options:
|
||||||
|
# -z zone Zone name or ID (required)
|
||||||
|
# -a instance Cloudflare instance name (default: uses credentials default)
|
||||||
|
# -t type Filter by record type (A, AAAA, CNAME, MX, TXT, etc.)
|
||||||
|
# -n name Filter by record name
|
||||||
|
# -f format Output format: table (default), json
|
||||||
|
# -h Show this help
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
MOSAIC_HOME="${MOSAIC_HOME:-$HOME/.config/mosaic}"
|
||||||
|
source "$MOSAIC_HOME/tools/_lib/credentials.sh"
|
||||||
|
source "$(dirname "$0")/_lib.sh"
|
||||||
|
|
||||||
|
ZONE=""
|
||||||
|
INSTANCE=""
|
||||||
|
TYPE=""
|
||||||
|
NAME=""
|
||||||
|
FORMAT="table"
|
||||||
|
|
||||||
|
while getopts "z:a:t:n:f:h" opt; do
|
||||||
|
case $opt in
|
||||||
|
z) ZONE="$OPTARG" ;;
|
||||||
|
a) INSTANCE="$OPTARG" ;;
|
||||||
|
t) TYPE="$OPTARG" ;;
|
||||||
|
n) NAME="$OPTARG" ;;
|
||||||
|
f) FORMAT="$OPTARG" ;;
|
||||||
|
h) head -14 "$0" | grep "^#" | sed 's/^# \?//'; exit 0 ;;
|
||||||
|
*) echo "Usage: $0 -z <zone> [-a instance] [-t type] [-n name] [-f format]" >&2; exit 1 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ -z "$ZONE" ]]; then
|
||||||
|
echo "Error: -z zone is required" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
cf_load_instance "$INSTANCE"
|
||||||
|
ZONE_ID=$(cf_resolve_zone "$ZONE") || exit 1
|
||||||
|
|
||||||
|
# Build query params
|
||||||
|
params="per_page=100"
|
||||||
|
[[ -n "$TYPE" ]] && params="${params}&type=${TYPE}"
|
||||||
|
[[ -n "$NAME" ]] && params="${params}&name=${NAME}"
|
||||||
|
|
||||||
|
response=$(curl -s -w "\n%{http_code}" \
|
||||||
|
-H "Authorization: $(cf_auth)" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
"${CF_API}/zones/${ZONE_ID}/dns_records?${params}")
|
||||||
|
|
||||||
|
http_code=$(echo "$response" | tail -n1)
|
||||||
|
body=$(echo "$response" | sed '$d')
|
||||||
|
|
||||||
|
if [[ "$http_code" != "200" ]]; then
|
||||||
|
echo "Error: Failed to list records (HTTP $http_code)" >&2
|
||||||
|
echo "$body" | jq -r '.errors[]?.message // empty' 2>/dev/null >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$FORMAT" == "json" ]]; then
|
||||||
|
echo "$body" | jq '.result'
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "RECORD ID TYPE NAME CONTENT PROXIED TTL"
|
||||||
|
echo "-------------------------------- ----- -------------------------------------- ------------------------------- ------- -----"
|
||||||
|
echo "$body" | jq -r '.result[] | [
|
||||||
|
.id,
|
||||||
|
.type,
|
||||||
|
.name,
|
||||||
|
.content,
|
||||||
|
(if .proxied then "yes" else "no" end),
|
||||||
|
(if .ttl == 1 then "auto" else (.ttl | tostring) end)
|
||||||
|
] | @tsv' | while IFS=$'\t' read -r id type name content proxied ttl; do
|
||||||
|
printf "%-32s %-5s %-38s %-31s %-7s %s\n" "$id" "$type" "${name:0:38}" "${content:0:31}" "$proxied" "$ttl"
|
||||||
|
done
|
||||||
86
tools/cloudflare/record-update.sh
Executable file
86
tools/cloudflare/record-update.sh
Executable file
@@ -0,0 +1,86 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
#
|
||||||
|
# record-update.sh — Update a DNS record in a Cloudflare zone
|
||||||
|
#
|
||||||
|
# Usage: record-update.sh -z <zone> -r <record-id> -t <type> -n <name> -c <content> [-a instance] [-l ttl] [-p] [-P priority]
|
||||||
|
#
|
||||||
|
# Options:
|
||||||
|
# -z zone Zone name or ID (required)
|
||||||
|
# -r record-id DNS record ID (required)
|
||||||
|
# -t type Record type: A, AAAA, CNAME, MX, TXT, etc. (required)
|
||||||
|
# -n name Record name (required)
|
||||||
|
# -c content Record value/content (required)
|
||||||
|
# -a instance Cloudflare instance name (default: uses credentials default)
|
||||||
|
# -l ttl TTL in seconds (default: 1 = auto)
|
||||||
|
# -p Enable Cloudflare proxy (orange cloud)
|
||||||
|
# -P priority MX/SRV priority
|
||||||
|
# -h Show this help
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
MOSAIC_HOME="${MOSAIC_HOME:-$HOME/.config/mosaic}"
|
||||||
|
source "$MOSAIC_HOME/tools/_lib/credentials.sh"
|
||||||
|
source "$(dirname "$0")/_lib.sh"
|
||||||
|
|
||||||
|
ZONE=""
|
||||||
|
INSTANCE=""
|
||||||
|
RECORD_ID=""
|
||||||
|
TYPE=""
|
||||||
|
NAME=""
|
||||||
|
CONTENT=""
|
||||||
|
TTL=1
|
||||||
|
PROXIED=false
|
||||||
|
PRIORITY=""
|
||||||
|
|
||||||
|
while getopts "z:a:r:t:n:c:l:pP:h" opt; do
|
||||||
|
case $opt in
|
||||||
|
z) ZONE="$OPTARG" ;;
|
||||||
|
a) INSTANCE="$OPTARG" ;;
|
||||||
|
r) RECORD_ID="$OPTARG" ;;
|
||||||
|
t) TYPE="$OPTARG" ;;
|
||||||
|
n) NAME="$OPTARG" ;;
|
||||||
|
c) CONTENT="$OPTARG" ;;
|
||||||
|
l) TTL="$OPTARG" ;;
|
||||||
|
p) PROXIED=true ;;
|
||||||
|
P) PRIORITY="$OPTARG" ;;
|
||||||
|
h) head -18 "$0" | grep "^#" | sed 's/^# \?//'; exit 0 ;;
|
||||||
|
*) echo "Usage: $0 -z <zone> -r <record-id> -t <type> -n <name> -c <content> [-a instance]" >&2; exit 1 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ -z "$ZONE" || -z "$RECORD_ID" || -z "$TYPE" || -z "$NAME" || -z "$CONTENT" ]]; then
|
||||||
|
echo "Error: -z, -r, -t, -n, and -c are all required" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
cf_load_instance "$INSTANCE"
|
||||||
|
ZONE_ID=$(cf_resolve_zone "$ZONE") || exit 1
|
||||||
|
|
||||||
|
payload=$(jq -n \
|
||||||
|
--arg type "$TYPE" \
|
||||||
|
--arg name "$NAME" \
|
||||||
|
--arg content "$CONTENT" \
|
||||||
|
--argjson ttl "$TTL" \
|
||||||
|
--argjson proxied "$PROXIED" \
|
||||||
|
'{type: $type, name: $name, content: $content, ttl: $ttl, proxied: $proxied}')
|
||||||
|
|
||||||
|
if [[ -n "$PRIORITY" ]]; then
|
||||||
|
payload=$(echo "$payload" | jq --argjson priority "$PRIORITY" '. + {priority: $priority}')
|
||||||
|
fi
|
||||||
|
|
||||||
|
response=$(curl -s -w "\n%{http_code}" \
|
||||||
|
-X PUT \
|
||||||
|
-H "Authorization: $(cf_auth)" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "$payload" \
|
||||||
|
"${CF_API}/zones/${ZONE_ID}/dns_records/${RECORD_ID}")
|
||||||
|
|
||||||
|
http_code=$(echo "$response" | tail -n1)
|
||||||
|
body=$(echo "$response" | sed '$d')
|
||||||
|
|
||||||
|
if [[ "$http_code" != "200" ]]; then
|
||||||
|
echo "Error: Failed to update record (HTTP $http_code)" >&2
|
||||||
|
echo "$body" | jq -r '.errors[]?.message // empty' 2>/dev/null >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Updated $TYPE record: $NAME → $CONTENT (ID: $RECORD_ID)"
|
||||||
59
tools/cloudflare/zone-list.sh
Executable file
59
tools/cloudflare/zone-list.sh
Executable file
@@ -0,0 +1,59 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
#
|
||||||
|
# zone-list.sh — List Cloudflare zones (domains)
|
||||||
|
#
|
||||||
|
# Usage: zone-list.sh [-a instance] [-f format]
|
||||||
|
#
|
||||||
|
# Options:
|
||||||
|
# -a instance Cloudflare instance name (default: uses credentials default)
|
||||||
|
# -f format Output format: table (default), json
|
||||||
|
# -h Show this help
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
MOSAIC_HOME="${MOSAIC_HOME:-$HOME/.config/mosaic}"
|
||||||
|
source "$MOSAIC_HOME/tools/_lib/credentials.sh"
|
||||||
|
source "$(dirname "$0")/_lib.sh"
|
||||||
|
|
||||||
|
INSTANCE=""
|
||||||
|
FORMAT="table"
|
||||||
|
|
||||||
|
while getopts "a:f:h" opt; do
|
||||||
|
case $opt in
|
||||||
|
a) INSTANCE="$OPTARG" ;;
|
||||||
|
f) FORMAT="$OPTARG" ;;
|
||||||
|
h) head -10 "$0" | grep "^#" | sed 's/^# \?//'; exit 0 ;;
|
||||||
|
*) echo "Usage: $0 [-a instance] [-f format]" >&2; exit 1 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
cf_load_instance "$INSTANCE"
|
||||||
|
|
||||||
|
response=$(curl -s -w "\n%{http_code}" \
|
||||||
|
-H "Authorization: $(cf_auth)" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
"${CF_API}/zones?per_page=50")
|
||||||
|
|
||||||
|
http_code=$(echo "$response" | tail -n1)
|
||||||
|
body=$(echo "$response" | sed '$d')
|
||||||
|
|
||||||
|
if [[ "$http_code" != "200" ]]; then
|
||||||
|
echo "Error: Failed to list zones (HTTP $http_code)" >&2
|
||||||
|
echo "$body" | jq -r '.errors[]?.message // empty' 2>/dev/null >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$FORMAT" == "json" ]]; then
|
||||||
|
echo "$body" | jq '.result'
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "ZONE ID NAME STATUS PLAN"
|
||||||
|
echo "-------------------------------- ---------------------------- -------- ----------"
|
||||||
|
echo "$body" | jq -r '.result[] | [
|
||||||
|
.id,
|
||||||
|
.name,
|
||||||
|
.status,
|
||||||
|
.plan.name
|
||||||
|
] | @tsv' | while IFS=$'\t' read -r id name status plan; do
|
||||||
|
printf "%-32s %-28s %-8s %s\n" "$id" "$name" "$status" "$plan"
|
||||||
|
done
|
||||||
@@ -50,45 +50,45 @@ Security vulnerability review focusing on:
|
|||||||
|
|
||||||
```bash
|
```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/
|
||||||
64
tools/context/mosaic-context-loader.sh
Executable file
64
tools/context/mosaic-context-loader.sh
Executable file
@@ -0,0 +1,64 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# mosaic-context-loader.sh — SessionStart hook for Claude Code
|
||||||
|
# Injects mandatory Mosaic config files into agent context at session init.
|
||||||
|
# Stdout from this script is added to Claude's context before processing.
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
MOSAIC_HOME="${MOSAIC_HOME:-$HOME/.config/mosaic}"
|
||||||
|
|
||||||
|
# Mandatory load order (per AGENTS.md contract)
|
||||||
|
MANDATORY_FILES=(
|
||||||
|
"$MOSAIC_HOME/SOUL.md"
|
||||||
|
"$MOSAIC_HOME/USER.md"
|
||||||
|
"$MOSAIC_HOME/STANDARDS.md"
|
||||||
|
"$MOSAIC_HOME/AGENTS.md"
|
||||||
|
"$MOSAIC_HOME/TOOLS.md"
|
||||||
|
)
|
||||||
|
|
||||||
|
# E2E delivery guide (canonical uppercase path)
|
||||||
|
E2E_DELIVERY=""
|
||||||
|
for candidate in \
|
||||||
|
"$MOSAIC_HOME/guides/E2E-DELIVERY.md"; do
|
||||||
|
if [[ -f "$candidate" ]]; then
|
||||||
|
E2E_DELIVERY="$candidate"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# Runtime-specific reference
|
||||||
|
RUNTIME_FILE="$MOSAIC_HOME/runtime/claude/RUNTIME.md"
|
||||||
|
|
||||||
|
# Project-local AGENTS.md (cwd at session start)
|
||||||
|
PROJECT_AGENTS=""
|
||||||
|
if [[ -f "./AGENTS.md" ]]; then
|
||||||
|
PROJECT_AGENTS="./AGENTS.md"
|
||||||
|
fi
|
||||||
|
|
||||||
|
emit_file() {
|
||||||
|
local filepath="$1"
|
||||||
|
local label="${2:-$(basename "$filepath")}"
|
||||||
|
if [[ -f "$filepath" ]]; then
|
||||||
|
echo "=== MOSAIC: $label ==="
|
||||||
|
cat "$filepath"
|
||||||
|
echo ""
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "=== MOSAIC CONTEXT INJECTION (SessionStart) ==="
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
for f in "${MANDATORY_FILES[@]}"; do
|
||||||
|
emit_file "$f"
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ -n "$E2E_DELIVERY" ]]; then
|
||||||
|
emit_file "$E2E_DELIVERY" "E2E-DELIVERY.md"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -n "$PROJECT_AGENTS" ]]; then
|
||||||
|
emit_file "$PROJECT_AGENTS" "Project AGENTS.md ($(pwd))"
|
||||||
|
fi
|
||||||
|
|
||||||
|
emit_file "$RUNTIME_FILE" "Claude RUNTIME.md"
|
||||||
|
|
||||||
|
echo "=== END MOSAIC CONTEXT INJECTION ==="
|
||||||
65
tools/coolify/README.md
Normal file
65
tools/coolify/README.md
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
# Coolify Tool Suite
|
||||||
|
|
||||||
|
Manage Coolify container deployment platform (projects, services, deployments, environment variables).
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- `jq` and `curl` installed
|
||||||
|
- Coolify credentials in `~/src/jarvis-brain/credentials.json` (or `$MOSAIC_CREDENTIALS_FILE`)
|
||||||
|
- Required fields: `coolify.url`, `coolify.app_token`
|
||||||
|
|
||||||
|
## Scripts
|
||||||
|
|
||||||
|
| Script | Purpose |
|
||||||
|
|--------|---------|
|
||||||
|
| `team-list.sh` | List teams |
|
||||||
|
| `project-list.sh` | List projects |
|
||||||
|
| `service-list.sh` | List all services |
|
||||||
|
| `service-status.sh` | Get service details and status |
|
||||||
|
| `deploy.sh` | Trigger service deployment |
|
||||||
|
| `env-set.sh` | Set environment variable on a service |
|
||||||
|
|
||||||
|
## Common Options
|
||||||
|
|
||||||
|
- `-f json` — JSON output (default: table)
|
||||||
|
- `-u uuid` — Service UUID (for service-specific operations)
|
||||||
|
- `-h` — Show help
|
||||||
|
|
||||||
|
## API Reference
|
||||||
|
|
||||||
|
- Base URL: `http://10.1.1.44:8000`
|
||||||
|
- API prefix: `/api/v1/`
|
||||||
|
- Auth: Bearer token in `Authorization` header
|
||||||
|
- Rate limit: 200 requests per interval
|
||||||
|
|
||||||
|
## Known Limitations
|
||||||
|
|
||||||
|
- **FQDN updates on compose sub-apps not supported via API.** Workaround: update directly in Coolify's PostgreSQL DB (`coolify-db` container, `service_applications` table).
|
||||||
|
- **Compose must be base64-encoded** in `docker_compose_raw` field when creating services via API.
|
||||||
|
- **Don't send `type` with `docker_compose_raw`** — API rejects payloads with both fields.
|
||||||
|
|
||||||
|
## Coolify Magic Variables
|
||||||
|
|
||||||
|
Coolify reads special env vars from compose files:
|
||||||
|
- `SERVICE_FQDN_{NAME}_{PORT}` — assigns a domain to a compose service
|
||||||
|
- `SERVICE_URL_{NAME}_{PORT}` — internal URL reference
|
||||||
|
- Must use list-style env syntax (`- SERVICE_FQDN_API_3001`), NOT dict-style.
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# List all projects
|
||||||
|
~/.config/mosaic/tools/coolify/project-list.sh
|
||||||
|
|
||||||
|
# List services as JSON
|
||||||
|
~/.config/mosaic/tools/coolify/service-list.sh -f json
|
||||||
|
|
||||||
|
# Check service status
|
||||||
|
~/.config/mosaic/tools/coolify/service-status.sh -u <uuid>
|
||||||
|
|
||||||
|
# Set an env var
|
||||||
|
~/.config/mosaic/tools/coolify/env-set.sh -u <uuid> -k DATABASE_URL -v "postgres://..."
|
||||||
|
|
||||||
|
# Deploy a service
|
||||||
|
~/.config/mosaic/tools/coolify/deploy.sh -u <uuid>
|
||||||
|
```
|
||||||
61
tools/coolify/deploy.sh
Executable file
61
tools/coolify/deploy.sh
Executable file
@@ -0,0 +1,61 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
#
|
||||||
|
# deploy.sh — Trigger Coolify service deployment
|
||||||
|
#
|
||||||
|
# Usage: deploy.sh -u <uuid> [-f]
|
||||||
|
#
|
||||||
|
# Options:
|
||||||
|
# -u uuid Service UUID (required)
|
||||||
|
# -f Force restart (stop then start)
|
||||||
|
# -h Show this help
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
MOSAIC_HOME="${MOSAIC_HOME:-$HOME/.config/mosaic}"
|
||||||
|
source "$MOSAIC_HOME/tools/_lib/credentials.sh"
|
||||||
|
load_credentials coolify
|
||||||
|
|
||||||
|
UUID=""
|
||||||
|
FORCE=false
|
||||||
|
|
||||||
|
while getopts "u:fh" opt; do
|
||||||
|
case $opt in
|
||||||
|
u) UUID="$OPTARG" ;;
|
||||||
|
f) FORCE=true ;;
|
||||||
|
h) head -11 "$0" | grep "^#" | sed 's/^# \?//'; exit 0 ;;
|
||||||
|
*) echo "Usage: $0 -u <uuid> [-f]" >&2; exit 1 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ -z "$UUID" ]]; then
|
||||||
|
echo "Error: -u uuid is required" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$FORCE" == "true" ]]; then
|
||||||
|
echo "Stopping service $UUID..."
|
||||||
|
curl -s -o /dev/null -w "" \
|
||||||
|
-X POST \
|
||||||
|
-H "Authorization: Bearer $COOLIFY_TOKEN" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
"${COOLIFY_URL}/api/v1/services/${UUID}/stop"
|
||||||
|
sleep 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Starting service $UUID..."
|
||||||
|
response=$(curl -s -w "\n%{http_code}" \
|
||||||
|
-X POST \
|
||||||
|
-H "Authorization: Bearer $COOLIFY_TOKEN" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
"${COOLIFY_URL}/api/v1/services/${UUID}/start")
|
||||||
|
|
||||||
|
http_code=$(echo "$response" | tail -n1)
|
||||||
|
body=$(echo "$response" | sed '$d')
|
||||||
|
|
||||||
|
if [[ "$http_code" != "200" && "$http_code" != "201" && "$http_code" != "202" ]]; then
|
||||||
|
echo "Error: Deployment failed (HTTP $http_code)" >&2
|
||||||
|
echo "$body" | jq -r '.' 2>/dev/null >&2 || echo "$body" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Deployment triggered successfully for service $UUID"
|
||||||
|
echo "$body" | jq -r '.message // empty' 2>/dev/null || true
|
||||||
65
tools/coolify/env-set.sh
Executable file
65
tools/coolify/env-set.sh
Executable file
@@ -0,0 +1,65 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
#
|
||||||
|
# env-set.sh — Set environment variable on a Coolify service
|
||||||
|
#
|
||||||
|
# Usage: env-set.sh -u <uuid> -k <key> -v <value> [--preview]
|
||||||
|
#
|
||||||
|
# Options:
|
||||||
|
# -u uuid Service UUID (required)
|
||||||
|
# -k key Environment variable name (required)
|
||||||
|
# -v value Environment variable value (required)
|
||||||
|
# --preview Set as preview-only variable
|
||||||
|
# -h Show this help
|
||||||
|
#
|
||||||
|
# Note: Changes take effect on next deploy/restart.
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
MOSAIC_HOME="${MOSAIC_HOME:-$HOME/.config/mosaic}"
|
||||||
|
source "$MOSAIC_HOME/tools/_lib/credentials.sh"
|
||||||
|
load_credentials coolify
|
||||||
|
|
||||||
|
UUID=""
|
||||||
|
KEY=""
|
||||||
|
VALUE=""
|
||||||
|
IS_PREVIEW="false"
|
||||||
|
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case $1 in
|
||||||
|
-u) UUID="$2"; shift 2 ;;
|
||||||
|
-k) KEY="$2"; shift 2 ;;
|
||||||
|
-v) VALUE="$2"; shift 2 ;;
|
||||||
|
--preview) IS_PREVIEW="true"; shift ;;
|
||||||
|
-h) head -15 "$0" | grep "^#" | sed 's/^# \?//'; exit 0 ;;
|
||||||
|
*) echo "Usage: $0 -u <uuid> -k <key> -v <value> [--preview]" >&2; exit 1 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ -z "$UUID" || -z "$KEY" || -z "$VALUE" ]]; then
|
||||||
|
echo "Error: -u uuid, -k key, and -v value are required" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
payload=$(jq -n \
|
||||||
|
--arg key "$KEY" \
|
||||||
|
--arg value "$VALUE" \
|
||||||
|
--argjson preview "$IS_PREVIEW" \
|
||||||
|
'{key: $key, value: $value, is_preview: $preview}')
|
||||||
|
|
||||||
|
response=$(curl -s -w "\n%{http_code}" \
|
||||||
|
-X PATCH \
|
||||||
|
-H "Authorization: Bearer $COOLIFY_TOKEN" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "$payload" \
|
||||||
|
"${COOLIFY_URL}/api/v1/services/${UUID}/envs")
|
||||||
|
|
||||||
|
http_code=$(echo "$response" | tail -n1)
|
||||||
|
body=$(echo "$response" | sed '$d')
|
||||||
|
|
||||||
|
if [[ "$http_code" != "200" && "$http_code" != "201" ]]; then
|
||||||
|
echo "Error: Failed to set environment variable (HTTP $http_code)" >&2
|
||||||
|
echo "$body" | jq -r '.' 2>/dev/null >&2 || echo "$body" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Set $KEY on service $UUID"
|
||||||
|
echo "Note: Redeploy the service to apply the change"
|
||||||
52
tools/coolify/project-list.sh
Executable file
52
tools/coolify/project-list.sh
Executable file
@@ -0,0 +1,52 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
#
|
||||||
|
# project-list.sh — List Coolify projects
|
||||||
|
#
|
||||||
|
# Usage: project-list.sh [-f format]
|
||||||
|
#
|
||||||
|
# Options:
|
||||||
|
# -f format Output format: table (default), json
|
||||||
|
# -h Show this help
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
MOSAIC_HOME="${MOSAIC_HOME:-$HOME/.config/mosaic}"
|
||||||
|
source "$MOSAIC_HOME/tools/_lib/credentials.sh"
|
||||||
|
load_credentials coolify
|
||||||
|
|
||||||
|
FORMAT="table"
|
||||||
|
|
||||||
|
while getopts "f:h" opt; do
|
||||||
|
case $opt in
|
||||||
|
f) FORMAT="$OPTARG" ;;
|
||||||
|
h) head -10 "$0" | grep "^#" | sed 's/^# \?//'; exit 0 ;;
|
||||||
|
*) echo "Usage: $0 [-f format]" >&2; exit 1 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
response=$(curl -s -w "\n%{http_code}" \
|
||||||
|
-H "Authorization: Bearer $COOLIFY_TOKEN" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
"${COOLIFY_URL}/api/v1/projects")
|
||||||
|
|
||||||
|
http_code=$(echo "$response" | tail -n1)
|
||||||
|
body=$(echo "$response" | sed '$d')
|
||||||
|
|
||||||
|
if [[ "$http_code" != "200" ]]; then
|
||||||
|
echo "Error: Failed to list projects (HTTP $http_code)" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$FORMAT" == "json" ]]; then
|
||||||
|
echo "$body" | jq '.'
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "UUID NAME DESCRIPTION"
|
||||||
|
echo "------------------------------------ ---------------------------- ----------------------------------------"
|
||||||
|
echo "$body" | jq -r '.[] | [
|
||||||
|
.uuid,
|
||||||
|
.name,
|
||||||
|
(.description // "—")
|
||||||
|
] | @tsv' | while IFS=$'\t' read -r uuid name desc; do
|
||||||
|
printf "%-36s %-28s %s\n" "$uuid" "${name:0:28}" "${desc:0:40}"
|
||||||
|
done
|
||||||
53
tools/coolify/service-list.sh
Executable file
53
tools/coolify/service-list.sh
Executable file
@@ -0,0 +1,53 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
#
|
||||||
|
# service-list.sh — List Coolify services
|
||||||
|
#
|
||||||
|
# Usage: service-list.sh [-f format]
|
||||||
|
#
|
||||||
|
# Options:
|
||||||
|
# -f format Output format: table (default), json
|
||||||
|
# -h Show this help
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
MOSAIC_HOME="${MOSAIC_HOME:-$HOME/.config/mosaic}"
|
||||||
|
source "$MOSAIC_HOME/tools/_lib/credentials.sh"
|
||||||
|
load_credentials coolify
|
||||||
|
|
||||||
|
FORMAT="table"
|
||||||
|
|
||||||
|
while getopts "f:h" opt; do
|
||||||
|
case $opt in
|
||||||
|
f) FORMAT="$OPTARG" ;;
|
||||||
|
h) head -10 "$0" | grep "^#" | sed 's/^# \?//'; exit 0 ;;
|
||||||
|
*) echo "Usage: $0 [-f format]" >&2; exit 1 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
response=$(curl -s -w "\n%{http_code}" \
|
||||||
|
-H "Authorization: Bearer $COOLIFY_TOKEN" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
"${COOLIFY_URL}/api/v1/services")
|
||||||
|
|
||||||
|
http_code=$(echo "$response" | tail -n1)
|
||||||
|
body=$(echo "$response" | sed '$d')
|
||||||
|
|
||||||
|
if [[ "$http_code" != "200" ]]; then
|
||||||
|
echo "Error: Failed to list services (HTTP $http_code)" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$FORMAT" == "json" ]]; then
|
||||||
|
echo "$body" | jq '.'
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "UUID NAME TYPE STATUS"
|
||||||
|
echo "------------------------------------ ---------------------------- ------------ ----------"
|
||||||
|
echo "$body" | jq -r '.[] | [
|
||||||
|
.uuid,
|
||||||
|
.name,
|
||||||
|
(.type // "unknown"),
|
||||||
|
(.status // "unknown")
|
||||||
|
] | @tsv' | while IFS=$'\t' read -r uuid name type status; do
|
||||||
|
printf "%-36s %-28s %-12s %s\n" "$uuid" "${name:0:28}" "${type:0:12}" "$status"
|
||||||
|
done
|
||||||
62
tools/coolify/service-status.sh
Executable file
62
tools/coolify/service-status.sh
Executable file
@@ -0,0 +1,62 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
#
|
||||||
|
# service-status.sh — Get Coolify service status and details
|
||||||
|
#
|
||||||
|
# Usage: service-status.sh -u <uuid> [-f format]
|
||||||
|
#
|
||||||
|
# Options:
|
||||||
|
# -u uuid Service UUID (required)
|
||||||
|
# -f format Output format: table (default), json
|
||||||
|
# -h Show this help
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
MOSAIC_HOME="${MOSAIC_HOME:-$HOME/.config/mosaic}"
|
||||||
|
source "$MOSAIC_HOME/tools/_lib/credentials.sh"
|
||||||
|
load_credentials coolify
|
||||||
|
|
||||||
|
UUID=""
|
||||||
|
FORMAT="table"
|
||||||
|
|
||||||
|
while getopts "u:f:h" opt; do
|
||||||
|
case $opt in
|
||||||
|
u) UUID="$OPTARG" ;;
|
||||||
|
f) FORMAT="$OPTARG" ;;
|
||||||
|
h) head -12 "$0" | grep "^#" | sed 's/^# \?//'; exit 0 ;;
|
||||||
|
*) echo "Usage: $0 -u <uuid> [-f format]" >&2; exit 1 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ -z "$UUID" ]]; then
|
||||||
|
echo "Error: -u uuid is required" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
response=$(curl -s -w "\n%{http_code}" \
|
||||||
|
-H "Authorization: Bearer $COOLIFY_TOKEN" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
"${COOLIFY_URL}/api/v1/services/${UUID}")
|
||||||
|
|
||||||
|
http_code=$(echo "$response" | tail -n1)
|
||||||
|
body=$(echo "$response" | sed '$d')
|
||||||
|
|
||||||
|
if [[ "$http_code" != "200" ]]; then
|
||||||
|
echo "Error: Failed to get service status (HTTP $http_code)" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$FORMAT" == "json" ]]; then
|
||||||
|
echo "$body" | jq '.'
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Service Details"
|
||||||
|
echo "==============="
|
||||||
|
echo "$body" | jq -r '
|
||||||
|
" UUID: \(.uuid)\n" +
|
||||||
|
" Name: \(.name)\n" +
|
||||||
|
" Type: \(.type // "unknown")\n" +
|
||||||
|
" Status: \(.status // "unknown")\n" +
|
||||||
|
" FQDN: \(.fqdn // "none")\n" +
|
||||||
|
" Created: \(.created_at // "unknown")\n" +
|
||||||
|
" Updated: \(.updated_at // "unknown")"
|
||||||
|
'
|
||||||
52
tools/coolify/team-list.sh
Executable file
52
tools/coolify/team-list.sh
Executable file
@@ -0,0 +1,52 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
#
|
||||||
|
# team-list.sh — List Coolify teams
|
||||||
|
#
|
||||||
|
# Usage: team-list.sh [-f format]
|
||||||
|
#
|
||||||
|
# Options:
|
||||||
|
# -f format Output format: table (default), json
|
||||||
|
# -h Show this help
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
MOSAIC_HOME="${MOSAIC_HOME:-$HOME/.config/mosaic}"
|
||||||
|
source "$MOSAIC_HOME/tools/_lib/credentials.sh"
|
||||||
|
load_credentials coolify
|
||||||
|
|
||||||
|
FORMAT="table"
|
||||||
|
|
||||||
|
while getopts "f:h" opt; do
|
||||||
|
case $opt in
|
||||||
|
f) FORMAT="$OPTARG" ;;
|
||||||
|
h) head -10 "$0" | grep "^#" | sed 's/^# \?//'; exit 0 ;;
|
||||||
|
*) echo "Usage: $0 [-f format]" >&2; exit 1 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
response=$(curl -s -w "\n%{http_code}" \
|
||||||
|
-H "Authorization: Bearer $COOLIFY_TOKEN" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
"${COOLIFY_URL}/api/v1/teams")
|
||||||
|
|
||||||
|
http_code=$(echo "$response" | tail -n1)
|
||||||
|
body=$(echo "$response" | sed '$d')
|
||||||
|
|
||||||
|
if [[ "$http_code" != "200" ]]; then
|
||||||
|
echo "Error: Failed to list teams (HTTP $http_code)" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$FORMAT" == "json" ]]; then
|
||||||
|
echo "$body" | jq '.'
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "ID NAME DESCRIPTION"
|
||||||
|
echo "---- ---------------------------- ----------------------------------------"
|
||||||
|
echo "$body" | jq -r '.[] | [
|
||||||
|
(.id | tostring),
|
||||||
|
.name,
|
||||||
|
(.description // "—")
|
||||||
|
] | @tsv' | while IFS=$'\t' read -r id name desc; do
|
||||||
|
printf "%-4s %-28s %s\n" "$id" "${name:0:28}" "${desc:0:40}"
|
||||||
|
done
|
||||||
1
tools/excalidraw/.gitignore
vendored
Normal file
1
tools/excalidraw/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
node_modules/
|
||||||
5
tools/excalidraw/launch.sh
Executable file
5
tools/excalidraw/launch.sh
Executable 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"
|
||||||
76
tools/excalidraw/loader.mjs
Normal file
76
tools/excalidraw/loader.mjs
Normal 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);
|
||||||
|
}
|
||||||
11
tools/excalidraw/package.json
Normal file
11
tools/excalidraw/package.json
Normal 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
323
tools/excalidraw/server.mjs
Normal 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);
|
||||||
7
tools/excalidraw/stubs/laser-pointer.mjs
Normal file
7
tools/excalidraw/stubs/laser-pointer.mjs
Normal 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 };
|
||||||
@@ -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
149
tools/git/detect-platform.sh
Executable file
@@ -0,0 +1,149 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# detect-platform.sh - Detect git platform (Gitea or GitHub) for current repo
|
||||||
|
# Usage: source detect-platform.sh && detect_platform
|
||||||
|
# or: ./detect-platform.sh (prints platform name)
|
||||||
|
|
||||||
|
detect_platform() {
|
||||||
|
local remote_url
|
||||||
|
remote_url=$(git remote get-url origin 2>/dev/null)
|
||||||
|
|
||||||
|
if [[ -z "$remote_url" ]]; then
|
||||||
|
echo "error: not a git repository or no origin remote" >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check for GitHub
|
||||||
|
if [[ "$remote_url" == *"github.com"* ]]; then
|
||||||
|
PLATFORM="github"
|
||||||
|
export PLATFORM
|
||||||
|
echo "github"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check for common Gitea indicators
|
||||||
|
# Gitea URLs typically don't contain github.com, gitlab.com, bitbucket.org
|
||||||
|
if [[ "$remote_url" != *"gitlab.com"* ]] && \
|
||||||
|
[[ "$remote_url" != *"bitbucket.org"* ]]; then
|
||||||
|
# Assume Gitea for self-hosted repos
|
||||||
|
PLATFORM="gitea"
|
||||||
|
export PLATFORM
|
||||||
|
echo "gitea"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
PLATFORM="unknown"
|
||||||
|
export PLATFORM
|
||||||
|
echo "unknown"
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
get_repo_info() {
|
||||||
|
local remote_url
|
||||||
|
remote_url=$(git remote get-url origin 2>/dev/null)
|
||||||
|
|
||||||
|
if [[ -z "$remote_url" ]]; then
|
||||||
|
echo "error: not a git repository or no origin remote" >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Extract owner/repo from URL
|
||||||
|
# Handles: git@host:owner/repo.git, https://host/owner/repo.git, https://host/owner/repo
|
||||||
|
local repo_path
|
||||||
|
if [[ "$remote_url" == git@* ]]; then
|
||||||
|
repo_path="${remote_url#*:}"
|
||||||
|
else
|
||||||
|
repo_path="${remote_url#*://}"
|
||||||
|
repo_path="${repo_path#*/}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Remove .git suffix if present
|
||||||
|
repo_path="${repo_path%.git}"
|
||||||
|
|
||||||
|
echo "$repo_path"
|
||||||
|
}
|
||||||
|
|
||||||
|
get_repo_owner() {
|
||||||
|
local repo_info
|
||||||
|
repo_info=$(get_repo_info)
|
||||||
|
echo "${repo_info%%/*}"
|
||||||
|
}
|
||||||
|
|
||||||
|
get_repo_name() {
|
||||||
|
local repo_info
|
||||||
|
repo_info=$(get_repo_info)
|
||||||
|
echo "${repo_info##*/}"
|
||||||
|
}
|
||||||
|
|
||||||
|
get_remote_host() {
|
||||||
|
local remote_url
|
||||||
|
remote_url=$(git remote get-url origin 2>/dev/null || true)
|
||||||
|
if [[ -z "$remote_url" ]]; then
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
if [[ "$remote_url" =~ ^https?://([^/]+)/ ]]; then
|
||||||
|
echo "${BASH_REMATCH[1]}"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
if [[ "$remote_url" =~ ^git@([^:]+): ]]; then
|
||||||
|
echo "${BASH_REMATCH[1]}"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Resolve a Gitea API token for the given host.
|
||||||
|
# Priority: Mosaic credential loader → GITEA_TOKEN env → ~/.git-credentials
|
||||||
|
get_gitea_token() {
|
||||||
|
local host="$1"
|
||||||
|
local script_dir
|
||||||
|
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
local cred_loader="$script_dir/../_lib/credentials.sh"
|
||||||
|
|
||||||
|
# 1. Mosaic credential loader (host → service mapping, run in subshell to avoid polluting env)
|
||||||
|
if [[ -f "$cred_loader" ]]; then
|
||||||
|
local token
|
||||||
|
token=$(
|
||||||
|
source "$cred_loader"
|
||||||
|
case "$host" in
|
||||||
|
git.mosaicstack.dev) load_credentials gitea-mosaicstack 2>/dev/null ;;
|
||||||
|
git.uscllc.com) load_credentials gitea-usc 2>/dev/null ;;
|
||||||
|
*)
|
||||||
|
for svc in gitea-mosaicstack gitea-usc; do
|
||||||
|
load_credentials "$svc" 2>/dev/null || continue
|
||||||
|
[[ "${GITEA_URL:-}" == *"$host"* ]] && break
|
||||||
|
unset GITEA_TOKEN GITEA_URL
|
||||||
|
done
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
echo "${GITEA_TOKEN:-}"
|
||||||
|
)
|
||||||
|
if [[ -n "$token" ]]; then
|
||||||
|
echo "$token"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 2. GITEA_TOKEN env var (may be set by caller)
|
||||||
|
if [[ -n "${GITEA_TOKEN:-}" ]]; then
|
||||||
|
echo "$GITEA_TOKEN"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 3. ~/.git-credentials file
|
||||||
|
local creds="$HOME/.git-credentials"
|
||||||
|
if [[ -f "$creds" ]]; then
|
||||||
|
local token
|
||||||
|
token=$(grep -F "$host" "$creds" 2>/dev/null | sed -n 's#https\?://[^@]*:\([^@/]*\)@.*#\1#p' | head -n 1)
|
||||||
|
if [[ -n "$token" ]]; then
|
||||||
|
echo "$token"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# If script is run directly (not sourced), output the platform
|
||||||
|
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
|
||||||
|
detect_platform
|
||||||
|
fi
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user