UMS — Update Memories and Skills

Actively review recent session context and update all relevant memory files and skill definitions to capture what was learned. Unlike record-learnings (which records individual facts in place as they arise), UMS is a reflective checkpoint: survey what accumulated, categorize it, and persist it all in one committed pass.

When this fires

  • As soon as a learning worth saving shows up — a corrected mistake, a new preference, a tool quirk, a workflow gap. This is the primary trigger: run UMS right then, interleaved with the main work, instead of batching learnings for a wrap-up step at the end. Don’t wait for the task to finish or for /clear to accumulate a backlog — and don’t gate it on approval or on a PR merging: capture the learning the moment it appears, even while the PR that taught it is still open and unreviewed. A user correction is a mandatory immediate trigger. Persist the lesson before resuming the main task; never wait for the user to invoke UMS or remind you a second time.
  • When a PR reaches a clean review verdict – the concrete checkpoint the bullet above leaves implicit, and the one the ardi loop exits on. Run the pass there rather than at the merge: the merge is human-gated and may land long after this session ends, while the verdict is the moment the review lifecycle has finished teaching.
  • Even when a new request arrives at that same moment – the mechanism that actually skips the two checkpoints above. A merge or clean verdict is when you report back, so it is also when the next instruction lands, and that instruction reads as the live task while the owed pass quietly evaporates: never refused, never deferred out loud, just never performed. A new request does not cancel a checkpoint. Run the pass first (it is short), or say in the same reply that it is owed and when it will run. Deferring out loud is fine; dropping it silently is the failure. Watch for it too when a skill ends in a UMS step (post-merge, ardi, wrap-up): reporting that skill complete asserts its final step ran, so confirm the pass happened rather than only the steps before it.
  • User says “ums”, “update memories and skills”, “record what we learned”
  • At the start of /clear — a backstop, not the primary trigger: catch anything accumulated since the last proactive pass before context is lost
  • After a workflow reveals a gap (e.g., a skill was followed but missed a step, or a preference wasn’t encoded)
  • When the user says “did you update memories?” (the answer should be “let me do that now”)
  • While paused waiting on a subagent or a long-running background process to complete. That idle stretch is exactly when there’s time to survey what’s accumulated so far and persist it, rather than only running UMS at a hard stop. Don’t let a real wait sit fully idle when a useful pass is available to run alongside it.

Procedure

  1. Scan recent context. Review the conversation for:

    • Mistakes made and corrected (skill gaps)
    • New preferences expressed by the user
    • Tool quirks discovered
    • Workflow steps that were missing or unclear in existing skills
    • Debugging insights
    • Codebase conventions discovered
  2. Categorize each learning. For each item, decide:

    • Is it a skill update? (workflow step missing, procedure unclear)
    • Is it a memory note? (tool quirk, preference, debugging insight)
    • Is it both? (general guidance → update skill AND preferences)
    • Is it already recorded? (grep before writing – avoid duplicates)
    • Is it cross-project or project-specific? (memories/preferences.md’s “Memory and skill storage” rule: cross-project lessons commit to d-morrison/ai-config; a convention/gotcha tied to one repo we own commits to that repo’s own agent docs instead — see the checklist item below for where. This changes step 4’s target, not just the content.)
  3. Apply updates. For each item:

    • Read the target file first (skill or memory) to understand current state
    • Grep that file for the item’s specific subject – the tool name, the API call, the error string – before appending anything. Reading the region you’re editing is not enough: a topical memory file runs to hundreds or a thousand-plus lines, so an existing entry on the same subject can sit far away in an unrelated cluster and never enter your view. Grep the whole memories/ directory rather than one file – a fact can plausibly sit in either of two adjacent topical files. When one exists, extend it in place; don’t add a second bullet. (ai-config#689: a list_workflow_runs cost bullet went in next to the related get_check_runs guidance while an entry on the same tool already sat ~2000 lines below in the write-access cluster – caught by the review bot, not by the author.)
    • Make the edit — concise bullet points, not prose
    • If updating a skill: the change should be specific enough that following the skill next time would avoid the mistake
  4. Commit and push — via a branch + PR, not direct to main, in whichever repo step 2 routed the item to.

    If the work will dispatch an expensive external action from a pinned commit (such as a release, deployment, or batch computation), create, push, and open the PR before dispatching it. The PR is the reviewable record of the exact SHA that performed the action; opening it afterward is too late.

    Do this in an isolated git worktree, not the shared ai-config checkout directlymemories/preferences.md’s “Run a local session in an isolated git worktree by DEFAULT” rule applies here too. The shared checkout is routinely in concurrent use by other sessions also running UMS; a git checkout <branch> from another session mid-command can silently redirect your git commit/git push onto their branch (and vice versa), and a local git status/git log read moments later can already reflect a third session’s activity, not your own. Every code block below creates (or reuses) a worktree first, then runs every git add/commit/push from inside it – never cd straight into the shared checkout itself to make a change.

    If a push is rejected non-fast-forward: fetch first and diff before assuming a real conflict – the branch may have picked up another session’s commit that needs separating out (git revert <their-commit>) rather than force-pushing over it. Verify the PR’s real, current content via gh api repos/<owner>/<repo>/pulls/<N>/files or git ls-remote/git show origin/<branch>:<path> (the GitHub-side truth), not the local checkout, which may have already moved again. (ai-config#748: a UMS commit collided with another concurrent session’s UMS commit on a shared branch name this way – both sessions’ content ended up interleaved on one branch before separating back out, resolved without data loss only because both sides fetched-before-pushing and diffed before force-acting.)

    Cross-project items (skills, cross-project memory notes): both live in the ai-config repo. Discover its path with git -C ~/.claude/skills/ums rev-parse --show-toplevel — point -C at a skill subdir (any one), not the ~/.claude/skills parent. bootstrap.sh may symlink skills per-child into a real ~/.claude/skills directory (cloud/web sessions pre-populate it), so the parent itself isn’t a symlink into the repo and git -C there fails with “not a git repository”; a child like …/skills/ums follows the symlink into the repo. (Both beat the older dirname "$(readlink …)", which resolves only one symlink hop.) Never leave ANY changes (skills, memories, etc.) as local-only uncommitted edits. Run one of the two paths below — not both:

    Stage only the files you actually edited — NEVER git add -A. The working tree often holds unrelated in-flight edits (the user’s own UMS commits, another skill being drafted); git add -A sweeps those into your commit and onto your PR, where they bloat the review and extend the cycle. List the specific paths instead. Then git status to confirm only your intended files are staged — if something unexpected is there, the working tree had in-flight work; unstage it rather than bundling it. (Avoid git add -p here: it needs a terminal and hangs in non-interactive sessions.)

    Every path below starts by resolving $repo, the shared checkout’s path (read-only – discovering the path doesn’t touch the shared working directory), then creates or reuses a worktree off it and does every write from inside that worktree instead.

    Already on the open PR’s branch (e.g. mid-ARDI): reuse a worktree for it, creating one if this is the first push in the worktree-ified flow.

    repo="$(git -C ~/.claude/skills/ums rev-parse --show-toplevel)"
    wt="../ai-config-worktrees/<branch>"
    git -C "$repo" worktree add "$wt" "<branch>" 2>/dev/null || true   # no-op if it already exists
    cd "$wt"
    git add "skills/<name>/SKILL.md" "memories/<file>.md"   # the files you touched
    git commit -m "ums: <brief summary>"   # COMMIT
    git push origin HEAD                   # PUSH

    No PR yet: branch off main first — a direct-to-main push is denied by auto-mode and bypasses review.

    Same-repo case (this checkout’s origin IS the repo you’re targeting):

    repo="$(git -C ~/.claude/skills/ums rev-parse --show-toplevel)"
    git -C "$repo" fetch origin main   # FETCH
    git -C "$repo" worktree add -b "ums-<topic>" "../ai-config-worktrees/ums-<topic>" origin/main   # CREATE_BRANCH
    cd "../ai-config-worktrees/ums-<topic>"
    git add "skills/<name>/SKILL.md" "memories/<file>.md"   # the files you touched
    git commit -m "ums: <brief summary>"   # COMMIT
    git push -u origin HEAD   # PUSH — PR creation is handled by the post-push verification step below

    Cross-fork case (this checkout’s origin is your own fork, not the upstream repo you’re targeting): don’t branch from a bare origin/main here – the fork’s main can be stale relative to upstream’s default branch. Fetch the intended upstream repo explicitly (not just look up its default-branch name) and branch the worktree from that fetched ref:

    repo="$(git -C ~/.claude/skills/ums rev-parse --show-toplevel)"
    base="$(gh repo view "<upstream-owner>/<repo>" --json defaultBranchRef -q .defaultBranchRef.name)" \
      && git -C "$repo" fetch "https://github.com/<upstream-owner>/<repo>.git" "$base" \
      && git -C "$repo" worktree add -b "ums-<topic>" "../ai-config-worktrees/ums-<topic>" FETCH_HEAD
    # chained with && on purpose -- a failed lookup or fetch must stop the
    # worktree creation, or it silently reuses an older FETCH_HEAD from a
    # prior fetch, recreating the stale-base problem this block exists to prevent
    cd "../ai-config-worktrees/ums-<topic>"
    git add "skills/<name>/SKILL.md" "memories/<file>.md"   # the files you touched
    git commit -m "ums: <brief summary>"   # COMMIT
    git push -u origin HEAD   # PUSH -- to your fork; PR creation is handled by the post-push verification step below

    CAUTION: if a compound add && commit && push is denied, nothing was committed — verify with git status / git log before any git reset --hard, or you’ll silently discard the still-uncommitted edits.

    After the PR merges, remove the worktree so it doesn’t accumulate: git -C "$repo" worktree remove "../ai-config-worktrees/<branch>" (the post-merge skill’s own tidy step does this automatically).

    After every push in UMS, verify PR state for the current branch in the intended base repo. gh pr list --head <owner>:<branch> silently returns empty for an owner-qualified head — it only matches a bare branch name, even when a matching PR genuinely exists (verified directly: gh pr list --head d-morrison:ums-pr635-lessons returned [] against a real open PR on that exact branch, while gh pr list --head ums-pr635-lessons found it). Query the REST API instead, whose head filter does honor the owner-qualified form: gh api --method GET "repos/<upstream-owner>/<repo>/pulls" -f "head=<head-owner>:<current-branch>" -f "state=open" --jq '.[] | {number, url, state}' (for dem-extra1/ai-config, that is gh api --method GET "repos/d-morrison/ai-config/pulls" -f "head=dem-extra1:<current-branch>" -f "state=open" ...). If no open PR exists and upstream is accessible, open it as a cross-fork PR: prepare explicit title and body first, show the draft for approval (per the “always show the draft before posting” rule in memories/preferences.md), then create non-interactively – bare gh pr create without --fill/--title/--body prompts interactively and can hang a headless session:

    gh repo view "<upstream-owner>/<repo>" --json defaultBranchRef \
      -q .defaultBranchRef.name   # discover the base -- don't hard-code main
    gh pr create --repo "<upstream-owner>/<repo>" --base "<discovered-default-branch>" \
      --head "<head-owner>:<current-branch>" \
      --title "ums: <summary>" --body-file /tmp/ums-pr-body.md \
      --reviewer d-morrison

    If upstream is not accessible in-session, push and explicitly hand off that upstream PR creation is still required.

    Project-specific items (a convention or gotcha tied to one repo we own): commit to that repo’s own agent docs (CLAUDE.md, .github/agents/*.md, .github/instructions/*.md, .github/copilot-instructions.md, or checked-in .claude/memories/) via a branch + PR in that repo — not ai-config. Discover its path the same way, cd-ing into that repo’s own checkout instead of the ai-config one, then follow the same branch/commit/push/PR steps above, substituting that repo’s own default branch for every main/origin main reference above (don’t hard-code main – a project routed here may default to master or another name; discover it the same way: gh repo view "<owner>/<repo>" --json defaultBranchRef -q .defaultBranchRef.name). If that repo has no agent-doc infrastructure yet, write to its local Claude project memory (~/.claude/projects/<project-path>/memory/) as short-lived staging only – this is not a durable destination; hand off that the project repo still needs agent-doc infrastructure added (via a PR) and the staged memory migrated there. See the checklist item below.

    Operational checklist (run in order):

  5. Report what was updated. Provide a brief summary table:

    What Where Change
    Poll for new reviews iterate/SKILL.md Added explicit polling procedure
    glab has no –state flag /memories/github.md New bullet

What to look for (checklist)

Relationship to record-learnings and staged capture

  • record-learnings = records individual facts in place, in the moment they arise
  • ums = a reflective, full-context sweep — survey what accumulated, categorize it, and persist it all in one committed pass

Both write to the same destinations. ums fires proactively, as soon as a learning worth saving shows up, rather than waiting to catch up later; the /clear trigger is only a backstop for anything that slipped through.

spot-skill-opportunities is the standing, continuous version of this skill’s “did a workflow emerge that could be a new skill?” checklist item — it runs the recognition judgment call live, in the moment, instead of only at this checkpoint. agent-builder is the sibling construction step for the other checklist item above — a recurring fan-out worker persona rather than a new user-invocable skill.

learn/promote-memory are a staged alternative for the uncertain case: record-learnings and this skill both write directly to committed memory the moment something looks worth remembering, which is right when you’re confident. When you’re not — a candidate whose generality or evidence isn’t solid yet — learn stages it instead, and a promote-memory pass (which a ums run can fold in, or run standalone) reviews staged candidates before they land in committed memory. Neither replaces the direct-write path; they add a review gate for the cases that need one.

Anti-patterns

  • ❌ Saying “I’ll remember that” without actually writing it down
  • ❌ Updating memories but not pushing skill changes to origin
  • ❌ Recording vague lessons (“be more careful”) instead of specific ones (“always poll for new review after pushing — check commit SHA matches”)
  • ❌ Skipping the “check existing notes” step and creating duplicates – specifically, reading only the region you’re appending to instead of grepping the whole target file for the subject (step 3)
  • ❌ Updating only preferences when a skill also needs the fix
  • git add -A — it sweeps unrelated in-flight edits (the user’s work, other draft skills) into your commit/PR. Stage the specific files you touched.
  • ❌ Creating memories/repo/<repo>.md for any repo — this pattern is retired. Put repo-specific lore in the repo’s own agent docs (.github/agents/, CLAUDE.md, .github/instructions/, .github/copilot-instructions.md, or checked-in .claude/memories/) via a PR; if the repo has no agent-doc infrastructure yet, this session’s own local project-memory mechanism (Claude Code: ~/.claude/projects/<project-path>/memory/ — substitute the equivalent for a non-Claude agent) is short-lived staging only — hand off that a PR adding those agent docs is still required. See the checklist item above and memories/preferences.md for the full rule.
  • ❌ Naming a tool, flag, or API identifier that appears nowhere else in the corpus without anchoring it somewhere checkable. A lone mention reads identically whether it is correct or hallucinated, so a later session has nothing to verify it against — and the guidance is only actionable if the name is right. When the identifier is a cross-model tool, add it to tool-mappings.yml (then regenerate) rather than leaving the memory bullet as its only home; otherwise cite where you confirmed it. Having used it successfully in the session you’re writing up is good evidence, but that evidence dies with the session. (ai-config#727: mcp__github__list_commits was flagged in review as unanchored; it was genuinely verified by use, and the fix was registering it as the LIST_COMMITS operation.)
  • ❌ Inserting a new bullet into any memory file with nested lists (including github-actions.md, preferences.md) without checking the surrounding indentation first. These files mix 0-indent top-level bullets with 2-/4-indent sub-bullets and multi-paragraph continuations; a new top-level bullet dropped in the middle of an existing parent’s sub-list re-parents whatever follows it in Markdown (a sibling sub-bullet silently becomes this new bullet’s child). Before committing an insertion, re-read the few lines immediately above and below the insertion point and confirm the indentation still matches what it did before — or place the new bullet after the complete enclosing list instead of inside it. (Caught by @claude review on ai-config#335: a new 0-indent bullet landed between two sibling sub-bullets of an existing parent, breaking the nesting.)
Back to top