| name | smith-update |
| description | Update Smith to the latest upstream version. Compares the installed commit SHA against https://github.com/ATTCKDigital/smith main, prompts to update if behind, and refreshes global install (~/.claude/skills, ~/.claude/hooks, ~/.smith) plus per-project artifacts (.specify/scripts, .claude/commands/smith.*, CLAUDE.md / constitution.md via migrate-templates, optional manifest sidecar bootstrap). Workflow-gate compliant. |
Smith — Update to Latest
End-to-end skill for keeping Smith up to date. Detects local-vs-upstream commit drift, prompts to update, and runs the existing installer plus per-project refresh steps when invoked inside a Smith-initialized project.
Arguments: $ARGUMENTS (no positional args; reserved for future flags like --check / --no-project)
When to Use
- "is smith out of date?" / "update smith" / "let's update smith"
- After learning of a new Smith feature you want to adopt
- After an old project's
.smith/index/ was generated against an earlier manifest schema and needs regeneration
Vault Logging
Throughout this action, log significant events to the vault session log. Read the session log path from .smith/vault/.current-session. If the file is missing or the vault is not initialized, skip all logging silently.
Format:
### [HH:MM:SS] /smith-update <event>
**User Request:**
> <verbatim user message that triggered this action>
**Synthesized Input:** <brief summary>
**Outcome:** <what happened>
**Artifacts:** <files created/modified>
**Systems affected:** smith-update (smith-repo distribution)
Log at: invocation, after version detection (with SHAs + commits-behind), after global update, after per-project update, on completion.
Natural Language Triggers
If the user says any of these (or similar), treat as invoking this command:
- "update smith"
- "let's update smith"
- "is smith out of date"
- "smith is behind"
- "pull the latest smith"
Phase 0: Activate Workflow Tracking (Workflow-Gate Compliance)
/smith-update writes many files: install.sh's destructive cp -R of skills, the version file, settings.json merge, per-project script copies, etc. The PreToolUse workflow-gate hook (PR #20) would deny all of these without an active marker. So create the marker first.
Skip if no .smith/ directory exists in the current working directory (the gate naturally exits silent in that case per PR #20, so no marker is needed). Otherwise:
TS=$(date -u +"%Y-%m-%dT%H-%M-%SZ")
PROJECT_DIR=$(pwd)
if [ -d "$PROJECT_DIR/.smith" ]; then
mkdir -p "$PROJECT_DIR/.smith/vault/active-workflows"
cat > "$PROJECT_DIR/.smith/vault/active-workflows/update-${TS}.yaml" << EOF
workflow: smith-update
feature: smith-version-sync
branch: $(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo unknown)
started: ${TS//-/:}
EOF
MARKER_PATH="$PROJECT_DIR/.smith/vault/active-workflows/update-${TS}.yaml"
fi
The marker is removed in Phase 6 cleanup (or on exit-with-no-changes paths). Use the shipped helper so it works under a Bash(rm:*) deny rule:
[ -n "${MARKER_PATH:-}" ] && [ -f "$MARKER_PATH" ] && \
"$PROJECT_DIR/.specify/scripts/bash/clear-active-workflow.sh" "update-${TS}" 2>/dev/null || true
Phase 1: Detect Upstream and Installed Versions
UPSTREAM_REMOTE="https://github.com/ATTCKDigital/smith.git"
SMITH_HOME="$HOME/.smith"
VERSION_FILE="$SMITH_HOME/.installed-version"
UPSTREAM_OUT=$(git ls-remote "$UPSTREAM_REMOTE" refs/heads/main 2>&1)
if [ $? -ne 0 ]; then
echo "Unable to reach upstream — $UPSTREAM_OUT" >&2
exit 0
fi
UPSTREAM_SHA=$(echo "$UPSTREAM_OUT" | awk '{print $1}')
if [ -f "$VERSION_FILE" ]; then
INSTALLED_SHA=$(cat "$VERSION_FILE" | tr -d ' \n\r')
else
mkdir -p "$SMITH_HOME"
echo "$UPSTREAM_SHA" > "$VERSION_FILE"
echo "Smith is up to date — version baseline established at ${UPSTREAM_SHA:0:7}."
exit 0
fi
Phase 2: Compute Commits-Behind
Per Q5-A: gh first, shallow-clone fallback.
COMMITS_BEHIND=""
if command -v gh >/dev/null 2>&1; then
COMMITS_BEHIND=$(gh api "repos/ATTCKDigital/smith/compare/${INSTALLED_SHA}...${UPSTREAM_SHA}" --jq '.ahead_by' 2>/dev/null)
fi
if [ -z "$COMMITS_BEHIND" ] || ! [[ "$COMMITS_BEHIND" =~ ^[0-9]+$ ]]; then
TMPCLONE=$(mktemp -d -t smith-update-XXXXXX)
trap 'rm -rf "$TMPCLONE"' EXIT
if ! git clone --quiet "$UPSTREAM_REMOTE" "$TMPCLONE/smith" 2>/dev/null; then
echo "Unable to reach upstream — clone failed." >&2
exit 0
fi
COMMITS_BEHIND=$(git -C "$TMPCLONE/smith" rev-list --count "${INSTALLED_SHA}..${UPSTREAM_SHA}" 2>/dev/null || echo "?")
fi
if [ "$COMMITS_BEHIND" = "0" ]; then
echo "Smith is up to date (commit ${UPSTREAM_SHA:0:7})."
exit 0
fi
echo "Smith is $COMMITS_BEHIND commits behind. Update? (y/n)"
read -r REPLY
case "$REPLY" in
[yY]|[yY][eE][sS]) ;;
*) echo "Update declined."; exit 0 ;;
esac
Phase 3: Snapshot (Q7-A Rollback Safety)
Before any destructive work, snapshot the global install state. On non-zero exit from install.sh, restore.
BACKUP_ROOT="$SMITH_HOME/.backups"
BACKUP_DIR="$BACKUP_ROOT/update-${TS}"
mkdir -p "$BACKUP_DIR"
cp -R "$HOME/.claude/skills" "$BACKUP_DIR/skills" 2>/dev/null || true
cp -R "$HOME/.claude/hooks" "$BACKUP_DIR/hooks" 2>/dev/null || true
cp "$HOME/.claude/settings.json" "$BACKUP_DIR/settings.json" 2>/dev/null || true
ls -t "$BACKUP_ROOT" 2>/dev/null | tail -n +4 | while read -r old; do
rm -rf "$BACKUP_ROOT/$old"
done
restore_snapshot() {
echo "Update failed — restoring from $BACKUP_DIR" >&2
rm -rf "$HOME/.claude/skills" "$HOME/.claude/hooks"
cp -R "$BACKUP_DIR/skills" "$HOME/.claude/skills" 2>/dev/null || true
cp -R "$BACKUP_DIR/hooks" "$HOME/.claude/hooks" 2>/dev/null || true
cp "$BACKUP_DIR/settings.json" "$HOME/.claude/settings.json" 2>/dev/null || true
}
Phase 4: Global Update
Clone smith-repo to a temp dir (or reuse the one from Phase 2's fallback), run installer, write version file.
if [ -z "${TMPCLONE:-}" ] || ! [ -d "$TMPCLONE/smith" ]; then
TMPCLONE=$(mktemp -d -t smith-update-XXXXXX)
trap 'rm -rf "$TMPCLONE"' EXIT
git clone --quiet "$UPSTREAM_REMOTE" "$TMPCLONE/smith"
fi
"$TMPCLONE/smith/scripts/dedupe-settings.sh" "$HOME/.claude/settings.json" 2>/dev/null || true
if ! "$TMPCLONE/smith/scripts/install.sh" -y; then
restore_snapshot
echo "install.sh failed. Restored from snapshot at $BACKUP_DIR." >&2
exit 1
fi
echo "$UPSTREAM_SHA" > "$VERSION_FILE"
Phase 5: Per-Project Update (Conditional)
Only runs if $PROJECT_DIR/.smith/ exists. Refresh Smith-owned files; never touch user files; offer manifest bootstrap if .smith/index/ is absent.
5.1 Refresh .specify/scripts/bash/ and .claude/commands/smith.*
Smith-owned files. Safe to overwrite. Detect orphans first (Q6-A + Q8-B unified pass).
if [ -d "$PROJECT_DIR/.smith" ]; then
UPSTREAM_SCRIPTS="$TMPCLONE/smith/skills/smith/scripts"
LOCAL_SCRIPTS="$PROJECT_DIR/.specify/scripts/bash"
ORPHAN_SCRIPTS=()
if [ -d "$LOCAL_SCRIPTS" ]; then
for f in "$LOCAL_SCRIPTS"/*; do
name=$(basename "$f")
[ -f "$UPSTREAM_SCRIPTS/$name" ] || ORPHAN_SCRIPTS+=("$name")
done
fi
UPSTREAM_CMDS="$TMPCLONE/smith/skills/smith/commands"
LOCAL_CMDS="$PROJECT_DIR/.claude/commands"
ORPHAN_CMDS=()
if [ -d "$LOCAL_CMDS" ]; then
for f in "$LOCAL_CMDS"/smith.*.md; do
[ -e "$f" ] || continue
name=$(basename "$f")
[ -f "$UPSTREAM_CMDS/$name" ] || ORPHAN_CMDS+=("$name")
done
fi
UPSTREAM_HOOKS="$TMPCLONE/smith/hooks"
LOCAL_HOOKS="$HOME/.claude/hooks"
ORPHAN_HOOKS=()
if [ -d "$LOCAL_HOOKS" ]; then
for f in "$LOCAL_HOOKS"/*.sh; do
[ -e "$f" ] || continue
name=$(basename "$f")
[ -f "$UPSTREAM_HOOKS/$name" ] || ORPHAN_HOOKS+=("$name")
done
fi
if [ ${#ORPHAN_SCRIPTS[@]} -gt 0 ] || [ ${#ORPHAN_CMDS[@]} -gt 0 ] || [ ${#ORPHAN_HOOKS[@]} -gt 0 ]; then
echo ""
echo "Orphaned Smith-owned files detected (no longer in upstream):"
[ ${#ORPHAN_HOOKS[@]} -gt 0 ] && printf " hooks: %s\n" "${ORPHAN_HOOKS[@]}"
[ ${#ORPHAN_SCRIPTS[@]} -gt 0 ] && printf " scripts: %s\n" "${ORPHAN_SCRIPTS[@]}"
[ ${#ORPHAN_CMDS[@]} -gt 0 ] && printf " commands: %s\n" "${ORPHAN_CMDS[@]}"
echo "Remove these? (y/N)"
read -r REPLY
case "$REPLY" in
[yY]|[yY][eE][sS])
for n in "${ORPHAN_HOOKS[@]}"; do rm -f "$LOCAL_HOOKS/$n"; done
for n in "${ORPHAN_SCRIPTS[@]}"; do rm -f "$LOCAL_SCRIPTS/$n"; done
for n in "${ORPHAN_CMDS[@]}"; do rm -f "$LOCAL_CMDS/$n"; done
;;
*) echo "Keeping orphaned files." ;;
esac
fi
if [ -d "$UPSTREAM_SCRIPTS" ] && [ -d "$LOCAL_SCRIPTS" ]; then
cp "$UPSTREAM_SCRIPTS"/*.sh "$LOCAL_SCRIPTS/" 2>/dev/null || true
chmod +x "$LOCAL_SCRIPTS"/*.sh
fi
if [ -d "$UPSTREAM_CMDS" ] && [ -d "$LOCAL_CMDS" ]; then
cp "$UPSTREAM_CMDS"/smith.*.md "$LOCAL_CMDS/" 2>/dev/null || true
fi
fi
5.2 Run /smith-index --migrate-templates
Non-destructively merge new template sections into CLAUDE.md and constitution.md. Invoke as a sub-action of this skill.
if [ -d "$PROJECT_DIR/.smith" ]; then
echo "Running /smith-index --migrate-templates..."
fi
Note: bash can't directly invoke another Claude skill. The PARENT ASSISTANT (the one running this skill) must invoke /smith-index --migrate-templates as a Claude tool call after the bash block above echoes its placeholder. This is the only sub-skill invocation in /smith-update.
5.3 Q9-B — Schema Version Check + Regeneration Prompt
if [ -d "$PROJECT_DIR/.smith/index" ]; then
UPSTREAM_SCHEMA_VERSION=$(cat "$TMPCLONE/smith/scripts/parsers/meta_schema_version.txt" 2>/dev/null | tr -d ' \n\r')
LOCAL_SCHEMA_VERSION=$(cat "$PROJECT_DIR/.smith/index/.schema-version" 2>/dev/null | tr -d ' \n\r')
LOCAL_SCHEMA_VERSION="${LOCAL_SCHEMA_VERSION:-1}"
if [ -n "$UPSTREAM_SCHEMA_VERSION" ] && [ "$UPSTREAM_SCHEMA_VERSION" != "$LOCAL_SCHEMA_VERSION" ]; then
echo ""
echo "Smith manifest schema mismatch:"
echo " Local: v${LOCAL_SCHEMA_VERSION}"
echo " Upstream: v${UPSTREAM_SCHEMA_VERSION}"
echo "Regenerate the manifest? (y/N) — runs /smith-index to refresh all .meta files."
read -r REPLY
case "$REPLY" in
[yY]|[yY][eE][sS])
echo "Running /smith-index..."
;;
*) echo "Keeping current manifest. You can regenerate later with /smith-index." ;;
esac
fi
fi
5.4 Manifest Sidecar Bootstrap Prompt (Q3-B)
If .smith/index/ doesn't exist at all (project predates manifest system, PR #19), offer the bootstrap.
if [ -d "$PROJECT_DIR/.smith" ] && ! [ -d "$PROJECT_DIR/.smith/index" ]; then
echo ""
echo "Smith manifest sidecar is missing for this project. Bootstrap now?"
echo " (1) Structural only — fast, runs /smith-index"
echo " (2) Structural + LLM descriptions — slow, runs /smith-index --describe"
echo " (3) Defer — skip (you can run /smith-index manually later)"
echo "Default: 3 (defer)"
read -r REPLY
case "$REPLY" in
1) echo "Running /smith-index..." ;;
2) echo "Running /smith-index --describe..." ;;
*) echo "Deferred. Run /smith-index when ready." ;;
esac
fi
5.5 Refresh .gitignore / .gitattributes Policy (Feature 36)
Re-merge the canonical team-shareable vault & index policy into the project-root
.gitignore and .gitattributes, idempotently, using the same sentinel logic as
/smith init. This is project-config, not vault data — merging managed lines in
the project-root .gitignore/.gitattributes does NOT violate the
"NEVER touch .smith/vault/" rule (which protects vault data: bank, ledger,
sessions, queue, agents, todo). Confirmed permitted for this step.
Source templates (upstream clone first, installed copy fallback):
$TMPCLONE/smith/skills/smith-index/templates/.gitignore-smith-additions
(or installed ~/.claude/skills/smith-index/templates/.gitignore-smith-additions)
$TMPCLONE/smith/skills/smith-index/templates/.gitattributes-smith-additions
Staleness guard + auto-repair (Feature 38). The merge logic below trusts
whatever template $SMITH_TPL_DIR resolves to. A pre-Feature-36 installed
template is the inverse policy (it IGNORES index/files/ + index/systems/)
and has NO sentinel marker. Applying it would silently write the wrong policy.
So before merging: if the resolved source is the INSTALLED copy and it lacks the
# >>> smith-gitignore-policy >>> sentinel, treat it as stale. In /smith-update
an upstream clone ($TMPCLONE) is normally present, so we AUTO-REPAIR — overwrite
the stale installed template from the clone, then merge from the refreshed copy.
(scripts/install.sh already cp -R'd the whole skill dir during Phase 4, so a
clean update keeps the installed copy current; this guard catches the case where
§5.5 runs against a stale installed copy anyway.)
if [ -d "$PROJECT_DIR/.smith" ]; then
INSTALLED_TPL_DIR="$HOME/.claude/skills/smith-index/templates"
CLONE_TPL_DIR="$TMPCLONE/smith/skills/smith-index/templates"
SENTINEL='# >>> smith-gitignore-policy >>>'
if [ -f "$CLONE_TPL_DIR/.gitignore-smith-additions" ]; then
SMITH_TPL_DIR="$CLONE_TPL_DIR"
else
SMITH_TPL_DIR="$INSTALLED_TPL_DIR"
fi
if [ "$SMITH_TPL_DIR" = "$INSTALLED_TPL_DIR" ] \
&& ! grep -qF "$SENTINEL" "$INSTALLED_TPL_DIR/.gitignore-smith-additions" 2>/dev/null; then
if [ -f "$CLONE_TPL_DIR/.gitignore-smith-additions" ]; then
echo "NOTE: installed gitignore template is stale (pre-Feature-36) — repairing from upstream clone."
mkdir -p "$INSTALLED_TPL_DIR"
cp -f "$CLONE_TPL_DIR/.gitignore-smith-additions" "$INSTALLED_TPL_DIR/.gitignore-smith-additions"
cp -f "$CLONE_TPL_DIR/.gitattributes-smith-additions" "$INSTALLED_TPL_DIR/.gitattributes-smith-additions" 2>/dev/null || true
SMITH_TPL_DIR="$INSTALLED_TPL_DIR"
else
echo "WARNING: installed gitignore template is stale (pre-Feature-36) and no upstream"
echo " clone is available to repair it. Skipping the .gitignore/.gitattributes"
echo " policy merge to avoid writing the wrong (inverted) policy. Re-run"
echo " /smith-update with network access to refresh the installed template."
SMITH_TPL_DIR=""
fi
fi
if [ -n "$SMITH_TPL_DIR" ]; then
GI="$PROJECT_DIR/.gitignore"
if [ -f "$GI" ]; then
OUTSIDE=$(awk '
/# >>> smith-gitignore-policy >>>/ {inblk=1}
!inblk {print}
/# <<< smith-gitignore-policy <<</ {inblk=0}
' "$GI")
for bad in '.smith/' '.smith/index/' '.smith/index/files/' '.smith/index/systems/'; do
if printf '%s\n' "$OUTSIDE" | grep -qxF "$bad"; then
LINE=$(grep -nxF "$bad" "$GI" | head -1 | cut -d: -f1)
echo ""
echo "WARNING: $GI line $LINE contains a bare '$bad' ignore OUTSIDE the Smith"
echo " sentinels. This shadows the now-committed shared paths"
echo " (manifest, .meta describe layer, ledger, bank, agents, sessions)."
echo " Smith will NOT remove it automatically. Remove it manually so the"
echo " shared Smith artifacts can be committed."
fi
done
fi
python3 - "$SMITH_TPL_DIR" "$PROJECT_DIR" <<'PYEOF'
import sys, pathlib
tpl_dir = pathlib.Path(sys.argv[1]); root = pathlib.Path(sys.argv[2])
def merge(target_path, tpl_path, open_marker, close_marker):
tpl = pathlib.Path(tpl_path)
if not tpl.exists():
print(f"WARNING: template not found: {tpl_path} — skipping"); return
block = tpl.read_text().rstrip("\n") + "\n"
target = pathlib.Path(target_path)
existing = target.read_text() if target.exists() else ""
if open_marker in existing and close_marker in existing:
pre = existing.split(open_marker, 1)[0]
post = existing.split(close_marker, 1)[1]
new = pre.rstrip("\n") + ("\n\n" if pre.strip() else "") + block + post.lstrip("\n")
action = "refreshed"
else:
sep = "" if (not existing or existing.endswith("\n\n")) else ("\n" if existing.endswith("\n") else "\n\n")
new = existing + sep + block
action = "appended"
target.write_text(new)
print(f"{action}: {open_marker.split()[1]} in {target}")
merge(root / ".gitignore", tpl_dir / ".gitignore-smith-additions",
"# >>> smith-gitignore-policy >>>", "# <<< smith-gitignore-policy <<<")
merge(root / ".gitattributes", tpl_dir / ".gitattributes-smith-additions",
"# >>> smith-gitattributes-policy >>>", "# <<< smith-gitattributes-policy <<<")
PYEOF
fi
fi
Per Q4-A the merge only manages the region BETWEEN the sentinels and never deletes
user-authored lines; the warning above is the safe nudge for the operator to clean up
a stale blanket ignore by hand. Per Feature 38 the staleness guard ensures the merge
never applies a pre-Feature-36 (inverted, sentinel-less) installed template: in
/smith-update it auto-repairs from the upstream clone; with no clone reachable it
skips the merge with a warning rather than writing the wrong policy.
Phase 6: Cleanup
[ -n "${TMPCLONE:-}" ] && rm -rf "$TMPCLONE"
if [ -n "${MARKER_PATH:-}" ] && [ -f "$MARKER_PATH" ]; then
"$PROJECT_DIR/.specify/scripts/bash/clear-active-workflow.sh" "update-${TS}" 2>/dev/null || rm -f "$MARKER_PATH"
fi
echo ""
echo "Smith updated to ${UPSTREAM_SHA:0:7}."
echo " Skills: refreshed in ~/.claude/skills/"
echo " Hooks: refreshed in ~/.claude/hooks/"
echo " Scheduler: refreshed in ~/.smith/scheduler/"
[ -d "$PROJECT_DIR/.smith" ] && echo " Per-project: .specify/scripts and .claude/commands/smith.* refreshed"
[ -d "$PROJECT_DIR/.smith" ] && echo " Git policy: .gitignore/.gitattributes Smith policy block refreshed"
echo " Snapshot: $BACKUP_DIR (delete after confirming the update works)"
Invoking Sub-Skills
/smith-update orchestrates two other skills as sub-actions:
/smith-index --migrate-templates — runs after the global update in any Smith-initialized project (Phase 5.2)
/smith-index or /smith-index --describe — runs only if Phase 5.4's bootstrap prompt is accepted
When the parent assistant runs /smith-update, it should treat the bash blocks above as the orchestration backbone and invoke the sub-skills via Claude Code's Skill tool at the marked points. The bash blocks emit Running /smith-index... placeholders to make the orchestration boundary visible in the session log.
Key Rules
- NEVER touch
.smith/vault/ — user data is sacred (vault is the bank, ledger, sessions, queue, agents, todo — all user-owned). This protects vault data; merging the managed Smith policy region in the project-root .gitignore/.gitattributes (Phase 5.5) is project-config, not vault data, and is permitted.
- NEVER touch project source code — only
.specify/scripts/, .claude/commands/smith.*, CLAUDE.md (via migrate-templates), constitution.md (via migrate-templates), .smith/index/ (only on explicit prompt)
- Active-workflow marker must be created BEFORE any file write (workflow-gate compliance)
- Snapshot before destructive work — if
install.sh fails partway, restore from snapshot
- Default-defer on LLM-heavy paths — Phase 5.4's bootstrap prompt defaults to (3); the schema-regen prompt in 5.3 defaults to (n)
- Settings.json dedupe touches only Smith-owned hook entries — never modify user-added entries (Q1-D)
python3 not python (Smith convention, per Rule 6 in ~/.claude/CLAUDE.md)