From 9f772bfe01ab205a670a758011c31065e582c4fc Mon Sep 17 00:00:00 2001 From: Eric Wang Date: Tue, 28 Jul 2026 22:44:50 -0700 Subject: [PATCH 1/2] feat(ps): goal column joined from launch receipts - container_goal(): last matching receipt wins (attach restamps write fresh receipts; create-time env cannot change), docker-inspect DEVA_GOAL env as fallback since receipt writes are warning-only - deva ps grows a GOAL column; deva status prints goal: when set - scripts/test-launch-receipts.sh: 13 hermetic round-trip cases (scratch XDG, stubbed docker) for writer + reader, wired into CI -- first test coverage for write_launch_receipt at all Refs #520 Co-Authored-By: Claude Fable 5 --- .github/workflows/ci.yml | 6 ++ CHANGELOG.md | 5 ++ DEV-LOGS.md | 8 +++ deva.sh | 30 +++++++++- scripts/test-launch-receipts.sh | 101 ++++++++++++++++++++++++++++++++ 5 files changed, 148 insertions(+), 2 deletions(-) create mode 100755 scripts/test-launch-receipts.sh diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6b18a54..f63cf19 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -189,6 +189,12 @@ jobs: set -euo pipefail ./scripts/test-version-targets.sh + - name: Smoke launch receipts and goal column + shell: bash + run: | + set -euo pipefail + ./scripts/test-launch-receipts.sh + - name: Smoke Chrome bridge entrypoint symlink shell: bash run: | diff --git a/CHANGELOG.md b/CHANGELOG.md index 19baffe..357ab57 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ### Added +- goal column in `deva ps` and `deva status` (#520): launches stamped + with `--goal` now show it. The last launch receipt wins (an attach + with a fresh `--goal` restamps via a new receipt); create-time + `DEVA_GOAL` env is the fallback when receipts are missing. Receipts + and the join are covered by `scripts/test-launch-receipts.sh` in CI. - launch receipts (#499): `--goal SLUG` stamps intent at launch. Exports `DEVA_GOAL` into the container (create-time env; attach restamps when a fresh `--goal` is given) and appends one JSONL diff --git a/DEV-LOGS.md b/DEV-LOGS.md index b20e491..b752dd1 100644 --- a/DEV-LOGS.md +++ b/DEV-LOGS.md @@ -13,6 +13,14 @@ - Minimal markdown markers, no unnecessary formatting, minimal emojis. - Reference issue numbers in the format `#` for easy linking. +# [2026-07-28] Dev Log: goal column in ps/status #520 +- Why: #499 receipts were write-only — nothing read them back, so "what agents run where, on what goal" stayed docker ps + memory. First rung of the ps -> attach -> serve ladder (#522). +- What: + - `container_goal()`: last matching receipt from `$XDG_DATA_HOME/ccx/launches/*.jsonl` wins (attach restamps write fresh receipts; create-time env cannot change), `docker inspect` `DEVA_GOAL` env as fallback (receipt writes are warning-only). Guarded assignments (`|| goal=""`) because a failed pipeline substitution aborts at the assignment under `set -e`. + - `deva ps` grows a GOAL column; `deva status` prints a `goal:` line when set. + - `scripts/test-launch-receipts.sh`: 13 hermetic round-trip cases (scratch XDG, stubbed docker) covering write_launch_receipt + container_goal; wired into ci.yml. First test coverage for the receipt writer at all. +- Result: receipts now have a reader. Scoping note: #520 was filed as a new `deva ps` command — ps already existed (list_containers_pretty); the real gap was only the goal join. + # [2026-07-28] Dev Log: home dir chown race bricks containers #506 - Why: intermittent `env: 'claude': Permission denied` on fresh containers. /home/deva stuck at build UID 1001 mode 750 (noble HOME_MODE) after remap to host UID — user can't traverse its own home. usermod's implicit home-tree chown walks live host mounts (~/.claude churning under concurrent sessions), aborts mid-walk with rc=12 AFTER updating passwd; shadow chowns the top dir last, so it never gets fixed. The 7511464 whitelist chowns subdirs, never $DEVA_HOME itself. Latent since 5807889 dropped the recursive home chown; only bites when the walk races live mounts, which is why sibling containers were fine. - What: explicit non-recursive `chown "$DEVA_UID:$DEVA_GID" "$DEVA_HOME"` in setup_nonroot_user, after the usermod block, using the adapted DEVA_UID so the usermod-failed-entirely variant stays consistent. Devlog with full forensics in docs/devlog/20260728-home-dir-chown-race.org. Verified by fault injection: stub usermod (passwd updated, chown skipped, exit 12) reproduces the brick unpatched, comes out clean patched. diff --git a/deva.sh b/deva.sh index 8098a2e..7137262 100755 --- a/deva.sh +++ b/deva.sh @@ -1246,6 +1246,28 @@ pick_container() { return 1 } +# Goal for a container: the last launch receipt wins (attach with a fresh +# --goal restamps via a new receipt; create-time env cannot change), the +# create-time DEVA_GOAL env is the fallback when receipts are missing +# (receipt writes are warning-only and the launches dir is prunable). +container_goal() { + local name="$1" goal="" + local dir="${XDG_DATA_HOME:-$HOME/.local/share}/ccx/launches" + if [ -d "$dir" ]; then + goal=$( + for f in "$dir"/*.jsonl; do + if [ -f "$f" ]; then cat "$f"; fi + done | jq -rs --arg c "$name" \ + '[.[] | select(.container == $c) | .goal] | last // empty' 2>/dev/null + ) || goal="" + fi + if [ -z "$goal" ]; then + goal=$(docker inspect --format '{{range .Config.Env}}{{println .}}{{end}}' "$name" 2>/dev/null | + sed -n 's/^DEVA_GOAL=//p' | head -n 1) || goal="" + fi + printf '%s' "${goal:---}" +} + list_containers_pretty() { local rows rows=$(project_container_rows) @@ -1257,9 +1279,9 @@ list_containers_pretty() { local output output=$( { - printf 'NAME\tAGENT\tSTATUS\tCREATED AT\n' + printf 'NAME\tAGENT\tGOAL\tSTATUS\tCREATED AT\n' printf '%s\n' "$rows" | while IFS=$'\t' read -r name status created; do - printf '%s\t%s\t%s\t%s\n' "$name" "$(extract_agent_from_name "$name")" "$status" "$created" + printf '%s\t%s\t%s\t%s\t%s\n' "$name" "$(extract_agent_from_name "$name")" "$(container_goal "$name")" "$status" "$created" done } ) @@ -1687,6 +1709,10 @@ cmd_status() { [ "$state" = "running" ] && printf ' up: %s' "$uptime_str" echo "" + local goal + goal=$(container_goal "$name") + [ "$goal" != "--" ] && printf ' goal: %s\n' "$goal" + if [ "$show_all" = true ]; then local ws_label ws_label=$(printf '%s' "$inspect_json" | jq -r '.[0].Config.Labels["deva.workspace"] // "--"') diff --git a/scripts/test-launch-receipts.sh b/scripts/test-launch-receipts.sh new file mode 100755 index 0000000..423405c --- /dev/null +++ b/scripts/test-launch-receipts.sh @@ -0,0 +1,101 @@ +#!/usr/bin/env bash +# Round-trip tests for --goal launch receipts (#499) and the ps/status +# goal column (#520): write_launch_receipt -> container_goal. +# Hermetic: scratch XDG_DATA_HOME, docker stubbed. +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" + +PASS=0 +FAIL=0 +ERRORS="" + +pass() { PASS=$((PASS + 1)); echo " ok: $1"; } +fail() { FAIL=$((FAIL + 1)); ERRORS="${ERRORS}\n FAIL: $1"; echo " FAIL: $1" >&2; } + +assert_eq() { + local label="$1" expected="$2" actual="$3" + if [ "$expected" = "$actual" ]; then + pass "$label" + else + fail "$label: expected='$expected' actual='$actual'" + fi +} + +source_helpers() { + eval "$(sed -n '/^write_launch_receipt()/,/^}/p' "$REPO_ROOT/deva.sh")" + eval "$(sed -n '/^container_goal()/,/^}/p' "$REPO_ROOT/deva.sh")" +} + +source_helpers + +SCRATCH="$(mktemp -d)" +trap 'rm -rf "$SCRATCH"' EXIT +export XDG_DATA_HOME="$SCRATCH/data" +LAUNCH_DIR="$XDG_DATA_HOME/ccx/launches" + +# docker stub: emits env lines only for the one container the fallback +# test expects; fails for everything else like a missing container would. +docker() { + if [ "${DOCKER_STUB_NAME:-}" = "${!#}" ]; then + printf 'PATH=/usr/bin\nDEVA_GOAL=%s\nHOME=/home/deva\n' "$DOCKER_STUB_GOAL" + return 0 + fi + return 1 +} + +echo "=== write_launch_receipt ===" + +GOAL="" +ACTIVE_AGENT="claude" +CONTAINER_NAME="deva--claude--auth-default--proj..abcd" +write_launch_receipt +if [ -d "$LAUNCH_DIR" ] && [ -n "$(ls -A "$LAUNCH_DIR" 2>/dev/null)" ]; then + fail "no goal writes no receipt" +else + pass "no goal writes no receipt" +fi + +GOAL="fix-auth-race" +write_launch_receipt +receipt_file="$LAUNCH_DIR/$(date -u +%Y-%m-%d).jsonl" +if [ -f "$receipt_file" ]; then + pass "receipt file created" +else + fail "receipt file created" +fi +assert_eq "receipt line count" "1" "$(wc -l <"$receipt_file" | tr -d ' ')" +assert_eq "receipt goal field" "fix-auth-race" "$(jq -r '.goal' "$receipt_file")" +assert_eq "receipt agent field" "claude" "$(jq -r '.agent' "$receipt_file")" +assert_eq "receipt container field" "$CONTAINER_NAME" "$(jq -r '.container' "$receipt_file")" +assert_eq "receipt source field" "deva" "$(jq -r '.source' "$receipt_file")" + +echo "=== container_goal ===" + +assert_eq "goal from receipt" "fix-auth-race" "$(container_goal "$CONTAINER_NAME")" + +GOAL="ship-ps-column" +write_launch_receipt +assert_eq "last receipt wins" "ship-ps-column" "$(container_goal "$CONTAINER_NAME")" + +GOAL="other-goal" +CONTAINER_NAME="deva--codex--auth-default--other..ef01" +write_launch_receipt +assert_eq "receipts keyed per container" "ship-ps-column" "$(container_goal "deva--claude--auth-default--proj..abcd")" + +DOCKER_STUB_NAME="deva--grok--auth-default--env..2345" +DOCKER_STUB_GOAL="from-create-env" +assert_eq "env fallback when no receipt" "from-create-env" "$(container_goal "$DOCKER_STUB_NAME")" + +assert_eq "no receipt no env is --" "--" "$(container_goal "deva--kimi--auth-default--gone..6789")" + +rm -rf "$LAUNCH_DIR" +assert_eq "missing launches dir falls through" "--" "$(container_goal "deva--claude--auth-default--proj..abcd")" + +echo "" +if [ "$FAIL" -gt 0 ]; then + printf 'FAILED: %d/%d%b\n' "$FAIL" "$((PASS + FAIL))" "$ERRORS" >&2 + exit 1 +fi +echo "All $PASS tests passed" From 7800a78b612dfa0a1905808d9a4efd11b3a2f9c5 Mon Sep 17 00:00:00 2001 From: Eric Wang Date: Tue, 28 Jul 2026 22:54:01 -0700 Subject: [PATCH 2/2] docs(devlog): receipts stack + session-manager roadmap session Decision chain for the #516/#523/#517 stack and issues #518-#524: scope corrections (ps already existed), stacked-PR reasoning, goal source-of-truth choice, and the #518 layout decision as the live wire. Co-Authored-By: Claude Fable 5 --- .../20260728-receipts-stack-and-roadmap.org | 86 +++++++++++++++++++ 1 file changed, 86 insertions(+) create mode 100644 docs/devlog/20260728-receipts-stack-and-roadmap.org diff --git a/docs/devlog/20260728-receipts-stack-and-roadmap.org b/docs/devlog/20260728-receipts-stack-and-roadmap.org new file mode 100644 index 0000000..976ef0c --- /dev/null +++ b/docs/devlog/20260728-receipts-stack-and-roadmap.org @@ -0,0 +1,86 @@ +* [2026-07-28] Shipping the receipts stack + session-manager roadmap :FEAT:ARCH:PROCESS: + +** Tension +The "launcher -> session manager" direction existed only as a chat +brainstorm. On disk: =feat/goal-receipts= (#499) committed but never +pushed, with two UNRELATED dirty files riding the worktree (pin bumps +in =versions.env=, a stale container-context block in =AGENTS.md=). +Receipts themselves were write-only — nothing read them back, so +=--goal= bought attribution for ccx archaeology but zero operator +visibility. And the roadmap (config/state split, options registry, +ps/doctor/serve) had no issues, so nothing was actionable. + +** Observation +- =deva ps= ALREADY EXISTS (=list_containers_pretty=, dispatch token + =ps | --ps=), and =deva status= already prints per-container + agent/auth/uptime/mounts plus a Health section that is half of the + imagined =deva doctor=. The brainstorm was designing features the + script already had. Filed issue #520 before checking; the PR + carries the scope correction. +- The #518 double-mount is live, not theoretical: =status= on a real + container shows =~/.config/deva/claude/.claude -> /home/deva/.claude= + AND =~/.config/deva -> /home/deva/.config/deva= simultaneously. +- =origin/main= had moved TWO releases (0.18.0, 0.18.1) under the + unpushed receipts branch. Both prepend-at-top files (CHANGELOG, + DEV-LOGS) conflict by construction on every rebase; everything else + merged clean. +- The =claude= security-audit action fails in ~2-4s on EVERY PR + (#507, #508 included — both merged anyway). Empty + =ANTHROPIC_API_KEY=, oauth token passed, dies right after settings + setup: smells like an expired secret (#524). A permanently red + check is training everyone to ignore it. +- Two test scripts (=test-ccx-args.sh=, =test-status-helpers.sh=) are + =100644= in git — the suite silently can't run via =./=. + +** Decision +- Three PRs, not one: #516 receipts as-committed, #517 chore + (pins + AGENTS block + exec bits), #523 goal column STACKED on #516 + (=--base feat/goal-receipts=) so its diff is only the join. + Rejected: folding the dirty files into #516 (unrelated concerns), + and waiting for #516 to merge before building on it (serializes an + autonomous session on a human review). +- Goal source of truth: last matching receipt wins, =docker inspect= + =DEVA_GOAL= env as fallback. Rejected: env-only (attach restamps + never reach create-time env, so it goes stale) and receipts-only + (writes are warning-only and the dir is prunable — the fallback is + the honest path for exactly those cases). +- =container_goal()= assignments are =|| goal=""= guarded — under + =set -euo pipefail= a failed pipeline substitution aborts AT the + assignment (same trap as the #484 silent exit); the receipt glob + loop uses =if/fi= not =[ ] &&= so the subshell never ends on a + failed test (the #414 flock lesson). +- CHANGELOG entries written in main's CURRENT Keep-a-Changelog format. + The claude-code-style restyle sits on local =chore/changelog-style= + with no PR; matching an unmerged style would conflict every branch. + +** Tradeoff +- Stacked PRs put a merge-order burden on the reviewer (#516 first; + GitHub retargets #523 to main when the branch deletes). +- =container_goal()= scans every =launches/*.jsonl= per container per + =ps= invocation. Fine at day-file scale; if launches accumulate for + years this wants a "recent files only" cut. Not built now — no + evidence it's needed. +- Issue #520's body describes a feature that didn't need building. + Left as-is with the correction in the PR; the lesson (check the + dispatch table before filing) is cheaper than rewriting history. + +** Next +The live wire is #518: the config/state split must be decided BEFORE +the bun port freezes the current layout, and it interlocks with the +accounts/ scoping from #497. Everything else on the roadmap +(#519 registry, #521 doctor, #522 serve) composes cleanly after it. + +** Artifacts +- PRs: #516 (receipts, rebased onto v0.18.1), #517 (pins chore, + 2 commits), #523 (goal column, stacked) +- Issues: #518 config/state split (+live evidence comment), + #519 options registry, #520 ps/goal, #521 doctor, #522 serve + umbrella, #524 broken PR bot +- =scripts/test-launch-receipts.sh=: 13 hermetic cases (scratch XDG, + stubbed docker), wired into ci.yml — first coverage for + =write_launch_receipt= at all +- Verified: shellcheck severity=error clean; local suite green + (mount-shape, image-precedence, kimi-auth, version-targets, + container-slug, status-helpers, launch-receipts); live =ps -g= / + =status -g= against the real daemon renders the GOAL column; all + five bumped pins resolve on npm.