Compare commits

...
Author SHA1 Message Date
Frank a550db6027 Merge remote-tracking branch 'origin/main'
# Conflicts:
#	AGENTS.md
#	agent/config.yml
#	omp-docs/log.md
2026-10-18 16:47:37 -07:00
Frank 3713339ffc chore: adopt pre-existing working-tree state before trunk merge
Uncommitted machine/session edits (live config, models, docs drift) were
blocking the merge of origin/main. Committed as-is to preserve them; the
merge that follows resolves overlapping prose files toward upstream
canonical, with local variants recoverable from this commit. Pre-merge
copies also in .AGENTTRASH/pre-merge-2026-10-18/.
2026-10-18 16:46:57 -07:00
dank 5a3d563a22 Merge pull request 'docs: attribution footer for machine-opened issues and PRs' (#14) from docs/session-trailer-rule into main 2026-10-18 16:46:00 -07:00
Frank 942787f0f2 docs: pin cwd meaning in attribution footer
First dogfooding round stamped the build worktree instead of the session's
own working directory (the value the paired session id resolves to). Rule
now says so explicitly. Refs #11
2026-10-18 16:40:14 -07:00
Frank 8fb357b483 docs: attribution footer for machine-opened issues and PRs
Every agent-opened issue/PR body ends with cwd + omp session id so machine
filed artifacts trace back to the box and session that produced them - the
live-sync daemon and filing agents make 'who/what wrote this' a real
question now. Mirrored in dank/serverAppConfigs by its own PR.

Refs #11
2026-10-18 16:32:48 -07:00
dank 72bcbd989e Merge pull request 'chore: re-sync agent skills to upstream mattpocock/skills' (#13) from chore/skills-upstream-sync into main 2026-10-18 16:30:02 -07:00
dank 964bd06073 Merge pull request 'feat: live-sync heartbeat for machine-written paths' (#12) from feat/11-live-sync into main 2026-10-18 16:28:57 -07:00
Frank 54f5555470 chore: run live-sync heartbeat hourly
Operator decision at review: 15 min is aggressive for config churn; hourly
keeps the stale-window acceptable (<=1 h) with a fifth of the wakeups.
Timer unit, tier law, and failure-table wording aligned.
2026-10-18 16:28:50 -07:00
Frank e52aa6a4a3 feat: live-sync heartbeat commits machine-written paths
The checkout IS the deployment and symlinks make live state (memories,
auto-learned skills, agent config, desktop settings) dirty tracked files
continuously; no commit law covered machine churn, so it accumulated
unpushed for weeks — one disk held the only copy of ~61 managed skills,
a settings fix stayed local-only, and dirty trees refused merges.

scripts/live-sync.sh (systemd user timer, ~15 min) commits ONLY the
allowlisted machine/desktop-live tiers with hooks armed, merges
origin/main, pushes, and reaps wt/* worktrees older than 7 days. Task
dirt outside the allowlist is never staged; conflicts abort and alert
via ntfy; branch guard refuses off main; never --no-verify, never force.
Black-box contract pinned by tests/live-sync.bats (9 tests, suite 36/36).

Refs #11
2026-10-18 16:13:59 -07:00
Frank 8702f3058d chore: re-sync agent skills to upstream mattpocock/skills
Local snapshots dated 2026-09-16; upstream has moved on since. Refreshes
7 SKILL.md files (code-review, diagnosing-bugs, implement, tdd, grilling,
handoff, teach): tracker-doc references generalized, CONTEXT.md renamed
GLOSSARY.md, implement gains ticket-fetch step + Skill-tool invocation,
grilling question wording rule, handoff names the OS temp dir, teach
workspace-path resolution + quiz answer-position rules, tdd seam notes.
find-skills (vercel-labs) and prototype already current. Skill updates
take effect next session (discovery runs at startup).
2026-10-18 14:57:20 -07:00
10 changed files with 592 additions and 81 deletions

No files matched your search

+24
View File
@@ -36,6 +36,11 @@ with YAML frontmatter, spec at `omp-docs/references/okf-spec.md`).
stay visible in history. **Never merge a PR without an explicit OK from Frank.** stay visible in history. **Never merge a PR without an explicit OK from Frank.**
- **The moment a PR is open, open it in Frank's browser** — `xdg-open <PR-URL>` - **The moment a PR is open, open it in Frank's browser** — `xdg-open <PR-URL>`
lands a tab in his Firefox; never make him hunt for it in Gitea. lands a tab in his Firefox; never make him hunt for it in Gitea.
- **Attribution footer (issues and PRs)** — every issue or PR body you open ends
with a `---` line, then `cwd: <working directory> · session: <omp session id>`
(session id from `/status`; `cwd` is that session's own working directory —
not the build checkout, when they differ). Machine-made artifacts stay attributable to the
box and session that spawned them.
- Releases: tag `vX.Y.Z` straight on `main` after verifying; tags are the deploy - Releases: tag `vX.Y.Z` straight on `main` after verifying; tags are the deploy
markers every other machine pulls to. Hotfixes are ordinary branches off the markers every other machine pulls to. Hotfixes are ordinary branches off the
tag you are fixing forward from. tag you are fixing forward from.
@@ -62,6 +67,25 @@ 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
+68 -19
View File
@@ -1,5 +1,6 @@
providers: providers:
openrouterVariant: floor openrouterVariant: online
fetch: auto
modelRoles: modelRoles:
slow: llamacpp (dankclaw)/frank-dragoon:medium slow: llamacpp (dankclaw)/frank-dragoon:medium
vision: llamacpp (dankclaw-3090)/frank-zealot:xhigh vision: llamacpp (dankclaw-3090)/frank-zealot:xhigh
@@ -19,8 +20,9 @@ modelRoles:
symbolPreset: nerd symbolPreset: nerd
composer: composer:
shape: band shape: band
tokenRate: true
theme: theme:
dark: dark-terminal dark: dark-gruvbox
light: light light: light
setupVersion: 2 setupVersion: 2
browser: browser:
@@ -57,8 +59,7 @@ retry:
- openai-codex/gpt-5.5 - openai-codex/gpt-5.5
- xai-oauth/grok-4.5 - xai-oauth/grok-4.5
- web/ollama - web/ollama
speech: speech: []
[]
llamacpp (dankserver)/frank-archon: llamacpp (dankserver)/frank-archon:
- llamacpp (dankclaw)/frank-dragoon - llamacpp (dankclaw)/frank-dragoon
maxRetries: 10 maxRetries: 10
@@ -67,7 +68,7 @@ searxng:
exa: exa:
enabled: false enabled: false
memory: memory:
backend: local backend: sharpshooter
autolearn: autolearn:
enabled: true enabled: true
autoContinue: true autoContinue: true
@@ -75,12 +76,12 @@ compaction:
remoteEndpoint: http://192.168.0.143:7771/v1/chat/completions remoteEndpoint: http://192.168.0.143:7771/v1/chat/completions
methodOrder: methodOrder:
- shake - shake
- snapcompact
- soft - soft
- handoff - handoff
- snapcompact
- remote - remote
idleThresholdTokens: 100000 idleThresholdTokens: 100000
thresholdPercent: 85 thresholdPercent: 95
idleEnabled: true idleEnabled: true
thresholdTokens: -1 thresholdTokens: -1
dropUseless: false dropUseless: false
@@ -90,13 +91,12 @@ compaction:
enabled: true enabled: true
experimentalContextManagement: false experimentalContextManagement: false
interruptMode: immediate interruptMode: immediate
defaultThinkingLevel: medium defaultThinkingLevel: xhigh
hideThinkingBlock: false hideThinkingBlock: false
proseOnlyThinking: true proseOnlyThinking: false
terminal: terminal:
showProgress: true showProgress: true
disabledExtensions: disabledExtensions: []
[]
statusLine: statusLine:
separator: powerline separator: powerline
preset: custom preset: custom
@@ -135,24 +135,31 @@ tui:
codexResetFireworks: false codexResetFireworks: false
hyperlinks: auto hyperlinks: auto
tight: true tight: true
resizeScrollback: rebuild
titleSpinner: pulse
display: display:
shimmer: kitt shimmer: kitt
smoothStreaming: true smoothStreaming: true
showTokenUsage: true showTokenUsage: true
pinnedAgents: collapsed
subagentLivePreview: true
task: task:
showResolvedModelBadge: true showResolvedModelBadge: true
maxConcurrency: 3 maxConcurrency: 3
agentModelOverrides: agentModelOverrides:
designer: llamacpp (dankclaw-3090)/frank-zealot designer: llamacpp (dankclaw-3090)/frank-zealot
librarian: llamacpp (dankclaw-3090)/frank-zealot librarian: llamacpp (dankclaw-3090)/frank-zealot
reviewer: llamacpp (dankclaw-3090)/frank-zealot reviewer: llamacpp (dankserver)/frank-archon
scout: llamacpp (dankclaw-3090)/frank-zealot scout: llamacpp (dankserver)/frank-archon
security-reviewer: llamacpp (dankclaw-3090)/frank-zealot security-reviewer: llamacpp (dankclaw)/frank-dragoon
sonic: llamacpp (dankclaw-3090)/frank-zealot sonic: llamacpp (dankclaw-3090)/frank-zealot
task: llamacpp (dankclaw-3090)/frank-zealot task: llamacpp (dankserver)/frank-archon
eager: default
enableLsp: true
snapcompact: snapcompact:
systemPrompt: none systemPrompt: none
toolResults: false toolResults: false
shape: auto
extendedContext: false extendedContext: false
ttsr: ttsr:
contextMode: discard contextMode: discard
@@ -162,12 +169,12 @@ checkpoint:
enabled: true enabled: true
readLineNumbers: true readLineNumbers: true
startup: startup:
showSplash: false showSplash: true
changelogMode: summary changelogMode: summary
update: update:
channel: stable channel: stable
features: features:
unexpectedStopDetection: smart unexpectedStopDetection: mechanical
stt: stt:
submitTrigger: say-submit submitTrigger: say-submit
enabled: true enabled: true
@@ -186,6 +193,7 @@ images:
- litterbox - litterbox
edit: edit:
mode: hashline mode: hashline
fuzzyThreshold: 0.9
dev: dev:
autoqaConsent: granted autoqaConsent: granted
astGrep: astGrep:
@@ -195,7 +203,7 @@ vault:
tts: tts:
localVoice: bm_george localVoice: bm_george
speech: speech:
enabled: false enabled: true
mode: yield mode: yield
enhanced: true enhanced: true
voice: am_fenrir voice: am_fenrir
@@ -204,6 +212,9 @@ secrets:
read: read:
renderMarkdown: true renderMarkdown: true
toolResultPreview: true toolResultPreview: true
defaultLimit: 1000
summarize:
prose: false
lsp: lsp:
formatOnWrite: true formatOnWrite: true
diagnosticsOnEdit: true diagnosticsOnEdit: true
@@ -213,4 +224,42 @@ speechgen:
live: live:
voice: vale voice: vale
contextPromotion: contextPromotion:
enabled: true enabled: true
title:
icons: emoji
generator: tiny
followUpMode: one-at-a-time
tools:
approvalMode: yolo
format: auto
marketplace:
autoUpdate: notify
bash:
allowCompoundCommands: true
gitGuard: true
bashInterceptor:
enabled: false
shellMinimizer:
sourceOutlineLevel: default
generate_image:
enabled: false
computer:
enabled: true
display: active
ratchet:
enabled: true
github:
enabled: true
security:
enabled: true
todo:
reminders: true
mcp:
notifications: false
plan:
defaultOnStartup: false
isolation:
backend: auto
worktree:
onStart: create
onExit: keep
+86 -62
View File
@@ -16,9 +16,9 @@ providers:
auth: none auth: none
models: models:
- id: frank-archon - id: frank-archon
# Model id renamed from frank-primary on 2026-10-08 (official # Model id renamed from frank-archon on 2026-10-08 (official
# unit-name scheme; the systemd service, scripts and runbooks keep # unit-name scheme; the systemd service, scripts and runbooks keep
# the frank-primary name — service names are unchanged). # the frank-archon name — service names are unchanged).
# Server-side compaction routes to compaction.remoteEndpoint in # Server-side compaction routes to compaction.remoteEndpoint in
# config.yml (192.168.0.143:7771 — frank-zealot's 3090); the # config.yml (192.168.0.143:7771 — frank-zealot's 3090); the
# compactionModel candidate keeps the wire model id valid there. # compactionModel candidate keeps the wire model id valid there.
@@ -29,12 +29,12 @@ providers:
# radiance (StillDeadcode, Apache-2.0) from the digest-pinned # radiance (StillDeadcode, Apache-2.0) from the digest-pinned
# stilldeadcode/radiance image (index digest sha256:4c3f4845..., 287 # stilldeadcode/radiance image (index digest sha256:4c3f4845..., 287
# MiB, kernels prebuilt for gfx1201), plain `podman run` through # MiB, kernels prebuilt for gfx1201), plain `podman run` through
# scripts/serve-frank-primary.sh, root unit (User=dank, dank's # scripts/serve-frank-archon.sh, root unit (User=dank, dank's
# rootless podman storage). Serving the OFFICIAL # rootless podman storage). Serving the OFFICIAL
# Qwen/Qwen3.8-Flash-Next-FP8 at NATIVE modelopt block-FP8 # Qwen/Qwen3.8-Flash-Next-FP8 at NATIVE modelopt block-FP8
# precision — a rad-convert no-recipe pure copy into # precision — a rad-convert no-recipe pure copy into
# qwen4exp-fp8-native.rad (172.76 GiB measured by plan-only), built by # qwen4exp-fp8-native.rad (172.76 GiB measured by plan-only), built by
# scripts/build-frank-primary-rad.sh against a staging dir whose # scripts/build-frank-archon-rad.sh against a staging dir whose
# config.json drops quantization_config so the shipped qwen4exp # config.json drops quantization_config so the shipped qwen4exp
# plugin's (qwen4exp, "") selection key matches; per-weight # plugin's (qwen4exp, "") selection key matches; per-weight
# encodings are untouched. Experts stream from a 40 GiB/rank # encodings are untouched. Experts stream from a 40 GiB/rank
@@ -44,7 +44,7 @@ providers:
# froggeric's fixed qwen3.8 v22.5 # froggeric's fixed qwen3.8 v22.5
# (scripts/qwen38-froggeric-v22.5.jinja) via # (scripts/qwen38-froggeric-v22.5.jinja) via
# --override-chat-template. Rollback: git tag # --override-chat-template. Rollback: git tag
# frank-primary-freetoken-pr587-20261005 (FreeToken, one card). # frank-archon-freetoken-pr587-20261005 (FreeToken, one card).
contextWindow: 200000 contextWindow: 200000
# maxTokens must survive the harness compaction gap AND the chars/4 # maxTokens must survive the harness compaction gap AND the chars/4
# token-estimate under-run on code-heavy prompts. The clamp is # token-estimate under-run on code-heavy prompts. The clamp is
@@ -96,8 +96,8 @@ providers:
auth: none auth: none
models: models:
- id: frank-dragoon - id: frank-dragoon
# Model id renamed from frank-large on 2026-10-08 (official # Model id renamed from frank-dragoon on 2026-10-08 (official
# unit-name scheme; the service/unit/scripts keep the frank-large # unit-name scheme; the service/unit/scripts keep the frank-dragoon
# name — service names are unchanged). # name — service names are unchanged).
name: "frank-dragoon (dankclaw-gpu)" name: "frank-dragoon (dankclaw-gpu)"
# Server-side compaction runs on the compaction.remoteEndpoint in # Server-side compaction runs on the compaction.remoteEndpoint in
@@ -107,37 +107,45 @@ providers:
compactionModel: llamacpp (dankclaw-3090)/frank-zealot compactionModel: llamacpp (dankclaw-3090)/frank-zealot
# halogen-flash-server 0.16.2 (ghcr.io/peonist-ai/ # halogen-flash-server 0.16.2 (ghcr.io/peonist-ai/
# halogen-flash-server@sha256:0c61bf84, digest-pinned) serving the # halogen-flash-server@sha256:0c61bf84, digest-pinned) serving the
# Quat3rnion ABLITERATED Swift1.5 finetune of peonist's # Quat3rnion NATIVE v2 ABLITERATED conversion of orcarouter's pinned
# Qwen3.8-Flash-Next v2 checkpoint (qwen38-flash-next-v2-swift15- # BF16 Qwen3.8-Flash-Next (qwen38-flash-next-v2-abliterated.hgn,
# abliterated.hgn, ~62 GiB resident, + REQUIRED n-gram table + # ~62 GiB resident, + REQUIRED n-gram table + vision tower, both
# vision tower) on the Strix Halo iGPU: TWO slots at # hardlinked from the swift15 dir) on the Strix Halo iGPU: TWO slots
# HALOGEN_CTX=262144 each with the MTP head ALWAYS ON # over ONE 401,408-position KV pool (= 2 x ctx, raised 2026-10-18) at
# HALOGEN_CTX=200704 (the ceiling lowered from the card's 262,144 on
# 2026-10-17; ~10.8 GiB reserved where 200,704 cost 5.5, 262,144 cost
# 7.2 and 524,288 measured 14.6), with the MTP head ALWAYS ON
# (HALOGEN_SPEC_ADAPT=0 — the default adaptive policy halves # (HALOGEN_SPEC_ADAPT=0 — the default adaptive policy halves
# decode). Card: 53-57 t/s greedy, ~1,640 t/s prefill at 34k, ~72% # decode). Measured 2026-10-17: 50 t/s greedy decode, 1,604 t/s
# fewer thinking tokens than the censored 27B. ABLITERATED base # cold prefill at 59k, NIAH 18/18. ABLITERATED base (no refusals:
# (refusals should not return) and VISION ON (OpenAI image_url # 0/5 on the hostile census) and VISION ON (OpenAI image_url
# parts; >=256 px images or it answers "video"). Runbook: # parts; >=256 px images or it answers "video"). Runbook:
# serverAppConfigs-docs/LLM/frank-large-flash-server-v2-swift15.md. # serverAppConfigs-docs/LLM/frank-dragoon-flash-server-v2-native.md.
# Per-SLOT window the harness may fill — deliberately BELOW the # Window the harness may fill — deliberately BELOW the engine
# engine ceiling (HALOGEN_CTX=262144): the clamp below leaves a # ceiling (HALOGEN_CTX=200704, lowered from 262,144 on 2026-10-17):
# 45k gap that absorbs the estimate error. # the clamp below leaves the 45k gap that absorbs the estimate error.
contextWindow: 217088 # Both slots SHARE one 401,408-position KV pool — exactly two full
# windows — so TWO maximum-length conversations can be resident at the
# same time (at the old pool == ctx 200,704 only two of <=100k could).
# The per-request ceiling is unchanged, so this window is unchanged.
contextWindow: 155648
# The clamp is max_tokens = min(maxTokens, contextWindow − est − # The clamp is max_tokens = min(maxTokens, contextWindow − est −
# 4096); the chars/4 est UNDER-counts the rendered prompt by ~3 % # 4096); the chars/4 est UNDER-counts the rendered prompt by ~3 %
# on code/agent traffic (measured 2026-10-07: est 152,938 -> # on code/agent traffic (measured 2026-10-07: est 152,938 ->
# server 157,388). Worst-case request = ctx − 4096 + 3 % est # server 157,388). Worst-case request = window − 4096 + 3 % est
# error ≈ 219k, inside the 262,144 engine ceiling with ~43k # error ≈ 156.2k, inside the 200,704 engine ceiling with ~44k
# margin (robust to a 10 % under-count). Compaction fires at 85 % # margin (robust to a ~25 % under-count). Compaction fires at 85 %
# of the window = 184,524 est ≈ 190k measured (Frank's 180k # of the window = 132,300 est ≈ 136k measured — the 180k target of
# target) and rides on frank-zealot: input ≈ threshold − # the 262,144 era is unreachable now (the window itself is 155,648)
# keepRecent(20k) ≈ 170k, inside its 200,704 slot. # — and rides on frank-zealot: input ≈ threshold − keepRecent(20k)
# HALOGEN_MAX_TOKENS_CAP in frank-large.env must stay >= this. # ≈ 112k, inside its 200,704 slot.
# HALOGEN_MAX_TOKENS_CAP in frank-dragoon.env must stay >= this.
maxTokens: 45056 maxTokens: 45056
reasoning: true reasoning: true
# VISION ON as of 2026-10-06: the v2 vision tower loads beside the # VISION ON as of 2026-10-06: the v2 vision tower loads beside the
# checkpoint (HALOGEN_VISION_TOWER; red-PNG probe verified on the # checkpoint (HALOGEN_VISION_TOWER; red-PNG probe verified on the
# card). image_url content parts only — the legacy top-level # card). image_url content parts only — the legacy top-level
# images field is the frank-secondary trap, do not repeat it here. # images field is the frank-zealot trap, do not repeat it here.
input: [text, image] input: [text, image]
compat: compat:
# flash-server emits thinking as `reasoning_content` — same # flash-server emits thinking as `reasoning_content` — same
@@ -155,7 +163,7 @@ providers:
max: xhigh max: xhigh
# The card's thinking recipe, on the wire. The flash-server # The card's thinking recipe, on the wire. The flash-server
# engine HAS a server-side sampler (HALOGEN_TEMPERATURE/TOP_P/ # engine HAS a server-side sampler (HALOGEN_TEMPERATURE/TOP_P/
# TOP_K = 1.0 / 0.95 / 20 in frank-large.env) — these match it, # TOP_K = 1.0 / 0.95 / 20 in frank-dragoon.env) — these match it,
# so per-request values are consistent, not load-bearing. # so per-request values are consistent, not load-bearing.
extraBody: extraBody:
temperature: 1.0 temperature: 1.0
@@ -167,35 +175,47 @@ providers:
auth: none auth: none
models: models:
- id: frank-zealot - id: frank-zealot
# Model id renamed from frank-secondary on 2026-10-08 (official # Model id renamed from frank-zealot on 2026-10-08 (official
# unit-name scheme; the service/unit/scripts keep the # unit-name scheme; the service/unit/scripts keep the
# frank-secondary name — service names are unchanged). # frank-zealot name — service names are unchanged).
name: "frank-zealot (dankclaw-3090)" name: "frank-zealot (dankclaw-3090)"
# Server-side compaction routes to compaction.remoteEndpoint in # Server-side compaction routes to compaction.remoteEndpoint in
# config.yml (192.168.0.143:7771 — this endpoint); the # config.yml (192.168.0.143:7771 — this endpoint); the
# compactionModel candidate keeps the wire model id valid there. # compactionModel candidate keeps the wire model id valid there.
compactionModel: llamacpp (dankclaw-3090)/frank-zealot compactionModel: llamacpp (dankclaw-3090)/frank-zealot
# Dankclaw's RTX 3090: llama.cpp (master, CUDA sm_86 build) serving # Dankclaw's RTX 3090: llama.cpp PrismML fork build (vendor 10777,
# HauhauCS/Qwen3.5-9B-Uncensored-HauhauCS-Aggressive at Q6_K # commit e3de71522 — the ONLY loader for this payload's custom tensor
# (7,359,259,008 B) — qwen35 dense hybrid (32 layers: 8 full-attention # type 142 + prism.hadamard.* metadata, and the minimum build that
# + 24 Gated DeltaNet, 4 KV heads x 256 dim, native 262,144), TWO # pairs MTP with a quantized V cache) serving
# conversations (--ctx-size 401408 / --parallel 2) at Frank's 200k cap # prism-ml/Ternary-Bonsai-2-9B-gguf at PQ2_0 (2,675,410,720 B) WITH the
# each. Exact KV: f16 K + f16 V. Vision via the publisher's OWN # MTP head in the file — qwen35 hybrid, 32 layers (8 full-attention +
# BF16 mmproj (921,704,448 B) — the model is natively multimodal. # 24 Gated DeltaNet), 4 KV heads x 256 dim, native 262,144 — in THREE
# **MTP IS OFF** because the file carries no nextn/mtp tensors — # conversations (--ctx-size 786432 / --parallel 3), each slot at the
# SPEC_TYPE=none strips both spec flags. Priced from the MiMo Q6_K # model's TRAINED window.
# boots at this geometry (21,276/2,849 at ubatch 1024; 21,846/2,279 at # KV is q8_0 K + q8_0 V (17 KiB/token over the 8 full layers =
# 2048). 2026-10-07 (late) swap from Gemma4-26B-A4B Q4_K_M (1x200,704). # 13,056 MiB pool, ledger-exact); f16 KV would bill 26,112 MiB and
# Runbook: # cannot boot this card. Boot ledger 17,876 used / 6,249 free. Vision
# serverAppConfigs-docs/LLM/frank-secondary-llama-qwen35-9b-q6k-2x200704.md. # through the publisher's Q8_0 mmproj (624,229,664 B — Q8_0 is the
# contextWindow is the PER-SLOT window (Frank's cap): a longer # publisher's requirement for concurrent image requests), whose
# prompt needs compaction. # worst-case lands outside the engine's fit projection. **MTP OFF —
contextWindow: 200704 # measured, not assumed**: the draft context bills its own f16 KV over
# the WHOLE pool (3,072 MiB) and at ubatch 2048 that squeeze halves
# decode (167.2 → 109.2 t/s); SPEC_TYPE=draft-mtp + UBATCH_SIZE=1024 is
# the 213.5 t/s arm if the card ever has room. 2026-10-18 swap from
# Ternary-Bonsai-2-27B PQ2_0 at 2 × 200,704. Measured: 167.2 t/s
# single-stream greedy decode, 280.2 t/s across three concurrent
# streams, 2,877 t/s prefill at 106,418. Base-alignment: censored base
# lineage; unlike the 27B there is NO CRACK re-export matching this
# file. Runbook:
# serverAppConfigs-docs/LLM/frank-zealot-llama-prism-bonsai9-mtp.md.
# contextWindow is the PER-SLOT window (= the native 262,144): a longer
# prompt needs compaction — llama.cpp answers a 400, never truncates.
contextWindow: 262144
# Prompt + budget must fit inside the slot window; thinking counts # Prompt + budget must fit inside the slot window; thinking counts
# against the budget. Compaction fires at 85 % of the window = # against the budget. Compaction fires at 85 % of the window =
# 170,598 est; worst-case prompt + budget ≈ 170,598 × 1.03 + # 222,822 est; worst-case prompt + budget ≈ 222,822 × 1.03 +
# 16,384 ≈ 192,100 ≤ 200,704 — a longer prompt is a 400 naming # 16,384 ≈ 245,891 ≤ 262,144. This endpoint is the fleet's
# the limit, never a truncation. # compaction hub: the widest window in the fleet.
maxTokens: 16384 maxTokens: 16384
reasoning: true reasoning: true
input: [text, image] input: [text, image]
@@ -204,29 +224,33 @@ providers:
# `reasoning_content` — the same field the MiMo/Ornith eras used; # `reasoning_content` — the same field the MiMo/Ornith eras used;
# harness reads unchanged. # harness reads unchanged.
reasoningContentField: "reasoning_content" reasoningContentField: "reasoning_content"
# The Qwen3.5 chat template has NO reasoning_effort variable (unlike # Bonsai-2's template DOES read reasoning_effort (the Qwen3.5-9B
# the Qwen3.8-template endpoints, where an unmapped level is a hard # template did not — the map used to be inert here): it accepts ONLY
# Jinja raise). llama.cpp passes chat_template_kwargs through to # xhigh (default), medium, low and calls raise_exception on anything
# Jinja, and Jinja ignores variables a template never reads — so the # else, so an unmapped harness level is a hard HTTP 500 on the role's
# map is inert here, kept so role-level overrides resolve identically # first request, naming Jinja and not the model. The fold below is
# across the fleet. # therefore load-bearing, identical to the frank-archon/dragoon law.
# llama.cpp passes chat_template_kwargs through to Jinja.
reasoningEffortMap: reasoningEffortMap:
minimal: low minimal: low
low: low low: low
medium: medium medium: medium
high: xhigh high: xhigh
max: xhigh max: xhigh
# The Qwen3.5 publisher recipe (model card, thinking mode): # The payload's OWN sampling metadata (general.sampling.* in the
# temperature 0.6 / top_p 0.95 / top_k 20 / min_p 0. Non-thinking # GGUF): temperature 1.0 / top_p 0.95 / top_k 20 — thinking-mode
# mode is 0.7/0.8/20/0 — the harness runs thinking mode. # recipe, the same trio the other Qwen3.8-family endpoints carry.
# (The old 0.6 was the Qwen3.5-9B publisher's card, not this file.)
# extraBody is merged last and wins over the harness default. # extraBody is merged last and wins over the harness default.
extraBody: extraBody:
temperature: 0.6 temperature: 1.0
top_p: 0.95 top_p: 0.95
top_k: 20 top_k: 20
min_p: 0.0 min_p: 0.0
presence_penalty: 0.0 presence_penalty: 0.0
chat_template_kwargs: chat_template_kwargs:
enable_thinking: true enable_thinking: true
# (no preserve_thinking: that kwarg belongs to the Qwen3.8 # The template DOES read preserve_thinking: when false it drops
# templates; nothing in this template reads it) # prior assistants' <think> blocks from history. Keep them —
# multi-turn tool traffic on this endpoint is the majority.
preserve_thinking: true
@@ -0,0 +1,12 @@
[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
@@ -0,0 +1,11 @@
[Unit]
Description=command-center live-sync heartbeat every hour
[Timer]
OnBootSec=5min
OnUnitActiveSec=1h
RandomizedDelaySec=3min
Persistent=true
[Install]
WantedBy=timers.target
+8
View File
@@ -11,6 +11,14 @@ timestamp: 2026-08-28
# Bundle Change Log # Bundle Change Log
## 2026-10-18 — Fleet law: issue/PR attribution footers
Every agent-opened issue and PR body now ends with a `cwd:` + `session:` footer (session id from `/status`), in AGENTS.md of both `dank/command-center` and `dank/serverAppConfigs` (mirrored PRs). Born from the live-sync review: once the machine starts filing and committing on its own, its artifacts must trace back to the box and session that produced them.
## 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 ## 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). 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).
+1
View File
@@ -8,6 +8,7 @@ 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.
+75
View File
@@ -0,0 +1,75 @@
---
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 |
+131
View File
@@ -0,0 +1,131 @@
#!/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
+176
View File
@@ -0,0 +1,176 @@
#!/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 ]
}