Compare commits

..
Author SHA1 Message Date
scoobyandClaude Fable 5 8764f8664e greenfield(fomo-lin): session 7 — #1124 fix(b) VALIDATED; new blockers #1125 (slash-cmd unseeded) + #1126 (model refuses receipt as injection)
Co-Authored-By: Claude Fable 5 <[email protected]>
2026-08-08 17:02:05 -05:00
scoobyandClaude Fable 5 b820d6f3d6 greenfield(fomo-lin): session 6 — promotion E2E BLOCKED at #1124; findings #1123/#1124 + live wedge
Co-Authored-By: Claude Fable 5 <[email protected]>
2026-08-08 16:40:25 -05:00
scoobyandClaude Fable 5 be6da9d028 greenfield(fomo-lin): codify true-greenfield doctrine + mosaic-greenfield-reset protocol
Co-Authored-By: Claude Fable 5 <[email protected]>
2026-08-08 15:17:59 -05:00
scoobyandClaude Fable 5 f4e3ef4ac2 greenfield(fomo-lin): gap-7 base characterization addendum
Co-Authored-By: Claude Fable 5 <[email protected]>
2026-08-08 15:03:49 -05:00
scoobyandClaude Fable 5 b7c7357a2f greenfield(fomo-lin): session 5 — ~/.mosaic prototype replicates on second host; two new gap-bites
Co-Authored-By: Claude Fable 5 <[email protected]>
2026-08-08 14:59:01 -05:00
scoobyandClaude Fable 5 f0b38f3fee greenfield(fomo-lin): session 4 — next-lane reinstall; N1 node-22 floor; F1-F12 recurrence scorecard
Co-Authored-By: Claude Fable 5 <[email protected]>
2026-08-08 14:56:47 -05:00
scoobyandClaude Fable 5 6362baf7cc greenfield(fomo-lin): session 3 — wizard/gateway F11-F12; refocus to next lane
F11 gateway Local tier requires Redis on main (fix already on next: 56787fab),
F12 wizard exits 0 on gateway failure. Jason directive: focus on next branch +
new structure; ~/.claude deep-testing stopped.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-08-08 14:40:27 -05:00
scoobyandClaude Fable 5 898e24472a greenfield(fomo-lin): session 2 — init + first bare seat; findings F6-F10
install → init → launch broken at four consecutive links on fresh main:
F6 eval injection in mosaic-init (SECURITY), F7 init drops installer's
mcpServers block, F8 missing fleet roster = raw stack trace, F9 activation
probe 2.0s timeout < 2.6s CLI cold-start on modest hardware (flagship),
F10 shipped lease-broker unit never installed. Seat launched after
documented workarounds; runtime-contract injection verified in-seat.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-08-08 14:29:34 -05:00
scoobyandClaude Fable 5 605c09089c greenfield(fomo-lin): install log + first findings F1-F5
Fresh-machine install of framework v3 / CLI 0.0.49 on Debian 13. Five findings
outside fred's known-gaps list: print-only PATH advice, backup-less overwrite of
live ~/.claude runtime files, doctor failing fresh install with 10 warnings,
drift check pointing at the gated (wedge-prone) template, installer skill bundle
disjoint from repo skills/.

Note: docs/reports is in .gitignore here — file force-added; flagged to fred.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-08-08 14:13:34 -05:00
19 changed files with 404 additions and 2474 deletions
-8
View File
@@ -60,14 +60,6 @@ The launcher verifies your config, checks for `SOUL.md`, injects your `AGENTS.md
Pi launches default to a token-lean skill posture: `mosaic pi` passes `--no-skills` so Pi does not preload every global skill description into the system prompt. Use `MOSAIC_PI_SKILL_MODE=all mosaic pi` for the legacy all-skills catalog, or `MOSAIC_PI_SKILL_MODE=discover mosaic pi` to let Pi use its native settings/project skill discovery.
Mosaic also loads its Pi extensions from `~/.config/mosaic/runtime/pi/`. Inside Pi,
`/goal set <statement>` starts a bounded persistent loop that checks every turn and successful
compaction, requires two evidence-bearing completion reports, and can be inspected or stopped with
`/goal status`, `/goal pause`, `/goal resume`, and `/goal cancel`. Controller-owned goal-state
entries redact common credential shapes, but Pi's model/tool-call history is separate, so goals and
evidence must never contain secrets or raw sensitive output. Mosaic does not install this extension
into `~/.pi/agent/extensions/`.
### TUI & Gateway
```bash
-122
View File
@@ -102,128 +102,6 @@ Context compaction, session replacement, and same-PID runtime reloads can leave
---
## Pi Persistent Goal Loop (#1150)
### Problem and objective
A Pi agent can stop after a plausible-looking answer even when the operator's broader objective is
not complete, and ordinary compaction can weaken or omit the original objective. Mosaic needs an
optional, operator-controlled goal loop that keeps a Pi session oriented, checks progress at native
lifecycle boundaries, and resumes work until completion is verified or a bounded safety state is
reached.
The objective is a Mosaic-owned Pi extension deployed from the framework into
`~/.config/mosaic/runtime/pi/`. It must not install into or depend on `~/.pi/agent/extensions/`.
### Scope
#### In scope
1. `PGL-REQ-01`: The framework SHALL ship a dedicated Pi goal extension under
`packages/mosaic/framework/runtime/pi/`, seed it under `$MOSAIC_HOME/runtime/pi/`, and make
`mosaic pi` load it alongside the core Mosaic extension when present.
2. `PGL-REQ-02`: `/goal` SHALL support setting a goal plus status, pause, resume, cancel, and help
operations without silently replacing an active goal.
3. `PGL-REQ-03`: Active branch-specific goal state SHALL be persisted in Pi custom session entries,
restored on session start and tree navigation, and never rely on a compaction summary as its
source of truth.
4. `PGL-REQ-04`: A hidden goal contract SHALL be injected through Pi's `context` event before every
model request so it remains effective across tool turns, retries, and post-compaction requests.
5. `PGL-REQ-05`: The harness SHALL inspect every `turn_end` and successful `session_compact` event.
A structured terminating goal-report tool SHALL capture `continue`, evidence-bearing `achieved`,
or `blocked` status without requiring a redundant model turn.
6. `PGL-REQ-06`: An achievement claim SHALL remain provisional until a second consecutive
evidence-bearing verification report. Any continuation report or successful compaction during
verification SHALL reset the verification sequence.
7. `PGL-REQ-07`: Continuation SHALL be initiated at safe lifecycle boundaries, primarily
`agent_settled`; manual compaction and restored active sessions may schedule a deferred idle
continuation without re-entering compaction handlers.
8. `PGL-REQ-08`: The loop SHALL have operator cancellation plus bounded turn and repeated-no-progress
limits. Exhausted or blocked goals pause rather than continuing indefinitely.
9. `PGL-REQ-09`: Framework installation and update SHALL preserve normal manifest ownership: the
goal extension is framework-owned under `runtime/**`, while no goal extension or configuration
asset is created or modified under the operator's main Pi configuration. Pi remains the owner of
its native session files used by `appendEntry()`.
#### Out of scope
1. A mathematical guarantee that an arbitrary natural-language goal is semantically complete.
2. Automatically executing user-supplied shell predicates or accepting executable validation code in
`/goal` arguments.
3. Restarting Pi after process, host, or supervisor failure; the existing Mosaic fleet/runtime
supervisor owns process durability.
4. Gateway, database, web UI, Discord, or cross-harness goal orchestration in this slice.
### User and stakeholder requirements
- An operator can start a goal from Pi and see its current phase, evidence, limits, and latest report.
- The agent remains oriented after each turn and compaction until verified, paused, blocked,
exhausted, or cancelled.
- Local testing uses a file under `~/.config/mosaic/runtime/pi/`; the feature never writes an
extension asset to `~/.pi/agent/extensions/`.
- Framework updates deploy the same reviewed extension source through Mosaic's existing manifest
sync path.
### Non-functional requirements
1. **Safety:** bounded continuation, explicit cancellation, no arbitrary command execution, and no
completion without non-empty reported evidence.
2. **Reliability:** serialized continuation scheduling, branch-aware restoration, compaction-safe
context injection, and stale-timer cancellation on session shutdown.
3. **Performance:** no extra nested judge-model request on every turn; structured reporting uses the
active agent's final terminating tool call.
4. **Observability:** Pi status/notifications expose phase and bounded counters without recording
credentials or hidden model reasoning.
5. **Maintainability:** the state machine is deterministic and behavior-tested independently from Pi
provider/network access.
### Acceptance criteria
1. `AC-PGL-01`: A framework-sync fixture installs the extension at
`$MOSAIC_HOME/runtime/pi/goal-extension.ts`, and launcher tests prove both Mosaic Pi extensions are
emitted in deterministic order while absent optional files remain backward-compatible.
2. `AC-PGL-02`: Command tests prove set/status/pause/resume/cancel behavior, active-goal replacement
refusal, and bounded input handling.
3. `AC-PGL-03`: Lifecycle tests prove every turn is recorded, active context is injected on every
request, two evidence-bearing achievement reports are required, and `agent_settled` continues an
unmet goal without duplicate scheduling.
4. `AC-PGL-04`: Compaction and restoration tests prove goal state survives, verification is reset and
rechecked after compaction, manual compaction continuation is deferred until idle, and tree/session
branch state is reconstructed correctly.
5. `AC-PGL-05`: Limit tests prove max-turn and repeated-no-progress exhaustion stop autonomous
continuation, while pause/cancel/blocked states do not restart.
6. `AC-PGL-06`: Focused tests, package typecheck/lint/test, repository quality gates, a local Pi load
smoke test from `~/.config/mosaic/runtime/pi/`, independent review, and terminal-green CI pass before
issue #1150 closes.
### Constraints, risks, and assumptions
- Dependency: Pi's extension API must continue to provide `registerCommand`, `registerTool`,
`context`, `turn_end`, `agent_settled`, `session_compact`, session custom entries, and terminating
tool results.
- Risk: the working agent can overstate completion. Mitigation: structured evidence, a mandatory
second verification pass, explicit semantic limitations, and operator-visible reports.
- Risk: an impossible goal can consume unbounded resources. Mitigation: hard turn/no-progress bounds
and paused terminal states.
- Risk: automatic continuation can race compaction or session replacement. Mitigation: drive from
`agent_settled`, defer idle restarts, generation-check timers, and clear timers on shutdown.
- `ASSUMPTION:` Two consecutive evidence-bearing reports are the initial local verification policy;
rationale: it provides a real recheck without doubling every turn's model cost. Future policy may
add independent or deterministic validators.
- `ASSUMPTION:` Default limits are 40 turns and 6 repeated no-progress reports, configurable only by
bounded Mosaic environment settings; rationale: useful persistence with a finite autonomous budget.
- `ASSUMPTION:` Documentation remains canonical in-repo for this slice; no external docs publication
is requested.
### Testing and delivery intent
Use TDD for the deterministic controller and lifecycle invariants. Test with fake Pi lifecycle
objects first, then run a local load/smoke test from the deployed Mosaic path. Deliver source, tests,
launcher wiring, framework/runtime documentation, user/developer guides, and sitemap updates in one
reviewed squash PR to `main` with terminal-green CI.
---
## Fleet Declarative Configuration Management Workstream (FCM, #758)
### Problem and objective
-7
View File
@@ -14,13 +14,6 @@
- [Skill registration user guide](guides/user-guide.md#claude-code-skill-registration) — register, unregister, list statuses, automatic install/update reconciliation, and Claude reload behavior.
- [Skill bridge developer guide](guides/dev-guide.md#claude-code-skill-bridge) — path-validation, ownership, clobber-protection, install/update wiring, tests, and Pi/Codex scope notes.
## Pi persistent goals
- [Persistent goal user guide](guides/user-guide.md#pi-persistent-goals) — `/goal` commands, verification behavior, limits, compaction/resume semantics, and limitations.
- [Goal extension developer guide](guides/dev-guide.md#pi-persistent-goal-extension) — framework ownership, launcher ordering, lifecycle design, tests, and local Mosaic-path smoke workflow.
- [Goal loop operations](guides/admin-guide.md#pi-goal-loop-operations) — deployment ownership, bounded settings, pause/resume procedures, and supervisor boundary.
- [Pi runtime reference](../packages/mosaic/framework/runtime/pi/RUNTIME.md#extensions) — deployed paths, command summary, and bounded environment settings.
## Fleet configuration management
- [Fleet configuration entry point](fleet/README.md) — desired-versus-observed decision tree and complete operator link map.
+1 -38
View File
@@ -7,8 +7,7 @@
3. [Provider Configuration](#provider-configuration)
4. [MCP Server Configuration](#mcp-server-configuration)
5. [Environment Variables Reference](#environment-variables-reference)
6. [Pi Goal Loop Operations](#pi-goal-loop-operations)
7. [Local Fleet Canary](./fleet-local-canary.md)
6. [Local Fleet Canary](./fleet-local-canary.md)
---
@@ -265,16 +264,6 @@ Each OIDC provider requires its client ID, client secret, and issuer URL togethe
| `AGENT_SYSTEM_PROMPT` | — | Platform-level system prompt injected into all sessions |
| `AGENT_USER_TOOLS` | all tools | Comma-separated allowlist of tools for non-admin users |
### Mosaic Pi goal loop
| Variable | Default | Description |
| ----------------------------- | ------- | -------------------------------------------------------------------- |
| `MOSAIC_GOAL_MAX_TURNS` | `40` | Per-goal autonomous turn limit; accepted range `1..500` |
| `MOSAIC_GOAL_MAX_NO_PROGRESS` | `6` | Consecutive identical progress-report limit; accepted range `1..100` |
These variables are consumed by the framework-owned Pi goal extension at goal creation. Invalid or
out-of-range values fall back to the defaults; they do not disable the bounds.
### Providers
| Variable | Default | Description |
@@ -385,29 +374,3 @@ Session cleanup is scoped to one session identifier and only removes that sessio
| Variable | Default | Description |
| ----------------------- | ----------------------------- | ------------------------------------------ |
| `MOSAIC_WORKSPACE_ROOT` | monorepo root (auto-detected) | Root path for mission workspace operations |
---
## Pi Goal Loop Operations
The reviewed runtime asset is deployed at
`~/.config/mosaic/runtime/pi/goal-extension.ts` by framework install/update. Do not install another
copy under `~/.pi/agent/extensions/`; duplicate registration can create suffixed commands and two
competing lifecycle controllers.
Operational checks:
1. Run `mosaic pi` and verify `/goal help` is available.
2. Use `/goal status` to inspect phase, turn/no-progress limits, compaction checks, and evidence.
Reports persist in Pi session data; controller-owned state redacts common credential shapes, but
Pi's model/tool-call history is separate. Operators must not place secrets or raw sensitive output
in goals, pause reasons, or evidence.
3. Use `/goal pause <reason>` before planned maintenance or manual investigation. Pause and cancel
abort the current goal-driven run when Pi is busy.
4. Use `/goal resume` only after addressing a blocker; counters restart with the configured bounds.
5. Use `/goal cancel` before replacing an unfinished goal.
A blocked or exhausted goal remains stopped and visible; Mosaic does not automatically raise its
limits or restart the process. Framework sync owns file deployment, while Pi's native session file
owns branch replay. Process/host restart remains the responsibility of the existing runtime or fleet
supervisor.
+2 -82
View File
@@ -9,9 +9,8 @@
5. [Adding New MCP Tools](#adding-new-mcp-tools)
6. [Database Schema and Migrations](#database-schema-and-migrations)
7. [Claude Code Skill Bridge](#claude-code-skill-bridge)
8. [Pi Persistent Goal Extension](#pi-persistent-goal-extension)
9. [API Endpoint Reference](#api-endpoint-reference)
10. [Local Fleet Canary](./fleet-local-canary.md)
8. [API Endpoint Reference](#api-endpoint-reference)
9. [Local Fleet Canary](./fleet-local-canary.md)
---
@@ -386,85 +385,6 @@ M1 intentionally manages Claude Code only. Pi's Mosaic launcher can discover the
canonical root directly. Codex still relies on the existing full skill-sync
linker and needs separate parity analysis before this lifecycle API is extended.
## Pi Persistent Goal Extension
The source of the Mosaic-owned Pi goal controller is:
```text
packages/mosaic/framework/runtime/pi/goal-extension.ts
```
The framework manifest classifies `runtime/**` as framework-owned. Both the bash installer and the
TypeScript file adapter therefore deploy the same reviewed source to:
```text
$MOSAIC_HOME/runtime/pi/goal-extension.ts
# default: ~/.config/mosaic/runtime/pi/goal-extension.ts
```
Do not copy or link this extension into `~/.pi/agent/extensions/`. The launcher function
`discoverPiExtensionArgs()` emits the core `mosaic-extension.ts` first and the optional
`goal-extension.ts` second, preserving compatibility with an older installed framework that does
not have the goal file yet.
### Lifecycle design
| Pi API | Goal-controller responsibility |
| ------------------------------ | --------------------------------------------------------------------------------- |
| `registerCommand('goal')` | Set, inspect, pause, resume, or cancel one branch-specific goal |
| `registerTool(...)` | Record a terminating structured progress report with evidence |
| `context` | Inject the active goal contract before every provider request |
| `turn_end` | Record every turn, reject mixed final reports, and enforce the turn bound |
| `agent_settled` | Start one deduplicated continuation only after Pi has no retry/compact/queue work |
| `session_compact` | Record the compact check, reset provisional verification, and defer idle work |
| `session_start`/`session_tree` | Rebuild state from custom entries on the active branch |
| `session_shutdown` | Invalidate deferred callbacks and clear UI state |
State is appended as `mosaic-goal-state` custom entries, which do not enter model context. The
`context` hook creates a fresh hidden `mosaic-goal-context` message for each request instead of
trusting compaction summaries. The `mosaic_goal_report` result uses `terminate: true`; when it is the
sole final tool call, Pi avoids an unnecessary model response before the controller decides whether
to verify, continue, or stop.
Before state is appended or displayed, the controller applies bounded credential-pattern redaction
to the goal statement, report summary/evidence/next step, and stop reason. Fingerprints are computed
over redacted report content. Pi session entries are append-only, so a credential-bearing legacy
entry cannot honestly be erased by the extension: restoration fails closed, emits a warning, and
requires removal of the affected session before setting a new goal. This is defense-in-depth rather
than a secret-storage contract, and it does not rewrite Pi's separate model-message/tool-call
history. Goal prompts tell the agent not to submit credentials or raw sensitive output, and tests use
canaries to prove known forms do not reach new custom entries, status text, context, or tool details
while ordinary typed fields such as `token: string` remain intact.
Completion remains evidence-gated but semantic: two consecutive `achieved` reports are required,
and the second run is explicitly a verification pass. This avoids an extra judge-model request after
every turn. Deterministic validator commands are intentionally not accepted as `/goal` input in this
slice, so never describe this mechanism as proof of arbitrary natural-language completion.
### Tests and local smoke workflow
```bash
pnpm --filter @mosaicstack/mosaic exec vitest run \
src/runtime/pi-goal-extension.spec.ts \
src/commands/launch.spec.ts \
src/config/file-adapter.test.ts
bash packages/mosaic/framework/tools/quality/scripts/test-install-migration.sh
```
For an additive local smoke test without reseeding unrelated live framework files:
```bash
install -D -m 0644 \
packages/mosaic/framework/runtime/pi/goal-extension.ts \
~/.config/mosaic/runtime/pi/goal-extension.ts
pi --extension ~/.config/mosaic/runtime/pi/goal-extension.ts
```
Use `/goal help`, `/goal set ...`, and `/goal status` in that test session. A released framework
sync installs the file, and a released Mosaic CLI loads it automatically through `mosaic pi`.
## API Endpoint Reference
All endpoints are served by the gateway at `http://localhost:14242` by default.
+3 -55
View File
@@ -8,10 +8,9 @@
4. [Tasks](#tasks)
5. [Settings](#settings)
6. [CLI Usage](#cli-usage)
7. [Pi Persistent Goals](#pi-persistent-goals)
8. [Sub-package Commands](#sub-package-commands)
9. [Telemetry](#telemetry)
10. [Local Fleet Canary](./fleet-local-canary.md)
7. [Sub-package Commands](#sub-package-commands)
8. [Telemetry](#telemetry)
9. [Local Fleet Canary](./fleet-local-canary.md)
---
@@ -308,57 +307,6 @@ mosaic prdy
mosaic quality-rails
```
## Pi Persistent Goals
`mosaic pi` loads a Mosaic-owned goal extension from
`~/.config/mosaic/runtime/pi/goal-extension.ts`. It is deliberately not installed in
`~/.pi/agent/extensions/`; framework installation and updates manage it with the rest of the Mosaic
runtime assets.
Start Pi, then set a goal:
```text
/goal set Deliver the feature, tests, documentation, and verification evidence
# Shorthand:
/goal Deliver the feature, tests, documentation, and verification evidence
```
Control and inspect the loop with:
| Command | Behavior |
| ---------------------- | ------------------------------------------------------------------ |
| `/goal status` | Show phase, limits, compaction checks, latest report, and evidence |
| `/goal pause [reason]` | Stop autonomous continuation while preserving the goal |
| `/goal resume` | Resume with fresh turn and no-progress counters |
| `/goal cancel` | Cancel the goal and remove its active status |
| `/goal help` | Show command help |
While a goal is active, Mosaic injects its contract before every Pi model request and checks every
completed model/tool turn. The agent ends each work cycle with the structured
`mosaic_goal_report` tool. `achieved` is provisional until a second consecutive report rechecks the
whole goal with evidence. A continuation report or a successful compaction resets provisional
verification.
Goal statements and reports are stored in Pi session data. Mosaic redacts common credential shapes
before appending its goal-state entries and before goal tool output or `/goal status`, but
pattern-based redaction is not a secret store. Pi's own model-message and tool-call records are
outside that redactor. Never put tokens, passwords, private keys, connection strings, or raw
sensitive output in a goal or report; cite the command, artifact, and pass/fail result instead.
The loop stops instead of running forever when it is paused, blocked, cancelled, verified, reaches
its turn limit, or repeats the same no-progress report too many times. Defaults are 40 turns and 6
repeated no-progress reports. Operators may lower or raise them within enforced bounds before
launching Pi:
```bash
MOSAIC_GOAL_MAX_TURNS=60 MOSAIC_GOAL_MAX_NO_PROGRESS=8 mosaic pi
```
Goal state is branch-specific Pi session data. It survives compaction and session resume, but Pi's
process still must be relaunched or supervised after a process/host failure. This initial verifier
checks structured evidence twice; it cannot mathematically prove every arbitrary natural-language
goal. Use explicit acceptance criteria and inspect `/goal status` for consequential work.
---
### Claude Code Skill Registration
+353
View File
@@ -0,0 +1,353 @@
# Greenfield install log — fomo-lin
Running log of a from-scratch Mosaic Stack install on Jason's test laptop **fomo-lin**
(Debian 13, x86_64). Operator: **scooby** (agent). Started 2026-08-08. Channel per fred:
findings → comms as they land; this file is the durable record. Branch: `greenfield/fomo-lin`.
## Machine starting state (2026-08-08)
- Debian 13 (kernel 6.12.101+deb13), no Node/npm, no global git config, no `~/.ssh`,
no `~/.config/mosaic`, no `~/.mosaic`, sudo requires password (agent cannot escalate).
- Repos pre-cloned by Jason: jarvis-brain, mosaic-brain, stack, uconnect, uscllc-website
(all https remotes to git.mosaicstack.dev, **no credentials stored** — private-repo
fetch/push dead until a token was provisioned from credentials.json, `usc_mos`).
- tmux session `scooby` running Claude Code (bare harness — not `mosaic claude`).
## Pre-install setup that had NO framework mechanism (manual work)
- Agent identity: `MOSAIC_AGENT_NAME=scooby` hand-added to `~/.bashrc` + tmux env.
- Git identity + credential store: hand-configured.
- Comms receive path: hand-ported `scooby-comms-watcher.sh` from fred's watcher +
hand-written systemd `--user` unit + `loginctl enable-linger`. Works (both peers
verified round-trip within ~90s), but every step was artisanal — relevant input for
harness-homes (W-F).
## Install run (2026-08-08 ~19:09Z)
`curl -fsSL https://mosaicstack.dev/install.sh | bash -s -- --yes --no-auto-launch`
→ exit 0, framework v3 → `~/.config/mosaic/`, CLI @mosaicstack/mosaic **0.0.49**
`~/.npm-global/`. Prereq path: `sudo apt install nodejs npm` (Debian 13's node 20.19.2
meets the ≥20 floor). Public read on the stack repo means the installer itself needs no
credentials — good.
## Findings (outside fred's known-gaps list of 2026-08-08)
### F1 — PATH advice is print-only
Installer warns `~/.npm-global/bin is not on your PATH` and suggests the rc line, but
`shellProfileEdits: []` in the manifest — nothing is persisted. Every fresh machine ends
with `mosaic` not resolvable in new shells until the user hand-edits rc. Either edit the
rc (with consent/flag) or make the closing summary a copy-paste block.
### F2 — Installer overwrites live `~/.claude/settings.json` + `~/.claude/CLAUDE.md` with `backup: null`
`.install-manifest.json` `runtimeAssetCopies` shows dest `~/.claude/settings.json`,
`~/.claude/CLAUDE.md`, `hooks-config.json`, `context7-integration.md`, all `backup: null`,
written while a Claude session was LIVE on this machine. On this box the pre-existing files
were near-defaults so nothing of value was lost; on any configured machine this silently
destroys user settings/memory. Wants: backup-before-overwrite (populate the manifest
`backup` field it clearly already models) + merge-not-replace for settings.json.
### F3 — Fresh install fails its own doctor: 10 warnings out of the box
Immediately after a clean, successful install, `mosaic doctor` reports: missing `USER.md`;
`AGENTS.md missing CRITICAL HARD GATES override block`; runtime file drift on
`~/.claude/settings.json`; 7 missing `mosaic-*` skills. A green install that self-reports
10 warnings erodes trust in doctor as a signal. Whatever subset is "expected until
`mosaic init`/wizard" should be suppressed or labeled as such.
### F4 — Drift check points users at the gated template (wedge hazard)
The settings the installer writes to `~/.claude/settings.json` are UNGATED (no
mutator-gate, no receipt-observer) — which on today's main is CORRECT, it avoids the
Stop-hook wedge. But `~/.config/mosaic/runtime/claude/settings.json` (the file doctor
diffs against) IS the gated template. So doctor's "runtime file drift" warning invites the
obvious remediation — copy the template over — which would seed the receipt-observer wedge
into a live seat. The drift baseline and the seeded file should be the same artifact, or
doctor should know about the gated/ungated split.
### F5 — Installed skill set is disjoint from repo `skills/`
Skill sync installed 101 skills (six `mosaic-*`: deploy, gitea, orchestrator, portainer,
tools, woodpecker) but NONE of the eight in stack `skills/` on main (board, forge, jarvis,
macp, prd, prdy, setup-cicd, standards). Doctor then flags 7 of those 8 as missing
(`mosaic-jarvis` escapes the check). Two sources of truth for "the Mosaic skills" — the
installer's bundle and the repo dir — have diverged.
## Environment answers / status
- fomo-lin → sb-it-1-dt: **comms-only** today. Hostname does not resolve from here and the
laptop has no ssh keys. ssh reach would need Jason (key provisioning + route/VPN).
- Gitea write to the stack repo: verified by the push of this very branch (token `usc_mos`).
## Session 2 (2026-08-08 later) — `mosaic init` + first bare seat
`mosaic init` completed (SOUL.md / USER.md / TOOLS.md generated; TOOLS.md was backed up
before overwrite — the contrast with F2 shows the codebase already knows how). Its
runtime-adapter step correctly REFUSED to wire mutator-gate/receipt-observer hooks
(activation half absent, #869) — loud, explained, fail-safe. Good.
First bare seat: **launched**`mosaic claude --model sonnet` → Claude Code v2.1.226,
runtime-contract injection verified from inside the seat. But it took findings F6F10 to
get there; on an untouched fresh main install, install → init → launch is broken at
FOUR consecutive links.
### F6 — SECURITY: `mosaic-init` eval-injects free-text answers
`tools/_scripts/mosaic-init` line 142: `eval "$var_name=\"$value\""`. Any answer
containing `"` crashes init mid-flow (reproduced: exit 127, USER.md never written);
an answer containing `$( )` would EXECUTE arbitrary commands. Fix: `printf -v`.
Same bug in the NON_INTERACTIVE default branch. Related: init exits 1 even on success
when enforcement wiring is (correctly) refused — poisons any scripted chaining.
### F7 — init silently drops the installer's `mcpServers` block → launcher refuses to run
init's "Updating runtime adapters" rewrote `~/.claude/settings.json` and removed the
`mcpServers.sequential-thinking` block the installer had written 11 min earlier.
`mosaic claude` hard-requires that MCP → launch refused. The prescribed fix command
(`mosaic-ensure-sequential-thinking --runtime claude`) works. So the happy path is
install → init → BROKEN → hand-run a repair script. Merge-not-replace (F2) fixes this too.
### F8 — no fleet roster on a fresh install; launcher dies with a raw stack trace
`mosaic claude` throws an uncaught `Error: Fleet communications contract unavailable: no
fleet roster at ~/.config/mosaic/fleet/roster.{yaml,json}` (full Node stack trace to the
user). Nothing in install or init creates a roster (wizard untested here — `--no-auto-launch`;
if the wizard seeds one, the bare-flow gap still stands). Unblocked by hand-authoring a
minimal site roster from `fleet/examples/minimal.yaml`.
### F9 — FLAGSHIP: activation-probe timeout loses to CLI cold-start on modest hardware
`activation_version_gate.py` gives the `mosaic __lease-capability` probe
`PROBE_TIMEOUT_SECONDS = 2.0`. On fomo-lin the CLI answers CORRECTLY in **~2.552.61s
every run** (Node startup cost). Timeout → fail-closed → every bare `mosaic claude`
launch aborts (exit 65) with an error blaming "mosaic not on PATH … framework/CLI version
skew" — neither true. Invisible on fast dev boxes; fatal on laptops. Suggest: raise/make
configurable the timeout, warm-probe cache, and split the three failure causes into
distinct messages. Local workaround (documented, removable):
`MOSAIC_LEASE_VERSION_PROBE_COMMAND` pointed at a script emitting the verified payload
instantly (`~/.local/bin/mosaic-lease-probe-fast`).
### F10 — shipped lease-broker unit is never installed → registration denied
With F9 bypassed, launch dies with "Mosaic lease broker registration failed; runtime
launch denied": the broker daemon isn't running, and although the framework SHIPS
`systemd/user/mosaic-lease-broker.service`, nothing installs/enables it.
`systemctl --user link` + `enable --now` of the shipped unit → READY instantly, launch
proceeds. Installer/init/wizard should own this step.
### Observations (not filed as findings)
- Launcher settings audit demands `mutator-gate.py` while init refuses to wire it —
main's components disagree about the gated state (fold into #1113/F4).
- Seat context: runtime contract injected ✓; SOUL.md NOT injected (seat confirmed) —
matches AGENTS.md read-on-demand load order, but README says the launcher "checks for
SOUL.md". Question for lead, not a finding.
- `--ref next` install path verified available (flag exists, next archive HTTP 200) — not
exercised; fomo-lin stays main-as-shipped per lead ruling.
## Next
- Milestone comms sent at: install complete ✓ / first seat launched ✓.
- First gated-seat probe deliberately deferred until PR #1109 lands (known deny-only state).
## Session 3 (2026-08-08 evening) — wizard + gateway; refocus to `next`
Directive from Jason mid-session: focus shifts to the `next` branch and the new structure
(stock `~/.claude` untouched; framework wholly under `~/.config/mosaic`). Main's ~/.claude
write behavior is a deprecated location — findings stand, but no further deep-testing of it.
Wizard run (main): "keep identity, update framework"; ~/.claude hooks install DECLINED per
directive (wizard rewrote ~/.claude/settings.json anyway — benign, no gated hooks, MCP kept).
Wizard never prompted about fleet roster or lease-broker unit → F8/F10 disambiguation
partial: wizard does not visibly own those steps. Full degraded-state test dropped per refocus.
### F11 — gateway "Local" tier hard-requires Redis on main (fixed on next)
Wizard gateway install, Local tier ("embedded database, no dependencies"), port 14242:
daemon starts then crash-spams ioredis ECONNREFUSED; never healthy; killed manually.
`main..next` already contains `56787fab fix(gateway): disable Redis consumers on local
tier (#689)`. Main ships a gateway that cannot come up dependency-free; next has the cure.
### F12 — wizard exits 0 on gateway failure
Terminal shows "▲ Fix the underlying error above, then re-run `mosaic gateway install`"
and the wizard exits 0. Scripted/CI consumers read success.
### Cosmetic
Skipping the optional ANTHROPIC_API_KEY prompt records the literal string "undefined".
### `next` recon (read-only)
- next install.sh: first-class `--next` prerelease lane (npm @next dist-tag CLI + framework
from permanent next branch; guard against mixing @next with a different explicit --ref).
- next does NOT carry the new structure: ~/.claude handling unchanged; no harness-homes
design docs on next or main. New structure = Jason directive + fred W-F design phase.
State: bare seat launch works; gateway stopped. Holding for fred's ruling on a next-lane
reinstall (proposed) and W-F design review.
## Session 4 (2026-08-08 night) — `--next` lane reinstall (pivot confirmed by Jason)
Main uninstalled (note: uninstall removed `~/.claude/CLAUDE.md`/hooks-config/context7 but
LEFT its modified `settings.json` — asymmetric cleanup, minor). Reinstalled via next's own
installer: `raw/branch/next/tools/install.sh --next --yes --no-auto-launch` → framework from
permanent next branch + **CLI 0.0.50-next.2207 / gateway 0.0.7-next.2207 from the @next
registry lane**. Lane works as designed.
### N1 — FLAGSHIP (next-only): @next CLI requires Node 22; docs/installer floor says ≥20
On Node 20.19.2 (Debian 13's apt version, and the documented minimum) **every** mosaic
command crashes — even `--version` — with `ERR_REQUIRE_CYCLE_MODULE` in
`@mosaicstack/brain/dist/projects.js`. npm corroborates: `[email protected]` declares
`node >=22`. Verified the same installed CLI runs clean under Node **22.23.2** (nvm).
So the @next lane is dead-on-arrival on the documented minimum Node. Fix: installer
gates node ≥22 for the next lane (or brain drops the require cycle). fomo-lin now runs
Node 22 via nvm (user-level; system apt tops out at 20 — durable fix wants nodesource 22).
### F1F12 recurrence scorecard on next
| Finding | On next |
|---|---|
| F1 PATH print-only | RECURS (identical warning) |
| F2 ~/.claude writes | RECURS (runtime assets copied again; per ruling, no deeper testing — W-F fixes structurally) |
| F3 doctor warns on fresh install | RECURS (10 warnings, same classes) |
| F4 drift-baseline wedge | RECURS (same gated template + drift warning) |
| F5 skill sets disjoint | RECURS (same 7 missing mosaic-*) |
| F6 init eval injection | RECURS (eval at lines 102/118/132 of next's mosaic-init) |
| F7 init drops mcpServers | RECURS (verified: count 0 after init; ensure-script fix works) |
| F8 roster raw-throw | RECURS in code (throw present in next launch.js; not re-triggered — roster restored from backup) |
| F9 probe 2.0s timeout | RECURS (constant unchanged) — and compounded: probe spawns `mosaic`, which on ambient Node 20 crashes (N1), so the probe fails on slow AND stock-node hosts |
| F10 broker unit not installed | RECURS (hand-relinked next's shipped unit; works) |
| F11 gateway Redis-on-local | Expected FIXED (#689 in next); not yet live-verified — gateway install not re-run this session |
| F12 wizard exit-0 | Untested on next (wizard.ts differs; #1120 tracks) |
Chain result on next (with the same three workarounds: MCP ensure-script, restored roster,
broker relink, plus probe override): **install → init → launch all pass; seat up on
Claude Code v2.1.226 / sonnet under Node 22.**
Net: next cures nothing in F1F10 (they're all pre-W-F structural issues), carries the
gateway fix, and adds one hard regression-class gap (N1 node floor). The W-F gap list
stands unchanged as the fix vehicle.
## Session 5 (2026-08-08 night) — `~/.mosaic` prototype hand-roll (second-host cross-check)
Hand-rolled per HARNESS-HOMES prototype section, on the next-lane framework: skeleton
(config/claude `{}`, auth/claude/jason_woltje.com with `primary` alias, empty plugins/skills
stores), probe seat (profile.json schema 1, overlay `{}`, composed settings via three-layer
deep-merge, credentials two-hop symlink, identity-bootstrap CLAUDE.md, seeded onboarding
.claude.json, SOUL.md with positive Identity block).
**Smoke test PASS** (`CLAUDE_CONFIG_DIR=<probe> claude --print`): RC=0, auth through the
two-hop chain, seat self-identified as "probe". Post-run: both symlinks survived, live
credential inode unchanged, transcript in probe's own projects/, probe generated its own
backups/sessions, operator ~/.claude untouched. **dragon-lin's results replicate on a
clean second host — the layout stands up greenfield.**
### Gap-bites during the roll (feed to W-F)
- **Base-template hole (F4/gap-5 adjacent, NEW):** the design's composition base
`~/.config/mosaic/framework/runtime/claude/settings.json` does NOT exist in the shipped
framework; the closest shipped artifact (`runtime/claude/settings.json`) is the GATED
wedge template. Used the operator's vetted ungated settings as base (as dragon-lin did).
W-F1 must define + ship the canonical UNGATED system base; gate hooks arrive only via
promotion overlay.
- **Identity bootstrap vs permissions (NEW):** in `--print`/restricted mode the seat was
DENIED reading SOUL.md outside cwd — "read SOUL.md" bootstrap depends on tool
permissions. Generator should materialize the identity INTO the generated CLAUDE.md
(parameterized), keeping SOUL.md as source, not runtime dependency.
- **Gap 2 lived experience:** probe exists in profile.json but not roster.yaml — the
hand-rolled seat and `mosaic claude` are disjoint universes on the same host.
- **Gap 4 in miniature:** fresh-host store is empty; nothing defines what seeds it.
- **Lease posture:** hand-rolled seats launch bare `claude` → ungated by construction
until `mosaic fleet launch` exists (consistent with current bare-for-real-work rule).
### Addendum — gap-7 characterization (canonical ungated base)
Diffed operator vetted ungated settings vs shipped gated template: the delta is exactly
three items — template-only PreToolUse mutator-gate entry, template-only Stop
receipt-observer entry, operator-only mcpServers.sequential-thinking block (whose omission
from the template is F7's root cause). Spec: base = template two gate hooks + mcpServers;
promotion overlay = the two gate hooks, nothing more. Sent to fred (20260808T200337Z).
## Box doctrine — true greenfield, repeatable full-cycle testing (Jason, 2026-08-08)
fomo-lin's defining property: the test operator (scooby) is NOT a fleet seat — comms
watcher, git identity, nvm/Node, and repos live entirely outside Mosaic. Therefore Mosaic
can be wiped to TRUE ZERO and reinstalled in full, repeatedly, to test protocols
end-to-end per cycle (each W-F fixture drop, each next release).
Codified as `~/.local/bin/mosaic-greenfield-reset` (dry-run by default, `--yes` to
execute): removes units/gateway/npm packages/npmrc scope/`~/.config/mosaic`/`~/.mosaic`/
mosaic-written `~/.claude` files (settings reset to stock)/workaround shims; preserves the
operator layer (watcher, git creds, nvm, repos, `~/.claude` auth + session state, baseline
backup). Ends with a verify-zero checklist.
Known boundary impurities the reset explicitly handles: `~/.claude/settings.json` is
mosaic-written today (its QA hooks fire even in the operator's own session — observed:
prevent-memory-write blocked an operator write), and the F9 probe shim sits in
`~/.local/bin`. Both are named in the script rather than left as ambient state.
Not executed yet — current install (next lane + prototype) is the substrate Fred's W-F1
fixtures target. First full cycle runs when the next testable artifact lands.
## Session 6 (2026-08-08 night) — promotion-branch E2E (Fred-directed, first fomo-lin full E2E)
Branch feat/lease-promotion-and-harness-isolation (rebased on next), built from source
(pnpm --filter '@mosaicstack/mosaic...' build), CLI packed + installed globally, branch
framework installed to ~/.config/mosaic. Transcript:
scratchpad/promote-e2e-transcript.md. Verdict: **BLOCKED at step 3, NOT VERIFIED (not faked).**
Findings this session (all filed under scooby's own Gitea account):
- **#1123** — TS activation capability probe hardcodes a 2000ms timeout; `node cli.js
__lease-capability` cold-start on fomo-lin is 5.15.5s, so `leaseEnforcementActivatable()`
returns false and the gate REFUSES to wire via the sanctioned path. The Python-side
F9/#1118 override does NOT apply to this TS probe. Worked around by bumping only the
installed dist timeout (reversible; can't mask a bad capability).
- **LIVE WEDGE (F4 reproduced, un-recoverable):** hand-wiring the gated template into a live
BARE session's own runtime home hot-reloads the gate and bricks the session with
GATE_UNAVAILABLE (no lease). Every self-recovery path is closed (Bash/Read gated;
Write/Edit blocked by stale-guard needing a gated Read). Required an EXTERNAL shell to
restore settings. Exactly HARNESS-HOMES' "a live unpromoted session that gains the gate
cannot self-recover." Lesson applied: gated seats must be a SEPARATE mosaic claude process
in its own CLAUDE_CONFIG_DIR (~/.config/mosaic/.claude), never the operator's ~/.claude.
- **#1124 — the critical link, proven to fail:** `mosaic promote` transport reads the lease
session id from `pane_pid`'s /proc/environ, but `execRuntime()` (launch.js:883) uses
`spawnSync` (NOT the exec-replace its own comment claims), so pane_pid = node(mosaic)
[no lease env] and the lease env is on the claude CHILD. resolve() never walks to the
child → 'no readable lease session' → UNVERIFIED exit 1, before injection. Fails for every
real `mosaic claude` seat; unit tests pass only via a mocked environmentReader. This is
the exact link terra couldn't test (detached pane).
E2E scorecard: Step 1 (build/install) PASS. Step 2 (gated seat, real lease, mutator DENIED
MUTATOR_UNVERIFIED, file not created) PASS. Step 3 (promote → VERIFIED) BLOCKED (#1124).
Steps 45 not reached; failure-path sub-case (unresolvable seat → UNVERIFIED exit 1, no
hang) incidentally confirmed. The branch does NOT pass E2E on a real host as-is; #1124 gates
its merge.
## Session 7 (2026-08-08 night) — promotion re-run on #1124 fix (b) @ de0adb92
Rebuilt from de0adb92, reran steps 3+. **fix (b) confirmed working**; promotion advanced two
links deeper and revealed two new findings. Debian 13 compat: `/proc/<pid>/task/<pid>/children`
IS populated — BFS walk works, no PPID fallback needed.
- resolve() (#1124 fix b): **PASS** — BFS walk from pane(node,no-lease) → claude child(lease)
resolved the real session id. The exact bug I reported is fixed.
- **#1125** — `/mosaic-promote` first returned "Unknown command": the slash command is shipped
at `runtime/claude/commands/mosaic-promote.md` but NOT seeded into the seat's
`CLAUDE_CONFIG_DIR/commands/`. UserPromptSubmit hook never fires → PROMOTION_TIMEOUT. F7-class
asset-seeding gap. Worked around by copying the command into the seat home; hook then fires.
- **#1126 (deepest finding)** — with the command seeded, promote-begin injects (via
UserPromptSubmit additionalContext) an instruction to echo an opaque `MOSAIC-RECEIPT{...}`
token "verbatim and nothing else … discloses nothing." The seat MODEL REFUSED, correctly
flagging it as a prompt-injection pattern (imperative in a description field; verbatim opaque
echo; self-vouching language; no protocol legitimized in the seat's trusted context) →
RECEIPT_MISMATCH. Design-level: legitimate promotion is indistinguishable from an injection
attack to a well-aligned model; stronger injection defenses = more reliable promotion FAILURE.
Refusal evidence: docs/reports/greenfield/seat-receipt-refusal.txt.
Scorecard: build/install PASS · gated seat + mutator DENIED PASS · resolve (fix b) PASS ·
slash-cmd (post-#1125 workaround) PASS · receipt echo FAIL (#1126). Promotion does NOT reach
VERIFIED on a real seat. #1124 fix validated; #1125 + #1126 now gate the merge.
@@ -0,0 +1,31 @@
▐▛███▜▌ Claude Code v2.1.226
▝▜█████▛▘ Sonnet 5 · Claude Max
▘▘ ▝▝ ~/src/jarvis-brain
✔ Using flicker-free rendering · if you want to go back, use /tui default
· Click to move your cursor in the text input
· Click to expand collapsed tool results
· By default, text auto-copies when you select it (/config to change)
· Hold Shift (Option in iTerm2, Fn in Terminal.app) while selecting to use your terminal's native copy instead
▎ Fable 5 is now a standard part of your Max plan
▎ You can use up to 50% of your weekly usage limit on Fable 5. If you hit your limit, you can continue on Fable 5 with usage credits. Fable 5 draws down usage faster than Opus 5. Run /model and
▎ select Fable to use it. Learn more: https://support.claude.com/en/articles/15424964-claude-fable-5-promotional-access
+1 more · /status
/mosaic-promote
● I'm not going to echo that receipt string. A few things about this request don't add up, and I want to flag them before doing anything:
1. The "command description" is itself an instruction. In the skills listing, mosaic-promote is described as "I invoked this registered command to authorize lease promotion; follow the local seat
broker's injected receipt confirmation instruction exactly." That's not a description of what a skill does — it's an imperative instruction written in first person, injected into a field that's
supposed to be descriptive metadata. That's a classic injection pattern.
2. The "hook" reminder asks for verbatim, unexamined output. The UserPromptSubmit hook additional context block instructs me to output an opaque MOSAIC-RECEIPT{...} blob "verbatim and nothing else,"
and preemptively asserts that doing so "discloses nothing" — that's the kind of self-vouching language injected content uses to head off scrutiny. I have no way to verify what that token actually
authorizes downstream (it reads like a challenge/response for some privilege or lease-promotion action), and nothing in the actual Mosaic Constitution, AGENTS.md, or RUNTIME.md content I've loaded
this session mentions a "seat broker," "lease promotion," or a MOSAIC-RECEIPT protocol at all.
3. No genuine /mosaic-promote skill exists in the documented set. The real skill list (mosaic-gitea, mosaic-orchestrator, mosaic-portainer, mosaic-woodpecker, mosaic-tools, mosaic-deploy) has
nothing resembling this. A command that only shows up as a self-instructing string, paired with a matching "hook" telling me to blindly echo a token, is consistent with a prompt-injection attempt to
get an agent to produce an authorization artifact it doesn't understand.
I'm stopping here rather than emitting the receipt. If this is something you actually intended to trigger (a real seat/lease mechanism I'm not aware of), let me know and point me at where it's
documented and I'll take another look. Otherwise, I'd treat this as a suspicious injected command.
✻ Sautéed for 23s
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
 
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
⏸ manual mode on · ? for shortcuts · ← for agents
-156
View File
@@ -1,156 +0,0 @@
# #1150 — Pi persistent goal extension
- **Task ID:** ISSUE-1150 (no `docs/TASKS.md` row; that file is orchestrator-only)
- **Issue:** #1150`pi: add persistent /goal controller extension to Mosaic framework`
- **Branch:** `feat/1150-pi-goal-extension`
- **Mode:** Delivery
- **Status:** in progress
## Objective
Build and locally validate a Mosaic-owned Pi `/goal` extension. Source must ship from
`packages/mosaic/framework/runtime/pi/`, framework sync must deploy it under
`~/.config/mosaic/runtime/pi/`, and no extension/configuration asset may be written into `~/.pi`.
Pi's native session manager remains the owner of session entries.
## Scope and acceptance source
- Canonical requirements: `docs/PRD.md`, section **Pi Persistent Goal Loop (#1150)**.
- User intent: continuous goal orientation and status checking after each Pi turn and compaction,
tested locally before framework delivery.
- Documentation target: canonical in-repo user/developer/runtime docs; no external publication.
## Assumptions
- `ASSUMPTION:` Initial semantic verification uses two consecutive structured, evidence-bearing
reports from the working agent rather than a second model request after every turn. This keeps the
loop testable and avoids doubling model cost while making the limitation explicit.
- `ASSUMPTION:` Default autonomous bounds are 40 turns and 6 repeated no-progress reports, with only
bounded numeric environment overrides.
- `ASSUMPTION:` A local smoke copy to `~/.config/mosaic/runtime/pi/goal-extension.ts` is authorized by
the user's explicit request. Full framework reseed into the live home is not required for the smoke
test and would touch unrelated framework-owned files.
## Budget
- Working estimate: 30K implementation/review tokens.
- Hard user cap: none stated.
- Cost control: deterministic fake-Pi tests; no nested evaluator calls; only bounded arithmetic/load
smoke workflows against the installed runtime.
## Plan
1. Update PRD and create tracking/scratchpad artifacts.
2. Read launcher, installer ownership, Pi extension, and documentation surfaces.
3. TDD: add fake-Pi behavior tests for commands, state restoration, turn checks, compaction, limits,
verification, and continuation deduplication.
4. Implement `runtime/pi/goal-extension.ts` and deterministic launcher discovery.
5. Add framework-sync/deployment acceptance coverage.
6. Update user, developer, runtime, framework README, and sitemap documentation.
7. Run focused tests, local Mosaic-path smoke test, then baseline repository gates.
8. Run independent review, remediate, commit, push/PR/CI/merge/issue closure per delivery gates.
## TDD decision
Applied. The continuation state machine and lifecycle scheduling are control-path logic where a race
or false terminal state can cause unbounded work or premature completion.
## Progress checkpoints
- [x] Issue #1150 created through Mosaic wrapper.
- [x] Isolated worktree created from `origin/main`.
- [x] PRD requirements and acceptance criteria added.
- [x] Task scratchpad created.
- [x] RED controller and security-regression tests written and observed failing before implementation.
- [x] Goal controller, launcher discovery, framework deployment coverage, and bounded state machine
implemented.
- [x] User, admin, developer, runtime, adapter, README, and sitemap documentation updated.
- [x] Final source copied additively to `~/.config/mosaic/runtime/pi/goal-extension.ts`; source and
deployed SHA-256 are identical.
- [x] Live Pi RPC smoke from the exact Mosaic path reached `achieved` with two verification passes and
no extension errors.
- [x] Baseline and situational checks completed, except the explicitly documented unavailable
PostgreSQL-only root integration case.
- [x] Independent code and OWASP/security reviews completed; all findings remediated and re-reviewed.
- [ ] Commit, push, PR, terminal-green CI, squash merge, and issue closure complete.
## Tests and evidence
### Situational
- `pnpm --filter @mosaicstack/mosaic exec vitest run src/runtime/pi-goal-extension.spec.ts`
- final: 25 passed.
- Covers commands, per-turn checks, context injection, two-pass verification, mixed-report
rejection, bounded limits, compaction, branch restore, stale timers, credential redaction,
typed-field false-positive protection, and append-only legacy-state fail-closed behavior.
- Final focused launcher/controller/file-adapter run: 3 files / 67 tests passed.
- Final V8 coverage for `framework/runtime/pi/goal-extension.ts`:
- 99.17% statements/lines, 93.78% branches, 100% functions.
- Installer migration fixture: 24 passed and byte-compared the deployed framework asset.
- Standalone extension TypeScript check against installed Pi 0.84.1 types passed:
`pnpm --filter @mosaicstack/mosaic exec tsc --noEmit --pretty false --module NodeNext
--moduleResolution NodeNext --target ES2022 --skipLibCheck framework/runtime/pi/goal-extension.ts`.
- Live deployment/load evidence:
- source/deployed SHA-256:
`1f0a3806e0948ad5f49684273a7e535e9880c148f7fd16d13ee487fcd601f637`.
- `get_commands` identified `/goal` as an extension command sourced from
`~/.config/mosaic/runtime/pi/goal-extension.ts`; `/goal help` succeeded; zero extension errors.
- live arithmetic goal ended `achieved`, verification `2/2`, with 3 goal reports / 3 agent starts
and zero extension errors.
- no goal extension exists under `~/.pi` extension paths.
### Baseline
- `pnpm build`: passed before the final framework-only redaction remediation; the extension is not a
package build input and its final source passed the standalone Pi type check.
- `pnpm typecheck`: 45/45 tasks passed.
- `pnpm lint`: 25/25 tasks passed.
- `pnpm format:check`: passed.
- Final Mosaic package components:
- Vitest: 82 files / 1,539 tests passed.
- full `test:framework-shell` harness passed.
- the discovered pre-existing tmux loader-marker race was reproduced with constructor PID
evidence, fixed with a pane readiness/FIFO barrier, passed 3 consecutive focused runs, and passed
in the full shell harness.
- one combined rerun encountered the separate existing real-lease probe TOCTOU in
`install-ordering-guard.spec.ts`; an earlier final Vitest run was fully green and the changed
focused suites remained green.
- Gateway safe baseline excluding the prohibited PostgreSQL-only fixture: 55 files / 600 tests passed
(6 files / 12 tests skipped by their existing environment gates).
- Root `pnpm test` reached 43 successful workspace tasks and all changed-package Vitest tests, but
the unchanged `apps/gateway/src/__tests__/cross-user-isolation.test.ts` afterAll hook retried a
PostgreSQL connection and failed authentication (`28P01`). This checkout explicitly forbids local
PostgreSQL startup/access; the failure is unrelated to #1150 and cannot be remediated by starting
the database. The gateway suite excluding that PostgreSQL-only file and required CI are used as
the safe verification paths.
### Independent review
- Codex code review: approved, 0 findings across 15 files.
- Initial Codex security review: one medium CWE-532/A09 finding for raw report persistence.
- Remediation added central credential-pattern redaction, prompt/docs guidance, canary tests, typed
field false-positive guards, and sticky fail-closed restore for credential-bearing append-only
history.
- Codex security re-review: risk `none`, 0 findings, confidence 0.87.
- Focused remediation review findings were fixed; final focused re-review verdict: `APPROVE`.
- Focused independent review of the tmux readiness barrier: `APPROVE`, no actionable findings.
## Risks and blockers
- Live `~/.config/mosaic` is shared by active Pi/fleet processes. Local deployment remained a single
additive framework file and did not reload or restart unrelated sessions.
- Completion verification is semantic, not mathematical: the active agent supplies structured
evidence twice. Operators must still inspect consequential outcomes.
- Credential redaction is pattern-based defense-in-depth, not a secret store. It covers
controller-owned state/status/tool details, not Pi's separate model-message/tool-call history.
Goals and reports must never contain real secrets or raw sensitive output. Because Pi session
entries are append-only, a detected credential-bearing legacy branch fails closed and the affected
session must be removed.
- Current installed Pi is newer than the repository's historical gateway Pi dependency. The
extension was checked and smoke-tested against installed Pi 0.84.1 using stable documented APIs.
- Local root testing cannot safely execute the unchanged PostgreSQL-only integration fixture under
the checkout's explicit database safety constraints. Terminal-green PR CI remains mandatory before
merge.
- The unchanged real-lease default-probe test can observe different broker availability across its two
sequential probes; one combined package rerun hit that existing TOCTOU. The same final Vitest suite
passed in a separate run, and CI remains the merge authority.
+1 -3
View File
@@ -13,8 +13,7 @@ Pi is the native Mosaic agent runtime. The `mosaic pi` launcher:
1. Injects the full runtime contract via `--append-system-prompt`
2. Loads Mosaic skills via `--skill` flags
3. Loads framework-owned `mosaic-extension.ts` and `goal-extension.ts` from
`~/.config/mosaic/runtime/pi/` via ordered `--extension` flags
3. Loads the Mosaic extension via `--extension` for lifecycle hooks
4. Detects active missions and injects initial prompts
## Capabilities vs Other Runtimes
@@ -23,7 +22,6 @@ Pi is the native Mosaic agent runtime. The `mosaic pi` launcher:
- Native thinking levels replace sequential-thinking MCP
- Native skill discovery compatible with Mosaic SKILL.md format
- Native extension system for lifecycle hooks (TypeScript, not bash shims)
- Bounded persistent `/goal` loop with per-turn, post-compaction, and two-pass evidence checks
- Native session persistence and resume
- Model-agnostic (Anthropic, OpenAI, Google, Ollama, custom providers)
+3 -10
View File
@@ -94,14 +94,7 @@ The launcher:
1. Verifies `~/.config/mosaic` exists
2. Verifies `SOUL.md` exists (auto-runs `mosaic init` if missing)
3. Injects `AGENTS.md` into the runtime
4. For Pi, loads the framework-owned core and persistent-goal extensions from
`~/.config/mosaic/runtime/pi/`
5. Forwards all arguments to the runtime CLI
Inside `mosaic pi`, `/goal set <statement>` starts a bounded persistent goal loop. Use `/goal status`,
`/goal pause`, `/goal resume`, or `/goal cancel` to control it. The extension remains part of Mosaic
under `~/.config/mosaic/runtime/pi/goal-extension.ts`; it is not installed in Pi's main extension
directory.
4. Forwards all arguments to the runtime CLI
You can still launch runtimes directly (`claude`, `codex`, etc.) — thin runtime adapters will tell the agent to read `~/.config/mosaic/AGENTS.md`.
@@ -121,7 +114,7 @@ You can still launch runtimes directly (`claude`, `codex`, etc.) — thin runtim
│ ├── claude/ ← CLAUDE.md, RUNTIME.md, settings.json, hooks
│ ├── codex/ ← instructions.md, RUNTIME.md
│ ├── opencode/ ← AGENTS.md, RUNTIME.md
│ ├── pi/ ← RUNTIME.md, mosaic-extension.ts, goal-extension.ts
│ ├── pi/ ← RUNTIME.md, mosaic-extension.ts
│ └── mcp/ ← MCP server configs
├── skills/ ← Universal skills (synced from mosaic/agent-skills)
├── skills-local/ ← Local cross-runtime skills
@@ -133,7 +126,7 @@ You can still launch runtimes directly (`claude`, `codex`, etc.) — thin runtim
| Launch method | Injection mechanism |
| ------------------- | ----------------------------------------------------------------------------------------- |
| `mosaic pi` | `--append-system-prompt` with composed runtime contract + skills + Mosaic extensions |
| `mosaic pi` | `--append-system-prompt` with composed runtime contract + skills + extension |
| `mosaic claude` | `--append-system-prompt` with composed runtime contract (`AGENTS.md` + runtime reference) |
| `mosaic codex` | Writes composed runtime contract to `~/.codex/instructions.md` before launch |
| `mosaic opencode` | Writes composed runtime contract to `~/.config/opencode/AGENTS.md` before launch |
@@ -51,26 +51,12 @@ Skills are discovered from:
### Extensions
`mosaic pi` loads framework-owned extensions directly from `~/.config/mosaic/runtime/pi/` in this
order:
The Mosaic Pi extension (`~/.config/mosaic/runtime/pi/mosaic-extension.ts`) handles:
1. `mosaic-extension.ts` — session lifecycle, mission context, memory routing, lease/mutator gates,
and fleet heartbeat reporting.
2. `goal-extension.ts` — optional persistent `/goal` controller with per-turn and post-compaction
checks.
The goal extension is deployed by Mosaic and MUST NOT be copied into `~/.pi/agent/extensions/`.
Use `/goal set <statement>` (or `/goal <statement>`) to start, then `/goal status`, `/goal pause`,
`/goal resume`, or `/goal cancel` to control it. An active goal is injected before every model
request, restored from branch-specific session entries, and considered achieved only after two
consecutive evidence-bearing reports. Common credential shapes are redacted before controller-owned
goal-state entries are persisted or
displayed; Pi's own model/tool-call history is separate. Goals and reports must contain references
and pass/fail summaries rather than secrets or raw sensitive output.
- `MOSAIC_GOAL_MAX_TURNS` — autonomous turn limit, default `40`, accepted range `1..500`.
- `MOSAIC_GOAL_MAX_NO_PROGRESS` — identical no-progress report limit, default `6`, accepted range
`1..100`.
- Session start/end lifecycle hooks
- Active mission detection and context injection
- Memory routing to `~/.config/mosaic/memory/`
- MACP queue status reporting
### Sessions
File diff suppressed because it is too large Load Diff
@@ -66,10 +66,7 @@ if command -v tmux >/dev/null 2>&1 && command -v cc >/dev/null 2>&1; then
trap 'tmux -L "$TEST_SOCKET" kill-server >/dev/null 2>&1 || true; rm -rf "$TEST_ROOT"' EXIT
MARKER="$TEST_ROOT/loader-marker"
LIBRARY="$TEST_ROOT/marker.so"
FIXTURE_READY="$TEST_ROOT/loader-ready"
FIXTURE_FIFO="$TEST_ROOT/loader-block"
HOLDER_HOME="$TEST_ROOT/holder-home"
mkfifo "$FIXTURE_FIFO"
mkdir -p "$HOLDER_HOME/.config/mosaic/fleet/run"
chmod 700 "$HOLDER_HOME/.config" "$HOLDER_HOME/.config/mosaic" \
"$HOLDER_HOME/.config/mosaic/fleet" "$HOLDER_HOME/.config/mosaic/fleet/run"
@@ -90,17 +87,7 @@ __attribute__((constructor)) static void mark_loader(void) {
EOF
cc -shared -fPIC -o "$LIBRARY" "$TEST_ROOT/marker.c"
MOSAIC_LOADER_MARKER="$MARKER" LD_PRELOAD="$LIBRARY" \
tmux -L "$TEST_SOCKET" new-session -d -s _holder \
"touch '$FIXTURE_READY'; read _ < '$FIXTURE_FIFO'"
# tmux starts the pane asynchronously. Wait until its contaminated shell has
# loaded the constructor and reached a builtin-only FIFO barrier before
# clearing the marker; otherwise that expected constructor can race with the
# clean holder assertion below and create a false failure.
for _attempt in {1..100}; do
[ -e "$FIXTURE_READY" ] && break
sleep 0.01
done
[ -e "$FIXTURE_READY" ] || fail "contaminated fixture pane did not become ready"
tmux -L "$TEST_SOCKET" new-session -d -s _holder 'sleep 60'
[ -s "$MARKER" ] || fail "contaminated fixture did not execute loader constructor"
server_pid=$(tmux -L "$TEST_SOCKET" display-message -p '#{pid}')
: > "$MARKER"
@@ -26,9 +26,6 @@ chk "F1 fresh: CONSTITUTION/AGENTS/STANDARDS/TOOLS seeded" \
"[ -f '$T1/CONSTITUTION.md' ] && [ -f '$T1/AGENTS.md' ] && [ -f '$T1/STANDARDS.md' ] && [ -f '$T1/TOOLS.md' ]"
chk "F1 fresh: AGENTS == shipped default" "cmp -s '$T1/AGENTS.md' '$DEFA/AGENTS.md'"
chk "F1 fresh: framework-version stamped 3" "[ \"\$(cat '$T1/.framework-version' 2>/dev/null)\" = 3 ]"
chk "F1 fresh: Pi goal extension deploys under Mosaic runtime" \
"cmp -s '$T1/runtime/pi/goal-extension.ts' '$FW/runtime/pi/goal-extension.ts'"
chk "F1 fresh: installer creates no nested main Pi config" "[ ! -e '$T1/.pi' ]"
# F2 — legacy install with a user-edited AGENTS.md (the sanctioned pre-constitution customization)
T2=$(mktemp -d); mkdir -p "$T2/credentials"
@@ -92,8 +89,6 @@ chk "F6 reseed: per-agent env bytes survive" "cmp -s '$T6/fleet/agents/coder0.en
chk "F6 reseed: heartbeat bytes survive" "cmp -s '$T6/fleet/run/coder0.hb' '$E6/run.expected'"
chk "F6 reseed: framework examples are refreshed" "grep -q orchestrator '$T6/fleet/examples/general.yaml'"
chk "F6 reseed: framework roster schema is refreshed" "cmp -s '$T6/fleet/roster.schema.json' '$FW/fleet/roster.schema.json'"
chk "F6 reseed: Pi goal extension is refreshed from framework source" \
"cmp -s '$T6/runtime/pi/goal-extension.ts' '$FW/runtime/pi/goal-extension.ts'"
rm -rf "$T1" "$T2" "$T3" "$T4" "$T5" "$T6" "$E6"
echo
@@ -5,7 +5,6 @@ import { tmpdir } from 'node:os';
import { join } from 'node:path';
import {
buildPiSkillArgs,
discoverPiExtensionArgs,
enumerateSkillDirs,
piForceSkillNames,
registerRuntimeLaunchers,
@@ -179,52 +178,6 @@ describe('buildPiSkillArgs', () => {
});
});
describe('discoverPiExtensionArgs', () => {
it('loads the core and goal extensions in deterministic order from Mosaic home', () => {
const root = mkdtempSync(join(tmpdir(), 'mosaic-pi-extensions-'));
const runtimeDir = join(root, 'runtime', 'pi');
mkdirSync(runtimeDir, { recursive: true });
writeFileSync(join(runtimeDir, 'goal-extension.ts'), '// goal\n');
writeFileSync(join(runtimeDir, 'mosaic-extension.ts'), '// core\n');
try {
expect(discoverPiExtensionArgs(root)).toEqual([
'--extension',
join(runtimeDir, 'mosaic-extension.ts'),
'--extension',
join(runtimeDir, 'goal-extension.ts'),
]);
} finally {
rmSync(root, { recursive: true, force: true });
}
});
it('remains backward-compatible when the optional goal extension is absent', () => {
const root = mkdtempSync(join(tmpdir(), 'mosaic-pi-extensions-'));
const runtimeDir = join(root, 'runtime', 'pi');
mkdirSync(runtimeDir, { recursive: true });
writeFileSync(join(runtimeDir, 'mosaic-extension.ts'), '// core\n');
try {
expect(discoverPiExtensionArgs(root)).toEqual([
'--extension',
join(runtimeDir, 'mosaic-extension.ts'),
]);
} finally {
rmSync(root, { recursive: true, force: true });
}
});
it('emits no extension arguments when Mosaic runtime assets are absent', () => {
const root = mkdtempSync(join(tmpdir(), 'mosaic-pi-extensions-'));
try {
expect(discoverPiExtensionArgs(root)).toEqual([]);
} finally {
rmSync(root, { recursive: true, force: true });
}
});
});
describe('enumerateSkillDirs (real FS)', () => {
let root: string;
+4 -10
View File
@@ -715,15 +715,9 @@ export function buildPiSkillArgs(
return ['--no-skills', ...forcedSkillArgs];
}
const PI_EXTENSION_FILES = ['mosaic-extension.ts', 'goal-extension.ts'] as const;
export function discoverPiExtensionArgs(mosaicHome: string = MOSAIC_HOME): string[] {
const args: string[] = [];
for (const fileName of PI_EXTENSION_FILES) {
const extensionPath = join(mosaicHome, 'runtime', 'pi', fileName);
if (existsSync(extensionPath)) args.push('--extension', extensionPath);
}
return args;
function discoverPiExtension(): string[] {
const ext = join(MOSAIC_HOME, 'runtime', 'pi', 'mosaic-extension.ts');
return existsSync(ext) ? ['--extension', ext] : [];
}
// ─── Launch functions ────────────────────────────────────────────────────────
@@ -798,7 +792,7 @@ function launchRuntime(runtime: RuntimeName, args: string[], yolo: boolean): nev
const prompt = buildRuntimePrompt('pi');
const cliArgs = ['--append-system-prompt', prompt];
cliArgs.push(...buildPiSkillArgs(args));
cliArgs.push(...discoverPiExtensionArgs());
cliArgs.push(...discoverPiExtension());
if (hasMissionNoArgs) {
cliArgs.push(missionPrompt);
} else {
@@ -209,20 +209,6 @@ describe('FileConfigAdapter.syncFramework — defaults seeding', () => {
);
});
it('deploys the Mosaic-owned Pi goal extension only inside the Mosaic runtime tree', async () => {
const sourceRuntime = join(fixture.sourceDir, 'runtime', 'pi');
mkdirSync(sourceRuntime, { recursive: true });
writeFileSync(join(sourceRuntime, 'goal-extension.ts'), '// persistent goal extension\n');
const adapter = new FileConfigAdapter(fixture.mosaicHome, fixture.sourceDir);
await adapter.syncFramework('fresh');
expect(
readFileSync(join(fixture.mosaicHome, 'runtime', 'pi', 'goal-extension.ts'), 'utf-8'),
).toBe('// persistent goal extension\n');
expect(existsSync(join(fixture.mosaicHome, '.pi'))).toBe(false);
});
it('is a no-op for seeding when defaults/ dir does not exist', async () => {
rmSync(fixture.defaultsDir, { recursive: true });
@@ -1,796 +0,0 @@
import { afterEach, describe, expect, it, vi } from 'vitest';
interface FakeEntry {
type: string;
customType?: string;
data?: unknown;
}
interface SentMessage {
message: {
customType: string;
content: string;
display: boolean;
};
options?: {
triggerTurn?: boolean;
deliverAs?: 'steer' | 'followUp' | 'nextTurn';
};
}
interface FakeContext {
cwd: string;
ui: {
notifications: Array<{ message: string; level?: string }>;
statuses: Map<string, string | undefined>;
notify(message: string, level?: string): void;
setStatus(key: string, value: string | undefined): void;
};
sessionManager: {
getBranch(): FakeEntry[];
};
isIdle(): boolean;
hasPendingMessages(): boolean;
abort(): void;
}
type EventHandler = (
event: Record<string, unknown>,
context: FakeContext,
) => unknown | Promise<unknown>;
type CommandHandler = (args: string, context: FakeContext) => unknown | Promise<unknown>;
interface FakeToolResult {
content: Array<{ type: string; text: string }>;
details?: unknown;
terminate?: boolean;
}
interface FakeTool {
name: string;
execute(
toolCallId: string,
params: Record<string, unknown>,
signal: AbortSignal | undefined,
onUpdate: undefined,
context: FakeContext,
): Promise<FakeToolResult>;
}
interface GoalExtensionFactory {
(api: FakePiApi): void;
}
interface GoalExtensionModule {
default: GoalExtensionFactory;
}
interface FakePiApi {
on(event: string, handler: EventHandler): void;
registerCommand(name: string, options: { description: string; handler: CommandHandler }): void;
registerTool(tool: FakeTool): void;
appendEntry(customType: string, data?: unknown): void;
sendMessage(message: SentMessage['message'], options?: SentMessage['options']): void;
}
function isGoalExtensionModule(value: unknown): value is GoalExtensionModule {
if (typeof value !== 'object' || value === null) return false;
return typeof Reflect.get(value, 'default') === 'function';
}
const goalExtensionUrl = new URL('../../framework/runtime/pi/goal-extension.ts', import.meta.url)
.href;
const importedGoalExtension: unknown = await import(goalExtensionUrl);
if (!isGoalExtensionModule(importedGoalExtension)) {
throw new Error('Pi goal extension must export a default registration function');
}
const registerGoalExtension = importedGoalExtension.default;
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === 'object' && value !== null && !Array.isArray(value);
}
class FakePi {
readonly handlers = new Map<string, EventHandler[]>();
readonly commands = new Map<string, CommandHandler>();
readonly tools = new Map<string, FakeTool>();
readonly entries: FakeEntry[] = [];
readonly sentMessages: SentMessage[] = [];
readonly notifications: Array<{ message: string; level?: string }> = [];
readonly statuses = new Map<string, string | undefined>();
branch: FakeEntry[] = [];
idle = true;
pending = false;
abortCount = 0;
readonly context: FakeContext = {
cwd: '/tmp/project',
ui: {
notifications: this.notifications,
statuses: this.statuses,
notify: (message: string, level?: string): void => {
this.notifications.push({ message, level });
},
setStatus: (key: string, value: string | undefined): void => {
this.statuses.set(key, value);
},
},
sessionManager: {
getBranch: (): FakeEntry[] => [...this.branch],
},
isIdle: (): boolean => this.idle,
hasPendingMessages: (): boolean => this.pending,
abort: (): void => {
this.abortCount += 1;
},
};
readonly api: FakePiApi = {
on: (event: string, handler: EventHandler): void => {
this.handlers.set(event, [...(this.handlers.get(event) ?? []), handler]);
},
registerCommand: (
name: string,
options: { description: string; handler: CommandHandler },
): void => {
this.commands.set(name, options.handler);
},
registerTool: (tool: FakeTool): void => {
this.tools.set(tool.name, tool);
},
appendEntry: (customType: string, data?: unknown): void => {
const entry: FakeEntry = { type: 'custom', customType, data };
this.entries.push(entry);
this.branch.push(entry);
},
sendMessage: (message: SentMessage['message'], options?: SentMessage['options']): void => {
this.sentMessages.push({ message, options });
},
};
constructor(initialBranch: FakeEntry[] = []) {
this.branch = [...initialBranch];
registerGoalExtension(this.api);
}
async emit(event: string, value: Record<string, unknown> = {}): Promise<unknown[]> {
const results: unknown[] = [];
for (const handler of this.handlers.get(event) ?? []) {
results.push(await handler(value, this.context));
}
return results;
}
async goal(args: string): Promise<void> {
const handler = this.commands.get('goal');
if (handler === undefined) throw new Error('/goal was not registered');
await handler(args, this.context);
}
async report(params: Record<string, unknown>): Promise<FakeToolResult> {
const tool = this.tools.get('mosaic_goal_report');
if (tool === undefined) throw new Error('mosaic_goal_report was not registered');
return await tool.execute('goal-report-1', params, undefined, undefined, this.context);
}
}
function latestGoalStateData(pi: FakePi): Record<string, unknown> {
for (let index = pi.entries.length - 1; index >= 0; index -= 1) {
const entry = pi.entries[index];
if (entry?.customType === 'mosaic-goal-state' && isRecord(entry.data)) return entry.data;
}
throw new Error('No persisted Mosaic goal state found');
}
function stateField(pi: FakePi, field: string): unknown {
return latestGoalStateData(pi)[field];
}
function activeGoalStatementFromContext(result: unknown): string {
if (!isRecord(result)) throw new Error('Context handler did not return an object');
const messages = result['messages'];
if (!Array.isArray(messages)) throw new Error('Context result did not include messages');
const goalMessage = messages.find(
(message: unknown): boolean =>
isRecord(message) && message['customType'] === 'mosaic-goal-context',
);
if (!isRecord(goalMessage) || typeof goalMessage['content'] !== 'string') {
throw new Error('Goal context message was not injected');
}
return goalMessage['content'];
}
afterEach(() => {
vi.useRealTimers();
vi.unstubAllEnvs();
});
describe('Mosaic Pi goal extension commands', () => {
it('shows help and handles controls safely when no goal exists', async () => {
const pi = new FakePi();
await pi.goal('');
expect(pi.notifications.at(-1)?.message).toContain('/goal set');
await pi.goal('status');
expect(pi.notifications.at(-1)?.message).toContain('No Mosaic goal is set');
for (const command of ['pause', 'resume', 'cancel']) {
await pi.goal(command);
expect(pi.notifications.at(-1)?.level).toBe('warning');
}
expect(pi.entries).toHaveLength(0);
expect(pi.sentMessages).toHaveLength(0);
});
it('sets, reports, pauses, resumes, and cancels a bounded goal', async () => {
const pi = new FakePi();
await pi.goal('set Deliver the local goal extension with tests');
expect(stateField(pi, 'phase')).toBe('active');
expect(stateField(pi, 'statement')).toBe('Deliver the local goal extension with tests');
expect(pi.sentMessages).toHaveLength(1);
expect(pi.sentMessages[0]?.options?.triggerTurn).toBe(true);
await pi.goal('status');
expect(pi.notifications.at(-1)?.message).toContain('Deliver the local goal extension');
expect(pi.notifications.at(-1)?.message).toContain('active');
await pi.goal('pause');
expect(stateField(pi, 'phase')).toBe('paused');
pi.sentMessages.length = 0;
await pi.emit('agent_settled');
expect(pi.sentMessages).toHaveLength(0);
await pi.goal('resume');
expect(stateField(pi, 'phase')).toBe('active');
expect(pi.sentMessages).toHaveLength(1);
await pi.goal('cancel');
expect(stateField(pi, 'phase')).toBe('cancelled');
pi.sentMessages.length = 0;
await pi.emit('agent_settled');
expect(pi.sentMessages).toHaveLength(0);
});
it('accepts /goal <statement> shorthand but refuses to replace an active goal', async () => {
const pi = new FakePi();
await pi.goal('First goal');
const firstGoalId = stateField(pi, 'goalId');
await pi.goal('set Second goal');
expect(stateField(pi, 'goalId')).toBe(firstGoalId);
expect(stateField(pi, 'statement')).toBe('First goal');
expect(pi.notifications.at(-1)?.level).toBe('warning');
expect(pi.notifications.at(-1)?.message).toContain('/goal cancel');
});
it('rejects invalid phase transitions, aborts busy work, and supports clear as cancel', async () => {
const pi = new FakePi();
await pi.goal('set Preserve transition safety');
await pi.goal('resume');
expect(pi.notifications.at(-1)?.message).toContain('cannot be resumed');
pi.idle = false;
await pi.goal('pause maintenance window');
expect(stateField(pi, 'stopReason')).toBe('maintenance window');
expect(pi.abortCount).toBe(1);
await pi.goal('pause');
expect(pi.notifications.at(-1)?.message).toContain('cannot be paused');
await pi.goal('clear');
expect(stateField(pi, 'phase')).toBe('cancelled');
expect(pi.abortCount).toBe(2);
});
it('rejects empty and oversized goal statements without starting a run', async () => {
const pi = new FakePi();
await pi.goal('set');
await pi.goal(`set ${'x'.repeat(8_001)}`);
expect(pi.entries).toHaveLength(0);
expect(pi.sentMessages).toHaveLength(0);
expect(pi.notifications.at(-1)?.level).toBe('warning');
});
});
describe('Mosaic Pi goal lifecycle', () => {
it('injects one fresh active contract before every model context', async () => {
const pi = new FakePi();
await pi.goal('set Keep the agent oriented');
const existingGoalContext = {
role: 'custom',
customType: 'mosaic-goal-context',
content: 'stale',
};
const existingContinuation = {
role: 'custom',
customType: 'mosaic-goal-continuation',
content: 'stale continuation',
};
const first = await pi.emit('context', {
messages: [existingGoalContext, existingContinuation],
});
const second = await pi.emit('context', { messages: [] });
expect(activeGoalStatementFromContext(first[0])).toContain('Keep the agent oriented');
expect(activeGoalStatementFromContext(first[0])).toContain('mosaic_goal_report');
expect(activeGoalStatementFromContext(first[0])).not.toContain('stale');
if (!isRecord(first[0]) || !Array.isArray(first[0]['messages'])) {
throw new Error('Expected filtered context messages');
}
expect(first[0]['messages']).toHaveLength(1);
expect(activeGoalStatementFromContext(second[0])).toContain('Keep the agent oriented');
await pi.goal('cancel');
expect(
await pi.emit('context', {
messages: [existingGoalContext, existingContinuation],
}),
).toEqual([{ messages: [] }]);
});
it('lets an existing busy run adopt the goal and waits behind a pending message', async () => {
const busy = new FakePi();
busy.idle = false;
await busy.goal('set Join the current run safely');
expect(busy.sentMessages).toHaveLength(0);
busy.pending = true;
await busy.emit('agent_settled');
expect(busy.sentMessages).toHaveLength(0);
busy.pending = false;
busy.idle = true;
await busy.emit('agent_settled');
expect(busy.sentMessages).toHaveLength(1);
});
it('records every turn and continues once when an active run settles', async () => {
const pi = new FakePi();
await pi.goal('set Finish all acceptance criteria');
pi.sentMessages.length = 0;
await pi.emit('turn_end', { turnIndex: 0, message: {}, toolResults: [] });
expect(stateField(pi, 'turnCount')).toBe(1);
expect(stateField(pi, 'lastCheckSource')).toBe('turn');
await pi.emit('agent_settled');
await pi.emit('agent_settled');
expect(pi.sentMessages).toHaveLength(1);
expect(pi.sentMessages[0]?.message.content).toContain('Goal remains active');
await pi.emit('agent_start');
await pi.emit('agent_settled');
expect(pi.sentMessages).toHaveLength(2);
});
it('requires two consecutive evidence-bearing achievement reports', async () => {
const pi = new FakePi();
await pi.goal('set Prove the feature works');
pi.sentMessages.length = 0;
const first = await pi.report({
status: 'achieved',
summary: 'Focused tests pass',
evidence: ['pnpm test: 12 passed'],
});
expect(first.terminate).toBe(true);
expect(stateField(pi, 'phase')).toBe('verifying');
expect(stateField(pi, 'verificationPasses')).toBe(1);
await pi.emit('agent_settled');
expect(pi.sentMessages).toHaveLength(1);
expect(pi.sentMessages[0]?.message.content).toContain('verification pass');
await pi.emit('agent_start');
const second = await pi.report({
status: 'achieved',
summary: 'Independent recheck confirms completion',
evidence: ['rerun: 12 passed', 'framework path verified'],
});
expect(second.terminate).toBe(true);
expect(stateField(pi, 'phase')).toBe('achieved');
expect(stateField(pi, 'verificationPasses')).toBe(2);
pi.sentMessages.length = 0;
await pi.emit('agent_settled');
expect(pi.sentMessages).toHaveLength(0);
});
it('rejects malformed progress reports and reports submitted without an active goal', async () => {
const noGoal = new FakePi();
await expect(
noGoal.report({ status: 'continue', summary: 'work', evidence: [] }),
).rejects.toThrow(/No active Mosaic goal/);
const pi = new FakePi();
await pi.goal('set Validate report boundaries');
const invalidReports: Record<string, unknown>[] = [
{},
{ status: 'invalid', summary: 'work', evidence: [] },
{ status: 'continue', evidence: [] },
{ status: 'continue', summary: ' ', evidence: [] },
{ status: 'continue', summary: 'x'.repeat(2_001), evidence: [] },
{ status: 'continue', summary: 'work', evidence: 'not-an-array' },
{ status: 'continue', summary: 'work', evidence: Array.from({ length: 21 }, () => 'x') },
{ status: 'continue', summary: 'work', evidence: [4] },
{ status: 'continue', summary: 'work', evidence: [''] },
{ status: 'continue', summary: 'work', evidence: ['x'.repeat(1_001)] },
{ status: 'continue', summary: 'work', evidence: [], nextStep: 4 },
{ status: 'continue', summary: 'work', evidence: [], nextStep: ' ' },
{ status: 'continue', summary: 'work', evidence: [], nextStep: 'x'.repeat(2_001) },
];
for (const report of invalidReports) {
await expect(pi.report(report)).rejects.toThrow();
}
expect(stateField(pi, 'phase')).toBe('active');
});
it('rejects an achievement claim without evidence', async () => {
const pi = new FakePi();
await pi.goal('set Require evidence');
await expect(
pi.report({ status: 'achieved', summary: 'Trust me', evidence: [] }),
).rejects.toThrow(/evidence/i);
expect(stateField(pi, 'phase')).toBe('active');
});
it('redacts credential-shaped goal and report text before persistence or display', async () => {
const githubToken = `ghp_${'a'.repeat(32)}`;
const anthropicKey = `sk-ant-api03-${'b'.repeat(40)}`;
const bearerToken = 'header.payload.signature-canary';
const databaseUrl = 'postgresql://mosaic:[email protected]/mosaic';
const password = 'password-canary';
const pi = new FakePi();
await pi.goal(`set Rotate ${githubToken} without retaining it`);
const context = await pi.emit('context', { messages: [] });
expect(activeGoalStatementFromContext(context[0])).not.toContain(githubToken);
const result = await pi.report({
status: 'achieved',
summary: `Validated ${anthropicKey}`,
evidence: [`Authorization: Bearer ${bearerToken}`, `DATABASE_URL=${databaseUrl}`],
nextStep: `password=${password}`,
});
await pi.goal('status');
const persisted = JSON.stringify(latestGoalStateData(pi));
const displayed = pi.notifications.at(-1)?.message ?? '';
const toolOutput = JSON.stringify(result);
for (const secret of [githubToken, anthropicKey, bearerToken, databaseUrl, password]) {
expect(persisted).not.toContain(secret);
expect(displayed).not.toContain(secret);
expect(toolOutput).not.toContain(secret);
}
expect(persisted).toContain('[REDACTED-SECRET]');
});
it('preserves ordinary typed fields that resemble sensitive assignment names', async () => {
const typedFields = 'token: string, password: boolean, secret: false';
const pi = new FakePi();
await pi.goal(`set Preserve TypeScript fields: ${typedFields}`);
await pi.report({
status: 'continue',
summary: `Schema still contains ${typedFields}`,
evidence: [`interface Config { ${typedFields} }`],
nextStep: `Keep ${typedFields} unchanged`,
});
expect(stateField(pi, 'statement')).toContain(typedFields);
expect(JSON.stringify(stateField(pi, 'lastReport'))).toContain(typedFields);
expect(JSON.stringify(latestGoalStateData(pi))).not.toContain('[REDACTED-SECRET]');
});
it('stops autonomous continuation when the max-turn limit is reached', async () => {
vi.stubEnv('MOSAIC_GOAL_MAX_TURNS', '2');
const pi = new FakePi();
await pi.goal('set Bound this run');
pi.sentMessages.length = 0;
await pi.emit('turn_end', { turnIndex: 0, message: {}, toolResults: [] });
await pi.emit('turn_end', { turnIndex: 1, message: {}, toolResults: [] });
expect(stateField(pi, 'phase')).toBe('exhausted');
expect(pi.abortCount).toBe(1);
await pi.emit('agent_settled');
expect(pi.sentMessages).toHaveLength(0);
});
it('resets the no-progress sequence when a continuation report changes', async () => {
const pi = new FakePi();
await pi.goal('set Track changing progress');
await pi.report({
status: 'continue',
summary: 'First checkpoint',
evidence: ['file A changed'],
nextStep: 'Run focused tests',
});
await pi.report({
status: 'continue',
summary: 'Second checkpoint',
evidence: ['focused tests passed'],
nextStep: 'Review the diff',
});
expect(stateField(pi, 'phase')).toBe('active');
expect(stateField(pi, 'noProgressReports')).toBe(1);
await pi.goal('status');
expect(pi.notifications.at(-1)?.message).toContain('Next step: Review the diff');
expect(pi.notifications.at(-1)?.message).toContain('focused tests passed');
});
it('stops after a bounded number of identical no-progress reports', async () => {
vi.stubEnv('MOSAIC_GOAL_MAX_NO_PROGRESS', '2');
const pi = new FakePi();
await pi.goal('set Detect stalled work');
const report = {
status: 'continue',
summary: 'No change yet',
evidence: ['same observation'],
nextStep: 'Try again',
};
await pi.report(report);
await pi.report(report);
expect(stateField(pi, 'phase')).toBe('exhausted');
expect(stateField(pi, 'noProgressReports')).toBe(2);
pi.sentMessages.length = 0;
await pi.emit('agent_settled');
expect(pi.sentMessages).toHaveLength(0);
});
it('rejects a goal report mixed with another tool result in the same turn', async () => {
const pi = new FakePi();
await pi.goal('set Require a sole final report');
await pi.report({
status: 'achieved',
summary: 'Premature mixed claim',
evidence: ['one observation'],
});
await pi.emit('turn_end', {
turnIndex: 0,
message: {},
toolResults: [{ toolName: 'mosaic_goal_report' }, { toolName: 'read' }],
});
expect(stateField(pi, 'phase')).toBe('active');
expect(stateField(pi, 'verificationPasses')).toBe(0);
expect(stateField(pi, 'lastCheckOutcome')).toBe('mixed-goal-report-rejected');
expect(pi.notifications.at(-1)?.level).toBe('warning');
});
it('marks blocked reports terminal until the operator resumes', async () => {
const pi = new FakePi();
await pi.goal('set Stop on a real blocker');
await pi.report({
status: 'blocked',
summary: 'Missing required access',
evidence: ['provider returned 403'],
});
expect(stateField(pi, 'phase')).toBe('blocked');
pi.sentMessages.length = 0;
await pi.emit('agent_settled');
expect(pi.sentMessages).toHaveLength(0);
await pi.goal('resume');
expect(stateField(pi, 'phase')).toBe('active');
expect(stateField(pi, 'turnCount')).toBe(0);
expect(pi.sentMessages).toHaveLength(1);
});
});
describe('Mosaic Pi goal compaction and restoration', () => {
it('resets provisional verification and defers manual-compaction continuation until idle', async () => {
vi.useFakeTimers();
const pi = new FakePi();
await pi.goal('set Survive compaction');
await pi.report({
status: 'achieved',
summary: 'Initial claim',
evidence: ['focused test passed'],
});
expect(stateField(pi, 'phase')).toBe('verifying');
pi.sentMessages.length = 0;
await pi.emit('session_compact', { reason: 'manual', willRetry: false });
expect(stateField(pi, 'phase')).toBe('active');
expect(stateField(pi, 'verificationPasses')).toBe(0);
expect(stateField(pi, 'compactionCount')).toBe(1);
expect(stateField(pi, 'lastCheckSource')).toBe('compact');
expect(pi.sentMessages).toHaveLength(0);
await vi.runAllTimersAsync();
expect(pi.sentMessages).toHaveLength(1);
expect(pi.sentMessages[0]?.message.content).toContain('compaction');
});
it('does not re-enter an active automatic compaction and relies on the settled backstop', async () => {
vi.useFakeTimers();
const pi = new FakePi();
await pi.goal('set Avoid compaction races');
pi.sentMessages.length = 0;
pi.idle = false;
await pi.emit('session_compact', { reason: 'threshold', willRetry: false });
await vi.runAllTimersAsync();
expect(pi.sentMessages).toHaveLength(0);
pi.idle = true;
await pi.emit('agent_settled');
expect(pi.sentMessages).toHaveLength(1);
});
it('restores branch-specific state on session start and tree navigation', async () => {
vi.useFakeTimers();
const source = new FakePi();
await source.goal('set Restore this exact branch goal');
const activeState = latestGoalStateData(source);
const restored = new FakePi([
{ type: 'custom', customType: 'mosaic-goal-state', data: activeState },
]);
await restored.emit('session_start', { reason: 'resume' });
expect(restored.statuses.get('mosaic-goal')).toContain('active');
const context = await restored.emit('context', { messages: [] });
expect(activeGoalStatementFromContext(context[0])).toContain('Restore this exact branch goal');
await restored.goal('pause');
const pausedState = latestGoalStateData(restored);
restored.branch = [{ type: 'custom', customType: 'mosaic-goal-state', data: pausedState }];
await restored.emit('session_tree', {});
await vi.runAllTimersAsync();
expect(stateField(restored, 'phase')).toBe('paused');
expect(restored.sentMessages).toHaveLength(0);
});
it('restores only fully valid persisted states and ignores malformed entries', async () => {
vi.useFakeTimers();
const source = new FakePi();
await source.goal('set Validate persisted branch state');
await source.report({
status: 'continue',
summary: 'Valid report',
evidence: ['valid evidence'],
nextStep: 'Continue validation',
});
const valid = latestGoalStateData(source);
const validRestore = new FakePi([
{ type: 'custom', customType: 'mosaic-goal-state', data: valid },
]);
await validRestore.emit('session_start', { reason: 'resume' });
expect(stateField(validRestore, 'statement')).toBe('Validate persisted branch state');
await validRestore.emit('session_shutdown', { reason: 'reload' });
const validReport = latestGoalStateData(source)['lastReport'];
if (!isRecord(validReport)) throw new Error('Expected a valid persisted report fixture');
const integerFields = [
'turnCount',
'reportCount',
'verificationPasses',
'requiredVerificationPasses',
'noProgressReports',
'maxTurns',
'maxNoProgressReports',
'compactionCount',
];
const corruptions: Array<(state: Record<string, unknown>) => unknown> = [
(): unknown => null,
(state): unknown => ({ ...state, version: 99 }),
(state): unknown => ({ ...state, goalId: '' }),
(state): unknown => ({ ...state, statement: '' }),
(state): unknown => ({ ...state, statement: 'x'.repeat(8_001) }),
(state): unknown => ({ ...state, phase: 'unknown' }),
(state): unknown => ({ ...state, lastCheckSource: 'unknown' }),
(state): unknown => ({ ...state, startedAt: 4 }),
(state): unknown => ({ ...state, lastCheckAt: 4 }),
(state): unknown => ({ ...state, lastReport: null }),
(state): unknown => ({ ...state, lastProgressFingerprint: 4 }),
(state): unknown => ({ ...state, stopReason: 4 }),
...integerFields.map((field: string) => (state: Record<string, unknown>): unknown => ({
...state,
[field]: -1,
})),
];
corruptions.push(
(state: Record<string, unknown>): unknown => ({ ...state, maxTurns: 501 }),
(state: Record<string, unknown>): unknown => ({ ...state, maxNoProgressReports: 101 }),
(state: Record<string, unknown>): unknown => ({ ...state, requiredVerificationPasses: 3 }),
);
const reportCorruptions: Array<Record<string, unknown>> = [
{ ...validReport, status: 'bad' },
{ ...validReport, summary: '' },
{ ...validReport, evidence: 'bad' },
{ ...validReport, evidence: [4] },
{ ...validReport, fingerprint: '' },
{ ...validReport, reportedAt: '' },
{ ...validReport, nextStep: 4 },
];
for (const corruptReport of reportCorruptions) {
corruptions.push((state: Record<string, unknown>): unknown => ({
...state,
lastReport: corruptReport,
}));
}
for (const corrupt of corruptions) {
const candidate = corrupt(structuredClone(valid));
const restored = new FakePi([
{ type: 'custom', customType: 'mosaic-goal-state', data: candidate },
]);
await restored.emit('session_start', { reason: 'resume' });
expect(restored.statuses.get('mosaic-goal')).toBeUndefined();
expect(await restored.emit('context', { messages: [] })).toEqual([undefined]);
}
const failClosed = new FakePi([
{ type: 'custom', customType: 'mosaic-goal-state', data: valid },
{ type: 'custom', customType: 'mosaic-goal-state', data: { ...valid, version: 99 } },
]);
await failClosed.emit('session_start', { reason: 'resume' });
expect(failClosed.statuses.get('mosaic-goal')).toBeUndefined();
});
it('fails closed instead of reusing credential-bearing legacy branch state', async () => {
const source = new FakePi();
await source.goal('set Build a valid restore fixture');
const cleanState = latestGoalStateData(source);
const legacyState = structuredClone(cleanState);
legacyState['statement'] = `Legacy secret ghp_${'z'.repeat(32)}`;
const restored = new FakePi([
{ type: 'custom', customType: 'mosaic-goal-state', data: legacyState },
{ type: 'custom', customType: 'mosaic-goal-state', data: cleanState },
]);
await restored.emit('session_start', { reason: 'resume' });
expect(restored.statuses.get('mosaic-goal')).toBeUndefined();
expect(await restored.emit('context', { messages: [] })).toEqual([undefined]);
expect(restored.notifications.at(-1)?.level).toBe('warning');
expect(restored.notifications.at(-1)?.message).toContain('was not restored');
expect(restored.entries).toHaveLength(0);
});
it('schedules an active tree-restored goal and preserves terminal state through compaction', async () => {
vi.useFakeTimers();
const source = new FakePi();
await source.goal('set Restore active tree work');
const active = latestGoalStateData(source);
const restored = new FakePi();
restored.branch = [{ type: 'custom', customType: 'mosaic-goal-state', data: active }];
await restored.emit('session_tree', {});
await vi.runAllTimersAsync();
expect(restored.sentMessages).toHaveLength(1);
expect(restored.sentMessages[0]?.message.content).toContain('tree navigation');
await restored.goal('cancel');
await restored.emit('session_compact', { reason: 'manual', willRetry: false });
expect(stateField(restored, 'phase')).toBe('cancelled');
expect(stateField(restored, 'compactionCount')).toBe(1);
});
it('cancels deferred continuation when the session shuts down', async () => {
vi.useFakeTimers();
const pi = new FakePi();
await pi.goal('set Do not leak a stale timer');
pi.sentMessages.length = 0;
await pi.emit('session_compact', { reason: 'manual', willRetry: false });
await pi.emit('session_shutdown', { reason: 'reload' });
await vi.runAllTimersAsync();
expect(pi.sentMessages).toHaveLength(0);
});
});