youmd 0.6.27 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,141 @@
1
+ #!/usr/bin/env bash
2
+ # bootstrap-new-mac.sh — set up a new Mac with Houston's agent skill/stack layer
3
+ #
4
+ # Safe to re-run (idempotent). Prints all steps before executing them.
5
+ # Does NOT run launchctl, does NOT push git, does NOT install anything
6
+ # beyond youmd CLI and git clones.
7
+ #
8
+ # After this script: run install-daemons.sh to activate the sync daemons.
9
+ #
10
+ # Usage: bash bootstrap-new-mac.sh
11
+
12
+ set -euo pipefail
13
+
14
+ AGENT_SHARED="${HOME}/.agent-shared"
15
+ SCISTACK="${HOME}/.claude/scistack"
16
+ CLAUDE_SKILLS="${HOME}/.claude/skills"
17
+
18
+ LOOSE_SKILLS="agent-runtime-guard agent-stack-sync continue skill-governor"
19
+
20
+ AGENT_SHARED_REPO="https://github.com/houstongolden/agent-shared.git"
21
+ SCISTACK_REPO="https://github.com/Hubify-Projects/scistack.git"
22
+
23
+ step() {
24
+ echo ""
25
+ echo "===> $*"
26
+ }
27
+
28
+ info() {
29
+ echo " $*"
30
+ }
31
+
32
+ warn() {
33
+ echo " WARN: $*"
34
+ }
35
+
36
+ # ---------------------------------------------------------------------------
37
+ step "1. Install youmd CLI"
38
+ # ---------------------------------------------------------------------------
39
+ if command -v youmd >/dev/null 2>&1; then
40
+ info "youmd already installed: $(youmd --version 2>/dev/null || echo '(version unknown)')"
41
+ else
42
+ info "Installing youmd via official install script..."
43
+ curl -fsSL https://you.md/install.sh | bash
44
+ info "youmd installed."
45
+ fi
46
+
47
+ # ---------------------------------------------------------------------------
48
+ step "2. Clone agent-shared repo → ${AGENT_SHARED}"
49
+ # ---------------------------------------------------------------------------
50
+ if [ -d "${AGENT_SHARED}/.git" ]; then
51
+ info "Already present: ${AGENT_SHARED} — skipping clone."
52
+ else
53
+ info "Cloning ${AGENT_SHARED_REPO} → ${AGENT_SHARED}"
54
+ mkdir -p "$(dirname "${AGENT_SHARED}")"
55
+ # Use gh if available for credential handling; fall back to git
56
+ if command -v gh >/dev/null 2>&1; then
57
+ gh repo clone houstongolden/agent-shared "${AGENT_SHARED}"
58
+ else
59
+ git clone "${AGENT_SHARED_REPO}" "${AGENT_SHARED}"
60
+ fi
61
+ info "Cloned agent-shared."
62
+ fi
63
+
64
+ # ---------------------------------------------------------------------------
65
+ step "3. Clone scistack repo → ${SCISTACK}"
66
+ # ---------------------------------------------------------------------------
67
+ if [ -d "${SCISTACK}/.git" ]; then
68
+ info "Already present: ${SCISTACK} — skipping clone."
69
+ else
70
+ info "Cloning ${SCISTACK_REPO} → ${SCISTACK}"
71
+ mkdir -p "$(dirname "${SCISTACK}")"
72
+ if command -v gh >/dev/null 2>&1; then
73
+ gh repo clone Hubify-Projects/scistack "${SCISTACK}"
74
+ else
75
+ git clone "${SCISTACK_REPO}" "${SCISTACK}"
76
+ fi
77
+ info "Cloned scistack."
78
+ fi
79
+
80
+ # ---------------------------------------------------------------------------
81
+ step "4. Restore loose skills from agent-shared/claude-skills/ → ~/.claude/skills/"
82
+ # ---------------------------------------------------------------------------
83
+ mkdir -p "${CLAUDE_SKILLS}"
84
+
85
+ for skill in ${LOOSE_SKILLS}; do
86
+ src="${AGENT_SHARED}/claude-skills/${skill}"
87
+ dst="${CLAUDE_SKILLS}/${skill}"
88
+
89
+ if [ ! -d "${src}" ]; then
90
+ warn "Loose skill '${skill}' not found in agent-shared yet — will be populated on first sync."
91
+ continue
92
+ fi
93
+
94
+ if [ -d "${dst}" ]; then
95
+ info "Skill '${skill}' already exists at ${dst} — skipping (run sync.sh to update)."
96
+ else
97
+ info "Restoring skill '${skill}' → ${dst}"
98
+ rsync -a "${src}/" "${dst}/"
99
+ info "Restored ${skill}."
100
+ fi
101
+ done
102
+
103
+ # ---------------------------------------------------------------------------
104
+ step "5. Reminder: restore secrets"
105
+ # ---------------------------------------------------------------------------
106
+ echo ""
107
+ echo " !! IMPORTANT: Secrets are NOT in git. Restore them separately:"
108
+ echo ""
109
+ echo " If you use the env-vault (~/Desktop/CODE_2025/youmd/cli/scripts/env-vault/):"
110
+ echo " youmd env restore <vault-file> # CLI wrapper (preferred)"
111
+ echo " bash ~/Desktop/CODE_2025/youmd/cli/scripts/env-vault/restore.sh <vault-file>"
112
+ echo ""
113
+ echo " Secrets to restore (set in your shell profile or .env files):"
114
+ echo " OPENROUTER_API_KEY"
115
+ echo " CONVEX_DEPLOY_KEY"
116
+ echo " YOUMD_API_KEY"
117
+ echo " GH_TOKEN (or: gh auth login)"
118
+ echo " ANTHROPIC_API_KEY"
119
+ echo ""
120
+
121
+ # ---------------------------------------------------------------------------
122
+ step "6. Next steps"
123
+ # ---------------------------------------------------------------------------
124
+ echo ""
125
+ echo " Bootstrap complete. To activate the auto-sync daemons, run:"
126
+ echo ""
127
+ echo " youmd stack daemon install # preferred (CLI-native)"
128
+ echo ""
129
+ echo " Or run the raw script directly:"
130
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
131
+ echo " bash ${SCRIPT_DIR}/install-daemons.sh"
132
+ echo ""
133
+ echo " To do a manual sync now:"
134
+ echo " youmd stack sync # preferred (CLI-native)"
135
+ echo ""
136
+ echo " To do a dry-run first (recommended):"
137
+ echo " youmd stack sync --dry-run"
138
+ echo ""
139
+ echo "================================================================"
140
+ echo "Bootstrap done. Welcome to the new machine."
141
+ echo "================================================================"
@@ -0,0 +1,36 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3
+ <plist version="1.0">
4
+ <dict>
5
+ <key>Label</key>
6
+ <string>com.youmd.identity-sync</string>
7
+
8
+ <key>ProgramArguments</key>
9
+ <array>
10
+ <string>/bin/zsh</string>
11
+ <string>-lc</string>
12
+ <string>youmd sync</string>
13
+ </array>
14
+
15
+ <key>StartInterval</key>
16
+ <integer>300</integer>
17
+
18
+ <key>RunAtLoad</key>
19
+ <true/>
20
+
21
+ <key>KeepAlive</key>
22
+ <false/>
23
+
24
+ <key>StandardOutPath</key>
25
+ <string>__HOME__/.youmd/logs/identity-sync.out.log</string>
26
+
27
+ <key>StandardErrorPath</key>
28
+ <string>__HOME__/.youmd/logs/identity-sync.err.log</string>
29
+
30
+ <key>EnvironmentVariables</key>
31
+ <dict>
32
+ <key>HOME</key>
33
+ <string>__HOME__</string>
34
+ </dict>
35
+ </dict>
36
+ </plist>
@@ -0,0 +1,38 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3
+ <plist version="1.0">
4
+ <dict>
5
+ <key>Label</key>
6
+ <string>com.youmd.skillstack-sync</string>
7
+
8
+ <key>ProgramArguments</key>
9
+ <array>
10
+ <string>/bin/zsh</string>
11
+ <string>-lc</string>
12
+ <string>youmd stack sync</string>
13
+ </array>
14
+
15
+ <key>StartInterval</key>
16
+ <integer>300</integer>
17
+
18
+ <key>RunAtLoad</key>
19
+ <true/>
20
+
21
+ <key>KeepAlive</key>
22
+ <false/>
23
+
24
+ <key>StandardOutPath</key>
25
+ <string>__HOME__/.youmd/logs/skillstack-sync.out.log</string>
26
+
27
+ <key>StandardErrorPath</key>
28
+ <string>__HOME__/.youmd/logs/skillstack-sync.err.log</string>
29
+
30
+ <key>EnvironmentVariables</key>
31
+ <dict>
32
+ <key>HOME</key>
33
+ <string>__HOME__</string>
34
+ <key>PATH</key>
35
+ <string>/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin</string>
36
+ </dict>
37
+ </dict>
38
+ </plist>
@@ -0,0 +1,75 @@
1
+ #!/usr/bin/env bash
2
+ # install-daemons.sh — install skillstack-sync and identity-sync LaunchAgents
3
+ #
4
+ # Idempotent: safe to re-run. Unloads before reloading.
5
+ # Does NOT need sudo — LaunchAgents run as the current user.
6
+ #
7
+ # Usage: ./install-daemons.sh
8
+
9
+ set -euo pipefail
10
+
11
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
12
+ LAUNCH_AGENTS_DIR="${HOME}/Library/LaunchAgents"
13
+ LOG_DIR="${HOME}/.youmd/logs"
14
+
15
+ PLISTS="com.youmd.skillstack-sync com.youmd.identity-sync"
16
+
17
+ echo "==> Creating log directory: ${LOG_DIR}"
18
+ mkdir -p "${LOG_DIR}"
19
+
20
+ echo "==> Creating LaunchAgents directory: ${LAUNCH_AGENTS_DIR}"
21
+ mkdir -p "${LAUNCH_AGENTS_DIR}"
22
+
23
+ for label in ${PLISTS}; do
24
+ src="${SCRIPT_DIR}/${label}.plist"
25
+ dst="${LAUNCH_AGENTS_DIR}/${label}.plist"
26
+
27
+ if [ ! -f "${src}" ]; then
28
+ echo "ERROR: Source plist not found: ${src}" >&2
29
+ exit 1
30
+ fi
31
+
32
+ echo ""
33
+ echo "--- Installing ${label} ---"
34
+
35
+ # Substitute __HOME__ placeholder with actual home dir
36
+ # BSD sed: use a temp file, no -i '' in-place on stdin
37
+ local_tmp="$(mktemp -t youmd_plist)"
38
+ sed "s|__HOME__|${HOME}|g" "${src}" > "${local_tmp}"
39
+
40
+ # Unload existing daemon if loaded (ignore errors — it may not be loaded)
41
+ if launchctl list "${label}" >/dev/null 2>&1; then
42
+ echo " Unloading existing ${label}..."
43
+ launchctl unload "${dst}" 2>/dev/null || true
44
+ fi
45
+
46
+ echo " Copying ${label}.plist → ${LAUNCH_AGENTS_DIR}/"
47
+ cp "${local_tmp}" "${dst}"
48
+ rm -f "${local_tmp}"
49
+
50
+ echo " Loading ${label}..."
51
+ launchctl load -w "${dst}"
52
+
53
+ echo " Loaded: ${label}"
54
+ done
55
+
56
+ echo ""
57
+ echo "================================================================"
58
+ echo "Both daemons installed and running."
59
+ echo ""
60
+ echo "Log files:"
61
+ echo " ${LOG_DIR}/skillstack-sync.log (combined sync log)"
62
+ echo " ${LOG_DIR}/skillstack-sync.out.log (daemon stdout)"
63
+ echo " ${LOG_DIR}/skillstack-sync.err.log (daemon stderr)"
64
+ echo " ${LOG_DIR}/identity-sync.out.log (youmd sync stdout)"
65
+ echo " ${LOG_DIR}/identity-sync.err.log (youmd sync stderr)"
66
+ echo ""
67
+ echo "Check daemon status:"
68
+ echo " launchctl list com.youmd.skillstack-sync"
69
+ echo " launchctl list com.youmd.identity-sync"
70
+ echo ""
71
+ echo "To uninstall:"
72
+ for label in ${PLISTS}; do
73
+ echo " launchctl unload ${LAUNCH_AGENTS_DIR}/${label}.plist && rm ${LAUNCH_AGENTS_DIR}/${label}.plist"
74
+ done
75
+ echo "================================================================"
@@ -0,0 +1,320 @@
1
+ #!/usr/bin/env bash
2
+ # sync.sh — cross-machine agent skill/stack syncer
3
+ # Compatible: bash 3.2+, macOS BSD coreutils (NO mapfile, NO GNU mktemp flags)
4
+ #
5
+ # Usage:
6
+ # ./sync.sh # sync all repos + loose skills
7
+ # ./sync.sh --dry-run # log intended actions only, write nothing
8
+ #
9
+ # Env overrides:
10
+ # SKILLSTACK_REPOS space-separated list of absolute repo paths (default: ~/.agent-shared ~/.claude/scistack)
11
+ # GIT_USER_NAME commit author name (default: Houston Golden)
12
+ # GIT_USER_EMAIL commit author email (default: houston@bamf.ai)
13
+
14
+ set -euo pipefail
15
+
16
+ # ---------------------------------------------------------------------------
17
+ # Config
18
+ # ---------------------------------------------------------------------------
19
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
20
+ LOG_DIR="${HOME}/.youmd/logs"
21
+ LOG_FILE="${LOG_DIR}/skillstack-sync.log"
22
+
23
+ GIT_USER_NAME="${GIT_USER_NAME:-Houston Golden}"
24
+ GIT_USER_EMAIL="${GIT_USER_EMAIL:-houston@bamf.ai}"
25
+
26
+ AGENT_SHARED="${HOME}/.agent-shared"
27
+ SCISTACK="${HOME}/.claude/scistack"
28
+
29
+ # 4 loose skills that live in ~/.claude/skills/ but aren't in any repo
30
+ LOOSE_SKILLS="agent-runtime-guard agent-stack-sync continue skill-governor"
31
+
32
+ # Repos to sync (space-separated absolute paths; override via env).
33
+ # v1 default is agent-shared ONLY — it's purpose-built for sync and safe to
34
+ # auto-commit. scistack is intentionally NOT auto-synced by default: it holds
35
+ # protected learning-loop IP where auto-committing WIP on a timer is undesirable.
36
+ # To opt scistack (or other repos) in, set:
37
+ # SKILLSTACK_REPOS="$HOME/.agent-shared $HOME/.claude/scistack"
38
+ DEFAULT_REPOS="${AGENT_SHARED}"
39
+ SKILLSTACK_REPOS="${SKILLSTACK_REPOS:-${DEFAULT_REPOS}}"
40
+
41
+ DRY_RUN=0
42
+
43
+ # ---------------------------------------------------------------------------
44
+ # Parse flags
45
+ # ---------------------------------------------------------------------------
46
+ for arg in "$@"; do
47
+ case "$arg" in
48
+ --dry-run) DRY_RUN=1 ;;
49
+ --once) : ;; # default — accepted for explicit invocation
50
+ *) echo "Unknown flag: $arg" >&2; exit 1 ;;
51
+ esac
52
+ done
53
+
54
+ # ---------------------------------------------------------------------------
55
+ # Logging
56
+ # ---------------------------------------------------------------------------
57
+ mkdir -p "${LOG_DIR}"
58
+
59
+ log() {
60
+ local ts
61
+ ts="$(date -u +"%Y-%m-%dT%H:%M:%SZ")"
62
+ local msg="[${ts}] $*"
63
+ echo "${msg}"
64
+ echo "${msg}" >> "${LOG_FILE}"
65
+ }
66
+
67
+ log_warn() {
68
+ log "WARN $*"
69
+ }
70
+
71
+ log_error() {
72
+ log "ERROR $*"
73
+ }
74
+
75
+ log_dry() {
76
+ log "DRY $*"
77
+ }
78
+
79
+ # ---------------------------------------------------------------------------
80
+ # Helpers
81
+ # ---------------------------------------------------------------------------
82
+
83
+ hostname_short() {
84
+ # BSD hostname (no --short flag)
85
+ hostname | cut -d. -f1
86
+ }
87
+
88
+ is_dry() {
89
+ [ "${DRY_RUN}" -eq 1 ]
90
+ }
91
+
92
+ # Check if repo is in a mid-merge/rebase state
93
+ repo_is_in_progress() {
94
+ local repo="$1"
95
+ if [ -f "${repo}/.git/MERGE_HEAD" ] \
96
+ || [ -d "${repo}/.git/rebase-merge" ] \
97
+ || [ -d "${repo}/.git/rebase-apply" ]; then
98
+ return 0
99
+ fi
100
+ return 1
101
+ }
102
+
103
+ # Diff two directories; returns 0 if they differ, 1 if identical
104
+ dirs_differ() {
105
+ local src="$1" dst="$2"
106
+ if diff -rq "${src}" "${dst}" >/dev/null 2>&1; then
107
+ return 1 # identical
108
+ fi
109
+ return 0 # different
110
+ }
111
+
112
+ # ---------------------------------------------------------------------------
113
+ # LOOSE SKILL MIRROR (local → agent-shared repo)
114
+ # ---------------------------------------------------------------------------
115
+ mirror_loose_skills_to_repo() {
116
+ local skill name src dst
117
+ log "--- Loose-skill mirror: ~/.claude/skills/ → ${AGENT_SHARED}/claude-skills/ ---"
118
+
119
+ for skill in ${LOOSE_SKILLS}; do
120
+ src="${HOME}/.claude/skills/${skill}"
121
+ dst="${AGENT_SHARED}/claude-skills/${skill}"
122
+
123
+ if [ ! -d "${src}" ]; then
124
+ log_warn "Loose skill '${skill}' not found at ${src}, skipping."
125
+ continue
126
+ fi
127
+
128
+ if is_dry; then
129
+ log_dry "Would rsync ${src}/ → ${dst}/"
130
+ else
131
+ mkdir -p "${dst}"
132
+ rsync -a "${src}/" "${dst}/"
133
+ log "Mirrored ${skill} → ${AGENT_SHARED}/claude-skills/${skill}"
134
+ fi
135
+ done
136
+ }
137
+
138
+ # ---------------------------------------------------------------------------
139
+ # REVERSE LOOSE SKILL MIRROR (agent-shared repo → local ~/.claude/skills/)
140
+ # Called AFTER agent-shared is synced, so we pick up remote edits.
141
+ # ---------------------------------------------------------------------------
142
+ mirror_loose_skills_from_repo() {
143
+ local skill src dst bak ts
144
+ log "--- Reverse loose-skill mirror: ${AGENT_SHARED}/claude-skills/ → ~/.claude/skills/ ---"
145
+
146
+ for skill in ${LOOSE_SKILLS}; do
147
+ src="${AGENT_SHARED}/claude-skills/${skill}"
148
+ dst="${HOME}/.claude/skills/${skill}"
149
+
150
+ if [ ! -d "${src}" ]; then
151
+ log_warn "Repo copy of loose skill '${skill}' not found at ${src}, skipping reverse mirror."
152
+ continue
153
+ fi
154
+
155
+ # Detect if there are actually changes to apply
156
+ if [ -d "${dst}" ] && ! dirs_differ "${src}" "${dst}"; then
157
+ log "Loose skill '${skill}': no changes, skipping reverse mirror."
158
+ continue
159
+ fi
160
+
161
+ if is_dry; then
162
+ log_dry "Would backup ${dst} → ${dst}.bak.<timestamp> and rsync ${src}/ → ${dst}/"
163
+ continue
164
+ fi
165
+
166
+ # Backup existing local copy before overwriting
167
+ if [ -d "${dst}" ]; then
168
+ ts="$(date -u +"%Y-%m-%dT%H-%M-%SZ")"
169
+ bak="${dst}.bak.${ts}"
170
+ log "Backing up ${dst} → ${bak}"
171
+ rsync -a "${dst}/" "${bak}/"
172
+ fi
173
+
174
+ mkdir -p "${dst}"
175
+ rsync -a "${src}/" "${dst}/"
176
+ log "Reverse-mirrored ${skill} → ${dst}"
177
+ done
178
+ }
179
+
180
+ # ---------------------------------------------------------------------------
181
+ # SYNC ONE REPO
182
+ # Returns 0 on success, 1 on non-fatal failure.
183
+ # ---------------------------------------------------------------------------
184
+ sync_repo() {
185
+ local repo="$1"
186
+ local repo_name
187
+ repo_name="$(basename "${repo}")"
188
+
189
+ log "=== Syncing repo: ${repo} ==="
190
+
191
+ # Guard: must be a git repo
192
+ if [ ! -d "${repo}/.git" ]; then
193
+ log_warn "Skipping ${repo}: not a git repository."
194
+ return 1
195
+ fi
196
+
197
+ # Guard: must not be mid-merge/rebase
198
+ if repo_is_in_progress "${repo}"; then
199
+ log_warn "Skipping ${repo}: repository is mid-merge or mid-rebase. Resolve manually first."
200
+ return 1
201
+ fi
202
+
203
+ # Stage and commit local changes if any
204
+ local status_out
205
+ status_out="$(git -C "${repo}" status --porcelain 2>&1)"
206
+ if [ -n "${status_out}" ]; then
207
+ local ts host commit_msg
208
+ ts="$(date -u +"%Y-%m-%dT%H:%M:%SZ")"
209
+ host="$(hostname_short)"
210
+ commit_msg="auto-sync: ${host} ${ts}"
211
+
212
+ if is_dry; then
213
+ log_dry "Would commit in ${repo}: '${commit_msg}'"
214
+ log_dry "Changed files:\n${status_out}"
215
+ else
216
+ git -C "${repo}" add -A
217
+ git -C "${repo}" \
218
+ -c "user.name=${GIT_USER_NAME}" \
219
+ -c "user.email=${GIT_USER_EMAIL}" \
220
+ commit -m "${commit_msg}"
221
+ log "Committed local changes in ${repo_name}: '${commit_msg}'"
222
+ fi
223
+ else
224
+ log "No local changes in ${repo_name}."
225
+ fi
226
+
227
+ # Pull (merge, no rebase)
228
+ if is_dry; then
229
+ log_dry "Would git pull --no-rebase --no-edit in ${repo}"
230
+ else
231
+ local pull_log
232
+ pull_log="$(mktemp -t skillstack_pull)"
233
+ local pull_ok=0
234
+ git -C "${repo}" pull --no-rebase --no-edit >"${pull_log}" 2>&1 || pull_ok=$?
235
+ # Echo pull output line by line into the log
236
+ while IFS= read -r line; do
237
+ log "pull|${repo_name}: ${line}"
238
+ done < "${pull_log}"
239
+ rm -f "${pull_log}"
240
+
241
+ if [ "${pull_ok}" -ne 0 ]; then
242
+ log_error "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"
243
+ log_error "CONFLICT in ${repo_name} — pull failed."
244
+ log_error "Aborting merge/rebase and skipping push."
245
+ log_error "Manual resolution needed: cd ${repo} && git status"
246
+ log_error "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"
247
+ # Abort any in-progress merge or rebase, leave working tree clean
248
+ git -C "${repo}" merge --abort 2>/dev/null || true
249
+ git -C "${repo}" rebase --abort 2>/dev/null || true
250
+ return 1
251
+ fi
252
+ log "Pull succeeded for ${repo_name}."
253
+ fi
254
+
255
+ # Push
256
+ if is_dry; then
257
+ log_dry "Would git push in ${repo}"
258
+ else
259
+ local push_log
260
+ push_log="$(mktemp -t skillstack_push)"
261
+ local push_ok=0
262
+ git -C "${repo}" push >"${push_log}" 2>&1 || push_ok=$?
263
+ while IFS= read -r line; do
264
+ log "push|${repo_name}: ${line}"
265
+ done < "${push_log}"
266
+ rm -f "${push_log}"
267
+
268
+ if [ "${push_ok}" -ne 0 ]; then
269
+ log_error "Push FAILED for ${repo_name}. Check connectivity/auth."
270
+ return 1
271
+ fi
272
+ log "Push succeeded for ${repo_name}."
273
+ fi
274
+
275
+ log "=== Done: ${repo_name} ==="
276
+ return 0
277
+ }
278
+
279
+ # ---------------------------------------------------------------------------
280
+ # MAIN
281
+ # ---------------------------------------------------------------------------
282
+ main() {
283
+ local mode
284
+ if is_dry; then
285
+ mode="DRY-RUN"
286
+ else
287
+ mode="LIVE"
288
+ fi
289
+
290
+ log "============================================================"
291
+ log "skillstack-sync START [${mode}] on $(hostname_short)"
292
+ log "============================================================"
293
+
294
+ # Step 1: Mirror loose skills into agent-shared BEFORE syncing it
295
+ mirror_loose_skills_to_repo
296
+
297
+ # Step 2: Sync each repo
298
+ local agent_shared_synced=0
299
+ for repo in ${SKILLSTACK_REPOS}; do
300
+ if sync_repo "${repo}"; then
301
+ if [ "${repo}" = "${AGENT_SHARED}" ]; then
302
+ agent_shared_synced=1
303
+ fi
304
+ fi
305
+ done
306
+
307
+ # Step 3: Reverse-mirror loose skills from agent-shared back to ~/.claude/skills/
308
+ # Only do this if agent-shared was successfully synced (or in dry-run mode)
309
+ if is_dry || [ "${agent_shared_synced}" -eq 1 ]; then
310
+ mirror_loose_skills_from_repo
311
+ else
312
+ log_warn "Skipping reverse loose-skill mirror because agent-shared sync did not complete cleanly."
313
+ fi
314
+
315
+ log "============================================================"
316
+ log "skillstack-sync DONE [${mode}]"
317
+ log "============================================================"
318
+ }
319
+
320
+ main "$@"