ops
Skill4 starsChanged 9 days agoMITView on GitHub
Process meeting content into structured documentation -- summaries, decision tracking, action propagation, file updates. Config-driven for any organization. Replaces project-ops, bravo-ops, management-ops, marketing-ops.
17 other projects use a version of this.
Adopted by
Most recently taken up by
- holon-run/holontheir own version, first seen Nov 8, 2026
- guaardvark/guaardvarktheir own version, first seen Nov 5, 2026
- jthingelstad/elixir-mcptheir own version, first seen Nov 4, 2026
- aya-taleb/biz-ready-skillstheir own version, first seen Nov 1, 2026
- vivekschaudhary/compasstheir own version, first seen Oct 25, 2026
- sethliao/one-person-studiotheir own version, first seen Oct 20, 2026
Used with
Projects with it also use status, design and 3 more.
Other files people use alongside this one.
Files found in the same public projects as a version of this one, more often than they turn up anywhere else. Each project counts once, large collections count for less, and this file's own repository is left out.
AnthropicSkillstatus
Case status summary by audience — client-facing (plain language), internal (for the professor), or court-ready (formal caption format per local rules). Same facts, different framing and depth. Use when a student needs to update the client, brief the professor, or prepare a court status report.
Also in 3 projects that use this
Microsoft AzureSkilldesign
Design rules and conventions for the API Center Portal. Use when editing detail pages, card components, badges, layout, navigation, sidebars, or headers. Covers badge styling, metadata placement, page navigation patterns, and layout alignment.
Also in 3 projects that use this
BlockSkillplan
Draft a TDD, PRD, or lightweight plan in the conversation without committing it.
Also in 3 projects that use this
SentrySkillreview
Fixture review skill.
Also in 3 projects that use this
VercelSkillsetup
Set up Vercel CLI and project configuration. Use when the user says "set up Vercel", "configure Vercel", "link to Vercel", or "vercel init".
Also in 3 projects that use this
Other versions17 projects use a version of this.
Word for word · 0
None found.
Edited · 0
None found.
Wrote their own version · 17
- guaardvark/guaardvark★ 306same name
- holon-run/holon★ 194same name
- drafthq/draft★ 40same name
- vivekschaudhary/compass★ 2same name
- vlting/claude-skills★ 2same name
- aya-taleb/biz-ready-skills★ 1same name
- Frutzmann/claude-code-setup★ 1same name
- harshsinghmp/muse-skills★ 1same name
- HDHNezherParking-cum-Y638-Intl-Ltd/titan★ 1same name
- jircdev/crew-plugin★ 1same name
7 more not listed
How matches are found. Word for word: the exact same text. Edited: shares passages with this file (“99% same” means 99% of its five-word phrases also appear here; 10% or more counts). Their own version: similar meaning, or, weakest, the same name. We can’t tell which project had it first. Projects are counted among the 200 most-starred candidates, and mentions are links to the repository we found, up to 50 per site, so both counts are a floor.
In this repository13 other files it gives its agents.
- CLAUDE.mdCLAUDE.md
- analyticsSkill
- companySkill
- correspondenceSkill
- handoffSkill
- inboxSkill
- insightsSkill
- ops-baseSkill
- outboxSkill
- preparationSkill
- tasksSkill
- transcriptSkill
- update-skillsSkill
The file
Show its rules and text
We split this skill into the rules it gives an agent
35 rules. 0 are common, 17 are distinctive, and 18 are very specific to this project.
0
Common
Most of the 14 edited versions we read keep these.
17
Distinctive
This author's own choices. Worth a look.
18
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 113
Check <vault>/_config/base.yaml for vault-wide override
- Distinctive: line 114
Read and parse each config
- Very specific: line 118
Check for project-level .claude/ops-config.yaml in current working directory
- Very specific: line 148
If ~/.claude/skills/acme-ops-config/, ~/.claude/skills/bravo-ops-config/, or ~/.claude/skills/delta-ops-config/ still exists, emit the …
- Very specific: line 149
If everything is clean, print a single Vault health: OK line.
- Distinctive: line 196
When non-conforming, the Vault Health section expands.
- Very specific: line 211
If the first word is help, present a usage guide instead of processing content.
- Distinctive: line 321
Critical rule: if the same content fits both files, it goes in the agenda file.
- Distinctive: line 355
Prepare checks what needs judgement: Read the decisions since the last agenda and ask of each: *does this change the config?* A source …
- Distinctive: line 441
After the date the card asks *demo held?
- Very specific: line 536
No roster, said (CR-101): When no people: is declared beside carry_forward, the round prints *No roster* and — where …
- Very specific: line 612
Before reporting the agenda as done, compare its ## headings against the declared order: Agenda, Status Overview, Key Updates, Reported …
- Distinctive: line 646
If the user provides pre-submitted team updates:
- Distinctive: line 666
Create the file using the Standup Preparation Template:
- Distinctive: line 760
Add "Focus Areas" section after agenda
- Distinctive: line 765
Add "Sprint Goals" or "Week Priorities" section
- Distinctive: line 767
Add "Carry-forward Items" from previous week
- Very specific: line 774
Look up meeting_types[<type>].preparation_mode in the merged config
- Very specific: line 775
If absent, default to single
- Very specific: line 825
After the meeting, when /ops [transcript] is run:
- Distinctive: line 828
No manual action needed
- Distinctive: line 835
Language: Follow same rules as normal /ops (per_claude_md, etc.)
- Very specific: line 872
Read ~/.claude/skills/ops-config/swedish_substitutions.yaml.
- Distinctive: line 878
Detect language: Treat the file as Swedish-context if any of:
- Very specific: line 890
Skip ambiguous substitutions: if --strict-no-ambiguous is set.
- Distinctive: line 897
Do not write anything
- Distinctive: line 901
Write the file back atomically (write to temp, rename)
- Very specific: line 902
Update or create a CHANGELOG.md entry in the file's parent folder (if a CHANGELOG exists):
- Distinctive: line 1059
Idempotent: If a row for this project already exists — another session got there — verify the link and do nothing else.
- Very specific: line 1133
Not automatic: No sweeper closes a project.
- Very specific: line 1148
Company profile (CR-125): After the script's blocks, the agent adds one line when the organisation has a <org>/_company/ whose company, …
- Distinctive: line 1191
Read-only version of the CR-018 pre-save template-contract check, run across a folder's existing files.
- Very specific: line 1276
Import fix (dee), - #7 Retest — dee, a blocker table's assignee cell.
- Very specific: line 1432
Resolve the ledger mode first (CR-041): Read workflows.task_ledger.mode before judging a missing _tasks.yaml:
- Very specific: line 1455
Link scope and classes (CR-137): Every .md file is link-checked, not only dated files, README.md and CHANGELOG.md -- that pattern skipped …
“Common” is measured: kept by at least 6 in 10 of the 14 edited versions of this file we could read. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Operations Framework
Unified meeting and operations processing. Behaviour is driven by config -- the same skill handles Bravo veckosynk, Acme management meetings, marketing standups, and project dev standups.
Base framework: Extends ops-base -- read ~/.claude/skills/ops-base/SKILL.md for shared standards (meeting formats, task management, workflows, archive policy).
IMPORTANT: The project CLAUDE.md is the single source of truth for vault-specific details. This skill defines the processing workflow; CLAUDE.md defines file locations and naming conventions.
SUBCOMMANDS
The subcommands are the loop's verbs (CR-089). /ops is the config layer over two of them
(prepare, process — /preparation and /transcript are their config-free forms) and owns three
more (orient, check, and declare through project new and status). normalize and help
sit outside the loop and are named for what they repair or explain.
Renamed in v1.79.0 (CR-089), old names removed in v1.81.0. brief is orient, lint is
check <folder>, sweep is check (vault), projects is project list. An old name is not
recognised: say which subcommand it became, and do not run it.
process -- the default: turn meeting content into the summary
Trigger: /ops process <content>, or /ops <content> — content with no subcommand is processed.
Runs the PROCESSING FLOW below (Steps 0.5–9). This is the loop's process step; /transcript is
the same step for a folder with no config.
When the input is incoming material rather than a meeting (CR-105) — a report, a newsletter,
an automated digest, a vendor's weekly mail — process writes a content file instead of a
summary. Same folder, same insight and changelog steps, four differences:
| summary | content | |
| Filename | YYMMDD-summary-<participants>-<topic>.md | YYMMDD-content-<source>-<topic>.md |
| Header | Participants, facilitator, duration | Sender, period covered, what it measures |
| Action table | Owners drawn from participants | Owners are ours — the sender owns nothing |
| Placement | meetings/ | not meetings/ — see below |
It does not go in meetings/. A weekly report is not an occurrence of anything; filing it
there makes the folder answer "what did we meet about?" wrongly, and every count over that folder
inherits the error. Put it where the declared verticals: path points if the config has one, else
a sibling folder in the same project.
Check the name before creating the folder. content/ is the obvious choice and is often
already taken by something else — in a marketing project it usually means material we produce,
which is the opposite of material we received. Where the name is taken, reports/ is the fallback;
the term content names the artifact, not the folder, and a collision here is worse than a
slightly different folder name.
No participant machinery runs. There is no diarisation to recover, no ASR spelling to check
against folder precedent, no owner to downgrade to ?. The sender is in a header and is correct.
Proper-noun verification (CR-016) still applies to names inside the content.
The action table is the point. A report states numbers; the value added is what we do about them, owned by us. A row owned by the sender is a routing error — they will never read this file.
When the input is a two-way thread with a person (CR-080) — someone wrote, you replied, they replied again — it is correspondence, not content and not a meeting. Content is one-way; the test is whether anyone replied.
| content | correspondence | |
| Shape | One-way: a report, a digest, a newsletter | Turns alternate between people |
| Filename | YYMMDD-content-<source>-<topic>.md | YYMMDD-correspondence-<person>-<topic>.md |
| Header | Sender, period, what it measures | Turns with time and direction, and whether a decision was reached |
| Placement | content.path in config, else reports/ | correspondence.path in config, else correspondence/ |
Never write a participant list. A thread has a sender and recipients per turn; flattening
them claims six people conversed when two did, and loses that a cc recipient was informed
rather than engaged. Only a from/to party may own an action item — never a cc.
The raw mail is not kept, and never goes in .transcripts/ (CR-080 decision 6, answered
2026-10-01). That archive is for transcripts of recordings the user took part in. A pasted mail
carries someone else's words, written to the user with an expectation about where they go. The
record keeps sender, recipients, timestamps and thread key; the wire form is discarded. A raw
email found in .transcripts/ is a finding, not a convention — it happened once, by default,
which is what settled the question.
Profiles (CR-123). process resolves the folder's profile first
(python3 ~/.claude/skills/ops-config/profiles.py <folder> --text). With no profile: it is
meeting and nothing in this skill changes. Under profile: podcast the unit is an episode: the
summary goes to episodes/ with the role keyword episode, section 1 is Follow-up
(announcements, no owners, no tasks), insights use the podcast types and voices, and index_key:
episode_index is the same index update as meetings_index. Full rules: /transcript Step 1 and
Step 3.5, which process follows.
Themes (CR-124). With themes: {enabled: true} in the config chain, process tags the summary and
its insights and updates <project>/_themes.yaml in the same step (reuse before create; /transcript
Step 3.5). /ops process --themes-backfill <project> tags summaries written before themes were
enabled: it proposes a Themes: line per summary, in date order, and writes each (summary line,
insight themes, registry entry) only after confirmation. A merge is a person's decision, recorded as
status: merged and merged_into; no step merges or deletes a theme on its own.
status -- Show available configurations
Trigger: /ops status
Parse the user's input. If the first word is status, execute this subcommand instead of the normal processing flow (Steps 1-9).
Steps:
- Scan for configs:
- Find vault root (walk up from CWD until
_inbox/,_outbox/, or.obsidian/; orVAULT_ROOTenv) - Glob
<vault>/*/_ops.yamlfor org-folder configs - Check
<vault>/_config/base.yamlfor vault-wide override - Read and parse each config
- Deprecated fallback: also scan
~/.claude/skills/*-ops-config/for legacy skill-based configs and emit a one-time deprecation warning per session if any are found (removed in v1.17.0)
- Find vault root (walk up from CWD until
- Detect active config:
- Check for project-level
.claude/ops-config.yamlin current working directory - Walk up from CWD looking for the nearest
<folder>/_ops.yaml - Determine org from CLAUDE.md
organizationfield or folder name pattern - Report which config would be loaded for the current directory
- Check for project-level
- Present report for each config:
- Organization name, language, swedish_chars
- Team members (name + role, abbreviated if >4 members)
- Workflows: update_files list, action_propagation status, agenda_management status, post_processing status
- Rolling plans: count + axes (if
workflows.rolling_plansconfigured) - Domain additions count
- Summary sections (custom or default TWO-TIER)
- Templates (if configured)
- Strings (custom or default)
- Show base defaults from
~/.claude/skills/ops-config/base.yaml
4b. Resolved config per project (CR-089). For every folder that carries an _ops.yaml or a
.claude/ops-config.yaml, print the config as it actually resolves — each key with the layer it
came from (project, folder, vault-wide, base default) — and validate it against
ops-config/schema.md. Report as findings: keys the schema does not know (usually a typo, which
otherwise silently falls back to the default); carry_forward enabled with no external_systems;
task_ledger.mode: external with no system or pointer; a retired key still set
(dashboard_refresh, ignored since v1.81.0). This is the one place a person can see what every
project is configured to do, and compare two projects that should behave alike.
- Vault health check (CR-010
conventions.single_inbox_outbox,conventions.prefix_conventions):- From the detected vault root, scan for stray inbox/outbox directories using the CR-025 fuzzy matcher (same as
/ops checkcheck 9):_inbox,_outbox,.inbox,.outbox, any*inbox*/*outbox*directory, and localized forms (inkorg*/utkorg*), case-insensitive, skipping.archive//.transcripts//.handoff//clones//node_modules/. Anything other than<vault>/_inboxand<vault>/_outboxis a stray; flag each unless listed inworkflows.sweep.structure_exemptions(exempt paths get a one-line note with their reason). Exact-name matching is not enough -- real-world strays have appeared as.inboxand_outbox-archive. - For every
<vault>/<folder>/_ops.yamlfound in step 1, the folder is treated as ops-aligned. Confirm each parses as YAML; flag any that don't. - List ops-aligned folders that are missing an
_ops.yamlonly when CLAUDE.md or_meta.yamlin that folder declaresorganization-- otherwise the folder is intentionally not ops-aligned and should be silent. - If
~/.claude/skills/acme-ops-config/,~/.claude/skills/bravo-ops-config/, or~/.claude/skills/delta-ops-config/still exists, emit the deprecation warning from step 1 here as a vault-health item too (one-line each, with theunlink/mvcommand to fix). - If everything is clean, print a single
Vault health: OKline. Only expand into a warning list when something is non-conforming.
- From the detected vault root, scan for stray inbox/outbox directories using the CR-025 fuzzy matcher (same as
Output format: Structured markdown report:
## /ops Configuration Status
### Active Config (current directory)
Organization: Acme
Config source: ~/.claude/skills/acme-ops-config/acme.yaml
Project override: none
### Available Organizations
#### Acme
Language: per_claude_md | Swedish chars: strict
Team: Bob (CEO), Carol (COO), Alex (CAIO), +9 more
Workflows:
update_files: summary, changelog, readme, task_matrix, meetings_index
action_propagation: disabled
agenda_management: disabled
post_processing: task_import (enabled), carry_forward (enabled)
Domain additions: 8 sections configured
Summary sections: default (TWO-TIER)
Strings: default (per language)
#### Bravo
Language: swedish | Swedish chars: strict
Team: Alex (Affärsutveckling), Hank (CTO)
Workflows:
update_files: summary, changelog
action_propagation: enabled -> BRAVO.md, ALEX.md, HANK.md
agenda_management: enabled -> BRAVO.md / "Nästa veckosynk"
post_processing: disabled
Domain additions: 4 sections configured
Summary sections: default (TWO-TIER)
Templates: meeting_reflection
### Base Defaults
Language: input
Team: (none -- must be defined in config)
Workflows: summary only
### Vault Health
Vault health: OKWhen non-conforming, the Vault Health section expands. Examples:
### Vault Health
- Stray `_inbox/` at acme/_inbox -- contract requires exactly one at vault root. Move contents to <vault>/_inbox/ and remove.
- Missing `_ops.yaml` in <vault>/acme/ -- folder declares `organization: Acme` but has no _ops.yaml. Drop one in or remove the organization declaration.
- Legacy skill `~/.claude/skills/acme-ops-config/` still present (CR-011, removed in v1.17.0). Run `unlink ~/.claude/skills/acme-ops-config`.help -- Show usage guide
Trigger: /ops help
If the first word is help, present a usage guide instead of processing content.
Render the working loop from working_loop in ecosystem.yaml — do not restate it here. That block
is the declaration both the README and the landing page render; a hand-written fourth copy in a help
command is the one most likely to go stale and the least likely to be caught, because a help command
is read precisely by people who cannot tell that it is wrong.
Render orientation FIRST, from orientation in ecosystem.yaml (CR-077) — do not restate it
here either. A person who types help is more often lost in the substrate than in the sequence:
where to start a session, what the folders are, what is in the context window. Present the entries
in declared order, each as its question then its answer, under a heading that marks them as
orientation rather than commands. Then the loop.
Include:
- The orientation entries, in declared order
- One-line description of what /ops does
- Available commands, in loop order:
/ops orient <folder>,/ops prepare [type],/ops process <content>(the default),/ops check <folder>,/ops check,/ops status,/ops project list,/ops project new <name>,/ops normalize <path>,/ops help - The working loop, grouped by
phasein declared order. Per step:- the
labeland what itdoes commandwhere the step has one — the thing a person actually typesconsumes→produces, because the question after which command is always what does it need and what do I get, and the declaration already holds both- where a step is
manual, show itswhy_manualin place of a command. That substitution is the most useful line in the output: it tells the reader nothing is missing - where a step is
external, say so — the archivers and the meeting itself are not skills
- the
- Config loading summary (3-line version, point to
/ops statusfor details) - Common usage patterns (examples including prepare)
Output: Markdown printed directly. No files created, and nothing is executed — /ops help describes
the loop, it never runs a step.
The declaration is checked (check-components.py): a step cannot be both manual and commanded,
cannot be silent about being neither, and cannot name a vault path nothing declares. So a command that
drifts fails the check rather than misleading a reader.
prepare -- Create pre-meeting preparation
Trigger: /ops prepare [type] [async-updates]
Generate a structured preparation document for an upcoming team meeting. When the organisation has a <org>/_company/ (CR-125), the company, positioning and strategy facets are read as standing context and cited by facet, never copied in; silent when there is none. Pulls context from recent meetings, tasks, and optionally incorporates pre-submitted async updates from team members.
The card comes first (CR-005, inherited by CR-106)
A preparation opens with the card, or it has failed at its one job. The shape is the same one
/preparation has carried since CR-005, and it applies here too:
- Top of file: at most five items, self-sufficient -- a reader who sees only this can lead the meeting
- Below a horizontal rule: everything else. Metadata, source links and rationale belong here, not above the card. A file that opens with 27 lines of provenance is not readable on a phone, whatever its own note claims
- Dual mode may declare its own card for the facilitator layer -- a facilitator needs time-boxing
and deflection cues where a 1-on-1 needs questions. Declared in
workflows, never assumed
Read the register, not only the recent meetings (CR-106, Step 2.6 in /preparation): query open
rows naming the participants, split into requires them and mentions them, and state the
remainder with a count when more rows require someone than fit in five.
Arguments:
type(optional): Meeting type hint (e.g.,standup,war-room,weekly). Default:standupasync-updates(optional): Pre-submitted text updates from team members to incorporate
Examples:
/ops prepare standup
/ops prepare war-room
/ops prepare standup [paste team updates here]Preparation modes (single vs dual)
The skill supports two preparation modes, configured per meeting type in _ops.yaml under meeting_types[<type>].preparation_mode:
| Mode | When to use | Files produced | Filename pattern |
single (default) | 1-on-1s, marketing meetings, anything where the facilitator and the participants are the same audience or there is no sensitive facilitator-only content | One file | YYMMDD-preparation-[context]-[type].md (or förberedelse for Swedish) |
dual | Group meetings (standups, weeklies, war rooms) where the facilitator needs a private layer (deflection strategies, time-boxing cues, sensitive probes, policy reminders that should NOT be visible to attendees) on top of the normal team-facing prep | Two files | YYMMDD-agenda-[context]-[type].md (visible to team -- the standard prep) + YYMMDD-facilitator-[context]-[type].md (private add-on layer) |
Default is single -- only opt into dual when the org explicitly configures a meeting type to need it. If no meeting_types config exists, fall back to single regardless of type argument.
Dual mode -- two-layer model:
The agenda file is the team-facing prep document. It looks like the prior-day prep file in that folder -- same shape, same sections, same level of detail. The facilitator sheet is a separate private layer that contains only the additional content the facilitator needs and the team should not see.
Agenda file (visible to all attendees) -- mirror the prior-day single-mode prep in the same folder:
- Status overview (per-person yesterday/done/today)
- Key updates from async chat or prior meetings
- Async updates verbatim (if any)
- Agenda items with owners and time-boxes
- Blockers table, decisions pending, open action items carry-forward
- Reference links, build status, metrics
- Anything participants need to come prepared
Facilitator sheet (private, NOT shared with the team) -- contains only the additional layer:
- Facilitator's role clarification (e.g., "Alex runs the meeting only -- not driving test or fix work")
- The lead's expectations from prior handover or 1-on-1s
- Time-box discipline cues ("standup has run 25 min recently, target 30, hard stop 35")
- "Watch for" cues during the meeting (e.g., "Dana defaulting to 'I'll check after standup' on R2 blockers -- push for concrete commitment now")
- "Things NOT to surface in this forum" list (vertical/board topics, personnel matters, etc.)
- Decisions the facilitator owns and the framing for each (esp. when filling in for someone)
- Post-standup follow-ups the facilitator drives
- Pre-meeting backstory from 1-on-1s or lunches with subset of attendees
Critical rule: the agenda file does NOT mention or hint at the facilitator sheet. The visible document must not advertise that a private one exists. Cross-references go from facilitator → agenda only, not the other direction.
Critical rule: if the same content fits both files, it goes in the agenda file. The facilitator sheet should only contain content that would change behaviour or expose sensitive context if shared with the team.
Step P0: Which agenda does this project use? (CR-082)
Run this before P1, and print the answer before writing anything.
Resolve carry_forward through the normal config chain (config() in build_agenda.py —
nearest declaration wins, walking up).
carry_forward.enabled | The agenda comes from |
| true — the loop is wired | build_agenda.py. The template in P3 is NOT used for the agenda |
| absent or false | P1–P5 as described below |
When the loop is wired:
python3 ~/.claude/skills/ops/build_agenda.py --dir <project>/meetings [--date YYMMDD]Validate the configuration first (CR-119) — part of every prepare, before anything else is read. A decision that changes the project's shape is not done until the config says so; otherwise the agenda keeps reading the old project and nobody notices until a number looks wrong in the room.
- The script checks what it can see and prints it in the sources block (
configlines) and, in card layout, on the card under Built from as Config: N finding(s): the milestone date passed, or undecided past its decide-by; a card without a milestone; a declared label the repo does not have; a declared report not found or three days old or more; a roster name absent from the last three rounds; an unknown kind in the last note; terms without a status in the project's definitions file; a milestonedate:with noset_on:, or ahistory:entry missingreasonorsource(CR-127); and, overmilestones:(CR-149), aproposeddate that passed without being confirmed or moved, an entry markedconfirmedwith nosource, two entries on the same date, and adonedate in the future. Nothing is changed by the script. - Prepare checks what needs judgement. Read the decisions since the last agenda and ask of each: does this
change the config? A source added or retired, a milestone confirmed or moved, a definition, someone joining or
leaving the meeting. Propose the edit out loud before generating, make it, then regenerate.
When a decision moves the milestone (CR-127), append the outgoing
date:tomilestone.history:with itsset_on, areasonand asourcebefore writing the newdate:andset_on:(a targeted edit, never a re-dump). The generator itself edits nothing. - State it. The generator leaves a slot,
<!-- CONFIG-REVIEW: … -->; prepare replaces it with Config reviewed against<decisions>./ops orientreports an agenda still carrying it as generated, config review not stated, after the digest (CR-101).
Card layout (CR-107, carry_forward.layout: card). The generated agenda then obeys the card
comes first (above): it opens with Milestone → Movement → Blockers → Dependencies → Decisions
needed today → Stuck? → One-minute round → Close, and writes everything else -- the Sources block,
the full carried list, Probably closed, the digest slot and the repo detail -- to a companion
YYMMDD-agenda-details-<…>.md. The meeting talks through the agenda; the details file is what it was
built from and is not read out. The chat post becomes the card too: milestone, release gap, blockers,
what is waited on, what must be decided, the round question.
| In the agenda (talked about) | In the details file (appendix) |
The milestone and days left (milestone:), or date not set; decide by …; with milestones: (CR-149) a table sorted by date, the state on every row and the days left on the next date after today | Sources and fetch status, declared reports and their age |
The movement block, with its checks (build_movement.py, live from the repo) | The full carried list with session counts |
Carried [blocker], [dependency], [decision] items | Probably closed |
| Tasks carried past the escalation threshold, as Stuck? | The digest slot (prepare fills it there) |
| The one-minute round: what moved toward the milestone, what is stuck | Repo, issue and pull-request detail |
The default (layout absent) is unchanged. In card layout the digest marker lives in the details file,
and /ops orient reads both files.
More than one date matters: milestones: (CR-149). The card ran toward one date while the project
it served had several -- a backend release landing, a go/no-go, three rollout steps after it. milestones:
is a list beside tracks:; each entry declares name, date, state, source, set_on, and CR-127's
optional decide_by, criteria and history. A lone milestone: is read as a list of one, so a project
that declares only it renders exactly as before.
state:tells a commitment from a proposal --confirmed,proposed,target,done, defaulting toproposed. The dates that prompted this lived in a plan document whose own header said every date in it was an unconfirmed proposal, and a reader meeting a bare date on the card could not tell which it was.confirmedneeds asource:-- a decision id, a plan section or a named confirmer. Without one the entry is shown asproposedand the config check says so: a commitment nobody signed is a proposal.- The card renders a table, the post a short list, both sorted by date, the state on every line, the days left on the next date after today, and a dates-only Sequence line under it. The post is pasted into a chat, which is why it is a list and not a table there.
- Deciding the dates is not the skill's. It shows what has been set and by what; confirming a plan's dates is the team's.
Refined after the first meeting run on it (CR-114):
- A blocker is a release blocker. With
labels:declared, the Release blockers section lists the open issues carrying the top priority label (the label is the definition), and the room confirms the list and the plan for each. Carried[blocker]lines without a label follow as Other blockers carried. A person waiting on another person is a[dependency]— dialogue, raised in the room, rarely in any tracker. Someone's own pending work is a task, not a dependency. - Five to ten minutes when nothing blocks. The card says so under its title. Statistics are read offline; each report's owner summarises it in their minute (the round names it for them).
- One current status per area, at a stable path (
reports[].current). A dated file per day is history, not a source: when today's is missing, nothing says what to rely on. The card shows each source's age, and a dated series with no stable path is reported as such. - The round routes each person's items to their row, blocked → waiting → decide, plus a count of their other carried tasks; the post names the order. A match in Probably closed never takes a blocker, dependency or decision off the card: it stays, marked.
- Report series kept in the vault (
external_systems.vault_reports, CR-116) are declared like repo reports: the newest file of a dated series, or a stablecurrentfile, with its age. Read locally, never fetched. - The card opens with Built from (CR-117): each source with its age in a word, then Not used or stale,
parsed from the sources block so the two cannot disagree; the post carries a short version. Recordings are
not listed (they feed the summary, not the agenda); the sources block keeps a
recordline saying how a declared store is checked. - The card is a draft its owners correct. Kinds are assigned by
processfrom a transcript; the first minutes of a meeting confirm or fix them, and the next note records the correction.
The card fits the project and the session (CR-121). The second project on the card layout moves improvements one at a time from a request sheet to staging to production, and nothing in it is a release. Its first card still opened by asking the room to confirm the release blockers, over a section reading None carried, and the session was that project's first demo day -- the thing the card itself named -- with no place for what the meeting was for. Both are now configurable, and both default to unchanged.
card: {release_blockers: false}(undercarry_forward, defaulttrue) drops the Release blockers section and the word release from the lead, which then reads Raise what you wait on, then one minute each. Carried[blocker]lines still appear, under Blockers. The repo-label table is not affected: that list exists because the repo declares the label, and hiding repo data on a config key would be the wrong direction --checkflags the contradiction instead.- A milestone
kind:--demo,releaseorreview, default none. Withkind: demoon the entry's own date, a Demo section sits directly under the milestone: Item | Shown by | State, built from the carried lines and the repo's gap-label issues that are fixed and waiting for a test -- "on staging or done", which is what there is to show -- and the lead becomes the demo one. A gap-label issue nobody has fixed yet is not a demo row; it stays on the card under Release blockers. After the date the card asks demo held? record it and set the next milestone. - On
layout: compactthe Demo section sits directly under the milestone in the agenda too (CR-155). CR-148 first kept it in the post only, because that agenda had no milestone to put it under; the compact agenda has its milestone again, so CR-121 holds there as on the card. - Shown by is read or blank, never guessed. The demo convention is that the person who raised an idea shows it, so a carried line's owner is the raiser; an issue nobody raised gets an empty cell for the room to fill. A plausible name against work nobody offered to present is the defect, not the blank.
checkreports the contradictions, and only reports them. A card with no repolabels:and a milestone kind other thanreleaseis suggestedrelease_blockers: false;release_blockers: falseagainst a repo that declares a gap label is named as one of the two being wrong. Which is right is the team's to say.
A shorter shape for a meeting that costs a working week: layout: compact (CR-148). The day's
generated post ran about 660 words of markdown into a chat whose own rule is three to five lines of plain
text -- where the asterisks show as asterisks -- and the 1,880-word agenda was walked row by row with the
dependencies appearing twice, once as a section and again in every person's cell of the round. Compact
redistributes what the card already computes across four outputs. Nothing is removed, only moved.
| Output | What it carries |
The post, plain text in a .txt | The milestones as a dated list with a state each, the blockers one per line grouped to fix and waiting for a test, and the ask. No round, no dependencies, no markdown |
| The agenda, screen-shared | The milestones, the release blockers (to fix first, fixed, waiting for a test under its own heading) and the one-minute round, with each person's open items read from the tracker's assignments (CR-155). No dependencies and no decisions section, plus the link to the details |
| The facilitator document | Time boxes and what to watch. The dependencies table, with every line read against its own source (CR-147) and the closed ones listed as closed, is off by default (compact.facilitator_dependencies: true turns it on): where the room raises dependencies itself, a pre-written table duplicates the round |
| The details file | Unchanged and complete, linked from the agenda |
compact.max_words(default 150) is a ceiling that holds, and the blocker list is the part that gives way. Over the limit the generator warns on stdout and names the longest part, and the post keeps as many whole blocker lines as fit, closing the list with one counted line -- and 3 more, in<agenda>-- so the reader is never shown a short list as if it were the whole one. The milestones and the ask are never what is dropped: the milestones are declared by the project and the ask is the reason the post exists. The full list is in the agenda and the details.max_words: 0means always warn and is read as 0, not as unset.- Remove only the duplication, never what the room confirms (CR-155). The first compact release took
the milestones and then the blockers out of the shared agenda, and the room had no dates to look at and
no list to confirm. Both are in the post and the agenda now; the compact lead is the card's, and
card: {release_blockers: false}still gives the no-release lead. - The round's cells come from the tracker's assignments (CR-155). Each person's open
blockerandcriticalissues (*open:*) and the fixed ones waiting for them to test (*verify:*), by assignee. A tracker login belongs to a roster person through an explicitgithub:key (a login or a list), else by a login that starts with their name or alias (three letters or more; a tie is "unknown", never a guess). A carried line goes only to its first-named owner, and a carried line about an issue the tracker already lists is not repeated -- a person was shown items that were not theirs because an owner field named several people. compact.facilitator_suffixnames the facilitator document; by default it is derived fromagenda_suffixexactly as the details file is (agenda-x->agenda-facilitator-x).- Pronouns come from the roster, or they are
they(CR-143). Every generated and staged text -- the agenda, the post, the recap, the daily summary, a manifest body -- uses the person's declaredpronouns:, writescanonical_short:where the roster declares one, and usestheyfor anyone the roster does not list. A name is never the basis for a guess: the measured incident was an assistant inferring a woman from a short form the roster had already corrected. Thepronounscheck and the staging gate enforce it. - No per-person figures in the facilitator document (CR-150). The round's per-person cells stay in the agenda, where they belong to the person rather than to whoever leads. The dependencies are ordered by age, never by owner, and an owner appears only beside the one item they own.
layout: cardstays the default. A project declaring neithercard:, norlayout: compact, nor a milestonekind:renders byte for byte as before.
The team's page about the meeting: templates/daily-playbook.md (CR-154). A project that runs a daily
series starts its "how the daily works" page from this template, one page of checklists: the meeting's four
parts, attendance, the four lines of a person's minute, what to do before and after, the roles, where things
go. The project's own parts (the leader, the verifier, the labels, the tracker, the channel) are {{placeholders}}
that stay visible until filled -- an unknown is a blank, never a guess. A page written in week one goes stale
within weeks, so a project may declare playbook: {path, decisions, stale_after} and check says, in one line,
when more than stale_after (default 8) decisions in the register are dated after the page's Reviewed:
date (or its file date). It reads two files and decides nothing.
It writes YYMMDD-<agenda_suffix>.md and the chat post, carrying what the transcript cannot:
the carried-forward block with session counts, the round from the declared roster, and the
Since the last standup block from the archives. Hand-writing the agenda in a wired project
silently drops all three — the agenda looks complete and is missing the half that comes from
outside the room.
A generated agenda is a draft until its digest is filled (CR-101) — required, not optional.
Where any declared chat has messages since the last note, the script writes a
Since the last standup — not said in the room section holding only a marker:
<!-- DIGEST: N messages since YYMMDD (…). Not yet read. -->. The script counts; it cannot judge.
So after generating:
- Read the counted messages in the chat archive.
Replace the marker with a digest — facts, not quotes:
In the digest Not in the digest What landed (PR, build, release), with its reference Message text reproduced Asks, with who is waiting on whom — the owner, or no owner namedWho said what, beyond naming the requester of an ask Dependencies on another team, with how long they have waited Anything personal, or off-topic, in a work chat Corrections to Probably closed evidence - Round rows may be extended from the digest (e.g. "test build 764"), and the sources block's
digestline becomesdigest filled by prepare <date>.
Do not report the agenda as done while the marker remains. The script printing read them and a
caller that does not read are two halves that each assume the other; this step gives the reading an
owner. /ops orient reports an agenda still carrying the marker as generated, digest not filled.
No roster, said (CR-101). When no people: is declared beside carry_forward, the round prints
No roster and — where meeting_types.<type>.participants exists — names who that lists. Never fill
the round from it: participants describe a schedule, not a round order. Declare people:.
In dual mode the facilitator sheet is still written here, as a layer on top of the generated agenda: read the generated file, add only facilitator content. Never regenerate the agenda's own content into it.
Stop if a session was recorded and never written up (CR-087). Where the folder declares
external_systems.transcripts, a recording newer than the newest note means a session happened and
left no note. The next agenda is built from that note, so building now drops the session
entirely — re-raising what it closed and carrying none of what it opened. Process it first, or pass
--skip-unprocessed so that skipping is a decision on the record rather than an accident.
Check for an existing preparation before writing (CR-086). Glob the target folder and its siblings for a preparation, agenda or facilitator sheet matching this meeting's date and participants. If one exists, report it and offer four choices, not three (CR-108):
| Choice | When |
| Open it | Nothing has changed since it was written |
| Update it | The sources have moved -- chat, repository, a processed meeting. Keep the structure, add what changed, and say what moved |
| Regenerate it | The structure is wrong, or the meeting changed |
| Write anyway | Never the default. Say why |
An update is not finished until every copy matches. Where the agenda has been staged in
_outbox/<item>/, copy the updated source over the staged copy, verify they are identical, and
edit the manifest in place under CR-102: append one line naming what changed, and never touch
status. A staged copy that differs from its source is the exact failure Kanonisk källa exists to
prevent, and nothing catches it today. build_agenda.py already refuses to overwrite an agenda; the hand-prepared path had no
equivalent, and a second prep for the same meeting is indistinguishable from the first until someone
opens both.
Three template sections the agenda needs (CR-108)
1. A pending decision carries its options. Decisions -> Pending stays one line per decision.
Each decision with more than one real answer gets a block under # Reference:
## Decision: <what is being decided>
| Option | What it means |
|---|---|
| **A. <option>** | <consequence> |
| **B. <option>** | <consequence> |
**Recommendation:** <A or B, one line why>, or *none -- the room decides*
**Decides:** <name> · **Open since:** <date>The agenda item then says "options under Reference". A decision with only one answer on the table is a confirmation, not a decision -- it goes in Status Overview's Today column instead.
2. A fixed section for what is deliberately NOT on the agenda. Between ## Decisions and
# Reference:
## Carried, not on today's agenda
- **<item>** -- <one line of state> · **<owner>**Same line shape as ## Carried forward, so /ops orient and a later wiring of the loop read it
without a second format. Write the section even when it is empty -- "Nothing deferred." An
omitted section means the same as an omitted ## Carried forward (CR-082): nobody can tell it from
a section someone forgot.
/ops process closes the loop: an item here that the meeting did not address is carried into the
summary's Next Steps rather than dropped.
3. An agenda item links to its material. An item whose discussion needs more than one line of
background gets a section under # Reference, and the item says so. Material does not go between
## Agenda and ## Blockers -- inline placement is what reordered the agenda in the first place.
Template check before reporting (CR-108, extends CR-018)
Before reporting the agenda as done, compare its ## headings against the declared order: Agenda,
Status Overview, Key Updates, Reported Updates, Blockers, Decisions, Carried-not-today, then
# Reference. Report any heading outside # Reference that the template does not name. This is
the template-contract check the summaries have had since CR-018, applied to the artifact that until
now had none.
build_agenda.py keeps its own order (sources block first, CR-084) and is out of scope.
Announce the path before saving — which route was taken (generated or template) and the exact filenames to be written. A hand-written agenda in a wired project then cannot happen silently; it is the one outcome the run states out loud.
Step P1: Gather Context
- Load config (same as normal /ops flow)
- Read recent meetings:
- Find last 1-3 meeting summaries in the target folder
- Extract action items assigned to each team member
- Note decisions made, blockers identified
- Read the task ledger (resolve
workflows.task_ledgerfirst -- see ops-base, CR-040):local: pull active tasks per person from_tasks.yaml(status: pending, in_progress, blocked)external: read the declaredpointerif it is reachable; otherwise state in the prep that open work lives insystemand was not read. Never fall back to an ancestor_tasks.yaml- Identify blockers and their owners
- Read CHANGELOG.md:
- Scan recent entries for context
- Triage scan (CR-022): if the vault has a registered triage doc (
_inbox/working document,type: working_doc+ tagdo-not-process), pull open bullets whose bracket-tag second segment resolves to this meeting's participants ([Möte · X],[Uppföljning · X], ...) into the prep's agenda/open-actions, and stamp each pulled bullet→ i prep YYMMDD-- the only write allowed to the triage doc (same rule as/preparationStep 2.4). Skip silently when no doc or no matches.
Step P2: Parse Async Updates (if provided)
If the user provides pre-submitted team updates:
- Identify team members using the name resolution algorithm:
- Match against config
team[](name, aliases) - Match against
_contacts/*/_meta.yaml(display_name, aliases) for external contacts - Matching is case-insensitive with Swedish character folding
- See Contact Metadata Schema
- Match against config
- Extract per-person updates:
- What they report as done
- What they're working on today
- Issues/blockers mentioned
- Decisions or announcements
- Correlate with yesterday's tasks:
- Match reported items against action items from previous meeting
- Determine status: DONE, IN PROGRESS, NOT STARTED, NO UPDATE
Step P3: Generate Preparation Document
Create the file using the Standup Preparation Template:
# [Organization/Project] [Meeting Type] -- [DD] [Month] [YYYY]
**[Next event note if relevant, e.g., "War room tomorrow: Wednesday March 12, 10:00-12:00 CET"]**
---
## Status Overview
| Person | Yesterday | Done | Today |
|--------|-----------|------|-------|
| **[Name]** | [Tasks assigned] | [What's done] | [Today's plan] |
| ... | ... | ... | ... |
**[Key metric if relevant, e.g., "Test results: 4 PASS / 4 FAIL (50%)"]**
---
## Key Updates
| Update | Source |
|--------|--------|
| [Important update or decision] | [Person] |
| ... | ... |
---
## Agenda
- [ ] 1. **[Person]** -- [Key questions]
- [ ] 2. **[Person]** -- [Key questions]
- [ ] ...
- [ ] N. **[Topic]** -- [Confirm/discuss]
---
## Reported Updates
**[Person]:**
> "[Their exact update text]"
**[Person]:**
> "[Their exact update text]"
...
---
## Blockers
| Issue | Status | Owner |
|-------|--------|-------|
| [Blocker description] | [Status] | [Owner] |
| ... | ... | ... |
---
## Decisions
**Made:**
- [Decision] -- [brief rationale]
**Pending:**
- [Decision needed]
---
# Reference
## [Relevant reference section, e.g., Test Results]
[Tables or details as needed]
## Builds / Versions
- [Current build info]
---
*Created: [YYYY-MM-DD]*Step P4: Template Variations
For standup:
- Focus on Status Overview (Yesterday → Done → Today)
- Keep agenda person-by-person
- Include reported updates if async input provided
For war-room:
- Add "Focus Areas" section after agenda
- Include test matrix reference
- Add "Participants" confirmation
For weekly/planning:
- Add "Sprint Goals" or "Week Priorities" section
- Include metrics summary
- Add "Carry-forward Items" from previous week
Step P5: Save and Report
- Resolve preparation mode:
- Look up
meeting_types[<type>].preparation_modein the merged config - If absent, default to
single
- Look up
Determine filename(s):
Single mode -- one file:
- Format:
YYMMDD-agenda-[org/project]-[type].mdin every language — the role keyword is an identifier (CR-089). Files written before v1.79.0 aspreparation/förberedelseare left as they are and still read - Include the organization or project name to distinguish preparations created the same day for different orgs/projects
- Examples:
260311-agenda-acme-mobile-daily-standup.md,260311-agenda-delta-veckosynk.md
Dual mode -- two files (English role keywords in every language, CR-089):
- Agenda file (team-facing):
YYMMDD-agenda-[org/project]-[type].md - Facilitator sheet (private add-on layer):
YYMMDD-facilitator-[org/project]-[type].md - Examples:
260505-agenda-coreteam-weekly-w19.md+260505-facilitator-coreteam-weekly-w19.md
Order of generation: write the agenda file first using the same template the same folder's prior-day single-mode prep used (status overview, blockers, action items, agenda, references). Then derive the facilitator sheet as a slim private layer on top -- only the content from the "Facilitator sheet" list above. If a section is present in both, keep it in the agenda and remove it from the facilitator sheet.
- Format:
- Save to meetings folder (per CLAUDE.md MEETING ROUTING). For dual mode, both files go in the same folder.
- For dual mode, ensure the cross-references are one-directional:
- The facilitator sheet MUST link to the agenda file at the top with a notice such as:
> **Private facilitator sheet.** The team-facing version is [YYMMDD-agenda-...md](YYMMDD-agenda-...md). Do not share this file with the team. - The agenda file MUST NOT mention or link to the facilitator sheet. The visible document must not advertise that a private one exists.
- The facilitator sheet MUST link to the agenda file at the top with a notice such as:
Report what was created:
Single mode:
Created: meetings/260311-agenda-acme-mobile-daily-standup.md Status Overview: - Dev1: 1 done, 4 in progress - ... Blockers: 4 items Agenda: 7 itemsDual mode:
Created (dual mode): - meetings/coreteam/260505-agenda-coreteam-weekly-w19.md (team-facing -- full prep) - meetings/coreteam/260505-facilitator-coreteam-weekly-w19.md (private add-on layer) Agenda file: status overview (8 people), 10 agenda items, 6 P0 blockers, 13 carry-forward actions Facilitator-only layer: 5 items (role clarification, time-box cues, "watch for" list, "do not surface" list, post-meeting follow-ups)
Step P6: Lifecycle
After the meeting, when /ops [transcript] is run:
- Single mode: the
agendafile (or a legacypreparation/förberedelsefile) is automatically marked as superseded (per Step 9 of normal flow) - Dual mode: BOTH the
facilitatorandagendafiles are marked as superseded - No manual action needed
Notes
- No CHANGELOG update for preparation files (they're pre-meeting)
- Language: Follow same rules as normal /ops (per_claude_md, etc.)
- If no async updates provided: Generate preparation from historical context only, with empty "Reported Updates" section or skip it
- If team member missing from updates: Show "No update" in Status Overview
normalize -- Restore Swedish characters in hand-written docs (CR-007)
Trigger: /ops normalize <path> [--dry-run] [--strict-no-ambiguous]
Lint pass that scans markdown and YAML files for known Swedish character drift (ar → är, for → för, pa → på, mote → möte, etc.) and restores the correct characters. Use it on hand-written ops documents that bypassed the /transcript and /ops pipelines and accumulated character errors.
Use cases:
- A folder of pasted notes (e.g.,
_projects/<client>/ops/*.md) was created without going through/opsand never got a Swedish-character pass - A legacy file from before CR-007 inheritance was added still contains old drift
- Bulk remediation across a sub-tree
Arguments:
| Argument | Meaning |
<path> | A single file OR a folder. If a folder, scans all .md and .yaml files recursively. |
--dry-run | Show the diff without writing. Use to preview before applying. |
--strict-no-ambiguous | Skip substitutions marked ambiguous: true in swedish_substitutions.yaml (e.g., ar → är, bor → bör, Andre → André). Recommended for first-pass scans. |
--names | (CR-017) Instead of Swedish characters, normalize person names: apply the org people[] roster (aliases → canonical) across the target's .md files and CHANGELOG. Reports each substitution for confirmation before writing; never runs implicitly. |
--filenames | (CR-021) Instead of file content, normalize filenames against the slug contract (restore å/ä/ö, fix digit-transliterations like m0te, unify date prefix to YYMMDD). Dry-run lists old → new; on apply, renames AND updates inbound references found in the same tree (CHANGELOG.md, README.md, supersede stamps, wikilinks). |
Examples:
/ops normalize _projects/bravo-project/ops/annonsering.md
/ops normalize _projects/bravo-project/ --dry-run
/ops normalize _projects/bravo-project/_insights.yaml --strict-no-ambiguousStep N1: Load substitution map
Read ~/.claude/skills/ops-config/swedish_substitutions.yaml. This file contains the seed substitution list (from MEMORY.md) and is the single source of truth for the linter.
Step N2: Scan target file(s)
For each file:
- Detect language. Treat the file as Swedish-context if any of:
- The file already contains å, ä, or ö characters
- The file is in a folder configured for Swedish (per CLAUDE.md or
language_inheritancein base.yaml) - The user explicitly passed
--lang sv
- Skip if not Swedish-context -- this is a Swedish-character normaliser, nothing else
- Tokenize the file while preserving:
- Code blocks (
...) -- skip - Inline code (
...) -- skip - URLs and file paths -- skip
- YAML keys (only
value:parts are scanned, not keys) -- skip keys - Lines containing
<!-- no-normalize -->-- skip the entire line
- Code blocks (
- For each token, look up against the substitution map. Use word-boundary matching (
\bword\b). - Skip ambiguous substitutions if
--strict-no-ambiguousis set.
Step N3: Apply or report
If --dry-run:
- Print a unified diff to stdout showing all proposed substitutions
- Print a summary: total substitutions, ambiguous skipped, files affected
- Do not write anything
If not dry-run:
- Apply substitutions to each file
- Write the file back atomically (write to temp, rename)
Update or create a
CHANGELOG.mdentry in the file's parent folder (if a CHANGELOG exists):- **YYMMDD: Normalize** [filename] -- restored Swedish characters (N substitutions). -> [file]
Step N4: Report
Normalized 4 files, 23 substitutions applied:
_projects/bravo-project/_insights.yaml 8 substitutions
_projects/bravo-project/ops/annonsering.md 7 substitutions
_projects/bravo-project/ops/byrasamarbete.md 5 substitutions
_projects/bravo-project/ops/c56-krav.md 3 substitutions
Ambiguous substitutions skipped (use without --strict-no-ambiguous to apply):
ar -> är: 14 occurrences
bor -> bör: 2 occurrences
Backup: none (use git to revert if needed)Notes
- No backup files written. Rely on git for revert. If the target is not a git repo, the user is warned and asked to confirm.
- The substitution list is data, not code. New common drifts can be added to
swedish_substitutions.yamlwithout changing skill code. - Defence in depth: the same substitution map is used by
/insightspre-write validation (see insights/SKILL.md). The normaliser is the after-the-fact remediation; the validator is the at-write-time gate.
project list -- which folders are pipelines, and which are just material (CR-065; was projects)
Trigger: /ops project list · python3 ~/.claude/skills/ops/list_projects.py [--root <vault>]
Read-only. A folder under a projects tree may be a running loop or a pile of transcripts, and from
the outside they are indistinguishable — same depth, same naming, several with a CHANGELOG and a
meetings/ folder. Measured on one vault: 40 project-shaped folders, 5 configured, 4 with the loop
wired.
Grouped by how far each is wired, because the grouping is the answer:
| Group | Means |
| Loop wired | carry_forward declared — /ops orient and build_agenda.py work here |
| Configured, no loop | /ops processes meetings; agenda and carry-forward do not apply |
| Material only | No config. Notes, transcripts, documents — not a pipeline, and often correctly so |
| Empty or dormant | No config, no dated notes, no changelog |
Per row: dated meeting count and when the record last moved — read from the changelog where there is one, because a changelog entry is a deliberate act and an mtime is whatever a sync did last.
A series IS its meetings folder; a project HAS one (CR-072). Both counts read the subfolder when it exists and the folder itself when it does not. Reading only the subfolder reported folders holding dated notes directly as having none and as never having moved — on one vault, three folders sat in empty or dormant while one of them had been written to four days earlier.
A recurring series declares itself. A weekly that lives in a meetings tree rather than under
_projects/ is invisible to a scan that only knows the project trees, however completely its loop runs.
A top-level series: block opts the folder in:
series:
name: <what the series is called>
cadence: weekly # shown in the listingDeclared, never inferred — every folder carrying a config would otherwise read as a series,
including the org root. The declaration does two things: it brings the folder into the scan, and it
licenses resolving carry_forward up the config chain, because a series inherits its loop from an
ancestor config and reading only its own file reports a running loop as no loop.
The chain walk is not applied to project folders, deliberately. An org-level carry_forward block
is typically written for one named series — its note_suffix names that series — so letting every
sibling project inherit it would report loops that do not exist. That is the CR-067 failure in the
opposite direction, and over-reporting a loop is worse than under-reporting one: the agenda it promises
is never generated and nobody finds out until an item has fallen through.
The cadence is shown because escalation thresholds are counted in sessions. escalate_after: 3 is
three weeks on a weekly and three months on a quarterly. The number means nothing without it.
It does not replace a project registry. A hand-written registry carries intent — what a person is driving, with status and sponsor — and is authoritative for it. "What am I driving" and "what is wired" are different questions, and answering the first from the second would drop every idea, dormant effort and discussion topic that correctly runs nowhere. This command links to the registry rather than restating it.
Pairs with /ops orient: projects is wide and shallow, brief is one project deep.
project new <name> -- create a project, then register it (CR-086)
Trigger: /ops project new <name> [--from-meeting <summary>] [--pre-phase-of <project>/<track> --exit "<criterion>"] [--graduates <plan>#<row>]
/ops project list stays read-only. A read-only command that sometimes writes is harder to trust
than two commands, and that guarantee is why the existing one is safe to run without thinking.
Step N1: Check before creating
Refuse if the folder exists. Glob for near-names and ask — a project created under a second spelling is invisible to everything that looks for the first, and the two then accumulate material in parallel.
Step N2: Scaffold five artefacts
In the organisation's projects tree, resolved from config:
| File | Content |
README.md | Roles line, STARTING + creation date, cadence, language; a Scope / Out of scope table; a Meetings table |
_ops.yaml | Organisation, language, people[] seeded from the organisation roster with its aliases — never re-typed, workflows, optional carry_forward |
_tasks.yaml | v2 header, empty tasks: |
CHANGELOG.md | Header plus a "Project created" entry |
meetings/ | Empty |
A hand-copied roster is where a misspelling enters and then resolves to nobody.
--from-meeting: the first CHANGELOG entry and the README's meetings row point at that summary.
--pre-phase-of <project>/<track> --exit "<criterion>": writes the pre-phase block into the
README — what blocks the parent track today, the exit criterion, and that the flow hands over when it
is met — plus a note in _ops.yaml naming the parent's register as the source of open questions.
A pre-phase that keeps its own register is a second register for one body of work.
This is a README convention, not a declared lifecycle. There is one known instance of the shape, and a lifecycle declared on a single case is a guess. If a second appears wanting the same three parts (exit criterion, handover, tombstone), that earns its own CR.
Step N3: Register it — declared, never hardcoded
registry:
projects:
- path: <register file> # vault-root portfolio, or an organisation index
section: <table heading>
mode: propose | write
structure_docs:
- <file whose folder tree lists projects>Declared at both layers and merged by the config chain. A vault may hold a cross-venture portfolio at its root and a per-organisation index inside each organisation; both are registers a new project must appear in. A project in an organisation that declares an index gets two rows; one in an organisation that declares none gets the portfolio row only. An organisation without an index is not misconfigured — it has one register.
mode: propose is the default for a hand-written register. Show the row, write on confirmation.
| The skill fills | Left for a person, marked |
Name, venture, type, status (New <date>) | The owner's role, sponsor/mode, where it runs |
The division is about what can be known, not about effort. A role column can legitimately contain a question — an owner writing "contributing?" about their own involvement is recording an open question, and a skill filling that cell would turn a question into an assertion. Judgement columns are written as marked placeholders, so an unfilled one is visible rather than merely empty.
Idempotent. If a row for this project already exists — another session got there — verify the link and do nothing else. This is what makes two sessions harmless rather than merely unlikely.
Unconfigured means skip, silently.
Step N4: Graduation (--graduates <plan>#<row>)
Annotate the row in place — moved to <project> on <date>; linked, not copied, the row stays as
history — in every plan that carries it, and seed the new README's background from the row.
The golden rule made executable: one item, one owner, one document. A row copied rather than linked becomes two rows that disagree within a week.
project close <project> -- the missing half of new (CR-104)
Trigger: /ops project close <project> --reason <handover|delivered|abandoned> [--to <project>/<track>]
project new registers a project in four places. Nothing unregisters it. The register keeps
claiming a project is live long after the work moved or stopped, and the only signal that it did
not is silence -- which reads the same as a project that is merely quiet.
Read-only by default, every write confirmed, in the manner of check.
Step C1: Report the open surface -- before anything is written
| What | Why it is shown |
Open rows in _tasks.yaml, with owners and ages | A project whose ledger holds eleven open P1s is not closed, it is abandoned |
| Outbox items not yet resolved | Material staged and never sent is an open loop, not a closed project |
| Register rows naming this project | What will be unwound in step C3 |
| Rolling-plan rows pointing at it | Someone else's plan may still depend on this |
Nothing is written before this is shown. The report is what makes the operator say which of the three reasons actually applies, rather than reaching for the flattering one.
Step C2: Offer the tombstone
In the existing Retirement Convention shape, naming the reason and -- for a handover -- the receiving project and track.
Step C3: Unwind what new wrote
The same declared registry: block, in reverse: the portfolio row moves to its closed
section, the structure doc's tree entry goes. mode: propose | write is honoured exactly as on
create.
Step C4: Ask for the evaluation, and only ask
Where the config declares one, close opens the evaluation document from its template with the
project's own numbers already filled in: duration, meeting count, participants, tasks opened
against tasks closed.
It never writes the judgement. What went well is human work, and a generated retrospective is worth nothing.
Config
lifecycle:
close:
evaluation:
template: <path> # omitted: no evaluation step
destination: <path>
tombstone: true # defaultAbsent the block, close still reports, offers the tombstone, and unwinds the register. The
evaluation is the part that varies by organisation, so it is declared, never assumed.
What close is not
- Not an archiver. Closing a project does not move its folder. The Archive Policy is unchanged.
- Not automatic. No sweeper closes a project.
checkmay report a project that looks finished and suggest the command; the decision that something is done stays with a person. - Not a status field. The tombstone and the register are the record.
orient <folder> -- where a recurring project stands, before work resumes (CR-061; was brief)
Trigger: /ops orient <folder> · python3 ~/.claude/skills/ops/project_brief.py --dir <folder>/meetings
Read-only. Writes nothing, fetches nothing, judges nothing.
The first line names the resolved profile (CR-123): Profile: podcast (from <file>), or
Profile: meeting (default). An unknown profile name is printed with its fallback.
Company profile (CR-125). After the script's blocks, the agent adds one line when the organisation has a
<org>/_company/ whose company, positioning or strategy facet is stale or still draft
(/company check has the full list). Silent when there is no _company/. Reads only; the script does not.
/ops status reports which config applies. /ops check audits closure debt across a vault. This
reports one folder's current state — the question a session asks when it opens a project cold and
would otherwise rebuild the answer from four files, losing whatever nobody wrote down.
Six blocks, each reading files that already exist — preceded, when there is one, by Fetch problems
(CR-088): any declared source whose archive _fetch.json records a result other than ok, reported
once, first. An expired login is the usual way a scheduled archive goes stale and the one thing in the
brief only a person can fix; re-running the fetch does nothing.
- Loop position — newest note, next session, whether that agenda exists yet — and, if it does, whether its digest is still unread (generated, digest not filled, CR-101). The common failure is not a missing note but a missing next agenda, and nothing else surfaces it; the second most common is an agenda that exists and is still a draft.
- Chain integrity — whether the newest note ends with
## Carried forward. The one failure in the loop that announces nothing: without it the next agenda carries zero items and looks correct. - What is carrying — each item with sessions, age and owner.
UNOWNEDis counted and named, because an item nobody is named against is the one that falls through an agenda that lists it. - Archive freshness — newest snapshot per declared chat and repository, and each archive's fetch
record (
fetched 07:00 ok,fetch failed 06:30: login required, orfetch not recorded). A stale archive is worse than none: retrieval still produces a block and it reads as current. The snapshot says how old the data is; the fetch record says whether that is because nothing happened. - Staged, not sent —
_outbox/folders naming this project whose status is not sent, with age. A file in a meetings folder carries no status; this is the only place an unsent item shows. - Record movement — the changelog's most recent entry. When did anyone last write this project down, not when was a file touched.
It is /bod for a coordination project, and the parallel is deliberate: both read state before work
begins and neither writes. The expensive mistake is not doing the wrong work — it is doing the right
work against a picture that was true yesterday.
A folder with no post_processing block still gets blocks 1, 5 and 6.
check <folder> -- Check a folder's files against template contracts and the carry-forward chain (CR-018; was lint)
Trigger: /ops check <folder>
Read-only version of the CR-018 pre-save template-contract check, run across a folder's existing files. Use it to detect template forking in a recurring series -- the drift class where each file is internally consistent but the series silently changed shape at some point.
Steps:
- Resolve the contract registry (
workflows.meeting_templatesfrom the merged config;default= CR-006 canonical). For each
YYMMDD-*.mdmeeting summary in the folder (skip preps, agendas, emails,.archive/): resolve its contract bymatchglob and run the three checks (heading sequence, action-table header row, empty-Beslut marker). 2a. Hygiene (CR-122, CR-123, CR-124). Runpython3 ~/.claude/skills/ops/hygiene_check.py <folder>and report its lines under their kinds:links(a raw(,)or space in a Markdown link target, or a relative target that does not exist),yaml(escaped letters such as\xE5: a file an agent re-dumped),tasks(_tasks.yamloutside its declared enums, including akindthe profile does not declare -- the enums are the contract's since CR-152 (task_source_types,task_scopes), and a legacy spelling declared intask_source_aliasesis not a finding: it is read, never rewritten, and reported as one advisory line per ledger naming the words and what they resolve to. What survives is a word that is neither canonical nor aliased, which is a one-line question for a person -- either it earns a row in the alias map or it is a typo. Measured on one vault the change took the class from 281 per-row findings over 22 words to 27 advisories and 2 real questions),profile(an unknown profile name) andthemes(the registry checks, when themes are enabled: registry, summary line and insights disagreeing in any direction, unknown or merged ids, too many primary themes, a missing synthesis page). Undertasksit also reports a finished task with neitherclosed:nor the legacycompleted:(CR-127: count and ids; the day is never guessed), and, when the vault declares a vocabulary (CR-135), averksamhet:outside it, aprojekt:with no portfolio row, a portfolio row no entry names (whole-vault runs only) and anobjekttyp:other than Uppgift filed as a task; an absent field is never a finding. Report only: the fix is a judgement each time.inferred(CR-142): an unconfirmed inference in a decision. A line inside a decisions section (## Decisions,## Beslut, a decision register) that still carries*inferred: <what>*and no*confirmed: <source>*. A register line reads as settled whatever it was built from, which is how three guessed phrases survived two days and one register line had to be corrected. The finding names what was inferred, so the reader knows which phrase to go and ask about. Report only here too: confirming an inference with its source, or dropping it and writing what is known, is a person's judgement -- and a tool editing the register would be making the same mistake the marker exists to prevent. A fenced block is skipped, as inlinks: a marker written inside a fence is an example of the convention and not a decision, and a#comment line inside a fence is not a heading that ends the section -- it used to end it, and the real inference below the fence was then missed.persons(CR-150): work counted per person in a team-facing file. A table whose rows are people and whose columns are counts, or a line pairing a person with a count of issues, pull requests, commits or lines. Counts of work per person belong in ONE owner-only document --metrics_document:in the config, which is exempt -- and never in the files the team reads or that the person leading the meeting reads aloud. The reasons are the rule: the numbers mislead (lines reward volume, closures reward splitting one job into three, and part of the code is written in agent sessions), and the person leading is often one of the people counted. The finding names the file and the line and does not repeat the figure -- a report quoting it would have copied the number into one more file, which is the one place the rule exists to keep it out of. A name beside an ITEM is never a finding: an owner, an assignee, a Shown by is accountability for a task and is most of what an agenda is, and a lint firing on every owner column is one somebody switches off within a week -- after which nothing is checked at all. The subject has to be a person the project DECLARED -- a name or alias in the governingpeople:roster, nearest declaration winning. The shape of a name is also the shape of every team, area and grand total a standup writes, so a check reading the shape reportsMobile: 7 PRs mergedandTotal: 40 PRs mergedas work counted per person, which is the opposite answer: counts per area are what a standup is for. With nopeople:roster the check finds nothing, and--checksays so once rather than guessing per line. A fenced block is skipped, as inlinksandinferred. Report only: whether a figure moves to the metrics document or goes is a person's judgement.The generator itself emits none of this (CR-150, proposal 1), in any layout: the agenda, the details file, the chat post, the compact layout's facilitator document. The appendix lists the author of every moved issue and pull request for traceability, with no count per author -- that listing is one
GROUP BYaway from a leaderboard, which is why the lint exists at all. A declared source that already carries per-person figures gets one line in the digest section (per-person figures present in the source, not copied) and none of its figures: the note is what stopspreparecopying them when it fills the digest slot.pronouns(CR-143): generated or staged text that contradicts the roster. Two findings. A line that names exactly ONE person the project declared, whose entry declarespronouns:, and then uses a singular gendered pronoun of the other form -- reported with the declared form, so the fix is in the finding. And a line writing a short spelling of a person whose entry declarescanonical_short:-- reported with the canonical one. The measured incident is both at once: a team member's name is also a common female name when shortened, the member is male, and an assistant wrote the short form and the wrong pronoun in two separate drafts after the roster and its own notes had both said otherwise. Two incidents in about ten days, both caught by the operator, none by a check.theyis never a finding, in either direction, and that is what makes this check survivable. It is the ordinary plural, it is the default for anyone the roster does not list, and it is correct for anyone declaring it -- so a sentence about a team, a plural subject and a possessive like their release stay clean. Nor is a gendered pronoun on a line naming two declared people: the subject is plural, the pronoun may belong to either, and that is a sentence to rewrite rather than a wrong fact to report. The pronoun must FOLLOW the name on the line: He reviewed it; Ann is next names a previous subject and a new one. A lint that fires on ordinary prose is switched off within a week, and then it checks nothing at all. Nor is either finding raised where an IDENTIFIER precedes the name --- acme/app #12 Import fix (dee),- #7 Retest — dee, a blocker table's assignee cell. Those lines are what/opsITSELF writes from a repo'sauthor.login/assignees[].login, and a login that happens to equal somebody's short form made every repo-detail line a finding. What rules them out is the position: an item with a handle on it names the item first and the name after it is an accountability slot, while prose about a person names the person first (Dee closed #12 yesterdayis still a finding). This is CR-150's rule, not a list of logins, so a new repo or a new login needs no configuration.What the check cannot know, and says so. The wrong-pronoun rule reads POSITION, not REFERENCE: it cannot resolve which noun phrase a pronoun refers to, and the two cases are structurally identical -- Dee led the review; his summary is filed (the pronoun is Dee's) against Dee filed the ticket the reporter raised; her account was locked (it is the reporter's). Telling them apart needs to know that the reporter denotes a person, which is a word list, and CR-150 records what a word list does to this kind of check. So the one intervening referent the lint can SEE is excluded -- another person named between the name and the pronoun, written as a two-word name -- and the finding itself says confirm the pronoun refers to them (this check reads position, not reference). A third party named by a role noun still produces a finding, which the author dismisses in one read. That is the honest cost of a line-based check, and it is stated here rather than left for the operator to discover. A roster entry that declares neither key yields nothing, and an unlisted person is always
they, never inferred from the name (CR-143, proposal 3): a name does not tell you anyone's pronouns, which is the whole finding of the CR. Scope is every.mdfile -- the agenda, the post, the recap, the daily note and a manifest body all are, and a rule about who somebody is does not apply to only some of the files that say so. A fenced block is skipped. Report only: the fix is the author's sentence.The same lint runs at STAGING (CR-143, proposal 4), in
resolved.py --validate, so a draft with the problem never reaches_outbox/-- see the/outboxstaging gate. Both surfaces readbuild_agenda.pronoun_linesfor the rule ANDbuild_agenda.pronoun_scopefor WHOSE ROSTER says so, so a draft cannot pass the gate and then be a finding, or the reverse. The scope resolution walks up from the file's own folder for the nearest config declaringpronouns:orcanonical_short:, exactly as every other key in this family resolves. Two walks is what shipped first and they disagreed: a project declaring its roster in.claude/ops-config.yamlwith no plain file at its own level was invisible to the check's scope list, and its drafts passed the gate against the project's roster and then became findings against the venture's.
2b. Company profile (CR-125). If the folder or its organisation has a _company/, run
python3 ~/.claude/skills/company/company_tool.py check <folder> and report its lines under their kinds:
stale, schema (frontmatter, sources.yaml, brands.yaml), unsourced, credential (a key value in
a profile is a defect), and with --copies a profile file outside _company/. Report only. The
judgement checks (status claims, facts without a source marker) belong to /company check.
2b. agenda_suffix names the project (CR-082). Where carry_forward is declared, check that
agenda_suffix contains the project's folder slug. A default or generic suffix
(agenda-daily-standup) produces filenames that say nothing about where they came from — and an
agenda leaves its folder routinely: staged in _outbox/, attached to a chat post, forwarded.
Report it with the rename as the offered fix; never rename implicitly
(/ops normalize --filenames stays the only route, and existing files are left alone).
3. Group findings by series and by first-deviating date -- the output should read "this series forked at YYMMDD", not a flat per-file list:
2c. The roster matches who actually attends (CR-084). No roster at all is the first finding
(CR-101): a folder with carry_forward and no people: beside it generates a round with no rows;
report it, naming meeting_types.<type>.participants where present. Otherwise compare people[] with the participant
lines of the last N notes and report two mismatches: present every time but not in the roster
(they have no row in the round, so nothing they carry is ever routed to them), and in the roster
but absent every time (suggest adjacent: true, which keeps the name for resolution and takes
the row out of the round). Report only — the roster is the project's to change, and a skill
editing who belongs in a room is not a lint fix.
2d. Carry-forward items name something the team can find (CR-084). Flag a carried line whose only identifier is vault-local — a register id that exists in coordination notes and nowhere the team works. Such an item leads the agenda and is skipped every session, because the person who owns it cannot resolve what it refers to. Every carried line should reference an issue, a ticket, a path in the repo, a chat message or a session — or state the matter in plain words.
CR-107 sharpens it. A term borrowed from another document is not plain words: carried
without its source, it reaches its owner as jargon (observed: a plan's mechanism name, carried
for days, that its named owner did not recognise in the room). Also flag: a count inside a
carried line (stale by the next session; the movement block reads it fresh), an unknown kind
prefix (only [blocker], [dependency], [decision]), and, in layout: card, no
milestone: declared (the card has nothing to run toward).
/ops check meetings/management
weekly-management (14 files checked):
OK through 260519. Forked at 260526:
- action_table: missing column "Prio" (260526, 260605, 260702 -- 3 files)
- heading "Sammanfattning" (banished) prepended (same 3 files)
1-on-1 files (22 checked): all OK.
To accept the new shape: declare it in workflows.meeting_templates.
To fix the files: edit manually or re-run /ops on the source transcripts.- Never rewrites files. Lint reports; the user decides between amending the contract (accept the fork as deliberate) and fixing the files.
Carry-forward chain check -- only where
workflows.post_processing.carry_forward.enabled.For each
YYMMDD-<note_suffix>.mdin the folder, check the summary ends with a## Carried forwardsection. This is the one failure in the meeting loop that is silent. A note missing the section does not error, does not warn, and produces a next agenda with zero carry-forward items that looks perfectly correct — the chain ends and the output stays plausible. Every other defect in the loop announces itself.Report the break, not the file count — the chain is what matters:
/ops check <project>/meetings carry-forward chain (5 notes checked): Chain intact 260916 -> 260919. BROKEN at 260921: - 260921-daily-standup.md has no "## Carried forward" section - so 260922's agenda was generated from 260919 and is 2 sessions stale 260918: section present but empty -- "nothing carried" is a real answer; say it in the section rather than omitting it.An empty section is a finding, not an error. A day where nothing carried is legitimate and worth recording; an omitted section is indistinguishable from a day nobody wrote up. Only the omission breaks the chain.
A reappearing item keeps its count. The counter counts appearances, not consecutive ones — an item carried on the 16th, dropped from the 19th and carried again on the 21st is on its third session, not its first. An earlier version broke on the first absence, which let a genuinely stuck item hide indefinitely by skipping every third session.
Two rules make that safe. A note with no carry-forward section is skipped, not counted as an absence — that note says nothing about any item, and reading its silence as resolved is the same mistake in a different place. And an item missing for more than
MAX_GAPsessions (default 2) is treated as a fresh raise, because carried in March, back in September is a new problem wearing an old name.Gaps are reported, not hidden —
3 · skipped 1. A gap has two readings that look identical from here: a note that dropped the item by mistake, or an item resolved and later re-raised. Only a person can tell them apart, so the count stands and the ambiguity is shown.
check -- Vault-wide closure/staleness audit (CR-019; was sweep)
Trigger: /ops check with no folder, or /ops check --vault <scope> for a subtree (default scope: vault root; depth 6; skip .archive/, .transcripts/, .handoff/, clones/, node_modules/, .ephemeral/)
Skills append reliably but never reconcile: indexes lag, ledgers rot, migrations leave live-looking corpses, sent outbox items never get archived. Bookkeeping follows attention, not structure -- so nothing catches the abandoned lane until a human stumbles on it. /ops check is the missing sweeper: one read-only pass that detects the closure-debt classes and offers fixes (report-only by default; every fix is confirmed, never automatic).
The checks:
- Index lag -- README.md / meetings/README.md whose newest referenced date lags the folder's newest
YYMMDD-*file or CHANGELOG head entry by >14 days. CHANGELOGs are the heartbeat; READMEs are the lag indicator -- compare them per folder. - Ledger rot --
_tasks.yamlwith open tasks whoselast_updatedlags folder activity by >30 days;_insights.yamlwhoselast_compiledstamp is absent or >30 days older than its newest entry (compile never ran / is stale). Open the class with the total, before any per-folder finding (CR-175):
One line -- insights: N ledgers, M entries. A never compiled, B stale (>30d), C current. -- plus what it costs in words when any ledger has never been compiled. The per-folder findings are the same findings as before; what was missing was the sentence. CR-020 wrote the stamp so a never-running loop would be visible, and this check has read it correctly all along -- once per folder. On a 133-ledger vault that means the operator meets the fact 42 times among a thousand other findings and never once sees the number 42, which is how a third of a corpus sat out of the loop for months. NEVER and STALE are separate counts and not one finding with anpython3 ~/.claude/skills/insights/compile_status.py <vault> --quietorin it: a ledger with no stamp has never been read back since the folder was created (a setup gap), a stale one ran and then stopped (a cadence gap), and the two have different fixes. The script is the arithmetic for/insights statusand/analyticstoo, so the three cannot report different numbers for one corpus. It reads; it compiles nothing. Resolve the ledger mode first (CR-041). Readworkflows.task_ledger.modebefore judging a missing_tasks.yaml:local(default): as above, unchanged.external: the rot check is skipped and replaced by two others -- (a) incoherent declaration:systemorpointerabsent, or the pointer unresolvable; that is the real failure mode for this shape, and it is silent otherwise. (b) The duplicate the declaration exists to prevent: a folder declaringexternalthat nonetheless contains a_tasks.yaml. Report both as findings with the same weight as rot.none: skip._insights.yamlstaleness is checked in every mode -- the knowledge layer is local regardless of where the work is tracked. Rationale worth keeping in the report: a declared absence is deliberate, an undeclared one is indistinguishable from neglect. Reporting a correctly-configured folder as broken every week is worse than not checking it -- the first time the sweep is right and nobody believes it, the check has stopped working.
- Migration corpses -- artifacts that look live but were superseded by a move: root symlinks/files whose same-purpose counterpart elsewhere is fresher (dashboards,
_TODAY-*); folders inactive >60 days whose participant/topic stream demonstrably continues in a sibling folder. Offered fix: a tombstone (see ops-base Retirement Convention). - Outbox aging -- run the
/outbox listlogic: sent-but-unarchived items, manifest-less items, manifests missingKanonisk källa(CR-032), items pending >30 days, items resolved without being sent (avskriven, offered fix/outbox close --all-resolved, which files superseded drafts to.archive/-- CR-103), manifests whosestatusis not a declared form (report the item, the word, and the closest declared form fromskills/outbox/resolved.py; never rewrite it -- CR-103), and items that were sent but whose manifest has no status (CR-102): where the dispatching surface keeps a record of what it posted (a post log naming the file it sent), an item it sent whose manifest carries nostatusis a finding -- the status was most likely lost to a rewrite. Where no such record is available the finding is skipped and the report sayssent-without-status: not checked (no dispatch record), never silently. Offered fix:/outbox close --all-sent. A sent item ages out (CR-133): askickaditem with at least one untickedSvar förväntas påbox and noUtfall(all boxes ticked is not aged out: every reply came in), older than the same 30 days (the--stale-daysofresolved.py, default 30; no separate key, so it cannot drift from CR-120), is reported with the offered outcomeSkickat <date>. Inget svar inkom inom <n> dagar.(resolved.py --survey→offered_outcome) -- report and offer, never close automatically. Three further findings (CR-120): (a) staging items that have aged --draft,utkastorklar-att-skickaolder than the same 30-day threshold, which until now covered only pending and so missed where material actually accumulates; (b) a staged copy that differs from itsKanonisk källain content -- normalise relative links before comparing, because a staged copy correctly points relatively from_outbox/while its source points from its own folder, and a byte comparison over one vault reported 6 differences of which 5 were link rewrites and 1 real drift; a check that cries wolf 5 times in 6 is one an operator learns to ignore; (c) material resolved as handled (hanterad, CR-120) -- offered fix/outbox close, and unlikeavskrivenit needs noUtfall. AKanonisk källathat resolves to nothing inside the vault is reported as unverifiable, not broken -- a path into another repository is legitimate. TheKanonisk källafinding matters because without it nobody can tell whether a folder is a disposable rendering or the only copy of the material -- which is what made a bulk clean-up unsafe in the 260828 audit (86 items, 3 of 71 manifests named a source). - Sync duplicates --
* 2.*/* 3.*files whose base file exists. Report size+mtime comparison side by side; never auto-delete (the larger "duplicate" is sometimes the newer content). - Unrouted residue --
unsorted/folders with files >30 days old; root-level files matching paste conventions (__*,xxx -*,Namnlös*, untitled); and the drop zones and the audio exit, which age with three different meanings (CR-169, CR-170):.ephemeral/paststale_after_days(declared on itsvault_conventionsentry, default 14) -- a sweep that has not run. The material was declared homeless on arrival, so it may die; nobody has decided when.<inbox>/.files/past its ownstale_after_days(same default, opposite meaning) -- a promise nobody kept. The folder's whole claim is this file has a home and someone will route it there. Until CR-169 it had no ageing rule at all, so nothing in it could ever be late; one vault held 8 entries and 52 MB, all of them past use, the oldest five weeks. Report the age and the entry it pairs with.- A file in
.files/with no vault destination is the mis-filing that makes the zone leak: it belongs in.ephemeral/, where it would have been swept. Filed here it is immortal. Where the pairing entry names no destination, or the file is owned by something outside the vault (a build artifact, a release archive), report it as mis-filed at the drop -- the decision is taken at the drop, not later by gravity. - Audio whose entry is already archived -- the transcript moved on and the sound stayed behind. That is the countable form of a missing exit (CR-170): one vault reached 64 files and 1.6 GB in 14 days, 115 MB a day, after a retired watcher was replaced and nothing took over the exit. Report the file, its entry, and whether
audio_pathstill points into<inbox>/.audio/. Offer, never execute. None of these moves or deletes anything. For audio the offered action is a verification first:python3 ~/.claude/skills/inbox/audio_verify.py --dir <inbox>/.audio --against <audio_archive>, which comparesmdatpayloads rather than bytes, because 37 of those 64 files existed in the archive and none was byte-identical while all 64 matched on content. A pair that does not verify is kept and reported. 27 of the 64 existed only in the vault, 8 of them meeting recordings from a machine that was never in the chain -- for those the inbox was not a staging area but the only copy, and a sweep written without this check would have deleted them.
- Triage hygiene (CR-022) -- if a triage doc is registered: INKORG items unsorted >7 days,
[x]items not yet moved to the KLART archive, week anchor >7 days stale, plaintext-credential-looking lines (no-secrets rule; lines marked<!-- secret-ok -->are a recorded owner decision and are skipped). Offered fix:/inbox triage refresh(which handles all but the sorting -- that stays human). - Contract alignment & repo privacy (CR-023/CR-029) -- if
workflows.sweep.alignment_check.commandand/orworkflows.sweep.privacy_scan.commandare configured (maintainer machines only; absent → skip silently): run each command (read-only by construction) and parse its[OK]/[DRIFT]/[FINDING]/[SKIP]verdict lines. The privacy scan watches the whole public-repo tree continuously — denylist identifiers, secret patterns, and name-like tokens missing from the invented-examples allowlist — so a leak that somehow lands is caught within a week, not at the next audit. Report each[DRIFT]component with expected-vs-actual version and a pointer to the update runbook (documented in the alignment script's header).[SKIP]is reported as unverified, not clean -- an unreachable component (e.g. a stale mount) is itself a finding, and has previously hidden six releases of drift. Never auto-applies fixes: cross-repo version references and live deploys are human-confirmed changes. The alignment command stays the single source of truth for the component list; the sweep is the scheduled reader that guarantees its output is actually seen. - Structure conformance (CR-025) -- enforce the CR-010 single-inbox/outbox rule with a fuzzy matcher, because reality drifts through variants that exact-name checks miss: scan for directories matching
_inbox,_outbox,.inbox,.outbox, any*inbox*/*outbox*, and localized forms (inkorg*/utkorg*), case-insensitive. Everything except the vault-root_inbox/+_outbox/pair is a finding -- including empty scaffolds (they re-seed the habit). Paths inworkflows.sweep.structure_exemptionsare skipped with a one-line(exempt: <path> -- <reason>)note: deliberate exceptions are recorded once and respected. Offered fixes (never auto-applied): merge pending items into the central folder via the normal/inbox//outboxflows so manifests and indexes stay true; archive already-resolved material to its destination folder; delete empty scaffolds; or add astructure_exemptionsentry if the exception is deliberate. Rationale worth repeating in the report: a second outbox means the central pending list lies. - Hygiene (CR-122) -- the folder check's
hygiene_check.pyrun over the whole scope: link targets, Link scope and classes (CR-137). Every.mdfile is link-checked, not only dated files,README.mdandCHANGELOG.md-- that pattern skipped 53% of one vault'sops/tree, including every standards document and every rolling plan, because it selects files born with a date and skips the ones that are maintained.workflows.sweep.links.scope: datedrestores the old set. A target resolves from the file's own folder only; the pre-CR-137 fallback to any ancestor accepted 199 of 10,698 targets on one vault that were wrong against their own file. Findings separate into moved (one candidate, path offered -- mechanical), ambiguous (several, none chosen), dead (no such file anywhere -- a content finding: the document claims a source that does not exist), ancestor (reported, not failed, so an adopting vault is not met by a wall), and privacy: a link into.handoff/from any living document, or into private material from a document staged in_outbox/. Its offered fix is removal, not repair -- a broken link into restricted material is an inoperative leak, and repairing it mechanically restores it. Links into private material from an owner's own notes are normal and are counted once per run as an advisory line, never per link..transcripts/is deliberately not in the sealed tier: a summary naming the raw file it was built from is the vault's own provenance convention. re-dumped YAML, task ledgers outside their enums, unknown profiles, theme consistency, and (CR-128) raw files in.transcripts/archived by a client (written_by:) with an emptysummaryfor more than 14 days ("archived, never filed", with the inbox item each points to), and (CR-150) a team-facing file counting work per person -- a people-by-counts table, or a line pairing a person with a count of issues, pull requests, commits or lines -- naming the file and the line and never the figure, with the declaredmetrics_document:exempt and a name beside an item never a finding. Report only. - Company profiles (CR-125) --
company_tool.py check <vault> --copiesover the whole vault: stale facets, schema errors insources.yamlandbrands.yaml, locked facets without sources, credential-looking values, profile files outside_company/. Report only. - Unregistered project (CR-086) -- a folder carrying an
_ops.yamlbut no row in any register declared underregistry:is a finding, with the proposed row as the offered fix. This is what catches a project created before/ops project newexisted, or created by hand after it. Registers are resolved through the normal config chain, so both a vault-root portfolio and a per-organisation index count; a vault that declares no register skips this check silently rather than reporting every project. Never auto-applies: a hand-written register carries judgement columns (role, sponsor, mode) the sweep cannot fill, and a row proposed with those blank is the point -- it shows what is missing instead of inventing it.
Output: one report grouped by class, each finding with its offered fix as a command or concrete action. End with a one-line scoreboard (9 classes: 5 clean, 3 with findings (14 items), 1 skipped) so repeat runs are comparable. Young folders are exempt via the age thresholds -- a fresh project reports nothing.
Wiring: suitable for a weekly scheduled run that drops its report into _inbox/ as a triage item (closing the loop through the existing daily-triage habit). The sweep itself never mutates content.
THE DAILY LOOP, END TO END
What actually runs, in order, for a recurring series. Three of these steps are not /ops, and two
are deliberately not automated at all — those are the interesting ones.
Before the meeting
0. /ops orient — where did this leave off? One read-only pass: loop position, whether the next
agenda exists, chain integrity, whether a session was recorded and never written up (CR-087), what
is carrying and what is unowned, how stale the archives are, what is staged and unsent. Run it when picking a project up cold, before deciding what the session is for.
1. Refresh the archives — external CLIs, not skills.
<the chat archiver> -> <venture>/.teamschats/ (CR-047)
<the repo archiver> -> <venture>/.githubmeta/ (CR-055)
<the repo archiver> -> <venture>/.movementmeta/ (optional; CR-121/CR-148)Both are local CLIs run on demand, each authenticated in its own right. No skill fetches. A skill reads what an archiver wrote, which is why an agenda can be generated with no credential and no connectivity — see Step 9, Pre-Meeting Retrieval.
.movementmeta/ is the one exception made good. The movement block (the release gap, the repo
blocker table, the demo rows) is the single live read left in the pipeline: build_movement.py calls
gh itself. Where <venture>/.movementmeta/<owner>-<repo>.json exists it is read instead, with
{"issues": [...], "labels": [...], "events": [[event, iso-timestamp, number, label], ...]} -- each list
exactly as gh returns it. That makes the movement half generable with no credential, and it is why
these branches now have test coverage at all. No file, no change: gh is still the fallback.
2. Generate the agenda.
python3 ~/.claude/skills/ops/build_agenda.py --dir <project>/meetings [--date YYMMDD]Reads the previous note's ## Carried forward, counts consecutive sessions per item, writes the
Since the last standup digest slot for prepare to fill (CR-101), and reads repository activity
— issues, pull requests and releases, as declared — into the appendix. Refuses to overwrite an
existing agenda.
/ops prepare in a wired project runs exactly this — see prepare, Step P0. There is one way
to make an agenda here, not two.
3. prepare drafts the facilitator sheet; the facilitator owns it. (CR-082)
The agenda carries facts; the sheet turns them into questions, and which question to ask is judgement. The sheet also holds person-axis material — who to draw out, what is likely to be avoided — which is precisely what must never be staged anywhere it could be sent.
A draft is judgement offered, not judgement made. prepare in dual mode writes it as a layer on
top of the generated agenda; the facilitator then edits it, and the edit is the point. What must not
happen is the sheet being treated as finished output — it is the one artifact in this loop whose value
comes from a person having disagreed with it.
After the meeting, the same day
4. Choose the transcript source. List, then let a human pick.
Two recordings of one meeting is the normal case, not the exception — one per capturing tool, differing
in length and speaker resolution. Present document_id, title, duration and transcript variant with a
recommendation; do not choose. Record the chosen id in the summary, so the record says which recording
it came from.
Where the project declares a preference, that is the recommendation (CR-145).
external_systems.transcripts.prefer: is named-speakers or longest (the default, and the reading of
an unknown value). Measured, two captures of one meeting existed and nothing in the config said which was
the source, so the assistant chose one and mentioned the other in a header — a defensible, undeclared
choice the next run could make differently with nothing noticing. A declared preference makes the choice
reproducible; it does not make it final, which is why this step still ends with a person picking. The
recording not used is LISTED, never dropped — the agenda details carry both, one marked used, because
a recording absent from the output is indistinguishable from one that never existed.
A connected store says so (CR-145). transcripts: connected: true beside a store: means this step
can pull the transcript itself, and the pre-flight then reports the recording source as checked instead
of not checked. Declaring it is the operator's decision and is never inferred — a store's name says
nothing about whether anything can reach it.
5. /ops — one pass produces the summary, the registers and one CHANGELOG line.
Step 9 then runs post-processing as configured. The summary ends with ## Carried forward — that
section is what tomorrow's agenda is built from, so a note without it silently ends the chain.
/ops check checks that chain; it is the only defect in this loop that does not announce itself.
Close from the source (CR-142). Before an item is written as closed -- in the summary, in the carry-forward section, in a decision register -- open its source and read the state: the issue, the ticket, the plan section the line names. Say which source was read, with its state, in the line that records the close. Said in the room is a reason to look, not a source: measured, a carried item was closed on a loose spoken "resolved" while its own issue was still open, and its title had drifted enough from the issue's to make the spoken word plausible. Where the source cannot be read -- no access, a failed fetch -- the item is not closed; it carries, and the summary says why. (The agenda's own certain close already works this way, CR-147; this is the same rule where a person writes the close.)
A deferred item the meeting never reached is carried, not dropped (CR-108). Where the agenda
carried a ## Carried, not on today's agenda section, read it when writing the summary: any item
in it that the meeting did not address goes into Next Steps, keeping its owner. Deferring
something once is a decision; losing it because nobody raised it is not.
6. The recap is offered, not written — see Step 9, Generate Post-Meeting Recap. Say what it would carry and wait to be asked. It is the one artifact that leaves the building; assembled automatically from the narrowest of the three inputs it is confidently incomplete, and invisibly so to readers who have no transcript to check it against.
7. /outbox — stage it, and let the manifest carry the send record.
_outbox/YYMMDD-<recipient>_<subject>/ _manifest.md + the recapThe Status field is the only record that a recap was actually posted. A human sends it and marks
it sent; nothing writes that unprompted, because the click is where the posted message gets read.
What each step must not do
| Step | Must not |
| Retrieval | Fetch. It reads archives; refreshing them is the archivers' job |
| Agenda | Have its generated sections hand-edited, or overwrite an existing one. The digest slot is the exception: prepare fills it, once (CR-101) |
| Facilitator sheet | Be staged in _outbox/, or be sent as drafted without a facilitator having edited it |
| Transcript | Be chosen by the machine when duplicates exist |
| Recap | Be written unasked, or keep a second copy outside _outbox/ |
| Manifest status | Be advanced by a tool |
Where it is manual, and why
Steps 3, 4 and 6 stay in human hands, and should. The facilitator sheet is judgement
about people — drafted by prepare (CR-082) but never finished by it;
choosing between duplicate transcripts is a judgement the machine cannot make honestly;
and the recap is asked for rather than produced. None of the three is an unbuilt feature.
WHEN TO USE /OPS vs /PREPARATION vs /TRANSCRIPT
Two loop steps have two doors each. The door depends on whether the folder has a config:
| Loop step | Without a config (a contact) | With a config (a project) |
| prepare | /preparation | /ops prepare |
| process | /transcript | /ops process (the default) |
/ops prepare: before a team meeting. Writes the agenda — generated bybuild_agenda.pywhere the loop is wired (Step P0), from the template otherwise — plus the facilitator sheet when the meeting type ispreparation_mode: dual./ops process: after a meeting. The summary, changelog, README, ledger, meetings index and task import, as the folder declares. Marks the agenda (and any legacy preparation file) superseded./transcript: ad-hoc recordings, personal calls, folders with no config. Summary + changelog + optional task import only.
Flow:
/ops prepare standup → the agenda (before the meeting)
[meeting happens]
/ops process <content> → the summary; the agenda is marked superseded (after the meeting)When /ops and /transcript both apply, prefer /ops -- it is a superset of /transcript functionality.
CONFIGURATION
Config Loading
- Determine organization from:
- Explicit
organizationfield in project CLAUDE.md - Project folder name pattern (e.g.,
bravo-*-> bravo,acme-*-> acme) - Participant names matching team members in configs
- Explicit
- Load config following resolution order:
- Project-level:
.claude/ops-config.yaml - Folder-local (CR-011): nearest
<folder>/_ops.yamlwalking up from CWD until vault root - Vault-wide (CR-011, optional):
<vault-root>/_config/base.yaml - Skill defaults:
~/.claude/skills/ops-config/base.yaml - Deprecated fallback (removed v1.17.0):
~/.claude/skills/{org}-ops-config/{org}.yaml
- Project-level:
- Merge layers -- project overrides folder-local, folder-local overrides vault-wide, vault-wide overrides skill defaults
What Config Controls
| Setting | Effect |
language | Output language (english/swedish/input/per_claude_md) |
swedish_chars | Swedish character enforcement (strict) |
team | Participant recognition and attribution |
responsibility_matrix | Owner assignments |
terminology | Domain-specific terms |
summary_sections | Custom summary structure (overrides TWO-TIER) |
status_terminology | Domain-specific status terms |
issue_id_format | Issue ID pattern |
workflows.update_files | Which files to update |
workflows.action_propagation | Propagate actions to external files |
workflows.agenda_management | Post-meeting agenda updates |
meeting_types[<type>].preparation_mode | single (default) or dual -- whether /ops prepare produces one file or a facilitator/agenda pair |
workflows.meeting_templates | Per-meeting-type shape contracts + lint mode (warn/strict) -- see ops-base Template Contracts (CR-018) |
workflows.post_processing | Task import, optional priorities artifact (priorities_artifact.enabled), recap on request (recap_artifact.enabled), and carry-forward -> next agenda (carry_forward.enabled, see build_agenda.py) after meeting |
workflows.rolling_plans | Participant-triggered per-axis living planning docs (update after a matching 1-on-1) |
domain_additions | Extra sections to add to summaries |
templates | Custom template paths |
strings | i18n string overrides |
PROCESSING FLOW
Step 0.5: Load Applicable Rules (CR-013)
Before parsing the input, load any promoted rules from the _insights.yaml chain in scope:
- Walk up from CWD collecting
_insights.yamlfiles at each level (max depth 6, skip.archive/,.handoff/,clones/). - Filter to rules: entries where
confidence: ruleANDstatus: active.consensusis never loaded (CR-123): it records what independent voices said, not how this session should work. An unknown confidence is read ashypothesis, neverrule. - Cap to 20 entries — if more, prefer highest
confirmation_count, ties broken by most recent confirmation date. - Build a one-line-per-rule preamble in the form
[type] summaryand treat it as additional standing instructions for this run. For example:[preference] Bob prefers concise weekly summaries[pattern] Bob raises customer-success topics last in 1-on-1s[decision] Board updates use English for India team
- Apply the rules during Step 3 (Create Meeting Summary) and Step 4 (Apply Domain Additions). Do not echo the preamble in the output; rules influence content, not chrome.
Scoping: Only rules in the CWD's parent chain apply. A rule in meetings/marketing/_insights.yaml is not loaded when running /ops from meetings/management/.
No rules found: Skip silently. The skill works without rules; this is purely additive context.
Step 1: Parse Input
Extract from the input (transcript, notes, standup content):
- Participants and their roles -- use name resolution algorithm:
- Match against config
people[]roster (canonical, aliases) for recurring non-contact persons (CR-017) - Match against config
team[](name, aliases) for internal team - Match against
_contacts/*/_meta.yaml(display_name, aliases) for external contacts - Use resolved canonical names in output (correct spelling, Swedish characters)
- Committed-spelling consistency (CR-017): before saving the summary AND before writing CHANGELOG/README entries, run the folder-precedent near-miss check from
/transcript("Committed-spelling consistency" section) -- compare draft names and anomalous proper nouns against the target folder's recent files and CHANGELOG; use the established spelling when it resolves, flag both forms when it doesn't, never silently introduce a spelling variant. CHANGELOGs are how misspellings propagate; this check gates them too.
- Match against config
- Completed work items (who did what)
- In-progress work (current status)
- Decisions made (with rationale)
- Action items (with owners and deadlines)
- Issues/blockers discovered
- Technical updates
- Version/build information (if mentioned)
- Metrics and KPIs (if mentioned)
Step 2: Determine Summary Format
If summary_sections is defined in config (non-empty list):
Use the configured section structure. Each section becomes a numbered heading with the specified type (table/subsections/freeform). Only include sections whose trigger condition is met.
If summary_sections is empty or not defined:
Use the TWO-TIER SUMMARY FORMAT from CLAUDE.md:
- Concise Operational for weekly syncs, quick calls, standups
- Detailed Strategic for quarterly reviews, major decisions, milestones
Step 3: Create Meeting Summary
- Determine date from transcript/content or use today
- Determine filename and location from CLAUDE.md MEETING ROUTING
- Build summary using the format from Step 2
- Include metadata header (date, time, participants, format)
- Write executive summary (2-3 sentences)
- Populate all applicable sections
- Attribute actions to correct people using
responsibility_matrix
Step 4: Apply Domain Additions
For each entry in domain_additions from config:
trigger: always-- always include the sectiontrigger: if_mentioned-- include if the topic appears in the inputtrigger: if_relevant-- include if contextually appropriate
Append domain-specific sections after the standard summary structure.
Step 5: Update Files
Update files per workflows.update_files from config:
| File key | Target | Action |
summary | Meeting summary | Always created (Step 3) |
changelog | CHANGELOG.md | Add dated entry at top |
readme | README.md | Update Current Status, Active Tasks, Recent Meetings (the list label is the profile's: strings.units.recent_<unit>, CR-123) |
task_yaml | _tasks.yaml | Update/create per-folder task file (v2 schema). Skipped entirely when workflows.task_ledger.mode is not local (CR-040). Under follow_up: announcements (podcast profile) Follow-up rows never become tasks; the ledger is the host's editorial backlog, each task with a kind from the profile's task_kinds (CR-123) |
meetings_index | <summary_folder>/README.md | Add entry to the unit index. episode_index is an accepted alias with the same behaviour (CR-123); the folder is the profile's summary_folder (meetings/ by default) |
For changelog, follow the format in ops-base. Always reference the meeting summary file.
What the skill does, and what a project configures (CR-084)
Printed by /ops help and /ops orient for the active project, so the split is visible rather than
remembered. A second user of this skill needs to know which half is theirs.
| The skill — the same for every project | The project — its config and templates |
| The sources block, the status block, and their order | Which chats, repositories and ticket boards are declared |
| The carry-forward check against the sources | people[], with track and adjacent |
| Ordering by age; handing the track on | round_columns |
With tracks: declared: the Last track column and the balance line (CR-090) | tracks:, carry_forward.balance_window |
Today as the default date (the next session once today has a note), the past-date refusal, the Base: header line (CR-139) | carry_forward.skip_dates (which days the series did not meet, and why), carry_forward.meets / schedule_days |
| The lint checks (roster, vault-only ids, suffix) | Track names and the taxonomy behind them |
| — | Recap and chat-post templates |
people[].track is the person's default track — used only when the previous note does not
already carry one. areas is a different thing and is never read as a track: an area is what
somebody works on, a track is the axis the round runs along.
With tracks: declared (CR-090), the Track column stays blank — it is stated in the room — and
a separate Last track column shows what the previous note recorded, marked (carried). Only a
value that matches a declared track is carried. Above the table, one balance line counts the last
session by declared track, zeros included, and asks about any empty one: "website and offboarding
had no one. Intended, or unbalanced?" — a question, never a verdict. carry_forward.balance_window: N
counts the last N notes instead of one. A note with no round table gives neither, and the sources
block says round … not recorded.
Step 5.4: Archive the Raw Source (silent, always) (CR-085)
Run the RAW SOURCE ARCHIVE contract in ops-base. Same definition /transcript Step 2.5 runs —
.transcripts/<summary-stem>-raw.md, frontmatter plus verbatim input, the Råmaterial: back-link in
the summary, one confirmation line, the read-back lock, and the skip conditions.
Do not restate the contract here. /ops is documented as a superset of /transcript and went
without this step precisely because the contract lived in one skill instead of the shared base.
Where the input came from a transcript store rather than pasted text, record the store's document id, original filename, duration and transcript variant — the id is what ties the archive to the recording rather than merely proving something was said.
One session, several folders: one raw file for the input, summaries: listing each summary it
fed, disposition: stating the split. A part routed to a frozen snapshot is named there and never
linked.
Step 5.5: Knowledge Extraction (silent)
After updating files, scan the meeting summary for durable insights worth accumulating. This step writes to _insights.yaml in the same folder as the CHANGELOG -- it is a silent accumulation layer that never surfaces in any skill output.
Same extraction logic as /transcript Step 3.5 (insight types, threshold, format, dedup). See the transcript skill for the full _insights.yaml schema and extraction criteria.
Reusability (CR-020): Names are allowed in insight entries (the former no-names privacy rule is retired). Prefer name-free phrasing in summary/tags when the insight generalizes -- context and source.file carry the who/what; names in rationale are always fine. Apply the write-time vocabulary guard from /transcript Step 3.5 (the resolved profile's types only, confidence = hypothesis|rule|consensus and never written here above hypothesis, YYMMDD dates, integer ids, max 5 tags, voice only from the profile's voices, targeted edits and never a re-dump, CR-122/CR-123).
Swedish characters: When writing Swedish content in _insights.yaml, ALL words MUST use correct å, ä, ö. YAML files are equally prone to missing characters.
source.step is an identifier (CR-178). Any edge_case or correction this skill writes --
from Step 5.5 here, or from the template-contract check in Step 8 -- carries the step id alone
("1", "5.5", "9") from feedback_steps in ecosystem.yaml, naming where the problem
occurred rather than where the entry was written. What the entry is about goes in tags. The
field was free text and accumulated 120 spellings of a handful of values across both skills, which
is what stopped /insights compile Pass 1 from clustering on skill + step at all. Existing
entries are read and never rewritten.
Additional ops-specific sources:
- Strategic decisions from
domain_additions->decisiontype - Cleared agenda items with resolution ->
learningordecisiontype - Configuration or workflow changes discussed ->
decisiontype
Process:
- Scan the meeting summary (including domain addition sections) for qualifying insights
- Write to
_insights.yamlin the same folder as the CHANGELOG - Dedup by
source.file-- if insights from this meeting file already exist, skip
Skip conditions:
- No CHANGELOG.md in the target folder
- Pure standup (short status updates without decisions)
- User explicitly said "skip insights"
Output: Same brief format as transcript Step 3.5:
Extracted [N] insights to _insights.yaml:
- [type] summary sentenceStep 6: Propagate Actions
If workflows.action_propagation.enabled is true:
- Decisions ->
targets.decisionsfile - Per-person actions ->
targets.actions_by_person.{name}file
Step 7: Manage Agenda
If workflows.agenda_management.enabled is true:
- Open the file at
agenda_management.file - Find the section matching
agenda_management.section - Clear items that were addressed in this meeting
- Add new follow-up items from this meeting
- Update date to next scheduled meeting (if determinable)
Step 8: Apply Language and Output
- Resolve the output language for the target file path:
- If
language: per_claude_md, look up the target file path in the project CLAUDE.md LANGUAGE POLICY table. Different paths within the same vault may require different languages (e.g.,meetings/management/= Swedish,projects/acme-mobile-v3/meetings/= English). - If
language: englishorlanguage: swedish, use that language for all output. - If
language: input, match the transcript/input language.
- If
- Apply the resolved language to all output: summary content, section headings, filename keywords, CHANGELOG entries. When creating preparation files (
/ops prepare), the preparation must use the language matching its target path -- not a vault-wide default. - CRITICAL: If
swedish_chars: strict, verify ALL Swedish text uses correct å, ä, ö before writing any file. Never write "for" instead of "för", "ar" instead of "är", "mote" instead of "möte", etc. See ops-base for full list. This is a blocking requirement. - Template-contract check (CR-018): resolve the meeting's shape contract from
workflows.meeting_templates(most specificmatchwins, elsedefault= CR-006 canonical) and verify heading sequence, action-table header row, and empty-Beslut marker before saving.mode: warn(default): save + report the diff + logedge_case;mode: strict: ask first. See ops-base "Template Contracts" for the full rule. Deliberate format changes are made by editing the contract, not by letting a file drift. - Include suggested CHANGELOG entry
- Include cross-references to related documents
- Include next steps or follow-up items
Step 9: Post-Processing
If workflows.post_processing is configured:
Task Import (if task_import.enabled)
- Extract action items from the meeting summary (same logic as /transcript Step 4)
- Resolve the task ledger (CR-040) before anything else:
mode: local(default): find the local_tasks.yamlin the folder where the meeting summary was saved (or nearest ancestor). Create with v2 schema if missing.mode: external: no ledger file is read, created, or walked to. Steps 3-6 below do not apply. Instead, present the extracted action items split in two -- those whose next action is implementation (recorded by reference in the summary usingreference_field, e.g. a CR id or issue key, and offered to the user to raise insystemif no reference exists yet), and those whose next action is a decision, an owner or an escalation (offered to the folder's declared coordination surface). State the system of record once in the summary so a reader of that file alone can find the work.mode: none: action items stay in the summary; no import is offered.
- Match extracted items against existing tasks -- update status/notes for tasks mentioned
- Present NEW action items and offer to import (yes/no/select)
- For imported tasks: assign defaults (P1, source = meeting file, context from folder)
- Mark completed items from the meeting in the local _tasks.yaml
- Triage target for personal items (CR-022): when an action item is personal/ad-hoc (owned by the user, no natural org/project folder) and the vault has a registered triage doc, offer "lägg i triage-INKORG" as an alternative target -- append the item as an open bullet under INKORG with a source link to the meeting file. One item = one home: it goes to the triage doc OR a
_tasks.yaml, never both.
Pre-Meeting Retrieval (if external_systems is declared)
A transcript only carries what was said out loud in the room. Decisions posted to the chat, issues opened or closed overnight, a document linked at 07:23 — none of it reaches the record, and most of it never comes up in the meeting either. Observed 2026-09-21: a QA lead posted the agreed browser and device matrix to the series chat hours before the standup; it appears in no transcript.
Retrieve before generating the agenda and the facilitator sheet, not after.
Sources come from external_systems (CR-054, contract 16) — resolved by the normal config chain,
.claude/ops-config.yaml then the folder's _ops.yaml. Do not invent a second key for this.
external_systems:
chats:
- id: "19:meeting_...@thread.v2" # the platform's own identifier
name: "Webapp-v3 project (standup)"
default: true
repos:
- url: "github.com/Org/repo"
reads: [docs, issues, releases] # metadata only, never the codechats: is a retrieval source, not only a send destination. A dispatcher reads it to know where to
post; this step reads the same declaration to know where to look. The archive under <venture>/.teamschats/
(CR-047) stores _chat.json carrying the platform id, so the folder is resolved by matching that id
rather than by a second hand-written name that would drift.
Read EVERY declared chat (CR-071). default: answers "where does this project post?" — one answer,
and the dispatcher's. Retrieval asks "what was said anywhere that bears on this session?", which has no
default. A project running across three chats whose agenda reads one produces a block that is
complete-looking and partial. Report the count per chat when more than one carried traffic: an
undivided total does not say where to go and read. A declared chat that is not in the archive prints as a
problem and is never counted as a message — same rule as reads: below, a skipped source that
announces itself is honest; a silent one looks like an empty result.
A one-to-one conversation can be a declared source (CR-145). A chats: entry may carry
kind: direct with a person:, and is then read exactly as a group chat is -- same archive, same id
match, same message reader. The kind changes only what the output calls it: direct: <person>
rather than the conversation's title, in the sources block, in the Built from line and in the digest
marker. Measured, an operator sent four one-to-one questions after a meeting, to the people who owed
the answers; the archive read only declared group chats, the replies were invisible, and the operator
relayed them by hand. A conversation title is not the fact a reader needs -- whose reply arrived is.
person: resolves against people: (declared name, alias or first name, the same resolution the
round table uses) and the output uses the roster's own spelling; an unresolved name is a config
finding and never a dropped source.
External dependency, stated rather than guessed: the archive has to CONTAIN the direct conversation. Declaring
kind: directdoes not make anything fetch one — the matching option belongs to the chat archiver, a separate tool in a separate repository. Until that archiver writes the conversation to<venture>/.teamschats/, a declared direct chat reports declared chat not in the archive: the honest answer, and the same one any unreachable chat gets.
Read the archives; do not fetch. <venture>/.teamschats/ (CR-047) and <venture>/.githubmeta/ (CR-055)
are siblings by design — an archiver writes, this reads. So an agenda generates with no credential and
no connectivity, and the morning it is needed is not when a token turns out to have expired.
reads: is a declared scope, not a capability. The archiver records the declared scope in
_repo.json; honour it there. Report issues where issues is listed and pull requests where pulls
is listed (CR-101) — merged, awaiting review with age, opened, closed unmerged — and releases where
releases is listed. Say so only when neither issues nor pulls is declared: a skipped source that
announces itself is honest; a silent one looks like an empty result. A repository that tracks its work
in a separate ticket system keeps its movement in pull requests, not issues.
Closure evidence can be contradicted (CR-101). Probably closed takes the newest matching evidence, not the first; a matching chat message with a negative (failing, not solved, reverted, still) is counter-evidence and cancels an older positive; a merged PR, a published release, a closed issue or a done ticket outranks any chat message; the evidence cell shows the date (and time) it used. A done-word is evidence of a claim, not of a state.
Matching is on explicit identifiers only (CR-141). Probably closed pairs a carried line with
evidence when they share an identifier — the line's own #id or ticket key (a written range like
TICK-9..12 counts as every key in it), or a pull request that names the issue (Re #12,
Fixes #12). There is no title-similarity matching: measured over three mornings on one project the
word heuristic it replaced was right 0 of 9, 2 of 5 and 1 of 7 times, because the evidence string
always carries the repository name and the ticket prefix. Consequences:
- A line with no identifier gets no marker. It stays carried and is listed in the details file
under Items with no identifier, cannot be checked — no marker there means not checked, not
not moved. Add the
#idto that line in the summary and the next agenda can check it. - Every marker states its basis, so a reader verifies it in one click:
issue #12 closed 2026-10-16 11:02,PR #7 (Re #12) merged,ticket TICK-9 done. - A state change is not a close. A ticket that only reached fixed, in review or in test is reported under Fixed, waiting for a test and stays carried; it is never probably closed.
A carried line is re-read from its source before the card is built (CR-147). Probably closed suggests; this acts, and only on a certain close. Measured over two consecutive meetings, about six of eleven dependency rows and then four of seven were removed on sight by the person leading, and for most of them the tracker already said closed, merged or done. The agenda's rule still stands — a machine that closes items silently is worse than one that repeats them — so the answer is not silence but a close that shows its basis:
- Certain means a state read off the work's own source: an issue whose state is closed, a pull
request that is merged and names the id (
Fixes #12,Re #12), or a ticket whose state contains done, closed or resolved. A merged pull request that names no id closes nothing — it is evidence about itself. A release is not a close, and neither is a chat word: both stay probably closed. A label change never closes (fixed, waiting for a testwins, CR-141's precedence). - The line leaves the card with a trace. The header gets one line, Closed in the data since the
last meeting:
<ids>— naming the ids whose own source closed them, never the rest of the line's id set — and the basis and the close time follow: in the details file on the card layout, in the agenda's own Closed in the data since the last meeting section on the list layout, which writes no companion file. It is not also offered under Probably closed — it is gone, not a question for the room. - A line nobody can read anything about is not asserted as live. With no identifier it comes off
the card from its second session on. Its first session is the one exception, and it is
deliberate: the line was raised aloud last night, the room is its confirmation, and proposal 4 is
that this pass may not unmake what the room created. So stays on the card only if its owner
confirms it is the rule from the second session, not the first. The line is listed in the details
file in the same Items with no identifier, cannot be checked table as above, with a column saying
why it is or is not on the card — first session, raised in the room, awaiting its owner's
word, confirmed by its owner, aged out — and how many sessions it has carried. It returns to the
card when its owner confirms it in their own minute and the summary writes
*confirmed: <what they said>*on the line; unconfirmed after two sessions it is marked aged out and the card stops asking. Aged out means off the card, not out of that table: the line keeps its row, because dropping it from the one list that names it would be dropping it silently. - The list layout is untouched by this proposal. Its carried-forward table is the full list by definition and keeps every line, so nothing comes off it for want of an identifier — and the On the card column, which would have no meaning there, is not printed rather than filled with a guess. A certain close still applies in both layouts: that is a state read off the source, not a judgement about what today's short list should hold.
- Creation stays in the room. The data can close a dependency, never open one: a closed issue nobody carried adds no line.
- A failed fetch names the age of what it left behind (CR-140, extended by CR-145). The agenda header carries one line per failed fetch: the failure, its reason, and when the data the sections below are reading is from (data is from 260921, or no successful fetch on record). Measured, a fetch failed for a morning and the repository section used older data without saying so. One line per fetch, extended — never a second line about the same fact.
- A source that could not be read removes nothing. A fetch CR-088 records as failed, or an archive the producer could not reach at all, holds every verdict it produced; the header names the source and why. A fetch that was never recorded is not a failed one.
An unconfirmed inference ages, exactly as a line nothing can be read about does (CR-142). A line
carrying *inferred: <what>* is in the same tracked state as cannot be checked above, with the same
two routes out and the same threshold:
- From its second session it comes off the card -- the agenda does not assert an inference -- and it is listed in the details file under Unconfirmed inferences, off the card, so it leaves with a trace. A line that also names no identifier is instead listed in the cannot be checked table above, which already carries it with a card column and a session count: one section per line, never two.
- The marker decides the state, not the identifier. Most inferences name no identifier at all -- an owner, a release day, which items belong together are exactly the phrases a garbled recording produces -- so a marker-bearing line reaches Confirm or drop whether or not it names one. A line carrying no marker and no identifier keeps CR-147's handling unchanged.
- After two sessions still unconfirmed it is listed on the card under Confirm or drop, owner first, with what was inferred and how many sessions it has carried. The question is for a named person, which is why the ordering is by owner and not by item.
- A line the data closed is not also asked about. Where the line's own source says closed, the
header reports it under Closed in the data since the last meeting and the line is gone; it is not
also offered under Confirm or drop, for the same reason CR-147 keeps it out of Probably closed.
A wrong guess inside a closed item is settled by the close -- and a decision register line carrying
the marker is still reported by
check. - It returns to being asserted the moment its owner confirms it and the summary writes the confirming source on the line -- which is proposal 5's rule, seen from the agenda's side.
A snapshot is a reading taken at a moment, not an event log. If the newest one predates the last note, say so rather than presenting stale rows as news.
build_agenda.py writes a Since the last standup — not said in the room section into the agenda
holding a marker, not the messages (CR-101): raw chat lines in a team-facing document are noise
and read as surveillance, while the facts they carry are the agenda. The digest is judgement, so the
script leaves the slot and prepare Step P0 fills it — see there. Both sources are best effort: a failure prints one line and the agenda is still generated, because
an agenda missing because a network call failed is worse than one missing its context block.
What to do with it: the agenda carries the facts; the facilitator sheet is where they turn into questions. An issue that moved with nobody assigned, or a decision taken in chat that half the room has not seen, is exactly what the round will otherwise skip.
Definitions: company standards and project terms (CR-118)
Two levels, one direction of authority. An organisation declares its standards folder (standards: in an org
config; its README is the index). A project declares its own definitions file (definitions: beside
carry_forward). /ops orient prints both, with the project file's counts.
- A project never redefines a standard term. It links to it; a narrower meaning says narrower than and links.
- Every project term carries a status in a table with a Status column: local (specific to this project) or proposed upward (looks general; named in the standards' open list).
- When a meeting decides or coins a term,
processwrites it to the project's definitions file, never to the standards. First question: would another product need this word? If yes, mark it proposed upward and name it in the organisation's open list. Promotion is a company decision (a forum, by consent), recorded where the organisation records decisions; the standards then point there, and the project keeps one linking line. - Nothing reaches the standards as decided from a project.
/ops checkreports: a project term that also appears in the standards index without a narrower than link; a term row without a status; a proposed upward term not named in the open list.
Carry-Forward (if carry_forward.enabled)
The gap this closes: a series writes an agenda, holds the meeting, records a note -- and nothing compares them. Items fall through silently, and the same item can lead the agenda twice without anyone noticing it was skipped twice. Observed 2026-09-21: six of seventeen agenda items fell through, and every one without a named owner fell.
The daily note ends with
## Carried forward-- one line per item that did not land. This is a contract, not a habit: the section is what the next agenda is built from, so a note without it silently ends the chain./ops checkverifies it (lint step 5) -- write the section even on a day when nothing carried, because an empty section and an absent one mean different things.Where nothing carried, say so in the section. An omitted section is indistinguishable from a note nobody finished.
The section is cumulative: it lists everything still open at the end of that session, not only what that session produced. The name means what it says — an item carries until it closes. This matters because the next agenda reads the newest note only: a short extra session that listed only its own two items would silently drop everything still open from the week before. Writing the section is therefore a review of the standing list, not a summary of the last hour.
(This is the one place the loop's rules pull in different directions. The recap is bounded to its session — §2b of the recap standard — because it reports what happened. The carry-forward section is cumulative because it reports what remains. Same series, opposite rule, and conflating them produces either a recap that invents history or a chain that loses items.)
note_suffixmay be a list where a series has more than one filename shape — a weekly plus its extra sessions, or a series renamed mid-history. Matching only the main shape silently drops the rest, and an extra session is where the most urgent items tend to live.- **<item>** — <note> · **<owner>** - **<Name>:** <what they owe> - [blocker] **<item>** — <source> · **<owner>** (CR-107: optional kind)Kind (CR-107). A line may open with
[blocker],[dependency]or[decision]; untagged is a task. In card layout the kind decides whether the item is talked about or sits in the appendix, soprocessassigns it while writing the section: blocker -- it blocks the release (CR-114; with labels declared, prefer the repo's label to a carried line); dependency -- a person waits on another person or on someone outside the room; decision -- the room must choose. A person's own pending work is a task, however important. Everything else is a task, and a task only reaches the card when it has carried past the escalation threshold.A carried line names something its owner can open -- an issue or ticket id, a path with a section, a decision id. A paraphrase of another document's term fails in the room: its owner does not recognise it. And it carries no counts: a number written into a carried line is stale by the next session, while the movement block reads it fresh. (Observed: two lines recognised by nobody, and a count read out after the movement block had already given the current one.)
A carried line's text does not replace its source's title (CR-142). Where the line is a paraphrase, quote the title once --
**Card in the production pipeline** ("a test account with a card, number and address", #12)-- and from then on the paraphrase is anchored. A change of wording needs the owner's word, written on the line like any other confirmation. Measured: a carried title drifted from a test account with a card, number and address to a card in the production pipeline, and the drift is what made a loose spoken "resolved" sound plausible about an issue that was still open.What was inferred is marked as inferred (CR-142). Where a phrase was not said and is in no source -- a garbled passage resolved by guessing, an owner nobody named, which items belonged together -- the summary writes
*inferred: <what>*in the source part of the line:- [dependency] **Backend answer on refunds** — TICK-9 *inferred: the release day* · **Ann**The same marker goes in a decision's note, and as
Inferred:on its own line in prose. It is the contract, not a habit:build_agenda.pyreads it on a carried line and/ops checkreads it in a decision (see Carry-Forward andcheck). Measured: one meeting's audio was partly garbled, three phrases were resolved by guessing and written as facts -- a release day, an owner, which items belonged together -- and two days later the people involved answered in the chat and all three were partly wrong; a decision register line had to be corrected after the fact.A confirmed inference loses its marker in the next summary, with the confirming source. Not silently:
*inferred: the release day*becomes*confirmed: release day from TICK-9*. The marker earns its keep only if it comes off when the answer arrives -- a register full of stale markers is read as decoration, and the next assistant stops writing them.- Owner is read from a defined position, never guessed from prose. Trailing bold after the last
middot, or the label itself when it is a person's own line. Anything else is
UNOWNED, and that is a finding rather than a formatting slip -- an item with no owner is precisely the one that falls through an agenda that lists it. Generate the next agenda -- do not hand-write it:
python3 ~/.claude/skills/ops/build_agenda.py --dir <project>/meetings [--date YYMMDD]It counts how many consecutive prior notes carried each item and puts them at the top of the agenda, before the round.
Run it with no
--datehere. This step follows step 5's summary for today, so today has a note and the default resolves to the next session — which is the agenda this step is for (CR-139).The full build is long-running — run it in the background (CR-140). It reads every declared source and, in card layout, the repository's whole issue history: about three minutes, which exceeds the default foreground command limit. Start it in the background and read the agenda when it lands.
Check the sources first —
--check(CR-140):python3 ~/.claude/skills/ops/build_agenda.py --dir <project>/meetings --checkOne table, one row per declared source — chat/repository/ticket fetch, each
reports:andvault_reports:entry, the previous note, the recording source — eachready,stale,failedormissing, with the time it was last good. It exits non-zero on any stale or missing source, so it is what a scheduler runs before the build.It answers in seconds: the fetch records, the local note and one status call per declared report, and none of the readers that ask what moved. That is the point — before
--check, the only way to learn that a source was missing was to wait out the three-minute build and read its sources block, which reports a three-day-old status report three days late.State Means Next step readyfetched since the last note, or dated within three days build stalethe last success predates the last summary, or a report is three days old or more re-run the fetch, or ask the report's owner failedthe fetch itself failed — a login expired, a timeout fix the fetch; the archive still holds the last good data, which is not today's missingno fetch recorded, a report not found, a declared recording archive not found, or the previous note has no ## Carried forwardwrite the close-out, or fix the config checkeda declared recording archive resolves, or a store:is declaredconnected: true(CR-145)nothing; the row never fails the check, because this script does not fetch not checkeda recording store is declared with no archive and not connected preparechecks the store by hand (CR-117)undeclared(CR-151)the declared status reports name a repository on more than three days that the project does not declare run propose_repos.pyand decide; it never fails the checkA report's age is read from its filename in either dated spelling — 6-digit
YYMMDD(the house convention, and whatvault_reports:patterns declare) or 4-digitYYYY-MM-DD. A name carrying neither is reported asmissingwith no date in the name, never as fresh. Acurrent:path's age is its last commit date, read in the zonecarry_forward.movement.tzdeclares — the same zone the full build reads it in, so the two cannot disagree about one report across the three-day boundary.checkedon the recording row means the declaredarchive:was found, not merely declared: a typo'd or deleted archive ismissing, because the archive reader would find nothing there.A missing report directory names a similarly named sibling, when the declared parent still holds one sharing a word:
NOT FOUND — ../board/status-reports · config stale? ../board/status-reports-2026 exists. A folder renamed overnight otherwise reads as source missing and sends a person looking for a report that exists, when the stale thing is the config. Same parent only, and a shared word of at least four letters: a suggestion that is wrong is read as an answer, so a loose one is worse than none. Letter runs and digit runs count as separate words, soreports→reports2026is matched too, while a year alone in common (agendas2026→minutes2026) is not. The declared directory is looked for at every level first: a sibling is suggested only when the declared folder is nowhere to be found. It decides nothing — the report is still reported missing.A repository the reports name and the config does not hold is a row, not a refusal (CR-151).
undeclaredis the opposite question from the other states: not is the declared source there but is a source missing from the declaration, so it leaves the exit code alone. Measured, a project declared one repository — its own portal — and the release depended on at least seven others, so the agenda, the movement block and the digest all read a fraction of the work and read as complete. The row is read from the same windowpropose_repos.pycounts over, so the warning and the proposal cannot disagree about the same repository. Deciding what belongs is the operator's: see below.A failed fetch is also named in the agenda's own header (CR-140), not only in the fetch's own file and one line of the sources block. The sections built from an archive use its last good data and read as current, so the page looked complete while a fetch had failed twice that morning.
Which repositories the project should declare —
propose_repos.py(CR-151):python3 ~/.claude/skills/ops/propose_repos.py --dir <project>/meetings [--days 14] [--date YYMMDD]It reads the declared
vault_reports:series (CR-116) over a window — 14 working days by default — and counts the repositories those reports name. Three shapes, which are the three ways a status report names one:owner/repo;repo#12,repo PR #12orrepo issue #12; and the first column of a table whose first-column header says it holds repositories. The header gate is the whole design of the third shape: a status report is mostly tables, and a reader that took the first cell of each would count areas, people and dates as repositories.Why it exists. A project declared one repository, its own portal, so the agenda, the movement block and the digest read that one only. The release depended on at least seven others — the public site, the proxy, the core API, two microservices, a configuration repository and the database — and the only place that said so was the daily status report: in a table of backend merges, in the release-branch paragraphs, in the blocker list. A count taken on the declared repository alone showed several people with nothing merged; over the others the same people had merged work every week. The first reading had been wrong and nobody had been told. Counting mentions across twelve consecutive daily reports gave the shape in minutes — one repository in every report, two in all but one, then a middle group named on five to eleven days, then nothing that was not an ordinary word.
What it prints, per repository: the number of days it was named (not mentions — a repository named nine times in one report was named on one day), the number of distinct pull requests or issues cited, whether the project declares it, and a one-line purpose copied from the report's own words. A purpose a script composed for a repository it has never read is a guess in a config file, and a guess in a config file is indistinguishable from a decision.
Listed as Means declaredthe project already holds it NOT DECLAREDa candidate for the proposed block cannot be read — no organisation in the reportsnothing in the window wrote it as owner/repoand the project does not declare it, so there is no address. Written with a marker, never an invented organisationprobably prose a word that matches a repository name — a repository called scripts, a wordbilling. Still shown, under its own heading: silently dropping it would mean the repository genuinely calledscriptsis never proposed at all. Anyowner/repospelling or any cited item number makes a name a repository insteaddeclared and not named in the window a re-run's other half (proposal 5). Never removed — a repository can be declared for a reason no report mentions. Reported only when the window is at least 30 working days, so a short window cannot call seven repositories quiet Nothing is written without a yes. The default run prints the proposal and exits. The block is written only by a second run that names, repository by repository, what the operator confirmed:
python3 ~/.claude/skills/ops/propose_repos.py --dir <project>/meetings --write --confirm core-api,site--writewithout--confirmis refused, and so is a name the reports did not show. New entries getreads: [pulls, releases]and nothing else (proposal 4) — a repository's issues are its own team's working surface, andissuesis added by the operator. The written block carries a comment naming its basis and date, so a reader meeting it in six months can tell a declaration somebody made from one a script proposed and somebody confirmed, and on what reading. The insertion is textual, under the project's existingexternal_systems:/repos:: these configs are hand-written and carry the comments that say why each declaration is there, and a YAML re-dump would return the same data with every one of those comments gone.A re-run is a proposal again, every time (proposal 5). It compares the declared list with the reports and names additions and quiet entries; it never removes an entry itself.
It resolves which files a
vault_reports:declaration names throughbuild_movement— the walk-up from the project root, the pattern,filename_date()— never a second resolver, so a window can never count mentions in a folder whose age nothing is reporting. A declared series with no dated report in the window, or a file that cannot be read, is reported rather than counted as a report that named nothing. Reading the work trackers behind those repositories is a separate source with its own access question, and out of scope here.The date defaults to TODAY (CR-139) — or to the next session, once today has a note. It used to default to the next scheduled day after the newest note, which is only right when the series meets every scheduled day and every meeting is written up the same day. On a morning after a day nobody met, that default built an agenda for the skipped day — and the generator then stamped its own output STALE, the right warning on the wrong file.
The loop runs this generator twice with no
--date, and the two runs want different dates:Run What exists What is built the morning run (step 2 of the loop, prepare)no note for today; the session is ahead today the same-day close-out (step 5 writes today's summary, then this step) today has a note the next session the series meets, read from meets:/schedule_daysA note for today is the signal that the close-out has happened, so it resolves the date rather than refusing it; nothing in the documented sequence needs
--date.One refusal comes with it, and it is not the pre-existing "exists -- delete it first" guard (which says only that this agenda was already generated):
Refused Why Way through a date earlier than today, not typed the day has gone; the agenda would be read after the meeting it was to prepare --date YYMMDD— the caller saying they mean itAn explicit
--datebehaves exactly as before, including one naming a day that already has a note: deleting the agenda and rebuilding it is the ordinary way to correct one, and refusing that would take a working case away.skip_dates:declares the days the series did not meet, each with a one-word reason. A skipped day is not a missing note: the base note stays the last meeting held, and nothing ages on a day nobody met — the elapsed-days count takes the declared days out.workflows: post_processing: carry_forward: meets: [mon, tue, wed, thu, fri] # alias for schedule_days; three letters or full names skip_dates: - date: 261020 reason: holiday-in - from: 261224 to: 261226 reason: leaveThe list is operator-written — the skill does not know which days a team is away, and there is no holiday calendar of its own. One unparseable entry is skipped and the rest are kept: a single bad line must not take the whole list down and silently restore the ageing it was written to stop. A bare
261020is accepted and reads as no reason given, and so isskip_dates: 261020written without the dash — one day said unambiguously, which YAML reads as a scalar. An entry giving onlyto:is the same one day as one giving onlyfrom:.meets:is an ALIAS forschedule_days, not a second key. Both answer the same question — which days this series meets — so a config declaring both would have no defined winner.schedule_dayskeeps precedence, which is what makes the alias additive: a config that already declares it behaves exactly as before.The agenda's header says its base:
Base: <note> (last meeting held <date>; skipped: <dates>). The base summary was already named in the sources block, but a reader working out why a session count did not move has to know which days did not count — so it goes above the carried list, not below it. Only skips between the base summary and this agenda are listed.Escalate on sessions OR elapsed days, whichever trips first.
escalate_after(default 3) andescalate_after_days(default 14).A session is not a unit of time. Three sessions is three days on a daily standup and up to three months on a fortnightly one, so a session count alone escalates far too late on an irregular series — which is precisely where items go missing. Measured across four real series, none was regular: nominally-weekly meetings showed gaps of 7, 14, 21 and 26 days, and a fortnightly one had a 59-day gap. On that series an item could carry for two months and still read as
2, below threshold and invisible.The days rule is effectively inert on a daily series, where sessions trip first. It exists for everything else.
The days are counted in sessions, not calendar days (CR-139): days declared in
skip_dates:are taken out, so a holiday or a week of leave does not escalate an item nobody could have worked on. Withoutskip_dates:the count is the calendar gap, exactly as before.At either threshold the agenda says so itself: an item that survives three agendas is not an agenda problem -- it has no owner who is present, or it is not actually being asked for. Agenda position alone does not get an item raised.
- The facilitator's close is the control: read back what carries and whose name is on each. An item read back without a name is the one that will be on the agenda again.
Config lives in workflows.post_processing.carry_forward, resolved by the normal chain -- a
project's .claude/ops-config.yaml, then a folder's _ops.yaml, nearest first. A recurring series is
not always a project: an org-level meetings folder has no project config, only an _ops.yaml further
up, and looking for the former alone finds nothing and falls back to defaults silently.
note_suffix takes a * wildcard, because real filenames are not uniform -- one series carries a
week number (coreteam-weekly-w38), another carries participants who change
(bi-weekly-Ann-Bo-Cai[-Dee]). An exact match finds neither.
Companion artifacts are excluded by name -- agendas, preparations, priorities, facilitator sheets,
appendices, recaps, staged messages. A greedy wildcard otherwise swallows them: coreteam-weekly-w*
matches the -appendix file, bi-weekly-* matches the -preparation. Treating a preparation as a note
would read next week's intentions as last week's record. The round table is built from the people roster, so the
mechanism is identical across series and only the labels differ.
Dashboard Refresh -- retired (v1.79.0, CR-089)
/daily-dashboard has left the suite, and since v1.81.0 dashboard_refresh is ignored. /ops status
reports it among unknown keys.
Mark Preparation as Superseded + Bidirectional Link (CR-005, always)
After the meeting summary is created, check if a preparation file exists for this meeting:
- Search the same folder as the meeting summary for files matching any of these patterns where
YYMMDDmatches the meeting date:YYMMDD-förberedelse-*andYYMMDD-preparation-*(single mode, legacy keywords — still read)YYMMDD-facilitator-*(dual mode, private)YYMMDD-agenda-*(dual mode, visible)
- If found (one or more files match), insert a blockquote at the very top of EACH matching file (before the H1 heading):
> **Superseded** by [YYMMDD-meeting-summary-filename.md](YYMMDD-meeting-summary-filename.md)
- Also write a back-link in the meeting summary (CR-005): add a line in the metadata footer of the summary file pointing to the preparation file(s). For dual mode, link both files:
*Preparation: [YYMMDD-agenda-filename.md](YYMMDD-agenda-filename.md)*or for dual mode:
*Preparation: [agenda](YYMMDD-agenda-filename.md) | [facilitator sheet](YYMMDD-facilitator-filename.md)*This creates bidirectional traceability -- prep -> transcript and transcript -> prep -- so a reader landing on either file can navigate to its counterpart.
- Do not move, archive, or delete the preparation file(s) -- they stay in place to preserve cross-reference links
- Do not modify any other content in the preparation file(s) (only the supersede blockquote is added). Preparation files are frozen per the
/preparationStep 0 rule -- do not edit them post-meeting beyond the supersede marker.
This marking makes it immediately clear that the meeting has been processed, while keeping the preparation available for historical reference. Monthly cleanup (per archive policy) can later move old superseded preparations to .archive/ in bulk.
Generate Post-Meeting Priorities Artifact (if priorities_artifact.enabled)
The comprehensive meeting summary (YYMMDD-<meeting-type>.md) is archive material -- searchable, traceable, comprehensive. It is not meant to be the team's daily working list. For meetings where the facilitator drives a working team (daily standups, weeklies, war rooms), produce a slim companion artifact that the team actually works from.
This pairs symmetrically with the pre-meeting dual mode (agenda + facilitator): pre-meeting has a two-layer artifact, post-meeting also gets a two-layer artifact (comprehensive summary + slim priorities).
Trigger: workflows.post_processing.priorities_artifact.enabled: true in the org or project ops-config. Default is false -- opt in per meeting type or per project.
Filename: YYMMDD-priorities-post-<meeting-type>.md in the same folder as the comprehensive summary. Examples:
260525-priorities-post-standup.md260605-priorities-post-weekly.md
Source priority for the slim artifact's content:
- Facilitator's post-meeting message (email / Teams / chat) if one exists -- reproduce verbatim with light formatting. This is the strongest signal because it is the facilitator's chosen prioritization layered on top of the discussion.
- Top items from the comprehensive summary's Action Items table if no facilitator message exists -- pick the items the facilitator emphasized; if unclear, ask the user before drafting rather than guessing.
- Skip the artifact if neither (1) nor (2) yields a clear priority list -- producing a slim doc that just restates the action-items table adds no value.
Content shape (target: 1 page, scannable in 30 seconds):
- Short header: facilitator name, source (e.g., "post-standup email"), one-line statement that this is the working list and the comprehensive summary is the archive
- A "top N" table for items the facilitator flagged as the immediate deadline (e.g., "complete by tomorrow EOD") -- columns: #, Item, Owner
- An "in parallel this week" table for non-immediate items
- A one-line note on longer-horizon work (e.g., R3 features) if mentioned
- Footer linking back to the comprehensive summary for context
Cross-references (bidirectional):
- The slim priorities file MUST link to the comprehensive summary at the top and in the footer ("Detail in
YYMMDD-<meeting-type>.mdif needed") - The comprehensive summary MUST link to the slim priorities file in its footer ("Team-working priorities (the slim version):
YYMMDD-priorities-post-<meeting-type>.md-- this file is the archive; that file is what the team works from")
Critical rule: the slim artifact is not a summary of the discussion. It is the working list. Omit narrative, omit decision rationale, omit cross-references beyond the one back to the comprehensive summary. If it grows past one page, it has drifted into being a second summary -- trim.
Critical rule: if the facilitator sends their priority list after the meeting summary has already been generated, regenerate the priorities artifact rather than editing in place. The artifact is meant to be the authoritative working list at the time it was sent; older versions stay in .archive/ if needed.
Generate Post-Meeting Recap (if recap_artifact.enabled) -- ON REQUEST
The third audience. The summary is the archive, the priorities artifact is for the people doing
the work, and the recap is for people who need to know and were not there. Format is a vault-side
standard (ops/_standards/post-meeting-recap.md); this section governs when it is produced and what it
must not do.
Trigger: workflows.post_processing.recap_artifact.enabled: true. Default false.
Read the declared standard before writing, every time — and never write from a previous recap. The format belongs to a vault-side document, not to this skill: this skill states no word, bullet or block counts and must never acquire any, because how much a recap contains is a property of what the session produced and of the venture's own contract, not of the tool. So the shape has to be fetched, not remembered.
Two failure modes, both observed on 2026-09-22:
- Writing from the last recap instead of the standard. The previous file is a precedent, and a precedent that already drifted hands the drift on with full confidence. Measured on one series: four consecutive recaps each ran several times the budget its own standard set, each written from the one before it, growing every day, and nothing noticed. A template that merely points at the standard is not the standard, and neither is yesterday's file.
- Not applying the rules that can be checked. Where the standard states something countable, count it before staging and put the measurement in the manifest. An unverified claim of conformance is what let four recaps drift in a row; a number in the manifest makes the next comparison possible.
Where a venture's own deviation and its standard pull against each other, say so rather than silently picking one — that conflict is the venture's to resolve, and it recurs every session until it is.
Offer it; do not write it. Unlike every other Step 9 artifact, the recap is generated on request. State that a recap looks warranted and what it would carry, then wait. It is the one artifact that leaves the building -- it reaches people who were not in the room and who read it once. Assembled from the transcript alone it is confidently incomplete, and invisibly so to its readers, who have no transcript to check it against. The chat archive and the repo archive hold things the room never said, and only a human knows whether a given week's recap needs them.
Then gate it on content. Even when asked, a recap is warranted only where the meeting produced something an absent reader must act on or know:
- a schedule, ownership or scope change
- a release that landed, or a date that moved
- a finding that changes how the work should be understood
- an ask of the wider team
Otherwise say so and stop. A recap that restates the working list trains people to stop reading recaps, which costs more than the missing recap does.
The source boundary -- the hardest constraint. Everything in a recap comes from the session being processed. Two other rules point the other way and must not be read as licence:
- The first block carries whatever most changes the reader's world governs ordering within a session, not eligibility.
- Reframe, do not just report means saying what was said more clearly, not adding what was not said.
Reasoning audience-first -- "what does the absent reader need to know?" -- answers from everything in context, including earlier sessions. The question is "what did THIS meeting produce that the absent need to know?" Three requirements follow: bound claims to this transcript (earlier summaries may inform framing, never supply facts); an unsent recap has expired, not accumulated -- it goes out late carrying its own date or the live parts go as a separate notice, never folded into the next one; and every claim traces to something said, since an oblique remark does not establish a decision.
Observed, not theorised: a hand-written recap once led with a cadence change announced four days earlier, in a session that never mentioned it. A human caught it on first read. A generator running unsupervised repeats that silently, every time.
Staging. A folder, never a loose file:
_outbox/YYMMDD-<recipient>_<subject>/ _manifest.md + the recap bodyThe folder names the subject of the send, the left side the recipient -- /outbox owns that
rule. Author the manifest; never write status or status-note. Those record what a dispatching
surface did first-hand, and the send itself is a human act. The one exception is the staging state
itself: an item staged unfinished may carry draft, and one staged ready may carry klar-att-skicka
-- those two words exactly, and nothing else (CR-103). "redo att skicka" or "ready" is an
undeclared status that each reader interprets differently; /ops check reports it.
The manifest is checked before it is written (CR-132). Kanonisk källa (a path, or
ingen (originalet bor här)) must be filled in -- ask the author if unknown, never default it -- and
Status is written **Status:** <declared form> with the colon outside the bold. Run
skills/outbox/resolved.py --validate <manifest>; do not write while it reports problems. /ops check
remains the backstop for legacy items and still never rewrites.
A new version is a new item, and the old one is retired by a person. When a draft is rewritten
and restaged, the earlier item stays in _outbox/ until someone marks it avskriven with the status note
ersatt av <new-item> (the dispatching surface offers this). Do not mark it yourself and do not
delete it: /outbox close --all-resolved files it to .archive/ (CR-103).
A manifest that already exists is edited, never regenerated (CR-102, invariant). Not writing a
field is not the same as keeping it: a rewrite from the template that leaves status out erases a
send as surely as overwriting it. So:
- Staging a new item writes the manifest from the template, as above.
- Changing an item that already has a manifest (adding a cross-reference, correcting a line):
read it, change only the lines you mean to change, write it back. Every other line is carried
through verbatim -- in particular
status,status-note,channelandcontact, which a dispatcher may have written since you staged it. - Read immediately before writing, and compare with what you last read. If it changed -- a status appeared, a field changed -- stop and report; do not write over it. Staging and sending often happen seconds apart in two different sessions, and in a synced vault the last writer wins.
- A manifest past draft (
klar-att-skicka,skickad …,avskriven …) is settled. Body sections may be appended to (## Utfall,## Tidslinje); the field block is left alone. A new version of the material is staged as a new item, not written into the settled one.
Do not define a channel or recipient vocabulary here. channel: in the config selects which value
to write into the manifest's existing field; it does not create a parallel enumeration. A second schema
beside the first is two implementations to keep in step.
Rendering, once the channel is known:
| Channel | Body |
| chat | **bold** labels, bullets, one message |
UPPERCASE labels, - bullets never nested, bare links on their own line, no greeting or sign-off |
The subject belongs in the manifest's subject field, not repeated at the top of the body. An email
recap must read as finished the moment it is staged -- that channel composes a draft and a human
presses send.
Classification is decided before writing and gates content. Default team-wide. At every level a recap never carries personnel matters, raw financials, security methods, regulatory-exposure wording, commercial terms under negotiation, or individual criticism. A project-assignment change is not a personnel matter -- who owns which workstream is what the team needs; frame it around the work.
Config:
workflows:
post_processing:
recap_artifact:
enabled: false # default
channel: teams # written into the manifest's channel field
classification: team-wide # team-wide | team-only | management-only
sections: [shipping, what-we-learned, customer-data]
dashboard_url: null # appended as the closing line when setsections is a preset, not a constraint -- a menu to start from. Forcing content into a label is
worse than inventing one.
Check Verticals (if configured)
If workflows.verticals is configured, check whether any vertical documents should be updated with new information from this meeting.
Verticals are living documents that aggregate insights across multiple meetings on a single strategic topic (e.g. a product tracker, a methodology insights document). Unlike meeting summaries (point-in-time), verticals are topic-longitudinal.
- For each vertical in the config, check if the
triggercondition is met:if_mentioned: scan the meeting summary for any of thetopicskeywords (case-insensitive)always: always suggest
- If one or more verticals are triggered, present a suggestion to the user:
Verticals to update: - Voice Lake (ops/management/voice-lake-vertikal.md) -- topics matched: voice lake, samtalssammanfattningar - AI i utvecklingsorganisationen (ops/management/insikter-ai-utvecklingsorganisation.md) -- topics matched: utvecklingshastighet Update now? (yes/no/select) - If user confirms (yes or select), read the vertical document and update relevant sections with new information from this meeting. Preserve the document's existing structure -- add to existing sections, update timelines, append new entries.
- If vertical file does not exist at the specified path, skip silently (no error).
- If no verticals are triggered, do not mention verticals at all.
Update Rolling Plans (if configured)
If workflows.rolling_plans is configured, check whether any rolling plan should be updated from this meeting.
Rolling plans are living, shareable per-axis planning documents that aggregate state across a recurring 1-on-1 relationship (one orthogonal workstream axis per partner). They are the participant-keyed counterpart to verticals (which are topic-keyed): unlike a meeting summary (point-in-time) or a vertical (topic-longitudinal), a rolling plan is relationship/axis-longitudinal -- "what's on now / next / later, and who owns what" for the workstream that partner owns.
- Match by participant. For each rolling plan in the config, resolve its
participantsusing the standard name-resolution algorithm (orgteam[]+_contacts/*/_meta.yaml, case-insensitive, Swedish-char folding). The plan is triggered when this meeting's resolved participant set intersectsparticipants(typically a 1-on-1, but any matching meeting qualifies). Skip plans withstatus: archived. - Suggest the update (same yes/no/select UX as verticals):
Rolling plan to update: - Alex-Bob (meetings/management/Bob/rolling-plan-Alex-Bob.md) -- axis: website / sign-up / data / go-to-market Update now? (yes/no/select) - On confirm, update the plan in place, preserving its structure (Overall goals -> Links/tools -> Cadence -> NOW / NEXT / LATER -> Carry-forward):
- move completed rows out -- into the meeting summary just written (they become the audit trail there; don't lose them),
- add new NOW items surfaced in the meeting, in the correct owner column,
- reflect decisions / status changes -- update wording to the current state; never translate stale wording back in,
- keep the doc in its configured
language.
- Golden rule -- one item = one owner = one doc. If a row clearly belongs to another registered plan's axis, link it rather than copy it. Keep each plan's "Sister documents" cross-link block consistent with the set of configured plans.
- If the target file does not exist, offer to scaffold it from the rolling-plan template (
ops-config/templates/rolling-plan.md, or the org'stemplates.rolling_planoverride) rather than erroring -- header + Sister-documents block (generated from the other configured plans'axisstrings) + empty NOW / NEXT / LATER tables. - If no rolling plan is triggered, do not mention rolling plans at all.
STATUS TERMINOLOGY
Default (from ops-base)
When no status_terminology is configured:
- Priority: P0 (critical), P1 (high), P2 (important), P3 (research)
- Status: BLOCKED, IN PROGRESS, ON TRACK, TODO, PLANNED, COMPLETE
Custom (from config)
When status_terminology is configured, use the provided terms in summary tables:
work_statusterms for current state of work itemsresolution_statusterms for resolution/outcome of issues
Issue IDs
When issue_id_format is configured, assign IDs to discovered issues following the pattern. Otherwise, no structured issue IDs.
FILE NAMING
- Follow project CLAUDE.md conventions for all file paths and names
- Only CHANGELOG.md and README.md are uppercase; all other files use lowercase
- Use hyphens between words in filenames
- Default meeting filename:
YYMMDD-summary-<participants>-<description>.md— unless the series declares anote_suffix, which always wins - Slug contract (CR-021): keep å/ä/ö in filenames (never transliterate or digit-substitute), always
YYMMDD-prefix, always include the role keyword — English, fromterms:(summary/agenda/facilitator/recap/priorities/handoff, CR-089; files written earlier withsamtal/förberedelse/preparation/sammanfattning/möteare never renamed and are still read), run the Swedish driftword check against the slug before saving. Full contract in ops-base General Naming Rules; retroactive cleanup via/ops normalize --filenames.
FALLBACK BEHAVIOUR
When no config is found:
- Use base.yaml defaults
- Create meeting summary only (
update_files: [summary]) - Use TWO-TIER format from CLAUDE.md (or sensible default if no CLAUDE.md)
- Match input language
- No action propagation
- No agenda management
- No domain additions
When a target file does not exist:
- For CHANGELOG.md: create with initial entry
- For README.md: create basic structure
- For _tasks.yaml: create with v2 schema (version: 2, context from folder, scope from path) -- unless
workflows.task_ledger.modeisexternalornone, in which case it is never created (CR-040) - For meetings/README.md: create basic index
OUTPUT REQUIREMENTS
Accuracy
- Verify participant attribution before finalizing
- Cross-check numbers, dates, and technical specs
- Ensure all decisions are captured with rationale
Cross-References
- Link to meeting summary from CHANGELOG
- Link to related documents where relevant
- Use relative paths for all links
Always Include
- Owner assignment per
responsibility_matrix - Follow-up commitments with deadlines
- Suggested CHANGELOG entry
- Next steps
ATTACHMENTS AND MEDIA
When processing meetings that reference presentations, PDFs, or other binary files:
Detection
- Look for mentions of "presentation", "slides", "PDF", "deck", "demo", "recording" in transcript
- Check if Deep Thought metadata includes a recording reference
- Ask user if attachment exists and where it is located
Placement
- Project-specific attachments:
.attachments/within the project folder - Vault-level attachments:
.attachments/at vault root (per vault CLAUDE.md) - Follow the project CLAUDE.md if it specifies a different location
Naming Convention
- Format:
YYMMDD-description.extordescription-YYYY-MM-DD.ext - Use lowercase with hyphens
- Match the meeting date when the attachment was presented
Linking
In meeting summaries, add a "Related:" section after the preview/context links:
**Related:**
- [Presentation Title (PDF)](../.attachments/YYMMDD-filename.pdf)
- [Agenda](YYMMDD-agenda-org-type.md)Workflow
- If attachment is mentioned but location unknown, ask user
- If attachment exists in wrong location (e.g.,
assets/), suggest moving to.attachments/ - Add link to meeting summary under "Related:"
- Ensure project CLAUDE.md documents
.attachments/in folder structure
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

