Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
154 commits
Select commit Hold shift + click to select a range
6172c30
feat: Stripe-rail hardening — DoS, on-chain pricing, audit integrity
marc0olo Jul 30, 2026
fdebc2d
fix: two-tier delay timeline — alert early, terminate per spec §5.3
marc0olo Jul 30, 2026
57450bf
feat: attach_payment + paidUsdCents — close the missing-webhook hole
marc0olo Jul 30, 2026
681b27c
feat: orders are never deleted; add an owner-verifiable receipt
marc0olo Jul 30, 2026
92e5d92
docs: reconcile every doc with what shipped
marc0olo Jul 30, 2026
6bcb6f6
feat: the buyer sees what they get, before and after paying
marc0olo Jul 30, 2026
5ee091b
fix: six money-safety findings from code review
marc0olo Jul 30, 2026
27208b2
fix: bound the notify stage by time, not by the retry count
marc0olo Jul 30, 2026
2166070
fix: derive the terminal escalation from the journal, not the status
marc0olo Jul 30, 2026
0687ae2
fix: one intent one credit, and route the escalation that actually fires
marc0olo Jul 30, 2026
848daf4
test: inject real NNS outages — my untestability claim was wrong
marc0olo Jul 30, 2026
7b566e2
fix: repair the local Stripe dev loop and state what it can't cover
marc0olo Jul 30, 2026
465b819
docs: manual Stripe sandbox test plan, and what it does not prove
marc0olo Jul 30, 2026
4fc45cf
docs: state how the test plan and the runbook divide the work
marc0olo Jul 30, 2026
7ef30d8
docs: turn RUNBOOK §9 into a monitoring plan, not a cadence list
marc0olo Jul 30, 2026
fdc29b8
docs: correct what a local network provides — probed, not assumed
marc0olo Jul 30, 2026
678801a
test: full Stripe end-to-end in PocketIC — no local network, no mainnet
marc0olo Jul 30, 2026
f2f2ea0
test: single gate script, CI that covers all three suites, coverage i…
marc0olo Jul 30, 2026
b8e3db1
test: DOM tests for main.ts, and a fully local Stripe sandbox runner
marc0olo Jul 30, 2026
d2fd5cc
docs: fix count drift and point the weaker dev loop at the better one
marc0olo Jul 30, 2026
9263094
docs: drop test counts — ids are stable, counts are not
marc0olo Jul 30, 2026
85e047e
ci: allow the gate to be dispatched manually
marc0olo Jul 30, 2026
6a710ce
ci: sync the integration lockfile so npm ci works
marc0olo Jul 30, 2026
b6059e8
ci: regenerate the integration lockfile from scratch
marc0olo Jul 30, 2026
59e2633
docs: the integration CI job is verified green
marc0olo Jul 30, 2026
d01f045
feat: local XRC via canister env var, scoped to the local environment
marc0olo Jul 30, 2026
4ec6ac6
docs: a local network runs the whole good path — verified, and writte…
marc0olo Jul 30, 2026
c69c837
feat: local Internet Identity and a fixed gateway port
marc0olo Jul 30, 2026
8b4052f
docs: correct why the local CMC rate is stale
marc0olo Jul 30, 2026
ac0b34f
Merge pull request #1 from marc0olo/ci/verification-gate
marc0olo Jul 30, 2026
56ece79
fix: pin the XRC fallback in tests; remove a refutable pattern from t…
marc0olo Aug 3, 2026
4d7b0be
test: make the real-Stripe-fixture capture turnkey (#4)
marc0olo Aug 4, 2026
2325e53
fix: correct a false claim about the Owner seam — M0145 warns, it doe…
marc0olo Aug 4, 2026
027ce92
Merge pull request #14 from marc0olo/hardening/agent-queue
marc0olo Aug 5, 2026
ab5281c
test: real Stripe fixtures — the cumulative-refund and reference assu…
marc0olo Aug 5, 2026
3002428
Merge pull request #16 from marc0olo/test/stripe-fixtures
marc0olo Aug 5, 2026
e35cdd0
fix: deferred hardening tail (#9) — pre-attribution credit check, gat…
marc0olo Aug 5, 2026
2db91e7
fix: review follow-ups + local deploy smoke test (#7) — xrcCanisterId…
marc0olo Aug 5, 2026
bdcafbf
docs: correct two statements this branch got wrong (review round 6)
marc0olo Aug 5, 2026
9f5ac20
Merge pull request #17 from marc0olo/hardening/deferred-tail
marc0olo Aug 5, 2026
90b7287
fix: detach the daily tally reconcile from the sweep's trap domain (#18)
marc0olo Aug 6, 2026
0867403
feat: moc -Werror on the gate, with zero allow-list (#15)
marc0olo Aug 6, 2026
5361d7d
test: smoke test refuses a bad MAC, not just a missing header (#19)
marc0olo Aug 6, 2026
7ae971c
fix: reconcile cadence was due every tick when it had never succeeded…
marc0olo Aug 6, 2026
2ef58d3
docs: three of the five missing fixtures are `stripe trigger` one-liners
marc0olo Aug 6, 2026
ce01699
Merge pull request #20 from marc0olo/hardening/reconcile-trap-domain
marc0olo Aug 6, 2026
8e5eddc
chore: switch ICP skills from a pinned lock to autosync
marc0olo Aug 6, 2026
8588992
Merge pull request #22 from marc0olo/chore/icskills-autosync
marc0olo Aug 6, 2026
f9cfa4e
IC brand system, two-audience flow, and browser tests (#21, #6) (#24)
marc0olo Aug 10, 2026
5c0ceed
Make the manual end-to-end run followable: one procedure, correct Pay…
marc0olo Aug 13, 2026
ad851d2
fix: seed the network that serves THIS gateway, not the first pocket-…
marc0olo Aug 14, 2026
de507ea
docs: AGENTS.md told the first agent to preserve what #35 deletes (#42)
marc0olo Aug 15, 2026
705dba5
chore: mops ic 4.0.0 → 4.2.0 (#44)
marc0olo Aug 25, 2026
d6300e0
Remove the ck-USDC rail entirely (#35) (#43)
marc0olo Aug 25, 2026
5fdcfbd
Restrict the destination to the caller's own cycles account (#29) (#45)
marc0olo Aug 25, 2026
f8b7da1
Rework the order status set and make the order record durable (#34) (…
marc0olo Aug 25, 2026
f234f2d
Per-order Stripe Checkout Sessions: the outcall path (#33, PR-A of 3)…
marc0olo Aug 25, 2026
75c55ab
Custom amounts, the $10/$100 bounds, and the rail's real on/off switc…
marc0olo Aug 25, 2026
0191fb6
Delete the Payment Link world: Retention.mo, attach_payment, and repr…
marc0olo Aug 25, 2026
4850a50
Deliver from the cycles reserve instead of minting per order (#30 PR-…
marc0olo Aug 26, 2026
6abef00
Sell only what the reserve holds: the solvency gate (#30 PR-B of 3) (…
marc0olo Aug 26, 2026
dbb4944
The names now say what the code does (#30 PR-C of 3) (#53)
marc0olo Aug 26, 2026
9f03e98
Retire the ICP float, the CMC mint path and the treasury order states…
marc0olo Aug 27, 2026
531b5c0
Release reserve capacity stranded by a missed expiry event (#52 PR-A)…
marc0olo Aug 27, 2026
f226f28
One open order per principal, and its own deadline frees the slot (#5…
marc0olo Aug 28, 2026
8e03d0c
The gate prints the branch it is running on (#57)
marc0olo Aug 28, 2026
72a453c
Real Stripe fixtures, the parity test, and the CLI credential collisi…
marc0olo Aug 28, 2026
d40fc0f
An unquoted heredoc ran its own body; the rule becomes a gate step (#59)
marc0olo Aug 28, 2026
40bd3f9
The rate fuse, and group C shrinking now that #4 has landed (#5) (#60)
marc0olo Aug 28, 2026
85c753d
Refusals tally instead of writing a line per attempt (closes #61) (#62)
marc0olo Aug 31, 2026
3099027
Problems live on the order; nothing drops (closes #37) (#64)
marc0olo Aug 31, 2026
e508394
The test plan's own status section was the stale artifact (#5) (#65)
marc0olo Sep 1, 2026
33e3de4
Admin visibility: read any order, and nothing returns an unbounded co…
marc0olo Sep 1, 2026
81eea87
Bound the reconcile by flow, not by lifetime sales (closes #63) (#71)
marc0olo Sep 1, 2026
0d0899a
Docs match the canister's actual surface, and a gate step keeps them …
marc0olo Sep 1, 2026
9987579
Replace design-docs/ with an enforced 215-line decision record (#73)
marc0olo Sep 1, 2026
d002932
Comment purge, pass 1: Reserve.mo and two Main.mo blocks documenting …
marc0olo Sep 1, 2026
03ecc78
Comment purge, pass 2: Types.mo and Gate.mo (#75)
marc0olo Sep 1, 2026
f5e1c36
Comment purge, pass 3: the delivery driver and escalation (#76)
marc0olo Sep 1, 2026
45aae86
Sweep every deleted mechanism's name for stale justifications (#77)
marc0olo Sep 1, 2026
d7e4c57
Make the sweep term list an artifact, not a memory (#78)
marc0olo Sep 1, 2026
748adb5
Vocabulary counts come from a command, and the table names its three …
marc0olo Sep 1, 2026
93806a6
Comment purge, pass 4: the remaining modules (#80)
marc0olo Sep 1, 2026
458fa21
Comment purge, final slice: src/backend/rails/, and state the glob (#81)
marc0olo Sep 1, 2026
1f3d834
The seed must observe the reserve even when the top-up fails (#82)
marc0olo Sep 1, 2026
d8f2383
Public delivery stats: cumulative totals counted at one site (closes …
marc0olo Sep 2, 2026
83a5820
Generate the suite's Candid bindings, and enforce them in the gate (c…
marc0olo Sep 2, 2026
0c934fd
Bound the delivery-journal walks to orders holding a promise (closes …
marc0olo Sep 2, 2026
d90356e
Scope the vocabulary sweep to added lines, and delete its counts (clo…
marc0olo Sep 2, 2026
6026bbc
Page a buyer's orders off their own index, not the whole store (close…
marc0olo Sep 2, 2026
8483f8b
Commit the stable baseline, so the gate catches an incompatible shape…
marc0olo Sep 3, 2026
fa7b2e1
Make the delivery config readable, and check that every setter has a …
marc0olo Sep 3, 2026
a3db7fe
operator_summary: is anything wrong right now, in one public call (#6…
marc0olo Sep 3, 2026
d105f1b
Split authz into two tiers, and enforce the split rather than documen…
marc0olo Sep 3, 2026
bd2a43a
The tier check's body boundary was never fixed; make the regression i…
marc0olo Sep 3, 2026
e309179
Operator console: the read surfaces, and four unchecked mirrors close…
marc0olo Sep 3, 2026
477e531
Simulation mode: a scaled gateway on mainnet against Stripe's sandbox…
marc0olo Sep 4, 2026
707ea4a
withdraw_reserve: return the reserve, guarded on nothing being owed (…
marc0olo Sep 4, 2026
1672206
Make the signed-in principal copyable, in all three places it is aske…
marc0olo Sep 4, 2026
f31a324
Landing page: one job, and the stats promoted to carry it (#108)
marc0olo Sep 7, 2026
083aef8
Document the own-gas floor by what it does, not by freezing (#114)
marc0olo Sep 7, 2026
e17e0f5
Operator console: what you can change, what it means, and the command…
marc0olo Sep 8, 2026
c19e75c
Post-purchase views: a dashboard, a record, and the guidance as its o…
marc0olo Sep 8, 2026
da66e33
Cycles ledger activity, from the index canister (#111)
marc0olo Sep 8, 2026
00fe08f
The order page is a checkout (#115)
marc0olo Sep 8, 2026
2d9737a
The CLI page stands on its own (#116)
marc0olo Sep 8, 2026
3e4ef98
The buy view asks one question (#117)
marc0olo Sep 8, 2026
fd2e370
Cancelling a paid order says what happened (#118)
marc0olo Sep 8, 2026
10ca056
Landing action, trust panel, custom-amount split, and the cancel/expi…
marc0olo Sep 8, 2026
a9d1af9
Motoko review pass: current toolchain, the deprecations it exposed, a…
marc0olo Sep 8, 2026
b623eff
Type the admin surface's arguments and errors as variants (#122, #123…
marc0olo Sep 8, 2026
7f7ac36
Split all 62 endpoints into feature mixins (closes #120) (#125)
marc0olo Sep 8, 2026
b66bfa7
Two sweep cadences were stable state, so editing them did nothing (cl…
marc0olo Sep 8, 2026
c035a7c
Type the three admin order-lifecycle errors as variants (#123, admin …
marc0olo Sep 8, 2026
be56fa8
Refuse the doc-leak and dead-export classes at the source (#128, #132…
marc0olo Sep 8, 2026
539bff9
Extract create_order's decision so its error precedence is testable (…
marc0olo Sep 9, 2026
28994a9
Extract cancel_order's classification, and assert the release it perf…
marc0olo Sep 9, 2026
d853738
Verify the withdrawal ordering 103e called unverifiable, and extract …
marc0olo Sep 9, 2026
cf6d814
A check for two live implementations of one rule (#127 follow-up) (#137)
marc0olo Sep 9, 2026
1af5a32
Extract delayed_deliveries' scan, and close out #127 (#138)
marc0olo Sep 9, 2026
8d4afd3
Type cancel_order's error, and enforce that no endpoint returns text …
marc0olo Sep 9, 2026
4a3f2f4
Accept loopback http as a return origin, and default to the local gat…
marc0olo Sep 9, 2026
7451d29
Open Stripe in the same tab, so the redirect lands where the buyer st…
marc0olo Sep 9, 2026
dc7e754
README accuracy, a six-step local guide, and drop what nothing uses (…
marc0olo Sep 9, 2026
83bcb20
Move to the current recipes: motoko v5.1.0 and static-site (certified…
marc0olo Sep 10, 2026
2e21a26
Re-enable the visual baselines, linux-only, and drop the manual stand…
marc0olo Sep 10, 2026
3de61cd
A baseline for the delivered ORDER view (closes #147) (#148)
marc0olo Sep 10, 2026
3c96f10
Seal both Stripe secrets with vetKD (closes #11) (#149)
marc0olo Sep 10, 2026
ade3a7c
Record the §7.3 acceptance, and what it actually rests on (#151)
marc0olo Sep 10, 2026
70f35f3
Adopt moc 1.16.0 (closes #150) (#152)
marc0olo Sep 10, 2026
8efae67
Document what a purchase actually buys (#153)
marc0olo Sep 10, 2026
cc59cd6
Restructure the operator console into five panels, with tables (#154)
marc0olo Sep 10, 2026
bc33ecb
Act on the #154 review: two broken runbook calls, and a gate for the …
marc0olo Sep 10, 2026
468687e
Prepare a mainnet simulation-mode deployment on cyclepay.raymondk.co …
marc0olo Sep 11, 2026
1f89d57
Record the mainnet canister ids, and correct the budget against the r…
marc0olo Sep 11, 2026
8f0663b
Say why pricing_status reads three nulls on a fresh deploy (#160)
marc0olo Sep 11, 2026
2f19316
Refuse an empty secret for a named environment instead of falling bac…
marc0olo Sep 11, 2026
7b8390d
A gateway with no tiers configured said "Loading amounts..." for ever…
marc0olo Sep 11, 2026
047c913
Card tiers are required for a usable page, and all three docs said ot…
marc0olo Sep 11, 2026
73a57c8
Add the demo playbook (closes #100) (#164)
marc0olo Sep 14, 2026
deebc25
Fold the go-live issues into the checklist, and fix what section 1 go…
marc0olo Sep 14, 2026
426cac0
Stop telling visitors nothing is deployed, and link the thing that is…
marc0olo Sep 14, 2026
094c469
One procedure per mode in docs/OPERATE.md, and a RUNBOOK entered by s…
marc0olo Sep 14, 2026
33af494
Add docs/ARCHITECTURE.md with the diagram, and cut the README to 151 …
marc0olo Sep 14, 2026
375c0b6
docs/VERIFY.md: the module-hash question is answered, and the answer …
marc0olo Sep 15, 2026
4a7947c
Release notes as a table, stale limits corrected, and the diagrams re…
marc0olo Sep 15, 2026
9e4f2ef
Anchor the README's section links, and gate the anchors (#178)
marc0olo Sep 15, 2026
61341ec
Release notes link to the changelog instead of copying it (#180)
marc0olo Sep 15, 2026
8a2656f
Rewrite the demo playbook as a lean outline (closes #179) (#181)
marc0olo Sep 16, 2026
dc29bb1
Provenance sweep: 1,112 issue references to 0, and a gate step that k…
marc0olo Sep 16, 2026
3de0858
The ⚠️ marker means a trap: a rule, a ratcheting check, and 368 marke…
marc0olo Sep 16, 2026
c872318
Verify the frontend by its state hash (#184)
marc0olo Sep 18, 2026
6eceea3
Make scripts/release.sh work: npm in the worktree, notes only for a v…
marc0olo Sep 18, 2026
1f28b41
Bump icp-seeding-secrets-poc: ic-bls12-381, faster decryption, correc…
marc0olo Sep 28, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .agents/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"hooks": {
"SessionStart": [
{
"hooks": [
{ "type": "command", "command": "bash .claude/sync-ic-skills.sh" }
]
}
]
}
}
206 changes: 206 additions & 0 deletions .agents/sync-ic-skills.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
#!/usr/bin/env bash
# sync-ic-skills.sh — mirror the latest Internet Computer skills into .claude/skills/
#
# Differential sync: fetches the discovery index once and re-downloads only the
# skills whose published `hash` changed (or are new). Skills already at the current
# hash are skipped entirely — no per-file downloads. Prints a one-line summary only
# when something actually changed.
#
# Idempotent and offline-safe. Only skills this script installed are ever pruned,
# so your own local skills are never touched.
set -euo pipefail

BASE="https://skills.internetcomputer.org/.well-known/skills"
INDEX_URL="$BASE/index.json"
DEST=".claude/skills"
MANIFEST="$DEST/.ic-managed.json" # { "<skill>": "<hash>" } of skills this script manages

mkdir -p "$DEST"

# --- Temp files. NEW_MANIFEST is built up as we go, then swapped in atomically.
# STAGING holds the skill dir currently being downloaded, so the trap can
# remove a half-written skill if the run is interrupted. ---
TMP_INDEX="$(mktemp)"
NEW_MANIFEST="$(mktemp)"
STAGING=""
trap 'rm -f "$TMP_INDEX" "$NEW_MANIFEST"; [ -n "$STAGING" ] && rm -rf "$STAGING"' EXIT

# Remove any staging dirs left by a previously interrupted run — an in-progress
# download is always safe to discard. (.old-* backups are handled by the recovery
# step below, which never deletes one that is still the only copy of a skill.)
rm -rf "${DEST:?}"/.staging-* 2>/dev/null || true

# --- Path-safety guards. `name` and `f` come from the remote index and flow into
# rm -rf / mv / file writes, so reject anything that could escape $DEST. ---
is_safe_name() { # a flat skill slug: non-empty, no slash, no ".."
case "$1" in
""|.|..|*/*|*..*) return 1 ;;
*) return 0 ;;
esac
}
is_safe_relpath() { # a file path within a skill: subdirs ok, but not absolute or ".."
case "$1" in
""|/*|*..*) return 1 ;;
*) return 0 ;;
esac
}

# --- Recover from a run interrupted mid-swap. A `.old-<name>.<pid>` dir is the
# previous good copy of <name>, moved aside just before its swap. If that swap
# never finished (the skill dir is now missing), restore it; otherwise it is
# stale and safe to drop. This runs BEFORE the index fetch, so an interrupted
# skill is restored even on an offline run — keeping the cached copy available. ---
for backup in "$DEST"/.old-*; do
[ -e "$backup" ] || continue # unmatched glob stays literal — skip
bname="$(basename "$backup")"; bname="${bname#.old-}"; bname="${bname%.*}"
if is_safe_name "$bname" && [ ! -e "$DEST/$bname" ]; then
mv "$backup" "$DEST/$bname"
echo "[autosync-ic-skills] recovered '$bname' from an interrupted sync" >&2
else
rm -rf "$backup"
fi
done

# --- Fetch the index. On any network failure, keep cached skills and exit cleanly. ---
if ! curl -fsSL --max-time 20 "$INDEX_URL" -o "$TMP_INDEX"; then
echo "[autosync-ic-skills] could not reach $INDEX_URL — keeping cached skills" >&2
exit 0
fi

# --- jq is required to parse the index. If absent, warn and exit without failing. ---
if ! command -v jq >/dev/null 2>&1; then
echo "[autosync-ic-skills] 'jq' not found — install jq to enable IC skill sync" >&2
exit 0
fi

# --- Previously-managed skill names. Supports the legacy manifest format
# (a bare array of names, no hashes) as well as the current object form. ---
managed_names() {
[ -f "$MANIFEST" ] || return 0
jq -r 'if type == "object" then keys[] elif type == "array" then .[] else empty end' \
"$MANIFEST" 2>/dev/null || true
}

# --- Stored hash for a skill, or empty if unknown (new skill, or legacy manifest). ---
stored_hash() {
[ -f "$MANIFEST" ] || return 0
jq -r --arg n "$1" 'if type == "object" then (.[$n] // "") else "" end' \
"$MANIFEST" 2>/dev/null || true
}

# --- Append a name->hash pair to the new manifest being built. ---
record() {
local tmp; tmp="$(mktemp)"
jq --arg n "$1" --arg h "$2" '.[$n] = $h' "$NEW_MANIFEST" > "$tmp" && mv "$tmp" "$NEW_MANIFEST"
}

NEW_NAMES="$(jq -r '.skills[].name' "$TMP_INDEX")"
MANAGED="$(managed_names)"
echo '{}' > "$NEW_MANIFEST"

# --- Prune: drop previously-managed skills that are no longer in the index. ---
removed=0
while IFS= read -r old; do
[ -n "$old" ] || continue
is_safe_name "$old" || { echo "[autosync-ic-skills] skipping unsafe managed name: $old" >&2; continue; }
if ! grep -qxF "$old" <<<"$NEW_NAMES"; then
rm -rf "${DEST:?}/$old"
removed=$((removed + 1))
echo "[autosync-ic-skills] removed: $old" >&2
fi
done <<<"$MANAGED"

# --- Sync: download only skills whose hash changed (new / hashless always download). ---
added=0; updated=0; unchanged=0
while IFS= read -r entry; do
name="$(jq -r '.name' <<<"$entry")"
[ -n "$name" ] && [ "$name" != "null" ] || continue
is_safe_name "$name" || { echo "[autosync-ic-skills] skipping skill with unsafe name: $name" >&2; continue; }
new_hash="$(jq -r '.hash // ""' <<<"$entry")"
old_hash="$(stored_hash "$name")"

# Skip when the hash is known, unchanged, and the files are already on disk.
if [ -n "$new_hash" ] && [ "$new_hash" = "$old_hash" ] && [ -d "$DEST/$name" ]; then
unchanged=$((unchanged + 1))
record "$name" "$new_hash"
continue
fi

# Otherwise download this skill into a fresh staging dir, then swap it in
# atomically. A clean staging dir means an intra-skill file rename or removal
# leaves no orphaned files behind, and a mid-download failure keeps the existing
# copy intact — the swap happens only after every file downloaded successfully.
ok=1
STAGING="$(mktemp -d "${DEST}/.staging-${name}.XXXXXX")"
while IFS= read -r f; do
[ -n "$f" ] || continue
if ! is_safe_relpath "$f"; then
echo "[autosync-ic-skills] warning: unsafe file path in $name: $f — skipping skill" >&2
ok=0
break
fi
mkdir -p "$(dirname "$STAGING/$f")" # files may live in subdirs (e.g. scripts/)
if ! curl -fsSL --max-time 20 "$BASE/$name/$f" -o "$STAGING/$f"; then
echo "[autosync-ic-skills] warning: failed to fetch $name/$f" >&2
ok=0
break
fi
done < <(jq -r '.files[]?' <<<"$entry")

if [ "$ok" -eq 1 ]; then
# Swap in the fresh copy. Move any existing dir aside first, move the new one
# into place, and only then drop the old copy — so a failed swap restores the
# existing copy intact, while files removed or renamed upstream don't survive.
backup=""
if [ -e "$DEST/$name" ]; then
backup="${DEST}/.old-${name}.$$"
rm -rf "$backup"
mv "$DEST/$name" "$backup"
fi
if mv "$STAGING" "$DEST/$name"; then
STAGING=""
[ -n "$backup" ] && rm -rf "$backup"
# Record the new hash so the next run can skip this skill. A hashless server
# records an empty hash, which never equals new_hash -> always re-downloads.
record "$name" "$new_hash"
if grep -qxF "$name" <<<"$MANAGED"; then
updated=$((updated + 1))
else
added=$((added + 1))
fi
else
# Swap failed: restore any existing copy and retry on the next run.
echo "[autosync-ic-skills] warning: failed to install $name — kept any existing copy; will retry next run" >&2
[ -n "$backup" ] && mv "$backup" "$DEST/$name"
rm -rf "$STAGING"
STAGING=""
record "$name" "$old_hash"
fi
else
# Download incomplete: discard the staging dir, keep the existing skill dir
# untouched, and keep the old hash so the next run retries this skill.
rm -rf "$STAGING"
STAGING=""
record "$name" "$old_hash"
fi
done < <(jq -c '.skills[]' "$TMP_INDEX")

# --- Swap in the updated manifest. ---
mv "$NEW_MANIFEST" "$MANIFEST"

# --- Report only when something changed; stay silent on a no-op sync. ---
# SessionStart hook stdout/stderr is NOT shown in the Claude Code UI — only JSON
# fields are surfaced. We emit a single JSON object on stdout:
# - systemMessage -> rendered to the USER as a visible system notice
# - additionalContext -> injected into Claude's context so it can mention it too
if [ $((added + updated + removed)) -gt 0 ]; then
summary="[autosync-ic-skills] ${added} added, ${updated} updated, ${removed} removed (${unchanged} unchanged) in $DEST"
jq -n --arg msg "$summary" '{
systemMessage: $msg,
hookSpecificOutput: {
reloadSkills: true,
hookEventName: "SessionStart",
additionalContext: $msg
}
}'
fi
Loading
Loading