Skip to content
Merged
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
2 changes: 1 addition & 1 deletion .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -393,7 +393,7 @@ report: <path>

### Which engine runs L2

L2 is an adapter too. `AUTODREAM_L2_ENGINE=<adapter name>` 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_<NAME>`, 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_<adapter>` 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=<adapter name>` 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_<NAME>`, 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_<adapter>` 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

Expand Down
6 changes: 5 additions & 1 deletion adapters/claude/adapter.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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 \
Expand All @@ -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: <literal path from line 2 of the prompt> then a 3-line summary (sessions reviewed, findings, pins proposed), then exit.'
--append-system-prompt "$sysprompt"
;;

skills-inventory)
Expand Down
5 changes: 4 additions & 1 deletion adapters/omp/adapter.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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 \
Expand All @@ -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: <literal path from line 2 of the prompt> then a 3-line summary (sessions reviewed, findings, pins proposed), then exit.'
--append-system-prompt "$sysprompt"
;;

skills-inventory)
Expand Down
27 changes: 27 additions & 0 deletions bin/l2-prompt.sh
Original file line number Diff line number Diff line change
@@ -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
}
31 changes: 25 additions & 6 deletions bin/run.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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=""
Expand Down Expand Up @@ -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: <literal path from line 2 of the prompt> 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"
Expand Down
2 changes: 2 additions & 0 deletions install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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/<name>/adapter.sh
# relative to the script dir, so the whole directory has to be reachable from the
Expand All @@ -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.
Expand Down
1 change: 1 addition & 0 deletions prompts/L2_SYSTEM.txt
Original file line number Diff line number Diff line change
@@ -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: <literal path from line 2 of the prompt> then a 3-line summary (sessions reviewed, findings, pins proposed), then exit.
Loading
Loading