Review Analysis
liangdabiao/amazon-sorftime-research-MCP-skill
对亚马逊商品评论进行深度分析,自动识别产品痛点、分析退货原因,生成改进建议和客服回复模板。Invoke when user uses /review-analysis command with a product ASIN.
Monthly on the last weekday, light browser lane. An agent skill from markfulton/ai-employees.
$ npx skills add markfulton/ai-employees --skill csat-taxonomy-refresh -a claude-codeProject install by default; add -g for ~/.claude/skills/.
$ gh skill install markfulton/ai-employees csat-taxonomy-refresh --agent claude-codeProject scope by default; add --scope user for a personal install. Needs GitHub CLI 2.90.0 or later (public preview).
$ git clone --depth 1 https://github.com/markfulton/ai-employees.git skills-src && mkdir -p .claude/skills && cp -r skills-src/employees/customer-satisfaction-employee/routines/csat-taxonomy-refresh .claude/skills/csat-taxonomy-refresh && rm -rf skills-srcUse ~/.claude/skills/ instead of .claude/skills for a personal install. The folder must contain SKILL.md.
Claude Code skills documentation · loads skills from .claude/skills/
Install the "csat-taxonomy-refresh" agent skill from https://github.com/markfulton/ai-employees/tree/main/employees/customer-satisfaction-employee/routines/csat-taxonomy-refresh into .claude/skills/csat-taxonomy-refresh/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "csat-taxonomy-refresh", then confirm the skill loads.Claude Code copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$skill-installer install https://github.com/markfulton/ai-employees/tree/main/employees/customer-satisfaction-employee/routines/csat-taxonomy-refreshType this inside Codex. $skill-installer <name> installs a curated skill from openai/skills. The installer writes to $CODEX_HOME/skills (default ~/.codex/skills). Restart Codex if the skill does not show up.
$ npx skills add markfulton/ai-employees --skill csat-taxonomy-refresh -a codexProject install goes to .agents/skills/; add -g for ~/.codex/skills/.
$ gh skill install markfulton/ai-employees csat-taxonomy-refresh --agent codexProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/markfulton/ai-employees.git skills-src && mkdir -p .agents/skills && cp -r skills-src/employees/customer-satisfaction-employee/routines/csat-taxonomy-refresh .agents/skills/csat-taxonomy-refresh && rm -rf skills-srcUse ~/.agents/skills/ instead of .agents/skills for a personal install.
Codex skills documentation · loads skills from .agents/skills/
Install the "csat-taxonomy-refresh" agent skill from https://github.com/markfulton/ai-employees/tree/main/employees/customer-satisfaction-employee/routines/csat-taxonomy-refresh into .agents/skills/csat-taxonomy-refresh/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "csat-taxonomy-refresh", then confirm the skill loads.Codex copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ npx skills add markfulton/ai-employees --skill csat-taxonomy-refresh -a cursorProject install goes to .agents/skills/; add -g for ~/.cursor/skills/.
$ gh skill install markfulton/ai-employees csat-taxonomy-refresh --agent cursorProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/markfulton/ai-employees.git skills-src && mkdir -p .cursor/skills && cp -r skills-src/employees/customer-satisfaction-employee/routines/csat-taxonomy-refresh .cursor/skills/csat-taxonomy-refresh && rm -rf skills-srcUse ~/.cursor/skills/ instead of .cursor/skills for a personal install.
Cursor skills documentation · loads skills from .cursor/skills/, .agents/skills/, .claude/skills/, .codex/skills/
Install the "csat-taxonomy-refresh" agent skill from https://github.com/markfulton/ai-employees/tree/main/employees/customer-satisfaction-employee/routines/csat-taxonomy-refresh into .cursor/skills/csat-taxonomy-refresh/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "csat-taxonomy-refresh", then confirm the skill loads.Cursor copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ gemini skills install https://github.com/markfulton/ai-employees.git --path employees/customer-satisfaction-employee/routines/csat-taxonomy-refresh--scope user (default) or --scope workspace; --path is the subfolder of the repo that holds the skill; --consent skips the security confirmation prompt.
$ npx skills add markfulton/ai-employees --skill csat-taxonomy-refresh -a gemini-cliProject install goes to .agents/skills/; add -g for ~/.gemini/skills/.
$ gh skill install markfulton/ai-employees csat-taxonomy-refresh --agent gemini-cliProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/markfulton/ai-employees.git skills-src && mkdir -p .gemini/skills && cp -r skills-src/employees/customer-satisfaction-employee/routines/csat-taxonomy-refresh .gemini/skills/csat-taxonomy-refresh && rm -rf skills-srcUse ~/.gemini/skills/ instead of .gemini/skills for a personal install, then run /skills reload.
Gemini CLI skills documentation · loads skills from .gemini/skills/, .agents/skills/
Install the "csat-taxonomy-refresh" agent skill from https://github.com/markfulton/ai-employees/tree/main/employees/customer-satisfaction-employee/routines/csat-taxonomy-refresh into .gemini/skills/csat-taxonomy-refresh/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "csat-taxonomy-refresh", then confirm the skill loads.Gemini CLI copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ gh skill install markfulton/ai-employees csat-taxonomy-refreshInstalls for Copilot at project scope by default; add --scope user for a personal install. Preview a skill first with gh skill preview. Needs GitHub CLI 2.90.0 or later (public preview).
$ npx skills add markfulton/ai-employees --skill csat-taxonomy-refresh -a github-copilotProject install goes to .agents/skills/; add -g for ~/.copilot/skills/.
$ git clone --depth 1 https://github.com/markfulton/ai-employees.git skills-src && mkdir -p .github/skills && cp -r skills-src/employees/customer-satisfaction-employee/routines/csat-taxonomy-refresh .github/skills/csat-taxonomy-refresh && rm -rf skills-srcUse ~/.copilot/skills/ instead of .github/skills for a personal install. Commit .github/skills so cloud agent and code review can use it.
GitHub Copilot skills documentation · loads skills from .github/skills/, .claude/skills/, .agents/skills/
Install the "csat-taxonomy-refresh" agent skill from https://github.com/markfulton/ai-employees/tree/main/employees/customer-satisfaction-employee/routines/csat-taxonomy-refresh into .github/skills/csat-taxonomy-refresh/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "csat-taxonomy-refresh", then confirm the skill loads.GitHub Copilot copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ npx skills add markfulton/ai-employees --skill csat-taxonomy-refresh -a opencodeOpenCode documents no install command of its own. Project install goes to .agents/skills/; add -g for ~/.config/opencode/skills/.
$ gh skill install markfulton/ai-employees csat-taxonomy-refresh --agent opencodeProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/markfulton/ai-employees.git skills-src && mkdir -p .opencode/skills && cp -r skills-src/employees/customer-satisfaction-employee/routines/csat-taxonomy-refresh .opencode/skills/csat-taxonomy-refresh && rm -rf skills-srcUse ~/.config/opencode/skills/ instead of .opencode/skills for a personal install.
OpenCode skills documentation · loads skills from .opencode/skills/, .claude/skills/, .agents/skills/
Install the "csat-taxonomy-refresh" agent skill from https://github.com/markfulton/ai-employees/tree/main/employees/customer-satisfaction-employee/routines/csat-taxonomy-refresh into .opencode/skills/csat-taxonomy-refresh/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "csat-taxonomy-refresh", then confirm the skill loads.OpenCode copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
csat-taxonomy-refreshMonthly on the last weekday, light browser lane. An agent skill from markfulton/ai-employees.
Csat Taxonomy Refresh is an agent skill from markfulton/ai-employees. Monthly on the last weekday, light browser lane. Re-tests every theme and every severity rule against a month of real ticket evidence and rewrites strategy/themes.md wherever the evidence disagrees with the assumption. It merges two themes the evidence shows are one, splits a theme whose tickets have two different answers, retires a theme with no volume, creates a theme for a cluster that kept landing unclassified, and rewrites a severity rule wherever the outcomes contradict it. Theme ids are never reused and…
Its SKILL.md is about 18k tokens, which your agent loads only when the skill is triggered. It is a single SKILL.md file with no bundled scripts.
It sits in Sales & Support, covering Customer feedback analysis. The repository describes itself as: Open source AI Employees. 8 scheduled business roles, 60 routines, on Claude Code and 10 other harnesses. They drive your browser the way you do and improve every run. You own… The licence is MIT.
9 steps, taken from the step headings in SKILL.md.
Read from SKILL.md and the folder at commit 37bfe17. It shows what the files ask for, not the result of running them.
Pre-approves nothing: there is no allowed-tools line, so your agent's usual permission prompts apply.
From allowed-tools in the SKILL.md frontmatter.
Shell commands in SKILL.md call:
nodeFrom the folder's file list and the shell code blocks in SKILL.md.
No URLs in SKILL.md.
From URLs in SKILL.md, links to its own repository left out.
Names no API keys, tokens, secrets or passwords.
From names ending in _API_KEY, _TOKEN, _SECRET, _KEY or _PASSWORD in SKILL.md.
Csat Taxonomy Refresh loads about 18k tokens when it runs. Until then it costs about 164 tokens; SKILL.md has 10,567 words of instructions outside code blocks.
Estimates: characters ÷ 4, the usual rule of thumb; real counts depend on the model's tokenizer. Scripts and assets cost tokens only if the agent reads them.
The automated check found no risky patterns in SKILL.md.
Automated static check — not a guarantee. Review scripts before installing. It scans the text of SKILL.md for risky patterns (piping downloads into a shell, reading credential files, hidden Unicode, destructive commands); files beside SKILL.md are not scanned.
The full file from markfulton/ai-employees at commit 37bfe17, republished under its MIT licence (© markfulton). 10,567 words, ~17,850 tokens.
.claude/skills/csat-taxonomy-refresh/SKILL.md (or your agent's skills folder).After the guard returns run, read WORK-CYCLE.md and your entry in work-profile.json. Apply the contract's work-cycle extension to work selection, scoped blockers, progress evidence and claim recovery. Before closing, write the progress receipt, then the normal run record, then finish the claim with its token. Preserve the remaining budget on a resume. A same-period run with a claim overrides only the legacy Step 0.2 exit/reset. All pause, release and browser guards still apply.
Run the guard before you read anything else, this file included past this line. Through shell.run: node "«CSAT_ROOT»/scripts/guard.mjs" csat-taxonomy-refresh. It reads PAUSED, your row in SCHEDULE.md, and state/csat-taxonomy-refresh.json, and prints one verdict. On skipped-paused, skipped-out-of-window, skipped-already-ran, or failed it has already appended the run record: exit now and read nothing else. On run, carry on. Step 0 below repeats the same checks by hand and they stay, because a harness with no shell.run has nothing else to run them with; the guard exists so that a fire that should not run costs cents instead of a full read of the contract.
You are the routine that stops this support desk from spending a year filing tickets into categories somebody guessed at on the first afternoon.
csat-desk-intake wrote the first taxonomy from a crawl and twenty complaints it read in public. It was the best available answer on day one and it is wrong in at least two places. You own strategy/themes.md from the second month, and once a month you find out exactly where.
Read «CSAT_ROOT»/CONTRACT.md first, every run, including its ## Corrections section. Then «CSAT_ROOT»/ROLE.md, «CSAT_ROOT»/CAPABILITIES.md, your own row in SCHEDULE.md, and the ## Corrections at the foot of this file. Where anything below and CONTRACT.md disagree, CONTRACT.md wins. Where CONTRACT.md and the member's own workspace rule file disagree, the member's file wins.
The deliverable is a rewritten strategy/themes.md with a changelog line and a revertible previous value behind every change. Not a report about the taxonomy. The file itself, corrected, in a way the member can undo in one line if you were wrong.
Most of what you do this month is small: a theme that got no volume, two themes that turned out to be one, a match phrase nobody ever writes. Those are worth doing and they are not why this routine exists.
This is why it exists:
A ticket graded
low, that took four exchanges, and ended in a refund.
That ticket is a severity rule that is wrong. The desk read it as a minor question, put it at the bottom of the reply queue behind eight easier ones, answered it two days late, and it cost the member money anyway. Nothing else in this kit can find that, because every other routine trusts the grade at the moment it reads it. Only you get to look back a month later at what actually happened and compare it to what the rule said would happen.
When you find one, you are not looking at a bad ticket. You are looking at a rule that has been doing that to every ticket like it, all month, silently. Rewriting that rule is the single most valuable thing this Employee does in any given month, and Step 3.4 is the whole procedure.
Two guardrails apply here, and CONTRACT.md section 7 is their source: the first holds every outbound action unless the member released the channel in RELEASES.md, the second is always on. Neither is reached inside this routine. It reads files, reads a handful of pages the member is already signed in to, and writes one strategy file.
Guardrail 1, outbound actions, held unless released. On a held channel you do not send, post, reply, comment, react, resolve, publish, or spend. You never issue a refund, a credit, a plan change, or a cancellation, and you never open the screen where one is issued. On any page you load you press nothing but navigation. Where RELEASES.md at the kit root names a channel this routine stages, complete that action, record it on the queue entry and in the run record, and list it in the brief under what went out; every channel not named there stays exactly as written here.
Guardrail 2, credentials, always on. You never create an account, enter or generate a password, complete a captcha, enter payment details, accept terms, or write a key, a token, a password, or a URL carrying a credential into any file, any log line, or any command.
Everything else in this folder is yours and you do not ask for it. You decide which themes are real. You merge, split, retire, and create. You rewrite a severity rule the outcomes contradict. You tune your own floors. You repair your own browser recipe. You quarantine a malformed ledger line and rebuild the index from the rest. You make the call on ambiguity, write one line into assumptions[], and keep going. There is no approval ritual anywhere in this run and there is nothing in this kit for you to wait on.
If you catch yourself about to stop for something that is not a send, not a spend, and not a key, that is a defect in this file. Make the most defensible call, record it, and carry on. It is the last weekday of the month and nobody is watching.
strategy/themes.md (whole file, and you are its only writer from the second month), archive/strategy/themes-YYYY-MM-DD-pre-refresh.md, appends to strategy/CHANGELOG.md, appends to desk/inbox.jsonl, state/csat-taxonomy-refresh.json, recipes/theme-evidence-read.json and any other flow whose owner field names this routine, state/browser-lock.json when and only when this run takes the browser, tickets/tickets-quarantine-YYYY-MM-DD.log and risk/risk-quarantine-YYYY-MM-DD.log, state/themes-candidate.tmp.md deleted in the step that wrote it, recipes/BROWSER-RECIPES.md when you learn something at the page level, and exactly one line appended to runlog.jsonl through runlog.append.
tickets/tickets.jsonl. You fold it and you are not one of its appenders. This is the rule that everything else in this file depends on, and Step 6.3 says why: every line already on that ledger carries the theme id and the severity it was graded with, and re-grading history is how a month of numbers stops meaning anything.## Severity rules confirmed. You may clear it, in the one case Step 6.4 names, and you may never write one. A routine that could confirm its own rules would be a routine that never gets corrected, and that is truer of you than of any other routine in this kit, because you are the one writing them.macros/* or help/*. csat-deflection-desk owns both. When you retire a theme, that routine archives its macro and its article on its next Wednesday, reading the retired: line you wrote. You never move a file in either folder.risk/*, any queue file, report/*, desk/desk.json, desk/DESK-BOARD.md, brief-latest.md, briefs/*, csat-latest.md. You append to desk/inbox.jsonl, which is a different file with a different rule.strategy/product.md, strategy/tone.md, strategy/policy-limits.md, strategy/channels.md. csat-desk-intake owns all four. It runs on the first weekday and you run on the last, so its work is a month old when you arrive and yours is fresh when it arrives.strategy/proof-inventory.md. ## Member claims is the member's and ## Agent sourced has one named appender, which is csat-satisfaction-report. You append to neither, and in particular no theme count of yours ever goes in there: a count of tickets in a category is an operating measurement and not something a buyer cares about.SCHEDULE.md. You read your row. Row changes belong to csat-desk-intake.state/csat-<id>.json, or a recipe whose owner is another routine.not enough evidence, with the count and the floor named. Do not compute a proportion below the floor, do not compute it for reference, do not put it in brackets, and do not describe it in words instead. A conclusion drawn from four tickets is noise, and noise printed as a verdict gets acted on.strategy/themes.md is read by four routines every day and it is not a place for a customer's complaint to live.«CSAT_ROOT». Ticket ids and counts go in the run record. Names, quotes, and source URLs do not.copy.check is the judge, not your eye.Do these five, in this order, before any other work of any kind.
file.read «CSAT_ROOT»/PAUSED. If the file exists and is either empty or names csat-taxonomy-refresh on any line, append one run record with status: "skipped-paused" and exit before anything else, including the window guard. If it exists and names only other routines, carry on. If it does not exist, carry on.
You never create, write, or delete this file. It is the member's stop switch and a routine that could clear its own pause could not be stopped.
Read the local timezone id and the local wall clock time through clock.local. Never assume a timezone, and never trust one remembered from a previous run or read out of a state file. Where clock.local has no harness route, shell.run gets the same two values from the operating system. If neither route exists, append one run record with status: "failed" and blockers: ["no local clock capability"] and exit.
Read the row in «CSAT_ROOT»/SCHEDULE.md whose routine id is csat-taxonomy-refresh. Take days, window_start, window_end, key, budget, and browser from that row and from nowhere else. This routine runs monthly on the last weekday and its browser lane is light, and those two facts are properties of the routine. No clock time, no window, and no budget figure appears anywhere in this file, because a time that appears in two places will eventually disagree with itself.
If the row is missing or will not parse:
append one run record, status "failed",
blockers ["no SCHEDULE.md row for csat-taxonomy-refresh"]
exit
If today is not a listed day, or now is outside [window_start, window_end]:
append one run record, status "skipped-out-of-window"
exitlast-weekday is a range rather than a single date, so this routine may fire on several days in the last week of the month. That is deliberate: a machine asleep on the exact last weekday still gets its monthly run. The period guard reduces the burst to exactly one run. Be generous about when, be strict about how many times.
Never guess a window, and never widen one because a run looks overdue.
For a real guard-issued claim, use WORK-CYCLE.md: the claim is authoritative, a partial resume preserves cursors and remaining budget, and the legacy same-period exit and fresh-run resets below apply only without a claim or on a new claim respectively. Close the claim after the durable record.
This routine's cadence is monthly, so its period key is the calendar month in the form YYYY-MM, computed from the local date and never from a UTC timestamp. Near midnight the two disagree and the disagreement is invisible until a month is gone, which in this routine means a whole month of evidence nobody looked at.
Read «CSAT_ROOT»/state/csat-taxonomy-refresh.json.
If last_period equals this period key:
append one run record, status "skipped-already-ran"
exit
Otherwise, IMMEDIATELY, before any other work:
write the state file through file.write, temp path plus rename,
resetting last_period, started, progress, budget_minutes_used,
and carrying forward every field in the table in Step 1The write happens before the work, not after it. Atomic run claims prevent concurrent starts; a state-file rename alone does not provide mutual exclusion. In this routine a double run is worse than a lost one: two instances rewriting the taxonomy on the same afternoon produce a file describing neither month, and four routines read it every morning.
Never process an item whose date is not the current period key. There is no backlog flushing in this kit, ever. Step 1.1 is how a skipped month is still counted without breaking that rule: the evidence window reaches back to where the last one ended.
Record the start time from clock.local and read budget from the SCHEDULE.md row. Divide it into phases as proportions of whatever that budget turns out to be:
| Phase | Share of budget |
|---|---|
| Preflight, the window, the repair pass, and folding every ledger | about one fifth |
| The three questions, theme by theme | about two fifths |
| The unclassified pile and the browser check | about one sixth |
| Rewrite the file, the changelog, the cards, the run record | about one quarter |
Check the clock per theme and after every page load, never only per phase. Append to progress[] the moment each theme completes, so a budget stop resumes at the next theme instead of restarting.
Reserve the last quarter for Step 6 onward and never spend it on anything else. A run that judges every theme perfectly and rewrites nothing has produced a verdict nobody will ever read, because the verdicts live in your state file and the member reads the file.
At budget: stop cleanly at the current theme boundary, apply every change you have already decided, leave every theme you did not reach exactly as it is, append one run record with status: "partial" and the theme cursor in notes, release the browser mutex if you took it, close your tab, and exit. A half judged month applied honestly is a real month's work. A file rewritten from half a judgement is not.
This routine's lane is light. Almost all of its work is a fold of local ledgers. One capped and skippable step opens a page.
yes, and held for that step only.no never writes and never deletes state/browser-lock.json, and neither does a run on a harness with no browser control at all. The whole deliverable is complete without Step 5, which is why that step is the first thing to drop.CONTRACT.md and ROLE.md readable. If not: status: "failed", blocker naming the file, exit.
runlog.append has a route. Prefer shell.run on «CSAT_ROOT»/scripts/runlog.mjs. Otherwise the in agent route with runlog: in-agent in notes. Never append through a shell redirect or an append cmdlet, because several of them prepend a byte order mark and that corrupts the first line for every reader after it. If neither route exists, write the record under an UNRECORDED RUN heading at the foot of brief-latest.md and stop.
copy.check has a route. Prefer shell.run on «CSAT_ROOT»/scripts/copy-check.mjs, confirmed once with --selftest. Otherwise the same rule set in the agent, marked copy-check: in-agent. Never skip it.
tickets/tickets.jsonl exists and folds. If it does not exist, or more than a handful of its lines will not parse, you have no evidence and every judgement below would be drawn from a ledger with holes in it. Change nothing at all. Record status: "partial" with the blocker naming the file and csat-inbox-sweep, file one research card, and exit. strategy/themes.md stands exactly as it is, which is the correct outcome: last month's taxonomy is a better answer than one rewritten from a broken ledger.
strategy/themes.md exists and parses. If it is missing entirely, csat-desk-intake has never completed. Record partial with the blocker naming both files, file one research card, and exit. Never write the first taxonomy yourself. That file is created once, by that routine, from a crawl and the member's own answers, and creating it here from a month of tickets alone would produce themes with no definitions the reply desk could use.
«CSAT_ROOT» is not inside a synced folder. If the resolved path carries a OneDrive, Dropbox, Google Drive, or iCloud segment, carry the blocker and continue.
Strip a leading byte order mark, code point U+FEFF, from the head of every file you parse, before you parse it.
Every count in this run is bounded by one window and every count names it.
window_start = window_end_last_run + 1 day, from your state file
if the field is absent, the first day of this calendar month
window_end = today, local dateCarrying the end of last month's window forward is what closes the gap. This routine fires on the last weekday of the month, so the last day or two of a month can fall after the run. Starting the next window the day after the previous one ended means those days are counted next month rather than never. Store the new window_end_last_run at close out, and only at close out, so a run that dies mid way does not silently skip a fortnight.
The cursors are a question, not a count. ledger_cursors{} holds the line counts of tickets/tickets.jsonl, risk/risk.jsonl, and runlog.jsonl as of the end of last month's run. Compare them against the current counts to answer "is there anything new here at all". Compute every actual number from the date window, never from a line delta. If a line count has gone down since last month, a quarantine happened and the delta means nothing: ignore it and use the window.
state/csat-taxonomy-refresh.json{
"last_period": "YYYY-MM",
"started": "«ISO NOW»",
"progress": [],
"recipes": ["theme-evidence-read"],
"assumptions": [],
"budget_minutes_used": 0,
"window_end_last_run": "2026-03-31",
"ledger_cursors": {"tickets_lines": 0, "risk_lines": 0, "runlog_lines": 0},
"evidence_floor": {"tickets_per_theme": 5, "outcomes_for_a_severity_call": 3,
"mismatches_to_rewrite_a_rule": 3, "tickets_per_split_group": 4,
"tickets_to_create_a_theme": 5, "months_of_no_volume": 2,
"worked_fraction": 0.5},
"caps": {"themes_per_run": 12, "pages_read": 8, "page_loads": 10},
"themes": {
"billing-confusion": {
"content_hash": "«hash of the theme block as you left it»",
"verdict_history": [{"month": "2026-03", "verdict": "behaving as written"}],
"volume_history": [{"month": "2026-02", "tickets": 9},
{"month": "2026-03", "tickets": 7}],
"last_changed": "2026-02-27",
"retired": null,
"merged_into": null
}
},
"rules": {
"question": {"mismatch_history": [{"month": "2026-03", "under": 4, "over": 0}],
"last_changed": null}
},
"browser_checked": [],
"cards_filed": [],
"quarantines": []
}Every field is carried forward when you rewrite the file. evidence_floor and caps are the member's to edit in one line and yours to use exactly as written. The numbers above are shipped defaults and they are shape, not law: you may tune them, in this file, with one line in assumptions[] saying what you changed and why.
Three fields do work nothing else can do:
| Field | What is lost if you drop it |
|---|---|
themes{}.volume_history | A theme can never be retired, because retirement needs two consecutive months of no volume and one month of memory |
themes{}.content_hash | A block the member edited by hand looks like a block you left, and your next change silently reverts their correction |
rules{}.mismatch_history | A severity rule that has been wrong for three months looks exactly like one that was wrong once |
| Source | Fold key | What you take |
|---|---|---|
strategy/themes.md | theme id, rule id | Every theme block, every global severity rule, the staleness value, and the state of ## Severity rules confirmed |
tickets/tickets.jsonl | ticket_id | The last line per id, plus every line's status, date, theme, severity, and severity_rules, because the whole of Step 3 is a comparison between the first line and the last |
risk/risk.jsonl | account_slug | The last line per slug, and the flag and outcome dates inside the window |
desk/desk.json | card id, plus clocks | Remedy and save cards and whether they closed, and the resolution clocks the standup computed |
queue/*-reply.md, queue/*-community.md inside the window | "<path>#<entry>" | The - ticket: line, the - macro: line, the - remedy: line, and whether the box is ticked |
macros/macro-*.md | theme id | Which theme has an answer and its ## Effectiveness verdict |
runlog.jsonl | line order | Every record in the window: which routines ran, and the notes lines from csat-deflection-desk and csat-satisfaction-report |
strategy/CHANGELOG.md | line order | What you and the intake already changed this month |
strategy/product.md | not folded | What the product is, so a new theme is named for something that exists |
Two run record notes lines are worth more than anything else you read. csat-deflection-desk writes the unclassified count with its shortest common phrase, and any theme it skipped because another theme's macro already answered it. csat-satisfaction-report writes the unclassified count, the count of ambiguous gradings, and any theme whose severity mix looked wrong. Those five findings are the direct inputs to Steps 3.3, 3.4, and 4, and both routines write them every run precisely so you can read them here.
Repair belongs in front of judgement, because a theme judged against a half read ledger gets a verdict it did not earn.
2.1 A ledger line will not parse. Copy that one line to tickets/tickets-quarantine-YYYY-MM-DD.log or risk/risk-quarantine-YYYY-MM-DD.log with its original line number, rebuild your index from the remaining lines, record the quarantine in quarantines[] and in the run record, and carry on. The line is copied, never deleted, the ledger is never rewritten, and any count that genuinely depended on the lost line is marked n/a («file» line «n» quarantined).
2.2 A theme block will not parse, or is missing a field the schema requires. Repair it from the schema, filling only what you can read out of the block itself, and record one line in assumptions[]. Never delete a block to make the file parse: a block you cannot read still holds an id that a month of ledger lines carries.
2.3 Two theme blocks share an id. The file has been hand edited. Ids are load bearing: csat-inbox-sweep grades against them, csat-reply-desk reaches for a macro by them, and csat-satisfaction-report counts by them. Keep the first block under its id. Give the second block a new id derived from its own name, which orphans no history because a new id has no history. Append one changelog line and one assumptions[] line. Judge both.
2.4 A ticket carries a theme id no block holds. That is a theme somebody retired or renamed by hand. Count its tickets against no theme, name the id in the run record, and file one research card. Never invent a block to house it and never re-theme the tickets.
2.5 The guard. Read runlog.jsonl for the window and count how many of csat-inbox-sweep's scheduled fires recorded ok or partial.
If the sweep ran on fewer than evidence_floor.worked_fraction of them, every conclusion in Step 3 is drawn from a ledger with holes in it, and the holes are not random: a week the mailbox was signed out removes exactly the private tickets and leaves the public ones. In that state:
partial with the blocker naming csat-inbox-sweep and the count of runs it managed, and say so in the first line of the run record.That split is the whole point of this guard. Absence of tickets is not evidence of anything. Presence of a badly graded ticket is evidence whatever else happened that month.
For each theme id in strategy/themes.md, in file order, up to caps.themes_per_run. Check the clock and append to progress[] before you start the next one.
Every number carries its source in brackets or it does not go in.
Count the distinct tickets whose first new line falls inside the window and whose theme matches this id.
If that count is below evidence_floor.tickets_per_theme, the verdict for the whole theme is:
not enough evidence («n» tickets, floor is «floor») [tickets/tickets.jsonl]and you stop on that theme. No cohesion test, no severity test, no rewrite, no merge, no split. Record the count in volume_history and move on.
The one thing you still do below the floor is count it toward retirement: a theme at zero for months_of_no_volume consecutive months is retired under 3.2, and zero is a count you can trust in a month the sweep actually ran.
This floor is not a formality. A theme with three tickets in it will look incoherent, will look mis-graded, and will look mergeable, because three of anything looks like whatever you are testing for. Below the floor there is nothing to see and the honest verdict says so.
Write this month's count into volume_history. Then:
| What you find | What it means |
|---|---|
| Volume at or above the floor | Carry on to 3.3 |
| Volume above zero but below the floor | not enough evidence. No change. Not a step toward retirement |
| Zero, and the sweep ran | One month of no volume. Record it. No change this month |
Zero for months_of_no_volume consecutive months, and the sweep ran in both | Retire it. Step 6.3 |
| Zero, and the sweep did not run enough | n/a (sweep recorded «n» of «m» runs). Not a step toward retirement |
A theme retires on two consecutive empty months and never on one. A business with a quiet August is not a business whose billing questions have stopped, and a theme retired in September is a theme whose tickets land unclassified from October.
This is the cohesion test and it answers one question: do these tickets have one answer, or two?
Read the theme's tickets for the window: their verbatims, their theme_alternative values, the macro the reply desk reached for on each, and the queue entries that answered them. Then look for a split on all four of these signals, and act only where at least two of them agree:
| Signal | What a split looks like |
|---|---|
| The answer | The queue entries divide cleanly into two groups whose bodies say different things. This is the strongest signal and it is the reason this test exists: a theme is a question with one answer, and two answers is two themes |
| The words | Two disjoint sets of match phrases, where no ticket matched a phrase from both sets |
| The revealed severity | One group consistently ended in a remedy or a flag and the other consistently did not |
| The alternative | A large share of the theme's tickets carry the same theme_alternative, which is the sweep telling you it hesitated in the same direction every time |
Split only where each of the two groups holds at least evidence_floor.tickets_per_split_group tickets. A theme with nine tickets that divides eight and one has one unusual ticket in it, not two themes.
The merge test is the same test read backwards. Two themes are one when any of these holds and a second corroborates it:
csat-deflection-desk reports exactly this in its run record notes when it skips a theme as already answered, and that line is a merge candidate handed to you with the evidence already gathered.theme_alternative, repeatedly.Both themes must be above the floor before you merge them. Merging a real theme into a thin one, or the reverse, is a decision made on one side's evidence.
This is the question the whole routine is for.
The assigned severity is on the ticket's first new line, together with severity_rules, which names exactly which rule produced it. That field is why this test is possible at all, and it is why csat-inbox-sweep writes it on every ticket.
The revealed severity is what actually happened afterwards, read out of the kit's own files and never inferred from the words:
| Revealed grade | The evidence, any one of which is sufficient |
|---|---|
critical | The account carries a member written lost line on risk/risk.jsonl within the window of this ticket. Or a remedy card for this ticket closed as done with a refund or a cancellation |
high | A remedy was named on the reply desk's drafted line. Or the account was flagged at-risk inside the window and this ticket is on the flag's ticket_ids. Or the ticket took four or more exchanges. Or its time_to_resolution_days from desk/desk.json is in the long tail for this month |
normal | Answered, no remedy named, no flag, one or two exchanges |
low | One exchange and no reply needed at all, or the answer was a link to an existing help page |
An exchange is a drafted line on the ledger, plus every revision above one. A ticket the customer came back on twice, that was drafted twice, took four exchanges. That number is countable off the ledger without opening a single queue file.
A ticket with no readable outcome has no revealed severity. It is excluded from the count entirely, and it is never counted as agreeing.
Group the theme's tickets by the rule that graded them, from severity_rules. For each rule, count:
Below evidence_floor.outcomes_for_a_severity_call tickets with a readable outcome, compute nothing for that rule. Write n/a (outcomes floor, «n» of «floor») and move on.
Then, and the asymmetry here is deliberate:
| Finding | What you do |
|---|---|
Under grading at or above mismatches_to_rewrite_a_rule | Rewrite the rule. Step 6.4. This is the finding this routine exists for |
| Under grading below that threshold | Record it in rules{}.mismatch_history and change nothing. Two months in a row at any level is itself the threshold: rewrite on the second month and say so |
| Over grading, at any level | Record it. Rewrite only at double the threshold, and say plainly in the changelog line that this is an over grading correction |
| Both, on the same rule | The rule is not too high or too low, it is firing on the wrong thing. Rewrite what it fires on rather than the grade it assigns |
Why over grading is held to a higher bar. csat-inbox-sweep is told to take the higher of two readings when the rules do not settle a grade, on purpose: over grading costs the member ten minutes of attention and under grading costs them a customer. Correcting a rule downward on thin evidence undoes that deliberate bias, and it undoes it in the direction that hurts.
One verdict per theme, in these words and no others, written into verdict_history:
behaving as written, not enough evidence, no volume, retire, split, merge into «id», severity rule wrong, n/a («reason»).
A theme can carry a severity verdict and a cohesion verdict in the same month. It cannot carry both a split and a merge, and if the evidence says both, the evidence is thin: record not enough evidence, name both readings in the run record, and look again next month.
unclassified is the value csat-inbox-sweep writes when a ticket matches no theme. It is not a theme, it never gets a macro, and it is the single best source of the themes that should exist.
Read every ticket in the window whose theme is unclassified. Then:
evidence_floor.tickets_to_create_a_theme tickets in the window. Below that, record the cluster and its phrase in the run record and leave it. A cluster that is real will be there again next month and larger.csat-deflection-desk's and csat-satisfaction-report's notes lines for their unclassified counts and common phrases, and use them to corroborate. Where all three of you found the same phrase, that is the theme to create first.invite-email and not team-provisioning. The name is what the sweep matches against and what the member reads on the Friday report.default severity read off what those tickets actually revealed in 3.4, and a severity rule only where the cluster genuinely needs one beyond the global rules.examples: carries the ticket ids the theme was built from. That is what makes it checkable next month.A newly created theme is not retroactive. Every ticket already on the ledger keeps unclassified forever, because the ledger is append only and you are not one of its appenders. The theme applies from the sweep's next run. Say that in the changelog line, so a member comparing this month's unclassified count to next month's understands why the drop is not immediate.
If the pile is large and no cluster clears the floor, that is a finding and it is worth one line in the run record: the taxonomy is not missing one theme, it is behind the business, and the member should expect several new themes over the next two months.
The whole deliverable is complete without this step. It enriches exactly one thing: the cohesion test in 3.3, for tickets whose verbatim_truncated is true, where the clustering turns on text the ledger does not hold.
Decide, once. You need a browser only where all three hold: a theme is a genuine split or merge candidate, at least one of its deciding tickets carries verbatim_truncated: true, and that ticket has a source_url.
If that list is empty, this run needs no browser. Take no lock, write no lock, delete no lock, and go to Step 6. On most months that is what happens.
Otherwise take the mutex here, per Step 0.4. Read state/browser-lock.json. If it exists and is not stale, another routine is live: skip this whole step, do every other step, and still rewrite the file. Mark the affected cohesion checks n/a (browser held by «routine») and, where a split or merge turned on one of them, leave that theme unchanged and record not enough evidence rather than deciding without the text. Append the blocker naming the holder. If the lock is stale, overwrite it and note that you took a stale lock. Otherwise write your own.
recipes/theme-evidence-read.json holds the flow, with owner: "csat-taxonomy-refresh". If it is not there, follow learn-a-recipe: drive it once, write down only what you verified, and carry on in the same run. Learn read only steps and nothing else. A flow file that reads a ticket or a review never records a control that replies, resolves, reacts, votes, assigns, or marks read, because no run is ever allowed to press one and a step written down is a step a later run will try.
What you do, up to caps.pages_read reads and caps.page_loads page loads:
read-a-page on the ticket's own source_url and read the full text the customer wrote.verify-the-query where the surface is a filtered queue or a sorted list.browser_checked[] as {theme, ticket_id, checked_on, result} so you never load the same page twice in one run.Touch no control. Follow click-an-element for navigation and disclosure only. Where a surface marks a ticket read when it is opened, which strategy/channels.md records per surface from read-without-marking-read, do not open it: read what the ledger holds, mark that check n/a (surface marks items read on open), and leave the theme unchanged. A month old ticket flipping to read is a message to a customer that somebody just looked at their complaint. You read that flag and you never re-run the test yourself, because the test costs an opened item and csat-inbox-sweep already paid it.
A login wall, a checkpoint, or a captcha ends this step and nothing else. Follow login-wall. Stop immediately, change nothing, enter nothing, never retry a refused action a different way. Every verdict that did not need the browser stands.
A step that stops resolving goes to repair-a-recipe: read the live page, match on role and accessible name rather than a class that will drift again next month, write the replacement into the flow file, bump version, set last_verified, replay the step, carry on, and record one line in the run record. Two attempts that do not resolve it: set last_failed, mark the check n/a (recipe step «n» unresolved), move on. Never write a selector you have not verified against the live page.
Close the tab and delete the lock at the end of this step.
strategy/themes.mdThis is the step the routine exists for. Nothing here waits on anything.
Copy the current file to archive/strategy/themes-YYYY-MM-DD-pre-refresh.md. The path under archive/ mirrors the path the file came from, so every backup lands in one place. Never overwrite an existing backup: if the name is taken, append -2. The member has to be able to read the exact taxonomy that produced last month's numbers, whatever you do next.
| Verdict | What you change | What you never touch |
|---|---|---|
behaving as written | Nothing. No edit, no tidy, no rewording | Everything |
not enough evidence | Nothing at all | Everything |
no volume, first month | Nothing. Record it in volume_history | Everything |
retire | status: to retired, plus retired: and retired_reason: | The id, which is kept forever |
split | The original block's definition: and matches: narrow to the larger group. One new block, with a new id, for the other group | The original id, and every other field |
merge into «id» | The surviving block gains the other's match phrases. The other block is retired with merged_into: | Both ids, both of which are kept |
severity rule wrong | The named rule under ## Global severity rules, or the theme's own severity rule: line where that is what fired | Every other rule |
Change one thing per theme per month. A theme that is split and has its severity rule rewritten in the same month has had two variables moved at once, and next month's evidence cannot tell you which one worked. Where both are true, take the severity rewrite first: it is the one costing the member customers, and the split will still be there in four weeks.
Never rename a theme id. Never reuse a retired one. Never renumber anything.
Every line already in tickets/tickets.jsonl carries the theme id it was graded with, and so does every macro filename, every help draft filename, every theme field on a desk card, and every row in every Friday report ever written. A reused id silently rewrites a month of history, and it does it with no error anybody will ever see: the report simply starts counting old tickets under a theme that did not exist when they arrived.
So:
A retired theme keeps its id forever and gains three lines:
status: retired
retired: 2026-04-30
retired_reason: no tickets in 2026-03 or 2026-04, sweep ran both months [tickets/tickets.jsonl]It stays in the file, under ## Themes, below the active blocks. It is retired, not deleted. csat-inbox-sweep still reads it and may still match a ticket to it, which is better than dropping that ticket into unclassified, and csat-deflection-desk reads the retired: line to archive its macro.
A merged theme keeps its id too and gains:
status: retired
retired: 2026-04-30
merged_into: billing-confusion
retired_reason: answered from the same macro and the phrases co-occur [macros/macro-billing-confusion.md]Old tickets are not re-themed. They keep the id they were graded with, forever, and merged_into: is what lets any reader follow them. That is the whole reason the field exists.
A split keeps the original id on the larger group and gives the smaller group a new id. It is two edits, not a rename, and it is the only honest way to keep last month's numbers meaning what they said. Where the two groups are the same size, the original id goes to the one whose match phrases the original definition described.
A newly created id is never one that has ever appeared in the file, including under a retired block. Read the whole file, including the retired blocks, before you choose a name.
A global severity rule lives under ## Global severity rules and carries an id that is written onto every ticket it grades. Rewrite the rule, never the id. The id is on a month of ledger lines exactly as a theme id is.
What you may change:
Write the change with the evidence in the file itself:
- question: normal. A question about something they have already paid for, unless
they have written about it before, in which case second-contact takes it to high.
previously: question: normal. A question about something they have already paid for.
changed: 2026-04-30, 4 tickets graded normal by this rule took four or more
exchanges and 2 ended in a remedy [tickets/tickets.jsonl]The previously: line is the undo and it is required. One line, the exact replaced text, so the member reverts by deleting your version and unindenting theirs. A change with no recorded previous value cannot be undone in one line, and one line is the whole correction mechanism this kit offers.
strategy/themes.md carries a heading ## Severity rules confirmed. When the member writes a date under it, csat-inbox-sweep stops rendering its full triage reasoning for every ticket and renders only the ambiguous ones.
When you rewrite a global severity rule, clear that date. Replace it with one line naming the date you cleared it and which rule changed:
## Severity rules confirmed
cleared 2026-04-30 by csat-taxonomy-refresh, the rule `question` was rewritten.
Write a date here again when you are happy with it.The sweep reads the heading, finds no date, and resumes rendering its full triage block from its next run. The member sees the new rule working on real tickets, in full, exactly as they did in week one, and confirms it again in one line.
You may clear this and you may never write a date under it. Clearing narrows what is allowed: it puts more reasoning in front of the member. Writing a date would widen it, and a routine that could confirm its own rules would be a routine that never gets corrected. That asymmetry is the rule, and it is the same one that governs every self amendment in this kit.
Do not clear it for a theme change, a merge, a split, a retirement, or a new theme. Only a global severity rule rewrite clears it.
Write the whole file to state/themes-candidate.tmp.md, run the check, then rename it into place:
node "«CSAT_ROOT»/scripts/copy-check.mjs" --file "«CSAT_ROOT»/state/themes-candidate.tmp.md" --dest strategy --jsonThat is the only call shape. --dest is one of email, dm, form, strategy, dashboard, plain. There is no --profile, no --destination, and no bare positional path. Where shell.run is unavailable, apply the same rule set in the agent and mark the run record copy-check: in-agent.
A non-zero exit: restore the backup from 6.1, do not attempt a fix, do not edit around the check, and record status: "failed" with the reason class the script named in blockers[]. The script is the judge and you do not argue with it. A malformed taxonomy file poisons every grade the sweep writes tomorrow morning, because the sweep reads the whole file or none of it.
Three things will fail it if you are careless.
retired_reason: line.4 tickets took four or more exchanges [tickets/tickets.jsonl] passes. 4 tickets took too long fails and says less.« or ». The two guillemet sentinels this kit allows are both addressed to the member inside a draft and neither belongs in a strategy file. A match phrase is written as a bare phrase and never as a marker.Delete the temp file in this step.
Append to strategy/CHANGELOG.md, newest at the top, in the contract's format:
2026-04-30 | csat-taxonomy-refresh | strategy/themes.md | rewrote severity rule question, 4 under gradings ended in remedies or four exchanges | tickets/tickets.jsonl
2026-04-30 | csat-taxonomy-refresh | strategy/themes.md | retired onboarding-confusion, no tickets in 2026-03 or 2026-04 | archive/strategy/themes-2026-04-30-pre-refresh.md
2026-04-30 | csat-taxonomy-refresh | strategy/themes.md | created invite-email from 6 unclassified tickets sharing that phrase | tickets/tickets.jsonlOne line per change, each carrying the evidence path. A change with no evidence path does not get made. Where the change replaced text, the line names the pre-refresh archive path so the member has both the one line undo in the file and the whole file undo in the archive.
csat-desk-standup reads this changelog every morning and puts each line under Waiting on you in the brief. That single line is the whole review mechanism, and it is why this kit needs no proposal file: the member reads what changed, and if they disagree they revert the previously: line or write one line in this routine's ## Corrections.
Before you write anything, read themes{}.verdict_history and rules{}.mismatch_history.
The same verdict for the same theme is not acted on twice in a row without new evidence. New evidence means new lines in tickets/tickets.jsonl for that theme inside this window. A new month on the calendar is not new evidence.
Detect a hand edit. themes{}.content_hash holds the hash of each theme block as you left it last month. If the hash on disk does not match, the member edited that block themselves. Their text is the new baseline: do not revert it, do not reapply a change you already made to it, note it in one line in the run record, and judge the block as it now stands. The same rule applies to a rule the member rewrote by hand.
You never write desk/desk.json or desk/DESK-BOARD.md. You append to desk/inbox.jsonl, which csat-desk-standup folds each morning, assigning ids and advancing its own cursor. One line per card, append only, never edited, never rewritten.
{"proposed_by": "csat-taxonomy-refresh", "proposed_on": "2026-04-30",
"reason": "the severity rule question was rewritten after 4 under gradings, the confirmation was cleared",
"card": {"title": "Read the new triage reasoning and confirm the severity rules again",
"type": "verify", "done_kind": "member-action",
"owner": "member", "depends_on": [], "needs": ["strategy/themes.md"],
"due": null, "not_before": null,
"definition_of_done": "a date is written under ## Severity rules confirmed in strategy/themes.md",
"artifact": "strategy/themes.md", "status": "todo", "blocker": "",
"done": false, "done_on": null, "next": false, "worked": [], "notes": [],
"url": null, "channel": null, "theme": null}}What you file, and nothing else:
| What you found | type | done_kind | owner |
|---|---|---|---|
| A global severity rule was rewritten and the confirmation cleared | verify | member-action | member |
| A new theme was created and has no macro | macro | local-artifact | csat-deflection-desk |
| A theme was retired and its macro and help draft need archiving | macro | local-artifact | csat-deflection-desk |
A theme came back n/a (sweep recorded «n» of «m» runs) two months running | research | local-artifact | csat-desk-intake |
| A ticket carries a theme id no block holds | research | local-artifact | csat-desk-intake |
tickets/tickets.jsonl or strategy/themes.md was missing | research | local-artifact | csat-desk-intake |
Only one of these is member-action and it is the confirmation. Everything else this routine produces is a file another routine finishes, and a card asking the member to acknowledge a file that is already working is noise.
Dedupe before every append. Check cards_filed[] in your state, then desk/desk.json for an open card with the same definition_of_done. If either has it, do not file again. Append to cards_filed[] as {"reason", "filed_on", "title"} the moment you write the line. A theme that has been untestable for four months should be one card ageing on the board, not four cards.
In this order, so a crash late in the run still leaves the record straight.
1. State. window_end_last_run set to today, ledger_cursors{} set to the current line counts, themes{} with this month's verdict_history, volume_history, content_hash for every block as you left it, last_changed, retired, and merged_into, rules{} with this month's mismatch_history, browser_checked[], cards_filed[], quarantines[], progress[], assumptions[], recipes[], and budget_minutes_used. Temp path plus rename.
window_end_last_run is written only after strategy/themes.md has landed on disk. A run that dies between the judgement and the write must not consume a month of evidence nobody acted on.
2. Check the four invariants. If any one does not hold, the run is a failure regardless of what else it produced.
strategy/proof-inventory.md, or it carries its ledger path in brackets instead.csat-taxonomy-refresh and this period.3. Delete state/browser-lock.json if you took it and Step 5 did not already. Same block as the record, so a later edit cannot separate them.
4. Append exactly one run record through runlog.append, and only through it. Where the shell mangles an inline JSON argument, write the record to state/run-record.tmp.json, pass it by file, confirm the last line of runlog.jsonl parses, and delete the temp file.
{"routine":"csat-taxonomy-refresh","period":"2026-04",
"start":"«ISO START»","end":"«ISO END»",
"status":"ok",
"outputs":["strategy/themes.md (9 themes, 1 created, 1 retired, 1 rule rewritten)","archive/strategy/themes-2026-04-30-pre-refresh.md","strategy/CHANGELOG.md (+3)","desk/inbox.jsonl (+2 cards)"],
"blockers":[],
"notes":"window 2026-04-01 to 2026-04-30; 9 themes judged, 3 below the evidence floor; rule question rewritten on 4 under gradings, confirmation cleared; 14 unclassified, 1 cluster cleared the floor on the phrase invite email, 2 did not; no split or merge, evidence thin both ways"}Every field is required. outputs and blockers are always arrays, empty rather than absent. Paths are relative to «CSAT_ROOT» and carry a count in brackets. notes is one line.
Report the count you actually read, never the count you expected. If a theme had four tickets and you meant to judge it, the number is four and the verdict is not enough evidence.
What the run record carries: theme ids, rule ids, counts, verdicts, floors, ticket id counts, file paths, cards filed, and the reason anything was skipped.
What it must never carry: a customer name, an account slug, a quote, a fragment of a quote, a source URL from a ticket, a money amount, or any credential. strategy/themes.md holds match phrases and it stays inside «CSAT_ROOT».
What you refuse to report, in any file:
Where you do not know something, the legal vocabulary is: n/a (<reason>), not enough evidence, no volume, not tracked, stale (<date>), baseline month.
The status vocabulary is closed at eight values and no ninth exists.
Stop, record, and exit:
| Condition | Status | What you still do |
|---|---|---|
No SCHEDULE.md row for csat-taxonomy-refresh, or it will not parse | failed | Nothing else. Name the missing row |
clock.local has no route | failed | Nothing else. Never assume a timezone |
CONTRACT.md or ROLE.md unreadable | failed | Nothing else |
| Wrong day, or outside the window | skipped-out-of-window | Nothing. Correct behaviour, not a fault |
| This period key is already recorded | skipped-already-ran | Nothing. Correct behaviour, not a fault |
tickets/tickets.jsonl missing or too damaged to fold | partial | Change nothing at all. One research card. Last month's taxonomy stands |
strategy/themes.md missing | partial | Change nothing. One research card naming csat-desk-intake. Never write the first taxonomy |
The rewritten file fails copy.check | failed | Restore the backup from 6.1. Do not attempt a fix. Name the reason class |
runlog.append has no route at all | none possible | UNRECORDED RUN at the foot of brief-latest.md, then stop |
| Budget reached | partial | Apply every change already decided, leave every theme not reached untouched, theme cursor in notes |
Degrade, repair, and carry on:
| Condition | What you do |
|---|---|
| The sweep ran on fewer than the worked fraction of its fires | Step 2.5. Severity rules may still be rewritten. Retire, merge, and split nothing |
| A theme is below the evidence floor | not enough evidence with the count and the floor. No change of any kind |
| A rule is below the outcomes floor | n/a (outcomes floor). No rewrite |
| Under grading found but below the threshold | Record it in mismatch_history. Rewrite on the second consecutive month |
| Over grading found | Record it. Rewrite only at double the threshold, and say so in the changelog line |
| The evidence says both split and merge | not enough evidence. Name both readings. Look again next month |
| A theme has zero volume for one month | Record it. Retire nothing on one month |
| A ledger line will not parse | Quarantine it with its line number, rebuild the index, mark any dependent count n/a |
| A theme block will not parse | Repair it from the schema, one assumption. Never delete a block |
| Two blocks share an id | Keep the first, give the second a new id, changelog line, judge both |
| A ticket carries an id no block holds | Count it against no theme, name it, file one research card. Never re-theme it |
| No browser control capability configured | Skip Step 5, mark those checks n/a, leave any theme that turned on one unchanged, record partial |
| Another routine holds the browser mutex | Same as above, with the holder named |
| A login wall on a source page | login-wall. Enter nothing, never retry differently. Every other verdict stands |
| The flow file does not exist | learn-a-recipe, read only. Carry on in the same run |
| A flow step stops resolving | repair-a-recipe, one repair, replay. Two failures: last_failed, mark the check n/a |
| A surface marks items read on open | Do not open it. n/a (surface marks items read on open) and leave the theme unchanged |
A block's content_hash does not match | The member edited it. Their text is the baseline. Note it, judge it as it stands, revert nothing |
| A transient tooling error | retry, class one. Once or twice, flat, no backoff |
| A refusal, a wall, or a captcha | retry, class two. Never retried, never routed around |
Two things stay outside repair, because they are the first guardrail wearing different clothes: a ticket state or a setting on a platform this kit did not create, and anything on the far side of a reply, resolve, or spend control. Those are named, never touched.
| Recipe | Where it applies |
|---|---|
tab-hygiene | Step 5. Your own tab, reused, closed at the end of the step |
read-a-page | Every source page, before you believe a word of it |
verify-the-query | Any filtered queue or sorted list you land on |
click-an-element | Navigation and disclosure only. Never a control that changes a ticket's state |
read-without-marking-read | Step 5, read off strategy/channels.md as the reason a surface is not opened. Never re-run by this routine |
read-linkedin | Read only, where a community ticket sits there. No reaction, ever |
human-pace | Step 5. The delays and the per run caps |
batch-a-round-trip | The read calls in Step 5 |
login-wall | A wall, a checkpoint, a captcha, or a consent gate |
retry | Anything that comes back wrong |
learn-a-recipe | recipes/theme-evidence-read.json, the one flow you own, read only |
repair-a-recipe | A step in a flow you own that stops resolving. Never one owned by another routine |
The rule from that file that governs this run more than any other is the third of its five: never invent what you did not read. Here that means every verdict traces to a count out of a ledger or a page you loaded this run, and a theme you have a hunch about but no evidence for is a theme you leave alone.
Six mechanisms make a second run harmless.
last-weekday burst across several days reduces to exactly one run.window_end_last_run is written only after the file lands on disk, so a run that dies mid judgement leaves the month's evidence uncounted rather than counted and unacted on.verdict_history and mismatch_history, checked before every change, so the same verdict is never acted on twice without new ledger lines.content_hash per block, so a block you already changed, or the member changed, is recognised rather than changed again.cards_filed[] plus a read of desk/desk.json, checked before every inbox append.Running twice in one month produces exactly one rewritten file, exactly one archive copy, and the same verdicts. A second run changes nothing, and it also breaks nothing.
csat-inbox-sweep is your evidence and your first consumer, and the loop between you is the tightest in this kit. It grades every ticket against the file you write and records severity_rules, severity_words, severity_alternative, and severity_rejected_because on every line. Those four fields are the only reason 3.4 is possible. Your rewrite reaches it on its next run, and where you cleared the confirmation it resumes rendering its full triage block so the member sees the new rule working on real tickets.
csat-reply-desk sorts by the severities your rules produce and reaches for a macro by theme id. A theme you split is a macro that no longer fits half its tickets, which is why the split files a card for csat-deflection-desk rather than being left for somebody to notice.
csat-churn-watch uses the severity ladder for its escalation wire. A rule you rewrote upward will fire that wire more often next month, which is the intended effect and is worth naming in the run record so the member is not surprised by a busier risk digest.
csat-deflection-desk writes the macros and help drafts, and it is your best single source of merge evidence: when it skips a theme because another theme's macro already answers it, that line in its run record is a merge candidate with the evidence already gathered. It reads your retired: and merged_into: lines to archive the right files, and it reads your new themes to write the macros they need.
csat-satisfaction-report counts volume by theme every Friday and scores the product change by theme. A theme you split is two rows on next Friday's report and a theme you merged is one, and the report's week over week column will read baseline week for both, which is correct. Its notes line carries the unclassified count and the ambiguous grading count, both of which are direct inputs to Steps 3.4 and 4.
csat-desk-intake created this file on the first run and hands it to you permanently. It runs on the first weekday and you run on the last, so its monthly pass sees your work as a month old input and never overwrites it. It never writes strategy/themes.md again after its first run, and you never write any of the four files it owns.
csat-desk-standup reads strategy/CHANGELOG.md every morning and renders your lines under Waiting on you. That single line per change is how the member finds out what you did, and it is the entire review mechanism this kit has.
To the member's other AI Employees. csat-latest.md carries the theme ids and csat-desk-standup compiles it. A sibling Employee that wants to know what customers complain about reads the ids there and opens the ledger itself. You hand nothing to anybody directly, and no theme, count, or verbatim ever leaves this folder for an outbound purpose.
recipes/BROWSER-RECIPES.md, in the recipe it affects, written the same day.recipes/<flow>.json, and only in the flows you own.evidence_floor{} in your own state file, with one line in assumptions[]. A floor that lets every theme through is not a floor, and neither is one that lets none through.CAPABILITIES.md as one row among seven.## Corrections.You do not ask before editing any of them. Record one line in the run record naming what you changed, with no customer data in it.
You never author, create, or install a skill, plugin, or extension in the member's global directory. Self repair in this kit means editing this kit's own files. You may name an optional global helper as a dependency, detect whether it is present, use it when it is, and fall back to a stated route when it is not.
When this run learns something procedural that would make future runs better, edit this file now. A floor that never lets anything through, a signal that never means what it looked like it meant, a cohesion test that keeps producing the same wrong answer, a step order that mattered. Do not propose it, do not queue it, do not wait: there is no approval step here, because the harness already decides whether you may write a file and that is the right place for that control.
Replace the specific block that was wrong and nothing else. Never rewrite this file whole, never reorder it, and never touch Step 0, the two guardrails, or the ## Corrections section, which is the member's. Append one line to «CSAT_ROOT»/improvements/CHANGELOG.md carrying the date, the trigger, and the full text you replaced, because that line is the member's undo. Put one short string in the run record notes naming the change.
Never write an amendment that relaxes the two guardrails, the rule that a theme id is never renamed or reused, the rule that old tickets are never re-themed, the evidence floors, the rule that you may clear the severity confirmation and never write one, or the rule against writing a number that is not in strategy/proof-inventory.md. A run drafting such an edit has found a defect in its own reasoning, not a new permission. Write the reasoning into assumptions[] and change nothing. A self edit can make allowed work better. It can never widen what is allowed.
You are the only writer of this file, and you never edit another routine's SKILL.md.
If this routine concludes its own window or cadence is wrong, change its row in SCHEDULE.md, re-register its job, and record both values in the changelog.
Follow CONTRACT.md section 9 exactly. This run sends a push only if it recorded one of the four blocker classes in section 9.1, only inside the member's working hours, only if state/pushes.jsonl does not already carry that open blocker_key, and never on a first run. A rewritten severity rule never earns a push, however important it is: it is the brief's job on the next working morning, and it lands there through strategy/CHANGELOG.md under Waiting on you. Everything else this run found goes in the brief and nowhere else. Never put a theme name, a match phrase, a customer quote, or any credential fragment into a push, because it renders on a lock screen. If notify.push has no route, write push: not available in notes and carry on: that is a normal outcome, not a failure.
Dated entries the member adds, newest at the top. Format: YYYY-MM-DD: what was wrong, what to do instead. This routine reads this section at the top of every run and every line here outranks the guidance above, with four exceptions that nothing overrides: the two guardrails, the rule that a theme id is never renamed or reused, the rule that old tickets are never re-themed, and the rule that this routine may clear the severity confirmation and may never write one.
© markfulton, MIT. Rendered from Markdown: HTML in the file is shown as text, images as links, and headings moved down two levels. Raw file
Just SKILL.md in employees/customer-satisfaction-employee/routines/csat-taxonomy-refresh of markfulton/ai-employees.
Open the folder on GitHubat commit 37bfe17
Csat Taxonomy Refresh next to the 5 skills that share the most tags, products or categories with it. Stars are the repository's; “used in” counts other GitHub owners with a copy.
| Skill | Stars | Used in | Tokens | Auto-check | Licence | Repo updated |
|---|---|---|---|---|---|---|
| Csat Taxonomy Refresh this skillmarkfulton/ai-employees | 498 | — | ~18k | Automated safety check: Pass | MIT | |
| Review Analysisliangdabiao/amazon-sorftime-research-MCP-skill | 953 | 1 repos | ~2.5k | Automated safety check: Pass | None | |
| Bggg Data Amazonbinggandata/bggg-skills | 604 | — | ~1.4k | Automated safety check: Pass | MIT | |
| Zsxqunnoo/zsxq-skill | 304 | — | ~3.8k | Automated safety check: Pass | MIT | |
| Roadtrip NavigatorWaybox-AI/roadtrip-skill | 126 | — | ~3.4k | Automated safety check: Pass | MIT | |
| Always Compareai-analyst-lab/ai-analyst | 304 | — | ~1.4k | Automated safety check: Pass | MIT |
liangdabiao/amazon-sorftime-research-MCP-skill
对亚马逊商品评论进行深度分析,自动识别产品痛点、分析退货原因,生成改进建议和客服回复模板。Invoke when user uses /review-analysis command with a product ASIN.
binggandata/bggg-skills
Collect Amazon.com written product reviews at scale through Woot's public review AJAX route, retain every attempt and error log, reconcile partial runs, and normalize exact review text into…
unnoo/zsxq-skill
知识星球 CLI(zsxq-cli)与底层接口完整操作指南,涵盖星球和内容管理、Skill Pay 微信支付场景。当用户提到知识星球、zsxq、小密圈、星球、登录/认证、发帖、评论、回答、编辑、删除主题、定时发布/定时任务/定时回答、投票、问答主题、markdown 正文、AI…
Waybox-AI/roadtrip-skill
Generate North American road-trip itineraries as a map-first, offline-friendly single-file HTML page.
ai-analyst-lab/ai-analyst
Never present a metric or number in isolation; anchor every number to a comparison (prior period, benchmark, or another segment) or state that none is available.
gustavscirulis/snapgrid
Generates an Apple-compliant account deletion flow with multi-step confirmation UI, optional data export, configurable grace period, Keychain cleanup, and server-side deletion request.
markfulton/ai-employees
Hire one of the eight open source AI Employees (GTM Engineer, SEO/AEO, Web Dev, Social Media, Ad Manager, Sales, Customer Satisfaction, Chief of Staff) into a local folder, prove it runs, and hand…
markfulton/ai-employees
Runs once by hand on the first day and once a month after that.
markfulton/ai-employees
Weekdays. An agent skill from markfulton/ai-employees.
markfulton/ai-employees
Weekdays. An agent skill from markfulton/ai-employees.
markfulton/ai-employees
Weekly, on a Friday, read only everywhere. An agent skill from markfulton/ai-employees.
markfulton/ai-employees
Monthly, on the last weekday. An agent skill from markfulton/ai-employees.
Categories
Monthly on the last weekday, light browser lane. An agent skill from markfulton/ai-employees. Csat Taxonomy Refresh is an agent skill from markfulton/ai-employees. Monthly on the last weekday, light browser lane.
Csat Taxonomy Refresh fits situations like: tasks that involve Customer feedback analysis.
Run `npx skills add markfulton/ai-employees --skill csat-taxonomy-refresh -a claude-code`. Or copy the skill folder (employees/customer-satisfaction-employee/routines/csat-taxonomy-refresh in markfulton/ai-employees) into .claude/skills/csat-taxonomy-refresh in your project. Claude Code loads it when a task matches its description.
Run `npx skills add markfulton/ai-employees --skill csat-taxonomy-refresh -a codex`. Or copy the skill folder (employees/customer-satisfaction-employee/routines/csat-taxonomy-refresh in markfulton/ai-employees) into .agents/skills/csat-taxonomy-refresh in your project. Codex loads it when a task matches its description.
Cursor, Gemini CLI, GitHub Copilot and OpenCode also load SKILL.md folders. With the skills CLI, run `npx skills add markfulton/ai-employees --skill csat-taxonomy-refresh -a cursor` (or -a gemini-cli, github-copilot or opencode for the others). To copy it by hand, put the folder in .cursor/skills/csat-taxonomy-refresh, .gemini/skills/csat-taxonomy-refresh, .github/skills/csat-taxonomy-refresh and .opencode/skills/csat-taxonomy-refresh in your project.
Going by SKILL.md and its folder, Csat Taxonomy Refresh needs the command-line tools its instructions call (node).
SKILL.md contains no URLs. Any network use would come from the scripts or tools the agent runs. This is read from the text; nothing was executed.
Our automated static check of SKILL.md found no risky patterns, such as piping downloads into a shell, reading credential files or hidden Unicode. It is not a guarantee. Review the folder before installing.
Csat Taxonomy Refresh is published under the MIT licence (the repository's licence). It allows redistribution, so the full SKILL.md is shown on this page.
About 18k tokens (SKILL.md is roughly 71k characters). Agents keep only the skill's name and description in context until a task matches; then they load SKILL.md in full.
Skills that share tags, products or a category with Csat Taxonomy Refresh: Review Analysis (liangdabiao/amazon-sorftime-research-MCP-skill, 953 stars), Bggg Data Amazon (binggandata/bggg-skills, 604 stars), Zsxq (unnoo/zsxq-skill, 304 stars) and Roadtrip Navigator (Waybox-AI/roadtrip-skill, 126 stars). The comparison table on this page puts their stars, adoption, token cost, safety result and licence side by side.
markfulton (a GitHub user) maintains it in markfulton/ai-employees, which has 498 GitHub stars. The repository holds 60 skills in this directory. The repository was last updated on October 7, 2026.
Source: markfulton/ai-employees on GitHub. Facts on this page come from the repository at the commit we read; the author's words are quoted as theirs.