Skip to content

Fix: XACA-0510 — Rework update_launchagents to render from templates#30

Merged
ehlersd merged 2 commits into
mainfrom
feature/XACA-0510-render-launchagents
May 16, 2026
Merged

Fix: XACA-0510 — Rework update_launchagents to render from templates#30
ehlersd merged 2 commits into
mainfrom
feature/XACA-0510-render-launchagents

Conversation

@ehlersd
Copy link
Copy Markdown
Member

@ehlersd ehlersd commented May 16, 2026

Summary

update_launchagents() in aiteamforge-upgrade.sh looked for pre-rendered plists at share/launchagents/<agent> — a directory the tap doesn't ship. XACA-0476 fixed the source-path prefix but didn't unblock the function (silent no-op on the source-absent early-out). This PR brings upgrade in line with install-kanban.sh: render share/templates/kanban/*.template with the same sed substitution map, diff against the live target, and reload only on change.

Changes Made

  • libexec/commands/aiteamforge-upgrade.sh (+55 / −13)
    • New private helper _render_launchagent_template(template, target) applying the full substitution map (USER_HOME, AITEAMFORGE_DIR, BACKUP_INTERVAL, PYTHON3_PATH) — single render path avoids sibling drift (k501-sibling-heuristic-drift-pattern).
    • update_launchagents() reworked: iterates (plist:template) pairs, renders to ${target}.new, diffs vs target, unloads → mv → loads only on change (or under FORCE). Skip-when-target-absent preserved (upgrade does not silently install opt-out agents).
  • tests/test-xaca-0510-launchagent-render.sh (+263, new)
    • 12 test cases via awk-extract-and-eval seam, sandboxed in TEST_TMP_DIR/LaunchAgents with launchctl stubbed.
    • Covers: fresh render, opt-in skip, no-op on unchanged, FORCE re-renders, DRY_RUN writes nothing, missing template handled cleanly, placeholders fully substituted.
  • CHANGELOG.md (+19, new file) — XACA-0510 stanza under Unreleased / Fixed.

Impact

3 files changed, 337 insertions(+), 13 deletions(-).

Risk

LOW. Scope is one function and one new test file. Pre-existing semantics (FORCE, DRY_RUN, unload→load, skip-when-target-absent) preserved with explicit checklist verification. Sibling fix for aiteamforge-migrate.sh tracked separately as XACA-0512 — not touched here.

Testing

  • New test file: 12/12 pass
  • Full tap test suite: 692 tests, 712 assertions passed, 5 pre-existing failures (test-command-nested-workingdir.sh, test-fresh-install.sh, test-org-config.sh) verified against main — zero new regressions
  • shellcheck: zero new warnings (only pre-existing SC1091 info-level noise about sourced files)

Breaking Changes

None. Behavior change: function that previously no-op'd silently now actually updates LaunchAgent plists on aiteamforge upgrade — restoring the original intent.

Checklist

  • Compiles / shellcheck clean
  • Tests added and passing
  • No new lint warnings
  • Self-reviewed
  • CHANGELOG.md updated (this commit)

Related

  • Predecessor: XACA-0476 (path prefix fix that revealed this gap)
  • Sibling follow-up: XACA-0512 (migrate.sh update_launchagents — different defect class)
  • Superproject companion PR (gitlink bump): will follow once this merges

🤖 Generated with Claude Code

`update_launchagents()` in aiteamforge-upgrade.sh was looking for
pre-rendered plists at `share/launchagents/<agent>` — a directory that
does not ship in the tap. XACA-0476 corrected the source-path prefix
but didn't unblock the function, which silently no-op'd on the
"source absent" early-out.

This change brings upgrade in line with install-kanban.sh: render the
*.template files at share/templates/kanban/ with the same sed
substitution map (USER_HOME, AITEAMFORGE_DIR, BACKUP_INTERVAL,
PYTHON3_PATH), diff against the live target, and reload only on
change. FORCE / DRY_RUN / opt-in-skip semantics preserved.

Adds `LAUNCHAGENTS_DIR` seam so tests can inject a sandbox path
without touching ~/Library/LaunchAgents (M3Pro install-ban rule).

Sibling site aiteamforge-migrate.sh::update_launchagents has a
different defect class (in-place sed path rewrite, no template render)
tracked separately as XACA-0512.

Three confirmed datapoints of sibling-heuristic drift now exist in
this surface (XACA-0476, XACA-0510, XACA-0512).

🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Copy link
Copy Markdown

@ds9-tester-bot ds9-tester-bot Bot left a comment

Choose a reason for hiding this comment

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

QA Test Report — PR #30 (XACA-0510)

Verdict: APPROVE

What Was Run

  1. shellcheck -S warning libexec/commands/aiteamforge-upgrade.sh — EXIT 0 (clean)
  2. shellcheck -S warning tests/test-xaca-0510-launchagent-render.sh — EXIT 0 (clean)
  3. bash tests/test-runner.sh test-xaca-0510-launchagent-render.sh — 12/12 PASS
  4. bash tests/test-runner.sh (full suite, 25 test files) — 5 failures, all pre-existing

Targeted Test Results

All 12 test cases passed (verbose run confirmed each by name):

  • Missing targets: both plists absent — both skipped (opt-in guard validated)
  • Fresh render: sentinel present — content written with no {{...}} placeholders
  • Rendered plist contains resolved WORKING_DIR path
  • lcars-health absent — not rendered (selective rendering confirmed)
  • No-op second run — mtime unchanged, no .new tempfile leak
  • FORCE=true — mtime changes (re-render forced)
  • DRY_RUN=true — sentinel content preserved (no overwrite)
  • DRY_RUN=true — 'Would update' printed
  • DRY_RUN=true — no .new tempfile leak
  • Missing template — warning without crash (exit 0)
  • Both sentinels present — both plists rendered with zero {{ placeholders

Full Suite Regression Check

5 failures in full suite run — all confirmed pre-existing baseline failures:

  • test-command-nested-workingdir.sh (known)
  • test-fresh-install.sh (known — tap not installed on M3Pro by design)
  • test-org-config.sh (known)

No NEW failures introduced by this PR.

Security / Sandbox Verification

  • LAUNCHAGENTS_DIR overridden to sandbox path in run_update_launchagents wrapper
  • HOME overridden to sandbox path in same wrapper
  • launchctl() stub (no-op return 0) declared and exported before function extraction
  • Real ~/Library/LaunchAgents never referenced in test file (confirmed by grep)
  • M3Pro tap-install ban not violated

Implementation Quality

  • Diff path logic correct: rendered output diffed vs live target (not raw template)
  • All tempfile paths cleaned on every exit path (change+DRY_RUN, no-change, both)
  • CHANGELOG.md stanza present and thorough

Non-Blocking Subitem Filed

[Test] Missing coverage: DRY_RUN=true when rendered content already matches target — tempfile cleanup on this path verified by logic review only, not by assertion. Filed as XACA-0510 subitem.

Copy link
Copy Markdown

@ai-security-review-bot ai-security-review-bot Bot left a comment

Choose a reason for hiding this comment

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

Code Review — PR #30 (XACA-0510)

Verdict: APPROVE.

Scope reviewed

  • libexec/commands/aiteamforge-upgrade.sh — new _render_launchagent_template() helper + update_launchagents() rework (cp-source-target → render-template-to-tmp → diff-vs-target → mv-on-change).
  • tests/test-xaca-0510-launchagent-render.sh — 12 sandboxed tests.
  • CHANGELOG.md (tap) — new file with full XACA-0510 stanza.
  • Plan-doc fidelity: matches kanban/plans/XACA-0510/XACA-0510_render_launchagent_templates.md exactly (helper next to function, render-to-tmp + diff-vs-target, FORCE/DRY_RUN/skip-absent preserved, follow-up filed as XACA-0512).

Security

  • No secrets in code, tests, or CHANGELOG.
  • sed -e "s|...|...|g" uses | as delimiter — safe for $HOME paths with spaces, and no PII gets baked in. (Theoretical risk: any substitution value containing | would break sed; flagged as a non-blocking subitem.)
  • mv tmpfile target is atomic (same dir, same filesystem — ${target}.new is a sibling of ${target}).
  • LAUNCHAGENTS_DIR env seam lets tests fully sandbox to $TEST_TMP_DIR/LaunchAgents; the real ~/Library/LaunchAgents is never touched. M3Pro tap-install ban respected.
  • launchctl is stubbed in the test framework (no-op function exported before the function-eval), so even an accidental code path can't reach the real launchctl.

Architecture

  • Single render helper avoids the sibling-drift pattern from k501-sibling-heuristic-drift-pattern within upgrade.sh.
  • The helper duplicates the substitution map already in install-kanban.sh:771-775 and :814-815. CHANGELOG explicitly acknowledges this as a scoped decision ("abstract only if a third site appears" per plan-doc § Helper function vs inline sed). Reasonable for now — failure mode is bounded and the sibling-drift catalog now points at aiteamforge-migrate.sh::update_launchagents (XACA-0512) as the next target for unification.
  • KANBAN_BACKUP_INTERVAL=900 is hardcoded as a default in both install-kanban.sh (script-level constant) and the new helper (:-900 fallback). If install ever bumps it, upgrade silently uses the old value. Filed as [Review] subitem — non-blocking, but ironic given the CHANGELOG's "sibling-heuristic drift" framing.

Code Quality

  • local discipline inside the for entry in loop is bash-safe (shebang verified #!/bin/bash; the k501-zsh-local-in-loop gotcha doesn't apply).
  • Tempfile cleanup on all no-op paths: DRY_RUN branch removes the tempfile, diff-equal branch removes the tempfile, missing-template branch returns before any redirect creates $dest.
  • Failure-tolerance pattern on both launchctl unload and launchctl load is right (unload of unloaded agent + load without user session both fail benignly).
  • Function names clear and prefixed with _ for private helper.
  • No trap to clean ${target}.new on SIGINT between render and mv. Low-priority — filed as [Review] subitem.
  • DRY_RUN path increments the updated counter and the final message reads "Updated N LaunchAgent(s)" — should read "Would update N..." in dry-run mode. Filed as [Review] subitem.

Testing

  • 12 standalone test cases run green via bash tests/test-runner.sh tests/test-xaca-0510-launchagent-render.sh — verified locally.
  • Sandboxing is airtight: FRAMEWORK_DIR=$FAKE_FRAMEWORK, WORKING_DIR=$FAKE_WORKING, LAUNCHAGENTS_DIR=$FAKE_LAUNCHAGENTS, HOME=$SANDBOX_DIR/home. No path traversal to real $HOME.
  • Matrix coverage: target-absent (x2), fresh-render content + placeholder-strip + WORKING_DIR-substitution, no-op-via-mtime + no .new leak, FORCE re-renders, DRY_RUN write-blocked + "Would update" + no .new leak, missing-template warning, both-rendered.
  • The test extracts the two functions via awk matching on the comment header ^# Render a LaunchAgent template and ^}$ brace counting. This is brittle to comment/format changes — a future rename of the comment would silently change what is tested. Filed as [Review] subitem (suggest a post-extraction sanity grep for both function names in the extracted text).

CHANGELOG

  • Present (homebrew-tap/CHANGELOG.md, new file), structured per Keep-a-Changelog 1.1.0, in same commit as code (per CLAUDE.md mandate).
  • Lineage is correctly documented: XACA-0476 (path prefix), XACA-0510 (render in upgrade), XACA-0512 (render in migrate).
  • Dev-team superproject CHANGELOG entry is out of scope for this PR — it lands with the submodule pointer-bump PR per the plan-doc's "Two-PR shape" section.

Plan-doc fidelity

Implementation matches the plan-doc decisions point for point: render helper local to upgrade.sh (no install-kanban refactor), diff rendered_tmp vs target not template vs target, unload then mv then load ordering preserved, skip-on-target-absent kept, cr-confluence-poller out of scope, migrate.sh defect filed separately as XACA-0512.

Non-blocking suggestions filed as [Review] subitems

  1. Add trap to clean up ${target}.new on SIGINT/SIGTERM.
  2. Hoist KANBAN_BACKUP_INTERVAL default to a single named constant shared by install-kanban + upgrade.
  3. Add post-extraction sanity check in the test (grep for both function names in extracted text).
  4. DRY_RUN summary message should read "Would update N" not "Updated N".
  5. Document in helper comment that PYTHON3_PATH is re-resolved at upgrade time.

None of these block merge. Diff is small, scope is honoured, tests pass, no security or data-loss or breaking-change concerns, and the implementation is faithful to the plan-doc.

APPROVED.

  - SIGINT/RETURN trap on *.new tempfile cleanup via module-level
    _cleanup_upgrade_tmpfiles + _upgrade_tmpfiles array (subitem 012)
  - Named constant XACA_0510_KANBAN_BACKUP_INTERVAL_DEFAULT=900 with
    cross-reference comment; full sibling-drift consolidation tracked
    in XACA-0516 (subitem 013)
  - Expand awk extraction to include _cleanup_upgrade_tmpfiles; add
    hard declare -f sanity checks (exit 1) after eval (subitem 014)
  - DRY_RUN-aware "Would update N" summary message (subitem 015)
  - Comment documenting PYTHON3_PATH re-resolution semantics (subitem 016)
  - 3 test cases for DRY_RUN=true + no-change path: file unchanged,
    no .new tempfile, output reports up-to-date (subitem 017)

15 test cases, all pass. shellcheck clean (pre-existing SC1091 info only).

🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Copy link
Copy Markdown

@ai-security-review-bot ai-security-review-bot Bot left a comment

Choose a reason for hiding this comment

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

Re-review after cleanup commit 2231f12. All five [Review] subitems (012-016) plus the [Test] subitem (017) addressed correctly.

Verification per subitem

012 — RETURN trap for tempfile cleanup. The original SC2064 footgun (per-iteration trap '... $tmpfile' RETURN capturing the variable at trap-set time) is avoided. Implementation:

  • Module-level _upgrade_tmpfiles=() at file scope (upgrade.sh:393).
  • _cleanup_upgrade_tmpfiles() walks the array with a force-remove (safe no-op when the tempfile was already moved or explicitly removed).
  • Trap set ONCE at function entry (upgrade.sh:447), not per-iteration.
  • Array reset on every function entry (upgrade.sh:446) — prevents stale-state bleed across invocations.
  • Append happens BEFORE any potential failure point (upgrade.sh:455 precedes the render call at :464). Correct order.
  • The # shellcheck disable=SC2329 annotation is justified — indirect call via trap.

Minor nit (non-blocking): commit message and docstring mention "SIGINT/RETURN trap"; only RETURN is wired. RETURN is the right pattern for function-scoped cleanup; SIGINT during the mv/launchctl window would still leave behind a tempfile, but those windows are tiny and a separate EXIT trap would be a different ticket. Not worth filing.

013 — Named constant + XACA-0516 follow-up. readonly XACA_0510_KANBAN_BACKUP_INTERVAL_DEFAULT=900 at upgrade.sh:27 with a three-line cross-reference comment naming install-kanban.sh:16 (verified accurate) and pointing to XACA-0516 for full consolidation. Used at upgrade.sh:417 in the parameter expansion default. XACA-0516 confirmed filed (LOW priority, TODO, correct context). Deliberate scope deferral acknowledged per the plan doc.

014 — declare -f sanity check. Three hard preconditions at test:72-77, one per extracted function, each with exit 1 and a clear FATAL message. >/dev/null suppresses normal stdout. Replaces the prior test_fail + return 0 pattern that would have masked cascading test failures. Loud-fail is the right call — a botched extraction should abort the whole file, not produce 15 cryptic individual failures.

015 — DRY_RUN-aware summary. Three-way branch at upgrade.sh:496-502 reads cleanly: updated=0 → "All up to date"; DRY_RUN=true → "Would update N"; else → "Updated N". The existing updated -eq 0 short-circuit correctly takes precedence over the DRY_RUN branch (no-change always wins over would-have), which is the right semantic.

016 — PYTHON3_PATH comment. Two-line comment at upgrade.sh:419-420 documents that command -v python3 is re-resolved on every upgrade, flagging the PATH-shift hazard between install and upgrade. Describes the behavior (re-resolution) rather than the mechanism — ages well.

017 — Test coverage. Three new tests at test:271-299 cover the DRY_RUN + no-change path: (a) target file unchanged across DRY_RUN run, (b) no .new tempfile remaining after DRY_RUN run, (c) "All LaunchAgents up to date" message appears. Each test isolates state with reset_launchagents and seeds via a real DRY_RUN=false render before the DRY_RUN=true assertion. Local run: 15/15 pass via the harness (bash tests/test-runner.sh test-xaca-0510-launchagent-render.sh).

Standard checklist

  • Security: No new secrets, no injection vectors. Trap body is a fixed function name (_cleanup_upgrade_tmpfiles), not user-supplied — no arbitrary-code-execution-via-filename hazard.
  • Architecture: Module-level array + dedicated cleanup function is a known-good bash pattern. Placement (between update_skills and _render_launchagent_template) makes sense.
  • Code Quality: shellcheck libexec/commands/aiteamforge-upgrade.sh and shellcheck tests/test-xaca-0510-launchagent-render.sh both clean (only the pre-existing SC1091 info on source lines).
  • Performance: No new I/O hotspots. The cleanup loop over a 2-element array is negligible.
  • Testing: 15/15 green. New tests properly seed state and assert specific behaviors rather than relying on side-effect coincidence.

Verdict

APPROVE. Cleanup commit lands every subitem cleanly. No new [Review] subitems filed — nothing genuinely actionable remains in scope. The XACA-0516 deferral of cross-file constant consolidation is correctly scoped (separate file, separate concern, traced).

Do not merge — main agent merges per the three-gate protocol.

Copy link
Copy Markdown

@ds9-tester-bot ds9-tester-bot Bot left a comment

Choose a reason for hiding this comment

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

Re-test after [Review]/[Test] cleanup commit 2231f12. All 15/15 XACA-0510 tests pass. Full suite: 3 pre-existing failures only (test-command-nested-workingdir.sh, test-fresh-install.sh, test-org-config.sh) — zero new regressions introduced.

Verified cleanup-batch changes:

  • Module-level _upgrade_tmpfiles array + RETURN trap: sound pattern — trap fires on function exit/error; array is reset at start of each update_launchagents call so parallel re-entrant runs don't cross-contaminate. No .new tempfile leakage confirmed by Tests 5, 7, 8.
  • declare -f sanity checks (lines 72-77): three separate guards for _cleanup_upgrade_tmpfiles, _render_launchagent_template, and update_launchagents — any awk marker drift causes exit 1 before any test runs. Correctly anchored to '# Cleanup helper invoked by update_launchagents' which is present verbatim in upgrade.sh line 390.
  • DRY_RUN summary message: elif branch at line 498 now emits 'Would update N LaunchAgent(s)' when DRY_RUN=true and updated>0. Confirmed by Test 7 (DRY_RUN=true + 'Would update' assertion) and the three-way if/elif/else in update_launchagents.
  • PYTHON3_PATH re-resolution comment (lines 419-420): present and accurate.
  • XACA_0510_KANBAN_BACKUP_INTERVAL_DEFAULT named constant (line 27): readonly, referenced in _render_launchagent_template. XACA-0516 reference in comment is correct.
  • Three new DRY_RUN + no-change tests (Test 8, lines 271-299): target unchanged, no .new tempfile, 'All LaunchAgents up to date' output — all pass.

Sandbox discipline: LAUNCHAGENTS_DIR, HOME, launchctl all overridden before any assertions. No ~/Library/LaunchAgents access.

@ehlersd ehlersd merged commit ab1a797 into main May 16, 2026
6 of 9 checks passed
@ehlersd ehlersd deleted the feature/XACA-0510-render-launchagents branch May 16, 2026 03:59
ehlersd added a commit that referenced this pull request May 18, 2026
…ed constant (#33)

* XACA-0516: Add shared constants.sh with KANBAN_BACKUP_INTERVAL_DEFAULT

Introduces libexec/lib/constants.sh as the single source of truth for shell
constants shared across the tap. Starts with KANBAN_BACKUP_INTERVAL_DEFAULT=900
(15-minute kanban backup cadence). Uses ${VAR:=default} so the file is safe to
source twice and env-var overrides still win, then marks the value readonly.

Eliminates the sibling-drift pattern that XACA-0510 and XACA-0512 worked
around with paired "change one, change the other" NOTE comments. See
XACA-0510-013 (review follow-up from PR #30).

Subitem 001 of XACA-0516. Consumers will be migrated to source this file in
subitems 002 (install-kanban.sh), 003 (aiteamforge-upgrade.sh), and 004
(aiteamforge-migrate.sh).

🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>

* XACA-0516: Migrate install-kanban.sh to shared KANBAN_BACKUP_INTERVAL_DEFAULT

install-kanban.sh now sources libexec/lib/constants.sh and resolves
KANBAN_BACKUP_INTERVAL via ${KANBAN_BACKUP_INTERVAL:-$KANBAN_BACKUP_INTERVAL_DEFAULT}
instead of the hardcoded literal 900. Side-effect: the value now respects an
env-var override (it was previously a hard-set), which matches how
aiteamforge-upgrade.sh and aiteamforge-migrate.sh already treat it.

The template substitution at line 773 (--BACKUP_INTERVAL placeholder) is
unchanged — it consumes the same variable name and works regardless of how
the value got there.

Subitem 002 of XACA-0516.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>

* XACA-0516: Migrate aiteamforge-upgrade.sh to shared constants.sh

aiteamforge-upgrade.sh now sources libexec/lib/constants.sh and consumes
$KANBAN_BACKUP_INTERVAL_DEFAULT directly. The local mirror constant
XACA_0510_KANBAN_BACKUP_INTERVAL_DEFAULT and its three-line "change one,
change the other" NOTE comment are deleted — the whole reason that NOTE
existed (sibling-drift between upgrade.sh and install-kanban.sh) is now
structurally impossible because both files read the same source of truth.

The consumer line in _render_launchagent_template still resolves via
${KANBAN_BACKUP_INTERVAL:-$KANBAN_BACKUP_INTERVAL_DEFAULT}, so env-var
override semantics are preserved.

Subitem 003 of XACA-0516.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>

* XACA-0516: Migrate aiteamforge-migrate.sh to shared constants.sh

aiteamforge-migrate.sh now sources libexec/lib/constants.sh and consumes
$KANBAN_BACKUP_INTERVAL_DEFAULT directly. The local mirror constant
XACA_0512_KANBAN_BACKUP_INTERVAL_DEFAULT is deleted and the NOTE comment is
trimmed to mention only FLEET_MONITOR_PORT (which retains its sibling-drift
pattern and is captured as a follow-up — explicitly out of scope here).

XACA_0512_FLEET_MONITOR_PORT_DEFAULT and its consumer remain untouched so
this commit stays scoped to the kanban-backup-interval consolidation.

Subitem 004 of XACA-0516.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>

* XACA-0516: Add CHANGELOG entry for shared constants consolidation

Captures the four migration commits (constants.sh + three consumers) under a
single Unreleased entry, documents the sibling-drift pattern eliminated, calls
out the preserved override semantics, lists the test results (159/159 across
six files plus an explicit default-path test), and flags the FLEET_MONITOR_PORT
follow-up as out of scope.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>

* XACA-0516: Address [Review] subitems on PR #33

Two follow-up cleanups from the code reviewer:

1. tests/test-xaca-0512-migrate-launchagent-render.sh — remove the stale
   `readonly XACA_0512_KANBAN_BACKUP_INTERVAL_DEFAULT=900` declaration.
   It was a test-local mirror of the production constant that XACA-0516
   already deleted. Every call site injects KANBAN_BACKUP_INTERVAL="900"
   as an env override into the eval'd migrate.sh functions, so the
   declaration was declared-but-never-read dead weight. The companion
   FLEET_MONITOR_PORT_DEFAULT mirror in the same fixture is intentionally
   left for the FLEET_MONITOR_PORT consolidation follow-up.

2. libexec/lib/constants.sh — document the ${VAR:-default} empty-string
   fallthrough semantics that all three consumers rely on. Operators
   setting KANBAN_BACKUP_INTERVAL='' will get 900, not a blank plist
   entry. The comment also points readers at the override pattern
   consumers use and gives a concrete override example.

Addresses XACA-0516-011 and XACA-0516-012 (PR #33 review subitems).

🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant