agentleFS
tandregbgtandregbg/core-claude-skills/skills/ops/SKILL.md

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

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.
  • AnthropicSkill

    status

    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 AzureSkill

    design

    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

  • BlockSkill

    plan

    Draft a TDD, PRD, or lightweight plan in the conversation without committing it.

    Also in 3 projects that use this

  • SentrySkill

    review

    Fixture review skill.

    Also in 3 projects that use this

  • VercelSkill

    setup

    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

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
  1. Very specific:

    Check <vault>/_config/base.yaml for vault-wide override

    line 113
  2. Distinctive:

    Read and parse each config

    line 114
  3. Very specific:

    Check for project-level .claude/ops-config.yaml in current working directory

    line 118
  4. Very specific:

    If ~/.claude/skills/acme-ops-config/, ~/.claude/skills/bravo-ops-config/, or ~/.claude/skills/delta-ops-config/ still exists, emit the …

    line 148
  5. Very specific:

    If everything is clean, print a single Vault health: OK line.

    line 149
  6. Distinctive:

    When non-conforming, the Vault Health section expands.

    line 196
  7. Very specific:

    If the first word is help, present a usage guide instead of processing content.

    line 211
  8. Distinctive:

    Critical rule: if the same content fits both files, it goes in the agenda file.

    line 321
  9. Distinctive:

    Prepare checks what needs judgement: Read the decisions since the last agenda and ask of each: *does this change the config?* A source …

    line 355
  10. Distinctive:

    After the date the card asks *demo held?

    line 441
  11. Very specific:

    No roster, said (CR-101): When no people: is declared beside carry_forward, the round prints *No roster* and — where …

    line 536
  12. Very specific:

    Before reporting the agenda as done, compare its ## headings against the declared order: Agenda, Status Overview, Key Updates, Reported …

    line 612
  13. Distinctive:

    If the user provides pre-submitted team updates:

    line 646
  14. Distinctive:

    Create the file using the Standup Preparation Template:

    line 666
  15. Distinctive:

    Add "Focus Areas" section after agenda

    line 760
  16. Distinctive:

    Add "Sprint Goals" or "Week Priorities" section

    line 765
  17. Distinctive:

    Add "Carry-forward Items" from previous week

    line 767
  18. Very specific:

    Look up meeting_types[<type>].preparation_mode in the merged config

    line 774
  19. Very specific:

    If absent, default to single

    line 775
  20. Very specific:

    After the meeting, when /ops [transcript] is run:

    line 825
  21. Distinctive:

    No manual action needed

    line 828
  22. Distinctive:

    Language: Follow same rules as normal /ops (per_claude_md, etc.)

    line 835
  23. Very specific:

    Read ~/.claude/skills/ops-config/swedish_substitutions.yaml.

    line 872
  24. Distinctive:

    Detect language: Treat the file as Swedish-context if any of:

    line 878
  25. Very specific:

    Skip ambiguous substitutions: if --strict-no-ambiguous is set.

    line 890
  26. Distinctive:

    Do not write anything

    line 897
  27. Distinctive:

    Write the file back atomically (write to temp, rename)

    line 901
  28. Very specific:

    Update or create a CHANGELOG.md entry in the file's parent folder (if a CHANGELOG exists):

    line 902
  29. Distinctive:

    Idempotent: If a row for this project already exists — another session got there — verify the link and do nothing else.

    line 1059
  30. Very specific:

    Not automatic: No sweeper closes a project.

    line 1133
  31. Very specific:

    Company profile (CR-125): After the script's blocks, the agent adds one line when the organisation has a <org>/_company/ whose company, …

    line 1148
  32. Distinctive:

    Read-only version of the CR-018 pre-save template-contract check, run across a folder's existing files.

    line 1191
  33. Very specific:

    Import fix (dee), - #7 Retest — dee, a blocker table's assignee cell.

    line 1276
  34. Very specific:

    Resolve the ledger mode first (CR-041): Read workflows.task_ledger.mode before judging a missing _tasks.yaml:

    line 1432
  35. Very specific:

    Link scope and classes (CR-137): Every .md file is link-checked, not only dated files, README.md and CHANGELOG.md -- that pattern skipped …

    line 1455

“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.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

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:

summarycontent
FilenameYYMMDD-summary-<participants>-<topic>.mdYYMMDD-content-<source>-<topic>.md
HeaderParticipants, facilitator, durationSender, period covered, what it measures
Action tableOwners drawn from participantsOwners are ours — the sender owns nothing
Placementmeetings/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.

contentcorrespondence
ShapeOne-way: a report, a digest, a newsletterTurns alternate between people
FilenameYYMMDD-content-<source>-<topic>.mdYYMMDD-correspondence-<person>-<topic>.md
HeaderSender, period, what it measuresTurns with time and direction, and whether a decision was reached
Placementcontent.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:

  1. Scan for configs:
    • Find vault root (walk up from CWD until _inbox/, _outbox/, or .obsidian/; or VAULT_ROOT env)
    • Glob <vault>/*/_ops.yaml for org-folder configs
    • Check <vault>/_config/base.yaml for 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)
  2. Detect active config:
    • Check for project-level .claude/ops-config.yaml in current working directory
    • Walk up from CWD looking for the nearest <folder>/_ops.yaml
    • Determine org from CLAUDE.md organization field or folder name pattern
    • Report which config would be loaded for the current directory
  3. 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_plans configured)
    • Domain additions count
    • Summary sections (custom or default TWO-TIER)
    • Templates (if configured)
    • Strings (custom or default)
  4. 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.

  1. 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 check check 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>/_inbox and <vault>/_outbox is a stray; flag each unless listed in workflows.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 .inbox and _outbox-archive.
    • For every <vault>/<folder>/_ops.yaml found 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.yaml only when CLAUDE.md or _meta.yaml in that folder declares organization -- 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 the unlink/mv command to fix).
    • If everything is clean, print a single Vault health: OK line. Only expand into a warning list when something is non-conforming.

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: OK

When 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:

  1. The orientation entries, in declared order
  2. One-line description of what /ops does
  3. 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
  4. The working loop, grouped by phase in declared order. Per step:
    • the label and what it does
    • command where the step has one — the thing a person actually types
    • consumes → 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 its why_manual in 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
  5. Config loading summary (3-line version, point to /ops status for details)
  6. 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: standup
  • async-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:

ModeWhen to useFiles producedFilename 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 contentOne fileYYMMDD-preparation-[context]-[type].md (or förberedelse for Swedish)
dualGroup 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 prepTwo filesYYMMDD-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.enabledThe agenda comes from
true — the loop is wiredbuild_agenda.py. The template in P3 is NOT used for the agenda
absent or falseP1–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.

  1. The script checks what it can see and prints it in the sources block (config lines) 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 milestone date: with no set_on:, or a history: entry missing reason or source (CR-127); and, over milestones: (CR-149), a proposed date that passed without being confirmed or moved, an entry marked confirmed with no source, two entries on the same date, and a done date in the future. Nothing is changed by the script.
  2. 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: to milestone.history: with its set_on, a reason and a source before writing the new date: and set_on: (a targeted edit, never a re-dump). The generator itself edits nothing.
  3. State it. The generator leaves a slot, <!-- CONFIG-REVIEW: … -->; prepare replaces it with Config reviewed against <decisions>. /ops orient reports 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 todaySources 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] itemsProbably 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 stuckRepo, 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 to proposed. 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.
  • confirmed needs a source: -- a decision id, a plan section or a named confirmer. Without one the entry is shown as proposed and 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 stable current file, 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 record line saying how a declared store is checked.
  • The card is a draft its owners correct. Kinds are assigned by process from 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} (under carry_forward, default true) 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 -- check flags the contradiction instead.
  • A milestone kind: -- demo, release or review, default none. With kind: demo on 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: compact the 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.
  • check reports the contradictions, and only reports them. A card with no repo labels: and a milestone kind other than release is suggested release_blockers: false; release_blockers: false against 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.

OutputWhat it carries
The post, plain text in a .txtThe 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-sharedThe 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 documentTime 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 fileUnchanged 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: 0 means 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 blocker and critical issues (*open:*) and the fixed ones waiting for them to test (*verify:*), by assignee. A tracker login belongs to a roster person through an explicit github: 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_suffix names the facilitator document; by default it is derived from agenda_suffix exactly 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 declared pronouns:, writes canonical_short: where the roster declares one, and uses they for 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. The pronouns check 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: card stays the default. A project declaring neither card:, nor layout: compact, nor a milestone kind: 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:

  1. Read the counted messages in the chat archive.
  2. Replace the marker with a digest — facts, not quotes:

    In the digestNot in the digest
    What landed (PR, build, release), with its referenceMessage 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 waitedAnything personal, or off-topic, in a work chat
    Corrections to Probably closed evidence
  3. Round rows may be extended from the digest (e.g. "test build 764"), and the sources block's digest line becomes digest 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):

ChoiceWhen
Open itNothing has changed since it was written
Update itThe sources have moved -- chat, repository, a processed meeting. Keep the structure, add what changed, and say what moved
Regenerate itThe structure is wrong, or the meeting changed
Write anywayNever 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

  1. Load config (same as normal /ops flow)
  2. 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
  3. Read the task ledger (resolve workflows.task_ledger first -- see ops-base, CR-040):
    • local: pull active tasks per person from _tasks.yaml (status: pending, in_progress, blocked)
    • external: read the declared pointer if it is reachable; otherwise state in the prep that open work lives in system and was not read. Never fall back to an ancestor _tasks.yaml
    • Identify blockers and their owners
  4. Read CHANGELOG.md:
    • Scan recent entries for context
  5. Triage scan (CR-022): if the vault has a registered triage doc (_inbox/ working document, type: working_doc + tag do-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 /preparation Step 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:

  1. 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
  2. Extract per-person updates:
    • What they report as done
    • What they're working on today
    • Issues/blockers mentioned
    • Decisions or announcements
  3. 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

  1. Resolve preparation mode:
    • Look up meeting_types[<type>].preparation_mode in the merged config
    • If absent, default to single
  2. Determine filename(s):

    Single mode -- one file:

    • Format: YYMMDD-agenda-[org/project]-[type].md in every language — the role keyword is an identifier (CR-089). Files written before v1.79.0 as preparation/förberedelse are 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.

  3. Save to meetings folder (per CLAUDE.md MEETING ROUTING). For dual mode, both files go in the same folder.
  4. 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.
  5. 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 items

    Dual 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 agenda file (or a legacy preparation/förberedelse file) is automatically marked as superseded (per Step 9 of normal flow)
  • Dual mode: BOTH the facilitator and agenda files 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 /ops and 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:

ArgumentMeaning
<path>A single file OR a folder. If a folder, scans all .md and .yaml files recursively.
--dry-runShow the diff without writing. Use to preview before applying.
--strict-no-ambiguousSkip 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-ambiguous

Step 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:

  1. 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_inheritance in base.yaml)
    • The user explicitly passed --lang sv
  2. Skip if not Swedish-context -- this is a Swedish-character normaliser, nothing else
  3. 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
  4. For each token, look up against the substitution map. Use word-boundary matching (\bword\b).
  5. Skip ambiguous substitutions if --strict-no-ambiguous is 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.md entry 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.yaml without changing skill code.
  • Defence in depth: the same substitution map is used by /insights pre-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:

GroupMeans
Loop wiredcarry_forward declared — /ops orient and build_agenda.py work here
Configured, no loop/ops processes meetings; agenda and carry-forward do not apply
Material onlyNo config. Notes, transcripts, documents — not a pipeline, and often correctly so
Empty or dormantNo 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 listing

Declared, 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:

FileContent
README.mdRoles line, STARTING + creation date, cadence, language; a Scope / Out of scope table; a Meetings table
_ops.yamlOrganisation, language, people[] seeded from the organisation roster with its aliases — never re-typed, workflows, optional carry_forward
_tasks.yamlv2 header, empty tasks:
CHANGELOG.mdHeader 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 fillsLeft 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

WhatWhy it is shown
Open rows in _tasks.yaml, with owners and agesA project whose ledger holds eleven open P1s is not closed, it is abandoned
Outbox items not yet resolvedMaterial staged and never sent is an open loop, not a closed project
Register rows naming this projectWhat will be unwound in step C3
Rolling-plan rows pointing at itSomeone 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             # default

Absent 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. check may 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.

  1. 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.
  2. 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.
  3. What is carrying — each item with sessions, age and owner. UNOWNED is counted and named, because an item nobody is named against is the one that falls through an agenda that lists it.
  4. 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, or fetch 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.
  5. 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.
  6. 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:

  1. Resolve the contract registry (workflows.meeting_templates from the merged config; default = CR-006 canonical).
  2. For each YYMMDD-*.md meeting summary in the folder (skip preps, agendas, emails, .archive/): resolve its contract by match glob and run the three checks (heading sequence, action-table header row, empty-Beslut marker). 2a. Hygiene (CR-122, CR-123, CR-124). Run python3 ~/.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.yaml outside its declared enums, including a kind the profile does not declare -- the enums are the contract's since CR-152 (task_source_types, task_scopes), and a legacy spelling declared in task_source_aliases is 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) and themes (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). Under tasks it also reports a finished task with neither closed: nor the legacy completed: (CR-127: count and ids; the day is never guessed), and, when the vault declares a vocabulary (CR-135), a verksamhet: outside it, a projekt: with no portfolio row, a portfolio row no entry names (whole-vault runs only) and an objekttyp: 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 in links: 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 governing people: 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 reports Mobile: 7 PRs merged and Total: 40 PRs merged as work counted per person, which is the opposite answer: counts per area are what a standup is for. With no people: roster the check finds nothing, and --check says so once rather than guessing per line. A fenced block is skipped, as in links and inferred. 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 BY away 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 stops prepare copying 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 declares pronouns:, 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 declares canonical_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.

    they is 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 /ops ITSELF writes from a repo's author.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 yesterday is 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 .md file -- 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 /outbox staging gate. Both surfaces read build_agenda.pronoun_lines for the rule AND build_agenda.pronoun_scope for 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 declaring pronouns: or canonical_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.yaml with 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.
  1. Never rewrites files. Lint reports; the user decides between amending the contract (accept the fork as deliberate) and fixing the files.
  2. Carry-forward chain check -- only where workflows.post_processing.carry_forward.enabled.

    For each YYMMDD-<note_suffix>.md in the folder, check the summary ends with a ## Carried forward section. 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_GAP sessions (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:

  1. 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.
  2. Ledger rot -- _tasks.yaml with open tasks whose last_updated lags folder activity by >30 days; _insights.yaml whose last_compiled stamp 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):
    python3 ~/.claude/skills/insights/compile_status.py <vault> --quiet
    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 an or in 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 status and /analytics too, so the three cannot report different numbers for one corpus. It reads; it compiles nothing. Resolve the ledger mode first (CR-041). Read workflows.task_ledger.mode before judging a missing _tasks.yaml:
    • local (default): as above, unchanged.
    • external: the rot check is skipped and replaced by two others -- (a) incoherent declaration: system or pointer absent, 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 declaring external that nonetheless contains a _tasks.yaml. Report both as findings with the same weight as rot.
    • none: skip. _insights.yaml staleness 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.
  3. 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).
  4. Outbox aging -- run the /outbox list logic: sent-but-unarchived items, manifest-less items, manifests missing Kanonisk 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 whose status is not a declared form (report the item, the word, and the closest declared form from skills/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 no status is a finding -- the status was most likely lost to a rewrite. Where no such record is available the finding is skipped and the report says sent-without-status: not checked (no dispatch record), never silently. Offered fix: /outbox close --all-sent. A sent item ages out (CR-133): a skickad item with at least one unticked Svar förväntas på box and no Utfall (all boxes ticked is not aged out: every reply came in), older than the same 30 days (the --stale-days of resolved.py, default 30; no separate key, so it cannot drift from CR-120), is reported with the offered outcome Skickat <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, utkast or klar-att-skicka older 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 its Kanonisk källa in 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 unlike avskriven it needs no Utfall. A Kanonisk källa that resolves to nothing inside the vault is reported as unverifiable, not broken -- a path into another repository is legitimate. The Kanonisk källa finding 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).
  5. 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).
  6. 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/ past stale_after_days (declared on its vault_conventions entry, 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 own stale_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_path still 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 compares mdat payloads 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.
  7. 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).
  8. Contract alignment & repo privacy (CR-023/CR-029) -- if workflows.sweep.alignment_check.command and/or workflows.sweep.privacy_scan.command are 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.
  9. 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 in workflows.sweep.structure_exemptions are 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//outbox flows so manifests and indexes stay true; archive already-resolved material to its destination folder; delete empty scaffolds; or add a structure_exemptions entry if the exception is deliberate. Rationale worth repeating in the report: a second outbox means the central pending list lies.
  10. Hygiene (CR-122) -- the folder check's hygiene_check.py run over the whole scope: link targets, Link scope and classes (CR-137). Every .md file is link-checked, not only dated files, README.md and CHANGELOG.md -- that pattern skipped 53% of one vault's ops/ 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: dated restores 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 empty summary for 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 declared metrics_document: exempt and a name beside an item never a finding. Report only.
  11. Company profiles (CR-125) -- company_tool.py check <vault> --copies over the whole vault: stale facets, schema errors in sources.yaml and brands.yaml, locked facets without sources, credential-looking values, profile files outside _company/. Report only.
  12. Unregistered project (CR-086) -- a folder carrying an _ops.yaml but no row in any register declared under registry: is a finding, with the proposed row as the offered fix. This is what catches a project created before /ops project new existed, 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 recap

The 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

StepMust not
RetrievalFetch. It reads archives; refreshing them is the archivers' job
AgendaHave its generated sections hand-edited, or overwrite an existing one. The digest slot is the exception: prepare fills it, once (CR-101)
Facilitator sheetBe staged in _outbox/, or be sent as drafted without a facilitator having edited it
TranscriptBe chosen by the machine when duplicates exist
RecapBe written unasked, or keep a second copy outside _outbox/
Manifest statusBe 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 stepWithout 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 by build_agenda.py where the loop is wired (Step P0), from the template otherwise — plus the facilitator sheet when the meeting type is preparation_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

  1. Determine organization from:
    • Explicit organization field in project CLAUDE.md
    • Project folder name pattern (e.g., bravo-* -> bravo, acme-* -> acme)
    • Participant names matching team members in configs
  2. Load config following resolution order:
    • Project-level: .claude/ops-config.yaml
    • Folder-local (CR-011): nearest <folder>/_ops.yaml walking 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
  3. Merge layers -- project overrides folder-local, folder-local overrides vault-wide, vault-wide overrides skill defaults

What Config Controls

SettingEffect
languageOutput language (english/swedish/input/per_claude_md)
swedish_charsSwedish character enforcement (strict)
teamParticipant recognition and attribution
responsibility_matrixOwner assignments
terminologyDomain-specific terms
summary_sectionsCustom summary structure (overrides TWO-TIER)
status_terminologyDomain-specific status terms
issue_id_formatIssue ID pattern
workflows.update_filesWhich files to update
workflows.action_propagationPropagate actions to external files
workflows.agenda_managementPost-meeting agenda updates
meeting_types[<type>].preparation_modesingle (default) or dual -- whether /ops prepare produces one file or a facilitator/agenda pair
workflows.meeting_templatesPer-meeting-type shape contracts + lint mode (warn/strict) -- see ops-base Template Contracts (CR-018)
workflows.post_processingTask 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_plansParticipant-triggered per-axis living planning docs (update after a matching 1-on-1)
domain_additionsExtra sections to add to summaries
templatesCustom template paths
stringsi18n 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:

  1. Walk up from CWD collecting _insights.yaml files at each level (max depth 6, skip .archive/, .handoff/, clones/).
  2. Filter to rules: entries where confidence: rule AND status: active. consensus is never loaded (CR-123): it records what independent voices said, not how this session should work. An unknown confidence is read as hypothesis, never rule.
  3. Cap to 20 entries — if more, prefer highest confirmation_count, ties broken by most recent confirmation date.
  4. Build a one-line-per-rule preamble in the form [type] summary and 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
  5. 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.
  • 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

  1. Determine date from transcript/content or use today
  2. Determine filename and location from CLAUDE.md MEETING ROUTING
  3. Build summary using the format from Step 2
  4. Include metadata header (date, time, participants, format)
  5. Write executive summary (2-3 sentences)
  6. Populate all applicable sections
  7. 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 section
  • trigger: if_mentioned -- include if the topic appears in the input
  • trigger: 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 keyTargetAction
summaryMeeting summaryAlways created (Step 3)
changelogCHANGELOG.mdAdd dated entry at top
readmeREADME.mdUpdate Current Status, Active Tasks, Recent Meetings (the list label is the profile's: strings.units.recent_<unit>, CR-123)
task_yaml_tasks.yamlUpdate/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.mdAdd 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 projectThe project — its config and templates
The sources block, the status block, and their orderWhich chats, repositories and ticket boards are declared
The carry-forward check against the sourcespeople[], with track and adjacent
Ordering by age; handing the track onround_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 -> decision type
  • Cleared agenda items with resolution -> learning or decision type
  • Configuration or workflow changes discussed -> decision type

Process:

  1. Scan the meeting summary (including domain addition sections) for qualifying insights
  2. Write to _insights.yaml in the same folder as the CHANGELOG
  3. 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 sentence

Step 6: Propagate Actions

If workflows.action_propagation.enabled is true:

  • Decisions -> targets.decisions file
  • 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

  1. 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: english or language: swedish, use that language for all output.
    • If language: input, match the transcript/input language.
  2. 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.
  3. 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.
  4. Template-contract check (CR-018): resolve the meeting's shape contract from workflows.meeting_templates (most specific match wins, else default = CR-006 canonical) and verify heading sequence, action-table header row, and empty-Beslut marker before saving. mode: warn (default): save + report the diff + log edge_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.
  5. Include suggested CHANGELOG entry
  6. Include cross-references to related documents
  7. Include next steps or follow-up items

Step 9: Post-Processing

If workflows.post_processing is configured:

Task Import (if task_import.enabled)

  1. Extract action items from the meeting summary (same logic as /transcript Step 4)
  2. Resolve the task ledger (CR-040) before anything else:
    • mode: local (default): find the local _tasks.yaml in 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 using reference_field, e.g. a CR id or issue key, and offered to the user to raise in system if 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.
  3. Match extracted items against existing tasks -- update status/notes for tasks mentioned
  4. Present NEW action items and offer to import (yes/no/select)
  5. For imported tasks: assign defaults (P1, source = meeting file, context from folder)
  6. Mark completed items from the meeting in the local _tasks.yaml
  7. 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 code

chats: 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: direct does 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 #id to 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 test wins, 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, process writes 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 check reports: 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.

  1. 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 check verifies 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_suffix may 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, so process assigns 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.py reads it on a carried line and /ops check reads it in a decision (see Carry-Forward and check). 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.

  2. 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.
  3. 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 --date here. 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 --check

    One table, one row per declared source — chat/repository/ticket fetch, each reports: and vault_reports: entry, the previous note, the recording source — each ready, stale, failed or missing, 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.

    StateMeansNext step
    readyfetched since the last note, or dated within three daysbuild
    stalethe last success predates the last summary, or a report is three days old or morere-run the fetch, or ask the report's owner
    failedthe fetch itself failed — a login expired, a timeoutfix 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 declared connected: 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 connectedprepare checks 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 declarerun propose_repos.py and decide; it never fails the check

    A report's age is read from its filename in either dated spelling — 6-digit YYMMDD (the house convention, and what vault_reports: patterns declare) or 4-digit YYYY-MM-DD. A name carrying neither is reported as missing with no date in the name, never as fresh. A current: path's age is its last commit date, read in the zone carry_forward.movement.tz declares — the same zone the full build reads it in, so the two cannot disagree about one report across the three-day boundary.

    checked on the recording row means the declared archive: was found, not merely declared: a typo'd or deleted archive is missing, 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, so reports → reports2026 is 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). undeclared is 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 window propose_repos.py counts 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 #12 or repo 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 asMeans
    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/repo and the project does not declare it, so there is no address. Written with a marker, never an invented organisation
    probably prosea word that matches a repository name — a repository called scripts, a word billing. Still shown, under its own heading: silently dropping it would mean the repository genuinely called scripts is never proposed at all. Any owner/repo spelling or any cited item number makes a name a repository instead
    declared and not named in the windowa 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

    --write without --confirm is refused, and so is a name the reports did not show. New entries get reads: [pulls, releases] and nothing else (proposal 4) — a repository's issues are its own team's working surface, and issues is 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 existing external_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 through build_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:

    RunWhat existsWhat is built
    the morning run (step 2 of the loop, prepare)no note for today; the session is aheadtoday
    the same-day close-out (step 5 writes today's summary, then this step)today has a notethe next session the series meets, read from meets: / schedule_days

    A 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):

    RefusedWhyWay through
    a date earlier than today, not typedthe day has gone; the agenda would be read after the meeting it was to prepare--date YYMMDD — the caller saying they mean it

    An explicit --date behaves 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: leave

    The 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 261020 is accepted and reads as no reason given, and so is skip_dates: 261020 written without the dash — one day said unambiguously, which YAML reads as a scalar. An entry giving only to: is the same one day as one giving only from:.

    meets: is an ALIAS for schedule_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_days keeps 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.

  4. Escalate on sessions OR elapsed days, whichever trips first. escalate_after (default 3) and escalate_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. Without skip_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.

  5. 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:

  1. Search the same folder as the meeting summary for files matching any of these patterns where YYMMDD matches the meeting date:
    • YYMMDD-förberedelse-* and YYMMDD-preparation-* (single mode, legacy keywords — still read)
    • YYMMDD-facilitator-* (dual mode, private)
    • YYMMDD-agenda-* (dual mode, visible)
  2. 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)
  1. 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.

  1. Do not move, archive, or delete the preparation file(s) -- they stay in place to preserve cross-reference links
  2. Do not modify any other content in the preparation file(s) (only the supersede blockquote is added). Preparation files are frozen per the /preparation Step 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.md
  • 260605-priorities-post-weekly.md

Source priority for the slim artifact's content:

  1. 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.
  2. 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.
  3. 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>.md if 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 body

The 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, channel and contact, 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:

ChannelBody
chat**bold** labels, bullets, one message
emailUPPERCASE 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 set

sections 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.

  1. For each vertical in the config, check if the trigger condition is met:
    • if_mentioned: scan the meeting summary for any of the topics keywords (case-insensitive)
    • always: always suggest
  2. 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)
  3. 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.
  4. If vertical file does not exist at the specified path, skip silently (no error).
  5. 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.

  1. Match by participant. For each rolling plan in the config, resolve its participants using the standard name-resolution algorithm (org team[] + _contacts/*/_meta.yaml, case-insensitive, Swedish-char folding). The plan is triggered when this meeting's resolved participant set intersects participants (typically a 1-on-1, but any matching meeting qualifies). Skip plans with status: archived.
  2. 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)
  3. 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.
  4. 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.
  5. 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's templates.rolling_plan override) rather than erroring -- header + Sister-documents block (generated from the other configured plans' axis strings) + empty NOW / NEXT / LATER tables.
  6. 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_status terms for current state of work items
  • resolution_status terms 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 a note_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, from terms: (summary/agenda/facilitator/recap/priorities/handoff, CR-089; files written earlier with samtal/förberedelse/preparation/sammanfattning/möte are 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.mode is external or none, 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.ext or description-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

  1. If attachment is mentioned but location unknown, ask user
  2. If attachment exists in wrong location (e.g., assets/), suggest moving to .attachments/
  3. Add link to meeting summary under "Related:"
  4. Ensure project CLAUDE.md documents .attachments/ in folder structure

Download now (ZIP)

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
What did you use it for? e.g. Reviewing pull requests in a Rails app

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.