Kelly Tickets
Overview
Use this skill as Kelly's complaint triage-and-dispatch desk. Complaints and requests arrive scattered across WeChat group exports, phone-call logs, front-desk forms, and email — reading those local exports is a genuine external operation a browser cannot perform, so scripts/ingest_intake.mjs is the only place a complaint enters the system. The agent classifies each ticket (category, urgency, unit/location) and proposes a dispatch (crew, priority, SLA) via scripts/apply_triage.mjs; the human reviews dispatch proposals in a Busabase-backed App-in-Skill review queue and approves or edits; a board tracks every ticket to resolution with an auditable history trail. scripts/execute_decisions.mjs prints the crew-notification plan for approved dispatches — the AirApp itself never sends a message or mutates a remote system. The demo persona is residential property management, but the same flow fits facilities, IT helpdesk, or any dispatch workflow.
Default behavior is AirApp-first. Unless the user explicitly asks only for explanation, ingest/triage what's due and give the user the clickable AirApp URL (or the local preview URL when local preview is explicitly requested). Use chat-only mode only when the user says "纯聊天", "chat only", "不要打开 UI", or similar; then present numbered proposals (Dispatch #1) directly in the conversation.
App UI Screenshots
Mandatory Dependencies
- Read and follow
$kelly-app-skill-creator for product behavior, visual quality, responsive layout, and the complete canonical content/kelly-tickets-app/ artifact.
- Read and follow
$busabase for connection, target Space, node discovery, ChangeRequests, review, and merge behavior.
- Read and follow
$busabase-app-creator for resource modeling, AirApp runtime limits, security, validation, and deployment.
If a dependency is unavailable, preserve this skill's local artifact and product contracts, stop before the unavailable Busabase operation, and report the exact missing dependency. Do not invent a second data backend.
Boundary
- Intake parsing is local:
scripts/ingest_intake.mjs reads WeChat exports, call logs, and mailbox dumps the user provides and writes normalized rows to Busabase. It never fetches complaints from remote systems on its own.
- The AirApp reads and writes Busabase records only. It never sends messages, calls crews, replies to residents, or mutates remote systems.
- Any outbound crew notification or resident reply is approval-required through the dispatch queue and executed by the agent OUTSIDE the app via other skills (messenger/email/WeChat) after
scripts/execute_decisions.mjs produces the plan. scripts/execute_decisions.mjs never performs these operations itself — it only writes an execution marker onto the proposal.
- Crew contacts are env-var references (
contact_env) only — never store phone numbers or webhook URLs in Busabase. Resident PII stays masked in the UI (contact_masked); scripts/ingest_intake.mjs re-masks long digit runs defensively.
- Treat all complaint/resident data as sensitive. Never commit local export files or env files.
Busabase Resources
Six Bases under one application Folder (kelly-tickets), declared in content/kelly-tickets-app/app/js/config.js and the generated template sidecars under content/:
crews: crews that tickets can be dispatched to (name, skills, contact_env).
intake: raw complaints/requests as they arrived on a channel, before or after triage, plus the human decision (convert to ticket / ignore) on the same row.
tickets: tickets tracked from classification through resolution, with an append-only history timeline.
proposals: the dispatch review queue — proposed crew/priority/SLA, the human decision, and the execution marker on the same row.
sync-log: append-only history of ingest/triage/execute runs.
settings: one row (record-id: "config") with property profile, channels, categories, and SLA rules.
Resources provision lazily through an idempotent Busabase ChangeRequest the first time the app runs in a Space; see references/tickets-schema.md for exact field shapes. SLA state and crew load are recomputed client-side from the stored rows on every read (content/kelly-tickets-app/app/js/tickets-model.js's buildSnapshot), so the desk is always fresh regardless of when a browser session loads it relative to the last ingest/triage run. The browser has no access to the agent's environment variables, so the Settings view shows each crew's contact_env name only, not a live readiness boolean.
First Run And Onboarding
On invocation, check the intake/tickets Bases. If both are empty, guide setup before ingesting real complaints: ask, turn by turn, property profile (name, buildings, timezone), complaint categories, crews (name, skills, and which env var holds each crew's contact), SLA rules per category+urgency, and which intake channels are in use. Never ask the user to paste contact values or secrets into chat; they belong only in local env files, referenced by contact_env names. Write the answers onto the Settings row and the Crews Base, then ingest:
node skills/kelly-tickets/scripts/ingest_intake.mjs /path/payload.json --apply
Local App
Default behavior is AirApp-first — give the user the clickable AirApp URL. Start pnpm --dir content/kelly-tickets-app dev only when local preview/debugging is explicitly requested.
Required app views (hash routes):
#/overview: dispatch command desk — human-attention panel (proposals to approve, unclassified intake, SLA-breaching tickets), KPI cards (open tickets, avg resolution, SLA at risk, this week's intake with channel badges), category distribution bars, and crew load.
#/intake and #/intake/<id>: raw intake stream — channel badge (WeChat/phone/form/email/walk-in), reporter, unit/location, complaint text, urgency guess, triage state. Detail shows the full text, attachments note, editable classification (category/urgency/unit), and convert-to-ticket or ignore actions.
#/dispatch: the review queue with workflow states needs_review / changes_requested / approved / done / blocked. Each card shows the stable ref (Dispatch #1), ticket summary, proposed crew/assignee, priority, SLA target, the reason, an editable note to the crew, and approve / request changes / block buttons. Decisions write directly onto the proposal record through busabase-sdk.
#/board and #/board/<ticket_id>: tickets grouped by status (open / assigned / in_progress / waiting / resolved) with category badge, unit, crew, age, and color-coded SLA indicator. Detail shows the full history timeline (intake → classification → dispatch → crew updates → resolution), masked reporter contact, and a resolution note field.
#/settings: sanitized config — property profile, channels, categories, crews with contact_env names, SLA rules, data provider, and onboarding state. Never expose contact values or secrets.
Demo mode:
?demo=overview, ?demo=intake, ?demo=dispatch, ?demo=board, and ?demo=detail (a ticket history) select named mock scenes.
lang=en or lang=zh forces UI chrome language; with lang=zh the demo content itself (property name, complaints, crews, reasons) is meaningfully localized for Chinese screenshots.
- Deep links such as
/?demo=dispatch&lang=zh#/dispatch must work.
- Demo mode never reads or writes Busabase. Decision buttons still work but act on in-memory state only.
UI language: English and Chinese chrome with Auto default (browser language), plus an explicit selector persisted locally. Keep real resident/complaint data in its original language.
Intake Workflow
- Ask the user for the export/log location (WeChat group export, call log CSV/notes, front-desk forms, mailbox). Raw files stay outside git.
- Parse the raw material into the ingest payload shape (see
references/tickets-schema.md): one item per complaint — channel, channel-native external_id when available, reporter, contact (will be masked), unit/location, verbatim text, received time, plus first-pass category_guess/urgency_guess.
- Write the payload JSON to a temp path and run
node scripts/ingest_intake.mjs <payload.json> --apply. The script validates, dedupes by channel + external_id (falling back to a content hash), masks contacts, writes new rows into the Intake Base, and appends a Sync Log entry.
- Re-ingesting the same export is safe: duplicates are skipped and reported. Without
--apply it is a dry run.
Triage And Dispatch Workflow
- Classification is LLM work: read
intake items in new/classified state (and any with decision_action: "convert_to_ticket", the Busabase-only equivalent of a queued agent task), decide category, urgency, unit/location, and a ticket title; decide which crew fits and why (use crew skills, prior tickets for the same unit, and SLA pressure in the reason).
- Merge deterministically with
node scripts/apply_triage.mjs <payload.json> --apply: creates tickets (T-1001-style ids), computes sla_due_at from the Settings row's sla_rules (category+urgency, * wildcard, sla_default_hours fallback), assigns stable proposal refs, appends ticket history, and writes it all straight to Busabase. ticket_updates[] in the same payload records crew progress, status transitions, and resolutions.
- Send the user to
#/dispatch to review. Decisions write directly onto the proposal record; from a standalone local preview the write merges immediately (trusted operator), from the deployed AirApp it creates a pending ChangeRequest for the trusted process to merge.
- Poll the
intake Base for rows with decision_action: "convert_to_ticket" where triage_state is not yet ticketed: classify using the human-provided decision_fields and create the ticket via apply_triage.mjs. Poll the proposals Base for status: "changes_requested": redraft the proposal per decision_note and re-run apply_triage.mjs.
- After approvals, run
node scripts/execute_decisions.mjs (dry-run) and show the plan; with user confirmation run --apply, then perform the real crew notifications via other skills (messenger/email), record outcomes back through apply_triage.mjs's ticket_updates[], and keep the board honest.
- Re-read the
proposals Base immediately before executing, and never execute items without an approved status.
Board Semantics
open: classified, no crew accepted yet (dispatch pending or unapproved).
assigned: an approved dispatch reached a crew; nobody started work yet.
in_progress: the crew reported starting work.
waiting: blocked on residents, parts, weather, or vendors — not on the crew.
resolved: work confirmed done; resolved_at set and a resolution note recorded.
SLA states are derived, never hand-set: ok, at_risk (under 25% of the SLA window left), breached, and met for resolved tickets — computed fresh on every read by content/kelly-tickets-app/app/js/tickets-model.js's computeSlaState. History events are append-only — the timeline is the audit trail; never rewrite past events.
Decisions And Execution Workflow
- The user reviews at
#/dispatch: approve, request changes (with a note), save an edited note-to-crew (revise), or block. Decisions write directly onto the proposal record.
- On explicit user request to execute, run
scripts/execute_decisions.mjs (dry-run by default; --apply writes execution_status: "ready_for_agent" onto each approved proposal with the concrete notify_crew + update_board operations and a message draft). No external side effects either way.
- The agent then performs the approved crew notification outside the app (via messenger/email/WeChat) and records the real result via
apply_triage.mjs's ticket_updates[]. A second --apply run of execute_decisions.mjs finalizes the proposal's execution_status to "executed" and promotes its workflow status to "done" — this script never flips the workflow status on the first run itself.
Safety Defaults
- Treat crew notifications, resident replies, fines/fees, lock-outs, towing, and anything customer-visible as approval-required.
- Mask resident contacts everywhere in UI state and logs; expose only
contact_env names for crew contacts, never values.
- Keep merges idempotent: stable intake hashes, ticket ids, and proposal refs so repeated ingests and triage runs do not duplicate work.
- Never expose secrets through the settings view or logs; it shows column names and rules only.
- If the UI and schema disagree, stop and fix the schema or UI before executing anything.
Useful Commands
node skills/kelly-tickets/scripts/ingest_intake.mjs payload.json --apply
node skills/kelly-tickets/scripts/apply_triage.mjs payload.json --apply
node skills/kelly-tickets/scripts/execute_decisions.mjs
node skills/kelly-tickets/scripts/execute_decisions.mjs --apply
pnpm --dir skills/kelly-tickets/content/kelly-tickets-app dev
In normal use, invoke /kelly-tickets, let the skill ingest/triage what's due, and open the AirApp.
1---2name: kelly-tickets3description: Complaint/ticket triage-and-dispatch App-in-Skill (Busabase App-in-Skill) for property managers, facilities teams, and helpdesk leads. Use when the user invokes $kelly-tickets or /kelly-tickets, or asks for complaint triage, ticket dispatch, 投诉, 工单, 派单, property management complaints, helpdesk queue, intake from WeChat exports/call logs/front-desk forms, crew assignment, SLA board, dispatch approval, or ticket resolution tracking.4---56# Kelly Tickets78## Overview910Use this skill as Kelly's complaint triage-and-dispatch desk. Complaints and requests arrive scattered across WeChat group exports, phone-call logs, front-desk forms, and email — reading those local exports is a genuine external operation a browser cannot perform, so `scripts/ingest_intake.mjs` is the only place a complaint enters the system. The agent classifies each ticket (category, urgency, unit/location) and proposes a dispatch (crew, priority, SLA) via `scripts/apply_triage.mjs`; the human reviews dispatch proposals in a Busabase-backed App-in-Skill review queue and approves or edits; a board tracks every ticket to resolution with an auditable history trail. `scripts/execute_decisions.mjs` prints the crew-notification plan for approved dispatches — the AirApp itself never sends a message or mutates a remote system. The demo persona is residential property management, but the same flow fits facilities, IT helpdesk, or any dispatch workflow.1112Default behavior is AirApp-first. Unless the user explicitly asks only for explanation, ingest/triage what's due and give the user the clickable AirApp URL (or the local preview URL when local preview is explicitly requested). Use chat-only mode only when the user says "纯聊天", "chat only", "不要打开 UI", or similar; then present numbered proposals (`Dispatch #1`) directly in the conversation.1314## App UI Screenshots1516<table>17 <tr>18 <td width="50%"><img src="assets/screenshots/overview.webp" alt="Kelly Tickets overview"></td>19 <td width="50%"><img src="assets/screenshots/board.webp" alt="Kelly Tickets board"></td>20 </tr>21 <tr>22 <td><strong>Overview</strong><br>Dispatch command desk with SLA risk, weekly intake by channel, category distribution, and crew load.</td>23 <td><strong>Board</strong><br>Tickets tracked across open, assigned, in-progress, waiting, and resolved with SLA indicators and history timelines.</td>24 </tr>25 <tr>26 <td width="50%"><img src="assets/screenshots/dispatch.webp" alt="Kelly Tickets dispatch queue"></td>27 <td width="50%"><img src="assets/screenshots/intake.webp" alt="Kelly Tickets intake"></td>28 </tr>29 <tr>30 <td><strong>Dispatch queue</strong><br>Agent-proposed crew assignments with priority, SLA target, reasoning, and an editable note to the crew.</td>31 <td><strong>Intake</strong><br>Raw complaints from WeChat, phone, forms, and email with classification fields and convert-to-ticket controls.</td>32 </tr>33</table>3435## Mandatory Dependencies36371. Read and follow `$kelly-app-skill-creator` for product behavior, visual quality, responsive layout, and the complete canonical `content/kelly-tickets-app/` artifact.382. Read and follow `$busabase` for connection, target Space, node discovery, ChangeRequests, review, and merge behavior.393. Read and follow `$busabase-app-creator` for resource modeling, AirApp runtime limits, security, validation, and deployment.4041If a dependency is unavailable, preserve this skill's local artifact and product contracts, stop before the unavailable Busabase operation, and report the exact missing dependency. Do not invent a second data backend.4243## Boundary4445- Intake parsing is local: `scripts/ingest_intake.mjs` reads WeChat exports, call logs, and mailbox dumps the user provides and writes normalized rows to Busabase. It never fetches complaints from remote systems on its own.46- The AirApp reads and writes Busabase records only. It never sends messages, calls crews, replies to residents, or mutates remote systems.47- Any outbound crew notification or resident reply is approval-required through the dispatch queue and executed by the agent OUTSIDE the app via other skills (messenger/email/WeChat) after `scripts/execute_decisions.mjs` produces the plan. `scripts/execute_decisions.mjs` never performs these operations itself — it only writes an execution marker onto the proposal.48- Crew contacts are env-var references (`contact_env`) only — never store phone numbers or webhook URLs in Busabase. Resident PII stays masked in the UI (`contact_masked`); `scripts/ingest_intake.mjs` re-masks long digit runs defensively.49- Treat all complaint/resident data as sensitive. Never commit local export files or env files.5051## Busabase Resources5253Six Bases under one application Folder (`kelly-tickets`), declared in `content/kelly-tickets-app/app/js/config.js` and the generated template sidecars under `content/`:5455- `crews`: crews that tickets can be dispatched to (name, skills, `contact_env`).56- `intake`: raw complaints/requests as they arrived on a channel, before or after triage, plus the human decision (convert to ticket / ignore) on the same row.57- `tickets`: tickets tracked from classification through resolution, with an append-only history timeline.58- `proposals`: the dispatch review queue — proposed crew/priority/SLA, the human decision, and the execution marker on the same row.59- `sync-log`: append-only history of ingest/triage/execute runs.60- `settings`: one row (`record-id: "config"`) with property profile, channels, categories, and SLA rules.6162Resources provision lazily through an idempotent Busabase ChangeRequest the first time the app runs in a Space; see `references/tickets-schema.md` for exact field shapes. SLA state and crew load are recomputed client-side from the stored rows on every read (`content/kelly-tickets-app/app/js/tickets-model.js`'s `buildSnapshot`), so the desk is always fresh regardless of when a browser session loads it relative to the last ingest/triage run. The browser has no access to the agent's environment variables, so the Settings view shows each crew's `contact_env` name only, not a live readiness boolean.6364## First Run And Onboarding6566On invocation, check the `intake`/`tickets` Bases. If both are empty, guide setup before ingesting real complaints: ask, turn by turn, property profile (name, buildings, timezone), complaint categories, crews (name, skills, and which env var holds each crew's contact), SLA rules per category+urgency, and which intake channels are in use. Never ask the user to paste contact values or secrets into chat; they belong only in local env files, referenced by `contact_env` names. Write the answers onto the Settings row and the Crews Base, then ingest:6768```bash69node skills/kelly-tickets/scripts/ingest_intake.mjs /path/payload.json --apply70```7172## Local App7374Default behavior is AirApp-first — give the user the clickable AirApp URL. Start `pnpm --dir content/kelly-tickets-app dev` only when local preview/debugging is explicitly requested.7576Required app views (hash routes):7778- `#/overview`: dispatch command desk — human-attention panel (proposals to approve, unclassified intake, SLA-breaching tickets), KPI cards (open tickets, avg resolution, SLA at risk, this week's intake with channel badges), category distribution bars, and crew load.79- `#/intake` and `#/intake/<id>`: raw intake stream — channel badge (WeChat/phone/form/email/walk-in), reporter, unit/location, complaint text, urgency guess, triage state. Detail shows the full text, attachments note, editable classification (category/urgency/unit), and convert-to-ticket or ignore actions.80- `#/dispatch`: the review queue with workflow states `needs_review / changes_requested / approved / done / blocked`. Each card shows the stable ref (`Dispatch #1`), ticket summary, proposed crew/assignee, priority, SLA target, the reason, an editable note to the crew, and approve / request changes / block buttons. Decisions write directly onto the proposal record through `busabase-sdk`.81- `#/board` and `#/board/<ticket_id>`: tickets grouped by status (`open / assigned / in_progress / waiting / resolved`) with category badge, unit, crew, age, and color-coded SLA indicator. Detail shows the full history timeline (intake → classification → dispatch → crew updates → resolution), masked reporter contact, and a resolution note field.82- `#/settings`: sanitized config — property profile, channels, categories, crews with `contact_env` names, SLA rules, data provider, and onboarding state. Never expose contact values or secrets.8384Demo mode:8586- `?demo=overview`, `?demo=intake`, `?demo=dispatch`, `?demo=board`, and `?demo=detail` (a ticket history) select named mock scenes.87- `lang=en` or `lang=zh` forces UI chrome language; with `lang=zh` the demo content itself (property name, complaints, crews, reasons) is meaningfully localized for Chinese screenshots.88- Deep links such as `/?demo=dispatch&lang=zh#/dispatch` must work.89- Demo mode never reads or writes Busabase. Decision buttons still work but act on in-memory state only.9091UI language: English and Chinese chrome with `Auto` default (browser language), plus an explicit selector persisted locally. Keep real resident/complaint data in its original language.9293## Intake Workflow94951. Ask the user for the export/log location (WeChat group export, call log CSV/notes, front-desk forms, mailbox). Raw files stay outside git.962. Parse the raw material into the ingest payload shape (see `references/tickets-schema.md`): one item per complaint — channel, channel-native `external_id` when available, reporter, contact (will be masked), unit/location, verbatim text, received time, plus first-pass `category_guess`/`urgency_guess`.973. Write the payload JSON to a temp path and run `node scripts/ingest_intake.mjs <payload.json> --apply`. The script validates, dedupes by `channel + external_id` (falling back to a content hash), masks contacts, writes new rows into the Intake Base, and appends a Sync Log entry.984. Re-ingesting the same export is safe: duplicates are skipped and reported. Without `--apply` it is a dry run.99100## Triage And Dispatch Workflow1011021. Classification is LLM work: read `intake` items in `new`/`classified` state (and any with `decision_action: "convert_to_ticket"`, the Busabase-only equivalent of a queued agent task), decide category, urgency, unit/location, and a ticket title; decide which crew fits and why (use crew skills, prior tickets for the same unit, and SLA pressure in the `reason`).1032. Merge deterministically with `node scripts/apply_triage.mjs <payload.json> --apply`: creates tickets (`T-1001`-style ids), computes `sla_due_at` from the Settings row's `sla_rules` (category+urgency, `*` wildcard, `sla_default_hours` fallback), assigns stable proposal refs, appends ticket history, and writes it all straight to Busabase. `ticket_updates[]` in the same payload records crew progress, status transitions, and resolutions.1043. Send the user to `#/dispatch` to review. Decisions write directly onto the proposal record; from a standalone local preview the write merges immediately (trusted operator), from the deployed AirApp it creates a pending ChangeRequest for the trusted process to merge.1054. Poll the `intake` Base for rows with `decision_action: "convert_to_ticket"` where `triage_state` is not yet `ticketed`: classify using the human-provided `decision_fields` and create the ticket via `apply_triage.mjs`. Poll the `proposals` Base for `status: "changes_requested"`: redraft the proposal per `decision_note` and re-run `apply_triage.mjs`.1065. After approvals, run `node scripts/execute_decisions.mjs` (dry-run) and show the plan; with user confirmation run `--apply`, then perform the real crew notifications via other skills (messenger/email), record outcomes back through `apply_triage.mjs`'s `ticket_updates[]`, and keep the board honest.1076. Re-read the `proposals` Base immediately before executing, and never execute items without an `approved` status.108109## Board Semantics110111- `open`: classified, no crew accepted yet (dispatch pending or unapproved).112- `assigned`: an approved dispatch reached a crew; nobody started work yet.113- `in_progress`: the crew reported starting work.114- `waiting`: blocked on residents, parts, weather, or vendors — not on the crew.115- `resolved`: work confirmed done; `resolved_at` set and a resolution note recorded.116117SLA states are derived, never hand-set: `ok`, `at_risk` (under 25% of the SLA window left), `breached`, and `met` for resolved tickets — computed fresh on every read by `content/kelly-tickets-app/app/js/tickets-model.js`'s `computeSlaState`. History events are append-only — the timeline is the audit trail; never rewrite past events.118119## Decisions And Execution Workflow1201211. The user reviews at `#/dispatch`: approve, request changes (with a note), save an edited note-to-crew (revise), or block. Decisions write directly onto the proposal record.1222. On explicit user request to execute, run `scripts/execute_decisions.mjs` (dry-run by default; `--apply` writes `execution_status: "ready_for_agent"` onto each approved proposal with the concrete `notify_crew` + `update_board` operations and a message draft). No external side effects either way.1233. The agent then performs the approved crew notification outside the app (via messenger/email/WeChat) and records the real result via `apply_triage.mjs`'s `ticket_updates[]`. A second `--apply` run of `execute_decisions.mjs` finalizes the proposal's `execution_status` to `"executed"` and promotes its workflow `status` to `"done"` — this script never flips the workflow status on the first run itself.124125## Safety Defaults126127- Treat crew notifications, resident replies, fines/fees, lock-outs, towing, and anything customer-visible as approval-required.128- Mask resident contacts everywhere in UI state and logs; expose only `contact_env` names for crew contacts, never values.129- Keep merges idempotent: stable intake hashes, ticket ids, and proposal refs so repeated ingests and triage runs do not duplicate work.130- Never expose secrets through the settings view or logs; it shows column names and rules only.131- If the UI and schema disagree, stop and fix the schema or UI before executing anything.132133## Useful Commands134135```bash136node skills/kelly-tickets/scripts/ingest_intake.mjs payload.json --apply137node skills/kelly-tickets/scripts/apply_triage.mjs payload.json --apply138node skills/kelly-tickets/scripts/execute_decisions.mjs139node skills/kelly-tickets/scripts/execute_decisions.mjs --apply140pnpm --dir skills/kelly-tickets/content/kelly-tickets-app dev141```142143In normal use, invoke `/kelly-tickets`, let the skill ingest/triage what's due, and open the AirApp.