Companion to VMsv2.md. Captures what to gather from a 1.16.3 dogfooding run, how to capture it, and what counts as a pass/fail. The data collected here is the input to P6 (snapshot fork gate) and the remaining P2 wire-in.
Before you start gathering, verify the build is the right one:
construct --version # expect: construct 1.16.3
ct sys doctor # all checks OKIf the version is wrong, you are testing a different build. Stop and re-pull.
If the doctor surfaces a Daemon Lock: held warning while you are NOT running ct, investigate before proceeding — a stale lock means a previous ct crashed mid-acquire.
This is the one data point that actually unblocks future work. Without it, the P6 gate stays closed.
| Metric | Source | Why |
|---|---|---|
| Median cold boot (s) | msb-boot: outcome=cold lines |
P6 gate input |
| Median recreate (s) | msb-boot: outcome=recreate lines |
P6 gate input |
| Median warm boot (s) | msb-boot: outcome=warm lines |
P6 gate input |
| Median reconnect (s) | msb-boot: outcome=reconnect lines |
P6 gate input |
| Recreate frequency | count of outcome=recreate per week |
P6 gate input |
# Per-outcome counts over the whole week
grep -h "msb-boot:" ~/.config/construct-cli/logs/*.log \
| sed 's/.*outcome=//; s/ .*//' | sort | uniq -c | sort -rn
# Per-outcome seconds (raw distribution; eyeball the median)
for outcome in cold recreate warm reconnect; do
echo "--- $outcome ---"
grep -h "msb-boot:" ~/.config/construct-cli/logs/*.log \
| grep "outcome=$outcome" \
| grep -oE 'seconds=[0-9]+' \
| sed 's/seconds=//' \
| sort -n
doneFor each outcome, the median is the middle value (or the average of the two middle values if you have an even count). Paste the medians into the table in docs/VMsv2.md section 10.
The telemetry emits at every return path of EnsureMsbDaemon. If you see zero msb-boot: outcome=cold lines after a week, the cold path never fired — that is a bug, not a quiet daemon.
The P6 gate (per VMsv2.md section 4): gate opens if median cold > ~120s AND recreates occur in normal workflow. If your medians show median cold <60s, P6 is not worth the spike cost; skip the snapshot fork entirely.
Validates the data layer (P2.1) + helper (P2.3) + CLI (P2.5/P2.6). The actual EnsureMsbDaemon wire-in (P2.2) is still pending; this test only confirms the registry + LRU + CLI work, NOT that the daemon mount set picks up the new root.
| Check | Expectation | Bug signal |
|---|---|---|
ct from project B after project A |
ONE outcome=recreate (the B visit) |
more = race; zero = P2.2 not wired (known) |
ct from project A again (after B) |
NO recreate (reconnect) | recreate = P2.2 not wired |
ct from project B again |
NO recreate (reconnect) | recreate = P2.2 not wired |
ct sys daemon roots list |
lists both A and B under "Daemon learned roots" | missing = registration broken |
# Pre: two repos at different roots, single-path daemon (default backend,
# do NOT set multi_paths_enabled)
mkdir -p ~/Dev/proj-A ~/Dev/proj-B
cd proj-A && ct pi "echo hi" # cold create + installs
cd proj-B && ct pi "echo hi" # expect ONE recreate (learned root)
cd proj-A && ct pi "echo hi" # expect NO recreate (reconnect)
cd proj-B && ct pi "echo hi" # expect NO recreate (reconnect)
ct sys daemon roots list # both /A and /B should appear
# Cap behavior: when the cap (default 8) is exceeded, oldest is evicted
# with one notice. To exercise this you need 9+ distinct projects; the
# CLI test above does not. Skip unless you have a real corpus.Validates the MaybePrepullImage spawn + the msb pull + msb image tag chain. The detached child writes to ~/.config/construct-cli/logs/prepull.log.
| Check | Expectation | Bug signal |
|---|---|---|
After ct sys prepull, prepull log exists |
line: prepull started |
missing = spawn failed silently |
| Prepull log outcome | prepull done (success) OR prepull FAILED at pull/tag: <err> |
failure = capture the error verbatim |
Next ct sys doctor image check |
"ready" / "Image is up to date" | "Preparing microVM image" = prepull didn't seed the cache |
msb image ls shows construct-box:latest after prepull |
present | absent = pull or tag step failed |
# Pre: remove the cached image so the prepull has work to do.
# If rm says "image not found" while doctor claims the image is loaded,
# list the stored ref first (msb image ls | grep construct-box) and rm
# the exact ref shown; msb matches rm by exact reference, not substring.
msb image rm construct-box:latest
# Watch the prepull log live
tail -f ~/.config/construct-cli/logs/prepull.log &
ct sys prepull # deterministic: runs the pull loop now
# Direct invocation runs the pull loop in the FOREGROUND and writes the
# same log (the "Spawned background prepull" line only appears when
# MaybePrepullImage spawns the detached child after a real update).
#
# Do NOT use `ct sys update` here: that is agent-update-inside-container
# (compose-only; fails closed on backend=microvm by design). And
# `ct sys self-update` only fires the prepull after an ACTUAL update;
# the "already on latest version" no-op path returns before the spawn.
# After: verify the prepull log has the expected lines
tail -10 ~/.config/construct-cli/logs/prepull.log
# Expect: "prepull started" -> [msb pull output] -> "prepull done"
# OR: "prepull FAILED at pull: ..." / "... at tag: ..." with the exact error.
# Now run any ct and confirm EnsureImage skips the pull path
ct sys doctor # image check should say "ready"
msb image ls | grep construct-box # must show construct-box:latest present
# Edge case: when prepull_image = true but backend is not "microvm"
# the user should see a one-time warning. Set backend to "auto" (the
# default), set prepull_image = true, and run any ct:
# Expect: "prepull_image is enabled but backend=...; the prepull only
# fires for backend=microvm. Set backend=microvm or disable prepull_image."Validates the host skills library reaches every supported agent's /home/construct/<agent>/skills. Read-only by default; opt-in RW via [sandbox] skills_read_only = false.
| Check | Expectation | Bug signal |
|---|---|---|
| Mount present in sandbox | ct pi "ls /home/construct/.claude/skills" shows your skills dir contents |
empty = mount missing or wrong target |
| Mount covers other agents | /home/construct/.antigravity/skills, .config/crush/skills, .kilocode/skills all populated |
only one agent = agent list drift |
| Read-only enforcement | ct pi "touch /home/construct/.claude/skills/write-test" returns "Read-only file system" or similar |
the file MUST NOT appear on the host afterwards (verify with ls ~/Dev/EstebanForge/AGENTS/skills/write-test) |
| Opt-in RW | with skills_read_only = false in config.toml + daemon restart, ct pi "echo NEW > /home/construct/.claude/skills/write-test" should succeed AND cat ~/Dev/EstebanForge/AGENTS/skills/write-test on the host should show "NEW" |
the file must be visible on the host after the agent writes it |
# Setup: ensure ~/Dev/EstebanForge/AGENTS/skills exists (it does in your env)
# List the candidate skills to confirm something is in there
ls ~/Dev/EstebanForge/AGENTS/skills/ | head -5
# Inside a ct session, check the mount shows up
ct pi "ls /home/construct/.claude/skills | head -5"
ct pi "ls /home/construct/.antigravity/skills | head -5"
ct pi "cat /home/construct/.claude/skills/acpx/SKILL.md | head -3"
# Verify RO: agent cannot write to the source
ct pi "touch /home/construct/.claude/skills/write-test 2>&1"
# Expect: "Read-only file system" (or similar) in the ct output
ls ~/Dev/EstebanForge/AGENTS/skills/write-test
# MUST say "No such file or directory" — the host source was not touched.
# Opt into RW and verify writes flow back
# Add to ~/.config/construct-cli/config.toml under [sandbox]:
# skills_read_only = false
ct sys daemon restart # pick up the new config
ct pi "echo NEW > /home/construct/.claude/skills/write-test"
cat ~/Dev/EstebanForge/AGENTS/skills/write-test # must show "NEW"
# Cleanup:
rm ~/Dev/EstebanForge/AGENTS/skills/write-test
# Restore skills_read_only = true, restart the daemonThese are not tied to a specific phase. If you hit any of them, capture the exact error and file as a follow-up.
Run two ct invocations in parallel from different terminals. Both should serialize cleanly via the new flock:
# Terminal 1
ct claude "long task"
# Terminal 2 (within the first 30s)
ct pi "echo hi"Expect:
- The second
ctwaits up to 10 minutes for the flock (no error; the user sees a "waiting for another construct invocation" notice after 250ms). - Both eventually run; the daemon is created once, not twice.
If you see "permission denied" on ~/.config/construct-cli/daemon.lock, file it — that means the lock file's mode 0600 was clobbered.
After a fresh construct build, ct sys doctor should show:
Sandbox Mounts:All sandbox mount knobs healthy(or appropriate warnings ifmount_home = trueorhost_binariesis set)Skills + Custom Override:No override foot-gunUNLESS you explicitly setallow_custom_compose_override = trueANDmount_skills = trueDaemon Lock:Daemon lock: free(during normalct; "held" only when a concurrentctis running)- The two new lines I added:
Live sessions: N(N = 0 between runs, 1 during act)
If any of these regress, the round-8 follow-up introduced the bug; capture the doctor's exact output.
The idle_watch.go::setDetachedAttrs uses Setsid (a POSIX primitive, not Linux-only per Go's syscall docs). Validate on macOS:
# Setup: set idle_stop_minutes to a small value for the test
# In config.toml under [daemon]: idle_stop_minutes = 1
# Run a ct, let it finish
ct pi "echo hi"
# Wait > 1 minute
sleep 75
# Check: is the daemon stopped?
ct sys daemon status
# Expect: "Status: Stopped" and "Live sessions: 0 (idle-watcher armed if ...)"The watcher should have stopped the daemon. If the daemon is still running after 75 seconds, the Setsid claim on macOS is wrong — file it as a critical macOS-specific bug.
If the daemon IS stopped, restart it for normal use:
ct sys daemon start
# Restore idle_stop_minutes to 45The watcher writes one line per decision to stderr:
grep "idle-watch:" ~/.config/construct-cli/logs/*.log
# Expect: "⏳ idle-watch armed: 1 minutes; checking every 30s" (or 45)
# "idle-watch: a session appeared, standing down" (if a ct ran during the wait)
# "💤 idle-watch: zero sessions past the deadline; stopping the daemon"If you see anything else (errors, panics), capture the exact line.
When you change a skills-related config key (toggle mount_skills, flip skills_read_only, add a new entry to ~/Dev/EstebanForge/AGENTS/skills/), the running daemon should recreate. Verify:
# Before
ct sys daemon status # record the daemon's "started" time (or uptime)
# Toggle something
sed -i 's/skills_read_only = true/skills_read_only = false/' \
~/.config/construct-cli/config.toml
ct pi "echo hi" # this should trigger a recreate
# After
ct sys daemon status # daemon's "started" time should be RECENT (post-toggle)
# The msb-boot: line should be outcome=recreate with reason
# "host skills mounts changed (source, mode, or targets)"
grep "msb-boot:" ~/.config/construct-cli/logs/*.log | tail -1If the daemon does NOT recreate, the skills hash label is not being checked (regression on the round-6 fix). File it.
- Section 10 medians + dogfood frequency in
docs/VMsv2.md. Format the medians as plain seconds (e.g.Median cold boot (create + installs): 47). - Any pass/fail data for P2.8 / P4.3 / P7.6 in the relevant phase section of
VMsv2.md(each phase has a "what" + "how" captured above; the result slots in as a sub-bullet under the phase). - Any macro captures (concurrency issues, idle-stop macOS, doctor regressions) as separate issue notes. The agent will turn them into commits or tracked follow-ups.
Send the data dumps to the agent with a clear "P0 medians for 1.16.3: cold=47, recreate=12, warm=23, reconnect=0, recreate_count=4" style summary; the agent will do the bookkeeping commits and push.