Compare commits
No commits in common. "a0762c563ee5105ea29dbf9595590b9f7592a571" and "5d14efc864ed15a3963752f3792ab8170e16e31a" have entirely different histories.
a0762c563e
...
5d14efc864
15 changed files with 179 additions and 531 deletions
|
|
@ -6,12 +6,7 @@
|
||||||
# 1. The execution-mode menu — writing-plans / subagent-driven-development script a
|
# 1. The execution-mode menu — writing-plans / subagent-driven-development script a
|
||||||
# "Subagent-Driven vs Inline Execution — which approach?" menu at the plan→execution
|
# "Subagent-Driven vs Inline Execution — which approach?" menu at the plan→execution
|
||||||
# handoff. boma's standing preference is to NEVER present it and proceed
|
# handoff. boma's standing preference is to NEVER present it and proceed
|
||||||
# subagent-driven. (Recorded by the 2026-06-10 kaizen review; the 2026-06-17 review
|
# subagent-driven. (Recorded by the 2026-06-10 kaizen review.)
|
||||||
# widened the matcher to also catch free-form *prose* re-asks of the same choice —
|
|
||||||
# e.g. "which execution approach?" — which the literal-menu matcher missed. The
|
|
||||||
# sibling push-vs-not re-ask is deliberately NOT hooked: a genuine "should I push?"
|
|
||||||
# is sometimes legitimate, so it stays a soft default via the
|
|
||||||
# dont-reask-settled-defaults memory rather than a hard block.)
|
|
||||||
# 2. The brainstorming spec-review gate — the brainstorming skill scripts "Spec written
|
# 2. The brainstorming spec-review gate — the brainstorming skill scripts "Spec written
|
||||||
# and committed … please review it before … the implementation plan." The standing
|
# and committed … please review it before … the implementation plan." The standing
|
||||||
# agreement is to move directly from the committed spec to writing-plans. (Recorded
|
# agreement is to move directly from the committed spec to writing-plans. (Recorded
|
||||||
|
|
@ -44,11 +39,7 @@ text=$(jq -rs '
|
||||||
low="${text,,}"
|
low="${text,,}"
|
||||||
|
|
||||||
if [[ "$low" == *"inline execution"* \
|
if [[ "$low" == *"inline execution"* \
|
||||||
&& ( "$low" == *"which approach"* || "$low" == *"two execution options"* ) ]] \
|
&& ( "$low" == *"which approach"* || "$low" == *"two execution options"* ) ]]; then
|
||||||
|| [[ "$low" == *"subagent-driven or inline"* || "$low" == *"inline or subagent"* ]] \
|
|
||||||
|| [[ "$low" == *"subagent-driven vs inline"* || "$low" == *"subagent vs inline"* \
|
|
||||||
|| "$low" == *"inline vs subagent"* ]] \
|
|
||||||
|| [[ "$low" == *"execution approach"* && "$low" == *"?"* ]]; then
|
|
||||||
cat <<'JSON'
|
cat <<'JSON'
|
||||||
{"decision":"block","reason":"Execution-mode menu detected in your final message. boma standing preference (docs/FRICTION.md + always-subagent-driven-execution memory): never present the subagent-driven-vs-inline menu. Drop the menu and proceed with subagent-driven execution directly (superpowers:subagent-driven-development)."}
|
{"decision":"block","reason":"Execution-mode menu detected in your final message. boma standing preference (docs/FRICTION.md + always-subagent-driven-execution memory): never present the subagent-driven-vs-inline menu. Drop the menu and proceed with subagent-driven execution directly (superpowers:subagent-driven-development)."}
|
||||||
JSON
|
JSON
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,12 @@
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
#
|
#
|
||||||
# PreToolUse guard (Bash): block `git commit` ONLY when the rbw vault agent is locked
|
# PreToolUse guard (Bash): block `git commit` when the rbw vault agent is locked.
|
||||||
# AND the commit would actually need the vault. The pre-commit ansible-lint hook decrypts
|
# The pre-commit ansible-lint hook decrypts vault.yml via rbw, so a commit while
|
||||||
# vault.yml via rbw — but it is scoped (`files: ^(roles|playbooks|inventories)/.*\.ya?ml$`,
|
# locked fails deep with a confusing error. This catches it early with a clear fix.
|
||||||
# always_run:false), so a docs-/config-only commit never triggers it and needs no vault.
|
|
||||||
# (2026-06-17 kaizen, docs/FRICTION.md: the old guard blocked *every* locked commit, so a
|
|
||||||
# docs-only commit got snagged needing a vault password it never uses.)
|
|
||||||
#
|
#
|
||||||
# Fails OPEN: blocks only on a definitive "Ansible content staged AND rbw locked" signal.
|
# Fails OPEN: only blocks on a definitive "rbw present AND not unlocked" signal.
|
||||||
# rbw missing, not a plain `git commit`, `--no-verify`, or no Ansible content staged → allow.
|
# If rbw is missing, the command isn't a plain `git commit`, or `--no-verify` is
|
||||||
# When unsure it errs toward blocking (asking for an unlock is cheap; a deep pre-commit
|
# used, the action is allowed.
|
||||||
# failure is not).
|
|
||||||
#
|
#
|
||||||
set -uo pipefail
|
set -uo pipefail
|
||||||
|
|
||||||
|
|
@ -26,25 +22,14 @@ case "$cmd" in
|
||||||
esac
|
esac
|
||||||
|
|
||||||
command -v rbw >/dev/null 2>&1 || exit 0 # rbw not installed — allow
|
command -v rbw >/dev/null 2>&1 || exit 0 # rbw not installed — allow
|
||||||
rbw unlocked >/dev/null 2>&1 && exit 0 # unlocked — allow
|
|
||||||
|
|
||||||
# rbw is LOCKED. Only block if this commit would run the vault-decrypting ansible-lint
|
if rbw unlocked >/dev/null 2>&1; then
|
||||||
# hook — i.e. staged content matches its `files:` scope. Mirror that regex exactly.
|
exit 0 # unlocked — allow
|
||||||
ANSIBLE_RE='^(roles|playbooks|inventories)/.*\.ya?ml$'
|
fi
|
||||||
|
|
||||||
cd "${CLAUDE_PROJECT_DIR:-.}" 2>/dev/null || exit 0
|
# rbw present but not unlocked (locked or agent not running) — the commit would
|
||||||
files=$(git diff --cached --name-only 2>/dev/null) || exit 0
|
# fail in the pre-commit hook, so block early with guidance.
|
||||||
# `git commit -a/--all` also sweeps in modified tracked files that aren't staged yet.
|
|
||||||
# (Substring match — errs toward including them, which only ever over-blocks. Safe.)
|
|
||||||
case " $cmd " in
|
|
||||||
*" -a"*|*"--all"*) files="$files"$'\n'"$(git diff --name-only 2>/dev/null)" ;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
# No Ansible content in the fileset → ansible-lint hook won't run → no vault needed → allow.
|
|
||||||
printf '%s\n' "$files" | grep -Eq "$ANSIBLE_RE" || exit 0
|
|
||||||
|
|
||||||
# Ansible content staged AND rbw locked — the commit would fail deep in pre-commit. Block.
|
|
||||||
cat <<'JSON'
|
cat <<'JSON'
|
||||||
{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":"rbw is locked and this commit stages Ansible content — the pre-commit ansible-lint hook needs the vault password to decrypt vault.yml. Run: rbw unlock (docs-/config-only commits are exempt and won't hit this guard.)"}}
|
{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":"rbw is locked — the pre-commit ansible-lint hook needs the vault password to decrypt vault.yml. Run: rbw unlock"}}
|
||||||
JSON
|
JSON
|
||||||
exit 0
|
exit 0
|
||||||
|
|
|
||||||
|
|
@ -69,10 +69,5 @@
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
|
||||||
"statusLine": {
|
|
||||||
"type": "command",
|
|
||||||
"command": "bash \"${CLAUDE_PROJECT_DIR:-.}/.claude/statusline.sh\"",
|
|
||||||
"padding": 0
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,63 +0,0 @@
|
||||||
#!/usr/bin/env bash
|
|
||||||
#
|
|
||||||
# Claude Code statusLine — shows working dir, model, and context-window usage.
|
|
||||||
# Wired via .claude/settings.json (statusLine.command). Receives the statusLine
|
|
||||||
# JSON on stdin; first stdout line is rendered (ANSI colour supported).
|
|
||||||
#
|
|
||||||
# Context usage comes straight from the input JSON — no transcript parsing:
|
|
||||||
# .context_window.used_percentage pre-calculated % of the window in use (input side)
|
|
||||||
# .context_window.context_window_size window size in tokens (1000000 for the 1M models)
|
|
||||||
# verified: Claude Code statusLine schema · code.claude.com/docs/en/statusline · 2026-06-17
|
|
||||||
#
|
|
||||||
# Fails soft: any parse problem prints nothing and exits 0 (never breaks the prompt).
|
|
||||||
set -uo pipefail
|
|
||||||
|
|
||||||
input=$(cat 2>/dev/null) || exit 0
|
|
||||||
command -v jq >/dev/null 2>&1 || exit 0
|
|
||||||
|
|
||||||
# pct<TAB>window<TAB>dir-basename<TAB>model-name (used_percentage preferred,
|
|
||||||
# else derived from current_usage, else 0). @tsv keeps spaces in the dir safe.
|
|
||||||
parsed=$(printf '%s' "$input" | jq -r '
|
|
||||||
(.workspace.current_dir // .cwd // "" | sub(".*/"; "")) as $dir
|
|
||||||
| (.model.display_name // "?") as $model
|
|
||||||
| (.context_window.context_window_size // 200000) as $win
|
|
||||||
| (
|
|
||||||
if (.context_window.used_percentage // null) != null then
|
|
||||||
.context_window.used_percentage
|
|
||||||
elif (.context_window.current_usage // null) != null then
|
|
||||||
((.context_window.current_usage.input_tokens
|
|
||||||
+ (.context_window.current_usage.cache_creation_input_tokens // 0)
|
|
||||||
+ (.context_window.current_usage.cache_read_input_tokens // 0)) / $win * 100)
|
|
||||||
else 0 end | floor
|
|
||||||
) as $pct
|
|
||||||
| [$pct, $win, $dir, $model] | @tsv
|
|
||||||
' 2>/dev/null) || exit 0
|
|
||||||
[ -z "$parsed" ] && exit 0
|
|
||||||
|
|
||||||
IFS=$'\t' read -r pct win dir model <<<"$parsed"
|
|
||||||
|
|
||||||
# Human window label: 1000000 -> 1M, 200000 -> 200k, else Nk.
|
|
||||||
case "$win" in
|
|
||||||
1000000) wlabel="1M" ;;
|
|
||||||
*) wlabel="$((win / 1000))k" ;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
# Colour the bar/percentage by pressure: green <70, yellow 70–89, red >=90.
|
|
||||||
if [ "$pct" -ge 90 ]; then col=$'\033[31m' # red
|
|
||||||
elif [ "$pct" -ge 70 ]; then col=$'\033[33m' # yellow
|
|
||||||
else col=$'\033[32m' # green
|
|
||||||
fi
|
|
||||||
dim=$'\033[2m'; rst=$'\033[0m'
|
|
||||||
|
|
||||||
# 10-cell bar; clamp fill to [0,10] so an over-100 reading can't overflow.
|
|
||||||
filled=$((pct / 10)); [ "$filled" -gt 10 ] && filled=10; [ "$filled" -lt 0 ] && filled=0
|
|
||||||
bar=""
|
|
||||||
for ((i = 0; i < 10; i++)); do
|
|
||||||
if [ "$i" -lt "$filled" ]; then bar+="█"; else bar+="░"; fi
|
|
||||||
done
|
|
||||||
|
|
||||||
printf '%s%s%s · %s · %s%s %d%%%s %sctx/%s%s\n' \
|
|
||||||
"$dim" "$dir" "$rst" \
|
|
||||||
"$model" \
|
|
||||||
"$col" "$bar" "$pct" "$rst" \
|
|
||||||
"$dim" "$wlabel" "$rst"
|
|
||||||
15
Makefile
15
Makefile
|
|
@ -23,11 +23,6 @@ MOLECULE_DOCKERFILE := .docker/molecule-debian13/Dockerfile
|
||||||
# (the Go module proxy 403s Hetzner IPs); push the pinned tag to the Forgejo registry.
|
# (the Go module proxy 403s Hetzner IPs); push the pinned tag to the Forgejo registry.
|
||||||
CADDY_IMAGE := forgejo.nyumbani.baobab.band/sjat/caddy-gandi:2.11.4
|
CADDY_IMAGE := forgejo.nyumbani.baobab.band/sjat/caddy-gandi:2.11.4
|
||||||
CADDY_DOCKERFILE := .docker/caddy-gandi/Dockerfile
|
CADDY_DOCKERFILE := .docker/caddy-gandi/Dockerfile
|
||||||
# Forgejo container registry (same host/user as the image tags above). `make registry-login`
|
|
||||||
# logs the Docker daemon in using vault.forgejo.registry_token (2026-06-17 kaizen) so image
|
|
||||||
# pushes are agent-completable non-interactively.
|
|
||||||
REGISTRY_HOST := forgejo.nyumbani.baobab.band
|
|
||||||
REGISTRY_USER := sjat
|
|
||||||
|
|
||||||
# For TF_ENV=offsite, source the Hetzner token from the vault into the environment
|
# For TF_ENV=offsite, source the Hetzner token from the vault into the environment
|
||||||
# (rbw must be unlocked). Read in-memory; never written to a tfvars file (CLAUDE.md).
|
# (rbw must be unlocked). Read in-memory; never written to a tfvars file (CLAUDE.md).
|
||||||
|
|
@ -42,7 +37,7 @@ endif
|
||||||
.PHONY: help setup collections lint test test-all check deploy encrypt decrypt \
|
.PHONY: help setup collections lint test test-all check deploy encrypt decrypt \
|
||||||
edit-vault check-vault new-role \
|
edit-vault check-vault new-role \
|
||||||
tf-init tf-plan tf-apply tf-output tf-inventory tf-inventory-offsite \
|
tf-init tf-plan tf-apply tf-output tf-inventory tf-inventory-offsite \
|
||||||
molecule-image molecule-image-push caddy-image caddy-image-push registry-login
|
molecule-image molecule-image-push caddy-image caddy-image-push
|
||||||
|
|
||||||
help:
|
help:
|
||||||
@echo ""
|
@echo ""
|
||||||
|
|
@ -74,7 +69,6 @@ help:
|
||||||
@echo " make molecule-image-push Push the test image to the Forgejo registry"
|
@echo " make molecule-image-push Push the test image to the Forgejo registry"
|
||||||
@echo " make caddy-image Build the custom Caddy + Gandi DNS-01 image (run on ubongo)"
|
@echo " make caddy-image Build the custom Caddy + Gandi DNS-01 image (run on ubongo)"
|
||||||
@echo " make caddy-image-push Push the Caddy image to the Forgejo registry"
|
@echo " make caddy-image-push Push the Caddy image to the Forgejo registry"
|
||||||
@echo " make registry-login Log Docker into the Forgejo registry (vaulted token)"
|
|
||||||
@echo ""
|
@echo ""
|
||||||
|
|
||||||
# ── Environment setup ─────────────────────────────────────────────────────────
|
# ── Environment setup ─────────────────────────────────────────────────────────
|
||||||
|
|
@ -165,13 +159,6 @@ caddy-image:
|
||||||
caddy-image-push: caddy-image
|
caddy-image-push: caddy-image
|
||||||
docker push $(CADDY_IMAGE)
|
docker push $(CADDY_IMAGE)
|
||||||
|
|
||||||
# Log the local Docker daemon into the Forgejo registry using the vaulted token, so the
|
|
||||||
# *-image-push targets above are agent-completable non-interactively (rbw must be unlocked).
|
|
||||||
registry-login:
|
|
||||||
@ANSIBLE_VAULT="$(ANSIBLE)-vault" PYTHON="$(PYTHON)" VAULT="$(VAULT)" \
|
|
||||||
REGISTRY_HOST="$(REGISTRY_HOST)" REGISTRY_USER="$(REGISTRY_USER)" \
|
|
||||||
bash scripts/registry-login.sh
|
|
||||||
|
|
||||||
# ── Terraform ─────────────────────────────────────────────────────────────────
|
# ── Terraform ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
tf-init:
|
tf-init:
|
||||||
|
|
|
||||||
101
docs/FRICTION.md
101
docs/FRICTION.md
|
|
@ -22,35 +22,90 @@ earning its keep.
|
||||||
|
|
||||||
_(append new raw signals here; the next kaizen review consumes them)_
|
_(append new raw signals here; the next kaizen review consumes them)_
|
||||||
|
|
||||||
|
- `[friction]` **Image push to the Forgejo registry fails with `no basic auth
|
||||||
|
credentials`** (2026-06-15): `make caddy-image-push` (and `molecule-image-push`) fail
|
||||||
|
unless the Docker daemon on ubongo has an interactive `docker login
|
||||||
|
forgejo.nyumbani.baobab.band` session — and those creds are **not in vault** (only
|
||||||
|
`gandi` + `hetzner` are), so an agent can't complete a push non-interactively. The
|
||||||
|
build half is fully automatable; the push half silently requires a human. → candidate:
|
||||||
|
document the `docker login` step in `docs/runbooks/claude-code-setup.md`, **or** store
|
||||||
|
a scoped Forgejo registry token in vault + a `make registry-login` target (login via
|
||||||
|
`--password-stdin`, `no_log`) so pushes are agent-completable like every other
|
||||||
|
vault-backed action.
|
||||||
|
|
||||||
|
- `[gotcha]` **Single-file Docker bind mount + atomic config rewrite = stale config in
|
||||||
|
the running container** (2026-06-16): `reverse_proxy` bind-mounted the Caddyfile as a
|
||||||
|
single file; `ansible.builtin.template` writes atomically (temp + rename → new inode),
|
||||||
|
so the running container kept the OLD inode and `caddy reload` (in-container, no restart)
|
||||||
|
re-read stale config and silently no-op'd (`"config is unchanged"`). The NetBird route
|
||||||
|
never loaded → Caddy never requested its cert; surfaced only by a TLS handshake failure.
|
||||||
|
Fix: mount the config **directory** (`./caddy` → `/etc/caddy`) — directory mounts reflect
|
||||||
|
inode swaps, so live reload works (proven on askari). NOTE the sibling case: NetBird also
|
||||||
|
single-file-mounts `config.yaml`, but its handler does `docker compose restart` (not an
|
||||||
|
in-container reload), and a restart DOES re-resolve the bind mount (verified: 0 before,
|
||||||
|
1 after) — so restart-based roles are safe; only in-place-reload roles need the dir mount.
|
||||||
|
→ candidate gotcha doc (`docs/testing/gotchas.md`): "reload-in-place needs a directory
|
||||||
|
mount; restart-based roles are fine with a single-file mount."
|
||||||
|
|
||||||
|
- `[friction]` **`make check` always fails on the first-ever deploy of a compose service
|
||||||
|
role** (2026-06-16): in check mode the "ensure base_dir" task is reported-but-not-run, so
|
||||||
|
the later `community.docker.docker_compose_v2` up fails with `"…is not a directory"`
|
||||||
|
(missing `project_src`). Not a defect — a real deploy creates the dir — but it means the
|
||||||
|
CLAUDE.md "always `make check` before `make deploy`" step is guaranteed-red for any brand
|
||||||
|
new stateful role, which erodes trust in the check. → candidate: guard the compose-up with
|
||||||
|
`not ansible_check_mode` (clean "skipped" in dry-run; compose can't be meaningfully
|
||||||
|
dry-run before first deploy anyway), OR document the one-time expected failure. Decide one.
|
||||||
|
|
||||||
|
- `[recurring]` **Re-asked the operator about settled defaults — push + execution mode**
|
||||||
|
(2026-06-17): at the M5 plan handoff I asked (a) whether to push to origin and (b) which
|
||||||
|
execution mode (subagent-driven vs inline) — both already settled: CLAUDE.md says push to
|
||||||
|
`origin` often (off-machine backup), and TODO 10.5 / the standing agreement is "always
|
||||||
|
subagent-driven" (there's even `guard-execution-mode-menu.sh`). Same shape as the 5×
|
||||||
|
"execution-mode menu asked AGAIN" ledger entries — but this time the ask was my own
|
||||||
|
free-form prose ("want those pushed now?", "which execution approach?"), which the
|
||||||
|
existing menu-text matcher does NOT catch (it keys on the writing-plans menu's literal
|
||||||
|
text). → the gap is that the guard only matches that literal menu; free-form re-asks slip
|
||||||
|
through. Candidate: widen the Stop-hook matcher to also flag prose re-asks of
|
||||||
|
push-vs-not / subagent-vs-inline, since prose reminders have already failed this many
|
||||||
|
times. Default behaviour: **push as backup and proceed subagent-driven without asking.**
|
||||||
|
|
||||||
|
- `[friction]` **A docs-only commit still tripped the `rbw`-locked pre-commit guard**
|
||||||
|
(2026-06-17): committing only `docs/superpowers/specs/*.md` (no ansible content) was
|
||||||
|
blocked needing the vault password, although the 2026-06-10 kaizen fix scoped the
|
||||||
|
pre-commit `ansible-lint` hook (`always_run: false` + `files:` ansible content) so
|
||||||
|
docs-/config-only commits skip it and need no vault. So either the hook's `files:`
|
||||||
|
pattern still matches `docs/**` (or `.md`), or a blanket pre-commit step needs the
|
||||||
|
vault regardless. → check `.pre-commit-config.yaml`'s `files:`/`exclude:` against the
|
||||||
|
spec/plan paths; docs-only commits should not require `rbw`.
|
||||||
|
|
||||||
|
- `[friction]` **The agent can't manage `ubongo` (the control node it runs ON) without
|
||||||
|
the operator granting access** (2026-06-17): enrolling `ubongo` in the mesh needed two
|
||||||
|
manual operator grants because the agent runs as `claude` (no sudo) but the inventory
|
||||||
|
manages `ubongo` as `sjat`: (1) `claude`'s SSH key added to `sjat`'s `authorized_keys`
|
||||||
|
(`Permission denied (publickey)` otherwise), then (2) `NOPASSWD` sudo for `sjat`
|
||||||
|
(`Missing sudo password` otherwise). So the "AI-worker control node" (ADR-015) can drive
|
||||||
|
the whole fleet but not itself, unattended. This is the **pending `ansible`-user
|
||||||
|
bootstrap** gap (STATUS) biting in practice. → the proper fix is ubongo's bootstrap to a
|
||||||
|
key-trusted, NOPASSWD `ansible` (or `sjat`) management identity as part of `base`/its
|
||||||
|
control-node recipe, so control-node self-management doesn't need ad-hoc operator grants.
|
||||||
|
|
||||||
|
- `[recurring]` **ADRs claim cross-doc reconciliation they didn't actually perform**
|
||||||
|
(2026-06-14): ADR-024's Status + Consequences asserted "ADR-017 prose that mentioned
|
||||||
|
Traefik is updated to read Caddy" — but ADR-008/017/019 + CAPABILITIES still said
|
||||||
|
Traefik; the rename was left half-done across the doc set and the ADR over-claimed its
|
||||||
|
own follow-through. Surfaced only by a full-repo `grep Traefik` during `/review-repo`.
|
||||||
|
Same shape as the deferred-decision-goes-stale signal (a decision lands in one place,
|
||||||
|
its promised ripple edits don't). → candidate `repo-scan.py` check: when an ADR's text
|
||||||
|
asserts "X is updated to Y" / supersedes a named tool, flag remaining occurrences of the
|
||||||
|
old name (or verify the claimed edit landed) — the structural cousin of `stale-deferred`.
|
||||||
|
(KEEP-OPEN per the 2026-06-14 `/kaizen` run — it's its own build task.)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Kaizen reviews — decisions ledger
|
## Kaizen reviews — decisions ledger
|
||||||
|
|
||||||
Consumed signals and where their resolution now lives. Newest first.
|
Consumed signals and where their resolution now lives. Newest first.
|
||||||
|
|
||||||
### 2026-06-17
|
|
||||||
|
|
||||||
Second `/kaizen` run. 7 signals triaged; all 7 consumed (0 kept open). Two heavier items
|
|
||||||
(the `rename-incomplete` scan check and the Forgejo registry-login path) were built by
|
|
||||||
parallel subagents and verified against the diff. **Bias-to-remove note:** one PARK
|
|
||||||
(the ubongo self-management gap — out-of-phase, already tracked in STATUS) and zero
|
|
||||||
REMOVE; the rest accreted (migrate/change). None of the open signals were `[unused]`
|
|
||||||
*tooling*, so there was nothing to delete — the only reductive move available was parking
|
|
||||||
the out-of-phase build. **Cadence:** healthy — 3 days after the first run, every signal
|
|
||||||
0–2 days old except the one carried over from 2026-06-14; the "recurring ≥3" nudge in
|
|
||||||
`scripts/friction-scan.py` didn't fire this pass (all recurrence counts were 1), so the
|
|
||||||
thresholds need no change.
|
|
||||||
|
|
||||||
| Signal (first seen) | Verdict | Resolution / where it lives now |
|
|
||||||
|---|---|---|
|
|
||||||
| ADRs claim cross-doc reconciliation they didn't perform (06-14) | SYSTEMATIZE | New `rename-incomplete` check in `scripts/repo-scan.py` (+7 tests): when a numbered ADR announces a rename `Old`→`New`, flag any design-doc line where `Old` still appears in present tense (skips the announcing ADR, lines also naming `New`, and historical/negation cues; rejects `ADR-NNN` tokens as terms). 0 findings on the current tree — the Traefik→Caddy ripple edits have landed. Structural cousin of `stale-deferred`; run by `/review-repo`. (Was KEEP-OPEN on 2026-06-14 — now built.) |
|
|
||||||
| Image push to the Forgejo registry needs an interactive `docker login` (06-15) | SYSTEMATIZE → vault | Vault-backed login path so pushes are agent-completable: `vault.forgejo.registry_token` stub (CHANGEME, operator-minted) + `scripts/registry-login.sh` (reads the token, `docker login --password-stdin`, never echoes it) + `make registry-login` + a prereq note in `docs/runbooks/claude-code-setup.md`. Works once the operator fills the token via `make edit-vault`. |
|
|
||||||
| Single-file bind mount + atomic rewrite = stale config (06-16) | SYSTEMATIZE | → `docs/testing/gotchas.md` — "Single-file bind mount + atomic rewrite = stale config (reload-in-place only)": `template` writes a new inode, a single-file bind mount pins the old one, so an in-container reload reads stale config. Mount the config *directory* for reload-in-place roles; restart-based roles are fine with a single-file mount. |
|
|
||||||
| `make check` always fails on the first-ever deploy of a compose service role (06-16) | CHANGE | `check_mode: false` on the `state: directory` scaffold tasks in `roles/reverse_proxy` + `roles/netbird_coordinator`, so the base dirs exist under `--check` and the rest of the dry-run (templates + compose) evaluates instead of failing on a missing `project_src`. Inert under converge → Molecule unchanged. |
|
|
||||||
| Re-asked settled defaults — push + execution mode, in prose (06-17) | CHANGE (exec) + ACCEPTED (push) | Widened `.claude/hooks/guard-execution-mode-menu.sh` to also catch free-form *prose* re-asks of the subagent-vs-inline choice (`"which execution approach?"`, `"subagent vs inline"`, …), not just the literal menu; tested. The push re-ask stays a soft default via the `dont-reask-settled-defaults` memory — a genuine "should I push?" is sometimes legitimate, so it is deliberately not hard-blocked. |
|
|
||||||
| Docs-only commit tripped the rbw-locked pre-commit guard (06-17) | CHANGE | Root cause was NOT the ansible-lint `files:` scope (innocent) — it was `.claude/hooks/guard-vault-preflight.sh` blocking *every* locked `git commit`. Rewrote it to inspect the staged set (`git diff --cached`, plus `-a`/`--all`) and block only when Ansible content (`^(roles\|playbooks\|inventories)/.*\.ya?ml$`) is staged; docs-/config-only commits are now exempt. Fail-safe to block when unsure. Tested. |
|
|
||||||
| Agent can't self-manage `ubongo` (the control node it runs on) without operator grants (06-17) | PARK | The knowledge already lives in `STATUS.md` (control-node row: the interim `claude`-key + `sjat` NOPASSWD grants, and **Pending:** the proper `ansible`-user bootstrap) and the `ubongo-self-sufficiency` memory. Out-of-phase — the fix is the control-node bootstrap recipe, a tracked future build. **Resurrection trigger:** when building ubongo's `base` hardening / `ansible`-user bootstrap, fold in key-trusted NOPASSWD self-management so control-node self-management needs no ad-hoc operator grants. |
|
|
||||||
|
|
||||||
### 2026-06-14
|
### 2026-06-14
|
||||||
|
|
||||||
First `/kaizen` run (dogfood). 12 signals triaged; 11 consumed, 1 kept open (#13 above —
|
First `/kaizen` run (dogfood). 12 signals triaged; 11 consumed, 1 kept open (#13 above —
|
||||||
|
|
|
||||||
|
|
@ -50,13 +50,6 @@ Don't install these until their trigger lands — then add them here and to
|
||||||
- **The venv-activate hook** — this repo expects the Python `.venv` active for Bash
|
- **The venv-activate hook** — this repo expects the Python `.venv` active for Bash
|
||||||
commands. If you use the user-level `~/.claude/hooks/activate-venv.sh` pattern,
|
commands. If you use the user-level `~/.claude/hooks/activate-venv.sh` pattern,
|
||||||
replicate it; otherwise `source .venv/bin/activate` per session after `make setup`.
|
replicate it; otherwise `source .venv/bin/activate` per session after `make setup`.
|
||||||
- **Forgejo registry login (for image pushes)** — `make caddy-image-push` /
|
|
||||||
`molecule-image-push` need the Docker daemon authenticated to
|
|
||||||
`forgejo.nyumbani.baobab.band`. Run **`make registry-login`** once per machine: it reads
|
|
||||||
`vault.forgejo.registry_token` from the vault and does `docker login --password-stdin`
|
|
||||||
(no interactive prompt, so an agent can complete a push). The token is operator-minted
|
|
||||||
(Forgejo → Settings → Applications → Generate Token, package read+write) and set via
|
|
||||||
`make edit-vault`; until then `registry-login` prints how to obtain it. (2026-06-17 kaizen.)
|
|
||||||
|
|
||||||
## 4. A note on user-level settings
|
## 4. A note on user-level settings
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -70,21 +70,3 @@ testing surprise is worth remembering past the session that hit it.
|
||||||
plus review. Only a real (or `--check`) call against the API surfaces them.
|
plus review. Only a real (or `--check`) call against the API surfaces them.
|
||||||
- → Treat a **check-mode run against the real API as a required gate** for such roles, or
|
- → Treat a **check-mode run against the real API as a required gate** for such roles, or
|
||||||
build a render-only assertion that materializes and inspects the rendered module args.
|
build a render-only assertion that materializes and inspects the rendered module args.
|
||||||
|
|
||||||
## Single-file bind mount + atomic rewrite = stale config (reload-in-place only)
|
|
||||||
|
|
||||||
- **`ansible.builtin.template` writes atomically** (temp file + rename → a *new inode*). A
|
|
||||||
Docker **single-file** bind mount pins the *old* inode, so a container that reloads
|
|
||||||
config **in place** (no restart) keeps reading the stale file. Live hit: `reverse_proxy`
|
|
||||||
bind-mounted the Caddyfile as a single file; `caddy reload` (in-container) re-read the
|
|
||||||
old inode and silently no-op'd (`"config is unchanged"`). The new NetBird route never
|
|
||||||
loaded → Caddy never requested its cert → surfaced only as a downstream TLS handshake
|
|
||||||
failure.
|
|
||||||
- **Fix for reload-in-place roles: bind-mount the config *directory*, not the file**
|
|
||||||
(`./caddy` → `/etc/caddy`). Directory mounts reflect the inode swap, so the reload sees
|
|
||||||
the new file (proven on askari).
|
|
||||||
- **Restart-based roles are fine with a single-file mount.** Sibling case: `netbird`
|
|
||||||
single-file-mounts `config.yaml`, but its handler does `docker compose restart` (not an
|
|
||||||
in-container reload), and a **restart re-resolves the bind mount** (verified: route
|
|
||||||
count 0 before, 1 after). Rule of thumb: **reload-in-place needs a directory mount;
|
|
||||||
restart-based roles don't.**
|
|
||||||
|
|
|
||||||
|
|
@ -1,106 +1,86 @@
|
||||||
$ANSIBLE_VAULT;1.1;AES256
|
$ANSIBLE_VAULT;1.1;AES256
|
||||||
30393235363534356266376565386264666431656562646165333332393034663939353961343531
|
32313030663934353361336234373562303537356334346238663836373238366136356331363761
|
||||||
6436663865303965393737633462393331393562336537650a326265306465366433393331343362
|
6337323031666565663430303562646565303533653531640a636662373939363632383838613431
|
||||||
62383039303432653139623636396533313336326266363264393065343834316666613765346265
|
38313365626365373539653266326661393765333737386161666165666534636562353165386537
|
||||||
6238336566383161630a653130623266323738656338616239393032303863656438333839396438
|
3934633033383966360a323965333139643764326236396635383863353437313966326665373537
|
||||||
36336264316331363033353934333463376462366165643737633334313761316565616635643361
|
65396564393130303030643861663964383436396561643666623837306366346333306430306238
|
||||||
63303361656536666130626337623832663065386263356364626530633435636537646664336363
|
66656136626566626262373037623531623633313664376166376161363336353930636538323339
|
||||||
37326435346639633463306337303232393363396638313631366365376230386231626435616364
|
38386564333432353363353663643539343765373662643836646666626339353539323033386230
|
||||||
34353237623761396537333230326266323063306466326539333237326233303036626161353965
|
31613165373035363533383862366638353035653836303737656534623361313064616365643131
|
||||||
65613461633261653763336432333830346431356636623738336332343865343035346263626231
|
64386165653835366137353339396364313661656333333635616338346561363765353934343162
|
||||||
30653337643964323336316365323461313338376131323861663962336238346235666664613932
|
64346462656566376539643030656461363161393936623332373632653731303031393437316636
|
||||||
62356566306233623936663734666639356233333537373866633361313933623532643161386463
|
36626165306161336262356161666531323336343663643661626365396437383230613636356530
|
||||||
36666565303737303663366631646535303863333761303731613332373665613261343064636561
|
62326363383138643162316464396666623332366434336462363531363836313833366237396464
|
||||||
35636337396139336461356338333164316135623838383564653066363064376662653039333664
|
38323635353238653432626361383434646538326531356333393337643066373262663462656466
|
||||||
62306165613433626264616334643761343263623566346432653638636138326136636335313235
|
65373036653265616137666533373930333239303732623832353337343434636434616562336135
|
||||||
64373236343630626632623337653334383132373662306165633038303830386336373634656438
|
38666137353266353130303235616362323633353735373163336138633838633738393637633964
|
||||||
31613431626166376536613439616363363464303763656238643339373365393636386138656634
|
66623866353265316336336566663034306664656365643832616232313732626464316563636335
|
||||||
34356332343366623462323434363338383061346430326434343139303866306331393465333332
|
63653930626565636630326661626561366539303964373933653437356537343361626438313439
|
||||||
36666339383066613762353866653032313435343663333433653563393564643038313062633338
|
35643165636662643463616337323063343633306536346538623331333365366533653634343538
|
||||||
37663565336636356631373238373638346233616336363630326134646434666165386336393530
|
63623261636366303261373338633939363338316463303065613436396163616537666265623439
|
||||||
32326431353637336432336639636663373330663433613931646638626564653364353036313562
|
31383361646531633863623230616635646138653630383537366335633030343530383735616435
|
||||||
31353763633561393538326663633731313363366230363230313232373230626162643062653561
|
35656464393432313563303030626133383761303763653530653837313930303034353136353237
|
||||||
61636166613761646534653230623234623035336332643961623636306161623934303364633664
|
37376366623836646236363062633938666135326631376235323061666465373865396235643937
|
||||||
38363239393863393431643662396362653437616138316336666638303663643831376466356633
|
32633736656539356332336237646137303534343337353139383637623165353338623566666535
|
||||||
32643661653635393836376663373838333366383833316635363864353862666534373530356231
|
30643134303235633362383064376234366235363262396362613731373364306362303634613138
|
||||||
39666132636361386236623734333862663133343939386432313730623439343961646633333936
|
39366230366262363237656631646361356464393266656166386337303663313136666261633836
|
||||||
36373232303263396435663163306135663361623531313734663438363730323835343866623533
|
32306132323239343539396232316564326361626462366561313561393635393233653633646431
|
||||||
37613830666236376461643238663336623032356238313563616437363638333338306438663461
|
39313039313139616262396334613035333633326135346365333537373138396535633137353832
|
||||||
62663035303737623635326536613564393962303462326233366137616565373034393839346565
|
63636335613237623234646234653435616635356637343964656463383864366534363438343938
|
||||||
66633637663463643030316639366633363866643366336166393535353366316364643335353035
|
39626364653832373062323434316134653831336534383934346231656533643435306465393065
|
||||||
32343935363536633162316634393530376332653635326464326631396464343931663133656465
|
31653731653438646361363732303664626438663533393837356562376633643933376132616236
|
||||||
38656335306639343736346334636661636635333066663565643764373432333161653063323231
|
65393432633831313433323930383736316630626230373963653536396637363436643136363962
|
||||||
37333464316536363037323362333432373162363833303130643534343461646563343564386565
|
37326534343237363961326438376137663034356532376433376461363337333562646136616462
|
||||||
33363437376237633739623863353232343438306639303561646139396137646234663534636237
|
61636131376264393236376532356539376536643632623864656331656630353362623133303830
|
||||||
30663332306162366233636666316336313538623730333162376264383463666661613436656265
|
34633461633539643262353263376363613566343261373930623139626364653232363538353330
|
||||||
37643738313335353030383663383461663036376637346334643531636438656133393731383138
|
33633634363232653439656236303262373265613762373165646131383537623438383835383962
|
||||||
63653338306639653266366337653865363432366364306262303232616536393738323334346334
|
33383931626136313036366562363732396561633631643561646536653665333733383261363833
|
||||||
33646331316330633738386236323533353734623234653335643633643866393761653232303162
|
66356461663965373234393237323037356331333339643931313936313234323432613563306630
|
||||||
31653530643934373435326539366663313033616338666633313131353334323931383635346235
|
33306638663839363565636661653830316265393639313065313062666534303039326465373636
|
||||||
32333166636135323438353735616265373132393762323037326635633162653363393639633939
|
64363033323837313030353132383562343337326366626635663439396231393537313932643337
|
||||||
37613231343766653437626235313666343837343865303863643035633863373235656439346538
|
30663031323231313938366436343735326165326433656633336465316630383961626664303536
|
||||||
32613934366138653331343931386131663134623063366435623835653535623334376333366562
|
38633964326431643362626631656131303539613033323039393630353766386339346363663362
|
||||||
65366365386239663338656233353435343639353037663732323064626261636363643261656164
|
33323034396136356362313163376438393739373738366363623636623634316537313461373066
|
||||||
66393538343638373064376536326537623735343064643736636466323636316130373565643635
|
38613062656231363532663133333438663535666566356336316266383763623765346237663838
|
||||||
31383531306431363533663935356339373934396361306138633266653431326132386563366565
|
64336435353437373264346561363265643339306532383539306363653564356362313430333066
|
||||||
34646465356139636563653732373932636461313935376235366266636136663238663030373662
|
65633733633938343830303537383231303036326132376263363531626565633664343038356661
|
||||||
31336635653666326538333931646235316262303838633138613464303837386162613263386438
|
31336139663061656437633138373438663966616338343565396562306638346437353730643664
|
||||||
34366230353439323531646637326634616336343963373831356163373462343664393538353835
|
30373133373863626137313062643062393035653463653231653465333166633063353137633538
|
||||||
66616235616363333430313539333637626665386562653834343139663133383463393236633662
|
62383331303164343236343539396461623738396234653333356632313664616263623061363563
|
||||||
30303937376661323535333132653966383231353033633334356663646265613934336636363138
|
34323165306533323362376161346364316135333535626261353730666131643938306366326263
|
||||||
65643265373334653932316262303136363763396134303730383536646538663134303964323432
|
31313934633137623638316534383234376333396131303034633636323037363732383263326335
|
||||||
39303833386361363364303231656364356365653939373661633661313739356665323834306635
|
32393766343161386537333062643434333333363538323366363231336666383161373432383563
|
||||||
32653933626166396462666235353238383266336361616531316433363433336238656539373661
|
65613537366139643032336230303133623431376231646662643666373532636565393639373930
|
||||||
62306362666333653865396364663235626230303133343764613536616237313634613734393631
|
65336630616462353837666431616662636635333532393331326539306233363539396266653239
|
||||||
32373761626161363935346437396161383365633138393164663539626439353832613437653261
|
31303031303330396632386131623134313536313433623064356636333230373962643339363736
|
||||||
35376464313236666236313965376133323333663030383965333935383237633461646266633363
|
30396130353466373136643935646436613636376636323530643031653334303863376432646534
|
||||||
35656561343033306162616361653062363538356136353837343866636632623765646366396638
|
39343165356232346539366233373135326338343663356164616265336235623332646365633466
|
||||||
35313134383361346332323232643030303434363732653236366538376166333237323566383930
|
35393533373663393762376332396136336236616635616535313336613034346436363665356565
|
||||||
62356162656538636464616432626637633531363237376337353562393731633635323536386363
|
32636536336634613531393434613435613962653862343737373237623261373836386663343831
|
||||||
31623936663935366164373062333465383137633661393062386135376635346262336538346165
|
66656135323838636638353963646638326531343635653937306230323237343933626135356533
|
||||||
34663561663134306635373737383430343332633365356238346264376136363862613762313166
|
66356263636438633164386535333762616438626439343462393833393731643037396662653737
|
||||||
39323635396434386537646435366630353462313463366666626135623633386336376466353830
|
31666361656530383437396230393663616133383764316437623939663631396561343266383766
|
||||||
64373763306464346336643531616233633237663534333536666264633564623030343533373430
|
62373636663631393637393763613337356337633264366434346561343263373931323335643135
|
||||||
38623738623264396564316666663763356136623865623662393766343566376465633839353466
|
31366661623137353336666630633365663764646234343035313130663562636361623532643461
|
||||||
66313333666131626639656464666130653239313864656330666666653237306131633139336565
|
63333961333338623966396662656262323830396439633337663431663235663962666238356630
|
||||||
33333536653638386431373432663132643033313566376261376535363563356135353735346464
|
30353331313462653061373638666235653938623931366466666164343566623238333237353265
|
||||||
66623032303639313163303739346637333839323939356364376133346363343163343661646661
|
30373064353132366634623966306632303832306630383637623465323134633133656333303964
|
||||||
65643462356234353562346435623463633665343037313032363362643461356566623731363030
|
35646637316236303364393363323137616132326437623238336631313530663230333362623633
|
||||||
66353530353237666132613435306537613038653062336663373366343834323238656162656538
|
34383032376538366464363032343262656164376166386237383563613630336666633965653730
|
||||||
31383037613535303564633732346332613939333037343334646530386331643336363834313365
|
64373236396564363164643637623736626532396630313131356563333238643665356166323837
|
||||||
66613535353631643566363062643930323966343636353666626237663733363062653564633562
|
31626338623665623165643763623661666439626435643237336433646132666366623661393832
|
||||||
39313034643238366331636332393537376130386463653831393366636634623833366436303961
|
37306533613966663936373061613331633934623462343236626234306130383738343631303231
|
||||||
34633162333462383834346637343337616166353035616362636462346364366539383333373964
|
32326339323738323537333363313538373266623363363636633462356234363466393263316235
|
||||||
62343635323331376237373761383535646233366465303364663731393061326436663137613238
|
39663033303165656366396334306535643361646663373935303230376466366632373563303231
|
||||||
35363338613339326336613535326635363236356264326330386662393666613264646465316339
|
64323264653036333039663965646630653934376239653236323063656137373830623563336463
|
||||||
36633263626632613862613439363630353462626636373265386332316162356439323134636433
|
37343461373737313539316361623763373733653930626532393565333938333761323631303332
|
||||||
34393334316561366231613339386339653663316661356662306230356537626431376133333736
|
39663530303439616561356561666532653762343339323435636164376664373731343132666539
|
||||||
31663034373033666663363634386134376237663061323732653365363662323335653433656665
|
63626637346563393765303065646564643661636130396439323736343764333633373331653333
|
||||||
30323566326530653365613937376264383435636438333734386131363737633163343734336564
|
66633465343433303038623638323965636533666639643266353163353436393036336639336133
|
||||||
32633432613033653263356334666436353036373738346430373133346239313566346236376138
|
32646664363565326539643763653832313336663262313634343635616333613434373333323036
|
||||||
64323565663961356331323937373032306330646664663761646131646235383731303564633366
|
61366435376265336638326132333439613431353633653762653836386235643965366436363866
|
||||||
32656537343836326465343264363531396631613161633236366363366537613439376464653866
|
35626664393139386337353335343930306130356335623131646261656434303966656431623231
|
||||||
61316164363032313630383063336436303661366438623666373538396432303331623139393833
|
66643730393430363838626434663933613536343533316262373564666665373663336363623166
|
||||||
33653061363766313465633964353439383136343832323630666630323865326461666137313132
|
63363037373634383961373035633239646235316137363036333765313864643365396165643432
|
||||||
62333963323564396139333130303539623935303733616138376466303461643439373934393432
|
36623465313036376261393566383539336638363836633232656136656533396663323366313062
|
||||||
38356535663333343232306135623261336534383233333632386638623232383737616239326666
|
64616632373333313466356362336234346564373832316433373963623263316635
|
||||||
38643139623730643439373939616562393162376564346130643135653334616338333739313935
|
|
||||||
33343563346664346531623161313937636562653132613831303132363665343333633232373633
|
|
||||||
30363034373362626266633562633634643635343161613262373463616437396566343733626561
|
|
||||||
30626462313261383461653532396263663139323134653764363963393665653165326130643031
|
|
||||||
36303962313433623034633930633762393263383830663261653631333830643637313332613136
|
|
||||||
66636364373137306539643566633830393030393932336632646565383638343639623232316165
|
|
||||||
63333637616237313262646539363835346138613234376164333238643139653934343365386632
|
|
||||||
37633562633863373033666564666665396261386537653866353533336233316339656365356563
|
|
||||||
33663561643531623837656237313938303363363662383830363435323661306164383932613438
|
|
||||||
63623963336361356639303162373935613734666466663933646239363939616239323164303364
|
|
||||||
62393765343539646633616630313463633963396432636637313162666432386637616233343532
|
|
||||||
39343366323236336266396438326565343561313330313539366439343262396361386662323434
|
|
||||||
30313433323063316531613338303239333539316133623961316432616632613066636136336436
|
|
||||||
35613864363731373333313637363036303561343263316466303935313664646635396331323039
|
|
||||||
31333134316535356134666365306263303337623636353430333731343037656661363333643165
|
|
||||||
30383734336662336632356537376232623664653436396464633037643839343238623330613136
|
|
||||||
31663137366437396330333231303636633461616230383339646261303964393539393637643730
|
|
||||||
30626463313434646566313939656432313066356232303435313734353930336265343431346362
|
|
||||||
36346666643230303565613563313634393338323735373965336138646432363534383066383236
|
|
||||||
30383230666335393861653937316233633534316666643865643439313938633230
|
|
||||||
|
|
|
||||||
|
|
@ -4,8 +4,6 @@
|
||||||
path: "{{ netbird_coordinator__base_dir }}"
|
path: "{{ netbird_coordinator__base_dir }}"
|
||||||
state: directory
|
state: directory
|
||||||
mode: "0750"
|
mode: "0750"
|
||||||
# create the scaffold even in --check so dry-run can evaluate templates + compose (idempotent mkdir)
|
|
||||||
check_mode: false
|
|
||||||
tags: [config]
|
tags: [config]
|
||||||
|
|
||||||
- name: Render the combined server config
|
- name: Render the combined server config
|
||||||
|
|
|
||||||
|
|
@ -4,8 +4,6 @@
|
||||||
path: "{{ reverse_proxy__base_dir }}"
|
path: "{{ reverse_proxy__base_dir }}"
|
||||||
state: directory
|
state: directory
|
||||||
mode: "0750"
|
mode: "0750"
|
||||||
# create the scaffold even in --check so dry-run can evaluate templates + compose (idempotent mkdir)
|
|
||||||
check_mode: false
|
|
||||||
tags: [config]
|
tags: [config]
|
||||||
|
|
||||||
- name: Ensure the Caddy config directory exists
|
- name: Ensure the Caddy config directory exists
|
||||||
|
|
@ -13,8 +11,6 @@
|
||||||
path: "{{ reverse_proxy__base_dir }}/caddy"
|
path: "{{ reverse_proxy__base_dir }}/caddy"
|
||||||
state: directory
|
state: directory
|
||||||
mode: "0750"
|
mode: "0750"
|
||||||
# create the scaffold even in --check so dry-run can evaluate templates + compose (idempotent mkdir)
|
|
||||||
check_mode: false
|
|
||||||
tags: [config]
|
tags: [config]
|
||||||
|
|
||||||
# Render into a directory that is bind-mounted whole (./caddy -> /etc/caddy). Mounting
|
# Render into a directory that is bind-mounted whole (./caddy -> /etc/caddy). Mounting
|
||||||
|
|
|
||||||
|
|
@ -1,32 +0,0 @@
|
||||||
#!/usr/bin/env bash
|
|
||||||
#
|
|
||||||
# Log the local Docker daemon into the Forgejo container registry using a token stored in
|
|
||||||
# the Ansible vault — so registry pushes (make caddy-image-push / molecule-image-push) are
|
|
||||||
# agent-completable non-interactively, like every other vault-backed action.
|
|
||||||
# (2026-06-17 kaizen, docs/FRICTION.md: the push half silently needed an interactive
|
|
||||||
# `docker login`; the creds weren't in the vault, so an agent couldn't complete a push.)
|
|
||||||
#
|
|
||||||
# Reads vault.forgejo.registry_token from the vault (rbw must be unlocked) and pipes it to
|
|
||||||
# `docker login --password-stdin`. The token never lands on argv or on disk and is never
|
|
||||||
# echoed (no `set -x`). Binaries/paths are overridable via env so the Makefile can pass the
|
|
||||||
# venv ansible-vault/python; defaults work when run from the repo root with the venv present.
|
|
||||||
#
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
ANSIBLE_VAULT="${ANSIBLE_VAULT:-.venv/bin/ansible-vault}"
|
|
||||||
PYTHON="${PYTHON:-.venv/bin/python}"
|
|
||||||
VAULT="${VAULT:-inventories/production/group_vars/all/vault.yml}"
|
|
||||||
REGISTRY_HOST="${REGISTRY_HOST:-forgejo.nyumbani.baobab.band}"
|
|
||||||
REGISTRY_USER="${REGISTRY_USER:-sjat}"
|
|
||||||
|
|
||||||
token="$("$ANSIBLE_VAULT" view "$VAULT" \
|
|
||||||
| "$PYTHON" -c 'import sys, yaml; d = yaml.safe_load(sys.stdin) or {}; print((((d.get("vault") or {}).get("forgejo") or {}).get("registry_token")) or "", end="")')"
|
|
||||||
|
|
||||||
if [ -z "$token" ] || [ "$token" = "CHANGEME" ]; then
|
|
||||||
echo "registry-login: vault.forgejo.registry_token is unset or still CHANGEME." >&2
|
|
||||||
echo " Mint a Forgejo token (Settings -> Applications -> Generate Token, with package" >&2
|
|
||||||
echo " read+write scope, user $REGISTRY_USER) and set it via: make edit-vault" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
printf '%s' "$token" | docker login "$REGISTRY_HOST" -u "$REGISTRY_USER" --password-stdin
|
|
||||||
|
|
@ -41,42 +41,6 @@ LIST_ITEM_RE = re.compile(r"^\s*(\d+\.|[-*+])\s+(.*)")
|
||||||
DEFER_REF_RE = re.compile(r"ADR-(\d{3})\D{0,40}?deferred\D{0,12}?(\d+)", re.I)
|
DEFER_REF_RE = re.compile(r"ADR-(\d{3})\D{0,40}?deferred\D{0,12}?(\d+)", re.I)
|
||||||
RESOLVE_WORD_RE = re.compile(r"\b(?:resolv\w*|decid\w*|address\w*|complet\w*|done)\b", re.I)
|
RESOLVE_WORD_RE = re.compile(r"\b(?:resolv\w*|decid\w*|address\w*|complet\w*|done)\b", re.I)
|
||||||
|
|
||||||
# Rename-incomplete detection: an ADR announces a rename/supersession of a named
|
|
||||||
# term (Old → New); verify the OLD name no longer lingers in the design-doc set.
|
|
||||||
# (The structural cousin of stale-deferred — see docs/FRICTION.md, ADR-024.)
|
|
||||||
# A "specific" name is a backticked token or a capitalised proper-noun/identifier;
|
|
||||||
# common connective words are rejected so they can't be mistaken for a tool name.
|
|
||||||
_NAME = r"(?:`[^`]+`|[A-Z][A-Za-z0-9_+.-]{2,})"
|
|
||||||
RENAME_STOPWORDS = {
|
|
||||||
"was", "were", "the", "this", "that", "with", "from", "into", "and", "but",
|
|
||||||
"for", "are", "has", "had", "been", "now", "not", "all", "any", "use", "used",
|
|
||||||
"via", "per", "its", "our", "one", "two", "old", "new", "phase", "step",
|
|
||||||
"adr", "read", "name", "term", "tool", "prose", "roadmap",
|
|
||||||
}
|
|
||||||
# Trigger forms — each captures (old, new) as raw name tokens; the connective words
|
|
||||||
# are case-insensitive but the names must still satisfy _NAME (specific tokens).
|
|
||||||
RENAME_ASSERT_RES = (
|
|
||||||
# renamed X to Y
|
|
||||||
re.compile(rf"renamed\s+(?:from\s+)?({_NAME})\s+to\s+({_NAME})", re.I),
|
|
||||||
# replaced X with Y
|
|
||||||
re.compile(rf"replac\w*\s+({_NAME})\s+with\s+({_NAME})", re.I),
|
|
||||||
# superseded X with/by Y
|
|
||||||
re.compile(rf"supersed\w*\s+({_NAME})\s+(?:with|by)\s+({_NAME})", re.I),
|
|
||||||
# X ... (is/are/was/were/been) updated to read Y
|
|
||||||
re.compile(rf"({_NAME})\b.{{0,40}}?\b(?:is|are|was|were|been)?\s*"
|
|
||||||
rf"updated\s+to\s+read\s+[\"']?({_NAME})", re.I),
|
|
||||||
# X → Y / X -> Y on a line that also carries a rename/supersede/update cue
|
|
||||||
re.compile(rf"({_NAME})\s*(?:->|→)\s*({_NAME})"),
|
|
||||||
)
|
|
||||||
RENAME_ARROW_RES = (RENAME_ASSERT_RES[-1],) # arrow forms need a cue word on the line
|
|
||||||
RENAME_CUE_RE = re.compile(r"\b(?:renam\w*|replac\w*|supersed\w*|updated|rename)\b", re.I)
|
|
||||||
# Historical / negation cues — a lingering OLD name on such a line is legitimate
|
|
||||||
# history, not a missed ripple edit, so it is skipped.
|
|
||||||
RENAME_HIST_RE = re.compile(
|
|
||||||
r"\b(?:was|were|formerly|previously|no longer|instead of|rather than|reject\w*|"
|
|
||||||
r"reconsider\w*|supersed\w*|deprecat\w*|legacy|history|heritage|V4|"
|
|
||||||
r"actually ran|used to)\b", re.I)
|
|
||||||
|
|
||||||
# ADR-structure check (ADR-023): numbered ADRs must carry the four mandatory
|
# ADR-structure check (ADR-023): numbered ADRs must carry the four mandatory
|
||||||
# sections and a parseable Status line. Presence only — section ORDER is a
|
# sections and a parseable Status line. Presence only — section ORDER is a
|
||||||
# template-demonstrated convention, not machine-enforced.
|
# template-demonstrated convention, not machine-enforced.
|
||||||
|
|
@ -178,84 +142,6 @@ def adr_structure_findings(adr_files):
|
||||||
return out
|
return out
|
||||||
|
|
||||||
|
|
||||||
def _clean_name(tok):
|
|
||||||
"""Strip backticks/quotes from a captured name token. Return the bare name, or
|
|
||||||
None if it is not a 'specific' token (empty, multi-word, or a stopword)."""
|
|
||||||
s = tok.strip().strip("`\"'").strip()
|
|
||||||
s = s.rstrip(".,;:!?)") # trailing sentence punctuation is not part of the name
|
|
||||||
if not s or " " in s:
|
|
||||||
return None
|
|
||||||
if s.lower() in RENAME_STOPWORDS:
|
|
||||||
return None
|
|
||||||
# An ADR reference (ADR-017) is a document pointer, never the renamed *term* — a
|
|
||||||
# sentence like "the ADR-017 prose ... is updated to read Caddy" must not parse
|
|
||||||
# ADR-017 as the old name. Reject it so such lines skip (precision >> recall).
|
|
||||||
if re.fullmatch(r"ADR-\d{3}", s):
|
|
||||||
return None
|
|
||||||
# Must be backtick-able identifier or a capitalised proper noun (the _NAME shape
|
|
||||||
# already enforced this on capture; this is the after-stripping re-check).
|
|
||||||
if not re.fullmatch(r"[A-Za-z0-9_+.-]{3,}", s):
|
|
||||||
return None
|
|
||||||
return s
|
|
||||||
|
|
||||||
|
|
||||||
def _rename_assertion(line):
|
|
||||||
"""Parse a single ADR line for a tight Old→New rename assertion. Returns
|
|
||||||
(old, new) of cleaned specific names, or None. Conservative: precision >> recall."""
|
|
||||||
for rx in RENAME_ASSERT_RES:
|
|
||||||
m = rx.search(line)
|
|
||||||
if not m:
|
|
||||||
continue
|
|
||||||
# Arrow form only counts when the line also carries a rename/supersede cue.
|
|
||||||
if rx in RENAME_ARROW_RES and not RENAME_CUE_RE.search(line):
|
|
||||||
continue
|
|
||||||
old, new = _clean_name(m.group(1)), _clean_name(m.group(2))
|
|
||||||
if old and new and old != new:
|
|
||||||
return old, new
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def rename_incomplete_findings(adr_files, extra_docs):
|
|
||||||
"""adr_files: {rel_path: [lines]} for docs/decisions/*.md (the numbered ADRs make
|
|
||||||
the assertions). extra_docs: {rel_path: [lines]} for CAPABILITIES.md / ROADMAP.md.
|
|
||||||
When a numbered ADR announces a rename 'Old' -> 'New', flag any DESIGN-doc line
|
|
||||||
where 'Old' still appears as a whole word in present tense (skipping the announcing
|
|
||||||
ADR, lines that also name 'New', and lines carrying a historical/negation cue)."""
|
|
||||||
out = []
|
|
||||||
# The design-doc set we search: all decisions/*.md plus the two extra docs.
|
|
||||||
doc_set = dict(adr_files)
|
|
||||||
doc_set.update(extra_docs)
|
|
||||||
# Collect assertions only from numbered ADRs (NNN-*.md).
|
|
||||||
assertions = [] # (adr_num, announcer_path, old, new)
|
|
||||||
for rpath, lines in sorted(adr_files.items()):
|
|
||||||
base = os.path.basename(rpath)
|
|
||||||
if not ADR_FILE_RE.match(base):
|
|
||||||
continue
|
|
||||||
adr_num = base[:3]
|
|
||||||
for line in lines:
|
|
||||||
parsed = _rename_assertion(line)
|
|
||||||
if parsed:
|
|
||||||
assertions.append((adr_num, rpath, parsed[0], parsed[1]))
|
|
||||||
for adr_num, announcer, old, new in assertions:
|
|
||||||
old_re = re.compile(r"\b" + re.escape(old) + r"\b") # case-sensitive whole word
|
|
||||||
for rpath, lines in sorted(doc_set.items()):
|
|
||||||
if rpath == announcer: # the ADR that made the claim is exempt
|
|
||||||
continue
|
|
||||||
for i, raw in enumerate(lines, 1):
|
|
||||||
if not old_re.search(raw):
|
|
||||||
continue
|
|
||||||
if new in raw: # rename is being explained on this line
|
|
||||||
continue
|
|
||||||
if RENAME_HIST_RE.search(raw): # legitimate history / negation
|
|
||||||
continue
|
|
||||||
out.append({"check": "rename-incomplete", "severity": "medium",
|
|
||||||
"path": rpath, "line": i,
|
|
||||||
"detail": f"ADR-{adr_num} announced rename '{old}' -> "
|
|
||||||
f"'{new}' but '{old}' still appears here; confirm the "
|
|
||||||
"ripple edit landed or soften the ADR claim"})
|
|
||||||
return out
|
|
||||||
|
|
||||||
|
|
||||||
def walk_files():
|
def walk_files():
|
||||||
for dirpath, dirnames, filenames in os.walk(ROOT):
|
for dirpath, dirnames, filenames in os.walk(ROOT):
|
||||||
dirnames[:] = [d for d in dirnames if d not in PRUNE]
|
dirnames[:] = [d for d in dirnames if d not in PRUNE]
|
||||||
|
|
@ -306,11 +192,8 @@ def scan():
|
||||||
findings = []
|
findings = []
|
||||||
adrs = adr_numbers()
|
adrs = adr_numbers()
|
||||||
adr_files = {} # docs/decisions/*.md → lines, for deferred-section parsing
|
adr_files = {} # docs/decisions/*.md → lines, for deferred-section parsing
|
||||||
extra_docs = {} # CAPABILITIES.md / ROADMAP.md → lines, for rename-incomplete
|
|
||||||
defer_refs = [] # repo-wide "resolves ADR-NNN deferred #K" references
|
defer_refs = [] # repo-wide "resolves ADR-NNN deferred #K" references
|
||||||
decisions_dir = os.path.join("docs", "decisions")
|
decisions_dir = os.path.join("docs", "decisions")
|
||||||
rename_extra = {os.path.join("docs", "CAPABILITIES.md"),
|
|
||||||
os.path.join("docs", "ROADMAP.md")}
|
|
||||||
for path in walk_files():
|
for path in walk_files():
|
||||||
rpath = rel(path)
|
rpath = rel(path)
|
||||||
if rpath.startswith(SKIP_PREFIX):
|
if rpath.startswith(SKIP_PREFIX):
|
||||||
|
|
@ -340,8 +223,6 @@ def scan():
|
||||||
|
|
||||||
if rpath.startswith(decisions_dir) and rpath.endswith(".md"):
|
if rpath.startswith(decisions_dir) and rpath.endswith(".md"):
|
||||||
adr_files[rpath] = lines
|
adr_files[rpath] = lines
|
||||||
if rpath in rename_extra:
|
|
||||||
extra_docs[rpath] = lines
|
|
||||||
|
|
||||||
for i, line in enumerate(lines, 1):
|
for i, line in enumerate(lines, 1):
|
||||||
for m in DEFER_REF_RE.finditer(line):
|
for m in DEFER_REF_RE.finditer(line):
|
||||||
|
|
@ -380,7 +261,6 @@ def scan():
|
||||||
"line": i, "detail": f"references '{ref}' which does not exist"})
|
"line": i, "detail": f"references '{ref}' which does not exist"})
|
||||||
findings.extend(deferred_findings(adr_files, defer_refs))
|
findings.extend(deferred_findings(adr_files, defer_refs))
|
||||||
findings.extend(adr_structure_findings(adr_files))
|
findings.extend(adr_structure_findings(adr_files))
|
||||||
findings.extend(rename_incomplete_findings(adr_files, extra_docs))
|
|
||||||
return findings
|
return findings
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -123,8 +123,5 @@ def test_nudge_line_overdue_on_age():
|
||||||
def test_load_signals_reads_real_friction_file():
|
def test_load_signals_reads_real_friction_file():
|
||||||
path = os.path.join(os.path.dirname(__file__), "..", "docs", "FRICTION.md")
|
path = os.path.join(os.path.dirname(__file__), "..", "docs", "FRICTION.md")
|
||||||
sigs = fs.load_signals(path, TODAY)
|
sigs = fs.load_signals(path, TODAY)
|
||||||
# May legitimately be empty right after a /kaizen pass consumes every open signal —
|
assert len(sigs) >= 1
|
||||||
# an empty Open-signals section is the goal state, not a failure. Assert the function
|
|
||||||
# parses the real file into well-formed signals (validity holds vacuously when empty).
|
|
||||||
assert isinstance(sigs, list)
|
|
||||||
assert all(s["tag"] in {"friction", "gotcha", "recurring", "unused"} for s in sigs)
|
assert all(s["tag"] in {"friction", "gotcha", "recurring", "unused"} for s in sigs)
|
||||||
|
|
|
||||||
|
|
@ -57,99 +57,3 @@ def test_non_numbered_file_is_skipped():
|
||||||
bare = ["# ADR template\n", "\n", "## Status\n", "\n", "<!-- hint -->\n"]
|
bare = ["# ADR template\n", "\n", "## Status\n", "\n", "<!-- hint -->\n"]
|
||||||
out = _checks(rs.adr_structure_findings({"docs/decisions/adr-template.md": bare}))
|
out = _checks(rs.adr_structure_findings({"docs/decisions/adr-template.md": bare}))
|
||||||
assert out == []
|
assert out == []
|
||||||
|
|
||||||
|
|
||||||
# --- rename-incomplete -------------------------------------------------------
|
|
||||||
|
|
||||||
def _renames(findings):
|
|
||||||
return [f for f in findings if f["check"] == "rename-incomplete"]
|
|
||||||
|
|
||||||
|
|
||||||
def test_rename_incomplete_flags_lingering_old_name():
|
|
||||||
# ADR announces `Foo` -> `Bar`; another decisions file still says Foo present-tense.
|
|
||||||
announcer = {"docs/decisions/050-rename.md": [
|
|
||||||
"## Decision\n", "We renamed `Foo` to `Bar` across the design docs.\n"]}
|
|
||||||
other = {} # extra_docs (CAPABILITIES/ROADMAP) — none here
|
|
||||||
lingering = {"docs/decisions/030-other.md": [
|
|
||||||
"The Foo proxy renders config from the catalog.\n"]}
|
|
||||||
announcer.update(lingering)
|
|
||||||
out = _renames(rs.rename_incomplete_findings(announcer, other))
|
|
||||||
assert len(out) == 1
|
|
||||||
assert out[0]["path"] == "docs/decisions/030-other.md"
|
|
||||||
assert out[0]["line"] == 1
|
|
||||||
assert out[0]["severity"] == "medium"
|
|
||||||
assert "Foo" in out[0]["detail"] and "Bar" in out[0]["detail"]
|
|
||||||
|
|
||||||
|
|
||||||
def test_rename_incomplete_clean_rename_has_no_findings():
|
|
||||||
# The rename announced, and no other doc still mentions Foo.
|
|
||||||
adr_files = {
|
|
||||||
"docs/decisions/050-rename.md": [
|
|
||||||
"## Decision\n", "We renamed `Foo` to `Bar` across the design docs.\n"],
|
|
||||||
"docs/decisions/030-other.md": [
|
|
||||||
"The Bar proxy renders config from the catalog.\n"],
|
|
||||||
}
|
|
||||||
out = _renames(rs.rename_incomplete_findings(adr_files, {}))
|
|
||||||
assert out == []
|
|
||||||
|
|
||||||
|
|
||||||
def test_rename_incomplete_skips_historical_cue_line():
|
|
||||||
# Foo lingers only on a line carrying a historical/negation cue → no finding.
|
|
||||||
adr_files = {
|
|
||||||
"docs/decisions/050-rename.md": [
|
|
||||||
"## Decision\n", "We renamed `Foo` to `Bar` across the design docs.\n"],
|
|
||||||
"docs/decisions/030-other.md": [
|
|
||||||
"Foo was rejected; we run Bar now.\n",
|
|
||||||
"The history of Foo informs the choice.\n"],
|
|
||||||
}
|
|
||||||
out = _renames(rs.rename_incomplete_findings(adr_files, {}))
|
|
||||||
assert out == []
|
|
||||||
|
|
||||||
|
|
||||||
def test_rename_incomplete_skips_announcing_adr_itself():
|
|
||||||
# The announcing ADR mentions Foo (it has to) — must not flag itself.
|
|
||||||
adr_files = {
|
|
||||||
"docs/decisions/050-rename.md": [
|
|
||||||
"## Decision\n",
|
|
||||||
"We renamed `Foo` to `Bar`.\n",
|
|
||||||
"Operators who configured Foo should switch their habits.\n"],
|
|
||||||
}
|
|
||||||
out = _renames(rs.rename_incomplete_findings(adr_files, {}))
|
|
||||||
assert out == []
|
|
||||||
|
|
||||||
|
|
||||||
def test_rename_incomplete_skips_line_naming_new_term():
|
|
||||||
# A line that mentions both Foo and Bar is explaining the rename → skipped.
|
|
||||||
adr_files = {
|
|
||||||
"docs/decisions/050-rename.md": [
|
|
||||||
"## Decision\n", "We renamed `Foo` to `Bar`.\n"],
|
|
||||||
"docs/decisions/030-other.md": [
|
|
||||||
"Foo is being phased out for Bar in this paragraph.\n"],
|
|
||||||
}
|
|
||||||
out = _renames(rs.rename_incomplete_findings(adr_files, {}))
|
|
||||||
assert out == []
|
|
||||||
|
|
||||||
|
|
||||||
def test_rename_incomplete_searches_extra_docs():
|
|
||||||
# A lingering OLD name in CAPABILITIES.md (an extra_docs file) is flagged.
|
|
||||||
adr_files = {"docs/decisions/050-rename.md": [
|
|
||||||
"## Decision\n", "We renamed `Foo` to `Bar`.\n"]}
|
|
||||||
extra = {"docs/CAPABILITIES.md": ["The Foo proxy is what we deploy.\n"]}
|
|
||||||
out = _renames(rs.rename_incomplete_findings(adr_files, extra))
|
|
||||||
assert len(out) == 1
|
|
||||||
assert out[0]["path"] == "docs/CAPABILITIES.md"
|
|
||||||
|
|
||||||
|
|
||||||
def test_rename_incomplete_ignores_ambiguous_adr_pointer_assertion():
|
|
||||||
# "the ADR-017 prose ... is updated to read Caddy" must NOT parse ADR-017 as the
|
|
||||||
# old name (it is a doc pointer). With ADR-017 rejected, no assertion → no finding,
|
|
||||||
# even though 'ADR-017' appears in many other docs.
|
|
||||||
adr_files = {
|
|
||||||
"docs/decisions/024-reverse-proxy.md": [
|
|
||||||
"## Consequences\n",
|
|
||||||
'- ADR-017 prose that mentioned Traefik is updated to read "Caddy".\n'],
|
|
||||||
"docs/decisions/008-testing.md": [
|
|
||||||
"Level 4 UI verification follows ADR-017.\n"],
|
|
||||||
}
|
|
||||||
out = _renames(rs.rename_incomplete_findings(adr_files, {}))
|
|
||||||
assert out == []
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue