# /wiki-compile — Compile session logs and Syncro data into wiki articles

Seed new wiki articles or refresh existing ones from session logs, client documents, and live Syncro PSA data.

---

## Usage

```
/wiki-compile client:<slug>          UPDATE an existing article (fast, incremental) or seed a new one
/wiki-compile client:<slug> --full   REBUILD: full re-synthesis from ALL sources (Sonnet, slow)
/wiki-compile client:<slug> --syncro Syncro dynamic fields only (hours/tickets) — instant, no LLM
/wiki-compile project:<slug>         Compile a project wiki article (no Syncro)
/wiki-compile system:<slug>          Compile a system wiki article (no Syncro)
/wiki-compile all                    Process all missing + stale articles
```

**Mode auto-detection:**
- If `wiki/clients/<slug>.md` **does not exist** → **Seed mode** (full synthesis, Sonnet subagent).
- If it **exists** and no flag → **Update mode** (fast, incremental — the default; see below).
- `--full` → **Rebuild** (full Sonnet re-synthesis from ALL sources; preserves Patterns/History).
- `--syncro` → **Syncro-only refresh** (dynamic fields only; instant, no LLM).

**Update vs Rebuild — why update is fast (this is the point).** A **Rebuild** (`--full`) reads
*every* session log for the client and regenerates the *entire* article with a Sonnet subagent —
correct, but slow and expensive, and wasteful when only one thing changed. **Update** reads ONLY
the session logs dated after the article's `last_compiled` (usually 1–3 files, often zero) plus the
current article, and applies a few **surgical section edits** — new History rows, and targeted
edits to any section a new log actually changes — leaving the rest of the article byte-for-byte
untouched. Small input + small output + no full-article Sonnet pass = typically many times faster.
Reach for `--full` only when the article structure has drifted, sections are stale/wrong, or you
want a periodic clean rebuild. For "I just did some work, capture it," use plain update.

---

## Phase 0 — Setup

```bash
# Resolves CLAUDETOOLS_ROOT + VAULT_ROOT from identity.json and reads the caller's
# per-user Syncro key from the SOPS vault. Never hardcode a repo path or an API key.
# Exports: CLAUDETOOLS_ROOT, VAULT_ROOT, SYNCRO_USER, SYNCRO_BASE, SYNCRO_API_KEY.
source "$(git rev-parse --show-toplevel)/.claude/scripts/syncro-env.sh" || true

VAULT="$CLAUDETOOLS_ROOT/.claude/scripts/vault.sh"
BASE="$SYNCRO_BASE"          # read-only: GET requests ONLY in this skill
API_KEY="$SYNCRO_API_KEY"    # empty if no key vaulted for this user -> skip enrichment

if [ -z "$API_KEY" ]; then
  echo "[WARNING] No Syncro key for user '${SYNCRO_USER:-unknown}' — Syncro enrichment skipped"
fi
```

**Synthesis engine:** seed/full article drafting is done by a **Sonnet subagent** (Agent tool, `model: "sonnet"`), not Ollama. The main agent gathers sources + Syncro data, delegates the draft, then reviews it before writing. No Ollama dependency.

---

## Phase 1 — Argument Parsing

Parse the target and flags:

```bash
# Extract type and slug from argument like "client:cascades-tucson" or "client:cascades-tucson --full"
TARGET_RAW="$1"                          # e.g. "client:cascades-tucson"
FULL_FLAG="${2:-}"                       # "--full" or empty

TARGET_TYPE="${TARGET_RAW%%:*}"          # client | project | system | all
SLUG="${TARGET_RAW#*:}"                  # cascades-tucson

# Determine article path
case "$TARGET_TYPE" in
  client)  ARTICLE_PATH="wiki/clients/${SLUG}.md" ;;
  project) ARTICLE_PATH="wiki/projects/${SLUG}.md" ;;
  system)  ARTICLE_PATH="wiki/systems/${SLUG}.md" ;;
  all)     # handled separately — see "all" mode below
esac

# Mode detection
if [ ! -f "$CLAUDETOOLS_ROOT/$ARTICLE_PATH" ]; then
  MODE="seed"
elif [ "$FULL_FLAG" = "--full" ]; then
  MODE="full"          # rebuild — full Sonnet re-synthesis
elif [ "$FULL_FLAG" = "--syncro" ]; then
  MODE="syncro"        # Syncro dynamic fields only
else
  MODE="update"        # default: fast incremental knowledge merge
fi

echo "[INFO] Mode: $MODE | Target: $TARGET_TYPE:$SLUG"

# For update mode, read the article's last_compiled so Phase 3 can select only newer logs.
LAST_COMPILED=""
if [ "$MODE" = "update" ]; then
  LAST_COMPILED=$(sed -n 's/^last_compiled:[[:space:]]*//p' "$CLAUDETOOLS_ROOT/$ARTICLE_PATH" | head -1)
  echo "[INFO] Update since last_compiled=${LAST_COMPILED:-unknown}"
fi
```

---

## Phase 2 — Syncro Enrichment (clients only, skip for project/system)

**Skip this phase entirely if `TARGET_TYPE != client` or `API_KEY` is empty.**

### 2a — Customer Search

Convert slug to a Syncro search query:

```bash
# Replace hyphens with spaces for the search query
SEARCH_QUERY=$(echo "$SLUG" | sed 's/-/ /g')
URLQ() { python -c "import urllib.parse,sys; print(urllib.parse.quote(sys.argv[1]))" "$1"; }

# Use the FUZZY `query=` param, not `name=`. `name=` is near-exact and misses
# singular/plural and word-order mismatches between the slug and the Syncro
# business name (e.g. slug `gonzvar-tax-services` vs Syncro "Gonzvar Tax Service").
CUST_RESULTS=$(curl -s "$BASE/customers?query=$(URLQ "$SEARCH_QUERY")&per_page=5&api_key=$API_KEY")
CUST_COUNT=$(echo "$CUST_RESULTS" | jq '.customers | length')

# Fallback ladder if 0: retry with progressively shorter fuzzy queries
# (first word, then the distinctive surname/token) before declaring "not found".
if [ "$CUST_COUNT" = "0" ]; then
  for Q in "$(echo "$SEARCH_QUERY" | awk '{print $1}')" "$(echo "$SEARCH_QUERY" | awk '{print $1}' | sed 's/s$//')"; do
    [ -z "$Q" ] && continue
    CUST_RESULTS=$(curl -s "$BASE/customers?query=$(URLQ "$Q")&per_page=10&api_key=$API_KEY")
    CUST_COUNT=$(echo "$CUST_RESULTS" | jq '.customers | length')
    [ "$CUST_COUNT" != "0" ] && echo "[SYNCRO] matched on fuzzy fallback '$Q'" && break
  done
fi
```

> If the fuzzy/fallback search returns several, fall through to the 2+ disambiguation
> below; if still 0, only THEN treat as "not in Syncro". Do not conclude "not found"
> from the exact `name=` search alone — that was the Gonzvar miss.

**If 0 results:**
```
[SYNCRO] No customer found matching '${SEARCH_QUERY}' — skipping Syncro enrichment.
         Proceeding with session logs only.
```
Set `SYNCRO_DATA=""` and continue to Phase 3.

**If 2+ results:** Show the list and PAUSE execution:
```
[SYNCRO] Multiple customers match '${SEARCH_QUERY}':
  1. <id>  <business_name>  (<city, state>)
  2. <id>  <business_name>  (<city, state>)
  ...

Which customer should be used for this wiki article? (Enter number, or 'skip' to skip Syncro)
```
Wait for user input before continuing. If user says `skip`, treat as 0 results.

**If exactly 1 result:** Proceed immediately.

### 2b — Customer Profile Pull

```bash
CUST_ID=$(echo "$CUST_RESULTS" | jq -r '.customers[0].id')

# Full customer record
CUST=$(curl -s "$BASE/customers/${CUST_ID}?api_key=$API_KEY" | jq '.customer')

DISPLAY_NAME=$(echo "$CUST" | jq -r '.business_name // .firstname + " " + .lastname')
PREPAY_HOURS=$(echo "$CUST" | jq -r '.prepay_hours // "0"')
NOTES=$(echo "$CUST" | jq -r '.notes // ""')

# Contacts: name, title, email, phone
CONTACTS=$(echo "$CUST" | jq -r '
  .contacts[]? |
  "\(.firstname) \(.lastname)" +
  (if .title != "" and .title != null then " (\(.title))" else "" end) +
  (if .email != "" and .email != null then " — \(.email)" else "" end) +
  (if .mobile != "" and .mobile != null then ", \(.mobile)" elif .phone != "" and .phone != null then ", \(.phone)" else "" end)
')
```

### 2c — Open Tickets

```bash
OPEN_TICKETS=$(curl -s "$BASE/tickets?customer_id=${CUST_ID}&status=New,In+Progress,Scheduled,Waiting+on+Customer&per_page=10&api_key=$API_KEY" | jq '.tickets[]? | {id, number, subject, status, created_at, user_id}')
TICKET_COUNT=$(echo "$OPEN_TICKETS" | jq -s 'length')
```

### 2d — Recent Invoices (last 12)

```bash
RECENT_INVOICES=$(curl -s "$BASE/invoices?customer_id=${CUST_ID}&per_page=12&api_key=$API_KEY" | jq '[.invoices[]? | {id, number, date, total, status}]')
```

Used only to infer billing pattern (break-fix vs prepaid, rate hints). Do not expose raw invoice data in the wiki.

### 2e — Asset Count

```bash
ASSET_COUNT=$(curl -s "$BASE/customer_assets?customer_id=${CUST_ID}&per_page=200&api_key=$API_KEY" | jq '[.assets[]?] | length')
```

Only the count is used — individual asset details go in session logs and client docs, not the wiki.

---

## Phase 2P — Authoritative Artifact Discovery (projects only)

**Applies when `TARGET_TYPE == project`.** Skip for clients/systems.

Session logs narrate *work done and why* — they are a structurally incomplete record of *what a product can do*. For a code-bearing project, the authoritative capability record is the **code, migrations, API routes, and roadmap**, not the logs. Compiling a project from logs alone WILL miss shipped features (this is exactly how the GuruRMM `user_session` command context was missed). So for projects, dig into the artifacts directly.

### 2P-a — Locate the repo and guard against a stale submodule

Many projects are tracked as a **pinned git submodule** whose commit deliberately lags the live repo. Reading the working tree alone gives stale artifacts. Always check:

```bash
REPO="$CLAUDETOOLS_ROOT/projects/<path-to-submodule-or-repo>"
cd "$REPO"
git fetch origin main 2>/dev/null
PINNED=$(git rev-parse --short HEAD)
LIVE=$(git rev-parse --short origin/main)
echo "pinned=$PINNED live=$LIVE"
# If they differ, read artifacts from origin/main, NOT the working tree.
REF="origin/main"   # use this ref for all artifact reads below; falls back to HEAD if no remote
```

Read live artifacts without disturbing the pinned pointer, either via `git show $REF:<path>` / `git ls-tree -r --name-only $REF -- <dir>`, or by creating a throwaway worktree: `git worktree add /tmp/<proj>-live $REF` (remove with `git worktree remove` when done).

### 2P-b — Gather authoritative artifacts (priority order)

1. **DB migrations** — `git ls-tree -r --name-only $REF -- <migrations-dir>`. Each migration is a feature/schema checkpoint; the filenames alone are a capability timeline (e.g. `041_add_command_context`). This is the most reliable signal and is usually current even when changelogs are not.
2. **API routes** — the real server surface. Read the route-registration file(s) (e.g. `server/src/api/mod.rs`) and the per-resource handler modules. Enumerate endpoints + notable request options (auth modes, contexts, scopes).
3. **Agent / client capabilities** — module tree of the agent/client (e.g. `agent/src/`): metrics, checks, command execution, updater, tunnel, watchdog, inventory, registry ops. Note **per-platform coverage**.
4. **Completed roadmap items** — `docs/FEATURE_ROADMAP.md` checked/done items.
5. **Specs** — `docs/specs/` (shipped vs proposed).
6. **Commit log** — `git log --oneline $REF` filtered for `feat`/`perf` since the article's `last_compiled`. Fuller and more current than changelogs.
7. **Changelogs** — read if present, but treat as **incomplete** (they are frequently stale — verified on GuruRMM: committed changelogs stopped at v0.6.22 while the fleet ran 0.6.39+). Never rely on them as the sole capability source.
8. **README / DESIGN / ARCHITECTURE docs** — for framing and locked decisions.

### 2P-c — Synthesize the Capabilities / Feature Set section

From the artifacts above, produce the **Capabilities / Feature Set** section (see project template). Organize by surface (monitoring, remote execution, management, integrations, security). Explicitly capture **execution modes and important options** — e.g. command contexts (`system` vs `user_session`), auth modes, policy scopes, platform coverage. Cross-check the existing article (full recompile) and **correct any capability statement that is now incomplete or wrong** (e.g. "runs as LocalSystem" without the user-session context).

For large repos, delegate the artifact read + synthesis to an agent (general-purpose) pointed at the live ref/worktree, and integrate the returned section after review — don't flood the main context with the full code/migration dump.

---

## Phase 3 — Session Log Discovery

Find all session logs that mention this client:

```bash
cd "$CLAUDETOOLS_ROOT"

# 1. Client-specific session logs (all)
CLIENT_LOGS=$(find "clients/${SLUG}/session-logs/" -name "*.md" 2>/dev/null | sort)

# 2. Client-specific docs (README, CONTEXT.md, overview.md, etc.)
CLIENT_DOCS=$(find "clients/${SLUG}/" -name "*.md" -not -path "*/session-logs/*" 2>/dev/null | sort)

# 3. Root session logs mentioning this client (case-insensitive grep on slug and display name)
ROOT_LOGS=$(grep -ril "$SLUG\|$(echo "$DISPLAY_NAME" | sed 's/ /\\|/g')" session-logs/*.md 2>/dev/null | sort)

# 4. Memory files referencing this client
MEMORY_FILES=$(grep -ril "$SLUG" .claude/memory/*.md 2>/dev/null | sort)

# Deduplicate and collect all source paths
ALL_SOURCES=$(echo "$CLIENT_LOGS $CLIENT_DOCS $ROOT_LOGS $MEMORY_FILES" | tr ' ' '\n' | sort -u | grep -v '^$')
SOURCE_COUNT=$(echo "$ALL_SOURCES" | grep -c '^' || echo 0)
echo "[INFO] Found $SOURCE_COUNT source files"
```

**Update mode — narrow to NEW sources only (this is the speedup).** In update mode, do NOT read
the full source set. Select only the logs the article has not yet incorporated: a source is "new"
if its filename date is **after** `LAST_COMPILED`, **or** it is not already listed in the article's
frontmatter `sources:`. Read only those.

```bash
if [ "$MODE" = "update" ]; then
  # existing sources already folded into the article
  EXISTING_SRC=$(awk '/^sources:/{f=1;next} /^[^ -]/{f=0} f&&/^[[:space:]]*-/{sub(/^[[:space:]]*-[[:space:]]*/,"");print}' "$CLAUDETOOLS_ROOT/$ARTICLE_PATH")
  NEW_SOURCES=$(echo "$ALL_SOURCES" | while read -r f; do
    [ -z "$f" ] && continue
    # (a) not yet in the article's sources list?
    if ! grep -qxF "$f" <<<"$EXISTING_SRC"; then echo "$f"; continue; fi
    # (b) filename carries a YYYY-MM-DD newer than last_compiled?
    d=$(echo "$f" | grep -oE '[0-9]{4}-[0-9]{2}-[0-9]{2}' | head -1)
    if [ -n "$d" ] && [ -n "$LAST_COMPILED" ] && [ "$d" \> "$LAST_COMPILED" ]; then echo "$f"; fi
  done | sort -u | grep -v '^$')
  NEW_COUNT=$(echo "$NEW_SOURCES" | grep -c '^' || echo 0)
  echo "[INFO] Update: $NEW_COUNT new source(s) since ${LAST_COMPILED:-unknown}"
fi
```

If `SOURCE_COUNT == 0` and no Syncro data: warn and stop.
```
[ERROR] No session logs and no Syncro data found for '${SLUG}'. Cannot compile.
        Create at least one session log in clients/${SLUG}/session-logs/ first.
```

---

## Phase 4 — Article Generation

### Update Mode (existing article, default) — fast incremental

Fold only what changed since the last compile. **Do NOT re-synthesize the whole article and do
NOT spawn a Sonnet subagent.** Two parts:

**Part A — Syncro dynamic fields** (the three surgical edits documented under *Syncro-only Refresh*
below): hours remaining, Active Work ticket list, and frontmatter (`last_compiled`, `compiled_by`).

**Part B — Incremental knowledge merge** — run ONLY if `NEW_COUNT > 0` (new logs from Phase 3):
1. Read the full text of the `NEW_SOURCES` logs **and the current article**. Do NOT read the full
   historical log set — that is what makes this fast.
2. Apply **targeted edits** for what those new logs actually establish:
   - **Always:** add one dated row per material change to **History Highlights** (chronological).
   - **Infrastructure** — add/adjust a row for any new or removed host, IP, service, or key path.
   - **Access** — add any new vault path or access route (vault path only, never the secret).
   - **Patterns & Known Issues** — add a genuinely new recurring issue, or mark an existing one
     resolved if a new log shows it fixed.
   - Touch **only** the sections a new log changes; leave every other byte of the article intact.
3. The delta is small, so **the main agent applies these edits directly** (Edit tool), or delegates
   only the prose wording to Ollama Tier-0 / `haiku` and reviews it. Follow the same Hard Rules
   (Syncro authoritative for billing; never inline secrets; never invent vault paths).
4. Append the `NEW_SOURCES` paths to frontmatter `sources:` (dedup).

If `NEW_COUNT == 0`: there is nothing new to fold — Part A (Syncro refresh) is the whole update.

Emit:
```
[OK] Update complete for wiki/clients/<slug>.md
     - New logs folded: <NEW_COUNT> (since <LAST_COMPILED>)
     - Sections touched: History[, Infrastructure, Access, Patterns] | none (Syncro-only)
     - Syncro: hours <PREPAY_HOURS>, tickets <TICKET_COUNT>
```

### Syncro-only Refresh (`--syncro`) — instant, no LLM

Perform surgical updates only. No Ollama call. Three edits:

**Edit 1 — Update hours remaining in Profile section:**
Find the `Hours remaining` line and replace with live Syncro value and today's date. Only run if `PREPAY_HOURS` is non-null and non-zero OR if the article currently shows a non-zero balance.

```
- **Hours remaining (if prepaid):** ${PREPAY_HOURS} hrs as of $(date +%Y-%m-%d)
```

**Edit 2 — Update Active Work ticket list:**
Replace the content of `## Active Work` with the Syncro open tickets formatted as:

```markdown
## Active Work

*As of $(date +%Y-%m-%d) — Syncro shows ${TICKET_COUNT} open ticket(s):*

| Ticket | Subject | Status | Opened |
|---|---|---|---|
| #<number> (ID: <id>) | <subject> | <status> | <created_at date> |
```

If `TICKET_COUNT == 0`:
```markdown
## Active Work

*No open tickets in Syncro as of $(date +%Y-%m-%d). See session logs for recent work.*
```

**Edit 3 — Update frontmatter:**
- `last_compiled`: today's date
- `compiled_by`: `<MACHINE>/claude-main`
- Append new source files to `sources:` list (deduplicate)

After edits, emit:
```
[OK] Refresh complete for wiki/clients/<slug>.md
     - Hours: updated to ${PREPAY_HOURS} hrs
     - Active tickets: ${TICKET_COUNT} open
     - Sources: ${SOURCE_COUNT} files tracked
```

### Seed Mode / Rebuild (`--full`) — Claude Synthesis (Sonnet subagent)

Prepare the synthesis context by reading the most relevant source files. For session logs, read the full content of client-specific logs and the first 200 lines of root session logs (to avoid overwhelming the prompt). For full recompile, also read the existing article.

Delegate the draft to a **Sonnet subagent** via the Agent tool (`model: "sonnet"`), passing the brief below. The subagent returns the article markdown; the main agent reviews it (billing/IPs/vault-paths accurate, Patterns/History preserved on full recompile) before writing in Phase 5.

**Subagent brief:**

```
You are compiling a wiki article for an MSP (managed service provider) client.
Produce a structured Markdown article using the template and source data provided.
Be concise, factual, and technical. No filler phrases. No emojis. Past tense for history.
Mark unknown fields as (verify).

---
CLIENT SLUG: <slug>
DISPLAY NAME: <display_name>
COMPILE MODE: <seed|full>

SYNCRO LIVE DATA:
Customer ID: <CUST_ID>
Prepaid Hours: <PREPAY_HOURS>
Asset Count: <ASSET_COUNT>
Open Tickets (<TICKET_COUNT>):
<formatted ticket list>
Contacts:
<formatted contact list>
Billing pattern from invoices: <inferred: prepaid block | break-fix | project-based>

SESSION LOG EXCERPTS:
<content of client session logs and relevant root log sections>

<if MODE=full:>
EXISTING ARTICLE (preserve Patterns and History, update everything else):
<existing article content>
</if>

---
TEMPLATE STRUCTURE TO FOLLOW:
<paste of wiki/_templates/client.md>

---
RULES:
1. BILLING — Syncro is the authoritative source for ALL billing-related fields. Never use session log values for these:
   - Hours remaining: use live `prepay_hours` from Syncro customer record
   - Contract type: if `prepay_hours > 0` → "Prepaid hour block"; if recent invoices show per-ticket billing → "Break-fix"; if large flat invoices → "Project"
   - Billing rate: use the `price_retail` from the most recent non-zero labor line item in recent invoices; if no invoices yet, write "(verify — check Syncro invoices)"
   - Customer ID: use Syncro `id` exactly as returned
   - Managed device count: use Syncro asset count
2. Infrastructure: derive from session logs; keep Syncro asset count in Profile, not in infrastructure tables
3. Patterns & Known Issues: synthesize from session logs; for full recompile preserve existing patterns verbatim unless session logs show they are resolved
4. Active Work: use Syncro open ticket list as the primary source
5. History Highlights: chronological, from session logs only, one-line entries with dates
6. Access: vault paths and IPs from session logs; never invent vault paths
6b. NEVER inline raw secrets (passwords, PSKs, RADIUS/shared secrets, API keys, PFX passwords) into the article, even when a session log exposes them. The wiki references the vault path only — e.g. `sysadmin (password: vault)` or `secret in vault (clients/<slug>/server.sops.yaml)`. Raw secrets live in session logs and the SOPS vault, never in the wiki knowledge layer. (Exception: a value the EXISTING article already discloses may be carried over to match its disclosure level — do not ADD new ones.)
7. For fields with no source data: write "(verify)" not placeholder text
8. Backlinks: list any wiki article slugs (clients/projects/systems) that this client is cross-referenced with
```

If the subagent is unavailable, the main agent writes the article directly using the same rules and all collected data.

---

## Phase 5 — Serialize, Stage, Review, Apply (Task 2)

Wiki writes are SERIALIZED + STAGED so two machines never recompile the same article
into a conflict, and no synthesis lands in the live article without a review.

**5.0 Claim a per-article coord lock** (via the `coord` skill):
`lock claim claudetools wiki/<type>/<slug> "wiki-compile <slug>" --ttl 1`.
- **Capture the returned lock ID** — the claim prints `[coord] lock id=<uuid> ...`. Save it
  (`LOCK_ID=<uuid>`); the release in Phase 6 takes the **lock ID, not the resource path**.
- The TTL auto-evicts a dead session's lock (no permanent stranding).
- If the lock is **already held** → emit `[SKIP] wiki/<type>/<slug> is being compiled on
  another machine; try again shortly` and exit cleanly.
- If **coord is unreachable** → emit `[WARN] coord down — proceeding without lock` and continue.
- RELEASE the lock in 5.3 — and on ANY error/abort before then.

**5.1 Write the synthesized article to STAGING, not the live tree:**
- Staging path: `.claude/wiki_staging/<type>-<slug>.md` (`mkdir -p .claude/wiki_staging`).
  Write the generated/recompiled article THERE. Do NOT touch `wiki/...` yet.

**5.2 Review the staged diff (NO blind merge):**
- `diff -u "<live wiki path>" ".claude/wiki_staging/<type>-<slug>.md" | head -120` (or
  `(new article)` if none). The main agent reviews: Patterns/History preserved on full
  recompile, IPs/paths/vault-paths accurate, billing Syncro-authoritative, NO structural
  corruption or duplicated headers. If the diff looks wrong → STOP, fix the staged file or
  abort (release the lock); do not apply.

**5.3 Apply the staged article to the live tree** (then index + commit in Phase 6):
- `cp .claude/wiki_staging/<type>-<slug>.md <live wiki path>` (seed/full); refresh edits
  already applied in Phase 4 still go via this staging review.

**Update `wiki/index.md`:**
- Check if `wiki/clients/<slug>.md` is listed in the Clients table
- If **not listed**: insert a new row in the Clients table:
  ```
  | [<display_name>](clients/<slug>.md) | <one-line summary from article intro> | <today's date> |
  ```
- If **listed**: update the `Last Compiled` date and summary
- Update the `Last updated` header date

---

## Phase 6 — Commit

```bash
cd "$CLAUDETOOLS_ROOT"
git add "wiki/clients/${SLUG}.md" wiki/index.md
git commit -m "wiki: compile ${SLUG} (${MODE})"
git fetch origin && git rebase origin/main   # serialized, but rebase defensively
git push origin main
# Release the per-article lock and clear staging (ALWAYS — even on an earlier abort):
# NOTE: `lock release` takes the LOCK ID (captured at claim in 5.0), NOT the resource path.
$PY "$CLAUDETOOLS_ROOT/.claude/skills/coord/scripts/coord.py" lock release "$LOCK_ID" 2>/dev/null || true
rm -f "$CLAUDETOOLS_ROOT/.claude/wiki_staging/${TYPE}-${SLUG}.md"
```

Emit:
```
[SUCCESS] wiki/clients/<slug>.md committed and pushed

Mode:     <seed|refresh|full>
Sources:  <N> session logs + Syncro (customer <CUST_ID>)
Article:  wiki/clients/<slug>.md
Index:    wiki/index.md updated
```

---

## "all" Mode

When invoked as `/wiki-compile all`:

1. Run the same checks as `/wiki-lint` Steps 1–2 to find missing and stale articles
2. For each missing article: run seed mode
3. For stale articles (compiled > 90 days ago with newer logs): run refresh mode
4. For each: pause after Syncro ambiguity check if needed — do not bulk-skip
5. After all articles processed: single commit with message `wiki: bulk compile (N articles)`

---

## Hard Rules

- **This skill is read-only against Syncro.** No POST, PUT, PATCH, or DELETE. GET requests only.
- **Syncro is authoritative for all billing fields.** Hours remaining, billing rate, contract type, customer ID, and asset count always come from Syncro live data — never from session logs. Session logs may be stale; Syncro is not.
- **Never invent vault paths.** If a credential is not mentioned in session logs, write "(verify)" in the Access section.
- **Never populate Infrastructure tables with placeholder rows.** Only include servers/services that appear in session logs or Syncro assets.
- **Syncro contacts are ground truth for the Profile section.** Do not override with session log guesses if the contact name differs.
- **Syncro-only refresh (`--syncro`) never touches Patterns or History.** It edits dynamic fields only.
- **Update mode may ADD to History (always) and may add/adjust Infrastructure, Access, and Patterns**
  strictly from the NEW logs — it never rewrites or removes existing prose. Wholesale re-synthesis
  (rewriting existing sections, reconciling contradictions across the full history) is `--full` only.
