diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 7818ae8..10b2873 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -72,7 +72,7 @@ jobs: tests/slim-transcript.sh tests/apply-pins.sh \ tests/session-window.sh tests/dream-triage.sh tests/chunk-transcript.sh tests/merge-chunks.sh \ tests/cookie-cadence.sh tests/review-skip.sh tests/review-cmux.sh tests/notes-path.sh \ - tests/tmp-cleanup.sh tests/x-bookmarks.sh tests/prune-worktrees.sh tests/triage-candidates.sh tests/pending-triage.sh tests/report-format.sh; do + tests/tmp-cleanup.sh tests/x-bookmarks.sh tests/prune-worktrees.sh tests/triage-candidates.sh tests/pending-triage.sh tests/report-format.sh tests/l2-prompt.sh; do echo "== $t" bash "$t" || rc=1 done diff --git a/CHANGELOG.md b/CHANGELOG.md index e02a515..8d690e0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ All notable changes to cc-autodream. Format loosely follows Keep a Changelog. ### Changed - **The report's two contracts are read in `bin/report-format.sh` and nowhere else.** The open-questions marker sed was copied in `report-format.sh`, `notify.sh` and `question-streaks.sh`, the marker presence grep in three places in `run.sh`, and the sentinel grep and awk in `run.sh` (four times) and `triage-dream.sh`. They are now `report_marker_count`, `report_has_marker`, `report_has_sentinel`, `report_body`, `report_after_sentinel` and `report_pin_lines`. The marker count stays strict (a well-formed comment) and the presence check stays loose (any line with the key), so `question-streaks.sh` still refuses to clear a streak on a malformed marker while `run.sh` still accepts a truncated report that carries the key. `run.sh` stops at startup with `FATAL` if the lib is missing, instead of failing on the first report read. `tests/report-format.sh` compares each helper with the code it replaced over 16 fixtures, a missing file among them (119 assertions). +- **The L2 system prompt is one file, `prompts/L2_SYSTEM.txt`.** The same 657 characters sat inline in the claude adapter, the omp adapter and `run.sh`'s built-in claude fallback. They state the stdout grammar that `bin/report-format.sh` parses (the report, `AUTODREAM_REPORT_END`, the optional pin block, the summary), so a drift between a copy and the parser would show up only as an L2 run whose output the runner cannot split. `bin/l2-prompt.sh` reads the file and returns 2 with a message when it cannot, and both adapters' `l2-argv` then exit 2 and print no argv, where an engine without the prompt would otherwise run with no word on where the report goes. `run.sh`'s fallback builds no argv without it and logs why. `install.sh` links the prompt and the loader. `tests/l2-prompt.sh` (25 assertions) pins the text, the missing-file failures and that no script keeps an inline copy. ## 2026-10-18 diff --git a/CLAUDE.md b/CLAUDE.md index 74b2cc6..514fcdb 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -393,7 +393,7 @@ report: ### Which engine runs L2 -L2 is an adapter too. `AUTODREAM_L2_ENGINE=` picks it (default: the first enabled adapter, so `claude` on an untouched install); a name that is not an accepted adapter stops the run before L1, not after. The adapter prints the command (`adapter.sh l2-argv [model]`, NUL-delimited, prompt on stdin, tools Glob and Read only) and `l1-env` supplies its environment. The model resolves `AUTODREAM_L2_MODEL_`, then `AUTODREAM_L2_MODEL`, then the manifest's `l2_model`; none at all is valid and means the engine's own default (claude). `run-stats.txt` records `l2_engine`, `l2_model` (`default` when none was named) and one `l1_model_` per source that had sessions, so a report that reads differently can be traced to the engine that wrote it. An install whose `adapters/` tree predates `l2-argv` keeps the built-in claude invocation, for claude only. +L2 is an adapter too. `AUTODREAM_L2_ENGINE=` picks it (default: the first enabled adapter, so `claude` on an untouched install); a name that is not an accepted adapter stops the run before L1, not after. The adapter prints the command (`adapter.sh l2-argv [model]`, NUL-delimited, prompt on stdin, tools Glob and Read only) and `l1-env` supplies its environment. The model resolves `AUTODREAM_L2_MODEL_`, then `AUTODREAM_L2_MODEL`, then the manifest's `l2_model`; none at all is valid and means the engine's own default (claude). `run-stats.txt` records `l2_engine`, `l2_model` (`default` when none was named) and one `l1_model_` per source that had sessions, so a report that reads differently can be traced to the engine that wrote it. An install whose `adapters/` tree predates `l2-argv` keeps the built-in claude invocation, for claude only. Every engine gets the same system prompt, `prompts/L2_SYSTEM.txt`, through `bin/l2-prompt.sh`; it names the stdout grammar `bin/report-format.sh` parses, so change them together. A missing prompt file makes `l2-argv` exit 2 rather than start an engine without it. ### The review triage LaunchAgent diff --git a/adapters/claude/adapter.sh b/adapters/claude/adapter.sh index aff773b..884dce2 100755 --- a/adapters/claude/adapter.sh +++ b/adapters/claude/adapter.sh @@ -218,6 +218,10 @@ case "$cmd" in # writer. The model is optional, and absent means the CLI's own default, so an upgrade of the # account upgrades the nightly report with nothing edited here. set -- ${1:+--model "$1"} + # The system prompt is prompts/L2_SYSTEM.txt, read through the loader. No file means no argv: + # an engine started without it would run L2 with no word on where the report goes. + # shellcheck source=/dev/null + sysprompt=$(. "$BIN/l2-prompt.sh" && l2_system_prompt) || exit 2 printf '%s\0' "${CLAUDE_BIN:-$HOME/.local/bin/claude}" \ --print \ --permission-mode bypassPermissions \ @@ -227,7 +231,7 @@ case "$cmd" in --disable-slash-commands \ --strict-mcp-config \ --settings '{"disableAllHooks":true}' \ - --append-system-prompt 'Headless aggregator. Read the per-session findings JSONs from the findings directory given on line 1 of the prompt, then produce the COMPLETE report only on standard output, ending with a line containing exactly AUTODREAM_REPORT_END. After that line, if you propose memory pins, print them between a line AUTODREAM_PINS_BEGIN and a line AUTODREAM_PINS_END, one JSON object per line. Do not use Write or Edit anywhere. Those paths are literal strings, not shell variables — never $-expand them. After the pin block print one line: report: then a 3-line summary (sessions reviewed, findings, pins proposed), then exit.' + --append-system-prompt "$sysprompt" ;; skills-inventory) diff --git a/adapters/omp/adapter.sh b/adapters/omp/adapter.sh index e6e9221..4b8b26c 100755 --- a/adapters/omp/adapter.sh +++ b/adapters/omp/adapter.sh @@ -143,6 +143,9 @@ case "$cmd" in [ "$#" -ge 1 ] && [ -n "$1" ] || exit 2 # The L1 overlay applies here too (advisor off, no local provider probes, no first-turn # recall). Glob and Read only: the report and the pins come back on stdout. + # The system prompt is prompts/L2_SYSTEM.txt, read through the loader. No file means no argv. + # shellcheck source=/dev/null + sysprompt=$(. "$BIN/l2-prompt.sh" && l2_system_prompt) || exit 2 printf '%s\0' "$(omp_bin)" \ --allow-home \ -p \ @@ -151,7 +154,7 @@ case "$cmd" in --config "${NO_ADVISOR_CFG:-$ADIR/l1-no-advisor.yml}" \ --model "$1" \ --tools=Glob,Read \ - --append-system-prompt 'Headless aggregator. Read the per-session findings JSONs from the findings directory given on line 1 of the prompt, then produce the COMPLETE report only on standard output, ending with a line containing exactly AUTODREAM_REPORT_END. After that line, if you propose memory pins, print them between a line AUTODREAM_PINS_BEGIN and a line AUTODREAM_PINS_END, one JSON object per line. Do not use Write or Edit anywhere. Those paths are literal strings, not shell variables — never $-expand them. After the pin block print one line: report: then a 3-line summary (sessions reviewed, findings, pins proposed), then exit.' + --append-system-prompt "$sysprompt" ;; skills-inventory) diff --git a/bin/l2-prompt.sh b/bin/l2-prompt.sh new file mode 100755 index 0000000..01b826e --- /dev/null +++ b/bin/l2-prompt.sh @@ -0,0 +1,27 @@ +#!/bin/bash +# Sourced, never executed. The system prompt every L2 engine runs under, kept in +# prompts/L2_SYSTEM.txt so the claude adapter, the omp adapter and run.sh's built-in claude +# fallback all say the same thing. It states the stdout grammar that bin/report-format.sh parses +# (the report, AUTODREAM_REPORT_END, the optional pin block, the summary), so a change to that +# grammar is one edit here and one in report-format.sh. +# +# l2_system_prompt prints the text with no trailing newline added. It returns 2 and says why on +# stderr when no copy can be read: an engine started without this prompt would run L2 with no +# instruction about where the report goes, and the output would look like a night's work. + +l2_system_prompt() { + local self dir f + self="${BASH_SOURCE[0]}" + # An installed copy is a symlink into the checkout, and the prompt sits beside bin/ there. + if [ -L "$self" ]; then + f=$(readlink "$self") + case "$f" in /*) self="$f" ;; *) self="$(dirname "$self")/$f" ;; esac + fi + dir=$(cd -P "$(dirname "$self")" 2>/dev/null && pwd) || dir="" + for f in "$dir/../prompts/L2_SYSTEM.txt" "${AUTODREAM_DIR:-}/L2_SYSTEM.txt"; do + # Not just non-empty: a file of blank lines would start an engine with an empty instruction. + if [ -r "$f" ] && grep -q '[^[:space:]]' "$f" 2>/dev/null; then cat "$f"; return 0; fi + done + echo "l2-prompt: prompts/L2_SYSTEM.txt not found or empty (looked beside ${dir:-the lib} and in AUTODREAM_DIR=${AUTODREAM_DIR:-unset})" >&2 + return 2 +} diff --git a/bin/run.sh b/bin/run.sh index ec1d5ec..775eb4f 100755 --- a/bin/run.sh +++ b/bin/run.sh @@ -3028,7 +3028,17 @@ run() { fatal_exit; return 1 fi # And an engine that cannot print a command at all (omp with no model resolved, an adapter - # without l2-argv) is refused for the same reason. claude is exempt: it has a built-in fallback. + # without l2-argv) is refused for the same reason. The argv probe skips claude, whose built-in + # fallback builds the command itself. + # + # The system prompt is checked for every engine, claude included: its fallback reads the same file, + # and an engine started without it runs L2 with no word on where the report goes. Found here, the + # night stops before L1 is paid for; found later, every L2 attempt fails after it. + # shellcheck source=/dev/null + if ! (_l=$(find_lib l2-prompt.sh) && . "$_l" && l2_system_prompt >/dev/null 2>&1); then + log_fatal "prompts/L2_SYSTEM.txt (read through bin/l2-prompt.sh) was not found beside run.sh, its link target or in AUTODREAM_DIR. Refusing to start." + fatal_exit; return 1 + fi _l2e="${AUTODREAM_L2_ENGINE:-${ENABLED_ADAPTERS%% *}}" if [ "$_l2e" != "claude" ] && [ -n "$_l2e" ]; then _l2m=$(adapter_l2_model "$_l2e" 2>/dev/null) || _l2m="" @@ -4046,11 +4056,20 @@ PY # An install whose adapters/ tree predates l2-argv (the per-file symlink installs this script # supports, see enumerate_for) still has to deliver the night. The fallback is claude-only, # for the same reason enumerate_for's is: it hardcodes one engine's invocation. - log " the claude adapter has no l2-argv here; using the built-in claude invocation" - L2_ARGV=("$CLAUDE_BIN" --print --permission-mode bypassPermissions ${L2_MODEL:+--model "$L2_MODEL"} \ - --no-session-persistence --tools Glob Read --disable-slash-commands --strict-mcp-config \ - --settings '{"disableAllHooks":true}' \ - --append-system-prompt "Headless aggregator. Read the per-session findings JSONs from the findings directory given on line 1 of the prompt, then produce the COMPLETE report only on standard output, ending with a line containing exactly AUTODREAM_REPORT_END. After that line, if you propose memory pins, print them between a line AUTODREAM_PINS_BEGIN and a line AUTODREAM_PINS_END, one JSON object per line. Do not use Write or Edit anywhere. Those paths are literal strings, not shell variables — never \$-expand them. After the pin block print one line: report: then a 3-line summary (sessions reviewed, findings, pins proposed), then exit.") + log " the claude adapter printed no L2 command here (an adapters/ tree that predates l2-argv); using the built-in claude invocation" + # The system prompt is prompts/L2_SYSTEM.txt, the same text the adapters read. Without it no + # argv is built at all (the warning below fires), because an L2 run with no word on where the + # report goes only looks like a night's work. + # shellcheck source=/dev/null + L2_SYS=$(_l=$(find_lib l2-prompt.sh) && . "$_l" && l2_system_prompt) || L2_SYS="" + if [ -n "$L2_SYS" ]; then + L2_ARGV=("$CLAUDE_BIN" --print --permission-mode bypassPermissions ${L2_MODEL:+--model "$L2_MODEL"} \ + --no-session-persistence --tools Glob Read --disable-slash-commands --strict-mcp-config \ + --settings '{"disableAllHooks":true}' \ + --append-system-prompt "$L2_SYS") + else + log "WARNING: the built-in claude invocation needs prompts/L2_SYSTEM.txt through bin/l2-prompt.sh, and neither could be read" + fi fi if [ "${#L2_ARGV[@]}" -eq 0 ]; then log "WARNING: the $L2_ENGINE adapter produced no L2 command; every L2 attempt will fail" diff --git a/install.sh b/install.sh index d4afc6d..b6fe2f9 100755 --- a/install.sh +++ b/install.sh @@ -162,6 +162,7 @@ link "$REPO_DIR/bin/lib-install-dir.sh" "$TARGET/lib-install-dir.sh" link "$REPO_DIR/bin/adapters.sh" "$TARGET/adapters.sh" link "$REPO_DIR/bin/preflight.sh" "$TARGET/preflight.sh" link "$REPO_DIR/bin/report-format.sh" "$TARGET/report-format.sh" +link "$REPO_DIR/bin/l2-prompt.sh" "$TARGET/l2-prompt.sh" link "$REPO_DIR/bin/pending-triage.sh" "$TARGET/pending-triage.sh" # The adapters TREE, not individual files: run.sh resolves adapters//adapter.sh # relative to the script dir, so the whole directory has to be reachable from the @@ -171,6 +172,7 @@ link "$REPO_DIR/adapters" "$TARGET/adapters" link "$REPO_DIR/prompts/PROMPT.md" "$TARGET/PROMPT.md" link "$REPO_DIR/prompts/SESSION_TRIAGE.md" "$TARGET/SESSION_TRIAGE.md" link "$REPO_DIR/prompts/TRIAGE_DREAM.md" "$TARGET/TRIAGE_DREAM.md" +link "$REPO_DIR/prompts/L2_SYSTEM.txt" "$TARGET/L2_SYSTEM.txt" # The global /autodream skill. A skill nobody asked for must not fail the install, so a # directory already sitting at the link's place (the user's own /autodream) is reported and left. diff --git a/prompts/L2_SYSTEM.txt b/prompts/L2_SYSTEM.txt new file mode 100644 index 0000000..3cad735 --- /dev/null +++ b/prompts/L2_SYSTEM.txt @@ -0,0 +1 @@ +Headless aggregator. Read the per-session findings JSONs from the findings directory given on line 1 of the prompt, then produce the COMPLETE report only on standard output, ending with a line containing exactly AUTODREAM_REPORT_END. After that line, if you propose memory pins, print them between a line AUTODREAM_PINS_BEGIN and a line AUTODREAM_PINS_END, one JSON object per line. Do not use Write or Edit anywhere. Those paths are literal strings, not shell variables — never $-expand them. After the pin block print one line: report: then a 3-line summary (sessions reviewed, findings, pins proposed), then exit. \ No newline at end of file diff --git a/tests/l2-prompt.sh b/tests/l2-prompt.sh new file mode 100755 index 0000000..f49bb04 --- /dev/null +++ b/tests/l2-prompt.sh @@ -0,0 +1,106 @@ +#!/bin/bash +# shellcheck disable=SC2016 # the `bash -c` bodies below are single-quoted on purpose: $1 is theirs, not ours +# Tests for the L2 system prompt: one file (prompts/L2_SYSTEM.txt), one loader (bin/l2-prompt.sh), +# three consumers (the claude adapter, the omp adapter, run.sh's built-in claude fallback). +# +# The text states the stdout grammar that bin/report-format.sh parses. Three byte-identical copies +# used to carry it; the risk was one of them drifting from the parser and an L2 run printing a +# report the runner could not split. A missing prompt must stop an engine from starting rather +# than start it with no instructions, so that failure is pinned here too. +# +# Usage: tests/l2-prompt.sh +# Exit: 0 if every assertion passes, 1 otherwise. + +set -u + +HERE=$(cd "$(dirname "$0")" && pwd) +REPO=$(cd "$HERE/.." && pwd) +# shellcheck source=/dev/null +. "$HERE/lib-tmp.sh"; suite_tmp ccl2p + +pass=0; fail=0 +ok(){ printf ' ok - %s\n' "$1"; pass=$((pass + 1)); } +no(){ printf ' FAIL - %s\n' "$1"; fail=$((fail + 1)); } +assert_eq(){ if [ "$1" = "$2" ]; then ok "$3"; else no "$3 (got [$1] want [$2])"; fi; } + +PROMPT_FILE="$REPO/prompts/L2_SYSTEM.txt" +WANT=$(cat "$PROMPT_FILE" 2>/dev/null) + +# Inside the suite's own temp root, which suite_tmp removes on every way out. +T=$(mktemp -d "${TMPDIR:-/tmp}/ccl2p.XXXXXX") + +# a minimal install tree: bin/, adapters/{claude,omp}, and optionally prompts/ +mk_tree(){ # $1=name $2=with-prompt(1|0) + local r="$T/$1" + mkdir -p "$r/bin" "$r/adapters/claude" "$r/adapters/omp" "$r/prompts" + cp "$REPO/bin/l2-prompt.sh" "$r/bin/" 2>/dev/null + cp "$REPO/adapters/claude/adapter.sh" "$r/adapters/claude/" + cp "$REPO/adapters/omp/adapter.sh" "$r/adapters/omp/" + [ "$2" = 1 ] && cp "$PROMPT_FILE" "$r/prompts/" 2>/dev/null + printf '%s' "$r" +} +# the value after --append-system-prompt in an adapter's NUL-delimited argv +sys_prompt_of(){ tr '\0' '\n' | awk '/^--append-system-prompt$/ { getline; print; exit }'; } + +echo "# the file states the grammar the report parser reads" +for tok in AUTODREAM_REPORT_END AUTODREAM_PINS_BEGIN AUTODREAM_PINS_END "line 1 of the prompt" "line 2 of the prompt"; do + case "$WANT" in *"$tok"*) ok "L2_SYSTEM.txt names $tok" ;; *) no "L2_SYSTEM.txt does not name $tok" ;; esac +done + +echo "# the loader" +# shellcheck source=/dev/null +. "$REPO/bin/l2-prompt.sh" +assert_eq "$(l2_system_prompt)" "$WANT" "prints the file" +cmp -s <(l2_system_prompt) "$PROMPT_FILE"; assert_eq "$?" "0" "byte for byte, no newline added" +r=$(mk_tree bare 0) +out=$(env -u AUTODREAM_DIR bash -c '. "$1/bin/l2-prompt.sh"; l2_system_prompt' _ "$r" 2>"$T/err"); rc=$? +assert_eq "$rc" "2" "a missing prompt returns 2" +assert_eq "$out" "" "and prints nothing on stdout" +case "$(cat "$T/err")" in *"L2_SYSTEM.txt not found"*) ok "and says which file is missing" ;; *) no "no message for a missing prompt ($(cat "$T/err"))" ;; esac +r=$(mk_tree emptyfile 0); : > "$r/prompts/L2_SYSTEM.txt" +env -u AUTODREAM_DIR bash -c '. "$1/bin/l2-prompt.sh"; l2_system_prompt' _ "$r" >/dev/null 2>&1; assert_eq "$?" "2" "an empty prompt file is refused too" +r=$(mk_tree viadir 0); mkdir -p "$T/ad"; cp "$PROMPT_FILE" "$T/ad/L2_SYSTEM.txt" +assert_eq "$(AUTODREAM_DIR="$T/ad" bash -c '. "$1/bin/l2-prompt.sh"; l2_system_prompt' _ "$r" 2>/dev/null)" "$WANT" "AUTODREAM_DIR's copy is the second place it looks" +mkdir -p "$T/farm"; ln -s "$REPO/bin/l2-prompt.sh" "$T/farm/l2-prompt.sh" +assert_eq "$(env -u AUTODREAM_DIR bash -c '. "$1/farm/l2-prompt.sh"; l2_system_prompt' _ "$T" 2>/dev/null)" "$WANT" "a symlinked lib resolves into its checkout, like an installed copy" + +r=$(mk_tree wsonly 0); printf ' \n\n\t\n' > "$r/prompts/L2_SYSTEM.txt" +env -u AUTODREAM_DIR bash -c '. "$1/bin/l2-prompt.sh"; l2_system_prompt' _ "$r" >/dev/null 2>&1; assert_eq "$?" "2" "a prompt file of only whitespace is refused too" +# a symlink whose target is RELATIVE, as a hand-made link might be (install.sh writes absolute ones) +mkdir -p "$T/rel/real/bin" "$T/rel/real/prompts" "$T/rel/farm" +cp "$REPO/bin/l2-prompt.sh" "$T/rel/real/bin/"; cp "$PROMPT_FILE" "$T/rel/real/prompts/" +ln -s ../real/bin/l2-prompt.sh "$T/rel/farm/l2-prompt.sh" +assert_eq "$(env -u AUTODREAM_DIR bash -c '. "$1/rel/farm/l2-prompt.sh"; l2_system_prompt' _ "$T" 2>/dev/null)" "$WANT" "a relative symlink target resolves against the link's own directory" + +echo "# the claude adapter" +r=$(mk_tree withprompt 1) +got=$(CLAUDE_BIN=/opt/test/claude bash "$r/adapters/claude/adapter.sh" l2-argv | sys_prompt_of) +assert_eq "$got" "$WANT" "l2-argv carries the file's text" +got=$(CLAUDE_BIN=/opt/test/claude bash "$REPO/adapters/claude/adapter.sh" l2-argv | sys_prompt_of) +assert_eq "$got" "$WANT" "the real adapter does too" +r=$(mk_tree noprompt 0) +out=$(env -u AUTODREAM_DIR CLAUDE_BIN=/opt/test/claude bash "$r/adapters/claude/adapter.sh" l2-argv 2>/dev/null | wc -c | tr -d ' ') +env -u AUTODREAM_DIR CLAUDE_BIN=/opt/test/claude bash "$r/adapters/claude/adapter.sh" l2-argv >/dev/null 2>&1; rc=$? +assert_eq "$rc" "2" "with no prompt file, l2-argv fails instead of starting an engine without one" +assert_eq "$out" "0" "and prints no argv" + +echo "# the omp adapter" +r=$(mk_tree ompwith 1) +got=$(OMP_BIN=/opt/test/omp bash "$r/adapters/omp/adapter.sh" l2-argv some-model | sys_prompt_of) +assert_eq "$got" "$WANT" "l2-argv carries the file's text" +r=$(mk_tree ompnone 0) +out=$(env -u AUTODREAM_DIR OMP_BIN=/opt/test/omp bash "$r/adapters/omp/adapter.sh" l2-argv some-model 2>/dev/null | wc -c | tr -d ' ') +env -u AUTODREAM_DIR OMP_BIN=/opt/test/omp bash "$r/adapters/omp/adapter.sh" l2-argv some-model >/dev/null 2>&1; rc=$? +assert_eq "$rc" "2" "with no prompt file, l2-argv fails" +assert_eq "$out" "0" "and prints no argv" +assert_eq "$(OMP_BIN=/opt/test/omp bash "$REPO/adapters/omp/adapter.sh" l2-argv some-model | sys_prompt_of)" "$WANT" "the real adapter carries it" + +echo "# one source: no consumer keeps its own copy" +copies=$(grep -ln "Headless aggregator" "$REPO"/bin/*.sh "$REPO"/adapters/*/adapter.sh 2>/dev/null || true) +assert_eq "$copies" "" "no script holds the prompt text inline" +if grep -q "l2-prompt.sh" "$REPO/bin/run.sh"; then ok "run.sh's built-in fallback reads it through the loader"; else no "run.sh does not use l2-prompt.sh"; fi +if grep -q "L2_SYSTEM.txt" "$REPO/install.sh"; then ok "install.sh links the prompt into the install"; else no "install.sh does not link L2_SYSTEM.txt"; fi +if grep -q "l2-prompt.sh" "$REPO/install.sh"; then ok "install.sh links the loader"; else no "install.sh does not link l2-prompt.sh"; fi + +printf '\npassed: %s failed: %s\n' "$pass" "$fail" +[ "$fail" -eq 0 ] diff --git a/tests/run-all.sh b/tests/run-all.sh index 6c61ba4..3e021c4 100755 --- a/tests/run-all.sh +++ b/tests/run-all.sh @@ -171,6 +171,7 @@ setup_env(){ mkdir -p "$root/projects/proj-a" "$root/autodream" "$root/dreams" "$root/cap" cp "$REPO/prompts/SESSION_TRIAGE.md" "$root/autodream/SESSION_TRIAGE.md" cp "$REPO/prompts/PROMPT.md" "$root/autodream/PROMPT.md" + cp "$REPO/prompts/L2_SYSTEM.txt" "$root/autodream/L2_SYSTEM.txt" # install.sh links it too printf '%s' "$root" } mk_session(){ # $1=root $2=name @@ -3464,6 +3465,7 @@ setup_env_altroot(){ # like setup_env, but with HOME inside the sandbox (no $1/p "$root/autodream" "$root/dreams" "$root/cap" cp "$REPO/prompts/SESSION_TRIAGE.md" "$root/autodream/SESSION_TRIAGE.md" cp "$REPO/prompts/PROMPT.md" "$root/autodream/PROMPT.md" + cp "$REPO/prompts/L2_SYSTEM.txt" "$root/autodream/L2_SYSTEM.txt" # install.sh links it too printf '%s' "$root" } mk_session_in(){ # $1=dir $2=name @@ -3959,6 +3961,7 @@ test_fresh_host_with_no_store_is_not_a_failure(){ mkdir -p "$T/home" "$T/autodream" "$T/dreams" cp "$REPO/prompts/SESSION_TRIAGE.md" "$T/autodream/SESSION_TRIAGE.md" cp "$REPO/prompts/PROMPT.md" "$T/autodream/PROMPT.md" + cp "$REPO/prompts/L2_SYSTEM.txt" "$T/autodream/L2_SYSTEM.txt" # install.sh links it too # No SESSION_ROOTS, no PROJECTS_DIR, and a HOME with no .claude at all. HOME="$T/home" AUTODREAM_CHANGELOG=0 CLAUDE_BIN="$MOCK" \ AUTODREAM_CONFIG="$T/autodream/config" AUTODREAM_CONSUME_DATE="$DATE" \ @@ -4583,6 +4586,7 @@ test_upgrade_lag_install_still_produces_a_report(){ mkdir -p "$T/home/.claude/projects/proj-a" "$T/autodream" "$T/dreams" cp "$REPO/prompts/SESSION_TRIAGE.md" "$T/autodream/SESSION_TRIAGE.md" cp "$REPO/prompts/PROMPT.md" "$T/autodream/PROMPT.md" + cp "$REPO/prompts/L2_SYSTEM.txt" "$T/autodream/L2_SYSTEM.txt" # install.sh links it too # Exactly what a pre-adapter install left behind: the helper scripts, and # run.sh as a symlink into the repo. Deliberately NOT lib-project.sh, # adapters.sh, preflight.sh or adapters/. @@ -5559,6 +5563,29 @@ test_window_degrades_when_the_helper_is_absent(){ rm -rf "$root" } +test_a_missing_l2_prompt_stops_the_run_before_l1(){ + echo "# L2 prompt: an install with no prompts/L2_SYSTEM.txt anywhere refuses to start, claude engine included" + local root; root=$(setup_env) + local inst="$root/inst"; mkdir -p "$inst/bin" + cp -R "$REPO/adapters" "$inst/adapters" + cp "$REPO"/bin/*.sh "$inst/bin/" + rm -f "$root/autodream/L2_SYSTEM.txt" # no AUTODREAM_DIR copy, and the copied tree has no prompts/ beside bin/ + mk_session "$root" s1 + mkdir -p "$root/shim-default"; printf '#!/bin/bash\nprintf %s "200"\n' "'%s'" > "$root/shim-default/curl"; chmod +x "$root/shim-default/curl" + PATH="$root/shim-default:$PATH" AUTODREAM_CHANGELOG=0 CLAUDE_BIN="$MOCK" AUTODREAM_CONFIG="$root/autodream/config" \ + AUTODREAM_CONSUME_DATE="$DATE" AUTODREAM_NETCHECK=0 AUTODREAM_RETRY_WAIT=0 AUTODREAM_L1_ROUNDS=1 \ + PROJECTS_DIR="$root/projects" AUTODREAM_DIR="$root/autodream" DREAMS_DIR="$root/dreams" \ + /bin/bash "$inst/bin/run.sh" "$DATE" > "$root/run.out" 2>&1 + local rc=$? + cat "$root/autodream/logs/run-$DATE.log" >> "$root/run.out" 2>/dev/null || true + [ "$rc" -ne 0 ] && ok "the run exits non-zero" || no "the run exits non-zero (got $rc)" + assert_grep "$root/run.out" 'L2_SYSTEM.txt' "the log names the missing prompt" + assert_grep "$root/run.out" 'Refusing to start' "and says it refused to start" + assert_nogrep "$root/run.out" 'L1 triage round' "no L1 round started, so nothing was paid for" + assert_no_file "$root/dreams/$DATE.md" "and no report was written" + rm -rf "$root" +} + test_window_is_dst_correct_end_to_end(){ echo "# window: the report day is local midnight to local midnight on a 23-hour DST day (America/New_York 2020-03-08)" local DATE=2020-03-08 root; root=$(setup_env) @@ -5780,6 +5807,7 @@ test_window_places_a_session_by_its_records_not_its_mtime test_window_cuts_a_multi_day_session_to_the_report_day test_window_off_restores_mtime_enumeration test_window_degrades_when_the_helper_is_absent +test_a_missing_l2_prompt_stops_the_run_before_l1 test_window_is_dst_correct_end_to_end test_rebuild_keeps_a_session_touched_after_its_day_and_retries_its_stale_err test_rebuild_of_an_omp_parent_touched_after_its_day_keeps_it_beside_its_advisor @@ -6327,7 +6355,7 @@ echo echo "===== unit suites =====" # The suites that drive launchd need plutil and launchctl, so they run on macOS only (CI runs # them in launchd.yml, when a launchd file changes). -_suites="portable lib-project preflight adapters adapter-claude adapter-omp adapter-contract slim-transcript session-window merge-chunks chunk-transcript apply-pins review-skip review-cmux notes-path tmp-cleanup dream-triage cookie-cadence x-bookmarks prune-worktrees triage-candidates pending-triage report-format" +_suites="portable lib-project preflight adapters adapter-claude adapter-omp adapter-contract slim-transcript session-window merge-chunks chunk-transcript apply-pins review-skip review-cmux notes-path tmp-cleanup dream-triage cookie-cadence x-bookmarks prune-worktrees triage-candidates pending-triage report-format l2-prompt" [ "$(uname -s)" = Darwin ] && _suites="$_suites scheduler-label autodream-now install-review-agent install-dry-run install-dir install-skill" for _suite in $_suites; do _out=$(bash "$HERE/$_suite.sh" 2>&1)