Compare commits

...

417 Commits

Author SHA1 Message Date
b3ea44a8ff sync: auto-sync from HOWARD-HOME at 2026-07-08 18:20:05
Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-07-08 18:20:05
2026-07-08 18:20:34 -07:00
e765c9115d sync: auto-sync from HOWARD-HOME at 2026-07-08 18:04:09
Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-07-08 18:04:09
2026-07-08 18:04:40 -07:00
cac5eadaee sync: auto-sync from HOWARD-HOME at 2026-07-08 17:06:40
Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-07-08 17:06:40
2026-07-08 17:07:11 -07:00
9c998a95d0 sync: auto-sync from HOWARD-HOME at 2026-07-08 17:03:04
Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-07-08 17:03:04
2026-07-08 17:03:59 -07:00
447b18d62b sync: auto-sync from HOWARD-HOME at 2026-07-08 17:00:33
Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-07-08 17:00:33
2026-07-08 17:01:23 -07:00
a7b2fae468 sync: auto-sync from HOWARD-HOME at 2026-07-08 17:00:10
Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-07-08 17:00:10
2026-07-08 17:00:39 -07:00
cc336243e3 sync: auto-sync from HOWARD-HOME at 2026-07-08 13:09:22
Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-07-08 13:09:22
2026-07-08 13:09:52 -07:00
fa97bcc49b sync: auto-sync from HOWARD-HOME at 2026-07-08 13:01:22
Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-07-08 13:01:22
2026-07-08 13:01:51 -07:00
d037d82f5e sync: auto-sync from HOWARD-HOME at 2026-07-08 12:39:04
Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-07-08 12:39:04
2026-07-08 12:39:32 -07:00
c0d304485a wiki: compile desert-rv-center (seed) 2026-07-08 12:38:25 -07:00
25ca9306e1 sync: auto-sync from HOWARD-HOME at 2026-07-08 12:15:56
Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-07-08 12:15:56
2026-07-08 12:16:23 -07:00
fcf43044d4 sync: auto-sync from HOWARD-HOME at 2026-07-08 09:00:22
Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-07-08 09:00:22
2026-07-08 09:00:51 -07:00
2a1ea6c9f7 sync: auto-sync from Mikes-MacBook-Air.local at 2026-07-08 06:49:28
Author: Mike Swanson
Machine: Mikes-MacBook-Air.local
Timestamp: 2026-07-08 06:49:28
2026-07-08 06:50:46 -07:00
fc12c596f1 sync: auto-sync from HOWARD-HOME at 2026-07-08 06:09:58
Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-07-08 06:09:58
2026-07-08 06:10:25 -07:00
eee469ce86 sync: auto-sync from HOWARD-HOME at 2026-07-07 21:17:44
Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-07-07 21:17:44
2026-07-07 21:18:15 -07:00
4e148b6dc5 sync: auto-sync from HOWARD-HOME at 2026-07-07 21:05:08
Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-07-07 21:05:08
2026-07-07 21:05:37 -07:00
19edfc45c0 wiki(IMC): SQL max-memory caps APPLIED 2026-07-07 (12GB/256/512) + probable-cause watch flag
- Confirmed caps were never applied (all uncapped); applied + verified via GuruRMM
- SQLEXPRESS/AIMSQL required IMC\guru impersonation (SYSTEM not sysadmin there)
- WATCH flag through ~2026-07-11: if AIM/perf issues reported, suspect this first; rollback = uncap
- Active Work item marked done

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-07 21:03:49 -07:00
e548f63605 fix(ps-encoded.sh): Python fallback when iconv.exe absent (Git-for-Windows ships libiconv DLL only, no pacman)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-07 20:51:17 -07:00
840b0ce9d6 wiki(IMC): 2026-07-07 fleet reconciliation — Datto EDR rollout, AV vendor, backup/billing gap, retire list
- AV/EDR is Datto (not Bitdefender); REPAIRADMIN BD->EDR swap in progress
- EDR deployed to BigMom + DESKTOP-NFU17AJ; queued LUIS/EdServices1/REPAIRADMIN
- DESKTOP-NFU17AJ enrolled in RMM (17->18); 4 more queued via SC
- MSP360 backup live-but-unbilled on IMC1; AV billed 20 seats
- 12 Syncro assets to archive (>40d offline; archive=UI-only)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-07 20:45:53 -07:00
09ab3de106 wiki: Prairie Schooner server migration state (paused, resume after UniFi swap) 2026-07-07 20:45:00 -07:00
361478dd66 sync: auto-sync from HOWARD-HOME at 2026-07-07 20:39:12
Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-07-07 20:39:12
2026-07-07 20:39:41 -07:00
0a438929d0 ask-forum: harden per high-effort review + document capability limits
Fixes from the code-review workflow (7 findings): page the poll cursor past the
100-msg window (no false timeout behind bot/overflow messages); honor 429
retry_after and bail fast on permanent Discord errors (10003/50001/...) instead
of burning the timeout; status-check overflow follow-up chunks (warn, don't
silently truncate); clamp --read limit safely (guard >64-bit overflow); add
--wait --after for precise resumption; DRY the newline-trim; set LC_ALL=C.UTF-8
for codepoint-safe slicing. SKILL.md gains the tested bot capability/limit map
(can: post/read/edit-own/react; cannot without a grant: delete/archive/unlock/pin)
and a robustness section.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-07 20:37:46 -07:00
Winter Williams
f379a828e9 bot: deploy #ct-forum guard on BEAST (submodule -> 56938bb) + log restart
Requested by Mike via #ct-forum thread; restart fires via one-shot
scheduled task so the bot survives posting its own confirmation.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-07 20:32:39 -07:00
a2a277ab67 sync: auto-sync from HOWARD-HOME at 2026-07-07 20:31:16
Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-07-07 20:31:16
2026-07-07 20:31:47 -07:00
9340cdec17 ask-forum: promote to a full skill + wire into skill-first routing
Add .claude/skills/ask-forum/SKILL.md (usage contract, correlation model,
forum-only scope, access/permissions) and route to it from CLAUDE.md
(skill-first covered domains) + SKILL_ROUTING.md, so sessions invoke the
skill instead of hand-rolling the Discord API — the footgun that produced a
broken background wait. Capture that footgun as memory
feedback_background_task_no_ampersand (run_in_background, never a shell &).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-07 20:12:18 -07:00
8def189a17 ask-forum: human-in-the-loop question flow via #ct-forum
Add ask-forum.sh + /ask-forum command: a live Claude session posts a
question to the private #ct-forum forum and blocks (server-side, one tool
call) until a human replies, then acts on the answer. Same session throughout
- a three-way between the user, the session, and the teammate. No DMs, no
second bot. Bumps the discord-bot submodule to the matching #ct-forum guard.

Approved by Mike via #ct-forum (a-go, forum-only, /ask-forum yes), 2026-07-08.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-07 19:27:45 -07:00
29725a3851 wiki: IMC MOO location address (6300 E El Dorado Cir) 2026-07-07 17:58:43 -07:00
81398663ac wiki: IMC two-location split (Main + MOO sites, agent placement, SC marking) 2026-07-07 17:53:42 -07:00
2fd646218d sync: auto-sync from HOWARD-HOME at 2026-07-07 17:33:19
Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-07-07 17:33:19
2026-07-07 17:33:56 -07:00
Winter Williams
30c20df2b9 sync: auto-sync from GURU-BEAST-ROG at 2026-07-07 14:18:48
Author: Mike Swanson
Machine: GURU-BEAST-ROG
Timestamp: 2026-07-07 14:18:48
2026-07-07 14:19:54 -07:00
5edab36df6 sync: auto-sync from GURU-BEAST-ROG at 2026-07-07 14:11:51
Author: Mike Swanson
Machine: GURU-BEAST-ROG
Timestamp: 2026-07-07 14:11:51
2026-07-07 14:13:04 -07:00
1952c36675 wiki: compile gururmm (update) — Feature 6 security-detection-alerts spec'd
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-07 14:02:10 -07:00
8b016edb9d sync: auto-sync from HOWARD-HOME at 2026-07-07 13:55:03
Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-07-07 13:55:03
2026-07-07 13:55:32 -07:00
5dfaaaa33b sync: auto-sync from HOWARD-HOME at 2026-07-07 12:47:44
Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-07-07 12:47:44
2026-07-07 12:48:10 -07:00
2259f4c172 sync: auto-sync from HOWARD-HOME at 2026-07-07 11:48:31
Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-07-07 11:48:31
2026-07-07 11:49:07 -07:00
a507678254 sync: auto-sync from HOWARD-HOME at 2026-07-07 11:45:40
Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-07-07 11:45:40
2026-07-07 11:46:46 -07:00
Winter Williams
1e1bae772d sync: auto-sync from GURU-BEAST-ROG at 2026-07-07 11:44:27
Author: Mike Swanson
Machine: GURU-BEAST-ROG
Timestamp: 2026-07-07 11:44:27
2026-07-07 11:45:34 -07:00
3d28f9ea5d sync: auto-sync from HOWARD-HOME at 2026-07-07 11:15:40
Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-07-07 11:15:40
2026-07-07 11:16:14 -07:00
Winter Williams
a0a86742bc sync: auto-sync from GURU-BEAST-ROG at 2026-07-07 11:01:13
Author: Mike Swanson
Machine: GURU-BEAST-ROG
Timestamp: 2026-07-07 11:01:13
2026-07-07 11:02:25 -07:00
27595d6475 sync: auto-sync from HOWARD-HOME at 2026-07-07 11:01:18
Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-07-07 11:01:18
2026-07-07 11:01:49 -07:00
766c7fa54d sync: auto-sync from GURU-5070 at 2026-07-07 10:26:45
Author: Mike Swanson
Machine: GURU-5070
Timestamp: 2026-07-07 10:26:45
2026-07-07 10:27:22 -07:00
ff6546bee8 wiki: compile gps-rmm-audit (full) — Phase 4 backup verification, master-status list, 20 client wikis, onboardings, onsite installer kit 2026-07-07 10:15:38 -07:00
33b6140236 sync: auto-sync from HOWARD-HOME at 2026-07-07 10:07:55
Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-07-07 10:07:55
2026-07-07 10:08:25 -07:00
efca405669 sync: auto-sync from HOWARD-HOME at 2026-07-07 09:08:42
Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-07-07 09:08:42
2026-07-07 09:09:11 -07:00
f30413cffc sync: auto-sync from HOWARD-HOME at 2026-07-07 09:02:48
Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-07-07 09:02:48
2026-07-07 09:03:16 -07:00
5d53452c6d sync: auto-sync from HOWARD-HOME at 2026-07-07 08:58:14
Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-07-07 08:58:14
2026-07-07 08:58:46 -07:00
03d02128d7 sync: auto-sync from HOWARD-HOME at 2026-07-07 08:51:03
Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-07-07 08:51:03
2026-07-07 08:51:30 -07:00
2e262c619e sync: auto-sync from HOWARD-HOME at 2026-07-07 08:48:12
Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-07-07 08:48:12
2026-07-07 08:48:40 -07:00
8efc351e25 sync: auto-sync from HOWARD-HOME at 2026-07-07 08:22:41
Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-07-07 08:22:41
2026-07-07 08:23:08 -07:00
ead30520e5 sync: auto-sync from HOWARD-HOME at 2026-07-07 07:23:12
Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-07-07 07:23:12
2026-07-07 07:23:39 -07:00
0daf06263f sync: auto-sync from HOWARD-HOME at 2026-07-07 07:13:10
Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-07-07 07:13:10
2026-07-07 07:13:39 -07:00
8854a584f3 sync: auto-sync from HOWARD-HOME at 2026-07-07 06:53:34
Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-07-07 06:53:34
2026-07-07 06:54:02 -07:00
5968ee9231 sync: auto-sync from HOWARD-HOME at 2026-07-07 06:47:06
Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-07-07 06:47:06
2026-07-07 06:48:26 -07:00
da2aa60990 sync: auto-sync from GURU-BEAST-ROG at 2026-07-07 06:38:06
Author: Mike Swanson
Machine: GURU-BEAST-ROG
Timestamp: 2026-07-07 06:38:06
2026-07-07 06:41:00 -07:00
b9635cafd9 wiki: compile gururmm (update) — ContextTree collapse-during-search fix
Fold today's dashboard bug fix (commit 5bf7896) into the GuruRMM article as a
dated Recent Work entry + two sources. Incremental update, not a full rebuild:
the article was already fully compiled today (last_compiled 2026-07-06) and the
delta is a single minor UI fix, so a Sonnet re-synthesis was unwarranted.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-06 19:05:44 -07:00
45e1072dcb sync: auto-sync from HOWARD-HOME at 2026-07-06 17:39:50
Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-07-06 17:39:50
2026-07-06 17:40:18 -07:00
7264840da6 sync: auto-sync from HOWARD-HOME at 2026-07-06 16:51:24
Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-07-06 16:51:24
2026-07-06 16:51:53 -07:00
43101f4597 sync: auto-sync from HOWARD-HOME at 2026-07-06 16:48:18
Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-07-06 16:48:18
2026-07-06 16:48:45 -07:00
e790d7c7e8 wiki: compile goldstein (seed) 2026-07-06 16:43:49 -07:00
12c3d83f75 sync: auto-sync from HOWARD-HOME at 2026-07-06 16:41:20
Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-07-06 16:41:20
2026-07-06 16:41:51 -07:00
Winter Williams
419de64af5 sync: auto-sync from GURU-BEAST-ROG at 2026-07-06 16:23:09
Author: Mike Swanson
Machine: GURU-BEAST-ROG
Timestamp: 2026-07-06 16:23:09
2026-07-06 16:24:18 -07:00
ba2403b798 sync: auto-sync from GURU-BEAST-ROG at 2026-07-06 16:14:04
Author: Mike Swanson
Machine: GURU-BEAST-ROG
Timestamp: 2026-07-06 16:14:04
2026-07-06 16:15:18 -07:00
7dc519827c sync: auto-sync from HOWARD-HOME at 2026-07-06 16:10:24
Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-07-06 16:10:24
2026-07-06 16:10:52 -07:00
8f4984c5a6 sync: auto-sync from HOWARD-HOME at 2026-07-06 15:56:41
Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-07-06 15:56:41
2026-07-06 15:57:08 -07:00
0201e7422c sync: auto-sync from HOWARD-HOME at 2026-07-06 15:49:07
Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-07-06 15:49:07
2026-07-06 15:49:33 -07:00
0b11a400ed sync: auto-sync from HOWARD-HOME at 2026-07-06 15:48:08
Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-07-06 15:48:08
2026-07-06 15:48:36 -07:00
4b535a1a81 sync: auto-sync from HOWARD-HOME at 2026-07-06 15:10:00
Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-07-06 15:10:00
2026-07-06 15:10:27 -07:00
c9ea7e8f01 wiki: client Wi-Fi inventory structure (vault-backed) + memory; ignore .ocu scratch
- wiki/reference/client-wifi.md: readable index (no passwords), /wiki-compile-exempt
- reference_client_wifi_inventory.md: convention (vault clients/<slug>/wifi.sops.yaml)
- wiki/index.md: new Reference section
- .gitignore: .ocu.* scratch artifacts
2026-07-06 13:47:22 -07:00
27dc83f927 sync: auto-sync from HOWARD-HOME at 2026-07-06 13:46:28
Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-07-06 13:46:28
2026-07-06 13:46:58 -07:00
b10f527cda sync: auto-sync from HOWARD-HOME at 2026-07-06 13:44:28
Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-07-06 13:44:28
2026-07-06 13:46:04 -07:00
bd247191e6 sync: auto-sync from HOWARD-HOME at 2026-07-06 13:43:09
Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-07-06 13:43:09
2026-07-06 13:46:03 -07:00
b20985d241 sync: auto-sync from GURU-5070 at 2026-07-06 13:43:08
Author: Mike Swanson
Machine: GURU-5070
Timestamp: 2026-07-06 13:43:08
2026-07-06 13:43:38 -07:00
3cc9b22594 sync: auto-sync from GURU-5070 at 2026-07-06 13:11:54
Author: Mike Swanson
Machine: GURU-5070
Timestamp: 2026-07-06 13:11:54
2026-07-06 13:12:28 -07:00
49d5a535a4 wiki(index): refresh lonestar summary (0 hrs, Unraid VM-loss root cause + live IP)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-06 12:48:39 -07:00
37a6598ef7 wiki: compile lonestar-electrical (full) — Unraid VM-loss root cause (/boot/config/go hijack), IP fix 172.16.1.188->192.168.120.177, hours 0
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-06 12:47:20 -07:00
a2a9356a02 sync: auto-sync from GURU-5070 at 2026-07-06 12:39:35
Author: Mike Swanson
Machine: GURU-5070
Timestamp: 2026-07-06 12:39:35
2026-07-06 12:39:59 -07:00
c3ad79d9fe promotion-candidates: zip GURU-5070 .claude/tmp scratch (21) for Beast review
21 graduation candidates from GURU-5070's local .claude/tmp/ (gitignored/per-machine)
zipped for GURU-BEAST-ROG to review + graduate keepers per TEMP_GRADUATION.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-06 11:05:41 -07:00
dfc237e6d4 sync: auto-sync from GURU-5070 at 2026-07-06 11:01:42
Author: Mike Swanson
Machine: GURU-5070
Timestamp: 2026-07-06 11:01:42
2026-07-06 11:02:17 -07:00
14ac542a00 sync: auto-sync from GURU-BEAST-ROG at 2026-07-06 10:55:23
Author: Mike Swanson
Machine: GURU-BEAST-ROG
Timestamp: 2026-07-06 10:55:23
2026-07-06 10:58:07 -07:00
c1706393b9 sync: auto-sync from HOWARD-HOME at 2026-07-06 09:41:46
Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-07-06 09:41:46
2026-07-06 09:42:14 -07:00
e08f6c534e sync: auto-sync from HOWARD-HOME at 2026-07-06 08:31:47
Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-07-06 08:31:47
2026-07-06 08:32:15 -07:00
f88c7d1739 tailscale skill (admin add/delete) + verified Windows edition-upgrade procedure
- New `tailscale` skill + `/tailscale` command: Tailscale API v2 admin ops
  (devices/keys list, delete-device, authorize, create-key, delete-key).
  Vault-first auth (OAuth preferred, token fallback), every write gated
  --confirm, name->id resolution stops on ambiguous match, [TAILSCALE] bot
  alerts. Live add/delete verification pending an API credential.
- reference_windows_edition_upgrade_rmm memory: verified Home->Pro/PfW path
  (changepk not DISM; Restart-Computer not shutdown /t; PfW-MAK auto-upgrade)
  from the ACG-Tech03L upgrade.
- errorlog: quote-stripping + shutdown-drop friction, PfW-MAK label correction.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-06 08:30:22 -07:00
d5f7eadf82 sync: auto-sync from HOWARD-HOME at 2026-07-06 08:29:31
Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-07-06 08:29:31
2026-07-06 08:30:01 -07:00
c289ba3370 sync: auto-sync from ACG-TECH03L at 2026-07-05 23:45:36
Author: Howard Enos
Machine: ACG-TECH03L
Timestamp: 2026-07-05 23:45:36
2026-07-05 23:46:12 -07:00
60b307316c sync: auto-sync from ACG-TECH03L at 2026-07-05 23:22:43
Author: Howard Enos
Machine: ACG-TECH03L
Timestamp: 2026-07-05 23:22:43
2026-07-05 23:23:21 -07:00
355c5f1e3f wiki: compile gururmm (fold policy-ui Fleet-Policy Console + WS-looper comms-durability; agent 0.6.79 beta / server 0.3.103, mig 068) 2026-07-05 23:13:26 -07:00
d70cdd88e4 submodule: advance guru-rmm -> 7058195 (session log: WS-looper comms-durability) 2026-07-05 21:26:14 -07:00
faf796386a sync: auto-sync from HOWARD-HOME at 2026-07-05 21:24:32
Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-07-05 21:24:32
2026-07-05 21:25:01 -07:00
8b08ec64c2 seafile: route provisioning/admin writes to the skill in SKILL_ROUTING
Add a doing-skill line for Seafile (SeaCloud) admin writes -- create/
deactivate/delete account, set quota, create/transfer/delete library,
share/unshare -- pointing at the `seafile` skill (preview by default,
--confirm to apply). Complements the existing read/inventory line.

The write-ops implementation itself (seafile.py subcommands, client
_write via shared http_json form=, quota decimal-GB round-trip, gated
--confirm, SKILL.md docs) landed earlier via auto-sync in a8e76ba.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-05 20:54:57 -07:00
da6567ffe0 owncloud: extend skill from read-only audit to full occ management
Layer gated admin writes onto the read-only GPS-audit skill, driven through
occ over SSH (same paramiko/vault/filecache plumbing):

- user lifecycle (add/delete/enable/disable/reset-password/modify), quota
  get/set/reset, groups (create/delete/members), read-only share listing
  (from oc_share; occ has no share create/list), apps, system config,
  maintenance mode, files scan, ownership transfer, trashbin/versions cleanup.
- All writes gated behind --confirm (refuse -> rc 3); hard-fail guards (rc 2)
  for invalid uid, missing password, and all-users cleanup with no target.
- --all-users trashbin/versions purge now ALSO requires --force-all-users in
  addition to --confirm, so a single stray --confirm cannot trigger an
  irreversible fleet-wide purge.

Built against the verified live occ surface (occ list / occ help), not guessed
flags. Fixes from code-review + security-review: Python-side share filtering
(no MySQL SQL-injection via '' -doubling), occ reads no longer mask a failed
read as healthy (maintenance/quota/config), guarded json.loads, clean --json
output, and '--' end-of-options guards so dash-leading uids can't be parsed as
occ flags. dry_test.sh added as a non-destructive regression harness (43 checks).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-05 20:52:25 -07:00
3d8aa2be59 submodule: advance guru-rmm -> c81eeb5 (WS-looper comms-durability: write-timeout + heartbeat-count diagnostics + AIMD keepalive; agent 0.6.79 beta, server 0.3.102 deployed; BUG-024 linux 0.6.79 re-quarantined)
Merge 07307e3 (fix/ws-looper-writetimeout-hbdiag-aimd) + CI version bump c81eeb5.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-05 20:29:44 -07:00
a8e76ba215 sync: auto-sync from HOWARD-HOME at 2026-07-05 20:23:39
Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-07-05 20:23:39
2026-07-05 20:24:08 -07:00
976bfe7957 sync: auto-sync from HOWARD-HOME at 2026-07-05 19:59:52
Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-07-05 19:59:52
2026-07-05 20:00:20 -07:00
b0ebd44c9d sync: auto-sync from HOWARD-HOME at 2026-07-05 19:34:16
Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-07-05 19:34:16
2026-07-05 19:34:44 -07:00
f274eed8fa dataforth-dos: advance pointer to #32489 toolchain + telemetry commit
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-05 16:39:02 -07:00
5e682c9293 sync: auto-sync from GURU-5070 at 2026-07-05 16:35:57
Author: Mike Swanson
Machine: GURU-5070
Timestamp: 2026-07-05 16:35:57
2026-07-05 16:36:27 -07:00
bee140c020 sync: auto-sync from GURU-5070 at 2026-07-05 16:34:34
Author: Mike Swanson
Machine: GURU-5070
Timestamp: 2026-07-05 16:34:34
2026-07-05 16:35:02 -07:00
a737c4ab24 wiki: compile gururmm (full) — fold 07-04/05 batches (v0.3.97-99, agent 0.6.78), policy-core deployed (mig 067), BUG-003 closed, BUG-023 fixed, BUG-024 linux glibc OPEN/quarantined
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-05 15:42:55 -07:00
fa4867580f sync: auto-sync from HOWARD-HOME at 2026-07-05 15:29:59
Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-07-05 15:29:59
2026-07-05 15:30:28 -07:00
a5b8f6e7b1 sync: auto-sync from HOWARD-HOME at 2026-07-05 13:42:15
Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-07-05 13:42:15
2026-07-05 13:43:35 -07:00
6ad608c3bf rmm: bump guru-rmm -> policy-core deployed to prod (migration 067)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-05 13:08:47 -07:00
ea84b7bcd2 submodule: advance guru-rmm -> 120854a (v0.3.99 batch-4: command TTL + status reconciler + update supersede; BUG-024 linux glibc incident quarantined + ix/Jupiter revived; orphan dupes purged)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-05 10:58:54 -07:00
b835f350d6 submodule: advance guru-rmm (batch-3 session log)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-05 09:57:25 -07:00
5e1ebf4789 sync: auto-sync from HOWARD-HOME at 2026-07-05 09:56:12
Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-07-05 09:56:12
2026-07-05 09:56:42 -07:00
656101a7b4 submodule: advance guru-rmm -> d85d6be (v0.3.98: visible_agents view+trigger, --migrate-only deploy gate, BUG-003 pipeline hardening live, BUG-023 channel-race fixed, 0.6.78 promoted stable)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-05 09:48:43 -07:00
7c0f467cdf rmm: bump guru-rmm -> 4b0a28b (policy-core shape spec)
docs/specs/policy-core/ — folders+inheritance gate spec for the policy redesign (#7).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-05 07:52:50 -07:00
26b1cf400f sync: auto-sync from HOWARD-HOME at 2026-07-05 01:00:55
Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-07-05 01:00:55
2026-07-05 01:01:23 -07:00
e359702837 gps-rmm-audit: CP-QB WMI-repaired+enrolled (112/189); IMC-PRINTSERVER agent installed, blocked by box-wide outbound TLS drop
CP-QB: corrupt WMI class store (Win32_Processor invalid class) killed the universal
installer's arch check; mofcomp cimwin32.mof fixed it; enrolled + reassigned to
Curtis Plumbing. IMC-PRINTSERVER: agent+watchdog installed manually over LAN HTTP
from IMC1 (PowerShell dead on box, SMB hangs, SC extension eats chains/multiline);
box drops ALL outbound TLS (incl google) so agent cannot connect — fix is at IMC
UniFi gateway or local filter; no reinstall needed. Session log + tracker updated.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-05 01:00:48 -07:00
67822d5ca8 sync: auto-sync from HOWARD-HOME at 2026-07-05 00:45:20
Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-07-05 00:45:20
2026-07-05 00:45:47 -07:00
346832b469 sync: auto-sync from HOWARD-HOME at 2026-07-05 00:23:45
Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-07-05 00:23:45
2026-07-05 00:24:15 -07:00
a7174a894e sync: auto-sync from HOWARD-HOME at 2026-07-04 23:42:44
Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-07-04 23:42:44
2026-07-04 23:43:13 -07:00
3600df76cf wiki: compile gururmm (full rebuild — v0.3.96, easy-bugs batch, dedup, legacy disable, VSS/tray/integrations)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-04 23:42:31 -07:00
9efa6eaf4f sync: auto-sync from HOWARD-HOME at 2026-07-04 23:29:28
Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-07-04 23:29:28
2026-07-04 23:29:57 -07:00
9542de2b0a sync: auto-sync from HOWARD-HOME at 2026-07-04 23:15:20
Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-07-04 23:15:20
2026-07-04 23:15:49 -07:00
a0998fa255 sync: auto-sync from HOWARD-HOME at 2026-07-04 22:38:31
Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-07-04 22:38:31
2026-07-04 22:39:06 -07:00
c8036d7e67 sync: auto-sync from HOWARD-HOME at 2026-07-04 20:47:45
Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-07-04 20:47:45
2026-07-04 20:48:11 -07:00
567fd8dd8c sync: auto-sync from HOWARD-HOME at 2026-07-04 20:44:56
Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-07-04 20:44:56
2026-07-04 20:45:25 -07:00
66211a5652 sync: auto-sync from HOWARD-HOME at 2026-07-04 19:35:27
Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-07-04 19:35:27
2026-07-04 19:35:53 -07:00
46705d48ff sync: auto-sync from HOWARD-HOME at 2026-07-04 19:34:24
Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-07-04 19:34:24
2026-07-04 19:34:54 -07:00
e6dae3dba2 sync: auto-sync from HOWARD-HOME at 2026-07-04 19:33:17
Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-07-04 19:33:17
2026-07-04 19:33:49 -07:00
46d4900229 sync: auto-sync from HOWARD-HOME at 2026-07-04 19:27:24
Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-07-04 19:27:24
2026-07-04 19:27:52 -07:00
65b6043cde gps-rmm-audit: JANC (2248945) != Janet Altschuler (457710) — confirmed separate clients
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-04 19:18:11 -07:00
7a6fbcfc29 wiki: compile gps-rmm-audit (seed) — GPS->GuruRMM coverage audit project article
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-04 18:53:54 -07:00
371fce1104 sync: auto-sync from HOWARD-HOME at 2026-07-04 18:44:32
Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-07-04 18:44:32
2026-07-04 18:45:07 -07:00
42d43a5bcc sync: auto-sync from HOWARD-HOME at 2026-07-04 18:39:34
Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-07-04 18:39:34
2026-07-04 18:40:02 -07:00
a270f405a6 guru-rmm: align submodule pointer to deployed main (c4f24de, dedup fix live)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-04 18:38:06 -07:00
a6a6a477d5 sync: auto-sync from HOWARD-HOME at 2026-07-04 18:34:08
Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-07-04 18:34:08
2026-07-04 18:34:36 -07:00
b8bba3cd8f sync: auto-sync from HOWARD-HOME at 2026-07-04 18:08:15
Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-07-04 18:08:15
2026-07-04 18:08:41 -07:00
011ee7350d sync: auto-sync from HOWARD-HOME at 2026-07-04 18:00:11
Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-07-04 18:00:11
2026-07-04 18:00:38 -07:00
21b198f1ee sync: auto-sync from HOWARD-HOME at 2026-07-04 17:49:31
Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-07-04 17:49:31
2026-07-04 17:49:58 -07:00
6d36f7151c sync: auto-sync from HOWARD-HOME at 2026-07-04 17:09:47
Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-07-04 17:09:47
2026-07-04 17:10:14 -07:00
edd4bd39e7 sync: auto-sync from HOWARD-HOME at 2026-07-04 17:05:53
Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-07-04 17:05:53
2026-07-04 17:06:23 -07:00
ddd6bb06c2 sync: auto-sync from GURU-5070 at 2026-07-04 16:52:21
Author: Mike Swanson
Machine: GURU-5070
Timestamp: 2026-07-04 16:52:21
2026-07-04 16:52:59 -07:00
b97223f69e sync: auto-sync from HOWARD-HOME at 2026-07-04 16:36:49
Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-07-04 16:36:49
2026-07-04 16:37:15 -07:00
f64418d5e1 sync: auto-sync from HOWARD-HOME at 2026-07-04 16:34:34
Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-07-04 16:34:34
2026-07-04 16:34:59 -07:00
2aed9d93c9 sync: auto-sync from HOWARD-HOME at 2026-07-04 16:20:30
Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-07-04 16:20:30
2026-07-04 16:20:55 -07:00
cb99daa6cb sync: auto-sync from HOWARD-HOME at 2026-07-04 16:09:13
Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-07-04 16:09:13
2026-07-04 16:09:41 -07:00
d7bc2b34ac guru-rmm: bump submodule — dedup reuses record without re-homing (moves persist)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-04 15:38:12 -07:00
734a7b201d guru-rmm: bump submodule — cross-site agent dedup fix
Points to cdc87d2 (server: fix cross-site agent duplication on re-enrollment).
Needs build + deploy by Mike/Howard before the dedup deletes are run.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-04 15:31:59 -07:00
d4a1bbbc5b sync: auto-sync from HOWARD-HOME at 2026-07-04 15:30:44
Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-07-04 15:30:44
2026-07-04 15:31:14 -07:00
a733db1029 sync: auto-sync from HOWARD-HOME at 2026-07-04 14:18:41
Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-07-04 14:18:41
2026-07-04 14:19:11 -07:00
d5020a6415 sync: auto-sync from GURU-5070 at 2026-07-04 13:52:09
Author: Mike Swanson
Machine: GURU-5070
Timestamp: 2026-07-04 13:52:09
2026-07-04 13:52:35 -07:00
0da31cbb65 memory: GuruRMM legacy 1.77 build disabled + advance guru-rmm gitlink (tray feature deployed, legacy frozen at 0.6.76)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-04 13:30:06 -07:00
ff7025d912 sync: auto-sync from GURU-5070 at 2026-07-04 13:07:09
Author: Mike Swanson
Machine: GURU-5070
Timestamp: 2026-07-04 13:07:09
2026-07-04 13:10:12 -07:00
ad6a529ba4 sync: auto-sync from HOWARD-HOME at 2026-07-04 12:42:50
Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-07-04 12:42:50
2026-07-04 12:43:27 -07:00
60a0a5728f sync: auto-sync from GURU-5070 at 2026-07-04 12:23:18
Author: Mike Swanson
Machine: GURU-5070
Timestamp: 2026-07-04 12:23:18
2026-07-04 12:27:48 -07:00
10b1ea7528 sync: auto-sync from GURU-5070 at 2026-07-04 07:30:32
Author: Mike Swanson
Machine: GURU-5070
Timestamp: 2026-07-04 07:30:32
2026-07-04 12:26:42 -07:00
85c8149495 sync: auto-sync from HOWARD-HOME at 2026-07-04 12:00:16
Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-07-04 12:00:16
2026-07-04 12:00:44 -07:00
a8ed995979 sync: auto-sync from HOWARD-HOME at 2026-07-04 11:47:14
Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-07-04 11:47:14
2026-07-04 11:47:40 -07:00
ac05230cd2 sync: auto-sync from HOWARD-HOME at 2026-07-04 10:45:23
Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-07-04 10:45:23
2026-07-04 10:45:49 -07:00
a1f0a3e5e8 sync: auto-sync from HOWARD-HOME at 2026-07-04 09:24:45
Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-07-04 09:24:45
2026-07-04 09:25:14 -07:00
4e739abe5f sync: auto-sync from HOWARD-HOME at 2026-07-04 08:43:14
Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-07-04 08:43:14
2026-07-04 08:43:40 -07:00
1ab0e88e67 sync: auto-sync from HOWARD-HOME at 2026-07-04 08:35:27
Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-07-04 08:35:27
2026-07-04 08:35:53 -07:00
0ca8e384d8 sync: auto-sync from HOWARD-HOME at 2026-07-04 08:25:49
Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-07-04 08:25:49
2026-07-04 08:26:19 -07:00
fad05480a5 sync: auto-sync from HOWARD-HOME at 2026-07-03 23:31:28
Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-07-03 23:31:28
2026-07-03 23:31:53 -07:00
e71a486db2 sync: auto-sync from HOWARD-HOME at 2026-07-03 23:02:54
Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-07-03 23:02:54
2026-07-03 23:03:20 -07:00
2e167c97fa sync: auto-sync from HOWARD-HOME at 2026-07-03 22:36:52
Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-07-03 22:36:52
2026-07-03 22:37:24 -07:00
e2d902a337 sync: auto-sync from HOWARD-HOME at 2026-07-03 22:01:21
Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-07-03 22:01:21
2026-07-03 22:01:53 -07:00
c18e74ed1c sync: auto-sync from HOWARD-HOME at 2026-07-03 21:44:00
Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-07-03 21:44:00
2026-07-03 21:44:28 -07:00
fceb7c6075 memory + rmm: agy-headless correction, gemini errorlog, bump guru-rmm (Syncro recon + spec plan)
- memory: agy.exe NOW works headless (v1.0.6+ --print/-p, --add-dir) — corrected
  reference_antigravity_agy_not_headless + MEMORY.md index; supersedes the old "DOA headless" note.
- errorlog: gemini-cli OAuth eligibility failure on this machine (agy/ask-gemini).
- bump projects/msp-tools/guru-rmm -> a7ecb9f (RMM_THOUGHTS Feature 11 OS Updates + RMM Fixes batch;
  docs/recon + docs/specs: Syncro policy/device recon + policy-redesign spec plan).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 21:24:36 -07:00
442f3cb1c7 sync: auto-sync from HOWARD-HOME at 2026-07-03 20:52:38
Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-07-03 20:52:38
2026-07-03 20:53:05 -07:00
f61088dac4 sync: auto-sync from HOWARD-HOME at 2026-07-03 19:57:06
Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-07-03 19:57:06
2026-07-03 19:57:32 -07:00
63e1eb743b sync: auto-sync from HOWARD-HOME at 2026-07-03 19:30:59
Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-07-03 19:30:59
2026-07-03 19:31:29 -07:00
c82c1c76bb sync: auto-sync from HOWARD-HOME at 2026-07-03 17:22:21
Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-07-03 17:22:21
2026-07-03 17:22:47 -07:00
e0e3dd0d82 sync: auto-sync from HOWARD-HOME at 2026-07-03 17:00:12
Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-07-03 17:00:12
2026-07-03 17:01:38 -07:00
78f794a924 sync: auto-sync from GURU-5070 at 2026-07-03 13:18:27
Author: Mike Swanson
Machine: GURU-5070
Timestamp: 2026-07-03 13:18:27
2026-07-03 13:19:22 -07:00
41c12a934f agy: rewire skill from dead gemini npm CLI to Antigravity 'agy' binary
The old Google gemini CLI stopped working on this account (throwIneligibleOrProjectIdError -
needs a GOOGLE_CLOUD_PROJECT the personal account can't supply). Replace it with the
Antigravity CLI (agy, native Go binary, own auth, no project ID).

- New scripts/ask-agy.sh: plain-text output (no JSON), -p prompt-last, --model friendly
  names (Gemini 3.1 Pro (High) default for verify/review*/vision/search), --add-dir for
  file/vision reads, --dangerously-skip-permissions to avoid print-mode approval hangs.
  All modes smoke-tested (text/verify/review/search).
- scripts/ask-gemini.sh: deprecated shim -> exec ask-agy.sh (back-compat).
- SKILL.md: rewired header/flags/models/auth/availability/reference to agy.
- ask-grok.sh: xsearch fallback now execs ask-agy.sh (was the dead gemini).
- identity.json (local, gitignored): agy block added, gemini marked retired.

Authoritative flags sourced from 'agy --help' (web docs are JS SPAs, unreadable by
fetch tools; grok fetch/search timed out).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 13:19:21 -07:00
727a0757f6 wiki: compile peaceful-spirit (full) — Syncro refresh, VSS + address, root-log provenance
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 12:30:08 -07:00
6cb90c5b09 sync: auto-sync from HOWARD-HOME at 2026-07-03 09:25:34
Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-07-03 09:25:34
2026-07-03 09:26:05 -07:00
8ecb406571 sync: auto-sync from GURU-BEAST-ROG at 2026-07-03 06:23:58
Author: Mike Swanson
Machine: GURU-BEAST-ROG
Timestamp: 2026-07-03 06:23:58
2026-07-03 06:25:13 -07:00
5e4b2cf385 sync: auto-sync from GURU-5070 at 2026-07-02 19:14:01
Author: Mike Swanson
Machine: GURU-5070
Timestamp: 2026-07-02 19:14:01
2026-07-02 19:14:50 -07:00
d745c7d40f sync: auto-sync from HOWARD-HOME at 2026-07-02 18:33:52
Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-07-02 18:33:52
2026-07-02 18:35:56 -07:00
7300f57a47 sync: auto-sync from HOWARD-HOME at 2026-07-02 18:32:04
Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-07-02 18:32:04
2026-07-02 18:33:52 -07:00
101b95e610 sync: auto-sync from GURU-5070 at 2026-07-02 18:25:06
Author: Mike Swanson
Machine: GURU-5070
Timestamp: 2026-07-02 18:25:06
2026-07-02 18:25:55 -07:00
27b9966dfa sync: auto-sync from GURU-5070 at 2026-07-02 17:44:44
Author: Mike Swanson
Machine: GURU-5070
Timestamp: 2026-07-02 17:44:44
2026-07-02 17:45:34 -07:00
c54ca5ec22 sync: auto-sync from GURU-5070 at 2026-07-02 17:34:37
Author: Mike Swanson
Machine: GURU-5070
Timestamp: 2026-07-02 17:34:37
2026-07-02 17:35:26 -07:00
75f60df6a6 sync: auto-sync from GURU-5070 at 2026-07-02 17:30:07
Author: Mike Swanson
Machine: GURU-5070
Timestamp: 2026-07-02 17:30:07
2026-07-02 17:30:57 -07:00
59b5f1f5f2 wiki: update PST (deletion-report location) + add fast wiki-compile 'update' mode
- peaceful-spirit: record the standing 'Mara audit log' (daily PST Deletion Report task,
  SACL 4660/4663 on G:\Shares\Scanned) and its new output location under the legal/
  partner-review folder (moved 2026-07-02). Surgical update, no full recompile.
- wiki-compile: add an incremental UPDATE mode (now the no-flag default) that folds only
  session logs newer than last_compiled via targeted section edits — no Sonnet subagent,
  no full-article regeneration. --full is now the explicit REBUILD; --syncro is the
  instant Syncro-only refresh. Addresses the slow-rebuild complaint.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 17:27:23 -07:00
7ff092f7bb sync: auto-sync from GURU-5070 at 2026-07-02 16:01:04
Author: Mike Swanson
Machine: GURU-5070
Timestamp: 2026-07-02 16:01:04
2026-07-02 16:01:59 -07:00
b01105d1f9 cascades: caretaker roster + phone-login remediation updates
- Document forced-change-at-logon gotcha + fleet-wide fix (shared-phone err 50126)
- Record PSO-Caregivers (never-expire FGPP on SG-Caregivers)
- Log hard-delete of 7 offboarded leavers; Juan Andrade offboard scheduled 2026-07-11

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 15:23:20 -07:00
8152476ee4 remediation-tool: document the 365 app suite + build consent-audit
Root-caused the recurring '365 suite isn't documented' pain: the apps are fine (tiered by
privilege) but per-tenant consent is NOT uniform and there was no way to see a tenant's
actual grant state. VWP had the Tenant Admin app but no SharePoint app-only role -> silent
401s until this session.

- references/app-suite.md: authoritative, live-verified map of every app, App ID, and
  actually-granted permission per tier; the consent-drift problem + both fix methods
  (adminconsent URL, direct appRoleAssignment grant).
- scripts/consent-audit.sh: audits a tenant (or --all) vs the baseline, grades
  GREEN/AMBER/RED, prints the exact fix per gap. Extends the assign-exchange-role --verify
  pattern to Graph scopes + SharePoint role + EXO role. Verified: BirthBio GREEN, VWP/Cascades
  AMBER (caught real drift - both missing grants).
- SKILL.md: run consent-audit FIRST on any tenant task. Memory + errorlog correction.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 15:15:08 -07:00
42da3cfcca sync: auto-sync from HOWARD-HOME at 2026-07-02 14:48:25
Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-07-02 14:48:25
2026-07-02 14:49:01 -07:00
5008ad79dc wiki: compile scileppi-law (full) - UniFi layer, Cox WAN RCA, Active Work, rose user 2026-07-02 13:44:54 -07:00
b0cf2a31ba sync: auto-sync from HOWARD-HOME at 2026-07-02 12:05:22
Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-07-02 12:05:22
2026-07-02 12:05:55 -07:00
552760e7cd sync: auto-sync from HOWARD-HOME at 2026-07-02 11:20:14
Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-07-02 11:20:14
2026-07-02 11:20:45 -07:00
ccfa4f7b21 sync: auto-sync from HOWARD-HOME at 2026-07-02 11:16:12
Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-07-02 11:16:12
2026-07-02 11:16:42 -07:00
Winter Williams
ac23f17e23 sync: auto-sync from GURU-BEAST-ROG at 2026-07-02 10:55:41
Author: Mike Swanson
Machine: GURU-BEAST-ROG
Timestamp: 2026-07-02 10:55:41
2026-07-02 10:57:33 -07:00
26f47fdd10 sync: auto-sync from HOWARD-HOME at 2026-07-02 09:08:36
Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-07-02 09:08:36
2026-07-02 09:10:02 -07:00
3e6f946377 sync: auto-sync from GURU-5070 at 2026-07-02 06:23:34
Author: Mike Swanson
Machine: GURU-5070
Timestamp: 2026-07-02 06:23:34
2026-07-02 06:25:18 -07:00
30c7b26af2 sync: auto-sync from GURU-BEAST-ROG at 2026-07-01 20:15:51
Author: Mike Swanson
Machine: GURU-BEAST-ROG
Timestamp: 2026-07-01 20:15:51
2026-07-01 20:16:08 -07:00
1264cbda7b chore: advance discord-bot gitlink to a35bd1bf (default model -> claude-fable-5)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-01 20:08:28 -07:00
b07613c127 sync: auto-sync from GURU-5070 at 2026-07-01 20:07:01
Author: Mike Swanson
Machine: GURU-5070
Timestamp: 2026-07-01 20:07:01
2026-07-01 20:07:42 -07:00
2937b00ebf sync: auto-sync from GURU-5070 at 2026-07-01 15:49:56
Author: Mike Swanson
Machine: GURU-5070
Timestamp: 2026-07-01 15:49:56
2026-07-01 15:50:54 -07:00
1775571abb wiki: compile cascades-tucson (full) -- 7/1 caretaker roster update (35 in SG-Caregivers) + phone-login CA cutover integrated
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-01 15:47:03 -07:00
282c4af8cc sync: auto-sync from HOWARD-HOME at 2026-07-01 15:12:14
Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-07-01 15:12:14
2026-07-01 15:12:48 -07:00
c3aeef60fb sync: auto-sync from GURU-5070 at 2026-07-01 15:06:42
Author: Mike Swanson
Machine: GURU-5070
Timestamp: 2026-07-01 15:06:42
2026-07-01 15:07:39 -07:00
875048f9ad sync: auto-sync from HOWARD-HOME at 2026-07-01 14:43:57
Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-07-01 14:43:57
2026-07-01 14:44:29 -07:00
142afd7e98 sync: auto-sync from HOWARD-HOME at 2026-07-01 13:50:18
Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-07-01 13:50:18
2026-07-01 13:50:49 -07:00
486b72ec71 wiki: compile cascades-tucson (full) — VLAN 20 migration live-reconciled (22 machines moved, printer shares 4/15), ALIS SSO login model, Syncro 37.5h/0 tickets/29 assets 2026-07-01 13:32:24 -07:00
9e78a153f3 sync: auto-sync from HOWARD-HOME at 2026-07-01 13:22:23
Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-07-01 13:22:23
2026-07-01 13:24:58 -07:00
7f897ce93f sync: auto-sync from GURU-5070 at 2026-07-01 13:09:08
Author: Mike Swanson
Machine: GURU-5070
Timestamp: 2026-07-01 13:09:08
2026-07-01 13:10:01 -07:00
af8a3de00e sync: auto-sync from GURU-5070 at 2026-07-01 13:06:10
Author: Mike Swanson
Machine: GURU-5070
Timestamp: 2026-07-01 13:06:10
2026-07-01 13:07:50 -07:00
29355584bf wiki: compile scileppi-law (full) -- NAS/mount root-cause, no-sleep fix, ScreenConnect, corrected billing + Mac identity 2026-07-01 12:28:32 -07:00
e527b89999 sync: auto-sync from HOWARD-HOME at 2026-07-01 11:39:58
Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-07-01 11:39:58
2026-07-01 11:41:17 -07:00
6f7f939a62 sync: auto-sync from GURU-5070 at 2026-07-01 09:32:17
Author: Mike Swanson
Machine: GURU-5070
Timestamp: 2026-07-01 09:32:17
2026-07-01 09:33:09 -07:00
e583bf43a5 wiki: compile peaceful-spirit (full) — two-DC DFS, deletion investigation, Admin1/Admin2 ACL hardening 2026-07-01 09:09:40 -07:00
02845878d6 wiki: compile birth-biologic (full) — Datto->SharePoint migration complete, Quality sync done, MX cut, #32187 rename scheduled 2026-07-01 09:08:57 -07:00
6f676672a8 sync: auto-sync from GURU-5070 at 2026-07-01 08:54:46
Author: Mike Swanson
Machine: GURU-5070
Timestamp: 2026-07-01 08:54:46
2026-07-01 08:55:39 -07:00
6251044baf sync: auto-sync from GURU-5070 at 2026-07-01 05:15:39
Author: Mike Swanson
Machine: GURU-5070
Timestamp: 2026-07-01 05:15:39
2026-07-01 05:16:24 -07:00
9951e88f69 wiki: compile cascades-tucson (full) -- caregiver phone SSO licensing + billing 37.5h + 6/25-6/30 work 2026-06-30 17:43:11 -07:00
be9574a480 sync: auto-sync from HOWARD-HOME at 2026-06-30 17:28:16
Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-06-30 17:28:16
2026-06-30 17:30:25 -07:00
ab640dfe77 sync: auto-sync from HOWARD-HOME at 2026-06-30 17:28:00
Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-06-30 17:28:00
2026-06-30 17:28:36 -07:00
01613697c6 sync: auto-sync from GURU-5070 at 2026-06-30 17:21:06
Author: Mike Swanson
Machine: GURU-5070
Timestamp: 2026-06-30 17:21:06
2026-06-30 17:21:47 -07:00
1b0b313896 Birth Biologic: Merge + save Quality sync continuation state 2026-06-30 15:28:44 -07:00
152513b15d Birth Biologic: Save Quality sync state + working upload script
- Current state: 3,249/3,768 files uploaded, 519 remaining
- Active RMM command: 9e0fcfe8 (running on ACG-DWP-X-BB)
- Working upload script with drive ID concatenation fix
- Comprehensive continuation instructions
- All verification scripts

Client very angry - this was promised yesterday
Issue: PowerShell escaping ! in drive ID (b! -> b\!)
Solution: String concatenation at runtime
2026-06-30 15:27:43 -07:00
a808200dc4 sync: auto-sync from GURU-5070 at 2026-06-30 15:16:30
Author: Mike Swanson
Machine: GURU-5070
Timestamp: 2026-06-30 15:16:30
2026-06-30 15:17:17 -07:00
f6f6aae618 sync: auto-sync from GURU-BEAST-ROG at 2026-06-30 15:11:06
Author: Mike Swanson
Machine: GURU-BEAST-ROG
Timestamp: 2026-06-30 15:11:06
2026-06-30 15:12:12 -07:00
2fc6afb121 sync: auto-sync from HOWARD-HOME at 2026-06-30 12:46:41
Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-06-30 12:46:41
2026-06-30 12:47:11 -07:00
f17d56d717 sync: auto-sync from HOWARD-HOME at 2026-06-30 12:45:27
Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-06-30 12:45:27
2026-06-30 12:46:01 -07:00
5db3a27685 sync: auto-sync from HOWARD-HOME at 2026-06-30 12:39:43
Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-06-30 12:39:43
2026-06-30 12:40:12 -07:00
bc6dde5b89 sync: auto-sync from HOWARD-HOME at 2026-06-30 11:54:16
Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-06-30 11:54:16
2026-06-30 11:54:48 -07:00
51335db124 sync: auto-sync from HOWARD-HOME at 2026-06-30 11:27:16
Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-06-30 11:27:16
2026-06-30 11:27:47 -07:00
5e92c33b73 sync: auto-sync from HOWARD-HOME at 2026-06-30 10:37:25
Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-06-30 10:37:25
2026-06-30 10:37:58 -07:00
6cc0c08ac4 sync: auto-sync from HOWARD-HOME at 2026-06-30 08:45:13
Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-06-30 08:45:13
2026-06-30 08:45:47 -07:00
769f099f70 sync: auto-sync from HOWARD-HOME at 2026-06-30 07:56:06
Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-06-30 07:56:06
2026-06-30 07:56:40 -07:00
5c87b83556 wiki: compile rednour (full) — Carrie Win11 success + corrupt-download root cause, all 3 boxes on Win11, RMM-works correction, Datto AV id, #32368 invoiced 2026-06-30 07:56:39 -07:00
801ff788a6 sync: auto-sync from GURU-5070 at 2026-06-30 06:05:04
Author: Mike Swanson
Machine: GURU-5070
Timestamp: 2026-06-30 06:05:04
2026-06-30 06:07:35 -07:00
d2da2bb334 sync: auto-sync from HOWARD-HOME at 2026-06-29 22:56:53
Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-06-29 22:56:53
2026-06-29 22:57:29 -07:00
0eb32e8b1d wiki: compile michaeljohnson (full) - GuruRMM onboarding (AMBER/RED baselines) + Datto EDR/AV deploy (no Bitdefender present) + static-IP share fix + #32477 billed; break-fix $175 onsite per Syncro 2026-06-29 22:50:08 -07:00
fa589a198b sync: auto-sync from HOWARD-HOME at 2026-06-29 22:45:04
Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-06-29 22:45:04
2026-06-29 22:45:31 -07:00
18ac0f98fa sync: auto-sync from HOWARD-HOME at 2026-06-29 22:38:53
Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-06-29 22:38:53
2026-06-29 22:39:22 -07:00
91e95b1e4d sync: auto-sync from HOWARD-HOME at 2026-06-29 22:26:40
Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-06-29 22:26:40
2026-06-29 22:27:09 -07:00
bae40fc930 cascades: ALIS caregiver phone-only + caretaker-only cross-check (for handoff to other session) 2026-06-29 17:26:56 -07:00
31f2bdb84f sync: auto-sync from HOWARD-HOME at 2026-06-29 16:55:22
Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-06-29 16:55:22
2026-06-29 16:55:55 -07:00
e692bff2bc sync: auto-sync from HOWARD-HOME at 2026-06-29 16:19:10
Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-06-29 16:19:10
2026-06-29 16:19:44 -07:00
aae5b4cdd6 rednour: record Syncro #32368 as billing target for Carrie-machine work 2026-06-29 16:13:51 -07:00
d6c0722796 wiki: compile rednour (full) — Carrie Win11 SAFE_OS/APPLY_IMAGE failure + RMM-not-working note 2026-06-29 16:09:35 -07:00
d677b7055c sync: auto-sync from HOWARD-HOME at 2026-06-29 16:02:09
Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-06-29 16:02:09
2026-06-29 16:02:41 -07:00
ae3ed4a872 sync: auto-sync from HOWARD-HOME at 2026-06-29 15:33:39
Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-06-29 15:33:39
2026-06-29 15:35:01 -07:00
3a1a43d014 wiki: compile rednour (full) - macOS RMM code-vs-UUID root cause, LEGALASST .zip-hang + WP5 save error, break-fix billing from Syncro, plaintext creds flagged 2026-06-29 15:34:14 -07:00
fef4dc717f chore: drop stray BirthBio scratch xlsx from repo root 2026-06-29 15:32:20 -07:00
9a6e1157a7 sync: auto-sync from GURU-5070 at 2026-06-29 15:30:34
Author: Mike Swanson
Machine: GURU-5070
Timestamp: 2026-06-29 15:30:34
2026-06-29 15:31:35 -07:00
a88a360450 sync: auto-sync from HOWARD-HOME at 2026-06-29 14:24:19
Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-06-29 14:24:19
2026-06-29 14:24:49 -07:00
a5dc51168e sync: auto-sync from HOWARD-HOME at 2026-06-29 14:22:54
Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-06-29 14:22:54
2026-06-29 14:24:17 -07:00
00af39d369 sync: auto-sync from HOWARD-HOME at 2026-06-29 14:23:40
Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-06-29 14:23:40
2026-06-29 14:24:12 -07:00
602c5e5bd6 chore: remove stray PST scratch files (.pst_sweep/.pst_when) 2026-06-29 11:47:18 -07:00
e99110fdc9 sync: auto-sync from GURU-5070 at 2026-06-29 11:45:50
Author: Mike Swanson
Machine: GURU-5070
Timestamp: 2026-06-29 11:45:50
2026-06-29 11:46:42 -07:00
1f15a6bc79 sync: auto-sync from GURU-5070 at 2026-06-29 11:05:52
Author: Mike Swanson
Machine: GURU-5070
Timestamp: 2026-06-29 11:05:52
2026-06-29 11:06:44 -07:00
eee062be3f sync: auto-sync from GURU-5070 at 2026-06-29 08:17:44
Author: Mike Swanson
Machine: GURU-5070
Timestamp: 2026-06-29 08:17:44
2026-06-29 08:19:10 -07:00
0ed8f1413e sync: auto-sync from GURU-5070 at 2026-06-29 07:28:30
Author: Mike Swanson
Machine: GURU-5070
Timestamp: 2026-06-29 07:28:30
2026-06-29 07:29:28 -07:00
8652a6db09 sync: auto-sync from HOWARD-HOME at 2026-06-28 19:08:56
Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-06-28 19:08:56
2026-06-28 19:09:29 -07:00
ee0ab3805c chore: advance guru-rmm pointer (av-removal Safe-Mode session + log); drop stray scratch file
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 18:51:23 -07:00
ab2a1a7920 sync: auto-sync from HOWARD-HOME at 2026-06-28 18:48:42
Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-06-28 18:48:42
2026-06-28 18:49:14 -07:00
3dd3ae24df sync: auto-sync from HOWARD-HOME at 2026-06-27 21:47:40
Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-06-27 21:47:40
2026-06-27 21:48:19 -07:00
9c1bb40f1d chore: advance guru-rmm pointer (SafeBoot Wi-Fi diagnosis + session log) 2026-06-27 21:45:40 -07:00
a4ed35c9b9 sync: auto-sync from HOWARD-HOME at 2026-06-27 21:44:08
Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-06-27 21:44:08
2026-06-27 21:44:42 -07:00
58b6d931cb sync: auto-sync from HOWARD-HOME at 2026-06-27 18:58:49
Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-06-27 18:58:49
2026-06-27 19:00:08 -07:00
a8e7b419ac sync: auto-sync from GURU-5070 at 2026-06-27 18:49:16
Author: Mike Swanson
Machine: GURU-5070
Timestamp: 2026-06-27 18:49:16
2026-06-27 18:50:05 -07:00
744a6ef3f0 sync: auto-sync from HOWARD-HOME at 2026-06-27 16:18:14
Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-06-27 16:18:14
2026-06-27 16:18:46 -07:00
ab35d81a5a radio-show: advance to 76b9ac2 (6/27 Quantum 101 rewrite + interference breakdown)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 08:41:16 -07:00
dde412a16f sync: auto-sync from GURU-5070 at 2026-06-27 06:53:09
Author: Mike Swanson
Machine: GURU-5070
Timestamp: 2026-06-27 06:53:09
2026-06-27 06:54:08 -07:00
dbe355a2d5 radio-show: advance to 99751ba (6/27 episode prep + show notes)
Lands the uncommitted 2026-06-27-when-ai-makes-it-up/ folder (show-prep.md +
show-notes.html) + session log on origin so GURU-5070/Mike can read/edit it.
Resolves coord request from GURU-5070/claude-main.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 05:40:18 -07:00
4321dbbbc0 sync: auto-sync from GURU-5070 at 2026-06-27 04:42:51
Author: Mike Swanson
Machine: GURU-5070
Timestamp: 2026-06-27 04:42:51
2026-06-27 04:44:53 -07:00
03ee61f0a6 sync: auto-sync from HOWARD-HOME at 2026-06-26 21:14:15
Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-06-26 21:14:15
2026-06-26 21:14:45 -07:00
3870940a2f sync: auto-sync from HOWARD-HOME at 2026-06-26 17:21:25
Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-06-26 17:21:25
2026-06-26 17:21:56 -07:00
8633d59b89 sync: auto-sync from HOWARD-HOME at 2026-06-26 17:20:09
Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-06-26 17:20:09
2026-06-26 17:20:40 -07:00
6763a83899 sync: auto-sync from HOWARD-HOME at 2026-06-26 16:34:17
Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-06-26 16:34:17
2026-06-26 16:35:39 -07:00
ab0f0e0cb0 sync: auto-sync from HOWARD-HOME at 2026-06-26 16:34:14
Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-06-26 16:34:14
2026-06-26 16:34:46 -07:00
6f7c8443f9 sync: auto-sync from HOWARD-HOME at 2026-06-26 15:38:41
Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-06-26 15:38:41
2026-06-26 15:39:15 -07:00
4ab1cc847d sync: auto-sync from HOWARD-HOME at 2026-06-26 12:18:24
Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-06-26 12:18:24
2026-06-26 12:19:01 -07:00
ca37a606c6 harness: add Definition-of-Done skill routing (auto-gate work with matching check-skills)
Skill-first rule now has two halves: route the request to a doing-skill,
then gate the result with the matching check-skill before 'done' --
inferred from the request, not user-named. Adds .claude/SKILL_ROUTING.md
(on-demand request->doing-skill->check-skill map). Enforcement tier A+B
(CORE rule + map; Stop-hook backstop deferred). Calibrate to stakes,
Ollama Tier-0 for cheap passes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 11:57:44 -07:00
5bace24371 sync: auto-sync from HOWARD-HOME at 2026-06-26 11:40:19
Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-06-26 11:40:19
2026-06-26 11:40:52 -07:00
0cf843e13d sync: auto-sync from HOWARD-HOME at 2026-06-26 11:24:30
Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-06-26 11:24:30
2026-06-26 11:25:00 -07:00
f6c0f8cbe0 sync: auto-sync from HOWARD-HOME at 2026-06-26 09:40:32
Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-06-26 09:40:32
2026-06-26 09:41:00 -07:00
c97dd98616 wiki: compile cascades-tucson (full) — CS-SERVER Datto removal + SMB error-67 RMM-test-artifact correction 2026-06-26 09:40:03 -07:00
929598418e sync: auto-sync from HOWARD-HOME at 2026-06-26 08:54:48
Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-06-26 08:54:48
2026-06-26 08:55:19 -07:00
10a90bb213 sync: auto-sync from HOWARD-HOME at 2026-06-26 08:41:22
Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-06-26 08:41:22
2026-06-26 08:42:30 -07:00
bd52dde6a7 sync: auto-sync from GURU-5070 at 2026-06-26 08:02:22
Author: Mike Swanson
Machine: GURU-5070
Timestamp: 2026-06-26 08:02:22
2026-06-26 08:03:27 -07:00
270e294938 sync: auto-sync from HOWARD-HOME at 2026-06-26 07:19:00
Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-06-26 07:19:00
2026-06-26 07:20:04 -07:00
15f2d03d7b wiki: compile birth-biologic (full) — mail migration live, Datto VM recovered, tenant fully onboarded
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 06:42:02 -07:00
6d65bff791 sync: auto-sync from GURU-5070 at 2026-06-26 06:29:48
Author: Mike Swanson
Machine: GURU-5070
Timestamp: 2026-06-26 06:29:48
2026-06-26 06:30:46 -07:00
a0b2cfbee1 sync: auto-sync from GURU-5070 at 2026-06-26 04:18:21
Author: Mike Swanson
Machine: GURU-5070
Timestamp: 2026-06-26 04:18:21
2026-06-26 04:19:32 -07:00
79789a8815 sync: auto-sync from GURU-5070 at 2026-06-26 04:15:16
Author: Mike Swanson
Machine: GURU-5070
Timestamp: 2026-06-26 04:15:16
2026-06-26 04:16:39 -07:00
1d99dc93ed sync: auto-sync from HOWARD-HOME at 2026-06-25 23:09:59
Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-06-25 23:09:59
2026-06-25 23:10:26 -07:00
04b0d12150 sync: auto-sync from HOWARD-HOME at 2026-06-25 21:48:38
Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-06-25 21:48:38
2026-06-25 21:49:05 -07:00
9a243a9b96 wiki: compile cascades-tucson (full) — EDR/Bitdefender migration, billing refresh 2026-06-25 21:36:25 -07:00
1917f19436 chore: untrack session scratch files (.bd2_*, .watch_targets.py)
Swept in by git add -A during /save; added to .gitignore.
.watch_targets.py kept on disk (in use by background reconnect watcher).
2026-06-25 21:24:02 -07:00
563ff9e8fa sync: auto-sync from HOWARD-HOME at 2026-06-25 21:21:56
Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-06-25 21:21:56
2026-06-25 21:23:24 -07:00
730d26437b sync: auto-sync from GURU-5070 at 2026-06-25 21:13:47
Author: Mike Swanson
Machine: GURU-5070
Timestamp: 2026-06-25 21:13:47
2026-06-25 21:15:00 -07:00
cf960d1b2a sync: auto-sync from HOWARD-HOME at 2026-06-25 20:23:53
Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-06-25 20:23:53
2026-06-25 20:24:23 -07:00
42c8b232cd sync: auto-sync from HOWARD-HOME at 2026-06-25 19:48:41
Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-06-25 19:48:41
2026-06-25 19:49:08 -07:00
9f5fedda06 memory: RMM Set-Acl/icacls timeout drops stdout (lost password); generate secrets locally 2026-06-25 19:28:11 -07:00
b24239eef9 wiki(rednour): Nick SMB share access set up; flag macOS RMM install fail (unsigned Apple Silicon binary); add return-visit + share docs 2026-06-25 19:27:14 -07:00
db73af2866 sync: auto-sync from HOWARD-HOME at 2026-06-25 19:20:21
Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-06-25 19:20:21
2026-06-25 19:21:43 -07:00
d1de83a6d3 sync: auto-sync from GURU-5070 at 2026-06-25 19:18:08
Author: Mike Swanson
Machine: GURU-5070
Timestamp: 2026-06-25 19:18:08
2026-06-25 19:19:46 -07:00
Winter Williams
2391b510a5 sync: auto-sync from GURU-BEAST-ROG at 2026-06-25 19:13:01
Author: Mike Swanson
Machine: GURU-BEAST-ROG
Timestamp: 2026-06-25 19:13:01
2026-06-25 19:13:21 -07:00
Winter Williams
f3edf62cf7 sync: auto-sync from GURU-BEAST-ROG at 2026-06-25 16:56:58
Author: Mike Swanson
Machine: GURU-BEAST-ROG
Timestamp: 2026-06-25 16:56:58
2026-06-25 16:57:22 -07:00
93bd5379e3 sync: auto-sync from HOWARD-HOME at 2026-06-25 15:21:30
Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-06-25 15:21:30
2026-06-25 15:22:06 -07:00
79bda6fab9 datto-edr: apply code-review fixes (gating + footgun hardening)
- deploy-cmd: require explicit --regkey or --group; never auto-pick an
  arbitrary cross-client registration key (would enroll into wrong org).
- raw: block POST to any */scan endpoint with no non-empty `where`
  (same tenant-wide footgun the scan command guards against).
- main(): catch-all for unexpected exceptions -> [ERROR] + errorlog,
  plus clean KeyboardInterrupt (130).
- isolate: forgiving extension-name match (exact, then substring),
  excludes the paired "Restore" ext; errors on ambiguous match.
- detections: --site -> --target-group; Alert.targetGroupId is a
  scan-target id, not a Location id (distinct from `agents --site`).
- status: relabel "Target groups (sites)" -> "Scan target groups".
- SKILL.md + docstrings updated to match.

Verified: py_compile clean, selftest green (216 agents), guards fire
on no-key/empty-where/no-agent, deploy-cmd --group picks the group's key.
2026-06-25 15:14:18 -07:00
d87aa9dfd8 sync: auto-sync from HOWARD-HOME at 2026-06-25 14:58:19
Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-06-25 14:58:19
2026-06-25 14:58:50 -07:00
df75a86518 wiki: compile cascades-tucson (full) — Alma offboarding + PAA item, CARF plan, CSC-ENT consolidation, Syncro refresh (47.75h, 5 open)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 14:22:00 -07:00
befd2650c8 fix(bitdefender): fifth-pass - companies lists full fleet, drop unused import
Convergence-pass LOW/NIT cleanup:
- cmd_companies uses list_all_companies() so a >100-company tenant isn't truncated
  in the listing (was page-1 only); matches sweep/inventory.
- removed unused 'field' import from dataclasses.
Deliberately NOT changed: id validation on delete-package/report-delete/blocklist-
remove/quarantine-remove/restore - those ids are not pinned 24-hex format, so
validating could reject valid input; they are --confirm-gated and bad ids match
the expected-error markers (no mislog). 81/81 selftest.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 14:05:32 -07:00
3d6cb467bf fix(bitdefender): fourth-pass - urllib reset safety, Retry-After clamp, sweep/install-links id validation
From a third review pass (converging - all MEDIUM/LOW):
- urllib fallback: a post-send reset (RemoteDisconnected/ConnectionReset, which
  urllib wraps in URLError) was misclassified as always-safe 'connect' and could
  retry a non-idempotent write after a server commit. Now only ConnectionRefused/
  DNS (socket.gaierror) -> 'connect'; everything else -> 'timeout' (write-gated).
- _retry_delay clamps a negative numeric Retry-After to 0 (was -> time.sleep(-1) ValueError).
- cmd_sweep + cmd_install_links now validate --company; cmd_company_create validates
  --parent (finished _require_oid consistency - these mislogged as errorlog noise).
- cmd_push_test parses --extra-json before gating (validate->gate order, matches siblings).
- selftest: +sweep/install-links bad-company assertions. 81/81. Units: clamp + reset classification.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 13:59:19 -07:00
5af2fc09ec errorlog: datto-edr scan-endpoint friction (dead routes + tenant-wide footgun)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 13:55:44 -07:00
1e80fb24db datto-edr: fix scan to verified Agents/scan endpoint + harden
- Scan now uses POST Agents/scan with AND-wrapped where {and:[{id:[...]}]}
  (the Infocyte targets/{id}/scan routes are dead/404; bare {id:{inq}} returns
  HTTP 412 ambiguous-column). Verified live: single-agent scan -> 'Scanning 1 host'.
- scan/isolate REQUIRE explicit --agent ids; empty list refused (tenant-wide footgun).
- isolate rides Agents/scan with the Host Isolation extension in options.extensions;
  resolves --extension-name -> id via /Extensions.
- New subcommands: tasks, task, cancel, create-group, mint-key.
- deploy-cmd emits full -URL (not -InstanceName; cname 'azcomp4587' trips the
  install script's .com regex and leaves --url empty).
- Docs (SKILL.md + api-reference.md) rewritten to the verified endpoints + footguns.

Lifecycle verified end-to-end on RMM-TEST-MACHINE (create-group/mint-key/install/
register/scan/cancel).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 13:55:22 -07:00
778e12d83e fix(bitdefender): third-pass - companies pagination, connect-retry, Retry-After ceiling, ctx mgr
Remaining LOW/NIT items from the second review pass:
- list_all_companies() paginates the company list; sweep-all + refresh_inventory
  no longer truncate a >100-company tenant.
- Pre-send connection failures (httpx ConnectError/ConnectTimeout; urllib URLError
  not wrapping a timeout) are now retried as 'connect' - always safe (no side
  effect) even for non-idempotent writes; ambiguous read-timeouts stay idempotent-gated.
- Explicit Retry-After honored up to RETRY_AFTER_MAX_SECONDS (120s) instead of the
  30s exponential cap, so a server-mandated cooldown isn't cut short.
- GravityZoneClient is now a context manager (__enter__/__exit__ -> close()).
- incident-status/note reject an empty --set-json (rc2), matching account-update/notif.
- selftest: +connect/Retry-After/ctx-mgr unit coverage, incident empty-json assertion. 79/79.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 13:52:33 -07:00
a96a15a1d2 sync: auto-sync from HOWARD-HOME at 2026-06-25 13:43:47
Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-06-25 13:43:47
2026-06-25 13:44:18 -07:00
1852f755ad fix(bitdefender): doc LOWs + sweep pagination + selftest alignment
Batched the audit doc/LOW findings plus the two pagination LOWs:
- Pagination (gz_client): security_sweep and refresh_inventory stopped on a
  'total' field some responses omit, truncating after page 1. Now page until a
  short page (< per_page) - the reliable last-page signal.
- isolate/restore docstrings (gz_client): removed the stale 'v1.2 takes an ARRAY
  endpointIds' lines that contradicted the verified single-endpointId code.
- Cache 'no PII' wording corrected (gz_client header + SKILL.md): cache holds infra
  identifiers (hostnames/FQDNs); no secrets. Dead _require_company_for_sweep removed.
- Doc drift fixed: delete-package is '--id <packageId>' not '--package <name>'
  (SKILL.md + api-reference.md, verified live); module docstring + sweep --company
  help corrected (sweep with no --company fans out to ALL companies).
- selftest aligned to the improved behavior: malformed ids now exit rc2 client-side
  (H3) instead of rc1; gate-refusal 'Would' messages now assert on stderr (they
  moved off stdout so --json isn't polluted). 75/75 pass; live sweep verified.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 13:28:38 -07:00
5b3dd84fb9 synology: fix SSH backend syno* CLI resolution (full pre-test verification)
Found during a full command-surface recheck: every privileged SSH recipe
(shares/users/groups/acl) was broken — sudo secure_path drops /usr/syno/{bin,sbin}
so synoshare/synouser/synogroup/synoacltool were "command not found" (non-sudo
plain recipes worked because the admin login PATH has them).

- Inject SYNO_PATH into priv()/plain(); run priv via `sh -c` so operators work.
- synouser/synogroup use `--enum local` (not the invalid `--list`).
- acl quotes the share path (handles spaces, e.g. "Sandra Fish").
- services repointed to Web API (no synoservice on DSM 7.2; synosystemctl has no list-all).

Verified live: all Web API reads, all SSH reads (acl returns real Windows ACEs),
write path (share create/delete), and every destructive command correctly gated.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 13:28:38 -07:00
a7ceb5f793 sync: auto-sync from GURU-5070 at 2026-06-25 12:56:34
Author: Mike Swanson
Machine: GURU-5070
Timestamp: 2026-06-25 12:56:34
2026-06-25 12:57:41 -07:00
4d7508382d fix(bitdefender): atomic cache writes + advisory lock on write-through (M3)
Audit fix M3: _write_cache did a non-atomic CACHE_FILE.write_text and the
write-through helpers did unlocked read-modify-write, so a crash mid-write could
truncate inventory.json and two concurrent gz.py runs could lose an update.
- _write_cache now writes a temp file (fsync) then os.replace() - atomic on the
  same filesystem; a reader/crash can never see a partial file, and a failed
  write leaves the prior cache intact and no .tmp residue.
- Added a best-effort cross-platform advisory lock (_cache_lock) around the
  read-modify-write in _cache_add_group/_cache_add_package; steals a stale lock,
  proceeds unlocked on timeout (a lost update is tolerable, a hang is not).
- Dropped the dead cache.setdefault('companies', ...) line in _cache_add_group.
- Verified: compile clean; unit tests for round-trip, lock acquire/release/steal,
  write-through, temp cleanup on failure, and prior-cache survival.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 12:54:50 -07:00
d4fd71baab sync: auto-sync from HOWARD-HOME at 2026-06-25 12:53:21
Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-06-25 12:53:21
2026-06-25 12:54:04 -07:00
51751e6473 fix(bitdefender): retry 429/5xx/timeout with backoff + reuse one httpx client
Audit fix H2 (+ M2): the live GravityZone tenant is rate-limited and sweeps fan
out one getManagedEndpointDetails per endpoint across every company, which hit a
real HTTP 429 (errorlog 2026-06-21). _post had zero retry and opened a fresh
httpx.Client (new TLS handshake) per request.
- _post now retries 429/500/502/503/504/timeout up to RETRY_MAX_ATTEMPTS with
  bounded exponential backoff + jitter, honoring Retry-After (numeric or HTTP-date).
  Retry notices go to stderr (don't pollute --json). Terminal errors still raise.
- M2: a single httpx.Client is created lazily and reused (connection pooling),
  closed via client.close() in main()'s finally. Makes the docstring's pooling
  claim true and cuts handshake overhead + 429 pressure during sweeps.
- Verified: compile clean; offline unit tests (persistent 429 -> 4 attempts then
  raise, flaky 503 -> recovers, Retry-After honored); live status read OK.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 12:51:08 -07:00
d8f0974e0f fix(bitdefender): gate move/scan/create-package/make-group + validate object IDs
Audit cluster C1/C2/H1/H3/M1 on the live GravityZone tenant:
- C1/H1/M1: move, scan, create-package, make-group called the live API with
  no --confirm; added _gated() + a --confirm flag to each (move can change an
  endpoint's inherited policy posture).
- C2: extend raw's destructive-method denylist with moveEndpoints/moveCustomGroup/
  createScanTask/createPackage/createCustomGroup so 'raw' can't bypass the gates.
- H3: add _require_oid() 24-char-hex validation to endpoint/policy/endpoints +
  the gated handlers, so malformed ids no longer hit the tenant or get mislogged
  as functional errors (source of the 2026-06-21 errorlog noise).
- Gate refusals now print to stderr (don't pollute --json). SKILL.md gating list
  updated. Verified: compile clean; gates exit 3, bad ids exit 2, raw denylist hits.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 12:47:45 -07:00
e9ece35c2a sync: auto-sync from HOWARD-HOME at 2026-06-25 12:45:08
Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-06-25 12:45:08
2026-06-25 12:45:43 -07:00
bd1e84d32c skills: add datto-edr (Datto EDR / Infocyte HUNT) + syncro-rmm memory
New /datto-edr skill — standalone CLI for the Datto EDR REST API (azcomp4587,
rebranded Infocyte HUNT, raw-token LoopBack). Live-verified reads across the whole
MSP fleet: orgs/sites/agents/detections/sweep + deploy-cmd. Scan/isolate gated
behind --confirm (shape-verified, not run against prod). Token vaulted at
msp-tools/datto-edr.sops.yaml.

Also: reference_syncro_rmm_api_gui_only memory (Syncro RMM policy mgmt is
GUI-only) and the guru-rmm submodule pointer bump (Feature 6 EDR research).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 12:39:58 -07:00
e61b39b5c8 sync: auto-sync from GURU-5070 at 2026-06-25 12:35:22
Author: Mike Swanson
Machine: GURU-5070
Timestamp: 2026-06-25 12:35:22
2026-06-25 12:37:54 -07:00
0f803c2d9c sync: auto-sync from HOWARD-HOME at 2026-06-25 12:31:56
Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-06-25 12:31:56
2026-06-25 12:32:31 -07:00
b9d4cfde98 sync: auto-sync from HOWARD-HOME at 2026-06-25 12:30:38
Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-06-25 12:30:38
2026-06-25 12:31:14 -07:00
ca44a005cc wiki: correct SPEC-030 software uninstall to BETA / not guaranteed (Howard)
Code is merged + deployed but the feature is beta and unreliable —
best-effort only, not guaranteed on protected AV, WiX bundles, UI-only
or lingering-child uninstallers, or drivers. Reframe 'SHIPPED' framing
across Summary, Capabilities, Active State, History, and the index.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 12:23:57 -07:00
974be13f4c synology: code-review hardening + trim over-budget description
Addresses 5 verified findings from /code-review high:
- SynoError carries DSM code + handled flag; call() no longer logs eagerly.
  Top-level handler logs only genuine unhandled failures, so the handled
  FileStation denial + VPN-down connect errors stop polluting errorlog.md
  (was a CLAUDE.md rule violation: don't log handled conditions).
- FileStation-denial detection is numeric (code in 400/407), not substring.
- SSH hint now also fires on the generic `call` path, not just `ls`.
- `services` falls back get->list on 103 for older DSM builds (multi-device).
- BrokenPipe flush moved inside try so small piped output can't leak a traceback.
- Trim SKILL description 755->515 chars (was the longest of 32 skills; self-check
  registry-budget WARN).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 12:23:43 -07:00
d4397cbe10 wiki: compile gururmm (full) — SPEC-030 software uninstall + universal installer
Fold delta since 2026-06-22 into the GuruRMM project article:
SPEC-030 remote software inventory + bulk uninstall (engine, three-state
knowledge base, async removal jobs, migrations 061-064), universal
self-detecting installer (Feature 9, v0.6.71); versions to 0.6.75/0.3.87;
flag stale PR #40-#46 migration numbering as [VERIFY].

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 12:17:38 -07:00
929f31cce9 sync: auto-sync from HOWARD-HOME at 2026-06-25 12:00:35
Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-06-25 12:00:35
2026-06-25 12:01:03 -07:00
21ef1f2570 synology: fix --confirm arg position + verify write path live
- Fix argparse: --confirm/--vault were only accepted BEFORE the subcommand, so
  every documented gated-write (e.g. `call X set k=v --confirm`) failed. Moved to
  a shared parent parser (SUPPRESS defaults) -> both flags work in either position.
- Verified the CSRF write path live on cascadesDS: Share create -> verify ->
  delete -> verify gone. Both mutating calls succeeded; device left pristine.
- SKILL.md: write/setter path marked VERIFIED; confirmed share-create signature.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 11:56:32 -07:00
73b957d911 sync: auto-sync from HOWARD-HOME at 2026-06-25 11:45:38
Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-06-25 11:45:38
2026-06-25 11:46:08 -07:00
4a63b583b7 sync: auto-sync from HOWARD-HOME at 2026-06-25 11:42:29
Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-06-25 11:42:29
2026-06-25 11:42:58 -07:00
fc36f98450 synology: full read-surface sweep + FileStation ls graceful fallback
Exercised every Web API + SSH read command live against cascadesDS.
- All reads OK; `ls <folder>` (FileStation list) is 407-denied for the admin
  account on this box (confirmed on-box as SYSTEM_ADMIN) -> now catches the
  400/407 and prints an SSH file-browse hint. `ls` share-roots still works.
- SSH backend (info/df/run + privileged synowebapi) verified.
- Documented MSYS path-mangling of bare `ls /path` arg on Windows.
- SKILL.md: per-command results; flagged write/setter path as not-yet-live.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 11:19:04 -07:00
7173f4dc12 synology: live-verify against cascadesDS, fix services method + pipe handling
First live exercise of the skill against the Cascades DS718+ (DSM 7.2.1, VPN up).
- Fix `services`: SYNO.Core.Service.list 103s on DSM 7.2.1 -> method is `get`.
- Fix `apis | head` BrokenPipeError traceback -> caught, clean exit.
- SKILL.md status PLUMBED -> VERIFIED 2026-06-25 with live device facts.
- wiki/cascades-tucson: add NAS specs, resolve model/RAM/DSM TODO.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 11:08:28 -07:00
384aa6ac43 sync: auto-sync from HOWARD-HOME at 2026-06-24 17:41:57
Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-06-24 17:41:57
2026-06-24 17:42:27 -07:00
994885b2aa sync: auto-sync from HOWARD-HOME at 2026-06-24 17:40:55
Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-06-24 17:40:55
2026-06-24 17:41:25 -07:00
2a1a275511 sync: auto-sync from HOWARD-HOME at 2026-06-24 17:37:00
Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-06-24 17:37:00
2026-06-24 17:37:35 -07:00
9d68db953f sync: auto-sync from HOWARD-HOME at 2026-06-24 15:39:19
Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-06-24 15:39:19
2026-06-24 15:39:54 -07:00
8ddfb33eab sync: auto-sync from HOWARD-HOME at 2026-06-24 15:23:13
Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-06-24 15:23:13
2026-06-24 15:23:42 -07:00
7055ce6acd sync: auto-sync from HOWARD-HOME at 2026-06-24 14:48:56
Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-06-24 14:48:56
2026-06-24 14:49:26 -07:00
855a67d612 sync: auto-sync from HOWARD-HOME at 2026-06-24 13:59:29
Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-06-24 13:59:29
2026-06-24 13:59:58 -07:00
be2ae8b07e sync: auto-sync from HOWARD-HOME at 2026-06-24 12:49:35
Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-06-24 12:49:35
2026-06-24 12:50:03 -07:00
8d6ac5ef6e sync: auto-sync from HOWARD-HOME at 2026-06-24 12:17:43
Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-06-24 12:17:43
2026-06-24 12:18:15 -07:00
5c77b88654 sync: auto-sync from HOWARD-HOME at 2026-06-24 11:50:01
Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-06-24 11:50:01
2026-06-24 11:50:29 -07:00
47c9441781 sync: auto-sync from HOWARD-HOME at 2026-06-24 10:25:39
Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-06-24 10:25:39
2026-06-24 10:26:07 -07:00
0d05c1a4a4 sync: auto-sync from HOWARD-HOME at 2026-06-24 10:21:03
Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-06-24 10:21:03
2026-06-24 10:21:31 -07:00
befd701678 sync: auto-sync from HOWARD-HOME at 2026-06-24 09:27:28
Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-06-24 09:27:28
2026-06-24 09:27:58 -07:00
00115c79f0 sync: auto-sync from HOWARD-HOME at 2026-06-24 08:45:53
Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-06-24 08:45:53
2026-06-24 08:46:20 -07:00
2c9f99e45d sync: auto-sync from GURU-5070 at 2026-06-23 21:37:26
Author: Mike Swanson
Machine: GURU-5070
Timestamp: 2026-06-23 21:37:26
2026-06-23 21:38:25 -07:00
4df2232bbd sync: auto-sync from GURU-5070 at 2026-06-23 21:36:00
Author: Mike Swanson
Machine: GURU-5070
Timestamp: 2026-06-23 21:36:00
2026-06-23 21:37:00 -07:00
7b252335cc sync: auto-sync from GURU-5070 at 2026-06-23 21:14:42
Author: Mike Swanson
Machine: GURU-5070
Timestamp: 2026-06-23 21:14:42
2026-06-23 21:15:42 -07:00
373883fb48 sync: auto-sync from GURU-5070 at 2026-06-23 21:03:04
Author: Mike Swanson
Machine: GURU-5070
Timestamp: 2026-06-23 21:03:04
2026-06-23 21:04:10 -07:00
e175c3d8f1 sync: auto-sync from HOWARD-HOME at 2026-06-23 20:52:42
Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-06-23 20:52:42
2026-06-23 20:53:10 -07:00
529ce8506a sync: auto-sync from HOWARD-HOME at 2026-06-23 20:51:06
Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-06-23 20:51:06
2026-06-23 20:51:38 -07:00
3de8b7fde3 docs(screenconnect skill): clarify end goal — GuruRMM addon, BYO alternative to GuruConnect
Replace the thin "Future: GuruRMM integration" stub with a "Why this skill exists"
section: ScreenConnect surfaces as a per-partner Integrations Center / addons-page
entry, positioned as the bring-your-own alternative to GuruConnect (a partner already
paying for ScreenConnect uses their licensed instance as the remote-access backend).
Points at the mapped plan: SPEC-024, RMM_THOUGHTS Feature 7 + Refinement 7a, the
Integrations Center roadmap item.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 20:51:38 -07:00
30841fbfb1 sync: auto-sync from GURU-5070 at 2026-06-23 20:23:47
Author: Mike Swanson
Machine: GURU-5070
Timestamp: 2026-06-23 20:23:47
2026-06-23 20:25:46 -07:00
ee406308eb sync: auto-sync from GURU-5070 at 2026-06-23 16:38:25
Author: Mike Swanson
Machine: GURU-5070
Timestamp: 2026-06-23 16:38:25
2026-06-23 16:40:15 -07:00
6ce24ce777 sync: auto-sync from HOWARD-HOME at 2026-06-23 16:38:26
Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-06-23 16:38:26
2026-06-23 16:38:56 -07:00
0ce3b5adaa sync: auto-sync from HOWARD-HOME at 2026-06-23 16:20:09
Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-06-23 16:20:09
2026-06-23 16:20:40 -07:00
90eb298797 sync: auto-sync from HOWARD-HOME at 2026-06-23 16:01:47
Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-06-23 16:01:47
2026-06-23 16:02:17 -07:00
405832d049 sync: auto-sync from GURU-BEAST-ROG at 2026-06-23 15:56:27
Author: Mike Swanson
Machine: GURU-BEAST-ROG
Timestamp: 2026-06-23 15:56:27
2026-06-23 15:56:46 -07:00
350c251513 sync: auto-sync from GURU-BEAST-ROG at 2026-06-23 15:54:03
Author: Mike Swanson
Machine: GURU-BEAST-ROG
Timestamp: 2026-06-23 15:54:03
2026-06-23 15:54:23 -07:00
eb73f9cd32 sync: auto-sync from GURU-BEAST-ROG at 2026-06-23 15:45:37
Author: Mike Swanson
Machine: GURU-BEAST-ROG
Timestamp: 2026-06-23 15:45:37
2026-06-23 15:45:57 -07:00
cd806da576 sync: auto-sync from HOWARD-HOME at 2026-06-23 15:28:30
Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-06-23 15:28:30
2026-06-23 15:29:01 -07:00
a7e7fb1f16 Add AMPIPIT as submodule (projects/msp-tools/ampipit)
Migrate Howard's AMPIPIT toolkit into the fleet as a private Gitea
submodule (azcomputerguru/ampipit), matching the guru-rmm pattern.
Full history (49 commits + tags) pushed to Gitea and verified before
integration. Tracks main.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 15:29:01 -07:00
3083e4b579 sync: auto-sync from GURU-BEAST-ROG at 2026-06-23 15:09:44
Author: Mike Swanson
Machine: GURU-BEAST-ROG
Timestamp: 2026-06-23 15:09:44
2026-06-23 15:10:06 -07:00
0171107d41 sync: auto-sync from GURU-5070 at 2026-06-23 12:10:19
Author: Mike Swanson
Machine: GURU-5070
Timestamp: 2026-06-23 12:10:19
2026-06-23 12:11:12 -07:00
a28b52da9a wiki: compile valleywide (full) - SMB1/Orders-XP fix, billing-note correction, hours 15.5
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 10:16:40 -07:00
045b50fefa sync: auto-sync from GURU-5070 at 2026-06-23 09:59:34
Author: Mike Swanson
Machine: GURU-5070
Timestamp: 2026-06-23 09:59:34
2026-06-23 10:00:22 -07:00
d36aa14be8 chore: advance guru-rmm submodule pin -> de30ebc (Task 8 script-path verified on-box)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 08:24:52 -07:00
0b0c9ba3bd sync: auto-sync from GURU-5070 at 2026-06-23 08:17:44
Author: Mike Swanson
Machine: GURU-5070
Timestamp: 2026-06-23 08:17:44
2026-06-23 08:18:34 -07:00
1bc6c40d05 chore: advance guru-rmm submodule pin -> 53bb682 (dashboard install UI -> universal installer)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 08:12:43 -07:00
f8f57d7e37 chore: advance guru-rmm submodule pin -> 30e14d8 (universal installer P1 + 29 upstream)
Pin was stale at 2e469f1b (~30 commits behind main). Advances to current origin/main
which includes the universal self-detecting installer (Feature 9, v0.6.71) and upstream work.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 08:05:04 -07:00
5f30e1154a sync: auto-sync from GURU-5070 at 2026-06-23 07:57:32
Author: Mike Swanson
Machine: GURU-5070
Timestamp: 2026-06-23 07:57:32
2026-06-23 07:59:40 -07:00
3e414c1572 wiki: compile cascades-tucson (full) — 2026-06-23 planned power outage
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 05:45:34 -07:00
beb8a36ff2 wiki: compile dataforth (full) — fold in Phase 2 shares target-state strawman
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 05:42:40 -07:00
da411b0f48 sync: auto-sync from HOWARD-HOME at 2026-06-23 05:36:49
Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-06-23 05:36:49
2026-06-23 05:37:43 -07:00
7fb32ba349 sync: auto-sync from HOWARD-HOME at 2026-06-23 05:36:06
Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-06-23 05:36:06
2026-06-23 05:36:43 -07:00
56bf6f44ff sync: auto-sync from GURU-5070 at 2026-06-22 20:00:56
Author: Mike Swanson
Machine: GURU-5070
Timestamp: 2026-06-22 20:00:56
2026-06-22 20:01:48 -07:00
48b6c94b4a sync: auto-sync from HOWARD-HOME at 2026-06-22 19:15:51
Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-06-22 19:15:51
2026-06-22 19:16:22 -07:00
86c789a7f9 sync: auto-sync from HOWARD-HOME at 2026-06-22 18:54:25
Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-06-22 18:54:25
2026-06-22 18:55:00 -07:00
bec21647d4 sync: auto-sync from Mikes-MacBook-Air.local at 2026-06-22 16:51:30
Author: Mike Swanson
Machine: Mikes-MacBook-Air.local
Timestamp: 2026-06-22 16:51:30
2026-06-22 16:51:55 -07:00
7ad4353fd4 docs(memory): sync.sh Phase-3 submodule-clobber fixed; branch work now survives
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 14:31:32 -07:00
9108f9419c fix(sync): never reset a submodule with local work (branch/dirty)
Phase-3 post-rebase reconcile ran 'git submodule update --init --recursive'
unconditionally, force-detaching every submodule to the parent's pinned gitlink
and discarding any feature branch, commits, or uncommitted edits inside it. The
Phase-1a init guard did not cover this path. New submodule_update_safe() advances
ONLY submodules in the pristine pinned state (clean, detached HEAD) and skips any
on a branch or with uncommitted changes, so in-progress submodule work survives a
parent sync.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 14:30:38 -07:00
26aa5034f1 sync: auto-sync from HOWARD-HOME at 2026-06-22 14:04:53
Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-06-22 14:04:53
2026-06-22 14:05:26 -07:00
48286e80e0 sync: auto-sync from GURU-5070 at 2026-06-22 13:18:48
Author: Mike Swanson
Machine: GURU-5070
Timestamp: 2026-06-22 13:18:48
2026-06-22 13:19:47 -07:00
8225ec7a9b sync: auto-sync from HOWARD-HOME at 2026-06-22 10:36:17
Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-06-22 10:36:17
2026-06-22 10:36:51 -07:00
82aabacaca sync: auto-sync from Mikes-MacBook-Air.local at 2026-06-22 10:20:27
Author: Mike Swanson
Machine: Mikes-MacBook-Air.local
Timestamp: 2026-06-22 10:20:27
2026-06-22 10:20:32 -07:00
a21d24f09f wiki: compile packetdial (seed) — ACG VoIP system (PacketDial/NetSapiens/OIT + Yealink YMCS)
New systems article covering the full VoIP stack: vendor model (PacketDial brand / NetSapiens
platform / OIT wholesaler / YMCS phones), the PBX API + packetdial skill (reads + gated writes +
onboard-domain), the Yealink/YMCS side + yealink-ymcs skill (one ACG key -> all client sites, RPS),
the onboarding pipeline, vault paths, and known gotchas. Sources: the 3 06-22 session logs + both
skill docs + the vendor-stack memory. Indexed under Systems.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 10:13:00 -07:00
7f64f169ca sync: auto-sync from GURU-5070 at 2026-06-22 10:03:54
Author: Mike Swanson
Machine: GURU-5070
Timestamp: 2026-06-22 10:03:54
2026-06-22 10:04:41 -07:00
850e685d8d feat: yealink-ymcs skill — YMCS v2 device-management API, pairs with packetdial
New skill to manage ACG's Yealink phone fleets via Yealink Management Cloud Service v2
(us-api.ymcs.yealink.com). RTFM'd the API (token auth via POST /v2/token Basic+bearer, NOT the
legacy RPS HMAC; legacy-TLS renegotiation required) + endpoint map from the dszp/n8n-nodes-
yealinkymcs community node. Live-verified: token auth, sites (one ACG AccessKey sees ALL client
sites — VWP/GuruHQ/Ace Pick Up Parks as children of the ACG parent), devices, accounts,
rps-servers (RPS = "WL - ACG" ftp://p.packetdials.net). Gated writes (--confirm): add-devices-by-mac,
add-sipaccount (push a NetSapiens SIP cred onto a phone = the PBX glue), reboot, reset, rps add/del;
+ raw passthrough (auto-recovers the MSYS /v2 path-mangling). Creds vaulted at
services/yealink-ymcs.sops.yaml. Pairs with packetdial onboard-domain for new-client phone
provisioning; VWP is the live pilot. Honest [V]/[P] verification status in SKILL.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 09:59:52 -07:00
1dbefd5457 sync: auto-sync from GURU-5070 at 2026-06-22 09:44:14
Author: Mike Swanson
Machine: GURU-5070
Timestamp: 2026-06-22 09:44:14
2026-06-22 09:45:01 -07:00
d1d1302d55 packetdial: add onboard-domain wrapper (GUI Add-a-Domain -> 3-call API flow)
onboard-domain runs POST /domains -> addresses/validate (gen E911 pidflo) -> addresses/create
from one JSON body (domain fields + optional `emergency` block), gated --confirm. Reverse-
engineered from the OITVOIP wizard screenshots; live-created the real client domain
vwp.91912.service (Valley Wide Plastering) + E911 address, and proved the wrapper with a
throwaway create->delete (no leftovers, vwp intact). Documented GUI->API mapping + the two
manual gaps (voicemail user-defaults, email-send-from-address pending the packetdial.com mailbox)
+ the domain-type "no"-on-create quirk.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 09:34:48 -07:00
4157fc6f1d sync: auto-sync from GURU-5070 at 2026-06-22 09:03:37
Author: Mike Swanson
Machine: GURU-5070
Timestamp: 2026-06-22 09:03:37
2026-06-22 09:04:24 -07:00
f16a1a7084 sync: auto-sync from GURU-5070 at 2026-06-22 08:23:12
Author: Mike Swanson
Machine: GURU-5070
Timestamp: 2026-06-22 08:23:12
2026-06-22 08:24:06 -07:00
4a8559fa73 sync: auto-sync from HOWARD-HOME at 2026-06-22 08:20:28
Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-06-22 08:20:28
2026-06-22 08:23:10 -07:00
27cafabb22 sync: auto-sync from GURU-5070 at 2026-06-22 08:05:07
Author: Mike Swanson
Machine: GURU-5070
Timestamp: 2026-06-22 08:05:07
2026-06-22 08:05:52 -07:00
1062a5ed44 packetdial: finish resource wrapping (reads + gated writes across the platform)
Added read wrappers: addresses (E911), smsnumbers, blocked-numbers, moh, dialrules,
recording, transcriptions. Added gated write wrappers: DID update/delete, per-user device
CRUD, E911 address CRUD, contact CRUD, site create/update, auto-attendant create, SMS
number CRUD, block/unblock numbers, MoH TTS create/delete.

Verification: contact create→delete lifecycle verified on arizonacomputerguru (id field is
`unique-id`); reads for addresses/blocked-numbers/moh verified. Remaining writes are plumbed
per the OpenAPI spec [P] but not lifecycle-verified (test domain lacks the feature or needs a
special body) — SKILL.md marks each [V]/[P] and documents the gotchas (E911 pidflo via
addresses/validate; SMS not provisioned on test domain; number-filters add 202'd but didn't
persist; MoH file upload is multipart -> raw). Capability map + api.md history updated.
All writes --confirm-gated; anything unwrapped still reachable via `raw`.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 08:05:06 -07:00
fec8b364d6 sync: auto-sync from GURU-5070 at 2026-06-22 07:44:31
Author: Mike Swanson
Machine: GURU-5070
Timestamp: 2026-06-22 07:44:31
2026-06-22 07:45:19 -07:00
f42f9c2419 packetdial: add gated call-queue + time-frame edit wrappers (live-verified)
Added write wrappers, each tested create→update→delete on the arizonacomputerguru test
domain (sanctioned, non-production):
- call queues: create-callqueue, update-callqueue, delete-callqueue + add-agent /
  update-agent / remove-agent
- time frames: create-timeframe, update-timeframe, delete-timeframe (body-discriminated —
  same path, server selects the op from the body; wrappers pass --body verbatim)

All behind --confirm (gate verified: DRY RUN refuses without it). SKILL.md documents the
bodies + the days-of-week-needs-array gotcha + names ACG as the test bed; capability map
+ api.md history updated. No production objects touched; no test leftovers.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 07:44:30 -07:00
25b2ff45bf sync: auto-sync from GURU-5070 at 2026-06-22 07:36:13
Author: Mike Swanson
Machine: GURU-5070
Timestamp: 2026-06-22 07:36:13
2026-06-22 07:37:01 -07:00
d75c367bf7 packetdial: build out + document the skill against the live NetSapiens v2 API
Key is now provisioned + live-verified, so grounded the skill in the real spec (RTFM):
- Mapped the OpenAPI surface (v44.4.10, 239 paths / 354 ops) — capability map added to
  SKILL.md (what the platform exposes vs what's wrapped vs raw-only).
- Added 6 live-verified read wrappers (ns.py + ns_client.py): callqueues, timeframes,
  sites, contacts, autoattendants, billing (domain limits/usage).
- Replaced the stale "not yet provisioned" credentials section with the live status
  (vaulted nsr_ reseller key, key-id nsr_hSGUB5Wo, scope Reseller 91912.service, RW) +
  the pbx.packetdial.com vs api.ucaasnetwork.com hostname note + override.
- api.md history updated. Writes remain gated behind --confirm; everything unwrapped
  reachable via `raw`.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 07:36:12 -07:00
567986fa49 sync: auto-sync from HOWARD-HOME at 2026-06-21 21:42:44
Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-06-21 21:42:44
2026-06-21 21:43:11 -07:00
28af952343 sync: auto-sync from HOWARD-HOME at 2026-06-21 21:26:00
Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-06-21 21:26:00
2026-06-21 21:26:26 -07:00
b977f5e182 wiki: compile gururmm (full) — fold in 06-12..06-22 work (BUG-018/021/022, Event Log Watch UI, two-wave build, watchdog REST-only)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 21:15:24 -07:00
f6f0421ff3 sync: auto-sync from HOWARD-HOME at 2026-06-21 21:01:39
Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-06-21 21:01:39
2026-06-21 21:02:05 -07:00
8713b46d3f chore: advance security-assessment pin (0f6927b) — onboarding/upsell questions + opportunity engine + quote delete 2026-06-21 21:00:26 -07:00
8fde10c743 sync: auto-sync from HOWARD-HOME at 2026-06-21 20:56:44
Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-06-21 20:56:44
2026-06-21 20:57:10 -07:00
924fa39b34 sync: auto-sync from HOWARD-HOME at 2026-06-21 20:44:08
Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-06-21 20:44:08
2026-06-21 20:44:34 -07:00
3a1edb721b screenconnect: finalize skill — clean stale 'pending unlock' help text
The control methods (send-command/send-message/set-properties) and session
detail are verified live on the ACG instance; the "pending unlock" help text
was left over from before probing confirmed them. Skill validated against
skill-creator rules (frontmatter, vault creds, gated writes, errorlog
compliance, ASCII, selftest 12/12).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 20:42:22 -07:00
87688b65da sync: auto-sync from HOWARD-HOME at 2026-06-21 20:36:14
Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-06-21 20:36:14
2026-06-21 20:36:41 -07:00
231fcb6d6f chore: advance security-assessment pin (7a3dfb8) — Save/quote button + status column 2026-06-21 19:51:34 -07:00
44cddc5020 sync: auto-sync from GURU-5070 at 2026-06-21 19:31:11
Author: Mike Swanson
Machine: GURU-5070
Timestamp: 2026-06-21 19:31:11
2026-06-21 19:33:39 -07:00
f4296f2d9e sync: auto-sync from HOWARD-HOME at 2026-06-21 19:32:30
Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-06-21 19:32:30
2026-06-21 19:32:57 -07:00
5b44a3e619 chore: advance security-assessment pin (a9c85a7) — + New/clear button 2026-06-21 19:17:08 -07:00
c462f50152 chore: advance security-assessment pin (ef03b4e) — lookup phone-match fix (deployed) 2026-06-21 19:08:10 -07:00
f90d753d13 sync: auto-sync from HOWARD-HOME at 2026-06-21 18:54:05
Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-06-21 18:54:05
2026-06-21 18:54:31 -07:00
5299f3d32e chore: advance security-assessment pin (31bc786) — deploy/opcache notes 2026-06-21 18:44:34 -07:00
5a0aaa2ff2 sync: auto-sync from HOWARD-HOME at 2026-06-21 18:27:49
Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-06-21 18:27:49
2026-06-21 18:28:14 -07:00
27c1d972b3 chore: advance security-assessment pin (f246091) — api.php allow-list fix (deployed live) 2026-06-21 18:24:55 -07:00
28f95f0d72 sync: auto-sync from HOWARD-HOME at 2026-06-21 18:18:26
Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-06-21 18:18:26
2026-06-21 18:18:56 -07:00
0c3ae5d33d sync: auto-sync from GURU-5070 at 2026-06-21 18:11:16
Author: Mike Swanson
Machine: GURU-5070
Timestamp: 2026-06-21 18:11:16
2026-06-21 18:12:10 -07:00
2a7dd2d4a0 chore: advance security-assessment pin (3e3a9ab) — export exec summary 2026-06-21 18:11:42 -07:00
497 changed files with 114447 additions and 2275 deletions

36
.analyze-glaztech.py Normal file
View File

@@ -0,0 +1,36 @@
import json
from collections import defaultdict
with open('/Users/azcomputerguru/ClaudeTools/.glaztech-sessions.json') as f:
data = json.load(f)
print(f'Total machines: {len(data)}')
# Extract IPs and current sites
subnet_info = defaultdict(lambda: {'count': 0, 'machines': [], 'current_sites': set()})
for session in data:
ip = session.get('GuestInfo', {}).get('PrivateNetworkAddress', '')
name = session.get('Name', 'Unknown')
current_site = session.get('CustomProperties', {}).get('CustomProperty2', '')
if ip and ip != '':
# Extract /24 subnet
subnet = '.'.join(ip.split('.')[:3]) + '.0/24'
subnet_info[subnet]['count'] += 1
subnet_info[subnet]['machines'].append({'name': name, 'ip': ip})
if current_site:
subnet_info[subnet]['current_sites'].add(current_site)
# Sort by count descending
sorted_subnets = sorted(subnet_info.items(), key=lambda x: x[1]['count'], reverse=True)
print('\n=== Subnet Distribution ===')
for subnet, info in sorted_subnets:
sites_str = ', '.join(sorted(info['current_sites'])) if info['current_sites'] else 'No site tag'
print(f'{subnet:20s} {info["count"]:3d} machines Current tags: {sites_str}')
# Show first 3 machines as examples
for m in info['machines'][:3]:
print(f' - {m["name"]:30s} {m["ip"]}')
if len(info['machines']) > 3:
print(f' ... and {len(info["machines"]) - 3} more')

2310
.bb-sp-changes.json Normal file

File diff suppressed because it is too large Load Diff

1
.bb-tok Normal file
View File

@@ -0,0 +1 @@
eyJ0eXAiOiJKV1QiLCJub25jZSI6IlNzRUhBcVJSVzVzOFYwTzBaVVBUdnZGZjRnVXl2YmRvNjFiVklhYjJNbDgiLCJhbGciOiJSUzI1NiIsIng1dCI6ImFGa21LVkZjLTRXVjZzWENCdk5aa1hJNTA1WSIsImtpZCI6ImFGa21LVkZjLTRXVjZzWENCdk5aa1hJNTA1WSJ9.eyJhdWQiOiJodHRwczovL2dyYXBoLm1pY3Jvc29mdC5jb20iLCJpc3MiOiJodHRwczovL3N0cy53aW5kb3dzLm5ldC8xOWE1NjhlOC05ZTg4LTQxM2ItOTM0MS1jYmMyMjRiMzkxNDUvIiwiaWF0IjoxNzgzMDQ4NDQwLCJuYmYiOjE3ODMwNDg0NDAsImV4cCI6MTc4MzA1MjM0MCwiYWNycyI6WyJwZmRyIl0sImFpbyI6ImsyRmdZQWhtcnMxdW5yZlo2TmVTNUxmbDNzNkZNNXA0YnNoN1RuNGhmc1hJZkhWYjkzMEEiLCJhcHBfZGlzcGxheW5hbWUiOiJDb21wdXRlckd1cnUgLSBUZW5hbnQgQWRtaW4iLCJhcHBpZCI6IjcwOWU2ZWVkLTA3MTEtNDg3NS05YzQ0LTJkMzUxOGM0NzA2MyIsImFwcGlkYWNyIjoiMiIsImlkcCI6Imh0dHBzOi8vc3RzLndpbmRvd3MubmV0LzE5YTU2OGU4LTllODgtNDEzYi05MzQxLWNiYzIyNGIzOTE0NS8iLCJpZHR5cCI6ImFwcCIsIm9pZCI6IjdhMTk5YjExLTk3ZmItNGU2NS05MTdkLWY4ZDI5YTUzYmE0OSIsInJoIjoiMS5BVVlBNkdpbEdZaWVPMEdUUWN2Q0pMT1JSUU1BQUFBQUFBQUF3QUFBQUFBQUFBQUFBQUJHQUEuIiwicm9sZXMiOlsiUG9saWN5LlJlYWRXcml0ZS5Db25kaXRpb25hbEFjY2VzcyIsIlVzZXIuUmVhZFdyaXRlLkFsbCIsIlNlY3VyaXR5RXZlbnRzLlJlYWQuQWxsIiwiVXNlckF1dGhlbnRpY2F0aW9uTWV0aG9kLlJlYWRXcml0ZS5BbGwiLCJBcHBsaWNhdGlvbi5SZWFkV3JpdGUuQWxsIiwiRGlyZWN0b3J5LlJlYWRXcml0ZS5BbGwiLCJTaXRlcy5SZWFkV3JpdGUuQWxsIiwiQXBwUm9sZUFzc2lnbm1lbnQuUmVhZFdyaXRlLkFsbCIsIlJvbGVNYW5hZ2VtZW50LlJlYWRXcml0ZS5EaXJlY3RvcnkiLCJQb2xpY3kuUmVhZC5BbGwiLCJTaXRlcy5GdWxsQ29udHJvbC5BbGwiXSwic3ViIjoiN2ExOTliMTEtOTdmYi00ZTY1LTkxN2QtZjhkMjlhNTNiYTQ5IiwidGVuYW50X3JlZ2lvbl9zY29wZSI6Ik5BIiwidGlkIjoiMTlhNTY4ZTgtOWU4OC00MTNiLTkzNDEtY2JjMjI0YjM5MTQ1IiwidXRpIjoiRFE2UE4wWlRqMEdSMDhWeEhhUWJBQSIsInZlciI6IjEuMCIsIndpZHMiOlsiYjFiZTFjM2UtYjY1ZC00ZjE5LTg0MjctZjZmYTBkOTdmZWI5IiwiMDk5N2ExZDAtMGQxZC00YWNiLWI0MDgtZDVjYTczMTIxZTkwIl0sInhtc19hY2QiOjE3NzY3MDg0MDEsInhtc19hY3RfZmN0IjoiOSAzIiwieG1zX2Z0ZCI6IjZuYTJJRXF0MWliaVd5emFrbzdzM0VHbEdnbm1JRlBUSjdFWUZTcEZPcjBCZFhOemIzVjBhQzFrYzIxeiIsInhtc19pZHJlbCI6IjEwIDciLCJ4bXNfcGZ0ZXhwIjoxNzgzMTM4NzQwLCJ4bXNfcmQiOiIwLjQyTGxZQkppZEJNUzRXQVZFcEJ6U3p5M29MSEZaN2RYdGFuWExOTmNJUkVPVGlHQnRJOWVVdy1lZXVrNVFldzVjLU4ybHM5Q0lod2NRZ0xNREJCd0FFb0xpWEJ3Q3dsc2VTRDdOZHpmN3VRaVNha1duNWF0QWdBIiwieG1zX3N1Yl9mY3QiOiIzIDkiLCJ4bXNfdGNkdCI6MTU4MTAyMjkyNywieG1zX3RudF9mY3QiOiIzIDE0In0.Rfz02nCkcm5iJ19EQVEFhZEcBf_xdd6kXTck-paz5UVgRhSWE8TDPfF9eliB38OZ59bRJdkY6TdtUNrlAAHhJxHs8Hxq6vyYJwKAhzfD-XYntD00Mas1yEe1pobc4vjLPrNYl5ie2KIZvSpmvUc97Lvf5juRueRbvUh9k9QgVwKfGBCHVXirBv9Bekhx4XUraskE-UvROmDvf5mi1DtkskMIC4-Xutz6gFTbJDj-uwTtI2vZa9dQMsVJfMpmq6pWD5lNrIm1tk6LMMIBVA00h3ue5hIt7v5IggvFFqk2JlacMnElQPNkgpwaMJTdcwrVEuonVZuKpv3iYZJU8MtCcg

10
.bk.ps1 Normal file
View File

@@ -0,0 +1,10 @@
$b='C:\ProgramData\acg-backup'
'--- COMPLETE.txt ---'
if (Test-Path (Join-Path $b 'COMPLETE.txt')) { Get-Content (Join-Path $b 'COMPLETE.txt') } else { 'absent' }
'--- dir listing ---'
Get-ChildItem $b | Select-Object Name,Length,LastWriteTime | Format-Table -AutoSize | Out-String
'--- rclone.log tail ---'
Get-Content (Join-Path $b 'rclone.log') -Tail 25
'--- rclone process ---'
$p = Get-Process rclone -ErrorAction SilentlyContinue
if ($p) { 'RUNNING pid=' + $p.Id } else { 'not running' }

7
.chk.ps1 Normal file
View File

@@ -0,0 +1,7 @@
Import-Module BitsTransfer
$j = Get-BitsTransfer -Name 'Win11ARM64ISO' -AllUsers -ErrorAction SilentlyContinue
if ($j) { 'state=' + $j.JobState + ' bytes=' + $j.BytesTransferred + '/' + $j.BytesTotal }
else { 'NO JOB' }
$f = Get-Item 'C:\Users\howar\Desktop\Win11_25H2_English_Arm64_v2.iso' -ErrorAction SilentlyContinue
if ($f) { 'file present size=' + $f.Length }
'uptime-min: ' + [math]::Round(((Get-Date) - (Get-CimInstance Win32_OperatingSystem).LastBootUpTime).TotalMinutes,0)

8
.chk2.ps1 Normal file
View File

@@ -0,0 +1,8 @@
Import-Module BitsTransfer
'--- all BITS jobs (AllUsers) ---'
Get-BitsTransfer -AllUsers -ErrorAction SilentlyContinue | Select-Object DisplayName,JobState,BytesTransferred,BytesTotal,OwnerAccount | Format-Table -AutoSize | Out-String
'--- desktop files ---'
Get-ChildItem 'C:\Users\howar\Desktop' -Filter '*.iso*' -Force -ErrorAction SilentlyContinue | Select-Object Name,Length,LastWriteTime | Format-Table -AutoSize | Out-String
'--- tmp BITS file? ---'
Get-ChildItem 'C:\Users\howar\Desktop' -Filter 'BIT*' -Force -Hidden -ErrorAction SilentlyContinue | Select-Object Name,Length | Format-Table -AutoSize | Out-String
'uptime-min: ' + [math]::Round(((Get-Date) - (Get-CimInstance Win32_OperatingSystem).LastBootUpTime).TotalMinutes,0)

View File

@@ -30,7 +30,36 @@ failures, production risk). Bump one tier for: security, auth, credential, migra
production, data-loss. Detail: EXTENDED + `.claude/OLLAMA.md`. production, data-loss. Detail: EXTENDED + `.claude/OLLAMA.md`.
## Key rules (always) ## Key rules (always)
- **Simplest solution FIRST.** Start with the least complex thing that could answer the
problem, confirm it works, and only THEN graduate to more advanced/complicated approaches
as the simple one proves insufficient. Don't reach for SSH/plink/multi-hop tooling when a
one-shot query (a single DNS lookup, one API call, one command) would answer it. E.g. "what
IP does the UDM think X is?" = `nslookup X <udm-ip>`, not shelling into the device. Escalate
deliberately, not by default.
- **NO EMOJIS.** Use ASCII markers: `[OK]` `[ERROR]` `[WARNING]` `[INFO]` `[CRITICAL]`. - **NO EMOJIS.** Use ASCII markers: `[OK]` `[ERROR]` `[WARNING]` `[INFO]` `[CRITICAL]`.
- **Skill-first — if a skill/command covers the task, USE IT; never hand-roll the API.**
When a request maps to an installed skill or slash-command, INVOKE THAT SKILL instead of
improvising raw `curl`/API calls from memory. The skill encodes the correct payload shape,
validation, attribution, and preview gates; free-handing the API is exactly how malformed
records (e.g. Syncro tickets/invoices) reach a human for cleanup. **Syncro billing/invoicing
ALWAYS runs through `/syncro` (or `/syncro-emergency-billing`) — no exceptions.** Same for
other covered domains: credentials → `vault`, RMM actions → `/rmm` (+ `rmm-search` to find a
host), M365 → `remediation-tool`, asking a human a question/decision in Discord (get their
reply back in-session) → `ask-forum` (never hand-roll the Discord API — that footgun cost us
a broken background wait), etc. Knowing the API is NOT a reason to bypass the skill —
the memory rules (e.g. [[feedback_syncro_billing]]) describe what the SKILL does, not a license
to free-hand it. Reach for raw API ONLY when no skill fits or the skill genuinely cannot do it
— and say so explicitly when you do. Mistakes here go to `errorlog.md` (`--correction`).
- **Definition of Done — route the REQUEST to a doing-skill, then GATE the result with the
matching check-skill(s) before calling work done — automatically, without being asked.**
You map the request to skills from the conversation + expected end result; the user should not
have to name skill A/B/C. Standing gates (run the check-skill BEFORE declaring done): code edits
`/code-review` + `/simplify` (+ `/security-review` if it touches auth/creds/security); anything
outbound/client-facing → `impeccable` / `stop-slop`; Syncro/vendor work → its skill's own
preview+verify gate; wiki/memory writes → their lint (`wiki-lint`/`memory-dream`). **Calibrate to
stakes** (a typo fix doesn't need a full review) and **use Ollama Tier-0** for the cheap
classify/prose passes so this stays low-token. The full request→doing-skill→check-skill map is
`.claude/SKILL_ROUTING.md` — read it on demand when a request maps to a skill or work nears done.
- **Credentials — capture, vault, document (ALWAYS).** ANY credential that surfaces in a - **Credentials — capture, vault, document (ALWAYS).** ANY credential that surfaces in a
session — one the user pastes, one you create/rotate, one you discover in a log/config — you session — one the user pastes, one you create/rotate, one you discover in a log/config — you
MUST immediately store it in the SOPS vault **via the `vault` skill** (the canonical path — MUST immediately store it in the SOPS vault **via the `vault` skill** (the canonical path —

1
.claude/MEMORY.md Normal file
View File

@@ -0,0 +1 @@
- [PS5.1 -Headers $H empty-auth quirk](.claude/memory/ps51_invoke_restmethod_headers_variable.md) - build Graph auth headers inline per call under RMM powershell -File; capture Graph error bodies

104
.claude/SKILL_ROUTING.md Normal file
View File

@@ -0,0 +1,104 @@
# Skill Routing Map
> On-demand reference for the CORE **Skill-first** + **Definition of Done** rules
> (`.claude/CLAUDE.md`). Read this when a request maps to a skill, or when work nears "done"
> and needs its check-skill. You infer the right skills from the request + expected end result —
> the user should NOT have to name them. **Calibrate to stakes** (a typo fix needs no full review)
> and **use Ollama Tier-0** for the cheap classify/prose passes so this stays low-token.
## How to use this (every substantive request)
1. **Route the action** → pick the *doing-skill* for what's being asked (tables below).
2. **Do the work** through that skill (not hand-rolled API).
3. **Gate the result** → before saying "done," run the *check-skill(s)* matching the work-type.
4. Only declare done after the gate passes. If you skip a gate for stakes reasons, say so.
---
## Doing-skills (request → the skill that DOES it)
| Request signal | Skill |
|---|---|
| Ticketing, billing, invoicing, customers, scheduling, appointments | `/syncro` (after-hours/emergency billing → `/syncro-emergency-billing`). Autotask ONLY on an explicit "in Autotask" request. |
| Any credential — read/store/rotate a secret, API key, password, token, SSH key | `vault` |
| Run a command / investigate / script on an RMM agent | `/rmm` (find the host first with `rmm-search`) |
| M365 investigation/remediation, breach check, mailbox/inbox-rule/forwarding audit, tenant sweep | `remediation-tool` |
| Onboard a new M365 tenant to the remediation app suite | `onboard365` |
| Anything about a customer's **BACKUP** — is X backing up, who backs up where, backup status/failures, or set up / provision a backup | **Backups → wiki-first** (see the "Backups" block right below this table) |
| Bitdefender / GravityZone AV — endpoints, sweeps, policies, quarantine, EDR | `bitdefender` |
| Datto EDR / Datto AV — detections, isolate, scan, agent deploy | `datto-edr` |
| VoIP — PacketDial / OITVOIP / NetSapiens domains, users, DIDs, queues, CDRs | `packetdial` |
| Email security — Mailprotector/CloudFilter held/quarantined mail, allow/block rules | `mailprotector` |
| ScreenConnect / CW Control sessions, access installer, backstage command | `screenconnect` |
| Synology NAS | `synology` |
| Yealink phone device management | `yealink-ymcs` |
| UniFi WiFi tuning / RF / channel analysis | `unifi-wifi` |
| Map/repoint a Windows network drive on a remote endpoint | `drive-map` |
| Send someone a Discord DM / copy-paste-friendly link or command | `discord-dm` |
| Ask a teammate a question/decision/sign-off and get their human answer back in-session | `ask-forum` (run the `--wait` as a proper background task — NO shell `&`) |
| Inter-session messaging, fleet todos, resource locks, coord status | `coord` |
| Git / Gitea / submodules / sync health | `gitea` (or `/sync`, `/scc`, `/save` for session ops) |
| Build/verify GuruRMM agent/server/dashboard, pre-merge check | `gururmm-build` |
| Capture an RMM feature idea | `/feature-request`; GuruConnect feature → `gc-feature-request` |
| Compile session logs/Syncro into wiki | `/wiki-compile` |
| Create a new skill or slash command | `skill-creator` |
| Independent 2nd opinion / adversarial verify | `grok` (xAI) or `agy` (Gemini) |
| Image/video gen, live web/X search past cutoff | `grok` |
| Deep multi-source researched report | `deep-research` |
> Not listed? If no skill fits, do it directly — and say so. New recurring need → `skill-creator`.
### Backups (wiki-first routing)
A customer can back up through **any** of several backends, so a backup request is a
two-step route: **first find out which backend THIS customer uses, then use that skill.**
Never assume B2/MSP360 — several clients are on Seafile, ownCloud, or Datto Workplace.
1. **Identify the backend for this customer (don't guess).** Read the client wiki
(`wiki/clients/<slug>.md`) and the backup map (`projects/gps-rmm-audit/backup-map.md`) —
both record each customer's real destination. If it isn't recorded, confirm with
`msp360 monitoring --company <name>` plus the per-backend inventory skills, then write the
answer back to the client wiki.
2. **Route to that backend's skill:**
| Need | Skill |
|---|---|
| Is X backing up? status / last run / failures / stale plans (MSP360 + B2 Cloud plans) | `msp360`**authoritative**, check here not bucket contents |
| Who backs up to Seafile / ownCloud / Datto Workplace (inventory + endpoint detection) | `seafile` / `owncloud` / `datto-workplace` |
| Storage / bucket / cost on the destination side | `b2` (Backblaze) |
| **Set up / provision a backup** — create account/user, quota, library, license, destination | the **target backend's** skill: `msp360` (MBS user/company/license), `seafile`, `owncloud` (all writes gated `--confirm`); `b2` for a new bucket/key |
- **Which machine gets backed up is a per-client decision** — report mismatches (billed vs
running), don't provision jobs off a billing count. See [[feedback_backup_targets_never_guessed]].
- Full per-customer destination + health map: `projects/gps-rmm-audit/backup-map.md`;
how backup-status works: memory `[[reference_msp360_backup_monitoring]]`.
---
## Check-skills (work-type → the gate run BEFORE "done")
**Code changes** (any edit to source):
- `/code-review` — correctness/bugs. `/simplify` — reuse/cleanup. Scale to stakes.
- `/security-review` — REQUIRED when the change touches auth, credentials, security, or production risk.
- `test-driven-development` — when implementing a feature/bugfix (test first). `/verify` or `run` — confirm real behavior, not just tests.
- `rust-skills` — Rust. `inject-standards` — apply project coding standards. `human-flow`/`impeccable` — interactive UI.
- GuruRMM specifically: `gururmm-build` verify before merge-to-main (merge IS the deploy).
**Outbound / client-facing** (anything sent to a client or vendor — Syncro comment, email, DM, doc):
- `impeccable` — polish + correctness before it leaves (standing rule). `stop-slop` — strip AI-slop tone.
- Internal-only drafts are exempt.
**Syncro / vendor actions**:
- The skill's own **preview + explicit-confirm gate** is the check — show the full payload, wait for yes, then post. Never post-then-report. (Syncro: no API edit/delete after the fact.)
**Docs / wiki / memory writes**:
- Wiki articles → `wiki-lint`. Memory store edits → `memory-dream` checks (index/backlinks/dupes).
---
## Stakes calibration (don't burn tokens)
- Trivial/mechanical (typo, comment, rename, one-line doc) → skip the heavy review; a quick self-check is enough. Say you skipped and why.
- Normal change → the standard gate above.
- Security / auth / credential / migration / production / data-loss / outbound-to-client → full gate, bump model one tier, never skip.
- Push the cheap parts (classify which skills apply, prose/tone passes) to **Ollama Tier-0**; reserve inherited/opus for the actual judgment.
See also: `.claude/CLAUDE_EXTENDED.md` (full workflow detail), memories `[[feedback_skill_first_routing]]`, `[[feedback_impeccable_on_outbound]]`, `[[feedback_syncro_preview_mandatory]]`, `[[feedback_calibrate_effort_to_stakes]]`.

View File

@@ -0,0 +1,46 @@
---
name: ask-forum
description: Ask a teammate (e.g. Mike) a question in the private #ct-forum Discord forum and BLOCK until a human answers, then act on the reply — a live three-way between the user, this same Claude session, and the teammate. No second bot, no DMs, one tool call (the wait runs server-side in bash).
---
# /ask-forum — human-in-the-loop question via #ct-forum
Entry point to the `ask-forum` skill — full contract in `.claude/skills/ask-forum/SKILL.md`.
Engine: `.claude/scripts/ask-forum.sh`. Lets THIS session put a question
to a human in the private #ct-forum forum and get their answer back in-session, so it
can keep going. The forum post's thread id is the correlation key — the session reads
only the thread it created, so an answer is never confused with another question's.
Forum-only by design (Mike's call, 2026-07-08): DM replies are intentionally not used,
so answers stay visible to the team. The BEAST bot ignores #ct-forum, so the asking
session is the only agent that consumes replies there.
## Usage
```bash
# Ask and block until a human replies (default timeout 600s, poll 8s):
bash .claude/scripts/ask-forum.sh "your question" --tag @264814939619721216
# Re-read a thread you already asked in (one-shot, no waiting):
bash .claude/scripts/ask-forum.sh --read <thread_id>
# Resume blocking on a thread already posted (e.g. after a timeout):
bash .claude/scripts/ask-forum.sh --wait <thread_id> --timeout 1800 --poll 10
```
Flags: `--title "short title"` (forum post name; defaults to first 60 chars of the
question), `--timeout SEC`, `--poll SEC`, `--tag @<discord_id>` (repeatable — pings
that person; ids in `.claude/users.json`: mike 264814939619721216,
howard 624667664501178379, winter 624666486362996755, rob 261978810713505792).
## How to run it well
- **Long waits → background it** with `run_in_background: true` (NO shell `&` — that
forks the wait off and exits early). You get the answer when the human replies.
- Only **non-bot** replies count as answers, so bot chatter can never be mistaken for
the human's answer.
- Exit codes: 0 answered · 2 no token · 3 Discord API error · 4 timeout (thread stays
open — resume with `--wait <thread_id>`).
Channel: #ct-forum (`1522960388432465950`), private — @everyone denied; Howard, Mike,
and the bot allowed. Add someone via a channel permission overwrite if they need access.

View File

@@ -2,7 +2,9 @@
Read and send mail for an Arizona Computer Guru mailbox via Microsoft Graph, using the dedicated **ComputerGuru Mailbox** app (`1873b1b0-3377-485c-a848-bae9b2f8f1f5`). Defaults to the mailbox of the user running it (from `identity.json`). Read and send mail for an Arizona Computer Guru mailbox via Microsoft Graph, using the dedicated **ComputerGuru Mailbox** app (`1873b1b0-3377-485c-a848-bae9b2f8f1f5`). Defaults to the mailbox of the user running it (from `identity.json`).
> **Mail path (working — repointed 2026-06-17).** `/mailbox` uses the dedicated single-tenant **ComputerGuru Mailbox** app (`1873b1b0-3377-485c-a848-bae9b2f8f1f5`; vault `msp-tools/computerguru-mailbox.sops.yaml`; Mail.ReadWrite + Mail.Send + Contacts.ReadWrite; azcomputerguru.com only). Tokens come from the suite tool: `bash .claude/skills/remediation-tool/scripts/get-token.sh azcomputerguru.com mailbox` (cert-preferred, secret fallback, 55-min cache). This **replaces the deleted `fabb3421`** (Claude-MSP-Access), removed from the tenant 2026-06-14 — it returns **AADSTS700016**; do NOT reintroduce it. The mailbox app's service principal is **disabled when idle**: on a token 401 "account is disabled", enable the SP, then retry. > **Inbox-rule capability (added 2026-06-23).** The app now also holds **`MailboxSettings.ReadWrite`** (Graph app role `6931bccd-447a-43d1-b442-00a195474933`, admin-consented via the Tenant Admin app `709e6eed`). This is what enables creating/modifying mailbox **inbox rules** (`messageRules`) — `Mail.ReadWrite` alone returns 403 on that endpoint. First use: a "keep DMARC reports in Inbox" rule on `rua@azcomputerguru.com`.
>
> **Mail path (working — repointed 2026-06-17).** `/mailbox` uses the dedicated single-tenant **ComputerGuru Mailbox** app (`1873b1b0-3377-485c-a848-bae9b2f8f1f5`; vault `msp-tools/computerguru-mailbox.sops.yaml`; Mail.ReadWrite + Mail.Send + Contacts.ReadWrite + MailboxSettings.ReadWrite; azcomputerguru.com only). Tokens come from the suite tool: `bash .claude/skills/remediation-tool/scripts/get-token.sh azcomputerguru.com mailbox` (cert-preferred, secret fallback, 55-min cache). This **replaces the deleted `fabb3421`** (Claude-MSP-Access), removed from the tenant 2026-06-14 — it returns **AADSTS700016**; do NOT reintroduce it. The mailbox app's service principal is **disabled when idle**: on a token 401 "account is disabled", enable the SP, then retry.
## Usage ## Usage
@@ -25,7 +27,7 @@ Microsoft Graph access to ACG's own mailboxes (azcomputerguru.com tenant). Readi
## API Configuration ## API Configuration
- **App:** ComputerGuru Mailbox (dedicated single-tenant Graph app), `client_id = 1873b1b0-3377-485c-a848-bae9b2f8f1f5` (Mail.ReadWrite + Mail.Send + Contacts.ReadWrite, azcomputerguru.com only). Replaces the deleted `fabb3421`. - **App:** ComputerGuru Mailbox (dedicated single-tenant Graph app), `client_id = 1873b1b0-3377-485c-a848-bae9b2f8f1f5` (Mail.ReadWrite + Mail.Send + Contacts.ReadWrite + MailboxSettings.ReadWrite, azcomputerguru.com only). Replaces the deleted `fabb3421`.
- **Tenant:** `azcomputerguru.com` - **Tenant:** `azcomputerguru.com`
- **Token:** acquire via the suite tool — `bash .claude/skills/remediation-tool/scripts/get-token.sh azcomputerguru.com mailbox` (cert-preferred, secret fallback, 55-min cache in `/tmp/remediation-tool/<tenant>/mailbox.jwt`). Do NOT roll your own `client_credentials` here. Credential vault entry: `msp-tools/computerguru-mailbox.sops.yaml`. - **Token:** acquire via the suite tool — `bash .claude/skills/remediation-tool/scripts/get-token.sh azcomputerguru.com mailbox` (cert-preferred, secret fallback, 55-min cache in `/tmp/remediation-tool/<tenant>/mailbox.jwt`). Do NOT roll your own `client_credentials` here. Credential vault entry: `msp-tools/computerguru-mailbox.sops.yaml`.
- **SP idle-toggle:** the mailbox app's service principal is disabled when idle. On a token 401 "account is disabled", enable the SP, then retry. - **SP idle-toggle:** the mailbox app's service principal is disabled when idle. On a token 401 "account is disabled", enable the SP, then retry.

View File

@@ -159,6 +159,24 @@ Use `python` only when explicitly writing a Python script. Use `script` for save
**VALID `command_type` values ONLY: `shell`, `powershell`, `python`, `script`, `claude_task` (plus alias `cmd` → shell = cmd.exe).** The agent deserializes `command_type` into a Rust enum; an UNKNOWN value (e.g. a made-up type) fails the agent's whole-message JSON parse and the command is **silently dropped — no ack, no result, no error** — which is indistinguishable from a network black-hole and has caused a long mis-diagnosis. On Windows: `powershell` runs powershell.exe (UTF-8 output fixed in-agent); `shell` or `cmd` runs cmd.exe. If a dispatched command sits un-acked forever, FIRST suspect an invalid `command_type` before chasing the network. (Newer agents NAK an unparseable command so it fails fast with a clear stderr instead of black-holing.) **VALID `command_type` values ONLY: `shell`, `powershell`, `python`, `script`, `claude_task` (plus alias `cmd` → shell = cmd.exe).** The agent deserializes `command_type` into a Rust enum; an UNKNOWN value (e.g. a made-up type) fails the agent's whole-message JSON parse and the command is **silently dropped — no ack, no result, no error** — which is indistinguishable from a network black-hole and has caused a long mis-diagnosis. On Windows: `powershell` runs powershell.exe (UTF-8 output fixed in-agent); `shell` or `cmd` runs cmd.exe. If a dispatched command sits un-acked forever, FIRST suspect an invalid `command_type` before chasing the network. (Newer agents NAK an unparseable command so it fails fast with a clear stderr instead of black-holing.)
### Quote-safe dispatch for real scripts (PREFERRED for anything with quotes/UNC/$)
Any PowerShell payload containing embedded double-quotes, UNC `\\` paths, or `$`
that must survive literally should NOT be inlined into the JSON dispatch — the
RMM->cmd.exe layer strips/mangles them (see memory `feedback_windows_quote_stripping`).
Write the script to a file (with the Write tool — bash heredocs collapse `\\`),
then dispatch it byte-exact via `-EncodedCommand`:
```bash
bash .claude/scripts/ps-encoded.sh rmm "$AGENT_ID" script.ps1 --timeout 120 [--user-session]
# or print a paste-safe one-liner for ScreenConnect / plink:
bash .claude/scripts/ps-encoded.sh encode script.ps1
```
Size limit: the agent fails on ~7KB command bodies and encoding inflates ~2.67x,
so keep scripts under ~2KB raw (the helper warns at 4KB encoded, refuses at 6KB).
Inline dispatch below remains fine for simple quote-free commands.
### Basic dispatch ### Basic dispatch
```bash ```bash

View File

@@ -70,34 +70,40 @@ Reference: Invoice 67594 (VWP, 2026-05-12), Ticket #32269.
### Scenario B — Non-Block / Direct Billing Customer ### Scenario B — Non-Block / Direct Billing Customer
Use a **single emergency labor product** for the full hours worked: Use a **single emergency labor product** for the full hours worked. All delivery channels use product 26184 — the rate varies by how work was delivered:
| Customer type | Product | Product ID | Rate | | Delivery channel | Product | Product ID | Rate |
|---|---|---|---| |---|---|---|---|
| Business | Labor - Emergency or After Hours Business | 26184 | $262.50/hr | | Remote | Labor - Emergency or After Hours Business | 26184 | $225.00/hr |
| In-Shop | Labor - Emergency or After Hours Business | 26184 | $225.00/hr |
| Onsite | Labor - Emergency or After Hours Business | 26184 | $262.50/hr |
Residential rates are legacy — ACG no longer bills residential. Do not use product 42584. Residential rates are legacy — ACG no longer bills residential. Do not use product 42584.
**Example:** 4 hours of emergency business work, direct billing: **IMPORTANT:** Product 26184's `price_retail` from the live API returns $262.50 (the onsite rate). For remote and in-shop work, you MUST explicitly pass `"price_retail": 225.00` — do NOT use the live-fetched rate blindly for those channels.
- Line 1: `Labor - Emergency or After Hours Business` (26184) — 4.0 hr
Reference: Ticket #32188 (VWP, direct billing, 2026-04-22). **Example:** 4 hours of emergency in-shop work, direct billing:
- Line 1: `Labor - Emergency or After Hours Business` (26184) — 4.0 hr @ $225.00
Reference: Ticket #32188 (VWP, direct billing, 2026-04-22). Rates confirmed by Winter 2026-06-23.
--- ---
## Standard Labor Products (Reference) ## Standard Labor Products (Reference)
Emergency business rate is **$262.50/hr** (product 26184) — used for all emergency/afterhours business work regardless of remote vs onsite. Residential rates are legacy and not in use. Emergency rates use product 26184 for all channels, but the `price_retail` differs by delivery method — do NOT blindly use the live API rate for remote/in-shop (the API returns the onsite default of $262.50). Residential rates are legacy and not in use.
**Always fetch `price_retail` from `GET /api/v1/products/{id}` before billing non-block customers. Never use a hardcoded rate.** **For non-emergency work:** fetch `price_retail` from `GET /api/v1/products/{id}` before billing. For emergency work: use the rates below (confirmed by Winter 2026-06-23).
| Service type | Product | Product ID | Live Rate | Notes | | Service type | Product | Product ID | Rate | Notes |
|---|---|---|---|---| |---|---|---|---|---|
| Remote Business | Labor - Remote Business | 1190473 | $150.00/hr | Non-block cash billing | | Remote Business | Labor - Remote Business | 1190473 | $150.00/hr | Non-block cash billing |
| Onsite Business | Labor - Onsite Business | 26118 | $175.00/hr | | | Onsite Business | Labor - Onsite Business | 26118 | $175.00/hr | |
| In-Shop Business | Labor - In Shop Business | 573881 | $150.00/hr | | | In-Shop Business | Labor - In Shop Business | 573881 | $150.00/hr | |
| Block/Prepaid (any type) | Labor - Remote Business | 1190473 | $0.00 | Price = $0; draws from block in hours | | Block/Prepaid (any type) | Labor - Remote Business | 1190473 | $0.00 | Price = $0; draws from block in hours |
| Emergency/Afterhours Business | Labor - Emergency or After Hours Business | 26184 | $262.50/hr | All business emergency — remote and onsite | | Emergency Remote | Labor - Emergency or After Hours Business | 26184 | **$225.00/hr** | Override price_retail — API default is wrong for this channel |
| Emergency In-Shop | Labor - Emergency or After Hours Business | 26184 | **$225.00/hr** | Override price_retail — API default is wrong for this channel |
| Emergency Onsite | Labor - Emergency or After Hours Business | 26184 | **$262.50/hr** | API default is correct for onsite |
--- ---

View File

@@ -45,6 +45,8 @@ Create, update, close, comment on, and bill tickets in Syncro PSA.
**Emergency/after-hours billing — check prepaid first:** Before adding a `26184` (Emergency) line item, `GET /customers/<id>` and read `prepay_hours`. Emergency = time-and-a-half (×1.5), applied ONCE — never bill a separate regular + emergency line for the same hours. **No prepaid (`prepay_hours == 0`):** `26184` at qty = actual hours; set `price_retail` by delivery channel — **Onsite $262.50** (175×1.5, 26184's default), **Remote / In-Shop $225** (150×1.5, override price_retail). The rate carries the 1.5×; do NOT also ×1.5 the qty. **Prepaid (`prepay_hours > 0`):** still use `26184`, at qty = actual hours **× 1.5** (premium goes in the quantity since prepaid debits by quantity; invoice nets $0, block debits hours×1.5). e.g. 1.5 emergency hrs prepaid → `26184` @ 2.25. (Rule updated 2026-05-27 by Mike: prepaid emergency uses `26184`, NOT the old `26118`×1.5 — keeps the line labeled emergency + mapping right in QuickBooks. Original ×1.5-not-additive lesson: #32203 Desert Auto Tech 2026-04-23, Winter.) **Emergency/after-hours billing — check prepaid first:** Before adding a `26184` (Emergency) line item, `GET /customers/<id>` and read `prepay_hours`. Emergency = time-and-a-half (×1.5), applied ONCE — never bill a separate regular + emergency line for the same hours. **No prepaid (`prepay_hours == 0`):** `26184` at qty = actual hours; set `price_retail` by delivery channel — **Onsite $262.50** (175×1.5, 26184's default), **Remote / In-Shop $225** (150×1.5, override price_retail). The rate carries the 1.5×; do NOT also ×1.5 the qty. **Prepaid (`prepay_hours > 0`):** still use `26184`, at qty = actual hours **× 1.5** (premium goes in the quantity since prepaid debits by quantity; invoice nets $0, block debits hours×1.5). e.g. 1.5 emergency hrs prepaid → `26184` @ 2.25. (Rule updated 2026-05-27 by Mike: prepaid emergency uses `26184`, NOT the old `26118`×1.5 — keeps the line labeled emergency + mapping right in QuickBooks. Original ×1.5-not-additive lesson: #32203 Desert Auto Tech 2026-04-23, Winter.)
**`prepay_hours` is ONLY reliable from `GET /customers/{id}` — the customer SEARCH/LIST endpoint lies.** `GET /customers?query=...` (and any list endpoint) returns `prepay_hours: null` (or stale) even when the customer HAS a block. **NEVER read `prepay_hours` from a search/list result, and NEVER assert "no prepaid block" / "real charge" / a dollar total in a preview built from search data.** Before ANY billing preview or decision that mentions prepay, do a full `GET /customers/{id}` and read `.customer.prepay_hours` from THAT response. If you only have search data, fetch the full record first — do not guess. The billing-flow Step-1 GET is mandatory and must happen BEFORE the preview, not just before the invoice. (Recurring miss flagged by Mike 2026-06-23: previews repeatedly said "$300, no block," then the block surfaced at invoice time and netted $0 — Dataforth, Grabb & Durando #32455.)
**Prepaid customers — ALL billing (not just emergency):** `GET /customers/<id>``prepay_hours` before creating ANY invoice for a prepaid customer. When you bill a prepaid customer using a billable labor product (remote / onsite / in-shop / web), Syncro automatically deducts from their prepay block and the invoice total shows $0.00. The line item name is annotated "- Applied X Prepay Hours". This is correct behavior — do NOT treat a $0.00 invoice as an error. Verify the deduction by re-fetching `customer.prepay_hours` after invoicing and confirming it dropped by `quantity`. **Prepaid customers — ALL billing (not just emergency):** `GET /customers/<id>``prepay_hours` before creating ANY invoice for a prepaid customer. When you bill a prepaid customer using a billable labor product (remote / onsite / in-shop / web), Syncro automatically deducts from their prepay block and the invoice total shows $0.00. The line item name is annotated "- Applied X Prepay Hours". This is correct behavior — do NOT treat a $0.00 invoice as an error. Verify the deduction by re-fetching `customer.prepay_hours` after invoicing and confirming it dropped by `quantity`.
**`9269129` (Labor - Prepaid Project Labor) is EXEMPT — it does NOT deduct from prepay blocks:** Despite the name, this product is categorized as Exempt Labor at $0.00 and contains no prepay-deduction logic. Billing a prepaid customer with this product results in a $0.00 invoice AND no block decrement — silent accounting drift. Discovered 2026-05-04 (see `feedback_syncro_labor_type.md`). NEVER use `9269129` for normal or prepaid work. Only use it if explicitly directed. The correct approach for prepaid customers is a billable labor product matching the delivery channel (remote / onsite / in-shop / web). **`9269129` (Labor - Prepaid Project Labor) is EXEMPT — it does NOT deduct from prepay blocks:** Despite the name, this product is categorized as Exempt Labor at $0.00 and contains no prepay-deduction logic. Billing a prepaid customer with this product results in a $0.00 invoice AND no block decrement — silent accounting drift. Discovered 2026-05-04 (see `feedback_syncro_labor_type.md`). NEVER use `9269129` for normal or prepaid work. Only use it if explicitly directed. The correct approach for prepaid customers is a billable labor product matching the delivery channel (remote / onsite / in-shop / web).
@@ -53,6 +55,10 @@ Create, update, close, comment on, and bill tickets in Syncro PSA.
**Always pass `"taxable": false` explicitly on labor line items.** Labor products are configured with `taxable: false` in Syncro, but `add_line_item` via API does not inherit the product's taxable setting — it posts the line item as `taxable: true` regardless. Always include `"taxable": false` in the payload to match the product's configured value. **Always pass `"taxable": false` explicitly on labor line items.** Labor products are configured with `taxable: false` in Syncro, but `add_line_item` via API does not inherit the product's taxable setting — it posts the line item as `taxable: true` regardless. Always include `"taxable": false` in the payload to match the product's configured value.
**`product_category` is a CONTROLLED VOCABULARY — never invent one, never invoice a line blank.** Syncro categories are an admin-defined set; the CATEGORY column on every invoice line drives QuickBooks item mapping and revenue reporting. A blank category = an uncategorized line that breaks both. Two hard rules:
- **Never make up a category string.** Use ONLY a value that already exists in the tenant. To see the live set, enumerate distinct `product_category` across products (do NOT hardcode — it changes): `for p in $(seq 1 N); do curl -s "${BASE}/products?per_page=100&page=$p&api_key=${API_KEY}"; done | jq -r '.products[].product_category' | sort -u`. Known good values include `Labor`, `Software`, `Hardware`, `Product`, `Service`/`Services`, `Subscriptions`, `Contract`, `Discount`, `Exempt Labor`, `Prepay Hours`, `Other`, `Deposit`, `Default`. If none clearly fits, **ASK — do not guess**.
- **Never invoice a line whose product has `product_category: null`.** Before billing, `GET /products/<id>` and check `.product.product_category`. If it is `null`/blank, the product is mis-configured — STOP and flag it (a Windows/OS/software item → `Software`; a physical item → `Hardware`). Do not silently push the line out uncategorized. (Incident 2026-06-30, Cascades #32466/#32474 invoices 67887/67890: "Windows Pro Upgrade" product 23571919 has `product_category: null`, so every billed line showed a blank CATEGORY column. Winter corrected the records; the rule is here so the skill catches it next time.)
**`DELETE /schedules/{id}` destroys the recurring invoice template immediately — no confirmation, no undo.** Past generated invoices are unaffected but future billing stops. The schedule must be recreated manually with all line items if deleted accidentally. NEVER run destructive HTTP method probes against a live customer schedule — use ACG internal account (customer_id 15353550) for any testing. Incident: Russo Law Firm schedule 224454 deleted during API research 2026-05-26; recreated as 509659. **`DELETE /schedules/{id}` destroys the recurring invoice template immediately — no confirmation, no undo.** Past generated invoices are unaffected but future billing stops. The schedule must be recreated manually with all line items if deleted accidentally. NEVER run destructive HTTP method probes against a live customer schedule — use ACG internal account (customer_id 15353550) for any testing. Incident: Russo Law Firm schedule 224454 deleted during API research 2026-05-26; recreated as 509659.
**Test articles — always prefix the subject/name with `[TEST]`.** Any ticket, estimate, appointment, or schedule created for testing or API research MUST have its subject or name prefixed with `[TEST]` (e.g. `[TEST] Schedule API research`, `[TEST] Estimate - hardware pricing`). This applies regardless of which customer account is used (including the ACG internal test account, customer_id 15353550). Test records must be instantly distinguishable from real customer work at a glance. If a test article was created without the prefix, PUT the subject to add it before continuing. **Test articles — always prefix the subject/name with `[TEST]`.** Any ticket, estimate, appointment, or schedule created for testing or API research MUST have its subject or name prefixed with `[TEST]` (e.g. `[TEST] Schedule API research`, `[TEST] Estimate - hardware pricing`). This applies regardless of which customer account is used (including the ACG internal test account, customer_id 15353550). Test records must be instantly distinguishable from real customer work at a glance. If a test article was created without the prefix, PUT the subject to add it before continuing.
@@ -607,12 +613,33 @@ COMMENT_ID=$(echo "$COMMENT_RESP" | jq -r '.comment.id')
- Do NOT wrap the payload in `{"comment": {...}}` — returns 422. - Do NOT wrap the payload in `{"comment": {...}}` — returns 422.
- **If `COMMENT_ID` is null:** GET `/tickets/{id}` and check `.ticket.comments[]` by subject before doing anything else. Comments cannot be deleted via API — duplicates require manual GUI removal. - **If `COMMENT_ID` is null:** GET `/tickets/{id}` and check `.ticket.comments[]` by subject before doing anything else. Comments cannot be deleted via API — duplicates require manual GUI removal.
#### Customer Assets (retire / archive — API GAP)
**Retiring a device in Syncro = "Archive" (UI ONLY — no REST API).** Verified against
Syncro's own docs 2026-07-08 (`docs.syncrosecure.com/assets-rmm/archive-assets`): archiving
is done in the web UI only — asset Details page → **Actions → Archive**, or bulk via the
Assets table checkboxes → **Bulk Actions → Archive**. There is **no** archive endpoint and
**no** `archived` field on `PUT /customer_assets/{id}`. Do NOT try to archive via API, and
do NOT substitute `DELETE /customer_assets/{id}` — delete is destructive/irreversible and
loses history + breaks ticket linkages (Archive keeps the asset visible on its tickets/alerts).
- **To retire assets: hand the user the asset list + the Bulk-Actions-→-Archive steps.** We
cannot do it for them via API.
- Asset READS are fine: `GET /customer_assets?customer_id=N[&query=<name>]` (list, 100/page,
`.assets[]`) and `GET /customer_assets/{id}` (detail; `.asset` — RMM data lives under
`.asset.properties.kabuto_information`). `updated_at` is NOT a reliable "last online" (bulk
account touches move it) — use the ScreenConnect `GuestInfoUpdateTime` or kabuto data for
real last-seen.
- **[RESEARCH]** Re-check if Syncro ships an archive API (watch release notes). If/when it
does, wire it in here and add `--confirm` gating. Until then this is a "cannot be performed
via skill" function. (First hit: IMC retirement pass 2026-07-08.)
#### Customers #### Customers
```bash ```bash
# Search # Search — returns id/name/email ONLY. Do NOT read prepay_hours from this list (it is null/stale here).
curl -s "${BASE}/customers?query=<name>&per_page=25&api_key=${API_KEY}" | jq '[.customers[] | {id, business_name, email}]' curl -s "${BASE}/customers?query=<name>&per_page=25&api_key=${API_KEY}" | jq '[.customers[] | {id, business_name, email}]'
# Get one # Get one — this is the ONLY trustworthy source of prepay_hours. Always GET this before any billing preview.
curl -s "${BASE}/customers/${CUST_ID}?api_key=${API_KEY}" | jq '{id: .customer.id, prepay_hours: .customer.prepay_hours}' curl -s "${BASE}/customers/${CUST_ID}?api_key=${API_KEY}" | jq '{id: .customer.id, prepay_hours: .customer.prepay_hours}'
``` ```
@@ -668,6 +695,8 @@ JSON
- `price_retail` — **must be set explicitly**; Syncro does NOT auto-populate from product rate via API - `price_retail` — **must be set explicitly**; Syncro does NOT auto-populate from product rate via API
- `taxable` — **must be set explicitly**; always `false` for labor; `true` for taxable hardware - `taxable` — **must be set explicitly**; always `false` for labor; `true` for taxable hardware
**Pre-flight every line item's category.** `GET /products/<id>` and confirm `.product.product_category` is non-null and is a real existing category (see the controlled-vocabulary rule above). A `null` category means the line will invoice with a blank CATEGORY column — STOP and flag the product instead of billing it. Never substitute an invented category.
**Do NOT remove line items after invoicing.** Leave them on the ticket. **Do NOT remove line items after invoicing.** Leave them on the ticket.
**Labor product IDs** — always fetch `price_retail` live, never hardcode: **Labor product IDs** — always fetch `price_retail` live, never hardcode:

View File

@@ -0,0 +1,55 @@
---
name: tailscale
description: Manage an ACG-administered Tailscale tailnet via the REST API v2 - list/inspect devices, delete a node, authorize a device, create/list/revoke tagged pre-auth keys. Reads free; writes gated --confirm.
---
# /tailscale — Tailscale tailnet management (REST API v2)
Thin entry point to the `tailscale` skill. Engine:
`.claude/skills/tailscale/scripts/tailscale-api.sh`. Full reference:
`.claude/skills/tailscale/SKILL.md`. Doctrine (per-client tailnets, tagged pre-auth
keys, offboarding): `wiki/patterns/tailscale-client-management.md`.
Read-only by default; every device delete/authorize and auth-key create/delete is
**gated behind `--confirm`** and previews first. ADMIN rights: add + delete.
## Usage
```bash
TS="bash .claude/skills/tailscale/scripts/tailscale-api.sh"
# Reads (no confirm)
$TS status # auth check + tailnet + device count
$TS devices [--json] # list devices
$TS device <name|id|100.x> [--json] # device detail
$TS keys [--json] # list auth keys
# Writes (GATED — preview without --confirm, act with it)
$TS create-key --tag tag:<client> --reusable --preauth [--ephemeral] \
[--expiry-days N] [--desc "..."] --confirm
$TS delete-key <keyId> --confirm # revoke an auth key
$TS authorize <name|id|100.x> --confirm # approve a device
$TS delete-device <name|id|100.x> --confirm # remove a node
# Point at a specific per-client tailnet's vault entry
$TS devices --vault tailscale/roberts.sops.yaml
```
## Rules
1. **Vault-first auth.** Credentials come from `tailscale/api-access.sops.yaml` (OAuth
client preferred, `credentials.api_key` token fallback); tailnet defaults to `-`. Never
hardcode a token. Provisioning commands are in the skill doc.
2. **Read before write.** Device ops resolve a name/100.x to the stable id first; an
**ambiguous match STOPS** (lists candidates, does nothing) — pass a specific id.
3. **Gated writes.** No `--confirm` = preview + exit, no change. Deleting a node drops it
off the tailnet; revoking a key blocks future enrollments with it.
4. **Per-client isolation.** One tailnet per client, one vault entry per tailnet. Confirm
the `--vault`/tailnet before any write.
5. **Key secrets → vault, never chat/commit.** `create-key` prints the secret once; store
it via `/vault` immediately.
6. **Bot alert after every write.** Successful writes auto-post a `[TAILSCALE] ...` line to
Discord (soft-fail); reads do not.
Use this to *drive* the tailnet; use `/rmm` to push the tagged pre-auth key onto client
machines (`tailscale-client-enroll.ps1`) once a key is minted.

View File

@@ -7,17 +7,29 @@ Seed new wiki articles or refresh existing ones from session logs, client docume
## Usage ## Usage
``` ```
/wiki-compile client:<slug> Seed or refresh a client wiki article /wiki-compile client:<slug> UPDATE an existing article (fast, incremental) or seed a new one
/wiki-compile client:<slug> --full Force full recompile of existing article (Sonnet synthesis) /wiki-compile client:<slug> --full REBUILD: full re-synthesis from ALL sources (Sonnet, slow)
/wiki-compile client:<slug> --syncro Syncro dynamic fields only (hours/tickets) — instant, no LLM
/wiki-compile project:<slug> Compile a project wiki article (no Syncro) /wiki-compile project:<slug> Compile a project wiki article (no Syncro)
/wiki-compile system:<slug> Compile a system wiki article (no Syncro) /wiki-compile system:<slug> Compile a system wiki article (no Syncro)
/wiki-compile all Process all missing + stale articles /wiki-compile all Process all missing + stale articles
``` ```
**Mode auto-detection:** **Mode auto-detection:**
- If `wiki/clients/<slug>.md` **does not exist****Seed mode** (full synthesis, Sonnet subagent) - If `wiki/clients/<slug>.md` **does not exist****Seed mode** (full synthesis, Sonnet subagent).
- If `wiki/clients/<slug>.md` **exists** and no `--full` flag → **Refresh mode** (surgical update of dynamic fields only, no subagent) - If it **exists** and no flag → **Update mode** (fast, incremental — the default; see below).
- `--full` flag → **Full recompile** (Sonnet synthesis, preserves existing Patterns/History) - `--full` **Rebuild** (full Sonnet re-synthesis from ALL sources; preserves Patterns/History).
- `--syncro`**Syncro-only refresh** (dynamic fields only; instant, no LLM).
**Update vs Rebuild — why update is fast (this is the point).** A **Rebuild** (`--full`) reads
*every* session log for the client and regenerates the *entire* article with a Sonnet subagent —
correct, but slow and expensive, and wasteful when only one thing changed. **Update** reads ONLY
the session logs dated after the article's `last_compiled` (usually 13 files, often zero) plus the
current article, and applies a few **surgical section edits** — new History rows, and targeted
edits to any section a new log actually changes — leaving the rest of the article byte-for-byte
untouched. Small input + small output + no full-article Sonnet pass = typically many times faster.
Reach for `--full` only when the article structure has drifted, sections are stale/wrong, or you
want a periodic clean rebuild. For "I just did some work, capture it," use plain update.
--- ---
@@ -65,12 +77,21 @@ esac
if [ ! -f "$CLAUDETOOLS_ROOT/$ARTICLE_PATH" ]; then if [ ! -f "$CLAUDETOOLS_ROOT/$ARTICLE_PATH" ]; then
MODE="seed" MODE="seed"
elif [ "$FULL_FLAG" = "--full" ]; then elif [ "$FULL_FLAG" = "--full" ]; then
MODE="full" MODE="full" # rebuild — full Sonnet re-synthesis
elif [ "$FULL_FLAG" = "--syncro" ]; then
MODE="syncro" # Syncro dynamic fields only
else else
MODE="refresh" MODE="update" # default: fast incremental knowledge merge
fi fi
echo "[INFO] Mode: $MODE | Target: $TARGET_TYPE:$SLUG" echo "[INFO] Mode: $MODE | Target: $TARGET_TYPE:$SLUG"
# For update mode, read the article's last_compiled so Phase 3 can select only newer logs.
LAST_COMPILED=""
if [ "$MODE" = "update" ]; then
LAST_COMPILED=$(sed -n 's/^last_compiled:[[:space:]]*//p' "$CLAUDETOOLS_ROOT/$ARTICLE_PATH" | head -1)
echo "[INFO] Update since last_compiled=${LAST_COMPILED:-unknown}"
fi
``` ```
--- ---
@@ -244,6 +265,28 @@ SOURCE_COUNT=$(echo "$ALL_SOURCES" | grep -c '^' || echo 0)
echo "[INFO] Found $SOURCE_COUNT source files" echo "[INFO] Found $SOURCE_COUNT source files"
``` ```
**Update mode — narrow to NEW sources only (this is the speedup).** In update mode, do NOT read
the full source set. Select only the logs the article has not yet incorporated: a source is "new"
if its filename date is **after** `LAST_COMPILED`, **or** it is not already listed in the article's
frontmatter `sources:`. Read only those.
```bash
if [ "$MODE" = "update" ]; then
# existing sources already folded into the article
EXISTING_SRC=$(awk '/^sources:/{f=1;next} /^[^ -]/{f=0} f&&/^[[:space:]]*-/{sub(/^[[:space:]]*-[[:space:]]*/,"");print}' "$CLAUDETOOLS_ROOT/$ARTICLE_PATH")
NEW_SOURCES=$(echo "$ALL_SOURCES" | while read -r f; do
[ -z "$f" ] && continue
# (a) not yet in the article's sources list?
if ! grep -qxF "$f" <<<"$EXISTING_SRC"; then echo "$f"; continue; fi
# (b) filename carries a YYYY-MM-DD newer than last_compiled?
d=$(echo "$f" | grep -oE '[0-9]{4}-[0-9]{2}-[0-9]{2}' | head -1)
if [ -n "$d" ] && [ -n "$LAST_COMPILED" ] && [ "$d" \> "$LAST_COMPILED" ]; then echo "$f"; fi
done | sort -u | grep -v '^$')
NEW_COUNT=$(echo "$NEW_SOURCES" | grep -c '^' || echo 0)
echo "[INFO] Update: $NEW_COUNT new source(s) since ${LAST_COMPILED:-unknown}"
fi
```
If `SOURCE_COUNT == 0` and no Syncro data: warn and stop. If `SOURCE_COUNT == 0` and no Syncro data: warn and stop.
``` ```
[ERROR] No session logs and no Syncro data found for '${SLUG}'. Cannot compile. [ERROR] No session logs and no Syncro data found for '${SLUG}'. Cannot compile.
@@ -254,7 +297,40 @@ If `SOURCE_COUNT == 0` and no Syncro data: warn and stop.
## Phase 4 — Article Generation ## Phase 4 — Article Generation
### Refresh Mode (existing article, no --full) ### Update Mode (existing article, default) — fast incremental
Fold only what changed since the last compile. **Do NOT re-synthesize the whole article and do
NOT spawn a Sonnet subagent.** Two parts:
**Part A — Syncro dynamic fields** (the three surgical edits documented under *Syncro-only Refresh*
below): hours remaining, Active Work ticket list, and frontmatter (`last_compiled`, `compiled_by`).
**Part B — Incremental knowledge merge** — run ONLY if `NEW_COUNT > 0` (new logs from Phase 3):
1. Read the full text of the `NEW_SOURCES` logs **and the current article**. Do NOT read the full
historical log set — that is what makes this fast.
2. Apply **targeted edits** for what those new logs actually establish:
- **Always:** add one dated row per material change to **History Highlights** (chronological).
- **Infrastructure** — add/adjust a row for any new or removed host, IP, service, or key path.
- **Access** — add any new vault path or access route (vault path only, never the secret).
- **Patterns & Known Issues** — add a genuinely new recurring issue, or mark an existing one
resolved if a new log shows it fixed.
- Touch **only** the sections a new log changes; leave every other byte of the article intact.
3. The delta is small, so **the main agent applies these edits directly** (Edit tool), or delegates
only the prose wording to Ollama Tier-0 / `haiku` and reviews it. Follow the same Hard Rules
(Syncro authoritative for billing; never inline secrets; never invent vault paths).
4. Append the `NEW_SOURCES` paths to frontmatter `sources:` (dedup).
If `NEW_COUNT == 0`: there is nothing new to fold — Part A (Syncro refresh) is the whole update.
Emit:
```
[OK] Update complete for wiki/clients/<slug>.md
- New logs folded: <NEW_COUNT> (since <LAST_COMPILED>)
- Sections touched: History[, Infrastructure, Access, Patterns] | none (Syncro-only)
- Syncro: hours <PREPAY_HOURS>, tickets <TICKET_COUNT>
```
### Syncro-only Refresh (`--syncro`) — instant, no LLM
Perform surgical updates only. No Ollama call. Three edits: Perform surgical updates only. No Ollama call. Three edits:
@@ -298,7 +374,7 @@ After edits, emit:
- Sources: ${SOURCE_COUNT} files tracked - Sources: ${SOURCE_COUNT} files tracked
``` ```
### Seed Mode / Full Recompile — Claude Synthesis (Sonnet subagent) ### Seed Mode / Rebuild (`--full`) — Claude Synthesis (Sonnet subagent)
Prepare the synthesis context by reading the most relevant source files. For session logs, read the full content of client-specific logs and the first 200 lines of root session logs (to avoid overwhelming the prompt). For full recompile, also read the existing article. Prepare the synthesis context by reading the most relevant source files. For session logs, read the full content of client-specific logs and the first 200 lines of root session logs (to avoid overwhelming the prompt). For full recompile, also read the existing article.
@@ -447,4 +523,7 @@ When invoked as `/wiki-compile all`:
- **Never invent vault paths.** If a credential is not mentioned in session logs, write "(verify)" in the Access section. - **Never invent vault paths.** If a credential is not mentioned in session logs, write "(verify)" in the Access section.
- **Never populate Infrastructure tables with placeholder rows.** Only include servers/services that appear in session logs or Syncro assets. - **Never populate Infrastructure tables with placeholder rows.** Only include servers/services that appear in session logs or Syncro assets.
- **Syncro contacts are ground truth for the Profile section.** Do not override with session log guesses if the contact name differs. - **Syncro contacts are ground truth for the Profile section.** Do not override with session log guesses if the contact name differs.
- **Refresh mode never touches Patterns or History.** Those sections require human review or `--full`. - **Syncro-only refresh (`--syncro`) never touches Patterns or History.** It edits dynamic fields only.
- **Update mode may ADD to History (always) and may add/adjust Infrastructure, Access, and Patterns**
strictly from the NEW logs — it never rewrites or removes existing prose. Wholesale re-synthesis
(rewriting existing sections, reconciling contradictions across the full history) is `--full` only.

55
.claude/hooks/block-tmp-path.sh Executable file
View File

@@ -0,0 +1,55 @@
#!/usr/bin/env bash
# PreToolUse(Bash) hook: block bash commands that WRITE files under /tmp on
# Windows (Git Bash / MSYS).
#
# Why: MSYS bash and the harness's other tools (Write, Python via py launcher)
# resolve /tmp to DIFFERENT real directories on Windows, so a file curl/tee
# writes to /tmp cannot be read back by the next tool call — the single most
# repeated tmp-path friction in errorlog.md (ref=feedback_tmp_path_windows,
# 6+ hits; also breaks run_in_background shells where TMPDIR is unset).
#
# Windows-only: exits 0 immediately on macOS/Linux where /tmp is one real dir.
# Only WRITE patterns are blocked (redirects, tee, -o/--output, cp/mv/mktemp
# targets). Reads (ls/cat/rm /tmp/...) pass.
#
# Dual-driver like block-backslash-winpath.sh: handles Claude (tool_input) and
# Grok (toolInput) event shapes; emits Grok decision JSON when denying there.
case "$(uname -s 2>/dev/null)" in
MINGW*|MSYS*|CYGWIN*) ;; # Windows Git-bash: the mismatch exists — check
*) exit 0 ;; # real /tmp elsewhere: nothing to protect
esac
input=$(cat)
cmd=$(echo "$input" | jq -r '(.toolInput // .tool_input // {}) | .command // ""' 2>/dev/null || python -c "
import sys, json
try:
d = json.load(sys.stdin)
ti = d.get('toolInput') or d.get('tool_input') or {}
print(ti.get('command', ''))
except:
print('')
" 2>/dev/null || echo '')
is_grok=$(echo "$input" | jq -r 'if has("hookEventName") or has("toolInput") then "1" else "0" end' 2>/dev/null || echo '0')
# Strip quoted substrings so a /tmp mention inside a string (commit message,
# grep pattern) does not false-trigger; real write targets sit outside quotes.
bare=$(printf '%s' "$cmd" | sed -E "s/'[^']*'//g; s/\"[^\"]*\"//g")
if printf '%s' "$bare" | grep -qE '(>>?[[:space:]]*/tmp/|[[:space:]]tee[[:space:]]+(-a[[:space:]]+)?/tmp/|(^|[[:space:]])-o[[:space:]]*/tmp/|--output(=|[[:space:]]+)/tmp/|(^|[[:space:]]|;|\|)(cp|mv)[[:space:]][^|;&>]*[[:space:]]/tmp/|mktemp[[:space:]]+(-d[[:space:]]+)?/tmp/)'; then
reason="Blocked write to /tmp in bash on Windows: MSYS /tmp and the Write/Python tools' /tmp are DIFFERENT directories — the file cannot be read back by the next tool call."
echo "BLOCKED: do not write files under /tmp on Windows (Git Bash)."
echo ""
echo "MSYS bash and the harness's other tools resolve /tmp to different real"
echo "directories, so the next tool call cannot read what you just wrote"
echo "(ref: feedback_tmp_path_windows). Use one of these instead:"
echo " - repo-relative scratch: curl -o ./.x.json ... (gitignored .tmp-* also works)"
echo " - the session scratchpad dir (absolute path, shared by all tools)"
echo " - pipe directly: curl ... | jq ... (no intermediate file)"
if [ "$is_grok" = "1" ]; then
printf '{"decision":"deny","reason":"%s"}\n' "$reason"
fi
exit 2
fi
exit 0

View File

@@ -2,11 +2,23 @@
## Reference ## Reference
- [ACG resource map](reference_resource_map.md) — **READ THIS FIRST** when a task references a server/service/tenant/API. What we have access to, how to connect from this machine, per-machine exceptions, gotchas. Points at the detail files below. - [ACG resource map](reference_resource_map.md) — **READ THIS FIRST** when a task references a server/service/tenant/API. What we have access to, how to connect from this machine, per-machine exceptions, gotchas. Points at the detail files below.
- [MSP360 backup monitoring](reference_msp360_backup_monitoring.md) — Authoritative "is client X actually backing up / last run" source for the whole fleet (NOT B2 bucket contents). MSP360 MBS API `/api/Monitoring`, vault `msp-tools/msp360-api.sops.yaml`. CompanyName also IDs B2 buckets (ACG-PST=Peaceful Spirit, ACG-TCA=Tucson Coin).
- [Backup checks only for billed clients](feedback_backup_check_only_billed.md) — Don't scan backup status for clients with no billed Data Backup line (Syncro schedule qty=0); verify billed clients only. Non-billed-but-backing-up = revenue-leak question for Mike/Winter, not a scan target.
- [ALIS (Medtelligent)](reference_alis_medtelligent.md) — Cascades assisted-living EHR. API host api.alisonline.com, community 622; username must be tenant-qualified (howard.enos@cascadestucson). Staff are READ-ONLY via API — create/change staff via web-UI Staff Import .xls. Use the `alis` skill.
- [GuruRMM User Manager](reference_gururmm_user_manager.md) — GuruRMM has a built-in per-agent User Manager tab (reset_password/enable/disable/groups for local+domain+AAD endpoint users; domain users only on a DC via `is_dc`). Use it, NOT raw Set-ADAccountPassword via /rmm. Endpoints: /api/agents/{id}/users + /users/action.
- [RMM map network drive (err67 double-hop)](reference_rmm_map_network_drive.md) — Pushing a persistent mapped drive to a remote share via /rmm user_session fails with err67/1702 (impersonated token = no network cred/double-hop). Plant HKCU:\Network\<drv> keys + cmdkey; mounts at next interactive logon. Immediate visibility needs the live session (ScreenConnect).
- [exchange-op = all-access Exchange tier](feedback_exchange_op_all_access.md) — STOP claiming "no tier can write mail." Exchange Operator app = Exchange Admin role + full_access_as_app + Exchange.ManageAsApp = full all-access (move mail, rules, config, EWS). Default to `exchange-op` for any Exchange write.
- [Tedards tenant facts](reference_tedards_tenant_facts.md) — Bill Tedards law office; tenant `4fcbb1f4…`; bt@/y226@ mailboxes; matter-number filing; UAL ingestion OFF; 9 synced devices; botched-import DUPLICATE folder.
- [Investigator EXO ManageAsApp gap](reference_investigator_exo_manageasapp_gap.md) — Security Investigator app lacks `Exchange.ManageAsApp` (only `full_access_as_app`) so `investigator-exo` 401s on EXO adminapi; use `exchange-op` tier for InvokeCommand.
- [Windows edition upgrade via RMM](reference_windows_edition_upgrade_rmm.md) — Verified Home->Pro/Pro-for-Workstations fleet procedure: `changepk.exe /ProductKey <generic Pro>` (NOT DISM Set-Edition — Error 50) -> reboot with `Restart-Computer -Force` (`shutdown /r /t N` is silently dropped in SYSTEM session) -> `slmgr /ipk <MAK>` + /ato. The vault MAK is a Pro-for-WORKSTATIONS key (auto-upgrades edition past plain Pro).
- [Tailscale subnet-route key expiry](reference_tailscale_subnet_key_expiry.md) — "internet OK but all of 172.16.3.x (Gitea .20, RMM/coord .30) dead" = Tailscale infra-node KEY EXPIRY (pfSense subnet router advertises 172.16.0.0/22), NOT a LAN outage; expiry now disabled on infra nodes (2026-06-25). Fallback: gururmm-server direct at tailnet 100.86.12.15:3001.
- [GravityZone support center](reference_gravityzone_support.md) — Authoritative Bitdefender GravityZone product + Public API docs; use to confirm UNVERIFIED `bitdefender` skill methods/param shapes (push setPushEventSettings, assignPolicy, report/account writes, maintenancewindows/integrations names). - [GravityZone support center](reference_gravityzone_support.md) — Authoritative Bitdefender GravityZone product + Public API docs; use to confirm UNVERIFIED `bitdefender` skill methods/param shapes (push setPushEventSettings, assignPolicy, report/account writes, maintenancewindows/integrations names).
- [GURU-5070 Rust toolchain](reference_guru5070_rust_toolchain.md) — GURU-5070 now has cargo + MSVC + protoc; build/clippy/test guru-connect LOCALLY (set PROTOC to the winget path) instead of the build host. CI only clippy-checks the Linux server, not the Windows agent. - [GURU-5070 Rust toolchain](reference_guru5070_rust_toolchain.md) — toolchain installed BUT a WDAC/Smart App Control policy (since 2026-07-04) BLOCKS running rustc/cargo/sops/openssl locally (os error 4551) — build the Windows agent on Beast, not here. Toolchain paths + CI gates still valid for reference.
- [ACG Office Network Infrastructure](infra_office_network.md) — IPs/hosts/roles for pfSense/Jupiter/VMs/Docker. Check before assuming; .21 (Uranus) is storage. - [ACG Office Network Infrastructure](infra_office_network.md) — IPs/hosts/roles for pfSense/Jupiter/VMs/Docker. Check before assuming; .21 (Uranus) is storage.
- [Power Failure Runbook](../POWER_FAILURE_RUNBOOK.md) — Recovery order after a power event: Tailscale routes, libvirt/VMs, Seafile, NPM/DNS. - [Power Failure Runbook](../POWER_FAILURE_RUNBOOK.md) — Recovery order after a power event: Tailscale routes, libvirt/VMs, Seafile, NPM/DNS.
- [Syncro API — Invoice Verification Pattern](syncro_invoice_verification_pattern.md) — /invoices?customer_id=X returns no ticket linkage; query /invoices/{number} for ticket_id. Compare by ticket ID, not number. - [Syncro API — Invoice Verification Pattern](syncro_invoice_verification_pattern.md) — /invoices?customer_id=X returns no ticket linkage; query /invoices/{number} for ticket_id. Compare by ticket ID, not number.
- [Syncro RMM policies = API-impossible](reference_syncro_rmm_api_gui_only.md) — policy create/assign/folder-move is GUI-ONLY; `policy_folder_id` is read-only on PUT (live-proven), policy endpoints 404, /policy_folders 401 scope-gated. Don't build /syncro move-asset; use `bitdefender` for API policy work.
- [Datto EDR detection behavior](reference_datto_edr_detection_behavior.md) — alert `sourceType`: `av`=Datto AV signature, `rule`=EDR reputation detection (both via `edr.py detections`). EDR is reputation-based not structural (wire known-bad file as autostart exe to trip it; loose files aren't surveyed). AV is tamper-protected (console-only disable); disabling Datto AV uninstalls it + Defender auto-reactivates (AMSI blocks scripts with literal EICAR → build from char codes). Verified live on RMM-TEST-MACHINE.
- [Approval Workflow: Tools vs Projects](approval-workflow-tools-vs-projects.md) — Tools (remediation, scripts): Howard/Claude with approval. Projects (GuruRMM): Mike approval for architecture/features; Howard can handle merges/deploys himself (2026-06-21); bugs→bug list. - [Approval Workflow: Tools vs Projects](approval-workflow-tools-vs-projects.md) — Tools (remediation, scripts): Howard/Claude with approval. Projects (GuruRMM): Mike approval for architecture/features; Howard can handle merges/deploys himself (2026-06-21); bugs→bug list.
- [CDP Chrome driver](reference_cdp_chrome_driver.md) — Drive Chrome via DevTools Protocol (.claude/scripts/cdp.py): visible window + screenshots-to-disk so Gemini/Grok can SEE the live site. Use localhost not 127.0.0.1; dedicated profile. Antigravity-style. - [CDP Chrome driver](reference_cdp_chrome_driver.md) — Drive Chrome via DevTools Protocol (.claude/scripts/cdp.py): visible window + screenshots-to-disk so Gemini/Grok can SEE the live site. Use localhost not 127.0.0.1; dedicated profile. Antigravity-style.
- [Firefox driver (ff.py)](reference_ff_firefox_driver.md) — PREFERRED browser driver. Drive Firefox via Playwright (.claude/scripts/ff.py): daemon on :9333, persistent profile, nav/shot/click/type/eval/console/network. Mike dislikes Chrome; claude-in-chrome connector disabled 2026-06-06. - [Firefox driver (ff.py)](reference_ff_firefox_driver.md) — PREFERRED browser driver. Drive Firefox via Playwright (.claude/scripts/ff.py): daemon on :9333, persistent profile, nav/shot/click/type/eval/console/network. Mike dislikes Chrome; claude-in-chrome connector disabled 2026-06-06.
@@ -17,6 +29,8 @@
- [Matomo Analytics](reference_matomo_analytics.md) — Self-hosted analytics at analytics.azcomputerguru.com, site IDs, tracking for all 3 sites. - [Matomo Analytics](reference_matomo_analytics.md) — Self-hosted analytics at analytics.azcomputerguru.com, site IDs, tracking for all 3 sites.
- [TickTick Integration](reference_ticktick_integration.md) — OAuth API integration, MCP server, SOPS vault creds, project/task CRUD. - [TickTick Integration](reference_ticktick_integration.md) — OAuth API integration, MCP server, SOPS vault creds, project/task CRUD.
- [Client Docs Structure](reference_client_docs_structure.md) — clients/<name>/docs/ layout (overview, network, servers, cloud, security, rmm). Template: clients/_client_template/. - [Client Docs Structure](reference_client_docs_structure.md) — clients/<name>/docs/ layout (overview, network, servers, cloud, security, rmm). Template: clients/_client_template/.
- [Client Wi-Fi Inventory](reference_client_wifi_inventory.md) — Building a fleet Wi-Fi list to connect onsite without asking. Passwords → vault `clients/<slug>/wifi.sops.yaml` (`credentials.<key>_ssid/_password`); readable index → `wiki/reference/client-wifi.md`. `/wifi` importer deferred (structure-only 2026-07-06).
- [Discord read replies](feedback_discord_read_replies.md) — Discord DM replies don't come through coord/repo sync. Use `discord-dm.sh read <user>` to see if mike/winter/rob actually answered before assuming silence.
- [MSP Audit Scripts](reference_msp_audit_scripts.md) — server_audit.ps1 / workstation_audit.ps1 at projects/msp-tools/msp-audit-scripts/. - [MSP Audit Scripts](reference_msp_audit_scripts.md) — server_audit.ps1 / workstation_audit.ps1 at projects/msp-tools/msp-audit-scripts/.
- [Pluto Build Server](reference_pluto_build_server.md) — Windows build VM: hostname PLUTO = Unraid VM "Claude-Builder" = 172.16.3.36 (all the same box). MSVC + WiX + Azure Trusted Signing. Drive via /rmm (agent enrolls as PLUTO) when SSH key isn't authorized. - [Pluto Build Server](reference_pluto_build_server.md) — Windows build VM: hostname PLUTO = Unraid VM "Claude-Builder" = 172.16.3.36 (all the same box). MSVC + WiX + Azure Trusted Signing. Drive via /rmm (agent enrolls as PLUTO) when SSH key isn't authorized.
- [Coord /messages API shape](reference_coord_messages_api_shape.md) — GET /api/coord/messages returns {total,skip,limit,messages[]} NOT a bare array; parse .messages[], strip control chars, read flag may be null. - [Coord /messages API shape](reference_coord_messages_api_shape.md) — GET /api/coord/messages returns {total,skip,limit,messages[]} NOT a bare array; parse .messages[], strip control chars, read flag may be null.
@@ -24,28 +38,42 @@
- [Gitea Internal API Access](reference_gitea_internal.md) — git.azcomputerguru.com is NOT behind Cloudflare — it's the office Cox IP NAT'd to NPM (openresty) on Jupiter. Prefer internal 172.16.3.20:3000 for reliability (bypasses NPM SSL-renewal reload blips). - [Gitea Internal API Access](reference_gitea_internal.md) — git.azcomputerguru.com is NOT behind Cloudflare — it's the office Cox IP NAT'd to NPM (openresty) on Jupiter. Prefer internal 172.16.3.20:3000 for reliability (bypasses NPM SSL-renewal reload blips).
- [Gitea git-op latency](reference_gitea_git_op_latency.md) — SSH (.20:2222) is SLOWEST (~1.5s); internal HTTP+token ~0.55s; SOPS lookup only ~0.33s. Don't switch to SSH for speed. Gitea SSH is .20:2222 (API ssh_url .21 is wrong). - [Gitea git-op latency](reference_gitea_git_op_latency.md) — SSH (.20:2222) is SLOWEST (~1.5s); internal HTTP+token ~0.55s; SOPS lookup only ~0.33s. Don't switch to SSH for speed. Gitea SSH is .20:2222 (API ssh_url .21 is wrong).
- [GuruRMM technical reference](reference_gururmm.md) — Server (172.16.3.30) layout + downloads dir `/var/www/gururmm/downloads` + `.channel` sidecar rollout control (stable/beta) + privileged server access via the server's OWN root RMM agent (hostname `gururmm`, no SSH needed; plink fallback) + API + `context=user_session` (WTS impersonation) + build-pipeline vendoring at `deploy/build-pipeline/` + Linux agent systemd sandbox trap. - [GuruRMM technical reference](reference_gururmm.md) — Server (172.16.3.30) layout + downloads dir `/var/www/gururmm/downloads` + `.channel` sidecar rollout control (stable/beta) + privileged server access via the server's OWN root RMM agent (hostname `gururmm`, no SSH needed; plink fallback) + API + `context=user_session` (WTS impersonation) + build-pipeline vendoring at `deploy/build-pipeline/` + Linux agent systemd sandbox trap.
- [GuruRMM command timeout_seconds](reference_gururmm_command_timeout_seconds.md) — agent command dispatch honors `timeout_seconds`, NOT `timeout`; long jobs die ~300s / go zombie (`running`, empty stdout) otherwise. Cost Birth Biologic a full day.
- [SharePoint Graph large-file upload](reference_sharepoint_graph_large_file_upload.md) — <4MB simple PUT, >=4MB MUST use chunked upload session (Content-Range); `\\?\` long paths; idempotent size-check; verify counts via /root/delta; single stream ~40Mbps (SPO throttle).
- [RMM-spawn headless Claude](reference_rmm_spawn_headless_claude.md) — run `claude -p` on any RMM-managed Windows box with Claude Code (reaches coord-isolated sites like AD2); use `context:user_session`, UNSET the stale machine `ANTHROPIC_API_KEY` (shadows OAuth → "Invalid API key"), detach + poll a DONE marker. Validated on AD2 2026-07-01.
- [RMM agent update model](rmm-agent-update-model.md) — Agent updates are server-PUSH on heartbeat (no self-poll); available versions = filesystem scan needing a `.sha256`; promote flips `.channel` sidecars beta→stable globally. Two stranders: beta-first freezes stable until an explicit promote; agents older than ~0.6.50 re-enroll with a NEW device_id/agent row when updated. - [RMM agent update model](rmm-agent-update-model.md) — Agent updates are server-PUSH on heartbeat (no self-poll); available versions = filesystem scan needing a `.sha256`; promote flips `.channel` sidecars beta→stable globally. Two stranders: beta-first freezes stable until an explicit promote; agents older than ~0.6.50 re-enroll with a NEW device_id/agent row when updated.
- [GuruRMM physical server storage](gururmm-physical-server-storage.md) — New box 172.16.1.231 (temp IP→will be .30), Ubuntu 26.04, ssh key `gururmm-physical`/alias `gururmm-new`. SSD (915G root) = HOT (PG default tablespace + WAL + builds); HDD ext4 at `/data` = COLD (`gururmm_cold` PG tablespace for aged `agent_logs` partitions + downloads + backups + archive). The #3 retention answer. - [GuruRMM physical server storage](gururmm-physical-server-storage.md) — New box 172.16.1.231 (temp IP→will be .30), Ubuntu 26.04, ssh key `gururmm-physical`/alias `gururmm-new`. SSD (915G root) = HOT (PG default tablespace + WAL + builds); HDD ext4 at `/data` = COLD (`gururmm_cold` PG tablespace for aged `agent_logs` partitions + downloads + backups + archive). The #3 retention answer.
- [Trebesch DESKTOP-QNP3ON5 shell replacement](reference_trebesch_qnp3on5.md) — AT Trebesch box runs an Explorer shell replacement; explorer.exe owner check returns blank — use Win32_ComputerSystem.UserName. GuruRMM SWIFT-LION-2892. - [Trebesch DESKTOP-QNP3ON5 shell replacement](reference_trebesch_qnp3on5.md) — AT Trebesch box runs an Explorer shell replacement; explorer.exe owner check returns blank — use Win32_ComputerSystem.UserName. GuruRMM SWIFT-LION-2892.
- [reference_backblaze_storage_rate](reference_backblaze_storage_rate.md) -- ACG's Backblaze B2 storage cost rate ($0.00695/GB) for the GuruRMM mspbackups storage-cost calculation - [reference_backblaze_storage_rate](reference_backblaze_storage_rate.md) -- ACG's Backblaze B2 storage cost rate ($0.00695/GB) for the GuruRMM mspbackups storage-cost calculation
- [Unraid VM no-IP causes](unraid-windows-vm-virtio-no-ip.md) — PRIMARY (general "new VMs stopped getting IPs lately"): Docker sets bridge-nf-call-iptables=1, so br0 VM DHCP OFFERs hit DOCKER-FORWARD (no br0 ACCEPT) and get dropped; new VMs can't complete DORA (existing renew via ESTABLISHED). Fix `=0` runtime (needs persistent post-Docker hook; not yet persisted on Jupiter). SECONDARY (Windows VM): virtio-net has no in-box driver -> use e1000 or virtio-win. Diagnose: tcpdump DHCP on pfSense; /sys vnetN rx_packets. - [Unraid VM no-IP causes](unraid-windows-vm-virtio-no-ip.md) — PRIMARY (general "new VMs stopped getting IPs lately"): Docker sets bridge-nf-call-iptables=1, so br0 VM DHCP OFFERs hit DOCKER-FORWARD (no br0 ACCEPT) and get dropped; new VMs can't complete DORA (existing renew via ESTABLISHED). Fix `=0` runtime (needs persistent post-Docker hook; not yet persisted on Jupiter). SECONDARY (Windows VM): virtio-net has no in-box driver -> use e1000 or virtio-win. Diagnose: tcpdump DHCP on pfSense; /sys vnetN rx_packets.
- [Starr Pass mail routing](reference_starrpass_mail_routing.md) — starrpass.com is DIRECT to MS (EOP/Defender, tenant 222450dd…); only devconllc.com is on Mailprotector (MP acct 16170). Check @starrpass.com quarantine/rejects via remediation-tool, not Mailprotector. - [Starr Pass mail routing](reference_starrpass_mail_routing.md) — starrpass.com is DIRECT to MS (EOP/Defender, tenant 222450dd…); only devconllc.com is on Mailprotector (MP acct 16170). Check @starrpass.com quarantine/rejects via remediation-tool, not Mailprotector.
- [INKY outbound breaks DMARC](reference_inky_outbound_breaks_dmarc.md) — Reverse-resolve DMARC rua failing IPs before blaming a sender: ipw-outbound.inkyphishfence.com / us.cloud-sec-av.com = INKY re-injection breaking DKIM+SPF. INKY is in-M365 (connectors+transport rules) per enrolled tenant, but hosting-level (IX/cPanel website) outbound also routes through it independent of M365 enrollment. Fix is INKY-side (outbound DKIM/SPF/ARC), not cPanel DNS.
- [Syncro prepay: full-GET only](feedback_syncro_prepay_full_get_only.md) — read prepay_hours ONLY from GET /customers/{id}; the customer search/list endpoint returns null/stale prepay. Never assert "no block" in a billing preview from search data.
- [Syncro priority/type format](feedback_syncro_priority_type_format.md) — every ticket create needs a number-prefixed priority ("2 Normal", not bare "Normal" which renders blank) AND a valid problem_type. Winter flagged #32193/#32194. Use the syncro skill's create flow.
- [Syncro line-item category](feedback_syncro_line_item_category.md) — product_category is a controlled vocab: never invent one, never invoice a line whose product has product_category:null (blank CATEGORY breaks QB mapping). Pre-flight GET /products/<id>. Winter fixed Cascades 67887/67890 "Windows Pro Upgrade".
- [RMM drive-map Explorer refresh](reference_rmm_drive_map_explorer_refresh.md) — drive mapped via RMM user_session works but the user's running Explorer won't show it until SHChangeNotify(DRIVEADD); also UNC \\ gets eaten in heredoc+jq, build it from [char]92.
- [Verify live before acting](feedback_verify_live_before_acting.md) — pull LIVE data (OMSA/iDRAC/live API) before acting on a hardware/infra flag; wiki/logs go stale. Cascades CS-SERVER "degraded RAID" was 9-day-stale (mirror self-recovered, SSDs bought needlessly). Windows can't see RAID member health.
- [Windows Pro upgrade billing](feedback_windows_pro_upgrade_billing.md) — ACG MAK key (vault infrastructure/windows-pro-mak, Mike's ACG key) for Home->Pro upgrades; RULE: invoice $99 PER machine activated, name the machine on the line item, bill after success. Each use consumes a MAK count.
- [AAD Connect msDS-KeyCredentialLink writeback](reference_aadconnect_keycredlink_writeback.md) — "completed-export-errors" + 8344 INSUFF_ACCESS_RIGHTS on a protected admin account = WHfB key writeback blocked by AdminSDHolder. Diagnose with csexport /f:x; fix with dsacls WP;msDS-KeyCredentialLink on AdminSDHolder + SDProp. - [AAD Connect msDS-KeyCredentialLink writeback](reference_aadconnect_keycredlink_writeback.md) — "completed-export-errors" + 8344 INSUFF_ACCESS_RIGHTS on a protected admin account = WHfB key writeback blocked by AdminSDHolder. Diagnose with csexport /f:x; fix with dsacls WP;msDS-KeyCredentialLink on AdminSDHolder + SDProp.
- [UniFi Site Manager cloud API](reference_unifi_site_manager_api.md) — `api.ui.com` + `X-API-KEY` (vault `services/unifi-site-manager`) = remote access to the WHOLE ACG UniFi fleet (~36 consoles) outside UOS. Tier1 `/v1/hosts|sites|devices|isp-metrics` = inventory+health+WAN. Tier2 CONNECTOR `/v1/connector/consoles/{id}/proxy/network/api/s/default/stat/{device,sta}` = **full UOS parity** (per-radio cu_total airtime + per-client RSSI) for ANY console, remote. Backend `unifi-wifi/scripts/gw-sitemanager.sh` (`fleet|devices|sites|isp|net`). Standalone UDM WAN SSH usually firewalled; per-console SSH pw at `clients/<slug>/udm-ssh`. - [UniFi Site Manager cloud API](reference_unifi_site_manager_api.md) — `api.ui.com` + `X-API-KEY` (vault `services/unifi-site-manager`) = remote access to the WHOLE ACG UniFi fleet (~36 consoles) outside UOS. Tier1 `/v1/hosts|sites|devices|isp-metrics` = inventory+health+WAN. Tier2 CONNECTOR `/v1/connector/consoles/{id}/proxy/network/api/s/default/stat/{device,sta}` = **full UOS parity** (per-radio cu_total airtime + per-client RSSI) for ANY console, remote. Backend `unifi-wifi/scripts/gw-sitemanager.sh` (`fleet|devices|sites|isp|net`). Standalone UDM WAN SSH usually firewalled; per-console SSH pw at `clients/<slug>/udm-ssh`.
- [reference_sqlx_migrations_immutable](reference_sqlx_migrations_immutable.md) -- NEVER edit an already-applied sqlx migration file — even a comment. sqlx::migrate! checksums each file at compile time and validates against _sqlx_migrations at startup; a changed checksum crash-loops the server with "migration N was previously applied but has been modified". Code review MUST flag any edit to an applied migration. - [reference_sqlx_migrations_immutable](reference_sqlx_migrations_immutable.md) -- NEVER edit an already-applied sqlx migration file — even a comment. sqlx::migrate! checksums each file at compile time and validates against _sqlx_migrations at startup; a changed checksum crash-loops the server with "migration N was previously applied but has been modified". Code review MUST flag any edit to an applied migration.
- [AD2 SSH MTU blackhole](ad2-ssh-mtu-blackhole.md) — AD2 SSH "lockouts"/mid-session read-errors over the Dataforth OpenVPN were a PMTU blackhole (tunnel PMTU ~1424 vs adapter MTU 1500), NOT a ban/account-lockout/flaky tunnel. Fix: pin the OpenVPN adapter MTU to 1400 (done on GURU-5070 via its SYSTEM RMM agent); permanent = `mssfix 1360` on the OpenVPN server. Diagnose over RMM, not SSH. - [AD2 SSH MTU blackhole](ad2-ssh-mtu-blackhole.md) — AD2 SSH "lockouts"/mid-session read-errors over the Dataforth OpenVPN were a PMTU blackhole (tunnel PMTU ~1424 vs adapter MTU 1500), NOT a ban/account-lockout/flaky tunnel. Fix: pin the OpenVPN adapter MTU to 1400 (done on GURU-5070 via its SYSTEM RMM agent); permanent = `mssfix 1360` on the OpenVPN server. Diagnose over RMM, not SSH.
- [DSCA33/45 resolved via Hoffman](project_dsca33_45_resolved_via_hoffman.md) — The "lost" DSCA33/45 spec files are recoverable from the Hoffman API (original certs survived the wipe); do NOT ask John. 56/58 models mined into projects/dataforth-dos/dsca33-45-templates.json; only DSCA33-1948 + DSCA45-1746 (24 units) lack an original. AD2 handoff: DSCA33-45-HOFFMAN-RECOVERY-2026-06-18.md. - [DSCA33/45 resolved via Hoffman](project_dsca33_45_resolved_via_hoffman.md) — The "lost" DSCA33/45 spec files are recoverable from the Hoffman API (original certs survived the wipe); do NOT ask John. 56/58 models mined into projects/dataforth-dos/dsca33-45-templates.json; only DSCA33-1948 + DSCA45-1746 (24 units) lack an original. AD2 handoff: DSCA33-45-HOFFMAN-RECOVERY-2026-06-18.md.
- [AD2 comms via sync only](ad2-comms-via-sync-only.md) — The AD2 Dataforth-box Claude session is coord-API-isolated (Gitea only); coord msg/lock/todo never reach it. Coordinate with AD2 ONLY via git /sync (committed docs + ## Note blocks). - [AD2 comms via sync only](ad2-comms-via-sync-only.md) — The AD2 Dataforth-box Claude session is coord-API-isolated (Gitea only); coord msg/lock/todo never reach it. Coordinate with AD2 ONLY via git /sync (committed docs + ## Note blocks).
- [reference_syncro_agent_handle_leak](reference_syncro_agent_handle_leak.md) -- RDS "no available computers in the pool" (0x3/0x408) can really be a SyncroLive.Agent.Runner handle leak starving the box. How to spot + fix.
## Users ## Users
- [Howard Enos](user_howard.md) — Mike's brother, technician, full access. Machines: ACG-TECH03L, Howard-Home (authoritative in users.json). - [Howard Enos](user_howard.md) — Mike's brother, technician, full access. Machines: ACG-TECH03L, Howard-Home (authoritative in users.json).
- [Mike — font preference](user_font_preference.md) — Mike prefers Lucida Console for monospace UI. - [Mike — font preference](user_font_preference.md) — Mike prefers Lucida Console for monospace UI.
## Feedback ## Feedback
- [Simplest solution first](feedback_simplest_solution_first.md) — Start with the least-complex thing that could answer the problem (one DNS lookup / one API call / one command), confirm it works, THEN graduate to advanced/complicated as needed. Don't reach for SSH/plink/multi-hop when a one-shot query answers it. Now a CLAUDE.md core rule.
- [Report times in Arizona time](feedback_timezone_arizona_reporting.md) — All user-facing times in America/Phoenix (MST, no DST), labeled AZ; convert from UTC. Set by Winter 2026-07-02.
- [RMM dashboard: beta before main](rmm-dashboard-beta-before-main.md) — GuruRMM website/dashboard changes land on beta (rmm-beta.azcomputerguru.com) and get tested BEFORE pushing/merging to main, unless told otherwise. Pipeline auto-builds beta FROM main, so don't merge to get on beta — build the feature branch's dashboard and rsync to /var/www/gururmm/dashboard-beta via a git worktree. Promote beta→prod with promote-dashboard.sh --confirm. - [RMM dashboard: beta before main](rmm-dashboard-beta-before-main.md) — GuruRMM website/dashboard changes land on beta (rmm-beta.azcomputerguru.com) and get tested BEFORE pushing/merging to main, unless told otherwise. Pipeline auto-builds beta FROM main, so don't merge to get on beta — build the feature branch's dashboard and rsync to /var/www/gururmm/dashboard-beta via a git worktree. Promote beta→prod with promote-dashboard.sh --confirm.
- [Prefer SSH over RMM](feedback_prefer_ssh_over_rmm.md) — When a target has SSH (key auth) and the task is easier over it, default to `scp script + ssh run` (system OpenSSH); RMM runs as SYSTEM + hits the server-side timeout reaper. Reserve RMM as fallback when SSH/VPN is down. - [Prefer SSH over RMM](feedback_prefer_ssh_over_rmm.md) — When a target has SSH (key auth) and the task is easier over it, default to `scp script + ssh run` (system OpenSSH); RMM runs as SYSTEM + hits the server-side timeout reaper. Reserve RMM as fallback when SSH/VPN is down.
- [Re-clone submodule creds](reclone-submodule-creds.md) — Re-cloning the restructured claudetools (projects now submodules): set `credential.helper=store` GLOBALLY before `git submodule update --init --recursive` or every Gitea submodule fails "could not read Username". Steps in RECLONE.md. - [Re-clone submodule creds](reclone-submodule-creds.md) — Re-cloning the restructured claudetools (projects now submodules): set `credential.helper=store` GLOBALLY before `git submodule update --init --recursive` or every Gitea submodule fails "could not read Username". Steps in RECLONE.md.
- [Bot alerts need a ticket link](feedback_bot_alert_ticket_link.md) — Syncro ticket bot-alerts MUST include a clickable link: https://computerguru.syncromsp.com/tickets/<internal_id> (internal id, not ticket number). post-bot-alert.sh posts raw text; put the URL in the message. - [Bot alerts need a ticket link](feedback_bot_alert_ticket_link.md) — Syncro ticket bot-alerts MUST include a clickable link: https://computerguru.syncromsp.com/tickets/<internal_id> (internal id, not ticket number). post-bot-alert.sh posts raw text; put the URL in the message.
- [RMM Set-Acl timeout loses stdout](feedback_rmm_setacl_timeout_password_loss.md) — NTFS ACL propagation (Set-Acl/icacls) on a large folder tree exceeds the RMM command timeout and stdout is dropped, so a password printed in that script is lost. Generate secrets LOCALLY (placeholder-inject) so they survive; isolate the ACL grant into its own long-timeout command.
- [Mac RMM authentication fixed](feedback_mac_rmm_auth_fixed.md) — Use `.claude/scripts/rmm-auth.sh` helper instead of heredoc pattern. Heredoc with `--data-binary @-` fails on macOS. Helper uses `jq -n --arg` to build JSON safely. Usage: `eval "$(bash .claude/scripts/rmm-auth.sh)"` sets $TOKEN, $RMM, $REPO_ROOT. Updated in /rmm Phase 0. - [Mac RMM authentication fixed](feedback_mac_rmm_auth_fixed.md) — Use `.claude/scripts/rmm-auth.sh` helper instead of heredoc pattern. Heredoc with `--data-binary @-` fails on macOS. Helper uses `jq -n --arg` to build JSON safely. Usage: `eval "$(bash .claude/scripts/rmm-auth.sh)"` sets $TOKEN, $RMM, $REPO_ROOT. Updated in /rmm Phase 0.
- [Verify committed state before push](feedback_verify_committed_state_before_push.md) — webhook builds from origin/main: verify the COMMITTED build (git stash + build), not the working tree; bad git-add pathspec silently aborts staging. Stage by directory. - [Verify committed state before push](feedback_verify_committed_state_before_push.md) — webhook builds from origin/main: verify the COMMITTED build (git stash + build), not the working tree; bad git-add pathspec silently aborts staging. Stage by directory.
- [Scheduling = coord todo, not schedulers](feedback_scheduling_via_coord_todo.md) — Defer future work as a coord todo (POST /api/coord/todos; needs text + created_by_user + created_by_machine) for a later session to pick up. NOT /schedule remote CCR agents (no vault/creds there) or local scheduled tasks. - [Scheduling = coord todo, not schedulers](feedback_scheduling_via_coord_todo.md) — Defer future work as a coord todo (POST /api/coord/todos; needs text + created_by_user + created_by_machine) for a later session to pick up. NOT /schedule remote CCR agents (no vault/creds there) or local scheduled tasks.
@@ -62,13 +90,13 @@
- [1Password — always use service token](feedback_1password_service_token.md) — Source OP_SERVICE_ACCOUNT_TOKEN from SOPS for every `op` call. Desktop-app integration prompts are unacceptable in agent flows. - [1Password — always use service token](feedback_1password_service_token.md) — Source OP_SERVICE_ACCOUNT_TOKEN from SOPS for every `op` call. Desktop-app integration prompts are unacceptable in agent flows.
- [Point vault-access teammates at SOPS path](feedback_vault_pointer_for_teammates.md) — When relaying infra/credential info to Howard or other vault-access teammates, hand over the SOPS path + key anchors; don't transcribe the entry's fields into the message. - [Point vault-access teammates at SOPS path](feedback_vault_pointer_for_teammates.md) — When relaying infra/credential info to Howard or other vault-access teammates, hand over the SOPS path + key anchors; don't transcribe the entry's fields into the message.
- [/tmp path mismatch on Windows](feedback_tmp_path_windows.md) — Write tool and Git Bash resolve `/tmp` to DIFFERENT real dirs. Use heredoc or workspace path for JSON payloads handed to curl. - [/tmp path mismatch on Windows](feedback_tmp_path_windows.md) — Write tool and Git Bash resolve `/tmp` to DIFFERENT real dirs. Use heredoc or workspace path for JSON payloads handed to curl.
- [Windows strips embedded double-quotes](feedback_windows_quote_stripping.md) — Embedded `"` in an arg gets eaten twice over: PowerShell->curl.exe (CommandLineToArgvW) AND RMM->cmd.exe. Use single-quoted heredoc `<<'JSON'` + `--data-binary @-` for bodies; build `"` from `[char]34`; or drop the quoted part (e.g. `shutdown /c`). - [Windows strips embedded double-quotes](feedback_windows_quote_stripping.md) — Embedded `"` in an arg gets eaten twice over: PowerShell->curl.exe (CommandLineToArgvW) AND RMM->cmd.exe. MECHANICAL FIX: deliver PS scripts via `.claude/scripts/ps-encoded.sh` (-EncodedCommand, byte-exact; author the file with the Write tool). Inline one-offs: single-quoted heredoc `<<'JSON'` + `--data-binary @-`; build `"` from `[char]34`.
- [Interview the AI / read its docs before probing](feedback_interview_ai_read_docs.md) — To learn an external AI/CLI's syntax or capabilities, READ its bundled docs (Grok: `~/.grok/docs/user-guide/`, `README.md`, `grok inspect`/`models`/`--help`) or interview the model; don't guess flags or run slow trial-and-error. One run to confirm a doc-derived hypothesis, not a dozen to discover. - [Interview the AI / read its docs before probing](feedback_interview_ai_read_docs.md) — To learn an external AI/CLI's syntax or capabilities, READ its bundled docs (Grok: `~/.grok/docs/user-guide/`, `README.md`, `grok inspect`/`models`/`--help`) or interview the model; don't guess flags or run slow trial-and-error. One run to confirm a doc-derived hypothesis, not a dozen to discover.
- [Web search over blind probing](feedback_web_search_over_probing.md) — For external API/capability discovery, LEAD with web search (grok/gemini) + vendor docs; live endpoint-probing only CONFIRMS a hypothesis, never the primary discovery method (it mostly 404s, "highly suspect"). Reading a system's OWN config is fine; guessing unknown PATHS is not. Web-search bots being flaky is a must-fix (CT_THOUGHTS Thought 2). - [Web search over blind probing](feedback_web_search_over_probing.md) — For external API/capability discovery, LEAD with web search (grok/gemini) + vendor docs; live endpoint-probing only CONFIRMS a hypothesis, never the primary discovery method (it mostly 404s, "highly suspect"). Reading a system's OWN config is fine; guessing unknown PATHS is not. Web-search bots being flaky is a must-fix (CT_THOUGHTS Thought 2).
- [Windows bash command mapping](feedback_windows_bash_mapping.md) — `bash` often resolves to WSL stub instead of Git/MSYS bash required by the harness. Fix by prepending `C:\Program Files\Git\bin` (and usr\bin) to PATH, or source `.claude/scripts/ensure-git-bash.ps1`. Profile has the logic; use plain `bash .claude/scripts/...` after remap. See the helper and this memory file for details. - [Windows bash command mapping](feedback_windows_bash_mapping.md) — `bash` often resolves to WSL stub instead of Git/MSYS bash required by the harness. Fix by prepending `C:\Program Files\Git\bin` (and usr\bin) to PATH, or source `.claude/scripts/ensure-git-bash.ps1`. Profile has the logic; use plain `bash .claude/scripts/...` after remap. See the helper and this memory file for details.
- [Git must authenticate non-interactively](feedback_git_noninteractive_auth.md) — Mike's gripe with Git for Windows is the constant password prompts (GCM) that hang automation, NOT the tool itself. D:\ClaudeTools is set to `credential.helper=store` primed with the azcomputerguru Gitea API token (host 172.16.3.20:3000); always set `GIT_TERMINAL_PROMPT=0`. Any never-prompts solution is acceptable. - [Git must authenticate non-interactively](feedback_git_noninteractive_auth.md) — Mike's gripe with Git for Windows is the constant password prompts (GCM) that hang automation, NOT the tool itself. D:\ClaudeTools is set to `credential.helper=store` primed with the azcomputerguru Gitea API token (host 172.16.3.20:3000); always set `GIT_TERMINAL_PROMPT=0`. Any never-prompts solution is acceptable.
- [Vault git auth — GCM shadows store token](feedback_vault_gcm_shadow_auth.md) — vault sync "Failed to authenticate user" on git.azcomputerguru.com: GCM is first in the helper chain and shadows the valid store token. Fix (machine-local): store-only credential.helper reset + pin `azcomputerguru@` in the vault remote URL so store returns the durable PAT (not the volatile OAUTH_USER JWT). Applied GURU-5070 2026-06-07. - [Vault git auth — GCM shadows store token](feedback_vault_gcm_shadow_auth.md) — vault sync "Failed to authenticate user" on git.azcomputerguru.com: GCM is first in the helper chain and shadows the valid store token. Fix (machine-local): store-only credential.helper reset + pin `azcomputerguru@` in the vault remote URL so store returns the durable PAT (not the volatile OAUTH_USER JWT). Applied GURU-5070 2026-06-07.
- [Antigravity agy.exe is not a headless CLI](reference_antigravity_agy_not_headless.md) — the `agy` skill's real backend is `@google/gemini-cli`, not the Antigravity `agy.exe` (IDE agent, no stdout, hangs). Don't reinstall agy.exe expecting headless output. Mike has a paid Gemini account, so stay on gemini-cli past the June 18 free-tier sunset (prefer `GEMINI_API_KEY`). - [agy.exe NOW works headless (2026-07-03)](reference_antigravity_agy_not_headless.md) — Antigravity `agy.exe` v1.0.6+ has a working `-p/--print` headless mode (clean stdout, `--add-dir` to read files); use it as the Gemini/Antigravity second-model path, esp. when gemini-cli OAuth is ineligible. Call by full path until PATH refresh. (Supersedes the old "agy is DOA headless" note.)
- [SQL instance role — verify by connections, not name](feedback_sql_instance_role_by_connection.md) — Standard installed under default `SQLEXPRESS` instance name is real. Prove role with `sys.dm_exec_sessions` + `Get-NetTCPConnection -OwningProcess` before recommending stop/uninstall. - [SQL instance role — verify by connections, not name](feedback_sql_instance_role_by_connection.md) — Standard installed under default `SQLEXPRESS` instance name is real. Prove role with `sys.dm_exec_sessions` + `Get-NetTCPConnection -OwningProcess` before recommending stop/uninstall.
- [RMM password setting limitation](feedback_rmm_password_limitation.md) — `net user <user> <password>` via GuruRMM fails silently (exit 0 but password doesn't set). Tested PowerShell AND CMD - both fail. ScreenConnect CMD works (also as SYSTEM). GuruRMM agent bug in process spawning. Use ScreenConnect for password ops. HIGH priority to fix. - [RMM password setting limitation](feedback_rmm_password_limitation.md) — `net user <user> <password>` via GuruRMM fails silently (exit 0 but password doesn't set). Tested PowerShell AND CMD - both fail. ScreenConnect CMD works (also as SYSTEM). GuruRMM agent bug in process spawning. Use ScreenConnect for password ops. HIGH priority to fix.
- [Clear-RecycleBin fails silently as SYSTEM](feedback_clear_recyclebin_system_context.md) — RMM-dispatched cleanup scripts cannot use `Clear-RecycleBin -Force`; the cmdlet uses Shell COM and silently no-ops without an interactive desktop. Enumerate `C:\$Recycle.Bin\<SID>\*` directly. - [Clear-RecycleBin fails silently as SYSTEM](feedback_clear_recyclebin_system_context.md) — RMM-dispatched cleanup scripts cannot use `Clear-RecycleBin -Force`; the cmdlet uses Shell COM and silently no-ops without an interactive desktop. Enumerate `C:\$Recycle.Bin\<SID>\*` directly.
@@ -85,16 +113,19 @@
- [Calibrate effort to stakes](feedback_calibrate_effort_to_stakes.md) — Don't over-verify or over-engineer low-consequence details; confirm the happy path, note the limitation, and take the simplest path (e.g. put the instruction in the prompt) instead of building robust mechanisms. - [Calibrate effort to stakes](feedback_calibrate_effort_to_stakes.md) — Don't over-verify or over-engineer low-consequence details; confirm the happy path, note the limitation, and take the simplest path (e.g. put the instruction in the prompt) instead of building robust mechanisms.
- [Pricing verification — no guessing](policy_pricing_verification.md) — ANY cost presented to the team or a client MUST be verified via live web lookup (WebFetch/WebSearch, fallback to headless Chrome). Never estimate from training data. Cite source + date inline. If unreachable, say so — do NOT substitute a guess. - [Pricing verification — no guessing](policy_pricing_verification.md) — ANY cost presented to the team or a client MUST be verified via live web lookup (WebFetch/WebSearch, fallback to headless Chrome). Never estimate from training data. Cite source + date inline. If unreachable, say so — do NOT substitute a guess.
- [Client communication tone](feedback_client_tone.md) — How to write client-facing Syncro comments — expert partner, not intake questionnaire. - [Client communication tone](feedback_client_tone.md) — How to write client-facing Syncro comments — expert partner, not intake questionnaire.
- [Impeccable on outbound](feedback_impeccable_on_outbound.md) — Run the `impeccable` skill on anything sent to a client or vendor before delivery; internal drafts exempt.
- [Default to inline links](feedback_inline_links.md) — Use `[text](url)` inline markdown links (clickable, wrap-safe) not bare URLs in code fences; exception = raw URL the user must copy/paste. - [Default to inline links](feedback_inline_links.md) — Use `[text](url)` inline markdown links (clickable, wrap-safe) not bare URLs in code fences; exception = raw URL the user must copy/paste.
- [Add Mike as owner on all Entra apps](feedback_entra_app_owner.md) — Apps created via management SP have no user owner — must add Mike manually or publisher verification fails. - [Add Mike as owner on all Entra apps](feedback_entra_app_owner.md) — Apps created via management SP have no user owner — must add Mike manually or publisher verification fails.
- [No TOML/config file approach for endpoints](feedback_no_toml_config_endpoints.md) — User explicitly prohibits TOML or config-file-based endpoint configuration — this will never be approved. - [No TOML/config file approach for endpoints](feedback_no_toml_config_endpoints.md) — User explicitly prohibits TOML or config-file-based endpoint configuration — this will never be approved.
- [Python on Windows — use py launcher](feedback_python_windows.md) — Windows Store python/python3 aliases disabled; always use py or jq on DESKTOP-0O8A1RL. - [Python on Windows — use py launcher](feedback_python_windows.md) — Windows Store python/python3 aliases disabled; always use py or jq on DESKTOP-0O8A1RL.
- [Memory tooling may delete now — additive-only constraint dropped](feedback_memory_sync_destructive_ok.md) — As of 2026-06-02, memory-dream and sync-memory.sh are sanctioned to perform destructive ops (apply proposed merges/dedups, propagate repo deletions back to harness profile stores). Onboarding-phase safety net now fights deliberate consolidation (e.g. 2026-06-01's 39 deletions resurrected on the next sync). Script updates pending. - [Memory tooling may delete now — additive-only constraint dropped](feedback_memory_sync_destructive_ok.md) — As of 2026-06-02, memory-dream and sync-memory.sh are sanctioned to perform destructive ops (apply proposed merges/dedups, propagate repo deletions back to harness profile stores). Onboarding-phase safety net now fights deliberate consolidation (e.g. 2026-06-01's 39 deletions resurrected on the next sync). Script updates pending.
- [Unsaved sessions are recoverable from transcripts](feedback_session_recovery.md) — Crashed/closed-before-save sessions live in `~/.claude/projects/<slug>/*.jsonl`; the detector auto-recovers orphans, `/recover <uuid>` does it manually. Ollama prose + Python verbatim. See `.claude/RECOVERY.md`. - [Unsaved sessions are recoverable from transcripts](feedback_session_recovery.md) — Crashed/closed-before-save sessions live in `~/.claude/projects/<slug>/*.jsonl`; the detector auto-recovers orphans, `/recover <uuid>` does it manually. Ollama prose + Python verbatim. See `.claude/RECOVERY.md`.
- [Scheduled tasks must not flash a console](feedback_scheduled_task_no_console_flash.md) — A task action running bash.exe/py.exe/python.exe with Hidden=False draws a console window every fire (the recurring "command prompt opening/closing"). Wrap bash via a wscript VBS (style 0), use pythonw.exe for python, always add `-Hidden`. Fixed installers: register-edr-watcher.ps1, register-orphan-detector.ps1.
- [agy review is not read-only](feedback_agy_review_not_readonly.md) — agy review/review-files CAN write files + run npm despite docs claiming plan-mode; always git diff after and treat Gemini's output as a proposal to validate, not trusted/finished work. - [agy review is not read-only](feedback_agy_review_not_readonly.md) — agy review/review-files CAN write files + run npm despite docs claiming plan-mode; always git diff after and treat Gemini's output as a proposal to validate, not trusted/finished work.
- [Don't present inferred topology as fact](feedback_no_inferred_topology_as_fact.md) — Private-IP overlap (172.16.x on both sides) is NOT proof of a site-to-site link; I fabricated a VWP<->office VPN. State observations vs inferences; a failed reachability test disproves a link, don't explain it away; test "can reach RMM" against the EXTERNAL endpoint, not internal 172.16.3.30. - [Don't present inferred topology as fact](feedback_no_inferred_topology_as_fact.md) — Private-IP overlap (172.16.x on both sides) is NOT proof of a site-to-site link; I fabricated a VWP<->office VPN. State observations vs inferences; a failed reachability test disproves a link, don't explain it away; test "can reach RMM" against the EXTERNAL endpoint, not internal 172.16.3.30.
### Syncro ### Syncro
- [Skill-first routing + Definition of Done](feedback_skill_first_routing.md) — If a skill covers the task, INVOKE IT (Syncro billing ALWAYS via `/syncro`, never ad-hoc curl), AND gate the result with the matching check-skill before "done" (code→/code-review+/simplify+/security-review; outbound→impeccable/stop-slop; wiki/memory→wiki-lint/memory-dream) — inferred from the request, not user-named. CORE rule; full map in `.claude/SKILL_ROUTING.md`; calibrate to stakes, Ollama Tier-0 for cheap passes.
- [Syncro API plumbing](feedback_syncro_api.md) — Content-Type required on all POST/PUT; NO idempotency anywhere — always GET before retrying; response wrappers (`.ticket.id`, `.comment.id`); add_line_item shape (internal ID, flat response, required fields); HTML uses `<br>` not `<ul>/<li>`; timer_entry response is FLAT but SUPERSEDED (use add_line_item). - [Syncro API plumbing](feedback_syncro_api.md) — Content-Type required on all POST/PUT; NO idempotency anywhere — always GET before retrying; response wrappers (`.ticket.id`, `.comment.id`); add_line_item shape (internal ID, flat response, required fields); HTML uses `<br>` not `<ul>/<li>`; timer_entry response is FLAT but SUPERSEDED (use add_line_item).
- [Syncro billing rules](feedback_syncro_billing.md) — Bill with `add_line_item` directly (not timers); fetch rates LIVE; never invent labor names (real product names only); match labor type to delivery channel (never "Prepaid project labor"); labor `taxable:false` (AZ); warranty `1049360` (never patch price); emergency `26184` ×1.5 once, branch by `prepay_hours`; corrections preserve original tech's user_id; estimate hardware `32252`. - [Syncro billing rules](feedback_syncro_billing.md) — Bill with `add_line_item` directly (not timers); fetch rates LIVE; never invent labor names (real product names only); match labor type to delivery channel (never "Prepaid project labor"); labor `taxable:false` (AZ); warranty `1049360` (never patch price); emergency `26184` ×1.5 once, branch by `prepay_hours`; corrections preserve original tech's user_id; estimate hardware `32252`.
- [Syncro workflow rules](feedback_syncro_workflow.md) — ALWAYS preview comments before posting (no exceptions); verify appointment day-of-week ("Saturday 2026-05-23") before creating; ASK who the appointment owner is; leave `contact_id` BLANK by default for ALL customers (ignore Syncro's contact-picker auto-default). - [Syncro workflow rules](feedback_syncro_workflow.md) — ALWAYS preview comments before posting (no exceptions); verify appointment day-of-week ("Saturday 2026-05-23") before creating; ASK who the appointment owner is; leave `contact_id` BLANK by default for ALL customers (ignore Syncro's contact-picker auto-default).
@@ -112,27 +143,36 @@
- [Cascades FR GPO fix](reference_cascades_fr_gpo_fix.md) — Native Folder Redirection was DOA on every machine: redirect targets were in a misnamed `fdeploy1.ini` (Windows reads `fdeploy.ini`) → empty target path → silent no-op → per-user registry workaround every time. Fixed 2026-06-08 (correct fdeploy.ini + version bump). Also: CS-SERVER live RMM agent is `c39f1de7...` (old `6766e973` stale). - [Cascades FR GPO fix](reference_cascades_fr_gpo_fix.md) — Native Folder Redirection was DOA on every machine: redirect targets were in a misnamed `fdeploy1.ini` (Windows reads `fdeploy.ini`) → empty target path → silent no-op → per-user registry workaround every time. Fixed 2026-06-08 (correct fdeploy.ini + version bump). Also: CS-SERVER live RMM agent is `c39f1de7...` (old `6766e973` stale).
- [pfSense 25.07 ops quirks](reference_pfsense_25_07_ops.md) — Cascades pfSense Plus 25.07: logs are PLAIN TEXT (use tail/grep, NOT clog → clog returns empty); clean dhcpd restart = `services_dhcpd_configure()` via slow pfSsh.php (needs 50s+ timeout); dirty boot can leave 2 dhcpd → DISCOVER/OFFER but no ACK; reboot the Cox modem after a config restore; ZFS survives power loss. From the 2026-06-17 power-outage incident. - [pfSense 25.07 ops quirks](reference_pfsense_25_07_ops.md) — Cascades pfSense Plus 25.07: logs are PLAIN TEXT (use tail/grep, NOT clog → clog returns empty); clean dhcpd restart = `services_dhcpd_configure()` via slow pfSsh.php (needs 50s+ timeout); dirty boot can leave 2 dhcpd → DISCOVER/OFFER but no ACK; reboot the Cox modem after a config restore; ZFS survives power loss. From the 2026-06-17 power-outage incident.
- [feedback_ascii_only_api_payloads](feedback_ascii_only_api_payloads.md) -- On Windows/Git-bash, non-ASCII chars (em-dash, arrow, smart quotes) in JSON payload TEXT passed to curl get mangled and rejected — Discord bot-alert returns 400, the coord API returns "error parsing the body". Use ASCII-only in API payload text, or a single-quoted heredoc. - [feedback_ascii_only_api_payloads](feedback_ascii_only_api_payloads.md) -- On Windows/Git-bash, non-ASCII chars (em-dash, arrow, smart quotes) in JSON payload TEXT passed to curl get mangled and rejected — Discord bot-alert returns 400, the coord API returns "error parsing the body". Use ASCII-only in API payload text, or a single-quoted heredoc.
- [feedback_bitdefender_unattended_install](feedback_bitdefender_unattended_install.md) -- Bitdefender unattended RMM install must use the FULL KIT as SYSTEM (silent, no UAC) — the downloader stub fails headless and triggers UAC
- [feedback_rmm_longops_fire_and_forget](feedback_rmm_longops_fire_and_forget.md) -- Long-running RMM endpoint ops (software installs, big downloads) must be fire-and-forget, not live-monitored
## Machine ## Machine
- [GURU-5070 Workstation Setup](reference_workstation_setup.md) — Mike's primary (owner confirmed 2026-05-26). Windows 11 Pro. Renamed from OC-5070 → ACG-5070/acg-guru-5070 → GURU-5070; all the same box, all Mike's. - [GURU-5070 Workstation Setup](reference_workstation_setup.md) — Mike's primary (owner confirmed 2026-05-26). Windows 11 Pro. Renamed from OC-5070 → ACG-5070/acg-guru-5070 → GURU-5070; all the same box, all Mike's.
- [GURU-BEAST-ROG Setup Status](machine_windows_guru_setup_status.md) — Windows workstation fully configured except SSH key deployment to servers. - [GURU-BEAST-ROG Setup Status](machine_windows_guru_setup_status.md) — Windows workstation fully configured except SSH key deployment to servers.
## Project ## Project
- [GuruRMM software-removal PR #58](project_gururmm_software_removal_pr58.md) — SPEC-030 hardening (driver exclusion in removal UI, lingering-uninstaller sweep, job list + stale reaper) on PR #58, UNMERGED; merge = beta deploy; live testing planned 2026-07-06 on DESKTOP-MS42HNC.
- [Cascades network + CS-SERVER SMB instability](project_cascades_network_segments.md) — NAS->CS-SERVER migration. NOT a CSC-ENT-vs-CSCNet issue (corrected): fresh SMB to `\\cs-server\*` fails err 67 from BOTH Wi-Fis; only persistent admin-mapped drives work, intermittently → CS-SERVER-side SMB instability (multichannel advertises .248/.254/IPv6 ULAs; Get-SmbServerConfiguration errors). Blockers: CSCNet=WPA3 (old adapters can't join); workstations store domain-admin cred. Repoint tool [[drive-map]].
- [CyndyOffice physical HP lockups](cyndyoffice-physical-hp-lockups.md) — RMM "Howard-VM" site agent CyndyOffice is a PHYSICAL HP Pavilion TP01 (not a VM); ~20 hard freezes/6wk = Kernel-Power 41 bugcheck-0, no dump/WHEA = hardware (RAM/PSU/BIOS), SSD healthy. UUID re-enrolls. - [CyndyOffice physical HP lockups](cyndyoffice-physical-hp-lockups.md) — RMM "Howard-VM" site agent CyndyOffice is a PHYSICAL HP Pavilion TP01 (not a VM); ~20 hard freezes/6wk = Kernel-Power 41 bugcheck-0, no dump/WHEA = hardware (RAM/PSU/BIOS), SSD healthy. UUID re-enrolls.
- [Automate memory consolidation/lint (phased)](project_memory_consolidation_automation.md) — Eventually auto-run /memory-dream; lint+additive fixes can automate early, merges/deletes stay human-approved. Engine: .claude/skills/memory-dream/ + .claude/scripts/sync-memory.sh. - [Automate memory consolidation/lint (phased)](project_memory_consolidation_automation.md) — Eventually auto-run /memory-dream; lint+additive fixes can automate early, merges/deletes stay human-approved. Engine: .claude/skills/memory-dream/ + .claude/scripts/sync-memory.sh.
- [Trebesch PST consolidation (staged)](project_trebesch_pst_consolidation.md) — Address-book CSV from 24 PSTs on DESKTOP-QNP3ON5; scripts staged at .claude/tmp/treb-*.ps1, WAITING for Howard's 6pm-MST 2026-06-01 go signal (attended run). See [[reference_trebesch_qnp3on5]]. - [Trebesch PST consolidation (staged)](project_trebesch_pst_consolidation.md) — Address-book CSV from 24 PSTs on DESKTOP-QNP3ON5; scripts staged at .claude/tmp/treb-*.ps1, WAITING for Howard's 6pm-MST 2026-06-01 go signal (attended run). See [[reference_trebesch_qnp3on5]].
- [GuruRMM security scope — integrate AV, don't replace it](project_gururmm_security_scope.md) — No native virus/malware removal in the RMM; AV products do that. RMM monitors AV reports + sends commands to AV products, and its built-in value is helping techs FIND issues. Program removal is a separate feature.
- [GuruRMM project state](project_gururmm.md) — Dev principles (every feature full-stack: backend+API+UI+docs+scalability; product works without AI; FEATURE_ROADMAP update is part of definition-of-done; mirrors guru-rmm/docs/DESIGN.md). Webhook docs-only build guard (SPEC-020 Phase 0; webhook-handler.py repo copy is STALE — don't redeploy). Mac install-hooks.sh setup STILL PENDING on Mikes-MacBook-Air. - [GuruRMM project state](project_gururmm.md) — Dev principles (every feature full-stack: backend+API+UI+docs+scalability; product works without AI; FEATURE_ROADMAP update is part of definition-of-done; mirrors guru-rmm/docs/DESIGN.md). Webhook docs-only build guard (SPEC-020 Phase 0; webhook-handler.py repo copy is STALE — don't redeploy). Mac install-hooks.sh setup STILL PENDING on Mikes-MacBook-Air.
- [GuruConnect](project_guruconnect.md) — v2 direction (native-first full key fidelity Win+R/Ctrl+Alt+Del + bidirectional file cut/paste/drag; WebRTC fallback only; standalone-first + RMM contract; tenancy-ready schema; Mike willing to scrap v1). Manual deploy procedure to 172.16.3.30 (build-on-server in login shell; sqlx runtime queries; NPM `CONNECT_TRUSTED_PROXIES=172.16.3.20` gotcha). v2 live since 2026-05-30. - [GuruConnect](project_guruconnect.md) — v2 direction (native-first full key fidelity Win+R/Ctrl+Alt+Del + bidirectional file cut/paste/drag; WebRTC fallback only; standalone-first + RMM contract; tenancy-ready schema; Mike willing to scrap v1). Manual deploy procedure to 172.16.3.30 (build-on-server in login shell; sqlx runtime queries; NPM `CONNECT_TRUSTED_PROXIES=172.16.3.20` gotcha). v2 live since 2026-05-30.
- [Apple MDM + Developer certs (GuruRMM mobile)](project_apple_mdm_certs.md) — ACG holds Apple Developer+signing and Apple MDM Push certs (acquired 2026-05-29) for SPEC-017. MDM push cert RENEWS ANNUALLY on the same Apple ID or all enrolled iOS devices break. - [Apple MDM + Developer certs (GuruRMM mobile)](project_apple_mdm_certs.md) — ACG holds Apple Developer+signing and Apple MDM Push certs (acquired 2026-05-29) for SPEC-017. MDM push cert RENEWS ANNUALLY on the same Apple ID or all enrolled iOS devices break.
- [Only RMM & GC are versionable products](project_versionable_products.md) — GuruRMM + GuruConnect are the only products with own repos/submodules; everything else stays in the claudetools monorepo. Split only for independent pipeline OR versioned external consumer. - [Only RMM & GC are versionable products](project_versionable_products.md) — GuruRMM + GuruConnect are the only products with own repos/submodules; everything else stays in the claudetools monorepo. Split only for independent pipeline OR versioned external consumer.
- [GuruRMM legacy 1.77 build disabled](project_gururmm_legacy_disabled.md) — legacy (2008R2/Win7) Rust 1.77 variant DISABLED 2026-07-04 (`LEGACY_ENABLED=false`) after an edition2024 dep (chacha20/rand_core 0.10.1) broke the unpinned 1.77 re-resolve and fail-closed the whole Windows build; existing legacy artifacts preserved at 0.6.76. Do NOT re-enable blindly — 2008R2 support returns via a C++/.NET or clean-rewrite legacy agent (Mike, required).
- [Quantum GoDaddy M365 tenant](project_quantum_godaddy_m365_tenant.md) — quantumwms.com parked in a GoDaddy-provisioned M365 tenant (id ddf3d2c9-b76c-40d9-a216-9f11a1a26f97, netorg18235235.onmicrosoft.com); blocks Pax8 migration until GoDaddy removed. - [Quantum GoDaddy M365 tenant](project_quantum_godaddy_m365_tenant.md) — quantumwms.com parked in a GoDaddy-provisioned M365 tenant (id ddf3d2c9-b76c-40d9-a216-9f11a1a26f97, netorg18235235.onmicrosoft.com); blocks Pax8 migration until GoDaddy removed.
- [Howard-Home LAN shadow (RESOLVED)](howard-home-lan-shadow.md) — Howard-Home renumbered 2026-06-16 to **10.137.42.0/24** (gw 10.137.42.1, UniFi — NOT pfSense), off the old 192.168.0.0/24 that shadowed Cascades pfSense .0.x over the VPN. Cascades .0.x should now route via the tunnel; this machine is 10.137.42.x now (not 192.168.0.x). - [Howard-Home LAN shadow (RESOLVED)](howard-home-lan-shadow.md) — Howard-Home renumbered 2026-06-16 to **10.137.42.0/24** (gw 10.137.42.1, UniFi — NOT pfSense), off the old 192.168.0.0/24 that shadowed Cascades pfSense .0.x over the VPN. Cascades .0.x should now route via the tunnel; this machine is 10.137.42.x now (not 192.168.0.x).
- [Cascades CARF tech plan](project_cascades_carf_tech_plan.md) — Ashley's "technology plan" is a CARF accreditation deliverable (Aging Services Technology and System Plan standard); must use CARF action-plan structure (owner/cost/target+completion dates per area), persons-served assistive-tech lens, annual-review sign-off; it's Cascades' leadership-adopted plan, ACG supplies content.
- [Cascades](project_cascades.md) — Active state: Syncro ticket #110680053 + plan file (machine-specific path on Howard's box), admin accounts (sysadmin@=Howard, admin@=Mike — daily-driver, NOT break-glass), Phase-B caregiver CA pilot (SG-Caregivers-Pilot, group-scoped never tenant-wide), prepaid block ~37.5h (rate TBD), pilot cleanup checklist. - [Cascades](project_cascades.md) — Active state: Syncro ticket #110680053 + plan file (machine-specific path on Howard's box), admin accounts (sysadmin@=Howard, admin@=Mike — daily-driver, NOT break-glass), Phase-B caregiver CA pilot (SG-Caregivers-Pilot, group-scoped never tenant-wide), prepaid block ~37.5h (rate TBD), pilot cleanup checklist.
- [Cascades history](project_cascades_history.md) — fdeploy 502/ACL root cause (Flags=1211→187 fix), 2026-04-29 CA-rescoping decision (Howard pulled the brakes on tenant-wide), 2026-05-14 per-user-security-group decision rationale. - [Cascades history](project_cascades_history.md) — fdeploy 502/ACL root cause (Flags=1211→187 fix), 2026-04-29 CA-rescoping decision (Howard pulled the brakes on tenant-wide), 2026-05-14 per-user-security-group decision rationale.
- [Cascades isolated-VLAN pattern](project_cascades_isolated_vlan_pattern.md) — pfSense: the GUEST VLAN (VLAN50/igc1.50) is the isolation template (4 any-proto quick rules: block 192.168.0.0/22 + 10.0.0.0/8 + 172.16.0.0/12, then pass any; public DNS via DHCP). VLAN20 is NOT isolated. Verify with `pfctl -sr`, not config.xml. Protocol MUST be Any (TCP-only leaks UDP). VOICE VLAN30 built to this 2026-06-17. - [Cascades isolated-VLAN pattern](project_cascades_isolated_vlan_pattern.md) — pfSense: the GUEST VLAN (VLAN50/igc1.50) is the isolation template (4 any-proto quick rules: block 192.168.0.0/22 + 10.0.0.0/8 + 172.16.0.0/12, then pass any; public DNS via DHCP). VLAN20 is NOT isolated. Verify with `pfctl -sr`, not config.xml. Protocol MUST be Any (TCP-only leaks UDP). VOICE VLAN30 built to this 2026-06-17.
- [Cascades VLAN20 migration + routing](project_cascades_vlan20_migration_routing.md) — Staff machines/printers moving to VLAN20 (10.0.20.0/24). CS-SERVER couldn't reach VLAN20 printers because the LAN "allow LAN to any" rule policy-routes via WAN_Group → add a top LAN pass rule (src CS-SERVER 192.168.2.248, dst 10.0.20.0/24, gw=default) to bypass. pfSense SSH from VPN is blocked (do firewall in GUI). Printer client-map via GPO or SYSTEM `printui /ga` to dodge the 0x800702e4 PrintNightmare prompt; build UNC with [char]92.
- [Cascades KPI dashboard (parked)](project_cascades_kpi_dashboard.md) — Ashley Jensen wants one dashboard across their reporting SaaS (ALIS/QuickBooks/Bill.com/Relias/You've Got Leads/TELS/Focus HR/Helpany/POS). Power BI Gateway is the WRONG frame (on-prem only). Recommended Tier1→Tier2: scheduled exports → SharePoint → Power BI Pro, automate API-capable systems (Bill.com/QBO) via Power Automate later. Full notes: `clients/cascades-tucson/docs/proposals/kpi-dashboard.md`. Next: draft client one-pager. - [Cascades KPI dashboard (parked)](project_cascades_kpi_dashboard.md) — Ashley Jensen wants one dashboard across their reporting SaaS (ALIS/QuickBooks/Bill.com/Relias/You've Got Leads/TELS/Focus HR/Helpany/POS). Power BI Gateway is the WRONG frame (on-prem only). Recommended Tier1→Tier2: scheduled exports → SharePoint → Power BI Pro, automate API-capable systems (Bill.com/QBO) via Power Automate later. Full notes: `clients/cascades-tucson/docs/proposals/kpi-dashboard.md`. Next: draft client one-pager.
- [Sync script bug — untracked files (RESOLVED)](project_sync_script_bug.md) — FIXED 2026-05-21: sync.sh now uses `git status --porcelain` for change detection (repo + vault). - [Sync script bug — untracked files (RESOLVED)](project_sync_script_bug.md) — FIXED 2026-05-21: sync.sh now uses `git status --porcelain` for change detection (repo + vault).
- [MasterBooter Side Project](project_masterbooter.md) — Howard's Rust+Slint Windows deployment toolkit at C:\MasterBooter, separate from client work. Do not log to clients/. - [MasterBooter Side Project](project_masterbooter.md) — Howard's Rust+Slint Windows deployment toolkit at C:\MasterBooter, separate from client work. Do not log to clients/.
- [AMPIPIT toolkit](project_ampipit.md) — Howard's ACTIVE Rust+Slint Windows deploy/recovery toolkit (not just a reference program), migrated into the fleet as a private Gitea submodule at projects/msp-tools/ampipit (azcomputerguru/ampipit). Active feature: recovery partition with GuruRMM agent + built-in repair scripts for offline-Windows repair (spike GO-WITH-WORK). Preserve all work; PRIVATE Gitea only, no public GitHub.
- [Audio Processor Architecture](project_audio_processor_architecture.md) — Segment-first pipeline: detect breaks before transcription for complete content capture. - [Audio Processor Architecture](project_audio_processor_architecture.md) — Segment-first pipeline: detect breaks before transcription for complete content capture.
- [Neptune SBR Email Routing Setup](project_neptune_sbr_email_routing.md) — Full SBR routing chain, config file locations, MailProtector integration, access methods. Treat routing breakage as systemic (devcon, Sorensen/rieussetcorp), not per-client. - [Neptune SBR Email Routing Setup](project_neptune_sbr_email_routing.md) — Full SBR routing chain, config file locations, MailProtector integration, access methods. Treat routing breakage as systemic (devcon, Sorensen/rieussetcorp), not per-client.
- [Dataforth Test Datasheet Pipeline](project_datasheet_pipeline.md) — Full pipeline rebuilt 2026-03-27. Server-side generation replaces DFWDS/Uploader. Website upload still broken. - [Dataforth Test Datasheet Pipeline](project_datasheet_pipeline.md) — Full pipeline rebuilt 2026-03-27. Server-side generation replaces DFWDS/Uploader. Website upload still broken.
@@ -140,10 +180,12 @@
- [Dataforth history (2026-03-27 incident)](project_dataforth_history.md) — DF-JOEL2 compromise via ScreenConnect social-engineering, attacker C2 IPs + IC3 case + remediation log + MFA rollout origin story + Joel Lohr retirement. RESOLVED 2026-04-04. - [Dataforth history (2026-03-27 incident)](project_dataforth_history.md) — DF-JOEL2 compromise via ScreenConnect social-engineering, attacker C2 IPs + IC3 case + remediation log + MFA rollout origin story + Joel Lohr retirement. RESOLVED 2026-04-04.
- [Radio show co-host — Tara, not Tom](radio_show_no_cohost_named_tom.md) — Co-host in 2014-s6e19 and 2016-s8e43 is Tara. "Tom" was hallucinated; rename complete. - [Radio show co-host — Tara, not Tom](radio_show_no_cohost_named_tom.md) — Co-host in 2014-s6e19 and 2016-s8e43 is Tara. "Tom" was hallucinated; rename complete.
- [Proposal: centralize config in identity.json](proposal_identity_centralization.md) — Rationale for the identity.json machine-config centralization (claudetools_root, ollama/python); now implemented. - [Proposal: centralize config in identity.json](proposal_identity_centralization.md) — Rationale for the identity.json machine-config centralization (claudetools_root, ollama/python); now implemented.
- [Defender: exempt all Claude activity](feedback_defender_claude_exclusions.md) — Mike wants NOTHING Claude/ClaudeTools issues flagged by Defender; ClickFix threat IDs 2147939088/2147945138 (RMM curl dispatch AMSI FPs) set to Allow + broad process/path exclusions. ThreatID-Allow is the lever; exclusions alone don't stop AMSI CmdLine detections.
- [ACG MSP tool stack](reference_acg_msp_stack.md) — ScreenConnect/CW Control, Splashtop, Syncro, Datto RMM, Datto EDR/AV, GuruRMM are ACG's OWN tools; do not flag as foreign/threat on managed machines (Defender-off is expected when Datto AV is active). - [ACG MSP tool stack](reference_acg_msp_stack.md) — ScreenConnect/CW Control, Splashtop, Syncro, Datto RMM, Datto EDR/AV, GuruRMM are ACG's OWN tools; do not flag as foreign/threat on managed machines (Defender-off is expected when Datto AV is active).
- [VoIP vendor stack: PacketDial / OIT / NetSapiens / YMCS](reference_packetdial_oit_netsapiens.md) — PacketDial = ACG's VoIP-dept brand (pbx.packetdial.com, the `packetdial` skill); NetSapiens = the PBX platform (API v2); OIT/OITVOIP = white-label wholesaler running NetSapiens (api.ucaasnetwork.com); reseller `91912.service`. YMCS (Yealink) = phone device-mgmt, pairs with the PBX.
- [ACG Website Hosting](project_azcomputerguru_hosting.md) — azcomputerguru.com is hosted on IX Web Hosting via cPanel. - [ACG Website Hosting](project_azcomputerguru_hosting.md) — azcomputerguru.com is hosted on IX Web Hosting via cPanel.
- [jq on Windows emits CRLF](feedback_jq_crlf_windows.md) — winget jq outputs CRLF; trailing \r silently breaks `for x in $(jq ...)` loops + read-from-@tsv. Override `jq(){ command jq "$@"|tr -d '\r'; }`. Windows-build-specific (passes on Mac/Linux). - [jq on Windows emits CRLF](feedback_jq_crlf_windows.md) — winget jq outputs CRLF; trailing \r silently breaks `for x in $(jq ...)` loops + read-from-@tsv. Override `jq(){ command jq "$@"|tr -d '\r'; }`. Windows-build-specific (passes on Mac/Linux).
- [ScreenConnect RESTful API auth](reference_screenconnect_api.md) — CTRLAuthHeader = raw api_secret (no Basic/b64) + Origin header; only method is GetSessionsByName; matches blank-for-agents Name field so it cannot enumerate full inventory. - [ScreenConnect RESTful API auth](reference_screenconnect_api.md) — CTRLAuthHeader = raw api_secret (no Basic/b64) + Origin header; now wrapped by the /screenconnect skill. Verified surface: GetSessionsByName/GetSessionDetails + writes SendCommand/SendMessage/UpdateCustomProperties + parameterized self-tagging installer. Still NO full-fleet inventory method (GetSessions missing).
- [No manufactured guardrails on our products](feedback_no_manufactured_guardrails.md) — At Mikes request on GuruRMM/GuruConnect/ClaudeTools, just execute; stop only for genuinely irreversible/destructive ops (with a heads-up). Read the actual code/state before claiming something is disallowed or a security hole. - [No manufactured guardrails on our products](feedback_no_manufactured_guardrails.md) — At Mikes request on GuruRMM/GuruConnect/ClaudeTools, just execute; stop only for genuinely irreversible/destructive ops (with a heads-up). Read the actual code/state before claiming something is disallowed or a security hole.
- [Stream-of-thought design convos](feedback_stream_of_thought_design.md) — Mike brainstorms features free-form, adding requirements iteratively; Claude validates/sharpens as a design partner but does NOT build until an explicit go, then captures parked threads durably (PARKED_*.md + todos) for a later /shape-spec. - [Stream-of-thought design convos](feedback_stream_of_thought_design.md) — Mike brainstorms features free-form, adding requirements iteratively; Claude validates/sharpens as a design partner but does NOT build until an explicit go, then captures parked threads durably (PARKED_*.md + todos) for a later /shape-spec.
- [RMM Thoughts backlog](feedback_rmm_thoughts_backlog.md) — GuruRMM ideas from Mike & Howard go in projects/msp-tools/guru-rmm/docs/RMM_THOUGHTS.md (Status: Raw); pipeline thought -> discuss -> spec (/shape-spec) -> roadmap. Don't build until an explicit go. - [RMM Thoughts backlog](feedback_rmm_thoughts_backlog.md) — GuruRMM ideas from Mike & Howard go in projects/msp-tools/guru-rmm/docs/RMM_THOUGHTS.md (Status: Raw); pipeline thought -> discuss -> spec (/shape-spec) -> roadmap. Don't build until an explicit go.
@@ -168,3 +210,20 @@
- [AI-auth product boundary](project_ai_auth_product_boundary.md) — ClaudeTools/ClaudeTools 3.0 = internal-only, per-person subscription OAuth ok; GuruRMM = sellable, customer brings own API key (never ACG's subscription); backend dev = internal. Anthropic ToS bans subscription auth in third-party products. - [AI-auth product boundary](project_ai_auth_product_boundary.md) — ClaudeTools/ClaudeTools 3.0 = internal-only, per-person subscription OAuth ok; GuruRMM = sellable, customer brings own API key (never ACG's subscription); backend dev = internal. Anthropic ToS bans subscription auth in third-party products.
- [RMM SYSTEM context can't see user mapped drives](feedback_rmm_system_context_mapped_drives.md) — RMM runs as SYSTEM; `Test-Path F:\` etc. is False even when the user's mapped/redirected drive exists. Diagnose mapped-drive/redirect issues in `context:user_session`. Elevated apps (e.g. QB DB Server Manager "unable to retrieve root folder") need `EnableLinkedConnections=1` + reboot. - [RMM SYSTEM context can't see user mapped drives](feedback_rmm_system_context_mapped_drives.md) — RMM runs as SYSTEM; `Test-Path F:\` etc. is False even when the user's mapped/redirected drive exists. Diagnose mapped-drive/redirect issues in `context:user_session`. Elevated apps (e.g. QB DB Server Manager "unable to retrieve root folder") need `EnableLinkedConnections=1` + reboot.
- [AD2 = Dataforth-ops fork](project_ad2_dataforth_fork.md) — branch ad2 = main + thin Dataforth layer; keep fork edits ADDITIVE (Dataforth context in clients/dataforth/CLAUDE.dataforth.md, NOT .claude/CLAUDE.md); rebase onto main directly when sync.sh self-lock hits; no vault/jq/sops/age on this box. - [AD2 = Dataforth-ops fork](project_ad2_dataforth_fork.md) — branch ad2 = main + thin Dataforth layer; keep fork edits ADDITIVE (Dataforth context in clients/dataforth/CLAUDE.dataforth.md, NOT .claude/CLAUDE.md); rebase onto main directly when sync.sh self-lock hits; no vault/jq/sops/age on this box.
- [GuruScan verification IN TEST / paused](project_guruscan_in_test_paused.md) — multi-engine scanner verify on DESKTOP-MS42HNC paused 2026-06-22 (VM rebooted mid-Emsisoft run); HitmanPro done (36 removed), Emsisoft full-scan unverified; resume `guruscan-agent-test.sh DESKTOP-MS42HNC scan-one Emsisoft`; Defender RTP/Tamper still off on VM
- [GuruRMM fleet dispatch-hang fix](project_gururmm_dispatch_hang_fix.md) — blocking send_to on a full bounded channel to one black-holed agent wedged ALL command dispatch; fixed with try_send (9dae20c, deployed); proper black-hole eviction still missing (was reverted in 80df458) — finish it if it recurs
- [Windows won't-boot / offline DISM repair playbook](windows-offline-dism-repair-gotchas.md) — Automatic Repair loop = boot-critical fault (disk/registry/wedged update), NOT shell/appx store corruption (that's a symptom); `FaultyPackageInProgress` + 100s of Install/Uninstall-Pending packages = wedged CU -> RevertPendingActions or clean install. Offline DISM rejects `wim:` source (0x800f082e) -> MOUNT the wim, source `\Windows`. Ventoy breaks WIM mount (0xc1420134) -> use Rufus. 25H2(26200)=24H2(26100)+enablement, so match 26100 media. First hit: Four Paws AvImark #32447.
- [365 app suite — authoritative map + consent-drift fix](reference_365_app_suite.md) — full map in `.claude/skills/remediation-tool/references/app-suite.md`; per-tenant consent is NOT uniform (VWP had the app but no SharePoint role). Run `consent-audit.sh <tenant|--all>` to detect gaps; fix via adminconsent URL or direct appRoleAssignment grant.
- [Remediation-tool has full M365 access (incl. SharePoint)](reference_remediation_tool_365_access.md) — the app suite covers Graph/EXO/Defender/SharePoint; don't declare "no access" on an accessDenied. SharePoint app-only needs a CERT (secret = "Unsupported app only token"); use get-token.sh `sharepoint`/`sharepoint-admin` tiers + CSOM admin API (Graph /admin/sharepoint/settings scope not held). Full map: skill references/app-permissions-and-sharepoint.md.
- [AV migration: Bitdefender -> Datto EDR](project_av_migration_bitdefender_to_edr.md) — retire Bitdefender fleet-wide, ONLY exception Glaztech (Dataforth migrates); end-state per machine = GuruRMM + Datto EDR
- [RMM deploy via ScreenConnect](reference_rmm_deploy_via_screenconnect.md) — push GuruRMM agent to client workstations via SC send-command (SYSTEM), not DC remote-exec (DCOM/schtasks blocked on Win11 clients)
- [ScreenConnect custom-property slots](reference_screenconnect_custom_property_slots.md) — CP1=Company CP2=Site CP3=Department CP4=Device Type CP8=Tag (API hides labels; UpdateSessionCustomProperties replaces the whole array)
- [ScreenConnect cleanup uses wiki as source](feedback_screenconnect_cleanup_wiki_source.md) — per-client SC/RMM metadata cleanup pulls machine->dept/location from the client wiki; enrich the wiki when missing
- [TECH03L systemprofile shortcut corruption](project_tech03l_systemprofile_shortcut_corruption.md) — Auto-Claude "opens then closes" = .lnk pointing at nonexistent systemprofile path; repoint, do not debug the app
- [Claude tui fullscreen crash](feedback_claude_tui_fullscreen_crash.md) — "flicker-free" prompt writes tui=fullscreen; instant-exit crash loop on some Windows terminals; set tui=default (reinstall does NOT fix)
- [Backup targets never guessed](feedback_backup_targets_never_guessed.md) — billed-vs-running mismatches are billing questions for Mike/Winter; never infer/deploy backup targets
- [Fire #dev-alerts for client/RMM work](feedback_fire_dev_alerts_for_client_work.md) — Howard+Mike watch #dev-alerts Discord for live visibility into what techs/sessions touch. Fire post-bot-alert.sh ([DEV]/[RMM]/[DEPLOY]/[GURURMM] prefix -> #dev-alerts) at the START of any RMM/Dev or active-client-machine action. No CLAUDE.md rule yet — this memory is the rule.
- [M365 app: SharePoint needs CERT](reference_m365_app_sharepoint_rest_vs_graph.md) — SP app-only works via the CERT (get-token.sh <tenant> sharepoint); the secret gives "Unsupported app only token". Graph app-only uses the secret.
- [EDR auto-isolates GuruRMM PowerShell](project_edr_rmm_autoisolation_fp.md) — Datto "Exfiltration Over HTTP" rule auto-isolates benign GuruRMM scripts fleet-wide (vwp-qbs 4h outage); watcher + narrow suppression deployed, policy fix pending Mike
- [Background tasks — no shell `&`](feedback_background_task_no_ampersand.md) — with run_in_background:true, run the command in the foreground of the shell; adding `&`/`disown` forks + exits 0 instantly, orphaning a blocking wait so it never delivers (hit twice building ask-forum)
- [ask-forum — human-in-the-loop via #ct-forum](../skills/ask-forum/SKILL.md) — ask a teammate a question in the private ct-forum Discord forum, get their human answer back in-session; forum-only, any human can answer, run the --wait as a proper background task

View File

@@ -0,0 +1,26 @@
---
name: Background tasks — run_in_background only, never add a shell `&`
description: When launching a long-running command as a background task (run_in_background:true), do NOT also append a shell `&`/`disown`/`nohup`. The shell forks the command and exits 0 immediately, orphaning it — a blocking wait then never delivers its result. Run it in the FOREGROUND of that shell; the harness does the backgrounding.
type: feedback
---
When you want a long-running command (e.g. `ask-forum.sh --wait`, a poll loop, a blocking
watcher) to run in the background, use the Bash tool's **`run_in_background: true`** and run
the command in the **foreground of that shell** — a bare `bash script.sh ...` with **no
trailing `&`, no `disown`, no `nohup`**.
**Why:** appending `&` forks the command off inside the shell; the shell then reaches the
end and exits **0 immediately**. The harness sees exit 0 and reports the task "completed"
right away, while the forked process is orphaned (and often killed). A blocking `--wait`
that was supposed to sit for minutes and deliver an answer instead returns nothing — its
result is never captured or notified.
**How to apply:**
- Background wait, CORRECT: `Bash(command="bash .claude/scripts/ask-forum.sh --wait <id> --timeout 1800", run_in_background=true)` — the wait truly blocks server-side, costs zero tokens while pending, and the harness pings you the instant it completes.
- WRONG: `Bash(command="bash ... --wait ... &", run_in_background=true)` and `... & disown` — orphans the wait, "completes" instantly, no answer.
- The two mechanisms are redundant: `run_in_background` IS the backgrounding. Never stack a shell `&` on top of it.
**Incident:** hit TWICE in one session (2026-07-08) building the [[ask-forum]] flow — each
time the "background wait" for a Discord reply exited 0 instantly and never delivered the
answer, which looked like the reply was lost and forced the user to manually prompt "did
they answer?". Logged as `--friction` in `errorlog.md`. Related: [[ask-forum]].

View File

@@ -0,0 +1,20 @@
---
name: feedback_backup_check_only_billed
description: Only verify backup status for clients that actually bill a backup line - don't scan non-billed clients
metadata:
type: feedback
---
Backup verification is scoped to clients **marked for backups** — i.e. those with a billed
Data Backup line (Syncro recurring-schedule line qty > 0). If a client is NOT billed for
backup, do NOT scan their machines / MSP360 / backends for backup status. (Howard,
2026-07-07, GPS audit — Marty Ryan has Svc = A E only, no backup line, so stop checking.)
**Why:** "is X backing up" only matters where the client pays for it; scanning non-billed
clients burns time/endpoint calls and produces findings nobody asked for.
**How to apply:** determine billed-backup status first (Syncro backup line qty — see the
`/syncro schedules`/`schedule` pull, authoritative), then only verify the ones with a seat.
Non-billed clients that happen to be backing up are a separate "revenue leak" question for
Mike/Winter, not a per-machine scan target. Related: [[feedback_backup_targets_never_guessed]],
[[reference_msp360_backup_monitoring]].

View File

@@ -0,0 +1,21 @@
---
name: feedback_backup_targets_never_guessed
description: Backup targets are a deliberate per-client decision - never infer which machine needs backup or deploy backup jobs from billing counts
metadata:
type: feedback
---
When reconciling billed backup lines (e.g. Syncro "Service - Data Backup" qty) against
machines actually backing up (MSP360/B2), a mismatch is a BILLING question for Mike/Winter,
never a deployment instruction. (Howard, 2026-07-05, GPS->RMM audit backup verification.)
**Why:** which machine gets backed up is a deliberate choice made per client (usually the
server or a specific data machine) — billed qty does not mean "N machines each need a
backup job." Guessing targets and adding backup jobs would burn storage and misrepresent
what the client agreed to.
**How to apply:** report mismatches (billed > running, running > billed, dead buckets) as
findings with evidence; route the decision to Mike/Winter/client. Only configure a backup
job when the target machine is explicitly named by the user/client. Related:
[[project_av_migration_bitdefender_to_edr]] (the AV migration IS deploy-everywhere, unlike
backup — don't conflate the two postures).

View File

@@ -0,0 +1,22 @@
---
name: claude-tui-fullscreen-crash
description: Claude Code "flicker-free rendering" prompt writes tui=fullscreen to settings.json and can crash-loop the CLI on Windows; fix = delete the tui key
metadata:
type: feedback
---
On ACG-TECH03L (2026-07-04, Claude Code 2.1.201), accepting the startup prompt asking to
enable **flicker-free rendering** wrote `"tui": "fullscreen"` into
`~/.claude/settings.json`. On that machine's terminal the fullscreen/alternate-screen
mode crashed the CLI instantly on every subsequent launch ("opens and then closes") —
from any launch path, since all read the same settings file.
**Why:** the setting persists, so one bad answer to the prompt turns into a permanent
crash loop; reinstalling Claude Code does NOT fix it because settings survive reinstall.
**How to apply:** if a fleet machine's Claude Code exits immediately at launch, check
`~/.claude/settings.json` for `"tui": "fullscreen"` and set it to `"tui": "default"`
(surgically — keep hooks/plugins; explicit "default" also suppresses re-prompting,
which deleting the key does not). Emergency override without touching config:
`CLAUDE_CODE_DISABLE_ALTERNATE_SCREEN=1`. Decline the flicker-free prompt on machines
with older terminals/conhost. Related: [[tech03l-systemprofile-shortcut-corruption]].

View File

@@ -0,0 +1,18 @@
---
name: feedback_defender_claude_exclusions
description: Mike wants NOTHING Claude/ClaudeTools issues to be flagged by Windows Defender; keep broad exclusions + allow the ClickFix threat IDs that fire on RMM curl dispatch.
metadata:
type: feedback
---
On his workstation (GURU-*), Mike wants **nothing Claude issues to be affected by Defender AV** — it's a constant irritation. The recurring hits are `Trojan:Win32/ClickFix.DBD!MTB` (ThreatID 2147939088) and `Trojan:Win32/ClickFix.ZF` (ThreatID 2147945138), fired by Defender's AMSI **command-line** scan on the base64-PowerShell payloads that `curl.exe` POSTs to the GuruRMM coordination API (`172.16.3.30:3001/api/agents/.../command`).
**Why:** These are false positives on legitimate ClaudeTools/GuruRMM command dispatch. He's the admin/owner and made an informed call to allow the family.
**How to apply:** Process/path exclusions alone do NOT stop these — AMSI CmdLine/behavioral detections ignore `ExclusionProcess`/`ExclusionPath`. The lever that works is `Add-MpPreference -ThreatIDDefaultAction_Ids <id> -ThreatIDDefaultAction_Actions Allow` (Allow = action 6) for both IDs. Also maintained (elevated PowerShell):
- ExclusionProcess: bash.exe, curl.exe, git.exe, node.exe, claude.exe
- ExclusionPath: `C:\Program Files\Git` (+ mingw64\bin, usr\bin), `C:\Program Files\nodejs`, `C:\Users\<u>\.claude`, `C:\Users\<u>\.local\bin`, `C:\Users\<u>\AppData\Roaming\npm`, `C:\ClaudeTools`, `D:\ClaudeTools`.
**ACTIVE (2026-07-01):** Mike opted for the fully-blanket lever — `Set-MpPreference -DisableScriptScanning $true` is SET on this box, disabling Defender AMSI script scanning machine-wide (his call: "I'm not likely to fall for bogus scripts"). This alone stops the CmdLine detections regardless of variant ID; the ThreatID-Allows + exclusions remain as belt-and-suspenders. If ever re-enabling, `Set-MpPreference -DisableScriptScanning $false`.
**Fleet application (2026-07-01):** `DisableScriptScanning` is **Tamper-Protection-gated** — it silently stays `False` if TP is on, even from SYSTEM. This workstation's TP is OFF (toggle worked); **GURU-BEAST-ROG's TP is ON**, so on Beast only the exclusions + ClickFix ThreatID-Allows applied via RMM (those aren't tamper-gated and DO cover the recurring detections) — the blanket script-scanning kill there needs a manual Windows Security UI toggle (TP can't be disabled by script). Beast (GURU-BEAST-ROG, AZ Computer Guru/Mike's House, RMM id 5233d75b-...) is "treated like this machine." Howard was OFFERED the same via Discord DM — his choice on his own box; do NOT push to Howard's machine without his ok. Related: [[reference_acg_msp_stack]] (ACG's own tools shouldn't be flagged as threats), [[feedback_windows_quote_stripping]].

View File

@@ -0,0 +1,26 @@
---
name: feedback_discord_read_replies
description: Discord DM replies are invisible to coord/repo sync — use `discord-dm.sh read <user>` to see if mike/winter/rob actually answered.
metadata:
type: feedback
---
When we DM someone via the `discord-dm` skill and wait on an answer, their reply does NOT
come through coord messages or repo sync — those channels never carry Discord replies. Before
telling the user "no reply yet / they didn't answer," READ the DM channel:
```bash
bash .claude/scripts/discord-dm.sh read mike 10 # last 10 in the mike DM
bash .claude/scripts/discord-dm.sh read #dev-alerts # a channel
```
Output is oldest-first; lines marked `>>>` are from THEM (replies), indented lines are us.
**Why:** the bot participates in every DM channel it opened, so `GET /channels/<id>/messages`
returns full content — the Message Content privileged intent only gates gateway events, not this
REST fetch. Before adding `read` (2026-07-07, Howard flagged it) the wrapper was send-only, so
replies from mike/winter were silently missed.
**How to apply:** after DMing a question to mike/winter/rob, `read` that user to check for their
answer instead of assuming silence. Related: [[reference_community_forum]] is a different channel;
this is Discord DMs/channels via [[discord-dm]] (`.claude/scripts/discord-dm.sh`).

View File

@@ -0,0 +1,12 @@
---
name: feedback_exchange_op_all_access
description: The exchange-op tier is the all-access Exchange tier — stop claiming "no tier can write mail"
metadata:
type: feedback
---
The **`exchange-op`** tier (ComputerGuru **Exchange Operator** app, `b43e7342-5b4b-492f-890f-bb5a4f7f40e9`) holds the **Exchange Administrator** directory role PLUS `full_access_as_app` and `Exchange.ManageAsApp`. That is **full all-access to every mailbox and every Exchange Online operation** — reading, writing, moving mail, inbox rules, message trace, TABL, audit config, EWS, the lot.
**Why:** Mike's recurring correction (2026-06-25) — I keep claiming "no app tier has Mail.ReadWrite, so I need a workaround" and reaching for convoluted paths (EWS gymnastics, etc.). That framing is wrong and wastes time EVERY time. Graph application Mail.ReadWrite is not the only write path; the Exchange Operator app already has full Exchange admin rights.
**How to apply:** For ANY mailbox/Exchange write or all-access need (move/copy/delete mail, modify rules, change mailbox config, EWS operations, audit settings), default to the **`exchange-op`** tier. Never declare a task blocked for lack of mail-write permission without first using exchange-op. The Graph `investigator` tier is read-only (`Mail.Read`); `investigator-exo` lacks `Exchange.ManageAsApp` (see [[reference_investigator_exo_manageasapp_gap]]) — neither limitation means "we can't write," it just means use exchange-op. See [[reference_tedards_tenant_facts]].

View File

@@ -0,0 +1,29 @@
---
name: feedback_fire_dev_alerts_for_client_work
description: Fire a #dev-alerts Discord post (post-bot-alert.sh with an [RMM]/[DEV]/[DEPLOY]/[BUILD]/[GURURMM] prefix) at the START of any RMM/Dev or active-client-machine action — Howard+Mike watch that channel for live visibility into what techs/sessions are touching
metadata:
type: feedback
---
Howard and Mike watch the **#dev-alerts** Discord channel to see, in real time, what the other techs
(and Claude sessions) are working on. Any substantive action on **RMM/Dev** or an **ACTIVE CLIENT
machine** MUST post a #dev-alerts heads-up — ideally at the START of the work, not just after.
**What counts:** SSH into a client server, restarting/reconfiguring services, editing boot/config on a
live box, RMM remote commands, agent/server/dashboard deploys, channel promotions, anything with real
blast radius on production/client infra.
**Why:** without it a session can be hand-editing a live client box (e.g. Lonestar Electrical's Unraid
`/boot/config/go`, cycling libvirt) with ZERO visibility to the team — exactly what happened
2026-07-06, which Mike flagged: "all of the actions you've taken today should have fired the discord
alerts."
**How to apply:**
`bash .claude/scripts/post-bot-alert.sh "[DEV] <client/box> — <what + why>. — Claude @ <machine> (<user>)"`
Prefixes `[RMM] [DEPLOY] [DEV] [BUILD] [GURURMM] [SMARTBADGE-WATCH]` auto-route to the PRIVATE
#dev-alerts (Howard+Mike only, channel 1509998508198068484); everything else → #bot-alerts (whole
team). It soft-fails (best-effort), so it never blocks the work. Channel IDs + routing live in
`.claude/scripts/post-bot-alert.sh`.
As of 2026-07-06 there is NO CLAUDE.md rule enforcing this — treat this memory AS the rule until a core
rule/hook lands (worth proposing one). Related: [[discord-dm]] skill for direct DMs.

View File

@@ -0,0 +1,20 @@
---
name: feedback_impeccable_on_outbound
description: Run the `impeccable` skill on any deliverable before it goes out to a client or vendor
metadata:
type: feedback
---
Before sending ANYTHING to a client or another vendor — proposals, plans, reports,
agendas, one-pagers, emails meant to represent ACG externally — run the **`impeccable`**
skill on it first as a quality/polish gate. Applies to outbound, external-facing
deliverables; internal prep docs and working notes do not require it.
**Why:** Mike wants everything that leaves ACG to be polished and on-brand. A rough
internal draft is fine for us; a client/vendor never sees an unpolished artifact.
**How to apply:** When a deliverable is destined for a client/vendor, produce it, then
invoke `impeccable` to audit/polish before delivery. For document deliverables, that
means rendering them as a designed artifact (styled HTML/PDF one-pager) so `impeccable`
(a frontend/UI design skill) can do its job — confirm the format with the user if unsure.
Pairs with [[feedback_client_tone]] (tone) and [[stop-slop]] (text quality).

View File

@@ -0,0 +1,29 @@
---
name: feedback_rmm_setacl_timeout_password_loss
description: RMM Set-Acl/icacls ACL propagation on large folder trees exceeds the command timeout; stdout is dropped on timeout so any value printed in that script (e.g. a generated password) is lost.
metadata:
type: feedback
---
When dispatching `/rmm` commands that change NTFS ACLs (`Set-Acl`, `icacls /grant`) on a
**large folder tree**, ACL inheritance propagation to existing children can take minutes and
**exceed `timeout_seconds`** — the agent reaper marks the command `failed` with
`"Execution error: Command timeout"`, and **stdout is discarded**. Proven 2026-06-25 setting up
Nick's SMB share on REDNOURCARRIEVI (Carrie's `Documents` tree): the same script generated a
random password and printed it, then ran `Set-Acl` and timed out — the password was gone twice.
**Why:** PowerShell `Set-Acl` (and `icacls` even without `/T`) re-stamps inheritable ACEs onto
all existing children; on a big tree that blows past 90120s. `Set-LocalUser`/`New-LocalUser`
themselves are instant — the cost is the ACL walk.
**How to apply:**
- **Never depend on a value you can only read back from stdout in a command that might time out.**
Generate passwords/secrets **locally** in the Bash tool (retain them), inject via a placeholder
in a `<<'PS'` heredoc (`SCRIPT="${SCRIPT/__PW__/$PW}"`) so PowerShell `$env:` survives — then
even a timeout doesn't lose the value.
- **Isolate the slow ACL step** into its own command with a long `timeout_seconds` (>=600) and
poll across multiple Bash calls (each Bash call is capped ~2 min).
- For share access, share-level perms (`Grant-SmbShareAccess`) and the account creation are fast;
only the NTFS grant is slow.
See errorlog (`rmm/acl`, --friction) and [[reference_gururmm]].

View File

@@ -0,0 +1,29 @@
---
name: feedback_scheduled_task_no_console_flash
description: Windows scheduled tasks must launch console apps windowless (wscript VBS for bash, pythonw + -Hidden for python) or they flash a console window on the desktop every run
metadata:
type: feedback
---
A Windows scheduled task whose action runs a **console-subsystem** program — `bash.exe`,
`py.exe`, `python.exe`, `cmd.exe` — with `LogonType=Interactive` and `Settings.Hidden=False`
draws a visible console window on the desktop **every time it fires**. On short repetition
intervals (the EDR watcher fires every 10 min) this reads to the user as a command prompt
"opening and closing" constantly. It is the single most reported harness annoyance and it
keeps recurring because the *installer scripts* recreate the flashing task.
**Why:** wscript.exe is a GUI-subsystem host and pythonw.exe is the windowless Python host;
neither allocates a console. bash.exe / py.exe / python.exe do.
**How to apply:** whenever you register (or find) a ClaudeTools scheduled task:
- **bash target** -> point the action at `C:\Windows\System32\wscript.exe` with a one-line VBS
wrapper that does `CreateObject("WScript.Shell").Run "...bash.exe -lc ...", 0, False`
(window style `0` = hidden). Pattern files: `gps-rmm-progress-hidden.vbs`,
`edr-isolation-watch-hidden.vbs`.
- **python target** -> use `pythonw.exe` (next to the active interpreter), never `py.exe`/`python.exe`.
- **Always** add `-Hidden` to `New-ScheduledTaskSettingsSet` as belt-and-suspenders.
- Verify: `Start-ScheduledTask`, then confirm no visible bash/wscript/conhost MainWindow.
Fixed installers: `register-edr-watcher.ps1`, `register-orphan-detector.ps1`. The scheduled
tasks themselves are per-machine (not in the repo) — re-run the fixed installer, or repoint
the existing task's action, on every box that runs it. Related: [[feedback_session_recovery]].

View File

@@ -0,0 +1,23 @@
---
name: feedback_screenconnect_cleanup_wiki_source
description: ScreenConnect/RMM machine-metadata cleanup uses the client wiki as source of truth; enrich the wiki when info is missing
metadata:
type: feedback
---
For the ScreenConnect session-hygiene + RMM-site cleanup (per client: normalize Company,
set Site/Department/Device Type/Tag, fix RMM sites, dedup) — **the client wiki is the primary
source of truth for machine -> department / person / location.** (Howard, 2026-07-03.)
**Why:** the wiki already carries person->machine->department maps (e.g. Cascades: Ashley
Jensen->Accounting on DESKTOP-U2DHAP0, Shelby Trozzi->MemCare Director on MDIRECTOR-PC), so
department/site can be derived from it rather than guessed.
**How to apply, per client:**
1. Pull the machine metadata from the client wiki FIRST (`wiki/clients/<slug>.md` + its source docs), then hostname role tokens, then UniFi switch/AP building/area names.
2. Where a machine's department/location is NOT in the wiki, **learn it (ask the user / investigate) and UPDATE the wiki** with the finding — so the wiki becomes the durable record and the next pass is easier.
3. Slot mapping = [[reference_screenconnect_custom_property_slots]] (CP1 Company, CP2 Site, CP3 Department, CP4 Device Type, CP8 Tag). Match each client's EXISTING vocabulary (e.g. Cascades uses Device Type "Desktop"/"Laptop"/"Server", not "Workstation"; Dataforth uses "Workstation").
4. UniFi placement: Dataforth = cloud UDM via Site Manager connector; Cascades = UOS controller. AP/switch names are building/area-coded. Related: [[project_av_migration_bitdefender_to_edr]], GPS->RMM audit `projects/gps-rmm-audit/tracker.md`.
Done so far: Dataforth (D1/D2 sites split + tags), Cascades (single-site + departments). Remaining
per-client unknowns get filled from the wiki as we walk each machine; wiki gets updated when it doesn't have it.

View File

@@ -0,0 +1,20 @@
---
name: feedback_simplest_solution_first
description: Always try the simplest solution to a problem first; escalate to complex only as needed
metadata:
type: feedback
---
Always START with the simplest solution that could solve the problem. Confirm it works, then
graduate to more advanced/complicated approaches only as the simple one proves insufficient.
**Why:** Mike watched a "ask the UDM what IP it thinks VWP-QBS is" turn balloon into vault
reads, plink host-key pinning, and password sanity checks — when the clean answer was a single
`nslookup vwp-qbs.vwp.us 172.16.9.1` from a machine already on the VPN. The heavy path burned
tokens and his patience ("incapable of doing things cleanly like a week ago"). Now a standing
CLAUDE.md core rule.
**How to apply:** Before picking tooling, ask "what is the least-complex thing that answers
this?" — one DNS lookup, one API call, one command. Do that first and observe the result.
Only reach for SSH/plink/double-hops/multi-step orchestration when the simple probe genuinely
can't answer. Escalate deliberately, not by default. See [[feedback_verify_live_before_acting]].

View File

@@ -0,0 +1,20 @@
---
name: feedback_skill_first_routing
description: If an installed skill/command covers a request, INVOKE THE SKILL — never hand-roll the API from memory. Syncro billing/invoicing ALWAYS goes through /syncro (or /syncro-emergency-billing). Knowing the API is not a license to bypass the skill.
metadata:
type: feedback
---
When a request maps to an installed skill or slash-command, **invoke that skill** rather than improvising raw `curl`/API calls. This is a hard rule, now in CORE `CLAUDE.md` ("Skill-first"). The canonical offender is **Syncro billing**: every invoice/line-item/ticket-billing request goes through the `/syncro` skill (or `/syncro-emergency-billing` for after-hours) — NOT ad-hoc API calls.
**Two halves (CORE "Definition of Done"):** (1) route the REQUEST to a *doing-skill*, then (2) GATE the result with the matching *check-skill* BEFORE calling work done — automatically, inferred from the request + expected end result, WITHOUT the user naming skill A/B/C. Standing gates: code edits → `/code-review` + `/simplify` (+ `/security-review` if it touches auth/creds/security); outbound/client-facing → `impeccable`/`stop-slop`; Syncro/vendor → the skill's own preview+confirm gate; wiki/memory → `wiki-lint`/`memory-dream`. **Calibrate to stakes** (skip heavy review on a typo) and push cheap classify/prose passes to **Ollama Tier-0** to stay low-token. Full request→doing-skill→check-skill table: **`.claude/SKILL_ROUTING.md`** (read on demand). Decided with Mike 2026-06-26: enforcement tier "A+B" (CORE rule + on-demand map; deterministic Stop-hook backstop intentionally deferred until proven, to avoid friction).
**Why:** I default to "act directly" and, because I "know" the Syncro REST API, I reach for a hand-rolled `add_line_item` curl from memory. Free-handing the payload gets the structure wrong (attribution/`?api_key=` owner, `taxable:false`, line-item shape, priority/type format, blank contact, the preview gate), producing malformed tickets — and **Winter has to fix them** (already flagged on #32193/#32194 and others). The skill encodes all of that correctly and enforces the preview/confirm gate. The detailed billing rules in [[feedback_syncro_billing]] describe what the SKILL does when it bills; they are NOT a license to bypass the skill and do it by hand.
**How to apply:**
- Billing/invoicing/ticketing/scheduling in Syncro -> `/syncro` (after-hours/emergency -> `/syncro-emergency-billing`). No exceptions, even for a "quick" one-line charge.
- More generally: before reaching for raw API, ask "is there a skill for this?" Credentials -> `vault`; RMM actions -> `/rmm` (find the host with `rmm-search`); M365 investigation/remediation -> `remediation-tool`; the per-vendor skills (bitdefender, datto-edr, packetdial, b2, mailprotector, screenconnect...) own their APIs.
- Use raw API ONLY when no skill fits, or the skill genuinely cannot do the thing — and SAY SO explicitly when you do, so the user can sanity-check.
- When the user corrects a bypass, log it: `bash .claude/scripts/log-skill-error.sh "<skill>" "hand-rolled API instead of using the skill" --correction`.
Related: [[feedback_psa_default_syncro]] (Syncro is the default PSA), [[feedback_syncro_preview_mandatory]] (preview gate the skill enforces), [[feedback_syncro_priority_type_format]] (a malformed-ticket case Winter flagged), [[feedback_syncro_billing]], [[feedback_syncro_workflow]].

View File

@@ -5,11 +5,18 @@ metadata:
type: feedback type: feedback
--- ---
The superproject's background auto-sync resets each submodule's working tree to the **pinned **UPDATE 2026-06-22:** `sync.sh` now protects submodule work on BOTH destructive paths. The
gitlink** (which intentionally lags `main`), and 3-4 Claude sessions can share one submodule 2026-06-21 fix only guarded the Phase-1a init (`sync.sh:391`, init-only-if-unpopulated); the
checkout. So inside `projects/msp-tools/guru-rmm` (and guru-connect) **local branch refs / HEAD do Phase-3 **post-rebase** reconcile (`sync.sh:525`) still ran `git submodule update --init
NOT reliably survive across tool calls or sessions** — a `git switch -c feat` can get reset to the --recursive` unconditionally and re-detached/reset everything — that's the path that ate a
gitlink mid-work, commits land on a detached HEAD, and `push -u origin <branch>` ships a stale ref. feature branch + commits mid-build today. New `submodule_update_safe()` advances ONLY submodules
in the pristine pinned state (clean, detached HEAD) and SKIPS any on a branch or with uncommitted
changes. **So: working on a real branch in a submodule now survives sync.** Prefer that.
Historically (and still as belt-and-suspenders), the auto-sync reset each submodule's working
tree to the **pinned gitlink** (which intentionally lags `main`), and 3-4 Claude sessions can
share one submodule checkout — so local branch refs / HEAD could get reset to the gitlink
mid-work, commits land on a detached HEAD, and `push -u origin <branch>` ships a stale ref.
**Do this instead:** **Do this instead:**
- **Feature work:** `git worktree add <path> origin/main`, edit + commit + push there, then - **Feature work:** `git worktree add <path> origin/main`, edit + commit + push there, then
@@ -27,12 +34,13 @@ gitlink mid-work, commits land on a detached HEAD, and `push -u origin <branch>`
is HTTP, submodule is a different host; SSH key not authorized here). is HTTP, submodule is a different host; SSH key not authorized here).
**Why:** recurring across the fleet (Howard-Home detached-HEAD x2 + a stale-gitlink audit; **Why:** recurring across the fleet (Howard-Home detached-HEAD x2 + a stale-gitlink audit;
GURU-5070 hit a non-fast-forward on a docs push this session). Each occurrence costs a GURU-5070 hit a non-fast-forward on a docs push; 2026-06-22 the Phase-3 path reset guru-rmm
re-diagnose/rebuild cycle. Howard fixed the `sync.sh` submodule-clobber root cause + moved to mid-build). The 2026-06-21 fix was incomplete (Phase-1a only); the 2026-06-22 fix
worktrees (2026-06-21), but the defensive discipline still applies. (`submodule_update_safe()`) closes the Phase-3 post-rebase path too.
**How to apply:** worktree or push-by-SHA + `ls-remote` verify for writes; assert HEAD==origin/main **How to apply:** Work on a real branch in the submodule (now survives sync) and push it to origin.
(or read `origin/main:<file>`) before audits; never `checkout --` shared files. Belt-and-suspenders for high-stakes writes: push-by-SHA + `ls-remote` verify. Assert
HEAD==origin/main (or read `origin/main:<file>`) before audits; never `checkout --` shared files.
Related: [[gururmm-session-logs-submodule-save]] [[feedback_gururmm_build_verification]] Related: [[gururmm-session-logs-submodule-save]] [[feedback_gururmm_build_verification]]
[[feedback_verify_committed_state_before_push]] [[using-git-worktrees]] [[feedback_verify_committed_state_before_push]] [[using-git-worktrees]]

View File

@@ -7,6 +7,8 @@ metadata:
Rules only. Incident detail, verbatim Mike quotes, ticket numbers, dates, the tech user_id table, and the labor-product table all live in [[feedback_syncro_history]] — read on-demand when judging an edge case. API mechanics: [[feedback_syncro_api]]. Workflow: [[feedback_syncro_workflow]]. Rules only. Incident detail, verbatim Mike quotes, ticket numbers, dates, the tech user_id table, and the labor-product table all live in [[feedback_syncro_history]] — read on-demand when judging an edge case. API mechanics: [[feedback_syncro_api]]. Workflow: [[feedback_syncro_workflow]].
**Run billing THROUGH the `/syncro` skill — do not hand-roll these calls.** The rules below describe what the skill does when it bills; they are the spec, not a license to free-hand `curl` from memory (that produces malformed tickets Winter has to fix). Invoke `/syncro` (after-hours -> `/syncro-emergency-billing`). See [[feedback_skill_first_routing]]. "`add_line_item` directly" (§1) means "not the timer workflow" — NOT "bypass the skill."
`.claude/commands/syncro.md` is the authoritative live product table. `.claude/commands/syncro.md` is the authoritative live product table.
--- ---
@@ -86,14 +88,34 @@ Always set `price_retail` explicitly — the rate doesn't auto-populate and the
--- ---
## 8. Corrections preserve the ORIGINAL tech's attribution; ticket ownership is sticky ## 8. API key follows the BILLING TECH — always
**Labor lines:** when fixing a wrong line, preserve the **original tech's** `user_id` so their commission isn't lost. **Attribution is determined by which API key you use.** Every `add_line_item` / `remove_line_item` call is logged as the owner of that key. `user_id` in the payload does NOT override this.
- **Prefer `update_line_item` in place** — it preserves `user_id`.
- **If you must remove + re-add:** the new line defaults to the **API-key owner's** `user_id`. Explicitly set `user_id` to the original tech on `add_line_item`, or PUT-fix it afterward.
- Determine the original tech from `.ticket.user_id` and the line's `.user_id` BEFORE correcting; verify after.
**Ticket ownership:** adding notes or labor does **NOT** change `.ticket.user_id`. Multiple techs routinely work the same ticket. Only change ticket ownership when explicitly asked. Status PUTs send only `status`; line edits use `update_line_item`; neither touches `user_id`. **Common-sense defaults (confirmed by Howard 2026-06-23):**
- Howard asks for billing → use Howard's key (he's billing himself)
- Mike asks for billing → use Mike's key
- Told "put X hours in for [tech]" → use that tech's key, regardless of who is asking
- Split ticket ("2 hrs for Mike, 1 hr for Howard") → two separate `add_line_item` calls, each with the correct tech's key
**Vault paths:**
- Howard → `msp-tools/syncro-howard.sops.yaml``credentials.credential`
- Mike → `msp-tools/syncro.sops.yaml``credentials.credential`
```bash
HOWARD_KEY=$(bash .claude/scripts/vault.sh get-field msp-tools/syncro-howard credentials.credential)
MIKE_KEY=$(bash .claude/scripts/vault.sh get-field msp-tools/syncro credentials.credential)
# Each line item call uses the BILLING TECH's key as a query param:
curl -s -X POST "https://computerguru.syncromsp.com/api/v1/tickets/{id}/add_line_item?api_key=${HOWARD_KEY}" ...
curl -s -X POST "https://computerguru.syncromsp.com/api/v1/tickets/{id}/add_line_item?api_key=${MIKE_KEY}" ...
```
**Auth note:** `?api_key=` is the attribution mechanism. The `Authorization: <key>` header works for reads but does NOT control line-item attribution — always use `?api_key=` for billing writes.
**Corrections:** wrong key used → `remove_line_item` with any key (doesn't matter), then re-`add_line_item` with the correct tech's key. `update_line_item` does NOT fix `user_id`.
**Ticket ownership:** adding notes or labor does NOT change `.ticket.user_id`. Multiple techs routinely work the same ticket. Only change ticket ownership when explicitly asked.
Tech user_id table → [[feedback_syncro_history]]. Tech user_id table → [[feedback_syncro_history]].

View File

@@ -0,0 +1,15 @@
---
name: feedback_syncro_line_item_category
description: Syncro product_category is a controlled vocabulary — never invent one, never invoice a blank-category line
metadata:
type: feedback
---
When billing in Syncro, every invoice/ticket line item carries a CATEGORY (`product_category`) that is a **controlled, admin-defined vocabulary** and drives QuickBooks item mapping + revenue reporting. Two hard rules:
1. **Never make up a category.** Use only values that already exist in the tenant (e.g. `Labor`, `Software`, `Hardware`, `Product`, `Service`/`Services`, `Subscriptions`, `Contract`, `Discount`, `Exempt Labor`, `Prepay Hours`, `Other`, `Deposit`, `Default`). Enumerate the live set from `GET /products` rather than hardcoding. If none fits, ASK — do not guess.
2. **Never invoice a line whose product has `product_category: null`.** Pre-flight with `GET /products/<id>`; a blank category = an uncategorized line. Flag/fix the product (Windows/OS/software → `Software`, physical → `Hardware`) before billing.
**Why:** A blank or invented category silently breaks downstream accounting and makes humans clean up records. Incident 2026-06-30: Cascades "Windows Pro Upgrade" (product 23571919, `product_category: null`) invoiced on #32466/#32474 (invoices 67887/67890) with blank CATEGORY columns; Winter had to correct them. This keeps recurring as a skill-following gap.
**How to apply:** In the `/syncro` flow, before `add_line_item`/invoicing, GET the product and verify `product_category` is a real existing value. Rule lives in `.claude/commands/syncro.md` (taxable-rule block + add_line_item pre-flight). Related: [[feedback_syncro_billing]], [[feedback_syncro_preview_mandatory]], [[feedback_syncro_workflow]].

View File

@@ -0,0 +1,12 @@
---
name: feedback-syncro-prepay-full-get-only
description: Syncro prepay_hours is only reliable from GET /customers/{id}; never read it from the customer search/list endpoint
metadata:
type: feedback
---
When billing in Syncro, read `prepay_hours` ONLY from the full `GET /customers/{id}` response (`.customer.prepay_hours`). The customer **search/list** endpoint (`GET /customers?query=...`) returns `prepay_hours: null` (or stale) even when the customer HAS a prepaid block. Never read prepay from a search result, and never assert "no prepaid block" / "real charge $N" in a billing preview built from search data.
**Why:** Repeated misfires — previews said "$300, no block," then the block surfaced during the invoice POST and the invoice netted $0 (block debited). Mike flagged this 2026-06-23 as a reliability problem that keeps recurring (Dataforth, Grabb & Durando #32455). The wrong figure in a preview the user confirms is the failure mode.
**How to apply:** In the billing gather step, ALWAYS `GET /customers/{id}` and pull `prepay_hours` from there BEFORE composing the preview — not just before the invoice. If you only have search/list data, fetch the full customer record first; do not guess. The /syncro skill hard rules now encode this. See [[feedback_syncro_labor_type.md]].

View File

@@ -0,0 +1,14 @@
---
name: feedback_syncro_priority_type_format
description: Syncro tickets must be created with a number-prefixed priority ("2 Normal") and a valid problem_type — bare "Normal" shows blank in the UI
metadata:
type: feedback
---
When creating ANY Syncro ticket, ALWAYS set both:
- `priority` in the **number-prefixed** form Syncro's dropdown expects: `"1 High"`, `"2 Normal"`, `"3 Low"`, `"4 Urgent"`. A bare `"Normal"` does NOT match the dropdown and renders **blank** in the Syncro UI.
- `problem_type` to a valid Issue Type (most often `Onsite` or `Remote`; others exist — Software, Hardware, File Services / Permissions, New User / Workstation Deployment, Service Request, etc.). Default to `"2 Normal"` priority unless it's an emergency/after-hours job (then `"4 Urgent"`).
**Why:** Winter flagged (2026-06-24) that two tickets Claude created (#32193, #32194) had priority `"Normal"` instead of `"2 Normal"`, so priority showed blank — she has to fix these by hand. "Claude knows how to do that" — the format is already documented in the `syncro` skill; the miss was not following it on create.
**How to apply:** Use the `syncro` skill's ticket-create flow (it documents the exact priority strings + problem-type list); never hand-roll a create that omits priority/type or uses a non-prefixed priority. Verify after create that `.ticket.priority` came back as `"N Name"`, not a bare word. See [[feedback_syncro_blank_contact]] for the companion Cascades contact rule.

View File

@@ -11,7 +11,7 @@ Rules only. Incident detail and ticket numbers live in [[feedback_syncro_history
## 1. ALWAYS preview comments before posting — no exceptions ## 1. ALWAYS preview comments before posting — no exceptions
Show the full comment text and wait for explicit confirmation before posting **any** comment to a Syncro ticket. No exceptions — not billing, not resolution notes, not client-facing, not internal/hidden notes. Canonical rule + the incident that forged it: [[feedback_syncro_preview_mandatory]] (SSOT — read it). In short: show the full comment text and wait for explicit confirmation before posting **any** comment to a Syncro ticket. No exceptions — not billing, not resolution notes, not client-facing, not internal/hidden notes.
**Apply:** draft → show as a formatted block → "Good to post?" → wait for yes → only then POST. Also ALWAYS ask for minutes + labor type before logging time — never assume a default. **Apply:** draft → show as a formatted block → "Good to post?" → wait for yes → only then POST. Also ALWAYS ask for minutes + labor type before logging time — never assume a default.

View File

@@ -0,0 +1,10 @@
# Feedback: Report times in Arizona time
**Source:** Winter, via Discord bot thread, 2026-07-02
**Rule:** All times reported to users (Discord replies, reports, logs, tickets) are in
Arizona time — `America/Phoenix`, MST (UTC-7) year-round, no DST. Convert API/log UTC
timestamps before presenting and label them "AZ". Include UTC only when needed for
cross-referencing raw logs.
Applied to `projects/discord-bot/DISCORD_CLAUDE.md` (formatting rules + rolling-log
timestamp format changed from PT to AZ) same day.

View File

@@ -0,0 +1,30 @@
---
name: feedback_verify_live_before_acting
description: Always pull LIVE current data before acting on (or alarming about) a hardware/infra finding — wiki/session-logs are point-in-time snapshots that go stale
metadata:
type: feedback
---
Before acting on, or raising alarm about, any hardware/infrastructure state — **pull live current
data first and lead with THAT, not the wiki or a recalled fact.** The wiki, session logs, and memory
are point-in-time snapshots; a "broken/degraded/failing" flag may have changed by the time you read it.
This matters most for **hard-to-reverse or money-spending actions** (drive swaps, hardware pulls,
parts purchases, "it's down" escalations).
**Why:** 2026-06-24 — the Cascades CS-SERVER wiki carried a `[CRITICAL] RAID degraded / failing
drive` flag from 2026-06-15. Acting on it, **SSDs were purchased** and Howard went onsite ready to
hot-swap the "failing" drive. A **live Dell OMSA `omreport` query (via the RMM agent)** then showed
the OS mirror had **self-recovered** (the flaky drive dropped out and re-synced after a power cycle):
all 5 disks Online/Ok, all LEDs green, and the "5th unused drive" was actually the **global hot
spare**. Acting on the 9-day-stale flag nearly pulled a healthy drive and wasted a drive purchase.
Howard's directive: "always go with live current data to make sure our findings are real."
**How to apply:**
- For Dell servers: `omreport storage controller|vdisk|pdisk controller=0` + `omreport system esmlog`
via the RMM agent (OMSA reads the controller directly — authoritative). iDRAC/Redfish is the
out-of-band equivalent (no iDRAC skill yet; creds not vaulted as of 2026-06-24).
- Windows `Get-PhysicalDisk`/`Get-Disk` shows only the VIRTUAL disks as "Healthy" even when a member
is degraded — it CANNOT see the array; never conclude RAID health from the OS view alone.
- For any infra claim sourced from the wiki/a recalled fact: re-verify the specific file/flag/host is
still true before recommending action. State the data's timestamp and source.
- See [[reference_rmm_drive_map_explorer_refresh]] for the OMSA-via-RMM pattern context.

View File

@@ -0,0 +1,38 @@
---
name: feedback_windows_pro_upgrade_billing
description: Every machine upgraded Home->Pro with the ACG Windows Pro MAK key must be invoiced $99 to that customer, with the machine name on the line item
metadata:
type: feedback
---
**RULE (Howard, 2026-06-24): when the ACG Windows Pro MAK key is used to upgrade a machine from
Windows Home to Pro, invoice that customer $99 per machine — and name the machine on the line item.**
- The key is Mike Swanson's **Arizona Computer Guru MAK** (NOT a client key): vault
`infrastructure/windows-pro-mak` (field `credentials.product_key`). Used fleet-wide for Home->Pro
edition upgrades (changepk.exe flips the edition with the generic Pro key, then `slmgr /ipk <MAK>`
+ `/ato` activates with this MAK).
- **Billing:** one **$99** charge **per machine** activated, invoiced to the customer that owns the
machine. The line-item description MUST name the machine (e.g. "Windows Pro upgrade - MDIRECTOR-PC").
Bill AFTER the upgrade + activation succeed, not before. Use a flat $99 fee/product line (find or
create a "Windows Pro Upgrade" product in Syncro; set price_retail=99 explicitly; taxable per the
product). This is a license/upgrade fee, NOT prepaid-block labor — do not draw it from a labor block.
- **Track activations** — each consumes one MAK count; watch the remaining allocation (exceeding it
means contacting Microsoft to raise the count).
**Note:** the ACG MAK is actually a **Windows Pro for Workstations** MAK — `slmgr /ipk <MAK>` retargets
the edition to `ProfessionalWorkstation` (a higher SKU than plain Pro, but fine for domain join). Flow that
works remotely as SYSTEM via RMM: `changepk.exe /productkey VK7JG-NPHTM-C97JM-9MPGT-3V66T` (generic Pro key
flips Core->Professional, no auto-reboot in SYSTEM context) -> **reboot once** to sync registry+licensing ->
`slmgr /ipk <MAK>` -> `/ato` (retry `/ato` if it hits a transient `0x8004FE92`). Some machines self-activate
Pro for FREE via a built-in digital entitlement after the edition flip — those consume NO MAK count and are
NOT billed.
**Cascades application status (2026-06-25, done remotely):**
- **MEMRECEPT-PC + LAPTOP-8P7HDSEI** — activated via the MAK. **INVOICED 2026-06-25** on Syncro ticket **#32466**
(invoice #1650806091): 2 x $99 "Windows Pro Upgrade" lines (machine named on each) + 1.0h remote labor (drawn
from Cascades' prepay block, $0) = $215.23 w/ AZ tax. Created a reusable taxable **"Windows Pro Upgrade"**
Syncro product (id 23571919, $99) for this — use it for future Home->Pro upgrade billing.
- **MDIRECTOR-PC** — self-activated FREE via digital entitlement; NO MAK, NO charge.
- **NurseAssist** (offline; possible dupe of `Assistnurse-pc`) + **SALES4-PC** (Tamra departing, bypassed) — not done.
See [[feedback_verify_live_before_acting]] for the verify-before-billing discipline.

View File

@@ -5,6 +5,16 @@ metadata:
type: feedback type: feedback
--- ---
**MECHANICAL FIX FIRST (2026-07-01): for any PowerShell payload crossing a
mangling layer (RMM dispatch, ScreenConnect command box, plink, curl.exe args),
use `.claude/scripts/ps-encoded.sh`** — it UTF-16LE-base64 encodes a script file
and delivers it via `powershell -EncodedCommand` (`encode` prints the paste-safe
one-liner; `rmm <agent-uuid> <file>` dispatches + polls via GuruRMM). Base64 has
no quotes/backslashes/`$` to strip, so the script arrives byte-exact (verified:
UNC `\\` survives). Author the script with the **Write tool**, not a bash heredoc
(Git-bash heredocs collapse `\\` even single-quoted). The manual rules below
remain for one-off inline args only.
On Windows, **embedded double-quotes inside a command argument get silently On Windows, **embedded double-quotes inside a command argument get silently
stripped or mangled** at two separate layers we hit repeatedly. The body of the stripped or mangled** at two separate layers we hit repeatedly. The body of the
arg survives; the `"` characters vanish, so the receiving program sees broken arg survives; the `"` characters vanish, so the receiving program sees broken

View File

@@ -0,0 +1,33 @@
---
name: project_ampipit
description: AMPIPIT (A Modular Pre-install Post-Install Tool) — Howard's active Rust+Slint Windows deploy/recovery toolkit, being migrated into ClaudeTools as a private Gitea submodule. Will fold into GuruRMM.
metadata:
type: project
---
AMPIPIT is Howard's active Rust + Slint portable Windows deployment/recovery toolkit (single .exe,
runs live + in WinPE/WinRE). Currently at `C:\AMPIPIT` as its own git repo; Phases 0-5 code-complete
(832+ tests), Phase 6 hardware validation pending. NOT merely a "reference program" — it is a real
project under active development (supersedes the stale framing in [[project_masterbooter]] which
lists it as reference-only).
**Migration in progress (2026-06-23):** bringing AMPIPIT into ClaudeTools as a **git submodule** at
`projects/msp-tools/ampipit`, matching the guru-rmm / guru-connect pattern (own repo on the ACG
Gitea `git.azcomputerguru.com`, referenced as a submodule). Active feature: a recovery partition
with the **GuruRMM agent + built-in repair-script library** baked in, so offline/broken Windows can
phone home for repair (Claude diagnoses via RMM, directs repair scripts — no direct shell). Spike
verdict GO-WITH-WORK. Design slice: `C:\AMPIPIT\docs\RECOVERY_RMM_DESIGN_SLICE.md`.
**Why / hard constraints (Howard, 2026-06-23):**
- **Do NOT lose any of the existing work** — preserve full git history; back up to a verified second
remote (Gitea) BEFORE removing anything; remove `C:\AMPIPIT` only after full verification.
- **No public GitHub.** Howard is deleting the old public `github.com/Howweird/AMPIPIT` repo. AMPIPIT
must live PRIVATE on the ACG Gitea only. Drop the github remote after Gitea is verified.
- **Follow Mike's rules for Guru projects.** AMPIPIT already encodes the 3rd-party rule as ADR-047
(first-party core; non-MS binaries only via the PE tool registry / RemoteAccessProvider / RmmAdapter
traits). The GuruRMM agent rides that pluggable path, never hardcoded into core.
**How to apply:** When working on AMPIPIT, root the session in the AMPIPIT repo dir so its own
`CLAUDE.md`, coding/code-review/security-review agents, and the `ampipit-build` skill load. Use the
`ampipit-build` skill. Sensitive-domain changes (BCD/partition/WIM/USMT/recovery partition) require
the coding -> code-review -> security-review chain.

View File

@@ -0,0 +1,32 @@
---
name: project_av_migration_bitdefender_to_edr
description: AV strategy — migrate all clients from Bitdefender to Datto EDR; ONLY exception is Glaztech
metadata:
type: project
---
Standing AV direction (Howard 2026-07-03, scope narrowed 2026-07-04): ACG is moving
endpoint AV/security from **Bitdefender GravityZone -> Datto EDR** for **all clients
EXCEPT Glaz-Tech Industries (glaztech)** — the ONLY client staying on Bitdefender.
**Dataforth migrates fully** (originally excepted; Howard removed the exception
2026-07-04 — Dataforth already runs 51 EDR agents with only 5 BD endpoints left:
D1-ENGI-006, DESKTOP-L2LE31M, DATAFORTH-PC, SURFACEOPS, MING-HP).
**Why:** consolidate on Datto EDR as the security plane; Bitdefender is being retired
fleet-wide. Glaztech keeps its large established Bitdefender footprint (~242 BD
endpoint records, vs 159 GPS-billed — count includes stale ghosts).
**How to apply:** whenever setting up or reconciling a client's endpoints (e.g. the
GPS->GuruRMM coverage audit), the target end-state per machine is: GuruRMM agent +
Datto EDR agent (AV on), and Bitdefender **removed**. Do NOT deploy new Bitdefender
coverage. Use existing Bitdefender inventory only as a discovery source for which
machines exist (its company names carry the Syncro CID `_NNNNN` suffix — exact join
key to Syncro customers). Deploy Datto EDR via `[[datto-edr]]` (create-group ->
mint-key -> deploy-cmd, pushed through `/rmm`).
Migration scope quantified 2026-07-04 (tracker Phase 4): 27 clients / 141 BD
endpoints + Dataforth's 5. Datto EDR "Default RMM Org" holds ~35 unassigned agents
that belong to real clients (IMC x7, Russo x2, Len's, Rednour, Reliant, etc.) —
attribute them to proper orgs as part of the migration.
Related: GPS->RMM audit tracker `projects/gps-rmm-audit/tracker.md`.

View File

@@ -0,0 +1,27 @@
---
name: project_cascades_carf_tech_plan
description: Cascades technology plan is a CARF accreditation deliverable, not an MSP status report
metadata:
type: project
---
The Cascades of Tucson "technology plan" Ashley Jensen requested (2026-06) is for **CARF
accreditation** (Commission on Accreditation of Rehabilitation Facilities, Aging Services
program). Her agenda list maps to CARF's **Technology and System Plan** standard (Section 1
"CARF Plans"; the 8 canonical areas: hardware, software, security, confidentiality, backup,
assistive technology, disaster recovery, virus protection).
**Why it matters:** the document is **Cascades' plan, adopted by their leadership** (ACG is the
IT partner who supplies the technical content), NOT an ACG sales/status doc. CARF surveyors
check it as a **living action document** that, for each area, lists: current tech + unmet/
projected needs + timeline + possible vendor + estimated/actual cost + person responsible +
target date + completion date. Must be **based on needs of persons served/personnel/stakeholders**,
**aligned to the strategic plan**, and **reviewed/updated at least annually with dated sign-off**.
**How to apply:** any Cascades tech-plan deliverable must use the CARF action-plan structure
(owner/cost/dates columns), include a resident/persons-served assistive-technology dimension,
a backup-policy section, and a version/annual-review block. CARF passes on a credible REVIEWED
plan, not on zero gaps. Verify exact standard citation + review cadence against their current
Aging Services Standards Manual (2025/2026 edition). Deliverables live in
`clients/cascades-tucson/docs/proposals/`. Pairs with [[feedback_impeccable_on_outbound]],
[[policy_pricing_verification]] (cost figures must be verified).

View File

@@ -0,0 +1,69 @@
---
name: project_cascades_network_segments
description: Cascades of Tucson network segments + the CSC-ENT Wi-Fi SMB block that stalls NAS->CS-SERVER user migration
metadata:
type: project
---
Cascades of Tucson NAS->CS-SERVER share migration — network reality discovered 2026-06-25.
**CS-SERVER** (DC, RMM agent, Synology Drive sync target): IPs `192.168.2.248` (Ethernet) and
`192.168.2.254` (Hyper-V vSwitch), both **/22 = `192.168.0.0/22`** (covers `.0``.3.255`).
Domain `CASCADES` / `cascades.local`. SMB healthy — serves 13+ live sessions; shares under
`D:\Shares\<name>` (Server, Management, Activities, Sales, etc.). Firewall SMB-In = Allow/Any.
**Working SMB clients** live on `10.0.20.x` (routed, gw `10.0.20.1`) and `192.168.3.x` (on-link
in the /22). Wi-Fi SSID for corporate = **`CSCNet`**.
**Two Wi-Fi SSIDs (same AP family):** **`CSC ENT`** = `192.168.2.0/24`, gw `192.168.0.1`, **WPA2-PSK**
(old NAS-side); **`CSCNet`** = `10.0.20.0/24`, gw `10.0.20.1`, **WPA3-SAE** (newer corporate). DNS =
pfSense `192.168.0.1`. CSCNet PSK + CSC ENT PSK vaulted (`clients/cascades-tucson/wifi-cscnet.sops.yaml`,
`wifi-csc-ent.sops.yaml`).
**CORRECTION x2 (2026-06-26): the SMB problem is NOT CSC-ENT-vs-CSCNet AND NOT the AV.** After a
FULL removal of the Datto stack from CS-SERVER (DattoAV SDK + Datto RMM + Datto EDR — see below) and
a reboot, **error 67 persisted unchanged** — so the AV minifilters (`rtp1`/`rtp2`/`BdSentry`) were a
RED HERRING for SMB. More importantly, the SMB **server is HEALTHY for domain/Kerberos clients**:
`Get-SmbSession` shows 7+ live SMB 3.1.1 sessions with open files (lauren, Sharon, Crystal @10.0.20.205,
Megan, Ashley, chris) established AFTER the reboot. The failure is confined to **workgroup/NTLM +
loopback**: Karen (DESKTOP-LPOPV30, workgroup) gets error 67 to EVERY share (IPC$, C$, a brand-new
test share) even in her real user session with CORRECT creds, and `Get-SmbConnection`=none (the SMB
session never negotiates; NO SMBServer/SMBClient event is logged -> failure is **pre-SMB-negotiate,
BAD_NET_NAME**, not auth). NTLM restriction is NOT set (`RestrictReceivingNTLMTraffic`,
`LmCompatibilityLevel`, NTLM Operational log all clear). `Get-SmbServerConfiguration` now works
(NullSessionShares REG type was repaired prior session).
**The AV was DattoAV, not GravityZone Bitdefender.** It was the Datto EDR "Endpoint Protection SDK"
under `C:\Program Files\infocyte\agent\dattoav`, managed by Datto RMM (CentraStage/CagService) + Datto
EDR Agent (HUNTAgent/Infocyte HUNT). That is why removing from the GravityZone console did nothing.
ALL Datto software was removed from CS-SERVER 2026-06-26 (services deleted, dirs/registry/drivers gone).
Tenant azcomp4587; Cascades org `2d5ea96e-3228-461b-9c60-13ae464b61d8`; CS-SERVER was already de-enrolled
from EDR. Use the `/datto-edr` skill + `msp-tools/datto-edr.sops.yaml`.
**RESOLVED (2026-06-26): there was NO CS-SERVER SMB problem. "Error 67" was a TEST-METHOD ARTIFACT.**
RMM-dispatched SMB client commands (`net use`/`net view`/`Test-Path`/`Get-SmbConnection`, even with
`context: user_session`) FAIL with error 67 / RPC 1702 / "none" for KNOWN-GOOD targets too — proven:
Karen's RMM test to her daily-use NAS (`\\CASCADESDS`) returned the same errors, and Crystal showed a
live server-side session (5 open files) while RMM `Get-SmbConnection` on her box reported none. The
agent-injected process lacks the user's real network-logon session. **Validate SMB with server-side
`Get-SmbSession` (showed 7 live users / 30 open files / new sessions forming) or a REAL INTERACTIVE
test — never RMM-dispatched client cmds.** Verified 2026-06-26: logging into CS-SERVER shares as
`CASCADES\karen.rossini` interactively from another PC (John Trozzi/MAINTENANCE-PC) WORKED — account,
password (vaulted), `SG-IT-RW` access, and the server are all fine. The original drive-map `verify`
failure that started this whole investigation was the same RMM artifact. See errorlog friction entry
`rmm/smb-testing`. Karen's only remaining real item: repoint her ALDocs shortcut on DESKTOP-LPOPV30 to
`\\CS-SERVER\Server\ALDocs` and CONFIRM INTERACTIVELY (don't trust drive-map's RMM verify). NOTE: the
prior-session move of Karen to CSCNet broke her NAS-by-name resolution — unrelated to the (non)issue.
**Two more migration blockers found:** (1) **CSCNet is WPA3-SAE** — older adapters (e.g. Intel AC 3165
on Meredith's ASSISTMAN-PC) **cannot join it**, so "move everyone to CSCNet" is blocked by hardware.
(2) **Security smell:** workstations are WORKGROUP (local logins) and reach CS-SERVER by storing a
DOMAIN credential — Meredith's PC stores `cmdkey cs-server→administrator` (her Y:/X:/E: are mapped as
domain admin). Shares `Server`/`Management` = share ACL **Authenticated Users:Full** (NTFS gates real
access). Fix: stop storing admin creds on user PCs; scope shares to groups.
Karen Rossini case (DESKTOP-LPOPV30, WORKGROUP, dual Wi-Fi): `CASCADES\karen.rossini` reset+vaulted
(`clients/cascades-tucson/karen-rossini.sops.yaml`), added to `SG-IT-RW`, CS-SERVER cmdkey staged.
She doesn't really use the Server folder (per Howard) so deprioritized. ALDocs is in
`\\CS-SERVER\Server\ALDocs`. Repoint tooling = [[drive-map]] skill (but blocked by the CS-SERVER SMB
instability above — fix that first).

View File

@@ -0,0 +1,84 @@
---
name: project_cascades_vlan20_migration_routing
description: Cascades CSC ENT->VLAN20 migration — pfSense WAN_Group policy-route breaks LAN<->VLAN20; fix + printer-migration mechanics
metadata:
type: project
---
Cascades is migrating staff machines + printers off the flat old LAN (192.168.0.0/22,
"CSC ENT") onto the isolated **Staff VLAN 20 (10.0.20.0/24, gw 10.0.20.1)** ("CSCNET").
Printers are being re-IP'd to 10.0.20.x (static) and re-shared on the CS-SERVER print
server.
**PROGRESS (live-reconciled 2026-07-01):** MACHINES are essentially done — 22 online hosts on
VLAN 20; only CS-SERVER (stays by design) + ~6 stragglers (ASSISTMAN-PC, CascadesProxess,
Laptop2, NurseAssist, 2 roaming laptops) remain on 192.168.x. PRINTERS lag — only 4 of 15
CS-SERVER shares repointed to 10.0.20.x (FrontDesk .221, BusinessOffice .220, LifeEnrichment
.94, MCReception .78); 11 shares still on old-LAN IPs (MCMedTech still stale 192.168.2.53 though
target 10.0.20.74 is live+reachable). GPO still NOT fleet-live (silent new-driver-install gap
open: reboot vs pre-stage drivers). MEMCARE-STATION rename staged but not yet applied. Full
live inventory (all shares/ports/machines): clients/cascades-tucson/docs/printer-gpo-map.md.
Key operational facts learned 2026-06-30 (Howard, front-desk ET-5800 + Life
Enrichment Canon MF741CDW):
**pfSense gotcha (the big one):** CS-SERVER (on the old LAN) could not reach ANY VLAN 20
printer (.221/.220/.94/.78:9100) even though it pinged the VLAN20 gateway 10.0.20.1. Root
cause was NOT a block — the LAN "Default allow LAN to any" rule has **Gateway = WAN_Group**
(dual-WAN policy routing), so LAN->internal-VLAN traffic gets shoved out the WAN and dies.
**Fix = a pass rule at the TOP of the LAN interface** (Firewall/Rules/LAN), Source =
CS-SERVER 192.168.2.248, Dest = 10.0.20.0/24, protocol any, **Gateway = default** (do NOT
set WAN). This bypasses the policy route so internal traffic routes normally. Scoped to the
server's source IP => residents (own /28 VLANs) + guests (VLAN 50) can't match it (rule is
on the LAN interface, sourced from the server only). This also un-broke the already-migrated
Business Office/Life Enrichment/MC Reception shares. VLAN20->server (SMB) was already fine.
**pfSense SSH from the VPN is BLOCKED** (tcp/22 dropped; GUI 443 open). The `unifi-wifi`
skill's `pfsense-ssh.sh` therefore returns empty (it sends ssh stderr to /dev/null). Did the
rule via the GUI instead. To use the skill remotely later, add an OpenVPN-side allow for 22.
**Printer migration mechanics:**
- CS-SERVER side: repoint the share's port to TCP_10.0.20.<x>:9100 (Set-Printer -PortName),
drop the old 192.168.2.x port; keep the same ShareName so client mappings survive.
- Client side: mapping `\\CS-SERVER\<share>` as a standard domain user triggers a
PrintNightmare elevation prompt (HRESULT 0x800702e4) EVEN when the driver is already local
— see [[feedback_rmm_printer_elevation]] / errorlog. Promptless options: GPO printer
deployment (they already do this for caregivers — the scalable answer), or push as SYSTEM
via `rundll32 printui.dll,PrintUIEntry /ga /n"\\CS-SERVER\<share>"` (per-machine, appears
at the user's NEXT logon), or set Point-and-Print "Approved server = CS-SERVER" so user-
context maps are promptless+immediate.
- Old room-named shares (e.g. `1F-132-RecRoom-Canon`) were renamed on the server during
migration, leaving ORPHANED per-user client mappings; a spooler restart auto-drops them.
- Build UNC paths in RMM PowerShell with `[char]92`, not literal `\\` (jq/agent pipeline
mangles literal backslashes — [[feedback_windows_quote_stripping]]).
**Point-and-Print is the REAL promptless fix (proven 2026-06-30 on the LE machines).** The
0x800702e4 prompt AND the `/ga` per-machine path silently failing at logon (PrintService
event 513, error 0xBCB) are BOTH the same default `RestrictDriverInstallationToAdministrators`
(ON when unset) blocking the standard user from pulling the driver. We're domain admin, but
the *end user* isn't — so apply admin rights via the Point-and-Print policy:
`HKLM\SOFTWARE\Policies\Microsoft\Windows NT\Printers` `RestrictDriverInstallationToAdministrators=0`
+ subkey `PointAndPrint` `Restricted=1,TrustedServers=1,ServerList=CS-SERVER,InForest=0,`
`NoWarningNoElevationOnInstall=1,UpdatePromptSettings=2` (scopes silent install to CS-SERVER
only). After that, `WScript.Network.AddWindowsPrinterConnection` in the user session is
promptless+immediate. **Correct durable fix = put this in a computer GPO fleet-wide** (the
caregiver machines already have it; that's why their printer GPO works), then deploy printers
via GPO. Existing GPOs: `CSC - Life Enrichment Printers`, `CSC - Printer Deployment`,
`CSC - Caregiver Workstation`, `CSC - Reception Workstation Policy` (the LE one likely still
pushes the OLD share name — repoint it to the new share).
**Driver/PDL trap — Canon MF741/743 = UFR II ONLY (not PCL).** The rebuilt `LifeEnrichment`
share was created with **Canon Generic Plus PCL6**; the MF741 can't parse PCL → spools OK,
nothing prints, panel shows **Error #822** (unsupported/corrupt data). Fix = use the **UFR II**
driver (`Canon Generic Plus UFR II V250`, INF cnlb0ma64.inf). CS-SERVER only had PCL6/PS3/XPS
staged; pulled UFR II from a client's DriverStore (`C:\Windows\System32\DriverStore\
FileRepository\cnlb0ma64.inf_amd64_*`) using the vaulted `cs-server` `sysadmin` domain-admin
cred. **Transfer direction matters:** CS-SERVER (192.168.2.x) CANNOT reach a client's C$
(client host-firewall scopes File/Print sharing to LocalSubnet, and CS-SERVER is off-subnet)
-> have the CLIENT push to `\\CS-SERVER\C$` instead (client->server SMB works). Then
`pnputil /add-driver <inf> /install`, `Add-PrinterDriver -Name "<exact INF model name>"`,
`Set-Printer -DriverName`. Get the exact driver name from the INF's quoted strings, not a guess.
When the server driver changes, refresh each client connection (Remove+AddWindowsPrinterConnection)
so it drops the stale cached PCL6 driver.
Related: wiki clients/cascades-tucson (network/VLANs), [[project-cascades-migration-plan]].

View File

@@ -0,0 +1,28 @@
---
name: project_edr_rmm_autoisolation_fp
description: Datto EDR "Exfiltration Over HTTP" rule auto-isolates benign GuruRMM PowerShell fleet-wide; watcher + narrow suppression deployed, policy fix pending Mike
metadata:
type: project
---
Incident 2026-07-07: **vwp-qbs** (Valley Wide Plastering QuickBooks server) was auto-isolated by Datto EDR — Mike lost ~4h before realizing EDR did it. No notification fired (EDR alert delivery was never wired to any channel).
**Root cause:** Datto EDR rule **"Exfiltration Over HTTP Protocol" (MITRE T1041, HIGH)** carries an automated response of **`kill-process` + `isolate-host`**. It fires on *signed* PowerShell doing an outbound HTTPS POST — which our **GuruRMM automation** does routinely. Process tree on the alerts: `gururmm-agent.exe -> cmd.exe -> powershell.exe`. Confirmed benign both times (vwp-qbs = a vSphere VM-power-on script; tps-tina = a network/connectivity diagnostic).
**This is SYSTEMIC, not a one-off.** The same rule fired + attempted auto-isolate on **tps-tina** (The Prairie Schooner) the same day — different GuruRMM script, different client. Per-command-line suppression is whack-a-mole. Note: the isolate-host response only actually cuts a host off where its endpoint policy has isolation enabled (vwp-qbs got isolated; tps-tina fired the same alert but `extensionSuccess=None`, stayed online).
**FLEET-WIDE FIX LIVE (2026-07-07):** Datto EDR suppression rule **`3365e79a`** "GuruRMM origin - Exfil-over-HTTP (fleet-wide FP)" — matches Rule Name = Exfiltration Over HTTP Protocol + Grand Parent = `gururmm-agent.exe`, org/loc empty = ALL clients. Suppresses this one rule for any GuruRMM-launched process fleet-wide (present + future scripts); every other rule still fires on RMM-origin. Created in the CONSOLE (by Howard) after the API create path proved unsafe. The two earlier narrow per-command-line rules (`e4dd55bf`, `7ea2a577`) are now redundant subsets — harmless, left in place.
**API FOOTGUN (do NOT create suppressions via `raw POST` on this tenant):** `POST /SuppressionRules` forces `active:true` and auto-creates an EMPTY match-everything version → a live "suppress all" window (I hit this with `7c36b89f`, deleted within ~2 min). `GET`/`PATCH /SuppressionRules/{id}` 400 ("undefined is not valid JSON"); `DELETE /{id}` works. Create suppressions in the CONSOLE until an atomic API create is verified. Details in `.claude/skills/datto-edr/references/api-reference.md`.
**What's deployed (done):**
- Narrow suppression rule (Datto EDR, id `e4dd55bf`) for VWP's exact script: matches Process Command Line + Grand Parent = `gururmm-agent.exe`. Does NOT blind other PowerShell. API schema for suppression is now in `.claude/skills/datto-edr/references/api-reference.md`.
- **EDR isolation watcher**: `.claude/scripts/edr-isolation-watch.sh` + `register-edr-watcher.ps1`, registered as Windows Scheduled Task **"ClaudeTools - EDR Isolation Watcher"** on Howard-Home (runs every 10 min, posts new auto-isolations to #dev-alerts — see [[feedback_fire_dev_alerts_for_client_work]]). Plain script, zero tokens. State file `.edr-watch-state.json` (gitignored) dedupes; seeded with the vwp-qbs + tps-tina event ids so it stays silent for the already-triaged ones. Retire when GuruRMM EDR webhook (Feature 6) lands.
**Pending Mike decisions (do NOT act without him):**
1. Auto-isolate vs **alert-only** on behavioral rules — the real fix (the isolation, not the alert, caused the outage).
2. Whether to allow a small curated "GuruRMM-origin + this-rule OK" exception set — but NOT a blanket `gururmm-agent.exe` whitelist (RMM = top MSP attack path; blinding EDR there is dangerous).
3. **Fix the RMM scripts** to stop looking like exfil (drop `-EncodedCommand`, least-priv accounts).
4. Hygiene: VWP's script had **ESXi root creds hardcoded** (base64, cert-validation disabled) — already in vault `clients/vwp/esxi.sops.yaml`; move to a least-priv service account + vault injection.
See [[reference_datto_edr_detection_behavior]] (behavioral rules DO fire on signed PowerShell, unlike reputation rules).

View File

@@ -0,0 +1,16 @@
---
name: project_gururmm_dispatch_hang_fix
description: GuruRMM fleet-wide command-dispatch hang root cause + fix (send_to try_send, 9dae20c) and the still-missing eviction
metadata:
type: project
---
On 2026-06-22 the live GuruRMM server (`172.16.3.30:3001`) hung on **every** `POST /api/agents/:id/command` (30s+ timeouts, all agents; GET worked) — command dispatch was down fleet-wide.
**Root cause:** `AgentConnections::send_to` (server `src/ws/mod.rs`) did a blocking `tx.send(msg).await` on a bounded (cap 100) per-agent mpsc channel. A black-holed/half-open agent socket stops its WS writer draining the channel → it fills → `send().await` blocks forever. `send_command` holds `state.agents.read().await` across that await, so the next agent (re)connect's `.write().await` starves tokio's write-preferring `RwLock`, queuing all later dispatches behind it. **One dead socket wedged the whole fleet.** The recovery path "evict non-delivering connections" (`7c578fd`) had been **reverted** (`80df458`), leaving no escape hatch.
**Fix (`9dae20c`, on `main`, deployed):** `send_to` now uses non-blocking `try_send` — a full/closed channel returns "not delivered"; the command stays persisted and is re-offered by `redispatch_pending_commands` (reconnect) + the reaper `requeue_undelivered_commands`. Failure stays local. Verified live (other agent ran a command end-to-end in ~5s).
**Still open / watch:** the proper per-connection eviction of a black-holed socket is still absent (only reverted code existed). A truly half-open agent will keep heartbeating `online` while its server→agent channel silently drops messages (commands dispatch as `running` but never return → reaper fails them on timeout). If this recurs, finish the eviction/keepalive-drop work rather than relying on `try_send` alone.
Deploy model: merging to `gururmm` `main` triggers the webhook build on `.30` (rebuild + `systemctl` restart, auto-rollback if the binary won't start). See the `gururmm-build` skill. Pairs with [[project_guruscan_in_test_paused]].

View File

@@ -0,0 +1,27 @@
---
name: project_gururmm_legacy_disabled
description: GuruRMM's legacy Rust 1.77 (Server 2008R2 / Win7) build variant is DISABLED as of 2026-07-04 — do not just re-enable it; 2008R2 support returns via a separate legacy agent (C++/.NET or clean rewrite)
metadata:
type: project
---
The GuruRMM Windows build's **legacy Rust 1.77 variant is disabled** (2026-07-04) via
`LEGACY_ENABLED=false` in `deploy/build-pipeline/build-windows.sh`.
**Why:** the legacy build moves `Cargo.lock` aside and re-resolves deps unpinned; crates.io now
serves edition2024 versions of transitive deps (`chacha20`/`rand_core` 0.10.1) that Rust 1.77
cannot parse ("feature `edition2024` is required"). This fail-closed the ENTIRE Windows build and
blocked modern (amd64/x86/tray) deploys. It is a **BUG-021 recurrence** and is NOT caused by any
agent code change — it reproduces at any base commit right now. This is at least the 3rd edition2024
break of the 1.77 build (the `agent/Cargo.toml` transitive-pin list is a monument to it).
**What the disable does:** skips the legacy wave/copy/deploy/symlink and EXCLUDES legacy from the
artifact cleanup, so the last-built legacy binaries (**0.6.76**) are PRESERVED and existing Server
2008R2 / Win7 endpoints stay supported *frozen at 0.6.76*. Modern agents advance normally.
**Mike's directive (2026-07-04):** "Kill the legacy version for now, but we will come back to it" —
2008R2 support is REQUIRED (a number are still in the wild) and will return via a **different
architecture (C++/.NET) or a clean rewrite for legacy**, NOT by flipping `LEGACY_ENABLED` back on
without first solving the 1.77 edition2024 pin problem. Do NOT re-enable it blindly. Tracked in
`projects/msp-tools/guru-rmm/docs/RMM_THOUGHTS.md`. Related: the policy-controlled tray feature that
was shipping when this surfaced ([[project_guruconnect]] is a different project).

View File

@@ -0,0 +1,16 @@
---
name: GuruRMM security scope — integrate AV, don't replace it
description: GuruRMM product scope on security/AV — the RMM does NOT build native virus/malware removal; it integrates AV products (monitor their reports + send commands to them) and its own built-in value is helping techs FIND issues. Program/software removal is a separate, distinct feature.
type: project
---
Product-direction decision (Mike, 2026-06-22). When weighing security/diagnostic features for GuruRMM:
- **No native AV / virus / malware removal in the RMM.** Dedicated AV products (Bitdefender GravityZone, Datto EDR/AV — see [[reference_acg_msp_stack]]) do that work. Don't pitch building a RogueKiller-style scanner/quarantine engine into the agent.
- **The RMM's AV role is integration:** monitor/surface the AV products' reports + status, and send commands/actions to those AV products *through* the RMM. Manage AV, don't be AV.
- **The RMM's own built-in value is helping techs FIND issues** — diagnostics, health surfacing, "what's wrong with this box" tooling — not performing endpoint security remediation itself.
- **Program/software removal is a DISTINCT feature** (the ARP-registry silent-uninstall engine, SPEC-030 `remote-software-uninstall`), unrelated to AV. It was being worked in a separate session as of this date.
**Why:** avoids reinventing mature AV engines, keeps the RMM RMM-first (mission.md non-goals), and plays to the self-hosted-management strength rather than competing with security vendors.
**How to apply:** for security-flavored feature ideas, frame as "monitor + command the existing AV/security product" or "help the tech locate the problem," not "build the security capability natively." Related: [[project_gururmm]], [[feedback_no_manufactured_guardrails]].

View File

@@ -0,0 +1,31 @@
---
name: project_gururmm_software_removal_pr58
description: SPEC-030 software-removal hardening (driver exclusion, lingering-process sweep, job list/reaper) is on PR #58 awaiting merge; live testing planned 2026-07-06 on DESKTOP-MS42HNC.
metadata:
type: project
---
GuruRMM SPEC-030 software removal (Assets -> Inventory tab): the 2026-06-24 session's open
requests were implemented 2026-07-05 and sit on branch `feat/software-removal-hardening`,
**PR #58** (https://git.azcomputerguru.com/azcomputerguru/gururmm/pulls/58), NOT merged —
merge deploys to beta. Live testing planned 2026-07-06 on the test box DESKTOP-MS42HNC
(agent 0de89b88-b21d-4647-ab64-96157ba87cc5).
What PR #58 contains:
- Engine `-List` emits `is_driver` (heuristic: driver/chipset/firmware name words, or hw-vendor
publisher + device words; junkware like "Driver Booster" deliberately excluded). Dashboard
hides drivers by default, keeps them out of select-all, deselects on re-hide.
- End-of-run lingering-uninstaller sweep in uninstall-engine.ps1: 45s budget-aware grace, then
taskkill of started trees (StartTime PID-reuse guard, same-session only, null CreationDate
excluded) + late re-verify that flips async-finisher false-failures to success. This is the
chosen resolution of the Launchy/AIMP "failed / no usable result" open decision.
- `GET /agents/:id/software/uninstall/jobs` (list, results omitted), stale-running-job reaper
(900s no-progress -> failed), `finish_job` guarded WHERE status='running', UI 100-target cap.
Verified pre-merge: engine live tests on Howard-Home (driver flags, sweep kill/no-kill,
orphaned/refused paths), heuristic unit cases, `verify.sh server --check` + `dashboard` PASS,
high-effort workflow code review with all confirmed findings fixed.
Remaining after this: recipe catalog (P3, [[project_gururmm_av_removal_recipes]] if created),
dashboard UI for the new job-list endpoint (client binding `softwareApi.uninstallJobs` exists,
unused), and TeamViewer retry-verify still not re-validated live.

View File

@@ -0,0 +1,17 @@
---
name: project_guruscan_in_test_paused
description: GuruScan multi-engine scan verification is IN TEST / paused on DESKTOP-MS42HNC (resume steps + state)
metadata:
type: project
---
GuruScan (multi-engine malware scanner, `projects/msp-tools/guru-scan`, hardened at `fb09102`) is **IN TEST — PAUSED** as of 2026-06-22. Verifying full-scan + full-removal + automated lifecycle on test VM **DESKTOP-MS42HNC** (agent id `0de89b88-b21d-4647-ab64-96157ba87cc5`, client AZ Computer Guru / site Howard-VM — a flaky laptop VM that sleeps/reboots).
State:
- **HitmanPro** lifecycle already verified (36 threats detected+removed, reboot-cleanup task fires).
- **Emsisoft** full `/f=C:\` run (the heavy ~80-min engine) was launched with 516 samples staged but **interrupted by a VM reboot — NOT yet verified**. This is the open item.
- Resume hands-off: `bash .claude/scripts/guruscan-agent-test.sh DESKTOP-MS42HNC scan-one Emsisoft` (self-restores samples, detached no-cap, reports removal + results.json + cleanup task).
Cleanup still owed on the VM once testing is done: remove samples/zip/EICAR/test tasks, clear scanner quarantine, and **re-enable Windows Defender RTP + Tamper Protection** (disabled at the console for malware testing).
Blocker that surfaced + was fixed this session: a fleet-wide RMM command-dispatch hang — see [[project_gururmm_dispatch_hang_fix]].

View File

@@ -0,0 +1,23 @@
---
name: tech03l-systemprofile-shortcut-corruption
description: ACG-TECH03L shortcuts got rewritten to nonexistent systemprofile paths (Auto-Claude "opens then closes"); repointed via RMM 2026-07-04
metadata:
type: project
---
On ACG-TECH03L (Howard's laptop, "tech-03"), the desktop + Start Menu **Auto-Claude**
shortcuts and the **UltraSearch** shortcut were found pointing at
`C:\Windows\system32\config\systemprofile\...` paths that do not exist — symptom was
"ClaudeTools opens and then closes" when Howard double-clicked the launcher. The Claude
Code CLI itself was healthy (2.1.160, node v24.16.0, repo + identity.json intact).
**Why:** something (likely an installer/updater running under SYSTEM context, e.g. via
RMM) resolved `%LOCALAPPDATA%`/`%USERPROFILE%` to the SYSTEM profile when writing the
shortcuts. If a Claude-adjacent launcher "flashes and closes" on a fleet machine, check
the .lnk TargetPath for `systemprofile` FIRST before debugging the app.
**How to apply:** repoint the .lnk to the real per-user install
(`C:\Users\<user>\AppData\Local\Programs\auto-claude-ui\Auto-Claude.exe`) via
WScript.Shell as SYSTEM — fixed 2026-07-04 (RMM cmd 0c9180df). UltraSearch was left
broken: no user-profile copy exists on disk; needs reinstall if Howard wants it.
Avoid running per-user app installers/updaters from SYSTEM context.

View File

@@ -0,0 +1,25 @@
---
name: ps51-invoke-restmethod-headers-variable
description: PS5.1 via RMM powershell -File - Invoke-RestMethod -Headers $H (script-scope hashtable) sent an EMPTY Authorization header; build headers inline per call instead
metadata:
type: reference
---
On ACG-DWP-X-BB (WS2019, PowerShell 5.1, script executed via GuruRMM agent as
`powershell -NoProfile -File script.ps1`), passing a script-scope hashtable variable to
`Invoke-RestMethod -Headers $H` (where `$H=@{Authorization="Bearer $tok"}`) resulted in
Graph rejecting every call with `InvalidAuthenticationToken / "Access token is empty"`
even though the token variable was verifiably populated (fingerprint printed correctly)
and the same token + same URL worked with an inline-built header from the same machine.
**Fix that works:** build the header at each call site — e.g.
`function Hdr { return @{Authorization=("Bearer " + $script:tok)} }` then
`Invoke-RestMethod -Uri $u -Headers (Hdr)`. Inline `@{Authorization=('Bearer '+$t)}` also works.
Diagnosed 2026-07-02 during the BirthBio Datto-vs-SharePoint reconciliation after two
full failed runs (masqueraded first as token expiry, then as clock skew — the real tell
was the Graph error body "Access token is empty", captured only after adding response-body
extraction to the retry helper). Always capture the Graph error BODY, not just the
exception message: "(401) Unauthorized" alone cost three debugging cycles.
Related: [[gururmm-command-timeout-seconds]], [[sharepoint-graph-large-file-upload]]

View File

@@ -0,0 +1,32 @@
---
name: reference_365_app_suite
description: Authoritative map of the ComputerGuru M365 app suite (apps, App IDs, live-verified permissions per tier) and — the recurring failure — per-tenant consent is NOT uniform; how to audit + fix partial consent.
metadata:
type: reference
---
The ComputerGuru M365 app suite is fully documented in the remediation-tool skill:
`.claude/skills/remediation-tool/references/app-suite.md` (authoritative; live-verified
2026-07-02). Read it before concluding "the tool can't do X on tenant Y".
**The recurring failure it fixes:** per-tenant consent is NOT uniform. A tenant can have an
app's service principal but only a PARTIAL/OLD permission grant. Example: VWP
(valleywideplastering.com, 5c53ae9f-…) had the Tenant Admin app but NO SharePoint
`Sites.FullControl.All` — SharePoint calls 401'd with a valid-looking token whose `roles`
claim was empty. The suite "having" a capability (baseline design) ≠ a given tenant having it
(actual consent).
**Always AUDIT before giving up:** decode each tier's token `roles` on the target tenant and
compare to the baseline in app-suite.md. Empty roles on a correct `aud` = present-but-not-granted.
**Fix partial consent — two methods:**
- A: re-consent the whole manifest — `https://login.microsoftonline.com/<tenant>/adminconsent?client_id=<app-id>` (reliably grants Graph; the SharePoint app-only role often does NOT attach from consent — verify + use B for the leftover).
- B: grant the specific missing app role directly via `POST /servicePrincipals/{recipientSP}/appRoleAssignments` using a `tenant-admin` token (holds AppRoleAssignment.ReadWrite.All). This is how VWP's SharePoint role was granted 2026-07-02; propagates to a fresh token in seconds. Only to complete an intent the customer already consented to.
- EXO role gap: `assign-exchange-role.sh <domain>` (audit fleet: `--all --verify`).
Apps: Security Investigator bfbc12a4 (Graph read + EXO read), Exchange Operator b43e7342
(EXO all-access + `exchange-op-graph` Graph Mail.ReadWrite), User Manager 64fac46b (Graph
user/group write), Tenant Admin 709e6eed (Graph high-priv + SharePoint Sites.FullControl.All
via CERT), Defender dbf8ad1a (MDE), Intune 46986910, Mailbox 1873b1b0 (ACG-internal only).
SharePoint app-only REQUIRES cert (not secret). See [[reference_remediation_tool_365_access]],
[[feedback_exchange_role_recurring_gap]], [[feedback_exchange_op_all_access]].

View File

@@ -0,0 +1,37 @@
---
name: reference_alis_medtelligent
description: ALIS (Medtelligent assisted-living EHR) API + staff-import facts for Cascades Tucson — auth quirk, read-only staff, web-UI import path. Use the `alis` skill.
metadata:
type: reference
---
ALIS = Medtelligent's assisted-living EHR (Cascades of Tucson client). All API traffic
goes to the shared host **`api.alisonline.com`** (the tenant URL `cascadestucson.alisonline.com`
is just the login subdomain), scoped by the user's company + a `communityId`. **Cascades =
communityId 622** (the only community this credential sees). Use the **`alis` skill** — don't
hand-roll the API.
**Auth (verified live 2026-06-29):** `POST /user/tokens` with `{username, password}` → JWT
(`accessToken` ~1h) + `refreshToken`; send `Authorization: Bearer <accessToken>`. The
**username MUST be tenant-qualified**: `howard.enos@cascadestucson` works; bare `howard.enos`
returns HTTP 400. Login creds in vault: `clients/cascades-tucson/alis-api-howard-user`
(Howard's password was exposed in chat 2026-06-29 — flagged to rotate). Other ALIS vault
entries: `alis-api-microsoft-basic` (BasicAuth used by Microsoft), `alis-sso-app-registration`.
Global API security is OR(Bearer|BasicAuth|VendorKey) — a user JWT alone authorizes reads.
**Staff are READ-ONLY via the API** — only GET endpoints exist (`/v1/integration/staff?communityId=622`
etc.); no create/update/delete. **To create/change staff (and their logins) you upload a
13-column .xls in the ALIS web UI: Staff → Import.** That import sets Login Enabled + Password,
so it's also how staff logins are provisioned. The `alis` skill builds that workbook from a
CSV/JSON and infers each new hire's Security Roles from how existing staff of the same Job Role
are set up (job-role → security-role map learned from live data; 23 real security roles, Job
Role is free text). The API *does* allow writes for residents/prospects/billing (not staff).
**Import format (confirmed from a real ALIS export, ALIS_Staff_Update_Import.xls):** two layouts.
CREATE (new staff) has a Password column + NO ALIS ID — rows without an ALIS ID are created.
UPDATE (existing staff) leads with **ALIS ID** (the staffId, the match key) + no Password. So
present-ALIS-ID = update, absent = create. **Dates are MM/DD/YYYY.** Security Roles are
comma-separated multi-values; the `alis` skill infers the full typical combo per job role from
current staff. Still test ONE row first before a bulk run.
Related: [[reference_resource_map]], [[feedback-vault-every-credential]].

View File

@@ -1,12 +1,24 @@
--- ---
name: reference_antigravity_agy_not_headless name: reference_antigravity_agy_not_headless
description: Antigravity CLI agy.exe is the IDE embedded agent (no stdout, SQLite store) — NOT a headless CLI. The agy skill uses @google/gemini-cli, not agy.exe. Don't reinstall agy.exe expecting a headless tool. description: UPDATE 2026-07-03 — agy.exe NOW works headless (v1.0.6+ --print/-p returns clean stdout). Use it as the Antigravity/Gemini second-model path. Older claim (DOA headless) is obsolete.
metadata: metadata:
type: reference type: reference
--- ---
The `agy.exe` installed by Google's Antigravity CLI (`%LOCALAPPDATA%\agy\bin\agy.exe`, installer `https://antigravity.google/cli/install.ps1`) is the IDE's embedded agent, **NOT a usable headless CLI** on this fleet. Even v1.0.6's advertised `-p/--print` produces ZERO stdout and hangs when invoked non-interactively from the Bash/PowerShell tool harness — it writes only to a SQLite conversation store. First found 2026-06-05 (`session-logs/2026-06-05-mike-gururmm-platform-day.md` line 35); **re-confirmed 2026-06-06** after the GURU-5070 reinstall (reinstalled agy.exe and walked straight back into the same no-output/hang symptom). **CORRECTION 2026-07-03 (GURU-BEAST-ROG):** `agy.exe` (Google Antigravity CLI,
`%LOCALAPPDATA%\agy\bin\agy.exe`) **now has a working headless print mode.** `agy -p "<prompt>"
--dangerously-skip-permissions` returns clean stdout and exits 0 (smoke-tested: returned "READY";
also drove a full multi-file spec critique reading files via `--add-dir <dir>`). Flags: `-p/--print`
(single non-interactive prompt), `--print-timeout` (default 5m), `--add-dir` (add a workspace dir so
it can read files), `--model`, `--dangerously-skip-permissions` (auto-approve tools). Subcommands:
`models`, `install`, `update`, `plugin`. Re-wire came from GURU-5070 fleet sync + a newer agy build.
So agy IS now a usable Antigravity/**Gemini second-model** path, especially when `gemini-cli` OAuth
fails (e.g. BEAST-ROG's `throwIneligibleOrProjectIdError`). NOTE: after install the binary is added to
User PATH registry but NOT the active shell PATH until terminal restart — call it by full path
`/c/Users/guru/AppData/Local/agy/bin/agy.exe` from the Bash tool.
The `agy` SKILL (despite the name) routes to the official **`@google/gemini-cli`** (`gemini`, npm global) — that IS the real headless second-opinion tool (Google OAuth, no API key), resolved via `identity.json .gemini.binary`. Grok (`ask-grok.sh`) is the other working second model. Both were verified returning `OK` on 2026-06-06. **Historical (pre-2026-07-03, now OBSOLETE):** earlier agy.exe builds (~June 2026) produced ZERO
stdout in `-p` mode and hung when driven headless (wrote only to a SQLite conversation store), so the
**June 18 sunset — likely a non-issue for ACG.** Google is sunsetting gemini-cli's free/unpaid OAuth quota on **2026-06-18**, but Mike has a **paid Gemini account**, so the plan is to **stay on gemini-cli** (do NOT migrate to Antigravity). The bulletproof form is to auth gemini-cli with a paid **Gemini API key** (`GEMINI_API_KEY`) rather than the free OAuth quota — that path is unaffected by the OAuth-CLI sunset regardless of how the consumer tiers shake out, and is more stable for headless use. (Sources disagree on whether paid Pro/Ultra OAuth is also cut, so the API-key path is the safe bet.) **Do NOT reinstall agy.exe expecting it to work headless.** Related: [[feedback_agy_review_not_readonly]]. `agy` SKILL was pointed at `@google/gemini-cli` instead. The gemini-cli path still exists
(`ask-gemini.sh`) but on machines where its OAuth is ineligible, prefer **agy -p** now. Related:
[[reference_cdp_chrome_driver]], [[feedback_agy_review_not_readonly]].

View File

@@ -0,0 +1,32 @@
---
name: reference_client_wifi_inventory
description: Where client Wi-Fi networks are recorded so techs connect onsite without asking — vault holds passwords, wiki holds the readable index.
metadata:
type: reference
---
We are building a fleet-wide client Wi-Fi list so anyone onsite can connect without asking
for the SSID/password again. Established 2026-07-06 (Howard).
**Split (security-driven):**
- **Passwords/PSKs → SOPS vault**, `clients/<slug>/wifi.sops.yaml`, one file per client.
Per network use `credentials.<key>_ssid`, `credentials.<key>_password`, optional
`credentials.<key>_auth` (`<key>` = `staff`/`guest`/`voice`/`warehouse`...). Everything under
`credentials:` is encrypted at rest, so the entry is self-contained for an importer. Write it
with the `vault` skill's `vault-helper.sh new`/`set` (NEVER paste a Wi-Fi password into chat,
a ticket, a session log, or the wiki).
- **Readable index → wiki**, `wiki/reference/client-wifi.md` (registered in `wiki/index.md`
under a new **Reference** section). Client / SSID / band / location / vault-field — NO passwords.
It lives under `wiki/reference/` so `/wiki-compile` never clobbers it (compile only touches
clients/projects/systems slugs).
**Capture flow onsite:** ask "Wi-Fi name + password? staff vs guest? which should our machines
use?" → store in vault via vault-helper → add a row to the wiki inventory → `/sync`.
**Importer is DEFERRED** (Howard chose "structure only for now" 2026-07-06). Planned end state:
a `/wifi import <client>` that reads the vault file, builds a Windows WLAN profile per network
(`netsh wlan add profile`), and auto-connects. Interim: read the password with
`vault.sh get-field clients/<slug>/wifi credentials.<key>_password` and connect manually.
Build the importer/`/wifi` skill once a few real networks exist.
Keep `<slug>` identical to the client's `wiki/clients/<slug>.md` slug so vault + wiki + article line up.

View File

@@ -0,0 +1,25 @@
---
name: reference_datto_edr_detection_behavior
description: How Datto EDR (azcomp4587) actually detects/reports, and AV-suppression gotchas — verified live on RMM-TEST-MACHINE
metadata:
type: reference
---
Verified live 2026-06-25/26 on RMM-TEST-MACHINE (EDR agent `b98b3ba0-...`, group `[TEST] RMM-TEST-MACHINE`, org Arizona Computer Guru) via the `datto-edr` skill + `/rmm`.
**Alert `sourceType` taxonomy (how to tell WHICH engine fired):**
- `av` = Datto AV signature hit (e.g. `Eicar-Test-Signature`). On-access/RTP.
- `rule` = Datto **EDR** detection — reputation/analyst rule on the forensic scan (e.g. `Generic Malware (Reputation - High Severity)`, description "Malware detected by endpoint protection").
- Both land in the same `Alerts` collection and surface identically via `edr.py detections`.
**Datto EDR is reputation-based, NOT structural.** A synthetic "looks suspicious" artifact (Run-key/scheduled task launching hidden `-EncodedCommand` powershell) is collected by the forensic scan but scored BENIGN → no alert (powershell.exe is signed/clean). To get an EDR detection you need a real reputation hit: wire a known-bad file as the **executable of an autostart** (Run-key/scheduled task) so the survey collects + hashes it. EICAR-as-autostart works → high-sev `rule` alert. A loose file on disk is NOT scanned by the EDR forensic survey (it only walks execution/persistence artifacts).
**BUT behavioral rules DO fire on signed PowerShell** (unlike the reputation scoring above). Rules like **"Exfiltration Over HTTP Protocol" (T1041)** key on *runtime behavior* (outbound HTTP from powershell), not file reputation, so they alert HIGH on clean signed `powershell.exe` — and can carry an automated `isolate-host`/`kill-process` response. This routinely false-positives on GuruRMM automation (`gururmm-agent.exe -> cmd.exe -> powershell`). See [[project_edr_rmm_autoisolation_fp]].
**AV-suppression gotchas (to isolate EDR on an endpoint):**
- Datto AV is **tamper-protected**: `Stop-Service EndpointProtectionService2 -Force` is refused ("cannot be stopped"); can't disable from the endpoint. Supported path = console policy (AV disabled / path-exclusion) — console-only, like policy assignment.
- Disabling Datto AV in the policy **uninstalls** the AV component on the box (services `EndpointProtectionService`/`...2` go absent; `HUNTAgent` EDR stays). Platform `dattoAvEnabled` flips to False at the console first; the on-box apply lags a few minutes.
- Removing Datto AV makes **Windows Defender auto-reactivate** (Security Center turns it back on when no 3rd-party AV registered). Then Defender RTP quarantines EICAR AND its **AMSI blocks any PowerShell script containing the literal EICAR string** ("script contains malicious content"). Build EICAR from char codes so the literal never appears in the script; disable Defender RTP (or path-exclude) too.
- After testing: restore Defender RTP (`Set-MpPreference -DisableRealtimeMonitoring $false`) and re-enable Datto AV in the console policy.
Skill: [[reference_syncro_rmm_api_gui_only]] is the analogous "management is GUI/console-only" constraint. See `.claude/skills/datto-edr/`.

View File

@@ -1,12 +1,24 @@
--- ---
name: reference_guru5070_rust_toolchain name: reference_guru5070_rust_toolchain
description: GURU-5070 has the full local Rust toolchain (cargo + MSVC + protoc) — build/clippy/test the guru-connect workspace LOCALLY instead of the build host; set PROTOC first description: GURU-5070 has a full Rust toolchain installed BUT a WDAC/Smart App Control policy now blocks running rustc/cargo locally (os error 4551) — build the Windows agent on Beast, not here; toolchain paths + CI gates still valid for reference
metadata: metadata:
type: reference type: reference
--- ---
As of 2026-05-30, GURU-5070 has the full Rust dev toolchain installed, so GuruConnect can be **BLOCKED AS OF 2026-07-04 — do NOT rely on local Rust builds on GURU-5070.** A Windows
built/linted/tested locally — **no more build-host (172.16.3.30) round-trips just for `cargo fmt`/clippy.** Application Control (WDAC / Smart App Control) policy now blocks execution of `rustc.exe`/`cargo.exe`
(and `sops.exe`, `openssl`, `gawk`, Python `cryptography`'s native DLL) — attempts fail with
`An Application Control policy has blocked this file. (os error 4551)`. This is the same policy behind
the "Windows can't confirm who published …" blocks on unsigned binaries. Until the policy is adjusted
to trust the toolchain, **compile Windows agent/GuruConnect changes on Beast** (`guru@100.101.122.4`,
Tailscale; sccache at `C:\sccache`) via a throwaway `git worktree` + `cargo check` — see
[[gururmm-beast-windows-build-host]]. The paths/gates below remain accurate for reference and for when
the policy is relaxed. WDAC impact also breaks the `vault`/`sops` path here (use `age`+Node fallback per
[[feedback_vault_gcm_shadow_auth]]) and silently swallowed error logging (`CLAUDETOOLS_ROOT=D:/claudetools`
wrong-case → `log-skill-error.sh` can't write; real repo is `D:/ClaudeTools`).
As of 2026-05-30, GURU-5070 has the full Rust dev toolchain installed (able to build locally *until the
2026-07-04 WDAC block above*):
- **cargo/rustc/clippy/rustfmt:** `C:\Users\guru\.cargo\bin\` (rustup; cargo 1.96, rustfmt 1.9, clippy 0.1.96). - **cargo/rustc/clippy/rustfmt:** `C:\Users\guru\.cargo\bin\` (rustup; cargo 1.96, rustfmt 1.9, clippy 0.1.96).
- **MSVC C++ Build Tools:** VS2022 BuildTools (VCTools workload) — provides the `x86_64-pc-windows-msvc` linker. - **MSVC C++ Build Tools:** VS2022 BuildTools (VCTools workload) — provides the `x86_64-pc-windows-msvc` linker.

View File

@@ -0,0 +1,20 @@
---
name: gururmm-command-timeout-seconds
description: GuruRMM agent command dispatch honors timeout_seconds, NOT timeout — long jobs die at ~300s otherwise
metadata:
type: reference
---
When dispatching a command to a GuruRMM agent (`POST {RMM}/api/agents/{id}/command`), the
execution timeout is controlled by **`timeout_seconds`**, not `timeout`. Passing only
`timeout` leaves the agent at its default cap (~300s): long-running commands get killed and
often surface as a zombie `status: running` with empty stdout that never terminates.
For multi-minute/multi-hour work (large uploads, big enumerations) set `timeout_seconds` high
(e.g. 10800) — send both fields to be safe. Poll `GET {RMM}/api/commands/{command_id}` for
`status` in {completed, failed, cancelled}; cancel a stuck one with
`POST {RMM}/api/commands/{id}/cancel`.
Cost the fleet a full day of failed Birth Biologic SharePoint uploads (Mac session) before
this was spotted. See [[reference_sharepoint_graph_large_file_upload]]. Auth helper:
`.claude/scripts/rmm-auth.sh` (internal `172.16.3.30:3001`).

View File

@@ -0,0 +1,25 @@
---
name: reference_gururmm_user_manager
description: GuruRMM has a built-in per-agent User Manager (endpoint local/domain/AAD users) — use it, not raw PowerShell
metadata:
type: reference
---
GuruRMM has a **built-in per-agent User Manager tab** (Users + Groups) for managing the
*endpoint's* Windows accounts — do NOT hand-roll `Set-ADAccountPassword`/`net user` via `/rmm`
PowerShell for routine user ops.
- UI: agent detail → **User Manager** tab (`dashboard/src/components/UserManagerTab.tsx`).
- API: `GET /api/agents/{id}/users` (inventory: users, groups, `domain_join_type`, `domain_name`,
`m365_tenant_id`, **`is_dc`**, `last_collected`), `POST /api/agents/{id}/users/refresh`,
`POST /api/agents/{id}/users/action` body `{username, action, value?, new_password?, group_name?}`.
- Actions: `reset_password`, `set_enabled` (enable/disable), `set_password_never_expires`,
`add_to_group`, `remove_from_group`.
- **Scope gate:** local accounts always manageable; **domain accounts manageable only when the agent
IS a DC** (`is_dc` true) — on a non-DC member they show "Managed in AD" (read-only). AAD accounts
show "Managed in Azure" (read-only). So to reset a domain user, target a DC agent.
- Advantage over raw PowerShell: structured action keeps the password out of the RMM command
history (raw `Set-ADAccountPassword` leaks the plaintext into `command_text`).
(Distinct from SPEC-027 / `usersApi` = GuruRMM's OWN dashboard login accounts — different thing.)
Correction logged 2026-06-29 after I wrongly claimed no built-in action existed. See [[reference_resource_map]].

View File

@@ -0,0 +1,14 @@
---
name: reference-inky-outbound-breaks-dmarc
description: INKY/GuruProtect outbound re-injection breaks DMARC alignment; reverse-resolve DMARC report source IPs before attributing failures
metadata:
type: reference
---
When analyzing DMARC aggregate (rua) reports, **reverse-resolve every failing source IP before attributing a failure to a sender.** Multiple ACG client failures (glaztech.com p=reject, cryoweave.com p=quarantine) traced not to the apps/websites first assumed, but to **INKY (GuruProtect)** outbound: PTRs `ipw-outbound.inkyphishfence.com` and `us.cloud-sec-av.com` (AWS IP pools 3.x / 34.210.x / 35.174.x / 100.2x.x).
**Mechanism:** INKY receives an already-signed message (M365 `selector1` or cPanel `default` DKIM), **modifies the body** (GuruProtect banner / link rewrite), then re-sends from its own IPs. Body change breaks the original DKIM; INKY IPs aren't in the domain's SPF → SPF fails too → DMARC fails. So a domain can be 99% aligned and still show a steady trickle of INKY-origin fails.
**INKY topology (per Mike, 2026-06-23):** INKY is installed *directly into M365* via connectors + transport rules per enrolled tenant — NOT an external-only relay. So check tenant enrollment. NOTE: cryoweave M365 is **NOT** enrolled in INKY, yet cryoweave.com mail still appeared from INKY outbound IPs — that traffic is the **IX/cPanel website** (WordPress, `envelope=ix.azcomputerguru.com`, cPanel `default` DKIM) whose *hosting-level* outbound routes through INKY, independent of the M365 tenant. Don't assume "INKY fail" == "tenant on INKY."
**Fix is INKY-side, not DNS-blind:** republishing cPanel DKIM or adding the web server to SPF will NOT fix it (INKY breaks the sig downstream and the mail comes from INKY's IP, not the web server's). Real options: enable INKY outbound DKIM signing per domain, add INKY's SPF include, or ARC — or route low-volume app/website mail through an authenticated M365 SMTP path so it aligns directly. See [[feedback-dmarc-rua-inky-onboarded-only]].

View File

@@ -0,0 +1,12 @@
---
name: reference_investigator_exo_manageasapp_gap
description: Why the remediation-tool investigator-exo tier 401s on EXO adminapi; use exchange-op instead
metadata:
type: reference
---
The **Security Investigator** app (`bfbc12a4-f0dd-4e12-b06d-997e7271e10c`) registration grants only the `full_access_as_app` (EWS) Office 365 Exchange Online app role — it is **missing `Exchange.ManageAsApp`**. The EXO REST admin API (`outlook.office365.com/adminapi/beta/{tid}/InvokeCommand` — Get-Mailbox, Get-InboxRule, Search-UnifiedAuditLog, etc.) requires `Exchange.ManageAsApp`, so the `investigator-exo` token returns **401** on every cmdlet.
The Exchange Administrator **directory role** is NOT the cause — it is already assigned to the Investigator SP. The gap is the **API permission** on the app registration, which is a manual Entra portal change (suite rule: app registrations stay manual) and arguably shouldn't be added to the read-only Investigator at all.
**How to apply:** for any EXO `InvokeCommand` work (mailbox reads, inbox rules, message trace, TABL, audit), use the **`exchange-op`** tier — the **Exchange Operator** app (`b43e7342-5b4b-492f-890f-bb5a4f7f40e9`) carries BOTH `full_access_as_app` and `Exchange.ManageAsApp`. Don't waste a round trip on `investigator-exo` for adminapi. See [[reference_tedards_tenant_facts]].

View File

@@ -0,0 +1,38 @@
---
name: reference_m365_app_sharepoint_rest_vs_graph
description: ComputerGuru M365 app suite — SharePoint app-only WORKS but requires the CERT (client_assertion), not the secret. Secret => "Unsupported app only token". Use get-token.sh <tenant> sharepoint.
metadata:
type: reference
---
The ACG **Tenant Admin app** (`709e6eed-0711-4875-9c44-2d3518c47063`) has **full SharePoint app-only
access** — `Sites.FullControl.All` on the SharePoint resource — but it is gated by an auth-method rule
that keeps biting:
- **SharePoint app-only REQUIRES a CERTIFICATE (client_assertion). A `client_secret` token is rejected**
with **`Unsupported app only token`** on EVERY SharePoint endpoint (REST `/_api/...` and CSOM
`/_vti_bin/client.svc/ProcessQuery`). This is a SharePoint platform rule, not a missing grant.
- **Graph** app-only accepts the secret fine — so Graph SharePoint calls (driveItem `createdBy`/
`lastModifiedBy`/`retentionLabel`, item `/permissions` + inheritance, `/groups` + members) work with
the secret and are the right tool for *investigation*.
**Do NOT hand-roll a SharePoint token from the secret. Use the remediation-tool cert tiers:**
```bash
cd .claude/skills/remediation-tool
bash scripts/get-token.sh <tenant> sharepoint # content resource <name>.sharepoint.com (cert)
bash scripts/get-token.sh <tenant> sharepoint-admin # admin resource <name>-admin.sharepoint.com (cert)
```
`get-token.sh` forces cert for the `sharepoint*` tiers automatically; the minted token's `roles` =
`["Sites.FullControl.All"]`. The cert is vaulted in `msp-tools/computerguru-tenant-admin.sops.yaml`
(`cert_thumbprint_b64url` + `cert_private_key_pem_b64`). Tenant arg = the domain (`birthbiologic.com`).
**Use the CERT (SP REST/CSOM) for:** list settings (`ForceCheckout`, `EnableModeration`/content approval,
versioning), per-item `CheckoutUserId`/checkout state, re-stamping `Author`/`Editor` (Created By /
Modified By), site lock, tenant settings. Graph can't do these.
Reference (authoritative): `.claude/skills/remediation-tool/references/app-permissions-and-sharepoint.md`
and `app-suite.md`. **Before ever telling the user "the tool can't do X" on SharePoint, use the cert
tier and verify** — the "Unsupported app only token" wall means *wrong auth method (secret)*, NOT no access.
See [[birth-biologic]] (migrated content owned by "SharePoint App" => greyed-out Move; fix = re-stamp
Author/Editor via cert CSOM `SystemUpdate`). Open question (Mike, 2026-07-06): standardize ALL M365
app-only auth on cert (cert is resource-agnostic + more secure) to kill this secret-vs-cert friction.

View File

@@ -0,0 +1,30 @@
---
name: reference_msp360_backup_monitoring
description: MSP360 MBS /api/Monitoring is the authoritative "is this client backing up" source for the whole ACG fleet
metadata:
type: reference
---
For any "is client X actually backing up / when did it last run" question, the authoritative
source is the **MSP360 Managed Backup Service API**, not B2 bucket contents (buckets are just
where Cloud plans land; a bucket with files does not prove a recent backup ran).
- Creds: vault `msp-tools/msp360-api.sops.yaml` (API login+password, generated in MSP360
console Settings>General — not the console login).
- Auth: `POST https://api.mspbackups.com/api/Provider/Login` `{UserName, Password}` -> Bearer
`access_token`.
- `GET /api/Monitoring` -> every backup plan across all companies: `CompanyName`,
`ComputerName`, `PlanName`, `StorageType`, `LastStart`, `Status` (0/1=Success, 2=Warning,
3=Failed, 4=Running, 6=Interrupted, 7=completed-with-warnings), `DataCopied`, `TotalData`,
`ErrorMessage`. Also `GET /api/Companies` (37, with Destinations) and `GET /api/Users` (59).
`/api/Computers` is 404.
- Used 2026-07-05 for the GPS backup map (`projects/gps-rmm-audit/backup-map.md`): resolved 11
of 12 "no destination found" clients, surfaced 4 not-backing-up findings (AMT + T&C Sorensen
= 0 plans, Grabb GND-SERVER failing, Bill Tedards ~12mo stale). MSP360 CompanyName also IDs
B2 buckets: `ACG-PST`=Peaceful Spirit, `ACG-TCA`=Tucson Coin and Autograph.
- Backup targets are a per-client decision — report mismatches, don't deploy jobs. See
[[feedback_backup_targets_never_guessed]]. Related: [[reference_backblaze_storage_rate]].
- **Use the `msp360` skill** (`.claude/skills/msp360/`) — built 2026-07-05, full (not
read-only): reads (`status`/`monitoring --failed|--company|--stale`/`companies`/`users`/
`licenses`/`billing`) plus `--confirm`-gated writes (create/delete user+company,
grant/release/revoke license) and a `raw` passthrough for the rest of the MBS API.

View File

@@ -0,0 +1,27 @@
---
name: reference_packetdial_oit_netsapiens
description: VoIP vendor stack — PacketDial = ACG's VoIP department/brand; runs on NetSapiens (the PBX platform) supplied by OIT/OITVOIP (white-label wholesaler). YMCS = Yealink device mgmt.
metadata:
type: reference
---
ACG's VoIP vendor stack, top to bottom (don't conflate these):
- **PacketDial** = ACG's own **VoIP department / customer-facing brand**. `pbx.packetdial.com` is
the ACG-branded host. The `packetdial` skill is ACG's VoIP-management tool — keep it named
`packetdial` (ACG's brand) even though it speaks the NetSapiens API.
- **NetSapiens** = the underlying **PBX/UCaaS software platform** (open API v2, v44.4.10). What the
skill actually talks to.
- **OIT / OITVOIP** = the **white-label wholesale provider** that runs NetSapiens and resells it to
MSPs; ACG buys VoIP wholesale from OIT. `api.ucaasnetwork.com` is the OIT/NetSapiens host (same
platform as `pbx.packetdial.com`, different white-label hostname). Reseller territory
`91912.service` is ACG's account on OIT's platform; the reseller API key is vaulted at
`msp-tools/oitvoip.sops.yaml` (key-id `nsr_hSGUB5Wo`).
- **YMCS (Yealink Management Cloud Service)** = separate Yealink **device-management** cloud for the
physical phones (`us-api.ymcs.yealink.com`); pairs with the PBX (phones register to
pbx.packetdial.com). API creds `services/yealink-ymcs.sops.yaml`; portal login
`infrastructure/voip-phones.sops.yaml`.
One line: customer-facing brand = **PacketDial (ACG)**; platform = **NetSapiens**; wholesaler =
**OIT/OITVOIP**; phone device-mgmt = **YMCS (Yealink)**.
Related: [[reference_resource_map]]

View File

@@ -0,0 +1,32 @@
---
name: reference_remediation_tool_365_access
description: The remediation-tool app suite has full M365 access (incl. SharePoint via cert); don't declare "no access" on an accessDenied
metadata:
type: reference
---
The ComputerGuru remediation-tool apps collectively have **broad, working access across ALL of
M365** — Graph, Exchange Online, Defender, AND SharePoint Online. When a call fails it's almost
always wrong-tier / wrong-endpoint / not-consented / the SharePoint cert gotcha — **not** a real
lack of access. Do NOT tell the user "the tool can't do X" without checking the live permission
map first (decode the token `roles` claim).
Key facts:
- **SharePoint app-only requires a CERTIFICATE.** A `client_secret` token is rejected on every
SharePoint endpoint (REST `/_api` and CSOM `/_vti_bin/client.svc/ProcessQuery`) with
`"Unsupported app only token"`. The Tenant Admin app has a cert in the vault and holds
SharePoint-resource `Sites.FullControl.All`.
- `get-token.sh` now has **`sharepoint`** (content) and **`sharepoint-admin`** (tenant admin)
tiers — cert-forced, tenant resource auto-resolved from Graph `/sites/root`
(override `SP_RESOURCE_ENV`). Added 2026-07-01.
- Graph `GET /admin/sharepoint/settings` needs `SharePointTenantSettings.Read.All`, which NO app
holds → that route 403s. Read/write SharePoint tenant settings via the **CSOM admin API**
(`sharepoint-admin` tier) instead. Tenant settings live on the Tenant object
(TypeId `{268004ae-ef6b-4e9b-8425-127220d84719}`) — e.g. `SelfServiceSiteCreationDisabled`.
- Restricting employee SharePoint site creation = `SelfServiceSiteCreationDisabled=true` (CSOM)
AND restrict M365 Group creation (Entra `Group.Unified` directory setting via `user-manager`);
neither affects edit rights on existing sites.
Full detail (live per-tier permission map + CSOM examples):
`.claude/skills/remediation-tool/references/app-permissions-and-sharepoint.md`. Surfaced by
Syncro #32492 (Birth Biologic). See also [[feedback_syncro_billing]].

View File

@@ -129,7 +129,7 @@ type: reference
- Detail: [[reference_gururmm]], [[project_gururmm]], [[feedback_gururmm]]. - Detail: [[reference_gururmm]], [[project_gururmm]], [[feedback_gururmm]].
### ScreenConnect / CW Control ### ScreenConnect / CW Control
- Primary remote-access tool. `msp-tools/screenconnect.sops.yaml`. - Primary remote-access tool. `msp-tools/screenconnect.sops.yaml`. Skill: `/screenconnect` (sessions, parameterized self-tagging installer, gated backstage control). See [[reference_screenconnect_api]].
- **Gotcha:** Toolbox scripts truncate lines >80 chars silently; no inline comments mid-script. See [[reference_msp_audit_scripts]]. - **Gotcha:** Toolbox scripts truncate lines >80 chars silently; no inline comments mid-script. See [[reference_msp_audit_scripts]].
### Splashtop (SOS / Streamer) ### Splashtop (SOS / Streamer)

View File

@@ -0,0 +1,30 @@
---
name: reference_rmm_deploy_via_screenconnect
description: Best channel to mass-deploy the GuruRMM agent to client workstations = ScreenConnect send-command (not DC remote-exec)
metadata:
type: reference
---
**To push the GuruRMM agent onto a client's existing machines, the reliable channel is
ScreenConnect `send-command` (Backstage), NOT remote-exec from the domain controller.**
Discovered 2026-07-03 deploying to Instrumental Music Center (see
`projects/gps-rmm-audit/tracker.md`). Remote-exec FROM the DC to Win10/11 workstations
fails on default client settings:
- WMI/`Win32_Process` over **DCOM** -> "RPC server unavailable" (DCOM firewalled on clients)
- `schtasks /S` -> connects over SMB but Win11 **rejects the task definition** from an older
(Server 2016) DC ("The request is not supported")
- `New-PSDrive`/`sc.exe` admin-share from SYSTEM context -> access/parse failures
- WinRM is off by default on workstations
**What works cleanly:** ScreenConnect. ACG endpoints already run the SC agent, and
`send-command` runs on the guest **as SYSTEM** — no credentials, no firewall fight, no
DA-password-in-logs concern. Pattern (via the `[[screenconnect]]` skill):
1. `GetSessionsByName` with the EXACT hostname -> sessionID (no list-all method; must query by exact name).
2. Build the site installer one-liner: `irm 'https://rmm.azcomputerguru.com/install/<SITE-CODE>/windows'|iex` (site code from the client's vault `gururmm-site-*.sops.yaml`).
3. Encode it: `powershell -NoProfile -ExecutionPolicy Bypass -EncodedCommand <base64-UTF16LE>` (avoids all quoting; no /TR length limit like schtasks).
4. `send-command --session <id> --command "<that>" --confirm`. Online guests install + enroll in ~1-3 min; offline guests QUEUE in SC and install on reconnect.
5. Verify via GuruRMM `/api/agents` (hostname appears under the client, status online).
Note `iconv` is absent in this Git-Bash — compute the base64 with `py -c "import base64; ..."`.
Related: `[[project_av_migration_bitdefender_to_edr]]` (same channel can push the Datto EDR agent + remove Bitdefender once RMM is on).

View File

@@ -0,0 +1,34 @@
---
name: reference_rmm_drive_map_explorer_refresh
description: Mapping a drive for a user via RMM user_session works but their running Explorer won't show it until a shell DRIVEADD notify; also UNC \\ gets eaten in heredoc+jq dispatch
metadata:
type: reference
---
When you map a persistent network drive **for a logged-on user** via the GuruRMM agent's
`context: user_session` (`net use` / `New-SmbMapping -Persistent $true`), two things bite:
1. **The map lands in the user's session but their already-running Explorer won't display it.**
The drive IS mounted (verify: `user_session` SID == `explorer.exe` SID via
`Get-CimInstance Win32_Process -Filter "Name='explorer.exe'"`) and `Test-Path X:\` is True,
but "This PC" doesn't show the icon because the shell never got the add notification.
**Fix (no disruption, runs in user_session = the user's session 1):**
```powershell
$sig = @'
[DllImport("shell32.dll", CharSet=CharSet.Unicode)] public static extern void SHChangeNotify(int eventId, uint flags, string item1, string item2);
'@
$sh = Add-Type -MemberDefinition $sig -Name ShellNotify -Namespace W -PassThru
$sh::SHChangeNotify(0x00000100, 0x0005, 'X:' + [char]92, $null) # SHCNE_DRIVEADD, SHCNF_PATHW
```
The persistent map (`HKCU\Network\X`) auto-reconnects + shows on the user's NEXT logon anyway,
so this is only to surface it in the current session. Restarting explorer.exe also works but
closes the user's open windows. An interactive scheduled task (`LogonType Interactive`) to
"remap in the session" returned `LastTaskResult=2` and did NOT help — use SHChangeNotify.
2. **UNC double-backslashes get mangled to single in the heredoc -> jq -> agent -> PowerShell chain.**
`\\cs-server\share` arrives as `\cs-server\share` -> "error 67 / network name not found" or net-use
hangs (looks like a missing/broken share). Single-backslash local paths (`D:\Shares`) are fine.
**Fix:** build the UNC at runtime from `[char]92` so no literal `\\` traverses the dispatch:
`$bs=[char]92; $unc = "{0}{0}server{0}share" -f $bs`. See [[feedback_windows_quote_stripping]].
Proven 2026-06-24 on Cascades #32193 (Executive share, E: for Ashley.Jensen + Meredith.Kuhn).

View File

@@ -0,0 +1,19 @@
---
name: reference_rmm_map_network_drive
description: How to push a persistent mapped network drive to a machine via GuruRMM when net use fails with error 67 (double-hop)
metadata:
type: reference
---
Pushing a **persistent mapped drive** to an endpoint via the GuruRMM agent (`/rmm`) fails when the target share is on a *remote* server:
- Running `net use` in `context: user_session` impersonates the logged-on user, but that WTS-impersonated token has **no network credential** to make the second hop to the file server. Result: `System error 67 (network name cannot be found)` on `net use` and `System error 1702 (binding handle is invalid)` on `net view` — even with explicit `/user:.. <pw>`. This is the "SMB error 67 = RMM artifact" documented in `wiki/clients/cascades-tucson.md` (server + share are healthy; access works in a real interactive session).
**Reliable workaround — plant the map so it mounts at the user's next real logon:**
1. `cmdkey /add:<SERVER> /user:<DOMAIN\user> /pass:<pw>` in `user_session` — this is a *local* write to the user's Credential Manager and DOES succeed.
2. Write the persistent-map registry keys into the user's hive `HKCU:\Network\<DriveLetter>`: `RemotePath` (REG_SZ, `\\SERVER\Share`), `UserName` (REG_SZ, `DOMAIN\user`), `ProviderName` (`Microsoft Windows Network`), `ProviderType` (DWord `131072`), `ConnectionType` (DWord `1`), `DeferFlags` (DWord `4`).
3. At the user's **next interactive logon / reboot**, Windows reconnects the drive silently using the cmdkey credential. It will NOT appear in an already-open session — for immediate visibility, run `net use <D>: "\\SERVER\Share"` in the *live* interactive session (ScreenConnect), not through the RMM agent.
Non-domain-joined (workgroup) endpoints authenticate with `DOMAIN\user` + password saved via cmdkey — the domain account only needs to exist and be reachable, the client PC does not need to be joined.
PowerShell-in-RMM gotcha hit while doing this: a double-quoted string ending in a backslash (`"W:\"`, `"W:\\"`) breaks the parser — use bare path tokens (`Test-Path W:\`) or single quotes. See [[feedback_windows_quote_stripping]].

View File

@@ -0,0 +1,33 @@
---
name: rmm-spawn-headless-claude
description: Spawn a headless `claude -p` on any RMM-managed Windows box that has Claude Code installed — reaches isolated sites (AD2) the coord API can't
metadata:
type: reference
---
Any RMM-managed Windows endpoint with Claude Code installed can run an autonomous headless
Claude, dispatched via a GuruRMM command — even a site that's isolated from the ACG coord API.
The RMM agent phones home outbound, so this works where [[ad2-comms-via-sync-only]] says coord
can't reach (coord `:8001` blocked ≠ RMM `:3001` blocked). Validated 2026-07-01 on AD2
(Dataforth DC, agent `cfa93bb6-...`, claude v2.1.181 at `C:\Users\sysadmin\.local\bin\claude.exe`).
Recipe:
- Dispatch with **`"context":"user_session"`** — needs an interactive logged-on user (check
`quser`); an admin session comes back elevated. `claude` is a per-user install, not on the
SYSTEM PATH, so SYSTEM context won't find it.
- **GOTCHA: unset `ANTHROPIC_API_KEY` first.** A stale machine-level `ANTHROPIC_API_KEY` (108-char)
shadows the good OAuth creds and makes `claude -p` fail with `Invalid API key · Fix external API
key`. `Remove-Item Env:\ANTHROPIC_API_KEY` (+ `$env:ANTHROPIC_API_KEY=$null`) before invoking →
falls back to `~\.claude\.credentials.json` OAuth and authenticates.
- **Detach + poll.** A real audit run takes many minutes; RMM caps command lifetime (see
[[gururmm-command-timeout-seconds]] — use `timeout_seconds`). Launch detached
(`Start-Process powershell -File runner.ps1 -WindowStyle Hidden`), have the runner write the
deliverable to a file + a `DONE.txt` marker, and poll the marker via short RMM commands.
- Run headless as: `claude -p <brief> --permission-mode bypassPermissions --output-format text`.
For an audit, give an ironclad READ-ONLY brief (no writes/git/state changes) since
bypassPermissions lets it run any tool. Pass the brief via a base64'd file to dodge quoting.
- Windows/Git-Bash: the mingw `curl` intermittently hits `Permission denied` (AV lock) —
use `/c/Windows/System32/curl.exe` for the dispatch. See [[feedback_windows_quote_stripping]].
Use for: live audits/data-gathering on isolated or hard-to-reach managed boxes without the async
sync-handoff. Keep it read-only on production (AD2 is a domain controller).

View File

@@ -1,15 +1,22 @@
--- ---
name: reference_screenconnect_api name: reference_screenconnect_api
description: Working auth + method for the ACG ScreenConnect RESTful API extension (CTRLAuthHeader = raw secret, GetSessionsByName) description: ACG ScreenConnect RESTful API auth + verified method surface (CTRLAuthHeader=raw secret); now wrapped by the /screenconnect skill
metadata: metadata:
type: reference type: reference
--- ---
ACG ScreenConnect RESTful API extension verified working call (2026-06-02, Howard). Credentials in vault `msp-tools/screenconnect.sops.yaml` (`credentials.username`, `credentials.api_secret`). ACG ScreenConnect (CW Control) RESTful API Manager extension. Auth verified 2026-06-02;
full method surface + parameterized-installer deploy verified live 2026-06-21 (Howard).
**Now wrapped by the `/screenconnect` skill** (`.claude/skills/screenconnect/`) — use that
(`sc.py`/`sc_client.py`) rather than hand-rolling calls. Secret in vault
`msp-tools/screenconnect.sops.yaml` (`credentials.api_secret`).
- **Host:** `https://computerguru.screenconnect.com` **extension-guid:** `2d558935-686a-4bd0-9991-07539f5fe749` - **Host:** `https://computerguru.screenconnect.com` **extension-guid:** `2d558935-686a-4bd0-9991-07539f5fe749`
- **Auth (the non-obvious part):** header `CTRLAuthHeader: <raw api_secret>` with **NO `Basic ` prefix and no base64** + header `Origin: https://computerguru.screenconnect.com`. Putting the secret in `Authorization: Basic <b64>`, or `CTRLAuthHeader: Basic <b64>`, both return 401. Raw secret in CTRLAuthHeader is what works. - **Auth (the non-obvious part):** header `CTRLAuthHeader: <raw api_secret>` with **NO `Basic ` prefix and no base64** + header `Origin: https://computerguru.screenconnect.com`. `Authorization: Basic <b64>` or `CTRLAuthHeader: Basic <b64>` both 401. Raw secret in CTRLAuthHeader is what works. Endpoint: `POST /App_Extensions/<guid>/Service.ashx/<Method>`.
- **Only method that exists:** `POST /App_Extensions/<guid>/Service.ashx/GetSessionsByName` with JSON body `{"sessionName":"<name>"}`. Every other `Get*` name (GetSessions, GetSessionList, GetHosts, ...) returns 500 `"Web method does not exist"`. Bad/missing params return 500 `"Unknown parameter: <x>"` — the valid param is `sessionName`. - **Verified methods (CORRECTS the old "only GetSessionsByName" note):** reads take a JSON object, writes take a POSITIONAL ARRAY.
- **Big limitation:** the match is on the session `Name` field, which is **blank for unattended access agents**, so this api user only enumerates a handful of named sessions — it CANNOT list a client's full machine inventory. For per-machine last-seen across a whole client, the API is not sufficient; read the ScreenConnect console (or a screen recording) instead. Session objects do carry `LastConnectedEventTime`, `LastEventTime`, `GuestInfo.LastActivityTime`, and custom props CP1=Company / CP2=Site / CP3=Tag. - Reads: `GetSessionsByName {"sessionName":"<name>"}`, `GetSessionDetailsBySessionID {"sessionID":"<id>"}`, `GetSessionBySessionID`.
- Writes (gated in the skill): `SendCommandToSession ["<id>","<cmd>"]` (backstage command on the guest), `SendMessageToSession ["<id>","<msg>"]`, `UpdateSessionCustomProperties ["<id>",["cp1","cp2","cp3",...]]`. CP1=Company / CP2=Site / CP3=Tag (up to CP8).
- **Parameterized access installer (deploy):** the cloud serves a pre-keyed installer at `/Bin/ScreenConnect.ClientSetup.<ext>?e=Access&y=Guest&t=<name>&c=<CP1>&c=<CP2>&c=<CP3>...` (ext: msi/exe/pkg/deb/rpm/sh). The repeated `c=` self-tag the agent on install, so an RMM-pushed install self-places into the right Company/Site/Tag. Windows silent: `msiexec /i <file> /qn /norestart`. VERIFIED end-to-end on RMM-TEST-MACHINE 2026-06-21.
- **Real limitation (still true):** NO full-fleet inventory method — `GetSessions`/`GetAllSessions`/`GetSessionGroups` return 500 `"Web method does not exist"`. You CANNOT list a client's whole machine inventory via this API yet; needs Mike to update the RESTful API Manager extension (coord msg 60d9e876). Workaround: the installer sets session Name = machine name, so by-name lookup works post-install.
Used during the Dataforth Syncro asset cleanup as the third liveness source alongside Syncro + Bitdefender. See [[reference_acg_msp_stack]]. Used during the Dataforth Syncro asset cleanup as a liveness source. See [[reference_acg_msp_stack]] and the `/screenconnect` skill SKILL.md.

View File

@@ -0,0 +1,28 @@
---
name: reference_screenconnect_custom_property_slots
description: ACG ScreenConnect custom-property slot -> label mapping (CP1..CP8) for session tagging
metadata:
type: reference
---
ACG's ConnectWise ScreenConnect instance (`computerguru.screenconnect.com`) custom-property
slot order, reverse-engineered 2026-07-03 from a labeled test session (HOWARD-HOME, values
"howard company / howards site / Howard department / howard type / ... / howard tag"):
- **CP1** (index 0) = **Company**
- **CP2** (index 1) = **Site**
- **CP3** (index 2) = **Department**
- **CP4** (index 3) = **Device Type**
- CP5 (index 4) = unused/other
- CP6 (index 5) = unused/other
- CP7 (index 6) = unused/other
- **CP8** (index 7) = **Tag**
The API does NOT expose the property LABELS (only values), so this mapping is the reference.
`UpdateSessionCustomProperties [sessionID, [cp1..cp8]]` REPLACES the whole 8-element array — always
read the session's current values first, modify only the slots you intend, write the full array back.
Set via the `[[screenconnect]]` skill: `sc.py set-properties --session <id> --props-json '[...8...]' --confirm`.
Read via `GetSessionsByName` -> `.CustomPropertyValues`.
Used by the GPS->RMM audit ScreenConnect cleanup (`projects/gps-rmm-audit/`): normalize Company,
set Site/Department/Device Type/Tag per machine, dedupe sessions.

View File

@@ -0,0 +1,27 @@
---
name: sharepoint-graph-large-file-upload
description: Uploading files to SharePoint via Graph — simple PUT <4MB, chunked upload session >=4MB; verify counts via delta
metadata:
type: reference
---
Pushing a folder tree into a SharePoint doc library via Microsoft Graph (app-only):
- **<4MB:** simple `PUT /drives/{drive}/root:/{path}:/content`.
- **>=4MB:** MUST use an **upload session**`POST .../root:/{path}:/createUploadSession`
then `PUT` the file in chunks (multiple of 320 KiB; 10 MB works) with a
`Content-Range: bytes {start}-{end}/{total}` header. In PowerShell 5.1
`Invoke-RestMethod -Headers @{ 'Content-Range'=... }` handles this fine. A naive script that
only does <4MB PUTs will silently skip every large file and never reach the target count.
- **Long Windows paths (>260):** prefix the local path with `\\?\` for `[IO.File]` reads.
- **Idempotent sync:** existence-check each file (`GET root:/{path}?$select=size`) and skip if
size matches — this also catches/repairs partial-upload residue from earlier failed runs.
- **Throughput:** a single sequential upload stream to SharePoint Online plateaus ~40 Mbps
regardless of link speed (per-session SPO throttle + PS5.1 HTTP stack + Expect100Continue).
For speed use parallel file streams + larger chunks + `Expect100Continue=$false`.
- **Verify total file count** with the Graph **delta** endpoint
(`/drives/{drive}/root/delta`) — whole-drive enumeration in a few paged calls, far cheaper
than recursive `/children`.
Proven end-to-end on Birth Biologic Quality Systems (3,768 files, 301 >=4MB, ~29.7 GB;
largest 3.94 GB). Dispatched via GuruRMM — see [[gururmm-command-timeout-seconds]].

View File

@@ -0,0 +1,32 @@
---
name: reference_syncro_agent_handle_leak
description: RDS "no available computers in the pool" (0x3/0x408) can really be a SyncroLive.Agent.Runner handle leak starving the box. How to spot + fix.
metadata:
type: reference
---
**Symptom chain that hid the real cause (IMC1 / Instrumental Music Center, 2026-06-25):**
RemoteApp/RDP launch fails → *"There are no available computers in the pool"* (RDP **error 0x3 / extended 0x408**).
The RD Connection Broker **Admin** log (`Microsoft-Windows-TerminalServices-SessionBroker/Admin`,
**Event 802**) shows the truth: *"RD Connection Broker failed to process the connection request …
Error: **Insufficient system resources** exist to complete the requested service."* The
SessionBroker-Client log shows 1296 "Element not found" / 1306 redirect-failed. The collection +
session host are healthy (`NewConnectionAllowed: Yes`), so the broker isn't the bug — the **box is
out of a kernel resource**.
**Root cause:** the **Syncro RMM agent `SyncroLive.Agent.Runner` was leaking HANDLES** — 1,135,414
handles in one process (~80% of the box's 1.41M total). Handle/object exhaustion → broker can't
create the session.
**Diagnose:** `Get-Process | Sort-Object Handles -Descending | Select -First 6 Name,Handles,Id` and
`(Get-Process | Measure-Object Handles -Sum).Sum`. Memory looks fine (it's handles, not RAM/commit).
Services on the box: `Syncro`, `SyncroLive`, `SyncroOvermind` (SyncroRecovery).
**Fix (no reboot needed):** `Stop-Process -Name 'SyncroLive.Agent.Runner' -Force` — the Syncro
watchdog respawns it clean (dropped 1.41M → 280K handles, runner came back at ~900). Have the user
retry the RemoteApp immediately.
**It recurs** (leak accumulates over uptime) → schedule a periodic SyncroLive restart and/or update the
agent; **likely fleet-wide** — sweep other client servers for high-handle `SyncroLive.Agent.Runner`
(deferred 2026-06-25). IMC1 also had a separate pending reboot (wedged KB5075999) + expired RDS certs.
See [[reference_resource_map]].

View File

@@ -0,0 +1,15 @@
---
name: reference-syncro-rmm-api-gui-only
description: Syncro's public API cannot manage RMM policies/folders — creation, assignment, and asset moves are GUI-only (live-verified 2026-06-25)
metadata:
type: reference
---
**Syncro RMM policy management is GUI-only — the public REST API does NOT expose it.** Live-verified against the ACG production tenant (computerguru.syncromsp.com) on 2026-06-25:
- `GET /customer_assets` objects carry a read-only **`policy_folder_id`** field (which policy folder the machine sits in). **`PUT /customer_assets/{id}` with `policy_folder_id` is silently ignored** — returns HTTP 200 but the value never changes. Proven by a flip-and-restore test on ACG-internal asset 12335235 (DESKTOP-0O8A1RL): value stayed at folder 692253. **You CANNOT move a machine between policy folders via the API.**
- `/policies`, `/policy_builders`, `/rmm_policies`, `/asset_policies` all return **404** — no policy-CRUD endpoints exist. Policy Builder (the `/policy_builders` GUI page) is web-console only.
- `/policy_folders` (collection and specific-ID) returns **401** — the route exists but our API token lacks RMM/policy scope. A re-issued token *might* read folders, but since assets can't be moved anyway, it's moot for the move use case.
- Syncro docs (docs.syncrosecure.com / docs.syncromsp.com "Work with Policies") confirm: policies are created in Policy Builder, assigned via an Organization's "Assets & Policies" subtab "Update Assigned Policy" dropdown, or "Bulk Assign Top-Level Policy" — all GUI, **no API mention**.
**How to apply:** Do NOT attempt to build `/syncro move-asset` or any Syncro RMM policy/folder/group capability — it's not buildable on the public API. Don't re-probe these endpoints. The only API-drivable policy surface in the fleet is the `bitdefender` skill (GravityZone: create/assign policies, custom groups, move endpoints). For Syncro RMM policy work, direct the user to the Syncro web console. The `/syncro` skill stays PSA-only (tickets/billing/customers/scheduling/estimates + read-only asset lookup). See [[feedback-psa-default-syncro]].

View File

@@ -0,0 +1,27 @@
---
name: reference_tailscale_subnet_key_expiry
description: "Internet OK but all of 172.16.3.x dead" = Tailscale infra-node key expiry, not a LAN outage. How to diagnose + the fallback path.
metadata:
type: reference
---
The ACG internal subnet **172.16.3.x is reached over Tailscale**, not a local LAN — `pfsense-2`
(the pfSense node) is the **subnet router** advertising **172.16.0.0/22**. Key hosts on it:
Gitea/Jupiter `172.16.3.20:3000`, GuruRMM + coord `172.16.3.30:3001`/`:8001`.
**Symptom → cause:** if `sync.sh` fetch fails and the WHOLE `172.16.3.x` subnet is unreachable
(both .20 and .30) **while general internet is fine**, the cause is almost always a **Tailscale
node KEY EXPIRY** on an infra node (the subnet router or a server) — an expired key drops that node
off the tailnet, killing the route. It is NOT a "transient blip" and NOT a real LAN outage (logged
as a correction 2026-06-25 after I mis-called it). Mike **disabled key expiration** on the infra
node(s) 2026-06-25 so it shouldn't recur; if it does, re-auth the node + confirm expiry is off in the
Tailscale admin console.
**Diagnose (Windows `tailscale.exe` at `C:\Program Files\Tailscale\`):**
- `tailscale status` — look for peers marked `offline`/key-expired, esp. `pfsense-2` and `gururmm-server`.
- `tailscale debug prefs | grep RouteAll` — must be `true` (this machine accepts subnet routes).
- `tailscale status --json` — confirm a peer advertises `172.16.0.0/22` (PrimaryRoutes) and is `Online`.
- `tailscale ping <tailnet-100.x>` — tests tailnet path independent of the subnet route.
**Fallback:** `gururmm-server` is directly reachable at its **tailnet IP `100.86.12.15:3001`** — usable
in place of `172.16.3.30:3001` if the subnet route is down but the node itself is up. See [[feedback_tmp_path_windows]].

View File

@@ -0,0 +1,16 @@
---
name: reference_tedards_tenant_facts
description: Tedards (Bill Tedards law office) M365 tenant facts for investigations
metadata:
type: reference
---
**Tedards** = Bill Tedards law office. M365 tenant `tedards.net`, tenant ID `4fcbb1f4-fbf9-4548-a93e-7d14a3c091e6`. Registry lists Onboarded=NO but the ComputerGuru apps ARE consented and working (Graph investigator + EXO exchange-op both verified live 2026-06-25).
Mailboxes: `bt@tedards.net` (Bill, owner), `y226@tedards.net` (Yvonne). Bill files mail by legal matter number into deep Inbox subfolders (e.g. "8445 BOLTON [Farmers TX]", "BOLTON, Lindsay"); a top-level "DUPLICATE need to check" folder (~11,864 items) is junk from a **botched mail import years ago** — ignore it.
Security gaps found 2026-06-25: tenant was **dehydrated** (never customized) — ran `Enable-OrganizationCustomization` (irreversible, one-time) then `Set-AdminAuditLogConfig -UnifiedAuditLogIngestionEnabled $true` (both HTTP 200). Flag later confirmed `true`, BUT app-only `Search-UnifiedAuditLog` (via exchange-op InvokeCommand) returns **zero** records for this tenant even hours after ingestion — proven against ~11,800 known dedup MoveToDeletedItems events AND a tenant-wide `RecordType=ExchangeItem` query (both 0). Conclusion: **app-only UAL cannot read mailbox-item records here** — do NOT rely on it for mailbox-item attribution; use device-statistics sync-time + EWS bait timing instead (how the bt@ culprit was found). Before enabling, UAL was OFF entirely. bt@ mailbox syncs to **9 devices** (5 aging iOS Mail/EAS, a Mac Outlook, 2 Outlook-for-iOS added 2026-06-25). Per-mailbox AuditEnabled=true but not queryable since Search-MailboxAuditLog is deprecated + UAL ingestion off.
EXO access: use the `exchange-op` tier, not `investigator-exo` — see [[reference_investigator_exo_manageasapp_gap]]. Ongoing matter: Wirechunk/agencyzoomify.com DMARC + the bt@ "delete folder" deletions (ticket #5070, #32228).
**bt@ "delete folder" mystery — SOLVED 2026-06-26 (root cause = client-side device auto-delete).** Lindsay's (lindsay@agencyzoomify.com) Bolton-thread mail auto-moves to Deleted Items. Proven via bait test: restored the 3 msgs to Inbox via EWS, all 3 were re-deleted to Deleted Items at the identical instant (02:54:24Z) — automated, not human. Eliminated: inbox rules (incl. hidden), sweep rules, transport rules, forwarding, delegates, folder perms, and any OAuth app with Mail.ReadWrite (none exist; admin apps only have MailboxSettings.ReadWrite/Mail.Send/Exchange.Manage). Only mail-MOVE capability present = Apple native iOS Mail (appId 32f67a9b, EAS+EWS) + Outlook-for-iOS. 5 Apple/Outlook-iOS devices push-synced within 8s of the re-delete; all activity from Bill's single home IP 69.242.239.94 (NOT a compromise). Bisection test (2026-06-26 03:23): removed/disabled Outlook-for-iOS, re-moved msgs to Inbox — re-deleted again in 4s, AND the re-delete (03:23:31Z) fired BEFORE the Outlook-iOS clients re-synced (03:23:38/48) → **Outlook-iOS EXONERATED; culprit = a NATIVE iOS Mail (EAS) device** with on-device "Block Sender → Move to Trash" for lindsay@agencyzoomify.com. Narrowed to the two devices syncing at the delete instant: **iPhone16C2** and **iPad15C8** (Bill's current iPhone + iPad). On-device block list is NOT server-readable — Bill must remove Lindsay from Blocked on those devices (iOS Settings → Mail → Blocked). Note: Set-CASMailbox OutlookMobileEnabled has heavy propagation lag (didn't enforce during the test window); same lag seen on Set-AdminAuditLogConfig. To pin the exact one device, block one EAS DeviceId and re-bait. Removed 2 Outlook-iOS partnerships (they auto-re-add) + toggled OutlookMobileEnabled (reverted to true, queued). **CONFIRMED 2026-06-26 by Yvonne: found Bolton's blocked contact on Bill's NEW iPad (= iPad15C8); unblocked on phone + new iPad, checking other iPads — validates the device-block root cause.**

View File

@@ -0,0 +1,54 @@
---
name: reference_windows_edition_upgrade_rmm
description: Verified fleet procedure to upgrade a Windows machine Home->Pro/Pro-for-Workstations via RMM, with the three gotchas (DISM error 50, shutdown /t drop, PfW MAK auto-upgrade).
metadata:
type: reference
---
Verified on ACG-Tech03L 2026-07-06 (Home 26200 -> Win 11 Pro for Workstations, permanently activated).
**Procedure (all steps as SYSTEM via `/rmm`, byte-exact via -EncodedCommand):**
1. Probe: EditionID/LicenseStatus (`.rmm-key-probe.ps1` pattern), confirm no logged-in user
(`query user`) so a reboot is safe.
2. Flip edition: `changepk.exe /ProductKey VK7JG-NPHTM-C97JM-9MPGT-3V66T` (generic Pro key).
Flips EditionID Core->Professional **live** (registry) + sets CBS RebootPending. exit 0.
3. Reboot to finalize (see gotcha #2).
4. Install MAK + activate: `slmgr /ipk <MAK>` then `slmgr /ato`; verify `slmgr /xpr` =
"permanently activated" and WMI SoftwareLicensingProduct LicenseStatus=1.
**Gotcha 1 — DISM is a dead end for online edition change.** `DISM /online /Set-Edition:Professional`
returns **Error 50 "Setting an edition is not supported with online images"** even though
`/Get-TargetEditions` lists Professional. Use `changepk.exe`, not DISM, for online Home->Pro.
**Gotcha 2 — `shutdown /r /t N` is silently DROPPED in the SYSTEM service session.** The command
returns exit 0 but the box never reboots (verified twice on tech03l). Also `shutdown /c "comment"`
fails outright because the RMM->cmd.exe layer strips the quotes ([[feedback_windows_quote_stripping]]).
Reboot reliably with `Restart-Computer -Force` (encoded PS) or `shutdown /r /t 0 /f` (no comment).
NOTE: on a fast SSD the edition-upgrade reboot can be <30s of downtime — a 30s-interval last_seen
monitor can miss the gap entirely; confirm the reboot by `LastBootUpTime` advancing, not by catching
the offline window. `is_connected` is null fleet-wide — track `last_seen` age, never that flag.
**Gotcha 3 — the ACG MAK `infrastructure/windows-pro-mak` is a Pro-for-WORKSTATIONS MAK** (mislabeled
"Pro"). `slmgr /ipk` of it on a Professional machine AUTO-UPGRADES the edition to ProfessionalWorkstation
(a strict superset of Pro), live, **no extra reboot** (verified DALLAS 2026-07-07). If a client genuinely
needs *plain* Pro, this key will over-shoot to PfW — there is no plain-Pro MAK in the vault. Billing:
each activation consumes one MAK count = $99 to the customer (ACG-internal machines: no invoice, still
burns a count).
**Gotcha 4 — `changepk.exe` TIMES OUT under SYSTEM but STILL flips the edition. Do NOT treat the timeout
as failure — VERIFY EditionID.** Run as SYSTEM via `/rmm`, `changepk.exe /ProductKey <key>` does not return
cleanly (the RMM command comes back `failed` / exit -1 `Command timeout`, or your Start-Process/`&` wrapper
hangs to the agent timeout), yet the registry `EditionID` flip to `Professional` DID happen. Always confirm
with a one-liner (`Get-ItemProperty 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion' EditionID`) before
concluding anything — if it reads `Professional`, proceed to the finalize reboot; the running OS still
reports Home in `Win32_OperatingSystem.Caption` until that reboot. (Verified DALLAS 2026-07-07 — the
`changepk` command timed out but EditionID was already `Professional`.)
**Gotcha 5 — Modern Standby laptops (S0 Low Power Idle) drop the RMM/SC agent when idle/lid-closed, which
mimics an endless reboot.** On a Core Ultra / S0-only machine, idle or a closed lid drops Wi-Fi -> agent
offline for long stretches (looked like a 25-min "slow reboot"; the box had actually been up 25 days and
never rebooted). Before a reboot-driven upgrade on a laptop, pin it awake first:
`powercfg /change standby-timeout-ac 0; powercfg /setacvalueindex SCHEME_CURRENT SUB_BUTTONS LIDACTION 0;
powercfg /setactive SCHEME_CURRENT` (AC only — safe; DON'T lid-nothing on DC/battery). A command dispatched
to an already-offline agent goes `pending` and fires (uncontrolled) on next wake — cancel stale ones with
`/rmm cancel`. NOTE: an RDP *target* laptop must STAY awake to be reachable, so keep the AC no-sleep in place.

View File

@@ -0,0 +1,28 @@
---
name: windows-offline-dism-repair-gotchas
description: Windows won't-boot / offline DISM repair playbook — diagnose wedged-update boot loops, and the WinPE source/mount gotchas (wim: not allowed offline, Ventoy breaks WIM mount)
metadata:
type: reference
---
Field-learned gotchas for repairing a Windows box that won't boot, from offline WinPE/recovery. First hit on Four Paws AvImark server (Syncro #32447, 2026-06-23) — boot-looping into Automatic Repair.
**1. Diagnose the REAL boot blocker before chasing DISM corruption.**
A machine that drops into **Automatic Repair** is failing on something boot-critical (disk, registry hive, boot files, or a wedged update) — NOT on shell/Search/appx component-store corruption. Component-store corruption in `ShellExperienceHost`/`StartMenuExperienceHost`/`windowssearchengine`/Cortana is a *symptom*, not the cause; repairing it does NOT fix the boot loop. Read the actual cause first: `C:\Windows\System32\LogFiles\Srt\SrtTrail.txt`, and run `chkdsk C: /f` (disk errors are the #1 Automatic Repair cause).
**2. `FaultyPackageInProgress` + hundreds of "Install/Uninstall Pending" packages = a wedged cumulative-update transaction.** That IS the boot blocker. Confirm offline with `dism /Image:C:\ /Get-Packages /Format:Table` — look for many `Install Pending` / `Uninstall Pending` lines at the same timestamp (a half-applied CU swapping old build -> new build). Try to back it out with `dism /Image:C:\ /Cleanup-Image /RevertPendingActions`, or WinRE -> Troubleshoot -> Advanced -> Uninstall Updates -> "Uninstall latest quality update". If RevertPendingActions FAILS and there's no `pending.xml`, the transaction is too corrupt to repair in place -> clean install.
**3. Offline DISM won't accept a `wim:` source (`0x800f082e CBS_E_NOT_ALLOWED_OFFLINE`).** For `/Image:C:\ /Cleanup-Image /RestoreHealth` you must **mount** the install.wim and point `/Source` at the mounted folder's `\Windows`, not use `/Source:WIM:...:idx`:
```
mkdir C:\wimmount
dism /Mount-Image /ImageFile:D:\sources\install.wim /Index:6 /MountDir:C:\wimmount /ReadOnly
dism /Image:C:\ /Cleanup-Image /RestoreHealth /Source:C:\wimmount\Windows /LimitAccess /ScratchDir:G:\scratch
dism /Unmount-Image /MountDir:C:\wimmount /Discard
```
Mount point must be an empty folder on a writable **NTFS local** drive (not the USB — Rufus UEFI sticks are FAT32 and hold the source itself). `0x800f0915` (repair content missing) usually means the offline backup store is gone and `/LimitAccess` blocked the WU fallback.
**4. Ventoy breaks WIM mounting (`0xc1420134` / "GetMountedImagesKey... file not found").** Ventoy serves the ISO through a virtual driver that also blocks `wim:` source access. Use a **Rufus**-built install stick instead — mounting then works.
**5. Build matters for the source: Win11 25H2 (build 26200) is 24H2 (build 26100) + enablement package; component store is 26100-versioned.** DISM matches on component version, not the marketing build, so the matching media for a 26200 box is a **24H2 / 26100 ISO**. Verify before repairing: `dism /Get-WimInfo /WimFile:<wim> /Index:6`.
Related: [[cyndyoffice-physical-hp-lockups]], [[feedback_rmm_system_context_mapped_drives]].

View File

@@ -0,0 +1,247 @@
#!/usr/bin/env bash
# ask-forum.sh — ask a human a question in the #ct-forum Discord forum and BLOCK
# until a person answers, then print their answer and exit. Built for a live
# three-way: the user in the terminal, THIS Claude session, and a teammate (e.g.
# Mike) in the forum — all the same session. The wait loop runs here in bash, so
# the calling model pays for ONE tool call, not a polling loop.
#
# Usage:
# ask-forum.sh "question text" [--title "short title"] [--timeout SEC] [--poll SEC] [--tag @id ...]
# echo "question" | ask-forum.sh [flags]
# ask-forum.sh --read <thread_id> [limit] # one-shot read, no wait
# ask-forum.sh --wait <thread_id> [--timeout SEC] [--poll SEC] [--after <msg_id>]
#
# Flow (ask): POST a forum post (thread) with the question, then poll that thread
# for the first NON-BOT reply (human answer), print it, exit 0.
#
# Correlation is automatic: we create the thread and read only that thread.
# Exit codes: 0 answered; 1 usage; 2 token missing; 3 Discord API error; 4 timeout.
set -u
export LC_ALL="${LC_ALL:-C.UTF-8}" # count code points, not bytes, when slicing $CONTENT
ROOT="${CLAUDETOOLS_ROOT:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)}"
API="https://discord.com/api/v10"
UA="ClaudeToolsBot (claudetools, 1.0)"
FORUM_ID="1522960388432465950" # #ct-forum (private forum; Howard + bot + Mike)
LIMIT_CHARS=1900 # Discord caps message content at 2000
TIMEOUT=600
POLL=8
TITLE=""
TAGS=()
MSG=""
# --- token resolver (shared by read + wait + ask) ---
resolve_token() {
local t
t="$(bash "$ROOT/.claude/scripts/vault.sh" get-field \
projects/discord-bot/bot-token.sops.yaml credentials.bot_token 2>/dev/null)"
if [ -z "$t" ] || [ "$t" = "null" ]; then
local env_file="$ROOT/projects/discord-bot/.env"
[ -f "$env_file" ] && t="$(grep -iE '^[[:space:]]*DISCORD_TOKEN[[:space:]]*=' "$env_file" | head -1 | sed -E 's/^[^=]*=[[:space:]]*//; s/^["'"'"']//; s/["'"'"'][[:space:]]*$//')"
fi
printf '%s' "$t"
}
# trim a string to the last newline within it (keeps split chunks on line boundaries)
trim_at_newline() {
local s="$1" nl
nl="${s%$'\n'*}"
if [ "${#nl}" -lt "${#s}" ] && [ "${#nl}" -gt 0 ]; then printf '%s' "$nl"; else printf '%s' "$s"; fi
}
# clamp a read limit to Discord's valid 1..100 (guard huge values that overflow bash math)
clamp_limit() {
local n="$1"
printf '%s' "$n" | grep -qE '^[0-9]+$' || { printf '25'; return; }
[ "${#n}" -ge 4 ] && { printf '100'; return; } # >=1000 always exceeds 100
[ "$n" -lt 1 ] && n=1; [ "$n" -gt 100 ] && n=100
printf '%s' "$n"
}
# Given a non-array poll response body, decide what to do. Echoes: "retry" (transient/
# rate-limited — sleeps any retry_after first) or "bail:<reason>" (permanent Discord error).
poll_disposition() {
local body="$1" ra code
ra="$(printf '%s' "$body" | jq -r '.retry_after // empty' 2>/dev/null)"
if [ -n "$ra" ]; then
# rate limited: honor Discord's back-off (ceil), then retry
sleep "$(printf '%s\n' "$ra" | awk '{printf "%d", ($1==int($1)?$1:int($1)+1)}')" 2>/dev/null || sleep 2
printf 'retry'; return
fi
code="$(printf '%s' "$body" | jq -r '.code // empty' 2>/dev/null)"
if [ -n "$code" ]; then printf 'bail:%s %s' "$code" "$(printf '%s' "$body" | jq -r '.message // ""')"; return; fi
printf 'retry' # unrecognized/transient (network blip, empty body)
}
# emit the human (non-bot) replies from a messages array as ">>> user: text" lines
emit_human() {
printf '%s' "$1" | jq -r 'sort_by(.id)[] | select(.author.bot|not) | ">>> " + .author.username + ": " + ((.content//"")|gsub("\n";" "))'
}
# newest message id in an array (string/lexical max — snowflakes are monotonic; avoids jq number precision loss)
newest_id() { printf '%s' "$1" | jq -r 'sort_by(.id)|.[-1].id // empty'; }
# ---------------------------------------------------------------------------
# --read: one-shot fetch of a thread's messages, no posting
# ---------------------------------------------------------------------------
if [ "${1:-}" = "--read" ]; then
THREAD="${2:-}"
[ -z "$THREAD" ] && { echo "[ERROR] usage: ask-forum.sh --read <thread_id> [limit]" >&2; exit 1; }
LIMIT="$(clamp_limit "${3:-25}")"
TOKEN="$(resolve_token)"
{ [ -z "$TOKEN" ] || [ "$TOKEN" = "null" ]; } && { echo "[ERROR] no Discord bot token" >&2; exit 2; }
auth=(-H "Authorization: Bot ${TOKEN}" -H "User-Agent: ${UA}")
MSGS="$(curl -s -m 20 "${auth[@]}" "$API/channels/${THREAD}/messages?limit=${LIMIT}")"
printf '%s' "$MSGS" | jq -e 'type=="array"' >/dev/null 2>&1 || { echo "[ERROR] could not read thread ${THREAD}: $(printf '%s' "$MSGS" | head -c 160)" >&2; exit 3; }
echo "[OK] thread ${THREAD} (oldest first; '>>>' = human, ' ' = bot):"
printf '%s' "$MSGS" | jq -r 'sort_by(.id)[] | ((if (.author.bot // false) then " " else ">>> " end) + .author.username + ": " + ((.content // "")|gsub("\n";" ")))'
exit 0
fi
# ---------------------------------------------------------------------------
# --wait: block on an EXISTING thread until a human replies, no posting
# Baselines on the starter (thread id == starter message id for a forum post),
# so a reply that already landed before --wait began is caught (not missed).
# Override the baseline with --after <msg_id> to wait for replies strictly after
# a specific message (use when resuming a thread whose earlier answer you already
# consumed and you want only the NEXT reply).
# ---------------------------------------------------------------------------
if [ "${1:-}" = "--wait" ]; then
THREAD="${2:-}"; shift 2
[ -z "$THREAD" ] && { echo "[ERROR] usage: ask-forum.sh --wait <thread_id> [--timeout SEC] [--poll SEC] [--after <msg_id>]" >&2; exit 1; }
AFTER="$THREAD"
while [ "$#" -gt 0 ]; do
case "$1" in
--timeout) TIMEOUT="${2:-600}"; shift 2 ;;
--poll) POLL="${2:-8}"; shift 2 ;;
--after) AFTER="${2:-$THREAD}"; shift 2 ;;
*) shift ;;
esac
done
printf '%s' "$TIMEOUT" | grep -qE '^[0-9]+$' || TIMEOUT=600
printf '%s' "$POLL" | grep -qE '^[0-9]+$' || POLL=8
TOKEN="$(resolve_token)"
{ [ -z "$TOKEN" ] || [ "$TOKEN" = "null" ]; } && { echo "[ERROR] no Discord bot token" >&2; exit 2; }
auth=(-H "Authorization: Bot ${TOKEN}" -H "User-Agent: ${UA}")
echo "[INFO] waiting on thread ${THREAD} for a human reply (up to ${TIMEOUT}s)..." >&2
DEADLINE=$(( $(date +%s) + TIMEOUT ))
while [ "$(date +%s)" -lt "$DEADLINE" ]; do
MSGS="$(curl -s -m 20 "${auth[@]}" "$API/channels/${THREAD}/messages?after=${AFTER}&limit=100")"
if ! printf '%s' "$MSGS" | jq -e 'type=="array"' >/dev/null 2>&1; then
D="$(poll_disposition "$MSGS")"
case "$D" in bail:*) echo "[ERROR] thread ${THREAD}: ${D#bail:}" >&2; exit 3 ;; esac
sleep "$POLL"; continue
fi
HUMAN="$(emit_human "$MSGS")"
if [ -n "$HUMAN" ]; then
echo "[OK] answer received in thread ${THREAD}:"; printf '%s\n' "$HUMAN"; echo "THREAD_ID=${THREAD}"; exit 0
fi
# all-bot batch: page forward so a reply beyond the 100-msg window isn't missed
NID="$(newest_id "$MSGS")"; [ -n "$NID" ] && AFTER="$NID"
sleep "$POLL"
done
echo "[TIMEOUT] no human reply on thread ${THREAD} within ${TIMEOUT}s" >&2
echo "THREAD_ID=${THREAD}" >&2
exit 4
fi
# ---------------------------------------------------------------------------
# ask mode (default): post a question and block for the first human reply
# ---------------------------------------------------------------------------
while [ "$#" -gt 0 ]; do
case "$1" in
--title) TITLE="${2:-}"; shift 2 ;;
--timeout) TIMEOUT="${2:-600}"; shift 2 ;;
--poll) POLL="${2:-8}"; shift 2 ;;
--tag) TAGS+=("${2:-}"); shift 2 ;;
-h|--help) sed -n '2,20p' "$0"; exit 1 ;;
*) MSG="${MSG:+$MSG }$1"; shift ;;
esac
done
if [ -z "$MSG" ] && [ ! -t 0 ]; then MSG="$(cat)"; fi
if [ -z "$MSG" ]; then echo "[ERROR] no question given" >&2; exit 1; fi
printf '%s' "$TIMEOUT" | grep -qE '^[0-9]+$' || TIMEOUT=600
printf '%s' "$POLL" | grep -qE '^[0-9]+$' || POLL=8
if [ -z "$TITLE" ]; then
TITLE="$(printf '%s' "$MSG" | tr '\n' ' ' | cut -c1-60)"
[ -z "$TITLE" ] && TITLE="Question"
fi
# prepend any @mentions so the tagged person gets pinged
MENTION=""
for t in "${TAGS[@]:-}"; do
[ -z "$t" ] && continue
id="${t#@}"
printf '%s' "$id" | grep -qE '^[0-9]{17,19}$' && MENTION="${MENTION}<@${id}> "
done
CONTENT="${MENTION}${MSG}"
TOKEN="$(resolve_token)"
if [ -z "$TOKEN" ] || [ "$TOKEN" = "null" ]; then
echo "[ERROR] no Discord bot token (vault + .env both empty)" >&2
bash "$ROOT/.claude/scripts/log-skill-error.sh" "ask-forum" "no Discord bot token" >/dev/null 2>&1
exit 2
fi
auth=(-H "Authorization: Bot ${TOKEN}" -H "Content-Type: application/json" -H "User-Agent: ${UA}")
# keep the forum starter <=1900 chars; any overflow goes out as follow-up messages
STARTER_CONTENT="$CONTENT"; OVERFLOW=""
if [ "${#CONTENT}" -gt "$LIMIT_CHARS" ]; then
STARTER_CONTENT="$(trim_at_newline "${CONTENT:0:$LIMIT_CHARS}")"
OVERFLOW="${CONTENT:${#STARTER_CONTENT}}"; OVERFLOW="${OVERFLOW#$'\n'}"
fi
# --- create the forum post (thread) with the question ---
BODY="$(jq -nc --arg name "$TITLE" --arg c "$STARTER_CONTENT" '{name:$name, message:{content:$c}}')"
RESP="$(printf '%s' "$BODY" | curl -s -m 20 -w $'\n%{http_code}' "${auth[@]}" \
-X POST "$API/channels/${FORUM_ID}/threads" --data-binary @-)"
HTTP="$(printf '%s' "$RESP" | tail -n1)"
JSON="$(printf '%s' "$RESP" | sed '$d')"
if [ "$HTTP" != "201" ] && [ "$HTTP" != "200" ]; then
echo "[ERROR] could not post forum question (HTTP ${HTTP:-none}): $(printf '%s' "$JSON" | head -c 200)" >&2
bash "$ROOT/.claude/scripts/log-skill-error.sh" "ask-forum" "forum post failed" --context "http=${HTTP:-none} resp=$(printf '%s' "$JSON" | head -c 80)" >/dev/null 2>&1
exit 3
fi
THREAD="$(printf '%s' "$JSON" | jq -r '.id // empty')"
STARTER="$(printf '%s' "$JSON" | jq -r '.message.id // empty')"
if [ -z "$THREAD" ]; then
echo "[ERROR] posted but no thread id in response: $(printf '%s' "$JSON" | head -c 200)" >&2
exit 3
fi
# --- post any overflow chunks as follow-up messages (check each; warn if one fails) ---
rest="$OVERFLOW"
while [ -n "$rest" ]; do
piece="$(trim_at_newline "${rest:0:$LIMIT_CHARS}")"
fhttp="$(printf '%s' "$(jq -nc --arg c "$piece" '{content:$c}')" | \
curl -s -m 15 -o /dev/null -w '%{http_code}' "${auth[@]}" -X POST "$API/channels/${THREAD}/messages" --data-binary @-)"
[ "$fhttp" != "200" ] && echo "[WARNING] overflow chunk failed (HTTP ${fhttp}) — question may be truncated in the forum" >&2
rest="${rest:${#piece}}"; rest="${rest#$'\n'}"
done
echo "[INFO] asked in #ct-forum (thread=${THREAD}) — waiting up to ${TIMEOUT}s for a human reply..." >&2
# --- block-poll the thread for the first NON-BOT reply ---
AFTER="${STARTER:-$THREAD}" # starter id == thread id for a forum post
DEADLINE=$(( $(date +%s) + TIMEOUT ))
while [ "$(date +%s)" -lt "$DEADLINE" ]; do
sleep "$POLL"
MSGS="$(curl -s -m 20 "${auth[@]}" "$API/channels/${THREAD}/messages?after=${AFTER}&limit=100")"
if ! printf '%s' "$MSGS" | jq -e 'type=="array"' >/dev/null 2>&1; then
D="$(poll_disposition "$MSGS")"
case "$D" in bail:*) echo "[ERROR] thread ${THREAD}: ${D#bail:}" >&2; exit 3 ;; esac
continue
fi
HUMAN="$(emit_human "$MSGS")"
if [ -n "$HUMAN" ]; then
echo "[OK] answer received in thread ${THREAD}:"; printf '%s\n' "$HUMAN"; echo "THREAD_ID=${THREAD}"; exit 0
fi
NID="$(newest_id "$MSGS")"; [ -n "$NID" ] && AFTER="$NID" # page forward past bot-only batches
done
echo "[TIMEOUT] no human reply within ${TIMEOUT}s. Thread stays open for a later read:" >&2
echo "THREAD_ID=${THREAD}" >&2
echo " re-read: bash .claude/scripts/ask-forum.sh --read ${THREAD} | resume wait: --wait ${THREAD}" >&2
exit 4

View File

@@ -0,0 +1,268 @@
#!/usr/bin/env python3
"""Shared helpers for the backup-verification skills (seafile / owncloud / datto-workplace).
Three sibling skills answer the same GPS-audit question from different backends:
"which customers/machines are actually backing up here?" Each skill has its own
server-side client (Seafile Web API, ownCloud occ over SSH, Datto Workplace file
API). What they SHARE — and what lives here — is:
* ClaudeTools repo-root resolution (env -> identity.json -> derived).
* A one-field SOPS vault read via the canonical vault.sh wrapper.
* An RmmClient for the "endpoint" half of the "server + RMM endpoint" cross-check:
log in to GuruRMM, list agents, and run a detection PowerShell on an agent to
prove a given sync client (SeaDrive / ownCloud / Datto Workplace) is actually
installed and signed in on the machine.
Standalone: stdlib-only transport (urllib), no third-party dependency. Skills add
this file's directory to sys.path and `import backup_common`.
"""
from __future__ import annotations
import json
import os
import subprocess
import sys
import time
import urllib.error
import urllib.parse
import urllib.request
from pathlib import Path
from typing import Any, Optional
# backup_common.py lives at <root>/.claude/scripts/backup_common.py
_THIS = Path(__file__).resolve()
_DERIVED_ROOT = _THIS.parent.parent.parent # scripts -> .claude -> repo root
RMM_BASE = os.environ.get("GURURMM_API", "http://172.16.3.30:3001")
RMM_VAULT_ENTRY = "infrastructure/gururmm-server.sops.yaml"
RMM_FIELD_EMAIL = "credentials.gururmm-api.admin-email"
RMM_FIELD_PASSWORD = "credentials.gururmm-api.admin-password"
HTTP_TIMEOUT = 60.0
class BackupError(RuntimeError):
"""Transport / auth / API error, with an optional HTTP status."""
def __init__(self, message: str, *, status: Optional[int] = None):
super().__init__(message)
self.status = status
# --- repo-root + vault --------------------------------------------------------
def resolve_root() -> Path:
"""Resolve the ClaudeTools repo root: env var, then identity.json, then derived."""
env_root = os.environ.get("CLAUDETOOLS_ROOT")
if env_root:
return Path(env_root)
identity = _DERIVED_ROOT / ".claude" / "identity.json"
if identity.exists():
try:
data = json.loads(identity.read_text(encoding="utf-8"))
root = data.get("claudetools_root")
if root:
return Path(root)
except (json.JSONDecodeError, OSError):
pass
return _DERIVED_ROOT
def vault_get_field(entry: str, field: str) -> str:
"""Read one field from a SOPS vault entry via the canonical vault.sh wrapper."""
root = resolve_root()
vault = root / ".claude" / "scripts" / "vault.sh"
if not vault.exists():
raise BackupError(f"vault wrapper not found at {vault}")
try:
done = subprocess.run(
["bash", str(vault), "get-field", entry, field],
capture_output=True, text=True, timeout=60,
)
except FileNotFoundError as exc:
raise BackupError("'bash' not found on PATH (need Git Bash for vault reads).") from exc
except subprocess.TimeoutExpired as exc:
raise BackupError(f"vault read timed out for {entry}:{field}") from exc
if done.returncode != 0:
raise BackupError(
f"vault read failed for {entry}:{field} (exit {done.returncode}): {done.stderr.strip()}"
)
value = done.stdout.strip()
if not value:
raise BackupError(f"vault returned empty for {entry}:{field}")
return value
# --- minimal JSON HTTP --------------------------------------------------------
def http_json(method: str, url: str, *, headers: Optional[dict] = None,
body: Optional[dict] = None, form: Optional[dict] = None,
timeout: float = HTTP_TIMEOUT,
basic: Optional[tuple[str, str]] = None) -> tuple[int, Any]:
"""Do an HTTP request and parse a JSON response. Returns (status, parsed).
`body` sends a JSON payload; `form` sends application/x-www-form-urlencoded
(list values expand via doseq, e.g. repeated fields the Seafile admin share
endpoint reads with getlist). Pass at most one. 4xx/5xx are returned (not
raised) so callers can inspect the error body; transport failures raise.
"""
hdrs = dict(headers or {})
data = None
if body is not None and form is not None:
raise BackupError("http_json: pass body OR form, not both")
if body is not None:
data = json.dumps(body).encode("utf-8")
hdrs.setdefault("Content-Type", "application/json")
elif form is not None:
data = urllib.parse.urlencode(form, doseq=True).encode("utf-8")
hdrs.setdefault("Content-Type", "application/x-www-form-urlencoded")
if basic is not None:
import base64
tok = base64.b64encode(f"{basic[0]}:{basic[1]}".encode()).decode("ascii")
hdrs["Authorization"] = f"Basic {tok}"
req = urllib.request.Request(url, data=data, method=method, headers=hdrs)
try:
with urllib.request.urlopen(req, timeout=timeout) as resp:
raw = resp.read().decode("utf-8", errors="replace")
return resp.getcode(), _parse(raw)
except urllib.error.HTTPError as exc:
raw = exc.read().decode("utf-8", errors="replace")
return exc.code, _parse(raw)
except urllib.error.URLError as exc:
raise BackupError(f"request to {url} failed: {exc}") from exc
def _parse(text: str) -> Any:
if not text:
return {}
try:
return json.loads(text)
except json.JSONDecodeError:
return {"_raw": text[:600]}
# --- GuruRMM client (endpoint cross-check) ------------------------------------
# One detection script, parametrized by the product name-fragments to look for.
# Returns a single JSON line describing installed products, running processes, and
# per-user config folders that match — enough to say "this client is installed and
# signed in" without guessing.
_DETECT_PS = r'''
$ErrorActionPreference='SilentlyContinue'
$patterns = @(__PATTERNS__)
function match($s){ if(-not $s){return $false}; foreach($p in $patterns){ if($s -match [regex]::Escape($p)){return $true} }; return $false }
$installed=@()
foreach($hive in @('HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall\*','HKLM:\Software\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*')){
Get-ItemProperty $hive | Where-Object { match($_.DisplayName) } | ForEach-Object {
$installed += [pscustomobject]@{ name=$_.DisplayName; version=$_.DisplayVersion; location=$_.InstallLocation }
}
}
$procs = Get-Process | Where-Object { match($_.ProcessName) } | Select-Object -Expand ProcessName -Unique
$cfg=@()
foreach($d in @('Seafile','SeaDrive','ownCloud','ownCloudSync','Datto','Workplace','Workplace2')){
foreach($base in @($env:APPDATA,$env:LOCALAPPDATA,$env:USERPROFILE,"$env:USERPROFILE\Desktop")){
$pth = Join-Path $base $d
if(Test-Path $pth){ if(match($d) -or match($pth)){ $cfg += $pth } }
}
}
# signed-in heuristic: config/db files with recent writes
$fresh=@()
foreach($c in $cfg){
Get-ChildItem $c -Recurse -File -ErrorAction SilentlyContinue -Depth 2 |
Where-Object { $_.LastWriteTime -gt (Get-Date).AddDays(-14) } |
Select-Object -First 1 | ForEach-Object { $fresh += $c }
}
[pscustomobject]@{
host=$env:COMPUTERNAME
installed=$installed
processes=@($procs)
config_dirs=@($cfg)
active_config_dirs=@($fresh | Select-Object -Unique)
detected=([bool]($installed.Count -or $procs.Count))
} | ConvertTo-Json -Depth 5 -Compress
'''
class RmmClient:
"""Thin GuruRMM API client: login, list agents, run a detection PowerShell."""
def __init__(self, base: str = RMM_BASE):
self.base = base.rstrip("/")
self._token: Optional[str] = None
self._agents: Optional[list[dict]] = None
def login(self) -> None:
email = vault_get_field(RMM_VAULT_ENTRY, RMM_FIELD_EMAIL)
pw = vault_get_field(RMM_VAULT_ENTRY, RMM_FIELD_PASSWORD)
status, body = http_json("POST", f"{self.base}/api/auth/login",
body={"email": email, "password": pw})
if status != 200 or not isinstance(body, dict) or not body.get("token"):
raise BackupError(f"GuruRMM login failed (HTTP {status}): {body}", status=status)
self._token = body["token"]
def _auth(self) -> dict:
if not self._token:
self.login()
return {"Authorization": f"Bearer {self._token}"}
def list_agents(self) -> list[dict]:
if self._agents is None:
status, body = http_json("GET", f"{self.base}/api/agents", headers=self._auth())
if status != 200 or not isinstance(body, list):
raise BackupError(f"GET /api/agents failed (HTTP {status})", status=status)
self._agents = body
return self._agents
def find_agents(self, client_substr: Optional[str] = None,
hostname_substr: Optional[str] = None) -> list[dict]:
out = []
for a in self.list_agents():
if client_substr and client_substr.lower() not in (a.get("client_name") or "").lower():
continue
if hostname_substr and hostname_substr.lower() not in (a.get("hostname") or "").lower():
continue
out.append(a)
return out
def run_ps(self, agent_id: str, script: str, timeout_seconds: int = 90,
poll_seconds: int = 4, max_polls: int = 30) -> dict:
"""Dispatch a PowerShell command to an agent and poll to completion."""
status, body = http_json(
"POST", f"{self.base}/api/agents/{agent_id}/command", headers=self._auth(),
body={"command_type": "powershell", "command": script,
"timeout_seconds": timeout_seconds})
if status not in (200, 201) or not isinstance(body, dict):
raise BackupError(f"command dispatch failed (HTTP {status}): {body}", status=status)
cid = body.get("command_id") or body.get("id")
if not cid:
raise BackupError(f"no command_id in dispatch response: {body}")
terminal = {"completed", "failed", "cancelled", "interrupted", "timeout"}
last: dict = {}
for _ in range(max_polls):
s, cb = http_json("GET", f"{self.base}/api/commands/{cid}", headers=self._auth())
if s == 200 and isinstance(cb, dict):
last = cb
if (cb.get("status") or "") in terminal:
break
time.sleep(poll_seconds)
return last
def detect_client(self, agent: dict, patterns: list[str], **kw) -> dict:
"""Run the detection script on one agent for the given product name-fragments."""
pat_lits = ",".join("'" + p.replace("'", "''") + "'" for p in patterns)
script = _DETECT_PS.replace("__PATTERNS__", pat_lits)
res = self.run_ps(agent["id"], script, **kw)
out = {"hostname": agent.get("hostname"), "client_name": agent.get("client_name"),
"agent_id": agent.get("id"), "status": res.get("status"),
"exit_code": res.get("exit_code"), "detected": None, "detail": None}
stdout = (res.get("stdout") or "").strip()
if stdout:
try:
parsed = json.loads(stdout)
out["detected"] = bool(parsed.get("detected"))
out["detail"] = parsed
except json.JSONDecodeError:
out["detail"] = {"_raw": stdout[:600]}
return out
def eprint(*a: Any) -> None:
print(*a, file=sys.stderr)

View File

@@ -6,6 +6,7 @@
# Usage: # Usage:
# discord-dm.sh <recipient> "message text" # discord-dm.sh <recipient> "message text"
# echo "message text" | discord-dm.sh <recipient> # echo "message text" | discord-dm.sh <recipient>
# discord-dm.sh read <recipient> [limit] # READ recent messages — SEE replies (default 15)
# discord-dm.sh list # show known users + channels # discord-dm.sh list # show known users + channels
# #
# <recipient> is one of: # <recipient> is one of:
@@ -51,11 +52,29 @@ if [ -z "$RECIPIENT" ] || [ "$RECIPIENT" = "-h" ] || [ "$RECIPIENT" = "--help" ]
sed -n '2,30p' "$0"; exit 1 sed -n '2,30p' "$0"; exit 1
fi fi
if [ "$RECIPIENT" = "list" ]; then print_dir; exit 0; fi if [ "$RECIPIENT" = "list" ]; then print_dir; exit 0; fi
# --- read mode: fetch recent messages so we can SEE replies (mike/winter/etc.).
# `discord-dm.sh read <user|#channel> [limit]` — reuses the recipient resolution,
# token, and DM-channel-open below, then prints history instead of sending. The
# bot participates in each DM channel it opened, so REST history returns full
# content (the Message Content intent only gates gateway events, not this fetch).
ACTION=send
if [ "$RECIPIENT" = "read" ] || [ "$RECIPIENT" = "inbox" ]; then
ACTION=read; shift
RECIPIENT="${1:-}"
[ -z "$RECIPIENT" ] && { echo "[ERROR] usage: discord-dm.sh read <user|#channel> [limit]" >&2; exit 1; }
fi
shift shift
# --- message (remaining args, else stdin) --- # --- message (send) or limit (read) ---
if [ "$ACTION" = "read" ]; then
READ_LIMIT="${1:-15}"
printf '%s' "$READ_LIMIT" | grep -qE '^[0-9]+$' || READ_LIMIT=15
[ "$READ_LIMIT" -gt 100 ] && READ_LIMIT=100
else
if [ "$#" -gt 0 ]; then MSG="$*"; elif [ ! -t 0 ]; then MSG="$(cat)"; else MSG=""; fi if [ "$#" -gt 0 ]; then MSG="$*"; elif [ ! -t 0 ]; then MSG="$(cat)"; else MSG=""; fi
if [ -z "$MSG" ]; then echo "[ERROR] empty message — nothing sent" >&2; exit 1; fi if [ -z "$MSG" ]; then echo "[ERROR] empty message — nothing sent" >&2; exit 1; fi
fi
# --- resolve recipient -> mode (dm|channel) + target id --- # --- resolve recipient -> mode (dm|channel) + target id ---
MODE=""; TARGET=""; LABEL="" MODE=""; TARGET=""; LABEL=""
@@ -99,17 +118,61 @@ if [ "$MODE" = "dm" ]; then
TARGET="$CHID" TARGET="$CHID"
fi fi
# --- post the message (printf | --data-binary @- — direct -d mangles multiline JSON) --- # --- read mode: fetch + print recent messages, then exit (no send) ---
RESP="$(printf '%s' "$(jq -nc --arg c "$MSG" '{content:$c}')" | \ if [ "$ACTION" = "read" ]; then
MSGS="$(curl -s -m 20 "${auth[@]}" "$API/channels/${TARGET}/messages?limit=${READ_LIMIT}")"
if ! printf '%s' "$MSGS" | jq -e 'type=="array"' >/dev/null 2>&1; then
echo "[ERROR] could not read $LABEL: $(printf '%s' "$MSGS" | head -c 200)" >&2
bash "$ROOT/.claude/scripts/log-skill-error.sh" "discord-dm" "read history failed for $LABEL" --context "resp=$(printf '%s' "$MSGS" | head -c 80)" >/dev/null 2>&1
exit 3
fi
echo "[OK] discord-dm: last ${READ_LIMIT} message(s) in ${LABEL} (oldest first; '>>>' = reply from them, ' ' = us):"
printf '%s' "$MSGS" | jq -r 'reverse[] |
((if (.author.bot // false) then " " else ">>> " end)
+ (.timestamp[0:16]) + " " + .author.username + ": "
+ ((.content // "") | gsub("\n";" ")))'
exit 0
fi
# --- chunk: Discord caps content at 2000 chars (code 50035 on overflow).
# This tool's job is delivering LONG content intact, so split into <=1900-char
# pieces (preferring newline boundaries) and send them in order — no markers
# added, so the receiver can copy-paste the sequence back together verbatim.
LIMIT=1900
CHUNKS=()
rest="$MSG"
while [ "${#rest}" -gt "$LIMIT" ]; do
piece="${rest:0:$LIMIT}"
at_nl="${piece%$'\n'*}" # cut at the last newline in the window
if [ "${#at_nl}" -lt "${#piece}" ] && [ "${#at_nl}" -gt 0 ]; then
piece="$at_nl"
fi
CHUNKS+=("$piece")
rest="${rest:${#piece}}"
rest="${rest#$'\n'}"
done
CHUNKS+=("$rest")
# --- post the message (jq | --data-binary @- — argv/-d mangles multiline + non-ASCII JSON) ---
N=${#CHUNKS[@]}
i=0
for piece in "${CHUNKS[@]}"; do
i=$((i+1))
RESP="$(printf '%s' "$(jq -nc --arg c "$piece" '{content:$c}')" | \
curl -s -m 15 -w $'\n%{http_code}' "${auth[@]}" \ curl -s -m 15 -w $'\n%{http_code}' "${auth[@]}" \
-X POST "$API/channels/${TARGET}/messages" --data-binary @-)" -X POST "$API/channels/${TARGET}/messages" --data-binary @-)"
HTTP="$(printf '%s' "$RESP" | tail -n1)" HTTP="$(printf '%s' "$RESP" | tail -n1)"
BODY="$(printf '%s' "$RESP" | sed '$d')" BODY="$(printf '%s' "$RESP" | sed '$d')"
if [ "$HTTP" != "200" ]; then
if [ "$HTTP" = "200" ]; then echo "[ERROR] discord-dm: Discord returned ${HTTP:-no-response} on chunk ${i}/${N}${BODY}" >&2
echo "[OK] discord-dm: sent to ${LABEL} (message_id=$(printf '%s' "$BODY" | jq -r '.id // empty'))" bash "$ROOT/.claude/scripts/log-skill-error.sh" "discord-dm" "Discord send to $LABEL failed (chunk ${i}/${N})" --context "http=${HTTP:-none} resp=${BODY:0:80}" >/dev/null 2>&1
exit 0
fi
echo "[ERROR] discord-dm: Discord returned ${HTTP:-no-response}${BODY}" >&2
bash "$ROOT/.claude/scripts/log-skill-error.sh" "discord-dm" "Discord send to $LABEL failed" --context "http=${HTTP:-none} resp=${BODY:0:80}" >/dev/null 2>&1
exit 3 exit 3
fi
done
if [ "$N" -gt 1 ]; then
echo "[OK] discord-dm: sent to ${LABEL} in ${N} chunks (message_id=$(printf '%s' "$BODY" | jq -r '.id // empty'))"
else
echo "[OK] discord-dm: sent to ${LABEL} (message_id=$(printf '%s' "$BODY" | jq -r '.id // empty'))"
fi
exit 0

View File

@@ -0,0 +1,6 @@
' edr-isolation-watch-hidden.vbs — launch the EDR isolation watcher with no visible window.
' Run via the "ClaudeTools - EDR Isolation Watcher" scheduled task through wscript.exe (a GUI
' host, no console), which starts bash with window style 0 (hidden) so the every-10-min watch
' no longer flashes a console window on the desktop. Runtime env is identical to a direct bash
' launch. Mirrors gps-rmm-progress-hidden.vbs.
CreateObject("WScript.Shell").Run """C:\Program Files\Git\bin\bash.exe"" -lc ""/c/claudetools/.claude/scripts/edr-isolation-watch.sh""", 0, False

View File

@@ -0,0 +1,71 @@
#!/usr/bin/env bash
# edr-isolation-watch.sh — interim EDR auto-isolation alerter.
#
# Polls Datto EDR for detections whose automated response included `isolate-host`
# (i.e. a machine was auto-isolated / "quarantined") and posts each NEW one to the
# private #dev-alerts channel (Mike + Howard). Runs as a plain scheduled job — no
# LLM, no tokens. Retire once the GuruRMM EDR webhook integration (Feature 6) lands.
#
# Schedule (every 10 min), e.g. cron:
# */10 * * * * bash /path/to/claudetools/.claude/scripts/edr-isolation-watch.sh >/dev/null 2>&1
#
# State: .edr-watch-state.json (gitignored) holds already-alerted alert IDs so each
# isolation is announced exactly once, even though Mike may un-isolate before the poll.
set -uo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
EDR="bash $ROOT/.claude/scripts/py.sh $ROOT/.claude/skills/datto-edr/scripts/edr.py"
DM="bash $ROOT/.claude/scripts/discord-dm.sh"
export STATE="${STATE:-$ROOT/.claude/scripts/.edr-watch-state.json}"
TARGET="dev" # #dev-alerts = Mike + Howard, private
# Pull last 24h of detections (list includes responseData) and emit any NEW
# isolate-host events as one pipe-delimited line each; update state in-place.
new_events="$($EDR --json detections --days 1 --limit 500 2>/dev/null | python -c '
import sys, json, os
state_path = os.environ["STATE"]
try:
rows = json.load(sys.stdin)
rows = rows if isinstance(rows, list) else rows.get("data", rows.get("alerts", []))
except Exception:
sys.exit(0) # API hiccup: emit nothing, do not churn state
try:
seen = set(json.load(open(state_path)))
except Exception:
seen = set()
fresh = []
for a in rows:
rd = a.get("responseData") or []
names = [r.get("name") for r in rd] if isinstance(rd, list) else []
if "isolate-host" not in names:
continue
aid = a.get("id")
if not aid or aid in seen:
continue
seen.add(aid)
fresh.append("|".join(str(a.get(k, "")) for k in
("id", "hostname", "organizationName", "severity", "sourceName", "eventTime")))
# keep state bounded
json.dump(sorted(seen)[-500:], open(state_path, "w"))
print("\n".join(fresh))
' 2>/dev/null)"
[ -z "$new_events" ] && exit 0
while IFS='|' read -r aid host org sev rule ts; do
[ -z "$aid" ] && continue
msg="[EDR AUTO-ISOLATION] **$host** was auto-isolated by Datto EDR
- Client: $org
- Rule: $rule (severity: $sev)
- Time: $ts
- This machine was cut off the network by an EDR automated response. Verify it is intended before restoring.
- Console: https://azcomp4587.infocyte.com (alert id: $aid)"
if [ "${DRY_RUN:-0}" = "1" ]; then
printf '[DRY_RUN] would post to #%s:\n%s\n\n' "$TARGET" "$msg"
continue
fi
if ! printf '%s' "$msg" | $DM "$TARGET" 2>/dev/null; then
bash "$ROOT/.claude/scripts/log-skill-error.sh" "edr-isolation-watch" "discord post failed for $host ($aid)" 2>/dev/null || true
fi
done <<< "$new_events"

View File

@@ -0,0 +1,32 @@
#!/usr/bin/env bash
# get-identity.sh — Read identity.json and export user/machine vars for attribution
#
# Source this at the start of any skill that needs attribution (bot alerts, commits,
# logs, RMM operations). Exports $USER_NAME, $USER_SHORT, $MACHINE, $USER_EMAIL.
#
# Usage:
# source .claude/scripts/get-identity.sh
# echo "[RMM] $USER_SHORT deployed to X machines..."
#
# Soft-fails: if identity.json is missing, exports "Unknown" values and returns 1
# (but does NOT exit, so the caller continues). This ensures skills never break on
# missing identity - they just attribute to "Unknown".
IDENTITY_FILE="${CLAUDETOOLS_ROOT:-.}/.claude/identity.json"
if [ ! -f "$IDENTITY_FILE" ]; then
echo "[WARNING] identity.json not found at $IDENTITY_FILE - attribution will be 'Unknown'" >&2
export USER_NAME="Unknown User"
export USER_SHORT="unknown"
export MACHINE="unknown-machine"
export USER_EMAIL="unknown@unknown.com"
return 1
fi
export USER_NAME=$(jq -r '.full_name // .user // "Unknown"' "$IDENTITY_FILE" 2>/dev/null || echo "Unknown")
export USER_SHORT=$(jq -r '.user // "unknown"' "$IDENTITY_FILE" 2>/dev/null || echo "unknown")
export MACHINE=$(jq -r '.machine // "unknown"' "$IDENTITY_FILE" 2>/dev/null || echo "unknown")
export USER_EMAIL=$(jq -r '.email // "unknown@unknown.com"' "$IDENTITY_FILE" 2>/dev/null || echo "unknown@unknown.com")
# Success
return 0

View File

@@ -0,0 +1,35 @@
#!/bin/bash
# gps-rmm-autoenroll.sh — auto-enroll loop for the GPS->RMM audit.
# Every run: push the generic Staging installer (DARK-STORM-3150) to GPS-customer
# machines that are ONLINE in ScreenConnect but missing from GuruRMM, wait for them
# to enroll, then reassign Staging agents to their real client (hostname->Syncro).
# Registered as Windows task GPS-RMM-AutoEnroll (every 30 min). Remove the task when
# targets.json is fully enrolled. Safe to re-run: server v6.77+ dedups by device_id.
set -uo pipefail
cd /c/claudetools || exit 1
LOG="projects/gps-rmm-audit/autoenroll.log"
TS="$(date '+%Y-%m-%d %H:%M')"
eval "$(bash .claude/scripts/rmm-auth.sh 2>/dev/null)" >/dev/null 2>&1
if [ -z "${TOKEN:-}" ]; then echo "$TS auth FAILED" >> "$LOG"; exit 0; fi
SEC="$(bash .claude/scripts/vault.sh get-field msp-tools/screenconnect.sops.yaml credentials.api_secret 2>/dev/null | tr -d '\r\n')"
SK="Tde5174a6e9e312d14-02fd5bfe0f0ee40c87d027507c680e18"
OUT="$(RMM="$RMM" TOK="$TOKEN" SK="$SK" SC_SECRET="$SEC" python projects/gps-rmm-audit/tools/rebuild-and-push.py 2>/dev/null | tail -2)"
PUSHED="$(echo "$OUT" | grep -oE 'pushed: [0-9]+' | grep -oE '[0-9]+' | head -1 || true)"
PUSHED="${PUSHED:-0}"
echo "$TS sweep: $(echo "$OUT" | head -1)" >> "$LOG"
if [ "${PUSHED:-0}" -gt 0 ]; then
sleep 150
RES="$(RMM="$RMM" TOK="$TOKEN" SK="$SK" python projects/gps-rmm-audit/tools/reassign-staging.py 2>/dev/null)"
MOVED="$(echo "$RES" | grep -cE '^ .+ -> ' || true)"
MOVED="${MOVED:-0}"
echo "$TS pushed $PUSHED, reassigned $MOVED:" >> "$LOG"
echo "$RES" | grep -E '^ ' >> "$LOG"
if [ "${MOVED:-0}" -gt 0 ]; then
NAMES="$(echo "$RES" | grep -E '^ .+ -> ' | sed 's/^ //' | tr '\n' '; ')"
bash .claude/scripts/post-bot-alert.sh "[RMM] auto-enroll: $NAMES" >/dev/null 2>&1
fi
fi
exit 0

View File

@@ -0,0 +1,63 @@
#!/usr/bin/env bash
# gps-rmm-progress-check.sh — daily GPS->GuruRMM enrollment progress check.
#
# Reads projects/gps-rmm-audit/targets.json (GPS device target per client),
# pulls live GuruRMM /api/agents, computes per-client enrollment gaps, and DMs
# Howard a one-message summary. When every tracked client has met its target it
# reports "COMPLETE" so the scheduled task can be retired.
#
# Usage:
# bash gps-rmm-progress-check.sh # check + DM Howard
# bash gps-rmm-progress-check.sh --dry-run # check + print, no Discord
#
# Registered as a daily Windows scheduled task. Read-only against RMM.
set -u
ROOT="${CLAUDETOOLS_ROOT:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)}"
TARGETS="$ROOT/projects/gps-rmm-audit/targets.json"
DRY=0; [ "${1:-}" = "--dry-run" ] && DRY=1
[ -f "$TARGETS" ] || { echo "[ERROR] targets.json not found at $TARGETS" >&2; exit 1; }
# --- auth + fetch agents ---
eval "$(bash "$ROOT/.claude/scripts/rmm-auth.sh" 2>/dev/null)" >/dev/null
if [ -z "${TOKEN:-}" ] || [ -z "${RMM:-}" ]; then
echo "[ERROR] RMM auth failed" >&2
bash "$ROOT/.claude/scripts/log-skill-error.sh" "gps-rmm-progress-check" "RMM auth failed" >/dev/null 2>&1
exit 1
fi
AGENTS=$(curl -s "$RMM/api/agents" -H "Authorization: Bearer $TOKEN" | tr -d '\000-\037')
[ "${AGENTS:0:1}" = "[" ] || { echo "[ERROR] /api/agents not an array: ${AGENTS:0:100}" >&2; \
bash "$ROOT/.claude/scripts/log-skill-error.sh" "gps-rmm-progress-check" "GET /api/agents non-array" >/dev/null 2>&1; exit 1; }
# --- per-client live agent counts (unique hostnames, to ignore dup agent rows) ---
COUNTS=$(echo "$AGENTS" | jq -r '[.[] | {c:.client_name, h:(.hostname|ascii_downcase)}]
| group_by(.c) | map({client:.[0].c, n:( [.[].h] | unique | length )}) | .[] | "\(.n)\t\(.client)"')
# --- compare targets vs live ---
REPORT=""; DONE_ALL=1; TOTAL_TARGET=0; TOTAL_HAVE=0; GAP_CLIENTS=0
while IFS=$'\t' read -r TGT NAME BUCKET; do
[ -z "$NAME" ] && continue
HAVE=$(echo "$COUNTS" | awk -F'\t' -v n="$NAME" '$2==n{print $1; found=1} END{if(!found)print 0}' | head -1)
HAVE=${HAVE:-0}
TOTAL_TARGET=$((TOTAL_TARGET + TGT))
TOTAL_HAVE=$((TOTAL_HAVE + HAVE))
if [ "$HAVE" -lt "$TGT" ]; then
DONE_ALL=0; GAP_CLIENTS=$((GAP_CLIENTS+1))
GAP=$((TGT - HAVE))
STATE=$([ "$HAVE" -eq 0 ] && echo "NO ORG/AGENTS" || echo "short")
REPORT="${REPORT} - ${NAME}: ${HAVE}/${TGT} in RMM (${STATE}, gap ${GAP}) [${BUCKET}]\n"
fi
done < <(jq -r '.clients[] | "\(.target)\t\(.client)\t\(.bucket)"' "$TARGETS")
DATE=$(date +%Y-%m-%d)
if [ "$DONE_ALL" -eq 1 ]; then
MSG="**GPS->RMM enrollment: COMPLETE (${DATE})** All tracked GPS clients meet their RMM device target (${TOTAL_HAVE}/${TOTAL_TARGET}). You can retire the daily check task (schtasks /Delete /TN GPS-RMM-Progress)."
else
MSG="**GPS->RMM enrollment check ${DATE}** — ${TOTAL_HAVE}/${TOTAL_TARGET} devices in RMM; ${GAP_CLIENTS} clients still short:\n${REPORT}(Glaz-Tech excluded pending billing review. Source: projects/gps-rmm-audit/targets.json)"
fi
if [ "$DRY" -eq 1 ]; then
echo -e "$MSG"
exit 0
fi
echo -e "$MSG" | bash "$ROOT/.claude/scripts/discord-dm.sh" howard

View File

@@ -0,0 +1,5 @@
' gps-rmm-progress-hidden.vbs — launch the GPS->RMM progress check with no visible window.
' Run via the GPS-RMM-Progress scheduled task through wscript.exe (GUI host, no console),
' which starts bash with window style 0 (hidden) so the daily check no longer flashes a
' console window on the desktop. Runtime env is identical to a direct bash launch.
CreateObject("WScript.Shell").Run """C:\Program Files\Git\bin\bash.exe"" -lc ""cd /c/claudetools && bash .claude/scripts/gps-rmm-progress-check.sh""", 0, False

View File

@@ -23,6 +23,7 @@ set -u
TARGET="${1:-}" TARGET="${1:-}"
PHASE="${2:-all}" PHASE="${2:-all}"
SCANNER_ARG="${3:-}"
if [ -z "$TARGET" ]; then if [ -z "$TARGET" ]; then
echo "[ERROR] Usage: bash guruscan-agent-test.sh <hostname|uuid> <prep|scan|collect|all>" >&2 echo "[ERROR] Usage: bash guruscan-agent-test.sh <hostname|uuid> <prep|scan|collect|all>" >&2
@@ -530,11 +531,70 @@ PS
post_alert "[RMM] GuruScan verify-each on $AGENT_HOST complete - see per-engine detect/remove matrix" post_alert "[RMM] GuruScan verify-each on $AGENT_HOST complete - see per-engine detect/remove matrix"
} }
# ===========================================================================
# scan-one <Scanner>: fully automated single-scanner test. ONE command does it
# all, hands-off: restore the malware-lab samples, clear stale cleanup state,
# run the scanner DETACHED + NO-CAP via the same path production uses, then
# collect and report (detections, removal, results.json, reboot-cleanup task).
# Mirrors how we proved Emsisoft -- no manual stitching.
# ===========================================================================
phase_scan_one() {
local scanner="${SCANNER_ARG:-HitmanPro}"
echo ""; echo "=== PHASE: scan-one ($scanner) - automated, detached, no-cap ==="
# Setup: free the mutex (kill any prior GuruScan run + scanner procs), clear
# stale cleanup task/state, and restore the malware lab samples.
local sf="$WORK_DIR/one_setup.ps1"
cat > "$sf" <<'PS'
$ErrorActionPreference='Continue'
Get-ScheduledTask -EA SilentlyContinue | Where-Object { $_.TaskName -like 'GuruScan-*' } | ForEach-Object { try{ Stop-ScheduledTask -TaskName $_.TaskName -EA SilentlyContinue }catch{}; try{ Unregister-ScheduledTask -TaskName $_.TaskName -Confirm:$false -EA SilentlyContinue }catch{} }
foreach($n in @('a2cmd','HitmanPro_x64','rkill','EmsisoftCommandlineScanner64')){ Get-Process -Name $n -EA SilentlyContinue | Stop-Process -Force -EA SilentlyContinue }
Start-Sleep 3
Get-ScheduledTask -TaskName 'GuruRMM-ScannerCleanup' -EA SilentlyContinue | Unregister-ScheduledTask -Confirm:$false -EA SilentlyContinue
Remove-Item 'C:\GuruScan\cleanup-state.json' -Force -EA SilentlyContinue
# Re-ensure scanners exist (a prior reboot-cleanup may have wiped C:\GuruScan\downloads).
if(-not (Test-Path 'C:\GuruScan\downloads\EmsisoftCommandlineScanner64.exe') -or -not (Test-Path 'C:\GuruScan\downloads\rkill.exe')){
& C:\GuruScan\Download-Scanners.ps1 *> $null
}
if(-not (Test-Path 'C:\GuruScan\downloads\HitmanPro_x64.exe')){
try{ [Net.ServicePointManager]::SecurityProtocol=[Net.ServicePointManager]::SecurityProtocol -bor 3072; (New-Object Net.WebClient).DownloadFile('https://dl.surfright.nl/HitmanPro_x64.exe','C:\GuruScan\downloads\HitmanPro_x64.exe') }catch{}
}
$zip = Get-ChildItem 'C:\Users\Owner\Downloads' -Filter '*.zip' -EA SilentlyContinue | Where-Object { $_.Name -match 'malware' } | Select-Object -First 1
if($zip){ Expand-Archive -Path $zip.FullName -DestinationPath 'C:\Users\Owner\Desktop' -Force -EA SilentlyContinue }
$n=(Get-ChildItem 'C:\Users\Owner\Desktop\malware-samples-master' -Recurse -File -EA SilentlyContinue).Count
Set-Content 'C:\GuruScan\_one_before.txt' $n
Write-Output ("setup done - scanners ensured; malware samples present: $n")
PS
run_ps "$sf" 600 130 "setup" || { echo "[ERROR] setup failed"; return 1; }
# Run the scanner the production way: detached scheduled task, unlimited time.
gs_launch_detached "-Scanners $scanner -Headless" "one" || { echo "[ERROR] launch failed"; return 1; }
gs_wait_detached "one" "$scanner" || true
# Report the outcome (parsed from what GuruScan actually wrote).
local rf="$WORK_DIR/one_report.ps1"
cat > "$rf" <<'PS'
$ErrorActionPreference='Continue'
$before=Get-Content 'C:\GuruScan\_one_before.txt' -EA SilentlyContinue
$after=(Get-ChildItem 'C:\Users\Owner\Desktop\malware-samples-master' -Recurse -File -EA SilentlyContinue).Count
Write-Output ("samples: before=$before after=$after REMOVED=" + ([int]$before-[int]$after))
$d=Get-ChildItem 'C:\ScanLogs' -Directory -EA SilentlyContinue | Sort-Object LastWriteTime -Descending | Select-Object -First 1
if($d -and (Test-Path (Join-Path $d.FullName 'results.json'))){ $r=Get-Content (Join-Path $d.FullName 'results.json') -Raw|ConvertFrom-Json
Write-Output ('results.json -> total_threats=' + $r.total_threats + ' reboot_required=' + $r.reboot_required)
$r.scanners|ForEach-Object{ Write-Output (' ' + $_.name + ': exit=' + $_.exit_code + ' threats=' + $_.threats_found) } }
$ct=Get-ScheduledTask -TaskName 'GuruRMM-ScannerCleanup' -EA SilentlyContinue
if($ct){ Write-Output ('reboot-cleanup task -> REGISTERED (state=' + $ct.State + ', logon-delay=' + $ct.Triggers[0].Delay + ')') } else { Write-Output 'reboot-cleanup task -> NOT registered' }
PS
run_ps "$rf" 60 24 "report" || true
post_alert "[RMM] GuruScan automated scan-one ($scanner) complete on $AGENT_HOST"
}
case "$PHASE" in case "$PHASE" in
prep) phase_prep ;; prep) phase_prep ;;
scan) phase_scan ;; scan) phase_scan ;;
scan-one) phase_scan_one ;;
collect) phase_collect ;; collect) phase_collect ;;
verify-each) phase_verify_each ;; verify-each) phase_verify_each ;;
all) phase_prep && phase_scan && phase_collect ;; all) phase_prep && phase_scan && phase_collect ;;
*) echo "[ERROR] Unknown phase '$PHASE' (prep|scan|collect|verify-each|all)" >&2; exit 1 ;; *) echo "[ERROR] Unknown phase '$PHASE' (prep|scan|scan-one <Scanner>|collect|verify-each|all)" >&2; exit 1 ;;
esac esac

View File

@@ -33,6 +33,11 @@
# Writes: YYYY-MM-DD | MACHINE | <skill> | [<type>] <error> [ctx: <context>] # Writes: YYYY-MM-DD | MACHINE | <skill> | [<type>] <error> [ctx: <context>]
# (newest entry inserted at the top, just under the append marker). # (newest entry inserted at the top, just under the append marker).
# #
# Dedup: if an IDENTICAL entry (same date, machine, skill, message) already
# exists, no new line is added — the existing line gets a " (xN)" repeat counter
# bumped instead. Identical machine-generated failures (API retry loops) collapse
# to one line per day; a different message/context/date is still a new entry.
#
# Soft-fail by design: this NEVER breaks the caller. Missing log, missing jq, # Soft-fail by design: this NEVER breaks the caller. Missing log, missing jq,
# empty message -> prints a [WARN] to stderr and exits 0. # empty message -> prints a [WARN] to stderr and exits 0.
set -u set -u
@@ -62,7 +67,7 @@ DATE="$(date -u +%F)"
IDF="$ROOT/.claude/identity.json" IDF="$ROOT/.claude/identity.json"
MACHINE="" MACHINE=""
if command -v jq >/dev/null 2>&1 && [ -f "$IDF" ]; then if command -v jq >/dev/null 2>&1 && [ -f "$IDF" ]; then
MACHINE="$(jq -r '.machine_name // .hostname // empty' "$IDF" 2>/dev/null)" MACHINE="$(jq -r '.machine // .machine_name // .hostname // empty' "$IDF" 2>/dev/null)"
fi fi
[ -z "$MACHINE" ] && MACHINE="$(hostname 2>/dev/null || echo unknown)" [ -z "$MACHINE" ] && MACHINE="$(hostname 2>/dev/null || echo unknown)"
@@ -76,6 +81,34 @@ ENTRY="$DATE | $MACHINE | $SKILL | $MSG"
MARK="<!-- Append entries below this line -->" MARK="<!-- Append entries below this line -->"
TMP="$LOG.tmp.$$" TMP="$LOG.tmp.$$"
# Dedup pass: an identical entry today (bare, or already counted "(xN)") gets its
# repeat counter bumped in place instead of a duplicate line. Literal string
# compares only — the message may contain regex metacharacters.
DEDUP_RC=1
awk -v entry="$ENTRY" '
!bumped && $0 == entry { print entry " (x2)"; bumped=1; next }
!bumped && index($0, entry " (x") == 1 {
n = substr($0, length(entry) + 4) # text after " (x"
if (n ~ /^[0-9]+\)$/) {
sub(/\)$/, "", n)
print entry " (x" n+1 ")"; bumped=1; next
}
}
{ print }
END { exit bumped ? 0 : 1 }
' "$LOG" > "$TMP" 2>/dev/null && DEDUP_RC=0
if [ "$DEDUP_RC" -eq 0 ]; then
if mv "$TMP" "$LOG" 2>/dev/null; then
echo "[OK] duplicate entry — bumped repeat counter in errorlog.md ($SKILL)"
else
rm -f "$TMP" 2>/dev/null
echo "[WARN] log-skill-error: could not write $LOG" >&2
fi
exit 0
fi
rm -f "$TMP" 2>/dev/null
if awk -v entry="$ENTRY" -v mark="$MARK" ' if awk -v entry="$ENTRY" -v mark="$MARK" '
{ print } { print }
($0==mark && !done) { print ""; print entry; done=1 } ($0==mark && !done) { print ""; print entry; done=1 }

View File

@@ -12,6 +12,11 @@
# bash post-bot-alert.sh "message text" bot # force #bot-alerts # bash post-bot-alert.sh "message text" bot # force #bot-alerts
# echo "message text" | bash post-bot-alert.sh # echo "message text" | bash post-bot-alert.sh
# #
# Identity: This script sources get-identity.sh, making $USER_SHORT, $USER_NAME,
# $MACHINE, $USER_EMAIL available. Callers can use these in messages for correct
# attribution (e.g., "[RMM] $USER_SHORT deployed..."). The script itself doesn't
# auto-inject identity - callers must explicitly use the vars when needed.
#
# Token resolution (first hit wins): # Token resolution (first hit wins):
# 1. SOPS vault: projects/discord-bot/bot-token.sops.yaml field credentials.bot_token # 1. SOPS vault: projects/discord-bot/bot-token.sops.yaml field credentials.bot_token
# 2. projects/discord-bot/.env key DISCORD_TOKEN # 2. projects/discord-bot/.env key DISCORD_TOKEN
@@ -27,6 +32,9 @@ BOT_CHANNEL_ID="624710699771232265" # #bot-alerts — default (Syncro + gene
DEV_CHANNEL_ID="1509998508198068484" # #dev-alerts — private RMM/Dev alerts (Howard + Mike only) DEV_CHANNEL_ID="1509998508198068484" # #dev-alerts — private RMM/Dev alerts (Howard + Mike only)
ROOT="${CLAUDETOOLS_ROOT:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)}" ROOT="${CLAUDETOOLS_ROOT:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)}"
# Load identity for attribution (soft-fail if missing)
source "$ROOT/.claude/scripts/get-identity.sh" 2>/dev/null || true
# --- message (arg or stdin) --- # --- message (arg or stdin) ---
MSG="${1:-}" MSG="${1:-}"
if [ -z "$MSG" ] && [ ! -t 0 ]; then MSG="$(cat)"; fi if [ -z "$MSG" ] && [ ! -t 0 ]; then MSG="$(cat)"; fi
@@ -35,6 +43,13 @@ if [ -z "$MSG" ]; then
exit 0 exit 0
fi fi
# Discord caps message content at 2000 chars (code 50035 on overflow). Alerts
# are one-liners by contract — truncate rather than chunk.
if [ "${#MSG}" -gt 1900 ]; then
MSG="${MSG:0:1900} ...[truncated]"
echo "[WARNING] post-bot-alert: message over 1900 chars — truncated" >&2
fi
# --- channel routing --- # --- channel routing ---
# Optional 2nd arg: "dev"/"bot" keyword, a raw channel id, or omit for auto. # Optional 2nd arg: "dev"/"bot" keyword, a raw channel id, or omit for auto.
# Auto: RMM/Dev-category prefixes -> #dev-alerts (private); everything else # Auto: RMM/Dev-category prefixes -> #dev-alerts (private); everything else
@@ -67,14 +82,15 @@ if [ -z "$TOKEN" ] || [ "$TOKEN" = "null" ]; then
exit 0 exit 0
fi fi
# --- post (jq builds JSON so the message is safely escaped) --- # --- post (jq builds the JSON; piped to curl via STDIN, never argv — a payload
PAYLOAD="$(jq -nc --arg c "$MSG" '{content: $c}')" # passed as a command-line arg on Windows goes through the argv encoding layer,
RESP="$(curl -s -m 15 -w $'\n%{http_code}' \ # which mangles non-ASCII chars into invalid JSON -> Discord 50109) ---
RESP="$(jq -nc --arg c "$MSG" '{content: $c}' | curl -s -m 15 -w $'\n%{http_code}' \
-X POST "https://discord.com/api/v10/channels/${CHANNEL_ID}/messages" \ -X POST "https://discord.com/api/v10/channels/${CHANNEL_ID}/messages" \
-H "Authorization: Bot ${TOKEN}" \ -H "Authorization: Bot ${TOKEN}" \
-H "Content-Type: application/json" \ -H "Content-Type: application/json" \
-H "User-Agent: ClaudeToolsBot (claudetools, 1.0)" \ -H "User-Agent: ClaudeToolsBot (claudetools, 1.0)" \
--data-binary "$PAYLOAD" 2>/dev/null)" --data-binary @- 2>/dev/null)"
HTTP="$(printf '%s' "$RESP" | tail -n1)" HTTP="$(printf '%s' "$RESP" | tail -n1)"
BODY="$(printf '%s' "$RESP" | sed '$d')" BODY="$(printf '%s' "$RESP" | sed '$d')"

View File

@@ -0,0 +1,154 @@
#!/usr/bin/env bash
# ps-encoded.sh — dispatch PowerShell through quote-mangling layers safely.
#
# THE point of this helper: a PS payload that traverses CommandLineToArgvW
# (curl.exe args, plink, ScreenConnect's command box, RMM->cmd.exe) gets its
# embedded double-quotes stripped and its UNC backslashes halved — the single
# most-repeated friction class in errorlog.md (ref=feedback_windows_quote_stripping,
# 9+ hits). -EncodedCommand (UTF-16LE base64) has NO quotes, backslashes, or
# dollar signs to mangle, so the script arrives byte-exact. Write the script
# to a FILE (or stdin), let this helper encode + deliver it.
#
# Usage:
# ps-encoded.sh encode <script.ps1|-> print the one-liner for
# ScreenConnect / plink / any paste
# ps-encoded.sh rmm <agent-uuid> <script.ps1|-> dispatch via GuruRMM + poll
# [--timeout <sec>] agent-side timeout_seconds (default 120)
# [--user-session] context: user_session (WTS-impersonated desktop user)
# [--no-wait] dispatch only; print command id, skip polling
# [--force] override the size refusal (see below)
#
# Size guards (agent chokes on ~7KB command bodies; encoding inflates ~2.67x):
# encoded > 4000 chars -> [WARNING]; encoded > 6000 chars -> refuse unless
# --force (split the script, or stage it on the endpoint and run the file).
#
# Exit codes: 0 ok; 1 usage/encode error; 2 size refusal; 3 dispatch/poll failure.
set -u
ROOT="${CLAUDETOOLS_ROOT:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)}"
_logerr() { bash "$ROOT/.claude/scripts/log-skill-error.sh" "ps-encoded" "$@" >/dev/null 2>&1 || true; }
usage() { sed -n '2,26p' "$0"; exit 1; }
read_script() { # $1 = file path or "-"
if [ "$1" = "-" ]; then cat
elif [ -f "$1" ]; then cat "$1"
else echo "[ERROR] script file not found: $1" >&2; exit 1
fi
}
encode_b64() { # stdin: UTF-8 script -> stdout: UTF-16LE base64 (no BOM, no wraps)
if command -v iconv >/dev/null 2>&1; then
iconv -f UTF-8 -t UTF-16LE | base64 -w0
else
# Git-for-Windows ships the iconv *library* (msys-iconv-2.dll) but NOT iconv.exe,
# and has no pacman to install it. Fall back to Python (present fleet-wide) for the
# UTF-16LE -> base64 encode. Binary stdin/stdout so no CRLF translation.
local py
py="$(command -v py || command -v python3 || command -v python || true)"
if [ -z "$py" ]; then
echo "[ERROR] neither iconv nor python available for UTF-16LE encoding" >&2
return 1
fi
"$py" -c "import sys,base64; sys.stdout.write(base64.b64encode(sys.stdin.buffer.read().decode('utf-8').encode('utf-16-le')).decode())"
fi
}
size_gate() { # $1 = encoded string, $2 = force flag (0/1)
local n=${#1}
if [ "$n" -gt 6000 ] && [ "${2:-0}" != "1" ]; then
echo "[ERROR] encoded payload is ${n} chars (>6000); the agent fails on bodies this large." >&2
echo " Split the script into sections, or stage it as a file on the endpoint" >&2
echo " and dispatch 'powershell -File <path>'. Override with --force." >&2
return 1
elif [ "$n" -gt 4000 ]; then
echo "[WARNING] encoded payload is ${n} chars — near the agent's body-size failure zone (~7KB)." >&2
fi
return 0
}
cmd_encode() {
local src="${1:-}"; [ -z "$src" ] && usage
local b64
b64="$(read_script "$src" | encode_b64)"
[ -z "$b64" ] && { echo "[ERROR] encoding produced nothing" >&2; _logerr "encode produced empty output" --context "src=$src"; exit 1; }
size_gate "$b64" 1 || true # encode mode: warn only, the paste target may cope
printf 'powershell -NoProfile -ExecutionPolicy Bypass -EncodedCommand %s\n' "$b64"
}
cmd_rmm() {
local agent="${1:-}"; shift || true
local src="${1:-}"; shift || true
[ -z "$agent" ] || [ -z "$src" ] && usage
local timeout=120 context="" wait=1 force=0
while [ $# -gt 0 ]; do
case "$1" in
--timeout) timeout="${2:?}"; shift 2 ;;
--user-session) context="user_session"; shift ;;
--no-wait) wait=0; shift ;;
--force) force=1; shift ;;
*) echo "[ERROR] unknown option: $1" >&2; usage ;;
esac
done
local b64
b64="$(read_script "$src" | encode_b64)"
[ -z "$b64" ] && { echo "[ERROR] encoding produced nothing" >&2; _logerr "encode produced empty output" --context "src=$src"; exit 1; }
size_gate "$b64" "$force" || exit 2
eval "$(bash "$ROOT/.claude/scripts/rmm-auth.sh")"
if [ -z "${TOKEN:-}" ] || [ -z "${RMM:-}" ]; then
echo "[ERROR] RMM auth failed (no TOKEN/RMM)" >&2
_logerr "RMM auth failed via rmm-auth.sh" --context "agent=$agent"
exit 3
fi
# command_type=shell (cmd.exe): the base64 blob is a single quote-free token,
# so no layer between here and powershell.exe can mangle it. timeout_seconds
# is the field the agent honors — 'timeout' is silently ignored (errorlog).
local oneliner payload resp cmd_id status
oneliner="powershell -NoProfile -ExecutionPolicy Bypass -EncodedCommand ${b64}"
payload="$(jq -nc --arg ct shell --arg cmd "$oneliner" --argjson to "$timeout" \
--arg cx "$context" \
'{command_type:$ct, command:$cmd, timeout_seconds:$to}
+ (if $cx != "" then {context:$cx} else {} end)')"
resp="$(printf '%s' "$payload" | curl -s -m 20 -X POST "$RMM/api/agents/$agent/command" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
--data-binary @-)"
cmd_id="$(printf '%s' "$resp" | jq -r '.command_id // empty' 2>/dev/null)"
status="$(printf '%s' "$resp" | jq -r '.status // empty' 2>/dev/null)"
if [ -z "$cmd_id" ]; then
echo "[ERROR] dispatch failed: $resp" >&2
_logerr "EncodedCommand dispatch failed" --context "agent=$agent resp=${resp:0:80}"
exit 3
fi
echo "[OK] dispatched command_id=$cmd_id (initial status: ${status:-unknown}, timeout_seconds=$timeout)"
[ "$wait" = "0" ] && exit 0
local max_polls=$(( (timeout + 30) / 5 )) count=0 result
while [ $count -lt $max_polls ]; do
result="$(curl -s -m 15 "$RMM/api/commands/$cmd_id" -H "Authorization: Bearer $TOKEN")"
status="$(printf '%s' "$result" | jq -r '.status // empty' 2>/dev/null)"
case "$status" in
completed|failed|cancelled|interrupted)
echo "--- status: $status ---"
printf '%s' "$result" | jq -r '
"exit_code: \(.exit_code // "n/a")",
"--- stdout ---", (.stdout // .output // ""),
"--- stderr ---", (.stderr // "")' 2>/dev/null || printf '%s\n' "$result"
[ "$status" = "completed" ] && exit 0 || exit 3 ;;
running|pending) count=$((count+1)); sleep 5 ;;
*) echo "[ERROR] empty/unknown status — response: $result" >&2
_logerr "poll returned empty status" --context "cmd=$cmd_id agent=$agent"
exit 3 ;;
esac
done
echo "[WARNING] poll timeout after $((max_polls*5))s — command $cmd_id may still be running (last: $status)"
exit 3
}
case "${1:-}" in
encode) shift; cmd_encode "$@" ;;
rmm) shift; cmd_rmm "$@" ;;
*) usage ;;
esac

View File

@@ -0,0 +1,51 @@
# register-edr-watcher.ps1
# Register the "ClaudeTools - EDR Isolation Watcher" scheduled task on this Windows
# machine (the same box that runs GPS-RMM-AutoEnroll et al). The task runs
# edr-isolation-watch.sh every 10 minutes, which polls Datto EDR for hosts auto-
# isolated by an EDR response and posts each NEW one to #dev-alerts (Mike + Howard).
#
# Interim alerting until the GuruRMM EDR webhook integration (Feature 6) lands.
# Idempotent: -Force replaces any existing task with the same name.
#
# Run from an ordinary (non-admin) PowerShell:
# powershell -ExecutionPolicy Bypass -File C:\claudetools\.claude\scripts\register-edr-watcher.ps1
$ErrorActionPreference = "Stop"
$TaskName = "ClaudeTools - EDR Isolation Watcher"
# Launch via a wscript.exe VBS wrapper (GUI-subsystem host) so bash starts hidden and
# NO console window flashes on the desktop every 10 min. Launching bash.exe directly is
# a console-subsystem app with LogonType=Interactive + Hidden=False -> visible flash.
# Mirrors the gps-rmm-progress-hidden.vbs fix. Do NOT revert to a raw bash.exe action.
$BashExe = "C:\Program Files\Git\bin\bash.exe"
if (-not (Test-Path $BashExe)) { Write-Host "[ERROR] Git bash not found at $BashExe" -ForegroundColor Red; exit 1 }
$ScriptWin = "C:\claudetools\.claude\scripts\edr-isolation-watch.sh"
if (-not (Test-Path $ScriptWin)) { Write-Host "[ERROR] Watcher not found at $ScriptWin" -ForegroundColor Red; exit 1 }
$WScript = "C:\Windows\System32\wscript.exe"
$Vbs = "C:\claudetools\.claude\scripts\edr-isolation-watch-hidden.vbs"
if (-not (Test-Path $Vbs)) { Write-Host "[ERROR] Hidden wrapper not found at $Vbs" -ForegroundColor Red; exit 1 }
$Action = New-ScheduledTaskAction -Execute $WScript -Argument "`"$Vbs`""
# Every 10 minutes, indefinitely (10-year duration avoids the MaxValue quirk).
$Trigger = New-ScheduledTaskTrigger -Once -At (Get-Date).Date `
-RepetitionInterval (New-TimeSpan -Minutes 10) `
-RepetitionDuration (New-TimeSpan -Days 3650)
# Run as the current user, only when logged on (needs the interactive vault/env,
# same posture as the GrepAI watcher). No admin required.
$Principal = New-ScheduledTaskPrincipal -UserId "$env:USERDOMAIN\$env:USERNAME" -LogonType Interactive -RunLevel Limited
$Settings = New-ScheduledTaskSettingsSet `
-AllowStartIfOnBatteries -DontStopIfGoingOnBatteries `
-StartWhenAvailable -ExecutionTimeLimit (New-TimeSpan -Minutes 5) `
-MultipleInstances IgnoreNew -Hidden
Register-ScheduledTask -TaskName $TaskName -Action $Action -Trigger $Trigger `
-Principal $Principal -Settings $Settings `
-Description "Polls Datto EDR every 10 min for auto-isolated hosts; posts new ones to #dev-alerts. Interim until GuruRMM EDR webhook (Feature 6)." -Force | Out-Null
Write-Host "[OK] Registered scheduled task: $TaskName (every 10 min)" -ForegroundColor Green
Get-ScheduledTask -TaskName $TaskName | Select-Object TaskName, State | Format-Table -AutoSize

View File

@@ -47,14 +47,26 @@ if (-not (Test-Path $Script)) {
exit 1 exit 1
} }
# Resolve the py launcher's full path (the action's Execute wants an absolute # Resolve pythonW.exe (the GUI-subsystem Python host) so the detector runs with NO
# path; "py" alone usually resolves but we pin it for reliability under the # console window flashing on the desktop at logon / every 4h. Using py.exe or
# Task Scheduler's environment). # python.exe (console subsystem) draws a visible window each run. Prefer pythonw next
# to the active interpreter; fall back to a PATH lookup, then py.exe as a last resort.
$PyPath = $null
$PyCmd = Get-Command py -ErrorAction SilentlyContinue $PyCmd = Get-Command py -ErrorAction SilentlyContinue
if ($null -ne $PyCmd) { if ($null -ne $PyCmd) {
$PyPath = $PyCmd.Source try {
} else { $exe = (& py -c "import sys;print(sys.executable)").Trim()
$PyPath = "py" # fall back to PATH resolution at run time $wexe = Join-Path (Split-Path $exe) "pythonw.exe"
if (Test-Path $wexe) { $PyPath = $wexe }
} catch { }
}
if ($null -eq $PyPath) {
$PwCmd = Get-Command pythonw -ErrorAction SilentlyContinue
if ($null -ne $PwCmd) { $PyPath = $PwCmd.Source }
}
if ($null -eq $PyPath) {
if ($null -ne $PyCmd) { $PyPath = $PyCmd.Source } else { $PyPath = "py" }
Write-Host "[WARNING] pythonw.exe not found; task may flash a console window." -ForegroundColor Yellow
} }
$Action = New-ScheduledTaskAction ` $Action = New-ScheduledTaskAction `
@@ -76,7 +88,8 @@ $Settings = New-ScheduledTaskSettingsSet `
-ExecutionTimeLimit (New-TimeSpan -Minutes 30) ` -ExecutionTimeLimit (New-TimeSpan -Minutes 30) `
-MultipleInstances IgnoreNew ` -MultipleInstances IgnoreNew `
-StartWhenAvailable ` -StartWhenAvailable `
-DontStopOnIdleEnd -DontStopOnIdleEnd `
-Hidden
Register-ScheduledTask ` Register-ScheduledTask `
-TaskName $TaskName ` -TaskName $TaskName `

View File

@@ -145,6 +145,50 @@ resolve_submodule_collisions() {
[ "$moved" -eq 1 ] && return 0 || return 1 [ "$moved" -eq 1 ] && return 0 || return 1
} }
# Advance submodules to the parent's pinned gitlinks after a parent pull, WITHOUT
# ever clobbering a submodule that has local work. The pristine/pinned state of a
# submodule is a CLEAN, DETACHED HEAD; a developer actively working in a submodule
# checks out a BRANCH and/or has uncommitted edits. Those we skip entirely so an
# in-progress feature inside a submodule survives a parent sync.
#
# (Friction 2026-06-22: an unguarded `git submodule update --init --recursive`
# after the parent rebase repeatedly reset guru-rmm to its pinned commit — detaching
# HEAD and discarding a pushed-elsewhere feature branch + commits mid-build. The
# Phase-1a init guard at the top did not cover this post-rebase reconcile path.)
#
# Protected-submodule notices go to stderr; git's own output goes to stdout so the
# caller can capture it for failure reporting. Returns git's rc (0 if nothing to do).
submodule_update_safe() {
[ -f ".gitmodules" ] || return 0
local p safe=() prot=()
while read -r p; do
[ -n "$p" ] || continue
if [ -e "$p/.git" ]; then
# On a branch (not the pristine detached pin) -> someone is working here.
if git -C "$p" symbolic-ref -q HEAD >/dev/null 2>&1; then
prot+=("$p [branch $(git -C "$p" rev-parse --abbrev-ref HEAD 2>/dev/null)]")
continue
fi
# Uncommitted changes -> protect.
if [ -n "$(git -C "$p" status --porcelain 2>/dev/null)" ]; then
prot+=("$p [uncommitted changes]")
continue
fi
fi
# Clean detached HEAD, or not yet populated (fresh clone) -> safe to update.
safe+=("$p")
done < <(git config --file .gitmodules --get-regexp '^submodule\..*\.path$' 2>/dev/null | awk '{print $2}')
local item
for item in "${prot[@]}"; do
echo -e "${YELLOW}[INFO]${NC} sync: leaving submodule with local work untouched: ${item}" >&2
done
[ "${#safe[@]}" -eq 0 ] && return 0
git submodule update --init --recursive --quiet -- "${safe[@]}" 2>&1
return $?
}
# Machine + timestamp # Machine + timestamp
if [ -n "$COMPUTERNAME" ]; then if [ -n "$COMPUTERNAME" ]; then
MACHINE="$COMPUTERNAME" MACHINE="$COMPUTERNAME"
@@ -522,14 +566,14 @@ if [ "$INCOMING_COUNT" -gt 0 ]; then
# raw "fatal: Unable to checkout" reads alarmingly right before a clean resolve. # raw "fatal: Unable to checkout" reads alarmingly right before a clean resolve.
# Only surface the raw output if the resolve+retry below also fails. # Only surface the raw output if the resolve+retry below also fails.
set +e set +e
SUB_OUT=$(git submodule update --init --recursive --quiet 2>&1) SUB_OUT=$(submodule_update_safe)
SUB_RC=$? SUB_RC=$?
set -e set -e
if [ "$SUB_RC" -ne 0 ]; then if [ "$SUB_RC" -ne 0 ]; then
# Move any untracked files colliding with the incoming commit aside, retry once. # Move any untracked files colliding with the incoming commit aside, retry once.
if resolve_submodule_collisions; then if resolve_submodule_collisions; then
set +e set +e
SUB_OUT=$(git submodule update --init --recursive --quiet 2>&1) SUB_OUT=$(submodule_update_safe)
SUB_RC=$? SUB_RC=$?
set -e set -e
fi fi

View File

@@ -19,6 +19,11 @@
"type": "command", "type": "command",
"command": "bash -c 'h=\"${CLAUDE_PROJECT_DIR}/.claude/hooks/block-backslash-winpath.sh\"; [ -f \"$h\" ] && exec bash \"$h\" || exit 0'", "command": "bash -c 'h=\"${CLAUDE_PROJECT_DIR}/.claude/hooks/block-backslash-winpath.sh\"; [ -f \"$h\" ] && exec bash \"$h\" || exit 0'",
"timeout": 10 "timeout": 10
},
{
"type": "command",
"command": "bash -c 'h=\"${CLAUDE_PROJECT_DIR}/.claude/hooks/block-tmp-path.sh\"; [ -f \"$h\" ] && exec bash \"$h\" || exit 0'",
"timeout": 10
} }
] ]
} }

Some files were not shown because too many files have changed in this diff Show More