State: to-be-approved. Deploy-verified against a live workspace: not yet. Treat Done when
below as the acceptance test and review cargo-ai cdk plan before deploying. Make no outcome claim
for this skill until it is approved.
The outcome
What happened today stops living in someone's head until Monday. Every evening one agent runs, and three things land:
- The raw dump. A committed script writes git commits, pull requests, and the
cadence files named for that day into
cadence/log/raw/standup/<date>.md. That is the evidence, and it is never edited by the agent. - The log entry. The agent reads the dump, the workspace (runs, usage, what is
deployed, through read-only
cargo-aicalls), and whatevercadence/already holds (call entries, carryover, metrics if you have them), and writescadence/log/<date>.md: what moved, what is stuck, what is worth remembering. - The Slack digest. The same recap, cut to what a teammate reads in fifteen
seconds, posted through Cargo's
slack.postMessage— not a Slack token, not a GitHub Action. The channel is locked on the action so the agent cannot pick a customer shared channel.
Then it opens one pull request and stops. A human merges the log. The Slack post goes out from the run itself, the way the team actually reads the day; the pull request is the reviewable record.
This is a scheduled CI workflow plus a hosted agent plus a Slack bot, collapsed
into resources declared in the same project as everything else the workspace
runs. The schedule, the repository binding and the Slack wiring live in
infra/standup/agents/standup.ts, and the instructions beside it in
standup.prompt.ts. harness: "claudeCode" is what buys the working tree: the
output of a day is a diff across markdown files, and only an agent with a
checkout can produce one.
Three properties make it safe enough to run unattended:
- The collection is deterministic. The agent does not fetch PRs.
scripts/collect/day.tsdoes, the same way every evening, and the agent is told not to improvise that step. Workspace data (runs, usage, what is deployed) comes from named read-onlycargo-aicommands at recap time, not from a second fetch inside the collector. - The pull request is the gate for the log. The agent has repository write access
and Slack
postMessageto one locked channel. It cannot email anyone, cannot touch the CRM, and cannot merge itself. - The channel is locked.
channelIdsits in the action'sconfig, the same waymailboxUuidis locked onsendEmail. Leave it as a field the agent fills and a mistype posts the internal recap into a customer channel.
Example
Every weekday at 6pm, recap what moved on our GTM initiatives into the repo and post the digest to #gtm-daily.
Illustrative output, fictional records:
:racing_car: *GTM - Tue Oct 6*
_Mid-market expansion moved; EU outbound is still not sending._
:dart: *Expansion into mid-market*
• Northwind signed, closed two weeks ahead of the forecast date (#318)
• Five Fabrikam dossiers drafted; Dana sends them Thursday
:dart: *EU outbound*
• Blocked: the sourcing play has not run since Friday (#312)
:zzz: *No movement:* Partner channel
:raising_hand: *Needs a human*
• Dana: approve the EU sourcing budget so the play can resume
Full log: https://github.com/northwind/gtm/pull/320
The team reads the day in fifteen seconds in Slack, while cadence/log/2026-10-06.md lands through the
"[cadence] log 2026-10-06" pull request for a human to merge.
Put it in your project
This folder is a worked example: real CDK resources written for some other company. The job is
to end up with the code your company would have written, in your project, and an agent does the
adapting. If the cargo-cdk skill is in your session it carries the long form of this; if not, this
is enough.
- Install it — the CLI does the copy. From inside the CDK project,
cargo-ai cdk add cookbook/standupwrites this example toinfra/standup/(resources andscripts/) and this procedure to.claude/skills/standup/. No project yet?cargo-ai cdk init <dir> --cookbook standup && cd <dir> && npm installdoes both; this folder never ships a shell. If you are reading this from the project's.claude/skills/, the install already happened — start at step 2. On a CLI too old to haveadd, copy this folder in as a sibling of what is there by hand; everything below is unchanged. - Reconcile it with what is already declared. If the project already has a GitHub connector, a
Slack connector, or an agents folder, rewire the imports to the existing one and drop the copy;
two resources with one slug is a collision at deploy. Append this folder's env needs to the
project's
.env.example; never overwrite it. - Point Slack at your channel. Authorize the Slack connector if the workspace does not have
one (
cargo-ai cdk add connector/slackopens the OAuth consent). Then setchannelIdininfra/agents/standup.tsto a channel id (C…) from that connector's channel autocomplete, invite the bot, and setSTANDUP_TITLEto the short name that should appear in the header.references/digest.md(installed beside this file) is the digest shape. - Adapt. Work the sections below in order: What should not change is what you argue back
about (say what breaks, then do it if they still want it); What you can change is what you
offer unprompted (nobody asks for a variant they do not know exists); What you will be asked is
the floor, and you derive before you ask. If you are asking more than about four questions you
have skipped lookups. Record what you changed and why under a
## Decisionssection in your copy of this file. - Run the collector by hand once, then plan.
npx tsx scripts/standup/collect/day.ts --dry-runfirst: it prints the dump and writes nothing. Then drop--dry-run. If it does not produce a raw file locally it will not produce one on a schedule, and that is far cheaper to find out now. Thennode --import tsx evals/contract.mjsfrom this skill's folder, thennpm run check && cargo-ai cdk planfrom the project root, show the diff, and deploy only on an explicit yes:cargo-ai cdk deploy. Nevercdk init --forceinto a non-empty directory. - Verify. Walk Done when line by line and report each with evidence. Deployed cleanly and
produced nothing is the normal failure — and the second normal failure is a Slack post to the
wrong channel, so read
channelIdout loud before you call this done.
What you will be asked
Derive before you ask. An input with a lookup is looked up, not asked. Only the ones marked asked genuinely live in the operator's head.
| Input | Kind | How it is answered | Why it matters |
|---|---|---|---|
repository binding (infra/agents/standup.ts) |
value | derived: leave repository, defaultBranch and connector unset and plan fills them from the git origin of the checkout, taking the GitHub connector from the project's own. cargo-ai cdk check prints what it resolved: confirm the line reads your repo and ./. |
This is the working tree the harness clones and the only place its log can land. An owner/name written by hand is the one value nobody notices is wrong until a pull request opens against a stranger's repository. |
Slack connector (infra/connectors/slack.ts) |
value | derived: cargo-ai connection connector list shows whether one is authorized; if not, cargo-ai cdk add connector/slack opens the OAuth consent. The declaration is default: true because a deploy cannot mint an OAuth grant. |
It is the agent's entire post path. Without it the run writes the log and has nowhere to send the digest. |
channelId (infra/agents/standup.ts) |
asked | the Slack channel id (C…) the digest is allowed to land in. Read it from the connector's channel autocomplete, not by guessing a name. Invite the bot. |
Locked on postMessage so the agent cannot pick a customer shared channel. A name (#general) collides; an empty lock posts nowhere useful and looks like success. |
STANDUP_TITLE (infra/agents/standup.ts) |
asked | the short name in the Slack header (:racing_car: *GTM - Sat Aug 1*). A founder-facing label, not the GitHub slug. |
Two standups in the same channel are told apart from the first line. A repo name here reads as infrastructure, not as the GTM day. |
GitHub connector (infra/connectors/git.ts) |
value | derived: cargo-ai connection connector list shows whether one is authorized; if not, cargo-ai cdk add connector/github opens the OAuth consent. The declaration is default: true because a deploy cannot mint an OAuth grant. |
It is the agent's entire write path into the repository. Without it the run does the work and has nowhere to put it. |
LLM connector and model (infra/connectors/anthropic.ts) |
value | derived: cargo-ai connection connector list shows whether an Anthropic connector is authorized; if not, cargo-ai cdk add connector/anthropic takes the key. default: true because a deploy cannot mint one. Any Anthropic model pairs with claudeCode; the agent's languageModel is a placeholder to set. |
A harness does not bring its own model — it runs against Cargo's LLM proxy, so this is what the nightly run is billed and metered against. Omit either and defineAgent throws at plan; pair claudeCode with an openAi connector and it typechecks green and fails at deploy. |
| cadence paths | value | derived: read cadence/README.md and ls cadence/log/ for what already exists |
The agent writes into a layer humans already curate. A second parallel folder splits the record in half. |
Checked before moving on, not after the deploy:
- the collector was run by hand once and wrote a real raw file
cargo-ai cdk checkprintsagent:standup bound to <your repo>#<branch>with no trailing subdirectory — the repo is the one holdingcadence/, and the GitHub grant can push to it. A trailingin infra/is the failure to catch here: it roots the harness where there is no node_modules, so the collector cannot runchannelIdis aC…id the Slack connector can see, and the bot is in that channelscripts/standup/package.jsonis present in the project after the installnode --import tsx evals/contract.mjspasses
What you can change
The code is a worked example. These reshapes are expected, and the agent offers them rather than waiting to be asked. Every one costs something; that is what makes it a variation and not the default.
| Variation | When it is right | How | What it costs |
|---|---|---|---|
move-the-hour |
The team reads Slack at a different time, or you are not on Pacific time | Change cron and STANDUP_TIMEZONE together in infra/agents/standup.ts. The cron is 05:00 UTC because that is 10pm PT in PDT. |
A morning cron recaps an incomplete day. A timezone the collector does not share with the prompt splits the dump and the log across two dates. |
skip-slack |
You want the log and the pull request, and a human will paste the digest | Drop the uses entry and the "Post the Slack digest" section of infra/agents/standup.prompt.ts |
The team stops seeing the day. The log still lands, but the thing people actually read is gone. |
header-emoji |
Two recaps land in the same channel and the reader has to tell them apart from the first line | Change :racing_car: in infra/agents/standup.prompt.ts and references/digest.md |
Cosmetic unless you pick the same emoji as another bot in that channel, in which case the two posts merge in the reader's eye. |
log-only-quiet |
A quiet day should not ping Slack | In infra/agents/standup.prompt.ts, skip the post (and say so on the PR) when What moved is empty |
Silence on a quiet day is indistinguishable from a missed run. The default is the opposite: a quiet day still gets an entry, because silence is signal. |
git-only |
The sandbox has no Cargo session, or this agent must not reach the workspace at all | Drop §1b of infra/agents/standup.prompt.ts |
The recap loses runs, usage and what is deployed, so "declared but it never ran" stops being visible. The git dump still lands, which is what §1b already falls back to when cargo-ai whoami fails. |
What should not change
However far you adapt, these hold. Ask for one anyway and the agent tells you what breaks, then does
it if you still want it, and records why under ## Decisions in your copy of this file.
- The agent does not fetch git; the script does. (
scripts/collect/day.ts) A fetch loop an agent re-derives every evening is a fetch loop that silently changes shape — a window that drifts, aghflag that quietly widens. The raw dump is the one thing here that has to be byte-identical in its rules every day, because everything downstream is diffed against it. Cargo workspace data is the other half of the day, and it is read at recap time by the agent, not by acargo-ailoop bolted into the collector. - The workspace reads are read-only, and they are the CLI. (
infra/agents/standup.prompt.ts) §1b names the commands:whoami, run counts and failures, usage, what is deployed. Nocapabilitiesblock is wired on the agent, because the read path is already in the sandbox and only the prompt can say "read, never execute".orchestration action execute,batch create,cdk deployand anything that removes are the forbidden half of that list — those spend or deploy, and Slack posting is the lockedslack.postMessageuse. Ifcargo-ai whoamifails, the recap says so and continues from the git dump; it does not invent the numbers. slack.postMessageis a connector action on the agent.channelIdis locked inconfig. (infra/agents/standup.ts) Wrapping it in a tool is ceremony, and this repo refuses those. ASLACK_TOKENscript, a GitHub Action, or achat.postMessagecurl is how the digest escapes Cargo: no run to inspect, no connector to revoke, and a token in the harness env that posts anywhere. Leaving the channel as a field the agent fills is how an internal recap lands in a customer shared channel.scripts/standup/package.jsonstays. (scripts/package.json) It is not decoration. The CDK loader imports every.tsunder the project root except directories carrying apackage.json; delete it andcargo-ai cdk planimports the collector and runs git/ghon every plan.- The harness root stays the directory holding the
package.jsonthat declares@cargo-ai/cdk. (infra/agents/standup.ts) That is wherenode_modulesis, so it is the only placenpx tsx …/collect/day.tsresolves — and in the scaffolded layout it is the repository root, which is also wherecadence/lives. Check it, do not assume it:cargo-ai cdk checkprints the resolved binding, and a line endingin infra/means the harness was rooted where there is no package.json and no node_modules. The collector then cannot run at all, and the evening reports clean and empty. On a CLI old enough to resolve it that way, pinrootDirectory: "."in the repository block until you upgrade. - The agent opens a pull request and never merges it. (
infra/agents/standup.prompt.ts) The log is what next Monday's plan is written from. Remove the review gate and a hallucinated number, a vendor call misread as pipeline, or a stuck item filed against the wrong owner becomes the team's memory of the day. - One log file per calendar day, and this run owns only its three sections.
(
infra/agents/standup.prompt.ts) Several runs write the same day's file. Add What moved / What is stuck / Worth remembering, never rewrite a section you did not write, and stop when those three are already there — that is the idempotency key. Matching on the Slack channel instead needs aconversations.historyread Cargo does not ship, and a sibling bot's post in that channel reads as "already ran" — the standup then skips silently for nights at a time. - The day is reported against the active initiatives, in a founder's words.
(
infra/agents/standup.prompt.ts) Every active initiative gets its block in the log and a line in the digest, including the ones that did nothing that day. Group by PR or by resource instead and the digest turns into a changelog: activity that reads as progress while a bet quietly stalls. The one exception runs the other way: a churn, a deal, a failing play or a spend spike that no initiative claims goes first, as "Outside the plan", rather than being squeezed into Engine upkeep. The voice rules (verdict first, plain words, no file paths) are what make it readable by someone who was not in the repo. - Do not invent a number. (
infra/agents/standup.prompt.ts) Drop a metrics line rather than carry yesterday's ARR forward. A made-up delta is worse than no delta, because the digest is what leadership reads. A CLI read that errors is a note on the PR, not a count you fill in.
Done when
--dry-runprinted the dump, and the run without it wrotecadence/log/raw/standup/<date>.md; running it twice that evening overwrote the same filenode --import tsx evals/contract.mjspasses: harness isclaudeCodebound to an Anthropic connector, no capability is wired,postMessageis onuseswithchannelIdlocked, and no tool wraps itcargo-ai cdk planreports the agent, the two connectors and the folder, and does not run git orghwhile planning- the first scheduled run opened one pull request titled
[cadence] log <date>whose diff contains the raw dump and the three log sections, and whose body carries## Slack digestplus a posted-or-not line - the digest landed once in the locked channel, with
Full log: <PR URL>as the last line - a re-run the same evening opened no second pull request and posted no second message
- every
status: activeinitiative appears in the log and in the digest, either in its own section or on theNo movementline, and anything that big outside the initiatives leads the post asOutside the plan - a quiet day still produced an entry that says so
- no number in the digest is absent from the raw dump, a metrics file dated that day, or a
cargo-airead the agent actually ran. A CLI read that errors is a note on the PR, not a made-up count
What it costs
The collector talks to git and, when gh is installed, to GitHub from the harness environment.
Those calls are not Cargo connector actions.
The Slack post is slack.postMessage on the bound connector. Immediately before the plan, run
cargo-ai orchestration action list postMessage --kind connector --integration-slug slack and
read the current cost. Say that number out loud as a per-post cost; the run posts once.
The recurring cost is the harness run itself, once a day, billed as LLM tokens through the bound
Anthropic connector, and it scales with how much the agent reads — the dump, the day's cadence
files, a short window of git history, and the read-only cargo-ai calls in §1b (whoami, run
counts, usage, what is deployed). Those reads are API calls, not connector actions, so they bill
nothing. There is no per-record fan-out, and the recap never runs a command that spends.
Composes into
weekly-planning (the week's logs are what it ranks against infra and runs).