Compare commits
6 Commits
feat/tools
...
feb0d8a58b
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
feb0d8a58b | ||
|
|
9b7e63f6c3 | ||
|
|
b23a7e81ae | ||
| 87f561c1f8 | |||
| 8c45857859 | |||
| 605221d42f |
3
.gitignore
vendored
3
.gitignore
vendored
@@ -12,3 +12,6 @@ docs/reports/
|
|||||||
|
|
||||||
# Step-CA dev password — real file is gitignored; commit only the .example
|
# Step-CA dev password — real file is gitignored; commit only the .example
|
||||||
infra/step-ca/dev-password
|
infra/step-ca/dev-password
|
||||||
|
|
||||||
|
# Scratch dirs created by the framework git-wrapper shell test harnesses
|
||||||
|
.mosaic-test-work/
|
||||||
|
|||||||
87
docs/scratchpads/559-560-wrapper-eval-login-20260620.md
Normal file
87
docs/scratchpads/559-560-wrapper-eval-login-20260620.md
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
# Wrapper hardening fold-in: #559 (eval removal) + #560 (host-derived login)
|
||||||
|
|
||||||
|
**Branch:** `fix/wrapper-hardening-tls-credpath-cicwait` (PR #551)
|
||||||
|
**Worker:** coderlite0 (Sonnet lane) · coordinated by mos-claude
|
||||||
|
**Date:** 2026-06-20
|
||||||
|
**Scope:** `packages/mosaic/framework/tools/git/*.sh` only
|
||||||
|
|
||||||
|
## What the issues asked for vs. what was already landed
|
||||||
|
|
||||||
|
Both issues were largely satisfied by prior merged work; this fold-in closes the
|
||||||
|
remaining gaps (regression tests + a loud diagnostic + one residual word-split site)
|
||||||
|
rather than re-implementing finished functionality.
|
||||||
|
|
||||||
|
### #559 — remove `eval` from issue-create.sh (and siblings)
|
||||||
|
|
||||||
|
- `eval`-based command construction was already removed across the wrapper surface
|
||||||
|
(landed in #549). A full scan of `tools/git/*.sh` finds **zero** `eval` usages.
|
||||||
|
- `issue-create.sh`, `pr-create.sh`, `issue-edit.sh`, `issue-assign.sh` already build
|
||||||
|
their `tea`/`gh` invocations as argv arrays (`CMD=(...)`, `"${CMD[@]}"`), so Markdown
|
||||||
|
bodies pass through verbatim.
|
||||||
|
- **Residual found & fixed:** `issue-comment.sh` still used unquoted
|
||||||
|
`$(get_gitea_repo_args)` word-splitting (the comment body itself was already safely
|
||||||
|
quoted, so no injection bug — but it was the inconsistent, fragile pattern #559 targets,
|
||||||
|
and it failed silently when no login resolved). Converted to an argv array with an
|
||||||
|
explicit, loud login-resolution error.
|
||||||
|
- **Added regression test:** `test-issue-create-body-safety.sh` — feeds a hostile
|
||||||
|
Markdown body (`$(touch SENTINEL)`, backticks, single/double quotes, `$HOME`/`${PATH}`,
|
||||||
|
pipes/`&&`/`;`) through `issue-create.sh` and asserts (1) no command substitution
|
||||||
|
executes (sentinel file never created) and (2) the `--description` `tea` receives is
|
||||||
|
byte-for-byte the original body.
|
||||||
|
|
||||||
|
### #560 — auto-detect Gitea `--login` from repo origin host
|
||||||
|
|
||||||
|
- Centralized host→login resolution already exists in `detect-platform.sh`
|
||||||
|
(`get_gitea_login_for_host` → `find_tea_login_for_host`, matching `urlparse(url).hostname`).
|
||||||
|
Every wrapper routes through it (or `get_gitea_login` / `get_gitea_login_for_repo_override`);
|
||||||
|
**no wrapper hardcodes `${GITEA_LOGIN:-mosaicstack}`**. Explicit `GITEA_LOGIN` wins only
|
||||||
|
when it matches the host (`tea_login_matches_host`), so stale overrides are rejected.
|
||||||
|
- **Gap fixed — silent failure → loud diagnostic:** the failure path of
|
||||||
|
`get_gitea_login_for_host` returned non-zero with no message. Added
|
||||||
|
`print_gitea_login_diagnostic`, emitted to **stderr** on resolution failure: names the
|
||||||
|
unresolved host, lists available tea logins (name + host), and gives the `GITEA_LOGIN`
|
||||||
|
override + `tea login add` fix. Stderr-only, so it never contaminates stdout (the
|
||||||
|
resolved login name) or the log-grep assertions in the existing harnesses. Callers with
|
||||||
|
an API fallback (pr-merge, issue-close, pr-create, issue-create) still follow with their
|
||||||
|
own "using API fallback" line, giving a clear "no login → fallback" trail.
|
||||||
|
- **Extended test:** `test-gitea-login-resolution.sh` now also asserts (a) the loud
|
||||||
|
diagnostic fires and lists available logins for an unresolved host, (b) login is derived
|
||||||
|
from origin host for **both** instances (mosaicstack + usc) via a scoped second `tea`
|
||||||
|
mock, and (c) a valid `GITEA_LOGIN` override is honored. The scoped mock keeps the
|
||||||
|
existing API-fallback assertions (which require mosaicstack to have _no_ tea login) valid.
|
||||||
|
|
||||||
|
## Files changed (wrapper surface only)
|
||||||
|
|
||||||
|
- `detect-platform.sh` — add `print_gitea_login_diagnostic`; call it on the
|
||||||
|
`get_gitea_login_for_host` failure path.
|
||||||
|
- `issue-comment.sh` — argv array + loud login-resolution error (was unquoted
|
||||||
|
`$(get_gitea_repo_args)`).
|
||||||
|
- `test-issue-create-body-safety.sh` — **new** (#559 regression).
|
||||||
|
- `test-gitea-login-resolution.sh` — extended (#560 diagnostic + both-host + override).
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
All wrapper harnesses pass locally:
|
||||||
|
|
||||||
|
- `test-issue-create-body-safety.sh` — PASS
|
||||||
|
- `test-gitea-login-resolution.sh` — PASS
|
||||||
|
- `test-pr-merge-gitea-empty-uid.sh` — PASS
|
||||||
|
- `test-pr-metadata-gitea.sh` — PASS
|
||||||
|
- `test-lane-brief-pr-linkage.sh` — PASS
|
||||||
|
|
||||||
|
## Open items flagged to mos-claude (orchestrator decisions)
|
||||||
|
|
||||||
|
1. **CHANGELOG absent.** The task said "update CHANGELOG (append-only), keep the existing
|
||||||
|
#550/#551 entry." No CHANGELOG file exists anywhere in the repo, and #550/#551 are not
|
||||||
|
recorded in one. **ASSUMPTION:** documenting #559/#560 in this scratchpad + the PR
|
||||||
|
description (`Closes #559 Closes #560`) follows the repo's actual convention
|
||||||
|
(`docs/scratchpads/`). Did not invent a new CHANGELOG structure.
|
||||||
|
2. **`docs/TASKS.md` is orchestrator single-writer.** It carries a "Workers read but never
|
||||||
|
modify" banner. As a worker I did **not** edit it; task tracking is via the linked Gitea
|
||||||
|
issues #559/#560 + this scratchpad. Orchestrator may add a rollup row if desired.
|
||||||
|
3. **Wrapper `test-*.sh` are not CI-wired.** `.woodpecker/ci.yml` runs `pnpm
|
||||||
|
typecheck/lint/format:check/test` (`turbo run test`); the framework dir has no
|
||||||
|
`package.json`, so these shell harnesses run **locally/manually only** — they do not gate
|
||||||
|
the PR in Woodpecker. **ASSUMPTION:** out of scope to wire a shell-test step into CI in
|
||||||
|
this PR (would broaden the diff beyond the wrapper surface). Flagging for a follow-up if
|
||||||
|
the fleet wants these gated.
|
||||||
@@ -29,7 +29,21 @@ Pi supports `--models` for Ctrl+P model cycling during a session. Use cheaper mo
|
|||||||
|
|
||||||
### Skills
|
### Skills
|
||||||
|
|
||||||
Mosaic skills are loaded natively via Pi's `--skill` flag. Skills are discovered from:
|
By default the launcher starts Pi with `--no-skills` to keep startup context small, then
|
||||||
|
force-loads a small set of fleet-critical skills via explicit `--skill` flags (an explicit
|
||||||
|
`--skill` overrides `--no-skills` for that path). The default forced set is `mosaic-tools`
|
||||||
|
(the must-use `~/.config/mosaic/tools/` cheatsheet: inter-agent messaging + git wrappers).
|
||||||
|
|
||||||
|
Tune skill loading with environment variables:
|
||||||
|
|
||||||
|
- `MOSAIC_PI_FORCE_SKILLS` — colon-separated skill dir names to force-load (default: `mosaic-tools`;
|
||||||
|
set to an empty string to disable force-loading). Missing skills are skipped silently.
|
||||||
|
- `MOSAIC_PI_SKILL_MODE=all` — link every skill found in `~/.config/mosaic/{skills,skills-local}/`
|
||||||
|
(full catalog; larger context).
|
||||||
|
- `MOSAIC_PI_SKILL_MODE=discover` — let Pi discover skills natively (no `--no-skills`), still
|
||||||
|
force-loading the fleet set on top.
|
||||||
|
|
||||||
|
Skills are discovered from:
|
||||||
|
|
||||||
- `~/.config/mosaic/skills/` (Mosaic global skills)
|
- `~/.config/mosaic/skills/` (Mosaic global skills)
|
||||||
- `~/.pi/agent/skills/` (Pi global skills)
|
- `~/.pi/agent/skills/` (Pi global skills)
|
||||||
|
|||||||
@@ -9,8 +9,8 @@
|
|||||||
2. Do NOT ask for routine confirmation before required push/merge/issue-close/release/tag actions.
|
2. Do NOT ask for routine confirmation before required push/merge/issue-close/release/tag actions.
|
||||||
3. Completion is forbidden at PR-open stage.
|
3. Completion is forbidden at PR-open stage.
|
||||||
4. Completion requires merged PR to `main` + terminal green CI + linked issue/internal task closed.
|
4. Completion requires merged PR to `main` + terminal green CI + linked issue/internal task closed.
|
||||||
5. Before push or merge, run queue guard: `~/.config/mosaic/rails/git/ci-queue-wait.sh --purpose push|merge -B main`.
|
5. Before push or merge, run queue guard: `~/.config/mosaic/tools/git/ci-queue-wait.sh --purpose push|merge -B main`.
|
||||||
6. For issue/PR/milestone operations, use Mosaic wrappers first (`~/.config/mosaic/rails/git/*.sh`).
|
6. For issue/PR/milestone operations, use Mosaic wrappers first (`~/.config/mosaic/tools/git/*.sh`).
|
||||||
7. If any required wrapper command fails: report `blocked` with the exact failed wrapper command and stop.
|
7. If any required wrapper command fails: report `blocked` with the exact failed wrapper command and stop.
|
||||||
8. Do NOT stop at "PR created" and do NOT ask "should I merge?" for routine flow.
|
8. Do NOT stop at "PR created" and do NOT ask "should I merge?" for routine flow.
|
||||||
|
|
||||||
@@ -58,7 +58,7 @@ ${QUALITY_GATES}
|
|||||||
2. If external git provider is available (Gitea/GitHub/GitLab), create/update issue(s) before coding and map them in `docs/TASKS.md`.
|
2. If external git provider is available (Gitea/GitHub/GitLab), create/update issue(s) before coding and map them in `docs/TASKS.md`.
|
||||||
3. If no external provider is available, use internal refs in `docs/TASKS.md` (example: `TASKS:T1`).
|
3. If no external provider is available, use internal refs in `docs/TASKS.md` (example: `TASKS:T1`).
|
||||||
4. Keep `docs/TASKS.md` status in sync with actual progress until completion.
|
4. Keep `docs/TASKS.md` status in sync with actual progress until completion.
|
||||||
5. For issue/PR/milestone actions, detect platform and use `~/.config/mosaic/rails/git/*.sh` wrappers first (no raw `gh`/`tea`/`glab` as first choice).
|
5. For issue/PR/milestone actions, detect platform and use `~/.config/mosaic/tools/git/*.sh` wrappers first (no raw `gh`/`tea`/`glab` as first choice).
|
||||||
6. If wrapper-driven merge/CI/issue-closure fails, report blocker with the exact failed wrapper command and stop (do not claim completion).
|
6. If wrapper-driven merge/CI/issue-closure fails, report blocker with the exact failed wrapper command and stop (do not claim completion).
|
||||||
|
|
||||||
## Documentation Contract
|
## Documentation Contract
|
||||||
@@ -88,7 +88,7 @@ Reference:
|
|||||||
5. Do not mark implementation complete until PR is merged.
|
5. Do not mark implementation complete until PR is merged.
|
||||||
6. Do not mark implementation complete until CI/pipeline status is terminal green.
|
6. Do not mark implementation complete until CI/pipeline status is terminal green.
|
||||||
7. Close linked issues/tasks only after merge + green CI.
|
7. Close linked issues/tasks only after merge + green CI.
|
||||||
8. Before push or merge, run CI queue guard: `~/.config/mosaic/rails/git/ci-queue-wait.sh --purpose push|merge -B main`.
|
8. Before push or merge, run CI queue guard: `~/.config/mosaic/tools/git/ci-queue-wait.sh --purpose push|merge -B main`.
|
||||||
|
|
||||||
## Container Release Strategy (When Applicable)
|
## Container Release Strategy (When Applicable)
|
||||||
|
|
||||||
@@ -138,8 +138,8 @@ When completing an orchestrated task:
|
|||||||
### Post-Coding Review
|
### Post-Coding Review
|
||||||
After implementing changes, code review is REQUIRED for any source-code modification.
|
After implementing changes, code review is REQUIRED for any source-code modification.
|
||||||
For orchestrated tasks, the orchestrator will run:
|
For orchestrated tasks, the orchestrator will run:
|
||||||
1. **Codex code review** — `~/.config/mosaic/rails/codex/codex-code-review.sh --uncommitted`
|
1. **Codex code review** — `~/.config/mosaic/tools/codex/codex-code-review.sh --uncommitted`
|
||||||
2. **Codex security review** — `~/.config/mosaic/rails/codex/codex-security-review.sh --uncommitted`
|
2. **Codex security review** — `~/.config/mosaic/tools/codex/codex-security-review.sh --uncommitted`
|
||||||
3. If blockers/critical findings: remediation task created
|
3. If blockers/critical findings: remediation task created
|
||||||
4. If clean: task marked done
|
4. If clean: task marked done
|
||||||
|
|
||||||
|
|||||||
@@ -135,7 +135,7 @@ ${QUALITY_GATES}
|
|||||||
## Issue Tracking
|
## Issue Tracking
|
||||||
|
|
||||||
Use external git provider issues when available. If no external provider exists, `docs/TASKS.md` is the canonical tracker for tasks, milestones, and issue-equivalent work.
|
Use external git provider issues when available. If no external provider exists, `docs/TASKS.md` is the canonical tracker for tasks, milestones, and issue-equivalent work.
|
||||||
For issue/PR/milestone operations, detect platform and use `~/.config/mosaic/rails/git/*.sh` wrappers first; do not use raw `gh`/`tea`/`glab` as first choice.
|
For issue/PR/milestone operations, detect platform and use `~/.config/mosaic/tools/git/*.sh` wrappers first; do not use raw `gh`/`tea`/`glab` as first choice.
|
||||||
If wrapper-driven merge/CI/issue-closure fails, report blocker with exact failed wrapper command and stop.
|
If wrapper-driven merge/CI/issue-closure fails, report blocker with exact failed wrapper command and stop.
|
||||||
Do NOT stop at "PR created" and do NOT ask "should I merge?" or "should I close the issue?" for routine delivery flow.
|
Do NOT stop at "PR created" and do NOT ask "should I merge?" or "should I close the issue?" for routine delivery flow.
|
||||||
|
|
||||||
@@ -147,9 +147,9 @@ Do NOT stop at "PR created" and do NOT ask "should I merge?" or "should I close
|
|||||||
5. Ensure `docs/PRD.md` or `docs/PRD.json` exists and is current before coding.
|
5. Ensure `docs/PRD.md` or `docs/PRD.json` exists and is current before coding.
|
||||||
6. Create scratchpad: `docs/scratchpads/{task-id}-{short-name}.md` and include issue/internal ref.
|
6. Create scratchpad: `docs/scratchpads/{task-id}-{short-name}.md` and include issue/internal ref.
|
||||||
7. Update `docs/TASKS.md` status + issue/internal ref before coding.
|
7. Update `docs/TASKS.md` status + issue/internal ref before coding.
|
||||||
8. Before push, run CI queue guard: `~/.config/mosaic/rails/git/ci-queue-wait.sh --purpose push -B main`.
|
8. Before push, run CI queue guard: `~/.config/mosaic/tools/git/ci-queue-wait.sh --purpose push -B main`.
|
||||||
9. Open PR to `main` for delivery changes (no direct push to `main`).
|
9. Open PR to `main` for delivery changes (no direct push to `main`).
|
||||||
10. Before merge, run CI queue guard: `~/.config/mosaic/rails/git/ci-queue-wait.sh --purpose merge -B main`.
|
10. Before merge, run CI queue guard: `~/.config/mosaic/tools/git/ci-queue-wait.sh --purpose merge -B main`.
|
||||||
11. Merge PRs that pass required checks and review gates with squash strategy only.
|
11. Merge PRs that pass required checks and review gates with squash strategy only.
|
||||||
12. Reference issues/internal refs in commits (`Fixes #123`, `Refs #123`, or `Refs TASKS:T1`).
|
12. Reference issues/internal refs in commits (`Fixes #123`, `Refs #123`, or `Refs TASKS:T1`).
|
||||||
13. Close issue/internal task only after testing and documentation gates pass, PR merge is complete, and CI/pipeline status is terminal green.
|
13. Close issue/internal task only after testing and documentation gates pass, PR merge is complete, and CI/pipeline status is terminal green.
|
||||||
@@ -176,10 +176,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.
|
||||||
|
|||||||
@@ -9,8 +9,8 @@
|
|||||||
2. Do NOT ask for routine confirmation before required push/merge/issue-close/release/tag actions.
|
2. Do NOT ask for routine confirmation before required push/merge/issue-close/release/tag actions.
|
||||||
3. Completion is forbidden at PR-open stage.
|
3. Completion is forbidden at PR-open stage.
|
||||||
4. Completion requires merged PR to `main` + terminal green CI + linked issue/internal task closed.
|
4. Completion requires merged PR to `main` + terminal green CI + linked issue/internal task closed.
|
||||||
5. Before push or merge, run queue guard: `~/.config/mosaic/rails/git/ci-queue-wait.sh --purpose push|merge -B main`.
|
5. Before push or merge, run queue guard: `~/.config/mosaic/tools/git/ci-queue-wait.sh --purpose push|merge -B main`.
|
||||||
6. For issue/PR/milestone operations, use Mosaic wrappers first (`~/.config/mosaic/rails/git/*.sh`).
|
6. For issue/PR/milestone operations, use Mosaic wrappers first (`~/.config/mosaic/tools/git/*.sh`).
|
||||||
7. If any required wrapper command fails: report `blocked` with the exact failed wrapper command and stop.
|
7. If any required wrapper command fails: report `blocked` with the exact failed wrapper command and stop.
|
||||||
8. Do NOT stop at "PR created" and do NOT ask "should I merge?" for routine flow.
|
8. Do NOT stop at "PR created" and do NOT ask "should I merge?" for routine flow.
|
||||||
|
|
||||||
@@ -68,7 +68,7 @@ ruff check . && mypy . && pytest tests/
|
|||||||
2. If external git provider is available (Gitea/GitHub/GitLab), create/update issue(s) before coding and map them in `docs/TASKS.md`.
|
2. If external git provider is available (Gitea/GitHub/GitLab), create/update issue(s) before coding and map them in `docs/TASKS.md`.
|
||||||
3. If no external provider is available, use internal refs in `docs/TASKS.md` (example: `TASKS:T1`).
|
3. If no external provider is available, use internal refs in `docs/TASKS.md` (example: `TASKS:T1`).
|
||||||
4. Keep `docs/TASKS.md` status in sync with actual progress until completion.
|
4. Keep `docs/TASKS.md` status in sync with actual progress until completion.
|
||||||
5. For issue/PR/milestone actions, detect platform and use `~/.config/mosaic/rails/git/*.sh` wrappers first (no raw `gh`/`tea`/`glab` as first choice).
|
5. For issue/PR/milestone actions, detect platform and use `~/.config/mosaic/tools/git/*.sh` wrappers first (no raw `gh`/`tea`/`glab` as first choice).
|
||||||
6. If wrapper-driven merge/CI/issue-closure fails, report blocker with the exact failed wrapper command and stop (do not claim completion).
|
6. If wrapper-driven merge/CI/issue-closure fails, report blocker with the exact failed wrapper command and stop (do not claim completion).
|
||||||
|
|
||||||
## Documentation Contract
|
## Documentation Contract
|
||||||
@@ -97,7 +97,7 @@ Reference:
|
|||||||
5. Do not mark implementation complete until PR is merged.
|
5. Do not mark implementation complete until PR is merged.
|
||||||
6. Do not mark implementation complete until CI/pipeline status is terminal green.
|
6. Do not mark implementation complete until CI/pipeline status is terminal green.
|
||||||
7. Close linked issues/tasks only after merge + green CI.
|
7. Close linked issues/tasks only after merge + green CI.
|
||||||
8. Before push or merge, run CI queue guard: `~/.config/mosaic/rails/git/ci-queue-wait.sh --purpose push|merge -B main`.
|
8. Before push or merge, run CI queue guard: `~/.config/mosaic/tools/git/ci-queue-wait.sh --purpose push|merge -B main`.
|
||||||
|
|
||||||
|
|
||||||
## Container Release Strategy (When Applicable)
|
## Container Release Strategy (When Applicable)
|
||||||
@@ -139,8 +139,8 @@ Use `${TASK_PREFIX}` for orchestrated tasks (e.g., `${TASK_PREFIX}-SEC-001`).
|
|||||||
### Post-Coding Review
|
### Post-Coding Review
|
||||||
After implementing changes, code review is REQUIRED for any source-code modification.
|
After implementing changes, code review is REQUIRED for any source-code modification.
|
||||||
For orchestrated tasks, the orchestrator will run:
|
For orchestrated tasks, the orchestrator will run:
|
||||||
1. **Codex code review** — `~/.config/mosaic/rails/codex/codex-code-review.sh --uncommitted`
|
1. **Codex code review** — `~/.config/mosaic/tools/codex/codex-code-review.sh --uncommitted`
|
||||||
2. **Codex security review** — `~/.config/mosaic/rails/codex/codex-security-review.sh --uncommitted`
|
2. **Codex security review** — `~/.config/mosaic/tools/codex/codex-security-review.sh --uncommitted`
|
||||||
3. If blockers/critical findings: remediation task created
|
3. If blockers/critical findings: remediation task created
|
||||||
4. If clean: task marked done
|
4. If clean: task marked done
|
||||||
|
|
||||||
|
|||||||
@@ -159,10 +159,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
|
||||||
```
|
```
|
||||||
|
|
||||||
See `~/.config/mosaic/guides/CODE-REVIEW.md` for the full review checklist.
|
See `~/.config/mosaic/guides/CODE-REVIEW.md` for the full review checklist.
|
||||||
@@ -186,7 +186,7 @@ See `~/.config/mosaic/guides/DOCUMENTATION.md` for required documentation delive
|
|||||||
## Issue Tracking
|
## Issue Tracking
|
||||||
|
|
||||||
Use external git provider issues when available. If no external provider exists, `docs/TASKS.md` is the canonical tracker for tasks, milestones, and issue-equivalent work.
|
Use external git provider issues when available. If no external provider exists, `docs/TASKS.md` is the canonical tracker for tasks, milestones, and issue-equivalent work.
|
||||||
For issue/PR/milestone operations, detect platform and use `~/.config/mosaic/rails/git/*.sh` wrappers first; do not use raw `gh`/`tea`/`glab` as first choice.
|
For issue/PR/milestone operations, detect platform and use `~/.config/mosaic/tools/git/*.sh` wrappers first; do not use raw `gh`/`tea`/`glab` as first choice.
|
||||||
If wrapper-driven merge/CI/issue-closure fails, report blocker with exact failed wrapper command and stop.
|
If wrapper-driven merge/CI/issue-closure fails, report blocker with exact failed wrapper command and stop.
|
||||||
Do NOT stop at "PR created" and do NOT ask "should I merge?" or "should I close the issue?" for routine delivery flow.
|
Do NOT stop at "PR created" and do NOT ask "should I merge?" or "should I close the issue?" for routine delivery flow.
|
||||||
|
|
||||||
@@ -198,9 +198,9 @@ Do NOT stop at "PR created" and do NOT ask "should I merge?" or "should I close
|
|||||||
5. Ensure `docs/PRD.md` or `docs/PRD.json` exists and is current before coding.
|
5. Ensure `docs/PRD.md` or `docs/PRD.json` exists and is current before coding.
|
||||||
6. Create scratchpad: `docs/scratchpads/{task-id}-{short-name}.md` and include issue/internal ref.
|
6. Create scratchpad: `docs/scratchpads/{task-id}-{short-name}.md` and include issue/internal ref.
|
||||||
7. Update `docs/TASKS.md` status + issue/internal ref before coding.
|
7. Update `docs/TASKS.md` status + issue/internal ref before coding.
|
||||||
8. Before push, run CI queue guard: `~/.config/mosaic/rails/git/ci-queue-wait.sh --purpose push -B main`.
|
8. Before push, run CI queue guard: `~/.config/mosaic/tools/git/ci-queue-wait.sh --purpose push -B main`.
|
||||||
9. Open PR to `main` for delivery changes (no direct push to `main`).
|
9. Open PR to `main` for delivery changes (no direct push to `main`).
|
||||||
10. Before merge, run CI queue guard: `~/.config/mosaic/rails/git/ci-queue-wait.sh --purpose merge -B main`.
|
10. Before merge, run CI queue guard: `~/.config/mosaic/tools/git/ci-queue-wait.sh --purpose merge -B main`.
|
||||||
11. Merge PRs that pass required checks and review gates with squash strategy only.
|
11. Merge PRs that pass required checks and review gates with squash strategy only.
|
||||||
12. Reference issues/internal refs in commits (`Fixes #123`, `Refs #123`, or `Refs TASKS:T1`).
|
12. Reference issues/internal refs in commits (`Fixes #123`, `Refs #123`, or `Refs TASKS:T1`).
|
||||||
13. Close issue/internal task only after testing and documentation gates pass, PR merge is complete, and CI/pipeline status is terminal green.
|
13. Close issue/internal task only after testing and documentation gates pass, PR merge is complete, and CI/pipeline status is terminal green.
|
||||||
|
|||||||
@@ -9,8 +9,8 @@
|
|||||||
2. Do NOT ask for routine confirmation before required push/merge/issue-close/release/tag actions.
|
2. Do NOT ask for routine confirmation before required push/merge/issue-close/release/tag actions.
|
||||||
3. Completion is forbidden at PR-open stage.
|
3. Completion is forbidden at PR-open stage.
|
||||||
4. Completion requires merged PR to `main` + terminal green CI + linked issue/internal task closed.
|
4. Completion requires merged PR to `main` + terminal green CI + linked issue/internal task closed.
|
||||||
5. Before push or merge, run queue guard: `~/.config/mosaic/rails/git/ci-queue-wait.sh --purpose push|merge -B main`.
|
5. Before push or merge, run queue guard: `~/.config/mosaic/tools/git/ci-queue-wait.sh --purpose push|merge -B main`.
|
||||||
6. For issue/PR/milestone operations, use Mosaic wrappers first (`~/.config/mosaic/rails/git/*.sh`).
|
6. For issue/PR/milestone operations, use Mosaic wrappers first (`~/.config/mosaic/tools/git/*.sh`).
|
||||||
7. If any required wrapper command fails: report `blocked` with the exact failed wrapper command and stop.
|
7. If any required wrapper command fails: report `blocked` with the exact failed wrapper command and stop.
|
||||||
8. Do NOT stop at "PR created" and do NOT ask "should I merge?" for routine flow.
|
8. Do NOT stop at "PR created" and do NOT ask "should I merge?" for routine flow.
|
||||||
|
|
||||||
@@ -72,7 +72,7 @@ pnpm typecheck && pnpm lint && pnpm test
|
|||||||
2. If external git provider is available (Gitea/GitHub/GitLab), create/update issue(s) before coding and map them in `docs/TASKS.md`.
|
2. If external git provider is available (Gitea/GitHub/GitLab), create/update issue(s) before coding and map them in `docs/TASKS.md`.
|
||||||
3. If no external provider is available, use internal refs in `docs/TASKS.md` (example: `TASKS:T1`).
|
3. If no external provider is available, use internal refs in `docs/TASKS.md` (example: `TASKS:T1`).
|
||||||
4. Keep `docs/TASKS.md` status in sync with actual progress until completion.
|
4. Keep `docs/TASKS.md` status in sync with actual progress until completion.
|
||||||
5. For issue/PR/milestone actions, detect platform and use `~/.config/mosaic/rails/git/*.sh` wrappers first (no raw `gh`/`tea`/`glab` as first choice).
|
5. For issue/PR/milestone actions, detect platform and use `~/.config/mosaic/tools/git/*.sh` wrappers first (no raw `gh`/`tea`/`glab` as first choice).
|
||||||
6. If wrapper-driven merge/CI/issue-closure fails, report blocker with the exact failed wrapper command and stop (do not claim completion).
|
6. If wrapper-driven merge/CI/issue-closure fails, report blocker with the exact failed wrapper command and stop (do not claim completion).
|
||||||
|
|
||||||
## Documentation Contract
|
## Documentation Contract
|
||||||
@@ -101,7 +101,7 @@ Reference:
|
|||||||
5. Do not mark implementation complete until PR is merged.
|
5. Do not mark implementation complete until PR is merged.
|
||||||
6. Do not mark implementation complete until CI/pipeline status is terminal green.
|
6. Do not mark implementation complete until CI/pipeline status is terminal green.
|
||||||
7. Close linked issues/tasks only after merge + green CI.
|
7. Close linked issues/tasks only after merge + green CI.
|
||||||
8. Before push or merge, run CI queue guard: `~/.config/mosaic/rails/git/ci-queue-wait.sh --purpose push|merge -B main`.
|
8. Before push or merge, run CI queue guard: `~/.config/mosaic/tools/git/ci-queue-wait.sh --purpose push|merge -B main`.
|
||||||
|
|
||||||
|
|
||||||
## Container Release Strategy (When Applicable)
|
## Container Release Strategy (When Applicable)
|
||||||
@@ -143,8 +143,8 @@ Use `${TASK_PREFIX}` for orchestrated tasks (e.g., `${TASK_PREFIX}-SEC-001`).
|
|||||||
### Post-Coding Review
|
### Post-Coding Review
|
||||||
After implementing changes, code review is REQUIRED for any source-code modification.
|
After implementing changes, code review is REQUIRED for any source-code modification.
|
||||||
For orchestrated tasks, the orchestrator will run:
|
For orchestrated tasks, the orchestrator will run:
|
||||||
1. **Codex code review** — `~/.config/mosaic/rails/codex/codex-code-review.sh --uncommitted`
|
1. **Codex code review** — `~/.config/mosaic/tools/codex/codex-code-review.sh --uncommitted`
|
||||||
2. **Codex security review** — `~/.config/mosaic/rails/codex/codex-security-review.sh --uncommitted`
|
2. **Codex security review** — `~/.config/mosaic/tools/codex/codex-security-review.sh --uncommitted`
|
||||||
3. If blockers/critical findings: remediation task created
|
3. If blockers/critical findings: remediation task created
|
||||||
4. If clean: task marked done
|
4. If clean: task marked done
|
||||||
|
|
||||||
|
|||||||
@@ -191,10 +191,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
|
||||||
```
|
```
|
||||||
|
|
||||||
See `~/.config/mosaic/guides/CODE-REVIEW.md` for the full review checklist.
|
See `~/.config/mosaic/guides/CODE-REVIEW.md` for the full review checklist.
|
||||||
@@ -218,7 +218,7 @@ See `~/.config/mosaic/guides/DOCUMENTATION.md` for required documentation delive
|
|||||||
## Issue Tracking
|
## Issue Tracking
|
||||||
|
|
||||||
Use external git provider issues when available. If no external provider exists, `docs/TASKS.md` is the canonical tracker for tasks, milestones, and issue-equivalent work.
|
Use external git provider issues when available. If no external provider exists, `docs/TASKS.md` is the canonical tracker for tasks, milestones, and issue-equivalent work.
|
||||||
For issue/PR/milestone operations, detect platform and use `~/.config/mosaic/rails/git/*.sh` wrappers first; do not use raw `gh`/`tea`/`glab` as first choice.
|
For issue/PR/milestone operations, detect platform and use `~/.config/mosaic/tools/git/*.sh` wrappers first; do not use raw `gh`/`tea`/`glab` as first choice.
|
||||||
If wrapper-driven merge/CI/issue-closure fails, report blocker with exact failed wrapper command and stop.
|
If wrapper-driven merge/CI/issue-closure fails, report blocker with exact failed wrapper command and stop.
|
||||||
Do NOT stop at "PR created" and do NOT ask "should I merge?" or "should I close the issue?" for routine delivery flow.
|
Do NOT stop at "PR created" and do NOT ask "should I merge?" or "should I close the issue?" for routine delivery flow.
|
||||||
|
|
||||||
@@ -230,9 +230,9 @@ Do NOT stop at "PR created" and do NOT ask "should I merge?" or "should I close
|
|||||||
5. Ensure `docs/PRD.md` or `docs/PRD.json` exists and is current before coding.
|
5. Ensure `docs/PRD.md` or `docs/PRD.json` exists and is current before coding.
|
||||||
6. Create scratchpad: `docs/scratchpads/{task-id}-{short-name}.md` and include issue/internal ref.
|
6. Create scratchpad: `docs/scratchpads/{task-id}-{short-name}.md` and include issue/internal ref.
|
||||||
7. Update `docs/TASKS.md` status + issue/internal ref before coding.
|
7. Update `docs/TASKS.md` status + issue/internal ref before coding.
|
||||||
8. Before push, run CI queue guard: `~/.config/mosaic/rails/git/ci-queue-wait.sh --purpose push -B main`.
|
8. Before push, run CI queue guard: `~/.config/mosaic/tools/git/ci-queue-wait.sh --purpose push -B main`.
|
||||||
9. Open PR to `main` for delivery changes (no direct push to `main`).
|
9. Open PR to `main` for delivery changes (no direct push to `main`).
|
||||||
10. Before merge, run CI queue guard: `~/.config/mosaic/rails/git/ci-queue-wait.sh --purpose merge -B main`.
|
10. Before merge, run CI queue guard: `~/.config/mosaic/tools/git/ci-queue-wait.sh --purpose merge -B main`.
|
||||||
11. Merge PRs that pass required checks and review gates with squash strategy only.
|
11. Merge PRs that pass required checks and review gates with squash strategy only.
|
||||||
12. Reference issues/internal refs in commits (`Fixes #123`, `Refs #123`, or `Refs TASKS:T1`).
|
12. Reference issues/internal refs in commits (`Fixes #123`, `Refs #123`, or `Refs TASKS:T1`).
|
||||||
13. Close issue/internal task only after testing and documentation gates pass, PR merge is complete, and CI/pipeline status is terminal green.
|
13. Close issue/internal task only after testing and documentation gates pass, PR merge is complete, and CI/pipeline status is terminal green.
|
||||||
|
|||||||
@@ -9,8 +9,8 @@
|
|||||||
2. Do NOT ask for routine confirmation before required push/merge/issue-close/release/tag actions.
|
2. Do NOT ask for routine confirmation before required push/merge/issue-close/release/tag actions.
|
||||||
3. Completion is forbidden at PR-open stage.
|
3. Completion is forbidden at PR-open stage.
|
||||||
4. Completion requires merged PR to `main` + terminal green CI + linked issue/internal task closed.
|
4. Completion requires merged PR to `main` + terminal green CI + linked issue/internal task closed.
|
||||||
5. Before push or merge, run queue guard: `~/.config/mosaic/rails/git/ci-queue-wait.sh --purpose push|merge -B main`.
|
5. Before push or merge, run queue guard: `~/.config/mosaic/tools/git/ci-queue-wait.sh --purpose push|merge -B main`.
|
||||||
6. For issue/PR/milestone operations, use Mosaic wrappers first (`~/.config/mosaic/rails/git/*.sh`).
|
6. For issue/PR/milestone operations, use Mosaic wrappers first (`~/.config/mosaic/tools/git/*.sh`).
|
||||||
7. If any required wrapper command fails: report `blocked` with the exact failed wrapper command and stop.
|
7. If any required wrapper command fails: report `blocked` with the exact failed wrapper command and stop.
|
||||||
8. Do NOT stop at "PR created" and do NOT ask "should I merge?" for routine flow.
|
8. Do NOT stop at "PR created" and do NOT ask "should I merge?" for routine flow.
|
||||||
|
|
||||||
@@ -58,7 +58,7 @@ uv run ruff check src/ tests/ && uv run ruff format --check src/ && uv run mypy
|
|||||||
2. If external git provider is available (Gitea/GitHub/GitLab), create/update issue(s) before coding and map them in `docs/TASKS.md`.
|
2. If external git provider is available (Gitea/GitHub/GitLab), create/update issue(s) before coding and map them in `docs/TASKS.md`.
|
||||||
3. If no external provider is available, use internal refs in `docs/TASKS.md` (example: `TASKS:T1`).
|
3. If no external provider is available, use internal refs in `docs/TASKS.md` (example: `TASKS:T1`).
|
||||||
4. Keep `docs/TASKS.md` status in sync with actual progress until completion.
|
4. Keep `docs/TASKS.md` status in sync with actual progress until completion.
|
||||||
5. For issue/PR/milestone actions, detect platform and use `~/.config/mosaic/rails/git/*.sh` wrappers first (no raw `gh`/`tea`/`glab` as first choice).
|
5. For issue/PR/milestone actions, detect platform and use `~/.config/mosaic/tools/git/*.sh` wrappers first (no raw `gh`/`tea`/`glab` as first choice).
|
||||||
6. If wrapper-driven merge/CI/issue-closure fails, report blocker with the exact failed wrapper command and stop (do not claim completion).
|
6. If wrapper-driven merge/CI/issue-closure fails, report blocker with the exact failed wrapper command and stop (do not claim completion).
|
||||||
|
|
||||||
## Documentation Contract
|
## Documentation Contract
|
||||||
@@ -87,7 +87,7 @@ Reference:
|
|||||||
5. Do not mark implementation complete until PR is merged.
|
5. Do not mark implementation complete until PR is merged.
|
||||||
6. Do not mark implementation complete until CI/pipeline status is terminal green.
|
6. Do not mark implementation complete until CI/pipeline status is terminal green.
|
||||||
7. Close linked issues/tasks only after merge + green CI.
|
7. Close linked issues/tasks only after merge + green CI.
|
||||||
8. Before push or merge, run CI queue guard: `~/.config/mosaic/rails/git/ci-queue-wait.sh --purpose push|merge -B main`.
|
8. Before push or merge, run CI queue guard: `~/.config/mosaic/tools/git/ci-queue-wait.sh --purpose push|merge -B main`.
|
||||||
|
|
||||||
## Container Release Strategy (When Applicable)
|
## Container Release Strategy (When Applicable)
|
||||||
|
|
||||||
|
|||||||
@@ -135,7 +135,7 @@ uv run ruff check src/ tests/ && uv run ruff format --check src/ && uv run mypy
|
|||||||
## Issue Tracking
|
## Issue Tracking
|
||||||
|
|
||||||
Use external git provider issues when available. If no external provider exists, `docs/TASKS.md` is the canonical tracker for tasks, milestones, and issue-equivalent work.
|
Use external git provider issues when available. If no external provider exists, `docs/TASKS.md` is the canonical tracker for tasks, milestones, and issue-equivalent work.
|
||||||
For issue/PR/milestone operations, detect platform and use `~/.config/mosaic/rails/git/*.sh` wrappers first; do not use raw `gh`/`tea`/`glab` as first choice.
|
For issue/PR/milestone operations, detect platform and use `~/.config/mosaic/tools/git/*.sh` wrappers first; do not use raw `gh`/`tea`/`glab` as first choice.
|
||||||
If wrapper-driven merge/CI/issue-closure fails, report blocker with exact failed wrapper command and stop.
|
If wrapper-driven merge/CI/issue-closure fails, report blocker with exact failed wrapper command and stop.
|
||||||
Do NOT stop at "PR created" and do NOT ask "should I merge?" or "should I close the issue?" for routine delivery flow.
|
Do NOT stop at "PR created" and do NOT ask "should I merge?" or "should I close the issue?" for routine delivery flow.
|
||||||
|
|
||||||
@@ -146,9 +146,9 @@ Do NOT stop at "PR created" and do NOT ask "should I merge?" or "should I close
|
|||||||
5. Ensure `docs/PRD.md` or `docs/PRD.json` exists and is current before coding.
|
5. Ensure `docs/PRD.md` or `docs/PRD.json` exists and is current before coding.
|
||||||
6. Create scratchpad: `docs/scratchpads/{task-id}-{short-name}.md` and include issue/internal ref.
|
6. Create scratchpad: `docs/scratchpads/{task-id}-{short-name}.md` and include issue/internal ref.
|
||||||
7. Update `docs/TASKS.md` status + issue/internal ref before coding.
|
7. Update `docs/TASKS.md` status + issue/internal ref before coding.
|
||||||
8. Before push, run CI queue guard: `~/.config/mosaic/rails/git/ci-queue-wait.sh --purpose push -B main`.
|
8. Before push, run CI queue guard: `~/.config/mosaic/tools/git/ci-queue-wait.sh --purpose push -B main`.
|
||||||
9. Open PR to `main` for delivery changes (no direct push to `main`).
|
9. Open PR to `main` for delivery changes (no direct push to `main`).
|
||||||
10. Before merge, run CI queue guard: `~/.config/mosaic/rails/git/ci-queue-wait.sh --purpose merge -B main`.
|
10. Before merge, run CI queue guard: `~/.config/mosaic/tools/git/ci-queue-wait.sh --purpose merge -B main`.
|
||||||
11. Merge PRs that pass required checks and review gates with squash strategy only.
|
11. Merge PRs that pass required checks and review gates with squash strategy only.
|
||||||
12. Reference issues/internal refs in commits (`Fixes #123`, `Refs #123`, or `Refs TASKS:T1`).
|
12. Reference issues/internal refs in commits (`Fixes #123`, `Refs #123`, or `Refs TASKS:T1`).
|
||||||
13. Close issue/internal task only after testing and documentation gates pass, PR merge is complete, and CI/pipeline status is terminal green.
|
13. Close issue/internal task only after testing and documentation gates pass, PR merge is complete, and CI/pipeline status is terminal green.
|
||||||
@@ -171,8 +171,8 @@ If you modify source code, independent code review is REQUIRED before completion
|
|||||||
Run independent reviews:
|
Run independent reviews:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
~/.config/mosaic/rails/codex/codex-code-review.sh --uncommitted
|
~/.config/mosaic/tools/codex/codex-code-review.sh --uncommitted
|
||||||
~/.config/mosaic/rails/codex/codex-security-review.sh --uncommitted
|
~/.config/mosaic/tools/codex/codex-security-review.sh --uncommitted
|
||||||
```
|
```
|
||||||
|
|
||||||
See `~/.config/mosaic/guides/CODE-REVIEW.md` for the full review checklist.
|
See `~/.config/mosaic/guides/CODE-REVIEW.md` for the full review checklist.
|
||||||
|
|||||||
@@ -9,8 +9,8 @@
|
|||||||
2. Do NOT ask for routine confirmation before required push/merge/issue-close/release/tag actions.
|
2. Do NOT ask for routine confirmation before required push/merge/issue-close/release/tag actions.
|
||||||
3. Completion is forbidden at PR-open stage.
|
3. Completion is forbidden at PR-open stage.
|
||||||
4. Completion requires merged PR to `main` + terminal green CI + linked issue/internal task closed.
|
4. Completion requires merged PR to `main` + terminal green CI + linked issue/internal task closed.
|
||||||
5. Before push or merge, run queue guard: `~/.config/mosaic/rails/git/ci-queue-wait.sh --purpose push|merge -B main`.
|
5. Before push or merge, run queue guard: `~/.config/mosaic/tools/git/ci-queue-wait.sh --purpose push|merge -B main`.
|
||||||
6. For issue/PR/milestone operations, use Mosaic wrappers first (`~/.config/mosaic/rails/git/*.sh`).
|
6. For issue/PR/milestone operations, use Mosaic wrappers first (`~/.config/mosaic/tools/git/*.sh`).
|
||||||
7. If any required wrapper command fails: report `blocked` with the exact failed wrapper command and stop.
|
7. If any required wrapper command fails: report `blocked` with the exact failed wrapper command and stop.
|
||||||
8. Do NOT stop at "PR created" and do NOT ask "should I merge?" for routine flow.
|
8. Do NOT stop at "PR created" and do NOT ask "should I merge?" for routine flow.
|
||||||
|
|
||||||
@@ -55,7 +55,7 @@ uv run ruff check src/ tests/ && uv run ruff format --check src/ && uv run mypy
|
|||||||
2. If external git provider is available (Gitea/GitHub/GitLab), create/update issue(s) before coding and map them in `docs/TASKS.md`.
|
2. If external git provider is available (Gitea/GitHub/GitLab), create/update issue(s) before coding and map them in `docs/TASKS.md`.
|
||||||
3. If no external provider is available, use internal refs in `docs/TASKS.md` (example: `TASKS:T1`).
|
3. If no external provider is available, use internal refs in `docs/TASKS.md` (example: `TASKS:T1`).
|
||||||
4. Keep `docs/TASKS.md` status in sync with actual progress until completion.
|
4. Keep `docs/TASKS.md` status in sync with actual progress until completion.
|
||||||
5. For issue/PR/milestone actions, detect platform and use `~/.config/mosaic/rails/git/*.sh` wrappers first (no raw `gh`/`tea`/`glab` as first choice).
|
5. For issue/PR/milestone actions, detect platform and use `~/.config/mosaic/tools/git/*.sh` wrappers first (no raw `gh`/`tea`/`glab` as first choice).
|
||||||
6. If wrapper-driven merge/CI/issue-closure fails, report blocker with the exact failed wrapper command and stop (do not claim completion).
|
6. If wrapper-driven merge/CI/issue-closure fails, report blocker with the exact failed wrapper command and stop (do not claim completion).
|
||||||
|
|
||||||
## Documentation Contract
|
## Documentation Contract
|
||||||
@@ -84,7 +84,7 @@ Reference:
|
|||||||
5. Do not mark implementation complete until PR is merged.
|
5. Do not mark implementation complete until PR is merged.
|
||||||
6. Do not mark implementation complete until CI/pipeline status is terminal green.
|
6. Do not mark implementation complete until CI/pipeline status is terminal green.
|
||||||
7. Close linked issues/tasks only after merge + green CI.
|
7. Close linked issues/tasks only after merge + green CI.
|
||||||
8. Before push or merge, run CI queue guard: `~/.config/mosaic/rails/git/ci-queue-wait.sh --purpose push|merge -B main`.
|
8. Before push or merge, run CI queue guard: `~/.config/mosaic/tools/git/ci-queue-wait.sh --purpose push|merge -B main`.
|
||||||
|
|
||||||
## Container Release Strategy (When Applicable)
|
## Container Release Strategy (When Applicable)
|
||||||
|
|
||||||
|
|||||||
@@ -125,7 +125,7 @@ uv run ruff check src/ tests/ && uv run ruff format --check src/ && uv run mypy
|
|||||||
## Issue Tracking
|
## Issue Tracking
|
||||||
|
|
||||||
Use external git provider issues when available. If no external provider exists, `docs/TASKS.md` is the canonical tracker for tasks, milestones, and issue-equivalent work.
|
Use external git provider issues when available. If no external provider exists, `docs/TASKS.md` is the canonical tracker for tasks, milestones, and issue-equivalent work.
|
||||||
For issue/PR/milestone operations, detect platform and use `~/.config/mosaic/rails/git/*.sh` wrappers first; do not use raw `gh`/`tea`/`glab` as first choice.
|
For issue/PR/milestone operations, detect platform and use `~/.config/mosaic/tools/git/*.sh` wrappers first; do not use raw `gh`/`tea`/`glab` as first choice.
|
||||||
If wrapper-driven merge/CI/issue-closure fails, report blocker with exact failed wrapper command and stop.
|
If wrapper-driven merge/CI/issue-closure fails, report blocker with exact failed wrapper command and stop.
|
||||||
Do NOT stop at "PR created" and do NOT ask "should I merge?" or "should I close the issue?" for routine delivery flow.
|
Do NOT stop at "PR created" and do NOT ask "should I merge?" or "should I close the issue?" for routine delivery flow.
|
||||||
|
|
||||||
@@ -136,9 +136,9 @@ Do NOT stop at "PR created" and do NOT ask "should I merge?" or "should I close
|
|||||||
5. Ensure `docs/PRD.md` or `docs/PRD.json` exists and is current before coding.
|
5. Ensure `docs/PRD.md` or `docs/PRD.json` exists and is current before coding.
|
||||||
6. Create scratchpad: `docs/scratchpads/{task-id}-{short-name}.md` and include issue/internal ref.
|
6. Create scratchpad: `docs/scratchpads/{task-id}-{short-name}.md` and include issue/internal ref.
|
||||||
7. Update `docs/TASKS.md` status + issue/internal ref before coding.
|
7. Update `docs/TASKS.md` status + issue/internal ref before coding.
|
||||||
8. Before push, run CI queue guard: `~/.config/mosaic/rails/git/ci-queue-wait.sh --purpose push -B main`.
|
8. Before push, run CI queue guard: `~/.config/mosaic/tools/git/ci-queue-wait.sh --purpose push -B main`.
|
||||||
9. Open PR to `main` for delivery changes (no direct push to `main`).
|
9. Open PR to `main` for delivery changes (no direct push to `main`).
|
||||||
10. Before merge, run CI queue guard: `~/.config/mosaic/rails/git/ci-queue-wait.sh --purpose merge -B main`.
|
10. Before merge, run CI queue guard: `~/.config/mosaic/tools/git/ci-queue-wait.sh --purpose merge -B main`.
|
||||||
11. Merge PRs that pass required checks and review gates with squash strategy only.
|
11. Merge PRs that pass required checks and review gates with squash strategy only.
|
||||||
12. Reference issues/internal refs in commits (`Fixes #123`, `Refs #123`, or `Refs TASKS:T1`).
|
12. Reference issues/internal refs in commits (`Fixes #123`, `Refs #123`, or `Refs TASKS:T1`).
|
||||||
13. Close issue/internal task only after testing and documentation gates pass, PR merge is complete, and CI/pipeline status is terminal green.
|
13. Close issue/internal task only after testing and documentation gates pass, PR merge is complete, and CI/pipeline status is terminal green.
|
||||||
@@ -161,8 +161,8 @@ If you modify source code, independent code review is REQUIRED before completion
|
|||||||
Run independent reviews:
|
Run independent reviews:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
~/.config/mosaic/rails/codex/codex-code-review.sh --uncommitted
|
~/.config/mosaic/tools/codex/codex-code-review.sh --uncommitted
|
||||||
~/.config/mosaic/rails/codex/codex-security-review.sh --uncommitted
|
~/.config/mosaic/tools/codex/codex-security-review.sh --uncommitted
|
||||||
```
|
```
|
||||||
|
|
||||||
See `~/.config/mosaic/guides/CODE-REVIEW.md` for the full review checklist.
|
See `~/.config/mosaic/guides/CODE-REVIEW.md` for the full review checklist.
|
||||||
|
|||||||
@@ -9,8 +9,8 @@
|
|||||||
2. Do NOT ask for routine confirmation before required push/merge/issue-close/release/tag actions.
|
2. Do NOT ask for routine confirmation before required push/merge/issue-close/release/tag actions.
|
||||||
3. Completion is forbidden at PR-open stage.
|
3. Completion is forbidden at PR-open stage.
|
||||||
4. Completion requires merged PR to `main` + terminal green CI + linked issue/internal task closed.
|
4. Completion requires merged PR to `main` + terminal green CI + linked issue/internal task closed.
|
||||||
5. Before push or merge, run queue guard: `~/.config/mosaic/rails/git/ci-queue-wait.sh --purpose push|merge -B main`.
|
5. Before push or merge, run queue guard: `~/.config/mosaic/tools/git/ci-queue-wait.sh --purpose push|merge -B main`.
|
||||||
6. For issue/PR/milestone operations, use Mosaic wrappers first (`~/.config/mosaic/rails/git/*.sh`).
|
6. For issue/PR/milestone operations, use Mosaic wrappers first (`~/.config/mosaic/tools/git/*.sh`).
|
||||||
7. If any required wrapper command fails: report `blocked` with the exact failed wrapper command and stop.
|
7. If any required wrapper command fails: report `blocked` with the exact failed wrapper command and stop.
|
||||||
8. Do NOT stop at "PR created" and do NOT ask "should I merge?" for routine flow.
|
8. Do NOT stop at "PR created" and do NOT ask "should I merge?" for routine flow.
|
||||||
|
|
||||||
@@ -56,7 +56,7 @@ ${QUALITY_GATES}
|
|||||||
2. If external git provider is available (Gitea/GitHub/GitLab), create/update issue(s) before coding and map them in `docs/TASKS.md`.
|
2. If external git provider is available (Gitea/GitHub/GitLab), create/update issue(s) before coding and map them in `docs/TASKS.md`.
|
||||||
3. If no external provider is available, use internal refs in `docs/TASKS.md` (example: `TASKS:T1`).
|
3. If no external provider is available, use internal refs in `docs/TASKS.md` (example: `TASKS:T1`).
|
||||||
4. Keep `docs/TASKS.md` status in sync with actual progress until completion.
|
4. Keep `docs/TASKS.md` status in sync with actual progress until completion.
|
||||||
5. For issue/PR/milestone actions, detect platform and use `~/.config/mosaic/rails/git/*.sh` wrappers first (no raw `gh`/`tea`/`glab` as first choice).
|
5. For issue/PR/milestone actions, detect platform and use `~/.config/mosaic/tools/git/*.sh` wrappers first (no raw `gh`/`tea`/`glab` as first choice).
|
||||||
6. If wrapper-driven merge/CI/issue-closure fails, report blocker with the exact failed wrapper command and stop (do not claim completion).
|
6. If wrapper-driven merge/CI/issue-closure fails, report blocker with the exact failed wrapper command and stop (do not claim completion).
|
||||||
|
|
||||||
## Documentation Contract
|
## Documentation Contract
|
||||||
@@ -85,7 +85,7 @@ Reference:
|
|||||||
5. Do not mark implementation complete until PR is merged.
|
5. Do not mark implementation complete until PR is merged.
|
||||||
6. Do not mark implementation complete until CI/pipeline status is terminal green.
|
6. Do not mark implementation complete until CI/pipeline status is terminal green.
|
||||||
7. Close linked issues/tasks only after merge + green CI.
|
7. Close linked issues/tasks only after merge + green CI.
|
||||||
8. Before push or merge, run CI queue guard: `~/.config/mosaic/rails/git/ci-queue-wait.sh --purpose push|merge -B main`.
|
8. Before push or merge, run CI queue guard: `~/.config/mosaic/tools/git/ci-queue-wait.sh --purpose push|merge -B main`.
|
||||||
|
|
||||||
## Container Release Strategy (When Applicable)
|
## Container Release Strategy (When Applicable)
|
||||||
|
|
||||||
|
|||||||
@@ -122,7 +122,7 @@ ${QUALITY_GATES}
|
|||||||
## Issue Tracking
|
## Issue Tracking
|
||||||
|
|
||||||
Use external git provider issues when available. If no external provider exists, `docs/TASKS.md` is the canonical tracker for tasks, milestones, and issue-equivalent work.
|
Use external git provider issues when available. If no external provider exists, `docs/TASKS.md` is the canonical tracker for tasks, milestones, and issue-equivalent work.
|
||||||
For issue/PR/milestone operations, detect platform and use `~/.config/mosaic/rails/git/*.sh` wrappers first; do not use raw `gh`/`tea`/`glab` as first choice.
|
For issue/PR/milestone operations, detect platform and use `~/.config/mosaic/tools/git/*.sh` wrappers first; do not use raw `gh`/`tea`/`glab` as first choice.
|
||||||
If wrapper-driven merge/CI/issue-closure fails, report blocker with exact failed wrapper command and stop.
|
If wrapper-driven merge/CI/issue-closure fails, report blocker with exact failed wrapper command and stop.
|
||||||
Do NOT stop at "PR created" and do NOT ask "should I merge?" or "should I close the issue?" for routine delivery flow.
|
Do NOT stop at "PR created" and do NOT ask "should I merge?" or "should I close the issue?" for routine delivery flow.
|
||||||
|
|
||||||
@@ -133,9 +133,9 @@ Do NOT stop at "PR created" and do NOT ask "should I merge?" or "should I close
|
|||||||
5. Ensure `docs/PRD.md` or `docs/PRD.json` exists and is current before coding.
|
5. Ensure `docs/PRD.md` or `docs/PRD.json` exists and is current before coding.
|
||||||
6. Create scratchpad: `docs/scratchpads/{task-id}-{short-name}.md` and include issue/internal ref.
|
6. Create scratchpad: `docs/scratchpads/{task-id}-{short-name}.md` and include issue/internal ref.
|
||||||
7. Update `docs/TASKS.md` status + issue/internal ref before coding.
|
7. Update `docs/TASKS.md` status + issue/internal ref before coding.
|
||||||
8. Before push, run CI queue guard: `~/.config/mosaic/rails/git/ci-queue-wait.sh --purpose push -B main`.
|
8. Before push, run CI queue guard: `~/.config/mosaic/tools/git/ci-queue-wait.sh --purpose push -B main`.
|
||||||
9. Open PR to `main` for delivery changes (no direct push to `main`).
|
9. Open PR to `main` for delivery changes (no direct push to `main`).
|
||||||
10. Before merge, run CI queue guard: `~/.config/mosaic/rails/git/ci-queue-wait.sh --purpose merge -B main`.
|
10. Before merge, run CI queue guard: `~/.config/mosaic/tools/git/ci-queue-wait.sh --purpose merge -B main`.
|
||||||
11. Merge PRs that pass required checks and review gates with squash strategy only.
|
11. Merge PRs that pass required checks and review gates with squash strategy only.
|
||||||
12. Reference issues/internal refs in commits (`Fixes #123`, `Refs #123`, or `Refs TASKS:T1`).
|
12. Reference issues/internal refs in commits (`Fixes #123`, `Refs #123`, or `Refs TASKS:T1`).
|
||||||
13. Close issue/internal task only after testing and documentation gates pass, PR merge is complete, and CI/pipeline status is terminal green.
|
13. Close issue/internal task only after testing and documentation gates pass, PR merge is complete, and CI/pipeline status is terminal green.
|
||||||
@@ -159,10 +159,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.
|
||||||
|
|||||||
@@ -16,7 +16,12 @@
|
|||||||
# After loading, service-specific env vars are exported.
|
# After loading, service-specific env vars are exported.
|
||||||
# Run `load_credentials --help` for details.
|
# Run `load_credentials --help` for details.
|
||||||
|
|
||||||
MOSAIC_CREDENTIALS_FILE="${MOSAIC_CREDENTIALS_FILE:-$HOME/src/jarvis-brain/credentials.json}"
|
if [[ -z "${MOSAIC_CREDENTIALS_FILE:-}" ]]; then
|
||||||
|
for _cand in "$HOME/.config/mosaic/credentials.json" "$HOME/src/jarvis-brain/credentials.json"; do
|
||||||
|
if [[ -f "$_cand" ]]; then MOSAIC_CREDENTIALS_FILE="$_cand"; break; fi
|
||||||
|
done
|
||||||
|
: "${MOSAIC_CREDENTIALS_FILE:=$HOME/src/jarvis-brain/credentials.json}"
|
||||||
|
fi
|
||||||
|
|
||||||
_mosaic_require_jq() {
|
_mosaic_require_jq() {
|
||||||
if ! command -v jq &>/dev/null; then
|
if ! command -v jq &>/dev/null; then
|
||||||
@@ -34,6 +39,19 @@ _mosaic_read_cred() {
|
|||||||
jq -r "$jq_path // empty" "$MOSAIC_CREDENTIALS_FILE"
|
jq -r "$jq_path // empty" "$MOSAIC_CREDENTIALS_FILE"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Decide curl TLS flag for a target URL: validate public hosts (MITM matters on
|
||||||
|
# WAN); allow self-signed only for private-network IP literals (trusted LAN) or an
|
||||||
|
# explicit $MOSAIC_INSECURE_TLS opt-in. Echoes "-k" or "" (empty).
|
||||||
|
_mosaic_tls_opt() {
|
||||||
|
local url="$1" host
|
||||||
|
[[ -n "${MOSAIC_INSECURE_TLS:-}" ]] && { echo "-k"; return; }
|
||||||
|
host=$(printf '%s' "$url" | sed -E 's#^[a-zA-Z]+://([^/:]+).*#\1#')
|
||||||
|
if [[ "$host" =~ ^(10\.|127\.|192\.168\.|172\.(1[6-9]|2[0-9]|3[01])\.) ]]; then
|
||||||
|
echo "-k"; return
|
||||||
|
fi
|
||||||
|
echo ""
|
||||||
|
}
|
||||||
|
|
||||||
# Sync Woodpecker credentials to ~/.woodpecker/<instance>.env
|
# Sync Woodpecker credentials to ~/.woodpecker/<instance>.env
|
||||||
# Only writes when values differ to avoid unnecessary disk writes.
|
# Only writes when values differ to avoid unnecessary disk writes.
|
||||||
_mosaic_sync_woodpecker_env() {
|
_mosaic_sync_woodpecker_env() {
|
||||||
@@ -261,7 +279,8 @@ mosaic_http() {
|
|||||||
local base_url="${4:-}"
|
local base_url="${4:-}"
|
||||||
|
|
||||||
local response
|
local response
|
||||||
response=$(curl -sk -w "\n%{http_code}" -X "$method" \
|
local _tls; _tls=$(_mosaic_tls_opt "${base_url}${endpoint}")
|
||||||
|
response=$(curl -sS $_tls -w "\n%{http_code}" -X "$method" \
|
||||||
-H "$auth_header" \
|
-H "$auth_header" \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
"${base_url}${endpoint}")
|
"${base_url}${endpoint}")
|
||||||
@@ -279,7 +298,8 @@ mosaic_http_post() {
|
|||||||
local base_url="${4:-}"
|
local base_url="${4:-}"
|
||||||
|
|
||||||
local response
|
local response
|
||||||
response=$(curl -sk -w "\n%{http_code}" -X POST \
|
local _tls; _tls=$(_mosaic_tls_opt "${base_url}${endpoint}")
|
||||||
|
response=$(curl -sS $_tls -w "\n%{http_code}" -X POST \
|
||||||
-H "$auth_header" \
|
-H "$auth_header" \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
-d "$data" \
|
-d "$data" \
|
||||||
@@ -297,7 +317,8 @@ mosaic_http_patch() {
|
|||||||
local base_url="${4:-}"
|
local base_url="${4:-}"
|
||||||
|
|
||||||
local response
|
local response
|
||||||
response=$(curl -sk -w "\n%{http_code}" -X PATCH \
|
local _tls; _tls=$(_mosaic_tls_opt "${base_url}${endpoint}")
|
||||||
|
response=$(curl -sS $_tls -w "\n%{http_code}" -X PATCH \
|
||||||
-H "$auth_header" \
|
-H "$auth_header" \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
-d "$data" \
|
-d "$data" \
|
||||||
|
|||||||
@@ -169,6 +169,43 @@ raise SystemExit(1)
|
|||||||
PY
|
PY
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Emit an actionable diagnostic to stderr when no tea login resolves for a host.
|
||||||
|
# Callers that have a working API fallback may ignore the non-zero return of
|
||||||
|
# get_gitea_login_for_host; this turns the previously SILENT failure into a loud,
|
||||||
|
# greppable hint (available logins + override + add-login instructions). Printed to
|
||||||
|
# stderr only, so it never contaminates stdout (the resolved login name) or log
|
||||||
|
# assertions that capture tea/curl invocations.
|
||||||
|
print_gitea_login_diagnostic() {
|
||||||
|
local host="${1:-<unknown>}"
|
||||||
|
local available
|
||||||
|
available=$(
|
||||||
|
command -v tea >/dev/null 2>&1 || { echo "(tea CLI not installed)"; exit 0; }
|
||||||
|
logins_json=$(tea login list --output json 2>/dev/null) || { echo "(could not query tea login list)"; exit 0; }
|
||||||
|
TEA_LOGINS_JSON="$logins_json" python3 - <<'PY'
|
||||||
|
import json, os
|
||||||
|
from urllib.parse import urlparse
|
||||||
|
try:
|
||||||
|
logins = json.loads(os.environ.get("TEA_LOGINS_JSON", "[]"))
|
||||||
|
except Exception:
|
||||||
|
logins = []
|
||||||
|
rows = []
|
||||||
|
for login in logins if isinstance(logins, list) else []:
|
||||||
|
name = str(login.get("name") or login.get("Name") or "")
|
||||||
|
url = str(login.get("url") or login.get("URL") or "")
|
||||||
|
host = urlparse(url).hostname or "?"
|
||||||
|
if name:
|
||||||
|
rows.append(f"{name} (host: {host})")
|
||||||
|
print("; ".join(rows) if rows else "(none configured)")
|
||||||
|
PY
|
||||||
|
)
|
||||||
|
{
|
||||||
|
echo "Error: no Gitea tea login matches host '$host'."
|
||||||
|
echo " Available tea logins: ${available}"
|
||||||
|
echo " Fix: set GITEA_LOGIN to a login whose URL host is '$host',"
|
||||||
|
echo " or add one: tea login add --name <name> --url https://$host --token <token>"
|
||||||
|
} >&2
|
||||||
|
}
|
||||||
|
|
||||||
get_gitea_login_for_host() {
|
get_gitea_login_for_host() {
|
||||||
local host="${1:-}"
|
local host="${1:-}"
|
||||||
local login
|
local login
|
||||||
@@ -190,6 +227,7 @@ get_gitea_login_for_host() {
|
|||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
print_gitea_login_diagnostic "$host"
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -53,7 +53,15 @@ if [[ "$PLATFORM" == "github" ]]; then
|
|||||||
gh issue comment "$ISSUE_NUMBER" --body "$COMMENT"
|
gh issue comment "$ISSUE_NUMBER" --body "$COMMENT"
|
||||||
echo "Added comment to GitHub issue #$ISSUE_NUMBER"
|
echo "Added comment to GitHub issue #$ISSUE_NUMBER"
|
||||||
elif [[ "$PLATFORM" == "gitea" ]]; then
|
elif [[ "$PLATFORM" == "gitea" ]]; then
|
||||||
tea issue comment "$ISSUE_NUMBER" "$COMMENT" $(get_gitea_repo_args)
|
# Build the invocation as an argv array (not unquoted $(get_gitea_repo_args)
|
||||||
|
# word-splitting) so the comment body — including Markdown backticks, $(...),
|
||||||
|
# and quotes — is passed verbatim and never re-split or shell-evaluated.
|
||||||
|
REPO_SLUG=$(get_repo_slug)
|
||||||
|
GITEA_LOGIN_NAME=$(get_gitea_login) || {
|
||||||
|
echo "Error: could not resolve a Gitea login for this repo; cannot comment on issue #$ISSUE_NUMBER." >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
tea issue comment "$ISSUE_NUMBER" "$COMMENT" --repo "$REPO_SLUG" --login "$GITEA_LOGIN_NAME"
|
||||||
echo "Added comment to Gitea issue #$ISSUE_NUMBER"
|
echo "Added comment to Gitea issue #$ISSUE_NUMBER"
|
||||||
else
|
else
|
||||||
echo "Error: Unknown platform"
|
echo "Error: Unknown platform"
|
||||||
|
|||||||
@@ -72,6 +72,11 @@ elif values and all(v == "success" for v in values):
|
|||||||
print("success")
|
print("success")
|
||||||
elif any(v in {"pending", "running", "queued", "waiting"} for v in values):
|
elif any(v in {"pending", "running", "queued", "waiting"} for v in values):
|
||||||
print("pending")
|
print("pending")
|
||||||
|
elif not values and not state:
|
||||||
|
# No pipeline/status of any kind reported for this commit. Distinct from
|
||||||
|
# "unknown" (an ambiguous/unrecognized status that should keep polling):
|
||||||
|
# this signals a repo/commit that simply has no CI configured.
|
||||||
|
print("no-status")
|
||||||
else:
|
else:
|
||||||
print("unknown")
|
print("unknown")
|
||||||
PY
|
PY
|
||||||
@@ -142,6 +147,21 @@ gitea_get_commit_status_json() {
|
|||||||
curl -fsSL -H "User-Agent: curl/8" -H "Authorization: token ${token}" "$url"
|
curl -fsSL -H "User-Agent: curl/8" -H "Authorization: token ${token}" "$url"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
gitea_get_default_branch() {
|
||||||
|
local host="$1"
|
||||||
|
local repo="$2"
|
||||||
|
local token="$3"
|
||||||
|
local url="https://${host}/api/v1/repos/${repo}"
|
||||||
|
curl -fsSL -H "User-Agent: curl/8" -H "Authorization: token ${token}" "$url" | python3 -c '
|
||||||
|
import json, sys
|
||||||
|
print((json.load(sys.stdin) or {}).get("default_branch", ""))
|
||||||
|
'
|
||||||
|
}
|
||||||
|
|
||||||
|
github_get_default_branch() {
|
||||||
|
gh api "repos/${OWNER}/${REPO}" --jq '.default_branch'
|
||||||
|
}
|
||||||
|
|
||||||
while [[ $# -gt 0 ]]; do
|
while [[ $# -gt 0 ]]; do
|
||||||
case "$1" in
|
case "$1" in
|
||||||
-n|--number)
|
-n|--number)
|
||||||
@@ -245,6 +265,51 @@ else
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# No-CI determination is TWO-TIER (primary: CI history; secondary: empty-poll streak).
|
||||||
|
#
|
||||||
|
# PRIMARY — "does this repo run CI at all?" Probed once, up front, from the DEFAULT
|
||||||
|
# BRANCH's commit status. A repo whose default branch carries CI statuses
|
||||||
|
# demonstrably runs CI, so an EMPTY status on the PR head means the pipeline simply
|
||||||
|
# has not registered YET (webhook/queue lag) — NOT that the repo is CI-less. In that
|
||||||
|
# case we must NEVER fast-green; we keep polling until the pipeline registers or the
|
||||||
|
# timeout fires (both safe). This closes the webhook-lag false-green: a slow-to-
|
||||||
|
# register pipeline feeding a merge gate can no longer be mistaken for "no CI".
|
||||||
|
#
|
||||||
|
# SECONDARY — the empty-poll streak below applies ONLY to genuinely CI-less repos
|
||||||
|
# (default branch also has no CI history, e.g. device-imaging class), where burning
|
||||||
|
# the full timeout would be pure waste. There, NO_CI_MAX empty polls => fast-exit 0.
|
||||||
|
#
|
||||||
|
# Probe failure is treated conservatively as REPO_HAS_CI=1 (assume CI present): we
|
||||||
|
# would rather wait-then-timeout than risk a false-green, per the merge-gate priority.
|
||||||
|
REPO_HAS_CI=1
|
||||||
|
detect_repo_ci() {
|
||||||
|
local def_branch def_status
|
||||||
|
# Every early exit returns 0: a probe miss must leave the conservative
|
||||||
|
# REPO_HAS_CI=1 default in place, never abort the caller under `set -e`.
|
||||||
|
if [[ "$PLATFORM" == "github" ]]; then
|
||||||
|
def_branch=$(github_get_default_branch 2>/dev/null) || {
|
||||||
|
echo "[pr-ci-wait] WARN: default-branch probe failed; assuming CI-enabled (will not fast-green on empty status)."; return 0; }
|
||||||
|
[[ -n "$def_branch" ]] || return 0
|
||||||
|
def_status=$(github_get_commit_status_json "$OWNER" "$REPO" "$def_branch" 2>/dev/null | extract_state_from_status_json) || return 0
|
||||||
|
else
|
||||||
|
def_branch=$(gitea_get_default_branch "$HOST" "$OWNER/$REPO" "$TOKEN" 2>/dev/null) || {
|
||||||
|
echo "[pr-ci-wait] WARN: default-branch probe failed; assuming CI-enabled (will not fast-green on empty status)."; return 0; }
|
||||||
|
[[ -n "$def_branch" ]] || return 0
|
||||||
|
def_status=$(gitea_get_commit_status_json "$HOST" "$OWNER/$REPO" "$TOKEN" "$def_branch" 2>/dev/null | extract_state_from_status_json) || return 0
|
||||||
|
fi
|
||||||
|
if [[ "$def_status" == "no-status" || -z "$def_status" ]]; then
|
||||||
|
REPO_HAS_CI=0
|
||||||
|
echo "[pr-ci-wait] default branch '${def_branch}' has no CI status history — treating repo as CI-less (empty-poll fast-exit enabled)."
|
||||||
|
else
|
||||||
|
REPO_HAS_CI=1
|
||||||
|
echo "[pr-ci-wait] default branch '${def_branch}' has CI history (state=${def_status}) — repo runs CI; empty status on PR head => awaiting registration, will not fast-green."
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
detect_repo_ci || true
|
||||||
|
|
||||||
|
NO_CI_STREAK=0
|
||||||
|
NO_CI_MAX=3
|
||||||
|
|
||||||
while true; do
|
while true; do
|
||||||
NOW_TS=$(date +%s)
|
NOW_TS=$(date +%s)
|
||||||
if (( NOW_TS > DEADLINE_TS )); then
|
if (( NOW_TS > DEADLINE_TS )); then
|
||||||
@@ -272,11 +337,35 @@ while true; do
|
|||||||
echo "Error: CI reported ${STATE} for PR #$PR_NUMBER." >&2
|
echo "Error: CI reported ${STATE} for PR #$PR_NUMBER." >&2
|
||||||
exit 1
|
exit 1
|
||||||
;;
|
;;
|
||||||
|
no-status)
|
||||||
|
if [[ "$REPO_HAS_CI" == "1" ]]; then
|
||||||
|
# PRIMARY tier: repo demonstrably runs CI but this commit's pipeline
|
||||||
|
# has not registered yet (webhook/queue lag). Do NOT fast-green — keep
|
||||||
|
# polling until it registers or the timeout fires. Reset the streak so
|
||||||
|
# a later genuine CI-less misread can't accumulate across this state.
|
||||||
|
NO_CI_STREAK=0
|
||||||
|
echo "[pr-ci-wait] empty status on PR head but repo runs CI — awaiting pipeline registration (webhook lag), not fast-greening."
|
||||||
|
else
|
||||||
|
# SECONDARY tier: genuinely CI-less repo (default branch has no CI
|
||||||
|
# history either). Empty polls => fast-exit green after NO_CI_MAX.
|
||||||
|
NO_CI_STREAK=$((NO_CI_STREAK + 1))
|
||||||
|
if (( NO_CI_STREAK >= NO_CI_MAX )); then
|
||||||
|
echo "[INFO] no CI configured for this repo/commit (PR #$PR_NUMBER, ${NO_CI_STREAK} consecutive empty polls, default branch also CI-less); treating as green."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
sleep "$INTERVAL_SEC"
|
||||||
|
;;
|
||||||
pending|unknown)
|
pending|unknown)
|
||||||
|
# A pipeline exists but hasn't reached a terminal state (or is
|
||||||
|
# transiently ambiguous) — keep waiting, and reset the no-CI streak
|
||||||
|
# since this commit is not in the "no CI at all" condition.
|
||||||
|
NO_CI_STREAK=0
|
||||||
sleep "$INTERVAL_SEC"
|
sleep "$INTERVAL_SEC"
|
||||||
;;
|
;;
|
||||||
*)
|
*)
|
||||||
echo "[pr-ci-wait] Unrecognized state '${STATE}', continuing to poll..."
|
echo "[pr-ci-wait] Unrecognized state '${STATE}', continuing to poll..."
|
||||||
|
NO_CI_STREAK=0
|
||||||
sleep "$INTERVAL_SEC"
|
sleep "$INTERVAL_SEC"
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
|
|||||||
@@ -230,4 +230,81 @@ if grep -q -- 'tea issue close 536 .*--login mosaicstack' "$LOG_FILE"; then
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# #560: loud diagnostic + host-derived login for BOTH instances + override-wins
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
# Loud diagnostic: a host with no matching tea login must emit an actionable
|
||||||
|
# error to stderr (the previous behavior was a SILENT failure). The original
|
||||||
|
# mock defines only usc/evil-usc logins, so mosaicstack resolution fails here.
|
||||||
|
git -C "$REPO_DIR" remote set-url origin https://git.mosaicstack.dev/mosaicstack/stack.git
|
||||||
|
diag_stderr=$(run_in_repo bash -c '
|
||||||
|
source "'"$SCRIPT_DIR"'/detect-platform.sh"
|
||||||
|
get_gitea_login_for_host git.mosaicstack.dev
|
||||||
|
' 2>&1 1>/dev/null || true)
|
||||||
|
if ! grep -q "no Gitea tea login matches host 'git.mosaicstack.dev'" <<<"$diag_stderr"; then
|
||||||
|
echo "Expected loud diagnostic naming the unresolved host; got: $diag_stderr" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if ! grep -q "Available tea logins:" <<<"$diag_stderr"; then
|
||||||
|
echo "Expected diagnostic to list available tea logins; got: $diag_stderr" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Both-instance host derivation + override-wins, using a mock that DOES define a
|
||||||
|
# mosaicstack login. Scoped to this section so the API-fallback assertions above
|
||||||
|
# (which rely on mosaicstack having NO tea login) remain valid.
|
||||||
|
BIN_DIR2="$WORK_DIR/bin2"
|
||||||
|
mkdir -p "$BIN_DIR2"
|
||||||
|
cp "$BIN_DIR/curl" "$BIN_DIR2/curl"
|
||||||
|
cat > "$BIN_DIR2/tea" <<'SH'
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
if [[ "$*" == "login list --output json" ]]; then
|
||||||
|
cat <<'JSON'
|
||||||
|
[
|
||||||
|
{"name":"mosaicstack","url":"https://git.mosaicstack.dev","user":"jason.woltje"},
|
||||||
|
{"name":"usc","url":"https://git.uscllc.com","user":"jason.woltje"}
|
||||||
|
]
|
||||||
|
JSON
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
printf 'tea %s\n' "$*" >> "$MOSAIC_TEST_LOG"
|
||||||
|
exit 0
|
||||||
|
SH
|
||||||
|
chmod +x "$BIN_DIR2/tea"
|
||||||
|
|
||||||
|
run_in_repo2() {
|
||||||
|
(
|
||||||
|
cd "$REPO_DIR"
|
||||||
|
PATH="$BIN_DIR2:$PATH" \
|
||||||
|
MOSAIC_CREDENTIALS_FILE="$CREDENTIALS_FILE" \
|
||||||
|
MOSAIC_TEST_LOG="$LOG_FILE" \
|
||||||
|
"$@"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
git -C "$REPO_DIR" remote set-url origin https://git.mosaicstack.dev/mosaicstack/stack.git
|
||||||
|
mosaic_login=$(run_in_repo2 bash -c 'source "'"$SCRIPT_DIR"'/detect-platform.sh"; get_gitea_login')
|
||||||
|
if [[ "$mosaic_login" != "mosaicstack" ]]; then
|
||||||
|
echo "Expected mosaicstack origin to derive login 'mosaicstack'; got '$mosaic_login'" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
git -C "$REPO_DIR" remote set-url origin https://git.uscllc.com/USC/uconnect.git
|
||||||
|
usc_login_derived=$(run_in_repo2 bash -c 'source "'"$SCRIPT_DIR"'/detect-platform.sh"; get_gitea_login')
|
||||||
|
if [[ "$usc_login_derived" != "usc" ]]; then
|
||||||
|
echo "Expected usc origin to derive login 'usc'; got '$usc_login_derived'" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Explicit GITEA_LOGIN override is honored when it matches the host.
|
||||||
|
git -C "$REPO_DIR" remote set-url origin https://git.mosaicstack.dev/mosaicstack/stack.git
|
||||||
|
override_wins=$(run_in_repo2 bash -c 'export GITEA_LOGIN=mosaicstack; source "'"$SCRIPT_DIR"'/detect-platform.sh"; get_gitea_login')
|
||||||
|
if [[ "$override_wins" != "mosaicstack" ]]; then
|
||||||
|
echo "Expected valid GITEA_LOGIN override to win on mosaicstack host; got '$override_wins'" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
git -C "$REPO_DIR" remote set-url origin https://git.uscllc.com/USC/uconnect.git
|
||||||
|
|
||||||
echo "Gitea login resolution regression harness passed"
|
echo "Gitea login resolution regression harness passed"
|
||||||
|
|||||||
102
packages/mosaic/framework/tools/git/test-issue-create-body-safety.sh
Executable file
102
packages/mosaic/framework/tools/git/test-issue-create-body-safety.sh
Executable file
@@ -0,0 +1,102 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Regression harness for issue-create.sh Markdown-body safety (#559).
|
||||||
|
#
|
||||||
|
# Guards against reintroduction of eval-based command construction. The wrapper
|
||||||
|
# builds its tea/gh invocation as an argv array, so a body containing command
|
||||||
|
# substitution ($(...)), backticks, quotes, and dollar signs MUST reach tea
|
||||||
|
# verbatim and MUST NOT be shell-evaluated. This test asserts both:
|
||||||
|
# 1. No command-substitution side effect (an injected `touch SENTINEL` never runs).
|
||||||
|
# 2. The --description value tea receives is byte-for-byte the original body.
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
WORK_DIR="${MOSAIC_TEST_WORK_DIR:-$PWD/.mosaic-test-work/issue-create-body-safety}"
|
||||||
|
REPO_DIR="$WORK_DIR/repo"
|
||||||
|
BIN_DIR="$WORK_DIR/bin"
|
||||||
|
SENTINEL="$WORK_DIR/INJECTION_SENTINEL"
|
||||||
|
BODY_FILE="$WORK_DIR/body.txt"
|
||||||
|
RECEIVED_FILE="$WORK_DIR/received-description.txt"
|
||||||
|
|
||||||
|
rm -rf "$WORK_DIR"
|
||||||
|
mkdir -p "$REPO_DIR" "$BIN_DIR"
|
||||||
|
|
||||||
|
git -C "$REPO_DIR" init -q
|
||||||
|
git -C "$REPO_DIR" remote add origin https://git.mosaicstack.dev/mosaicstack/stack.git
|
||||||
|
|
||||||
|
# Hostile Markdown body. The unquoted heredoc expands $SENTINEL (a real path we
|
||||||
|
# want embedded) but every shell metacharacter we care about is backslash-escaped
|
||||||
|
# so the TEST shell writes them literally into the file — the bytes the wrapper
|
||||||
|
# must then preserve.
|
||||||
|
cat > "$BODY_FILE" <<EOF
|
||||||
|
# Release notes
|
||||||
|
|
||||||
|
Inline code: \`rm -rf /\` must stay literal.
|
||||||
|
Command sub attempt: \$(touch $SENTINEL)
|
||||||
|
Backtick cmd attempt: \`touch $SENTINEL\`
|
||||||
|
Dollars: \$HOME \${PATH} \$5.00 and 100% done
|
||||||
|
Quotes: "double" and 'single' and \`mixed\`
|
||||||
|
Trailing pipe-ish: foo | bar && baz ; qux
|
||||||
|
EOF
|
||||||
|
|
||||||
|
BODY="$(cat "$BODY_FILE")"
|
||||||
|
|
||||||
|
# Mock tea: resolve a mosaicstack login, then capture the --description verbatim.
|
||||||
|
cat > "$BIN_DIR/tea" <<'SH'
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
if [[ "$*" == "login list --output json" ]]; then
|
||||||
|
cat <<'JSON'
|
||||||
|
[
|
||||||
|
{"name":"mosaicstack","url":"https://git.mosaicstack.dev","user":"jason.woltje"}
|
||||||
|
]
|
||||||
|
JSON
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "${1:-}" == "issue" && "${2:-}" == "create" ]]; then
|
||||||
|
desc=""
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--description) desc="$2"; shift 2 ;;
|
||||||
|
*) shift ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
printf '%s' "$desc" > "$MOSAIC_TEST_RECEIVED"
|
||||||
|
echo "#1 created"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
exit 0
|
||||||
|
SH
|
||||||
|
chmod +x "$BIN_DIR/tea"
|
||||||
|
|
||||||
|
(
|
||||||
|
cd "$REPO_DIR"
|
||||||
|
PATH="$BIN_DIR:$PATH" \
|
||||||
|
MOSAIC_TEST_RECEIVED="$RECEIVED_FILE" \
|
||||||
|
"$SCRIPT_DIR/issue-create.sh" -t "Body safety test" -b "$BODY"
|
||||||
|
) >/dev/null
|
||||||
|
|
||||||
|
# 1. No command substitution executed anywhere in the pipeline.
|
||||||
|
if [[ -e "$SENTINEL" ]]; then
|
||||||
|
echo "FAIL: injected command substitution executed (sentinel file created): $SENTINEL" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 2. tea actually received the body (issue create path taken, not silently dropped).
|
||||||
|
if [[ ! -f "$RECEIVED_FILE" ]]; then
|
||||||
|
echo "FAIL: tea issue create was never invoked with a --description" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 3. The description tea received is byte-for-byte the original body.
|
||||||
|
if [[ "$(cat "$RECEIVED_FILE")" != "$BODY" ]]; then
|
||||||
|
echo "FAIL: body was not preserved verbatim through issue-create.sh" >&2
|
||||||
|
echo "--- expected ---" >&2; printf '%s\n' "$BODY" >&2
|
||||||
|
echo "--- received ---" >&2; cat "$RECEIVED_FILE" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "issue-create.sh Markdown body-safety regression harness passed"
|
||||||
@@ -12,7 +12,7 @@ wp_resolve_repo_id() {
|
|||||||
local full_name="$1"
|
local full_name="$1"
|
||||||
local response http_code body repo_id
|
local response http_code body repo_id
|
||||||
|
|
||||||
response=$(curl -sk -w "\n%{http_code}" \
|
response=$(curl -sS -w "\n%{http_code}" \
|
||||||
-H "Authorization: Bearer $WOODPECKER_TOKEN" \
|
-H "Authorization: Bearer $WOODPECKER_TOKEN" \
|
||||||
"${WOODPECKER_URL}/api/repos/lookup/${full_name}")
|
"${WOODPECKER_URL}/api/repos/lookup/${full_name}")
|
||||||
|
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ fi
|
|||||||
# Resolve owner/repo to numeric ID (Woodpecker v3 API)
|
# Resolve owner/repo to numeric ID (Woodpecker v3 API)
|
||||||
REPO_ID=$(wp_resolve_repo_id "$REPO") || exit 1
|
REPO_ID=$(wp_resolve_repo_id "$REPO") || exit 1
|
||||||
|
|
||||||
response=$(curl -sk -w "\n%{http_code}" \
|
response=$(curl -sS -w "\n%{http_code}" \
|
||||||
-H "Authorization: Bearer $WOODPECKER_TOKEN" \
|
-H "Authorization: Bearer $WOODPECKER_TOKEN" \
|
||||||
"${WOODPECKER_URL}/api/repos/${REPO_ID}/pipelines?perPage=${LIMIT}")
|
"${WOODPECKER_URL}/api/repos/${REPO_ID}/pipelines?perPage=${LIMIT}")
|
||||||
|
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ REPO_ID=$(wp_resolve_repo_id "$REPO") || exit 1
|
|||||||
_wp_fetch() {
|
_wp_fetch() {
|
||||||
local ep="$1"
|
local ep="$1"
|
||||||
local resp http_code body
|
local resp http_code body
|
||||||
resp=$(curl -sk -w "\n%{http_code}" \
|
resp=$(curl -sS -w "\n%{http_code}" \
|
||||||
-H "Authorization: Bearer $WOODPECKER_TOKEN" \
|
-H "Authorization: Bearer $WOODPECKER_TOKEN" \
|
||||||
"$ep")
|
"$ep")
|
||||||
http_code=$(echo "$resp" | tail -n1)
|
http_code=$(echo "$resp" | tail -n1)
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ REPO_ID=$(wp_resolve_repo_id "$REPO") || exit 1
|
|||||||
|
|
||||||
echo "Triggering pipeline for $REPO on branch $BRANCH..."
|
echo "Triggering pipeline for $REPO on branch $BRANCH..."
|
||||||
|
|
||||||
response=$(curl -sk -w "\n%{http_code}" -X POST \
|
response=$(curl -sS -w "\n%{http_code}" -X POST \
|
||||||
-H "Authorization: Bearer $WOODPECKER_TOKEN" \
|
-H "Authorization: Bearer $WOODPECKER_TOKEN" \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
-d "$(jq -n --arg b "$BRANCH" '{branch: $b}')" \
|
-d "$(jq -n --arg b "$BRANCH" '{branch: $b}')" \
|
||||||
|
|||||||
@@ -1,6 +1,15 @@
|
|||||||
import { describe, it, expect, vi, beforeEach, afterEach, type MockInstance } from 'vitest';
|
import { describe, it, expect, vi, beforeEach, afterEach, type MockInstance } from 'vitest';
|
||||||
import { Command } from 'commander';
|
import { Command } from 'commander';
|
||||||
import { buildPiSkillArgs, registerRuntimeLaunchers, type RuntimeLaunchHandler } from './launch.js';
|
import { mkdtempSync, mkdirSync, writeFileSync, symlinkSync, rmSync } from 'node:fs';
|
||||||
|
import { tmpdir } from 'node:os';
|
||||||
|
import { join } from 'node:path';
|
||||||
|
import {
|
||||||
|
buildPiSkillArgs,
|
||||||
|
enumerateSkillDirs,
|
||||||
|
piForceSkillNames,
|
||||||
|
registerRuntimeLaunchers,
|
||||||
|
type RuntimeLaunchHandler,
|
||||||
|
} from './launch.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tests for the commander wiring between `mosaic <runtime>` / `mosaic yolo <runtime>`
|
* Tests for the commander wiring between `mosaic <runtime>` / `mosaic yolo <runtime>`
|
||||||
@@ -23,6 +32,7 @@ function buildProgram(handler: RuntimeLaunchHandler): Command {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const fakeSkills = ['--skill', '/skills/test-driven-development', '--skill', '/skills/pdf'];
|
const fakeSkills = ['--skill', '/skills/test-driven-development', '--skill', '/skills/pdf'];
|
||||||
|
const fakeForced = ['--skill', '/skills/mosaic-tools'];
|
||||||
|
|
||||||
// `process.exit` returns `never`, so vi.spyOn demands a replacement with the
|
// `process.exit` returns `never`, so vi.spyOn demands a replacement with the
|
||||||
// same signature. We throw from the mock to short-circuit into test-land.
|
// same signature. We throw from the mock to short-circuit into test-land.
|
||||||
@@ -66,16 +76,42 @@ describe('registerRuntimeLaunchers — non-yolo subcommands', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('buildPiSkillArgs', () => {
|
describe('buildPiSkillArgs', () => {
|
||||||
it('defaults to disabling Pi skill discovery to keep startup context small', () => {
|
it('disables auto-discovery but force-loads fleet-critical skills by default', () => {
|
||||||
expect(buildPiSkillArgs([], {}, fakeSkills)).toEqual(['--no-skills']);
|
expect(buildPiSkillArgs([], {}, fakeSkills, fakeForced)).toEqual([
|
||||||
|
'--no-skills',
|
||||||
|
'--skill',
|
||||||
|
'/skills/mosaic-tools',
|
||||||
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('keeps explicit user skills while disabling automatic discovery', () => {
|
it('ignores _runtimeArgs (user --skill flags reach Pi via the launch handler, not here)', () => {
|
||||||
expect(buildPiSkillArgs(['--skill', '/tmp/custom'], {}, fakeSkills)).toEqual(['--no-skills']);
|
expect(buildPiSkillArgs(['--skill', '/tmp/custom'], {}, fakeSkills, fakeForced)).toEqual([
|
||||||
|
'--no-skills',
|
||||||
|
'--skill',
|
||||||
|
'/skills/mosaic-tools',
|
||||||
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('supports legacy all-skills mode without double-loading settings skills', () => {
|
it('emits only --no-skills when no forced skills are present on disk', () => {
|
||||||
expect(buildPiSkillArgs([], { MOSAIC_PI_SKILL_MODE: 'all' }, fakeSkills)).toEqual([
|
expect(buildPiSkillArgs([], {}, fakeSkills, [])).toEqual(['--no-skills']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('all-skills mode merges the forced set in without duplicating discovered skills', () => {
|
||||||
|
expect(buildPiSkillArgs([], { MOSAIC_PI_SKILL_MODE: 'all' }, fakeSkills, fakeForced)).toEqual([
|
||||||
|
'--no-skills',
|
||||||
|
'--skill',
|
||||||
|
'/skills/test-driven-development',
|
||||||
|
'--skill',
|
||||||
|
'/skills/pdf',
|
||||||
|
'--skill',
|
||||||
|
'/skills/mosaic-tools',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('all-skills mode does not double-load a forced skill already discovered', () => {
|
||||||
|
expect(
|
||||||
|
buildPiSkillArgs([], { MOSAIC_PI_SKILL_MODE: 'all' }, fakeSkills, ['--skill', '/skills/pdf']),
|
||||||
|
).toEqual([
|
||||||
'--no-skills',
|
'--no-skills',
|
||||||
'--skill',
|
'--skill',
|
||||||
'/skills/test-driven-development',
|
'/skills/test-driven-development',
|
||||||
@@ -84,8 +120,117 @@ describe('buildPiSkillArgs', () => {
|
|||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('supports native Pi discovery when explicitly requested', () => {
|
it('force-loads fleet skills under native Pi discovery when not already discoverable', () => {
|
||||||
expect(buildPiSkillArgs([], { MOSAIC_PI_SKILL_MODE: 'discover' }, fakeSkills)).toEqual([]);
|
// Empty native set => Pi would not find mosaic-tools on its own, so force it.
|
||||||
|
expect(
|
||||||
|
buildPiSkillArgs([], { MOSAIC_PI_SKILL_MODE: 'discover' }, fakeSkills, fakeForced, new Set()),
|
||||||
|
).toEqual(['--skill', '/skills/mosaic-tools']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('discover mode drops a forced skill Pi already discovers natively (no double-load)', () => {
|
||||||
|
// mosaic-tools is reachable from a Pi native root, so native discovery
|
||||||
|
// covers it — forcing it again would register the same skill twice.
|
||||||
|
expect(
|
||||||
|
buildPiSkillArgs(
|
||||||
|
[],
|
||||||
|
{ MOSAIC_PI_SKILL_MODE: 'discover' },
|
||||||
|
fakeSkills,
|
||||||
|
fakeForced,
|
||||||
|
new Set(['/skills/mosaic-tools']),
|
||||||
|
),
|
||||||
|
).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('discover mode keeps a forced skill that no native root provides', () => {
|
||||||
|
expect(
|
||||||
|
buildPiSkillArgs(
|
||||||
|
[],
|
||||||
|
{ MOSAIC_PI_SKILL_MODE: 'discover' },
|
||||||
|
fakeSkills,
|
||||||
|
fakeForced,
|
||||||
|
new Set(['/skills/some-other-skill']),
|
||||||
|
),
|
||||||
|
).toEqual(['--skill', '/skills/mosaic-tools']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('discover mode collapses a forced skill listed twice to a single --skill', () => {
|
||||||
|
// Mirror 'all' mode: intra-forced-set duplicates (same realpath) dedup.
|
||||||
|
expect(
|
||||||
|
buildPiSkillArgs(
|
||||||
|
[],
|
||||||
|
{ MOSAIC_PI_SKILL_MODE: 'discover' },
|
||||||
|
fakeSkills,
|
||||||
|
['--skill', '/skills/mosaic-tools', '--skill', '/skills/mosaic-tools'],
|
||||||
|
new Set(),
|
||||||
|
),
|
||||||
|
).toEqual(['--skill', '/skills/mosaic-tools']);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('enumerateSkillDirs (real FS)', () => {
|
||||||
|
let root: string;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
root = mkdtempSync(join(tmpdir(), 'mosaic-skills-'));
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
rmSync(root, { recursive: true, force: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
function makeSkill(parent: string, name: string): string {
|
||||||
|
const dir = join(parent, name);
|
||||||
|
mkdirSync(dir, { recursive: true });
|
||||||
|
writeFileSync(join(dir, 'SKILL.md'), `# ${name}\n`);
|
||||||
|
return dir;
|
||||||
|
}
|
||||||
|
|
||||||
|
it('accepts a symlinked skill dir (regression: synced fleet skills are symlinks)', () => {
|
||||||
|
// Real skill lives under `canonical/`; the scanned root only has a symlink to it.
|
||||||
|
const canonical = makeSkill(join(root, 'canonical'), 'mosaic-tools');
|
||||||
|
const scanned = join(root, 'scanned');
|
||||||
|
mkdirSync(scanned, { recursive: true });
|
||||||
|
symlinkSync(canonical, join(scanned, 'mosaic-tools'), 'dir');
|
||||||
|
|
||||||
|
expect(enumerateSkillDirs([scanned])).toEqual(['--skill', join(scanned, 'mosaic-tools')]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('dedups by real path when the same skill is reachable from two roots', () => {
|
||||||
|
// Root A holds the real dir; root B symlinks to it — one --skill, not two.
|
||||||
|
const rootA = join(root, 'a');
|
||||||
|
const rootB = join(root, 'b');
|
||||||
|
const real = makeSkill(rootA, 'mosaic-tools');
|
||||||
|
mkdirSync(rootB, { recursive: true });
|
||||||
|
symlinkSync(real, join(rootB, 'mosaic-tools'), 'dir');
|
||||||
|
|
||||||
|
expect(enumerateSkillDirs([rootA, rootB])).toEqual(['--skill', real]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('skips directories without a SKILL.md and missing roots', () => {
|
||||||
|
mkdirSync(join(root, 'present', 'not-a-skill'), { recursive: true });
|
||||||
|
makeSkill(join(root, 'present'), 'real-skill');
|
||||||
|
|
||||||
|
expect(enumerateSkillDirs([join(root, 'present'), join(root, 'does-not-exist')])).toEqual([
|
||||||
|
'--skill',
|
||||||
|
join(root, 'present', 'real-skill'),
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('piForceSkillNames', () => {
|
||||||
|
it('defaults to mosaic-tools when MOSAIC_PI_FORCE_SKILLS is unset', () => {
|
||||||
|
expect(piForceSkillNames({})).toEqual(['mosaic-tools']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('treats an empty string as "disable force-loading" (distinct from unset)', () => {
|
||||||
|
expect(piForceSkillNames({ MOSAIC_PI_FORCE_SKILLS: '' })).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('parses a colon list, trimming blanks and whitespace', () => {
|
||||||
|
expect(piForceSkillNames({ MOSAIC_PI_FORCE_SKILLS: 'mosaic-tools: mosaic-gitea ::' })).toEqual([
|
||||||
|
'mosaic-tools',
|
||||||
|
'mosaic-gitea',
|
||||||
|
]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,15 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { execFileSync, execSync, spawnSync } from 'node:child_process';
|
import { execFileSync, execSync, spawnSync } from 'node:child_process';
|
||||||
import { existsSync, mkdirSync, readFileSync, writeFileSync, readdirSync, rmSync } from 'node:fs';
|
import {
|
||||||
|
existsSync,
|
||||||
|
mkdirSync,
|
||||||
|
readFileSync,
|
||||||
|
writeFileSync,
|
||||||
|
readdirSync,
|
||||||
|
realpathSync,
|
||||||
|
rmSync,
|
||||||
|
} from 'node:fs';
|
||||||
import { createRequire } from 'node:module';
|
import { createRequire } from 'node:module';
|
||||||
import { homedir } from 'node:os';
|
import { homedir } from 'node:os';
|
||||||
import { join, dirname } from 'node:path';
|
import { join, dirname } from 'node:path';
|
||||||
@@ -428,25 +436,74 @@ function ensureRuntimeConfig(runtime: RuntimeName, destPath: string): void {
|
|||||||
|
|
||||||
// ─── Pi skill/extension discovery ────────────────────────────────────────────
|
// ─── Pi skill/extension discovery ────────────────────────────────────────────
|
||||||
|
|
||||||
function discoverPiSkills(): string[] {
|
/** Resolve a skill dir to its canonical real path so symlinked duplicates
|
||||||
|
* (e.g. ~/.pi/agent/skills/X -> ~/.config/mosaic/skills/X) collapse to one key.
|
||||||
|
* Falls back to the literal path if it can't be resolved (e.g. broken link). */
|
||||||
|
function skillRealPath(dir: string): string {
|
||||||
|
try {
|
||||||
|
return realpathSync(dir);
|
||||||
|
} catch {
|
||||||
|
return dir;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Skill roots Pi auto-discovers natively (no `--skill` needed): its global
|
||||||
|
* skills dir and the project-local one relative to the launch cwd. */
|
||||||
|
function piNativeSkillRoots(cwd: string = process.cwd()): string[] {
|
||||||
|
return [join(homedir(), '.pi', 'agent', 'skills'), join(cwd, '.pi', 'skills')];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Enumerate skill dirs under a set of roots, deduped by real path. A directory
|
||||||
|
* counts as a skill when it (or its symlink target) contains a SKILL.md.
|
||||||
|
* Exported for tests (real-FS coverage of symlink acceptance + realpath dedup). */
|
||||||
|
export function enumerateSkillDirs(roots: string[]): string[] {
|
||||||
|
const seen = new Set<string>();
|
||||||
const args: string[] = [];
|
const args: string[] = [];
|
||||||
for (const skillsRoot of [join(MOSAIC_HOME, 'skills'), join(MOSAIC_HOME, 'skills-local')]) {
|
for (const skillsRoot of roots) {
|
||||||
if (!existsSync(skillsRoot)) continue;
|
if (!existsSync(skillsRoot)) continue;
|
||||||
try {
|
try {
|
||||||
for (const entry of readdirSync(skillsRoot, { withFileTypes: true })) {
|
for (const entry of readdirSync(skillsRoot, { withFileTypes: true })) {
|
||||||
if (!entry.isDirectory()) continue;
|
// Synced fleet skills land as symlinks, so accept both dirs and links.
|
||||||
|
if (!entry.isDirectory() && !entry.isSymbolicLink()) continue;
|
||||||
const skillDir = join(skillsRoot, entry.name);
|
const skillDir = join(skillsRoot, entry.name);
|
||||||
if (existsSync(join(skillDir, 'SKILL.md'))) {
|
if (!existsSync(join(skillDir, 'SKILL.md'))) continue;
|
||||||
args.push('--skill', skillDir);
|
const key = skillRealPath(skillDir);
|
||||||
}
|
if (seen.has(key)) continue;
|
||||||
|
seen.add(key);
|
||||||
|
args.push('--skill', skillDir);
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
// skip
|
// skip unreadable roots
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return args;
|
return args;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Every skill dir Pi would link under `MOSAIC_PI_SKILL_MODE=all`: the Mosaic
|
||||||
|
* global/local catalog plus Pi's own native roots. `--no-skills` suppresses
|
||||||
|
* native auto-discovery, so 'all' must re-add the native roots explicitly or
|
||||||
|
* they would be silently dropped. Deduped by real path. */
|
||||||
|
function discoverPiSkills(cwd: string = process.cwd()): string[] {
|
||||||
|
return enumerateSkillDirs([
|
||||||
|
join(MOSAIC_HOME, 'skills'),
|
||||||
|
join(MOSAIC_HOME, 'skills-local'),
|
||||||
|
...piNativeSkillRoots(cwd),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Real paths of skills Pi will auto-discover from its native roots. Used to
|
||||||
|
* drop redundant force-loads in 'discover' mode (which keeps native discovery
|
||||||
|
* on) so the same skill is not registered twice. */
|
||||||
|
function piNativeSkillRealPaths(cwd: string = process.cwd()): Set<string> {
|
||||||
|
const args = enumerateSkillDirs(piNativeSkillRoots(cwd));
|
||||||
|
const set = new Set<string>();
|
||||||
|
for (let i = 1; i < args.length; i += 2) {
|
||||||
|
const dir = args[i];
|
||||||
|
if (dir !== undefined) set.add(skillRealPath(dir));
|
||||||
|
}
|
||||||
|
return set;
|
||||||
|
}
|
||||||
|
|
||||||
type PiSkillMode = 'none' | 'all' | 'discover';
|
type PiSkillMode = 'none' | 'all' | 'discover';
|
||||||
|
|
||||||
function normalizePiSkillMode(env: NodeJS.ProcessEnv): PiSkillMode {
|
function normalizePiSkillMode(env: NodeJS.ProcessEnv): PiSkillMode {
|
||||||
@@ -455,22 +512,96 @@ function normalizePiSkillMode(env: NodeJS.ProcessEnv): PiSkillMode {
|
|||||||
return 'none';
|
return 'none';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fleet-critical Pi skills that are force-loaded on every Pi launch regardless
|
||||||
|
* of MOSAIC_PI_SKILL_MODE. They cover the highest-frequency cross-agent and
|
||||||
|
* git-provider operations where Pi workers historically improvised raw CLIs
|
||||||
|
* (raw `tmux send-keys`, raw `tea`/`gh`/`glab`) instead of the maintained
|
||||||
|
* `~/.config/mosaic/tools/` wrappers.
|
||||||
|
*
|
||||||
|
* An explicit `--skill <dir>` overrides `--no-skills` for that path, so forcing
|
||||||
|
* a single targeted skill surfaces the must-use toolkit without loading the full
|
||||||
|
* ~100-skill catalog (context bloat). Missing skills are skipped silently, so
|
||||||
|
* this is a no-op until the named skill is synced into ~/.config/mosaic/skills/.
|
||||||
|
*
|
||||||
|
* Override with MOSAIC_PI_FORCE_SKILLS (colon-separated skill dir names; set to
|
||||||
|
* an empty string to disable force-loading entirely).
|
||||||
|
*/
|
||||||
|
const DEFAULT_PI_FORCE_SKILLS = ['mosaic-tools'];
|
||||||
|
|
||||||
|
export function piForceSkillNames(env: NodeJS.ProcessEnv): string[] {
|
||||||
|
const override = env['MOSAIC_PI_FORCE_SKILLS'];
|
||||||
|
if (override === undefined) return DEFAULT_PI_FORCE_SKILLS;
|
||||||
|
return override
|
||||||
|
.split(':')
|
||||||
|
.map((name) => name.trim())
|
||||||
|
.filter(Boolean);
|
||||||
|
}
|
||||||
|
|
||||||
|
function forcedPiSkillArgs(env: NodeJS.ProcessEnv = process.env): string[] {
|
||||||
|
const args: string[] = [];
|
||||||
|
for (const name of piForceSkillNames(env)) {
|
||||||
|
const skillDir = join(MOSAIC_HOME, 'skills', name);
|
||||||
|
if (existsSync(join(skillDir, 'SKILL.md'))) {
|
||||||
|
args.push('--skill', skillDir);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return args;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Concatenate `--skill <dir>` arg groups, dropping any skill already seen.
|
||||||
|
* Dedup is by real path, so a forced skill and the same skill reached via a
|
||||||
|
* different (e.g. symlinked) directory collapse to a single `--skill`. */
|
||||||
|
function mergeSkillArgs(...groups: string[][]): string[] {
|
||||||
|
const seen = new Set<string>();
|
||||||
|
const out: string[] = [];
|
||||||
|
for (const group of groups) {
|
||||||
|
for (let i = 0; i < group.length; i += 2) {
|
||||||
|
const dir = group[i + 1];
|
||||||
|
if (group[i] !== '--skill' || dir === undefined) continue;
|
||||||
|
const key = skillRealPath(dir);
|
||||||
|
if (seen.has(key)) continue;
|
||||||
|
seen.add(key);
|
||||||
|
out.push('--skill', dir);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
export function buildPiSkillArgs(
|
export function buildPiSkillArgs(
|
||||||
_runtimeArgs: string[],
|
_runtimeArgs: string[],
|
||||||
env: NodeJS.ProcessEnv = process.env,
|
env: NodeJS.ProcessEnv = process.env,
|
||||||
discoveredSkillArgs: string[] = discoverPiSkills(),
|
discoveredSkillArgs: string[] = discoverPiSkills(),
|
||||||
|
forcedSkillArgs: string[] = forcedPiSkillArgs(env),
|
||||||
|
nativeSkillRealPaths: Set<string> = piNativeSkillRealPaths(),
|
||||||
): string[] {
|
): string[] {
|
||||||
const mode = normalizePiSkillMode(env);
|
const mode = normalizePiSkillMode(env);
|
||||||
|
|
||||||
if (mode === 'discover') {
|
if (mode === 'discover') {
|
||||||
return [];
|
// Native Pi discovery stays on, so only force-load fleet skills it will NOT
|
||||||
|
// already find under its native roots — otherwise the same skill is
|
||||||
|
// registered twice (once natively, once via --skill). mergeSkillArgs first
|
||||||
|
// collapses any intra-forced-set realpath duplicates, mirroring 'all' mode.
|
||||||
|
const deduped = mergeSkillArgs(forcedSkillArgs);
|
||||||
|
const out: string[] = [];
|
||||||
|
for (let i = 0; i < deduped.length; i += 2) {
|
||||||
|
const dir = deduped[i + 1];
|
||||||
|
if (deduped[i] !== '--skill' || dir === undefined) continue;
|
||||||
|
if (nativeSkillRealPaths.has(skillRealPath(dir))) continue;
|
||||||
|
out.push('--skill', dir);
|
||||||
|
}
|
||||||
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mode === 'all') {
|
if (mode === 'all') {
|
||||||
return ['--no-skills', ...discoveredSkillArgs];
|
// 'all' links the full catalog; merge in the forced set so fleet-critical
|
||||||
|
// skills are guaranteed present even if they live only under skills-local/.
|
||||||
|
// discoverPiSkills already covers Pi's native roots, which `--no-skills`
|
||||||
|
// would otherwise suppress.
|
||||||
|
return ['--no-skills', ...mergeSkillArgs(discoveredSkillArgs, forcedSkillArgs)];
|
||||||
}
|
}
|
||||||
|
|
||||||
return ['--no-skills'];
|
return ['--no-skills', ...forcedSkillArgs];
|
||||||
}
|
}
|
||||||
|
|
||||||
function discoverPiExtension(): string[] {
|
function discoverPiExtension(): string[] {
|
||||||
|
|||||||
Reference in New Issue
Block a user