Compare commits
10
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a550db6027 | ||
|
|
3713339ffc | ||
|
|
5a3d563a22 | ||
|
|
942787f0f2 | ||
|
|
8fb357b483 | ||
|
|
72bcbd989e | ||
|
|
964bd06073 | ||
|
|
54f5555470 | ||
|
|
e52aa6a4a3 | ||
|
|
8702f3058d |
No files matched your search
@@ -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.**
|
||||
- **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.
|
||||
- **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
|
||||
markers every other machine pulls to. Hotfixes are ordinary branches off the
|
||||
tag you are fixing forward from.
|
||||
@@ -62,6 +67,25 @@ 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
|
||||
|
||||
+68
-19
@@ -1,5 +1,6 @@
|
||||
providers:
|
||||
openrouterVariant: floor
|
||||
openrouterVariant: online
|
||||
fetch: auto
|
||||
modelRoles:
|
||||
slow: llamacpp (dankclaw)/frank-dragoon:medium
|
||||
vision: llamacpp (dankclaw-3090)/frank-zealot:xhigh
|
||||
@@ -19,8 +20,9 @@ modelRoles:
|
||||
symbolPreset: nerd
|
||||
composer:
|
||||
shape: band
|
||||
tokenRate: true
|
||||
theme:
|
||||
dark: dark-terminal
|
||||
dark: dark-gruvbox
|
||||
light: light
|
||||
setupVersion: 2
|
||||
browser:
|
||||
@@ -57,8 +59,7 @@ retry:
|
||||
- openai-codex/gpt-5.5
|
||||
- xai-oauth/grok-4.5
|
||||
- web/ollama
|
||||
speech:
|
||||
[]
|
||||
speech: []
|
||||
llamacpp (dankserver)/frank-archon:
|
||||
- llamacpp (dankclaw)/frank-dragoon
|
||||
maxRetries: 10
|
||||
@@ -67,7 +68,7 @@ searxng:
|
||||
exa:
|
||||
enabled: false
|
||||
memory:
|
||||
backend: local
|
||||
backend: sharpshooter
|
||||
autolearn:
|
||||
enabled: true
|
||||
autoContinue: true
|
||||
@@ -75,12 +76,12 @@ compaction:
|
||||
remoteEndpoint: http://192.168.0.143:7771/v1/chat/completions
|
||||
methodOrder:
|
||||
- shake
|
||||
- snapcompact
|
||||
- soft
|
||||
- handoff
|
||||
- snapcompact
|
||||
- remote
|
||||
idleThresholdTokens: 100000
|
||||
thresholdPercent: 85
|
||||
thresholdPercent: 95
|
||||
idleEnabled: true
|
||||
thresholdTokens: -1
|
||||
dropUseless: false
|
||||
@@ -90,13 +91,12 @@ compaction:
|
||||
enabled: true
|
||||
experimentalContextManagement: false
|
||||
interruptMode: immediate
|
||||
defaultThinkingLevel: medium
|
||||
defaultThinkingLevel: xhigh
|
||||
hideThinkingBlock: false
|
||||
proseOnlyThinking: true
|
||||
proseOnlyThinking: false
|
||||
terminal:
|
||||
showProgress: true
|
||||
disabledExtensions:
|
||||
[]
|
||||
disabledExtensions: []
|
||||
statusLine:
|
||||
separator: powerline
|
||||
preset: custom
|
||||
@@ -135,24 +135,31 @@ tui:
|
||||
codexResetFireworks: false
|
||||
hyperlinks: auto
|
||||
tight: true
|
||||
resizeScrollback: rebuild
|
||||
titleSpinner: pulse
|
||||
display:
|
||||
shimmer: kitt
|
||||
smoothStreaming: true
|
||||
showTokenUsage: true
|
||||
pinnedAgents: collapsed
|
||||
subagentLivePreview: true
|
||||
task:
|
||||
showResolvedModelBadge: true
|
||||
maxConcurrency: 3
|
||||
agentModelOverrides:
|
||||
designer: llamacpp (dankclaw-3090)/frank-zealot
|
||||
librarian: llamacpp (dankclaw-3090)/frank-zealot
|
||||
reviewer: llamacpp (dankclaw-3090)/frank-zealot
|
||||
scout: llamacpp (dankclaw-3090)/frank-zealot
|
||||
security-reviewer: llamacpp (dankclaw-3090)/frank-zealot
|
||||
reviewer: llamacpp (dankserver)/frank-archon
|
||||
scout: llamacpp (dankserver)/frank-archon
|
||||
security-reviewer: llamacpp (dankclaw)/frank-dragoon
|
||||
sonic: llamacpp (dankclaw-3090)/frank-zealot
|
||||
task: llamacpp (dankclaw-3090)/frank-zealot
|
||||
task: llamacpp (dankserver)/frank-archon
|
||||
eager: default
|
||||
enableLsp: true
|
||||
snapcompact:
|
||||
systemPrompt: none
|
||||
toolResults: false
|
||||
shape: auto
|
||||
extendedContext: false
|
||||
ttsr:
|
||||
contextMode: discard
|
||||
@@ -162,12 +169,12 @@ checkpoint:
|
||||
enabled: true
|
||||
readLineNumbers: true
|
||||
startup:
|
||||
showSplash: false
|
||||
showSplash: true
|
||||
changelogMode: summary
|
||||
update:
|
||||
channel: stable
|
||||
features:
|
||||
unexpectedStopDetection: smart
|
||||
unexpectedStopDetection: mechanical
|
||||
stt:
|
||||
submitTrigger: say-submit
|
||||
enabled: true
|
||||
@@ -186,6 +193,7 @@ images:
|
||||
- litterbox
|
||||
edit:
|
||||
mode: hashline
|
||||
fuzzyThreshold: 0.9
|
||||
dev:
|
||||
autoqaConsent: granted
|
||||
astGrep:
|
||||
@@ -195,7 +203,7 @@ vault:
|
||||
tts:
|
||||
localVoice: bm_george
|
||||
speech:
|
||||
enabled: false
|
||||
enabled: true
|
||||
mode: yield
|
||||
enhanced: true
|
||||
voice: am_fenrir
|
||||
@@ -204,6 +212,9 @@ secrets:
|
||||
read:
|
||||
renderMarkdown: true
|
||||
toolResultPreview: true
|
||||
defaultLimit: 1000
|
||||
summarize:
|
||||
prose: false
|
||||
lsp:
|
||||
formatOnWrite: true
|
||||
diagnosticsOnEdit: true
|
||||
@@ -213,4 +224,42 @@ speechgen:
|
||||
live:
|
||||
voice: vale
|
||||
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
@@ -16,9 +16,9 @@ providers:
|
||||
auth: none
|
||||
models:
|
||||
- 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
|
||||
# the frank-primary name — service names are unchanged).
|
||||
# the frank-archon name — service names are unchanged).
|
||||
# Server-side compaction routes to compaction.remoteEndpoint in
|
||||
# config.yml (192.168.0.143:7771 — frank-zealot's 3090); the
|
||||
# compactionModel candidate keeps the wire model id valid there.
|
||||
@@ -29,12 +29,12 @@ providers:
|
||||
# radiance (StillDeadcode, Apache-2.0) from the digest-pinned
|
||||
# stilldeadcode/radiance image (index digest sha256:4c3f4845..., 287
|
||||
# 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
|
||||
# Qwen/Qwen3.8-Flash-Next-FP8 at NATIVE modelopt block-FP8
|
||||
# precision — a rad-convert no-recipe pure copy into
|
||||
# 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
|
||||
# plugin's (qwen4exp, "") selection key matches; per-weight
|
||||
# encodings are untouched. Experts stream from a 40 GiB/rank
|
||||
@@ -44,7 +44,7 @@ providers:
|
||||
# froggeric's fixed qwen3.8 v22.5
|
||||
# (scripts/qwen38-froggeric-v22.5.jinja) via
|
||||
# --override-chat-template. Rollback: git tag
|
||||
# frank-primary-freetoken-pr587-20261005 (FreeToken, one card).
|
||||
# frank-archon-freetoken-pr587-20261005 (FreeToken, one card).
|
||||
contextWindow: 200000
|
||||
# maxTokens must survive the harness compaction gap AND the chars/4
|
||||
# token-estimate under-run on code-heavy prompts. The clamp is
|
||||
@@ -96,8 +96,8 @@ providers:
|
||||
auth: none
|
||||
models:
|
||||
- id: frank-dragoon
|
||||
# Model id renamed from frank-large on 2026-10-08 (official
|
||||
# unit-name scheme; the service/unit/scripts keep the frank-large
|
||||
# Model id renamed from frank-dragoon on 2026-10-08 (official
|
||||
# unit-name scheme; the service/unit/scripts keep the frank-dragoon
|
||||
# name — service names are unchanged).
|
||||
name: "frank-dragoon (dankclaw-gpu)"
|
||||
# Server-side compaction runs on the compaction.remoteEndpoint in
|
||||
@@ -107,37 +107,45 @@ providers:
|
||||
compactionModel: llamacpp (dankclaw-3090)/frank-zealot
|
||||
# halogen-flash-server 0.16.2 (ghcr.io/peonist-ai/
|
||||
# halogen-flash-server@sha256:0c61bf84, digest-pinned) serving the
|
||||
# Quat3rnion ABLITERATED Swift1.5 finetune of peonist's
|
||||
# Qwen3.8-Flash-Next v2 checkpoint (qwen38-flash-next-v2-swift15-
|
||||
# abliterated.hgn, ~62 GiB resident, + REQUIRED n-gram table +
|
||||
# vision tower) on the Strix Halo iGPU: TWO slots at
|
||||
# HALOGEN_CTX=262144 each with the MTP head ALWAYS ON
|
||||
# Quat3rnion NATIVE v2 ABLITERATED conversion of orcarouter's pinned
|
||||
# BF16 Qwen3.8-Flash-Next (qwen38-flash-next-v2-abliterated.hgn,
|
||||
# ~62 GiB resident, + REQUIRED n-gram table + vision tower, both
|
||||
# hardlinked from the swift15 dir) on the Strix Halo iGPU: TWO slots
|
||||
# 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
|
||||
# decode). Card: 53-57 t/s greedy, ~1,640 t/s prefill at 34k, ~72%
|
||||
# fewer thinking tokens than the censored 27B. ABLITERATED base
|
||||
# (refusals should not return) and VISION ON (OpenAI image_url
|
||||
# decode). Measured 2026-10-17: 50 t/s greedy decode, 1,604 t/s
|
||||
# cold prefill at 59k, NIAH 18/18. ABLITERATED base (no refusals:
|
||||
# 0/5 on the hostile census) and VISION ON (OpenAI image_url
|
||||
# parts; >=256 px images or it answers "video"). Runbook:
|
||||
# serverAppConfigs-docs/LLM/frank-large-flash-server-v2-swift15.md.
|
||||
# Per-SLOT window the harness may fill — deliberately BELOW the
|
||||
# engine ceiling (HALOGEN_CTX=262144): the clamp below leaves a
|
||||
# 45k gap that absorbs the estimate error.
|
||||
contextWindow: 217088
|
||||
# serverAppConfigs-docs/LLM/frank-dragoon-flash-server-v2-native.md.
|
||||
# Window the harness may fill — deliberately BELOW the engine
|
||||
# ceiling (HALOGEN_CTX=200704, lowered from 262,144 on 2026-10-17):
|
||||
# the clamp below leaves the 45k gap that absorbs the estimate error.
|
||||
# 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 −
|
||||
# 4096); the chars/4 est UNDER-counts the rendered prompt by ~3 %
|
||||
# on code/agent traffic (measured 2026-10-07: est 152,938 ->
|
||||
# server 157,388). Worst-case request = ctx − 4096 + 3 % est
|
||||
# error ≈ 219k, inside the 262,144 engine ceiling with ~43k
|
||||
# margin (robust to a 10 % under-count). Compaction fires at 85 %
|
||||
# of the window = 184,524 est ≈ 190k measured (Frank's 180k
|
||||
# target) and rides on frank-zealot: input ≈ threshold −
|
||||
# keepRecent(20k) ≈ 170k, inside its 200,704 slot.
|
||||
# HALOGEN_MAX_TOKENS_CAP in frank-large.env must stay >= this.
|
||||
# server 157,388). Worst-case request = window − 4096 + 3 % est
|
||||
# error ≈ 156.2k, inside the 200,704 engine ceiling with ~44k
|
||||
# margin (robust to a ~25 % under-count). Compaction fires at 85 %
|
||||
# of the window = 132,300 est ≈ 136k measured — the 180k target of
|
||||
# the 262,144 era is unreachable now (the window itself is 155,648)
|
||||
# — and rides on frank-zealot: input ≈ threshold − keepRecent(20k)
|
||||
# ≈ 112k, inside its 200,704 slot.
|
||||
# HALOGEN_MAX_TOKENS_CAP in frank-dragoon.env must stay >= this.
|
||||
maxTokens: 45056
|
||||
reasoning: true
|
||||
# VISION ON as of 2026-10-06: the v2 vision tower loads beside the
|
||||
# checkpoint (HALOGEN_VISION_TOWER; red-PNG probe verified on the
|
||||
# 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]
|
||||
compat:
|
||||
# flash-server emits thinking as `reasoning_content` — same
|
||||
@@ -155,7 +163,7 @@ providers:
|
||||
max: xhigh
|
||||
# The card's thinking recipe, on the wire. The flash-server
|
||||
# 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.
|
||||
extraBody:
|
||||
temperature: 1.0
|
||||
@@ -167,35 +175,47 @@ providers:
|
||||
auth: none
|
||||
models:
|
||||
- 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
|
||||
# frank-secondary name — service names are unchanged).
|
||||
# frank-zealot name — service names are unchanged).
|
||||
name: "frank-zealot (dankclaw-3090)"
|
||||
# Server-side compaction routes to compaction.remoteEndpoint in
|
||||
# config.yml (192.168.0.143:7771 — this endpoint); the
|
||||
# compactionModel candidate keeps the wire model id valid there.
|
||||
compactionModel: llamacpp (dankclaw-3090)/frank-zealot
|
||||
# Dankclaw's RTX 3090: llama.cpp (master, CUDA sm_86 build) serving
|
||||
# HauhauCS/Qwen3.5-9B-Uncensored-HauhauCS-Aggressive at Q6_K
|
||||
# (7,359,259,008 B) — qwen35 dense hybrid (32 layers: 8 full-attention
|
||||
# + 24 Gated DeltaNet, 4 KV heads x 256 dim, native 262,144), TWO
|
||||
# conversations (--ctx-size 401408 / --parallel 2) at Frank's 200k cap
|
||||
# each. Exact KV: f16 K + f16 V. Vision via the publisher's OWN
|
||||
# BF16 mmproj (921,704,448 B) — the model is natively multimodal.
|
||||
# **MTP IS OFF** because the file carries no nextn/mtp tensors —
|
||||
# SPEC_TYPE=none strips both spec flags. Priced from the MiMo Q6_K
|
||||
# boots at this geometry (21,276/2,849 at ubatch 1024; 21,846/2,279 at
|
||||
# 2048). 2026-10-07 (late) swap from Gemma4-26B-A4B Q4_K_M (1x200,704).
|
||||
# Runbook:
|
||||
# serverAppConfigs-docs/LLM/frank-secondary-llama-qwen35-9b-q6k-2x200704.md.
|
||||
# contextWindow is the PER-SLOT window (Frank's cap): a longer
|
||||
# prompt needs compaction.
|
||||
contextWindow: 200704
|
||||
# Dankclaw's RTX 3090: llama.cpp PrismML fork build (vendor 10777,
|
||||
# commit e3de71522 — the ONLY loader for this payload's custom tensor
|
||||
# type 142 + prism.hadamard.* metadata, and the minimum build that
|
||||
# pairs MTP with a quantized V cache) serving
|
||||
# prism-ml/Ternary-Bonsai-2-9B-gguf at PQ2_0 (2,675,410,720 B) WITH the
|
||||
# MTP head in the file — qwen35 hybrid, 32 layers (8 full-attention +
|
||||
# 24 Gated DeltaNet), 4 KV heads x 256 dim, native 262,144 — in THREE
|
||||
# conversations (--ctx-size 786432 / --parallel 3), each slot at the
|
||||
# model's TRAINED window.
|
||||
# KV is q8_0 K + q8_0 V (17 KiB/token over the 8 full layers =
|
||||
# 13,056 MiB pool, ledger-exact); f16 KV would bill 26,112 MiB and
|
||||
# cannot boot this card. Boot ledger 17,876 used / 6,249 free. Vision
|
||||
# through the publisher's Q8_0 mmproj (624,229,664 B — Q8_0 is the
|
||||
# publisher's requirement for concurrent image requests), whose
|
||||
# worst-case lands outside the engine's fit projection. **MTP OFF —
|
||||
# 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
|
||||
# against the budget. Compaction fires at 85 % of the window =
|
||||
# 170,598 est; worst-case prompt + budget ≈ 170,598 × 1.03 +
|
||||
# 16,384 ≈ 192,100 ≤ 200,704 — a longer prompt is a 400 naming
|
||||
# the limit, never a truncation.
|
||||
# 222,822 est; worst-case prompt + budget ≈ 222,822 × 1.03 +
|
||||
# 16,384 ≈ 245,891 ≤ 262,144. This endpoint is the fleet's
|
||||
# compaction hub: the widest window in the fleet.
|
||||
maxTokens: 16384
|
||||
reasoning: true
|
||||
input: [text, image]
|
||||
@@ -204,29 +224,33 @@ providers:
|
||||
# `reasoning_content` — the same field the MiMo/Ornith eras used;
|
||||
# harness reads unchanged.
|
||||
reasoningContentField: "reasoning_content"
|
||||
# The Qwen3.5 chat template has NO reasoning_effort variable (unlike
|
||||
# the Qwen3.8-template endpoints, where an unmapped level is a hard
|
||||
# Jinja raise). llama.cpp passes chat_template_kwargs through to
|
||||
# Jinja, and Jinja ignores variables a template never reads — so the
|
||||
# map is inert here, kept so role-level overrides resolve identically
|
||||
# across the fleet.
|
||||
# Bonsai-2's template DOES read reasoning_effort (the Qwen3.5-9B
|
||||
# template did not — the map used to be inert here): it accepts ONLY
|
||||
# xhigh (default), medium, low and calls raise_exception on anything
|
||||
# else, so an unmapped harness level is a hard HTTP 500 on the role's
|
||||
# first request, naming Jinja and not the model. The fold below is
|
||||
# therefore load-bearing, identical to the frank-archon/dragoon law.
|
||||
# llama.cpp passes chat_template_kwargs through to Jinja.
|
||||
reasoningEffortMap:
|
||||
minimal: low
|
||||
low: low
|
||||
medium: medium
|
||||
high: xhigh
|
||||
max: xhigh
|
||||
# The Qwen3.5 publisher recipe (model card, thinking mode):
|
||||
# temperature 0.6 / top_p 0.95 / top_k 20 / min_p 0. Non-thinking
|
||||
# mode is 0.7/0.8/20/0 — the harness runs thinking mode.
|
||||
# The payload's OWN sampling metadata (general.sampling.* in the
|
||||
# GGUF): temperature 1.0 / top_p 0.95 / top_k 20 — 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:
|
||||
temperature: 0.6
|
||||
temperature: 1.0
|
||||
top_p: 0.95
|
||||
top_k: 20
|
||||
min_p: 0.0
|
||||
presence_penalty: 0.0
|
||||
chat_template_kwargs:
|
||||
enable_thinking: true
|
||||
# (no preserve_thinking: that kwarg belongs to the Qwen3.8
|
||||
# templates; nothing in this template reads it)
|
||||
# The template DOES read preserve_thinking: when false it drops
|
||||
# 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
|
||||
@@ -11,6 +11,14 @@ timestamp: 2026-08-28
|
||||
|
||||
# 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
|
||||
|
||||
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,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.
|
||||
* [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.
|
||||
|
||||
@@ -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 |
|
||||
Executable
+131
@@ -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
|
||||
Executable
+176
@@ -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 ]
|
||||
}
|
||||
Reference in new issue
Block a user