|
| 1 | +#!/usr/bin/env bash |
| 2 | +# wiki.sh — the one entry point for team-wiki access. |
| 3 | +# |
| 4 | +# Every subcommand appends one line to the access log before it returns, which |
| 5 | +# is the whole reason the helper exists. A wiki is a directory, and `grep` and |
| 6 | +# `cat` would read it perfectly well; what they cannot do is tell you afterwards |
| 7 | +# whether anyone read it. Route access through one command and the answer is a |
| 8 | +# file you can count. |
| 9 | +# |
| 10 | +# The log write is best-effort on purpose. A failed append warns and the read |
| 11 | +# still returns its content, because an agent that cannot log is still an agent |
| 12 | +# that needs the page. |
| 13 | +set -euo pipefail |
| 14 | + |
| 15 | +usage() { |
| 16 | + cat <<'USAGE' |
| 17 | +usage: wiki.sh <command> [args] |
| 18 | +
|
| 19 | + setup [<city-root>] create the wiki and drop a pointer to this helper |
| 20 | + in the city root, so agents can find it by one path |
| 21 | + search <pattern> grep the wiki (exit 1 on no match, like grep) |
| 22 | + read <path> print one page |
| 23 | + write <path> write a page from stdin and commit it |
| 24 | + list [<path>] list pages |
| 25 | + log print the access log |
| 26 | +
|
| 27 | +The wiki lives at $TEAM_WIKI_PATH, defaulting to team-wiki inside the city. |
| 28 | +USAGE |
| 29 | +} |
| 30 | + |
| 31 | +# ── Paths ───────────────────────────────────────────────────────────── |
| 32 | +# Three contexts run this script — an agent session, the order's exec, and the |
| 33 | +# participant's own shell — and all three have to resolve the same root, or the |
| 34 | +# order counts a wiki nobody is writing to. GC_CITY_PATH covers the first two, |
| 35 | +# because the controller sets it for a session and for an exec order alike, and |
| 36 | +# FACTORY_PATH covers the third. |
| 37 | +# |
| 38 | +# GC_STORE_ROOT is deliberately not in that chain. For a rig-scoped order it is |
| 39 | +# the RIG root rather than the city root, and this pack installs rig-scoped, so |
| 40 | +# reading it first is what splits the order off from everything else. |
| 41 | +# |
| 42 | +# `setup` also takes the root as an argument, which is how the agents' pre_start |
| 43 | +# passes {{.CityRoot}} before either variable is guaranteed to be there. |
| 44 | +# Resolving to empty is a real state rather than an error, because `setup` has |
| 45 | +# to survive it: a pre_start that exits non-zero is a pre_start that stops the |
| 46 | +# agent from starting. |
| 47 | +CITY="${GC_CITY_PATH:-${FACTORY_PATH:-}}" |
| 48 | +WIKI="${TEAM_WIKI_PATH:-${CITY:+$CITY/team-wiki}}" |
| 49 | +LOG="${WIKI_ACCESS_LOG:-${CITY:+$CITY/wiki-access.jsonl}}" |
| 50 | + |
| 51 | +require_city() { |
| 52 | + [ -n "$WIKI" ] || { |
| 53 | + echo "wiki.sh: set TEAM_WIKI_PATH, or run this where GC_CITY_PATH or FACTORY_PATH is set" >&2 |
| 54 | + exit 2 |
| 55 | + } |
| 56 | +} |
| 57 | + |
| 58 | +# ── The log ─────────────────────────────────────────────────────────── |
| 59 | +# One compact JSON object per line. `agent` is whoever ran the command, which |
| 60 | +# is an agent's own name when the factory did it and your shell user when you |
| 61 | +# did it by hand; both are worth telling apart when you read the balance. |
| 62 | +log_event() { |
| 63 | + local op="$1" target="${2-}" query="${3-}" hits="${4-}" |
| 64 | + command -v jq >/dev/null 2>&1 || { |
| 65 | + echo "wiki.sh: jq is not installed, so this access went unlogged" >&2 |
| 66 | + return 0 |
| 67 | + } |
| 68 | + jq -nc \ |
| 69 | + --arg ts "$(date -u '+%Y-%m-%dT%H:%M:%SZ')" \ |
| 70 | + --arg agent "${GC_AGENT:-${USER:-unknown}}" \ |
| 71 | + --arg op "$op" --arg target "$target" --arg query "$query" --arg hits "$hits" \ |
| 72 | + '{ts: $ts, agent: $agent, op: $op} |
| 73 | + + (if $target == "" then {} else {target: $target} end) |
| 74 | + + (if $query == "" then {} else {query: $query} end) |
| 75 | + + (if $hits == "" then {} else {hits: ($hits | tonumber)} end)' \ |
| 76 | + >> "$LOG" 2>/dev/null \ |
| 77 | + || echo "wiki.sh: could not append to $LOG, so this access went unlogged" >&2 |
| 78 | +} |
| 79 | + |
| 80 | +require_wiki() { |
| 81 | + require_city |
| 82 | + [ -d "$WIKI/.git" ] || { |
| 83 | + echo "wiki.sh: no wiki at $WIKI — run 'wiki.sh setup' first" >&2 |
| 84 | + exit 2 |
| 85 | + } |
| 86 | +} |
| 87 | + |
| 88 | +# ── Commands ────────────────────────────────────────────────────────── |
| 89 | +# Runs from the agents' pre_start as well as by hand, so it never fails: an |
| 90 | +# agent that cannot get a wiki should still start, and the helper says what is |
| 91 | +# missing when the agent reaches for it. |
| 92 | +cmd_setup() { |
| 93 | + local root="${1:-$CITY}" |
| 94 | + [ -n "$WIKI" ] || WIKI="${root:+$root/team-wiki}" |
| 95 | + [ -n "$LOG" ] || LOG="${root:+$root/wiki-access.jsonl}" |
| 96 | + if [ -z "$WIKI" ]; then |
| 97 | + echo "wiki.sh: no city root to hang the wiki off, so nothing was created" >&2 |
| 98 | + return 0 |
| 99 | + fi |
| 100 | + if [ ! -d "$WIKI/.git" ]; then |
| 101 | + seed_wiki || echo "wiki.sh: could not create the wiki at $WIKI" >&2 |
| 102 | + fi |
| 103 | + # The prompt fragment calls the helper as <city-root>/wiki.sh, because a |
| 104 | + # prompt template can resolve the city root and cannot reach the pack |
| 105 | + # directory this script actually lives in. The symlink closes that gap, and |
| 106 | + # `ls -l` on it shows where the real script is. |
| 107 | + if [ -n "$root" ] && [ -d "$root" ]; then |
| 108 | + local self |
| 109 | + self="$(cd "$(dirname "$0")" && pwd)/$(basename "$0")" |
| 110 | + # Run as `./wiki.sh setup` from the city root and $0 is the symlink, so |
| 111 | + # re-linking would point it at itself and every later call dies on ELOOP. |
| 112 | + if [ "$self" != "$root/wiki.sh" ]; then |
| 113 | + ln -sfn "$self" "$root/wiki.sh" \ |
| 114 | + || echo "wiki.sh: could not link $root/wiki.sh" >&2 |
| 115 | + fi |
| 116 | + fi |
| 117 | + return 0 |
| 118 | +} |
| 119 | + |
| 120 | +seed_wiki() { |
| 121 | + mkdir -p "$WIKI" |
| 122 | + git -C "$WIKI" init --quiet |
| 123 | + cat > "$WIKI/README.md" <<'SEED' |
| 124 | +# Team wiki |
| 125 | +
|
| 126 | +Durable findings, written once and read by everyone after. |
| 127 | +
|
| 128 | +A page belongs here when a colleague hitting the same thing in six months would |
| 129 | +save time by reading it: a non-obvious failure mode and its workaround, an |
| 130 | +incident and how it was found, a decision and the reasoning behind it. Anything |
| 131 | +that only matters to the work in flight belongs on the bead instead. |
| 132 | +
|
| 133 | +Write pages through `wiki.sh` rather than your editor, so every read and write |
| 134 | +lands in the access log and the factory can tell a wiki people use from a |
| 135 | +directory nobody opens. |
| 136 | +SEED |
| 137 | + git -C "$WIKI" add README.md |
| 138 | + git -C "$WIKI" -c user.email=wiki@localhost -c user.name="team wiki" \ |
| 139 | + commit --quiet -m "Start the team wiki" |
| 140 | + echo "Created $WIKI" |
| 141 | +} |
| 142 | + |
| 143 | +cmd_search() { |
| 144 | + require_wiki |
| 145 | + [ $# -ge 1 ] || { usage >&2; exit 2; } |
| 146 | + local pattern="$1"; shift |
| 147 | + local out rc=0 |
| 148 | + # Run from inside the wiki so hits come back as wiki-relative paths, which |
| 149 | + # is what `wiki.sh read` takes. |
| 150 | + out="$(cd "$WIKI" && grep -rn --exclude-dir=.git -- "$pattern" . 2>/dev/null | sed 's|^\./||')" || rc=$? |
| 151 | + local hits=0 |
| 152 | + [ -n "$out" ] && hits="$(printf '%s\n' "$out" | wc -l | tr -d ' ')" |
| 153 | + log_event search "" "$pattern" "$hits" |
| 154 | + [ -n "$out" ] && printf '%s\n' "$out" |
| 155 | + return "$rc" |
| 156 | +} |
| 157 | + |
| 158 | +cmd_read() { |
| 159 | + require_wiki |
| 160 | + [ $# -eq 1 ] || { usage >&2; exit 2; } |
| 161 | + local rel="$1" |
| 162 | + [ -f "$WIKI/$rel" ] || { echo "wiki.sh: no page at $rel" >&2; exit 1; } |
| 163 | + log_event read "$rel" |
| 164 | + cat "$WIKI/$rel" |
| 165 | +} |
| 166 | + |
| 167 | +cmd_write() { |
| 168 | + require_wiki |
| 169 | + [ $# -eq 1 ] || { usage >&2; exit 2; } |
| 170 | + local rel="$1" |
| 171 | + mkdir -p "$(dirname "$WIKI/$rel")" |
| 172 | + cat > "$WIKI/$rel" |
| 173 | + git -C "$WIKI" add -- "$rel" |
| 174 | + # Nothing staged means the body matched what was already there, which is a |
| 175 | + # real outcome rather than an error: two agents can reach the same finding. |
| 176 | + if git -C "$WIKI" diff --cached --quiet -- "$rel"; then |
| 177 | + log_event write "$rel" |
| 178 | + echo "No change to $rel" |
| 179 | + return 0 |
| 180 | + fi |
| 181 | + git -C "$WIKI" -c user.email=wiki@localhost -c user.name="team wiki" \ |
| 182 | + commit --quiet -m "Update $rel" |
| 183 | + log_event write "$rel" |
| 184 | + echo "Wrote $rel" |
| 185 | +} |
| 186 | + |
| 187 | +cmd_list() { |
| 188 | + require_wiki |
| 189 | + local rel="${1-}" |
| 190 | + log_event list "$rel" |
| 191 | + ( cd "$WIKI" && git ls-files -- "${rel:-.}" ) |
| 192 | +} |
| 193 | + |
| 194 | +cmd_log() { |
| 195 | + # Reading the log is not wiki access, so this one does not log itself. |
| 196 | + require_city |
| 197 | + [ -f "$LOG" ] && cat "$LOG" || echo "No access log yet at $LOG" >&2 |
| 198 | +} |
| 199 | + |
| 200 | +case "${1-}" in |
| 201 | + setup) shift; cmd_setup "$@" ;; |
| 202 | + search) shift; cmd_search "$@" ;; |
| 203 | + read) shift; cmd_read "$@" ;; |
| 204 | + write) shift; cmd_write "$@" ;; |
| 205 | + list) shift; cmd_list "$@" ;; |
| 206 | + log) shift; cmd_log "$@" ;; |
| 207 | + -h|--help|help) usage ;; |
| 208 | + *) usage >&2; exit 2 ;; |
| 209 | +esac |
0 commit comments