deploy-agent proven end-to-end on RMM-TEST-MACHINE: vaulted Howard Test MSI URL -> GuruRMM
push (msiexec /qn, SYSTEM) -> Syncro service Running -> auto-enrolled as asset 12676769 under
the client + the policy folder embedded in the installer token (5092561) in ~1 min.
installed_applications populates fast; patches lag. POST /policy_folders {policy_folder}
requires parent_id (null->422); move-asset flip-restore verified on the real asset; DELETE
policy_folder works. Shapes + findings recorded.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Live-tested the remaining write resources on Howard Test. Recorded:
- wrapped response shapes (.lead.id/.vendor.id/.product.id/.purchase_order.id/
.po_line_item.id/.worksheet_result.id) added to Verified Response Shapes.
- create_po_line_item requires product maintain_stock:true.
- worksheet templates live in GET /tickets/settings (.worksheet_templates[]), no standalone
endpoint; need a valid worksheet_template_id.
- PUT /products requires name+description; leads/vendors/products/POs have NO DELETE (global
test records persist -> GUI cleanup).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Decoded the Syncro RMM installer URL from 4 client samples:
base64(v1-{customer_id}-{A}-48753-{policy_folder_id}). 48753 = our account id (constant);
customer_id + policy_folder_id are API-readable; A is an unguessable per-client security token
NOT exposed anywhere in the API -> the URL is NOT constructible and must be harvested per client
and vaulted. Installer is an MSI -> deploy uses msiexec /qn /norestart.
deploy-agent now reads clients/<slug>/syncro-agent-installer (credentials.installer_url) from
the vault (cascades-tucson, grabb-durando, reliant, howard-test seeded) and pushes via GuruRMM.
Findings recorded in test-findings.md.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Test campaign on Howard Test sandbox (customer 36118743) surfaced traps the skill must not
re-learn:
- invoice/estimate email endpoints return 200 "Email sent" even with NO recipient
(customer.email null) -> never report delivery from the 200; verify customer.email first.
- customer.email is UNIQUE tenant-wide; PUT /customers failure returns
{success:false,message:[...]} not {customer:{...}} -> check .success.
- POST/PUT /contacts return a FLAT object (.id), not {contact:{...}}; contact recipient
flags (primary/receives_invoices) are ignored via API.
- POST /contracts write succeeds but body doesn't echo the object; /contracts?customer_id
doesn't filter server-side -> GET-verify + client-filter.
New living log .claude/standards/syncro/test-findings.md; critical items folded into
syncro.md Hard Rules + Verified Response Shapes. Correction logged to errorlog.md.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add comprehensive Syncro coverage beyond PSA core:
- New .claude/standards/syncro/api-reference.md: complete verified inventory of ~180
endpoints across 38 resource types (generated from live OpenAPI 3.0 spec + tenant
probe 2026-07-10), with worked GET/POST/PUT/DELETE templates and token-capability matrix.
- /syncro: asset read intelligence (patches, installed_applications), asset create/update,
policy-folder move (move-asset), RMM alerts, and deploy-agent (hybrid installer push via
GuruRMM using SyncroSetup --console --allow-force-reboot).
- move-asset ships a capability preflight (GET /policy_folders?customer_id -> 401 = missing
Assets-Policy-Change) + mandatory post-write verify, because an under-scoped token returns
HTTP 200 and silently no-ops the move.
Correct the "Syncro RMM is API-impossible" belief: it was a token-scope gap, not an API
limit. Live-verified the asset move (flip-and-restore 692253->692278->692253). Token scope
today: Howard + Winter full; Mike (vaulted ...ebbeb3) still 401 pending re-vault.
Corrects memory reference-syncro-rmm-api-gui-only; correction logged to errorlog.md.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Two defects found while running /wiki-compile, both from tooling assuming
a path instead of resolving it.
1. Plaintext Syncro API keys. Mike's and Howard's live PSA keys were
copy-pasted into three command files, a script, and two catalog docs --
despite both already being vaulted at msp-tools/syncro and
msp-tools/syncro-howard. Replaced with reads from the SOPS vault via a
new sourced helper, .claude/scripts/syncro-env.sh. Write paths fail
closed; read-only paths degrade to skipped enrichment rather than a
wrong key. Per-user mapping is unchanged, so Syncro attribution is too.
2. Hardcoded repo root. wiki-compile/wiki-lint/inject-standards and
gen_b64.py hardcoded D:/claudetools; this machine is C:/claudetools.
syncro.md also read ~/.claude/identity.json before the repo copy -- the
same bug that made remediation-tool's consent-audit report a fully
consented tenant as RED. Root now resolves from the script's own
location, with identity.json claudetools_root as the override.
get-identity.sh had a chicken-and-egg bug: it read ${CLAUDETOOLS_ROOT:-.}
but never set it, so every caller had to already know the root. It now
self-resolves and exports CLAUDETOOLS_ROOT + VAULT_ROOT.
Verified: all three command setup blocks authenticate against live Syncro;
get-identity.sh works from any cwd and honors a pre-set root; gps-rmm
autoenroll resolves its key from the vault. security-review: no findings.
NOTE: both keys remain valid in git history. Rotation in the Syncro portal
is the required follow-up -- this commit does not resolve that exposure.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Extends the invoice Message (note) automation into a single reusable helper
set_invoice_note <invoice_id> <customer_id> [pre_billing_prepay]:
- no block (prepay_hours==0) -> "Interested in discounted labor? Ask us about block-rate pricing."
- block, >=4 hrs left -> "Block hours remaining: N."
- block, <4 hrs left -> remaining + renew line, AND tags Winter (<@624666486362996755>)
in #bot-alerts (low-block heads-up; mentions ping, no allowed_mentions)
Pre-billing prepay arg keeps a just-depleted block counted as a block customer (shows renew, not upsell).
Never clobbers a non-empty note.
Wired into billing Step 3 (set_invoice_note "$INVOICE_ID" "$CUST_ID" "$PREPAY"), and a new
"Recurring invoice note sweep" applies the same policy to Syncro's auto-generated recurring invoices
(schedule_id != null, recent, current balance) — idempotent, run after each recurring run.
Branch logic + a real e2e note set/restore validated on the ACG internal test account (#67741); the
<4hr Winter alert was stubbed in testing so no real ping fired.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The on-screen "Invoice Message" text block IS the invoice `note` field, editable via
PUT /invoices/{id} {"note": "..."} (response {"invoice": {...}}). Verified on the ACG
internal test account (#67741: set/verify/restore).
Billing flow now sets a one-line upsell hint on the invoice note — "Interested in
discounted labor? Ask us about block-rate pricing." — ONLY for customers with no prepaid
block (prepay_hours == 0). Block customers (prepay_hours > 0) get no hint; never clobber
a non-empty note.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Task 3/3a of the harness-optimization spec. Mike confirmed normal billing uses
add_line_item; timers stay available only for explicit outlier requests, never the
normal loop. Rewrote time-entry-protocol.md to defer to the /syncro command (SSOT for
billing mechanics) and state timers are outlier-only; aligned the command's two
absolute "no timers" lines. Contradiction removed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Any ticket, estimate, appointment, or schedule created for testing or API
research must have its subject/name prefixed with [TEST]. Added as a Hard
Rule and cross-referenced in the recurring schedules section.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
"Emergency" is a billing modifier, not a delivery channel. Added explicit
hard rule that Remote/Onsite/In-Shop must be confirmed separately when billing
emergency — the delivery channel determines price_retail and cannot be guessed.
Updated both the Hard Rules section and the Billing workflow Step 1 gather prompt.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Phase 2 migration complete:
sync.sh:
- Read Python command from identity.json first (.python.command)
- Fall back to auto-detection for legacy machines
- Eliminates per-session detection overhead
syncro.md:
- Read Ollama endpoint from identity.json (.ollama.endpoint // .ollama.fallback)
- Read Python command from identity.json (.python.command)
- Both sections have legacy fallbacks with detection
- Eliminates 2-second curl probe on every write operation
- Updated day-of-week verification code example
- Updated Ollama draft call section
Impact: All scripts now read machine-specific config from identity.json
(populated by migrate-identity.sh). Faster, explicit, offline-safe.
- Updated sync.sh to read claudetools_root from identity.json
- Updated syncro.md skill to use identity.json for repo path
- Updated CLAUDE.md onboarding to include claudetools_root field
- Eliminates cross-architecture path detection issues
- Fallback to git rev-parse for legacy machines
Each machine sets claudetools_root during onboarding, just like vault_path.
The linked ticket subject must start with "Estimate - " before an estimate
task is considered complete. Added as criterion [4] in both the hard rules
block and the workflow success criteria block, with a self-correction step
(PUT /tickets/{id}) if the check fails.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Change the linked ticket subject format from "Estimate: <subject>" to
"Estimate - <subject>" per Winter's request for consistent formatting.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Per Winter: leave appointment type blank unless user names one of the known
types. If omitting, include delivery method in ticket subject for calendar
visibility. Applies to both the gather-inputs table and the appointment POST.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Only set contact_id when ticket is opened by/regarding a named contact.
Removed address_id, appointment_owner, and do_not_invite fields from the
default gather step — these are edge cases, not routine inputs.
Updated preview template to reflect default primary contact behavior.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Billing now uses add_line_item directly; timer_entry/charge_timer_entry removed
- Added Verified Response Shapes table for all endpoints (tested live against ACG internal customer)
- Billing workflow rewritten as strict 5-step locked script with no branches
- Added STOP rule: never try alternative endpoints/formats on unexpected responses
- bot-alerts section: explicit success ([OK] + message_id) and failure ([WARNING]) criteria
- Updated feedback memory to supersede the old timer-first rule
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add .claude/scripts/post-bot-alert.sh — reusable, soft-failing Discord
poster that reads the bot token from the SOPS vault (bot-token.sops.yaml,
credentials.bot_token) with a .env fallback, so it works from any machine.
Wire it into the /syncro skill: a Hard Rules pointer, a billing-workflow
step (17), and a "Post to #bot-alerts" reference section with the message
format and ticket/invoice/customer link mapping (computerguru.syncromsp.com).
Scoped to write ops (create/update/close/comment/bill/customer); reads post
nothing. Best-effort — never fails the Syncro write it follows.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Add hard rule: 9269129 (Prepaid Project Labor) is Exempt and does NOT deduct
from prepay_hours block — never use for normal work (verified 2026-05-04)
- Expand prepay_hours check from emergency-only to ALL billing workflows
- Fix emergency/prepaid branching table to use delivery-channel product instead
of hardcoding 26118 (Onsite) for remote and other labor types
- Clarify invoice step 15: $0.00 invoice total is correct for prepaid customers;
verify by checking customer.prepay_hours dropped by quantity
- Field 7 (Assigned Tech): add explicit default to API key owner; mark as MUST
always be included in POST payload to prevent null user_id on ticket create
- Add billing workflow hard rule: read prepay_hours before any billing, not just
emergency, so prepaid invoice behavior is known before execution begins
Triggered by ticket #32265 (Russo Law Firm) missing assignee/priority/billing.
Russo Law has 12.5 prepaid hrs — 0.5 hrs correctly deducted via invoice #67578.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Promote timer_entry → charge_timer_entry to default billing path; demote
bare add_line_item to a clearly-labeled fallback for non-time items only.
Mike caught the bare-add_line_item bug across 31 tickets on 2026-04-30;
repeated on 3 tickets 2026-05-01. Time entries are required for Syncro
reporting (hours per client, tech productivity, prepay burn).
- Replace /tmp/*.json payload pattern with heredoc throughout. /tmp resolves
to C:\tmp\ in the Write tool but %LOCALAPPDATA%\Temp\ in Git Bash on
Windows — different real directories. Caused a wrong-comment incident on
ticket #32225 2026-05-01 (rogue payload from prior session). Heredoc
avoids the file handoff entirely.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Write operations (bill, comment, create) now send a prompt to Ollama
(qwen3:14b) for comment body and billing description drafting. Claude
reviews the output against the rate/prepaid/formatting checklist before
presenting the preview. If neither Ollama endpoint is reachable, Claude
drafts directly — same review and confirmation flow either way.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add local rate table (pulled 2026-04-24) for all 7 labor products; always
set price_retail explicitly — Syncro API does not auto-apply product rates
- Replace vault-based key fetch with inline case block on identity.json user;
both Mike and Howard keys included for correct per-user attribution
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Documents the 3-call create pattern (ticket → Initial Issue comment →
appointment), adds problem type and appointment type dropdowns with IDs,
fixes priority format to number-prefixed strings ("2 Normal"), adds Howard
to tech user ID table, and adds asset/contact lookup steps.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Syncro auto-calculates price from the product's configured rate — omit price_retail.
Cleared Howard's messages from for-mike.md (both items addressed).
Left reply for Howard in for-howard.md confirming fix is live.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add .claude/scripts/vault.sh wrapper (reads vault_path from identity.json)
- get-token.sh + patch-tenant-admin-manifest.sh read identity.json for vault root
- syncro.md uses wrapper via CLAUDETOOLS_ROOT
- CLAUDE.md + ONBOARDING.md document the pattern and prompt for vault_path on onboarding
- identity.json now includes vault_path (D:/vault on DESKTOP-0O8A1RL)
Howard and Mac need vault_path added to their identity.json after pulling.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>