Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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: |
Expand Down
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 8 additions & 0 deletions DEV-LOGS.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,14 @@
- Minimal markdown markers, no unnecessary formatting, minimal emojis.
- Reference issue numbers in the format `#<issue-number>` 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.
Expand Down
30 changes: 28 additions & 2 deletions deva.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Comment on lines +1260 to +1261

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Ignore malformed receipt lines when joining goals

When any archived receipt is malformed—something write_launch_receipt can produce when a valid workspace path contains a quote or certain backslashes because cwd is interpolated without JSON escaping—this whole-archive jq -s invocation exits before filtering. container_goal then discards every valid receipt and falls back to the create-time environment or -- for all containers, so one malformed JSONL line globally disables newer attach-time goal restamps.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Disambiguate receipts across container recreations

After deva rm or deva clean, relaunching the same workspace and agent recreates the same deterministic container name, while historical receipts intentionally remain. If the new launch has no --goal, this name-only join selects the previous container generation's receipt instead of the new container's empty environment, causing ps and status to report a stale goal; constrain matches using the current container's creation time or another generation identifier.

Useful? React with 👍 / 👎.

) || 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)
Expand All @@ -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
}
)
Expand Down Expand Up @@ -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"] // "--"')
Expand Down
86 changes: 86 additions & 0 deletions docs/devlog/20260728-receipts-stack-and-roadmap.org
Original file line number Diff line number Diff line change
@@ -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/<tag> 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.
101 changes: 101 additions & 0 deletions scripts/test-launch-receipts.sh
Original file line number Diff line number Diff line change
@@ -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"
Loading