feat(release): self-determination - ensure at launch, drift warnings, M20 packages/* decision (#38)

- release.sh ensure: aligned no-op; drift -> package-if-needed + health-gated
  activate (M3 gate-then-flip, automated)
- ensure_release_aligned in common.sh: invoked by hello/verify/agent;
  MOSAIC_ENSURE_SKIP guards recursion; run-task warns on drift without
  auto-aligning (workers/suites never trigger builds or model gates)
- ROADMAP: M20 decision recorded - v2 adopts packages/* monorepo at usurpation
- BUILD-LOG Phase 20 + tool-race process note

Live-verified: post-reset pointer loss auto-restored via health-gated
ensure; drift warning fires on desired-version bump; idempotent no-op on
aligned state.

Closes #38
This commit is contained in:
2026-09-03 14:42:09 -05:00
parent 0a17d29bce
commit db330c12c7
8 changed files with 75 additions and 5 deletions
+26
View File
@@ -401,3 +401,29 @@ left them unwritten at the time of work. Ground truth for each entry: the git
history (commit subjects/bodies), the Gitea milestone/issue records (#32, #34, history (commit subjects/bodies), the Gitea milestone/issue records (#32, #34,
#35, #36), and the suite files themselves. No facts were reconstructed from #35, #36), and the suite files themselves. No facts were reconstructed from
memory alone. memory alone.
---
## Phase 20: Release self-determination (M16)
### Entry 20.1 — before
- Timestamp: 2026-09-03
- Intended action: The system determines what is installed and aligns itself; the user never manually runs release.sh. `release.sh ensure` (align-or-noop) invoked automatically by human-facing launchers (hello, verify, agent); run-task reports drift without auto-aligning (workers/suites must not trigger builds or model gates mid-automation).
- Reason: Owner direction — intelligence in operation; post-reset pointer loss required a manual release command, which contradicts the self-healing design.
- Expected result: post-reset pointer loss auto-restores via health-gated ensure; drift warns on run-task; aligned state is a no-op; M20 packages/* decision recorded.
### Entry 20.2 — after
- Timestamp: 2026-09-03
- Commands run: ensure with pointer removed (live drift); ensure idempotence; drift-warning demo via RELEASE bump without packaging; full suites.
- Observed result: drift detected (active: none, desired: 0.0.11) -> health-gated activate -> aligned; second ensure no-op; run-task drift warning fired on desired-version bump without packaging; all suites green (24/68/17/14 + verify).
- Failure or correction:
1. First run-task edit anchor missed (inline comment mismatch) — reapplied with exact text.
2. RELEASE was temporarily bumped to 0.0.12 without packaging for the drift demo — restored to 0.0.11; state pointer remained aligned.
- M20 decision recorded in ROADMAP.md: v2 adopts packages/* monorepo structure at usurpation (owner, continuity-first).
- Process note (tool races): mechanism confirmed — batching a file write/edit and a dependent bash command in one parallel block runs the bash before the write flushes. Consequences: lost doc updates, stale anchors, one premature grep. Operating rule: dependent calls sequential; every write verified before claimed complete.
## Result (M16)
Release self-determination live: the system aligns itself to RELEASE without manual commands. Suites 24/68/17/14 + verify green.
+3 -4
View File
@@ -120,10 +120,9 @@ usurpation, v2 content becomes THE next-branch content on
mosaicstack/stack — a full refactor-and-replace with a functional system. mosaicstack/stack — a full refactor-and-replace with a functional system.
Not ready yet. Not ready yet.
Open decision at usurpation time: whether v2 reorganizes to the stack DECIDED (owner, 2026-09-03): v2 reorganizes to the `packages/*` monorepo
monorepo layout (`packages/*` continuity with existing tooling) or keeps structure for continuity with the existing stack tooling and expectations.
its flat structure and defines the new convention (clean break). Reference Reference: harvest worktree `~/src/mosaic-stack-worktrees/v2-harvest`
for either: harvest worktree `~/src/mosaic-stack-worktrees/v2-harvest`
(branch `next` @ 0db2d19a) and `packages/mosaic` (CLI), `packages/auth`. (branch `next` @ 0db2d19a) and `packages/mosaic` (CLI), `packages/auth`.
Usurpation criteria to be defined before any replacement (proposed: Usurpation criteria to be defined before any replacement (proposed:
functional parity of P0 operations on the v2 runtime + owner sign-off). functional parity of P0 operations on the v2 runtime + owner sign-off).
+1
View File
@@ -40,6 +40,7 @@ case "$NAME" in *[!A-Za-z0-9._-]*|'') echo "agent: invalid agent name" >&2; exit
load_config load_config
load_release load_release
bootstrap_runtime_dir bootstrap_runtime_dir
ensure_release_aligned
# Onboarding gate (M16): a TUI agent cannot launch without a user profile. # Onboarding gate (M16): a TUI agent cannot launch without a user profile.
# The onboarding wizard runs automatically here - the TTY is already yours. # The onboarding wizard runs automatically here - the TTY is already yours.
+16
View File
@@ -55,3 +55,19 @@ bootstrap_runtime_dir() {
# auto-invoked by agent.sh) creates it from the template. # auto-invoked by agent.sh) creates it from the template.
mkdir -p "$MOSAIC_DEV_DIR/user" mkdir -p "$MOSAIC_DEV_DIR/user"
} }
# M16 release self-determination: launchers call this after load_release.
# Cheap no-op when the active pointer matches the desired RELEASE; on drift
# it delegates to `release.sh ensure` (package if needed, health-gated
# activate). The health gate's own task run sets MOSAIC_ENSURE_SKIP so the
# ensure never re-enters itself.
ensure_release_aligned() {
[ -n "${MOSAIC_ENSURE_SKIP:-}" ] && return 0
local pointer="$MOSAIC_DEV_DIR/state/active.json"
local active=""
if [ -f "$pointer" ]; then
active="$(node -e 'try{const p=JSON.parse(require("fs").readFileSync(process.argv[1],"utf8"));process.stdout.write(p.release||"")}catch{}' "$pointer" 2>/dev/null)"
fi
[ "$active" = "$MOSAIC_RELEASE" ] && return 0
MOSAIC_ENSURE_SKIP=1 scripts/release.sh ensure
}
+1
View File
@@ -14,6 +14,7 @@ load_config
load_release load_release
bootstrap_runtime_dir bootstrap_runtime_dir
ensure_release_aligned
# -T: no pseudo-TTY, so stdout is clean model output. # -T: no pseudo-TTY, so stdout is clean model output.
# </dev/null: detach stdin. Pi's print mode reads piped stdin until EOF; # </dev/null: detach stdin. Pi's print mode reads piped stdin until EOF;
+20 -1
View File
@@ -140,10 +140,29 @@ cmd_rollback() {
activate "$prev_release" "$prev" "rollback" activate "$prev_release" "$prev" "rollback"
} }
cmd_ensure() {
# M16 release self-determination: the system determines what is installed
# and aligns itself. Aligned (pointer == RELEASE, image present) -> no-op.
# Drifted -> package if needed, then health-gated activate.
local pointer="$MOSAIC_DEV_DIR/state/active.json"
local active=""
if [ -f "$pointer" ]; then
active="$(node -e 'try{const p=JSON.parse(require("fs").readFileSync(process.argv[1],"utf8"));process.stdout.write(p.release||"")}catch{}' "$pointer")"
fi
if [ "$active" = "$MOSAIC_RELEASE" ] && image_exists "$MOSAIC_IMAGE_TAG"; then
echo "release: aligned at $MOSAIC_RELEASE ($MOSAIC_IMAGE_TAG)"
return 0
fi
echo "release: drift (active: ${active:-none}, desired: $MOSAIC_RELEASE) - aligning"
image_exists "$MOSAIC_IMAGE_TAG" || cmd_package
activate "$MOSAIC_RELEASE" "$MOSAIC_IMAGE_TAG" "activate"
}
case "${1:-}" in case "${1:-}" in
package) cmd_package ;; package) cmd_package ;;
activate) shift; cmd_activate "$@" ;; activate) shift; cmd_activate "$@" ;;
rollback) cmd_rollback ;; rollback) cmd_rollback ;;
status) cmd_status ;; status) cmd_status ;;
*) echo "usage: scripts/release.sh package | activate [--fault-injection] | rollback | status" >&2; exit 4 ;; ensure) shift; cmd_ensure "$@" ;;
*) echo "usage: scripts/release.sh package | activate [--fault-injection] | rollback | status | ensure" >&2; exit 4 ;;
esac esac
+7
View File
@@ -14,6 +14,13 @@ load_config
load_release # compose requires MOSAIC_IMAGE_TAG; task runs are release-scoped too load_release # compose requires MOSAIC_IMAGE_TAG; task runs are release-scoped too
bootstrap_runtime_dir bootstrap_runtime_dir
# Workers never auto-align releases (no builds or model gates mid-automation);
# drift is reported so the operator or a human-facing launcher can align.
if [ -f "$MOSAIC_DEV_DIR/state/active.json" ]; then
ACTIVE_REL="$(node -e 'try{const p=JSON.parse(require("fs").readFileSync(process.argv[1],"utf8"));process.stdout.write(p.release||"")}catch{}' "$MOSAIC_DEV_DIR/state/active.json")"
[ "$ACTIVE_REL" = "$MOSAIC_RELEASE" ] || echo "run-task: note: release drift (active: ${ACTIVE_REL:-none}, desired: $MOSAIC_RELEASE) - run scripts/release.sh ensure to align" >&2
fi
if [ ! -f "$MOSAIC_DEV_DIR/user/USER.md" ]; then if [ ! -f "$MOSAIC_DEV_DIR/user/USER.md" ]; then
echo "run-task: note: user profile not onboarded - continuing without user context (scripts/onboard.sh)" >&2 echo "run-task: note: user profile not onboarded - continuing without user context (scripts/onboard.sh)" >&2
fi fi
+1
View File
@@ -30,6 +30,7 @@ IMAGE="$MOSAIC_IMAGE_TAG"
# Ensure the configured data root exists (host-owned) before the mount, # Ensure the configured data root exists (host-owned) before the mount,
# otherwise Docker would auto-create a root-owned directory. # otherwise Docker would auto-create a root-owned directory.
bootstrap_runtime_dir bootstrap_runtime_dir
ensure_release_aligned
# 1. Build the image only if it is not already present. # 1. Build the image only if it is not already present.
if ! docker image inspect "$IMAGE" >/dev/null 2>&1; then if ! docker image inspect "$IMAGE" >/dev/null 2>&1; then