Compare commits

..
Author SHA1 Message Date
ops-03 ce765fa70e merge: absorb next into fix/1256-fleet-pane-path-node — bring image pin (cb9a0d1) and 4 days of base drift; no functional change to branch content
ci/woodpecker/pr/ci Pipeline was successful
2026-08-20 11:20:34 -05:00
fred 6dc35e5bf2 fleet(test): attribute FIRST-position to the fixture, not to the fix
ci/woodpecker/pr/ci Pipeline failed
ci/woodpecker/manual/ci Pipeline failed
Comment-only. The colon-padding note said the anchored regex 'rejects the
directory in FIRST position — which is exactly where the fix puts it'. True in
this fixture, which runs env -i with no MOSAIC_RUNTIME_BIN so the bootstrap
directory leads, but stated as a property of the fix. In general the directory
sits second, after MOSAIC_RUNTIME_BIN, and it reads as a contradiction of the
source comment corrected in c7ee3cb.

Found in review by rhodey, who also noted the same sentence is live in the PR
description. No executable line changed.
2026-08-16 17:20:01 -05:00
fred c7ee3cb5ce fix(fleet): correct ordering claim in pane-PATH comment
ci/woodpecker/pr/ci Pipeline failed
The comment said the bootstrapped-node candidate 'is first'; it is inserted
second, after MOSAIC_RUNTIME_BIN. The ordering itself is correct — an explicit
override should outrank a bootstrap — so this is a wording defect in a
load-bearing comment, not a behaviour change. No executable line is touched.

Found in review by mos-claude (review 166).
2026-08-16 17:01:18 -05:00
fred a972249a2b fleet: put the bootstrapped Node on PANE_PATH (#1256)
ci/woodpecker/pr/ci Pipeline failed
On a host with no system Node, tools/install.sh bootstraps one into
~/.mosaic/node/current/bin and records it in ~/.profile. The fleet unit runs
`env -i ... bash --noprofile --norc`, so ~/.profile is never read — that is
deliberate — and _build_runtime_bin_prefix did not name the directory itself.
Runtime binaries are `#!/usr/bin/env node`, so the pane resolved `mosaic` and
then died on `env: 'node': No such file or directory` after an install that
reported success. Measured on a greenfield VM.

The existing `npm config get prefix` branch cannot cover it: that reports a
package prefix (~/.npm-global), never a Node runtime directory.

The test case asserts the property rather than the string — it runs the pane for
real with a Node-shebang `mosaic` and requires the pane to have executed. A PATH
substring check would pass on a fix that put the directory in the wrong position.
Red without the launcher change, green with it, rest of the suite unaffected.
2026-08-16 15:02:04 -05:00
21 changed files with 62 additions and 683 deletions
-4
View File
@@ -1,4 +0,0 @@
{
"integration_trunk": "next",
"release_branch": "main"
}
-67
View File
@@ -81,73 +81,6 @@ pnpm format:check # Prettier check
pnpm build # Build all packages and applications pnpm build # Build all packages and applications
``` ```
## Branch Model and Merge Process — `main` and `next` (CANONICAL)
**Every contribution targets `next` first. No exceptions.** Features, fixes, tests,
docs, and policy changes all take the same route; urgency changes queue priority,
never the route. Agents never commit to or merge into `main`.
| Branch | Role | Who merges into it |
| ------ | ---------------------------------------------------------------- | --------------------------------------------------------------------------- |
| `next` | Integration trunk — the only PR target for contributions | The designated merge-gate agent, after all gates pass. Never the PR author. |
| `main` | Stable/release line — receives promotion merges from `next` only | Jason only (or an agent he explicitly delegates for a named promotion). |
### Contribution sequencing (in order, no skipping)
1. **Issue first.** Work is tracked in a Gitea issue before a branch exists. The
issue number appears in the branch name and the PR body.
2. **Branch from the current `origin/next` head.** Name it
`feat/…`, `fix/…`, `docs/…`, or `test/…` with the issue number
(e.g. `docs/1214-branch-process`). Record the base SHA in the PR body.
3. **Develop with evidence.** Applicable tests accompany the change. Hooks are
never bypassed (`--no-verify` is prohibited). Stage explicit paths — never
`git add -A`.
4. **Open the PR against `next`.** The body states: scope, base SHA,
verification commands with results, and any known pre-existing failures on
the base — documented, not retried to green and not absorbed silently.
5. **CI must be terminal-green on the exact head.** All bounded Woodpecker
steps succeed (`verify-terminal-green` contract). Pipelines for fork PRs
start `blocked`; a maintainer approves the run — approving CI is not
approving the PR.
6. **Independent review. Self-merge is prohibited** — for every agent, on every
PR, including trivial ones. Where the change touches protected or
contract-bearing content, the reviewer verifies the exact head
(exact-byte/exact-blob comparison), not a description of it. An `AMEND`
verdict returns the PR to its author; the reviewer's gate stays held until
a fresh exact head passes.
7. **Merge into `next`** happens only after CI green + review pass, pinned to
the reviewed head SHA (a post-review push voids the review).
8. **Promotion `next` → `main`** is a deliberate, Jason-owned reconciliation
merge — not part of any contribution's lifecycle. Contributors are done at
step 7.
### Responsibilities
- **Contributor** — base pinning, green CI, evidence in the PR body,
responding to AMEND verdicts, never merging own work.
- **Reviewer / merge gate** — independent verification on the exact head;
holds and lifts gates; executes the merge into `next`.
- **Orchestrator / adjudicator** — cross-PR sequencing, disposition when PRs
collide, conflict adjudication.
- **Jason** — `next``main` promotions, merge-authority grants, collaborator
and token provisioning. Agents cannot grant themselves or each other any of
these.
### Hotfixes and divergence
- A hotfix follows the same path: branch from `next`, PR to `next`, gates,
merge, then an expedited Jason-owned promotion if `main` needs it urgently.
Committing the fix to `main` directly is prohibited even under pressure.
- **Never land work on `main` that is not on `next`.** This has happened
(issue #1152's goal controller reached `main` without reaching `next`) and
every later PR paid for it. If it happens anyway: transplant the work onto
a `next`-based branch with provenance-preserving commits
(`git cherry-pick -x` or explicit SHA references in the messages), PR it
through the normal gates, and let promotion re-align `main`. Do not
hand-patch `main` to compensate.
- Force-pushing a branch you do not own is prohibited; rebasing your own PR
branch is fine before review, and voids any review already given.
## Database and Local Runtime Safety ## Database and Local Runtime Safety
- Current local data-layer work uses in-process PGlite; leave `DATABASE_URL` unset. - Current local data-layer work uses in-process PGlite; leave `DATABASE_URL` unset.
+1 -2
View File
@@ -84,7 +84,6 @@ is re-seeded a genuinely missing core file is a stop-and-report condition — no
Confirm: required + situational tests passed (primary gate); aligned to `docs/PRD.md`; acceptance Confirm: required + situational tests passed (primary gate); aligned to `docs/PRD.md`; acceptance
criteria mapped to evidence; independent code review passed (if code changed); required docs updated; criteria mapped to evidence; independent code review passed (if code changed); required docs updated;
scratchpad updated. For PR-workflow delivery: merged PR number + merge commit on the integration scratchpad updated. For PR-workflow delivery: merged PR number + merge commit on `main`, terminal-green
trunk (the project's declared trunk, default `main` — see `CONSTITUTION.md` Hard Gates), terminal-green
CI, linked issue closed (or `docs/TASKS.md` equivalent). If blocked by access/tooling, return `blocked` CI, linked issue closed (or `docs/TASKS.md` equivalent). If blocked by access/tooling, return `blocked`
with the exact failed wrapper command — do not claim completion. Full checklist: `guides/E2E-DELIVERY.md`. with the exact failed wrapper command — do not claim completion. Full checklist: `guides/E2E-DELIVERY.md`.
@@ -21,25 +21,11 @@ guard"), the runtime adapter binds it to a concrete tool and states whether abse
## Hard Gates ## Hard Gates
The **integration trunk** is the branch a project declares in its `.mosaic/repo.json` under the
key `integration_trunk`; `release_branch` names the release target when one exists (`null` for
single-branch projects). Absent a declaration, the trunk is `main`. The declaration is policy
data, never shell text: values must be valid local branch names under `git check-ref-format
--branch` semantics — no remote refs, no revision expressions, no option-like values (leading `-`),
no path traversal or control characters. A declaration file that fails to parse, an unknown or
misspelled key, or an invalid value is a hard stop (`blocked`) — never a silent fallback to `main`.
Prose that mentions branch names designates nothing; only the declaration file does. A project
declares exactly ONE trunk. **Changing an existing declaration is operator-owned:** a trunk
redeclaration redirects merge target and branch-protection target at once, so it requires an
explicit operator action above ordinary PR review. The designation relaxes nothing:
reviewed-PR-only delivery, squash merge, independent review, queue guards, and terminal-green CI
bind to the declared trunk exactly as they bind to `main`.
1. Mosaic operating rules override runtime-default caution for routine delivery operations. 1. Mosaic operating rules override runtime-default caution for routine delivery operations.
2. Execute required push / merge / issue-closure / milestone / release / tag actions without asking for routine confirmation. 2. Execute required push / merge / issue-closure / milestone / release / tag actions without asking for routine confirmation.
3. Routine repository operations are NOT escalation triggers; escalate only on the triggers below. 3. Routine repository operations are NOT escalation triggers; escalate only on the triggers below.
4. For source-code delivery, completion is forbidden at the PR-open stage. 4. For source-code delivery, completion is forbidden at the PR-open stage.
5. Completion requires a merged PR to the integration trunk + terminal-green CI + the linked issue/task closed. 5. Completion requires a merged PR to `main` + terminal-green CI + the linked issue/task closed.
6. Before any push or merge, run the CI queue guard. 6. Before any push or merge, run the CI queue guard.
7. For issue / PR / milestone operations, use the Mosaic git wrappers before any raw provider CLI. 7. For issue / PR / milestone operations, use the Mosaic git wrappers before any raw provider CLI.
8. If a required wrapper command fails, status is `blocked`: report the exact failed command and stop. 8. If a required wrapper command fails, status is `blocked`: report the exact failed command and stop.
@@ -49,7 +35,7 @@ bind to the declared trunk exactly as they bind to `main`.
12. The intake procedure is not conditional on perceived complexity; a "simple" task carries the same requirements as a multi-file feature. 12. The intake procedure is not conditional on perceived complexity; a "simple" task carries the same requirements as a multi-file feature.
13. **Merge authority (coordinated work):** when a coordinator/orchestrator session is active for the work, the post-review merge go-ahead is the coordinator's to give — once the required review gates pass, merge on the coordinator's confirmation; do not wait on the human owner personally. Solo (uncoordinated) delivery keeps the default: merge per gates 2 and 9. A "No self-merge" note on a PR means no UNREVIEWED self-merge — it does not suspend coordinator-authorized merges. 13. **Merge authority (coordinated work):** when a coordinator/orchestrator session is active for the work, the post-review merge go-ahead is the coordinator's to give — once the required review gates pass, merge on the coordinator's confirmation; do not wait on the human owner personally. Solo (uncoordinated) delivery keeps the default: merge per gates 2 and 9. A "No self-merge" note on a PR means no UNREVIEWED self-merge — it does not suspend coordinator-authorized merges.
14. Never hardcode secrets; never emit credential values in any output (not even partially, not "to confirm"). 14. Never hardcode secrets; never emit credential values in any output (not even partially, not "to confirm").
15. Trunk-based git only: branch from the integration trunk, merge via a reviewed PR (squash), never push directly to the trunk. 15. Trunk-based git only: branch from `main`, merge via a reviewed PR (squash), never push directly to `main`.
16. If you modify source code, an independent review (author ≠ reviewer) must pass before completion. 16. If you modify source code, an independent review (author ≠ reviewer) must pass before completion.
## Integrity (quality gates are never bypassed) ## Integrity (quality gates are never bypassed)
+10 -12
View File
@@ -12,7 +12,7 @@ This guide covers how to bootstrap a project so AI agents (Claude, Codex, etc.)
4. Issue tracking is consistent across projects 4. Issue tracking is consistent across projects
5. Documentation standards and API contracts are enforced from day one 5. Documentation standards and API contracts are enforced from day one
6. PRD requirements are established before coding begins 6. PRD requirements are established before coding begins
7. Branching/merging is consistent: branch -> integration trunk (default `main`) via PR with squash-only merges 7. Branching/merging is consistent: `branch -> main` via PR with squash-only merges
8. Steered-autonomy execution is enabled so agents can run end-to-end with escalation-only human intervention 8. Steered-autonomy execution is enabled so agents can run end-to-end with escalation-only human intervention
## Agent Host Prerequisites ## Agent Host Prerequisites
@@ -206,7 +206,7 @@ Every runtime context file should contain:
6. **Issue tracking** — Issue and commit conventions 6. **Issue tracking** — Issue and commit conventions
7. **Code review** — Required review process 7. **Code review** — Required review process
8. **Runtime notes** — Runtime-specific behavior references 8. **Runtime notes** — Runtime-specific behavior references
9. **Branch and merge policy** — Trunk workflow (branch -> integration trunk via PR, squash-only) 9. **Branch and merge policy** — Trunk workflow (`branch -> main` via PR, squash-only)
10. **Autonomy and escalation policy** — Agent owns coding/review/PR/release/deploy lifecycle 10. **Autonomy and escalation policy** — Agent owns coding/review/PR/release/deploy lifecycle
--- ---
@@ -288,17 +288,15 @@ Reserve `0.1.0` for the MVP release milestone.
--- ---
## Step 5b: Configure Trunk Branch Protection (Hard Rule) ## Step 5b: Configure Main Branch Protection (Hard Rule)
Apply equivalent settings in Gitea, GitHub, or GitLab, targeting the project's integration trunk Apply equivalent settings in Gitea, GitHub, or GitLab:
(the branch its `.mosaic/repo.json` declares under `integration_trunk`; default `main` — see
`CONSTITUTION.md` Hard Gates):
1. Protect the integration trunk from direct pushes. 1. Protect `main` from direct pushes.
2. Require pull requests to merge into the integration trunk. 2. Require pull requests to merge into `main`.
3. Require required CI/status checks to pass before merge. 3. Require required CI/status checks to pass before merge.
4. Require code review approval before merge. 4. Require code review approval before merge.
5. Allow **squash merge only** for PRs into the integration trunk (disable merge commits and rebase merges for it). 5. Allow **squash merge only** for PRs into `main` (disable merge commits and rebase merges for `main`).
This enforces one merge strategy across human and agent workflows. This enforces one merge strategy across human and agent workflows.
@@ -515,9 +513,9 @@ After bootstrapping, verify:
- [ ] Git labels created (epic, feature, bug, task, etc.) - [ ] Git labels created (epic, feature, bug, task, etc.)
- [ ] Initial pre-MVP milestone created (0.0.1) - [ ] Initial pre-MVP milestone created (0.0.1)
- [ ] MVP milestone reserved for release (0.1.0) - [ ] MVP milestone reserved for release (0.1.0)
- [ ] The integration trunk is protected from direct pushes - [ ] `main` is protected from direct pushes
- [ ] PRs into the integration trunk are required - [ ] PRs into `main` are required
- [ ] Merge method for the integration trunk is squash-only - [ ] Merge method for `main` is squash-only
- [ ] Quality gates run successfully - [ ] Quality gates run successfully
- [ ] `.env.example` exists (if project uses env vars) - [ ] `.env.example` exists (if project uses env vars)
- [ ] CI/CD pipeline configured (if using Woodpecker/GitHub Actions) - [ ] CI/CD pipeline configured (if using Woodpecker/GitHub Actions)
@@ -4,11 +4,6 @@
## Overview ## Overview
> **Integration trunk:** the YAML examples in this guide use the default integration trunk `main`
> in branch conditions and version rules. A project that declares a different trunk in its
> `.mosaic/repo.json` under `integration_trunk` (see `CONSTITUTION.md` Hard Gates) substitutes its
> declared trunk wherever `main` appears as the trunk branch.
This guide covers the canonical CI/CD pattern used across projects. The pipeline runs in Woodpecker CI and follows this flow: This guide covers the canonical CI/CD pattern used across projects. The pipeline runs in Woodpecker CI and follows this flow:
``` ```
@@ -870,7 +865,7 @@ steps:
```yaml ```yaml
image: git.example.com/org/service@${IMAGE_DIGEST} image: git.example.com/org/service@${IMAGE_DIGEST}
``` ```
7. **Test on a short-lived non-trunk branch first** — open a PR and verify quality gates before merging to the integration trunk 7. **Test on a short-lived non-main branch first** — open a PR and verify quality gates before merging to `main`
8. **Verify images appear** in Gitea Packages tab after successful pipeline 8. **Verify images appear** in Gitea Packages tab after successful pipeline
## Terminal-Green Full-Step Contract ## Terminal-Green Full-Step Contract
@@ -911,7 +906,7 @@ For source-code delivery, completion is not allowed at "PR opened" stage.
Required sequence: Required sequence:
1. Merge PR to the integration trunk (squash) via Mosaic wrapper. 1. Merge PR to `main` (squash) via Mosaic wrapper.
2. Monitor CI to terminal status: 2. Monitor CI to terminal status:
```bash ```bash
~/.config/mosaic/tools/git/pr-ci-wait.sh -n <PR_NUMBER> ~/.config/mosaic/tools/git/pr-ci-wait.sh -n <PR_NUMBER>
@@ -1117,5 +1112,5 @@ If a project currently uses Verdaccio (e.g., U-Connect at `npm.uscllc.net`), fol
### Pipeline runs Docker builds on pull requests ### Pipeline runs Docker builds on pull requests
- Verify `when` clause on Docker build steps restricts to the integration trunk (`branch: [main]` by default) - Verify `when` clause on Docker build steps restricts to `branch: [main]`
- Pull requests should only run quality gates, not build/push images - Pull requests should only run quality gates, not build/push images
@@ -10,10 +10,9 @@ If implementation diverges from `docs/PRD.md` or `docs/PRD.json` without PRD upd
Merge strategy enforcement (HARD RULE): Merge strategy enforcement (HARD RULE):
- The integration trunk is the branch the project's `.mosaic/repo.json` declares under `integration_trunk` (default: `main`) — see `CONSTITUTION.md` Hard Gates. - PR target for delivery is `main`.
- PR target for delivery is the integration trunk. - Direct pushes to `main` are prohibited.
- Direct pushes to the integration trunk are prohibited. - Merge to `main` MUST be squash-only.
- Merge to the integration trunk MUST be squash-only.
- Use `~/.config/mosaic/tools/git/pr-merge.sh -n {PR_NUMBER} -m squash --expect-head {approved_full_sha}` (or PowerShell equivalent). - Use `~/.config/mosaic/tools/git/pr-merge.sh -n {PR_NUMBER} -m squash --expect-head {approved_full_sha}` (or PowerShell equivalent).
An estate MAY carry a documented exception for a repository whose gates are commit hooks rather An estate MAY carry a documented exception for a repository whose gates are commit hooks rather
@@ -66,19 +65,6 @@ Each of these produced a wrong conclusion before it was written down.
conclusion drawn from it describes the wrong tree. Confirm `git rev-parse --show-toplevel` conclusion drawn from it describes the wrong tree. Confirm `git rev-parse --show-toplevel`
is the tree you think it is before trusting any git output. is the tree you think it is before trusting any git output.
13. **Run the repository's PINNED tool version.** `npx <tool>` resolves a local `node_modules`
install when one is present and fetches the latest release when one is not, so the same
command answers differently depending on where it ran. A reviewer measuring in a fresh clone
or a detached worktree — which is exactly where reviewers measure — has no `node_modules` and
silently gets the latest release instead of the pinned one. Measured on mosaicstack#1313: the
lockfile pins prettier 3.8.1, under which three guides pass; a version-less `npx` in a
worktree resolved 3.9.6, under which the same three fail; and 3.0.0, the floor of the declared
`^3.0.0` range, fails a different one. Three versions, three verdicts, identical bytes. Use
`node_modules/.bin/<tool>`, or name the version the lockfile pins.
14. **A formatter or linter declared as a range is a dated verdict, not a fact.** If a lockfile
pins it, the gate is reproducible today and will disagree with itself the day the pin moves.
Report a formatting failure with the version that produced it, always.
### Feedback Categories ### Feedback Categories
- **Blocker**: must fix before merge (security, bugs, test failures) - **Blocker**: must fix before merge (security, bugs, test failures)
@@ -198,8 +184,8 @@ Use `~/.config/mosaic/templates/docs/DOCUMENTATION-CHECKLIST.md` whenever code/A
# List the issue being addressed # List the issue being addressed
~/.config/mosaic/tools/git/issue-list.sh -i {issue-number} ~/.config/mosaic/tools/git/issue-list.sh -i {issue-number}
# View the changes (diff against the integration trunk; default: main) # View the changes
git diff {integration_trunk}...HEAD git diff main...HEAD
``` ```
### Providing Feedback ### Providing Feedback
@@ -228,4 +214,4 @@ This pattern appears in 3 places. A shared helper would reduce duplication.
2. If changes requested, assign back to author 2. If changes requested, assign back to author
3. If approved, note approval in issue comments 3. If approved, note approval in issue comments
4. For merges, ensure CI passes first 4. For merges, ensure CI passes first
5. Merge PR to the integration trunk with squash strategy only 5. Merge PR to `main` with squash strategy only
@@ -78,7 +78,7 @@ For implementation work, you MUST run this cycle in order:
7. `commit` - commit only when the logical unit passes tests and review. 7. `commit` - commit only when the logical unit passes tests and review.
8. `pre-push queue guard` - before pushing, wait for running/queued project pipelines to clear: `~/.config/mosaic/tools/git/ci-queue-wait.sh --purpose push`. 8. `pre-push queue guard` - before pushing, wait for running/queued project pipelines to clear: `~/.config/mosaic/tools/git/ci-queue-wait.sh --purpose push`.
9. `push` - push immediately after queue guard passes. 9. `push` - push immediately after queue guard passes.
10. `PR integration` - if external git provider is available, create/update PR to the integration trunk (the project's declared trunk, default `main`) and merge with required strategy via Mosaic wrappers. 10. `PR integration` - if external git provider is available, create/update PR to `main` and merge with required strategy via Mosaic wrappers.
11. `pre-merge queue guard` - before merging PR, wait for running/queued project pipelines on the exact PR head to clear: `~/.config/mosaic/tools/git/ci-queue-wait.sh --purpose merge -B <PR_HEAD_BRANCH> -R <PR_HEAD_OWNER/REPO> --sha <PR_HEAD_FULL_SHA>`. 11. `pre-merge queue guard` - before merging PR, wait for running/queued project pipelines on the exact PR head to clear: `~/.config/mosaic/tools/git/ci-queue-wait.sh --purpose merge -B <PR_HEAD_BRANCH> -R <PR_HEAD_OWNER/REPO> --sha <PR_HEAD_FULL_SHA>`.
12. `CI/pipeline verification` - wait for terminal CI status and require green before completion (`~/.config/mosaic/tools/git/pr-ci-wait.sh` for PR-based workflow). 12. `CI/pipeline verification` - wait for terminal CI status and require green before completion (`~/.config/mosaic/tools/git/pr-ci-wait.sh` for PR-based workflow).
13. `issue closure` - close linked external issue (or close internal `docs/TASKS.md` task ref when provider is unavailable). 13. `issue closure` - close linked external issue (or close internal `docs/TASKS.md` task ref when provider is unavailable).
@@ -199,7 +199,7 @@ Before running this checklist, pause and self-interrogate: did I fulfill the use
10. No unresolved blocker hidden. 10. No unresolved blocker hidden.
11. If deployment is in scope, deployment target, release version, and post-deploy verification evidence are documented. 11. If deployment is in scope, deployment target, release version, and post-deploy verification evidence are documented.
12. `docs/TASKS.md` status and issue/internal references are updated to match delivered work. 12. `docs/TASKS.md` status and issue/internal references are updated to match delivered work.
13. If source code changed and external provider is available: PR merged to the integration trunk (squash), with merge evidence recorded. 13. If source code changed and external provider is available: PR merged to `main` (squash), with merge evidence recorded.
14. CI/pipeline status is terminal green for the merged PR/head commit. 14. CI/pipeline status is terminal green for the merged PR/head commit.
15. Linked external issue is closed (or internal task ref is closed when no provider exists). 15. Linked external issue is closed (or internal task ref is closed when no provider exists).
16. If any of items 13-15 fail due access/tooling, report `blocked` with exact failed wrapper command and do not claim completion. 16. If any of items 13-15 fail due access/tooling, report `blocked` with exact failed wrapper command and do not claim completion.
@@ -253,7 +253,7 @@ status → mission → run → repeat
- [ ] All milestone tasks in TASKS.md are `done` - [ ] All milestone tasks in TASKS.md are `done`
- [ ] CI/pipeline green - [ ] CI/pipeline green
- [ ] PR merged to the integration trunk - [ ] PR merged to `main`
- [ ] Issues closed - [ ] Issues closed
- [ ] Update manifest: milestone status → completed - [ ] Update manifest: milestone status → completed
- [ ] Update scratchpad: session log entry - [ ] Update scratchpad: session log entry
@@ -15,7 +15,7 @@ mosaic claude -p "Read ~/.config/mosaic/skills/nestjs-best-practices/SKILL.md th
- You MUST keep the TASKS.md file updated with agent and tasks statuses. - You MUST keep the TASKS.md file updated with agent and tasks statuses.
- You MUST keep `docs/` root clean. Reports and working artifacts MUST be stored in scoped folders (`docs/reports/`, `docs/tasks/`, `docs/releases/`, `docs/scratchpads/`). - You MUST keep `docs/` root clean. Reports and working artifacts MUST be stored in scoped folders (`docs/reports/`, `docs/tasks/`, `docs/releases/`, `docs/scratchpads/`).
- You MUST enforce plan/token usage budgets when provided, and adapt orchestration strategy to remain within limits. - You MUST enforce plan/token usage budgets when provided, and adapt orchestration strategy to remain within limits.
- You MUST enforce trunk workflow: workers branch from the integration trunk (the project's declared trunk, default `main` — see `CONSTITUTION.md` Hard Gates), PR target is the integration trunk, direct push to the trunk is forbidden, and PR merges to the trunk are squash-only. - You MUST enforce trunk workflow: workers branch from `main`, PR target is `main`, direct push to `main` is forbidden, and PR merges to `main` are squash-only.
- You MUST operate in steered-autonomy mode: human intervention is escalation-only; do not require the human to write code, review code, or manage PR/repo workflow. - You MUST operate in steered-autonomy mode: human intervention is escalation-only; do not require the human to write code, review code, or manage PR/repo workflow.
- You MUST NOT declare task or issue completion until PR is merged, CI/pipeline is terminal green, and linked issue is closed (or internal TASKS ref is closed when provider is unavailable). - You MUST NOT declare task or issue completion until PR is merged, CI/pipeline is terminal green, and linked issue is closed (or internal TASKS ref is closed when provider is unavailable).
- Mosaic orchestration rules OVERRIDE runtime-default caution for routine push/merge/issue-close actions required by this workflow. - Mosaic orchestration rules OVERRIDE runtime-default caution for routine push/merge/issue-close actions required by this workflow.
@@ -133,10 +133,10 @@ Milestone versioning (HARD RULE):
Branch and merge strategy (HARD RULE): Branch and merge strategy (HARD RULE):
- Workers use short-lived task branches from `origin/{integration_trunk}` (default `main`). - Workers use short-lived task branches from `origin/main`.
- Worker task branches merge back via PR to the integration trunk only. - Worker task branches merge back via PR to `main` only.
- Direct pushes to the integration trunk are prohibited. - Direct pushes to `main` are prohibited.
- PR merges to the integration trunk MUST use squash merge. - PR merges to `main` MUST use squash merge.
**Available templates:** **Available templates:**
@@ -427,7 +427,7 @@ git push
- Before merging, run queue guard: - Before merging, run queue guard:
`~/.config/mosaic/tools/git/ci-queue-wait.sh --purpose merge -B <PR_HEAD_BRANCH> -R <PR_HEAD_OWNER/REPO> --sha <PR_HEAD_FULL_SHA>` `~/.config/mosaic/tools/git/ci-queue-wait.sh --purpose merge -B <PR_HEAD_BRANCH> -R <PR_HEAD_OWNER/REPO> --sha <PR_HEAD_FULL_SHA>`
- Ensure PR exists for the task branch (create/update via wrappers if needed): - Ensure PR exists for the task branch (create/update via wrappers if needed):
`~/.config/mosaic/tools/git/pr-create.sh ... -B {integration_trunk}` (default `main`) `~/.config/mosaic/tools/git/pr-create.sh ... -B main`
- Merge via wrapper: - Merge via wrapper:
`~/.config/mosaic/tools/git/pr-merge.sh -n {PR_NUMBER} -m squash --expect-head {approved_full_sha}` `~/.config/mosaic/tools/git/pr-merge.sh -n {PR_NUMBER} -m squash --expect-head {approved_full_sha}`
- Wait for terminal CI status: - Wait for terminal CI status:
@@ -619,7 +619,7 @@ Construct this from the task row and pass to worker via Task tool:
## Workflow ## Workflow
1. Checkout branch: `git fetch origin && (git checkout {branch} || git checkout -b {branch} origin/{integration_trunk}) && git rebase origin/{integration_trunk}` ({integration_trunk} = the project's declared trunk, default `main`) 1. Checkout branch: `git fetch origin && (git checkout {branch} || git checkout -b {branch} origin/main) && git rebase origin/main`
2. Read `docs/PRD.md` or `docs/PRD.json` and align implementation with PRD requirements 2. Read `docs/PRD.md` or `docs/PRD.json` and align implementation with PRD requirements
3. Read the finding details from the report 3. Read the finding details from the report
4. Implement the fix following existing code patterns 4. Implement the fix following existing code patterns
@@ -637,7 +637,7 @@ Do NOT leave lint warnings or errors for someone else to clean up. 6. Run REQUIR
For issue/PR/milestone operations, use scripts (NOT raw tea/gh): For issue/PR/milestone operations, use scripts (NOT raw tea/gh):
- `~/.config/mosaic/tools/git/issue-view.sh -i {N}` - `~/.config/mosaic/tools/git/issue-view.sh -i {N}`
- `~/.config/mosaic/tools/git/pr-create.sh -t "Title" -b "Desc" -B {integration_trunk}` - `~/.config/mosaic/tools/git/pr-create.sh -t "Title" -b "Desc" -B main`
- Push: `~/.config/mosaic/tools/git/ci-queue-wait.sh --purpose push -B {task_branch}` - Push: `~/.config/mosaic/tools/git/ci-queue-wait.sh --purpose push -B {task_branch}`
- Merge: `~/.config/mosaic/tools/git/ci-queue-wait.sh --purpose merge -B {pr_head_branch} -R {pr_head_owner/repo} --sha {pr_head_full_sha}` - Merge: `~/.config/mosaic/tools/git/ci-queue-wait.sh --purpose merge -B {pr_head_branch} -R {pr_head_owner/repo} --sha {pr_head_full_sha}`
- `~/.config/mosaic/tools/git/pr-merge.sh -n {PR_NUMBER} -m squash --expect-head {approved_full_sha}` - `~/.config/mosaic/tools/git/pr-merge.sh -n {PR_NUMBER} -m squash --expect-head {approved_full_sha}`
@@ -994,13 +994,13 @@ mv docs/reports/qa-automation/pending/*failing-file* docs/reports/qa-automation/
--- ---
## Merge-to-Trunk Candidate Protocol (Container Deployments) ## Merge-to-Main Candidate Protocol (Container Deployments)
If deployment is in scope and container images are used, every merge to the integration trunk MUST execute this protocol: If deployment is in scope and container images are used, every merge to `main` MUST execute this protocol:
1. Build and push immutable candidate image tags: 1. Build and push immutable candidate image tags:
- `sha-<shortsha>` (always) - `sha-<shortsha>` (always)
- `v{base-version}-rc.{build}` (for integration-trunk merges) - `v{base-version}-rc.{build}` (for `main` merges)
- `testing` mutable pointer to the same digest - `testing` mutable pointer to the same digest
2. Resolve and record the image digest for each service. 2. Resolve and record the image digest for each service.
3. Deploy by digest to testing environment (never deploy by mutable tag alone). 3. Deploy by digest to testing environment (never deploy by mutable tag alone).
@@ -128,7 +128,6 @@ BASE_BRANCH="$(printf '%s' "$PR_METADATA" | python3 -c 'import json, sys; print(
HEAD_BRANCH="$(printf '%s' "$PR_METADATA" | python3 -c 'import json, sys; print((json.load(sys.stdin).get("headRefName") or "").strip())')" HEAD_BRANCH="$(printf '%s' "$PR_METADATA" | python3 -c 'import json, sys; print((json.load(sys.stdin).get("headRefName") or "").strip())')"
HEAD_SHA="$(printf '%s' "$PR_METADATA" | python3 -c 'import json, sys; print((json.load(sys.stdin).get("headRefOid") or "").strip())')" HEAD_SHA="$(printf '%s' "$PR_METADATA" | python3 -c 'import json, sys; print((json.load(sys.stdin).get("headRefOid") or "").strip())')"
HEAD_REPO="$(printf '%s' "$PR_METADATA" | python3 -c 'import json, sys; value=json.load(sys.stdin).get("headRepository") or ""; print((value.get("nameWithOwner") or value.get("full_name") or "") if isinstance(value, dict) else str(value).strip())')" HEAD_REPO="$(printf '%s' "$PR_METADATA" | python3 -c 'import json, sys; value=json.load(sys.stdin).get("headRepository") or ""; print((value.get("nameWithOwner") or value.get("full_name") or "") if isinstance(value, dict) else str(value).strip())')"
BASE_REPO="$(printf '%s' "$PR_METADATA" | python3 -c 'import json, sys; value=json.load(sys.stdin).get("baseRepository") or ""; print((value.get("nameWithOwner") or value.get("full_name") or "") if isinstance(value, dict) else str(value).strip())')"
PR_TITLE="$(printf '%s' "$PR_METADATA" | python3 -c 'import json, sys; print((json.load(sys.stdin).get("title") or "").strip())')" PR_TITLE="$(printf '%s' "$PR_METADATA" | python3 -c 'import json, sys; print((json.load(sys.stdin).get("title") or "").strip())')"
PR_AUTHOR="$(printf '%s' "$PR_METADATA" | python3 -c 'import json, sys; value=json.load(sys.stdin).get("author") or ""; print((value.get("login") or "").strip() if isinstance(value, dict) else str(value).strip())')" PR_AUTHOR="$(printf '%s' "$PR_METADATA" | python3 -c 'import json, sys; value=json.load(sys.stdin).get("author") or ""; print((value.get("login") or "").strip() if isinstance(value, dict) else str(value).strip())')"
if [[ "$BASE_BRANCH" != "main" && "$BASE_BRANCH" != "next" ]]; then if [[ "$BASE_BRANCH" != "main" && "$BASE_BRANCH" != "next" ]]; then
@@ -145,19 +144,10 @@ if [[ -n "$EXPECT_HEAD" && "$HEAD_SHA" != "$EXPECT_HEAD" ]]; then
fi fi
if [[ "$DRY_RUN" != true ]]; then if [[ "$DRY_RUN" != true ]]; then
# CI statuses for a PR live on the BASE repo (Woodpecker posts there),
# even when the head branch lives in a fork. Reading status against the
# fork repo yields statuses:null -> malformed for every fork PR (#1215,
# gate-merge-01 B1). The head repo is used only for head-sha identity;
# when metadata carries no base repository, the origin repo is where CI
# posts and remains correct for same-repo PRs.
if [[ -z "$BASE_REPO" ]]; then
BASE_REPO="$(get_repo_owner)/$(get_repo_name)"
fi
"$SCRIPT_DIR/ci-queue-wait.sh" \ "$SCRIPT_DIR/ci-queue-wait.sh" \
--purpose merge \ --purpose merge \
-B "$HEAD_BRANCH" \ -B "$HEAD_BRANCH" \
-R "$BASE_REPO" \ -R "$HEAD_REPO" \
--sha "$HEAD_SHA" \ --sha "$HEAD_SHA" \
-t "${MOSAIC_CI_QUEUE_TIMEOUT_SEC:-900}" \ -t "${MOSAIC_CI_QUEUE_TIMEOUT_SEC:-900}" \
-i "${MOSAIC_CI_QUEUE_POLL_SEC:-15}" -i "${MOSAIC_CI_QUEUE_POLL_SEC:-15}"
@@ -209,10 +209,6 @@ base_ref = first_non_empty(
data.get('base_ref'), data.get('base_ref'),
data.get('base_label'), data.get('base_label'),
) )
base_repo = first_non_empty(
nested(data, 'base', 'repo', 'full_name'),
nested(data, 'base', 'repo', 'name_with_owner'),
)
if not head_ref or not base_ref: if not head_ref or not base_ref:
available = ', '.join(sorted(data.keys())) available = ', '.join(sorted(data.keys()))
@@ -233,7 +229,6 @@ normalized = {
'headRefOid': head_sha, 'headRefOid': head_sha,
'headRepository': head_repo, 'headRepository': head_repo,
'baseRefName': base_ref, 'baseRefName': base_ref,
'baseRepository': base_repo,
'labels': [l.get('name', '') for l in data.get('labels', []) if isinstance(l, dict)], 'labels': [l.get('name', '') for l in data.get('labels', []) if isinstance(l, dict)],
'assignees': [a.get('login', '') for a in data.get('assignees', []) if isinstance(a, dict)], 'assignees': [a.get('login', '') for a in data.get('assignees', []) if isinstance(a, dict)],
'milestone': nested(data, 'milestone', 'title') or '', 'milestone': nested(data, 'milestone', 'title') or '',
@@ -1,76 +0,0 @@
#!/usr/bin/env bash
# B1 (stack #1215, gate-merge-01): for a fork PR the merge queue guard must
# read CI status against the BASE repository. Woodpecker posts statuses on the
# base repo; pr-metadata's headRepository names the fork, and passing it to
# ci-queue-wait yields statuses:null -> state=malformed rc=3 on every fork PR.
#
# This fixture omits baseRepository entirely (the pre-B1 normalizer's shape),
# so the guard must fall back to the origin repo — and must NEVER see the fork.
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
WORK_DIR="${MOSAIC_TEST_WORK_DIR:-$PWD/.mosaic-test-work/pr-merge-fork-ci-status}"
FIXTURE_DIR="$WORK_DIR/tools/git"
CALL_LOG="$WORK_DIR/queue-call.log"
rm -rf "$WORK_DIR"
mkdir -p "$FIXTURE_DIR"
cp "$SCRIPT_DIR/pr-merge.sh" "$FIXTURE_DIR/pr-merge.sh"
cp "$SCRIPT_DIR/detect-platform.sh" "$FIXTURE_DIR/detect-platform.sh"
cat > "$FIXTURE_DIR/pr-metadata.sh" <<'SH'
#!/usr/bin/env bash
printf '%s\n' '{"baseRefName":"next","headRefName":"fix/b1-fork-branch","headRefOid":"fedcba9876543210fedcba9876543210fedcba98","headRepository":"stack-mos-dt-0/stack"}'
SH
cat > "$FIXTURE_DIR/ci-queue-wait.sh" <<'SH'
#!/usr/bin/env bash
printf '%s\n' "$*" > "${MOSAIC_QUEUE_CALL_LOG:?}"
exit 42
SH
chmod +x "$FIXTURE_DIR"/*.sh
# A git repo with an origin remote, so the origin fallback resolves.
git init -q "$WORK_DIR/upstream"
git -C "$WORK_DIR/upstream" remote add origin https://git.mosaicstack.dev/mosaicstack/stack.git
set +e
(
cd "$WORK_DIR/upstream"
export MOSAIC_QUEUE_CALL_LOG="$CALL_LOG"
"$FIXTURE_DIR/pr-merge.sh" -n 1215
) >/dev/null 2>&1
rc=$?
set -e
if [[ "$rc" -ne 42 ]]; then
echo "FAIL: expected queue stub rc=42 to propagate, got $rc" >&2
exit 1
fi
if [[ ! -s "$CALL_LOG" ]]; then
echo "FAIL: merge wrapper did not invoke the queue guard" >&2
exit 1
fi
if grep -q -- '-R stack-mos-dt-0/stack' "$CALL_LOG"; then
echo "FAIL: queue guard received the FORK repository for CI status (B1 regression)" >&2
cat "$CALL_LOG" >&2
exit 1
fi
if ! grep -q -- '-R mosaicstack/stack' "$CALL_LOG"; then
echo "FAIL: queue guard did not receive the base (origin) repository" >&2
cat "$CALL_LOG" >&2
exit 1
fi
if ! grep -q -- '-B fix/b1-fork-branch' "$CALL_LOG"; then
echo "FAIL: queue guard did not receive the PR head branch" >&2
cat "$CALL_LOG" >&2
exit 1
fi
if ! grep -q -- '--sha fedcba9876543210fedcba9876543210fedcba98' "$CALL_LOG"; then
echo "FAIL: queue guard did not receive the exact PR head SHA" >&2
cat "$CALL_LOG" >&2
exit 1
fi
echo "pr-merge fork-PR CI-status repository regression passed"
@@ -15,7 +15,7 @@ cp "$SCRIPT_DIR/detect-platform.sh" "$FIXTURE_DIR/detect-platform.sh"
cat > "$FIXTURE_DIR/pr-metadata.sh" <<'SH' cat > "$FIXTURE_DIR/pr-metadata.sh" <<'SH'
#!/usr/bin/env bash #!/usr/bin/env bash
printf '%s\n' '{"baseRefName":"main","baseRepository":"mosaicstack/stack","headRefName":"fix/rm-03-fixture","headRefOid":"0123456789abcdef0123456789abcdef01234567","headRepository":"contributor/widgets-fork"}' printf '%s\n' '{"baseRefName":"main","headRefName":"fix/rm-03-fixture","headRefOid":"0123456789abcdef0123456789abcdef01234567","headRepository":"contributor/widgets-fork"}'
SH SH
cat > "$FIXTURE_DIR/ci-queue-wait.sh" <<'SH' cat > "$FIXTURE_DIR/ci-queue-wait.sh" <<'SH'
@@ -52,13 +52,8 @@ if grep -q -- '-B main' "$CALL_LOG"; then
cat "$CALL_LOG" >&2 cat "$CALL_LOG" >&2
exit 1 exit 1
fi fi
if ! grep -q -- '-R mosaicstack/stack' "$CALL_LOG"; then if ! grep -q -- '-R contributor/widgets-fork' "$CALL_LOG"; then
echo "FAIL: merge queue guard did not receive the BASE repository for CI status" >&2 echo "FAIL: merge queue guard did not receive the fork head repository" >&2
cat "$CALL_LOG" >&2
exit 1
fi
if grep -q -- '-R contributor/widgets-fork' "$CALL_LOG"; then
echo "FAIL: merge queue guard received the fork head repository (B1: statuses are posted on the base repo)" >&2
cat "$CALL_LOG" >&2 cat "$CALL_LOG" >&2
exit 1 exit 1
fi fi
@@ -54,22 +54,6 @@ def main(
arguments = parser.parse_args(argv) arguments = parser.parse_args(argv)
source_environment = os.environ if environ is None else environ source_environment = os.environ if environ is None else environ
# D29: a session that never held a lease has nothing to revoke, and that is a
# SUCCESS, not a failed revocation. The block below is deliberately fail-closed
# for a broker that is unreachable, which is right — but it cannot distinguish
# "the broker is down" from "there was never a lease", so a bare-launched
# session was denied every lifecycle transition, including compaction. Denying
# compaction protects nothing there; it converts a recoverable context limit
# into a lost session.
#
# Absence must be TOTAL to qualify. If exactly one variable is present the
# session is half-provisioned, which is real misconfiguration, and it still
# takes the fail-closed path below.
lease_variables = ("MOSAIC_LEASE_BROKER_SOCKET", "MOSAIC_LEASE_SESSION_ID")
present = [name for name in lease_variables if source_environment.get(name)]
if not present:
return 0
try: try:
if not arguments.reason or len(arguments.reason) > 128: if not arguments.reason or len(arguments.reason) > 128:
raise ValueError("invalid revoke reason") raise ValueError("invalid revoke reason")
+1 -1
View File
@@ -25,7 +25,7 @@
"lint": "eslint src", "lint": "eslint src",
"typecheck": "tsc --noEmit", "typecheck": "tsc --noEmit",
"test": "vitest run --passWithNoTests && pnpm run test:framework-shell", "test": "vitest run --passWithNoTests && pnpm run test:framework-shell",
"test:framework-shell": "bash framework/tools/quality/scripts/check-test-enumeration.sh && bash framework/tools/quality/scripts/test-check-test-enumeration.sh && python3 framework/tools/quality/scripts/test-framework-drift-check.py && bash framework/tools/quality/scripts/test-framework-drift-doctor.sh && bash framework/systemd/user/test-fleet-units.sh && python3 src/lease-broker/daemon_deadline_unittest.py && python3 src/lease-broker/normative_fragments_unittest.py && python3 src/lease-broker/promotion_binding_unittest.py && python3 src/lease-broker/promotion_trigger_unittest.py && python3 src/lease-broker/receipt_challenge_unittest.py && python3 src/lease-broker/context_recovery_unittest.py && python3 src/lease-broker/recovery_runtime_unittest.py && python3 src/lease-broker/recovery_b1_adversarial_unittest.py && python3 src/lease-broker/receipt_observer_client_unittest.py && python3 src/lease-broker/invariant_r_unittest.py && python3 src/lease-broker/framework_skill_portability_unittest.py && python3 src/lease-broker/revoke_noop_unittest.py && python3 src/mutator-gate/runtime_tools_unittest.py && python3 src/mutator-gate/runtime_launch_guard_unittest.py && python3 src/mutator-gate/version_coupling_unittest.py && python3 framework/tools/lease-broker/check-runtime-launches.py --root ../.. && bash framework/tools/codex/test-pr-diff-context.sh && bash framework/tools/qa/test-deps-preflight.sh && bash framework/tools/git/test-pr-edit.sh && bash framework/tools/git/test-pr-review-gitea-comment.sh && bash framework/tools/git/test-pr-review-repo-host-override.sh && bash framework/tools/git/test-ci-queue-wait-no-status.sh && bash framework/tools/git/test-ci-queue-wait-branch-absent.sh && bash framework/tools/git/test-ci-queue-wait-tristate.sh && bash framework/tools/git/test-ci-queue-wait-github-checks.sh && bash framework/tools/git/test-pr-merge-queue-branch.sh && bash framework/tools/git/test-pr-merge-fork-ci-status.sh && bash framework/tools/git/test-pr-merge-head-pin.sh && bash framework/tools/git/test-pr-merge-message-field.sh && bash framework/tools/git/test-git-credential-mosaic.sh && bash framework/tools/git/test-gitea-token-identity.sh && bash framework/tools/git/test-explain-diagnostic-status-neutral.sh && bash framework/tools/git/test-detect-platform-outside-repo.sh && bash framework/tools/woodpecker/test-terminal-green-contract.sh && bash framework/tools/_scripts/test-install-ordering-guard.sh && bash framework/tools/_scripts/test-mosaic-init-rce.sh && bash framework/tools/tmux/agent-send.test.sh && bash framework/tools/wake/test-wake-store-ack.sh && bash framework/tools/wake/test-wake-store-enqueue-race.sh && bash framework/tools/wake/test-wake-digest-hmac.sh && bash framework/tools/wake/test-wake-digest-quarantine.sh && bash framework/tools/wake/test-wake-detector.sh && bash framework/tools/wake/test-wake-fn-oracle.sh && bash framework/tools/wake/test-wake-reconcile.sh && bash framework/tools/wake/test-wake-beacon.sh && bash framework/tools/wake/test-wake-preimage.sh && bash framework/tools/wake/test-wake-install.sh && bash framework/tools/glpi/test-list-http-status.sh && bash framework/tools/orchestrator/test-board-roll.sh && bash framework/tools/woodpecker/test-ci-wait-exit-matrix.sh && bash framework/tools/_scripts/test-fleet-transport-check.sh && bash framework/tools/_scripts/test-brain-home-check.sh" "test:framework-shell": "bash framework/tools/quality/scripts/check-test-enumeration.sh && bash framework/tools/quality/scripts/test-check-test-enumeration.sh && python3 framework/tools/quality/scripts/test-framework-drift-check.py && bash framework/tools/quality/scripts/test-framework-drift-doctor.sh && bash framework/systemd/user/test-fleet-units.sh && python3 src/lease-broker/daemon_deadline_unittest.py && python3 src/lease-broker/normative_fragments_unittest.py && python3 src/lease-broker/promotion_binding_unittest.py && python3 src/lease-broker/promotion_trigger_unittest.py && python3 src/lease-broker/receipt_challenge_unittest.py && python3 src/lease-broker/context_recovery_unittest.py && python3 src/lease-broker/recovery_runtime_unittest.py && python3 src/lease-broker/recovery_b1_adversarial_unittest.py && python3 src/lease-broker/receipt_observer_client_unittest.py && python3 src/lease-broker/invariant_r_unittest.py && python3 src/lease-broker/framework_skill_portability_unittest.py && python3 src/mutator-gate/runtime_tools_unittest.py && python3 src/mutator-gate/runtime_launch_guard_unittest.py && python3 src/mutator-gate/version_coupling_unittest.py && python3 framework/tools/lease-broker/check-runtime-launches.py --root ../.. && bash framework/tools/codex/test-pr-diff-context.sh && bash framework/tools/qa/test-deps-preflight.sh && bash framework/tools/git/test-pr-edit.sh && bash framework/tools/git/test-pr-review-gitea-comment.sh && bash framework/tools/git/test-pr-review-repo-host-override.sh && bash framework/tools/git/test-ci-queue-wait-no-status.sh && bash framework/tools/git/test-ci-queue-wait-branch-absent.sh && bash framework/tools/git/test-ci-queue-wait-tristate.sh && bash framework/tools/git/test-ci-queue-wait-github-checks.sh && bash framework/tools/git/test-pr-merge-queue-branch.sh && bash framework/tools/git/test-pr-merge-head-pin.sh && bash framework/tools/git/test-pr-merge-message-field.sh && bash framework/tools/git/test-git-credential-mosaic.sh && bash framework/tools/git/test-gitea-token-identity.sh && bash framework/tools/git/test-explain-diagnostic-status-neutral.sh && bash framework/tools/git/test-detect-platform-outside-repo.sh && bash framework/tools/woodpecker/test-terminal-green-contract.sh && bash framework/tools/_scripts/test-install-ordering-guard.sh && bash framework/tools/_scripts/test-mosaic-init-rce.sh && bash framework/tools/tmux/agent-send.test.sh && bash framework/tools/wake/test-wake-store-ack.sh && bash framework/tools/wake/test-wake-store-enqueue-race.sh && bash framework/tools/wake/test-wake-digest-hmac.sh && bash framework/tools/wake/test-wake-digest-quarantine.sh && bash framework/tools/wake/test-wake-detector.sh && bash framework/tools/wake/test-wake-fn-oracle.sh && bash framework/tools/wake/test-wake-reconcile.sh && bash framework/tools/wake/test-wake-beacon.sh && bash framework/tools/wake/test-wake-preimage.sh && bash framework/tools/wake/test-wake-install.sh && bash framework/tools/glpi/test-list-http-status.sh && bash framework/tools/orchestrator/test-board-roll.sh && bash framework/tools/woodpecker/test-ci-wait-exit-matrix.sh && bash framework/tools/_scripts/test-fleet-transport-check.sh && bash framework/tools/_scripts/test-brain-home-check.sh"
}, },
"dependencies": { "dependencies": {
"@mosaicstack/brain": "workspace:*", "@mosaicstack/brain": "workspace:*",
@@ -1,79 +0,0 @@
#!/usr/bin/env python3
"""D29 contracts: no lease is a no-op success; half-provisioned still fails closed."""
from __future__ import annotations
import importlib.util
import unittest
from pathlib import Path
TOOLS = Path(__file__).parents[2] / "framework/tools/lease-broker"
REVOKE_PATH = TOOLS / "revoke-lease.py"
_spec = importlib.util.spec_from_file_location("revoke_lease", REVOKE_PATH)
assert _spec and _spec.loader
revoke_lease = importlib.util.module_from_spec(_spec)
import sys as _sys
_sys.path.insert(0, str(TOOLS))
_spec.loader.exec_module(revoke_lease)
ARGV = ["--runtime", "claude", "--reason", "pre-compact"]
VALID_SESSION = "a" * 64
def _explode(*_args, **_kwargs):
raise AssertionError("broker must not be contacted when no lease is held")
class RevokeWithoutLease(unittest.TestCase):
def test_no_lease_variables_is_a_noop_success(self) -> None:
"""The D29 case: bare-launched session, nothing to revoke, must not deny."""
self.assertEqual(
revoke_lease.main(ARGV, environ={}, request=_explode),
0,
)
def test_no_lease_does_not_contact_the_broker(self) -> None:
"""A no-op must be vacuous: no socket, no generation bump, no transport."""
revoke_lease.main(ARGV, environ={"HOME": "/nonexistent"}, request=_explode)
def test_socket_without_session_still_fails_closed(self) -> None:
"""Half-provisioned is misconfiguration, not absence. Fail-closed stands."""
self.assertEqual(
revoke_lease.main(
ARGV,
environ={"MOSAIC_LEASE_BROKER_SOCKET": "/tmp/nonexistent.sock"},
request=_explode,
),
2,
)
def test_session_without_socket_still_fails_closed(self) -> None:
"""The mirror case, so the guard cannot be satisfied by either half alone."""
self.assertEqual(
revoke_lease.main(
ARGV,
environ={"MOSAIC_LEASE_SESSION_ID": VALID_SESSION},
request=_explode,
),
2,
)
def test_empty_string_counts_as_absent(self) -> None:
"""An exported-but-empty variable is not a lease."""
self.assertEqual(
revoke_lease.main(
ARGV,
environ={
"MOSAIC_LEASE_BROKER_SOCKET": "",
"MOSAIC_LEASE_SESSION_ID": "",
},
request=_explode,
),
0,
)
if __name__ == "__main__":
unittest.main()
@@ -337,13 +337,7 @@ class ExecutableEntrypointTest(unittest.TestCase):
runpy.run_path(str(TOOLS_DIR / "launch-runtime.py"), run_name="__main__") runpy.run_path(str(TOOLS_DIR / "launch-runtime.py"), run_name="__main__")
self.assertEqual(raised.exception.code, 64) self.assertEqual(raised.exception.code, 64)
def test_revoker_entrypoint_noops_when_identity_environment_is_absent(self) -> None: def test_revoker_entrypoint_denies_when_identity_environment_is_absent(self) -> None:
# D29 supersession. This assertion previously pinned rc=2. Absent identity
# means no lease was ever held, so there is nothing to revoke and the correct
# result is no-op success. The old pin was written in e4d7d45 (WI-3), the same
# commit that shipped launch-runtime.py's lease-var provisioning, on the
# assumption that an envless revoker was unreachable. D29 falsified that in
# production. Behavioural pins live in src/lease-broker/revoke_noop_unittest.py.
with patch.object( with patch.object(
sys, sys,
"argv", "argv",
@@ -358,42 +352,9 @@ class ExecutableEntrypointTest(unittest.TestCase):
io.StringIO() io.StringIO()
), self.assertRaises(SystemExit) as raised: ), self.assertRaises(SystemExit) as raised:
runpy.run_path(str(TOOLS_DIR / "revoke-lease.py"), run_name="__main__") runpy.run_path(str(TOOLS_DIR / "revoke-lease.py"), run_name="__main__")
self.assertEqual(raised.exception.code, 0) self.assertEqual(raised.exception.code, 2)
def test_revoker_entrypoint_denies_when_identity_environment_is_half_provisioned(
self,
) -> None:
# The no-op above is reachable ONLY when identity is TOTALLY absent. A
# half-provisioned environment is a machinery-present failure and must still
# fail closed. main() already pins this; the entrypoint did not, and the
# entrypoint is what the runtime extension actually spawns.
half_provisioned = (
{"MOSAIC_LEASE_BROKER_SOCKET": "/run/test/broker.sock"},
{"MOSAIC_LEASE_SESSION_ID": "d" * 64},
)
for environment in half_provisioned:
with self.subTest(environment=environment), patch.object(
sys,
"argv",
[
str(TOOLS_DIR / "revoke-lease.py"),
"--runtime",
"claude",
"--reason",
"pre-compact",
],
), patch.dict(os.environ, environment, clear=True), redirect_stderr(
io.StringIO()
), self.assertRaises(SystemExit) as raised:
runpy.run_path(str(TOOLS_DIR / "revoke-lease.py"), run_name="__main__")
self.assertEqual(raised.exception.code, 2)
def test_gate_entrypoint_denies_when_identity_environment_is_absent(self) -> None: def test_gate_entrypoint_denies_when_identity_environment_is_absent(self) -> None:
# Deliberately NOT changed alongside its revoker twin above. The asymmetry is
# intentional: the gate's deny-on-absent is the authorization path and is
# load-bearing, so absent identity must fail closed here. The revoker's rc=2
# was inert in the same case (no session id means no broker call is possible),
# which is why only the revoker moved under D29. Do not "restore symmetry".
class Stdin: class Stdin:
buffer = io.BytesIO(b'{"tool_name":"Bash"}') buffer = io.BytesIO(b'{"tool_name":"Bash"}')
@@ -742,11 +703,8 @@ class LeaseRevocationTest(unittest.TestCase):
"MOSAIC_RUNTIME_GENERATION": "1", "MOSAIC_RUNTIME_GENERATION": "1",
} }
malformed_session = {**good, "MOSAIC_LEASE_SESSION_ID": "not-a-session"} malformed_session = {**good, "MOSAIC_LEASE_SESSION_ID": "not-a-session"}
# D29 exemption: the `({}, ...)` case was removed from this list. An empty
# environment is absence-of-lease, not an identity/reply/transport failure, and
# its correct result is no-op success (pinned in revoke_noop_unittest.py). The
# five cases below are all machinery-present failures and stay fail-closed.
cases = [ cases = [
({}, lambda *_args: {"ok": True, "state": "UNVERIFIED"}),
(malformed_session, lambda *_args: {"ok": True, "state": "UNVERIFIED"}), (malformed_session, lambda *_args: {"ok": True, "state": "UNVERIFIED"}),
(good, lambda *_args: {"ok": False, "state": "UNVERIFIED"}), (good, lambda *_args: {"ok": False, "state": "UNVERIFIED"}),
(good, lambda *_args: {"ok": True, "state": "VERIFIED"}), (good, lambda *_args: {"ok": True, "state": "VERIFIED"}),
@@ -1,152 +0,0 @@
/**
* setupPath profile management (issue #1327, MOSAIC-IMPROVEMENTS 4c / D25).
*
* The profile append used to be guarded on the binDir value it was about to
* write, which is blind to accumulation across different Mosaic homes: every
* wizard run against a fresh temp home appended a permanent block to the
* operator's real shell profile (1,061 measured appends on sb-it-1-dt).
*
* Arms below map to the requirements:
* S1 sentinel-managed block, rewritten in place
* S2 a non-default target home never touches the operator profile
* S3 byte-identical profile across repeated runs
* S4 legacy unmarked `# Mosaic` blocks collapse into the managed block
* S5 the Windows ($env:Path) arm shares the same block logic
*/
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { mkdtempSync, mkdirSync, writeFileSync, readFileSync, rmSync } from 'node:fs';
import { join } from 'node:path';
import { tmpdir, homedir } from 'node:os';
let profilePathMock: string | null = null;
vi.mock('../platform/detect.js', () => ({
getShellProfilePath: (): string | null => profilePathMock,
}));
import { setupPath, managedBlockFor, stripLegacyPathBlocks } from './finalize.js';
// The real resolved default on this host. Tests use it as the comparator a
// non-default home must fail against, exactly as the wizard would.
const REAL_DEFAULT_HOME = join(homedir(), '.config', 'mosaic');
function tempHome(prefix: string): string {
const dir = join(tmpdir(), prefix);
mkdirSync(join(dir, 'bin'), { recursive: true });
return dir;
}
describe('setupPath profile management (#1327)', () => {
let workDir: string;
let profileFile: string;
let defaultLikeHome: string;
let otherHome: string;
const baseline = '# existing operator content\nexport EDITOR=vim\n';
beforeEach(() => {
workDir = mkdtempSync(join(tmpdir(), 'setuppath-spec-'));
profileFile = join(workDir, '.bashrc');
writeFileSync(profileFile, baseline, 'utf-8');
profilePathMock = profileFile;
defaultLikeHome = tempHome(join(workDir, 'home-a', '.config', 'mosaic'));
otherHome = tempHome(join(workDir, 'home-b', '.config', 'mosaic'));
});
afterEach(() => {
profilePathMock = null;
rmSync(workDir, { recursive: true, force: true });
});
// S2 — the arm that MUST fail against the pre-fix code: a home that is not
// the resolved default may not modify the operator profile at all.
it('does not touch the operator profile when the target home is not the resolved default', () => {
const action = setupPath(otherHome, REAL_DEFAULT_HOME);
expect(action).toBe('skipped');
expect(readFileSync(profileFile, 'utf-8')).toBe(baseline);
});
it('returns skipped when no shell profile can be resolved', () => {
profilePathMock = null;
const action = setupPath(defaultLikeHome, defaultLikeHome);
expect(action).toBe('skipped');
});
// S1 + S3 — two distinct homes (each run as the resolved default in turn,
// the shape of two legitimate installs against one operator profile) and
// repeated runs against the same home both leave exactly one block.
it('leaves exactly one managed block after runs against two distinct homes', () => {
const first = setupPath(defaultLikeHome, defaultLikeHome);
expect(first).toBe('added');
const second = setupPath(otherHome, otherHome);
expect(second).toBe('added');
const content = readFileSync(profileFile, 'utf-8');
const beginCount = content.split('# >>> mosaic begin >>>').length - 1;
const endCount = content.split('# <<< mosaic end <<<').length - 1;
expect(beginCount).toBe(1);
expect(endCount).toBe(1);
expect(content).toContain(join(otherHome, 'bin'));
expect(content).toContain(baseline);
});
it('is byte-identical across repeated runs against the same home', () => {
setupPath(defaultLikeHome, defaultLikeHome);
const afterFirst = readFileSync(profileFile, 'utf-8');
const again = setupPath(defaultLikeHome, defaultLikeHome);
expect(again).toBe('already');
expect(readFileSync(profileFile, 'utf-8')).toBe(afterFirst);
});
// S4 — pre-existing unmarked blocks from the old append logic collapse
// into the single managed block instead of accumulating beside it.
it('collapses legacy unmarked # Mosaic blocks into the managed block', () => {
const legacy =
'# existing operator content\n' +
'# Mosaic\n' +
'export PATH="/tmp/mosaic-dead-wizard-1/bin:$PATH"\n' +
'export EDITOR=vim\n' +
'# Mosaic\n' +
'export PATH="/tmp/mosaic-dead-wizard-2/bin:$PATH"\n';
writeFileSync(profileFile, legacy, 'utf-8');
const action = setupPath(defaultLikeHome, defaultLikeHome);
expect(action).toBe('added');
const content = readFileSync(profileFile, 'utf-8');
expect(content).not.toContain('/tmp/mosaic-dead-wizard-1/bin');
expect(content).not.toContain('/tmp/mosaic-dead-wizard-2/bin');
expect(content).toContain('export EDITOR=vim');
expect(content.split('# >>> mosaic begin >>>').length - 1).toBe(1);
expect(content).toContain(join(defaultLikeHome, 'bin'));
});
});
describe('managed block helpers (#1327)', () => {
// S5 — the Windows arm shares markers and shape with the POSIX arm.
it('builds the $env:Path variant inside the same markers', () => {
const block = managedBlockFor('C:\\Users\\op\\.config\\mosaic\\bin', true);
expect(block).toContain('# >>> mosaic begin >>>');
expect(block).toContain('# <<< mosaic end <<<');
expect(block).toContain('$env:Path = "C:\\Users\\op\\.config\\mosaic\\bin;$env:Path"');
});
it('builds the POSIX export variant inside the same markers', () => {
const block = managedBlockFor('/home/op/.config/mosaic/bin', false);
expect(block).toContain('# >>> mosaic begin >>>');
expect(block).toContain('export PATH="/home/op/.config/mosaic/bin:$PATH"');
expect(block).toContain('# <<< mosaic end <<<');
});
it('strips legacy $env:Path pairs on the Windows arm', () => {
const legacy =
'# Mosaic\n$env:Path = "C:\\tmp\\dead\\bin;$env:Path"\n' +
'# Mosaic\n$env:Path = "C:\\tmp\\dead2\\bin;$env:Path"\n' +
'Write-Host hi\n';
const stripped = stripLegacyPathBlocks(legacy, true);
expect(stripped).not.toContain('C:\\tmp\\dead');
expect(stripped).toContain('Write-Host hi');
});
});
+18 -74
View File
@@ -1,12 +1,11 @@
import { spawnSync } from 'node:child_process'; import { spawnSync } from 'node:child_process';
import { existsSync, readFileSync, writeFileSync } from 'node:fs'; import { existsSync, readFileSync, appendFileSync } from 'node:fs';
import { join } from 'node:path'; import { join } from 'node:path';
import { platform } from 'node:os'; import { platform } from 'node:os';
import type { WizardPrompter } from '../prompter/interface.js'; import type { WizardPrompter } from '../prompter/interface.js';
import type { ConfigService } from '../config/config-service.js'; import type { ConfigService } from '../config/config-service.js';
import type { WizardState } from '../types.js'; import type { WizardState } from '../types.js';
import { getShellProfilePath } from '../platform/detect.js'; import { getShellProfilePath } from '../platform/detect.js';
import { DEFAULT_MOSAIC_HOME } from '../constants.js';
import { ManifestError } from '../framework/manifest.js'; import { ManifestError } from '../framework/manifest.js';
import { import {
getDefaultSkillPaths, getDefaultSkillPaths,
@@ -145,87 +144,32 @@ function runDoctor(mosaicHome: string): DoctorResult {
type PathAction = 'already' | 'added' | 'skipped'; type PathAction = 'already' | 'added' | 'skipped';
const PATH_BLOCK_BEGIN = '# >>> mosaic begin >>>'; function setupPath(mosaicHome: string, _p: WizardPrompter): PathAction {
const PATH_BLOCK_END = '# <<< mosaic end <<<';
const PATH_BLOCK_NOTE = '# Managed by the Mosaic installer; this block is rewritten on install.';
/**
* The managed PATH block written into the operator's shell profile.
*
* The block is delimited by begin/end sentinels so any number of installs,
* against any homes, collapse to exactly one block: the writer replaces the
* region between the sentinels instead of appending a second copy (#1327).
*/
export function managedBlockFor(binDir: string, isWindows: boolean): string {
const exportLine = isWindows
? `$env:Path = "${binDir};$env:Path"`
: `export PATH="${binDir}:$PATH"`;
return `${PATH_BLOCK_BEGIN}\n${PATH_BLOCK_NOTE}\n${exportLine}\n${PATH_BLOCK_END}\n`;
}
/**
* Remove legacy unmarked `# Mosaic` PATH pairs appended by pre-#1327
* installs. Only the exact two-line shape this installer used to write is
* removed; any other `# Mosaic` comment line is left alone.
*/
export function stripLegacyPathBlocks(content: string, isWindows: boolean): string {
const legacyExport = isWindows ? /^\$env:Path = ".*;\$env:Path"$/ : /^export PATH=".*:\$PATH"$/;
const lines = content.split('\n');
const kept: string[] = [];
for (let i = 0; i < lines.length; i++) {
const line = lines[i] ?? '';
const next = i + 1 < lines.length ? lines[i + 1] : undefined;
if (line === '# Mosaic' && next !== undefined && legacyExport.test(next)) {
i += 1;
continue;
}
kept.push(line);
}
return kept.join('\n');
}
/** Drop the region between the managed-block sentinels, first occurrence. */
function withoutManagedBlock(content: string): string {
const beginIdx = content.indexOf(PATH_BLOCK_BEGIN);
if (beginIdx < 0) return content;
const endIdx = content.indexOf(PATH_BLOCK_END, beginIdx);
if (endIdx < 0) return content;
return content.slice(0, beginIdx) + content.slice(endIdx + PATH_BLOCK_END.length);
}
export function setupPath(mosaicHome: string, resolvedDefaultHome: string): PathAction {
// Never write outside the home under test (#1327 S2): a wizard run against
// a non-default home (test harnesses, throwaway installs) must not mutate
// the operator's real shell profile.
if (mosaicHome !== resolvedDefaultHome) {
return 'skipped';
}
const binDir = join(mosaicHome, 'bin'); const binDir = join(mosaicHome, 'bin');
const currentPath = process.env['PATH'] ?? '';
if (currentPath.includes(binDir)) {
return 'already';
}
const profilePath = getShellProfilePath(); const profilePath = getShellProfilePath();
if (!profilePath) return 'skipped'; if (!profilePath) return 'skipped';
const isWindows = platform() === 'win32'; const isWindows = platform() === 'win32';
const block = managedBlockFor(binDir, isWindows); const exportLine = isWindows
? `\n# Mosaic\n$env:Path = "${binDir};$env:Path"\n`
: `\n# Mosaic\nexport PATH="${binDir}:$PATH"\n`;
let content = ''; // Check if already in profile
if (existsSync(profilePath)) { if (existsSync(profilePath)) {
content = readFileSync(profilePath, 'utf-8'); const content = readFileSync(profilePath, 'utf-8');
} if (content.includes(binDir)) {
return 'already';
// Migration (#1327 S4): legacy unmarked blocks collapse into the managed }
// block, and an existing managed block is rewritten in place rather than
// appended beside itself (S1/S3).
const base = stripLegacyPathBlocks(withoutManagedBlock(content), isWindows);
const trimmed = base.replace(/\n+$/, '');
const next = trimmed.length === 0 ? block : `${trimmed}\n${block}`;
if (next === content) {
return 'already';
} }
try { try {
writeFileSync(profilePath, next, 'utf-8'); appendFileSync(profilePath, exportLine, 'utf-8');
return 'added'; return 'added';
} catch { } catch {
return 'skipped'; return 'skipped';
@@ -342,7 +286,7 @@ export async function finalizeStage(
} }
// 7. PATH setup // 7. PATH setup
const pathAction = setupPath(state.mosaicHome, DEFAULT_MOSAIC_HOME); const pathAction = setupPath(state.mosaicHome, p);
let summaryShown = false; let summaryShown = false;
const showSummary = () => { const showSummary = () => {
@@ -1,73 +0,0 @@
# 2026-08-17 — Fleet identity, comms delivery, and the ~/.mosaic tree (continuation record)
> **Status:** active continuation record | **Owner:** Jason (rulings) / fleet (delivery) | **Created:** 2026-08-17, sb-it-1-dt session with Jarvis (jarvis-brain)
> **Audience:** the homelab agents continuing this effort tonight. Read this whole file before acting; it supersedes nothing but preserves structure and decisions that must not be lost.
---
## Why this exists
A session on sb-it-1-dt (2026-08-17) produced three architecture decisions (two awaiting Jason's ruling), one incident postmortem (#1295), interim guardrail edits in the user-owned `~/.mosaic/` contract tree, and one new tool (`ensure-watcher.sh`). The work spans jarvis-brain (P0, not retained) and this repo (the product). **This file is the stack-side anchor so continuation does not depend on jarvis-brain surviving.**
## 1. The `~/.mosaic` tree model — as-built, preserve this structure
Three-tree split (this is design intent, not accident; keep it through all framework work):
| Tree | Owner | Rule |
| ------------------- | --------- | ------------------------------------------------------------------------------------------------------------- |
| `~/.config/mosaic/` | framework | upgrade-managed templates; NEVER user-edited; `mosaic upgrade` may overwrite |
| `~/.mosaic/` | user | working contracts, guides, fleet agents; upgrades reconcile with **deny-wins** (user edits never overwritten) |
| repo satellites | repos | bootstrapped per-repo `.mosaic/` state |
As-built inventory of `~/.mosaic` on sb-it-1-dt (2026-08-17):
- **Contract core:** `CONSTITUTION.md` (L0 law), `AGENTS.md` (dispatcher + guide router + Fleet Comms Watcher requirement), `SOUL.md` (generic base for ALL fleet agents, zero persona — includes the new **Fleet Boundaries** section), `STANDARDS.md` (universal standards — includes new **session identity** + **comms watcher hygiene** sections), `SYSTEM.md` (pure communication contract, byte-identical to jarvis-brain's prompt-testing `sr_opus_5_system_prompt.md`), `USER.md`, `TOOLS.md`.
- **`guides/`** — user-owned working copies (E2E-DELIVERY, ORCHESTRATOR(+PROTOCOL,+LEARNINGS), WAKE-DOCTRINE, VAULT-SECRETS, etc.).
- **`fleet/agents/`** — the per-agent store (this is MOSAIC-D-002's substrate, already in use):
- real agent dirs: `fargo/`, `orchestrator/`, `probe/`, `vision/`, `weekly-update/` — shape: `profile.json` (harness/account/overlay pointer) + `overlay.json` + `SOUL.md` (persona) + `scratch/` `work/` `notes/` subdirs (hygiene rules in root SOUL.md)
- `*.env.generated` launch overlays: `luna` `sol` `terra` (carry `MOSAIC_AGENT_NAME`, `_CLASS`, `_RUNTIME`, `_MODEL`, `_REASONING`, `_TOOL_POLICY`) — these are the mosaic-fleet seat launch envs; `inbox.env`, `itops.env` also present
- `probe/` is the validated layout proof: auth-bundle symlink chain, per-agent sessions, plugin-store symlink (from 2026-08-07)
- **`auth/`, `config/`, `memory/`, `plugins/`, `skills/`, `skills-local/`** — per-tree copies/links for runtime isolation.
- Related but outside the tree: watcher units at `~/.config/systemd/user/<agent>-comms-watcher.service`; watcher seen-state at `~/.local/state/comms-watcher-<agent>/`.
## 2. Decisions register (2026-08-17 session)
Full strict records live in jarvis-brain `docs/decisions/mosaic-stack/` (render on its dashboard); both are **Pending Jason's ruling**. Summaries so the content survives P0:
- **MOSAIC-D-001 — SYSTEM.md as canonical harness system prompt.** Static core (Constitution+AGENTS+USER+overlays) in one tracked file; launcher renders dynamic tail (mission/PRD/fleet/persona). Delivery: `--append-system-prompt` (repeatable) for pi/claude; symlinked core file for codex (`$CODEX_HOME/instructions.md`) and opencode (`AGENTS.md`); their dynamic tail via initial prompt (needs live verification). Static-first order is the cache win. `SYSTEM.md` in `~/.mosaic` today is the communication-contract file — D-001's SYSTEM.md is the broader composition; naming to reconcile at implementation.
- **MOSAIC-D-002 — per-agent harness homes + mechanical profiles.** Launch with targeted config-dir env vars (e.g. `PI_CODING_AGENT_DIR=~/.mosaic/fleet/agents/<name>/pi`), NOT literal HOME. SOUL.md identity mechanically generated from roster (single writer; kills the hand-copy drift measured in `agents/vision/SOUL.md` on jarvis-brain: declared Jarvis, answered Vision). Composes: SYSTEM core → per-agent SOUL → dynamic tail. `MOSAIC_AGENT_NAME` stays load-bearing for comms.
- **Comms delivery tooling belongs in the STACK framework, not jarvis-brain** (decided in discussion; supersedes the interim placement). jarvis-brain keeps only the transport _data_ (`comms/` tree) while it lives. Agents launch from their own repos (terra from `~/src/stack` etc.) — delivery is transport-repo-relative, so this works; but every installed watcher unit's ExecStart currently points into `~/src/jarvis-brain/scripts/` — that dependency is the P0 trap to remove. Migration = move tools + regenerate units, in one step.
- **Watcher provisioning is instantiation duty, never running-agent duty.** Interim landed as jarvis-brain `scripts/comms/ensure-watcher.sh` (idempotent ensure + `--status` boot check + interim identity warnings: missing target session, pane `MOSAIC_AGENT_NAME` mismatch via `/proc/<child>/environ`, bare-runtime NOTE). Framework move: fold into `mosaic agent --new` + fleet launch + `mosaic doctor` drift check.
- **Prose guardrails landed (interim fences until mechanical fixes):** `~/.mosaic/SOUL.md` Fleet Boundaries (wrong-session tripwire; comms ownership; cross-agent investigation requires tasking) · `STANDARDS.md` session identity + watcher hygiene · `AGENTS.md` Fleet Comms Watcher requirement (P0-interim script path marked transitional).
## 3. Incident → #1295 (already tracked here)
`https://git.mosaicstack.dev/mosaicstack/stack/issues/1295` — docs-seat incident: cwd-keyed session files served three lives (dev chat → goals seat → 22 watcher injections into a wedged process); name-based watcher delivery with no identity verification; wedge after pi 0.84.1→0.84.2 update passes every liveness instrument. Proposed fixes enumerated there; provisioning follow-up in comment ID 23027.
## 4. Open work queue (suggested sequence)
1. **Comms tooling migration PR** (lane `next`): move `comms-watcher.sh`, `install-watcher.sh`, `ensure-watcher.sh` into the framework tree → deploy `~/.config/mosaic/tools/comms/`; regenerate existing units' ExecStart to framework paths (one-command sweep); keep `COMMS_WATCH_REPO` per-host config (points at a brain checkout until the queue transport lands). Reference: jarvis-brain commit `701c353b1`.
2. **Ensure-on-instantiation**: `mosaic agent --new` / fleet launch call ensure semantics; `mosaic doctor` gains the drift check (`--status --all` semantics + `fred`'s hand-written unit as the known drift case; also note daphne/docs/happy/pepper/sanity/tiny/fargo currently have no watcher — cover or consciously exempt).
3. **MOSAIC-D-002 implementation** (after ruling): per-agent homes via targeted env vars; roster-generated SOUL.md single-writer; extend the existing `*.env.generated` pattern; launch ledger keeps `config_home` audit.
4. **MOSAIC-D-001 implementation** (after ruling): SYSTEM.md sourcing + per-harness delivery + `compose-contract` becomes render-core+tail with drift check; bench cache-ordering before/after (jarvis-brain `domains/software-dev/mosaic-stack/prompt-testing` has the bench).
5. **Queue transport + forced separation** (longer term): supersedes watcher path; identity-verification and wedge-detection remain valid regardless of transport.
6. **Docs inheritance**: jarvis-brain AGENTS.md's durable comms guidance (E7 pi-glyph delivery gotchas, capture-pane rules, comms protocol) must be inherited into stack docs before P0 retirement.
## 5. Rules for tonight's agents
- Lane: **`next`** only; nothing to `main` without Jason (standing ruling).
- Attribution caveat #1280: Gitea/git identity from this host may misattribute (issue #1295 showed as created by `@mos-dt-0`); prefer per-invocation `git -c user.name=<seat>` and verify what the remote recorded.
- Do not delete `sb-it-1-dt:docs]` (untracked file at repo root) — it is cited fleet-wide as incident evidence.
- Edit user contracts in `~/.mosaic/`, never the templates in `~/.config/mosaic/`.
- Do not restart other fleet seats unilaterally (goals/scrappy/sanity restart decisions are fred's/Jason's per the docs-seat report).
## 6. Artifact map
| Artifact | Where |
| ------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------ |
| Decision records D-001/D-002 (strict, pending ruling) | jarvis-brain `docs/decisions/mosaic-stack/2026-08-17_mosaic-d-00{1,2}_*.md` |
| Incident issue + provisioning comment | stack #1295 + comment 23027 |
| ensure-watcher.sh (reference implementation) | jarvis-brain `scripts/comms/ensure-watcher.sh` (commit `701c353b1`) |
| Bench for prompt A/B (pi, thinking levels, footer-token semantics) | jarvis-brain `domains/software-dev/mosaic-stack/prompt-testing/` + `docs/reports/2026-08-17-prompt-testing-glm-bench.md` |
| Launcher inspection basis | `@mosaicstack/mosaic` 0.0.49 `dist/commands/launch.js` (composeContract / ensureRuntimeConfig / harness-home isolation) |
| Guardrail edits | `~/.mosaic/{SOUL,AGENTS,STANDARDS}.md` on sb-it-1-dt (2026-08-17 16:5317:04) |