@punks/cli 1.0.1 → 1.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. package/README.md +47 -4
  2. package/dist/data/AGENTS.md +0 -6
  3. package/dist/data/catalog/hooks.ts +26 -0
  4. package/dist/data/catalog/lint.ts +11 -26
  5. package/dist/data/catalog/packs.ts +5 -3
  6. package/dist/data/catalog/skills.ts +9 -1
  7. package/dist/data/catalog/tools.ts +13 -1
  8. package/dist/data/scripts/sync-subagents.mjs +163 -120
  9. package/dist/data/subagents/manifest.mjs +148 -0
  10. package/dist/index.js +2589 -1944
  11. package/dist/skills/agnostic/backend/logging-best-practices/SKILL.md +127 -0
  12. package/dist/skills/agnostic/backend/logging-best-practices/rules/context.md +157 -0
  13. package/dist/skills/agnostic/backend/logging-best-practices/rules/pitfalls.md +118 -0
  14. package/dist/skills/agnostic/backend/logging-best-practices/rules/structure.md +193 -0
  15. package/dist/skills/agnostic/backend/logging-best-practices/rules/wide-events.md +113 -0
  16. package/dist/skills/agnostic/cli/dp-cli/SKILL.md +84 -0
  17. package/dist/skills/agnostic/cli/dp-cli/references/commands.md +33 -0
  18. package/dist/skills/agnostic/cli/dp-cli/references/post-command-flow.md +47 -0
  19. package/dist/skills/agnostic/debug/debug-agent/SKILL.md +184 -0
  20. package/dist/skills/agnostic/requirements/write-backlog/REFERENCE.md +1 -1
  21. package/dist/skills/languages/python/async-python-patterns/SKILL.md +735 -0
  22. package/dist/skills/languages/python/python-code-style/SKILL.md +360 -0
  23. package/dist/skills/languages/python/python-design-patterns/SKILL.md +433 -0
  24. package/dist/skills/languages/python/python-project-structure/SKILL.md +252 -0
  25. package/dist/skills/languages/python/python-testing-patterns/SKILL.md +622 -0
  26. package/dist/skills/languages/python/python-testing-patterns/references/advanced-patterns.md +411 -0
  27. package/dist/skills/languages/typescript/quality-types/SKILL.md +93 -0
  28. package/docs/README.md +14 -4
  29. package/docs/reference/dp-requirements.md +16 -1
  30. package/docs/runbooks/dp-cli-scaffolding.md +82 -10
  31. package/package.json +5 -2
@@ -0,0 +1,411 @@
1
+ # Python Testing Patterns — Advanced Reference
2
+
3
+ Advanced testing patterns including async code, monkeypatching, temporary files, conftest setup, property-based testing, database testing, CI/CD integration, and configuration.
4
+
5
+ ## Pattern 6: Testing Async Code
6
+
7
+ ```python
8
+ # test_async.py
9
+ import pytest
10
+ import asyncio
11
+
12
+ async def fetch_data(url: str) -> dict:
13
+ """Fetch data asynchronously."""
14
+ await asyncio.sleep(0.1)
15
+ return {"url": url, "data": "result"}
16
+
17
+
18
+ @pytest.mark.asyncio
19
+ async def test_fetch_data():
20
+ """Test async function."""
21
+ result = await fetch_data("https://api.example.com")
22
+ assert result["url"] == "https://api.example.com"
23
+ assert "data" in result
24
+
25
+
26
+ @pytest.mark.asyncio
27
+ async def test_concurrent_fetches():
28
+ """Test concurrent async operations."""
29
+ urls = ["url1", "url2", "url3"]
30
+ tasks = [fetch_data(url) for url in urls]
31
+ results = await asyncio.gather(*tasks)
32
+
33
+ assert len(results) == 3
34
+ assert all("data" in r for r in results)
35
+
36
+
37
+ @pytest.fixture
38
+ async def async_client():
39
+ """Async fixture."""
40
+ client = {"connected": True}
41
+ yield client
42
+ client["connected"] = False
43
+
44
+
45
+ @pytest.mark.asyncio
46
+ async def test_with_async_fixture(async_client):
47
+ """Test using async fixture."""
48
+ assert async_client["connected"] is True
49
+ ```
50
+
51
+ ## Pattern 7: Monkeypatch for Testing
52
+
53
+ ```python
54
+ # test_environment.py
55
+ import os
56
+ import pytest
57
+
58
+ def get_database_url() -> str:
59
+ """Get database URL from environment."""
60
+ return os.environ.get("DATABASE_URL", "sqlite:///:memory:")
61
+
62
+
63
+ def test_database_url_default():
64
+ """Test default database URL."""
65
+ # Will use actual environment variable if set
66
+ url = get_database_url()
67
+ assert url
68
+
69
+
70
+ def test_database_url_custom(monkeypatch):
71
+ """Test custom database URL with monkeypatch."""
72
+ monkeypatch.setenv("DATABASE_URL", "postgresql://localhost/test")
73
+ assert get_database_url() == "postgresql://localhost/test"
74
+
75
+
76
+ def test_database_url_not_set(monkeypatch):
77
+ """Test when env var is not set."""
78
+ monkeypatch.delenv("DATABASE_URL", raising=False)
79
+ assert get_database_url() == "sqlite:///:memory:"
80
+
81
+
82
+ class Config:
83
+ """Configuration class."""
84
+
85
+ def __init__(self):
86
+ self.api_key = "production-key"
87
+
88
+ def get_api_key(self):
89
+ return self.api_key
90
+
91
+
92
+ def test_monkeypatch_attribute(monkeypatch):
93
+ """Test monkeypatching object attributes."""
94
+ config = Config()
95
+ monkeypatch.setattr(config, "api_key", "test-key")
96
+ assert config.get_api_key() == "test-key"
97
+ ```
98
+
99
+ ## Pattern 8: Temporary Files and Directories
100
+
101
+ ```python
102
+ # test_file_operations.py
103
+ import pytest
104
+ from pathlib import Path
105
+
106
+ def save_data(filepath: Path, data: str):
107
+ """Save data to file."""
108
+ filepath.write_text(data)
109
+
110
+
111
+ def load_data(filepath: Path) -> str:
112
+ """Load data from file."""
113
+ return filepath.read_text()
114
+
115
+
116
+ def test_file_operations(tmp_path):
117
+ """Test file operations with temporary directory."""
118
+ # tmp_path is a pathlib.Path object
119
+ test_file = tmp_path / "test_data.txt"
120
+
121
+ # Save data
122
+ save_data(test_file, "Hello, World!")
123
+
124
+ # Verify file exists
125
+ assert test_file.exists()
126
+
127
+ # Load and verify data
128
+ data = load_data(test_file)
129
+ assert data == "Hello, World!"
130
+
131
+
132
+ def test_multiple_files(tmp_path):
133
+ """Test with multiple temporary files."""
134
+ files = {
135
+ "file1.txt": "Content 1",
136
+ "file2.txt": "Content 2",
137
+ "file3.txt": "Content 3"
138
+ }
139
+
140
+ for filename, content in files.items():
141
+ filepath = tmp_path / filename
142
+ save_data(filepath, content)
143
+
144
+ # Verify all files created
145
+ assert len(list(tmp_path.iterdir())) == 3
146
+
147
+ # Verify contents
148
+ for filename, expected_content in files.items():
149
+ filepath = tmp_path / filename
150
+ assert load_data(filepath) == expected_content
151
+ ```
152
+
153
+ ## Pattern 9: Custom Fixtures and Conftest
154
+
155
+ ```python
156
+ # conftest.py
157
+ """Shared fixtures for all tests."""
158
+ import pytest
159
+
160
+ @pytest.fixture(scope="session")
161
+ def database_url():
162
+ """Provide database URL for all tests."""
163
+ return "postgresql://localhost/test_db"
164
+
165
+
166
+ @pytest.fixture(autouse=True)
167
+ def reset_database(database_url):
168
+ """Auto-use fixture that runs before each test."""
169
+ # Setup: Clear database
170
+ print(f"Clearing database: {database_url}")
171
+ yield
172
+ # Teardown: Clean up
173
+ print("Test completed")
174
+
175
+
176
+ @pytest.fixture
177
+ def sample_user():
178
+ """Provide sample user data."""
179
+ return {
180
+ "id": 1,
181
+ "name": "Test User",
182
+ "email": "test@example.com"
183
+ }
184
+
185
+
186
+ @pytest.fixture
187
+ def sample_users():
188
+ """Provide list of sample users."""
189
+ return [
190
+ {"id": 1, "name": "User 1"},
191
+ {"id": 2, "name": "User 2"},
192
+ {"id": 3, "name": "User 3"},
193
+ ]
194
+
195
+
196
+ # Parametrized fixture
197
+ @pytest.fixture(params=["sqlite", "postgresql", "mysql"])
198
+ def db_backend(request):
199
+ """Fixture that runs tests with different database backends."""
200
+ return request.param
201
+
202
+
203
+ def test_with_db_backend(db_backend):
204
+ """This test will run 3 times with different backends."""
205
+ print(f"Testing with {db_backend}")
206
+ assert db_backend in ["sqlite", "postgresql", "mysql"]
207
+ ```
208
+
209
+ ## Pattern 10: Property-Based Testing
210
+
211
+ ```python
212
+ # test_properties.py
213
+ from hypothesis import given, strategies as st
214
+ import pytest
215
+
216
+ def reverse_string(s: str) -> str:
217
+ """Reverse a string."""
218
+ return s[::-1]
219
+
220
+
221
+ @given(st.text())
222
+ def test_reverse_twice_is_original(s):
223
+ """Property: reversing twice returns original."""
224
+ assert reverse_string(reverse_string(s)) == s
225
+
226
+
227
+ @given(st.text())
228
+ def test_reverse_length(s):
229
+ """Property: reversed string has same length."""
230
+ assert len(reverse_string(s)) == len(s)
231
+
232
+
233
+ @given(st.integers(), st.integers())
234
+ def test_addition_commutative(a, b):
235
+ """Property: addition is commutative."""
236
+ assert a + b == b + a
237
+
238
+
239
+ @given(st.lists(st.integers()))
240
+ def test_sorted_list_properties(lst):
241
+ """Property: sorted list is ordered."""
242
+ sorted_lst = sorted(lst)
243
+
244
+ # Same length
245
+ assert len(sorted_lst) == len(lst)
246
+
247
+ # All elements present
248
+ assert set(sorted_lst) == set(lst)
249
+
250
+ # Is ordered
251
+ for i in range(len(sorted_lst) - 1):
252
+ assert sorted_lst[i] <= sorted_lst[i + 1]
253
+ ```
254
+
255
+ ## Testing Database Code
256
+
257
+ ```python
258
+ # test_database_models.py
259
+ import pytest
260
+ from sqlalchemy import create_engine, Column, Integer, String
261
+ from sqlalchemy.ext.declarative import declarative_base
262
+ from sqlalchemy.orm import sessionmaker, Session
263
+
264
+ Base = declarative_base()
265
+
266
+
267
+ class User(Base):
268
+ """User model."""
269
+ __tablename__ = "users"
270
+
271
+ id = Column(Integer, primary_key=True)
272
+ name = Column(String(50))
273
+ email = Column(String(100), unique=True)
274
+
275
+
276
+ @pytest.fixture(scope="function")
277
+ def db_session() -> Session:
278
+ """Create in-memory database for testing."""
279
+ engine = create_engine("sqlite:///:memory:")
280
+ Base.metadata.create_all(engine)
281
+
282
+ SessionLocal = sessionmaker(bind=engine)
283
+ session = SessionLocal()
284
+
285
+ yield session
286
+
287
+ session.close()
288
+
289
+
290
+ def test_create_user(db_session):
291
+ """Test creating a user."""
292
+ user = User(name="Test User", email="test@example.com")
293
+ db_session.add(user)
294
+ db_session.commit()
295
+
296
+ assert user.id is not None
297
+ assert user.name == "Test User"
298
+
299
+
300
+ def test_query_user(db_session):
301
+ """Test querying users."""
302
+ user1 = User(name="User 1", email="user1@example.com")
303
+ user2 = User(name="User 2", email="user2@example.com")
304
+
305
+ db_session.add_all([user1, user2])
306
+ db_session.commit()
307
+
308
+ users = db_session.query(User).all()
309
+ assert len(users) == 2
310
+
311
+
312
+ def test_unique_email_constraint(db_session):
313
+ """Test unique email constraint."""
314
+ from sqlalchemy.exc import IntegrityError
315
+
316
+ user1 = User(name="User 1", email="same@example.com")
317
+ user2 = User(name="User 2", email="same@example.com")
318
+
319
+ db_session.add(user1)
320
+ db_session.commit()
321
+
322
+ db_session.add(user2)
323
+
324
+ with pytest.raises(IntegrityError):
325
+ db_session.commit()
326
+ ```
327
+
328
+ ## CI/CD Integration
329
+
330
+ ```yaml
331
+ # .github/workflows/test.yml
332
+ name: Tests
333
+
334
+ on: [push, pull_request]
335
+
336
+ jobs:
337
+ test:
338
+ runs-on: ubuntu-latest
339
+
340
+ strategy:
341
+ matrix:
342
+ python-version: ["3.9", "3.10", "3.11", "3.12"]
343
+
344
+ steps:
345
+ - uses: actions/checkout@v3
346
+
347
+ - name: Set up Python
348
+ uses: actions/setup-python@v4
349
+ with:
350
+ python-version: ${{ matrix.python-version }}
351
+
352
+ - name: Install dependencies
353
+ run: |
354
+ pip install -e ".[dev]"
355
+ pip install pytest pytest-cov
356
+
357
+ - name: Run tests
358
+ run: |
359
+ pytest --cov=myapp --cov-report=xml
360
+
361
+ - name: Upload coverage
362
+ uses: codecov/codecov-action@v3
363
+ with:
364
+ file: ./coverage.xml
365
+ ```
366
+
367
+ ## Configuration Files
368
+
369
+ ```ini
370
+ # pytest.ini
371
+ [pytest]
372
+ testpaths = tests
373
+ python_files = test_*.py
374
+ python_classes = Test*
375
+ python_functions = test_*
376
+ addopts =
377
+ -v
378
+ --strict-markers
379
+ --tb=short
380
+ --cov=myapp
381
+ --cov-report=term-missing
382
+ markers =
383
+ slow: marks tests as slow
384
+ integration: marks integration tests
385
+ unit: marks unit tests
386
+ e2e: marks end-to-end tests
387
+ ```
388
+
389
+ ```toml
390
+ # pyproject.toml
391
+ [tool.pytest.ini_options]
392
+ testpaths = ["tests"]
393
+ python_files = ["test_*.py"]
394
+ addopts = [
395
+ "-v",
396
+ "--cov=myapp",
397
+ "--cov-report=term-missing",
398
+ ]
399
+
400
+ [tool.coverage.run]
401
+ source = ["myapp"]
402
+ omit = ["*/tests/*", "*/migrations/*"]
403
+
404
+ [tool.coverage.report]
405
+ exclude_lines = [
406
+ "pragma: no cover",
407
+ "def __repr__",
408
+ "raise AssertionError",
409
+ "raise NotImplementedError",
410
+ ]
411
+ ```
@@ -0,0 +1,93 @@
1
+ ---
2
+ name: quality-types
3
+ description: Use when writing or reviewing TypeScript/full-stack code. Encodes principles for type safety (branded types, discriminated unions, end-to-end types), real tests over mocks, OpenTelemetry observability, and picking the right abstractions instead of premature ones.
4
+ ---
5
+
6
+ # Writing quality full-stack TypeScript
7
+
8
+ Apply these principles when writing or reviewing TypeScript code.
9
+
10
+ ## Make impossible states unrepresentable
11
+
12
+ Use the type system to make invalid states fail at compile time. Fewer reachable states = easier code to read and change.
13
+
14
+ ### Branded types
15
+
16
+ Brand primitives so they can't be mixed up. Validate once at the boundary; downstream code trusts the type.
17
+
18
+ ```ts
19
+ type PhoneNumber = string & { __brand: "PhoneNumber" };
20
+
21
+ function parsePhone(input: string): PhoneNumber {
22
+ if (!/^\+?\d{10,15}$/.test(input)) throw new Error(`Invalid: ${input}`);
23
+ return input as PhoneNumber;
24
+ }
25
+
26
+ function sendSMS(to: PhoneNumber, body: string) {
27
+ /* input is trusted */
28
+ }
29
+ ```
30
+
31
+ If the project already uses a library with native branded-type support (e.g. Effect), use their primitives instead of rolling your own.
32
+
33
+ ### Discriminated unions over flag bags
34
+
35
+ ```ts
36
+ // Don't — invalid combos representable
37
+ type State = { loading: boolean; user?: User; error?: string };
38
+
39
+ // Do — only valid states exist
40
+ type State =
41
+ | { status: "loading" }
42
+ | { status: "success"; user: User }
43
+ | { status: "error"; error: string };
44
+ ```
45
+
46
+ ## Let the types flow end-to-end
47
+
48
+ DB schema → server → client should share types without manual duplication. Use whatever end-to-end type tool the project already has (tRPC, oRPC, Elysia, TanStack Start). A `users.email` branded as `Email` should arrive on the client still branded.
49
+
50
+ Don't restate types you can derive. Reach for `Pick`, `Omit`, `Parameters`, `ReturnType`, `Awaited`, `typeof` etc. before writing a new interface. For function arguments, infer from the source instead of typing them by hand:
51
+
52
+ ```ts
53
+ // Don't — duplicate shape, drifts when the row changes
54
+ type UserSummary = { id: string; email: Email };
55
+ function renderUser(u: UserSummary) {
56
+ /* ... */
57
+ }
58
+
59
+ // Do — derive from the source of truth
60
+ type User = Awaited<ReturnType<typeof db.query.users.findFirst>>;
61
+ function renderUser(u: Pick<User, "id" | "email">) {
62
+ /* ... */
63
+ }
64
+ ```
65
+
66
+ ## Pass objects, not positional args
67
+
68
+ ```ts
69
+ // Don't — swap two args, still compiles
70
+ sendEmail("Welcome!", "Hi there");
71
+ // Do — order-independent, self-documenting
72
+ sendEmail({ to: "alice@x.com", body: "Hi there" });
73
+ ```
74
+
75
+ Skip on hot perf-critical paths; use elsewhere by default.
76
+
77
+ ## Standard Schema for shared validation
78
+
79
+ For libraries or code that doesn't want to pick a validator, accept `StandardSchemaV1<unknown, T>`.
80
+
81
+ ## Tests as real as possible
82
+
83
+ Don't mock things you can run. Spin up real services:
84
+
85
+ - LocalStack for AWS
86
+ - Miniflare for Cloudflare Workers
87
+ - Real Postgres/SQLite (e.g. `bun:sqlite`), not a mock DB
88
+
89
+ Mock only third-party services that have no test environment.
90
+
91
+ ## OpenTelemetry, not print logging
92
+
93
+ When adding observability, instrument with OTel spans. The setup cost pays back the first time a user sends a request ID and you can answer instead of guess.
package/docs/README.md CHANGED
@@ -5,16 +5,26 @@
5
5
 
6
6
  Implementation notes:
7
7
 
8
- - canonical scaffold metadata and shared assets live in `src/data/`
8
+ - canonical bundled scaffold metadata and shared assets live in `src/data/`
9
+ - runtime scaffold content resolves from a `Baseline`: latest remote stable by default, bundled fallback when offline or forced with `--baseline bundled`
9
10
  - pack-owned lint asset metadata lives in `src/data/catalog/lint.ts`
10
11
  - distributed skill assets live in `skills/`
11
12
  - runtime projection/writing logic lives in `src/scaffold/`
12
13
  - `punks update` refreshes scaffold-managed assets from `.devpunks/scaffold-manifest.json`
14
+ - CLI startup checks npm's `latest` dist-tag for `@punks/cli` and prints a self-update notice when the installed CLI is behind. Set `DP_NO_UPDATE_CHECK=1` to skip the check or `DP_UPDATE_TAG=next` to compare against another dist-tag.
15
+ - CLI startup also checks for the `dp-cli` skill through the `skills` CLI, installing it globally when absent and updating it when present. Set `DP_NO_SKILL_UPDATE_CHECK=1` to skip this best-effort pass.
16
+ - baseline releases use `baseline/stable/*` GitHub release tags, separate from npm executable tags such as `v1.0.1`
13
17
  - shared neutral hook and sync assets live in `src/data/hooks/` and `src/data/scripts/`
14
- - scaffolded required tools always include `portless` so generated guidance can standardize local dev origins and avoid worktree port collisions
18
+ - scaffolded required tools always include `portless` and `skills` so generated guidance can standardize local dev origins and keep skill entrypoints up to date
19
+ - `punks scaffold setup` checks the base required tools (`portless`, `skills`) before repo detection and checks selected-pack tools after pack confirmation.
20
+ - Oxlint specs/starter config are scaffolded only when scanned manifests already declare `oxlint`; the auto format/lint hook is scaffolded only when manifests declare `oxfmt`. Other lint/format stacks are intentionally left untouched for now.
21
+ - the default debug pack scaffolds the local `debug-agent` skill and installs/verifies the `debug-agent` CLI without running its agent-install wizard
15
22
  - scaffolded repos keep project-local skills in `.agents/skills/`; only `.claude/skills` is a compatibility symlink mirror
16
23
  - React scaffold surfaces include `async-react-patterns` alongside the existing React composition, structure, and Vercel guidance so agents avoid outdated manual async state patterns.
17
24
  - Next.js detection implies the React pack even when `react` / `react-dom` are not directly listed in scanned manifests.
18
25
  - Frontend surface detection scaffolds `frontend-domain-structure` with the frontend pack when React or Next.js is detected.
19
- - Backend surface detection scaffolds `backend-domain-structure` and `backend-recoverable-actions` when backend framework/data/auth packages are detected, or when workspace names/paths clearly identify API, backend, server, or service packages.
20
- - Non-surface agnostic skill groups are mandatory scaffold packs (`docs`, `planning`, `quality`, `research`, `requirements`, `subagents`); `frontend` and `backend` remain detection-driven.
26
+ - Backend surface detection scaffolds `backend-domain-structure`, `backend-recoverable-actions`, and `logging-best-practices` when backend framework/data/auth packages are detected, or when workspace names/paths clearly identify API, backend, server, or service packages. Workspace prompt specs apply backend packs only to backend-looking workspaces, not frontend workspaces that happen to share repo-level backend/data detections.
27
+ - Drizzle detection selects the `drizzle` pack for data-layer prompt/lint metadata, but does not imply Effect skills or Effect opensrc references unless `effect` / `@effect/*` is also detected.
28
+ - Scaffold no longer preselects concrete opensrc repositories. Post-scaffold agents must choose and clone the core detected libraries whose source behavior matters before authoring final prompts or plans.
29
+ - The default subagent manifest includes a read-only `code-review` template that uses `simplify` and `improve-codebase-architecture`; root prompt guidance routes review requests to findings-first review before broad refactor planning.
30
+ - Non-surface agnostic skill groups are mandatory scaffold packs (`debug`, `docs`, `planning`, `quality`, `research`, `requirements`, `subagents`); `frontend` and `backend` remain detection-driven.
@@ -67,7 +67,7 @@ That wiki tree is not the same thing as `docs/`. It owns specs, raw inputs, and
67
67
  It should:
68
68
 
69
69
  - detect repo facts, not ask the agent to invent them
70
- - resolve those facts to predefined DevPunks packs
70
+ - resolve those facts to predefined Devpunks packs
71
71
  - scaffold the shared AI setup
72
72
  - emit instructions/specs the next agent can use to generate repo-scoped prompts and subagent config
73
73
 
@@ -106,6 +106,8 @@ Initial technology mapping:
106
106
  - `turbo` -> `turborepo`
107
107
  - `effect`, `@effect/*` -> `effect`
108
108
 
109
+ `drizzle` is data-layer detection only. It must not imply Effect skills or Effect source references unless the `effect` pack is selected independently.
110
+
109
111
  ## Pack Contract
110
112
 
111
113
  Pack resolution happens at pack level only during `punks scaffold setup`.
@@ -147,6 +149,11 @@ Framework/data packs may layer on top of that surface:
147
149
  - `tanstack-query`
148
150
  `tanstack-query`
149
151
 
152
+ Backend-oriented agnostic skills should be grouped into a single detected backend surface pack:
153
+
154
+ - `backend`
155
+ `backend-domain-structure`, `backend-recoverable-actions`, `logging-best-practices`
156
+
150
157
  ## Prompt Contract
151
158
 
152
159
  Pre-boilerplate commands use fixed stdout operator prompts.
@@ -188,6 +195,10 @@ Current examples:
188
195
 
189
196
  The scaffold output should also record the resolved tool requirements in a machine-readable manifest.
190
197
 
198
+ `punks scaffold setup` should check base required tools before repo detection so missing `skills` or `portless` is surfaced immediately. Tools required only by selected packs can be checked after pack confirmation.
199
+
200
+ Scaffold output should not preselect concrete opensrc repositories. The generated post-scaffold instructions should require the next agent to identify the core detected libraries whose source behavior matters, ask the user when that set is ambiguous, and run `opensrc path <package>` or `opensrc path owner/repo` for only that focused set.
201
+
191
202
  ## Lint Scaffold Contract
192
203
 
193
204
  The shipped starter/root lint baseline should exclude generated and non-source surfaces by default, including:
@@ -196,6 +207,10 @@ The shipped starter/root lint baseline should exclude generated and non-source s
196
207
  - all dot-directories
197
208
  - generated agent/harness folders unless a target repo explicitly opts them back into lint scope
198
209
 
210
+ When scaffold guidance leads an agent to adopt Oxlint or Oxfmt, it must tell the agent to replace existing lint/format entrypoints deliberately: package scripts, task pipelines, CI, editor/docs references, and agent hooks should agree on the new tools.
211
+
212
+ Until additional lint/format stacks are supported, repo-aware setup should emit Oxlint specs/starter config only when `oxlint` is declared in scanned package manifests and emit the Oxfmt/Oxlint format hook only when `oxfmt` is declared. Repos without those packages should get no lint/format scaffold surfaces.
213
+
199
214
  ## Dedicated CLI Repo Contract
200
215
 
201
216
  The standalone private `wearedevpunks/cli` repo remains the source of truth for: