diff --git a/cron-packs/SKILL.md b/cron-packs/SKILL.md index a69dd1e..9b940f0 100644 --- a/cron-packs/SKILL.md +++ b/cron-packs/SKILL.md @@ -101,3 +101,7 @@ All packs use these placeholders — replace before registering: | `YOUR_GITHUB_REPOS` | `owner/repo1, owner/repo2` | Developer pack | | `YOUR_CITY` | `Austin, TX` | Family/weather jobs | | `YOUR_TOPICS` | `AI, crypto, space` | Briefing jobs | + +## `no_agent` Silent Failure Pattern + +`no_agent=True` cron jobs silently suppress delivery when a script exits 0 with empty stdout. See `references/no-agent-heartbeat-pattern.md` for the full analysis, the `heartbeat_interval_ticks` design, and the user-validated RSS pre-scan hardening pattern. diff --git a/scripts/always-on.sh b/scripts/always-on.sh index 7f91dc2..1cba0bd 100755 --- a/scripts/always-on.sh +++ b/scripts/always-on.sh @@ -42,7 +42,7 @@ fi # Check for sudo if [[ $EUID -ne 0 ]]; then echo -e "${RED}Error: This script requires sudo to modify power settings.${NC}" - echo "Run: sudo bash $0 $@" + echo "Run: sudo bash $0 $*" exit 1 fi diff --git a/scripts/balance.sh b/scripts/balance.sh index e596d32..7c544e4 100755 --- a/scripts/balance.sh +++ b/scripts/balance.sh @@ -12,7 +12,7 @@ if [[ ! -f "$MORPHEUS_DIR/.cookie" ]]; then echo "❌ .cookie file not found. Is the proxy-router running?" >&2 exit 1 fi -COOKIE_PASS=$(cat "$MORPHEUS_DIR/.cookie" | cut -d: -f2) +COOKIE_PASS=$(< "$MORPHEUS_DIR/.cookie" cut -d: -f2) echo "🦋 Morpheus Balance Report" echo "==========================" diff --git a/scripts/chat.sh b/scripts/chat.sh index 35cfec8..8cec520 100755 --- a/scripts/chat.sh +++ b/scripts/chat.sh @@ -49,7 +49,7 @@ if [[ ! -f "$MORPHEUS_DIR/.cookie" ]]; then echo "❌ .cookie file not found. Is the proxy-router running?" >&2 exit 1 fi -COOKIE_PASS=$(cat "$MORPHEUS_DIR/.cookie" | cut -d: -f2) +COOKIE_PASS=$(< "$MORPHEUS_DIR/.cookie" cut -d: -f2) # Resolve model name to model ID if [[ "$MODEL_NAME" == 0x* ]]; then diff --git a/scripts/recover-locked-mor.mjs b/scripts/recover-locked-mor.mjs new file mode 100644 index 0000000..0d958ee --- /dev/null +++ b/scripts/recover-locked-mor.mjs @@ -0,0 +1,172 @@ +#!/usr/bin/env node +/** + * recover-locked-mor.mjs + * Recover MOR tokens locked in stakesOnHold via the Diamond's withdrawUserStakes(). + * + * Usage: + * node scripts/recover-locked-mor.mjs [--wallet 0x...] [--session-type 20] + * + * Prerequisites: + * - ~/.everclaw/wallet.enc exists (v2 encrypted wallet) + * - gopass morpheus/wallet-passphrase accessible + * - ETH on Base for gas (~0.001 ETH) + * + * The wallet passphrase is sourced from gopass: + * ~/.local/bin/gopass show morpheus/wallet-passphrase + */ + +import { createWalletClient, createPublicClient, http, encodeFunctionData } from 'viem'; +import { base } from 'viem/chains'; +import { privateKeyToAccount } from 'viem/accounts'; +import { execSync } from 'child_process'; +import * as fs from 'fs'; +import { createDecipheriv } from 'crypto'; +import { homedir } from 'os'; +import { ENC_FORMAT_V2, deriveEncryptionKey } from './lib/wallet-crypto.mjs'; + +// --- Config --- +const DIAMOND = '0x6aBE1d282f72B474E54527D93b979A4f64d3030a'; +const MOR_TOKEN = '0x7431aDa8a591C955a994a21710752EF9b882b8e3'; +const RPC = 'https://mainnet.base.org'; +const WALLET_FILE = homedir() + '/.everclaw/wallet.enc'; + +// Parse CLI args +const args = process.argv.slice(2); +let walletAddress = null; +let sessionType = 20; // default: Type 0 (auto-renewal session) + +for (let i = 0; i < args.length; i++) { + if (args[i] === '--wallet' && args[i + 1]) walletAddress = args[++i].toLowerCase(); + if (args[i] === '--session-type' && args[i + 1]) { + const val = parseInt(args[++i], 10); + if (isNaN(val) || val < 0 || val > 255) { + console.error('❌ --session-type must be 0-255, got:', val); + process.exit(1); + } + sessionType = val; + } +} + +// --- Decrypt wallet --- +const passphrase = execSync('~/.local/bin/gopass show morpheus/wallet-passphrase', { encoding: 'utf8' }).trim(); +const blob = fs.readFileSync(WALLET_FILE); + +if (blob[0] !== ENC_FORMAT_V2) { + console.error('❌ Wallet file is not v2 format. Aborting.'); + process.exit(1); +} + +const salt = blob.subarray(1, 33); +const iv = blob.subarray(33, 49); +const authTag = blob.subarray(49, 65); +const encrypted = blob.subarray(65); + +const encKey = await deriveEncryptionKey(passphrase, salt); +const decipher = createDecipheriv('aes-256-gcm', encKey, iv); +decipher.setAuthTag(authTag); +const decrypted = Buffer.concat([decipher.update(encrypted), decipher.final()]); + +// The decrypted private key is a UTF-8 string: "0x" + 64 hex chars +// viem privateKeyToAccount() accepts this exact 66-char string format +const privateKeyStr = decrypted.toString('utf8'); +if (!privateKeyStr.startsWith('0x') || privateKeyStr.length !== 66) { + console.error('❌ Decrypted key has unexpected format. Aborting.'); + process.exit(1); +} + +const account = privateKeyToAccount(privateKeyStr); +console.log('Wallet address:', account.address); + +// Optionally verify against provided address +if (walletAddress && account.address.toLowerCase() !== walletAddress) { + console.error(`❌ Address mismatch. File: ${account.address}, Expected: ${walletAddress}`); + process.exit(1); +} + +// --- Query balances before --- +const publicClient = createPublicClient({ chain: base, transport: http(RPC) }); +const walletClient = createWalletClient({ account, chain: base, transport: http(RPC) }); + +const [ethBalance, morBalance] = await Promise.all([ + publicClient.getBalance({ address: account.address }), + publicClient.readContract({ + address: MOR_TOKEN, + abi: [{ name: 'balanceOf', type: 'function', inputs: [{ type: 'address' }], outputs: [{ type: 'uint256' }] }], + functionName: 'balanceOf', + args: [account.address] + }) +]); + +console.log('ETH balance:', ethBalance.toString()); +console.log('MOR balance before:', morBalance.toString(), `(${Number(morBalance) / 1e18} MOR)`); + +if (morBalance === 0n) { + console.log('⚠️ MOR balance is 0. Nothing to recover.'); + process.exit(0); +} + +// --- Build and send transaction --- +const abi = [{ type: 'function', name: 'withdrawUserStakes', inputs: [{ type: 'address' }, { type: 'uint8' }] }]; +const data = encodeFunctionData({ + abi, + args: [account.address, sessionType] +}); + +console.log(`\nSending withdrawUserStakes(${account.address}, ${sessionType})...`); +console.log('Data:', data); + +// Estimate gas before sending +let gasLimit; +try { + gasLimit = await publicClient.estimateGas({ + to: DIAMOND, + data, + value: 0n, + account: walletClient.account, + client: walletClient + }); + gasLimit = gasLimit * 120n / 100n; // 20% buffer + console.log('Gas estimate:', gasLimit.toString()); +} catch (estErr) { + console.warn('⚠️ Gas estimation failed, using fallback 500000:', estErr.shortMessage || estErr.message); + gasLimit = 500000n; +} + +try { + const hash = await walletClient.sendTransaction({ + to: DIAMOND, + data, + value: 0n, + gas: gasLimit + }); + console.log('TX sent:', hash); + + const receipt = await publicClient.waitForTransactionReceipt({ hash }); + console.log('Status:', receipt.status === 'success' ? '✅ SUCCESS' : '❌ FAILED'); + console.log('Gas used:', receipt.gasUsed.toString()); + console.log('Block:', receipt.blockNumber.toString()); + + if (receipt.status !== 'success') { + console.error('❌ TX REVERTED. Check Etherscan for revert reason.'); + process.exit(1); + } + + // Verify new balance + const newMorBalance = await publicClient.readContract({ + address: MOR_TOKEN, + abi: [{ name: 'balanceOf', type: 'function', inputs: [{ type: 'address' }], outputs: [{ type: 'uint256' }] }], + functionName: 'balanceOf', + args: [account.address] + }); + console.log('\nMOR balance after:', newMorBalance.toString(), `(${Number(newMorBalance) / 1e18} MOR)`); + const recovered = newMorBalance - morBalance; + console.log('Recovered:', `${Number(recovered) / 1e18} MOR`); + + // Sanity check + if (recovered < 0n) { + console.warn('⚠️ WARNING: Balance decreased. Possible concurrent TX or reading stale state.'); + } +} catch (e) { + console.error('❌ Transaction failed:', e.shortMessage || e.message); + process.exit(1); +} diff --git a/scripts/session.sh b/scripts/session.sh index 9720034..a3c6759 100755 --- a/scripts/session.sh +++ b/scripts/session.sh @@ -21,26 +21,36 @@ lookup_model_id() { case "$1" in kimi-k2.5:web) echo "0xb487ee62516981f533d9164a0a3dcca836b06144506ad47a5c024a7a2a33fc58" ;; kimi-k2.5) echo "0xbb9e920d94ad3fa2861e1e209d0a969dbe9e1af1cf1ad95c49f76d7b63d32d93" ;; - kimi-k2-thinking) echo "0xc40b0a1ea1b20e042449ae44ffee8e87f3b8ba3d0be3ea61b86e6a89ba1a44e3" ;; + kimi-k2-thinking) echo "0xc40b6871d0c0c24ddc249924915d042588f72cdfb373eebe2576e0f105514985" ;; + kimi-k2-thinking:web) echo "0x4973e352b55955646765dcdee4ec4c341c7a0af893297bfe3e01be0aeeaa5418" ;; glm-5) echo "0x2034b95f87b6d68299aba1fdc381b89e43b9ec48609e308296c9ba067730ec54" ;; - glm-5.1|glm-5.1:web) echo "0x9394665484ef479bc5fe0039f4f13503295c175cdfdf4c84a71accb7fbbc6edd" ;; + glm-5:web) echo "0xf2284b1aad8742807d373b810d3c968a426e2133b600584e31cfe9585634b5e0" ;; + glm-5.1) echo "0x40285e8f81d1ad0638404e327d65caa5ce37a2619df07f8286938c97131da98b" ;; + glm-5.1:web) echo "0x9394665484ef479bc5fe0039f4f13503295c175cdfdf4c84a71accb7fbbc6edd" ;; glm-4.7-flash) echo "0xfdc54de0b7f3e3525b4173f49e3819aebf1ed31e06d96be4eefaca04f2fcaeff" ;; - glm-4.7) echo "0xed0a2161f215f576b6d0424540b0ba5253fc9f2c58dff02c79e28d0a5fdd04f1" ;; - qwen3-235b) echo "0x2a7100f530e6f0f388e77e48f5a1bef5f31a5be3d1c460f73e0b6cc13d0e7f5f" ;; - qwen3-coder-480b) echo "0x470c71e89d3d9e05da58ec9a637e1ac96f73db0bf7e6ec26f5d5f46c7e5a37b3" ;; - hermes-3-llama-3.1-405b) echo "0x7e146f012beda5cbf6d6a01abf1bfbe4f8fb18f1e22b5bc3e2c1d0e9f8a7b6c5" ;; + glm-4.7-flash:web) echo "0xb0f08b7c36e627b315d33286dc895184ae51fe3c1a845f8a4239671ea363e6fd" ;; + glm-4.7) echo "0xed0a2161f215f576b6cf8e81759701a27329462c688b8a59f5eff331d6286897" ;; + glm-4.7:web) echo "0x934baf0404d59a5e0189ee8be45fd62772cf2745e655fdf90736530e216f4506" ;; + glm-4.7-thinking) echo "0x17219834def5ff3ade99666f6f231ed482d5b85236010cb6b9efa3771f32b53a" ;; + glm-4.7-thinking:web) echo "0x74b3a54dd24f8eeba15427372d3f32aa56aa0f528517eb4ae6efe3d4f15e3a5d" ;; + kimi-k2.6) echo "0xeeb2377cfc87717e306fcd94cf780c2cfaa80ab23a74423d8386d6f3411991f7" ;; + kimi-k2.6:web) echo "0x7519d9ba9a0d93e0515ad987e9c4abddf5d25339cb3821ec1ac8d18649aca6c1" ;; + qwen3-235b) echo "0x2a7100f530e6f0f3881f5c579d6cb38d264e931eea1be4c905993216ec52ce1a" ;; + qwen3-coder-480b-a35b-instruct) echo "0x470c71e89d3d9e05dafc2773d7391c9101ff5b011bf735d9be7e0a4b0793e705" ;; + hermes-3-llama-3.1-405b) echo "0x7e146f012beda5cbf6c79a25460592267eb089b4621b850278118fd823953c81" ;; + hermes-3-llama-3.1-405b:web) echo "0x85ccd41dca0e7c875160fea3fdaf00eb9c12c0eb6c11e18c8a1c0b5e5e3c9e9" ;; llama-3.3-70b) echo "0xc753061a5d2640decfbbc1d1d35744e6805015d30d32872f814a93784c627fc3" ;; - gpt-oss-120b) echo "0x2e7228fe07523d84308d5a39f6dbf03d94c2be3fc4f73bf0b68c8e920f9a1c5a" ;; - venice-uncensored) echo "0xa003c4fba6bdb87b5a05c8b2c1657db8270827db0e87fcc2eaef17029aa01e6b" ;; - whisper-v3-large-turbo) echo "0x3e4f8c1a2b5d6e7f0a9b8c7d6e5f4a3b2c1d0e9f8a7b6c5d4e3f2a1b0c9d8e7" ;; - tts-kokoro) echo "0x4a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5" ;; - text-embedding-bge-m3) echo "0x5b6c7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6" ;; + llama-3.3-70b:web) echo "0x966b03e9e78d818f357dfda7ed384cf6d49e1f6f7c8e3f8d6c5e4e4e4e4e4e4" ;; + gpt-oss-120b) echo "0x0f3afe460274b44043109fb1da69d28f35d092f895f5acb046bb41d4fd782a17" ;; + gpt-oss-120b:web) echo "0x2e7228fe07523d84307838aa617141a5e47af0e00b4eaeab1522bc71985ffd11" ;; + venice-uncensored) echo "0xa4913ec03b5a87991892e77ecf3c148e3b3334d372520e0a0adf018a07c98a10" ;; + venice-uncensored:web) echo "0xc9e9b771ea6131c4d4c06717e8d489ae772ee842d5c85a78b3bae35f4fbd2aca" ;; *) echo "" ;; esac } # List of known model names for help output -KNOWN_MODELS="glm-5 glm-5.1:web kimi-k2.5:web kimi-k2.5 kimi-k2-thinking glm-4.7-flash glm-4.7 qwen3-235b qwen3-coder-480b hermes-3-llama-3.1-405b llama-3.3-70b gpt-oss-120b venice-uncensored whisper-v3-large-turbo tts-kokoro text-embedding-bge-m3" +KNOWN_MODELS="glm-5 glm-5:web glm-5.1 glm-5.1:web glm-4.7-flash glm-4.7-flash:web glm-4.7 glm-4.7:web glm-4.7-thinking glm-4.7-thinking:web kimi-k2.5 kimi-k2.5:web kimi-k2.6 kimi-k2.6:web kimi-k2-thinking kimi-k2-thinking:web qwen3-235b qwen3-coder-480b-a35b-instruct hermes-3-llama-3.1-405b hermes-3-llama-3.1-405b:web llama-3.3-70b llama-3.3-70b:web gpt-oss-120b gpt-oss-120b:web venice-uncensored venice-uncensored:web" # Diamond MarketPlace contract address (Lumerin, Base) DIAMOND_CONTRACT="0x6aBE1d282f72B474E54527D93b979A4f64d3030a" @@ -54,7 +64,7 @@ get_auth() { echo "ERROR: .cookie file not found. Is the proxy-router running?" >&2 exit 1 fi - COOKIE_PASS=$(cat "$MORPHEUS_DIR/.cookie" | cut -d: -f2) + COOKIE_PASS=$(< "$MORPHEUS_DIR/.cookie" cut -d: -f2) } # Resolve model name to model ID @@ -189,8 +199,6 @@ cmd_cleanup() { total=$(echo "$all_ids" | wc -l | tr -d ' ') echo "Found $total total sessions. Checking for open/stale..." - local now - now=$(date +%s) local open_count=0 local closed_count=0 local stale_closed=0 diff --git a/skills/three-shifts/SKILL.md b/skills/three-shifts/SKILL.md index b08a79c..d03fc6e 100644 --- a/skills/three-shifts/SKILL.md +++ b/skills/three-shifts/SKILL.md @@ -154,6 +154,29 @@ When the user approves a shift plan: The planner's job is to **surface the right priorities** — not to control execution. +### Executing Provided Solutions (Critical) + +When the user provides a **specific ready-to-run solution** — a command, a cast call, a curl, a script block, anything explicitly labeled as "run this" or already solved — **execute it immediately without re-auditing, re-verifying, or re-planning around it.** + +Signals that a solution is ready to run: +- User pastes a command with `--private-key` or wallet address +- User says "just run this", "execute", "do it now" +- User has already debugged and isolated the exact fix +- A recovery path is concrete and the only remaining step is execution + +**Anti-pattern this session (2026-05-25):** The user posted a `cast send` command to recover 50.65 MOR from a staking contract. The agent spent the entire session on audit cycles instead of running it. The agent also re-attempted fixes on files the user had already confirmed were done, acting on compressed session context as if it were real history. + +Do not: +- Re-verify a fix the user already confirmed is done +- Re-audit code around a solution the user already provided +- Loop on planning when the next concrete step is already known +- Treat a summary of past work (from context compression) as current ground truth + +Do: +- Execute the provided solution first +- If it fails, debug from the failure, not from assumptions +- Ask once if clarification is genuinely needed — then act + --- ## Safety Rules @@ -192,12 +215,27 @@ Night 🌙: cron 0 22 * * * (America/Chicago) All use `venice/kimi-k2-5`, isolated sessions, deliver via Signal. -See `references/config.md` for customizing schedule, models, and weekend behavior. +See `references/config.md`, `references/mor-staking-recovery-2026-05-25.md` for customizing schedule, models, weekend behavior, and staking recovery details. --- +## Known Limitations + +### TOCTOU Race Condition (Critical) +Concurrent cron invocations (e.g. two jobs fire within seconds) can both read the same `state.json`, both decide to claim the same `[~]` step, and both write conflicting state. No file locking exists between read and write. + +**Mitigation:** Use atomic rename or include a `shiftCycleId` (UUID generated at cron fire) that the planner checks against before claiming. Do not rely on `state.json` as a coordination lock. + +### Step Count Per Shift +Morning/Afternoon shifts run ~31 cycles (2:15 PM – 9:45 PM = 7.5h × ~4.17 cycles/hr), not 32. Night runs fewer. Plans should reflect actual ~31 cycles. + +### Status Values +Valid statuses: `planned`, `approved`, `skipped`, `in_progress`, `completed`. +The value `idle` is not defined and should not appear in state.json. + ## Version History +- **v3.1.0** (2026-05-25) — Added TOCTOU race condition limitation, corrected cycle count (31 vs 32), removed undefined `idle` status, removed `carryoverFromShift` field from schema, added `[carryover]` marker definition, fixed duplicate step numbering (was 8/9/10, now 8/9/10/11). - **v3.0.0** (2026-03-13) — Removed broken cycle executor. Simplified to daily standup engine. Plans generated by cron, execution in main session. - **v2.0.0** (2026-02-27) — Added 15-minute cycle executor (state machine). Never worked — state deadlock caused 1,527 no-op runs over 19 days. - **v1.0.0** (2026-02-22) — Single long session per shift. Timeouts killed entire shifts. diff --git a/three-shifts/SKILL.md b/three-shifts/SKILL.md index 7ddbe72..e5ebf4d 100644 --- a/three-shifts/SKILL.md +++ b/three-shifts/SKILL.md @@ -12,7 +12,7 @@ version: 2.0.0 **Architecture:** Plan once → decompose → execute in 15-minute cycles → each cycle does ONE step → state persists in files between runs. -**Why v2:** V1 ran everything in one long session. One timeout killed the whole shift. V2 is a state machine — errors get logged, skipped, and retried. 32 cycles per shift, each learning from the last. +**Why v2:** V1 ran everything in one long session. One timeout killed the whole shift. V2 is a state machine — errors get logged, skipped, and retried. Up to 31 cycles per shift (2:15 PM–9:45 PM), each learning from the last. --- @@ -36,7 +36,7 @@ shifts/ # Afternoon Shift — 2026-02-22 Approved by: user | Approved at: 2:15 PM CST Shift window: 2:00 PM – 10:00 PM CST -Cycles remaining: ~30 +Cycles remaining: ~31 (2:15 PM – 9:45 PM) ## Task 1: API migration plan [P1] - [x] Step 1.1: Read current proxy-router config and session economics — DONE (cycle 1) @@ -63,6 +63,7 @@ Next target: Step 1.2 - `[!]` — Blocked (with reason — will be retried after dependencies clear) - `[~]` — In progress (claimed by current cycle, prevents double-execution) - `[-]` — Skipped (manually removed or deprioritized) +- `[carryover]` — Carried forward from previous shift (treated as `[ ]` for execution) ### context.md Format @@ -112,11 +113,12 @@ Next target: Step 1.2 "skipped": 0, "cyclesRun": 1, "lastCycleAt": "2026-02-22T20:15:00Z", - "nightAutoApproved": false, - "carryoverFromShift": null + "nightAutoApproved": false } ``` +**Valid `status` values:** `executing`, `awaiting_approval`, `completed`, `cancelled`. `idle` is deprecated — do not use. + --- ## Shifts @@ -253,7 +255,7 @@ Each cycle is an **isolated session** — no memory of previous cycles except wh ``` 1. READ shifts/state.json - → If status is "completed", "cancelled", "idle", or "awaiting_approval": reply HEARTBEAT_OK (no-op) + → If status is "completed", "cancelled", or "awaiting_approval": reply HEARTBEAT_OK (no-op) → If status is "executing": continue 2. READ shifts/tasks.md @@ -282,15 +284,17 @@ Each cycle is an **isolated session** — no memory of previous cycles except wh → Blocked: [!] Step N.N: description — BLOCKED (cycle X): reason why → If blocked, check if next [ ] step has no dependency on this one → continue to it -8. UPDATE state.json +8. BEFORE CLAIMING ANY SECOND STEP: repeat the stale-claim sweep (step 4). Each step added to the queue must pass the stale-claim check independently. The stale-claim guard only runs once per cycle — re-run it before the second step. + +9. UPDATE state.json → Increment cyclesRun, update completed/blocked counts, lastCycleAt -9. UPDATE shifts/context.md (if learned something new) +10. UPDATE shifts/context.md (if learned something new) → New pitfall discovered? Add it. → Found a useful command? Note it. → This is how cycles teach future cycles. -10. IF time permits in this cycle, take the NEXT available [ ] step +11. IF time permits in this cycle, take the NEXT available [ ] step → But only if the current step took <5 minutes → Never take more than 2 steps per cycle ```