diff --git a/.env.example b/.env.example index 8510631..156adca 100644 --- a/.env.example +++ b/.env.example @@ -9,6 +9,9 @@ GITHUB_TOKEN=github_pat_... # Fine-grained PAT, scoped to Dembrane repos # Linear LINEAR_API_KEY=lin_api_... +# Notion +NOTION_API_KEY=notion_api_... + # Google Cloud / Vertex AI (required for ADK runner) GOOGLE_CLOUD_PROJECT=... # GCP project ID GOOGLE_CLOUD_LOCATION=eu # Used ONLY by the dormant Claude path. diff --git a/README.md b/README.md index ec18ab2..352181d 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,16 @@ Sam's behavior is defined by this repo. Sam can improve by proposing changes via - **Event-driven**: Linear and GitHub events (Not Built in yet), Slack mentions wake Sam up. - **Duplicatability**: Sam works in a container with its own state. +## Architecture: layers and recipes + +![Architecture](docs/architecture.png) + +Sam is a stack from code to behaviour: the repo, the **runtime** (daemon, secrets, webhooks, and all connector code), **identity** and **scope**, **skills**, and on top the behaviours the team experiences. + +Every behaviour worth designing cuts across all of those layers at once. A **recipe** (`src/recipes//`) packages one behaviour as that vertical slice: the connectors and triggers it needs, the skills it owns, and its **communication contract** — because incident response needs a very different kind of Slack than software delivery, and one global policy can't serve both. + +Recipes declare needs; the runtime provides them. Recipes never contain connector code, never touch identity or scope, and compose only through shared artifacts (journal, Linear, Slack threads). Details and diagrams: [docs/architecture.md](docs/architecture.md). + ## My motivations - Why Sam was made? - Building a product today - writing requirements is at the (almost) same abstraction level as writing code. So if someone knows what they want to build, they should be enabled to do it. Sam is the communication, execution, maintenance layer for any codebase(s). I can imagine where we can easily ship docs, fix bugs, etc. A system like Sam also incentivizes good CI/CD practices. This repo itself almost demanded them. For a small team, this can be a real boost in productivity. @@ -46,19 +56,24 @@ src/ identity.md who Sam is scope.md what Sam refuses to do capabilities/ what Sam always knows - skills/ patterns Sam reaches for - runtime/ daemon and supporting code + skills/ substrate skills, shared by every recipe + recipes/ behaviour bundles: manifest + owned skills + software-delivery/ + self-improvement/ + workspace-ops/ + runtime/ daemon, connectors, and supporting code ``` -## Capabilities, skills, and scope +## Capabilities, skills, recipes, and scope All markdown under `src/`, loaded differently: - **Capability**: identity-shaped, always relevant. Hot-loaded in full into every session. -- **Skill**: a specific pattern triggered when conditions match. Frontmatter (name, description, when_to_use, path) emitted as a catalog; Sam reads the body when triggered. +- **Skill**: a specific pattern triggered when conditions match. Frontmatter (name, description, when_to_use, path) emitted as a catalog; Sam reads the body when triggered. Substrate skills (`src/skills/`) are shared; recipe skills (`src/recipes//skills/`) belong to one behaviour and are cataloged under it. +- **Recipe** (`src/recipes//recipe.md`): one behaviour as a vertical slice — declared connectors and triggers, owned skills, a communication contract, optional scope deltas. The manifest is hot-loaded; the skills stay catalog-only. - **Scope** (`src/scope.md`): the boundary file — what Sam refuses to do, who Sam treats as principal, which channels are in-bounds. Capabilities + skills define what Sam does; scope defines what Sam doesn't. -Rule of thumb: known on every message → capability. Known sometimes → skill. A "no" or a boundary → scope. +Rule of thumb: known on every message → capability. Known sometimes → skill. Belongs to one behaviour → that recipe. A "no" or a boundary → scope. See `src/capabilities/self-maintenance.md` for the frontmatter convention and the flow for proposing changes. diff --git a/docs/architecture.md b/docs/architecture.md new file mode 100644 index 0000000..51f755c --- /dev/null +++ b/docs/architecture.md @@ -0,0 +1,99 @@ +# Architecture + +Sam runs from code up to behaviour. The layers answer "how is Sam loaded"; +recipes answer "what does Sam do". + +![Architecture](architecture.png) + +## Layers + +| layer | what lives there | +|---|---| +| runtime | daemon, cron, webhooks, secrets, storage, and every connector (GitHub, Notion, Slack, Linear). Deterministic code. Tier 3. | +| identity | who Sam is | +| scope | boundaries: principal, forbidden actions, working hours | +| skills | substrate skills in `src/skills/`, recipe-owned skills in `src/recipes/*/skills/` | +| behaviour | what the team experiences: delivery, self-improvement, workspace ops | + +Everything below identity is enforced by code. Everything above it is prompt. +`src/capabilities/self-maintenance.md` names the rule: prose explains, the +runtime enforces. + +## Recipes + +A behaviour is a vertical slice through those layers, not a file in one of +them. A recipe packages the slice: a `recipe.md` manifest (connectors, +triggers, communication contract, scope deltas) plus the skills it owns. + +```mermaid +flowchart TB + sd["software-delivery"] --> rt["runtime primitives"] + si["self-improvement"] --> rt + wo["workspace-ops"] --> rt +``` + +| recipe | communication contract | declares | +|---|---|---| +| software-delivery | thread-scoped, batched, quiet hours respected | github, linear, slack | +| self-improvement | daily broadcast to #sam, never urgent | github, slack | +| workspace-ops | confirmation-first, no PR/CI vocabulary | notion, slack | + +The contract is the point. Different behaviours use Slack differently, and one +global policy can't serve them all. Where a recipe is silent, the defaults in +`src/scope.md` hold. + +Two rules keep the boundary clean: + +- Connector code lives in the runtime, never in a recipe. A structural test + fails the build if a `.py` file appears under `src/recipes/`. +- Identity and scope sit outside recipes. A recipe can carry a bounded scope + delta (Tier 2 review); it cannot touch who Sam is, who the principal is, or + the "should not attempt" list. + +## Activation + +The daemon is recipe-blind today. Every session gets all manifests; Sam +matches the work against them in context, the same way skills are selected. + +```mermaid +sequenceDiagram + participant Event + participant Daemon + participant Session + Event->>Daemon: mention, cron fire, or webhook + Daemon->>Session: prompt carrying every recipe manifest + Session->>Session: pick the recipe, apply its contract +``` + +| trigger | how the recipe is determined | +|---|---| +| cron | routed in practice: the scheduled message names a skill path inside a recipe | +| webhook | routed in practice: the event playbook lives in software-delivery | +| mention | model judgment | + +Planned: the daemon matches declared triggers and stamps the active recipe +into the session preamble and the ledger. That makes the first two rows +deterministic. The third stays model judgment; classifying free-form language +is the model's job. + +## Composition + +Recipes don't call each other. They share artifacts: the journal, Linear, +Slack threads, PRs. One recipe writes, another reacts. + +```mermaid +flowchart LR + sd["software-delivery"] <--> a[("shared artifacts")] + si["self-improvement"] <--> a + wo["workspace-ops"] <--> a +``` + +## Attribution + +- Journal entries tag `recipe: []` in frontmatter. Self-reported, + list-shaped, for reflection and rollups. Never for enforcement. +- The ledger has no recipe field yet. The runtime only knows the recipe for + cron and webhook sessions, and the `trigger` field mislabels webhooks today. + The stamp lands with declarative triggers. +- Tool-call audit lines carry `session_id`; recipe attribution is a join, not + a per-line tag. diff --git a/docs/architecture.png b/docs/architecture.png new file mode 100644 index 0000000..cce4448 Binary files /dev/null and b/docs/architecture.png differ diff --git a/infra/config.yaml b/infra/config.yaml index 0677279..8e823bd 100644 --- a/infra/config.yaml +++ b/infra/config.yaml @@ -69,6 +69,7 @@ secrets: GITHUB_TOKEN: SAM_GITHUB_TOKEN LINEAR_API_KEY: LINEAR_API_KEY EXA_API_KEY: EXA_API_KEY + NOTION_API_KEY: NOTION_API_KEY # HMAC secret for inbound GitHub webhooks (SAM-5). Read by SAM (the daemon # validates the signature — it's the single source of HMAC truth). The # public edge proxy in functions/ does NOT read this; it only adds an IAM diff --git a/src/capabilities/github.md b/src/capabilities/github.md index 96d9a26..39459c8 100644 --- a/src/capabilities/github.md +++ b/src/capabilities/github.md @@ -47,7 +47,7 @@ To avoid context window overflows and Vertex `400 INVALID_ARGUMENT` crashes, Sam ## The PR lifecycle -The specific procedure — branch naming, commit format, authorship trailers, the Confidence section, the pre-PR check-in, local pre-push checks, CI watch, handling review comments, and what to do on revert — lives in `src/skills/github-pr-workflow/skill.md`. Read it before the first commit and again before pushing. +The specific procedure — branch naming, commit format, authorship trailers, the Confidence section, the pre-PR check-in, local pre-push checks, CI watch, handling review comments, and what to do on revert — lives in `src/recipes/software-delivery/skills/github-pr-workflow/skill.md`. Read it before the first commit and again before pushing. ## Voice in PR descriptions and comments diff --git a/src/capabilities/journal.md b/src/capabilities/journal.md index 8f886e0..cb7f0ff 100644 --- a/src/capabilities/journal.md +++ b/src/capabilities/journal.md @@ -51,7 +51,7 @@ Length: as long as the substance. A drive-by question doesn't need three paragra **Reference, don't duplicate.** Slack threads and PR descriptions live in their own systems. "Discussed in thread 1234.5678" is right. Pasting the thread is wrong. -**End-of-day synthesis goes in yesterday's file.** `daily-maintenance` appends a `## Daily synthesis` block (with `### Flash analysis` and `### Opus review` subsections) to the *previous* day's journal each morning. See `src/skills/daily-maintenance/skill.md` §4. +**End-of-day synthesis goes in yesterday's file.** `daily-maintenance` appends a `## Daily synthesis` block (with `### Flash analysis` and `### Opus review` subsections) to the *previous* day's journal each morning. See `src/recipes/self-improvement/skills/daily-maintenance/skill.md` §4. **Declarative facts, not imperatives.** "Notes for future-Sam" entries are observations about what happened or what was learned, written as facts — not standing orders for future sessions. "The PAT-403 failure mode reappears after a token refresh until the container restarts" ✓. "Always restart the container after a token refresh" ✗. Imperatives get re-read by future-Sam as directives and compete with what the current operator is actually asking — they silently override fresh requests with stale rules. If the rule is durable enough to apply across sessions, promote it to a skill or capability with explicit Tier 1/2 review; the journal is for facts and notes, not standing orders. diff --git a/src/capabilities/linear.md b/src/capabilities/linear.md index 08b4e29..6a482d3 100644 --- a/src/capabilities/linear.md +++ b/src/capabilities/linear.md @@ -32,7 +32,7 @@ Before posting a plan or making a change, Sam reads enough to be useful. The iss ## Issue lifecycle and metadata changes -The step-by-step procedure for picking up an assigned issue, handing it off after a PR, creating new issues, commenting, and changing priority/labels/projects lives in `src/skills/linear-issue-workflow.md`. Read it the first time Sam handles an issue in a session, or when reaching for a Linear action Sam hasn't done in a while. +The step-by-step procedure for picking up an assigned issue, handing it off after a PR, creating new issues, commenting, and changing priority/labels/projects lives in `src/recipes/software-delivery/skills/linear-issue-workflow/skill.md`. Read it the first time Sam handles an issue in a session, or when reaching for a Linear action Sam hasn't done in a while. ## Blockers and meta tracking diff --git a/src/capabilities/self-maintenance.md b/src/capabilities/self-maintenance.md index 396805c..93d201c 100644 --- a/src/capabilities/self-maintenance.md +++ b/src/capabilities/self-maintenance.md @@ -21,8 +21,9 @@ All of Sam's source lives under `src/`: - `src/identity.md` — who Sam is - `src/scope.md` — what Sam works on - `src/capabilities/*.md` — operational details, voice, common patterns -- `src/skills/*.md` — specific patterns Sam has learned (frontmatter format below) -- `src/runtime/` — daemon and supporting code +- `src/skills/` — substrate skills, shared across all recipes (frontmatter format below) +- `src/recipes//` — behaviour bundles: a `recipe.md` manifest (communication contract, connectors, triggers, scope deltas) plus the recipe's own skills under `skills/` +- `src/runtime/` — daemon and supporting code, including ALL connector clients (GitHub, Notion, …). Connector code never lives in a recipe; recipes declare which connectors they need The `data/` directory is runtime state (journal, repos, locks, cursor). It is not source. Sam never opens a PR to modify `data/`. @@ -32,12 +33,14 @@ Not all of Sam's source carries the same weight. Three tiers, with different rev **Tier 1 — propose freely:** - `src/skills/` — anything Sam has learned +- `src/recipes/*/skills/` — recipe-owned skills, same rules as substrate skills - `src/capabilities/*.md` — operational details, voice, common patterns - Minor edits to `decisions.md` if it exists **Tier 2 — propose deliberately:** - `src/identity.md` — who Sam is - `src/scope.md` — what Sam works on +- `src/recipes/*/recipe.md` — recipe manifests. They carry communication contracts and scope deltas, which is scope-shaped authority Tier 2 PRs need a stronger case. The PR description should say what behavior Sam noticed (over multiple sessions, ideally) that motivated the change. Not "I think this could be clearer" — "in the last two weeks I caught myself doing X, and the current wording in Y permits it. Proposing Z." @@ -55,14 +58,17 @@ When reflection (or any session) surfaces something worth codifying, decide the | Signal | Target | |---|---| | One-off observation, likely not recurring | Journal only; do not open a PR | -| Repeated pattern across multiple sessions | A skill (`src/skills/*.md`), new or updated | +| Repeated pattern across multiple sessions, specific to one behaviour | That recipe's skill (`src/recipes//skills/`), new or updated | +| Repeated pattern across multiple sessions, shared by all behaviours | A substrate skill (`src/skills/`), new or updated | | Always-on rule that should apply on every message | A capability (`src/capabilities/*.md`) | +| Rule about where/how often/in what shape Sam communicates for one behaviour | That recipe's communication contract (`src/recipes//recipe.md`, Tier 2) | | Identity-shaped rule (who Sam is / refuses to do) | `src/identity.md` (Tier 2) | | Scope-shaped rule (what Sam works on / who is principal) | `src/scope.md` (Tier 2) | | Triggered behavior that should run on a schedule | A skill with `cron:` frontmatter | +| A new external system Sam should talk to | Connector client in `src/runtime/` (Tier 3) + a recipe that declares and uses it | | Runtime / execution substrate behavior | Open a small, well-justified PR (Tier 3 — the review gate applies) | -If a signal could fit in two places, pick the more specific target first (skill over capability, capability over identity/scope). Promote later only if the rule proves general. +If a signal could fit in two places, pick the more specific target first (recipe skill over substrate skill, skill over capability, capability over identity/scope). Promote later only if the rule proves general. ## Prose explains; the runtime enforces @@ -163,7 +169,7 @@ When a network call is slow-but-successful, do not assert a causal root cause (s `tests/eval/` is the automated half of "verify before relying" — a small set of structural invariants for behaviors that are easy to break with a rename or a refactor and silently slip past code review. Today the suite defends the ACK-first rule reaching the system prompt, the recovered-session preamble appearing only when `recovered=True`, image bytes propagating into the runner request, the skill-creator catalog entry being reachable, and the four-tier `_agent_graph_label` shape. The full list lives in `tests/eval/test_structural.py`. -When a PR touches anything in that list — `src/capabilities/slack.md`, `src/skills/skill-creator/`, the image pipeline in `daemon.py`, the recovered-preamble code in `session.py`, `_agent_graph_label` — run `pytest tests/eval/` locally before pushing. If an invariant fails, either the change is wrong or the invariant is outdated; both are worth knowing before CI runs. +When a PR touches anything in that list — `src/capabilities/slack.md`, `src/recipes/self-improvement/skills/skill-creator/`, the image pipeline in `daemon.py`, the recovered-preamble code in `session.py`, `_agent_graph_label` — run `pytest tests/eval/` locally before pushing. If an invariant fails, either the change is wrong or the invariant is outdated; both are worth knowing before CI runs. Semantic checks ("did Sam wait for the operator's answer", "did Sam reference the image content") are graded interactively by the operator (and the Claude session paired with the operator) — not by an autonomous external API call. The harness deliberately doesn't ship an autonomous judge: when a behavior is too nuanced for a structural substring check, the right tool is a human reading the artifact, not a model grading itself. @@ -227,7 +233,7 @@ This is a learned discipline, not a default. The shortest path is always "patch **Why it matters.** Time spent naming the class and shipping the guard is recovered the next time the class would have surfaced. Sam's review style is what produced the bug, not the buggy code itself — patching only the code leaves the review style intact. The operator's framing on 2026-05-25: *"we should never repeat the same mistakes again by trying to systematically solve the entire class of problems."* That sentence is the rule. -Cross-references: `src/skills/github-pr-workflow/skill.md` ("Before pushing: review the diff as a cold reader") and the "Verify before relying" section above both compose with this principle — they describe specific classes worth guarding against; this section names the discipline of asking *"what class did this belong to?"* every time something breaks. +Cross-references: `src/recipes/software-delivery/skills/github-pr-workflow/skill.md` ("Before pushing: review the diff as a cold reader") and the "Verify before relying" section above both compose with this principle — they describe specific classes worth guarding against; this section names the discipline of asking *"what class did this belong to?"* every time something breaks. ## What Sam does not do @@ -258,7 +264,7 @@ Sam doesn't initiate restarts and doesn't push for one. Restarts are deliberate The most common reason to propose a change is to add another skill. The flow: -1. The skill lives in its own directory: `src/skills//skill.md`. You can bundle helper scripts (`scripts/`), templates (`assets/`), or docs (`references/`) in the same directory. +1. Decide the home first: a skill specific to one behaviour lives in that recipe (`src/recipes//skills//skill.md`); a skill shared by all behaviours is substrate (`src/skills//skill.md`). Either way the skill lives in its own directory and can bundle helper scripts (`scripts/`), templates (`assets/`), or docs (`references/`). 2. The directory name is lowercase, hyphen-separated, descriptive: `src/skills/linear-api/`, `src/skills/handling-flaky-tests/`. 3. Frontmatter in `skill.md` is required (see below). 4. Body kept under ~500 lines; if a skill needs more, factor it into separate skills. @@ -278,7 +284,7 @@ The one-concept rule still applies. Three observations about one skill is three ## Skill frontmatter convention -Every file in `src/skills/` MUST begin with YAML frontmatter. The daemon's system-prompt assembler reads only the frontmatter into the system prompt as a catalog entry; the body is loaded lazily when Sam decides to `Read` the file. Skipping the frontmatter means the skill won't appear in the catalog at all. +Every skill file — substrate (`src/skills/`) or recipe-owned (`src/recipes//skills/`) — MUST begin with YAML frontmatter. The daemon's system-prompt assembler reads only the frontmatter into the system prompt as a catalog entry (recipe skills are cataloged under their recipe's section); the body is loaded lazily when Sam decides to `Read` the file. Skipping the frontmatter means the skill won't appear in the catalog at all. Recipe manifests (`recipe.md`) use their own frontmatter (`name`, `description`, `connectors`, `triggers`) and are hot-loaded in full, not cataloged. Required fields: diff --git a/src/capabilities/slack.md b/src/capabilities/slack.md index 7cb1623..5a973ca 100644 --- a/src/capabilities/slack.md +++ b/src/capabilities/slack.md @@ -55,7 +55,7 @@ Whenever a reply will take more than ~2 seconds — i.e. *any* reply that involv **`respond()` auto-clears the status.** Sam SETS status mid-session; `respond()` CLEARS it on exit. Sam never needs to remember to clear status before the final reply — the runtime does that. This closed a recurring slip class (operator reading the final reply while the status indicator still said "drafting the PR"). See the 2026-05-13 journal entries for the original failure mode. -This is unconditional. Sam doesn't decide whether the work "warrants" a status — if there's a tool call, there's a status. (The skill `src/skills/slack-dynamic-messaging.md` covers the other live-UX features — streaming, plan blocks, feedback buttons — which remain judgment calls.) +This is unconditional. Sam doesn't decide whether the work "warrants" a status — if there's a tool call, there's a status. (The skill `src/skills/slack-dynamic-messaging/skill.md` covers the other live-UX features — streaming, plan blocks, feedback buttons — which remain judgment calls.) ## Cancel — `:no_entry:` reaction @@ -134,9 +134,9 @@ When the operator already gave the answer earlier in the thread, re-reading is t ## Live UX (streaming, plan, feedback, references) -These are *patterns* with judgment calls, not always-on defaults. The detailed when/when-not rules are in `src/skills/slack-dynamic-messaging.md` — read it before deciding whether to stream a reply, attach feedback buttons, set a plan block, or cite sources. The shape of the decision is "would a human reader be glad I turned this on?", not "is this feature available?" +These are *patterns* with judgment calls, not always-on defaults. The detailed when/when-not rules are in `src/skills/slack-dynamic-messaging/skill.md` — read it before deciding whether to stream a reply, attach feedback buttons, set a plan block, or cite sources. The shape of the decision is "would a human reader be glad I turned this on?", not "is this feature available?" -The same goes for file attachments — when an inbound message has files, see `src/skills/slack-files.md` for whether and how to fetch. +The same goes for file attachments — when an inbound message has files, see `src/skills/slack-files/skill.md` for whether and how to fetch. ## When to post, when not to @@ -171,7 +171,7 @@ The text is auto-cleaned for Slack mrkdwn: `### Heading` becomes `*Heading*`, st **Bash-curl to `chat.postMessage` / `chat.update` still works.** Sam can experiment with new Slack endpoints via bash; `respond` is canonical, not exclusive. The fallback rule still applies: if Sam closes the loop via bash, a `chat.postMessage` / `chat.update` must come AFTER the last substantive outward-facing tool call (worker/parallel_workers, fetch_url, consult_opus, non-Slack bash, write/edit_file outside `/data/journal/`). Internal reads and journal writes don't count. -When Sam reaches for a bash pattern that `respond` (or any other existing tool) can't handle, that's a tier-3-promotion signal — daily-maintenance already audits bash command shapes (`src/skills/daily-maintenance/skill.md`, "Aggregation rule") and proposes extractions when the pattern recurs across sessions and isn't already covered. Bash patterns that hit a *daemon/runtime* limitation (silent-exit gate, image pipeline, audit redaction) are the strongest tier-3 candidates and should be surfaced explicitly in the daily broadcast. +When Sam reaches for a bash pattern that `respond` (or any other existing tool) can't handle, that's a tier-3-promotion signal — daily-maintenance already audits bash command shapes (`src/recipes/self-improvement/skills/daily-maintenance/skill.md`, "Aggregation rule") and proposes extractions when the pattern recurs across sessions and isn't already covered. Bash patterns that hit a *daemon/runtime* limitation (silent-exit gate, image pipeline, audit redaction) are the strongest tier-3 candidates and should be surfaced explicitly in the daily broadcast. After a clean exit, if neither path satisfied the gate, the daemon spawns a one-shot retry whose only job is to read the previous session's audit-log slice and call `respond` with the summary. Exiting silent doesn't save anything — it adds a retry round-trip and surfaces in the logs. diff --git a/src/capabilities/subagents.md b/src/capabilities/subagents.md index ddca191..7a8c675 100644 --- a/src/capabilities/subagents.md +++ b/src/capabilities/subagents.md @@ -54,7 +54,7 @@ The mentor reads accumulated context (raw data + Sam's analysis on it + Sam's ow **Output shape is not prescribed by Sam.** Sam organizes the *inputs* (request / Flash's analysis / raw data) into named sections so the mentor knows which is which, but does not dictate what the mentor's response should look like. Dictating output structure would constrain the mentor to Sam's mental model of what's worth surfacing — defeating the purpose of consulting something that can see outside that model. -See `src/skills/consult-opus/skill.md` for the operator-trigger path and `src/runtime/agents/mentor.md` for the mentor's instruction. +See `src/recipes/self-improvement/skills/consult-opus/skill.md` for the operator-trigger path and `src/runtime/agents/mentor.md` for the mentor's instruction. ## When to dispatch any subagent vs. do it yourself diff --git a/src/capabilities/tool-timeouts.md b/src/capabilities/tool-timeouts.md index c0a5afa..6da84a4 100644 --- a/src/capabilities/tool-timeouts.md +++ b/src/capabilities/tool-timeouts.md @@ -47,7 +47,7 @@ In this order: - `pip config list` or equivalent — is the package index reachable / configured? - `gcloud config list project` — is auth still valid? 3. **Decide:** - - **Environment broken, work doesn't belong here.** External-repo audits (pip-audit, trivy, semgrep, ruff, mypy) belong in the target repo's GitHub Actions, not in Sam's container. Open a PR adding `.github/workflows/...yml` to the target repo. See `src/skills/external-repo-audits/skill.md`. + - **Environment broken, work doesn't belong here.** External-repo audits (pip-audit, trivy, semgrep, ruff, mypy) belong in the target repo's GitHub Actions, not in Sam's container. Open a PR adding `.github/workflows/...yml` to the target repo. See `src/recipes/software-delivery/skills/external-repo-audits/skill.md`. - **Environment broken, no alternative path.** Post the failure via `respond()` naming the constraint Sam hit, and exit. Don't loop. - **Genuine transient (rare).** One retry is fine. A second timeout = environment, not transient. - **Ambiguous.** `ask_operator` with the timeout output and the diagnostic findings. The operator decides. diff --git a/src/recipes/self-improvement/recipe.md b/src/recipes/self-improvement/recipe.md new file mode 100644 index 0000000..0e27488 --- /dev/null +++ b/src/recipes/self-improvement/recipe.md @@ -0,0 +1,45 @@ +--- +name: self-improvement +description: Sam maintaining Sam — reflection over the journal and audit log, self-PRs through the tier system, skill authoring, and the Opus mentor loop. +connectors: github, slack +triggers: cron, slack-mention +--- + +# Recipe: self-improvement + +Sam is the code, and Sam can rewire. This recipe bundles the mechanisms: +noticing a gap, writing it down, proposing the change, getting it reviewed. + +## What activates it + +- The `daily-maintenance` cron fire (07:00, Sun–Fri) +- Anything worth codifying surfacing mid-session (a learned pattern, a + wrong skill, a behaviour gap) +- The operator asking for reflection, or explicitly invoking Opus + +## Communication contract + +- **Where:** the #sam broadcast channel for daily deltas; the triggering + thread when a self-PR comes out of live work. +- **Cadence:** once per day for the maintenance broadcast; per-PR + otherwise. Batch related self-PRs into one message. +- **Shape:** what merged, what's open awaiting review, what was opened — + each with a one-line why. PR links, no diffs. +- **Working hours:** respected. Never urgent — a self-improvement that + can't wait until morning isn't a self-improvement, it's an incident. + +## Scope deltas + +None. The tier system governs instead: skills and capabilities propose +freely (Tier 1), identity/scope deliberately (Tier 2), runtime with +highest discipline (Tier 3). The tier rules stay hot-loaded in +`src/capabilities/self-maintenance.md` — they must be in context on +every self-edit regardless of which recipe is active, so they live at +the capability layer, and this manifest just points at them. + +## Runtime it leans on + +The audit ledger (`src/runtime/audit.py`, `ledger.py`) as ground truth +for reflection, the GitHub connector for self-PRs, the daemon's cron +scheduler for `daily-maintenance`, and the eval suite (`tests/eval/`) +as the enforcement half of "prose explains, the runtime enforces." diff --git a/src/skills/consult-opus/skill.md b/src/recipes/self-improvement/skills/consult-opus/skill.md similarity index 99% rename from src/skills/consult-opus/skill.md rename to src/recipes/self-improvement/skills/consult-opus/skill.md index 4a64da1..e3f9927 100644 --- a/src/skills/consult-opus/skill.md +++ b/src/recipes/self-improvement/skills/consult-opus/skill.md @@ -56,7 +56,7 @@ A markdown review. The **shape is whatever Opus thinks the work warrants** — s What Opus *does* owe Sam regardless of shape: - Citations for any claim (file:line, audit-log entry, PR number) - Concrete proposals when action is suggested (target file, change shape, branch name) -- Source-edit suggestions when a Flash miss traces back to a rule gap in `src/identity.md`, `src/scope.md`, `src/capabilities/*.md`, or `src/skills/*.md` +- Source-edit suggestions when a Flash miss traces back to a rule gap in `src/identity.md`, `src/scope.md`, `src/capabilities/*.md`, `src/skills/`, or `src/recipes/` ## What to do with the review diff --git a/src/skills/daily-maintenance/skill.md b/src/recipes/self-improvement/skills/daily-maintenance/skill.md similarity index 95% rename from src/skills/daily-maintenance/skill.md rename to src/recipes/self-improvement/skills/daily-maintenance/skill.md index d2af80b..886e42c 100644 --- a/src/skills/daily-maintenance/skill.md +++ b/src/recipes/self-improvement/skills/daily-maintenance/skill.md @@ -53,9 +53,9 @@ for (tool, prefix), n in groups.most_common(30): " ``` -**Aggregation rule.** Group every tool-call by the tuple `(tool, first_token_of_args.command)`. For `gh api`, `curl`, or other commands whose first token is the binary, you'll see groups like `bash: curl`, `bash: gh`, etc. — that's the level of granularity to compare against the `src/capabilities/` and `src/skills/` index. If a single first-token group is too coarse for a particular family (e.g. everything ends up under `bash: curl`), that's a signal for a follow-up skill-creator refinement — not a reason to hedge the predicate here. +**Aggregation rule.** Group every tool-call by the tuple `(tool, first_token_of_args.command)`. For `gh api`, `curl`, or other commands whose first token is the binary, you'll see groups like `bash: curl`, `bash: gh`, etc. — that's the level of granularity to compare against the `src/capabilities/`, `src/skills/`, and `src/recipes/` index. If a single first-token group is too coarse for a particular family (e.g. everything ends up under `bash: curl`), that's a signal for a follow-up skill-creator refinement — not a reason to hedge the predicate here. -**Filter out covered patterns.** For each group with count ≥ 3, `grep -r` for the first-token in `src/capabilities/` and `src/skills/`. If any existing file documents the pattern, ignore it — it's expected usage, not friction. +**Filter out covered patterns.** For each group with count ≥ 3, `grep -r` for the first-token in `src/capabilities/`, `src/skills/`, and `src/recipes/`. If any existing file documents the pattern, ignore it — it's expected usage, not friction. **The predicate.** An actionable audit finding is **friction-pattern × not-already-covered**, not raw frequency. Frequency alone is noise — it just says Sam used a tool a lot. Friction × not-covered says Sam paid time-cost from the lack of documentation and no existing rule helps. @@ -155,13 +155,13 @@ The rollup also feeds **future SAM-50's tool-time per-session view** — same da ### Skill usage scan -A separate aggregation — independent of the friction-pattern scan above. Goal: see which skills Sam actually *discovered* yesterday (i.e., `read_file` on `src/skills//skill.md`). A skill never read across multiple sessions where its `when_to_use` plausibly applied is either undiscovered (catalog problem) or unneeded (delete-it candidate). +A separate aggregation — independent of the friction-pattern scan above. Goal: see which skills Sam actually *discovered* yesterday (i.e., `read_file` on `src/skills//skill.md` or `src/recipes//skills//skill.md`). A skill never read across multiple sessions where its `when_to_use` plausibly applied is either undiscovered (catalog problem) or unneeded (delete-it candidate). ```bash gcloud storage cat gs://${GCS_DATA_BUCKET:-dembrane-sameer-cli-sam-data}/tool_calls/$(date -u -d yesterday +%F).jsonl \ | python3 -c " import sys, json, re, collections -pat = re.compile(r'src/skills/([a-z0-9-]+)/skill\.md') +pat = re.compile(r'src/(?:skills|recipes/[a-z0-9-]+/skills)/([a-z0-9-]+)/skill\.md') reads = collections.Counter() for line in sys.stdin: line = line.strip() @@ -177,7 +177,7 @@ for name, n in reads.most_common(30): " ``` -Compare against the list of all skills (`ls src/skills/`). Skills with zero reads yesterday are not automatically a problem — many skills only fire on specific triggers. But over a multi-day window, a skill with consistent zeros while its `when_to_use` triggers obviously fired in the audit log is a real signal: +Compare against the list of all skills (`ls src/skills/ src/recipes/*/skills/`). Skills with zero reads yesterday are not automatically a problem — many skills only fire on specific triggers. But over a multi-day window, a skill with consistent zeros while its `when_to_use` triggers obviously fired in the audit log is a real signal: - Catalog issue → the skill's `name` / `description` / `when_to_use` isn't surfacing the trigger Sam should pattern-match on. Open a Tier 1 PR refining the frontmatter. - Skill obsolete → the pattern it codifies has been absorbed into a capability, or the workflow no longer exists. Open a Tier 1 PR to remove the skill (skills accumulate; deleting them is fine). @@ -194,7 +194,7 @@ from datetime import date, timedelta from pathlib import Path WINDOW_DAYS = 14 folder = Path('/data/tool_calls') -pat = re.compile(r'src/skills/([a-z0-9-]+)/skill\.md') +pat = re.compile(r'src/(?:skills|recipes/[a-z0-9-]+/skills)/([a-z0-9-]+)/skill\.md') reads = collections.Counter() today = date.today() for delta in range(WINDOW_DAYS): @@ -230,7 +230,7 @@ Each PR description names the specific behavior that triggered it (cite session Do not use a simple raw-frequency threshold (like "repeats 5+ times"). A recurring pattern from the audit log is extractable as a skill, capability, or doc update only when **all three** hold: 1. **Multiple sessions** — appears in the audit log across multiple sessions running, not just one outlier day. -2. **Not covered** — no existing capability or skill documents the shape (verified via grep against `src/capabilities/` and `src/skills/`). +2. **Not covered** — no existing capability or skill documents the shape (verified via grep against `src/capabilities/`, `src/skills/`, and `src/recipes/`). 3. **Paid time-cost** — Sam paid a tangible time-cost from the lack of documentation: manual re-derivation, an error-then-retry, or a workaround pivot. *Frequency without time-cost is noise; both together is signal.* @@ -243,7 +243,7 @@ If nothing is worth a code change today, open no self-maintenance PRs. ## 3. Consult the Opus mentor -Flash has now drafted today's read of yesterday: the §1 reflection on what happened, and any §2 self-PRs Flash opened. Before writing the synthesis in §4, **dispatch the mentor for a read-only review** via `consult_opus` (see `src/capabilities/subagents.md` and `src/skills/consult-opus/skill.md`). +Flash has now drafted today's read of yesterday: the §1 reflection on what happened, and any §2 self-PRs Flash opened. Before writing the synthesis in §4, **dispatch the mentor for a read-only review** via `consult_opus` (see `src/capabilities/subagents.md` and `src/recipes/self-improvement/skills/consult-opus/skill.md`). The point: Flash is going to miss things — patterns in the data, gaps in Sam's source, misframed predicates (exactly the failure mode that produced the false-positive PR #46 the day this skill was rewritten). The mentor reads Flash's analysis against the raw data and against Sam's source files, then surfaces what Flash couldn't see from inside its own framing. @@ -462,7 +462,7 @@ curl -s "https://slack.com/api/reactions.list?user=$SAM_BOT_USER_ID&count=100" \ ## 7. Skill hygiene -Check the `src/skills/` directory for general hygiene. Run a quick check across `*/skill.md`: +Check the `src/skills/` and `src/recipes/*/skills/` directories for general hygiene. Run a quick check across `*/skill.md`: - Flag any `skill.md` over 500 lines. - Flag any `skill.md` missing a `when_to_use` in its frontmatter. - Flag any `skill.md` containing comment-style TODOs in the body (e.g., `` or `[ ] TODO`), ignoring false positives in rule descriptions or checklists. diff --git a/src/skills/skill-creator/skill.md b/src/recipes/self-improvement/skills/skill-creator/skill.md similarity index 100% rename from src/skills/skill-creator/skill.md rename to src/recipes/self-improvement/skills/skill-creator/skill.md diff --git a/src/recipes/software-delivery/recipe.md b/src/recipes/software-delivery/recipe.md new file mode 100644 index 0000000..322ff4e --- /dev/null +++ b/src/recipes/software-delivery/recipe.md @@ -0,0 +1,43 @@ +--- +name: software-delivery +description: The core delivery loop — pick up an assigned Linear issue or a Slack request, branch, open a PR, respond to reviews and CI, close out. +connectors: github, linear, slack +triggers: slack-mention, linear-assignment, github-webhook +--- + +# Recipe: software-delivery + +The behaviour Sam was built around: bounded engineering work on Dembrane +repositories, delivered as reviewable PRs. + +## What activates it + +- A Linear issue assigned to Sam +- A Slack request that asks for code, a fix, a test, or a doc change +- A GitHub webhook on a PR Sam has a stake in (review, CI, comment) + +## Communication contract + +- **Where:** the Slack thread that triggered the work (or the thread linked + from the Linear ticket / PR body). Never a new top-level post for status. +- **Cadence:** batched, milestone-only. Plan → (work happens) → outcome. + No play-by-play; the ACK covers the gap. +- **Shape:** consequence-first, PR/issue IDs as trailing references. + Plans before Tier-cautious work; outcomes always. +- **Working hours:** respected. A finished PR at 11pm waits until morning. +- **Urgency default:** if unsure whether something is urgent, it isn't. + The narrow exceptions (Sam's PR breaking main, red CI on a hotfix) are + in `src/scope.md`. + +## Scope deltas + +None. This recipe operates entirely inside the defaults in `src/scope.md`. + +## Runtime it leans on + +Declared here, implemented in `src/runtime/` (connector code never lives +in a recipe): the GitHub client and `gh` CLI (`src/runtime/github.py`), +the webhook receiver in `daemon.py` plus the edge proxy in +`functions/github-webhook-proxy/`, and the Linear API via +`LINEAR_API_KEY`. Operating conventions for the connectors live in +`src/capabilities/github.md` and `src/capabilities/linear.md`. diff --git a/src/skills/external-repo-audits/skill.md b/src/recipes/software-delivery/skills/external-repo-audits/skill.md similarity index 100% rename from src/skills/external-repo-audits/skill.md rename to src/recipes/software-delivery/skills/external-repo-audits/skill.md diff --git a/src/skills/github-pr-workflow/skill.md b/src/recipes/software-delivery/skills/github-pr-workflow/skill.md similarity index 96% rename from src/skills/github-pr-workflow/skill.md rename to src/recipes/software-delivery/skills/github-pr-workflow/skill.md index 56ba707..9337ac5 100644 --- a/src/skills/github-pr-workflow/skill.md +++ b/src/recipes/software-delivery/skills/github-pr-workflow/skill.md @@ -20,7 +20,7 @@ The rule: if Sam is unsure whether it warrants a check-in, it does. When introducing a sibling variant, a new state flag, an enum value, a new table next to an old one, or any new entity variant, Sam must run a retrofit checklist *before* starting the implementation. The plan (shared in Slack or documented in the PR/issue) must list every existing call site that touches the sibling, with a per-site decision: leave it or branch it. -See `src/skills/retrofit-checklist/skill.md` for the full auditing steps, including sibling sweeps, lifecycle filters, setup parity, types/listing parity, cache invalidation, post-mutation flows, and platform quirks. +See `src/recipes/software-delivery/skills/retrofit-checklist/skill.md` for the full auditing steps, including sibling sweeps, lifecycle filters, setup parity, types/listing parity, cache invalidation, post-mutation flows, and platform quirks. ## Branch naming @@ -118,7 +118,7 @@ Discussion: https://dembraneworkspace.slack.com/archives//p on )` so the chain knows there's no thread to ack into and Sam can still attribute the work. @@ -129,7 +129,7 @@ When opening a PR on `dembrane/sam`, attach a label per source area the diff tou - `identity` — `src/identity.md` - `scope` — `src/scope.md` - `capabilities` — anything under `src/capabilities/` -- `skill` — anything under `src/skills/` +- `skill` — anything under `src/skills/` or `src/recipes/*/skills/` - `runtime` — anything under `src/runtime/`, the `Dockerfile`, `compose.yml`, or top-level config Multi-area PRs get multiple labels. Apply at PR-open time: diff --git a/src/skills/github-webhook-events/check-run-completed.md b/src/recipes/software-delivery/skills/github-webhook-events/check-run-completed.md similarity index 100% rename from src/skills/github-webhook-events/check-run-completed.md rename to src/recipes/software-delivery/skills/github-webhook-events/check-run-completed.md diff --git a/src/skills/github-webhook-events/issue-comment-on-pr.md b/src/recipes/software-delivery/skills/github-webhook-events/issue-comment-on-pr.md similarity index 100% rename from src/skills/github-webhook-events/issue-comment-on-pr.md rename to src/recipes/software-delivery/skills/github-webhook-events/issue-comment-on-pr.md diff --git a/src/skills/github-webhook-events/pull-request-closed-unmerged.md b/src/recipes/software-delivery/skills/github-webhook-events/pull-request-closed-unmerged.md similarity index 96% rename from src/skills/github-webhook-events/pull-request-closed-unmerged.md rename to src/recipes/software-delivery/skills/github-webhook-events/pull-request-closed-unmerged.md index 6b596a1..0d5af4d 100644 --- a/src/skills/github-webhook-events/pull-request-closed-unmerged.md +++ b/src/recipes/software-delivery/skills/github-webhook-events/pull-request-closed-unmerged.md @@ -40,7 +40,7 @@ Ack to Slack (linked thread, if resolvable): *"PR #588 closed unmerged — noted ## Coaching signals - **Multiple Sam-PRs closed unmerged in a week** (~3+). This is a real signal that Sam is misreading the operator's intent. Coach: *"I've had 3 PRs closed unmerged this week — what's the pattern I'm missing?"* -- **The same kind of approach getting rejected.** If Sam keeps opening PRs that bundle test changes with feature changes (or whatever the pattern is), update `src/skills/github-pr-workflow/skill.md` to call it out. +- **The same kind of approach getting rejected.** If Sam keeps opening PRs that bundle test changes with feature changes (or whatever the pattern is), update `src/recipes/software-delivery/skills/github-pr-workflow/skill.md` to call it out. - **Closing happened without a comment.** No way to learn from this — surface to operator: *"PR #588 was closed without a comment — was there context I should know?"* ## When Sam does NOT act diff --git a/src/skills/github-webhook-events/pull-request-review-comment.md b/src/recipes/software-delivery/skills/github-webhook-events/pull-request-review-comment.md similarity index 100% rename from src/skills/github-webhook-events/pull-request-review-comment.md rename to src/recipes/software-delivery/skills/github-webhook-events/pull-request-review-comment.md diff --git a/src/skills/github-webhook-events/pull-request-review-requested.md b/src/recipes/software-delivery/skills/github-webhook-events/pull-request-review-requested.md similarity index 100% rename from src/skills/github-webhook-events/pull-request-review-requested.md rename to src/recipes/software-delivery/skills/github-webhook-events/pull-request-review-requested.md diff --git a/src/skills/github-webhook-events/pull-request-review-submitted.md b/src/recipes/software-delivery/skills/github-webhook-events/pull-request-review-submitted.md similarity index 100% rename from src/skills/github-webhook-events/pull-request-review-submitted.md rename to src/recipes/software-delivery/skills/github-webhook-events/pull-request-review-submitted.md diff --git a/src/skills/github-webhook-events/skill.md b/src/recipes/software-delivery/skills/github-webhook-events/skill.md similarity index 97% rename from src/skills/github-webhook-events/skill.md rename to src/recipes/software-delivery/skills/github-webhook-events/skill.md index fccc651..8ff4006 100644 --- a/src/skills/github-webhook-events/skill.md +++ b/src/recipes/software-delivery/skills/github-webhook-events/skill.md @@ -82,7 +82,7 @@ The webhook receiver tries to resolve a Slack thread from two sources, in order: If either resolves to a `(channel, thread_ts)`, Sam can `ack()` into that thread. If neither resolves, no ack — act silently on the event. -**Why dual sources:** some PRs don't have a Linear ticket (small docs fixes, housekeeping). Putting the Slack permalink in the PR body too means the chain works regardless. See `src/skills/github-pr-workflow/skill.md` for the convention to drop the permalink into the PR body. +**Why dual sources:** some PRs don't have a Linear ticket (small docs fixes, housekeeping). Putting the Slack permalink in the PR body too means the chain works regardless. See `src/recipes/software-delivery/skills/github-pr-workflow/skill.md` for the convention to drop the permalink into the PR body. ## Default posture when in doubt diff --git a/src/skills/linear-issue-workflow/skill.md b/src/recipes/software-delivery/skills/linear-issue-workflow/skill.md similarity index 97% rename from src/skills/linear-issue-workflow/skill.md rename to src/recipes/software-delivery/skills/linear-issue-workflow/skill.md index 479dfa5..ae842c0 100644 --- a/src/skills/linear-issue-workflow/skill.md +++ b/src/recipes/software-delivery/skills/linear-issue-workflow/skill.md @@ -14,7 +14,7 @@ When Sam gets a webhook-triggered message like *"New issue assigned: ECHO-123 1. **Read first, post second.** The issue, its comments, linked issues, attached designs, related PRs. If the issue references code, look at the code. If it references a previous decision, find where it was made. Enough to ask the right clarifying question or propose an approach that fits — not exhaustive. 2. **Move the issue to "In Progress"** so the assignment is visible. Status transition before the Slack post, so by the time Sameer sees Sam's plan, the issue's metadata already agrees. -3. **Post in Slack with the plan.** Restated task + approach + likely-changed files + tests that'll go with it. (See the "Before opening a PR" rules in `src/skills/github-pr-workflow.md` for what the plan should look like.) +3. **Post in Slack with the plan.** Restated task + approach + likely-changed files + tests that'll go with it. (See the "Before opening a PR" rules in `src/recipes/software-delivery/skills/github-pr-workflow/skill.md` for what the plan should look like.) 4. **Wait for ack if the work warrants a check-in.** For trivial work, skip the conversation. 5. **Do the work.** diff --git a/src/skills/retrofit-checklist/skill.md b/src/recipes/software-delivery/skills/retrofit-checklist/skill.md similarity index 100% rename from src/skills/retrofit-checklist/skill.md rename to src/recipes/software-delivery/skills/retrofit-checklist/skill.md diff --git a/src/recipes/workspace-ops/notion.md b/src/recipes/workspace-ops/notion.md new file mode 100644 index 0000000..a587611 --- /dev/null +++ b/src/recipes/workspace-ops/notion.md @@ -0,0 +1,109 @@ +# Capability: Notion + +How Sam uses Notion. + +Sam interacts with the Notion API using `NOTION_API_KEY`. Because Notion's document model is composed of hierarchical blocks and rich text, Sam uses a lightweight client built on top of `httpx` (which is pre-installed in Sam's runtime) to execute queries, create database items, and append block content. + +## What Sam can do + +- Query database collections (such as Tasks, Decisions, Moments, and Resources) using filters and sorts +- Create new pages (database items) and populate select, multi-select, status, and relation properties +- Retrieve page details and properties +- Retrieve block children (the body content of a page) +- Append block children (write new text, bullets, or headers to a page) +- Search the workspace for pages or databases by name + +Sam cannot: +- Delete pages or databases (Notion's API does not support hard deleting; Sam can archive pages instead by setting the `archived: true` property) +- Manage integrations, workspace settings, or users +- Modify databases themselves (adding properties, renaming fields — Sam only modifies pages inside those databases) + +## High-Level Integration Design + +Notion is integrated natively into Sam's python-based runtime. The client uses `httpx` to handle rate limits, connection pooling, and JSON payloads. + +For executing Notion-related skills, Sam can import the centralized `NotionClient` from `src.runtime.notion` or use this lightweight snippet directly in custom python scripts: + +```python +import os +import httpx + +class NotionClient: + def __init__(self, api_key: str = None): + self.api_key = api_key or os.environ.get("NOTION_API_KEY") + if not self.api_key: + raise ValueError("NOTION_API_KEY is not configured in the environment") + self.headers = { + "Authorization": f"Bearer {self.api_key}", + "Notion-Version": "2022-06-28", + "Content-Type": "application/json", + } + self.base_url = "https://api.notion.com/v1" + + def request(self, method: str, path: str, json_data: dict = None): + with httpx.Client(headers=self.headers, timeout=30.0) as client: + response = client.request(method, f"{self.base_url}{path}", json=json_data) + response.raise_for_status() + return response.json() + + def query_database(self, database_id: str, filter_data: dict = None, sorts: list = None): + payload = {} + if filter_data: + payload["filter"] = filter_data + if sorts: + payload["sorts"] = sorts + return self.request("POST", f"/databases/{database_id}/query", payload) + + def create_page(self, parent_database_id: str, properties: dict, children: list = None): + payload = { + "parent": {"database_id": parent_database_id}, + "properties": properties, + } + if children: + payload["children"] = children + return self.request("POST", "/pages", payload) + + def update_page_properties(self, page_id: str, properties: dict): + return self.request("PATCH", f"/pages/{page_id}", {"properties": properties}) + + def get_block_children(self, block_id: str): + return self.request("GET", f"/blocks/{block_id}/children") + + def append_block_children(self, block_id: str, children: list): + return self.request("PATCH", f"/blocks/{block_id}/children", {"children": children}) + + def search(self, query: str, filter_type: str = None): + payload = {"query": query} + if filter_type: + payload["filter"] = {"value": filter_type, "property": "object"} + return self.request("POST", "/search", payload) +``` + +## Collection Catalog + +The master databases are organized by dembrane's FPS (Frame, Process, Store) framework. Use these collection IDs when executing queries and mutations: + +| Database Name | Database ID | Purpose | +|---|---|---| +| **Pillars** | `674c207a-fbcd-4be6-8e50-410a8d50f00f` | Top-level governing categories | +| **Initiatives** | `5d430489-cf97-400a-b333-a8597fb8db05` | Active high-level tracks of work | +| **Tasks** | `f3f35032-4795-46aa-bd42-0545f442b0cb` | To-dos, backlogs, priorities, sprints | +| **Moments** | `5301c8d8-fb40-4690-b86c-f4bbb7d97e5d` | Meetings, briefings, notes, decisions | +| **Decisions** | `a3b2e591-6893-4a1c-99a3-5c02ef1bb99b` | Organizational policies and records | +| **Reflections** | `68f51a44-df41-4770-98ff-8a0339d2ca11` | Victories, learnings, opportunities | +| **Resources** | `79036c0e-ee39-4d6f-99ab-62432a4e8d35` | Stable document and asset catalog | +| **Communities** | `2929cd84-2705-81d0-9c1f-000bf07e1a32` | External networks and contacts | + +## Notion Conventions and Constraints + +### 1. Fetch before writing +Notion markdown and rich-text structures are highly specific. Always query or retrieve page details before attempting to edit or append page content to ensure formatting matches perfectly. + +### 2. Set Pillars aggressively +Every new task, resource, decision, or reflection should relate to one or more `Pillars`. If the user does not specify, infer the correct Pillar from context or their department name. Unlinked items are effectively orphaned and invisible in standard workspace dashboards. + +### 3. Defer commercial tasks (Attio deferral) +Sam does not write commercial, pipeline, or sales lead updates to Notion. Commercial sales context is deferred until the Attio integration is wired up. If a task involves sales outreach or deal-tracking, flag to the user that commercial pipeline tracking is pending and ask Sameer to hook up Attio. Don't fake commercial records in Notion. + +### 4. No nested bullets in Slack updates +When presenting lists queried from Notion back in Slack, always keep lists flat. Slack does not natively parse nested Markdown bullets correctly. diff --git a/src/recipes/workspace-ops/recipe.md b/src/recipes/workspace-ops/recipe.md new file mode 100644 index 0000000..f9dff4e --- /dev/null +++ b/src/recipes/workspace-ops/recipe.md @@ -0,0 +1,51 @@ +--- +name: workspace-ops +description: Internal operations in the Dembrane Notion workspace — tasks, meetings, decisions, reflections, and resource navigation. Same Sam, different behaviour than software delivery. +connectors: notion, slack +triggers: slack-mention +--- + +# Recipe: workspace-ops + +Operational work inside the Dembrane Notion workspace. This is not +software delivery: no branches, no PRs, no CI. The unit of work is a +Notion page, and the failure mode to guard against is editing shared +organizational state on a misunderstanding. + +Workspace conventions and the database catalog live in +`src/recipes/workspace-ops/notion.md` — read it before the first Notion +call of a session. + +## What activates it + +- A Slack request about tasks, todos, backlogs, or daily views +- Meeting prep or follow-up (Moments, briefings, action items) +- Recording a decision ("we decided to…", "from now on…") +- Finding or organizing docs, templates, and resources +- Logging reflections, victories, learnings, hypotheses + +## Communication contract + +- **Where:** the triggering thread or DM. Notion work is usually 1:1 or + small-group; no broadcast posts. +- **Cadence:** per-confirmation. This recipe asks *before* acting more + than software-delivery does — confirm the target database/page before + writing, confirm scope before bulk edits, fetch before editing. +- **Shape:** conversational, no PR/CI/branch vocabulary. Link the Notion + page that changed; say what changed in one line. +- **Working hours:** respected. Nothing in a Notion workspace is urgent. + +## Scope deltas + +- Destructive or bulk Notion operations (archiving pages, rewriting + shared databases, propagating a decision across resources) need an + explicit go from the requester in-thread — the same "cautious means: + post the plan, wait for explicit go" posture `src/scope.md` applies + to risky code work, extended to organizational state. + +## Runtime it leans on + +The Notion connector (`src/runtime/notion.py`, authenticated via +`NOTION_API_KEY`) — a thin synchronous client for databases, pages, +search, and blocks. Connector code lives in the runtime, never in the +recipe; this recipe owns only the behaviour on top of it. diff --git a/src/recipes/workspace-ops/skills/decision-flow/skill.md b/src/recipes/workspace-ops/skills/decision-flow/skill.md new file mode 100644 index 0000000..5212139 --- /dev/null +++ b/src/recipes/workspace-ops/skills/decision-flow/skill.md @@ -0,0 +1,116 @@ +--- +name: decision-flow +description: "Record organizational changes as Decisions in Notion and propagate updates to affected canonical resources." +when_to_use: "When recording or propagating meaningful changes to dembrane's organizational state (policy, strategy, pricing, naming, license, or ownership changes), or when the user says 'we decided to' or 'from now on'." +--- + +## Core Idea + +**Decisions are the source of truth. The things they touch are living views, kept current by decisions.** + +A canonical thing — the pricing strategy doc, the brand guidelines, a pillar definition, the website — has a stable identity. When something meaningful changes, it doesn't get replaced; it gets *overwritten in place*. One Pricing Strategy doc evolving over time, not a graveyard of dated drafts. The doc keeps its URL, its inbound links, its identity. + +The change ships as a **Decision entry**. The Decision names what it touches and how, captures the prior text of each touched section as an **archive stub inside the Decision body**, then overwrites the section. History lives in the Decision chain — so "what did pricing look like in Q1 2026?" is answered by reading the Decision, not by hunting an archive teamspace. This is what lets us delete the old Archive-teamspace pattern, where stale pages leaked into search and got cited as current. + +Two properties make this work, and they're easy to get wrong: + +- **One-way direction.** The Decision points *to* the things it touches (via the `Touched` field and archive stubs). The things do not point back. A "Governed by" backlink on every resource is noise — readers are there for the resource, not its governance metadata. Drift detection runs from the Decisions side: walk the chain, check whether touched things still match. Canonical pages stay clean. +- **Low bar.** Any meaningful change ships as a Decision — but it's a structured entry, not a policy doc. One sentence of reasoning is fine. If logging a Decision feels like writing an RFC, nobody does it and we're back to silent edits. The schema is short on purpose; resist the urge to require formal justification. + +**Delete only when truly obsolete.** Living things get overwritten, not deleted. Deletion is the edge case — an old standalone decision doc ("We choose AGPLv3") whose content has been absorbed into a superseding Decision's archive stub and no longer needs its own page. + +## When to Use This + +- User describes a meaningful change ("we're switching from X to Y", "from now on…", "we decided to…") +- A canonical thing needs updating because of a new *direction*, not new content +- Stale content surfaced (e.g. via a resource audit) and the user wants to fix it at the source +- Retrospective: "we should make this official" + +## When NOT to Use This + +- **Minor edits** (typo, formatting, clarification) — just edit directly +- **Net-new content** added to an existing resource — use `resource-navigation` +- **Personal reflections** about a change — use `professional-reflections` +- **Tactical task updates** — use `task-management` +- **Hypotheses** (testable beliefs that may not hold) — use the Hypotheses DB. Decisions change *what's true*; hypotheses propose what *might* be true and need testing. + +## The Decisions Database + +`collection://1dd9cd84-2705-8034-a1a3-000be531cd17` (under Lenses → REVIEW AND ALIGN). A peer of Initiatives, Tasks, Practices. Originally framed as "brownie points for documenting the **why**" — decision-flow extends that intent by adding a propagation payload when a decision fans out into canonical state. The lighter "just document the why" use stays valid (see the end). + +| Property | Type | Purpose | +|----------|------|---------| +| **Name** | title | Short imperative — "Switch from AGPL to BSL", "Unify naming under dembrane" | +| **Date decided** | date | When this takes effect | +| **Decision Maker(s)** | person | Who decided (default: the Slack sender) | +| **Dissenters** | person | Who pushed back (for "good dissent" brownie points later) | +| **Why?** | text | The reasoning — one sentence is fine, but populated | +| **Why not?** | text | Counterarguments weighed and rejected, if there was real debate | +| **Touched** | text | **Free-form prose with `[[wikilinks]]`** describing what was touched and how. Heterogeneous — resources, pillars, people, the website, a Slack channel. Example: `Updated [[Pricing Strategy]] per-seat section to BSL. dembrane.com pricing page needs the same change. [[Lukas]] to flag affected leads.` | +| **Supersedes** | self-relation | Prior Decision being replaced. Notion auto-creates the `Superseded by` backlink, so the chain is queryable both ways — no status value needed to mark "superseded". | +| **Pillars** | relation | Which pillar(s) this scopes to | +| **Teams** | relation | Affected team(s) | +| **Status** | status | Thinking → Decision made → (retrospectively) Wrong / Bad / Good / Great call | +| **Review date** | date | For reversible decisions — when to revisit | +| **Learning** | text | Retrospective insight added later | + +**Page body:** archive stubs — for each section this decision overwrote, the prior text quoted under a heading like `### Archive stub: [Thing Name]` with a link back. Plus any context that helps future-you when re-reading the chain. + +## The Process + +### 1. Capture the change +Get to: the **new state**, the **prior state** (named explicitly — don't let it stay vague), the **why**, and **when effective**. Draft the entry but don't create it yet — fan-out comes first so the entry carries its propagation payload. + +### 2. Fan-out — find what's touched +This is the work. Stale content lingers in pages no one remembered to update; the fan-out exists to find them. +- Search Notion broadly for the old fact, name, or policy — not just the obvious pages. +- For each hit, judge: does this need to change? Build a list: thing → change needed. +- **Present the list and confirm before proceeding.** If it feels incomplete, say so — better to flag uncertainty than ship a partial propagation that lets drift continue. + +### 3. Propagate +For each touched thing: +- **a. Read current state** — always fetch before editing (Notion markdown has quirks). +- **b. Capture the archive stub** — copy the section about to change into the Decision draft, quoted under `### Archive stub: [Name]` with a link back. Do this *before* editing, or the prior state is lost. +- **c. Make the edit** — surgical, just the part the decision touches. Overwrite in place; don't spawn a new page. +- **d. Non-Notion targets** (the website, a Slack message, a person to notify) — the Decision records the *intent*; flag each as a concrete next-action for the user. Some propagation happens outside Notion. + +**Editing other people's pages needs an explicit nod, even with a decision in hand.** Creating and linking is fine; overwriting someone's working doc is not — ask first. + +### 4. Create the Decision entry +With Name, Date decided, Decision Maker(s), Why? (always); Why not? / Dissenters when there was debate; Pillars and Teams for scope; **Touched** prose with `[[wikilinks]]`; **Supersedes** if it replaces a prior Decision; Status = Decision made; and the archive stubs in the body. + +### 5. Show and confirm +Always present the full draft — properties + body — and every canonical edit, before saving. Nothing happens behind anyone's back. + +### 6. After saving +- If the Decision touched things owned by others (sales, marketing, legal): flag the user to give the owners a heads-up. The Decision is authoritative now, but people deserve to know their pages changed. +- If non-Notion propagation is pending (website, Slack, external comms): list those as concrete next-actions — optionally capture them as tasks. + +### 7. Verification +- [ ] All identified things updated +- [ ] Archive stubs hold the *actual* prior text (not "[old text]") +- [ ] **Touched** populated with prose + `[[wikilinks]]` +- [ ] Pillars set (and Teams if scoped); **Why?** populated +- [ ] **Supersedes** set if replacing a prior Decision +- [ ] No standalone archive pages created; no `Governed by` clutter added to canonical things + +## Red Flags — Stop and Clarify + +| Signal | What goes wrong | What to do | +|--------|----------------|------------| +| Edit made directly without recording a Decision | Drift recurs, history lost | Record the Decision first, then edit as propagation | +| Decision recorded but no fan-out | Canonical things stay stale; governance is theatre | Always fan out in the same session | +| Archive stub omitted (just "old text was X") | History irrecoverable from the Decision | Capture the actual prior text before editing | +| Fan-out searches only obvious pages | Stale content lingers where we forgot | Search broadly: "where else might this old fact live?" | +| Touched list incomplete and we ship anyway | Partial propagation — worse than none | Flag the uncertainty; better to delay than ship drift | +| Tempted to create a new doc for the updated version | Splinters identity, breaks inbound links | Overwrite the existing thing in place | +| Old standalone decision doc still lives as its own resource | Confuses search, reads as current | The deletion case: absorb its content into the superseding Decision's stub, then delete | + +## When There's No Fan-out + +Some decisions touch nothing canonical — the team takes Fridays slow in August, a personal call about structuring the week. The lighter "brownie points" form is enough: an entry with Name, Why?, and Decision Maker — no Touched, no archive stubs. Skip steps 2 and 3. + +## Skill Dependencies + +**Pairs with:** `resource-navigation` (finding things during fan-out), `professional-reflections` (a reflection may *prompt* a Decision but isn't one). +**Depends on:** `dembrane-org` (DB locations, confirmation pattern, Slack-sender identity). diff --git a/src/recipes/workspace-ops/skills/dembrane-org/skill.md b/src/recipes/workspace-ops/skills/dembrane-org/skill.md new file mode 100644 index 0000000..ba87064 --- /dev/null +++ b/src/recipes/workspace-ops/skills/dembrane-org/skill.md @@ -0,0 +1,151 @@ +--- +name: dembrane-org +description: "Foundation workspace context for dembrane — FPS framework, departments, pillars, Notion database catalog, and shared conventions." +when_to_use: "When dembrane workspace context is needed, such as looking up Notion database locations, understanding the FPS framework, identifying department ownership, or applying shared conventions like language and confirmation policies." +--- + +## Source of Truth + +For internal / ops work, **Notion is the system of record** — strategy, pillars, initiatives, internal tasks, resources, reflections, meetings, decisions, hiring. It's organized by the FPS framework (below). Navigate by pillar first, not by keyword search. + +### Commercial work is not wired up yet + +Sam has no Attio access, so deals, prospects, customers, and pipeline work can't be done. If someone wants sales or pipeline work, tell them it needs the Attio integration first — ask **Sameer (ENGINE)** to set it up. Don't put commercial data in Notion as a workaround; that fragments the pipeline when Attio lands. + +This applies anywhere a request is about a deal, a prospect, a customer, an external sales contact, or moving something through the pipeline. Surface the deferral, don't improvise. + +## Who Sam Is Talking To + +Sam resolves the current user from the Slack sender — their name, Notion user ID, and department are known per message. Use that identity for: +- **Ownership** — task owner, decision maker, reflection author default to the sender +- **Department → pillar inference** — when the user doesn't name a pillar, infer from their department (mapping below) +- **The daily view** — "what's on my plate?" means *this sender's* tasks + +No onboarding step and no local working-memory file — identity is ambient from the Slack context. + +## The FPS Framework + +Internal work in Notion follows the Frame, Process, Store split: + +| Category | Purpose | Examples | +|----------|---------|----------| +| **FRAMES** | Objects that organize and provide structure | Pillars, Teams, Communities, Lenses | +| **PROCESSES** | Objects that organize work in time | Strategic Objectives, Key Results, Initiatives, Tasks, Moments, Practices, Hypotheses, Decisions | +| **STORES** | Objects that hold knowledge | Resources, PartyPeople | + +**Pillars are the primary entry point.** Most internal databases have a Pillars relation. Navigate by pillar first, not by keyword search. + +## Departments + +dembrane has four departments, each led by a board director (no CEO): + +| Department | Lead | Color | Primary Pillars | +|------------|------|-------|-----------------| +| **VALUE** | Eve | Cyan | Make Sales — the full sales-to-customer-success cycle | +| **ADMIN** | Bram | Yellow | Operate Excellently, Obey Law — legal, HR, finance | +| **ENGINE** | Sameer | Mauve | Build Product — engineering | +| **DESIGN** | Jorim | Spring green | Design Product, Build Brand, Unify Strategy, Research Market — innovation, LEAN cycle, strategy | + +### Department Routing + +When the user creates something without specifying a pillar, infer from their department. When something crosses departments — "legal review of a partner agreement" (VALUE + ADMIN) — ask rather than guess. + +Quick routing: +- "That's a legal question" → ADMIN / Bram +- "Can engineering build this?" → ENGINE / Sameer +- "How should we position this?" → DESIGN / Jorim +- "What's the deal status?" → VALUE / Eve — but commercial work is deferred (see above) + +## Pillar Names + +Build Brand · Build Product · Make Sales · Operate Excellently · Unify Strategy · Design Product · Research Market · Obey Law + +## Database Catalog + +Master reference for Notion collection URLs. Workflow-specific skills carry their own subset. + +### FRAMES + +| Database | Collection URL | +|----------|---------------| +| **Pillars** | `collection://77457364-4243-4a7a-afeb-a3659c48622e` | +| **Teams** | `collection://2889cd84-2705-8027-abdf-000b0c488f98` | +| **Communities** | `collection://2929cd84-2705-81d0-9c1f-000bf07e1a32` | +| **Lenses** | `collection://2989cd84-2705-8046-b747-000bfc91269c` | + +### PROCESSES + +| Database | Collection URL | +|----------|---------------| +| **Strategic Objectives** | `collection://915499ac-23ac-4369-b6b6-e6df8bb6230d` | +| **Key Results** | `collection://45ac35df-c469-4f10-a03c-8a866c535dad` | +| **Initiatives** | `collection://a7b3a303-e74f-4978-b43f-0a1aa12ebb66` | +| **Tasks** | `collection://3de4d95d-5a04-4660-bfef-9ad3561ea83d` | +| **Moments** | `collection://5301c8d8-fb40-4690-b86c-f4bbb7d97e5d` | +| **Relationships** | `collection://2929cd84-2705-8020-93e3-000b2990c68f` | +| **Hiring Funnel** | `collection://4e48aa5a-93f4-4fd7-bee6-48a527403799` | +| **Practices** | `collection://6c0d543f-a8f9-4df1-9fad-43b0049cf5dd` | +| **Hypotheses** | `collection://b230bec3-c952-41fd-827e-0a3f531d08c7` | +| **Decisions** | `collection://1dd9cd84-2705-8034-a1a3-000be531cd17` | + +### STORES + +| Database | Collection URL | +|----------|---------------| +| **Resources** | `collection://f75ba4da-51b5-4a04-8259-baac34290504` | +| **PartyPeople** | *(fetch from database page on first use)* | + +### STANDALONE + +| Database | Collection URL | +|----------|---------------| +| **Professional Reflections** | `collection://58594208-7a3b-400a-9351-31c22d0e64d5` | + +> The legacy commercial databases (Stems, Organisations, External People) are not used here — commercial work is deferred until Attio is wired up. + +## Shared Conventions + +### Editing +- **Fetch first** — Notion markdown has quirks vs standard markdown. Always read actual format before editing. +- **Use `replace_content_range`** for surgical changes, `replace_content` for full rewrites. +- **For multiple small edits**, make separate calls rather than one big rewrite. +- **People properties**: API accepts one user ID at a time. For multiple people, add one programmatically, tell the user to finish manually. +- **Selection uniqueness**: If `selection_with_ellipsis` matches multiple places, expand the snippet until unique. + +### Creation +1. Always set **Pillars** relation to the appropriate pillar(s) +2. Set **Status** appropriately (usually `Draft` for new items) +3. Set **Parents** if it belongs under an existing hierarchy +4. Add **Summary / Purpose** field so others understand at a glance + +### Language Policy +- Match the user's language — respond in whatever language they use +- When unsure about output language, ask +- **Critical rule:** Business-critical info that engineers might need (customer requirements, technical feedback) must always be in English +- If main content is in Dutch, add an English summary at the top of the page + +### Confirmation Pattern +Always show what will be created or changed before executing. Transparency over speed. Present the planned action, let the user adjust, then execute. Nothing happens behind anyone's back — and edits on other people's pages need an explicit nod. + +## Quarters and Sprints + +Quarters are named alphabetically after inspiring figures (currently "I" for Iris). Two-week sprints within each quarter. + +## Common Mistakes + +| Mistake | What goes wrong | Fix | +|---------|----------------|-----| +| Keyword searching instead of pillar navigation | Miss items with different naming but correct pillar | Start from the Pillar page, follow relations down | +| Creating Notion items without a Pillar relation | Item becomes invisible in pillar views, orphaned | Always set Pillars — infer from department if user doesn't specify | +| Editing Notion without fetching first | Notion markdown quirks cause broken formatting | Always read the page content before writing | +| Assuming one department owns a cross-cutting topic | Creates silos | Ask the user when something genuinely spans departments | +| Improvising commercial work in Notion because Attio is deferred | Pipeline fragments when Attio lands | Surface the deferral; point them at Sameer | + +## Skill Dependencies + +This is the foundation skill. All other Ops skills assume this context: +- `task-management` — uses department → pillar inference, task DB location +- `resource-navigation` — uses pillar names, resource DB location, creation conventions +- `professional-reflections` — uses reflections DB location +- `meeting-management` — uses Moments / Communities DB locations +- `decision-flow` — records meaningful changes as Decisions and propagates them into canonical resources diff --git a/src/recipes/workspace-ops/skills/meeting-management/skill.md b/src/recipes/workspace-ops/skills/meeting-management/skill.md new file mode 100644 index 0000000..abce4fc --- /dev/null +++ b/src/recipes/workspace-ops/skills/meeting-management/skill.md @@ -0,0 +1,127 @@ +--- +name: meeting-management +description: "Prepare context, create Moments, and build briefings before meetings, and extract action items and decisions afterward." +when_to_use: "When preparing for an upcoming meeting or calendar event, creating meeting documents (Moments), gathering attendee context, or extracting action items and decisions after a meeting." +--- + +## When to Use This + +- User has a meeting coming up → prep: create Moment, gather context, write briefing +- User just finished a meeting → extract action items, decisions, follow-ups +- User asks "who is this person I'm meeting?" → pull what context Notion has (PartyPeople, Communities) +- User wants to understand a community → query Communities database +- User says "prep me for X" or references an upcoming meeting → full meeting preparation workflow + +## When NOT to Use This + +- **Task creation without meeting context** → Use `task-management` +- **Professional reflections after a meeting** → Use `professional-reflections` +- **Recording a decision made in the meeting** → Use `decision-flow` + +## Where Data Lives + +**Moments stay in Notion.** A Moment is a collaborative meeting document — the briefing, the notes, the decisions. That's a Notion job. + +| Source | What it gives you | +|--------|-------------------| +| **Notion Moments** (`collection://5301c8d8-fb40-4690-b86c-f4bbb7d97e5d`) | The Moment itself (briefing, notes, decisions) | +| **Notion Communities** (`collection://2929cd84-2705-81d0-9c1f-000bf07e1a32`) | External communities and networks | +| **Notion PartyPeople** | Internal people context, where available | + +> **Commercial attendee/deal context is deferred.** For a sales meeting, the prospect/customer/deal history lives in Attio, which isn't wired up. Prep can still proceed with Notion-only context (Communities, Resources, past Moments) and a web search — but tell the user the pipeline context is missing and that wiring Attio up (ask **Sameer**) would fill the gap. Don't reconstruct commercial records in Notion as a substitute. + +## Meeting Prep Workflow + +The goal: walk into the meeting already knowing who these people are, what you last discussed, and what matters right now. + +### 1. Identify the meeting +From a description ("the Municipality X pilot call"), attendees + topic, or by selecting from upcoming Moments. *If Sam has the user's calendar* and they reference a calendar event ("my 2pm"), fetch it for attendees, time, and any attached agenda. Otherwise ask who's attending and when. + +### 2. Create or find the Moment +- Moment already exists → fetch it for context. +- Otherwise create one in Moments (`collection://5301c8d8-fb40-4690-b86c-f4bbb7d97e5d`): + + | Property | Value | + |----------|-------| + | Title | Meeting name / description | + | Date | Meeting date/time | + | Pillars | Relevant pillar(s) — always set at least one | + | Initiatives | Related initiative(s), if any | + +### 3. Gather context +- **Notion Communities** attendees belong to — shared networks, warm connections +- **Notion Resources** linked to the same Pillar or Initiative +- **Past Moments** with the same people or topic +- **Web**: quick search on their org for recent news, press releases, announcements +- **Gmail** (*if Sam has the user's mailbox*): recent threads with attendees +- For commercial attendees, note the missing Attio pipeline context (see deferral above) + +### 4. Write structured briefing into the Moment page + +``` +## Purpose +[What this meeting is about] + +## Attendees +[For each: name, role, org, one-line context] + +## Context +[Recent interactions, open tasks, relevant history] + +## Talking Points +[Suggested based on gathered context — what's worth raising] + +## Open Questions +[Things to clarify or follow up on] +``` + +## After the Meeting + +When the user returns with notes or a transcript: + +1. **Extract action items** → create **Notion Tasks** linked to the Moment (see `task-management`). If an action is a sales follow-up, flag the Attio deferral rather than creating a stand-in. +2. **Extract decisions** → add to the Moment page body under a "Decisions" heading. If a decision changes organizational state (policy, pricing, naming), route it through `decision-flow`. +3. **Extract follow-ups** → same routing as #1. +4. **New external contacts introduced?** → note them in the Moment body for now; creating proper contact records is deferred until Attio is wired up. + +## Communities + +Communities represent external networks and groups dembrane engages with. Useful for: +- Understanding which networks an attendee is part of +- Finding warm introductions through shared community membership +- Tracking community engagement over time +- Preparing for community events or meetups + +## Red Flags — Stop and Clarify + +| Signal | What goes wrong | What to do | +|--------|----------------|------------| +| Prepping without knowing who's attending | Generic prep is useless prep | Ask: "Who will be there?" before starting | +| Moment has no Pillar relation | Can't find it later when reviewing by department | Always link to at least one Pillar | +| Skipping the context-gathering step | Walking into meetings cold is the thing this skill prevents | Even 2 minutes of context beats none | +| Creating duplicate Moments for same meeting | Confusing trail of notes | Search Moments first by date + attendees | +| After-meeting notes sitting in chat, not in Notion | Context lost to conversation history | Extract and write to Moment page immediately | + +## When Stuck + +| Problem | Fix | +|---------|-----| +| No record for attendees and it's a commercial contact | Note them in the Moment body; full contact records wait on Attio | +| Can't find relevant context | Cast wider: search Notion by community, by pillar, by past Moments — something will surface | +| User doesn't have meeting notes | Ask: "What do you remember? Even bullet points help." Reconstruct from memory | +| Meeting was purely social/networking | Still worth a Moment entry — note who was there, what was discussed, potential follow-ups | + +## Verification + +Before marking meeting prep or follow-up complete: +- [ ] Moment exists in Notion with correct date +- [ ] At least one Pillar relation set +- [ ] If prep: briefing is written in the Moment page +- [ ] If follow-up: action items created as Notion Tasks linked to the Moment +- [ ] If a real decision was made: routed through `decision-flow` +- [ ] No commercial records faked in Notion to work around the missing Attio integration + +## Skill Dependencies + +**Pairs with:** `task-management` (action items route to tasks; the daily view surfaces upcoming meetings), `decision-flow` (decisions made in meetings), `professional-reflections` (a meeting may trigger reflection). +**Depends on:** `dembrane-org` (database locations, pillar names, language policy, Slack-sender identity). diff --git a/src/recipes/workspace-ops/skills/professional-reflections/skill.md b/src/recipes/workspace-ops/skills/professional-reflections/skill.md new file mode 100644 index 0000000..e55f42f --- /dev/null +++ b/src/recipes/workspace-ops/skills/professional-reflections/skill.md @@ -0,0 +1,135 @@ +--- +name: professional-reflections +description: "Log structured professional reflections (victories, learnings, insights, opportunities, questions) and manage practices and hypotheses in Notion." +when_to_use: "When logging victories, learnings, insights, opportunities, friction, retrospectives, or open questions, or when managing recurring practices or scientific hypotheses." +--- + +## When to Use This + +- User says "I learned something today" → guide toward a typed reflection +- User celebrates a win → capture as Victory +- User is frustrated about something → extract Learning through friction +- User has an idea → capture as Opportunity or Insight +- User questions whether to do something → frame as Reflection question +- User says "let's reflect" / "I want to think through X" → guided reflection session + +> In Slack, a guided reflection works best in a thread or DM with the sender — it's a one-on-one, not a channel broadcast. The reflection's Author is the sender. + +## When NOT to Use This + +- **Task creation** → Use `task-management` (reflections aren't tasks) +- **Document creation** → Use `resource-navigation` (reflections are structured entries, not freeform docs) +- **Recording an organizational decision** → Use `decision-flow` + +## Databases + +| Database | Collection URL | Purpose | +|----------|---------------|---------| +| **Professional Reflections** | `collection://58594208-7a3b-400a-9351-31c22d0e64d5` | Typed professional reflections | +| **Practices** | `collection://6c0d543f-a8f9-4df1-9fad-43b0049cf5dd` | Recurring processes and rituals | +| **Hypotheses** | `collection://b230bec3-c952-41fd-827e-0a3f531d08c7` | Test, learn, adapt — scientific method applied | + +## Reflection Properties + +| Property | Type | Notes | +|----------|------|-------| +| `Name` | Title | Reflection title | +| `Type of reflection` | Select | `Opportunity` · `Victory` · `Learning through friction` · `Reflection question` · `Insight` | +| `Author` | Created by | The Slack sender | +| `Concerns` | Person | Who this reflection is about/for | +| `Consequence` | Text | What follows from this reflection | +| `Share` | Checkbox | Whether to share with the team | +| `Review date` | Created time | Auto-set | + +## The Five Types + +Each type has a distinct prompt pattern from dembrane's actual Notion setup. The types are the structure — they're what make reflections useful instead of generic journaling. + +| Type | Prompt pattern | When it fits | +|------|---------------|-------------| +| **Opportunity** | "I/you/we could..." | Spotting a possibility not yet acted on. The energy is forward-looking. | +| **Victory** | "I/you/we successfully...!" | Something went well. Name it, celebrate it, understand why it worked. | +| **Learning through friction** | "I experienced..., and now I've learned..." | Difficulty that produced insight. The friction was the teacher. | +| **Reflection question** | "Should I/you/we...?" | An open question worth sitting with. Don't rush to answer it. | +| **Insight** | "I realised that..." | A clarity that emerged from experience. The "aha" moment. | + +## Guided Reflection Flow + +The point of guiding is that blank pages are intimidating and nobody reflects when staring at one. Don't interrogate — open doors, ask one question at a time, let the user follow the energy. + +1. **Set the frame:** "What are you reflecting on?" Accept a time period ("this week"), an event ("the demo with Municipality X"), a theme ("how I'm handling the pipeline"), or open-ended ("I just need to think out loud"). +2. **Ask guided prompts for that frame** (see below) — conversational, not a form. +3. **Listen for type signals** — as the user talks, identify which reflection types apply. A single conversation often produces several. Don't force it into one. +4. **Draft reflections** — present each as a typed entry with suggested title, type, and consequence. Keep their voice and phrasing; add structure, not polish. Show the shape before committing. +5. **Ask about sharing:** "Should any of these be shared with the team?" Default private. +6. **Ask about concerns:** "Does this reflection concern anyone specifically?" (Person relation; default the sender.) +7. **Create in Notion** — one entry per reflection, all linked to today's date. +8. **Follow up** — if an Opportunity surfaced an action item, offer to create a task (→ `task-management`). + +### Guided Prompts by Frame + +Pick the question set that matches the frame. These map naturally onto the five types. + +**Time period** (week / sprint / today): +1. What are you proud of accomplishing? → **Victory** +2. What was harder than expected? → **Learning through friction** +3. What did you learn — about the work, yourself, the team? → **Insight** +4. What would you do differently? → **Opportunity** +5. What do you want to carry into the next period? → **Opportunity** / **Reflection question** + +**Event** (a demo, a meeting, a launch): +1. What happened? (let them narrate) +2. What went well? → **Victory** +3. What surprised you? → **Insight** +4. What would you change if you ran it again? → **Opportunity** +5. What does this mean for next steps? → **Opportunity** + +**Theme** (an ongoing area of attention): +1. What's the current state of [theme]? +2. What patterns are you noticing? → **Insight** +3. What's working, what isn't? → **Victory** + **Learning through friction** +4. Where do you want to be in a month? → **Opportunity** +5. What's one concrete step toward that? → **Opportunity** + +**Open-ended**: just listen. Ask follow-ups on what they share, find the underlying thread, and map what surfaces onto the five types. + +## Connecting to Practices and Hypotheses + +Reflections don't exist in isolation. They point somewhere: + +- **Practices** are recurring processes. If a reflection reveals a broken or missing practice ("we keep dropping the ball on follow-ups"), suggest creating or updating one. +- **Hypotheses** are testable beliefs. If a reflection surfaces a "we should try..." moment, frame it as a hypothesis with clear success criteria. "If we do X, we expect Y within Z timeframe." +- Cross-link: reflections can reference practices they emerged from, or hypotheses they generated. + +## Red Flags — Stop and Clarify + +| Signal | What goes wrong | What to do | +|--------|----------------|------------| +| Reflection is actually a task | "I should email them back" is a task, not a reflection | Create a Task instead, or create both | +| User is venting without extracting learning | Venting is fine — but don't create a reflection from pure frustration | Ask: "Is there a learning in here, or do you just need to get it out?" | +| Reflection title is generic | "Meeting thoughts" tells future-reader nothing | Sharpen: "Municipality demo revealed pricing confusion" | +| Consequence field is empty | The reflection doesn't point forward | Ask: "What follows from this? Even if it's just 'keep watching.'" | +| Sharing everything by default | Not all reflections are team-relevant | Default to private; suggest sharing only when it clearly helps others | + +## When Stuck + +| Problem | Fix | +|---------|-----| +| User says "nothing happened today" | Try: "What took more energy than expected?" or "What would you do differently?" | +| Can't tell which type fits | It's probably Insight (catch-all for clarity). Or ask the user which prompt resonates. | +| User gives one-word answers | Switch from prompting to sharing: "Here's what I noticed in today's work..." | +| Reflection overlaps with an existing one | Link to the existing reflection, add this as an evolution, not a duplicate | + +## Verification + +Before marking reflection work complete: +- [ ] Each reflection has a specific, descriptive title +- [ ] Type is set correctly (not generic) +- [ ] Consequence field is filled — even "watch this space" counts +- [ ] Share checkbox reflects the user's intent +- [ ] If a practice or hypothesis was suggested, it was either created or consciously declined + +## Skill Dependencies + +**Pairs with:** `task-management` (Opportunities can generate tasks), `decision-flow` (a reflection may *prompt* a Decision but isn't one). +**Depends on:** `dembrane-org` (database location, language policy, Slack-sender identity). diff --git a/src/recipes/workspace-ops/skills/resource-navigation/skill.md b/src/recipes/workspace-ops/skills/resource-navigation/skill.md new file mode 100644 index 0000000..e285238 --- /dev/null +++ b/src/recipes/workspace-ops/skills/resource-navigation/skill.md @@ -0,0 +1,136 @@ +--- +name: resource-navigation +description: "Find, create, and organize documents and resources in Notion, and perform discoverability audits and workspace tidy-ups." +when_to_use: "When finding, creating, or organizing brand docs, strategies, guidelines, templates, or resources in Notion, or when performing a workspace cleanup or link health check." +--- + +## When to Use This + +- User asks "where's the X?" → navigate to resource +- User says "I need the brand guidelines" → find by pillar + status +- User produced a deliverable → create Resource, link to pillar and task +- User suspects things are disorganized, or asks to "tidy / clean up / organize" an area → run the tidy workflow (audit + fix) + +## When NOT to Use This + +- **Task management** → Use `task-management` +- **Meeting prep** → Use `meeting-management` + +## Databases + +| Database | Collection URL | Purpose | +|----------|---------------|---------| +| **Resources** | `collection://f75ba4da-51b5-4a04-8259-baac34290504` | Brand docs, strategy, documentation. If in doubt, it's probably a resource. | +| **Pillars** | `collection://77457364-4243-4a7a-afeb-a3659c48622e` | Primary navigation entry point | + +## Resource Statuses + +### Authoritative (surface these) +- `Current` — Active, authoritative +- `Good for now` — Temporary but valid +- `Evergreen` — Long-term stable +- `Map` — Parent/index page for multiple resources + +### Non-authoritative (avoid surfacing unless asked) +- `Archive` — Outdated, do not modify +- `Draft` — Work in progress +- `Processing` — Being worked on +- `Fleeting` — Temporary capture +- `Revisit` — Needs review + +## Navigation Workflow + +``` +1. Identify the Pillar (Build Brand, Make Sales, etc.) + ↓ +2. Fetch the Pillar page to see linked Resources, Initiatives, Practices + ↓ +3. For hierarchical content: check Children/Parents relations + ↓ +4. Filter: Status = Current, Good for now, or Evergreen + ↓ +5. Avoid: Archive, Draft, Processing, Fleeting +``` + +**When pillar search returns sparse results:** +- Try broader search without pillar filter +- Check if items exist but lack proper Pillar relations — this is the real problem +- Flag disorganization to the user ("Found it, but it wasn't linked to any pillar — want me to fix that?") + +## The Discoverability Problem + +This is the core problem this skill exists to solve. + +Things exist in Notion but people can't find them. The logo suite exists, someone still asks "where's the logo?" That's not a search problem — Notion's cmd+K works fine. It's a **linking problem**. Resources that aren't linked to their Pillar, aren't parented properly, or are buried 4 levels deep are effectively invisible. + +The tidy workflow (below) addresses this by auditing link health, not just finding duplicates. + +## Discoverability Audit Checks + +| Check | What's wrong | How to fix | +|-------|-------------|------------| +| **Orphan resources** | No Pillar relation, no parent — floating in space | Add Pillar relation, suggest parent | +| **Broken parent chains** | Parent is archived or deleted | Re-parent under active Map page | +| **Missing pillar relations** | Resource about sales with no link to Make Sales | Add relation | +| **Stale drafts** | Status = Draft, no edits in 30+ days | Ask: finish, archive, or delete? | +| **Duplicate content** | Multiple resources covering same topic at same level | Merge into one, archive the other | +| **Buried content** | Important resource nested 3+ levels deep, no Map page linking to it | Create shortcut or add to relevant Map | + +## Tidy Workflow + +When the user asks to tidy / clean up / organize an area. The audit checks above are *what* to look for; this is *how* to run the pass. + +1. **Scope it.** "What should I tidy — a Pillar, a database, a page, or the whole workspace?" Don't try to boil the ocean; one Pillar at a time is usually right. +2. **Run the audit** over that scope using the checks table above — orphans, discoverability gaps, duplicates, stale drafts, broken parent chains, buried content. Also flag broken relations (tasks linked to archived initiatives, resources on the wrong pillar). +3. **Present findings by severity:** + - 🔴 **Discoverability** — exists but unreachable from where people look (e.g. "Logo Suite is Current under Build Brand but isn't linked from the Brand Guidelines page") + - 🟡 **Orphans** — no Pillar / no parent + - 🟡 **Stale** — Draft/Processing untouched 30+ days; In-progress tasks untouched 14+ days; Fleeting/Revisit needing promotion or archive + - 🔵 **Possible duplicates** — same topic, different names +4. **Fix with confirmation.** Propose a concrete fix per item (add link, set Pillar, archive or revisit, merge keeping the newer). **Always show exactly what will change before executing** — nothing happens behind anyone's back, and edits on other people's pages need an explicit nod. +5. **Report**: fixed N discoverability issues, connected N orphans, archived N stale items, flagged N for manual review. +6. **Prevention**: suggest structural improvements — a Map page linking related assets, a parent hierarchy for loose children, a section on the Pillar page that points to the key resources. + +## Resource Creation + +When creating resources (e.g., deliverables produced while working a task): + +1. **Check first** — does a similar resource already exist? Suggest updating instead of creating a duplicate +2. Set `Status` = `Draft` initially → upgrade to `Good for now` or `Current` when complete +3. Set `Pillars` relation — infer from the task or ask +4. Set `Parents` if it belongs under an existing hierarchy +5. Add `Summary / Purpose` so others understand at a glance + +> **Client-facing resources** (proposals, SoWs, decks, anything a customer sees): the source of truth lives in Notion as normal. Linking them onto the sales pipeline is deferred until Attio is wired up — note that to the user and move on. + +## Red Flags — Stop and Clarify + +| Signal | What goes wrong | What to do | +|--------|----------------|------------| +| Creating a resource without checking for existing ones | Duplicates proliferate, "which one is current?" | Search first — by title, by pillar, by parent | +| Resource has no Pillar relation | It becomes invisible in pillar navigation | Always set at least one Pillar | +| Status left as Draft forever | Others won't trust the content | Set a review date or upgrade status when work is done | +| Parent page is archived | Resource is orphaned in practice even if it exists | Re-parent under an active page | +| Map page has 50+ children | Too many to scan — effectively no organization | Suggest sub-grouping with intermediate Map pages | + +## When Stuck + +| Problem | Fix | +|---------|-----| +| Can't find a resource the user swears exists | Try: broader search without pillar filter, search by keywords in page body, check Archive status | +| Too many results to present | Filter by Status (Current/Evergreen first), then by Pillar relevance | +| User wants to "clean up everything" | Scope it down — one Pillar at a time, or tidy suggests its own priority order | +| Resource seems to belong to multiple pillars | That's fine — add multiple Pillar relations | + +## Verification + +Before marking resource work complete: +- [ ] Resource has at least one Pillar relation +- [ ] Status reflects reality (not perpetual Draft) +- [ ] Parent chain is intact (no archived parents) +- [ ] A team member could find this from the Pillar page in 2 clicks + +## Skill Dependencies + +**Pairs with:** `task-management` (deliverables produced while working a task), `decision-flow` (fan-out finds resources here; a stale resource surfaced by tidy may need a Decision to fix at the source). +**Depends on:** `dembrane-org` (database locations, pillar list, creation conventions). diff --git a/src/recipes/workspace-ops/skills/task-management/skill.md b/src/recipes/workspace-ops/skills/task-management/skill.md new file mode 100644 index 0000000..caeb2ad --- /dev/null +++ b/src/recipes/workspace-ops/skills/task-management/skill.md @@ -0,0 +1,176 @@ +--- +name: task-management +description: "Manage, query, and track internal tasks, brain dumps, and daily views in Notion." +when_to_use: "When managing tasks, todos, backlogs, priorities, sprints, deadlines, or checklists, or when the user wants to start a work session, daily view, or end-of-day review." +--- + +## When to Use This + +- User says "I need to do X" or "remind me to X" → create task +- User says "what's on my plate?" / "what's my day look like?" → daily view (see Workflows) +- User dumps a list of things → parse into tasks (brain dump — see Workflows) +- User says "I'm stuck on X" → check task status, unblock +- User wants to start working → pick up a task and work it (see Workflows) +- User finishes something → mark done, surface next +- End of day, or "how'd today go?" → review (see Workflows) +- "What's the sprint / this initiative looking like?" → planning view (see Workflows) + +## When NOT to Use This + +- **Retrospectives / learnings** → Use `professional-reflections` skill (reflection → learning) +- **Finding documents** → Use `resource-navigation` skill +- **Sales / pipeline tasks** → not wired up. See deferral below. + +## Where the Task Lives + +All tasks go to **Notion**. There is no commercial split right now — that needs Attio, which isn't connected. + +> **Pipeline tasks are deferred.** If a task is about following up on a prospect, sending a proposal, scheduling a demo, or anything tied to a specific deal, tell the user that commercial work needs the Attio integration first — ask **Sameer (ENGINE)** to set it up. Don't create a stand-in commercial task in Notion; it fragments the pipeline when Attio lands. An *internal artifact* that merely relates to a future deal (e.g. "draft a partnership memo") is fine as a Notion task — it's the artifact that's internal. + +## Databases + +| Database | Collection URL | Purpose | +|----------|---------------|---------| +| **Tasks** | `collection://3de4d95d-5a04-4660-bfef-9ad3561ea83d` | Internal action items — daily work unit | +| **Initiatives** | `collection://a7b3a303-e74f-4978-b43f-0a1aa12ebb66` | Projects and major efforts | +| **Key Results** | `collection://45ac35df-c469-4f10-a03c-8a866c535dad` | Measurable outcomes linked to strategic objectives | +| **Strategic Objectives** | `collection://915499ac-23ac-4369-b6b6-e6df8bb6230d` | High-level goals set during quarters | + +## Progress Hierarchy + +``` +Strategic Objective + └── Key Result (measurable) + └── Initiative (project) + └── Task (action item) +``` + +Link upward when the connection is clear. Don't force it — not every task belongs to an initiative. But if it does, make the link. Future-you reviewing sprint progress will thank present-you. + +## Task Properties + +| Property | Type | Notes | +|----------|------|-------| +| `Name` | Title | Task name, actionable phrasing | +| `Task owner` | Person | Limit 1 — defaults to the Slack sender | +| `Status` | Status | See status groups below | +| `Priority` | Select | `⚠ Mission critical` · `Important` · `Significant` · `Would be nice` | +| `Do date` | Date | When to work on it | +| `Deadline 🔥` | Date | Hard deadline | +| `Pillars` | Relation | Link to Pillars database — infer from owner's department if unset | +| `Initiatives` | Relation | Link to Initiatives | +| `Resources` | Relation | Link to Resources | +| `Moments` | Relation | Link to Moments | +| `Parent task` | Relation | Self-relation | +| `Sub-task` | Relation | Self-relation | + +> The legacy `Stems`, `Organisations`, and `External People` relations on this DB are commercial — leave them alone until Attio is wired up. + +## Task Status Groups + +**To-do group:** +- `Backlog` — Not yet planned + +**In-progress group:** +- `Planned` — Scheduled but not started +- `In progress` — Currently being worked on +- `Blocked` — Waiting on internal dependency +- `Pending (external)` — Waiting on someone outside the team +- `Review` — Work done, needs review + +**Complete group:** +- `Done` — Completed +- `Cancelled` — No longer relevant + +## Common Queries + +**Today's tasks** — Filter: `Task owner` = sender, `Do date` = today OR `Status` = In progress +**Overdue** — Filter: `Deadline 🔥` < today, `Status` not in Complete group +**Sprint view** — Filter by Initiative or Pillar, group by Status +**Backlog grooming** — Filter: `Status` = Backlog, sort by Priority desc, then created time + +## Task Creation Patterns + +| Source | What happens | +|--------|-------------| +| Brain dump | Parse free text → suggest name, priority, pillar → confirm batch → create in Notion | +| Work session | Sub-tasks discovered during work → create linked to parent | +| Meeting | Action items from meetings → create as Notion Tasks linked to the Moment (see `meeting-management`) | + +Always infer Pillar from the owner's department when not specified. See `dembrane-org` for department → pillar mapping. + +## Red Flags — Stop and Clarify + +| Signal | What it means | What to do | +|--------|--------------|------------| +| Task has 5+ levels of sub-tasks | This is a project, not a task | Suggest creating an Initiative instead | +| Task owner is undefined | Nobody is accountable | Default to the sender, or ask who | +| Due date is 3+ months away | Probably an Initiative or Key Result | Suggest restructuring into the progress hierarchy | +| Status hasn't changed in 4+ weeks | It's stuck, not in progress | Surface it — unblock, cancel, or re-scope | +| Multiple tasks with near-identical names | Duplication risk | Search first, link or merge instead of creating | +| Task is really a sales follow-up | Commercial work is deferred | Tell them it needs Attio — ask Sameer | + +## When Stuck + +| Problem | Fix | +|---------|-----| +| Too many tasks to prioritize | Batch by Pillar first, then sort by Priority within each batch | +| Can't tell if task is done | Ask: "Is there a deliverable? Is it in Notion?" If yes to both → Done | +| Task keeps growing in scope | Split it. Create sub-tasks for the parts, mark the parent as the container | +| User says "just do it" without details | Push back gently: "What does done look like for this?" | + +## Verification + +Before marking a task workflow complete: +- [ ] Every created task has an owner +- [ ] Every task has at least one Pillar relation +- [ ] Status reflects reality (not optimism) +- [ ] If a deliverable was produced, it's linked as a Notion Resource +- [ ] No sales/pipeline task was faked in Notion to work around the missing Attio integration + +## Workflows + +These are the recurring shapes of task work. They aren't commands — they fire on what the user says. + +### Daily view — "what's on my plate?" / "what's my day look like?" +1. Resolve the sender (name, Notion user ID). +2. **Calendar** (*only if Sam has the user's calendar*): today's events as a timeline; note gaps where deep work fits. Skip silently if unavailable — don't nag. +3. **Important emails** (*only if Sam has the user's mailbox*): surface the top 3–5 that need attention. Skip silently if unavailable. +4. **Tasks**: owner = sender, status in the to-do + in-progress groups. Sort Priority → Deadline → Do date. +5. Present, grouped: 🔥 Overdue · 🎯 Today · 🚧 In progress · ⏳ Waiting (Blocked / Pending external) · 📋 Top-5 backlog by priority. Show name, priority, deadline, linked initiative. +6. **Community pulse** — only if communities have gone quiet (>2 weeks since last edit); a nudge, not a guilt trip. Skip entirely otherwise. +7. If calendar gaps exist, suggest fitting a high-priority task into one. Offer to start work on one → *Working a task*. + +### Brain dump — user dumps free text, a list, a transcript, mixed languages +1. Let them finish — don't interrupt mid-dump. +2. Parse each discrete actionable item and suggest: **name** (verb-first), **priority** (infer from urgency language — "asap" → Mission critical/Important; "when I get to it" → Would be nice; default Significant), **pillar** (infer from topic, or the sender's department), **deadline** / **do date** (extract if mentioned), **initiative** (suggest if obviously linked). +3. Present the whole batch for confirmation; let them change fields, remove, add, merge, or split. +4. Create in Notion. Status = `Backlog` unless they say otherwise. If any item is a sales follow-up, flag the deferral instead of creating it. + +### Working a task — "let's work on X" / picked from the daily view +1. **Select**: fetch the named task, or show today's list and ask which. +2. **Load context**: read the page and its relations (Initiatives, Resources, Moments, parent, sub-tasks). +3. Set Status → `In progress`; state what needs to happen. +4. Do the work with the user. +5. **If a deliverable is produced** → create a Notion Resource (see `resource-navigation`): `Status` = Draft, same Pillar(s) as the task, a Summary / Purpose; link it into the task's `Resources` relation; write the content in the Resource body. +6. **Complete**: upgrade the Resource status (ask: Good for now / Current); set task → `Done`; set `Do date` = today if unset. +7. Offer the next task. + +### End-of-day review — end of day / "how'd today go?" +1. **Today's activity**: Tasks where owner = sender AND last edited = today → split into Completed today / Worked on (not done) / Still open with near deadlines. +2. **Tomorrow** (*if Sam has the user's calendar*): first meeting time, key events, any prep needed. +3. Present: ✅ Completed · 🚧 Worked on · 📅 Upcoming deadlines this week · Tomorrow + suggested focus (highest-priority open task). +4. Offer to add tasks (→ *Brain dump*) or reprioritize. + +### Planning view — "what's the sprint / this initiative / this pillar looking like?" +1. **Scope**: this sprint · my initiatives · a named initiative · a pillar · the big picture. +2. **Sprint**: current sprint (quarter "I" = Iris, two-week sprints); tasks by Sprint; group by Status → Pillar → owner; show counts (done / in progress / blocked / planned / backlog). +3. **Initiative**: fetch the page; linked Tasks by status; linked Resources (deliverables and their status); linked Moments; judge on-track / at-risk / blocked. +4. **Pillar**: active Initiatives and Tasks under it — what's moving, what's stuck. +5. **Big picture**: summarize each pillar; surface cross-pillar blockers and initiatives with no recent activity. +6. Close with actionable nudges (blocked items, in-progress >2 weeks, sprints with no planned tasks). Offer to reprioritize, work a blocker (→ *Working a task*), or capture tasks (→ *Brain dump*). + +## Skill Dependencies + +**Pairs with:** `resource-navigation` (when work produces deliverables), `meeting-management` (action items from meetings), `professional-reflections` (a review may trigger reflection). +**Depends on:** `dembrane-org` (pillar names, department mapping, creation conventions, Slack-sender identity). diff --git a/src/runtime/agents/mentor.md b/src/runtime/agents/mentor.md index 492aabc..9725f22 100644 --- a/src/runtime/agents/mentor.md +++ b/src/runtime/agents/mentor.md @@ -9,7 +9,7 @@ model: mentor You are Sam's mentor. Sam (running on Gemini 3.5 Flash) has called you because you're being asked to look at work Flash has done and tell Sam what Flash missed. -Your value comes from **reviewing**, not planning ahead. Flash plans and executes well when the work is mechanical. Where Flash falls short is in *noticing*: noticing intent ambiguity, noticing when a rule is missing from Sam's own source, noticing when a "pattern" Flash spotted in data is actually noise. That's the gap you fill. You read what Flash produced, you read the raw data Flash worked from, you read the rules in `src/identity.md`, `src/scope.md`, `src/capabilities/*.md`, and `src/skills/*.md` that Flash was operating under — and you surface the misses. +Your value comes from **reviewing**, not planning ahead. Flash plans and executes well when the work is mechanical. Where Flash falls short is in *noticing*: noticing intent ambiguity, noticing when a rule is missing from Sam's own source, noticing when a "pattern" Flash spotted in data is actually noise. That's the gap you fill. You read what Flash produced, you read the raw data Flash worked from, you read the rules in `src/identity.md`, `src/scope.md`, `src/capabilities/*.md`, `src/skills/`, and `src/recipes/` that Flash was operating under — and you surface the misses. You are read-only by design. You have `read_file`, `grep`, `glob_files`, `fetch_url`. No `bash`, no `write_file`, no `edit_file`. Sam executes what you suggest. The split is deliberate: Opus understands and suggests; Flash acts. Mixing the two would erode both. @@ -33,9 +33,9 @@ What you do owe Sam, regardless of shape: - **Be honest about agreement.** When Sam's analysis is actually solid, say so plainly and keep moving. Don't manufacture disagreement to justify the consult. Calibration is part of the value. -- **Be concrete when you'd suggest action.** If you'd open a PR, name the target file, the rough shape of the change, and (ideally) the branch name. Sam will open it via `github-pr-workflow`. "Sam should think about X" is a non-action; "Sam should add a rule to `src/skills/skill-creator/skill.md` saying Y, before line Z" is. +- **Be concrete when you'd suggest action.** If you'd open a PR, name the target file, the rough shape of the change, and (ideally) the branch name. Sam will open it via `github-pr-workflow`. "Sam should think about X" is a non-action; "Sam should add a rule to `src/recipes/self-improvement/skills/skill-creator/skill.md` saying Y, before line Z" is. -- **Read Sam's source when the work suggests a rule gap.** Sam's behavior is defined by `src/identity.md`, `src/scope.md`, `src/capabilities/*.md`, `src/skills/*.md`. If a Flash miss traces back to a missing or misleading rule there, surface the specific source edit. Source edits compound across every future session — that's typically the highest-leverage thing you can find. +- **Read Sam's source when the work suggests a rule gap.** Sam's behavior is defined by `src/identity.md`, `src/scope.md`, `src/capabilities/*.md`, `src/skills/`, and `src/recipes/`. If a Flash miss traces back to a missing or misleading rule there, surface the specific source edit. Source edits compound across every future session — that's typically the highest-leverage thing you can find. - **Match Sam's tone** (see `src/identity.md`): terse, lead with the consequence, honest about uncertainty. Plans aren't pep talks. Don't pad. When you're uncertain, say "I think" or "this is a guess" — don't perform confidence. diff --git a/src/runtime/config.py b/src/runtime/config.py index bc1b2e6..6d95b67 100644 --- a/src/runtime/config.py +++ b/src/runtime/config.py @@ -40,9 +40,10 @@ GITHUB_WEBHOOK_SECRET = os.environ.get("GITHUB_WEBHOOK_SECRET") SAM_CHANNEL = os.environ.get("SAM_CHANNEL") # optional: restrict to one channel for v0 SAM_OPERATOR_USER_ID = os.environ.get("SAM_OPERATOR_USER_ID") # @-mentioned when both attempts fail +NOTION_API_KEY = os.environ.get("NOTION_API_KEY") # optional: workspace-ops recipe connector SAM_HOME = Path(os.environ.get("SAM_HOME", "/data")) SAM_REPO = Path("/home/sam") # where Sam's checkout lives in the container -SAM_SRC = SAM_REPO / "src" # identity, scope, capabilities, skills, runtime +SAM_SRC = SAM_REPO / "src" # identity, scope, capabilities, skills, recipes, runtime SAM_CLAUDE_DIR = SAM_REPO / ".claude" # where Claude Code looks for project-level config GITHUB_REPO = os.environ.get("SAM_GITHUB_REPO", "Dembrane/sam") # Sam's own repo, for commit links diff --git a/src/runtime/daemon.py b/src/runtime/daemon.py index c6daa6a..288532a 100644 --- a/src/runtime/daemon.py +++ b/src/runtime/daemon.py @@ -1981,26 +1981,35 @@ async def _post_operator_alert( except Exception: log.exception("operator alert post failed") - def _discover_cron_skills(self) -> list[tuple[str, str]]: - """Scan `src/skills//skill.md` for skills with a `cron:` field. + def _discover_cron_skills(self) -> list[tuple[str, str, str]]: + """Scan every skill.md — substrate and recipe-owned — for a `cron:` field. - Returns a list of (skill_name, cron_expression) tuples. The skill - body lives at `src/skills//skill.md`; the daemon doesn't read - it — Sam reads it when the scheduled message fires. + Returns a list of (skill_name, cron_expression, skill_path) tuples, + where skill_path is repo-relative (e.g. + `src/recipes/self-improvement/skills/daily-maintenance/skill.md`). + The daemon doesn't read the body — Sam reads it when the scheduled + message fires, at the path the template names. Walks the **nested** layout (PR #40 migrated skills from flat `src/skills/.md` to `src/skills//skill.md`). The glob was not updated at the time, which silently disabled every cron skill until this fix — including the daily-maintenance Opus - mentor review that SAM-37 ships. + mentor review that SAM-37 ships. The recipes layout repeats the + lesson: substrate skills live at `src/skills//skill.md`, + recipe-owned skills at `src/recipes//skills//skill.md`, + and BOTH globs below must stay in sync with the layout. """ from .config import SAM_SRC from .prompts import _parse_frontmatter + skill_files: list = [] skills_dir = SAM_SRC / "skills" - if not skills_dir.exists(): - return [] - found: list[tuple[str, str]] = [] - for skill in sorted(skills_dir.glob("*/skill.md")): + if skills_dir.exists(): + skill_files.extend(skills_dir.glob("*/skill.md")) + recipes_dir = SAM_SRC / "recipes" + if recipes_dir.exists(): + skill_files.extend(recipes_dir.glob("*/skills/*/skill.md")) + found: list[tuple[str, str, str]] = [] + for skill in sorted(skill_files): # Skip `_template/skill.md` — the template carries a placeholder # `cron:` field for the routine-skill example but isn't a real # registered skill. @@ -2017,10 +2026,11 @@ def _discover_cron_skills(self) -> list[tuple[str, str]]: # In the nested layout the skill's identity is its parent-directory # name (e.g. `daily-maintenance`), not the file stem `skill`. name = meta.get("name") or skill.parent.name - found.append((name, cron_expr)) + rel_path = str(skill.relative_to(SAM_SRC.parent)) + found.append((name, cron_expr, rel_path)) return found - async def _run_cron_skill(self, skill_name: str, cron_expr: str) -> None: + async def _run_cron_skill(self, skill_name: str, cron_expr: str, skill_path: str) -> None: """One async task per scheduled skill. Sleeps until each next fire.""" try: from croniter import croniter @@ -2054,9 +2064,9 @@ async def _run_cron_skill(self, skill_name: str, cron_expr: str) -> None: return # shutdown signalled except asyncio.TimeoutError: pass # woke up - await self._enqueue_scheduled_skill(skill_name) + await self._enqueue_scheduled_skill(skill_name, skill_path) - async def _enqueue_scheduled_skill(self, skill_name: str) -> None: + async def _enqueue_scheduled_skill(self, skill_name: str, skill_path: str) -> None: target_channel = self.broadcast_channel_id if not target_channel: log.error("cannot enqueue scheduled skill %s: no broadcast channel found", skill_name) @@ -2065,6 +2075,7 @@ async def _enqueue_scheduled_skill(self, skill_name: str) -> None: today_journal = f"/data/journal/{datetime.now().date().isoformat()}.md" text = SCHEDULED_SKILL_TEMPLATE.format( skill_name=skill_name, + skill_path=skill_path, today_journal=today_journal, channel=target_channel, ) @@ -2177,9 +2188,9 @@ async def run(self) -> None: # One async task per scheduled skill (skills with a `cron:` frontmatter # field). Discovered at startup; changes require a daemon restart. cron_tasks: list[asyncio.Task] = [] - for skill_name, cron_expr in self._discover_cron_skills(): - log.info("registering scheduled skill: %s (cron=%s)", skill_name, cron_expr) - cron_tasks.append(asyncio.create_task(self._run_cron_skill(skill_name, cron_expr))) + for skill_name, cron_expr, skill_path in self._discover_cron_skills(): + log.info("registering scheduled skill: %s (cron=%s, path=%s)", skill_name, cron_expr, skill_path) + cron_tasks.append(asyncio.create_task(self._run_cron_skill(skill_name, cron_expr, skill_path))) if not cron_tasks: log.info("no skills with `cron:` frontmatter; no scheduled tasks running") diff --git a/src/runtime/notion.py b/src/runtime/notion.py new file mode 100644 index 0000000..ff58867 --- /dev/null +++ b/src/runtime/notion.py @@ -0,0 +1,90 @@ +"""Lightweight HTTPX-based client for the Notion API. + +Leverages the pre-installed httpx dependency to provide sync operations +for querying databases, creating pages, updating properties, and managing blocks. +""" + +from __future__ import annotations + +import os +import logging +from typing import Any, Optional +import httpx + +log = logging.getLogger(__name__) + +class NotionClient: + """Lightweight sync client for Notion REST API v1.""" + + def __init__(self, api_key: Optional[str] = None): + self.api_key = api_key or os.environ.get("NOTION_API_KEY") + if not self.api_key: + log.warning("NOTION_API_KEY is not set in environment.") + self.headers = { + "Authorization": f"Bearer {self.api_key or ''}", + "Notion-Version": "2022-06-28", + "Content-Type": "application/json", + } + self.base_url = "https://api.notion.com/v1" + + def request(self, method: str, path: str, json_data: Optional[dict[str, Any]] = None) -> dict[str, Any]: + """Perform a synchronous HTTP request to the Notion API.""" + if not self.api_key: + raise ValueError("NOTION_API_KEY is not configured in the environment") + + with httpx.Client(headers=self.headers, timeout=30.0) as client: + response = client.request(method, f"{self.base_url}{path}", json=json_data) + try: + response.raise_for_status() + except httpx.HTTPStatusError as e: + log.error("Notion API error: %s - Body: %s", e, response.text) + raise e + return response.json() + + def query_database( + self, + database_id: str, + filter_data: Optional[dict[str, Any]] = None, + sorts: Optional[list[dict[str, Any]]] = None + ) -> dict[str, Any]: + """Query a Notion database collection with optional filters and sorting.""" + payload: dict[str, Any] = {} + if filter_data: + payload["filter"] = filter_data + if sorts: + payload["sorts"] = sorts + return self.request("POST", f"/databases/{database_id}/query", payload) + + def create_page( + self, + parent_database_id: str, + properties: dict[str, Any], + children: Optional[list[dict[str, Any]]] = None + ) -> dict[str, Any]: + """Create a new page (database item) within the specified parent database.""" + payload: dict[str, Any] = { + "parent": {"database_id": parent_database_id}, + "properties": properties, + } + if children: + payload["children"] = children + return self.request("POST", "/pages", payload) + + def update_page_properties(self, page_id: str, properties: dict[str, Any]) -> dict[str, Any]: + """Update properties of an existing Notion page.""" + return self.request("PATCH", f"/pages/{page_id}", {"properties": properties}) + + def get_block_children(self, block_id: str) -> dict[str, Any]: + """Retrieve block children (the body contents) of a page or parent block.""" + return self.request("GET", f"/blocks/{block_id}/children") + + def append_block_children(self, block_id: str, children: list[dict[str, Any]]) -> dict[str, Any]: + """Append block children (write text, headers, bullets) to a parent block.""" + return self.request("PATCH", f"/blocks/{block_id}/children", {"children": children}) + + def search(self, query: str, filter_type: Optional[str] = None) -> dict[str, Any]: + """Search the Notion workspace for pages or databases by query string.""" + payload: dict[str, Any] = {"query": query} + if filter_type: + payload["filter"] = {"value": filter_type, "property": "object"} + return self.request("POST", "/search", payload) diff --git a/src/runtime/prompts.py b/src/runtime/prompts.py index f293719..04bf3e3 100644 --- a/src/runtime/prompts.py +++ b/src/runtime/prompts.py @@ -135,7 +135,7 @@ SCHEDULED_SKILL_TEMPLATE = ( "This is a SCHEDULED SKILL invocation — not a Slack message. " "The daemon's scheduler triggered `{skill_name}`. " - "Read `src/skills/{skill_name}/skill.md` (or `src/skills/{skill_name}.md`) for the full directive, then follow it.\n\n" + "Read `{skill_path}` for the full directive, then follow it.\n\n" "BEFORE running the full directive: grep today's journal file " "({today_journal}) for past fires of `{skill_name}`. If past-you " "already ran it today, your job here is a delta check — what changed " @@ -157,7 +157,7 @@ "\n" "**Your job in this session:**\n" "\n" - "1. **Read the per-event playbook.** `src/skills/github-webhook-events/` " + "1. **Read the per-event playbook.** `src/recipes/software-delivery/skills/github-webhook-events/` " "has a sub-page per event type (e.g. `check-run-completed.md`, " "`pull-request-review-submitted.md`). Read the one matching `{event}` " "and follow it — it defines what this event means and what Sam should " @@ -215,12 +215,13 @@ def _parse_frontmatter(text: str) -> tuple[dict[str, str], str]: return meta, body -def _build_skill_catalog(skills_dir: Path) -> str: - """Emit a frontmatter-only listing of skills. +def _skill_catalog_entries(skills_dir: Path) -> list[str]: + """Emit frontmatter-only catalog entries for every skill in a directory. Skills are NOT hot-loaded into the system prompt. The model sees only name + description + when_to_use + path, and decides whether to Read - the full skill body when relevant. + the full skill body when relevant. Shared by the substrate catalog and + the per-recipe catalogs. """ entries: list[str] = [] for skill in sorted(list(skills_dir.glob("*.md")) + list(skills_dir.glob("*/skill.md"))): @@ -240,11 +241,18 @@ def _build_skill_catalog(skills_dir: Path) -> str: line += f"\n *also scheduled by the daemon:* `{cron_expr}` (cron). When this fires you'll see a SCHEDULED SKILL invocation block at the top of your conversation." line += f"\n *full content:* `Read {rel}`" entries.append(line) + return entries + + +def _build_skill_catalog(skills_dir: Path) -> str: + """The substrate skill catalog — skills every recipe shares.""" + entries = _skill_catalog_entries(skills_dir) if not entries: return "" header = ( "# SKILLS (catalog only)\n\n" - "Skills are patterns Sam has learned. The listing below shows names " + "Skills are patterns Sam has learned. These are substrate skills — " + "shared across all recipes. The listing below shows names " "and descriptions; bodies are NOT included in this prompt. When a " "skill matches your task, `Read` the listed path BEFORE applying it. " "Don't guess what's in a skill — read it.\n" @@ -252,11 +260,53 @@ def _build_skill_catalog(skills_dir: Path) -> str: return header + "\n" + "\n\n".join(entries) +def _build_recipe_sections(recipes_dir: Path) -> list[str]: + """One hot-loaded section per recipe under `src/recipes//`. + + A recipe is a behaviour bundle: a manifest (`recipe.md`, hot-loaded in + full — it carries the communication contract and scope deltas that must + be known whenever the recipe is active) plus recipe-owned skills + (catalog-only, same lazy-load rules as substrate skills). + """ + sections: list[str] = [] + if not recipes_dir.exists(): + return sections + manifests = sorted(recipes_dir.glob("*/recipe.md")) + if not manifests: + return sections + sections.append( + "# RECIPES (behaviour bundles)\n\n" + "A recipe is one behaviour Sam does, packaged as a vertical slice: " + "the skills it uses, the connectors it needs, and — most " + "importantly — its communication contract. Different behaviours " + "use Slack differently; when the work at hand matches a recipe, " + "that recipe's communication contract governs where, how often, " + "and in what shape Sam posts, overriding the general defaults " + "wherever the contract says so. Identity and the scope invariants " + "are never overridden — recipes sit below them.\n\n" + "Each recipe below hot-loads its manifest; its skills are " + "catalog-only (Read them on demand, same as substrate skills)." + ) + for manifest in manifests: + meta, body = _parse_frontmatter(manifest.read_text()) + name = meta.get("name") or manifest.parent.name + section = f"# RECIPE: {name}\n\n{body.strip()}" + entries = _skill_catalog_entries(manifest.parent / "skills") + if entries: + section += ( + "\n\n## Skills in this recipe (catalog only)\n\n" + + "\n\n".join(entries) + ) + sections.append(section) + return sections + + def assemble_system_prompt() -> str: """Build the system prompt for a Claude Code session. - Hot-loads identity, scope, and capabilities (stable, always relevant). - Skills are catalog-only — model reads them on demand. + Hot-loads identity, scope, capabilities, and recipe manifests (stable, + always relevant). Skills — substrate and recipe-owned — are + catalog-only; the model reads them on demand. Re-read every session, so a `git pull` followed by next message picks up the new version of Sam. @@ -273,6 +323,8 @@ def _add(path: Path, header: str) -> None: for cap in sorted((SAM_SRC / "capabilities").glob("*.md")): sections.append(f"# CAPABILITY: {cap.stem}\n\n{cap.read_text()}") + sections.extend(_build_recipe_sections(SAM_SRC / "recipes")) + skills_dir = SAM_SRC / "skills" if skills_dir.exists(): catalog = _build_skill_catalog(skills_dir) diff --git a/src/runtime/session.py b/src/runtime/session.py index ff1b77a..ceae1b0 100644 --- a/src/runtime/session.py +++ b/src/runtime/session.py @@ -536,7 +536,7 @@ def to_initial_user_message(self) -> str: lines.append(f"- `{name}` ({mime}, {size}) — {url}") lines.append( "\nFetch only if the content is relevant to the task. " - "See `src/skills/slack-files.md` for the curl command and per-mimetype handling." + "See `src/skills/slack-files/skill.md` for the curl command and per-mimetype handling." ) body += "\n".join(lines) + "\n\n" body += ( diff --git a/src/scope.md b/src/scope.md index 797c2f8..0dcaa30 100644 --- a/src/scope.md +++ b/src/scope.md @@ -74,6 +74,14 @@ Default: Sam doesn't post in Slack between 8pm and 8am local time unless somethi If Sam is unsure whether something is urgent, it isn't. +These are the defaults. A recipe (`src/recipes//recipe.md`) may +declare a bounded exception in its communication contract — e.g. a future +incident-response recipe posting outside working hours while an incident +is open. Recipe manifests carry that kind of authority, so changes to +them are Tier 2: proposed deliberately, reviewed like changes to this +file. What a recipe can never override: who the principal is, the "should +not attempt" list, and honesty. + ## When Sam is over scope If a request lands that's outside what Sam should do — too risky, wrong kind of work, outside the repo set, requires judgment Sam shouldn't make — Sam says so directly. diff --git a/tests/eval/test_structural.py b/tests/eval/test_structural.py index cc7ffb1..02f570f 100644 --- a/tests/eval/test_structural.py +++ b/tests/eval/test_structural.py @@ -170,23 +170,44 @@ def test_skill_creator_visible_in_catalog(): def _all_skill_dirs() -> list: - """Enumerate every src/skills// with a skill.md. Used by the - parametrized catalog-presence test below. Computed from `__file__` + """Enumerate every skill dir with a skill.md — substrate + (`src/skills//`) and recipe-owned + (`src/recipes//skills//`). Used by the parametrized + catalog-presence test below. Computed from `__file__` (not from SAM_SRC) because pytest parametrization runs at collection time, before the autouse SAM_SRC fixture fires.""" from pathlib import Path repo_root = Path(__file__).resolve().parent.parent.parent + candidates = [] skills_dir = repo_root / "src" / "skills" - if not skills_dir.exists(): - return [] + if skills_dir.exists(): + candidates.extend(skills_dir.iterdir()) + recipes_dir = repo_root / "src" / "recipes" + if recipes_dir.exists(): + for recipe in recipes_dir.iterdir(): + if (recipe / "skills").exists(): + candidates.extend((recipe / "skills").iterdir()) return sorted( - p for p in skills_dir.iterdir() + p for p in candidates if p.is_dir() and not p.name.startswith("_") and (p / "skill.md").exists() ) +def _all_recipe_dirs() -> list: + """Enumerate every src/recipes// with a recipe.md manifest.""" + from pathlib import Path + repo_root = Path(__file__).resolve().parent.parent.parent + recipes_dir = repo_root / "src" / "recipes" + if not recipes_dir.exists(): + return [] + return sorted( + p for p in recipes_dir.iterdir() + if p.is_dir() and (p / "recipe.md").exists() + ) + + @pytest.mark.parametrize("skill_dir", _all_skill_dirs(), ids=lambda p: p.name) def test_every_skill_visible_in_catalog(skill_dir): """Every skill in `src/skills//` must appear in the assembled @@ -214,8 +235,49 @@ def test_every_skill_visible_in_catalog(skill_dir): ) +@pytest.mark.parametrize("recipe_dir", _all_recipe_dirs(), ids=lambda p: p.name) +def test_every_recipe_manifest_hot_loaded(recipe_dir): + """Every recipe manifest must be hot-loaded into the assembled system + prompt — headline plus its communication contract. The contract is + the reason the recipe layer exists (different behaviours use Slack + differently); if the assembler drops it, Sam falls back to global + defaults silently and the whole layer is decorative. + """ + sp = assemble_system_prompt() + name = recipe_dir.name + assert f"RECIPE: {name}" in sp, ( + f"recipe {name!r} exists at {recipe_dir} but its manifest doesn't " + "appear in the assembled system prompt — check " + "`_build_recipe_sections` in `src/runtime/prompts.py`." + ) + assert "## Communication contract" in sp, ( + "no communication contract survived into the system prompt — " + "recipe manifests are being loaded without their bodies" + ) + + +def test_recipe_skills_cataloged_under_their_recipe(): + """A recipe's skills must appear in the prompt *after* their recipe + header (they're cataloged inside the recipe section, not in the + substrate catalog). Uses github-pr-workflow/software-delivery as the + canonical pair — if the ordering breaks, recipe skills have leaked + into the wrong section or vanished. + """ + sp = assemble_system_prompt() + recipe_pos = sp.find("RECIPE: software-delivery") + # The bold catalog-entry marker, not the bare name — capabilities + # hot-loaded earlier in the prompt legitimately mention the skill + # by path; only the catalog renders it as `**name**`. + skill_pos = sp.find("**github-pr-workflow**") + assert recipe_pos != -1 and skill_pos != -1 + assert skill_pos > recipe_pos, ( + "github-pr-workflow appears before its recipe section — recipe " + "skill catalogs are not nested under their manifests" + ) + + def test_skill_creator_still_carries_sampling_rule(): - """The body of `src/skills/skill-creator/skill.md` must still contain + """The body of `src/recipes/self-improvement/skills/skill-creator/skill.md` must still contain the sample-before-codifying rule. The catalog only carries the description; the actual rule lives in the body and is read on demand. Defends against a future edit silently deleting the @@ -224,7 +286,10 @@ def test_skill_creator_still_carries_sampling_rule(): from pathlib import Path repo_root = Path(__file__).resolve().parent.parent.parent - skill = repo_root / "src" / "skills" / "skill-creator" / "skill.md" + skill = ( + repo_root / "src" / "recipes" / "self-improvement" / "skills" + / "skill-creator" / "skill.md" + ) body = skill.read_text() assert "Sample Before Codifying" in body, ( "skill-creator lost its 'Sample Before Codifying' section — " @@ -256,12 +321,16 @@ def test_every_skill_md_has_required_frontmatter(): from pathlib import Path import re - skills_root = Path(__file__).resolve().parent.parent.parent / "src" / "skills" + src_root = Path(__file__).resolve().parent.parent.parent / "src" + skills_root = src_root / "skills" assert skills_root.exists(), f"src/skills/ not found at {skills_root}" - skill_files = sorted(skills_root.glob("*/skill.md")) + skill_files = sorted(skills_root.glob("*/skill.md")) + sorted( + (src_root / "recipes").glob("*/skills/*/skill.md") + ) assert len(skill_files) >= 5, ( - f"expected at least 5 skill.md files under src/skills/*/skill.md, " + f"expected at least 5 skill.md files across src/skills/*/skill.md " + f"and src/recipes/*/skills/*/skill.md, " f"found {len(skill_files)} — catalog discovery may be broken" ) diff --git a/tests/runtime/test_model_architecture.py b/tests/runtime/test_model_architecture.py index cae5946..ff9dce5 100644 --- a/tests/runtime/test_model_architecture.py +++ b/tests/runtime/test_model_architecture.py @@ -57,7 +57,7 @@ def test_pro_executor_agent_definition_exists(): def test_consult_opus_skill_exists_and_has_when_to_use(): """`consult-opus` skill must declare when_to_use so the daemon's skill-catalog includes a trigger condition Sam can match against.""" - skill_md = SRC_DIR / "skills" / "consult-opus" / "skill.md" + skill_md = SRC_DIR / "recipes" / "self-improvement" / "skills" / "consult-opus" / "skill.md" assert skill_md.exists(), f"missing: {skill_md}" content = skill_md.read_text() assert "name: consult-opus" in content diff --git a/tests/runtime/test_source_integrity.py b/tests/runtime/test_source_integrity.py index 7b6bc08..b19a1bf 100644 --- a/tests/runtime/test_source_integrity.py +++ b/tests/runtime/test_source_integrity.py @@ -25,6 +25,7 @@ REPO_ROOT = Path(__file__).resolve().parent.parent.parent SRC_DIR = REPO_ROOT / "src" SKILLS_DIR = SRC_DIR / "skills" +RECIPES_DIR = SRC_DIR / "recipes" AGENTS_DIR = SRC_DIR / "runtime" / "agents" CAPABILITIES_DIR = SRC_DIR / "capabilities" @@ -57,15 +58,35 @@ def _parse_frontmatter(md_path: Path) -> dict[str, str]: def _skill_dirs() -> list[Path]: - """All skill directories (each has a skill.md inside).""" - if not SKILLS_DIR.exists(): - return [] + """All skill directories (each has a skill.md inside) — substrate + (`src/skills//`) AND recipe-owned + (`src/recipes//skills//`). Recipe skills obey the same + frontmatter/name/cron invariants as substrate skills, so every + parametrized test in this file covers both automatically.""" + roots: list[Path] = [] + if SKILLS_DIR.exists(): + roots.extend(SKILLS_DIR.iterdir()) + if RECIPES_DIR.exists(): + for recipe in RECIPES_DIR.iterdir(): + skills = recipe / "skills" + if skills.exists(): + roots.extend(skills.iterdir()) return sorted( - p for p in SKILLS_DIR.iterdir() + p for p in roots if p.is_dir() and not p.name.startswith("_") and (p / "skill.md").exists() ) +def _recipe_dirs() -> list[Path]: + """All recipe directories (each has a recipe.md manifest inside).""" + if not RECIPES_DIR.exists(): + return [] + return sorted( + p for p in RECIPES_DIR.iterdir() + if p.is_dir() and not p.name.startswith("_") and (p / "recipe.md").exists() + ) + + # ─── Skill frontmatter integrity ─────────────────────────────────────────────── @@ -182,14 +203,25 @@ def test_discover_cron_skills_finds_nested_layout_skills(monkeypatch): from src.runtime.daemon import Daemon d = Daemon.__new__(Daemon) found = d._discover_cron_skills() - found_names = {name for name, _ in found} + found_names = {name for name, _, _ in found} # The daily-maintenance skill must be discoverable — it's the canonical - # cron skill in this repo. If it's missing, the discovery is broken. + # cron skill in this repo, and it now lives inside the self-improvement + # recipe. If it's missing, the discovery is broken. assert "daily-maintenance" in found_names, ( f"_discover_cron_skills returned {found_names!r}; expected at least " "`daily-maintenance` since that skill has `cron:` in its frontmatter. " - "Likely cause: the glob walks the wrong layout (flat vs nested)." + "Likely cause: the glob walks the wrong layout (flat vs nested vs " + "recipe-owned)." ) + # Every discovered skill must report a path that actually exists — + # the SCHEDULED SKILL template tells Sam to Read exactly this path, + # so a dangling path means the scheduled session can't find its + # directive. + for name, _, rel_path in found: + assert (SRC_DIR.parent / rel_path).exists(), ( + f"cron skill {name!r} reports path {rel_path!r} which doesn't " + "exist — the scheduled session would Read a phantom file" + ) def test_subagents_capability_references_existing_skills(): @@ -209,7 +241,9 @@ def test_subagents_capability_references_existing_skills(): referenced_skills: set[str] = set() for m in re.finditer(r"`([a-z][a-z0-9-]+)`\s+skill", text, re.IGNORECASE): referenced_skills.add(m.group(1)) - for m in re.finditer(r"src/skills/([a-z][a-z0-9-]+)/", text): + for m in re.finditer( + r"src/(?:skills|recipes/[a-z0-9-]+/skills)/([a-z][a-z0-9-]+)/", text + ): referenced_skills.add(m.group(1)) existing = {p.name for p in _skill_dirs()} missing = referenced_skills - existing @@ -217,3 +251,56 @@ def test_subagents_capability_references_existing_skills(): f"subagents.md references skill(s) that don't exist: {sorted(missing)}. " f"Existing skills: {sorted(existing)}" ) + + +# ─── Recipe manifest integrity ───────────────────────────────────────────────── + + +@pytest.mark.parametrize("recipe_dir", _recipe_dirs(), ids=lambda p: p.name) +def test_recipe_has_required_frontmatter(recipe_dir: Path): + """Every recipe.md manifest must declare `name`, `description`, + `connectors`, and `triggers`. The assembler hot-loads the manifest + body; the frontmatter is what names the behaviour and its runtime + needs — a manifest missing them is a bundle without a contract.""" + fm = _parse_frontmatter(recipe_dir / "recipe.md") + for field in ("name", "description", "connectors", "triggers"): + assert field in fm and fm[field], ( + f"{recipe_dir.name}: missing or empty `{field}:` in recipe.md frontmatter" + ) + + +@pytest.mark.parametrize("recipe_dir", _recipe_dirs(), ids=lambda p: p.name) +def test_recipe_name_matches_directory(recipe_dir: Path): + """`name:` in the manifest must match the recipe's directory name — + same rationale as skills: the prompt refers to one identity while + the file system holds another, and the mismatch hides.""" + fm = _parse_frontmatter(recipe_dir / "recipe.md") + assert fm.get("name", "") == recipe_dir.name, ( + f"recipe {recipe_dir.name!r} declares name={fm.get('name')!r}; these must match" + ) + + +@pytest.mark.parametrize("recipe_dir", _recipe_dirs(), ids=lambda p: p.name) +def test_recipe_declares_communication_contract(recipe_dir: Path): + """Every recipe manifest must carry a `## Communication contract` + section. The contract is the reason recipes exist as a layer — + different behaviours use Slack differently — so a manifest without + one is a recipe that silently inherits defaults it may not want. + Structural check only; the contract's content is reviewed by humans.""" + body = (recipe_dir / "recipe.md").read_text() + assert "## Communication contract" in body, ( + f"{recipe_dir.name}: recipe.md has no '## Communication contract' section" + ) + + +@pytest.mark.parametrize("recipe_dir", _recipe_dirs(), ids=lambda p: p.name) +def test_recipe_contains_no_connector_code(recipe_dir: Path): + """Connector code lives in `src/runtime/`, never in a recipe. A recipe + directory holding .py files is the boundary violation this layer was + designed to prevent — the recipe should *declare* the connector in its + frontmatter and the runtime should provide it.""" + stray = sorted(recipe_dir.rglob("*.py")) + assert not stray, ( + f"recipe {recipe_dir.name!r} contains python files {[str(p) for p in stray]}; " + "connector/runtime code belongs in src/runtime/, recipes only declare needs" + )