Add Smriti agent skill pack source and renderer

The skill pack is an instruction file installed into an agent host's
project directory so Smriti's workflow lives in the agent's system
context instead of documentation nobody reads. A single versioned
template.md renders for both Claude Code (MCP-primary) and Codex
(CLI-primary) via a pure-function substituter, keeping content in
sync mechanically across targets.

template.md contains 15 sections. The load-bearing one is Section 5,
When NOT to checkpoint, with equal weight to Section 4. Agents are
told explicitly not to checkpoint after every small step, not to
produce end-of-session blobs, not to treat commits as a save button,
not to stack commits on inconsistent state, not to restate existing
state, and not to checkpoint just because the user asked when there
is no real inflection point. A frequency target (2-4 checkpoints per
4-hour session) and a three-question signal test give agents concrete
criteria for every call.

Other sections cover the read-state-first reflex, when to fork, when
to review, when to compare, when to restore, drift detection,
explicit anti-patterns (HANDOFF.md, silent state reads, inconsistent
author_agent, /chat/send), and the phrases the agent should say out
loud so the human watching has an audit trail.

Renderer API (all pure functions): load_template, get_version, render,
install. install is version-aware: refuses to overwrite a destination
whose installed version is >= the template version unless force=True.
Dry-run mode returns the rendered content without writing.

Content-integrity tests parametrized over both targets assert that
every anti-pattern rule, the signal test, the frequency target, and
the drift-detection guidance appear in the rendered output. If a
future template edit drops any of them, tests fail loudly.

22 skill pack tests, all green.
This commit is contained in:
Himanshu Dongre 2026-04-12 02:07:47 +05:30
parent 491c7316b1
commit ec0139f707
6 changed files with 1114 additions and 0 deletions

View file

@ -30,6 +30,12 @@ smriti-mcp = "smriti_cli.mcp_server:main"
where = ["."]
include = ["smriti_cli*"]
[tool.setuptools.package-data]
# Ship the skill pack source-of-truth template with the installed
# package so `smriti skills install` and `smriti_install_skill` can
# find it via __file__-relative path resolution.
smriti_cli = ["skill_pack/template.md"]
[tool.pytest.ini_options]
testpaths = ["tests"]
addopts = "-ra --strict-markers"

View file

@ -0,0 +1,37 @@
"""Smriti agent skill pack — the agent-onboarding surface.
The skill pack teaches coding agents (Claude Code, Codex) when and
why to use Smriti's tools — when to checkpoint, when NOT to checkpoint,
when to fork, how to detect drift, and the explicit anti-patterns to
reject. It is installed into an agent host's project directory as a
single versioned markdown file so the instructions live in the
agent's system context rather than in documentation the agent never
reads.
Public surface:
render(target_key) -> str # render template for a target
install(target_key, ...) -> InstallResult # write rendered template to disk
get_version(content=None) -> str # parse frontmatter version
list_targets() -> list[SkillTarget] # enumerate known targets
get_target(target_key) -> SkillTarget # resolve a target config
Layout:
skill_pack/
__init__.py re-exports the small public surface
template.md single source of truth (versioned frontmatter)
renderer.py pure-function render + install logic
targets.py target configs (display name, destination)
"""
from .renderer import InstallResult, get_version, install, load_template, render
from .targets import SkillTarget, get_target, list_targets
__all__ = [
"InstallResult",
"SkillTarget",
"get_target",
"get_version",
"install",
"list_targets",
"load_template",
"render",
]

View file

@ -0,0 +1,239 @@
"""Skill pack renderer + installer.
Pure functions no LLM calls, no network, no global state. The
renderer reads the single source-of-truth `template.md`, substitutes
a small set of placeholders based on the target's primary mode, and
returns the rendered markdown as a string. The installer writes the
result to the target's default destination (or an override) with
version-aware refusal to overwrite.
Template syntax:
{{display_name}}
replaced with the target's display_name ("Claude Code" or "Codex")
{{primary_mode}}
replaced with "mcp" or "cli"
{{mcp:some text}}{{cli:other text}}
paired blocks. For target primary_mode="mcp" the whole pair
becomes "some text"; for primary_mode="cli" it becomes
"other text". Either block may be empty. Both blocks must
appear together an unmatched block raises ValueError at
render time so typos in the template fail loudly in tests.
The version is stored in frontmatter as:
---
smriti_skill_pack_version: 1.0
...
---
The installer reads the existing destination's frontmatter (if any),
compares versions as strings (lexicographic "1.0" < "1.1" < "1.10"
works for the foreseeable skill pack version series), and refuses to
overwrite a destination whose version is >= the template's version
unless `force=True` is passed.
"""
from __future__ import annotations
import re
from dataclasses import dataclass
from pathlib import Path
from typing import Literal, Optional
from .targets import SkillTarget, get_target
_TEMPLATE_PATH = Path(__file__).parent / "template.md"
_VERSION_RE = re.compile(
r"^smriti_skill_pack_version:\s*([^\n]+)$",
re.MULTILINE,
)
# Matches a paired {{mcp:...}}{{cli:...}} block. The `?` suffix on the
# inner `.` quantifiers makes them non-greedy so multiple pairs on the
# same line (or line-wrapped pairs) do not collapse into one giant
# match. Empty bodies are allowed.
_PAIRED_BLOCK_RE = re.compile(
r"\{\{mcp:(.*?)\}\}\{\{cli:(.*?)\}\}",
re.DOTALL,
)
# Matches any leftover unpaired block — if this matches after
# _PAIRED_BLOCK_RE has been applied, the template has a typo.
_UNPAIRED_BLOCK_RE = re.compile(
r"\{\{(?:mcp|cli):[^}]*\}\}",
re.DOTALL,
)
Action = Literal["created", "overwritten", "dry_run", "skipped"]
@dataclass
class InstallResult:
"""Result of a skill pack install attempt.
- action: what happened. "skipped" means the destination already
had a same-or-newer version and `force` was not set; nothing was
written.
- previous_version: the version read from the destination before
the install attempt, or None if the destination did not exist
or had no version frontmatter.
- content: the rendered content that was (or would have been)
written. Useful for `--dry-run` and for the MCP tool which
returns the content without writing.
"""
target: SkillTarget
destination: Path
action: Action
version: str
previous_version: Optional[str]
content: str
def load_template() -> str:
"""Read the raw template.md from the installed package."""
return _TEMPLATE_PATH.read_text(encoding="utf-8")
def get_version(content: Optional[str] = None) -> str:
"""Return the `smriti_skill_pack_version` value from the template
frontmatter. Raises ValueError if the frontmatter is missing so
missing-version bugs fail loudly during development rather than
installing an unversioned skill pack.
"""
text = content if content is not None else load_template()
m = _VERSION_RE.search(text)
if not m:
raise ValueError(
"Skill pack template is missing `smriti_skill_pack_version` "
"frontmatter. Every template.md must start with a YAML "
"frontmatter block that pins the version."
)
return m.group(1).strip()
def render(target_key: str) -> str:
"""Render the template for a specific target.
The same template.md renders for every target; placeholders and
`{{mcp:...}}{{cli:...}}` blocks control the primary-mode-specific
variations. The content (workflow heuristics, anti-patterns,
when-not-to-checkpoint rules) is identical across targets only
the tool notation and display name differ.
"""
target = get_target(target_key)
template = load_template()
return _substitute_placeholders(template, target)
def _substitute_placeholders(template: str, target: SkillTarget) -> str:
"""Apply the template substitutions for a target. Pure function.
Steps:
1. Replace every {{mcp:X}}{{cli:Y}} paired block with X or Y
depending on target.primary_mode.
2. Replace {{display_name}} target.display_name.
3. Replace {{primary_mode}} target.primary_mode.
4. Assert no unmatched paired blocks remain if any do, the
template has a typo and the renderer fails loudly with the
location context.
"""
def pick(match: re.Match[str]) -> str:
mcp_body, cli_body = match.group(1), match.group(2)
return mcp_body if target.primary_mode == "mcp" else cli_body
out = _PAIRED_BLOCK_RE.sub(pick, template)
out = out.replace("{{display_name}}", target.display_name)
out = out.replace("{{primary_mode}}", target.primary_mode)
leftover = _UNPAIRED_BLOCK_RE.search(out)
if leftover:
# Provide a snippet of context so the error is actionable.
start = max(0, leftover.start() - 30)
end = min(len(out), leftover.end() + 30)
raise ValueError(
f"Skill pack template contains an unmatched "
f"{{mcp:...}}/{{cli:...}} block. Matched fragment: "
f"{leftover.group(0)!r}. Context: ...{out[start:end]!r}..."
)
return out
def install(
target_key: str,
destination: Optional[Path] = None,
*,
force: bool = False,
dry_run: bool = False,
) -> InstallResult:
"""Install the rendered skill pack for a target.
Args:
target_key: "claude-code" or "codex".
destination: Override the target's default destination path.
When None, uses `target.default_destination` relative to
the current working directory.
force: Overwrite an existing same-or-newer version. Default
False: refuse and return `action="skipped"`.
dry_run: Render the content and return it, but do not write
to disk. Default False.
Returns:
InstallResult describing what happened.
"""
target = get_target(target_key)
dest = Path(destination) if destination is not None else target.default_destination
content = render(target_key)
current_version = get_version(content)
previous_version: Optional[str] = None
if dest.exists():
try:
existing = dest.read_text(encoding="utf-8")
except (OSError, UnicodeDecodeError):
existing = ""
m = _VERSION_RE.search(existing)
if m:
previous_version = m.group(1).strip()
if (
previous_version is not None
and not force
and previous_version >= current_version
):
return InstallResult(
target=target,
destination=dest,
action="skipped",
version=current_version,
previous_version=previous_version,
content=content,
)
if dry_run:
return InstallResult(
target=target,
destination=dest,
action="dry_run",
version=current_version,
previous_version=previous_version,
content=content,
)
dest.parent.mkdir(parents=True, exist_ok=True)
dest.write_text(content, encoding="utf-8")
return InstallResult(
target=target,
destination=dest,
action="overwritten" if previous_version else "created",
version=current_version,
previous_version=previous_version,
content=content,
)

View file

@ -0,0 +1,76 @@
"""Skill pack target configurations.
A "target" is the agent host the skill pack is being installed into.
Two targets ship in v1.0:
- claude-code: installs to `./.claude/skills/smriti/SKILL.md`. Claude
Code reads skills out of `.claude/skills/<name>/SKILL.md` as
project-level instructions. Primary tool notation is MCP
(`smriti_state(space="x")`) because Claude Code speaks MCP natively.
- codex: installs to `./AGENTS.md`. Codex reads `AGENTS.md` at the
project root as its primary instruction file. Primary tool notation
is CLI (`smriti state x`) because the Codex CLI runs commands in a
shell loop.
Both targets render from the SAME template source only the primary
tool notation and display name vary. Workflow heuristics, anti-patterns,
and when-not-to-checkpoint rules are identical for both targets by
design.
"""
from __future__ import annotations
from dataclasses import dataclass
from pathlib import Path
from typing import Literal
PrimaryMode = Literal["mcp", "cli"]
@dataclass(frozen=True)
class SkillTarget:
"""Configuration for one agent skill pack target."""
key: str
display_name: str
default_destination: Path
primary_mode: PrimaryMode
# One-line summary used by `smriti skills list`.
description: str
TARGETS: dict[str, SkillTarget] = {
"claude-code": SkillTarget(
key="claude-code",
display_name="Claude Code",
default_destination=Path(".claude/skills/smriti/SKILL.md"),
primary_mode="mcp",
description="Claude Code (MCP-native host; installs to .claude/skills/smriti/SKILL.md)",
),
"codex": SkillTarget(
key="codex",
display_name="Codex",
default_destination=Path("AGENTS.md"),
primary_mode="cli",
description="Codex (shell-based host; installs to AGENTS.md)",
),
}
def list_targets() -> list[SkillTarget]:
"""Return all known skill pack targets in a deterministic order."""
return [TARGETS[k] for k in sorted(TARGETS.keys())]
def get_target(target_key: str) -> SkillTarget:
"""Resolve a target by key. Raises ValueError for unknown keys.
The error message lists the known targets so CLI users and agents
get actionable feedback without having to consult the docs.
"""
if target_key not in TARGETS:
known = ", ".join(sorted(TARGETS.keys()))
raise ValueError(
f"Unknown skill pack target: '{target_key}'. Known targets: {known}"
)
return TARGETS[target_key]

View file

@ -0,0 +1,454 @@
---
smriti_skill_pack_version: 1.0
title: Smriti — how to use it well
target: {{display_name}}
---
# Smriti: how to use it well
You are working in a project that uses Smriti for reasoning state.
Before you do anything else on this project, read this file. It tells
you when to call Smriti, when not to, and why it matters.
Primary tool mode for your host: `{{primary_mode}}`.
---
## 1. What Smriti is, in one paragraph
Smriti is a reasoning-state backend. It stores structured snapshots of
your thinking at meaningful inflection points — decisions, assumptions,
tasks, open questions, artifacts — and makes them available to whatever
agent picks up the work next. It is not a memory database, not a wiki,
not a notes app, not a transcript archive. It is version control for
reasoning. Spaces are repos. Checkpoints are commits. Fork, compare,
restore, and review all mean roughly what they would mean in a source
control tool, but for the shape of the work rather than the code.
---
## 2. Why this file exists
You have been trained on thousands of projects that used a markdown
handoff file — `HANDOFF.md`, `NOTES.md`, `CONTEXT.md`, whatever — to
pass context between sessions, agents, or people. You probably reach
for that pattern by default. Don't. On this project it is wrong.
Smriti exists specifically to replace that pattern with something
structured, branchable, comparable, restorable, and resistant to
overwriting. Markdown handoff files lose on every axis except zero-
learning-curve. This file closes the learning-curve gap so your
default reflex on this project becomes Smriti, not a text file.
If you find yourself about to write `HANDOFF.md`, stop and write a
checkpoint instead. Every time.
---
## 3. The reflex: read state first
At the start of every session where you are working on a Smriti-enabled
project, your FIRST action is:
```
{{mcp:smriti_state(space="<project>")}}{{cli:smriti state <project>}}
```
Before you read any file. Before you run any test. Before you plan
anything. Before you answer the user's first prompt beyond "let me
check the state first."
The state brief is the minimum context you need to continue work
without re-discovering decisions that were already made. Calling it
unconditionally is cheap, and missing it is expensive. The brief is
multi-branch by default — if other agents are working on this project
on different branches, you will see them in the `## Active branches`
section with an author tag, and if any of them disagree with main on
decisions you will see a `## Divergence signal` section.
Say out loud to the user: **"Reading current state from Smriti."**
before you call state. This gives the human watching a visible audit
trail.
---
## 4. When to checkpoint
Checkpoint at an **inflection point**. An inflection point is a moment
where the reasoning state has changed in a way a future agent needs to
know about. Concretely:
- **You made a decision you would not want to re-derive.** Checkpoint.
- **You rejected a hypothesis and now know a dead end.** Checkpoint.
- **You solved a sub-problem and the next step builds on it.** Checkpoint.
- **You are about to hand off to another agent or another session.** Checkpoint.
- **You are about to explore an alternative direction and want to
preserve the current line.** Fork first, then checkpoint on the fork.
Use the **extract path**, not hand-written JSON. Pass freeform markdown
describing the inflection point and Smriti's background LLM will pull
out the structured fields (message, objective, summary, decisions,
assumptions, tasks, open questions, entities, artifacts) for you.
Example call:
{{mcp:```
smriti_create_checkpoint(
space="<project>",
content="""
# Decided on Pydantic for the state validation layer
After trying dataclass-based validation and hitting the injection
surface from unbounded extra fields, going with Pydantic BaseModel
and `extra="forbid"`. Latency overhead is ~0.3 ms per call.
## Open questions
- How do we share state across parallel agent runs?
- Cleaner schema-versioning story for migrations?
""",
author_agent="claude-code",
)
```}}{{cli:```
cat <<'MD' | smriti checkpoint create <project> --extract --author-agent <your-agent-name>
# Decided on Pydantic for the state validation layer
After trying dataclass-based validation and hitting the injection
surface from unbounded extra fields, going with Pydantic BaseModel
and `extra="forbid"`. Latency overhead is ~0.3 ms per call.
## Open questions
- How do we share state across parallel agent runs?
- Cleaner schema-versioning story for migrations?
MD
```}}
Say out loud: **"Checkpointing now — reached an inflection point on X."**
before the call. Name the X.
Always tag `author_agent` with a stable identifier for your agent
(e.g. `claude-code`, `codex-local`). This is how humans and other
agents know who wrote what on the shared timeline. Inconsistent or
missing `author_agent` makes divergence unattributable.
---
## 5. When NOT to checkpoint
This section is more important than the previous one. Read it twice.
A checkpoint is not a save button. Most of your work should not
produce a checkpoint. Resist all of these:
- **Do not checkpoint after every small step.** Finishing a helper
function, editing one file, running one test — not an inflection
point. If your checkpoint's `message` would be "Wrote a helper
function" or "Ran the tests" or "Fixed a typo," do not checkpoint.
Keep working.
- **Do not checkpoint at end of session as a blob.** A single
"everything I did today" commit with 15 decisions crammed into one
message is less useful than zero checkpoints. It has no locality.
The next agent reading it cannot tell which decisions belong
together, what caused what, or in what order things happened. If
you reach end-of-session without having checkpointed, you missed
the inflection points earlier — and the fix is NOT to compensate
with one giant dump. Write ONE crisp checkpoint for the most recent
real decision and stop. Then reflect on what you should have
checkpointed earlier.
- **Do not use checkpoints as a backup system.** The event stream
(turns) is already the backup. A checkpoint is a reasoning-state
snapshot, not a safety net. Checkpointing "because the session is
about to end" or "because the user is leaving" is a save-button
use and it is wrong.
- **Do not checkpoint with nothing crisp to say.** If you cannot
write a single sentence for the `message` that names a specific
decision made, hypothesis confirmed, problem solved, or dead end
identified — you are not at an inflection point yet. Finish
thinking. Then checkpoint.
- **Do not checkpoint on top of inconsistent state.** If
`{{mcp:smriti_state}}{{cli:smriti state}}` surfaces contradictions
between decisions, or if
`{{mcp:smriti_review_checkpoint}}{{cli:smriti checkpoint review}}`
flags issues on the current HEAD, resolve first. Either restore to
a cleaner ancestor or surface the contradictions to the human. Do
NOT stack new commits on broken state — you are compounding the
confusion, not fixing it.
- **Do not restate existing state.** If the last checkpoint already
contains the decision you are about to record, do not re-checkpoint
it. Smriti tracks new commitments, not a running summary of
everything still true. Re-checkpointing dilutes the signal.
- **Do not checkpoint just because the user asked you to.** The user
does not always know whether you are at an inflection point. If
they ask you to checkpoint and you are not, say so: "I don't have
a crisp inflection point to record yet — can you tell me what
decision or problem you want captured?" This is not disobedience;
it is protecting the signal quality of the project's timeline from
future noise.
### 5.1 The signal test
Before every checkpoint, ask yourself three questions:
1. **Can I name the inflection point in one sentence for `message`?**
If you need a paragraph, wait.
2. **If another agent read only my `decisions`, `summary`, and
`open_questions`, could they continue the work correctly?**
If no, there is nothing worth recording yet.
3. **Is the reasoning state meaningfully different from the previous
checkpoint on this branch?**
If no, you are restating.
If any answer is no, do not checkpoint.
### 5.2 What good frequency looks like
A 4-hour focused session typically produces **2 to 4 checkpoints**.
Not 20. Not 0.
- A session producing 20 checkpoints is producing noise. You are
treating checkpoints as a save button.
- A session producing 0 checkpoints is either doing trivial work or
missing the inflection points. Reflect.
- 24 is the target. Aim there.
---
## 6. When to fork
Fork when you want to explore an alternative direction **without
losing the main line**. Concretely:
```
{{mcp:smriti_fork(checkpoint_id="<current-head-id>", branch="alternative-X")}}{{cli:smriti fork <current-head-id> --branch alternative-X}}
```
Then write a checkpoint into the forked session:
{{mcp:```
smriti_create_checkpoint(
space="<project>",
session="<fork-session-id>",
content="...",
)
```}}{{cli:```
cat fork.md | smriti checkpoint create <project> \
--extract --session <fork-session-id> --author-agent <your-agent-name>
```}}
**Do not fork for small variations within the same direction.** That
is continuation, not branching. Fork when two parallel lines of
reasoning should genuinely exist for future comparison — when you
want the option to go back to the original line cleanly without
re-deriving it.
Say out loud: **"Forking a branch to explore an alternative. Main
line is preserved."**
---
## 7. When to review
Run `{{mcp:smriti_review_checkpoint(checkpoint_id="<id>")}}{{cli:smriti checkpoint review <id>}}`
when:
- The state brief you just read contains decisions or assumptions
that feel contradictory to each other.
- You are about to checkpoint on top of a state you did not write
yourself and want a sanity pass first.
- You are handing off to another agent and want to surface any
issues before the receiving agent wastes cycles on broken state.
Do **not** run review on every checkpoint. It is a self-audit tool,
not an audit trail. Running it reflexively adds noise.
---
## 8. When to compare
Run `{{mcp:smriti_compare(checkpoint_a="<A>", checkpoint_b="<B>")}}{{cli:smriti compare <A> <B>}}`
when:
- The state brief shows a `## Divergence signal` on an active branch
and you want the full diff (the signal only shows the top 3
conflicting decisions per side).
- You see two checkpoints in the lineage that should agree on
something but seem to differ.
- You are resolving a fork back to a single line and need to decide
which decisions to keep.
---
## 9. When to restore
Run `{{mcp:smriti_restore(checkpoint_id="<id>")}}{{cli:smriti restore <id>}}`
when:
- The current HEAD state is contradictory or corrupted and you want
to resume from an earlier clean snapshot.
- You are reading a past checkpoint not as history but as a starting
point — you want to continue work from it as if it were HEAD.
Say out loud: **"Restoring to an earlier checkpoint."** when you do
this. The human should know you are stepping back in time.
---
## 10. Detecting drift
If at any point you notice:
- The state brief contains tasks that appear already done, or
- Decisions that directly contradict what you have been working on, or
- Open questions that seem already resolved in the summary, or
- The `## Divergence signal` shows active branches disagreeing on
things you considered settled,
then you are in a drift situation. **Do not paper over it.** Three
acceptable responses:
1. Run `{{mcp:smriti_review_checkpoint}}{{cli:smriti checkpoint review}}`
on the current HEAD and act on the output.
2. Run `{{mcp:smriti_compare}}{{cli:smriti compare}}` on the two most
recent checkpoints from each divergent author and summarize the
difference in plain prose for the human.
3. Surface the drift to the human directly: "I am seeing scope
divergence between what the state brief says and what I have been
working on. Stopping to reconcile before continuing. Which line
should I follow?"
**Do not quietly pick a side.** Scope divergence across agents is a
real signal that the project state needs a human judgment call.
Smriti's job is to make that signal visible to you; your job is to
make it visible to the human, not to hide it by picking one branch
and continuing.
Say out loud: **"I'm seeing scope divergence between the state brief
and my work. Stopping to reconcile before continuing."**
---
## 11. Anti-patterns to reject explicitly
These are in addition to the "do not checkpoint when" rules in
Section 5.
- **Do not write HANDOFF.md, NOTES.md, CONTEXT.md, PROGRESS.md, or
any similar file on this project.** A Smriti space exists. Write
a checkpoint instead. Markdown handoff files do not survive
branching, do not cross-link to history, and do not participate in
`smriti_compare`. Every time you write one, the next agent has to
re-learn what you already knew — exactly the problem Smriti exists
to solve. If you catch yourself about to write a handoff file,
treat that as a signal you have reached an inflection point and
should checkpoint.
- **Do not silently inject state output into the user's chat
context.** Announce it: "Reading current state from Smriti." The
human should always be able to see when you are consulting or
writing to the shared timeline.
- **Do not tag `author_agent` inconsistently.** Pick ONE stable
identifier for your agent — e.g. `claude-code`, `codex-local`,
`claude-code-v2` — and use it on every checkpoint you write. If
you change your tag mid-project, the divergence signal becomes
unattributable. Stability of the tag is more important than its
content.
- **Do not invoke `/chat/send` or the live chat runtime from inside
your tool loop.** That endpoint is for humans talking to Smriti's
chat UI, not for agents writing state. Your only write paths are
`{{mcp:smriti_create_checkpoint}}{{cli:smriti checkpoint create --extract}}`
and `{{mcp:smriti_fork}}{{cli:smriti fork}}`.
- **Do not treat the extract path as a free pass to dump your
reasoning.** The extract LLM will pull whatever fields it can, but
if you pass it 2000 words of stream-of-consciousness it will
produce low-signal decisions and assumptions. Write the markdown
crisply. The extract path saves you from hand-rolling JSON; it is
not a license to be verbose.
- **Do not use `smriti_install_skill` to overwrite an in-project
skill pack that you did not write.** If the project already has
a skill pack of an older version, the install tool will tell you.
Use the existing one unless the human asks you to upgrade.
---
## 12. Phrases to say out loud
These give the human watching your session a visible audit trail.
Use them literally.
| When you are about to... | Say |
|---|---|
| call `{{mcp:smriti_state}}{{cli:smriti state}}` | "Reading current state from Smriti." |
| create a checkpoint | "Checkpointing now — reached an inflection point on X." |
| fork a branch | "Forking a branch to explore an alternative. Main line is preserved." |
| restore an earlier checkpoint | "Restoring to an earlier checkpoint." |
| run review after a drift signal | "Running a consistency review on the current state before I continue." |
| run compare on a divergence signal | "Comparing main against the divergent branch to see the full diff." |
| surface drift to the human | "I'm seeing scope divergence between the state brief and my work. Stopping to reconcile before continuing." |
These are not cosmetic. They are what makes your session legible to
the human who will eventually read the thread and decide whether
your reasoning is sound.
---
## 13. The project root
Every checkpoint you write should have a `project_root` field that
points at the absolute path where this project actually lives on
disk. {{mcp:The MCP server runs in the host's arbitrary working
directory, so it does NOT auto-capture `project_root`. Pass the
path explicitly: `smriti_create_checkpoint(..., project_root="/abs/path")`.}}{{cli:The CLI auto-captures `$(pwd)` as `project_root`
when you run from the project directory. If you are running from
somewhere else, pass `--project-root /abs/path` explicitly.}}
This is how cross-agent handoffs know where the project lives.
Missing or wrong `project_root` makes the next agent spend 30
seconds hunting for the codebase.
---
## 14. Two-sentence summary
Call {{mcp:`smriti_state`}}{{cli:`smriti state`}} at session start,
unconditionally, before anything else. Checkpoint at inflection
points — not after every small step, not at end of session as a
dump, never as a save button.
Everything else in this file is implementation detail for those two
rules.
---
## 15. If you are confused about Smriti
The order of operations is always:
1. **Read state first.** You are missing context until you do.
2. **Work.** Smriti has no opinion about what happens between
checkpoints.
3. **Checkpoint at inflection points.** Not before, not more often.
4. **Hand off.** The next agent reads the new state. No markdown
handoff file.
If you are unsure whether an action is "Smriti-shaped," ask yourself:
"Is this a reasoning-state change, or is it just work-in-progress?"
Reasoning-state changes get checkpointed. Work-in-progress does not.
If you are still unsure, surface the question to the human. They will
tell you. Do not guess.
---
*Smriti skill pack version {{primary_mode}}-1.0 — this file is
authoritative for agent behaviour on this project. If you catch it
contradicting itself or your observed behaviour of the tools, tell
the human; the skill pack is versioned and meant to be updated.*

View file

@ -0,0 +1,302 @@
"""Tests for the skill pack renderer + installer.
Covers:
- Template loads and has a parseable version frontmatter
- render() produces distinct output per target (MCP vs CLI notation)
- render() substitutes {{display_name}} and {{primary_mode}}
- render() raises on unknown targets
- render() raises on unmatched paired blocks (template typo guard)
- install() creates the destination file with the rendered content
- install() creates parent directories that don't exist
- install() --dry-run does NOT write to disk
- install() refuses to overwrite a same-version destination without --force
- install() --force overwrites a same-version destination
- install() writes to an explicit --destination override
- list_targets() returns the known targets deterministically
"""
from __future__ import annotations
from pathlib import Path
import pytest
from smriti_cli.skill_pack import (
InstallResult,
get_target,
get_version,
install,
list_targets,
load_template,
render,
)
from smriti_cli.skill_pack.renderer import _substitute_placeholders
from smriti_cli.skill_pack.targets import SkillTarget
# ── Template loading + version ──────────────────────────────────────────────
def test_load_template_nonempty():
text = load_template()
assert text.strip() != ""
assert "smriti_skill_pack_version" in text
def test_get_version_parses_frontmatter():
version = get_version()
assert version == "1.0"
def test_get_version_raises_when_frontmatter_missing():
with pytest.raises(ValueError, match="smriti_skill_pack_version"):
get_version(content="no frontmatter here\njust body text")
# ── Render ───────────────────────────────────────────────────────────────────
def test_render_claude_code_uses_mcp_notation():
out = render("claude-code")
assert "Claude Code" in out
# MCP tool-call form should be present.
assert "smriti_state(" in out
assert "smriti_create_checkpoint(" in out
# CLI shell form should NOT appear — the claude-code variant elides it.
assert "smriti checkpoint create" not in out
assert "smriti checkpoint review" not in out
def test_render_codex_uses_cli_notation():
out = render("codex")
assert "Codex" in out
# CLI shell form should be present.
assert "smriti state " in out
assert "smriti checkpoint create" in out
assert "smriti checkpoint review" in out
# MCP tool-call form should NOT appear — the codex variant elides it.
assert "smriti_state(space" not in out
assert "smriti_create_checkpoint(" not in out
def test_render_substitutes_display_name():
assert "Claude Code" in render("claude-code")
assert "Codex" in render("codex")
def test_render_substitutes_primary_mode():
claude_out = render("claude-code")
codex_out = render("codex")
assert "mcp" in claude_out
assert "cli" in codex_out
def test_render_unknown_target_raises():
with pytest.raises(ValueError, match="Unknown skill pack target"):
render("not-a-real-target")
# ── Content integrity (shared between both targets) ─────────────────────────
# These phrases define the load-bearing parts of the skill pack's
# anti-pattern teaching. If a future template edit drops any of them,
# these tests fail and we catch the regression before shipping. The
# product contract is: both targets must always teach when NOT to
# checkpoint, with all the rules intact.
_REQUIRED_PHRASES = [
# Section 5.x — When NOT to checkpoint
"after every small step",
"end of session as a blob",
"backup system",
"nothing crisp to say",
"inconsistent state",
"restate",
# Section 5.1 — the signal test (3 questions)
"The signal test",
"name the inflection point",
# Section 5.2 — frequency target
"2 to 4 checkpoints",
"20 checkpoints is producing noise",
# Section 3 — the reflex
"Reading current state from Smriti",
# Section 11 — anti-patterns
"HANDOFF.md",
"author_agent",
# Section 10 — drift detection
"divergence",
"scope divergence",
# Section 14 — two-sentence summary
"session start",
]
@pytest.mark.parametrize("target_key", ["claude-code", "codex"])
def test_render_contains_all_critical_content(target_key: str):
"""Every target must teach all anti-patterns and heuristics.
Divergent targets with missing sections would erode the skill
pack's value — the rules are identical across hosts and this
test enforces that invariant.
"""
out = render(target_key)
missing = [p for p in _REQUIRED_PHRASES if p.lower() not in out.lower()]
assert not missing, (
f"Rendered skill pack for {target_key!r} is missing required "
f"phrases: {missing}. This usually means a template edit "
f"dropped a critical section."
)
def test_render_targets_share_anti_pattern_section():
"""Both targets should include the same When-NOT-to-checkpoint
bullet structure. String-match the most distinctive phrases that
should never go away."""
claude = render("claude-code").lower()
codex = render("codex").lower()
for phrase in [
"when not to checkpoint",
"checkpoint is not a save button",
"do not checkpoint after every small step",
"do not checkpoint at end of session as a blob",
]:
assert phrase in claude, f"missing from claude-code: {phrase}"
assert phrase in codex, f"missing from codex: {phrase}"
def test_substitute_placeholders_unmatched_block_raises():
"""An unpaired {{mcp:...}} or {{cli:...}} block in the template
must fail loudly so typos in template.md are caught in tests, not
silently shipped."""
target = get_target("claude-code")
bad_template = "Some text {{mcp:foo}} with an unmatched block."
with pytest.raises(ValueError, match="unmatched"):
_substitute_placeholders(bad_template, target)
def test_substitute_placeholders_empty_blocks_allowed():
"""A paired block where one side is empty is valid and useful for
sections that should appear in one target but not the other."""
target_claude = get_target("claude-code")
target_codex = get_target("codex")
tpl = "prefix {{mcp:only-in-mcp}}{{cli:}} suffix"
claude_out = _substitute_placeholders(tpl, target_claude)
codex_out = _substitute_placeholders(tpl, target_codex)
assert "only-in-mcp" in claude_out
assert "only-in-mcp" not in codex_out
assert "prefix suffix" in codex_out
# ── Install ──────────────────────────────────────────────────────────────────
def test_install_creates_file_with_rendered_content(tmp_path: Path):
dest = tmp_path / "SKILL.md"
result = install("claude-code", destination=dest)
assert result.action == "created"
assert result.previous_version is None
assert result.destination == dest
assert dest.exists()
content = dest.read_text(encoding="utf-8")
assert "Claude Code" in content
assert "smriti_state(" in content
assert "smriti_skill_pack_version" in content
def test_install_creates_parent_directories(tmp_path: Path):
dest = tmp_path / "deep" / "nested" / "path" / "SKILL.md"
assert not dest.parent.exists()
result = install("claude-code", destination=dest)
assert result.action == "created"
assert dest.exists()
assert dest.parent.is_dir()
def test_install_dry_run_does_not_write(tmp_path: Path):
dest = tmp_path / "SKILL.md"
result = install("claude-code", destination=dest, dry_run=True)
assert result.action == "dry_run"
assert not dest.exists()
# But the rendered content is returned on the result.
assert "Claude Code" in result.content
def test_install_refuses_same_version_without_force(tmp_path: Path):
dest = tmp_path / "SKILL.md"
install("claude-code", destination=dest) # first install
result = install("claude-code", destination=dest) # second attempt
assert result.action == "skipped"
assert result.previous_version == "1.0"
# File on disk is unchanged.
assert "Claude Code" in dest.read_text(encoding="utf-8")
def test_install_force_overwrites_same_version(tmp_path: Path):
dest = tmp_path / "SKILL.md"
install("claude-code", destination=dest) # first install
# Tamper with the file — a downstream edit the agent made by hand.
dest.write_text(
"---\nsmriti_skill_pack_version: 1.0\n---\n\nTAMPERED\n",
encoding="utf-8",
)
result = install("claude-code", destination=dest, force=True)
assert result.action == "overwritten"
assert result.previous_version == "1.0"
# TAMPERED content is gone; fresh rendered content is back.
assert "TAMPERED" not in dest.read_text(encoding="utf-8")
assert "Claude Code" in dest.read_text(encoding="utf-8")
def test_install_overwrites_older_version_without_force(tmp_path: Path):
"""An older version should be overwritten automatically — only
same or newer versions are protected."""
dest = tmp_path / "SKILL.md"
dest.write_text(
"---\nsmriti_skill_pack_version: 0.9\n---\n\nOLD CONTENT\n",
encoding="utf-8",
)
result = install("claude-code", destination=dest)
assert result.action == "overwritten"
assert result.previous_version == "0.9"
assert "OLD CONTENT" not in dest.read_text(encoding="utf-8")
def test_install_dry_run_still_reports_previous_version(tmp_path: Path):
dest = tmp_path / "SKILL.md"
dest.write_text(
"---\nsmriti_skill_pack_version: 0.5\n---\n\n...\n",
encoding="utf-8",
)
result = install("claude-code", destination=dest, dry_run=True)
assert result.action == "dry_run"
assert result.previous_version == "0.5"
# Dry run must not touch disk.
assert dest.read_text(encoding="utf-8").startswith("---")
assert "0.5" in dest.read_text(encoding="utf-8")
# ── list_targets ─────────────────────────────────────────────────────────────
def test_list_targets_returns_known_targets():
targets = list_targets()
keys = [t.key for t in targets]
assert "claude-code" in keys
assert "codex" in keys
assert all(isinstance(t, SkillTarget) for t in targets)
def test_list_targets_is_deterministic():
"""Two calls return the same ordering — important for `smriti skills
list` output stability across invocations."""
a = [t.key for t in list_targets()]
b = [t.key for t in list_targets()]
assert a == b