From af31e4ab9c09b83560ebb2327dc5e54de33b777d Mon Sep 17 00:00:00 2001 From: PP <121104417+BohnBawerick@users.noreply.github.com> Date: Thu, 20 Aug 2026 12:20:59 +0800 Subject: [PATCH 1/5] Add operating picture data/memory/now.md support to memory compile --- AGENTS.md | 2 +- bin/fm-memory-compile.sh | 170 +++++++++++++++++++++++---- docs/configuration.md | 3 +- docs/documentation-audiences.json | 4 + docs/examples/now.md | 7 ++ tests/fm-memory-compile.test.sh | 188 ++++++++++++++++++++++++++++++ 6 files changed, 350 insertions(+), 24 deletions(-) create mode 100644 docs/examples/now.md diff --git a/AGENTS.md b/AGENTS.md index d021652b35..edbcf8d359 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -81,7 +81,7 @@ data/ personal fleet records; LOCAL, gitignored as a whole backlog.md task queue, dependencies, history captain.md this home's domain-local captain preferences and working style; LOCAL, gitignored, canonical even if harness memory mirrors it, and updated with inspect-then-update captain-shared.md main-authoritative shared captain preferences propagated read-only to secondmate homes; LOCAL, gitignored, owned by secondmate-provisioning - memory/ fleet-local operational knowledge as one atomic note per claim, plus an optional standing core, the regenerable catalog, and the never-injected drop tray; LOCAL, gitignored; curated with inspect-then-update - rewrite and prune rather than append forever, the same contract as captain.md; bin/fm-memory-compile.sh owns the note format and what session start injects, and bin/fm-memory-migrate.sh owns creating this layout from a home's legacy learnings.md + memory/ fleet-local operational knowledge as one atomic note per claim, plus an optional standing core, the dated operating picture now.md, the regenerable catalog, and the never-injected drop tray; LOCAL, gitignored; curated with inspect-then-update - rewrite and prune rather than append forever, the same contract as captain.md; bin/fm-memory-compile.sh owns the note format and what session start injects, and bin/fm-memory-migrate.sh owns creating this layout from a home's legacy learnings.md projects.md thin fleet navigation registry recording each project's standing delivery posture; firstmate-private, parsed for mechanical sync and seeding by fm-project-mode.sh (section 6) secondmates.md local and remote secondmate routing table; firstmate-private, maintained by the secondmate seed helpers (section 6) /brief.md per-task crewmate brief, or per-secondmate charter brief when kind=secondmate diff --git a/bin/fm-memory-compile.sh b/bin/fm-memory-compile.sh index c82fddf73f..d91c58d7bf 100755 --- a/bin/fm-memory-compile.sh +++ b/bin/fm-memory-compile.sh @@ -28,6 +28,10 @@ # constitution by default and is used as the core when core.md # is absent, so a home that never authors core.md loses nothing. # core.md takes precedence the moment it exists. +# now.md the dated operating picture. Carries this-shift pins and +# ceilings with a front matter date. Injected only when dated +# today; a stale file is dropped and reported, while absence is +# silent. # notes/*.md atomic notes, one claim each. # catalog.md the regenerable index, one line per note: claim title, file # name under notes/, first triggers, and updated date. @@ -45,14 +49,22 @@ # Matching is case-insensitive and bounded by non-alphanumeric characters at # both ends, so `lint` matches `commands.lint` but not `linting`. # +# OPERATING PICTURE FORMAT. now.md opens with a YAML-style front matter block +# delimited by a bare `---` on line 1 and the next bare `---`: +# date: ISO date (YYYY-MM-DD), matched against today's date +# An undated or stale now.md is never injected. +# # SELECTION AND CAP. config/startup-memory-budget owns the cap and # bin/fm-startup-memory-budget-lib.sh owns the ceil(UTF-8 bytes / 3) estimate. # This script accounts memory CONTENT bytes against that cap, exactly as # bin/fm-startup-memory-budget.sh report does, and excludes its own framing -# lines. Precedence under pressure is core, then catalog, then hot notes: +# lines. Precedence under pressure is core, then operating picture, then catalog, +# then hot notes: # - core is never dropped and never truncated; # - core alone over budget prints core plus a loud MEMORY_BUDGET_WARNING and -# no catalog and no notes; +# no operating picture, no catalog, and no notes; +# - the operating picture (now.md) is kept ahead of the catalog when dated +# today, because a stale ceiling is the failure this tier exists to prevent; # - the catalog is kept ahead of every hot note, because it is the thing that # tells the next turn a note exists at all; # - hot notes are added newest-updated first, and one that does not fit is @@ -502,8 +514,68 @@ tokens_of_file() { CORE_PATH= CORE_LABEL= CORE_TOKENS=0 +NOW_PATH= +NOW_LABEL= +NOW_TOKENS=0 +NOW_VALID=0 +NOW_DATE= NOTICES=() +parse_now_date() { + local path=$1 + [ -f "$path" ] && [ ! -L "$path" ] || return 1 + awk ' + function clean(s) { + gsub(/\t/, " ", s) + sub(/^[[:space:]]+/, "", s) + sub(/[[:space:]]+$/, "", s) + gsub(/^["\047]+|["\047]+$/, "", s) + return s + } + BEGIN { fm = 0; date = "" } + FNR == 1 { + if ($0 ~ /^---[[:space:]]*$/) { fm = 1; next } + exit + } + fm && $0 ~ /^---[[:space:]]*$/ { exit } + fm { + if (match($0, /^[A-Za-z_][A-Za-z0-9_-]*:[[:space:]]*/)) { + key = tolower(substr($0, 1, index($0, ":") - 1)) + val = clean(substr($0, RLENGTH + 1)) + if (key == "date" || key == "updated") { + date = val + } + } + } + END { + print date + } + ' "$path" +} + +is_today_date() { + local d=$1 today_utc today_local + [ -n "$d" ] || return 1 + if [ -n "${FM_MEMORY_TODAY_OVERRIDE:-${FM_MEMORY_NOW_DATE:-}}" ]; then + [ "$d" = "${FM_MEMORY_TODAY_OVERRIDE:-${FM_MEMORY_NOW_DATE:-}}" ] && return 0 + return 1 + fi + today_utc=$(date -u +%Y-%m-%d 2>/dev/null || true) + today_local=$(date +%Y-%m-%d 2>/dev/null || true) + if [ "$d" = "$today_utc" ] || [ "$d" = "$today_local" ]; then + return 0 + fi + return 1 +} + +get_today_display() { + if [ -n "${FM_MEMORY_TODAY_OVERRIDE:-${FM_MEMORY_NOW_DATE:-}}" ]; then + printf '%s' "${FM_MEMORY_TODAY_OVERRIDE:-${FM_MEMORY_NOW_DATE:-}}" + return + fi + date +%Y-%m-%d 2>/dev/null || date -u +%Y-%m-%d 2>/dev/null || printf 'today' +} + if [ "$NOTES_DIR_SYMLINK" -eq 1 ]; then NOTICES+=("MEMORY_NOTICE: $REL_LABEL/notes/ is a symlink, so no note was read through it. Any notes under it are NOT in this bundle and are reported here as absent. Replace the symlink with a real directory.") fi @@ -534,6 +606,23 @@ else NOTICES+=("MEMORY_NOTICE: no core memory - both $REL_LABEL/core.md and data/captain.md are ABSENT, so this home is running on the firstmate repo built-in defaults.") fi +if [ "$MEMORY_DIR_OK" -eq 1 ] && [ -L "$MEMORY/now.md" ]; then + NOTICES+=("MEMORY_NOTICE: $REL_LABEL/now.md is a symlink, so nothing was read through it. Replace the symlink with a real file, or remove it.") +elif [ "$MEMORY_DIR_OK" -eq 1 ] && [ -f "$MEMORY/now.md" ]; then + NOW_DATE=$(parse_now_date "$MEMORY/now.md") + if is_today_date "$NOW_DATE"; then + NOW_VALID=1 + NOW_PATH="$MEMORY/now.md" + NOW_LABEL="$REL_LABEL/now.md" + NOW_TOKENS=$(tokens_of_file "$NOW_PATH") + elif [ -n "$NOW_DATE" ]; then + TODAY_DISP=$(get_today_display) + NOTICES+=("MEMORY_NOTICE: $REL_LABEL/now.md is dated $NOW_DATE (not today, $TODAY_DISP) and is NOT injected. Update it with this shift's pins and ceilings, or remove it.") + else + NOTICES+=("MEMORY_NOTICE: $REL_LABEL/now.md has no date in front matter and is NOT injected. Add a date (date: YYYY-MM-DD), or remove it.") + fi +fi + render_catalog > "$TMP/catalog" CATALOG_TOKENS=$(tokens_of_file "$TMP/catalog") @@ -549,17 +638,28 @@ if [ -f "$DATA/learnings.md" ] && [ ! -L "$DATA/learnings.md" ]; then NOTICES+=("MEMORY_NOTICE: data/learnings.md is still present (${LEARNINGS_TOKENS} estimated tokens) and is NOT injected. Migrate it into notes with bin/fm-memory-migrate.sh, or read it directly when a turn needs it.") fi -# Precedence under pressure: core, then catalog, then hot notes. +# Precedence under pressure: core, then operating picture, then catalog, then hot notes. TOTAL=$CORE_TOKENS CORE_OVER=0 +NOW_KEPT=0 CATALOG_KEPT=1 + if ! fm_startup_memory_decimal_le "$TOTAL" "$BUDGET"; then CORE_OVER=1 CATALOG_KEPT=0 -elif ! fm_startup_memory_decimal_le "$((TOTAL + CATALOG_TOKENS))" "$BUDGET"; then +elif [ "$NOW_VALID" -eq 1 ] && ! fm_startup_memory_decimal_le "$((TOTAL + NOW_TOKENS))" "$BUDGET"; then + NOW_KEPT=0 CATALOG_KEPT=0 else - TOTAL=$((TOTAL + CATALOG_TOKENS)) + if [ "$NOW_VALID" -eq 1 ]; then + NOW_KEPT=1 + TOTAL=$((TOTAL + NOW_TOKENS)) + fi + if ! fm_startup_memory_decimal_le "$((TOTAL + CATALOG_TOKENS))" "$BUDGET"; then + CATALOG_KEPT=0 + else + TOTAL=$((TOTAL + CATALOG_TOKENS)) + fi fi HOT_KEPT=0 @@ -613,22 +713,41 @@ fi if [ "$CORE_OVER" -eq 1 ]; then printf '\nMEMORY_BUDGET_WARNING: the core alone is %s estimated tokens against a %s budget. It was printed in full and NOTHING else was: no catalog, no notes. Trim the core (%s/core.md, or data/captain.md when no core.md exists) or raise config/startup-memory-budget.\n' \ "$CORE_TOKENS" "$BUDGET" "$REL_LABEL" -elif [ "$CATALOG_KEPT" -eq 0 ]; then - printf '\nMEMORY_BUDGET_WARNING: the core plus catalog is %s estimated tokens against a %s budget, so the catalog and every note were dropped. Trim the core or raise config/startup-memory-budget; until then this session cannot see what notes exist.\n' \ - "$((CORE_TOKENS + CATALOG_TOKENS))" "$BUDGET" +elif [ "$NOW_VALID" -eq 1 ] && [ "$NOW_KEPT" -eq 0 ]; then + printf '\nMEMORY_BUDGET_WARNING: the core plus operating picture is %s estimated tokens against a %s budget, so the operating picture, catalog, and every note were dropped. Trim the core or operating picture or raise config/startup-memory-budget.\n' \ + "$((CORE_TOKENS + NOW_TOKENS))" "$BUDGET" else - printf '\ncatalog (compiled from %s/notes/)\n%s\n' "$REL_LABEL" "$RULE" - cat "$TMP/catalog" - - while IFS= read -r base; do - [ -n "$base" ] || continue - printf '\nhot note: notes/%s\n%s\n' "$base" "$RULE" - cat "$NOTES_DIR/$base" - done < "$TMP/selected" - - if [ "$HOT_DROPPED" -gt 0 ]; then - printf '\nMEMORY_BUDGET_NOTICE: %s trigger-matched note(s) did not fit the budget and were not injected. Every one of them is still listed in the catalog above; read it by path when its title matches.\n' \ - "$HOT_DROPPED" + if [ "$NOW_KEPT" -eq 1 ]; then + printf '\noperating picture: %s\n%s\n' "$NOW_LABEL" "$RULE" + if [ -s "$NOW_PATH" ]; then + cat "$NOW_PATH" + else + printf '(present, empty)\n' + fi + fi + + if [ "$CATALOG_KEPT" -eq 0 ]; then + if [ "$NOW_KEPT" -eq 1 ]; then + printf '\nMEMORY_BUDGET_WARNING: the core plus operating picture plus catalog is %s estimated tokens against a %s budget, so the catalog and every note were dropped. Trim the core or operating picture or raise config/startup-memory-budget; until then this session cannot see what notes exist.\n' \ + "$((TOTAL + CATALOG_TOKENS))" "$BUDGET" + else + printf '\nMEMORY_BUDGET_WARNING: the core plus catalog is %s estimated tokens against a %s budget, so the catalog and every note were dropped. Trim the core or raise config/startup-memory-budget; until then this session cannot see what notes exist.\n' \ + "$((CORE_TOKENS + CATALOG_TOKENS))" "$BUDGET" + fi + else + printf '\ncatalog (compiled from %s/notes/)\n%s\n' "$REL_LABEL" "$RULE" + cat "$TMP/catalog" + + while IFS= read -r base; do + [ -n "$base" ] || continue + printf '\nhot note: notes/%s\n%s\n' "$base" "$RULE" + cat "$NOTES_DIR/$base" + done < "$TMP/selected" + + if [ "$HOT_DROPPED" -gt 0 ]; then + printf '\nMEMORY_BUDGET_NOTICE: %s trigger-matched note(s) did not fit the budget and were not injected. Every one of them is still listed in the catalog above; read it by path when its title matches.\n' \ + "$HOT_DROPPED" + fi fi fi @@ -644,12 +763,19 @@ done STATUS=within-budget if [ "$CORE_OVER" -eq 1 ]; then STATUS=over-budget +elif [ "$NOW_VALID" -eq 1 ] && [ "$NOW_KEPT" -eq 0 ]; then + STATUS=capped elif [ "$CATALOG_KEPT" -eq 0 ] || [ "$HOT_DROPPED" -gt 0 ]; then STATUS=capped fi -printf '\nMEMORY_ACCOUNTING: budget=%s core=%s catalog=%s hot_notes=%s hot_notes_tokens=%s notes_total=%s hot_dropped=%s injected_total=%s status=%s\n' \ - "$BUDGET" "$CORE_TOKENS" \ +NOW_ACC="" +if [ "$NOW_VALID" -eq 1 ]; then + NOW_ACC=" now=$([ "$NOW_KEPT" -eq 1 ] && printf '%s' "$NOW_TOKENS" || printf '0')" +fi + +printf '\nMEMORY_ACCOUNTING: budget=%s core=%s%s catalog=%s hot_notes=%s hot_notes_tokens=%s notes_total=%s hot_dropped=%s injected_total=%s status=%s\n' \ + "$BUDGET" "$CORE_TOKENS" "$NOW_ACC" \ "$([ "$CATALOG_KEPT" -eq 1 ] && printf '%s' "$CATALOG_TOKENS" || printf '0')" \ "$HOT_KEPT" "$HOT_TOKENS" \ "$(wc -l < "$TMP/inventory" | tr -d ' ')" \ diff --git a/docs/configuration.md b/docs/configuration.md index de28cca9a7..e0b646ca4c 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -163,8 +163,9 @@ Shared captain preferences that apply across secondmate domains live only in the ## Compiled working memory (data/memory/) -Fleet-local operational facts and gotchas live locally as one atomic note per claim under `data/memory/notes/`, alongside an optional standing constitution in `data/memory/core.md`, the regenerable index `data/memory/catalog.md`, and the never-injected candidate tray `data/memory/drop/`. +Fleet-local operational facts and gotchas live locally as one atomic note per claim under `data/memory/notes/`, alongside an optional standing constitution in `data/memory/core.md`, the dated operating picture `data/memory/now.md`, the regenerable index `data/memory/catalog.md`, and the never-injected candidate tray `data/memory/drop/`. The standing constitution `data/memory/core.md` holds standing preferences, authority boundaries, and core guidelines with a 1,500-2,500 estimated-token target. +The dated operating picture `data/memory/now.md` holds perishable shift pins and ceilings with a front matter date, and is injected only when dated today. The whole directory is gitignored and is created by `bin/fm-memory-migrate.sh`, which also splits a home's legacy `data/learnings.md` into notes and freezes the original under `data/memory/raw/` before archiving it to `data/memory-archive.md`. Completed tasks deposit candidate findings and tactical gotchas into `data/memory/drop/.md` through `bin/fm-memory-drop.sh`. Generations of memory live under `data/memory/gen//` and are activated atomically by pointing `data/memory/HEAD` at the active generation. diff --git a/docs/documentation-audiences.json b/docs/documentation-audiences.json index 87b0a6453c..e7b59db9ae 100644 --- a/docs/documentation-audiences.json +++ b/docs/documentation-audiences.json @@ -256,6 +256,10 @@ "path": "docs/examples/crew-dispatch.json", "audience": "operator-example" }, + { + "path": "docs/examples/now.md", + "audience": "operator-example" + }, { "path": "docs/examples/wedge-alarm", "audience": "operator-example" diff --git a/docs/examples/now.md b/docs/examples/now.md new file mode 100644 index 0000000000..116fde12da --- /dev/null +++ b/docs/examples/now.md @@ -0,0 +1,7 @@ +--- +date: 2026-08-20 +--- + +# Operating picture + +- 2 claude + firstmate, 2 grok, 1 opencode, 2 pi, 1 codex last resort diff --git a/tests/fm-memory-compile.test.sh b/tests/fm-memory-compile.test.sh index cd3a7a0dc7..a254887154 100755 --- a/tests/fm-memory-compile.test.sh +++ b/tests/fm-memory-compile.test.sh @@ -25,6 +25,18 @@ new_home() { printf '%s\n' "$home" } +# write_now [content] [key] +write_now() { + local home=$1 date=$2 content=${3:-"STANDING-NOW-TEXT"} key=${4:-"date"} + mkdir -p "$home/data/memory" + { + printf -- '---\n' + printf '%s: %s\n' "$key" "$date" + printf -- '---\n\n' + printf '%s\n' "$content" + } > "$home/data/memory/now.md" +} + # write_note <triggers> <updated> [padding-lines] write_note() { local home=$1 slug=$2 title=$3 triggers=$4 updated=$5 pad=${6:-0} i @@ -561,6 +573,176 @@ test_migration_on_a_home_with_no_learnings_still_builds_the_layout() { pass 'a home with no learnings file still gets a compilable data/memory/ layout' } +# --- operating picture (now.md) -------------------------------------------- + +test_operating_picture_dated_today_is_injected_ahead_of_catalog() { + local home out core_pos now_pos cat_pos + home=$(new_home now-today) + printf '# core\n\nSTANDING-CORE-TEXT\n' > "$home/data/memory/core.md" + write_now "$home" 2026-08-20 'PINS-AND-CEILINGS-TEXT' + write_note "$home" matched 'A matched claim' 'healthlog' 2026-08-18 + printf -- '- healthlog [no-mistakes] - a project\n' > "$home/data/projects.md" + + out=$(FM_MEMORY_TODAY_OVERRIDE=2026-08-20 compile "$home") + + assert_contains "$out" 'STANDING-CORE-TEXT' 'core.md body was not injected' + assert_contains "$out" 'PINS-AND-CEILINGS-TEXT' 'now.md body was not injected' + assert_contains "$out" 'operating picture: data/memory/now.md' 'operating picture header was missing' + assert_contains "$out" 'A matched claim' 'catalog was missing' + assert_contains "$out" 'BODY-OF-matched' 'matched note was missing' + + core_pos=$(printf '%s\n' "$out" | grep -n '^core:' | cut -d: -f1) + now_pos=$(printf '%s\n' "$out" | grep -n '^operating picture:' | cut -d: -f1) + cat_pos=$(printf '%s\n' "$out" | grep -n '^catalog ' | cut -d: -f1) + + [ -n "$core_pos" ] && [ -n "$now_pos" ] && [ -n "$cat_pos" ] \ + || fail "could not find positions in output: $out" + [ "$core_pos" -lt "$now_pos" ] \ + || fail "expected core ($core_pos) before operating picture ($now_pos)" + [ "$now_pos" -lt "$cat_pos" ] \ + || fail "expected operating picture ($now_pos) before catalog ($cat_pos)" + + [ "$(accounting_field "$out" status)" = within-budget ] \ + || fail "expected within-budget status: $out" + + # Test with updated: in front matter as well + write_now "$home" 2026-08-20 'UPDATED-PINS-TEXT' updated + out=$(FM_MEMORY_TODAY_OVERRIDE=2026-08-20 compile "$home") + assert_contains "$out" 'UPDATED-PINS-TEXT' 'now.md with updated: key was not injected' + + pass 'a now.md dated today appears in the bundle, clearly delimited, ahead of the catalog' +} + +test_operating_picture_dated_other_day_is_dropped_and_reports_why() { + local home out + home=$(new_home now-stale) + printf '# core\n\nSTANDING-CORE-TEXT\n' > "$home/data/memory/core.md" + write_now "$home" 2026-08-19 'YESTERDAYS-CEILINGS' + write_note "$home" matched 'A matched claim' 'healthlog' 2026-08-18 + printf -- '- healthlog [no-mistakes] - a project\n' > "$home/data/projects.md" + + out=$(FM_MEMORY_TODAY_OVERRIDE=2026-08-20 compile "$home") + + assert_contains "$out" 'STANDING-CORE-TEXT' 'core.md body was not injected' + assert_not_contains "$out" 'YESTERDAYS-CEILINGS' 'stale now.md body was injected' + assert_not_contains "$out" 'operating picture:' 'operating picture section was present for stale file' + assert_contains "$out" 'MEMORY_NOTICE: data/memory/now.md is dated 2026-08-19' \ + 'bundle did not explain why stale now.md was dropped' + assert_contains "$out" 'not today' 'stale notice did not mention today date' + assert_contains "$out" 'A matched claim' 'catalog was dropped when now.md was stale' + assert_contains "$out" 'BODY-OF-matched' 'matched note was dropped when now.md was stale' + + pass 'a now.md dated any other day does not appear in the bundle, and the bundle says why' +} + +test_operating_picture_with_no_date_is_dropped_and_reports_why() { + local home out + home=$(new_home now-nodate) + printf '# core\n\nSTANDING-CORE-TEXT\n' > "$home/data/memory/core.md" + printf '# Operating picture\n\nUNDATED-CEILINGS\n' > "$home/data/memory/now.md" + + out=$(FM_MEMORY_TODAY_OVERRIDE=2026-08-20 compile "$home" --no-auto-context) + + assert_not_contains "$out" 'UNDATED-CEILINGS' 'undated now.md body was injected' + assert_not_contains "$out" 'operating picture:' 'operating picture section was present for undated file' + assert_contains "$out" 'MEMORY_NOTICE: data/memory/now.md has no date in front matter' \ + 'bundle did not explain why undated now.md was dropped' + + pass 'a now.md with no date in front matter is dropped and reported' +} + +test_absent_now_md_produces_byte_identical_output_with_no_notice() { + local home out + home=$(new_home now-absent) + printf '# core\n\nSTANDING-CORE-TEXT\n' > "$home/data/memory/core.md" + write_note "$home" matched 'A matched claim' 'healthlog' 2026-08-18 + + out=$(FM_MEMORY_TODAY_OVERRIDE=2026-08-20 compile "$home" --no-auto-context) + + assert_not_contains "$out" 'now.md' 'absent now.md was mentioned in output' + assert_not_contains "$out" 'operating picture' 'absent now.md produced operating picture section' + + pass 'no now.md produces output with no notice or operating picture section' +} + +test_budget_cap_precedence_with_operating_picture() { + local home out full core_tokens now_tokens catalog_tokens small_tokens budget + home=$(new_home budget-now 1000000) + printf '# core\n\nSTANDING-CORE-TEXT\n' > "$home/data/memory/core.md" + write_now "$home" 2026-08-20 'NOW-PINS-TEXT' + write_note "$home" big 'Big claim' 'bigtrig' 2026-08-18 200 + write_note "$home" small 'Small claim' 'smalltrig' 2026-08-17 1 + + full=$(FM_MEMORY_TODAY_OVERRIDE=2026-08-20 compile "$home" --no-auto-context --context 'bigtrig smalltrig') + [ "$(accounting_field "$full" hot_notes)" = 2 ] \ + || fail "an unconstrained compile did not take both matched notes: $full" + core_tokens=$(accounting_field "$full" core) + catalog_tokens=$(accounting_field "$full" catalog) + small_tokens=$(accounting_field "$(FM_MEMORY_TODAY_OVERRIDE=2026-08-20 compile "$home" --no-auto-context --context smalltrig)" hot_notes_tokens) + now_tokens=$(accounting_field "$full" now) + + [ -n "$now_tokens" ] && [ "$now_tokens" -gt 0 ] \ + || fail "now_tokens was not accounted: $full" + + # Case 1: Budget fits core + now + catalog + small note. Big note dropped. + budget=$((core_tokens + now_tokens + catalog_tokens + small_tokens)) + printf '%s\n' "$budget" > "$home/config/startup-memory-budget" + out=$(FM_MEMORY_TODAY_OVERRIDE=2026-08-20 compile "$home" --no-auto-context --context 'bigtrig smalltrig') + assert_contains "$out" 'STANDING-CORE-TEXT' 'core was dropped' + assert_contains "$out" 'NOW-PINS-TEXT' 'now was dropped' + assert_contains "$out" 'Big claim' 'catalog was dropped' + assert_contains "$out" 'BODY-OF-small' 'small note was dropped' + assert_not_contains "$out" 'BODY-OF-big' 'big note was injected over budget' + [ "$(accounting_field "$out" status)" = capped ] \ + || fail "dropping a note was not reported as capped: $out" + + # Case 2: Budget fits core + now, but NOT catalog. Catalog and notes dropped, now kept. + printf '%s\n' "$((core_tokens + now_tokens))" > "$home/config/startup-memory-budget" + out=$(FM_MEMORY_TODAY_OVERRIDE=2026-08-20 compile "$home" --no-auto-context --context 'bigtrig smalltrig') + assert_contains "$out" 'STANDING-CORE-TEXT' 'core was dropped' + assert_contains "$out" 'NOW-PINS-TEXT' 'now was dropped when catalog had no room' + assert_not_contains "$out" 'Big claim' 'catalog was injected without room' + assert_contains "$out" 'MEMORY_BUDGET_WARNING:' 'dropping catalog was silent' + + # Case 3: Budget fits core, but NOT now. Now, catalog, and notes dropped. Core alone kept. + printf '%s\n' "$core_tokens" > "$home/config/startup-memory-budget" + out=$(FM_MEMORY_TODAY_OVERRIDE=2026-08-20 compile "$home" --no-auto-context --context 'bigtrig smalltrig') + assert_contains "$out" 'STANDING-CORE-TEXT' 'core was dropped' + assert_not_contains "$out" 'NOW-PINS-TEXT' 'now was injected without room' + assert_not_contains "$out" 'Big claim' 'catalog was injected without room' + assert_contains "$out" 'MEMORY_BUDGET_WARNING:' 'dropping now was silent' + [ "$(accounting_field "$out" status)" = capped ] \ + || fail "dropping now was not reported as capped: $out" + + # Case 4: Not even room for core. Core alone printed with loud over-budget warning. + printf '1\n' > "$home/config/startup-memory-budget" + out=$(FM_MEMORY_TODAY_OVERRIDE=2026-08-20 compile "$home" --no-auto-context --context 'bigtrig smalltrig') + assert_contains "$out" 'STANDING-CORE-TEXT' 'over-budget core was dropped' + assert_contains "$out" 'MEMORY_BUDGET_WARNING: the core alone is' \ + 'over-budget core did not emit warning' + assert_not_contains "$out" 'NOW-PINS-TEXT' 'now was injected with over-budget core' + [ "$(accounting_field "$out" status)" = over-budget ] \ + || fail "over-budget core did not report over-budget: $out" + + pass 'the budget cap drops notes first, then catalog, then operating picture, and never core' +} + +test_symlinked_now_md_is_guarded() { + local home out outside + home=$(new_home now-symlink) + printf 'CORE\n' > "$home/data/memory/core.md" + outside="$TMP_ROOT/outside-now.md" + printf -- '---\ndate: 2026-08-20\n---\nOUTSIDE-NOW-SECRET\n' > "$outside" + ln -s "$outside" "$home/data/memory/now.md" + + out=$(FM_MEMORY_TODAY_OVERRIDE=2026-08-20 compile "$home" --no-auto-context) + assert_not_contains "$out" 'OUTSIDE-NOW-SECRET' 'symlinked now.md was read' + assert_contains "$out" 'MEMORY_NOTICE: data/memory/now.md is a symlink' \ + 'symlinked now.md did not emit notice' + + pass 'a symlinked now.md is skipped rather than followed' +} + test_bundle_is_core_catalog_and_matched_notes_only test_core_falls_back_to_captain_then_reports_absence test_core_shadowing_captain_emits_notice @@ -579,5 +761,11 @@ test_migration_freezes_and_archives_before_removing_the_original test_migration_dry_run_and_keep_learnings_write_nothing_away test_migration_refuses_to_remove_history_it_could_not_archive test_migration_on_a_home_with_no_learnings_still_builds_the_layout +test_operating_picture_dated_today_is_injected_ahead_of_catalog +test_operating_picture_dated_other_day_is_dropped_and_reports_why +test_operating_picture_with_no_date_is_dropped_and_reports_why +test_absent_now_md_produces_byte_identical_output_with_no_notice +test_budget_cap_precedence_with_operating_picture +test_symlinked_now_md_is_guarded echo '# all fm-memory-compile tests passed' From f0a1a6d3db84079af3c8784a295230e2b24394b5 Mon Sep 17 00:00:00 2001 From: PP <121104417+BohnBawerick@users.noreply.github.com> Date: Thu, 20 Aug 2026 12:36:47 +0800 Subject: [PATCH 2/5] no-mistakes(review): Read now.md home-level, pin local date, keep catalog --- bin/fm-memory-compile.sh | 114 +++++++++++++++++----------- docs/configuration.md | 3 +- tests/fm-memory-compile.test.sh | 129 +++++++++++++++++++++++++++++++- 3 files changed, 198 insertions(+), 48 deletions(-) diff --git a/bin/fm-memory-compile.sh b/bin/fm-memory-compile.sh index d91c58d7bf..e71820a4ff 100755 --- a/bin/fm-memory-compile.sh +++ b/bin/fm-memory-compile.sh @@ -31,7 +31,10 @@ # now.md the dated operating picture. Carries this-shift pins and # ceilings with a front matter date. Injected only when dated # today; a stale file is dropped and reported, while absence is -# silent. +# silent. It is read from data/memory/now.md in the home, never +# from the active generation, because it is perishable shift +# state rather than versioned knowledge: a published generation +# must not freeze it, and publishing must not discard it. # notes/*.md atomic notes, one claim each. # catalog.md the regenerable index, one line per note: claim title, file # name under notes/, first triggers, and updated date. @@ -52,6 +55,11 @@ # OPERATING PICTURE FORMAT. now.md opens with a YAML-style front matter block # delimited by a bare `---` on line 1 and the next bare `---`: # date: ISO date (YYYY-MM-DD), matched against today's date +# updated: the same ISO date under the note format's key name, read only as a +# fallback when no `date:` key is present, so `date:` always wins +# Today is the local host date (`date +%Y-%m-%d`), one clock rather than two, so +# the window a file stays valid for is never wider than a day. +# FM_MEMORY_TODAY_OVERRIDE replaces that date and exists so a test can pin it. # An undated or stale now.md is never injected. # # SELECTION AND CAP. config/startup-memory-budget owns the cap and @@ -65,6 +73,9 @@ # no operating picture, no catalog, and no notes; # - the operating picture (now.md) is kept ahead of the catalog when dated # today, because a stale ceiling is the failure this tier exists to prevent; +# - an operating picture that does not fit beside the core is dropped with a +# loud MEMORY_BUDGET_WARNING and the fill continues, so the newest tier can +# never blank the catalog the way one oversized file otherwise would; # - the catalog is kept ahead of every hot note, because it is the thing that # tells the next turn a note exists at all; # - hot notes are added newest-updated first, and one that does not fit is @@ -532,7 +543,7 @@ parse_now_date() { gsub(/^["\047]+|["\047]+$/, "", s) return s } - BEGIN { fm = 0; date = "" } + BEGIN { fm = 0; date = ""; fallback = ""; have_date = 0 } FNR == 1 { if ($0 ~ /^---[[:space:]]*$/) { fm = 1; next } exit @@ -542,38 +553,41 @@ parse_now_date() { if (match($0, /^[A-Za-z_][A-Za-z0-9_-]*:[[:space:]]*/)) { key = tolower(substr($0, 1, index($0, ":") - 1)) val = clean(substr($0, RLENGTH + 1)) - if (key == "date" || key == "updated") { + if (key == "date") { date = val + have_date = 1 + } else if (key == "updated" && !have_date) { + fallback = val } } } END { - print date + print have_date ? date : fallback } ' "$path" } +# Today is settled once, from the local host clock the captain reads, so the +# validity window is exactly one day everywhere rather than widening wherever +# local time and UTC disagree. +TODAY_DATE="${FM_MEMORY_TODAY_OVERRIDE:-}" +if [ -z "$TODAY_DATE" ]; then + TODAY_DATE=$(date +%Y-%m-%d 2>/dev/null || true) +fi + is_today_date() { - local d=$1 today_utc today_local + local d=$1 [ -n "$d" ] || return 1 - if [ -n "${FM_MEMORY_TODAY_OVERRIDE:-${FM_MEMORY_NOW_DATE:-}}" ]; then - [ "$d" = "${FM_MEMORY_TODAY_OVERRIDE:-${FM_MEMORY_NOW_DATE:-}}" ] && return 0 - return 1 - fi - today_utc=$(date -u +%Y-%m-%d 2>/dev/null || true) - today_local=$(date +%Y-%m-%d 2>/dev/null || true) - if [ "$d" = "$today_utc" ] || [ "$d" = "$today_local" ]; then - return 0 - fi - return 1 + [ -n "$TODAY_DATE" ] || return 1 + [ "$d" = "$TODAY_DATE" ] } get_today_display() { - if [ -n "${FM_MEMORY_TODAY_OVERRIDE:-${FM_MEMORY_NOW_DATE:-}}" ]; then - printf '%s' "${FM_MEMORY_TODAY_OVERRIDE:-${FM_MEMORY_NOW_DATE:-}}" + if [ -n "$TODAY_DATE" ]; then + printf '%s' "$TODAY_DATE" return fi - date +%Y-%m-%d 2>/dev/null || date -u +%Y-%m-%d 2>/dev/null || printf 'today' + printf 'today' } if [ "$NOTES_DIR_SYMLINK" -eq 1 ]; then @@ -606,20 +620,28 @@ else NOTICES+=("MEMORY_NOTICE: no core memory - both $REL_LABEL/core.md and data/captain.md are ABSENT, so this home is running on the firstmate repo built-in defaults.") fi -if [ "$MEMORY_DIR_OK" -eq 1 ] && [ -L "$MEMORY/now.md" ]; then - NOTICES+=("MEMORY_NOTICE: $REL_LABEL/now.md is a symlink, so nothing was read through it. Replace the symlink with a real file, or remove it.") -elif [ "$MEMORY_DIR_OK" -eq 1 ] && [ -f "$MEMORY/now.md" ]; then - NOW_DATE=$(parse_now_date "$MEMORY/now.md") +# The operating picture is home-level, exactly like the drop tray: it is read +# from data/memory/now.md whatever generation HEAD points at, so a generation +# home sees this shift's pins and a publish never freezes or discards them. +NOW_ROOT_OK=1 +if [ -L "$DATA/memory" ] || [ ! -d "$DATA/memory" ]; then + NOW_ROOT_OK=0 +fi + +if [ "$NOW_ROOT_OK" -eq 1 ] && [ -L "$DATA/memory/now.md" ]; then + NOTICES+=("MEMORY_NOTICE: data/memory/now.md is a symlink, so nothing was read through it. Replace the symlink with a real file, or remove it.") +elif [ "$NOW_ROOT_OK" -eq 1 ] && [ -f "$DATA/memory/now.md" ]; then + NOW_DATE=$(parse_now_date "$DATA/memory/now.md") if is_today_date "$NOW_DATE"; then NOW_VALID=1 - NOW_PATH="$MEMORY/now.md" - NOW_LABEL="$REL_LABEL/now.md" + NOW_PATH="$DATA/memory/now.md" + NOW_LABEL="data/memory/now.md" NOW_TOKENS=$(tokens_of_file "$NOW_PATH") elif [ -n "$NOW_DATE" ]; then TODAY_DISP=$(get_today_display) - NOTICES+=("MEMORY_NOTICE: $REL_LABEL/now.md is dated $NOW_DATE (not today, $TODAY_DISP) and is NOT injected. Update it with this shift's pins and ceilings, or remove it.") + NOTICES+=("MEMORY_NOTICE: data/memory/now.md is dated $NOW_DATE (not today, $TODAY_DISP) and is NOT injected. Update it with this shift's pins and ceilings, or remove it.") else - NOTICES+=("MEMORY_NOTICE: $REL_LABEL/now.md has no date in front matter and is NOT injected. Add a date (date: YYYY-MM-DD), or remove it.") + NOTICES+=("MEMORY_NOTICE: data/memory/now.md has no date in front matter and is NOT injected. Add a date (date: YYYY-MM-DD), or remove it.") fi fi @@ -642,18 +664,23 @@ fi TOTAL=$CORE_TOKENS CORE_OVER=0 NOW_KEPT=0 +NOW_OVER=0 CATALOG_KEPT=1 if ! fm_startup_memory_decimal_le "$TOTAL" "$BUDGET"; then CORE_OVER=1 CATALOG_KEPT=0 -elif [ "$NOW_VALID" -eq 1 ] && ! fm_startup_memory_decimal_le "$((TOTAL + NOW_TOKENS))" "$BUDGET"; then - NOW_KEPT=0 - CATALOG_KEPT=0 else if [ "$NOW_VALID" -eq 1 ]; then - NOW_KEPT=1 - TOTAL=$((TOTAL + NOW_TOKENS)) + if fm_startup_memory_decimal_le "$((TOTAL + NOW_TOKENS))" "$BUDGET"; then + NOW_KEPT=1 + TOTAL=$((TOTAL + NOW_TOKENS)) + else + # One oversized operating picture is dropped on its own and the fill goes + # on, so it can never take the catalog - the only thing that tells the + # next turn a note exists - down with it. + NOW_OVER=1 + fi fi if ! fm_startup_memory_decimal_le "$((TOTAL + CATALOG_TOKENS))" "$BUDGET"; then CATALOG_KEPT=0 @@ -711,19 +738,20 @@ else fi if [ "$CORE_OVER" -eq 1 ]; then - printf '\nMEMORY_BUDGET_WARNING: the core alone is %s estimated tokens against a %s budget. It was printed in full and NOTHING else was: no catalog, no notes. Trim the core (%s/core.md, or data/captain.md when no core.md exists) or raise config/startup-memory-budget.\n' \ - "$CORE_TOKENS" "$BUDGET" "$REL_LABEL" -elif [ "$NOW_VALID" -eq 1 ] && [ "$NOW_KEPT" -eq 0 ]; then - printf '\nMEMORY_BUDGET_WARNING: the core plus operating picture is %s estimated tokens against a %s budget, so the operating picture, catalog, and every note were dropped. Trim the core or operating picture or raise config/startup-memory-budget.\n' \ - "$((CORE_TOKENS + NOW_TOKENS))" "$BUDGET" + if [ "$NOW_VALID" -eq 1 ]; then + printf '\nMEMORY_BUDGET_WARNING: the core alone is %s estimated tokens against a %s budget. It was printed in full and NOTHING else was: no operating picture, no catalog, no notes. Trim the core (%s/core.md, or data/captain.md when no core.md exists) or raise config/startup-memory-budget.\n' \ + "$CORE_TOKENS" "$BUDGET" "$REL_LABEL" + else + printf '\nMEMORY_BUDGET_WARNING: the core alone is %s estimated tokens against a %s budget. It was printed in full and NOTHING else was: no catalog, no notes. Trim the core (%s/core.md, or data/captain.md when no core.md exists) or raise config/startup-memory-budget.\n' \ + "$CORE_TOKENS" "$BUDGET" "$REL_LABEL" + fi else if [ "$NOW_KEPT" -eq 1 ]; then printf '\noperating picture: %s\n%s\n' "$NOW_LABEL" "$RULE" - if [ -s "$NOW_PATH" ]; then - cat "$NOW_PATH" - else - printf '(present, empty)\n' - fi + cat "$NOW_PATH" + elif [ "$NOW_OVER" -eq 1 ]; then + printf '\nMEMORY_BUDGET_WARNING: the core plus operating picture is %s estimated tokens against a %s budget, so the operating picture was dropped and this session is running without today'"'"'s pins and ceilings. The catalog and notes below were filled from what remains. Trim %s or raise config/startup-memory-budget.\n' \ + "$((CORE_TOKENS + NOW_TOKENS))" "$BUDGET" "$NOW_LABEL" fi if [ "$CATALOG_KEPT" -eq 0 ]; then @@ -763,9 +791,7 @@ done STATUS=within-budget if [ "$CORE_OVER" -eq 1 ]; then STATUS=over-budget -elif [ "$NOW_VALID" -eq 1 ] && [ "$NOW_KEPT" -eq 0 ]; then - STATUS=capped -elif [ "$CATALOG_KEPT" -eq 0 ] || [ "$HOT_DROPPED" -gt 0 ]; then +elif [ "$NOW_OVER" -eq 1 ] || [ "$CATALOG_KEPT" -eq 0 ] || [ "$HOT_DROPPED" -gt 0 ]; then STATUS=capped fi diff --git a/docs/configuration.md b/docs/configuration.md index e0b646ca4c..b1a5a9a826 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -165,7 +165,8 @@ Shared captain preferences that apply across secondmate domains live only in the Fleet-local operational facts and gotchas live locally as one atomic note per claim under `data/memory/notes/`, alongside an optional standing constitution in `data/memory/core.md`, the dated operating picture `data/memory/now.md`, the regenerable index `data/memory/catalog.md`, and the never-injected candidate tray `data/memory/drop/`. The standing constitution `data/memory/core.md` holds standing preferences, authority boundaries, and core guidelines with a 1,500-2,500 estimated-token target. -The dated operating picture `data/memory/now.md` holds perishable shift pins and ceilings with a front matter date, and is injected only when dated today. +The dated operating picture `data/memory/now.md` holds perishable shift pins and ceilings with a front matter date, and is injected only when dated today, matched against the local host date. +It is read from the home at `data/memory/now.md` even when `data/memory/HEAD` points at a generation, exactly like the drop tray, so publishing a generation neither freezes nor discards the current shift's picture. The whole directory is gitignored and is created by `bin/fm-memory-migrate.sh`, which also splits a home's legacy `data/learnings.md` into notes and freezes the original under `data/memory/raw/` before archiving it to `data/memory-archive.md`. Completed tasks deposit candidate findings and tactical gotchas into `data/memory/drop/<task-id>.md` through `bin/fm-memory-drop.sh`. Generations of memory live under `data/memory/gen/<N>/` and are activated atomically by pointing `data/memory/HEAD` at the active generation. diff --git a/tests/fm-memory-compile.test.sh b/tests/fm-memory-compile.test.sh index a254887154..7574771cd2 100755 --- a/tests/fm-memory-compile.test.sh +++ b/tests/fm-memory-compile.test.sh @@ -25,15 +25,20 @@ new_home() { printf '%s\n' "$home" } -# write_now <home> <date> [content] [key] +# write_now <home> <date> [content] [key] [padding-lines] write_now() { - local home=$1 date=$2 content=${3:-"STANDING-NOW-TEXT"} key=${4:-"date"} + local home=$1 date=$2 content=${3:-"STANDING-NOW-TEXT"} key=${4:-"date"} pad=${5:-0} i mkdir -p "$home/data/memory" { printf -- '---\n' printf '%s: %s\n' "$key" "$date" printf -- '---\n\n' printf '%s\n' "$content" + i=0 + while [ "$i" -lt "$pad" ]; do + printf 'pinned ceiling %s aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\n' "$i" + i=$((i + 1)) + done } > "$home/data/memory/now.md" } @@ -661,6 +666,8 @@ test_absent_now_md_produces_byte_identical_output_with_no_notice() { assert_not_contains "$out" 'now.md' 'absent now.md was mentioned in output' assert_not_contains "$out" 'operating picture' 'absent now.md produced operating picture section' + [ -z "$(accounting_field "$out" now)" ] \ + || fail "absent now.md added a now= field to MEMORY_ACCOUNTING: $out" pass 'no now.md produces output with no notice or operating picture section' } @@ -704,13 +711,15 @@ test_budget_cap_precedence_with_operating_picture() { assert_not_contains "$out" 'Big claim' 'catalog was injected without room' assert_contains "$out" 'MEMORY_BUDGET_WARNING:' 'dropping catalog was silent' - # Case 3: Budget fits core, but NOT now. Now, catalog, and notes dropped. Core alone kept. + # Case 3: Budget fits core and nothing else. Now, catalog, and notes dropped. printf '%s\n' "$core_tokens" > "$home/config/startup-memory-budget" out=$(FM_MEMORY_TODAY_OVERRIDE=2026-08-20 compile "$home" --no-auto-context --context 'bigtrig smalltrig') assert_contains "$out" 'STANDING-CORE-TEXT' 'core was dropped' assert_not_contains "$out" 'NOW-PINS-TEXT' 'now was injected without room' assert_not_contains "$out" 'Big claim' 'catalog was injected without room' assert_contains "$out" 'MEMORY_BUDGET_WARNING:' 'dropping now was silent' + [ "$(accounting_field "$out" now)" = 0 ] \ + || fail "a dropped operating picture was still accounted: $out" [ "$(accounting_field "$out" status)" = capped ] \ || fail "dropping now was not reported as capped: $out" @@ -727,6 +736,115 @@ test_budget_cap_precedence_with_operating_picture() { pass 'the budget cap drops notes first, then catalog, then operating picture, and never core' } +test_oversized_operating_picture_is_dropped_but_the_catalog_survives() { + local home full core_tokens catalog_tokens now_tokens out + home=$(new_home now-oversized 1000000) + printf '# core\n\nSTANDING-CORE-TEXT\n' > "$home/data/memory/core.md" + write_now "$home" 2026-08-20 'NOW-PINS-TEXT' date 400 + write_note "$home" small 'Small claim' 'smalltrig' 2026-08-17 1 + + full=$(FM_MEMORY_TODAY_OVERRIDE=2026-08-20 compile "$home" --no-auto-context --context smalltrig) + core_tokens=$(accounting_field "$full" core) + catalog_tokens=$(accounting_field "$full" catalog) + now_tokens=$(accounting_field "$full" now) + [ "$now_tokens" -gt "$catalog_tokens" ] \ + || fail "the oversized operating picture was not larger than the catalog: $full" + + # Room for core plus catalog, but not for core plus operating picture. + printf '%s\n' "$((core_tokens + catalog_tokens))" > "$home/config/startup-memory-budget" + out=$(FM_MEMORY_TODAY_OVERRIDE=2026-08-20 compile "$home" --no-auto-context --context smalltrig) + + assert_contains "$out" 'STANDING-CORE-TEXT' 'core was dropped' + assert_not_contains "$out" 'NOW-PINS-TEXT' 'the oversized operating picture was injected' + assert_contains "$out" 'MEMORY_BUDGET_WARNING: the core plus operating picture is' \ + 'dropping the oversized operating picture was silent' + assert_contains "$out" 'Small claim' 'the catalog went down with the operating picture' + [ "$(accounting_field "$out" catalog)" = "$catalog_tokens" ] \ + || fail "the catalog was not accounted after the operating picture was dropped: $out" + [ "$(accounting_field "$out" now)" = 0 ] \ + || fail "a dropped operating picture was still accounted: $out" + [ "$(accounting_field "$out" status)" = capped ] \ + || fail "dropping the operating picture was not reported as capped: $out" + + pass 'an operating picture too large to fit is dropped alone and never takes the catalog with it' +} + +test_operating_picture_is_read_from_the_home_in_a_generation_home() { + local home out + home=$(new_home now-generation) + mkdir -p "$home/data/memory/gen/1/notes" + printf '# core\n\nGENERATION-CORE-TEXT\n' > "$home/data/memory/gen/1/core.md" + printf 'gen/1\n' > "$home/data/memory/HEAD" + write_now "$home" 2026-08-20 'HOME-LEVEL-PINS' + + out=$(FM_MEMORY_TODAY_OVERRIDE=2026-08-20 compile "$home" --no-auto-context) + + assert_contains "$out" 'COMPILED WORKING MEMORY (data/memory/gen/1)' \ + 'the generation was not the compiled memory directory' + assert_contains "$out" 'GENERATION-CORE-TEXT' 'the generation core was not injected' + assert_contains "$out" 'operating picture: data/memory/now.md' \ + 'the home-level operating picture was not injected in a generation home' + assert_contains "$out" 'HOME-LEVEL-PINS' 'the home-level operating picture body was missing' + + pass 'a generation home still reads the home-level data/memory/now.md' +} + +test_date_key_wins_over_updated_key_in_the_operating_picture() { + local home out + home=$(new_home now-date-precedence) + printf '# core\n\nSTANDING-CORE-TEXT\n' > "$home/data/memory/core.md" + { + printf -- '---\n' + printf 'date: 2026-08-20\n' + printf 'updated: 2020-01-01\n' + printf -- '---\n\n' + printf 'DATE-KEY-PINS\n' + } > "$home/data/memory/now.md" + + out=$(FM_MEMORY_TODAY_OVERRIDE=2026-08-20 compile "$home" --no-auto-context) + + assert_contains "$out" 'DATE-KEY-PINS' 'an updated: key overrode the authoritative date: key' + assert_not_contains "$out" 'MEMORY_NOTICE: data/memory/now.md is dated' \ + 'a now.md with an authoritative date: key was reported stale' + + pass 'the date: key is authoritative over updated: in the operating picture' +} + +test_stale_operating_picture_notice_names_both_dates() { + local home out + home=$(new_home now-stale-dates) + printf '# core\n\nSTANDING-CORE-TEXT\n' > "$home/data/memory/core.md" + write_now "$home" 2026-08-19 'YESTERDAYS-CEILINGS' + + out=$(FM_MEMORY_TODAY_OVERRIDE=2026-08-20 compile "$home" --no-auto-context) + + assert_contains "$out" 'MEMORY_NOTICE: data/memory/now.md is dated 2026-08-19 (not today, 2026-08-20)' \ + 'the stale notice did not name both the file date and today' + + pass 'the stale operating picture notice names the date the file carries and today' +} + +test_shipped_example_operating_picture_compiles() { + local home example_date out + home=$(new_home now-example) + printf '# core\n\nSTANDING-CORE-TEXT\n' > "$home/data/memory/core.md" + cp "$ROOT/docs/examples/now.md" "$home/data/memory/now.md" + + example_date=$(awk 'NR > 1 && /^---[[:space:]]*$/ { exit } tolower($1) == "date:" { print $2; exit }' \ + "$ROOT/docs/examples/now.md") + [ -n "$example_date" ] || fail 'docs/examples/now.md carries no date: key in its front matter' + + out=$(FM_MEMORY_TODAY_OVERRIDE="$example_date" compile "$home" --no-auto-context) + + assert_contains "$out" 'operating picture: data/memory/now.md' \ + 'the shipped example template was not injected on the date it carries' + assert_contains "$out" '# Operating picture' 'the example body was missing from the bundle' + [ "$(accounting_field "$out" now)" -gt 0 ] \ + || fail "the example template was not accounted: $out" + + pass 'the shipped docs/examples/now.md template compiles as a valid operating picture' +} + test_symlinked_now_md_is_guarded() { local home out outside home=$(new_home now-symlink) @@ -766,6 +884,11 @@ test_operating_picture_dated_other_day_is_dropped_and_reports_why test_operating_picture_with_no_date_is_dropped_and_reports_why test_absent_now_md_produces_byte_identical_output_with_no_notice test_budget_cap_precedence_with_operating_picture +test_oversized_operating_picture_is_dropped_but_the_catalog_survives +test_operating_picture_is_read_from_the_home_in_a_generation_home +test_date_key_wins_over_updated_key_in_the_operating_picture +test_stale_operating_picture_notice_names_both_dates +test_shipped_example_operating_picture_compiles test_symlinked_now_md_is_guarded echo '# all fm-memory-compile tests passed' From 7f9284d208b1db8009d32717e73a9f73557d0c3f Mon Sep 17 00:00:00 2001 From: PP <121104417+BohnBawerick@users.noreply.github.com> Date: Thu, 20 Aug 2026 12:46:47 +0800 Subject: [PATCH 3/5] no-mistakes(review): Scope now.md to named memory dir, dedupe warnings --- bin/fm-memory-compile.sh | 67 +++++++++++++++++++-------------- docs/configuration.md | 3 +- tests/fm-memory-compile.test.sh | 44 ++++++++++++++++++++++ 3 files changed, 85 insertions(+), 29 deletions(-) diff --git a/bin/fm-memory-compile.sh b/bin/fm-memory-compile.sh index e71820a4ff..60bfb6dfbf 100755 --- a/bin/fm-memory-compile.sh +++ b/bin/fm-memory-compile.sh @@ -31,10 +31,13 @@ # now.md the dated operating picture. Carries this-shift pins and # ceilings with a front matter date. Injected only when dated # today; a stale file is dropped and reported, while absence is -# silent. It is read from data/memory/now.md in the home, never -# from the active generation, because it is perishable shift -# state rather than versioned knowledge: a published generation -# must not freeze it, and publishing must not discard it. +# silent. It is read from data/memory/now.md in the home rather +# than from the generation HEAD names, because it is perishable +# shift state rather than versioned knowledge: a published +# generation must not freeze it, and publishing must not discard +# it. --memory-dir and --gen move it to the named directory +# along with everything else, so verifying one generation stays +# a function of that generation alone. # notes/*.md atomic notes, one claim each. # catalog.md the regenerable index, one line per note: claim title, file # name under notes/, first triggers, and updated date. @@ -620,28 +623,36 @@ else NOTICES+=("MEMORY_NOTICE: no core memory - both $REL_LABEL/core.md and data/captain.md are ABSENT, so this home is running on the firstmate repo built-in defaults.") fi -# The operating picture is home-level, exactly like the drop tray: it is read -# from data/memory/now.md whatever generation HEAD points at, so a generation -# home sees this shift's pins and a publish never freezes or discards them. -NOW_ROOT_OK=1 -if [ -L "$DATA/memory" ] || [ ! -d "$DATA/memory" ]; then - NOW_ROOT_OK=0 +# The operating picture follows the directory the caller asked for, and only +# falls back to the home-level data/memory/now.md when the caller asked for +# nothing. A named directory therefore compiles from itself alone, so +# bin/fm-memory-verify.sh's budget gate on a proposed generation stays a +# function of that generation and never of today's perishable shift file. +NOW_DIR="$DATA/memory" +NOW_REL="data/memory" +NOW_DIR_OK=1 +if [ -n "$EXPLICIT_MEMORY_DIR" ]; then + NOW_DIR="$MEMORY" + NOW_REL="$REL_LABEL" + NOW_DIR_OK=$MEMORY_DIR_OK +elif [ -L "$DATA/memory" ] || [ ! -d "$DATA/memory" ]; then + NOW_DIR_OK=0 fi -if [ "$NOW_ROOT_OK" -eq 1 ] && [ -L "$DATA/memory/now.md" ]; then - NOTICES+=("MEMORY_NOTICE: data/memory/now.md is a symlink, so nothing was read through it. Replace the symlink with a real file, or remove it.") -elif [ "$NOW_ROOT_OK" -eq 1 ] && [ -f "$DATA/memory/now.md" ]; then - NOW_DATE=$(parse_now_date "$DATA/memory/now.md") +if [ "$NOW_DIR_OK" -eq 1 ] && [ -L "$NOW_DIR/now.md" ]; then + NOTICES+=("MEMORY_NOTICE: $NOW_REL/now.md is a symlink, so nothing was read through it. Replace the symlink with a real file, or remove it.") +elif [ "$NOW_DIR_OK" -eq 1 ] && [ -f "$NOW_DIR/now.md" ]; then + NOW_DATE=$(parse_now_date "$NOW_DIR/now.md") if is_today_date "$NOW_DATE"; then NOW_VALID=1 - NOW_PATH="$DATA/memory/now.md" - NOW_LABEL="data/memory/now.md" + NOW_PATH="$NOW_DIR/now.md" + NOW_LABEL="$NOW_REL/now.md" NOW_TOKENS=$(tokens_of_file "$NOW_PATH") elif [ -n "$NOW_DATE" ]; then TODAY_DISP=$(get_today_display) - NOTICES+=("MEMORY_NOTICE: data/memory/now.md is dated $NOW_DATE (not today, $TODAY_DISP) and is NOT injected. Update it with this shift's pins and ceilings, or remove it.") + NOTICES+=("MEMORY_NOTICE: $NOW_REL/now.md is dated $NOW_DATE (not today, $TODAY_DISP) and is NOT injected. Update it with this shift's pins and ceilings, or remove it.") else - NOTICES+=("MEMORY_NOTICE: data/memory/now.md has no date in front matter and is NOT injected. Add a date (date: YYYY-MM-DD), or remove it.") + NOTICES+=("MEMORY_NOTICE: $NOW_REL/now.md has no date in front matter and is NOT injected. Add a date (date: YYYY-MM-DD), or remove it.") fi fi @@ -738,13 +749,12 @@ else fi if [ "$CORE_OVER" -eq 1 ]; then + DROPPED_LIST='no catalog, no notes' if [ "$NOW_VALID" -eq 1 ]; then - printf '\nMEMORY_BUDGET_WARNING: the core alone is %s estimated tokens against a %s budget. It was printed in full and NOTHING else was: no operating picture, no catalog, no notes. Trim the core (%s/core.md, or data/captain.md when no core.md exists) or raise config/startup-memory-budget.\n' \ - "$CORE_TOKENS" "$BUDGET" "$REL_LABEL" - else - printf '\nMEMORY_BUDGET_WARNING: the core alone is %s estimated tokens against a %s budget. It was printed in full and NOTHING else was: no catalog, no notes. Trim the core (%s/core.md, or data/captain.md when no core.md exists) or raise config/startup-memory-budget.\n' \ - "$CORE_TOKENS" "$BUDGET" "$REL_LABEL" + DROPPED_LIST='no operating picture, no catalog, no notes' fi + printf '\nMEMORY_BUDGET_WARNING: the core alone is %s estimated tokens against a %s budget. It was printed in full and NOTHING else was: %s. Trim the core (%s/core.md, or data/captain.md when no core.md exists) or raise config/startup-memory-budget.\n' \ + "$CORE_TOKENS" "$BUDGET" "$DROPPED_LIST" "$REL_LABEL" else if [ "$NOW_KEPT" -eq 1 ]; then printf '\noperating picture: %s\n%s\n' "$NOW_LABEL" "$RULE" @@ -755,13 +765,14 @@ else fi if [ "$CATALOG_KEPT" -eq 0 ]; then + KEPT_LIST='the core plus catalog' + TRIM_LIST='the core' if [ "$NOW_KEPT" -eq 1 ]; then - printf '\nMEMORY_BUDGET_WARNING: the core plus operating picture plus catalog is %s estimated tokens against a %s budget, so the catalog and every note were dropped. Trim the core or operating picture or raise config/startup-memory-budget; until then this session cannot see what notes exist.\n' \ - "$((TOTAL + CATALOG_TOKENS))" "$BUDGET" - else - printf '\nMEMORY_BUDGET_WARNING: the core plus catalog is %s estimated tokens against a %s budget, so the catalog and every note were dropped. Trim the core or raise config/startup-memory-budget; until then this session cannot see what notes exist.\n' \ - "$((CORE_TOKENS + CATALOG_TOKENS))" "$BUDGET" + KEPT_LIST='the core plus operating picture plus catalog' + TRIM_LIST='the core or operating picture' fi + printf '\nMEMORY_BUDGET_WARNING: %s is %s estimated tokens against a %s budget, so the catalog and every note were dropped. Trim %s or raise config/startup-memory-budget; until then this session cannot see what notes exist.\n' \ + "$KEPT_LIST" "$((TOTAL + CATALOG_TOKENS))" "$BUDGET" "$TRIM_LIST" else printf '\ncatalog (compiled from %s/notes/)\n%s\n' "$REL_LABEL" "$RULE" cat "$TMP/catalog" diff --git a/docs/configuration.md b/docs/configuration.md index b1a5a9a826..dfd05438fb 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -166,7 +166,8 @@ Shared captain preferences that apply across secondmate domains live only in the Fleet-local operational facts and gotchas live locally as one atomic note per claim under `data/memory/notes/`, alongside an optional standing constitution in `data/memory/core.md`, the dated operating picture `data/memory/now.md`, the regenerable index `data/memory/catalog.md`, and the never-injected candidate tray `data/memory/drop/`. The standing constitution `data/memory/core.md` holds standing preferences, authority boundaries, and core guidelines with a 1,500-2,500 estimated-token target. The dated operating picture `data/memory/now.md` holds perishable shift pins and ceilings with a front matter date, and is injected only when dated today, matched against the local host date. -It is read from the home at `data/memory/now.md` even when `data/memory/HEAD` points at a generation, exactly like the drop tray, so publishing a generation neither freezes nor discards the current shift's picture. +It is read from the home at `data/memory/now.md` even when `data/memory/HEAD` points at a generation, exactly like the drop tray, so publishing a generation neither freezes nor discards the current shift's picture; `--memory-dir` and `--gen` move it to the named directory along with everything else, so verifying a proposed generation stays a function of that generation alone. +See [`docs/examples/now.md`](examples/now.md) for a copyable starting point. The whole directory is gitignored and is created by `bin/fm-memory-migrate.sh`, which also splits a home's legacy `data/learnings.md` into notes and freezes the original under `data/memory/raw/` before archiving it to `data/memory-archive.md`. Completed tasks deposit candidate findings and tactical gotchas into `data/memory/drop/<task-id>.md` through `bin/fm-memory-drop.sh`. Generations of memory live under `data/memory/gen/<N>/` and are activated atomically by pointing `data/memory/HEAD` at the active generation. diff --git a/tests/fm-memory-compile.test.sh b/tests/fm-memory-compile.test.sh index 7574771cd2..b68c48232d 100755 --- a/tests/fm-memory-compile.test.sh +++ b/tests/fm-memory-compile.test.sh @@ -789,6 +789,37 @@ test_operating_picture_is_read_from_the_home_in_a_generation_home() { pass 'a generation home still reads the home-level data/memory/now.md' } +test_an_explicit_memory_dir_never_reads_the_home_operating_picture() { + local home out + home=$(new_home now-explicit-dir) + mkdir -p "$home/data/memory/gen/1/notes" + printf '# core\n\nGENERATION-CORE-TEXT\n' > "$home/data/memory/gen/1/core.md" + write_now "$home" 2026-08-20 'HOME-LEVEL-PINS' + + out=$(FM_MEMORY_TODAY_OVERRIDE=2026-08-20 compile "$home" --no-auto-context \ + --memory-dir "$home/data/memory/gen/1") + + assert_contains "$out" 'GENERATION-CORE-TEXT' 'the named generation core was not injected' + assert_not_contains "$out" 'HOME-LEVEL-PINS' \ + 'the home operating picture leaked into a compile of a named directory' + assert_not_contains "$out" 'operating picture' \ + 'a named directory with no now.md still emitted an operating picture section' + [ -z "$(accounting_field "$out" now)" ] \ + || fail "the home operating picture was accounted against a named directory: $out" + + # The named directory's own now.md is the one it reads. + printf -- '---\ndate: 2026-08-20\n---\n\nGENERATION-PINS\n' > "$home/data/memory/gen/1/now.md" + out=$(FM_MEMORY_TODAY_OVERRIDE=2026-08-20 compile "$home" --no-auto-context \ + --memory-dir "$home/data/memory/gen/1") + assert_contains "$out" 'GENERATION-PINS' 'the named directory own now.md was not injected' + assert_contains "$out" 'operating picture: data/memory/gen/1/now.md' \ + 'the operating picture was not labelled with the named directory' + assert_not_contains "$out" 'HOME-LEVEL-PINS' \ + 'the home operating picture leaked in beside the named directory own now.md' + + pass 'a compile of a named memory directory depends on that directory alone' +} + test_date_key_wins_over_updated_key_in_the_operating_picture() { local home out home=$(new_home now-date-precedence) @@ -833,6 +864,10 @@ test_shipped_example_operating_picture_compiles() { example_date=$(awk 'NR > 1 && /^---[[:space:]]*$/ { exit } tolower($1) == "date:" { print $2; exit }' \ "$ROOT/docs/examples/now.md") [ -n "$example_date" ] || fail 'docs/examples/now.md carries no date: key in its front matter' + case "$example_date" in + [0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]) ;; + *) fail "docs/examples/now.md carries a non-ISO date, so a captain copying it is stale on day one: $example_date" ;; + esac out=$(FM_MEMORY_TODAY_OVERRIDE="$example_date" compile "$home" --no-auto-context) @@ -842,6 +877,14 @@ test_shipped_example_operating_picture_compiles() { [ "$(accounting_field "$out" now)" -gt 0 ] \ || fail "the example template was not accounted: $out" + # The same shipped file on any other day must trip the stale gate, so the + # date really is read rather than echoed back by the override. + out=$(FM_MEMORY_TODAY_OVERRIDE=1999-12-31 compile "$home" --no-auto-context) + assert_not_contains "$out" 'operating picture: data/memory/now.md' \ + 'the shipped example template was injected on a day it is not dated' + assert_contains "$out" "MEMORY_NOTICE: data/memory/now.md is dated $example_date (not today, 1999-12-31)" \ + 'the shipped example template did not trip the stale notice on another day' + pass 'the shipped docs/examples/now.md template compiles as a valid operating picture' } @@ -886,6 +929,7 @@ test_absent_now_md_produces_byte_identical_output_with_no_notice test_budget_cap_precedence_with_operating_picture test_oversized_operating_picture_is_dropped_but_the_catalog_survives test_operating_picture_is_read_from_the_home_in_a_generation_home +test_an_explicit_memory_dir_never_reads_the_home_operating_picture test_date_key_wins_over_updated_key_in_the_operating_picture test_stale_operating_picture_notice_names_both_dates test_shipped_example_operating_picture_compiles From 9b06f35f63ef5c1c7f650bc9aa3c07fcc5ad77e2 Mon Sep 17 00:00:00 2001 From: PP <121104417+BohnBawerick@users.noreply.github.com> Date: Thu, 20 Aug 2026 13:08:42 +0800 Subject: [PATCH 4/5] no-mistakes(review): Condition drop warning remainder and update startup memory docs --- AGENTS.md | 2 +- bin/fm-memory-compile.sh | 8 ++++++-- docs/configuration.md | 2 +- docs/examples/now.md | 4 +++- tests/fm-memory-compile.test.sh | 13 +++++++++++++ 5 files changed, 24 insertions(+), 5 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index edbcf8d359..4a5fd7fe03 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -171,7 +171,7 @@ When that section reports its checks still in progress it names exactly what is 6. **Network checks** - after the fleet-state digest, the deferred stage's result, or an explicit statement of what it has not confirmed yet. A read-only session runs no network checks at all and says so. 7. **Context digest and next step** - last of the bulk sections, the full contents of `data/projects.md`, `data/secondmates.md`, and `data/captain-shared.md`, plus this session's curated memory, each clearly delimited, followed by the closing reminder. - Curated memory is compiled and capped by `bin/fm-memory-compile.sh`, never dumped: it carries a standing core, a catalog of every note that exists, and the notes whose triggers matched live fleet work. + Curated memory is compiled and capped by `bin/fm-memory-compile.sh`, never dumped: it carries a standing core, the dated operating picture when `data/memory/now.md` is dated today, a catalog of every note that exists, and the notes whose triggers matched live fleet work. Reading one further note by its catalog path when its title matches what the turn needs is expected and is not a re-read; a home with no `data/memory/` layout, or a session whose compile failed, falls back to the whole-file print of `data/captain.md` and `data/learnings.md`. A file that does not exist prints an explicit `ABSENT` marker, never confused with an empty-but-present file: absence is meaningful (`captain.md` absent means use the firstmate repo's built-in defaults, `projects.md` absent means rebuild it from the clones under `projects/`, etc.). The closing reminder points back to the emitted supervision block and preserves only the lock, afk, Relay, and read-once reminders. diff --git a/bin/fm-memory-compile.sh b/bin/fm-memory-compile.sh index 60bfb6dfbf..b4787042d8 100755 --- a/bin/fm-memory-compile.sh +++ b/bin/fm-memory-compile.sh @@ -760,8 +760,12 @@ else printf '\noperating picture: %s\n%s\n' "$NOW_LABEL" "$RULE" cat "$NOW_PATH" elif [ "$NOW_OVER" -eq 1 ]; then - printf '\nMEMORY_BUDGET_WARNING: the core plus operating picture is %s estimated tokens against a %s budget, so the operating picture was dropped and this session is running without today'"'"'s pins and ceilings. The catalog and notes below were filled from what remains. Trim %s or raise config/startup-memory-budget.\n' \ - "$((CORE_TOKENS + NOW_TOKENS))" "$BUDGET" "$NOW_LABEL" + NOW_OVER_REMAINDER='' + if [ "$CATALOG_KEPT" -eq 1 ]; then + NOW_OVER_REMAINDER=' The catalog and notes below were filled from what remains.' + fi + printf '\nMEMORY_BUDGET_WARNING: the core plus operating picture is %s estimated tokens against a %s budget, so the operating picture was dropped and this session is running without today'"'"'s pins and ceilings.%s Trim %s or raise config/startup-memory-budget.\n' \ + "$((CORE_TOKENS + NOW_TOKENS))" "$BUDGET" "$NOW_OVER_REMAINDER" "$NOW_LABEL" fi if [ "$CATALOG_KEPT" -eq 0 ]; then diff --git a/docs/configuration.md b/docs/configuration.md index dfd05438fb..fb7e3ee56b 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -175,7 +175,7 @@ Every proposed generation must pass the mechanical verifier in `bin/fm-memory-ve The verifier enforces four safety properties: working memory must fit within `config/startup-memory-budget` with its catalog intact, every note and a non-empty `core.md` must cite at least one existing file on disk, the standing constitution in force must survive into the generation, reading both the published generation and the proposed one through the compiler's own core precedence (`core.md` first, `data/captain.md` only when there is none), and single-generation deletions cannot exceed the diff bounds cap or remove every baseline note. There is no shared notes directory by captain decision. -Session start injects this memory through `bin/fm-memory-compile.sh`, which selects a core, a catalog of every note, and the notes whose triggers match live fleet work, and refuses to emit more than the startup memory budget below allows. +Session start injects this memory through `bin/fm-memory-compile.sh`, which selects a core, the dated operating picture when `data/memory/now.md` is dated today, a catalog of every note, and the notes whose triggers match live fleet work, and refuses to emit more than the startup memory budget below allows. When `data/memory/HEAD` is present, the compiler reads from that active generation directory; otherwise it reads directly from `data/memory/`. The budget caps the compiled memory bundle only; `data/captain-shared.md` is printed outside that cap. That script's header is the single owner of the note format, the trigger-matching rule, and the precedence that decides what is dropped first under budget pressure. diff --git a/docs/examples/now.md b/docs/examples/now.md index 116fde12da..ab370404ff 100644 --- a/docs/examples/now.md +++ b/docs/examples/now.md @@ -4,4 +4,6 @@ date: 2026-08-20 # Operating picture -- 2 claude + firstmate, 2 grok, 1 opencode, 2 pi, 1 codex last resort +- Ceiling: at most 3 crewmates running at once today, so a fourth task waits. +- Freeze: no deploy to production until the maintenance window closes at 18:00. +- Pin: the release branch is cut, so land fixes only, no refactors. diff --git a/tests/fm-memory-compile.test.sh b/tests/fm-memory-compile.test.sh index b68c48232d..4f9445bf46 100755 --- a/tests/fm-memory-compile.test.sh +++ b/tests/fm-memory-compile.test.sh @@ -765,6 +765,19 @@ test_oversized_operating_picture_is_dropped_but_the_catalog_survives() { || fail "a dropped operating picture was still accounted: $out" [ "$(accounting_field "$out" status)" = capped ] \ || fail "dropping the operating picture was not reported as capped: $out" + assert_contains "$out" 'The catalog and notes below were filled from what remains.' \ + 'the drop warning did not say the catalog was still filled' + + # Room for the core alone: the picture is dropped and so is the catalog, so + # the warning must not promise a catalog that never follows. + printf '%s\n' "$core_tokens" > "$home/config/startup-memory-budget" + out=$(FM_MEMORY_TODAY_OVERRIDE=2026-08-20 compile "$home" --no-auto-context --context smalltrig) + + assert_contains "$out" 'MEMORY_BUDGET_WARNING: the core plus operating picture is' \ + 'dropping the oversized operating picture was silent' + assert_not_contains "$out" 'Small claim' 'the catalog was injected without room' + assert_not_contains "$out" 'The catalog and notes below were filled from what remains.' \ + 'the drop warning promised a catalog that was dropped in the same compile' pass 'an operating picture too large to fit is dropped alone and never takes the catalog with it' } From b36dae0ffeb4ee5e90adef8a93049ac6fa034493 Mon Sep 17 00:00:00 2001 From: PP <121104417+BohnBawerick@users.noreply.github.com> Date: Thu, 20 Aug 2026 17:44:01 +0800 Subject: [PATCH 5/5] Fix no-mistakes init flag and avoid login shell overhead in watch spawn --- .opencode/plugins/fm-primary-watch-arm.js | 12 ++++++++---- .pi/extensions/fm-primary-pi-watch.ts | 2 +- bin/fm-landing-remote.sh | 2 +- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/.opencode/plugins/fm-primary-watch-arm.js b/.opencode/plugins/fm-primary-watch-arm.js index e88c248f78..fcdb2a25b4 100644 --- a/.opencode/plugins/fm-primary-watch-arm.js +++ b/.opencode/plugins/fm-primary-watch-arm.js @@ -111,6 +111,12 @@ function shouldArm(paths) { } } +function parentPid(pid) { + const result = spawnSync("ps", ["-o", "ppid=", "-p", pid], { encoding: "utf8" }); + if (result.status !== 0) return ""; + return result.stdout.trim(); +} + async function sessionOwnsLock(paths) { let lockPid = ""; try { @@ -122,9 +128,7 @@ async function sessionOwnsLock(paths) { let pid = String(process.pid); for (let i = 0; i < 8; i += 1) { if (pid === lockPid) return true; - const result = await runProcess("ps", ["-o", "ppid=", "-p", pid]); - if (result.code !== 0) return false; - pid = result.stdout.trim(); + pid = parentPid(pid); if (!pid || pid === "1") return false; } return false; @@ -302,7 +306,7 @@ function spawnArm(paths, sessionID, client, predecessorArmPid = "") { FM_CONFIG_OVERRIDE: paths.config, FM_WATCH_PREDECESSOR_ARM_PID: predecessorArmPid, }; - const armChild = spawn("bash", ["-lc", 'config_dir="${FM_CONFIG_OVERRIDE:-$FM_HOME/config}"; [ -f "$config_dir/x-mode.env" ] && . "$config_dir/x-mode.env"; exec "$FM_ROOT_OVERRIDE/bin/fm-watch-arm.sh" --restart'], { + const armChild = spawn("bash", ["-c", 'config_dir="${FM_CONFIG_OVERRIDE:-$FM_HOME/config}"; [ -f "$config_dir/x-mode.env" ] && . "$config_dir/x-mode.env"; exec "$FM_ROOT_OVERRIDE/bin/fm-watch-arm.sh" --restart'], { cwd: paths.root, env, stdio: ["ignore", "pipe", "pipe"], diff --git a/.pi/extensions/fm-primary-pi-watch.ts b/.pi/extensions/fm-primary-pi-watch.ts index 923ec6c310..d679e73690 100644 --- a/.pi/extensions/fm-primary-pi-watch.ts +++ b/.pi/extensions/fm-primary-pi-watch.ts @@ -394,7 +394,7 @@ export default function (pi: ExtensionAPI) { FM_WATCH_ARM_SCRIPT: armScript, FM_WATCH_PREDECESSOR_ARM_PID: predecessorArmPid, }; - const armChild = spawn("bash", ["-lc", "config_dir=\"${FM_CONFIG_OVERRIDE:-$FM_HOME/config}\"; [ -f \"$config_dir/x-mode.env\" ] && . \"$config_dir/x-mode.env\"; exec \"$FM_WATCH_ARM_SCRIPT\" --restart"], { + const armChild = spawn("bash", ["-c", "config_dir=\"${FM_CONFIG_OVERRIDE:-$FM_HOME/config}\"; [ -f \"$config_dir/x-mode.env\" ] && . \"$config_dir/x-mode.env\"; exec \"$FM_WATCH_ARM_SCRIPT\" --restart"], { cwd: fmRoot, env, stdio: ["ignore", "pipe", "pipe"], diff --git a/bin/fm-landing-remote.sh b/bin/fm-landing-remote.sh index 588ec37dc0..b5a3e59356 100755 --- a/bin/fm-landing-remote.sh +++ b/bin/fm-landing-remote.sh @@ -387,7 +387,7 @@ refresh_no_mistakes() { echo "warning: no-mistakes is not on PATH; after origin points at the landing remote, run: no-mistakes init" >&2 return 0 fi - ( cd "$REPO" && no-mistakes init ) || return 1 + ( cd "$REPO" && no-mistakes --yes init ) || return 1 } cmd_status() {