From 01dec9ed6bf2052e1eebcf7e39890e3fcfaf23bd Mon Sep 17 00:00:00 2001 From: Eric Wang Date: Mon, 27 Jul 2026 09:52:11 -0700 Subject: [PATCH] feat: --goal launch receipts for ccx session attribution - validate slug, export DEVA_GOAL (create env; attach restamps on fresh --goal only) - append JSONL receipt to $XDG_DATA_HOME/ccx/launches/ after the DRY_RUN gate; warning-only on failure, never blocks a launch - receipts are evidence: ccx data dir, not the ops ledger; ccx joins receipt->session by cwd+time (ccx side ships separately) Refs #499 Co-Authored-By: Claude Fable 5 --- CHANGELOG.md | 9 +++++++++ DEV-LOGS.md | 7 +++++++ deva.sh | 46 ++++++++++++++++++++++++++++++++++++++++++++-- 3 files changed, 60 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7ec3b45..19baffe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added +- 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 + receipt host-side to `$XDG_DATA_HOME/ccx/launches/YYYY-MM-DD.jsonl` + (`ts`, `goal`, `agent`, `cwd`, `container`, `source`). ccx joins + receipt->session by cwd+time so sessions stop being born orphans. + Receipt writes never block a launch; no `--goal` = zero change. + ## [0.18.1] - 2026-07-28 ### Fixed diff --git a/DEV-LOGS.md b/DEV-LOGS.md index d90710c..b20e491 100644 --- a/DEV-LOGS.md +++ b/DEV-LOGS.md @@ -23,6 +23,13 @@ - What: hoist generate_auth_tag() out of the container-name rewrite branch (it already produced `auth-default | auth-file- | api-key- | env` for naming and the `deva.auth_tag` label) and export it as `DEVA_AUTH_TAG` on every run. No naming/label change. Verified all three modes via scratch-HOME `--dry-run`. - Result: in-container tooling gets a stable account handle. Statusline side (chip + per-account cache dirs keyed on the tag) lands in claude-code-statusline. /status stays wrong by design until a per-account `.claude.json` overlay exists — follow-up candidate for `--auth-with`. +# [2026-07-27] Dev Log: --goal launch receipts #499 +- Why: every session is born an orphan — no record of which goal a launch serves, so attribution is post-hoc archaeology over JSONL. Stamp intent at launch instead; the ops-ledger loop (brief/debrief) needs this as its ground truth. +- What: + - `--goal SLUG` flag: slug validated `[a-z0-9][a-z0-9-]*` at parse; `DEVA_GOAL` exported at create; attach exec restamps only when a fresh `--goal` is passed (set-u-safe `${arr[@]+...}` expansion for the optional env pair). + - `write_launch_receipt`: one JSONL line to `$XDG_DATA_HOME/ccx/launches/YYYY-MM-DD.jsonl`, after the DRY_RUN gate, warning-only on failure. Receipts are evidence, so they live in the ccx data dir — the ops ledger cites session IDs, it does not store evidence. +- Result: verified live from a deva container against the host daemon: bad slug rejected, dry-run writes nothing, `-Q --goal smoke-goal claude -- --version` exits 0 and appends a correct receipt; `DEVA_GOAL=env-check` visible in assembled docker args. Not live-tested: the attach-exec restamp path (code-reviewed only). ccx-side join lands in thevibeworks/ccx. + # [2026-07-27] Dev Log: cloak release policy + closing the unverified VNC/daemon gap #456 - Why: two loose ends before shipping. (1) The 2026-07-21 entry left "x11vnc serving RFB and a live daemon inside the built image" unverified — that env couldn't apt-install. (2) release.yml made the GitHub Release `needs` the cloak build, so a ~200MB Chromium bake per arch (arm64 under QEMU) sat on the critical path of every release, including patch releases that touch nothing in that layer. - What: diff --git a/deva.sh b/deva.sh index 1910983..8098a2e 100755 --- a/deva.sh +++ b/deva.sh @@ -89,6 +89,7 @@ DEBUG_MODE=false DRY_RUN=false CODEX_BROWSER_MCP=false HOST_TMUX=false +GOAL="" if [ -t 0 ] && [ -t 1 ]; then DOCKER_TERMINAL_ARGS=(-it) @@ -157,6 +158,10 @@ Deva flags: -Q, --quick Bare mode: no host config mounts, no .deva loading, no autolink, implies --rm. Like emacs -Q. Mutually exclusive with -c. --host-net Use host networking for the agent container + --goal SLUG Stamp this launch with a goal slug ([a-z0-9-]): + exports DEVA_GOAL in-container and appends a + launch receipt to $XDG_DATA_HOME/ccx/launches/ + for ccx session attribution --host-tmux Opt in to container->host tmux: mounts ~/.ssh (ro) and passes DEVA_HOST_USER so the in-container deva-tmux CLI can reach the host tmux server. @@ -3196,6 +3201,19 @@ parse_wrapper_args() { i=$((i + 1)) continue ;; + --goal) + if [ $((i + 1)) -ge ${#incoming[@]} ]; then + echo "error: --goal requires a slug" >&2 + exit 1 + fi + GOAL="${incoming[$((i + 1))]}" + if ! [[ "$GOAL" =~ ^[a-z0-9][a-z0-9-]*$ ]]; then + echo "error: --goal slug must match [a-z0-9][a-z0-9-]* (got '$GOAL')" >&2 + exit 1 + fi + i=$((i + 2)) + continue + ;; --browser-mcp | --codex-browser-mcp | --with-browser) CODEX_BROWSER_MCP=true i=$((i + 1)) @@ -4064,6 +4082,7 @@ done DOCKER_ARGS+=(-e "DEVA_CONTAINER_NAME=${CONTAINER_NAME}") DOCKER_ARGS+=(-e "DEVA_WORKSPACE=$(pwd)") DOCKER_ARGS+=(-e "DEVA_EPHEMERAL=${EPHEMERAL_MODE}") +[ -n "$GOAL" ] && DOCKER_ARGS+=(-e "DEVA_GOAL=${GOAL}") # Back up .claude.json before mounting, without touching live credential files. if [ "$QUICK_MODE" != true ] && [ "$DRY_RUN" != true ]; then @@ -4206,6 +4225,24 @@ if [ "$DRY_RUN" = true ]; then exit 0 fi +# Launch receipt: one JSONL line per --goal launch, host-side, into the +# ccx data dir. Receipts are evidence (ccx joins receipt->session by +# cwd+time); the ops ledger cites session IDs, it does not store these. +# Never blocks a launch: a failed write is a warning, not an error. +write_launch_receipt() { + [ -n "$GOAL" ] || return 0 + local dir="${XDG_DATA_HOME:-$HOME/.local/share}/ccx/launches" + if ! mkdir -p "$dir" 2>/dev/null; then + echo "warning: cannot create $dir; skipping launch receipt" >&2 + return 0 + fi + printf '{"ts":"%s","goal":"%s","agent":"%s","cwd":"%s","container":"%s","source":"deva"}\n' \ + "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$GOAL" "${ACTIVE_AGENT:-}" "$(pwd)" "$CONTAINER_NAME" \ + >>"$dir/$(date -u +%Y-%m-%d).jsonl" 2>/dev/null || + echo "warning: failed to write launch receipt" >&2 +} +write_launch_receipt + inject_workspace_context if [ "$EPHEMERAL_MODE" = false ]; then @@ -4274,6 +4311,11 @@ if [ "$EPHEMERAL_MODE" = false ]; then _trace_env="DEVA_TRACE=0" [ "${DEVA_TRACE_ACTIVE:-false}" = true ] && _trace_env="DEVA_TRACE=1" + # Same override rule for the goal: attach with a fresh --goal restamps + # the container; without it the create-time DEVA_GOAL (if any) stands. + _goal_env=() + [ -n "$GOAL" ] && _goal_env=(-e "DEVA_GOAL=${GOAL}") + # Trace UI reachability is fixed at container create (port publish); # attaching to a container created without it cannot gain the mapping. announce_trace_ui existing @@ -4281,10 +4323,10 @@ if [ "$EPHEMERAL_MODE" = false ]; then announce_cloak_vnc existing if [ "$AUTH_PROVISION_MODE" = true ]; then - docker exec -e "$_trace_env" "${DOCKER_TERMINAL_ARGS[@]}" "$CONTAINER_NAME" /usr/local/bin/docker-entrypoint.sh "${AGENT_COMMAND[@]}" || true + docker exec -e "$_trace_env" ${_goal_env[@]+"${_goal_env[@]}"} "${DOCKER_TERMINAL_ARGS[@]}" "$CONTAINER_NAME" /usr/local/bin/docker-entrypoint.sh "${AGENT_COMMAND[@]}" || true finish_auth_provision else - exec docker exec -e "$_trace_env" "${DOCKER_TERMINAL_ARGS[@]}" "$CONTAINER_NAME" /usr/local/bin/docker-entrypoint.sh "${AGENT_COMMAND[@]}" + exec docker exec -e "$_trace_env" ${_goal_env[@]+"${_goal_env[@]}"} "${DOCKER_TERMINAL_ARGS[@]}" "$CONTAINER_NAME" /usr/local/bin/docker-entrypoint.sh "${AGENT_COMMAND[@]}" fi else echo "Launching ${ACTIVE_AGENT} (ephemeral mode) via $(docker_image_ref)"