Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .claude/CLAUDE.md
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
See @../.cursor/rules/base.mdc for information on your desired behavior.
See @../.cursor/rules/best-practices.mdc for our best practices (snapshot we own and evolve).
See @../.cursor/rules/conventions-index.mdc for the index of on-demand convention rules (read before writing or reviewing code).
See @../.cursor/rules/learnings-index.mdc for this repo's learnings index (read the pointed skill before working in an entry's territory).
54 changes: 54 additions & 0 deletions .cursor/rules/learnings-index.mdc
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
---
description: Always-on index of this repo's learnings — lessons distilled from reviews, builds, and scans. Read the pointed skill before working in an entry's territory.
alwaysApply: true
---

# Learnings index

One line per learning: `[domain/slug]`, the trigger sentence, the skill
pointer. The line buys awareness; the pointed skill carries the rule, a
wrong/right pair, and the origin — read it before working in that entry's
territory. Detail skills live in `.cursor/skills/learnings/<slug>/`
(architecture digests in `.cursor/skills/architecture/<topic>/`); gem and
org skills are pointed at wherever their channel installs them.

Capture and curation go through the capture-learning skill (IDE sessions)
or ai-flow's `/learn` (GitHub comments) — both land as draft PRs; human
merge is the gate. Soft cap ~50 entries: at the cap, an addition must
propose a retirement, a consolidation, or a glob-scoped sub-index split.

## testing

- [testing/rspock-bare-assertions] In rspock `transform!`-ed test classes,
bare `Then`/`Expect` statements are assertions; outside a transformed
class they silently assert nothing.
→ .agents/skills/gem-rspock--rspock/ (ships in the rspock gem; linked by
`dev up` / `dev install-deps`)

## process

- [process/ai-flow-gitlink-exclusion] Commit sweeps (`git add -A`) must
exclude nested-checkout gitlinks such as `.ai-flow`.
→ .cursor/skills/learnings/ai-flow-gitlink-exclusion/

## architecture

- [architecture/command-dispatch] Global builtins (cd, plan, cred,
knowledge) dispatch before the dev.yml gate; project commands are
yaml-declared and run through Runner.
→ .cursor/skills/architecture/command-dispatch/
- [architecture/module-map] What owns what: src/dev is the typed CLI core;
each `lib/dev/<module>` is one feature whose Accessor is its only CLI
surface. → .cursor/skills/architecture/module-map/
- [architecture/hook-points] `dev up` / `install-deps` / `dev plan` double
as the idempotent hygiene hooks (skill links, knowledge sync): never
raise, never block on the network.
→ .cursor/skills/architecture/hook-points/

## org tier

Org-wide invariants and knowledge live in the configured org knowledge
repo (`knowledge_repo:`), not here: dev renders the always-on slice into
the generated `.cursor/rules/org-invariants.mdc` and links the on-demand
corpus into `~/.cursor/skills/`. A lesson about how we build software —
not about this repo — belongs there.
35 changes: 35 additions & 0 deletions .cursor/skills/architecture/command-dispatch/SKILL.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
---
name: command-dispatch
description: >-
MUST be used when adding a dev command or changing how bin/dev routes
argv — global builtins vs project (dev.yml) commands.
---

# dev command dispatch: two classes of command

`bin/dev` (sh shim → Ruby) puts `src/` and `lib/` on the load path, then
routes argv through two layers:

1. **Global builtins** — `Dev::GlobalDispatch`
(`src/dev/global_dispatch.rb`) runs first, before any dev.yml lookup,
so `cd`, `plan`, `cred`, and `knowledge` work from any directory. Each
owns host- or workspace-global state, never project config.
2. **Project commands** — everything else builds `Dev::Runner`
(`src/dev/runner.rb`), which requires a dev.yml in the cwd's ancestry
(`DevYamlNotFoundError` at the CLI boundary) and runs the
yaml-declared command, plus project builtins like `up` /
`install-deps`.

The seams:

- A new global command joins `GlobalDispatch::GLOBAL_COMMANDS` and gets a
feature module under `lib/dev/<name>/` whose `Accessor` is its only CLI
surface (usage, arg parsing, clean failures) — see `Cd::Accessor`,
`Plan::Accessor`, `Knowledge::Accessor`.
- Project commands are declared in each repo's dev.yml, never hardcoded
in dev's core.
- Workspace-global commands resolve their root as nearest dev.yml, else
nearest `.git`, else cwd (`GlobalDispatch#workspace_root`).

origin: seeded by the dev#58 architecture pass
date: 2026-07-25
30 changes: 30 additions & 0 deletions .cursor/skills/architecture/hook-points/SKILL.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
---
name: hook-points
description: >-
MUST be used when adding side-work (links, syncs, renders) to dev up,
dev install-deps, or dev plan — the rules hygiene rides by.
---

# dev hook points: hygiene rides, never blocks

`dev up` / `dev install-deps` (`Runner#install_locked_deps`) and every
`dev plan` invocation (`Plan::Accessor#run`) double as the refresh points
for agent-facing hygiene: shipped-skill links, gem-skill links, the org
knowledge TTL fetch, and the org-invariants render. There is no separate
setup step by design — riding existing commands is what keeps the
artifacts fresh without asking anything of the user.

Anything added to a hook point must obey both rules:

- **Hygiene must not block correctness.** The hook paths never raise —
failures warn on stderr and the carrying command proceeds (see
`GemSkillLinker#link_all`, `Knowledge::Synchronizer#sync`,
`SkillInstaller#install`).
- **No network on the hot path.** Remote refreshes are TTL-gated and
async (`Knowledge::Cache#refresh_async`); the synchronous work is
idempotent, content-compared, millisecond-scale symlink and render
checks. Blocking network belongs only behind an explicit command
(`dev knowledge sync`).

origin: seeded by the dev#58 architecture pass
date: 2026-07-25
36 changes: 36 additions & 0 deletions .cursor/skills/architecture/module-map/SKILL.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
---
name: module-map
description: >-
MUST be used when deciding where new dev code lives or which module owns
a concern — the ownership map of src/ and lib/.
---

# dev module map

- **`src/dev/`** — the typed (Sorbet) CLI core: Runner, command
parser/registry, dev.yml config parsing, CLI UI, GlobalDispatch. Owns
dispatch and execution, no feature logic.
- **`lib/dev/cd/`** — checkout jumping: RepoDiscovery walks the workspace
root, Matcher ranks, ShellHook owns the RC function (a child process
cannot cd its parent shell).
- **`lib/dev/plan/`** — Cursor plans ⇄ GitHub issues sync (the issue is
canonical; a stored merge base guards against clobbering remote edits).
- **`lib/dev/deps/`** — dependency management. Layering is canonical in
`.cursor/rules/separation-of-concerns.mdc`: Repository resolves,
Integration installs, Lockfile serializes, the orchestrator
coordinates — one class, one layer.
- **`lib/dev/knowledge/`** — org knowledge distribution: Cache (TTL git
clone), Synchronizer (orchestration), InvariantsRenderer (the generated
org-invariants.mdc).
- **`lib/dev/skill_installer.rb`** — the one symlink mechanism behind all
three skill channels (shipped, org, gem); the gem channel's lockfile
scan is `lib/dev/deps/gem_skill_linker.rb`.
- **`lib/dev/credentials.rb`** (+ `credential_accessor.rb`) — XDG-scoped
credential storage behind `dev cred`.
- **`lib/shadowenv_*.rb`** — per-toolchain env provisioning written into
`.shadowenv.d/` by project setup.
- **`share/cursor-skills/`** — skills dev ships user-globally (ai-flow,
capture-learning).

origin: seeded by the dev#58 architecture pass
date: 2026-07-25
33 changes: 33 additions & 0 deletions .cursor/skills/learnings/ai-flow-gitlink-exclusion/SKILL.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
---
name: ai-flow-gitlink-exclusion
description: >-
MUST be used when committing swept changes (git add -A / git add .) in a
worktree that may contain nested checkouts, e.g. an .ai-flow directory.
---

# Exclude .ai-flow gitlinks from commit sweeps

A nested checkout inside the worktree (such as an `.ai-flow` runner
checkout) is a gitlink to git: a sweep stages it as a bare mode-160000
entry — a broken submodule pointer with no `.gitmodules` — and the commit
ships it silently. Stage explicitly, or exclude known nested-checkout
directories from the sweep.

Wrong:

```sh
git add -A && git commit -m "iterate on PR feedback"
# the commit now carries: .ai-flow (mode 160000)
```

Right:

```sh
git add -A -- ':!.ai-flow'
# and check `git status --porcelain` — never stage an unexpected
# 160000 entry
```

learned-from: d3mlabs/dev#35 (stray `.ai-flow` gitlink committed by the
iteration job; removed by hand in 0631a2b)
date: 2026-07-25
5 changes: 3 additions & 2 deletions Gemfile
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,8 +5,9 @@ source "https://rubygems.org"
# Runtime deps (cli-ui, sorbet-runtime) come from the gemspec.
gemspec

# RSpock (from RubyGems) for test_helper and rspock-style tests
gem "rspock", "~> 2.3"
# RSpock (from RubyGems) for test_helper and rspock-style tests; 3.0 also
# ships the rspock agent skill the learnings index points at.
gem "rspock", "~> 3.0"

# Test (dev repo's own tests)
gem "minitest"
Expand Down
30 changes: 14 additions & 16 deletions Gemfile.lock
Original file line numberDiff line numberDiff line change
Expand Up@@ -10,10 +10,10 @@ GEM
specs:
ansi (1.5.0)
ast (2.4.3)
ast_transform (2.1.4)
parser (>= 3.0)
ast_transform (3.0.0)
parser (>= 3.3)
prism (>= 1.5)
unparser (>= 0.6)
unparser (>= 0.8)
benchmark (0.5.0)
builder (3.3.0)
byebug (13.0.0)
Expand All@@ -35,11 +35,11 @@ GEM
builder
minitest (>= 5.0)
ruby-progressbar
mocha (3.0.2)
mocha (3.1.0)
ruby2_keywords (>= 0.0.5)
netrc (0.11.0)
parallel (1.27.0)
parser (3.3.10.2)
parser (3.3.12.0)
ast (~> 2.4.1)
racc
prism (1.9.0)
Expand All@@ -65,12 +65,10 @@ GEM
io-console (~> 0.5)
require-hooks (0.2.3)
rexml (3.4.4)
rspock (2.5.0)
ast_transform (~> 2.0)
rspock (3.0.0)
ast_transform (~> 3.0)
minitest (~> 5.0)
mocha (>= 1.0)
parser (>= 3.0)
unparser (>= 0.6)
rubocop (1.88.2)
json (~> 2.3)
language_server-protocol (~> 3.17.0.2)
Expand DownExpand Up@@ -129,7 +127,7 @@ GEM
unicode-display_width (3.2.0)
unicode-emoji (~> 4.1)
unicode-emoji (4.2.0)
unparser (0.8.2)
unparser (0.9.0)
diff-lcs (>= 1.6, < 3)
parser (>= 3.3.0)
prism (>= 1.5.1)
Expand All@@ -151,7 +149,7 @@ DEPENDENCIES
pry-byebug (~> 3.11)
rake
rbs (~> 4.0.0.dev.5)
rspock (~> 2.3)
rspock (~> 3.0)
rubocop-shopify (~> 3.0)
simplecov (~> 0.22)
sorbet
Expand All@@ -160,7 +158,7 @@ DEPENDENCIES
CHECKSUMS
ansi (1.5.0) sha256=5408253274e33d9d27d4a98c46d2998266fd51cba58a7eb9d08f50e57ed23592
ast (2.4.3) sha256=954615157c1d6a382bc27d690d973195e79db7f55e9765ac7c481c60bdb4d383
ast_transform (2.1.4) sha256=3414a65911cf583a32af3ebf643e29033e135a817c1286548a7452cf49e71ac8
ast_transform (3.0.0) sha256=b437f0ac9fbb107312a6e8fab15f37c29e366c32c95def964de4dc49158fed0d
benchmark (0.5.0) sha256=465df122341aedcb81a2a24b4d3bd19b6c67c1530713fd533f3ff034e419236c
builder (3.3.0) sha256=497918d2f9dca528fdca4b88d84e4ef4387256d984b8154e9d5d3fe5a9c8835f
byebug (13.0.0) sha256=d2263efe751941ca520fa29744b71972d39cbc41839496706f5d9b22e92ae05d
Expand All@@ -178,10 +176,10 @@ CHECKSUMS
method_source (1.1.0) sha256=181301c9c45b731b4769bc81e8860e72f9161ad7d66dd99103c9ab84f560f5c5
minitest (5.27.0) sha256=2d3b17f8a36fe7801c1adcffdbc38233b938eb0b4966e97a6739055a45fa77d5
minitest-reporters (1.7.1) sha256=5060413a0c95b8c32fe73e0606f3631c173a884d7900e50013e15094eb50562c
mocha (3.0.2) sha256=3d8029531d8b71bff5ae07040758f77fdf578174189ffda1bef7749a59e92df8
mocha (3.1.0) sha256=75f42d69ebfb1f10b32489dff8f8431d37a418120ecdfc07afe3bc183d4e1d56
netrc (0.11.0) sha256=de1ce33da8c99ab1d97871726cba75151113f117146becbe45aa85cb3dabee3f
parallel (1.27.0) sha256=4ac151e1806b755fb4e2dc2332cbf0e54f2e24ba821ff2d3dcf86bf6dc4ae130
parser (3.3.10.2) sha256=6f60c84aa4bdcedb6d1a2434b738fe8a8136807b6adc8f7f53b97da9bc4e9357
parser (3.3.12.0) sha256=21a6d7f755d5a24dfbdc6e6b772e4e879a52e7631a88bc5a3a134606052c9828
prism (1.9.0) sha256=7b530c6a9f92c24300014919c9dcbc055bf4cdf51ec30aed099b06cd6674ef85
pry (0.16.0) sha256=d76c69065698ed1f85e717bd33d7942c38a50868f6b0673c636192b3d1b6054e
pry-byebug (3.12.0) sha256=594e094ae8a8390a7ad4c7b36ae36e13304ed02664c67417d108dc5f7213d1b7
Expand All@@ -194,7 +192,7 @@ CHECKSUMS
reline (0.6.3) sha256=1198b04973565b36ec0f11542ab3f5cfeeec34823f4e54cebde90968092b1835
require-hooks (0.2.3) sha256=224be5b4be0fd2a47cb73286c500da366704a54ec195b6627366380c950efac8
rexml (3.4.4) sha256=19e0a2c3425dfbf2d4fc1189747bdb2f849b6c5e74180401b15734bc97b5d142
rspock (2.5.0) sha256=5bac4d22eab6f10a04523d9d5368f451b4c261c51cab092d8a8e868318f9b348
rspock (3.0.0) sha256=279095618eb3f154a6770829a1b8537d65bb22a41555f5d89a727aeb19d0bb1e
rubocop (1.88.2) sha256=8def251c90cd955feb4daa3edc0ab56893250c4ce90ef81e6c80c03f9a939bbf
rubocop-ast (1.50.0) sha256=b9ca88300da0803ee222ad20cdb30494c0a784eed06fdc35d254b06d662788db
rubocop-shopify (3.0.1) sha256=4adffa6313294bd9da2b0896ae44c5eb8e419336b2413de20c38b7691a7e6774
Expand All@@ -215,7 +213,7 @@ CHECKSUMS
tsort (0.2.0) sha256=9650a793f6859a43b6641671278f79cfead60ac714148aabe4e3f0060480089f
unicode-display_width (3.2.0) sha256=0cdd96b5681a5949cdbc2c55e7b420facae74c4aaf9a9815eee1087cb1853c42
unicode-emoji (4.2.0) sha256=519e69150f75652e40bf736106cfbc8f0f73aa3fb6a65afe62fefa7f80b0f80f
unparser (0.8.2) sha256=654af48a6cfa25baf145563e01f40c60ca7d5c7520ff506912d0bb2f1240fd00
unparser (0.9.0) sha256=4331f174a73a23b69250b13d47da3794ed1449711ee0f9ed8947dc020ba76067
yard (0.9.38) sha256=721fb82afb10532aa49860655f6cc2eaa7130889df291b052e1e6b268283010f
yard-sorbet (0.9.0) sha256=03d1aa461b9e9c82b886919a13aa3e09fcf4d1852239d2967ed97e92723ffe21

Expand Down
4 changes: 4 additions & 0 deletions README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -373,6 +373,10 @@ dev distributes agent-facing skills (Cursor-style `SKILL.md` directories) over t
- **Gem-shipped skills.** A gem's skill is part of what installing that dependency means, so `dev up` / `dev install-deps` finish by scanning the resolved (lockfile-matched) gem set for `skills/*/SKILL.md` and linking each project-scoped as `.agents/skills/gem-<gem>--<skill>` (gitignored; an agent-neutral dir, so the mechanism isn't Cursor-locked). Links for gems that leave the lock are pruned on the next install — a skill-set change rides the same staleness story as any dependency change.
- **Org knowledge** (opt-in). With `knowledge_repo: <owner>/<repo>` in `~/.config/dev/config.yml` (or `DEV_KNOWLEDGE_REPO`), dev keeps a machine-local cache of the org knowledge repo under `~/.local/share/dev/knowledge`, refreshed on a TTL (`knowledge_ttl:` seconds, default 900). The fetch is async and rides the user's `gh` auth; no dev command ever blocks on the network for knowledge — offline serves the cache. From the cache, dev links the repo's `skills/*` user-globally into `~/.cursor/skills/` and renders the index's `## Invariants (always-on)` section into the current project as `.cursor/rules/org-invariants.mdc` — generated, content-compared, and never committed (a participating repo's only footprint is one `.gitignore` line), so drift from the canonical repo is structurally impossible. Machines without the setting simply have no org sync: dev is public and ships only the mechanism, never the content. `dev knowledge sync` forces a refresh; `dev knowledge status` reports cache age.

### Repo learnings

Alongside the distributed channels, a repo can carry **committed learnings** — lessons distilled from review feedback, builds, and scans — as an always-on index (`.cursor/rules/learnings-index.mdc`, one `[domain/slug]` line + trigger sentence per learning) pointing at on-demand detail skills (`.cursor/skills/learnings/<slug>/SKILL.md`; architecture digests under `.cursor/skills/architecture/<topic>/`). Committed files need no distribution step: every checkout — IDE, runner, worktree — has them by construction. The index defines its own format in its preamble; this repo's copy is the reference. Capture goes through the `capture-learning` skill (shipped in `share/cursor-skills/`, so it is available in every IDE session) or ai-flow's `/learn` command on GitHub surfaces — both stage learnings as draft PRs, and human merge is the curation gate.

## Build container & caching model

For repos that declare a `build.container`, dev builds and runs commands inside a content-addressed Docker image, backed by host-side caches it owns end to end. The guiding principle throughout is **content-addressing**: an artifact's identity is a hash of its inputs, so distinct versions coexist instead of overwriting, and identical inputs are never rebuilt.
Expand Down
Loading
Loading