Compare commits
7
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
527596abe5 | ||
|
|
879d200bc1 | ||
|
|
dc046a7cb0 | ||
|
|
3b16ac3984 | ||
|
|
1a7c687cb4 | ||
|
|
6324d54168 | ||
|
|
a986d77449 |
No files matched your search
@@ -13,32 +13,26 @@ with YAML frontmatter, spec at `omp-docs/references/okf-spec.md`).
|
|||||||
(`dank/command-center` on the Dankclaw instance). There is no staging copy:
|
(`dank/command-center` on the Dankclaw instance). There is no staging copy:
|
||||||
**the checkout is the deployment.** Branch switches swap live config files out
|
**the checkout is the deployment.** Branch switches swap live config files out
|
||||||
from under the running agent — keep feature branches short-lived, and never end
|
from under the running agent — keep feature branches short-lived, and never end
|
||||||
a work session checked out on anything but `main`.
|
a work session checked out on anything but `develop` or `main`.
|
||||||
- Contents that belong here: `AGENTS.md`, `omp-docs/` (knowledge base),
|
- Contents that belong here: `AGENTS.md`, `omp-docs/` (knowledge base),
|
||||||
`scripts/` (small system utilities), `agent/` + `plugins/` (harness config and
|
`scripts/` (small system utilities), `agent/` + `plugins/` (harness config and
|
||||||
plugin manifests). Everything else under this directory is runtime state and is
|
plugin manifests). Everything else under this directory is runtime state and is
|
||||||
gitignored on principle — see `.gitignore`. History must mean "the configuration
|
gitignored on principle — see `.gitignore`. History must mean "the configuration
|
||||||
changed", not "the agent breathed".
|
changed", not "the agent breathed".
|
||||||
|
|
||||||
## Trunk-based SDLC (since 2026-10-18; replaces the old git-flow regime)
|
## Git-flow SDLC (full flow, from day one)
|
||||||
|
|
||||||
- `main` is the only long-lived branch. Frank is the only dev; `develop` and the
|
|
||||||
release-branch ceremony are retired — no `develop`, no back-merges, no
|
|
||||||
`release/X.Y.Z` branches.
|
|
||||||
- **Before every work item: fetch and branch off the freshest `origin/main`.**
|
|
||||||
If main moves under you, merge it into your branch (never rebase — Frank
|
|
||||||
dislikes the history rewrite).
|
|
||||||
- Work item = Gitea issue on `dank/command-center` (Gitea at 192.168.0.143:3000,
|
- Work item = Gitea issue on `dank/command-center` (Gitea at 192.168.0.143:3000,
|
||||||
owner `dank`). Small fixes may go straight to a branch; anything with more than
|
owner `dank`). Small fixes may go straight to a branch; anything with more than
|
||||||
one moving part gets an issue first.
|
one moving part gets an issue first.
|
||||||
- Branch naming: `<type>/<issue#>-<slug>`, type ∈ feat|fix|docs|chore.
|
- Branch naming: `<type>/<issue#>-<slug>`, type ∈ feat|fix|docs|chore.
|
||||||
- **Pull requests target `main` directly.** Merge with `--no-ff` so feature arcs
|
Release cuts: `release/X.Y.Z` from `develop`; hotfixes: `hotfix/<slug>` from `main`.
|
||||||
stay visible in history. **Never merge a PR without an explicit OK from Frank.**
|
- **Merge, never rebase**, when updating a branch against a moved main — Frank
|
||||||
- **The moment a PR is open, open it in Frank's browser** — `xdg-open <PR-URL>`
|
dislikes the history rewrite. **Never merge a PR without an explicit OK from Frank.**
|
||||||
lands a tab in his Firefox; never make him hunt for it in Gitea.
|
- Merge into `develop` with `--no-ff` so feature arcs stay visible in history.
|
||||||
- Releases: tag `vX.Y.Z` straight on `main` after verifying; tags are the deploy
|
- Releases: cut `release/X.Y.Z` from `develop`, verify, merge to `main`, tag
|
||||||
markers every other machine pulls to. Hotfixes are ordinary branches off the
|
`vX.Y.Z`, back-merge `main` into `develop`. Tags are the deploy markers every
|
||||||
tag you are fixing forward from.
|
other machine pulls to.
|
||||||
- Trash convention: never `rm` anything user-authored — move it to `.AGENTTRASH/`
|
- Trash convention: never `rm` anything user-authored — move it to `.AGENTTRASH/`
|
||||||
(gitignored). Deletion is a pruning session with Frank present.
|
(gitignored). Deletion is a pruning session with Frank present.
|
||||||
|
|
||||||
@@ -62,25 +56,6 @@ commit"). Applied here:
|
|||||||
gate is the point of the hook).
|
gate is the point of the hook).
|
||||||
- Messages: imperative mood, what + why, `Refs #N` when a Gitea issue exists.
|
- Messages: imperative mood, what + why, `Refs #N` when a Gitea issue exists.
|
||||||
|
|
||||||
### Live-sync tiers (the machine is a committer too — Refs #11)
|
|
||||||
|
|
||||||
The checkout is the deployment, so several tracked files are live state
|
|
||||||
written continuously through symlinks by the harness, auto-learn, and
|
|
||||||
desktop GUIs. "Dirty tree" is judged by the writer:
|
|
||||||
|
|
||||||
| Tier | Paths | Commit law |
|
|
||||||
|------|-------|------------|
|
|
||||||
| Human/agent prose | `AGENTS.md`, `omp-docs/`, `scripts/`, `plugins/`, `agent/skills/` | branch → PR → Frank OK (unchanged) |
|
|
||||||
| Machine live | `agent/memories/`, `agent/managed-skills/`, `agent/config.yml`, `agent/models.yml` | `scripts/live-sync.sh` heartbeat |
|
|
||||||
| Desktop live | `dotfiles/` | same heartbeat |
|
|
||||||
|
|
||||||
The heartbeat (systemd user timer, hourly) stages ONLY allowlisted tiers,
|
|
||||||
commits with hooks armed — the secret gate included — merges `origin/main`,
|
|
||||||
pushes, and reaps `wt/*` worktrees older than 7 days. Non-allowlist dirt is
|
|
||||||
never committed, only surfaced. Conflicts abort and alert (ntfy); it never
|
|
||||||
force-pushes, auto-resolves, or uses `--no-verify`. Details:
|
|
||||||
[omp-docs/tools/live-sync.md](omp-docs/tools/live-sync.md).
|
|
||||||
|
|
||||||
## Secret gate (HARD rule)
|
## Secret gate (HARD rule)
|
||||||
|
|
||||||
The pre-commit hook (`.githooks/pre-commit`, enabled via
|
The pre-commit hook (`.githooks/pre-commit`, enabled via
|
||||||
|
|||||||
@@ -1,12 +0,0 @@
|
|||||||
[Unit]
|
|
||||||
Description=command-center live-sync heartbeat (commits machine-written paths)
|
|
||||||
Documentation=file://%h/.omp/omp-docs/tools/live-sync.md
|
|
||||||
|
|
||||||
[Service]
|
|
||||||
# One-shot heartbeat: commit allowlisted live paths, merge trunk, push, reap
|
|
||||||
# stale wt/* worktrees. Self-healing by design — a failed fetch/merge alerts
|
|
||||||
# via ntfy and simply retries on the next tick; nothing to restart.
|
|
||||||
Type=oneshot
|
|
||||||
Nice=19
|
|
||||||
IOSchedulingClass=idle
|
|
||||||
ExecStart=%h/.omp/scripts/live-sync.sh
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
[Unit]
|
|
||||||
Description=command-center live-sync heartbeat every hour
|
|
||||||
|
|
||||||
[Timer]
|
|
||||||
OnBootSec=5min
|
|
||||||
OnUnitActiveSec=1h
|
|
||||||
RandomizedDelaySec=3min
|
|
||||||
Persistent=true
|
|
||||||
|
|
||||||
[Install]
|
|
||||||
WantedBy=timers.target
|
|
||||||
@@ -11,22 +11,6 @@ timestamp: 2026-08-28
|
|||||||
|
|
||||||
# Bundle Change Log
|
# Bundle Change Log
|
||||||
|
|
||||||
## 2026-10-18 — live-sync heartbeat for machine-written paths (Refs #11)
|
|
||||||
|
|
||||||
Root-caused the month of silent divergence in the live checkout (~61 auto-learned skills on one disk only, a stranded `unexpectedStopDetection` fix, merge-refusals from dirty trees): the checkout IS the deployment, symlinks make machine writes dirty tracked files by design, and no commit law covered that churn. Added `scripts/live-sync.sh` + systemd user timer — allowlist tiers (memories, managed-skills, live config, dotfiles) auto-commit with hooks armed, merge `origin/main`, push, and reap stale `wt/*` worktrees; everything outside the allowlist is never staged, conflicts abort-and-alert. Black-box suite `tests/live-sync.bats` (9 tests). Tier law in AGENTS.md § Live-sync tiers; reference page `tools/live-sync.md` registered in the tools index.
|
|
||||||
|
|
||||||
## 2026-10-18 — Fleet law: filed PRs open in Frank's browser immediately
|
|
||||||
|
|
||||||
Both repos' git rules now require it: the moment a PR is opened, `xdg-open <PR-URL>` drops a Firefox tab on the workstation (AGENTS.md § Trunk-based SDLC here; § Git Workflow step 6 in `dank/serverAppConfigs`, riding their PR #6 branch).
|
|
||||||
|
|
||||||
## 2026-10-18 — Trunk law: extension route closed unmerged; direct repo alignment instead
|
|
||||||
|
|
||||||
Built and live-verified a cwd-scoped `before_agent_start` extension (quote probes: fires under `~/Projects`, silent elsewhere; kept as `.AGENTTRASH/trunk-law.js`). **Operator closed PR #9 unmerged** in favor of editing each repo's own AGENTS.md. Durable finding from the exploration: every context-file provider (AGENTS.md walk-up, `.omp/` dirs, CLAUDE.md) resolves at or below the repo root — a `~/Projects/AGENTS.md` for ancestor scope can never load; user-level `APPEND_SYSTEM.md` broadcasts to *all* sessions. Second finding: **serverAppConfigs never had git-flow** — its AGENTS.md mandated no branch regime at all (direct commit+push); a trunk + PR law is being added there by PR in that repo, not corrected.
|
|
||||||
|
|
||||||
## 2026-10-18 — SDLC switched to trunk-based development
|
|
||||||
|
|
||||||
`develop` retired by operator decision (single-dev repo; the branch bought nothing and cost a back-merge every release). New law in AGENTS.md § *Trunk-based SDLC*: branch off the freshest `origin/main` before every work item, PRs target `main` directly, `--no-ff` merges, tags `vX.Y.Z` straight on main. No back-merges, no release branches. The last back-merge (`527596a`) lands with this switch; the managed skill `gitflow-back-merge-plumbing` is now historical.
|
|
||||||
|
|
||||||
## 2026-10-18 — Sibling-vault overlap census; system docs realigned to live ground truth
|
## 2026-10-18 — Sibling-vault overlap census; system docs realigned to live ground truth
|
||||||
|
|
||||||
Census of `omp-docs/` against `~/Projects/serverAppConfig/serverAppConfigs-docs/` (subagent + live-ssh verification) found four contradicting pairs; all OURS-side stale pages corrected, THEIRS side flagged only (their repo, their PR flow). Ground truth from read-only probes on both boxes 2026-10-18.
|
Census of `omp-docs/` against `~/Projects/serverAppConfig/serverAppConfigs-docs/` (subagent + live-ssh verification) found four contradicting pairs; all OURS-side stale pages corrected, THEIRS side flagged only (their repo, their PR flow). Ground truth from read-only probes on both boxes 2026-10-18.
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ Configuration guides and reference material for installed software. Each tool ha
|
|||||||
|
|
||||||
* [[cachyos|cachyos]] — Arch-based rolling release with performance-optimized kernels, repositories, and gaming stack. Running on the workstation.
|
* [[cachyos|cachyos]] — Arch-based rolling release with performance-optimized kernels, repositories, and gaming stack. Running on the workstation.
|
||||||
* [comfyui](comfyui/index.md) — Node-based AI image generation engine on dankclaw. FLUX.2 klein 4B, CPU backend, systemd managed.
|
* [comfyui](comfyui/index.md) — Node-based AI image generation engine on dankclaw. FLUX.2 klein 4B, CPU backend, systemd managed.
|
||||||
* [live-sync](live-sync.md) — Heartbeat daemon committing machine-written paths (memories, managed-skills, live config, dotfiles) in the command-center checkout; systemd user timer, alerts via ntfy.
|
|
||||||
* [[applications|applications]] — Default userspace applications on CachyOS with Niri — browsers, editor, terminal, file manager, audio, screenshots, gaming.
|
* [[applications|applications]] — Default userspace applications on CachyOS with Niri — browsers, editor, terminal, file manager, audio, screenshots, gaming.
|
||||||
* ](ghostty/index.md) — GPU-accelerated terminal emulator with extensive configuration, keybindings, and shaders.
|
* ](ghostty/index.md) — GPU-accelerated terminal emulator with extensive configuration, keybindings, and shaders.
|
||||||
* ](gitea/index.md) — Private Git hosting on dankclaw. Quadlet-managed containers: Gitea, PostgreSQL, Caddy.
|
* ](gitea/index.md) — Private Git hosting on dankclaw. Quadlet-managed containers: Gitea, PostgreSQL, Caddy.
|
||||||
|
|||||||
@@ -1,75 +0,0 @@
|
|||||||
---
|
|
||||||
type: reference
|
|
||||||
title: live-sync heartbeat
|
|
||||||
description: Auto-commit daemon for machine-written paths in the command-center checkout — the deployment is live, so symlinks make config churn dirty tracked files continuously.
|
|
||||||
tags:
|
|
||||||
- git
|
|
||||||
- automation
|
|
||||||
- systemd
|
|
||||||
- deployment
|
|
||||||
- fleet
|
|
||||||
verified: 2026-10-18
|
|
||||||
timestamp: 2026-10-18
|
|
||||||
---
|
|
||||||
|
|
||||||
# live-sync heartbeat
|
|
||||||
|
|
||||||
The command-center checkout IS the deployment, and several tracked files are
|
|
||||||
live state: the harness writes memories and managed-skills, the operator
|
|
||||||
edits `config.yml`/`models.yml` through the `cfg://` surface, and desktop
|
|
||||||
GUIs (Noctalia, niri) rewrite dotfiles through symlinks. Before Refs #11 this
|
|
||||||
machine churn just piled up unpushed — one disk holding the only copy of ~61
|
|
||||||
auto-learned skills, a settings fix stranded locally, and merge-refusals
|
|
||||||
because the dirty tree collided with incoming work.
|
|
||||||
|
|
||||||
`scripts/live-sync.sh` is the pressure valve: a heartbeat that commits the
|
|
||||||
machine's own writes on a schedule. Tier law (which paths it may touch) lives
|
|
||||||
in [AGENTS.md](../../AGENTS.md) § Live-sync tiers.
|
|
||||||
|
|
||||||
## Contract (black-box)
|
|
||||||
|
|
||||||
Run = one pass: flock → branch guard (`main` only) → stage allowlisted dirty
|
|
||||||
tiers → commit (hooks armed) → fetch + merge `origin/main` → push → reap
|
|
||||||
stale `wt/*` worktrees. Exit `0` = quiet heartbeat (synced/no-op/reaped);
|
|
||||||
exit `1` = an alert was raised. Pinned by `tests/live-sync.bats`.
|
|
||||||
|
|
||||||
Hard properties:
|
|
||||||
|
|
||||||
- **Allowlist only.** Task dirt (`plugins/`, `omp-docs/`, prose) is never
|
|
||||||
staged — merge conflicts with in-flight work become impossible by
|
|
||||||
construction.
|
|
||||||
- **Hooks stay armed.** A secret-gate refusal aborts the commit, leaves
|
|
||||||
changes staged for a human, and alerts. Never `--no-verify`, never force.
|
|
||||||
- **Never fights the remote.** Merge conflict → `merge --abort`, local
|
|
||||||
commit kept, alert. Retries are automatic next tick.
|
|
||||||
- **Branch guard.** On a non-`main` checkout it refuses and alerts — same
|
|
||||||
rule as the doctrine ("never end a session off main").
|
|
||||||
- **Polite reaper.** `wt/wt-*` worktrees untouched for 7+ days are removed
|
|
||||||
with their `wt/*` branch; dirty or process-occupied worktrees are skipped.
|
|
||||||
|
|
||||||
## Alerts
|
|
||||||
|
|
||||||
`notify()` writes stderr always; pushes to ntfy when the topic file
|
|
||||||
`~/.config/ntfy/live-sync` (mode 600, topic text only) exists, throttled to
|
|
||||||
one push per hour. Test seams: `LIVE_SYNC_REPO`, `LIVE_SYNC_NOTIFY`,
|
|
||||||
`LIVE_SYNC_NTFY_TOPIC_FILE`, `LIVE_SYNC_AGE_DAYS`.
|
|
||||||
|
|
||||||
## Install
|
|
||||||
|
|
||||||
Units ship in `dotfiles/systemd-user/.config/systemd/user/` and propagate
|
|
||||||
with the usual dotfiles chain:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
systemctl --user enable --now live-sync.timer # after the unit is linked
|
|
||||||
systemctl --user start live-sync.service # one manual heartbeat
|
|
||||||
journalctl --user -u live-sync.service -n 20 # observe
|
|
||||||
```
|
|
||||||
|
|
||||||
## Failure modes
|
|
||||||
|
|
||||||
| Event | Result |
|
|
||||||
|-------|--------|
|
|
||||||
| Harness mid-write | commits a valid snapshot; remainder lands next heartbeat |
|
|
||||||
| Gitea down | commit safe locally; push retries next tick (≤1 h stale window) |
|
|
||||||
| Feature branch checked out in main checkout | refuses + alerts; protects the never-end-off-main rule |
|
|
||||||
| Secret shape in machine-written content | hook blocks, changes stay staged, alert |
|
|
||||||
@@ -1,131 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
# live-sync.sh — heartbeat committer for machine-written paths in the command
|
|
||||||
# center checkout. Refs #11 (see omp-docs/tools/live-sync.md).
|
|
||||||
#
|
|
||||||
# The checkout IS the deployment; the harness, auto-learn, and desktop GUIs
|
|
||||||
# write tracked files continuously through symlinks. This script commits ONLY
|
|
||||||
# the allowlisted machine-live + desktop-live tiers (AGENTS.md § Live-sync
|
|
||||||
# tiers), merges the trunk, pushes, and reaps stale session worktrees. Task
|
|
||||||
# dirt outside the allowlist is NEVER committed — only surfaced.
|
|
||||||
#
|
|
||||||
# Black-box contract (tests/live-sync.bats pins it):
|
|
||||||
# input git state of $LIVE_SYNC_REPO
|
|
||||||
# output exit 0 = quiet heartbeat (synced, no-op, or reaped)
|
|
||||||
# exit 1 = alert raised (branch guard, hook refusal, merge conflict,
|
|
||||||
# fetch/push failure); alert goes to stderr + notify sink
|
|
||||||
# Env overrides (test seams):
|
|
||||||
# LIVE_SYNC_REPO target repo (default $HOME/.omp)
|
|
||||||
# LIVE_SYNC_NOTIFY alert command, message on stdin
|
|
||||||
# LIVE_SYNC_NTFY_TOPIC_FILE ntfy topic file, mode 600 (default
|
|
||||||
# ~/.config/ntfy/live-sync; absent/empty = no push)
|
|
||||||
# LIVE_SYNC_AGE_DAYS wt/wt-* reap threshold (default 7)
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
REPO="${LIVE_SYNC_REPO:-$HOME/.omp}"
|
|
||||||
AGE_DAYS="${LIVE_SYNC_AGE_DAYS:-7}"
|
|
||||||
TOPIC_FILE="${LIVE_SYNC_NTFY_TOPIC_FILE:-$HOME/.config/ntfy/live-sync}"
|
|
||||||
STATE_DIR="${XDG_RUNTIME_DIR:-/tmp}"
|
|
||||||
|
|
||||||
# Allowlist tiers. Human prose (AGENTS.md, omp-docs, scripts, plugins,
|
|
||||||
# agent/skills) is deliberately absent: branch -> PR territory, never swept.
|
|
||||||
LIVE_PATHS=(
|
|
||||||
agent/memories
|
|
||||||
agent/managed-skills
|
|
||||||
agent/config.yml
|
|
||||||
agent/models.yml
|
|
||||||
dotfiles
|
|
||||||
)
|
|
||||||
|
|
||||||
notify() {
|
|
||||||
local msg="live-sync: $1" stamp now
|
|
||||||
stamp=$(cat "$STATE_DIR/live-sync.alert" 2>/dev/null || echo 0)
|
|
||||||
now=$(date +%s)
|
|
||||||
if (( now - stamp >= 3600 )); then
|
|
||||||
date +%s > "$STATE_DIR/live-sync.alert"
|
|
||||||
if [[ -n "${LIVE_SYNC_NOTIFY:-}" ]]; then
|
|
||||||
printf '%s' "$msg" | $LIVE_SYNC_NOTIFY || true
|
|
||||||
elif [[ -s "$TOPIC_FILE" ]]; then
|
|
||||||
curl -fsS -m 10 -d "$msg" "https://ntfy.sh/$(<"$TOPIC_FILE")" >/dev/null 2>&1 || true
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
printf '%s\n' "$msg" >&2
|
|
||||||
}
|
|
||||||
|
|
||||||
cd "$REPO" 2>/dev/null || exit 0
|
|
||||||
exec 9>"$STATE_DIR/live-sync.lock"
|
|
||||||
flock -n 9 || exit 0 # another heartbeat owns the repo
|
|
||||||
git rev-parse --verify -q HEAD >/dev/null || exit 0
|
|
||||||
|
|
||||||
branch=$(git symbolic-ref --quiet --short HEAD || true)
|
|
||||||
if [[ "$branch" != main ]]; then
|
|
||||||
notify "on branch '$branch', not main — refusing to sync, nothing changed"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# 1. stage only allowlisted paths that actually hold dirt (pathspecs that
|
|
||||||
# match nothing would make git add fatal, so check per-path)
|
|
||||||
for p in "${LIVE_PATHS[@]}"; do
|
|
||||||
if [[ -n $(git status --porcelain -- "$p") ]]; then
|
|
||||||
git add -A -- "$p"
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
if ! git diff --cached --quiet; then
|
|
||||||
# 2. commit WITH hooks armed — the secret gate stays pointed at the diff;
|
|
||||||
# a refusal here is the system working, never bypassed
|
|
||||||
if ! git commit -q -m "live: heartbeat sync $(date -Is) (Refs #11)"; then
|
|
||||||
notify "commit rejected by hook/secret gate — live changes left STAGED for a human"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# 3. integrate trunk and publish; never fight the remote
|
|
||||||
if git remote get-url origin >/dev/null 2>&1; then
|
|
||||||
if ! git fetch -q origin; then
|
|
||||||
notify "fetch failed — commit is safe locally, will retry next heartbeat"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
if git rev-parse --verify -q origin/main >/dev/null \
|
|
||||||
&& ! git merge -q --no-edit origin/main; then
|
|
||||||
git merge --abort || true
|
|
||||||
notify "merge conflict with origin/main — local commit kept; needs you at the keyboard"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
if ! git push -q origin main; then
|
|
||||||
notify "push failed — commit is safe locally, will retry next heartbeat"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
# 4. reap session worktrees older than AGE_DAYS. Dirty worktrees are left
|
|
||||||
# alone (git refuses), and so are any with a live process cwd inside.
|
|
||||||
in_use() {
|
|
||||||
local p
|
|
||||||
for p in /proc/[0-9]*/cwd; do
|
|
||||||
if [[ "$(readlink "$p" 2>/dev/null || true)" == "$1"* ]]; then
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
git worktree prune
|
|
||||||
reap_path="" reap_ref=""
|
|
||||||
reap_flush() {
|
|
||||||
if [[ "$reap_path" == "$REPO/wt/wt-"* && -d "$reap_path" ]] \
|
|
||||||
&& [[ -z $(find "$reap_path" -mtime "-$AGE_DAYS" -print -quit) ]] \
|
|
||||||
&& ! in_use "$reap_path"; then
|
|
||||||
git worktree remove -- "$reap_path" 2>/dev/null && git worktree prune || true
|
|
||||||
if [[ -n "$reap_ref" ]]; then
|
|
||||||
git branch -q -d "${reap_ref#refs/heads/}" 2>/dev/null || true
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
reap_path="" reap_ref=""
|
|
||||||
}
|
|
||||||
while IFS= read -r line; do
|
|
||||||
case "$line" in
|
|
||||||
"worktree "*) reap_flush; reap_path="${line#worktree }" ;;
|
|
||||||
"branch "*) reap_ref="${line#branch }" ;;
|
|
||||||
esac
|
|
||||||
done < <(git worktree list --porcelain)
|
|
||||||
reap_flush
|
|
||||||
@@ -1,176 +0,0 @@
|
|||||||
#!/usr/bin/env bats
|
|
||||||
# tests/live-sync.bats — black-box tests for scripts/live-sync.sh (Refs #11)
|
|
||||||
#
|
|
||||||
# Contract (see script header): the heartbeat commits ONLY allowlisted live
|
|
||||||
# paths, merges origin/main, pushes, and reaps stale wt/* worktrees. Nothing
|
|
||||||
# is observed except exit status, stderr, the notify-sink log, and git state.
|
|
||||||
|
|
||||||
setup() {
|
|
||||||
TEST_TMP="$(mktemp -d)"
|
|
||||||
REPO="$TEST_TMP/repo"
|
|
||||||
export XDG_RUNTIME_DIR="$TEST_TMP/xdg"
|
|
||||||
mkdir -p "$XDG_RUNTIME_DIR"
|
|
||||||
export GIT_CONFIG_GLOBAL="$TEST_TMP/gitconfig" GIT_CONFIG_SYSTEM=/dev/null
|
|
||||||
export GIT_AUTHOR_DATE="2026-10-18T12:00:00+00:00" GIT_COMMITTER_DATE="$GIT_AUTHOR_DATE"
|
|
||||||
export LIVE_SYNC_REPO="$REPO"
|
|
||||||
export LIVE_SYNC_NTFY_TOPIC_FILE="$TEST_TMP/no-topic-file"
|
|
||||||
export LIVE_SYNC_NOTIFY="$TEST_TMP/notify-stub"
|
|
||||||
export LIVE_SYNC_AGE_DAYS=7
|
|
||||||
|
|
||||||
# notify sink: appends each alert line to alerts.log
|
|
||||||
cat > "$LIVE_SYNC_NOTIFY" <<STUB
|
|
||||||
#!/usr/bin/env bash
|
|
||||||
cat >> "$TEST_TMP/alerts.log"
|
|
||||||
echo >> "$TEST_TMP/alerts.log"
|
|
||||||
STUB
|
|
||||||
chmod +x "$LIVE_SYNC_NOTIFY"
|
|
||||||
|
|
||||||
# bare "origin" + seeded clone with all tiers represented
|
|
||||||
git init -q --bare -b main "$TEST_TMP/origin.git"
|
|
||||||
git clone -q "$TEST_TMP/origin.git" "$REPO"
|
|
||||||
git -C "$REPO" config user.email frank@dank
|
|
||||||
git -C "$REPO" config user.name Frank
|
|
||||||
git -C "$REPO" checkout -q -b main
|
|
||||||
mkdir -p "$REPO/agent/memories" "$REPO/agent/managed-skills/s1" \
|
|
||||||
"$REPO/dotfiles/bash" "$REPO/plugins/bench" "$REPO/scripts"
|
|
||||||
echo base > "$REPO/agent/config.yml"
|
|
||||||
echo base > "$REPO/agent/models.yml"
|
|
||||||
echo base > "$REPO/agent/memories/learned.md"
|
|
||||||
echo base > "$REPO/agent/managed-skills/s1/SKILL.md"
|
|
||||||
echo base > "$REPO/dotfiles/bash/bashrc"
|
|
||||||
echo base > "$REPO/AGENTS.md"
|
|
||||||
echo base > "$REPO/scripts/deploy.sh"
|
|
||||||
echo base > "$REPO/plugins/bench/server.py"
|
|
||||||
git -C "$REPO" add -A
|
|
||||||
git -C "$REPO" commit -q -m seed
|
|
||||||
git -C "$REPO" push -q origin main
|
|
||||||
SEED="$(git -C "$REPO" rev-parse HEAD)"
|
|
||||||
}
|
|
||||||
|
|
||||||
teardown() { rm -rf "$TEST_TMP"; }
|
|
||||||
|
|
||||||
@test "heartbeat commits allowlisted live dirt only; task dirt stays dirty" {
|
|
||||||
echo new > "$REPO/agent/memories/learned.md"
|
|
||||||
mkdir "$REPO/agent/managed-skills/s2"
|
|
||||||
echo new > "$REPO/agent/managed-skills/s2/SKILL.md"
|
|
||||||
echo new > "$REPO/AGENTS.md"
|
|
||||||
echo new > "$REPO/plugins/bench/server.py"
|
|
||||||
|
|
||||||
run "$BATS_TEST_DIRNAME/../scripts/live-sync.sh"
|
|
||||||
[ "$status" -eq 0 ]
|
|
||||||
|
|
||||||
# exactly one new commit ahead of the seed
|
|
||||||
[ "$(git -C "$REPO" rev-list --count "$SEED"..main)" -eq 1 ]
|
|
||||||
# it touches ONLY live paths (untracked new dirs picked up too)
|
|
||||||
run git -C "$REPO" show --pretty=format: --name-only HEAD
|
|
||||||
printf '%s\n' "${lines[@]}" | grep -v '^$' | sort | diff -q - <(
|
|
||||||
printf '%s\n' agent/managed-skills/s2/SKILL.md agent/memories/learned.md)
|
|
||||||
# task dirt untouched, uncommitted
|
|
||||||
[ -n "$(git -C "$REPO" status --porcelain -- AGENTS.md plugins)" ]
|
|
||||||
# pushed: origin caught up
|
|
||||||
[ "$(git -C "$REPO" rev-parse origin/main)" = "$(git -C "$REPO" rev-parse main)" ]
|
|
||||||
}
|
|
||||||
|
|
||||||
@test "clean tree is a silent no-op" {
|
|
||||||
run "$BATS_TEST_DIRNAME/../scripts/live-sync.sh"
|
|
||||||
[ "$status" -eq 0 ]
|
|
||||||
[ "$(git -C "$REPO" rev-list --count "$SEED"..main)" -eq 0 ]
|
|
||||||
[ ! -e "$TEST_TMP/alerts.log" ]
|
|
||||||
}
|
|
||||||
|
|
||||||
@test "non-allowlist dirt alone never commits" {
|
|
||||||
echo new > "$REPO/AGENTS.md"
|
|
||||||
echo new > "$REPO/scripts/deploy.sh"
|
|
||||||
run "$BATS_TEST_DIRNAME/../scripts/live-sync.sh"
|
|
||||||
[ "$status" -eq 0 ]
|
|
||||||
[ "$(git -C "$REPO" rev-list --count "$SEED"..main)" -eq 0 ]
|
|
||||||
[ -n "$(git -C "$REPO" status --porcelain -- AGENTS.md scripts)" ]
|
|
||||||
}
|
|
||||||
|
|
||||||
@test "merge conflict aborts, keeps local commit, alerts, leaves remote alone" {
|
|
||||||
# a second clone advances origin/main on the same live file
|
|
||||||
git clone -q -b main "$TEST_TMP/origin.git" "$TEST_TMP/pocket"
|
|
||||||
git -C "$TEST_TMP/pocket" config user.email pocket@dank
|
|
||||||
git -C "$TEST_TMP/pocket" config user.name Pocket
|
|
||||||
echo remote-side > "$TEST_TMP/pocket/agent/models.yml"
|
|
||||||
git -C "$TEST_TMP/pocket" commit -qam "remote advance"
|
|
||||||
git -C "$TEST_TMP/pocket" push -q origin main
|
|
||||||
|
|
||||||
echo local-side > "$REPO/agent/models.yml"
|
|
||||||
|
|
||||||
run "$BATS_TEST_DIRNAME/../scripts/live-sync.sh"
|
|
||||||
[ "$status" -eq 1 ]
|
|
||||||
[[ "$output" == *"merge conflict"* ]]
|
|
||||||
[ -s "$TEST_TMP/alerts.log" ]
|
|
||||||
# HEAD is my local commit, merge left no residue, remote untouched
|
|
||||||
[[ "$(git -C "$REPO" log --format=%s -1)" == live:*"heartbeat sync"* ]]
|
|
||||||
[ ! -e "$REPO/.git/MERGE_HEAD" ]
|
|
||||||
[ "$(git -C "$REPO" rev-parse origin/main)" = "$(git -C "$TEST_TMP/pocket" rev-parse main)" ]
|
|
||||||
}
|
|
||||||
|
|
||||||
@test "branch guard refuses to sync on a feature branch" {
|
|
||||||
git -C "$REPO" checkout -q -b feature/whatever
|
|
||||||
echo new > "$REPO/agent/config.yml"
|
|
||||||
|
|
||||||
run "$BATS_TEST_DIRNAME/../scripts/live-sync.sh"
|
|
||||||
[ "$status" -eq 1 ]
|
|
||||||
[[ "$output" == *"not main"* ]]
|
|
||||||
[ "$(git -C "$REPO" rev-list --count "$SEED"..feature/whatever)" -eq 0 ]
|
|
||||||
[ -n "$(git -C "$REPO" status --porcelain -- agent/config.yml)" ]
|
|
||||||
}
|
|
||||||
|
|
||||||
@test "pre-commit hook refusal is surfaced, not bypassed; changes stay staged" {
|
|
||||||
mkdir "$TEST_TMP/hooks"
|
|
||||||
printf '#!/bin/sh\nexit 1\n' > "$TEST_TMP/hooks/pre-commit"
|
|
||||||
chmod +x "$TEST_TMP/hooks/pre-commit"
|
|
||||||
git -C "$REPO" config core.hooksPath "$TEST_TMP/hooks"
|
|
||||||
echo new > "$REPO/dotfiles/bash/bashrc"
|
|
||||||
|
|
||||||
run "$BATS_TEST_DIRNAME/../scripts/live-sync.sh"
|
|
||||||
[ "$status" -eq 1 ]
|
|
||||||
[[ "$output" == *"rejected"* ]]
|
|
||||||
[ -n "$(git -C "$REPO" diff --cached --name-only -- dotfiles)" ]
|
|
||||||
[ "$(git -C "$REPO" rev-list --count "$SEED"..main)" -eq 0 ]
|
|
||||||
}
|
|
||||||
|
|
||||||
@test "repo without origin still commits locally and exits clean" {
|
|
||||||
git -C "$REPO" remote remove origin
|
|
||||||
echo new > "$REPO/dotfiles/bash/bashrc"
|
|
||||||
run "$BATS_TEST_DIRNAME/../scripts/live-sync.sh"
|
|
||||||
[ "$status" -eq 0 ]
|
|
||||||
[ "$(git -C "$REPO" rev-list --count "$SEED"..main)" -eq 1 ]
|
|
||||||
}
|
|
||||||
|
|
||||||
@test "stale wt/* worktree and its branch are reaped; fresh one survives" {
|
|
||||||
git -C "$REPO" worktree add -q wt/wt-old -b wt/old
|
|
||||||
git -C "$REPO" worktree add -q wt/wt-new -b wt/new
|
|
||||||
touch -d "10 days ago" "$REPO/wt/wt-old"
|
|
||||||
find "$REPO/wt/wt-old" -exec touch -d "10 days ago" {} +
|
|
||||||
|
|
||||||
run "$BATS_TEST_DIRNAME/../scripts/live-sync.sh"
|
|
||||||
[ "$status" -eq 0 ]
|
|
||||||
[ ! -d "$REPO/wt/wt-old" ]
|
|
||||||
[ -d "$REPO/wt/wt-new" ]
|
|
||||||
run git -C "$REPO" show-ref --verify refs/heads/wt/old
|
|
||||||
[ "$status" -ne 0 ]
|
|
||||||
git -C "$REPO" show-ref --verify --quiet refs/heads/wt/new
|
|
||||||
}
|
|
||||||
|
|
||||||
@test "second heartbeat inside the throttle window alerts once, never twice" {
|
|
||||||
# two conflicting heartbeats: alert sink must see exactly one line
|
|
||||||
git clone -q -b main "$TEST_TMP/origin.git" "$TEST_TMP/pocket"
|
|
||||||
git -C "$TEST_TMP/pocket" config user.email pocket@dank
|
|
||||||
git -C "$TEST_TMP/pocket" config user.name Pocket
|
|
||||||
echo remote-side > "$TEST_TMP/pocket/agent/config.yml"
|
|
||||||
git -C "$TEST_TMP/pocket" commit -qam "remote advance"
|
|
||||||
git -C "$TEST_TMP/pocket" push -q origin main
|
|
||||||
echo local-side > "$REPO/agent/config.yml"
|
|
||||||
|
|
||||||
run "$BATS_TEST_DIRNAME/../scripts/live-sync.sh"
|
|
||||||
[ "$status" -eq 1 ]
|
|
||||||
rm -f "$XDG_RUNTIME_DIR/live-sync.lock" # flock releases with the process; state stays
|
|
||||||
echo local-side2 > "$REPO/agent/config.yml"
|
|
||||||
run "$BATS_TEST_DIRNAME/../scripts/live-sync.sh"
|
|
||||||
[ "$status" -eq 1 ]
|
|
||||||
[ "$(wc -l < "$TEST_TMP/alerts.log")" -eq 1 ]
|
|
||||||
}
|
|
||||||
Reference in new issue
Block a user