Skip to content

Commit 9f772bf

Browse files
lroolleclaude
andcommitted
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 <noreply@anthropic.com>
1 parent 01dec9e commit 9f772bf

5 files changed

Lines changed: 148 additions & 2 deletions

File tree

.github/workflows/ci.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,12 @@ jobs:
189189
set -euo pipefail
190190
./scripts/test-version-targets.sh
191191
192+
- name: Smoke launch receipts and goal column
193+
shell: bash
194+
run: |
195+
set -euo pipefail
196+
./scripts/test-launch-receipts.sh
197+
192198
- name: Smoke Chrome bridge entrypoint symlink
193199
shell: bash
194200
run: |

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
88
## [Unreleased]
99

1010
### Added
11+
- goal column in `deva ps` and `deva status` (#520): launches stamped
12+
with `--goal` now show it. The last launch receipt wins (an attach
13+
with a fresh `--goal` restamps via a new receipt); create-time
14+
`DEVA_GOAL` env is the fallback when receipts are missing. Receipts
15+
and the join are covered by `scripts/test-launch-receipts.sh` in CI.
1116
- launch receipts (#499): `--goal SLUG` stamps intent at launch.
1217
Exports `DEVA_GOAL` into the container (create-time env; attach
1318
restamps when a fresh `--goal` is given) and appends one JSONL

DEV-LOGS.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,14 @@
1313
- Minimal markdown markers, no unnecessary formatting, minimal emojis.
1414
- Reference issue numbers in the format `#<issue-number>` for easy linking.
1515

16+
# [2026-07-28] Dev Log: goal column in ps/status #520
17+
- 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).
18+
- What:
19+
- `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`.
20+
- `deva ps` grows a GOAL column; `deva status` prints a `goal:` line when set.
21+
- `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.
22+
- 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.
23+
1624
# [2026-07-28] Dev Log: home dir chown race bricks containers #506
1725
- 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.
1826
- 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.

deva.sh

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1246,6 +1246,28 @@ pick_container() {
12461246
return 1
12471247
}
12481248
1249+
# Goal for a container: the last launch receipt wins (attach with a fresh
1250+
# --goal restamps via a new receipt; create-time env cannot change), the
1251+
# create-time DEVA_GOAL env is the fallback when receipts are missing
1252+
# (receipt writes are warning-only and the launches dir is prunable).
1253+
container_goal() {
1254+
local name="$1" goal=""
1255+
local dir="${XDG_DATA_HOME:-$HOME/.local/share}/ccx/launches"
1256+
if [ -d "$dir" ]; then
1257+
goal=$(
1258+
for f in "$dir"/*.jsonl; do
1259+
if [ -f "$f" ]; then cat "$f"; fi
1260+
done | jq -rs --arg c "$name" \
1261+
'[.[] | select(.container == $c) | .goal] | last // empty' 2>/dev/null
1262+
) || goal=""
1263+
fi
1264+
if [ -z "$goal" ]; then
1265+
goal=$(docker inspect --format '{{range .Config.Env}}{{println .}}{{end}}' "$name" 2>/dev/null |
1266+
sed -n 's/^DEVA_GOAL=//p' | head -n 1) || goal=""
1267+
fi
1268+
printf '%s' "${goal:---}"
1269+
}
1270+
12491271
list_containers_pretty() {
12501272
local rows
12511273
rows=$(project_container_rows)
@@ -1257,9 +1279,9 @@ list_containers_pretty() {
12571279
local output
12581280
output=$(
12591281
{
1260-
printf 'NAME\tAGENT\tSTATUS\tCREATED AT\n'
1282+
printf 'NAME\tAGENT\tGOAL\tSTATUS\tCREATED AT\n'
12611283
printf '%s\n' "$rows" | while IFS=$'\t' read -r name status created; do
1262-
printf '%s\t%s\t%s\t%s\n' "$name" "$(extract_agent_from_name "$name")" "$status" "$created"
1284+
printf '%s\t%s\t%s\t%s\t%s\n' "$name" "$(extract_agent_from_name "$name")" "$(container_goal "$name")" "$status" "$created"
12631285
done
12641286
}
12651287
)
@@ -1687,6 +1709,10 @@ cmd_status() {
16871709
[ "$state" = "running" ] && printf ' up: %s' "$uptime_str"
16881710
echo ""
16891711
1712+
local goal
1713+
goal=$(container_goal "$name")
1714+
[ "$goal" != "--" ] && printf ' goal: %s\n' "$goal"
1715+
16901716
if [ "$show_all" = true ]; then
16911717
local ws_label
16921718
ws_label=$(printf '%s' "$inspect_json" | jq -r '.[0].Config.Labels["deva.workspace"] // "--"')

scripts/test-launch-receipts.sh

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
#!/usr/bin/env bash
2+
# Round-trip tests for --goal launch receipts (#499) and the ps/status
3+
# goal column (#520): write_launch_receipt -> container_goal.
4+
# Hermetic: scratch XDG_DATA_HOME, docker stubbed.
5+
set -euo pipefail
6+
7+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
8+
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
9+
10+
PASS=0
11+
FAIL=0
12+
ERRORS=""
13+
14+
pass() { PASS=$((PASS + 1)); echo " ok: $1"; }
15+
fail() { FAIL=$((FAIL + 1)); ERRORS="${ERRORS}\n FAIL: $1"; echo " FAIL: $1" >&2; }
16+
17+
assert_eq() {
18+
local label="$1" expected="$2" actual="$3"
19+
if [ "$expected" = "$actual" ]; then
20+
pass "$label"
21+
else
22+
fail "$label: expected='$expected' actual='$actual'"
23+
fi
24+
}
25+
26+
source_helpers() {
27+
eval "$(sed -n '/^write_launch_receipt()/,/^}/p' "$REPO_ROOT/deva.sh")"
28+
eval "$(sed -n '/^container_goal()/,/^}/p' "$REPO_ROOT/deva.sh")"
29+
}
30+
31+
source_helpers
32+
33+
SCRATCH="$(mktemp -d)"
34+
trap 'rm -rf "$SCRATCH"' EXIT
35+
export XDG_DATA_HOME="$SCRATCH/data"
36+
LAUNCH_DIR="$XDG_DATA_HOME/ccx/launches"
37+
38+
# docker stub: emits env lines only for the one container the fallback
39+
# test expects; fails for everything else like a missing container would.
40+
docker() {
41+
if [ "${DOCKER_STUB_NAME:-}" = "${!#}" ]; then
42+
printf 'PATH=/usr/bin\nDEVA_GOAL=%s\nHOME=/home/deva\n' "$DOCKER_STUB_GOAL"
43+
return 0
44+
fi
45+
return 1
46+
}
47+
48+
echo "=== write_launch_receipt ==="
49+
50+
GOAL=""
51+
ACTIVE_AGENT="claude"
52+
CONTAINER_NAME="deva--claude--auth-default--proj..abcd"
53+
write_launch_receipt
54+
if [ -d "$LAUNCH_DIR" ] && [ -n "$(ls -A "$LAUNCH_DIR" 2>/dev/null)" ]; then
55+
fail "no goal writes no receipt"
56+
else
57+
pass "no goal writes no receipt"
58+
fi
59+
60+
GOAL="fix-auth-race"
61+
write_launch_receipt
62+
receipt_file="$LAUNCH_DIR/$(date -u +%Y-%m-%d).jsonl"
63+
if [ -f "$receipt_file" ]; then
64+
pass "receipt file created"
65+
else
66+
fail "receipt file created"
67+
fi
68+
assert_eq "receipt line count" "1" "$(wc -l <"$receipt_file" | tr -d ' ')"
69+
assert_eq "receipt goal field" "fix-auth-race" "$(jq -r '.goal' "$receipt_file")"
70+
assert_eq "receipt agent field" "claude" "$(jq -r '.agent' "$receipt_file")"
71+
assert_eq "receipt container field" "$CONTAINER_NAME" "$(jq -r '.container' "$receipt_file")"
72+
assert_eq "receipt source field" "deva" "$(jq -r '.source' "$receipt_file")"
73+
74+
echo "=== container_goal ==="
75+
76+
assert_eq "goal from receipt" "fix-auth-race" "$(container_goal "$CONTAINER_NAME")"
77+
78+
GOAL="ship-ps-column"
79+
write_launch_receipt
80+
assert_eq "last receipt wins" "ship-ps-column" "$(container_goal "$CONTAINER_NAME")"
81+
82+
GOAL="other-goal"
83+
CONTAINER_NAME="deva--codex--auth-default--other..ef01"
84+
write_launch_receipt
85+
assert_eq "receipts keyed per container" "ship-ps-column" "$(container_goal "deva--claude--auth-default--proj..abcd")"
86+
87+
DOCKER_STUB_NAME="deva--grok--auth-default--env..2345"
88+
DOCKER_STUB_GOAL="from-create-env"
89+
assert_eq "env fallback when no receipt" "from-create-env" "$(container_goal "$DOCKER_STUB_NAME")"
90+
91+
assert_eq "no receipt no env is --" "--" "$(container_goal "deva--kimi--auth-default--gone..6789")"
92+
93+
rm -rf "$LAUNCH_DIR"
94+
assert_eq "missing launches dir falls through" "--" "$(container_goal "deva--claude--auth-default--proj..abcd")"
95+
96+
echo ""
97+
if [ "$FAIL" -gt 0 ]; then
98+
printf 'FAILED: %d/%d%b\n' "$FAIL" "$((PASS + FAIL))" "$ERRORS" >&2
99+
exit 1
100+
fi
101+
echo "All $PASS tests passed"

0 commit comments

Comments
 (0)