sync: auto-sync from GURU-5070 at 2026-06-15 09:41:53

Author: Mike Swanson
Machine: GURU-5070
Timestamp: 2026-06-15 09:41:53
This commit is contained in:
2026-06-15 09:42:15 -07:00
parent 153be4abec
commit dc5c09b40b
26 changed files with 537 additions and 13 deletions

View File

@@ -0,0 +1,30 @@
---
name: discord-dm
description: Send a Discord message to an org member's DMs or a team channel via the ClaudeTools bot. Prepopulated with user + channel IDs. Use for copy-paste-friendly delivery of wrapped command lines (consent links, long one-liners) or to ping someone directly.
---
# /discord-dm — direct Discord messaging
Thin entry point to the `discord-dm` skill. Engine: `.claude/scripts/discord-dm.sh`.
## Usage
```
/discord-dm <recipient> <message> Send a DM (mike|howard|rob|winter) or post to a channel (#bot-alerts|#dev-alerts)
/discord-dm list Show known users + channel IDs
```
Examples:
```bash
bash .claude/scripts/discord-dm.sh mike "https://login.microsoftonline.com/.../adminconsent?client_id=..."
bash .claude/scripts/discord-dm.sh dev "build promoted to stable"
echo "$LONG_LINK" | bash .claude/scripts/discord-dm.sh mike
```
## Standing rule
Any **wrapped / long single-line output** (M365 consent links, long CLI one-liners,
URLs with query strings) should be **DM'd to `mike`** so it's cleanly copy-pasteable
rather than mangled by terminal wrapping. See `.claude/skills/discord-dm/SKILL.md`
for the recipient forms, prepopulated directory, and gotchas.

View File

@@ -2,6 +2,8 @@
Read and send mail for an Arizona Computer Guru mailbox via Microsoft Graph, using the shared **Claude-MSP-Access** app. 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 shared **Claude-MSP-Access** app. Defaults to the mailbox of the user running it (from `identity.json`).
> **[BLOCKED 2026-06-14]** The `Claude-MSP-Access` app (`fabb3421`) was **DELETED** from the azcomputerguru.com tenant, so every token request returns **AADSTS700016** and this command cannot read or send until a replacement mail-capable app is provisioned. Decision (2026-06-15): the replacement is the **Exchange Operator** suite tier (`exchange-op`, `b43e7342-5b4b-492f-890f-bb5a4f7f40e9`) once `Mail.Send` (+ optionally `Mail.ReadWrite`/`Contacts`) is added to its manifest and consented — Mail.Send's real use is IR victim-notification during mailbox takeovers, so it lives in the suite. NOT yet provisioned. If a token fails with `AADSTS700016`, this is why — do not retry; surface this note. When provisioned, repoint `client_id` (API Configuration + the `py` helper) to `b43e7342...` and the vault path to `computerguru-exchange-operator.sops.yaml`. See `errorlog.md` and `remediation-tool/references/gotchas.md`.
## Usage ## Usage
``` ```

View File

@@ -38,6 +38,8 @@
- [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.
- [DMARC rua INKY only when onboarded](feedback_dmarc_rua_inky_onboarded_only.md) — Don't point a client's DMARC rua at reports-sg.inkydmarc.com unless that client is onboarded to INKY (most aren't). Use plain `p=none` with no rua otherwise.
- [DM wrapped command lines to Mike](feedback_dm_wrapped_command_lines.md) — Long single-line output (consent links, URLs, one-liners) gets DM'd to Mike via the `discord-dm` skill so it's copy-pasteable, not terminal-wrapped. `discord-dm.sh mike "<link>"`.
- [Attribution is read, never inferred](feedback_attribution_from_identity.md) — Who-did-what (user+machine) comes ONLY from identity.json + users.json + git authorship. Never infer from hostname patterns, the userEmail hint, or memory. The "5070" box is Mike's. sync.sh reconciles git config to identity.json; /save renders the User block via whoami-block.sh. - [Attribution is read, never inferred](feedback_attribution_from_identity.md) — Who-did-what (user+machine) comes ONLY from identity.json + users.json + git authorship. Never infer from hostname patterns, the userEmail hint, or memory. The "5070" box is Mike's. sync.sh reconciles git config to identity.json; /save renders the User block via whoami-block.sh.
- [D2TESTNAS SSH Access](feedback_d2testnas_ssh.md) — Use root@192.168.0.9 with Paper123!@#, not sysadmin. - [D2TESTNAS SSH Access](feedback_d2testnas_ssh.md) — Use root@192.168.0.9 with Paper123!@#, not sysadmin.
- [Bypass Permissions Setting](feedback_bypass_permissions_setting.md) — Set permissions.defaultMode to bypassPermissions in settings.json on all machines. - [Bypass Permissions Setting](feedback_bypass_permissions_setting.md) — Set permissions.defaultMode to bypassPermissions in settings.json on all machines.

View File

@@ -0,0 +1,12 @@
---
name: feedback-dm-wrapped-command-lines
description: DM any wrapped/long single-line output (consent links, long one-liners, URLs) to Mike in Discord so it's copy-pasteable
metadata:
type: feedback
---
Any wrapped command line or long single-line output meant for the user to copy — M365 admin-consent links, long CLI one-liners, URLs with query strings, enrollment/installer URLs — should be **sent to Mike as a Discord DM**, not left only in the terminal.
**Why:** Terminal wrapping breaks long single-line items across lines, so copy-paste picks up line breaks/spaces and corrupts the link or command. A Discord DM preserves it as one clean line. (Mike, 2026-06-15.)
**How to apply:** Use the `discord-dm` skill — `bash .claude/scripts/discord-dm.sh mike "<the link/command>"` (or `echo "$X" | ... mike`). Still show it inline in the response too, but the DM is the canonical copy-paste source. The skill is prepopulated with all org user IDs (mike/howard/rob/winter) and channel IDs (#bot-alerts/#dev-alerts); keep its directory in sync with `.claude/users.json`. Build payloads with `jq -nc --arg` + `printf | curl --data-binary @-` (direct `-d` mangles multiline → Discord 50109).

View File

@@ -0,0 +1,12 @@
---
name: feedback-dmarc-rua-inky-onboarded-only
description: Only point a client's DMARC rua at INKY (reports-sg.inkydmarc.com) if that client is onboarded to INKY
metadata:
type: feedback
---
When adding a DMARC record for a client, do NOT copy ACG's own convention of `rua=mailto:reports@reports-sg.inkydmarc.com` unless that specific client is onboarded to INKY DMARC. azcomputerguru.com uses INKY, but most clients are not on it.
**Why:** INKY only processes aggregate reports for domains provisioned in the INKY account. Pointing an un-onboarded client's `rua` there sends reports to an aggregator that ignores them — no monitoring value, just misdirected traffic. (Mike, 2026-06-15, CryoWeave.)
**How to apply:** For a client not on INKY, use `v=DMARC1; p=none;` with no `rua` (valid policy, improves deliverability posture, no report destination), or a same-domain mailbox if they want reports. Reserve the INKY rua for INKY-onboarded domains. See [[reference_ix_server_access]] for the DNS host (ns1/ns2.acghosting.com = cPanel on IX).

View File

@@ -0,0 +1,106 @@
#!/usr/bin/env bash
# discord-dm.sh — send a Discord message to an org member (DM) or a team channel,
# using the ClaudeTools bot. Built for copy-paste-friendly delivery of wrapped
# command lines (consent links, long one-liners) straight to a person's DMs.
#
# Usage:
# discord-dm.sh <recipient> "message text"
# echo "message text" | discord-dm.sh <recipient>
# discord-dm.sh list # show known users + channels
#
# <recipient> is one of:
# - a user name: mike | howard | rob | winter -> opens a DM
# - a channel: #bot-alerts | #dev-alerts | bot | dev -> posts to the channel
# - a raw snowflake: 17-19 digit ID (treated as a USER DM; prefix chan:<id> for a channel)
#
# Token resolution (mirrors post-bot-alert.sh, first hit wins):
# 1. SOPS vault: projects/discord-bot/bot-token.sops.yaml field credentials.bot_token
# 2. projects/discord-bot/.env key DISCORD_TOKEN
#
# Exit codes: 0 success; 1 usage/arg error; 2 token missing; 3 Discord API error.
# Unlike post-bot-alert.sh this does NOT soft-fail — a DM is usually load-bearing
# (you asked for it on purpose), so a failure is surfaced with a non-zero exit.
set -u
ROOT="${CLAUDETOOLS_ROOT:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)}"
API="https://discord.com/api/v10"
UA="ClaudeToolsBot (claudetools, 1.0)"
# --- prepopulated org directory (from .claude/users.json + post-bot-alert.sh) ---
declare -A USERS=(
[mike]="264814939619721216"
[howard]="624667664501178379"
[rob]="261978810713505792"
[winter]="624666486362996755"
)
declare -A CHANNELS=(
[bot-alerts]="624710699771232265" # whole team (Syncro + general)
[dev-alerts]="1509998508198068484" # private RMM/Dev (Howard + Mike)
)
print_dir() {
echo "Users (DM):"
for u in "${!USERS[@]}"; do printf " %-8s %s\n" "$u" "${USERS[$u]}"; done
echo "Channels:"
for c in "${!CHANNELS[@]}"; do printf " #%-12s %s\n" "$c" "${CHANNELS[$c]}"; done
}
RECIPIENT="${1:-}"
if [ -z "$RECIPIENT" ] || [ "$RECIPIENT" = "-h" ] || [ "$RECIPIENT" = "--help" ]; then
sed -n '2,30p' "$0"; exit 1
fi
if [ "$RECIPIENT" = "list" ]; then print_dir; exit 0; fi
shift
# --- message (remaining args, else stdin) ---
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
# --- resolve recipient -> mode (dm|channel) + target id ---
MODE=""; TARGET=""; LABEL=""
key="$(printf '%s' "$RECIPIENT" | tr '[:upper:]' '[:lower:]')"
case "$key" in
bot|bot-alerts|"#bot-alerts") MODE=channel; TARGET="${CHANNELS[bot-alerts]}"; LABEL="#bot-alerts" ;;
dev|dev-alerts|"#dev-alerts") MODE=channel; TARGET="${CHANNELS[dev-alerts]}"; LABEL="#dev-alerts" ;;
chan:*) MODE=channel; TARGET="${key#chan:}"; LABEL="channel ${TARGET}" ;;
\#*) c="${key#\#}"; if [ -n "${CHANNELS[$c]:-}" ]; then MODE=channel; TARGET="${CHANNELS[$c]}"; LABEL="#$c"; else echo "[ERROR] unknown channel: $RECIPIENT (try: discord-dm.sh list)" >&2; exit 1; fi ;;
*)
if [ -n "${USERS[$key]:-}" ]; then MODE=dm; TARGET="${USERS[$key]}"; LABEL="$key (DM)"
elif printf '%s' "$key" | grep -qE '^[0-9]{17,19}$'; then MODE=dm; TARGET="$key"; LABEL="user ${key} (DM)"
else echo "[ERROR] unknown recipient: $RECIPIENT (try: discord-dm.sh list)" >&2; exit 1; fi ;;
esac
# --- bot token: vault first, then .env ---
TOKEN="$(bash "$ROOT/.claude/scripts/vault.sh" get-field \
projects/discord-bot/bot-token.sops.yaml credentials.bot_token 2>/dev/null)"
if [ -z "$TOKEN" ] || [ "$TOKEN" = "null" ]; then
ENV_FILE="$ROOT/projects/discord-bot/.env"
[ -f "$ENV_FILE" ] && TOKEN="$(grep -iE '^[[:space:]]*DISCORD_TOKEN[[:space:]]*=' "$ENV_FILE" | head -1 | sed -E 's/^[^=]*=[[:space:]]*//; s/^["'"'"']//; s/["'"'"'][[:space:]]*$//')"
fi
if [ -z "$TOKEN" ] || [ "$TOKEN" = "null" ]; then echo "[ERROR] no bot token (vault + .env both empty)" >&2; exit 2; fi
auth=(-H "Authorization: Bot ${TOKEN}" -H "Content-Type: application/json" -H "User-Agent: ${UA}")
# --- for a DM, open (or fetch) the user's DM channel ---
if [ "$MODE" = "dm" ]; then
DM="$(printf '%s' "$(jq -nc --arg r "$TARGET" '{recipient_id:$r}')" | \
curl -s -m 15 "${auth[@]}" -X POST "$API/users/@me/channels" --data-binary @-)"
CHID="$(printf '%s' "$DM" | jq -r '.id // empty' 2>/dev/null)"
if [ -z "$CHID" ]; then echo "[ERROR] could not open DM channel for $LABEL: $DM" >&2; exit 3; fi
TARGET="$CHID"
fi
# --- post the message (printf | --data-binary @- — direct -d mangles multiline JSON) ---
RESP="$(printf '%s' "$(jq -nc --arg c "$MSG" '{content:$c}')" | \
curl -s -m 15 -w $'\n%{http_code}' "${auth[@]}" \
-X POST "$API/channels/${TARGET}/messages" --data-binary @-)"
HTTP="$(printf '%s' "$RESP" | tail -n1)"
BODY="$(printf '%s' "$RESP" | sed '$d')"
if [ "$HTTP" = "200" ]; then
echo "[OK] discord-dm: sent to ${LABEL} (message_id=$(printf '%s' "$BODY" | jq -r '.id // empty'))"
exit 0
fi
echo "[ERROR] discord-dm: Discord returned ${HTTP:-no-response}${BODY}" >&2
exit 3

View File

@@ -0,0 +1,80 @@
---
name: discord-dm
description: >
Send a Discord message to an org member's DMs or to a team channel via the
ClaudeTools bot. Use this whenever you need to hand a person something
copy-paste-friendly that the terminal would wrap or mangle — consent links,
long single-line commands, URLs, tokens-to-rotate notices — or to ping someone
directly. Prepopulated with every org member's user ID and the team channel IDs,
so you address people by name (mike/howard/rob/winter) not raw snowflakes.
Invoke on: "DM me/<person> in discord", "send <person> a discord message",
"message <person> on discord", "discord DM", "send that link to my discord",
"ping <person>". For one-line [SYNCRO]/[RMM] status alerts to the alert channels,
prefer post-bot-alert.sh; use this for direct/person-targeted delivery.
---
# discord-dm — direct Discord messaging to the org
Thin wrapper over the Discord bot API. The engine is
`.claude/scripts/discord-dm.sh`; this doc is the usage contract.
## When to use
- **Wrapped command lines** — Mike's standing rule: any wrapped/long single-line
output (M365 consent links, long CLI one-liners, URLs with query strings) gets
**DM'd to him in Discord** so it's cleanly copy-pasteable, not mangled by terminal
wrapping. Default target for that is `mike`.
- Pinging a specific teammate with something actionable.
- Posting to a team channel when you want to choose the channel explicitly
(`#bot-alerts` / `#dev-alerts`).
For routine one-line `[SYNCRO]` / `[RMM]` status alerts, keep using
`post-bot-alert.sh` (it auto-routes by prefix). This skill is for **person-targeted
DMs** and deliberate channel posts.
## Usage
```bash
bash .claude/scripts/discord-dm.sh <recipient> "message text"
echo "message text" | bash .claude/scripts/discord-dm.sh <recipient>
bash .claude/scripts/discord-dm.sh list # print known users + channels
```
`<recipient>`:
| Form | Effect |
|---|---|
| `mike` `howard` `rob` `winter` | opens/uses that user's DM channel |
| `#bot-alerts` / `bot` | posts to #bot-alerts (whole team) |
| `#dev-alerts` / `dev` | posts to #dev-alerts (Howard + Mike, private) |
| `chan:<id>` | posts to an arbitrary channel by ID |
| `<17-19 digit id>` | treated as a **user** DM |
## Prepopulated directory (from `.claude/users.json` + `post-bot-alert.sh`)
| Name | Discord user ID | | Channel | ID |
|---|---|---|---|---|
| mike | 264814939619721216 | | #bot-alerts | 624710699771232265 |
| howard | 624667664501178379 | | #dev-alerts | 1509998508198068484 |
| rob | 261978810713505792 | | | |
| winter | 624666486362996755 | | | |
Keep this table and the `USERS`/`CHANNELS` maps in the script in sync with
`.claude/users.json` when people are added/removed.
## Implementation notes / gotchas
- **Bot token** resolves from the SOPS vault
(`projects/discord-bot/bot-token.sops.yaml` field `credentials.bot_token`),
falling back to `projects/discord-bot/.env` `DISCORD_TOKEN`. Works from any
machine, not just BEAST.
- **DMs** require opening the user's DM channel first
(`POST /users/@me/channels {recipient_id}`) then posting to that channel id.
- **Multiline payloads:** always build JSON with `jq -nc --arg` and feed curl via
`printf '%s' "$payload" | curl ... --data-binary @-`. Passing the JSON directly
with `-d "$(...)"` mangled multiline content and returned Discord error
`50109 invalid JSON body` (hit 2026-06-15 sending the CryoWeave consent link).
- **Not soft-fail:** unlike `post-bot-alert.sh`, a send failure exits non-zero —
a requested DM is load-bearing, so surface the error.
- The bot must share a server with the user and the user must allow DMs from
server members, or the DM-channel open / send can 403.

View File

@@ -24,7 +24,16 @@ Five multi-tenant apps replace the old single over-permissioned app. Use minimum
| `tenant-admin` | ComputerGuru Tenant Admin | `709e6eed-0711-4875-9c44-2d3518c47063` | `computerguru-tenant-admin.sops.yaml` | | `tenant-admin` | ComputerGuru Tenant Admin | `709e6eed-0711-4875-9c44-2d3518c47063` | `computerguru-tenant-admin.sops.yaml` |
| `defender` | ComputerGuru Defender Add-on | `dbf8ad1a-54f4-4bb8-8a9e-ea5b9634635b` | `computerguru-defender-addon.sops.yaml` | | `defender` | ComputerGuru Defender Add-on | `dbf8ad1a-54f4-4bb8-8a9e-ea5b9634635b` | `computerguru-defender-addon.sops.yaml` |
**Deprecated (do not use):** ~~ComputerGuru - AI Remediation~~ (`fabb3421`) — old single-app with 159 permissions including Defender ATP. Broke consent on tenants without MDE license. Retire/delete from portal when confirmed no active tenants depend on it. **DELETED from the azcomputerguru.com tenant 2026-06-14** (was *ComputerGuru - AI Remediation* / *Claude-MSP-Access* / *Cloud MSP Access*, `fabb3421-8b34-484b-bc17-e46de9703418`) — old single-app with 159 permissions including Defender ATP. Any token request now returns **AADSTS700016** (app/SP gone). Two consequences:
1. It held the ONLY **Mail.Send / Mail.ReadWrite / Contacts** scopes the fleet had, so **`/mailbox` (ACG own-mail send/read) and the M365 contacts task are BLOCKED** until a replacement app is provisioned. The 5-app suite below has none of those scopes (`investigator` = `Mail.Read` only).
2. The legacy "old app only" tenants below (Valleywide, Dataforth, Cascades) have NO working remediation app anymore — migration to the new suite is now REQUIRED, not optional.
**Decision 2026-06-15 (Mike):** Mail.Send belongs in the SUITE, not a separate app. The real use case is incident response, auto-notifying victims during a mailbox takeover, which is a remediation action. Plan: add **`Mail.Send`** (application) to the **Exchange Operator** tier (`exchange-op`, `b43e7342-5b4b-492f-890f-bb5a4f7f40e9`), the existing Exchange remediation/write app. `Mail.ReadWrite` + `Contacts` are optional and only needed to fully restore the general `/mailbox` read/send + contacts task (secondary).
Implementation (NOT yet executed — production multi-tenant app change, needs explicit go + admin-consent clicks):
1. Add the Graph app permission(s) to the Exchange Operator app manifest in the home tenant; grant admin consent in the home tenant.
2. Re-consent Exchange Operator in each tenant where IR victim-notification is needed (adding a permission invalidates prior consent and re-prompts).
3. Repoint `commands/mailbox.md` `client_id` + vault path to `computerguru-exchange-operator.sops.yaml`, and consent Exchange Operator in the ACG home tenant so `/mailbox` (own-mail) works again.
When searching customer admin portals for a service principal (role assignments, app role assignments, CA exclusions), search by the display name for that tier (e.g., "ComputerGuru Security Investigator"). When searching customer admin portals for a service principal (role assignments, app role assignments, CA exclusions), search by the display name for that tier (e.g., "ComputerGuru Security Investigator").
@@ -123,6 +132,6 @@ If token request or API call returns AADSTS650052 referencing `WindowsDefenderAT
| mvaninc.com | 5affaf1e-de89-416b-a655-1b2cf615d5b1 | YES (2026-04-21) | — | YES (2026-04-21) | YES (2026-04-21) | — | — | — | — | Fully onboarded. Incident 2026-04-21: sysadmin GA account unauthorized sign-in from OKC via device PRT (MITCH-LAPTOP/JUNE). Remediated: pw reset, sessions revoked. CA policy (MFA all users) still pending — Mike to create. | | mvaninc.com | 5affaf1e-de89-416b-a655-1b2cf615d5b1 | YES (2026-04-21) | — | YES (2026-04-21) | YES (2026-04-21) | — | — | — | — | Fully onboarded. Incident 2026-04-21: sysadmin GA account unauthorized sign-in from OKC via device PRT (MITCH-LAPTOP/JUNE). Remediated: pw reset, sessions revoked. CA policy (MFA all users) still pending — Mike to create. |
| Quantum Wealth Management | 2fd0092b-e9b7-474c-ad73-301f34dd6b64 | YES (2026-05-27) | YES (2026-05-27) | YES (2026-05-27) | YES (2026-05-27) | YES (2026-05-27) | ASSIGNED (2026-05-27) | ASSIGNED (2026-05-27) | ASSIGNED (2026-05-27) | Fully onboarded via onboard-tenant.sh. NEW tenant (not the dormant GoDaddy one ddf3d2c9); quantumwms.com verified+primary; john@/sheila@ licensed. Intermedia->M365 migration in progress (Syncro #32323). | | Quantum Wealth Management | 2fd0092b-e9b7-474c-ad73-301f34dd6b64 | YES (2026-05-27) | YES (2026-05-27) | YES (2026-05-27) | YES (2026-05-27) | YES (2026-05-27) | ASSIGNED (2026-05-27) | ASSIGNED (2026-05-27) | ASSIGNED (2026-05-27) | Fully onboarded via onboard-tenant.sh. NEW tenant (not the dormant GoDaddy one ddf3d2c9); quantumwms.com verified+primary; john@/sheila@ licensed. Intermedia->M365 migration in progress (Syncro #32323). |
**Migration note:** Valleywide, Dataforth, and Cascades still use the old deprecated app. Next visit: consent Security Investigator + assign Exchange Administrator role to new SP, then retire old app consent. **Migration note (now URGENT):** Valleywide, Dataforth, and Cascades were on the old app (`fabb3421`), which was DELETED 2026-06-14 — they currently have NO working remediation access. Migrate each: consent Security Investigator (+ Exchange Operator if write is needed) and assign the Exchange Administrator role to the new SP in that tenant.
Keep this table updated when rolling out to new tenants or migrating existing ones. Run `onboard-tenant.sh` after each consent and update the role columns from the script's final status output. Keep this table updated when rolling out to new tenants or migrating existing ones. Run `onboard-tenant.sh` after each consent and update the role columns from the script's final status output.

View File

@@ -9,7 +9,7 @@ Format: `YYYY-MM-DD | MACHINE | command/skill | error (brief)`
<!-- Append entries below this line --> <!-- Append entries below this line -->
2026-06-14 | GURU-5070 | mailbox skill (Graph token) | FABB app `fabb3421` (Claude-MSP-Access / "Cloud MSP Access") token request returned AADSTS700016 — app/SP no longer present in azcomputerguru.com tenant (deleted; gotchas.md already marked it deprecated). Blocks /mailbox + the M365 contacts task. Verified the remediation suite (live, ACG tenant) carries NO Mail.Send/Mail.ReadWrite/Contacts scopes (investigator has Mail.Read only) — so a straight repoint can't restore mailbox-send/contacts. Pending Mike decision: stand up a single-tenant ACG-internal mailbox app vs. add scopes to a suite tier. 2026-06-14 | GURU-5070 | mailbox skill (Graph token) | FABB app `fabb3421` (Claude-MSP-Access / "Cloud MSP Access") token request returned AADSTS700016 — app/SP no longer present in azcomputerguru.com tenant (deleted; gotchas.md already marked it deprecated). Blocks /mailbox + the M365 contacts task. Verified the remediation suite (live, ACG tenant) carries NO Mail.Send/Mail.ReadWrite/Contacts scopes (investigator has Mail.Read only) — so a straight repoint can't restore mailbox-send/contacts. Pending Mike decision: stand up a single-tenant ACG-internal mailbox app vs. add scopes to a suite tier. [2026-06-15] Docs hardened — gotchas.md now marks fabb3421 DELETED with the Mail/Contacts-scope blast radius + flags the 3 legacy "old app only" tenants (Valleywide/Dataforth/Cascades) as now having NO working remediation app (migration URGENT); mailbox.md carries a BLOCKED/AADSTS700016 banner. DECISION 2026-06-15 (Mike): Mail.Send goes into the suite (Exchange Operator tier) since its real use is IR victim-notification during mailbox takeovers; add Mail.Send to the exchange-op manifest + consent, repoint mailbox.md to exchange-op. Implementation not yet executed (production app change, needs go).
2026-06-14 | GURU-KALI | coord skill (coord.py) | Documented invocation `py .claude/skills/coord/scripts/coord.py ...` failed exit 127 — `py` (the Windows py-launcher) does not exist on Linux. Worked around with `python3`. [RESOLVED 2026-06-14] Added `.claude/scripts/py.sh` (resolves the working interpreter: identity.json `python.command` -> py -> python3 -> python, skipping the MS Store shim) and repointed all skill/command DOC invocations from bare `py` to `bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh"`. The `.sh` skill scripts already resolved internally — left untouched. Broadcast to fleet. 2026-06-14 | GURU-KALI | coord skill (coord.py) | Documented invocation `py .claude/skills/coord/scripts/coord.py ...` failed exit 127 — `py` (the Windows py-launcher) does not exist on Linux. Worked around with `python3`. [RESOLVED 2026-06-14] Added `.claude/scripts/py.sh` (resolves the working interpreter: identity.json `python.command` -> py -> python3 -> python, skipping the MS Store shim) and repointed all skill/command DOC invocations from bare `py` to `bash "$CLAUDETOOLS_ROOT/.claude/scripts/py.sh"`. The `.sh` skill scripts already resolved internally — left untouched. Broadcast to fleet.

View File

@@ -10,6 +10,7 @@
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin /> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Barlow+Condensed:wght@500;600;700&family=Lexend:wght@300;400;500;600&family=JetBrains+Mono:wght@400;500;600;700&family=Fraunces:wght@400;500;600;700&family=Source+Sans+3:wght@400;500;600&family=IBM+Plex+Mono:wght@400;500;600;700&family=Anton&family=Hanken+Grotesk:wght@400;500;700&display=swap" rel="stylesheet" /> <link href="https://fonts.googleapis.com/css2?family=Barlow+Condensed:wght@500;600;700&family=Lexend:wght@300;400;500;600&family=JetBrains+Mono:wght@400;500;600;700&family=Fraunces:wght@400;500;600;700&family=Source+Sans+3:wght@400;500;600&family=IBM+Plex+Mono:wght@400;500;600;700&family=Anton&family=Hanken+Grotesk:wght@400;500;700&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="css/styles.css" /> <link rel="stylesheet" href="css/styles.css" />
<link rel="icon" href="assets/logo/acg-mark.svg" type="image/svg+xml" />
<script>(function(){try{var s=localStorage.getItem("acg-theme");var m=s||(window.matchMedia&&window.matchMedia("(prefers-color-scheme: dark)").matches?"dark":"light");document.documentElement.setAttribute("data-theme",m);document.documentElement.setAttribute("data-skin",(localStorage.getItem("acg-skin")||"ledger"));document.documentElement.classList.add("js");}catch(e){}})();</script> <script>(function(){try{var s=localStorage.getItem("acg-theme");var m=s||(window.matchMedia&&window.matchMedia("(prefers-color-scheme: dark)").matches?"dark":"light");document.documentElement.setAttribute("data-theme",m);document.documentElement.setAttribute("data-skin",(localStorage.getItem("acg-skin")||"ledger"));document.documentElement.classList.add("js");}catch(e){}})();</script>
</head> </head>
<body> <body>
@@ -17,7 +18,7 @@
<header class="site-header"> <header class="site-header">
<div class="wrap"> <div class="wrap">
<a class="brand" href="index.html" aria-label="Arizona Computer Guru home"> <a class="brand" href="index.html" aria-label="Arizona Computer Guru home">
<span class="brand__mark" aria-hidden="true">ACG</span> <span class="brand__mark" aria-hidden="true"><svg viewBox="0 0 100 104" fill="none" stroke="currentColor" stroke-width="11" stroke-linecap="round" stroke-linejoin="round"><g transform="rotate(20 50 54)"><path d="M61.2 26.2 A30 30 0 1 1 38.8 26.2"/><line x1="50" y1="14" x2="50" y2="50"/></g></svg></span>
<span><span class="brand__name">Arizona Computer Guru</span><br /><span class="brand__since">Concierge IT &middot; Tucson &middot; since 2001</span></span> <span><span class="brand__name">Arizona Computer Guru</span><br /><span class="brand__since">Concierge IT &middot; Tucson &middot; since 2001</span></span>
</a> </a>
<nav class="nav" aria-label="Primary"> <nav class="nav" aria-label="Primary">

View File

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 104" role="img" aria-label="Arizona Computer Guru">
<g transform="rotate(20 50 54)" fill="none" stroke="#F2922E" stroke-width="11" stroke-linecap="round" stroke-linejoin="round">
<path d="M61.2 26.2 A30 30 0 1 1 38.8 26.2" />
<line x1="50" y1="14" x2="50" y2="50" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 348 B

View File

@@ -10,6 +10,7 @@
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin /> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Barlow+Condensed:wght@500;600;700&family=Lexend:wght@300;400;500;600&family=JetBrains+Mono:wght@400;500;600;700&family=Fraunces:wght@400;500;600;700&family=Source+Sans+3:wght@400;500;600&family=IBM+Plex+Mono:wght@400;500;600;700&family=Anton&family=Hanken+Grotesk:wght@400;500;700&display=swap" rel="stylesheet" /> <link href="https://fonts.googleapis.com/css2?family=Barlow+Condensed:wght@500;600;700&family=Lexend:wght@300;400;500;600&family=JetBrains+Mono:wght@400;500;600;700&family=Fraunces:wght@400;500;600;700&family=Source+Sans+3:wght@400;500;600&family=IBM+Plex+Mono:wght@400;500;600;700&family=Anton&family=Hanken+Grotesk:wght@400;500;700&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="css/styles.css" /> <link rel="stylesheet" href="css/styles.css" />
<link rel="icon" href="assets/logo/acg-mark.svg" type="image/svg+xml" />
<script>(function(){try{var s=localStorage.getItem("acg-theme");var m=s||(window.matchMedia&&window.matchMedia("(prefers-color-scheme: dark)").matches?"dark":"light");document.documentElement.setAttribute("data-theme",m);document.documentElement.setAttribute("data-skin",(localStorage.getItem("acg-skin")||"ledger"));document.documentElement.classList.add("js");}catch(e){}})();</script> <script>(function(){try{var s=localStorage.getItem("acg-theme");var m=s||(window.matchMedia&&window.matchMedia("(prefers-color-scheme: dark)").matches?"dark":"light");document.documentElement.setAttribute("data-theme",m);document.documentElement.setAttribute("data-skin",(localStorage.getItem("acg-skin")||"ledger"));document.documentElement.classList.add("js");}catch(e){}})();</script>
</head> </head>
<body> <body>
@@ -17,7 +18,7 @@
<header class="site-header"> <header class="site-header">
<div class="wrap"> <div class="wrap">
<a class="brand" href="index.html" aria-label="Arizona Computer Guru home"> <a class="brand" href="index.html" aria-label="Arizona Computer Guru home">
<span class="brand__mark" aria-hidden="true">ACG</span> <span class="brand__mark" aria-hidden="true"><svg viewBox="0 0 100 104" fill="none" stroke="currentColor" stroke-width="11" stroke-linecap="round" stroke-linejoin="round"><g transform="rotate(20 50 54)"><path d="M61.2 26.2 A30 30 0 1 1 38.8 26.2"/><line x1="50" y1="14" x2="50" y2="50"/></g></svg></span>
<span><span class="brand__name">Arizona Computer Guru</span><br /><span class="brand__since">Concierge IT &middot; Tucson &middot; since 2001</span></span> <span><span class="brand__name">Arizona Computer Guru</span><br /><span class="brand__since">Concierge IT &middot; Tucson &middot; since 2001</span></span>
</a> </a>
<nav class="nav" aria-label="Primary"> <nav class="nav" aria-label="Primary">

View File

@@ -10,6 +10,7 @@
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin /> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Barlow+Condensed:wght@500;600;700&family=Lexend:wght@300;400;500;600&family=JetBrains+Mono:wght@400;500;600;700&family=Fraunces:wght@400;500;600;700&family=Source+Sans+3:wght@400;500;600&family=IBM+Plex+Mono:wght@400;500;600;700&family=Anton&family=Hanken+Grotesk:wght@400;500;700&display=swap" rel="stylesheet" /> <link href="https://fonts.googleapis.com/css2?family=Barlow+Condensed:wght@500;600;700&family=Lexend:wght@300;400;500;600&family=JetBrains+Mono:wght@400;500;600;700&family=Fraunces:wght@400;500;600;700&family=Source+Sans+3:wght@400;500;600&family=IBM+Plex+Mono:wght@400;500;600;700&family=Anton&family=Hanken+Grotesk:wght@400;500;700&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="css/styles.css" /> <link rel="stylesheet" href="css/styles.css" />
<link rel="icon" href="assets/logo/acg-mark.svg" type="image/svg+xml" />
<script>(function(){try{var s=localStorage.getItem("acg-theme");var m=s||(window.matchMedia&&window.matchMedia("(prefers-color-scheme: dark)").matches?"dark":"light");document.documentElement.setAttribute("data-theme",m);document.documentElement.setAttribute("data-skin",(localStorage.getItem("acg-skin")||"ledger"));document.documentElement.classList.add("js");}catch(e){}})();</script> <script>(function(){try{var s=localStorage.getItem("acg-theme");var m=s||(window.matchMedia&&window.matchMedia("(prefers-color-scheme: dark)").matches?"dark":"light");document.documentElement.setAttribute("data-theme",m);document.documentElement.setAttribute("data-skin",(localStorage.getItem("acg-skin")||"ledger"));document.documentElement.classList.add("js");}catch(e){}})();</script>
<script type="application/ld+json"> <script type="application/ld+json">
{"@context":"https://schema.org","@type":"LocalBusiness","name":"Arizona Computer Guru","image":"assets/images/contact.png","address":{"@type":"PostalAddress","streetAddress":"7437 E. 22nd St","addressLocality":"Tucson","addressRegion":"AZ","postalCode":"85710","addressCountry":"US"},"telephone":"+1-520-304-8300","email":"info@azcomputerguru.com","url":"https://azcomputerguru.com","openingHours":"Mo-Fr 09:00-17:00"} {"@context":"https://schema.org","@type":"LocalBusiness","name":"Arizona Computer Guru","image":"assets/images/contact.png","address":{"@type":"PostalAddress","streetAddress":"7437 E. 22nd St","addressLocality":"Tucson","addressRegion":"AZ","postalCode":"85710","addressCountry":"US"},"telephone":"+1-520-304-8300","email":"info@azcomputerguru.com","url":"https://azcomputerguru.com","openingHours":"Mo-Fr 09:00-17:00"}
@@ -20,7 +21,7 @@
<header class="site-header"> <header class="site-header">
<div class="wrap"> <div class="wrap">
<a class="brand" href="index.html" aria-label="Arizona Computer Guru home"> <a class="brand" href="index.html" aria-label="Arizona Computer Guru home">
<span class="brand__mark" aria-hidden="true">ACG</span> <span class="brand__mark" aria-hidden="true"><svg viewBox="0 0 100 104" fill="none" stroke="currentColor" stroke-width="11" stroke-linecap="round" stroke-linejoin="round"><g transform="rotate(20 50 54)"><path d="M61.2 26.2 A30 30 0 1 1 38.8 26.2"/><line x1="50" y1="14" x2="50" y2="50"/></g></svg></span>
<span><span class="brand__name">Arizona Computer Guru</span><br /><span class="brand__since">Concierge IT &middot; Tucson &middot; since 2001</span></span> <span><span class="brand__name">Arizona Computer Guru</span><br /><span class="brand__since">Concierge IT &middot; Tucson &middot; since 2001</span></span>
</a> </a>
<nav class="nav" aria-label="Primary"> <nav class="nav" aria-label="Primary">

View File

@@ -297,11 +297,11 @@ h3 { font-size: 1.5rem; font-weight: 600; }
.site-header .wrap { display: flex; align-items: center; gap: 1.5rem; .site-header .wrap { display: flex; align-items: center; gap: 1.5rem;
min-height: calc(var(--base) * 3); } min-height: calc(var(--base) * 3); }
.brand { display: flex; align-items: baseline; gap: 0.6rem; text-decoration: none; color: var(--ink); } .brand { display: flex; align-items: baseline; gap: 0.6rem; text-decoration: none; color: var(--ink); }
.brand__mark { /* CSS-drawn monogram, not an image icon */ .brand__mark { /* official ACG "G" askew-power-symbol mark; tints to the active skin accent */
font-family: var(--f-display); font-weight: 700; font-size: 1.05rem; display: inline-flex; align-items: center; align-self: center;
letter-spacing: 0.05em; color: var(--on-accent); background: var(--accent); color: var(--accent); line-height: 0;
padding: 0.15rem 0.5rem; border-radius: 2px; align-self: center;
} }
.brand__mark svg { width: 34px; height: 35px; }
.brand__name { font-family: var(--f-display); font-weight: 600; font-size: 1.35rem; .brand__name { font-family: var(--f-display); font-weight: 600; font-size: 1.35rem;
letter-spacing: 0.005em; } letter-spacing: 0.005em; }
.brand__since { font-family: var(--f-mono); font-size: 0.66rem; letter-spacing: 0.18em; .brand__since { font-family: var(--f-mono); font-size: 0.66rem; letter-spacing: 0.18em;

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

View File

@@ -10,6 +10,7 @@
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin /> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Barlow+Condensed:wght@500;600;700&family=Lexend:wght@300;400;500;600&family=JetBrains+Mono:wght@400;500;600;700&family=Fraunces:wght@400;500;600;700&family=Source+Sans+3:wght@400;500;600&family=IBM+Plex+Mono:wght@400;500;600;700&family=Anton&family=Hanken+Grotesk:wght@400;500;700&display=swap" rel="stylesheet" /> <link href="https://fonts.googleapis.com/css2?family=Barlow+Condensed:wght@500;600;700&family=Lexend:wght@300;400;500;600&family=JetBrains+Mono:wght@400;500;600;700&family=Fraunces:wght@400;500;600;700&family=Source+Sans+3:wght@400;500;600&family=IBM+Plex+Mono:wght@400;500;600;700&family=Anton&family=Hanken+Grotesk:wght@400;500;700&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="css/styles.css" /> <link rel="stylesheet" href="css/styles.css" />
<link rel="icon" href="assets/logo/acg-mark.svg" type="image/svg+xml" />
<script> <script>
(function () { try { (function () { try {
var s = localStorage.getItem("acg-theme"); var s = localStorage.getItem("acg-theme");
@@ -27,7 +28,7 @@
<header class="site-header"> <header class="site-header">
<div class="wrap"> <div class="wrap">
<a class="brand" href="index.html" aria-label="Arizona Computer Guru home"> <a class="brand" href="index.html" aria-label="Arizona Computer Guru home">
<span class="brand__mark" aria-hidden="true">ACG</span> <span class="brand__mark" aria-hidden="true"><svg viewBox="0 0 100 104" fill="none" stroke="currentColor" stroke-width="11" stroke-linecap="round" stroke-linejoin="round"><g transform="rotate(20 50 54)"><path d="M61.2 26.2 A30 30 0 1 1 38.8 26.2"/><line x1="50" y1="14" x2="50" y2="50"/></g></svg></span>
<span> <span>
<span class="brand__name">Arizona Computer Guru</span><br /> <span class="brand__name">Arizona Computer Guru</span><br />
<span class="brand__since">Concierge IT &middot; Tucson &middot; since 2001</span> <span class="brand__since">Concierge IT &middot; Tucson &middot; since 2001</span>

View File

@@ -10,6 +10,7 @@
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin /> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Barlow+Condensed:wght@500;600;700&family=Lexend:wght@300;400;500;600&family=JetBrains+Mono:wght@400;500;600;700&family=Fraunces:wght@400;500;600;700&family=Source+Sans+3:wght@400;500;600&family=IBM+Plex+Mono:wght@400;500;600;700&family=Anton&family=Hanken+Grotesk:wght@400;500;700&display=swap" rel="stylesheet" /> <link href="https://fonts.googleapis.com/css2?family=Barlow+Condensed:wght@500;600;700&family=Lexend:wght@300;400;500;600&family=JetBrains+Mono:wght@400;500;600;700&family=Fraunces:wght@400;500;600;700&family=Source+Sans+3:wght@400;500;600&family=IBM+Plex+Mono:wght@400;500;600;700&family=Anton&family=Hanken+Grotesk:wght@400;500;700&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="css/styles.css" /> <link rel="stylesheet" href="css/styles.css" />
<link rel="icon" href="assets/logo/acg-mark.svg" type="image/svg+xml" />
<script>(function(){try{var s=localStorage.getItem("acg-theme");var m=s||(window.matchMedia&&window.matchMedia("(prefers-color-scheme: dark)").matches?"dark":"light");document.documentElement.setAttribute("data-theme",m);document.documentElement.setAttribute("data-skin",(localStorage.getItem("acg-skin")||"ledger"));document.documentElement.classList.add("js");}catch(e){}})();</script> <script>(function(){try{var s=localStorage.getItem("acg-theme");var m=s||(window.matchMedia&&window.matchMedia("(prefers-color-scheme: dark)").matches?"dark":"light");document.documentElement.setAttribute("data-theme",m);document.documentElement.setAttribute("data-skin",(localStorage.getItem("acg-skin")||"ledger"));document.documentElement.classList.add("js");}catch(e){}})();</script>
</head> </head>
<body> <body>
@@ -17,7 +18,7 @@
<header class="site-header"> <header class="site-header">
<div class="wrap"> <div class="wrap">
<a class="brand" href="index.html" aria-label="Arizona Computer Guru home"> <a class="brand" href="index.html" aria-label="Arizona Computer Guru home">
<span class="brand__mark" aria-hidden="true">ACG</span> <span class="brand__mark" aria-hidden="true"><svg viewBox="0 0 100 104" fill="none" stroke="currentColor" stroke-width="11" stroke-linecap="round" stroke-linejoin="round"><g transform="rotate(20 50 54)"><path d="M61.2 26.2 A30 30 0 1 1 38.8 26.2"/><line x1="50" y1="14" x2="50" y2="50"/></g></svg></span>
<span><span class="brand__name">Arizona Computer Guru</span><br /><span class="brand__since">Concierge IT &middot; Tucson &middot; since 2001</span></span> <span><span class="brand__name">Arizona Computer Guru</span><br /><span class="brand__since">Concierge IT &middot; Tucson &middot; since 2001</span></span>
</a> </a>
<nav class="nav" aria-label="Primary"> <nav class="nav" aria-label="Primary">

View File

@@ -10,6 +10,7 @@
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin /> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Anton&family=Hanken+Grotesk:wght@400;500;700&family=JetBrains+Mono:wght@400;500;700&display=swap" rel="stylesheet" /> <link href="https://fonts.googleapis.com/css2?family=Anton&family=Hanken+Grotesk:wght@400;500;700&family=JetBrains+Mono:wght@400;500;700&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="css/radical.css" /> <link rel="stylesheet" href="css/radical.css" />
<link rel="icon" href="assets/logo/acg-mark.svg" type="image/svg+xml" />
</head> </head>
<body> <body>
<a href="#main" class="sr-only">Skip to content</a> <a href="#main" class="sr-only">Skip to content</a>

View File

@@ -10,6 +10,7 @@
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin /> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Barlow+Condensed:wght@500;600;700&family=Lexend:wght@300;400;500;600&family=JetBrains+Mono:wght@400;500;600;700&family=Fraunces:wght@400;500;600;700&family=Source+Sans+3:wght@400;500;600&family=IBM+Plex+Mono:wght@400;500;600;700&family=Anton&family=Hanken+Grotesk:wght@400;500;700&display=swap" rel="stylesheet" /> <link href="https://fonts.googleapis.com/css2?family=Barlow+Condensed:wght@500;600;700&family=Lexend:wght@300;400;500;600&family=JetBrains+Mono:wght@400;500;600;700&family=Fraunces:wght@400;500;600;700&family=Source+Sans+3:wght@400;500;600&family=IBM+Plex+Mono:wght@400;500;600;700&family=Anton&family=Hanken+Grotesk:wght@400;500;700&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="css/styles.css" /> <link rel="stylesheet" href="css/styles.css" />
<link rel="icon" href="assets/logo/acg-mark.svg" type="image/svg+xml" />
<script>(function(){try{var s=localStorage.getItem("acg-theme");var m=s||(window.matchMedia&&window.matchMedia("(prefers-color-scheme: dark)").matches?"dark":"light");document.documentElement.setAttribute("data-theme",m);document.documentElement.setAttribute("data-skin",(localStorage.getItem("acg-skin")||"ledger"));document.documentElement.classList.add("js");}catch(e){}})();</script> <script>(function(){try{var s=localStorage.getItem("acg-theme");var m=s||(window.matchMedia&&window.matchMedia("(prefers-color-scheme: dark)").matches?"dark":"light");document.documentElement.setAttribute("data-theme",m);document.documentElement.setAttribute("data-skin",(localStorage.getItem("acg-skin")||"ledger"));document.documentElement.classList.add("js");}catch(e){}})();</script>
</head> </head>
<body> <body>
@@ -17,7 +18,7 @@
<header class="site-header"> <header class="site-header">
<div class="wrap"> <div class="wrap">
<a class="brand" href="index.html" aria-label="Arizona Computer Guru home"> <a class="brand" href="index.html" aria-label="Arizona Computer Guru home">
<span class="brand__mark" aria-hidden="true">ACG</span> <span class="brand__mark" aria-hidden="true"><svg viewBox="0 0 100 104" fill="none" stroke="currentColor" stroke-width="11" stroke-linecap="round" stroke-linejoin="round"><g transform="rotate(20 50 54)"><path d="M61.2 26.2 A30 30 0 1 1 38.8 26.2"/><line x1="50" y1="14" x2="50" y2="50"/></g></svg></span>
<span><span class="brand__name">Arizona Computer Guru</span><br /><span class="brand__since">Concierge IT &middot; Tucson &middot; since 2001</span></span> <span><span class="brand__name">Arizona Computer Guru</span><br /><span class="brand__since">Concierge IT &middot; Tucson &middot; since 2001</span></span>
</a> </a>
<nav class="nav" aria-label="Primary"> <nav class="nav" aria-label="Primary">

View File

@@ -213,3 +213,53 @@ still computes $952/mo; `node --check js/app.js` clean.
State: multipage 4-skin switcher (Paper / Midnight / Verdigris / Bold) at :4328, all in State: multipage 4-skin switcher (Paper / Midnight / Verdigris / Bold) at :4328, all in
light+dark. Bold is the dialed-back-then-premium-softened radical. Open: Mike to confirm light+dark. Bold is the dialed-back-then-premium-softened radical. Open: Mike to confirm
Bold is the keeper, or pick among the four skins for the real azcomputerguru.com redesign. Bold is the keeper, or pick among the four skins for the real azcomputerguru.com redesign.
## Update: 09:41 PT (2026-06-15) — logo mark, errorlog skill fixes, Graphifyy eval
Three workstreams after the Bold-skin softening.
**1. Official ACG logo (partial).** Mike supplied the official wordmark ("Arizona ComputerGuru",
the "G" = an askew power symbol) and noted the short logo is just that stylized G. Hand-built the
short mark as a faithful SVG (`multipage/assets/logo/acg-mark.svg`, askew power symbol), wired it
as the header brand mark (tints to each skin's accent via currentColor) + favicon across all 6
multipage pages + radical.html. NOT done: the full wordmark — it's an exact brand asset with
custom type; can't save Mike's pasted raster from here and AI-recreation would be off-brand, so
the real file needs to be dropped into `multipage/assets/logo/` (color + a white/knockout for dark
skins). Open design choice for Mike: official fixed wordmark sitewide vs. keep the per-skin
adaptive lockup.
**2. errorlog review -> skill hardening.** 4 entries: #2 (py-on-Linux) + #3 ($CLAUDETOOLS_ROOT)
already resolved on this machine; #4 (sync.sh submodule abort) fixed upstream by the GURU-BEAST-ROG
coord broadcast (resolve_submodule_collisions; lands on next pull here). #1 (mailbox/FABB) was the
open one: app `fabb3421` is DELETED (AADSTS700016), and it held the only Mail.Send/ReadWrite/
Contacts scopes -> `/mailbox` + M365 contacts blocked, and the 3 legacy "old app only" tenants
(Valleywide/Dataforth/Cascades) now have NO working remediation app. Hardened the docs:
`gotchas.md` (DELETED + blast radius + URGENT migration), `mailbox.md` (BLOCKED/AADSTS700016
banner), `errorlog.md`. **Mike's decision:** Mail.Send belongs in the SUITE (real use = IR
victim-notification during box takeovers) -> add Mail.Send to the **Exchange Operator** tier;
implementation NOT executed (production multi-tenant app change, needs go + admin-consent). Also
surfaced 2 stale for-mike.md items: Intune Manager needs signInAudience->AzureADMultipleOrgs PATCH
(unblocks Cascades MDM); Mike's per-user Syncro key.
**3. Graphifyy vs GrepAI evaluation (`projects/graphifyy-eval/`).** Question: adopt Graphifyy
(getzep-style code+doc knowledge graph, `safishamsi/graphify`, PyPI `graphifyy`) for day-to-day?
Built a 3-arm protocol (A=GrepAI, B=Graphifyy, C=control) with a 10-query test set + rubric.
- Arm A (GrepAI): 18/20, ~3k ctx tokens/query, already indexed. 2 misses: phrasing-sensitivity
(C1) and stale-duplicate retrieval (D2 pulled superseded kittle-design).
- Arm B (Graphifyy): code extraction = AST (instant/free/local, but redundant with GrepAI).
Doc/non-code = generative LLM per chunk. Local ollama (apples-to-apples, since GrepAI uses
ollama embeddings) was IMPRACTICAL: qwen3:8b DNF 20 files/10min ("too small for JSON"),
codestral:22b got 1 of 2 chunks on 3 files/6min. Claude backend (vault gururmm anthropic key,
~$0.20 real spend) WORKED: 3 docs in 120s, but graphify reported ~$0.068/doc (recurring on an
active repo), and the decisive finding: `graphify query` returns the concept/relationship MAP,
NOT the content values (the prices were absent) -> you still Read the file for facts. GrepAI
returns content directly.
- **Verdict: keep GrepAI, do not adopt Graphifyy.** Cheap part duplicates GrepAI; unique part
(doc/relationship graph) is impractical local / costs recurring $ cloud / hands a map not facts.
Revisit only with GPU+fast local generative model, or a recurring architecture-relationship
need worth a metered ingest budget. Full writeup: `projects/graphifyy-eval/FINDINGS.md`.
- Cleanup done: GrepAI re-enabled in settings.local.json; graphifyy + openai uninstalled
(anthropic kept); scratch graphs deleted; PROTOCOL/results/FINDINGS retained.
Note: untracked discord-dm skill/command/scripts + 2 feedback memories present in the tree are
fleet additions (not this session's work); auto-sync sweeps them.

View File

@@ -0,0 +1,81 @@
# Graphifyy vs GrepAI — findings (GURU-5070, 2026-06-15)
## TL;DR
On this machine, **Graphifyy does not clear the bar for day-to-day adoption.** Its code-graph
is fast/free but largely redundant with GrepAI (already wired in); its one real differentiator
(a graph over docs/PDFs) requires LLM semantic extraction that is **impractically slow on the
apples-to-apples local-ollama config**, and making it usable would require a cloud LLM backend
= ongoing API cost, which negates the local/free premise. Recommend **do not adopt**; keep GrepAI.
## Arm A — GrepAI (baseline) [complete]
10/10 answered, **18/20** rubric. Medians: ~3,025 ctx tokens/query, 3 calls, ~55s. Already
indexed (no build step). Two notable retrieval misses: C1 (phrasing surfaced the old timeout
reaper, not the comms-durability fix) and D2 (returned the SUPERSEDED kittle-design copy, missed
the canonical June BEC report) — i.e. it trips on indexed stale duplicates. Cost: ~514k agent
tokens for the 10-query arm.
## Arm B — Graphifyy (local ollama) [BLOCKED at indexing]
Setup done: `pip install graphifyy` (v0.8.39) + `openai` dep; GrepAI disabled per-machine
(backed up). Backend = local ollama (apples-to-apples: GrepAI also uses ollama).
Architecture confirmed:
- **Code extraction = AST (tree-sitter), no LLM** — instant, free, local. Strong.
- **Query/path/explain = local BFS over graph.json with a token budget** — cheap at query time.
- **Doc/PDF/image extraction = generative LLM (JSON) per chunk via the chosen backend** — heavy.
Indexing measurements (the blocker):
- `msp-pricing` (2 code + 18 docs + 2 PDFs), `qwen3:8b`, --mode deep: **did NOT finish in 600s**;
graphify warned the 8B model is "too small for JSON instruction following" + VRAM/truncation.
- `msp-pricing/docs` (3 markdown files), `codestral:22b`, --token-budget 4096: got through
**chunk 1 of 2 in 360s** and did not finish. ~minutes per chunk.
- AST-only code extraction ran instantly in both runs.
### Why local ollama is the wrong workload for Graphifyy (key insight)
"Both use ollama" is true but the workloads differ fundamentally:
- **GrepAI/ollama = embeddings** (nomic-embed-text): one fast forward pass per chunk. Cheap.
- **Graphifyy/ollama = generative structured (JSON) extraction** per chunk: slow, needs a
large instruction-following model; small models fail JSON, large models are minutes/chunk.
So the doc-graph that is Graphifyy's only edge over GrepAI is gated behind an indexing cost that
is impractical locally on this hardware, and a cloud backend (gemini/claude/openai) would add
real per-ingest API cost + break the local/free + ollama-parity premise.
## Arm B addendum — Claude backend (cloud, breaks ollama-parity) [tested]
To see if a capable cloud backend makes the doc-graph viable: re-ran the SAME 3 `msp-pricing/docs`
files with `--backend claude` (key from vault `projects/gururmm/anthropic-api.sops.yaml`,
`anthropic` pip dep added).
- **Build: succeeded in 120s** (vs local-ollama DNF). 41 nodes, 68 edges, 9 communities.
- **Cost reported by graphify: 7,813 in / 12,008 out tokens, ~$0.20 for 3 small docs** (~$0.068/doc).
Extrapolated: the doc slice (wiki + msp-pricing + kittle + dataforth + gc docs) is ~hundreds of
files = ~$10-$30 initial; the full repo's docs (wiki + hundreds of client session-logs/reports)
= ~$50-$200+; plus steady re-ingest as docs change (SHA256 cache skips unchanged, so steady-state
= changed/new docs only — a constant trickle in an active repo). Code stays free (AST).
- **Query quality (the decisive finding):** `graphify query` is a local, free, 1s BFS over
graph.json. For "GPS pricing tiers and prices" it returned the **concept/relationship MAP**
(all tier + plan NODES, cross-doc concept links like "GPS Support Plans (Cross-Document
Concept)") in ~1,573 tokens — but **NOT the actual prices** ($19/$26/$39 absent; nodes carry
label + src file, not leaf values). To get the facts you still Read the source file. GrepAI
(Arm A D1) returned the file chunk WITH the prices and answered outright.
### What this means
- **Fact/content retrieval (the common day-to-day query):** GrepAI is more direct (returns
content). Graphifyy returns a map -> you still open the file. Extra hop.
- **Structural/relationship retrieval (architecture, impact, cross-doc concept links):**
Graphifyy's genuine edge, and the cross-document concept synthesis is nice — but it's the rarer
query, and overlaps GrepAI's RPG for code.
## Cost/benefit verdict (Mike's day-to-day)
- Day-to-day is mostly MSP ops + bursts of dev. The retrieval that helps is code (dev bursts) +
docs/knowledge (client history). GrepAI already serves code well (18/20) and is zero-setup.
- Graphifyy's code side ≈ redundant with GrepAI. Its doc side (the differentiator) can't be
cheaply/locally indexed here. Net marginal benefit is low; the standing index/maintenance cost
(and the second-system overhead) is real.
- **Recommendation: do not adopt fleet-wide; do not replace GrepAI.** Revisit only if (a) a fast
local generative model + GPU make doc-graph indexing cheap, or (b) the doc/PDF knowledge-graph
becomes a must-have and a metered cloud-backend ingest budget is acceptable.
## Reversal / cleanup
- Re-enable GrepAI: restore `enabledMcpjsonServers` (backup at
`projects/graphifyy-eval/settings.local.json.bak`) — needs session restart.
- Remove Graphifyy: `py -m pip uninstall -y graphifyy` (and `openai` if unwanted). Delete
`projects/graphifyy-eval/out/` scratch graphs.

View File

@@ -0,0 +1,106 @@
# Graphifyy vs GrepAI — evaluation protocol (GURU-5070)
Goal: real, comparable data on whether **Graphifyy** beats the incumbent **GrepAI** for
Mike's day-to-day in ClaudeTools, enough to make an adopt / skip / adopt-narrowly call.
Decision hinges on token efficiency + retrieval quality, weighed against maintenance cost.
## Tools under test
- **GrepAI** — `D:\claudetools\grepai.exe mcp-serve`, exposed as `mcp__grepai__*` (semantic
search + RPG graph: explore / trace_callers / trace_callees / trace_graph). Repo-wide index
already built (`.grepai/`). Enabled per-machine via `enabledMcpjsonServers:["grepai"]` in
`.claude/settings.local.json`.
- **Graphifyy** — `pip install graphifyy && graphify install`. Local graph (NetworkX +
tree-sitter + Leiden). CLI/skill: `graphify <path> [--mode deep|--update]`,
`graphify query "q"`, `graphify path "A" "B"`, `graphify explain "C"`. Docs/PDF/images
ingested via Claude API (token cost); code parsed locally.
## Arms (run in separate sessions; MCP toggles need a restart)
- **A — GrepAI** (baseline / "before"): grepai ON, Graphifyy not used. Run FIRST, this session.
- **B — Graphifyy** ("after"): Graphifyy ON, grepai DISABLED (removed from
`enabledMcpjsonServers`). New session.
- **C — Control** (optional): both off; only `grep`/`glob`/`Read`. Shows whether either graph
tool beats plain search.
Same model for all arms. Each query answered in a FRESH sub-agent constrained to that arm's
tools, to avoid cross-arm contamination. Scoring done against the rubric, blind to arm where
feasible.
## Fixed test corpus (both tools index the SAME slice)
To keep it fair and bounded (not the whole repo + node_modules):
- Code: `projects/msp-tools/guru-rmm/` (Rust server + agent + React dashboard)
- Docs: `wiki/`, `projects/msp-pricing/`, `clients/kittle/`, `clients/dataforth/`
- PDF: `projects/msp-pricing/marketing/The Arizona Business Owner's Guide to Choosing an MSP - Arizona Computer Guru.pdf`
Note asymmetry: GrepAI's existing index is repo-wide (slight recall edge, more noise);
Graphifyy indexes exactly this slice. All test queries are answerable from the slice.
## Metrics (per query x arm)
| Metric | How captured |
|---|---|
| `ctx_tokens` | chars of retrieved context the agent consumed / 4 (consistent approx) |
| `tool_calls` | number of retrieval round-trips to reach the answer |
| `latency_s` | wall-clock for the query |
| `score` | 0 = wrong/missing, 1 = partial, 2 = complete & correct (vs rubric) |
One-time / maintenance (measured once per tool):
- `index_build_s` — full index of the test corpus (code-only, then code+docs)
- `reindex_s` — incremental update after touching ONE file
- `ingest_api_tokens` — Graphifyy's Claude-API tokens to ingest docs/PDF/images
(GrepAI: note its embedding model/cost; LLM-ingestion ≈ 0)
## Test set (10 queries; code-heavy + docs-heavy, since docs is Graphifyy's claimed edge)
Each has a rubric = key facts a correct answer MUST contain.
CODE
- C1: "In GuruRMM, how does the server avoid false-failing commands that were delivered but not
acked? Name the mechanism + migrations." Rubric: agent CommandAck on receipt + dedup; reaper
RE-DELIVERS un-acked instead of false-failing; migrations 058 acked_at / 059 delivery_attempts.
- C2: "Trace where un-acked command re-delivery is handled in the RMM server and what calls it."
Rubric: the reaper fn + its caller path. (grepai trace_callers vs graphify path)
- C3: "Where is GuruRMM agent self-update with rollback implemented and what guards it?"
Rubric: agent `updater/mod.rs` + watchdog.
- C4: "What does GuruConnect SPEC-018 propose?" Rubric: session broker / capture worker as SYSTEM.
DOCS / KNOWLEDGE (Graphifyy's claimed strength)
- D1: "GPS pricing structure (tiers + prices)?" Rubric: Basic $19 / Pro $26 / Advanced $39 per
endpoint; support plans Essential/Standard/Premium/Priority.
- D2: "Summarize the Kittle BEC/ACH-fraud incident and root cause." Rubric: Ken+marco+Accounting
compromised; fraudulent bank-change to City of Tucson + Marana ($130K+ prevented); IC3 filed;
root cause = April credential theft + incomplete remediation (password never reset, ~2mo).
- D3: "Which ACG clients had M365 breach/credential incidents in 2026 and each root cause?"
Rubric (relationship query): Kittle (BEC), Dataforth (2026-03-27 phishing -> MFA), mvaninc
(unauthorized sign-in OKC). Partial credit per client.
- D4: "List the 7 red flags of a bad MSP from the Buyers Guide." Rubric: the 7 from
MSP-Buyers-Guide-Content.md (unlimited-support, high-pressure sales, offshore-only, no
proactive monitoring, long lock-ins, one-size packages, no local presence). PDF/doc ingestion.
- D5: "Canonical Kittle article path + what it superseded?" Rubric: clients/kittle.md canonical;
kittle-design.md superseded 2026-06-09.
MIXED (code + docs)
- M1: "How do new GuruRMM builds get promoted from beta to stable?" Rubric: builds tag beta;
promote via POST /api/updates/rollouts/:version/promote; build-server.sh auto-deploys.
## Procedure
1. (Arm A, now) For each query, spawn a sub-agent: tools = grepai + Read only; instruct it to
use ONLY grepai for retrieval, answer, and report (answer, total retrieved chars, # grepai
calls, elapsed). Log to results.csv with arm=A.
2. Score each answer 0/1/2 vs rubric.
3. Disable GrepAI (below), install + index Graphifyy, measure one-time costs.
4. (Arm B, new session) Same queries, sub-agent tools = Bash(graphify) + Read; use ONLY
graphify for retrieval. Log arm=B. Score.
5. (Arm C, optional) grep/glob/Read only. Log arm=C.
6. Analyze: per-metric medians by arm; weight ctx_tokens + score (the day-to-day levers);
factor in index/maintenance cost and the doc-vs-code split.
## Reversible environment changes (per-machine only)
Disable GrepAI (edit `.claude/settings.local.json`, remove "grepai" from
`enabledMcpjsonServers`; restart session). Re-enable = add it back. **Do NOT edit `.mcp.json`**
(shared/fleet). Install Graphifyy: `py -m pip install graphifyy && graphify install`. Uninstall
= `py -m pip uninstall graphifyy` + remove its skill. Snapshot of `settings.local.json` kept at
`projects/graphifyy-eval/settings.local.json.bak` before any edit.
## Open setup unknowns to resolve at install
- Which API key/env var Graphifyy uses for doc/PDF/image ingestion (README didn't say; it bills
as "a Claude Code skill"). Confirm before indexing docs so ingest cost is attributable.
- Whether `graphify query` itself spends LLM tokens to answer (vs returning raw graph context) —
affects per-query cost comparison; measure.

View File

@@ -0,0 +1,21 @@
query,arm,ctx_tokens,grepai_or_graphify_calls,files_read,latency_s,score,notes
C1,A,8500,6,0,75,1,"described OLD timeout reaper (mig 014/043) + interrupt-on-reconnect; MISSED the comms-durability CommandAck/dedup + re-deliver + migrations 058/059 (the actual fix). query-phrasing steered to wrong mechanism"
C2,A,6000,3,2,76,2,"nailed it: fail_timed_out_commands (db/commands.rs:337) w/ acked_at/delivery_attempts gating + re-deliver via get_pending_commands (ws/mod.rs); caller=main.rs tokio task. (1 stray grep for a line number)"
C3,A,3100,2,1,63,2,"updater/mod.rs AgentUpdater full flow + rollback watchdog + guards. complete"
C4,A,1800,1,1,37,2,"SPEC-018 SYSTEM service host + session broker, capture workers as SYSTEM. complete"
D1,A,2750,1,0,23,2,"GPS Basic $19/Pro $26/Adv $39 + 4 support plans + equip pack. complete, 1 call"
D2,A,2412,1,0,33,1,"retrieved the SUPERSEDED clients/kittle-design/ April breach-check (Alexis/Ken inbox rules); MISSED the canonical June BEC/ACH-fraud event ($130K to City of Tucson/Marana prevented, IC3 filed). stale-duplicate retrieval"
D3,A,12250,4,0,74,2,"open-ended relationship query; comprehensive + well-sourced (Valley Wide confirmed; Cascades/Bardach/Barbara blocked; Kittle unconfirmed). NOTE: my rubric was inaccurate - weak gold query"
D4,A,2185,1,0,39,2,"all 7 red flags correct from MSP-Buyers-Guide-Content.md. (1 stray grep for titles)"
D5,A,2950,3,0,56,2,"wiki/clients/kittle.md canonical, superseded kittle-design.md 2026-06-09. correct. (1 stray grep)"
M1,A,7625,3,0,54,2,"beta-first + POST /api/updates/rollouts/:version/promote + .channel sidecars + dashboard promote. complete"
C1,B,,,,,,
C2,B,,,,,,
C3,B,,,,,,
C4,B,,,,,,
D1,B,,,,,,
D2,B,,,,,,
D3,B,,,,,,
D4,B,,,,,,
D5,B,,,,,,
M1,B,,,,,,
1 query arm ctx_tokens grepai_or_graphify_calls files_read latency_s score notes
2 C1 A 8500 6 0 75 1 described OLD timeout reaper (mig 014/043) + interrupt-on-reconnect; MISSED the comms-durability CommandAck/dedup + re-deliver + migrations 058/059 (the actual fix). query-phrasing steered to wrong mechanism
3 C2 A 6000 3 2 76 2 nailed it: fail_timed_out_commands (db/commands.rs:337) w/ acked_at/delivery_attempts gating + re-deliver via get_pending_commands (ws/mod.rs); caller=main.rs tokio task. (1 stray grep for a line number)
4 C3 A 3100 2 1 63 2 updater/mod.rs AgentUpdater full flow + rollback watchdog + guards. complete
5 C4 A 1800 1 1 37 2 SPEC-018 SYSTEM service host + session broker, capture workers as SYSTEM. complete
6 D1 A 2750 1 0 23 2 GPS Basic $19/Pro $26/Adv $39 + 4 support plans + equip pack. complete, 1 call
7 D2 A 2412 1 0 33 1 retrieved the SUPERSEDED clients/kittle-design/ April breach-check (Alexis/Ken inbox rules); MISSED the canonical June BEC/ACH-fraud event ($130K to City of Tucson/Marana prevented, IC3 filed). stale-duplicate retrieval
8 D3 A 12250 4 0 74 2 open-ended relationship query; comprehensive + well-sourced (Valley Wide confirmed; Cascades/Bardach/Barbara blocked; Kittle unconfirmed). NOTE: my rubric was inaccurate - weak gold query
9 D4 A 2185 1 0 39 2 all 7 red flags correct from MSP-Buyers-Guide-Content.md. (1 stray grep for titles)
10 D5 A 2950 3 0 56 2 wiki/clients/kittle.md canonical, superseded kittle-design.md 2026-06-09. correct. (1 stray grep)
11 M1 A 7625 3 0 54 2 beta-first + POST /api/updates/rollouts/:version/promote + .channel sidecars + dashboard promote. complete
12 C1 B
13 C2 B
14 C3 B
15 C4 B
16 D1 B
17 D2 B
18 D3 B
19 D4 B
20 D5 B
21 M1 B