Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0edae3323d |
@@ -62,25 +62,6 @@ commit"). Applied here:
|
||||
gate is the point of the hook).
|
||||
- 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)
|
||||
|
||||
The pre-commit hook (`.githooks/pre-commit`, enabled via
|
||||
|
||||
@@ -10,7 +10,7 @@ Two-axis review of the diff between `HEAD` and a fixed point the user supplies:
|
||||
|
||||
Both axes run as **parallel sub-agents** so they don't pollute each other's context, then this skill aggregates their findings.
|
||||
|
||||
The issue tracker should have been provided to you. If `docs/agents/issue-tracker.md` is missing, tell the user to run `/setup-matt-pocock-skills`.
|
||||
The issue tracker should have been provided to you. If not, tell the user to run `/setup-matt-pocock-skills`.
|
||||
|
||||
## Process
|
||||
|
||||
@@ -26,14 +26,14 @@ Before going further, confirm the fixed point resolves (`git rev-parse <fixed-po
|
||||
|
||||
Look for the originating spec, in this order:
|
||||
|
||||
1. Issue references in the commit messages (`#123`, `Closes #45`, GitLab `!67`, etc.), fetched via the workflow in `docs/agents/issue-tracker.md`.
|
||||
1. Issue references in the commit messages (`#123`, `Closes #45`, GitLab `!67`, etc.), fetched via the workflow in the tracker doc.
|
||||
2. A path the user passed as an argument.
|
||||
3. A spec file under `docs/`, `specs/`, or `.scratch/` matching the branch name or feature.
|
||||
4. If nothing is found, ask the user where the spec is. If they say there isn't one, the **Spec** sub-agent will skip and report "no spec available".
|
||||
|
||||
### 3. Identify the standards sources
|
||||
|
||||
Anything in the repo that documents how code should be written, such as `CODING_STANDARDS.md` or `CONTRIBUTING.md`.
|
||||
Search the repo for every file that documents how code should be written. When `CODING_STANDARDS.md` or `CONTRIBUTING.md` exists, it must be on the list.
|
||||
|
||||
On top of whatever the repo documents, the Standards axis always carries the **smell baseline** below: a fixed set of Fowler code smells (_Refactoring_, ch.3) that applies even when a repo documents nothing. Two rules bind it:
|
||||
|
||||
@@ -57,6 +57,8 @@ Each smell reads *what it is* → *how to fix*; match it against the diff:
|
||||
|
||||
### 4. Spawn both sub-agents in parallel
|
||||
|
||||
Issue both sub-agent calls together, in the foreground, and aggregate the reports they return.
|
||||
|
||||
**Standards sub-agent prompt** should include:
|
||||
|
||||
- The full diff command and commit list.
|
||||
|
||||
@@ -7,7 +7,7 @@ description: Diagnosis loop for hard bugs and performance regressions. Use when
|
||||
|
||||
A discipline for hard bugs. Skip phases only when explicitly justified.
|
||||
|
||||
When exploring the codebase, read `CONTEXT.md` (if it exists) to get a clear mental model of the relevant modules, and check ADRs in the area you're touching.
|
||||
When exploring the codebase, read `GLOSSARY.md` (if it exists) to get a clear mental model of the relevant modules, and check ADRs in the area you're touching.
|
||||
|
||||
## Redact
|
||||
|
||||
@@ -122,7 +122,7 @@ A correct seam is one where the test exercises the **real bug pattern** as it oc
|
||||
If a correct seam exists:
|
||||
|
||||
1. Turn the minimised repro into a failing test at that seam.
|
||||
2. Watch it fail.
|
||||
2. Watch it fail. If you forced the red by mutating code or a fixture, `diff` against a pristine copy to prove the mutation landed before you trust it.
|
||||
3. Apply the fix.
|
||||
4. Watch it pass.
|
||||
5. Re-run the Phase 1 feedback loop against the original (un-minimised) scenario.
|
||||
|
||||
@@ -21,6 +21,8 @@ Format a round like so:
|
||||
➡️ <your recommended answer>
|
||||
```
|
||||
|
||||
Word each question so "yes" accepts your recommended answer.
|
||||
|
||||
Each round the user answers reshapes the tree: settled decisions push the frontier outward and unblock questions that depended on them. Recompute the frontier and ask the next round. A question whose answer depends on another question still open in this round belongs to a _later_ round, not this one.
|
||||
|
||||
Finding _facts_ is your job, never the user's. When a frontier question needs a fact from the environment (filesystem, tools, etc.), dispatch a sub-agent to find it; don't ask the user for anything you could look up yourself. Don't block on it: a running exploration is an unsettled prerequisite, so only the questions downstream of it wait for the sub-agent to report; ask the rest of the frontier now. The _decisions_ are the user's: put each to them and wait.
|
||||
|
||||
@@ -5,7 +5,7 @@ argument-hint: "What will the next session be used for?"
|
||||
disable-model-invocation: true
|
||||
---
|
||||
|
||||
Write a handoff document summarising the current conversation so a fresh agent can continue the work. Save to the temporary directory of the user's OS - not the current workspace.
|
||||
Write a handoff document summarising the current conversation so a fresh agent can continue the work. Save to the temporary directory of the user's OS (`$TMPDIR`, else `/tmp`; `%TEMP%` on Windows) - not the current workspace.
|
||||
|
||||
Include a "suggested skills" section in the document, naming which skills the next agent should call the Skill tool for.
|
||||
|
||||
|
||||
@@ -6,10 +6,12 @@ disable-model-invocation: true
|
||||
|
||||
Implement the work described by the user in the spec or tickets.
|
||||
|
||||
Use /tdd where possible, at pre-agreed seams.
|
||||
If the user passes a ticket reference, fetch it from the issue tracker and state its title before starting. If the reference is ambiguous, ask.
|
||||
|
||||
Call the Skill tool with "tdd" where possible, at pre-agreed seams.
|
||||
|
||||
Run typechecking regularly, single test files regularly, and the full test suite once at the end.
|
||||
|
||||
Once done, use /code-review to review the work.
|
||||
Once done, call the Skill tool with "code-review" to review the work.
|
||||
|
||||
Commit your work to the current branch.
|
||||
|
||||
@@ -7,7 +7,7 @@ description: Test-driven development. Use when the user wants to build features
|
||||
|
||||
TDD is the red → green loop. This skill is the reference that makes that loop produce tests worth keeping: what a good test is, where tests go, the anti-patterns, and the rules of the loop. Every section applies on every cycle: consult them before and during the loop, not after.
|
||||
|
||||
When exploring the codebase, read `CONTEXT.md` (if it exists) so test names and interface vocabulary match the project's domain language, and respect ADRs in the area you're touching.
|
||||
When exploring the codebase, read `GLOSSARY.md` (if it exists) so test names and interface vocabulary match the project's domain language, and respect ADRs in the area you're touching.
|
||||
|
||||
## What a good test is
|
||||
|
||||
@@ -21,7 +21,7 @@ A **seam** is the public boundary you test at: the interface where you observe b
|
||||
|
||||
**Test only at pre-agreed seams.** Before writing any test, write down the seams under test and confirm them with the user. No test is written at an unconfirmed seam. You can't test everything, so agreeing the seams up front is how testing effort lands on the critical paths and complex logic instead of every edge case.
|
||||
|
||||
Ask: "What's the public interface, and which seams should we test?"
|
||||
Ask: "What's the public interface, and which seams should we test?" Give each proposed seam a one-line note on what it catches and what it misses.
|
||||
|
||||
When the shape of that interface is itself in question (how deep the module is, where the seam belongs, what the interface should expose), call the Skill tool with "codebase-design" for the vocabulary. It is the shared source of the module, interface, depth, seam, adapter, leverage and locality terms, and it is a reference to consult, not a session to run.
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ The user has asked you to teach them something. This is a stateful request - the
|
||||
|
||||
## Teaching Workspace
|
||||
|
||||
Treat the current directory as a teaching workspace. The state of their learning is captured in this directory in several files:
|
||||
Treat the current directory as a teaching workspace. Workspace paths (`./lessons/` and the rest) resolve from the directory `/teach` was run in; only the `*-FORMAT.md` links resolve from this skill's folder. The state of their learning is captured in this directory in several files:
|
||||
|
||||
- `MISSION.md`: A document capturing the _reason_ the user is interested in the topic. This should be used to ground all teaching. Use the format in [MISSION-FORMAT.md](./MISSION-FORMAT.md).
|
||||
- `./reference/*.html`: A directory of reference materials. These are the compressed learnings from the lessons - cheat sheets, reference algorithms, syntax, yoga poses, glossaries. They are the raw units of learning. They should be beautiful documents which print out well, and are designed for quick reference.
|
||||
@@ -107,7 +107,7 @@ For skill acquisition, difficulty is the tool. Effortful retrieval is what build
|
||||
|
||||
Each of these should be based on a **feedback loop**, where the user receives feedback on their performance. This feedback loop should be as tight as possible, giving feedback immediately - and ideally automatically.
|
||||
|
||||
For quizzes, each answer should be exactly the same number of words (and characters, if possible). Don't give the user any clues about the answer through formatting.
|
||||
For quizzes, each answer should be exactly the same number of words (and characters, if possible). Vary which position holds the correct answer across questions. Don't give the user any clues about the answer through formatting or order.
|
||||
|
||||
## Acquiring Wisdom
|
||||
|
||||
|
||||
@@ -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,10 +11,6 @@ timestamp: 2026-08-28
|
||||
|
||||
# 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).
|
||||
|
||||
@@ -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.
|
||||
* [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.
|
||||
* ](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.
|
||||
|
||||
@@ -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