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.**
- **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
View File
@@ -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
View File
@@ -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
+8
View File
@@ -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).
+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.
* [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.
+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 ]
}