Skip to content

Commit 3ad33f2

Browse files
authored
Merge branch 'main' into remote-pack-install-in-l2-l5-labs
2 parents 9ed855e + 634b755 commit 3ad33f2

21 files changed

Lines changed: 1769 additions & 15 deletions

File tree

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
# internal-wiki
2+
3+
Shared team memory, so your factory stops re-deriving what it already learned.
4+
5+
Almost every agent you run is ephemeral: it spawns, does one thing, and exits with everything it discovered. That is the right design for work and the wrong one for knowledge, and [W6](../../../progression/W6-advanced-concepts.md) names the two mechanics that fix it. This pack ships both, sized for a lab block rather than for a production factory.
6+
7+
## Install
8+
9+
```bash
10+
cd "$FACTORY_PATH"
11+
gc import add --rig <rig-name> $SFI_PATH/sf-tutorial/artifacts/packs/internal-wiki
12+
gc reload
13+
```
14+
15+
Rig scope, because the pack patches the rig's own agents. It imports `architect-rig` the way the other option packs do, so it'll compose on top of [the base factory](../base-factory/README.md) alone and you can take the options in any order.
16+
17+
Nothing else to configure. The wiki is created on first use at `team-wiki` inside the city, which is enough to watch the whole loop work before you've got a shared repository to point it at.
18+
19+
## The two halves
20+
21+
```mermaid
22+
flowchart LR
23+
subgraph prompt["Per task — a prompt fragment"]
24+
A["Agent wakes"] --> B["Search the wiki<br/>before researching"]
25+
B --> C["Do the work"]
26+
C --> D["Write a page<br/>at the boundary"]
27+
end
28+
subgraph periodic["Periodic — an order"]
29+
E["Hourly"] --> F["Count the access log"]
30+
F --> G["Reads vs writes<br/>in WIKI_LOG.md"]
31+
end
32+
D -.->|"every access<br/>leaves a line"| F
33+
```
34+
35+
Reading is per task, so it belongs in a prompt. The fragment in `template-fragments/` carries the two habits, and the patches in `pack.toml` append it to the polecat and the refinery. This is the first fragment in any of the tutorial's packs, and W4 called it: the moment two agents mustn't contradict each other on a rule, that rule wants to be a fragment rather than a paragraph copied into both prompts.
36+
37+
Counting is periodic. Nothing asks for the read-to-write balance and no bead's ever going to carry it, which is what makes `orders/wiki-balance.toml` an order rather than work.
38+
39+
## Why one helper instead of grep
40+
41+
A wiki is a directory, and `grep` reads a directory perfectly well. What `grep` can't do is tell you afterwards whether anybody read it.
42+
43+
So every access goes through `assets/scripts/wiki.sh`, which appends one line to an access log before it returns. That turns "is the team actually using the wiki" from a question you argue about into a file you can count, and counting it is the order's whole job. The log write is best-effort on purpose: if it fails, the read still returns its content, because an agent that can't log is still an agent that needs the page.
44+
45+
## Try it
46+
47+
Two agents, one finding. Play both parts yourself and it's about a minute's work.
48+
49+
```bash
50+
cd "$FACTORY_PATH"
51+
../sf-tutorial/artifacts/packs/internal-wiki/assets/scripts/wiki.sh setup
52+
53+
# The first agent hits something surprising and writes it down.
54+
./wiki.sh write operations/worktree-stale-lock.md <<'PAGE'
55+
# A stale lock file survives a crashed worktree setup
56+
57+
Expected the next run to clean up after itself. It did not: the lock file
58+
outlives the process that wrote it, so every later run fails the same way.
59+
Delete the lock before re-running, and check for one first when setup hangs.
60+
PAGE
61+
62+
# The second agent is about to research the same thing, and checks first.
63+
./wiki.sh search "stale lock"
64+
./wiki.sh read operations/worktree-stale-lock.md
65+
66+
# What the factory now knows about its own reading and writing.
67+
gc order run wiki-balance
68+
```
69+
70+
The first line is the only one that spells out the pack path. `setup` leaves the symlink behind, so everything after it reaches the helper as `./wiki.sh`.
71+
72+
That last line prints the balance and appends it to `WIKI_LOG.md` in the city root. `gc order check` says when it's next due, and it fires by itself hourly.
73+
74+
The half you can't see in a minute is the agents doing it unprompted. Sling any bead after installing this and read the polecat's prompt with `gc prime`: the fragment is on the end of it, and `wiki-access.jsonl` starts filling with lines whose agent isn't you.
75+
76+
## The helper
77+
78+
```bash
79+
./wiki.sh setup [<city-root>] # create the wiki, and link the helper into the city root
80+
./wiki.sh search <pattern> # grep the wiki; exits 1 on no match, like grep
81+
./wiki.sh read <path> # print one page
82+
./wiki.sh write <path> # write a page, body on stdin, committed for you
83+
./wiki.sh list [<path>] # what pages exist
84+
./wiki.sh log # the raw access log
85+
```
86+
87+
`setup` runs from the patched agents' `pre_start` as well as by hand, and it's written so it can't fail: an agent that couldn't get a wiki should still start. The symlink it leaves at `<city>/wiki.sh` is what lets the prompt fragment name one path that works, since a prompt template can resolve the city root but can't reach the pack directory the real script lives in. `ls -l` on it shows where that is.
88+
89+
## Variables
90+
91+
| Variable | Default | What it does |
92+
|---|---|---|
93+
| `TEAM_WIKI_PATH` | `team-wiki` inside the city | Where the wiki lives. Point it at a shared repository once you have one. |
94+
| `WIKI_ACCESS_LOG` | `wiki-access.jsonl` in the city root | Where accesses are recorded. |
95+
96+
Set both in your shell for the commands you run by hand. The order needs its own copy, because an exec order doesn't load the city's `.env`; uncomment `[order.env]` at the bottom of `orders/wiki-balance.toml` and put the path there.
97+
98+
## Pointing it at a real repository
99+
100+
The default wiki is a git repo with no remote, which is private note-taking with extra steps. The point is the shared read, so once the pack has earned its place, give it somewhere everyone can reach:
101+
102+
```bash
103+
git -C "$FACTORY_PATH/team-wiki" remote add origin <your-wiki-repo-url>
104+
git -C "$FACTORY_PATH/team-wiki" push -u origin main
105+
```
106+
107+
`wiki.sh` commits and doesn't push, so pushing stays a thing a person decides to do. That's the same line the self-improvement option draws: the factory may write a proposal, and a human decides what leaves the machine.
108+
109+
## Turning it off
110+
111+
```bash
112+
gc import remove --rig <rig-name> internal-wiki
113+
gc reload
114+
```
115+
116+
That takes the order and the prompt fragment with it. The wiki, the access log and `WIKI_LOG.md` are yours and stay where they are; delete them by hand if you'd rather they were gone.
Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
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
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
#!/usr/bin/env bash
2+
# wiki_balance.sh — one line per firing: how much the factory read the wiki,
3+
# how much it wrote, and how long since anyone added a page. Fired hourly by
4+
# orders/wiki-balance.toml.
5+
#
6+
# A wiki fails in two directions and the pair of numbers tells you which one you
7+
# have. Reads near zero means a filing cabinet nobody opens, and the fix is in
8+
# the agents' prompts. Writes near zero with healthy reads means it is going
9+
# stale, and the fix is at the end of a task rather than the start.
10+
set -euo pipefail
11+
12+
# GC_CITY_PATH is set for an exec order and for an agent session alike, and
13+
# FACTORY_PATH is what a participant has by hand, so all three read the same
14+
# root. GC_STORE_ROOT is the rig root for a rig-scoped order, which is how this
15+
# script would end up counting a wiki nobody writes to; see wiki.sh.
16+
CITY="${GC_CITY_PATH:-${FACTORY_PATH:-}}"
17+
[ -n "$CITY" ] || { echo "wiki_balance.sh: no GC_CITY_PATH or FACTORY_PATH to resolve the city" >&2; exit 2; }
18+
19+
WIKI="${TEAM_WIKI_PATH:-$CITY/team-wiki}"
20+
LOG="${WIKI_ACCESS_LOG:-$CITY/wiki-access.jsonl}"
21+
OUT="$CITY/WIKI_LOG.md"
22+
23+
if [ ! -s "$OUT" ]; then
24+
cat > "$OUT" <<'HEADER'
25+
# Wiki log
26+
27+
Written hourly by the `wiki-balance` order in the `internal-wiki` pack. Each
28+
line is the running total of reads and writes against the team wiki, plus how
29+
long it has been since anyone added a page. Subtract two lines to get the
30+
balance for the hours between them.
31+
32+
Reads near zero means nobody is consulting the wiki, so the factory is still
33+
re-deriving what it already knows. Writes near zero means nothing new is going
34+
in. Both are worth noticing, and neither shows up anywhere else.
35+
36+
HEADER
37+
fi
38+
39+
count_op() {
40+
[ -s "$LOG" ] || { printf '0'; return 0; }
41+
jq -r --arg op "$1" 'select(.op == $op) | .op' "$LOG" 2>/dev/null | wc -l | tr -d ' '
42+
}
43+
44+
READS=$(( $(count_op search) + $(count_op read) + $(count_op list) ))
45+
WRITES="$(count_op write)"
46+
47+
if [ -d "$WIKI/.git" ]; then
48+
PAGES="$(cd "$WIKI" && git ls-files '*.md' | wc -l | tr -d ' ')"
49+
# The newest commit stands in for the newest page: every write goes through
50+
# `wiki.sh write`, which commits, so the two move together.
51+
LAST="$(cd "$WIKI" && git log -1 --format=%cr 2>/dev/null || printf 'never')"
52+
else
53+
PAGES=0
54+
LAST="no wiki yet"
55+
fi
56+
57+
LINE="- balance $(date -u '+%Y-%m-%dT%H:%MZ') — reads $READS, writes $WRITES, pages $PAGES, newest page $LAST"
58+
printf '%s\n' "$LINE" >> "$OUT"
59+
printf '%s\n' "$LINE"

0 commit comments

Comments
 (0)