claude-dev-env 2.14.0 → 2.15.0
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.
- package/AGENTS.md +106 -32
- package/agents/AGENTS.md +0 -5
- package/agents/test_agent_frontmatter.py +7 -7
- package/bin/AGENTS.md +6 -4
- package/bin/install-constants.mjs +51 -0
- package/bin/install.codex-rules.test.mjs +173 -0
- package/bin/install.cursor-rules.test.mjs +103 -0
- package/bin/install.mjs +130 -19
- package/bin/install.profile-root.test.mjs +8 -0
- package/bin/install.prune.test.mjs +2 -1
- package/bin/install.test.mjs +3 -3
- package/bin/install.transaction.test.mjs +1 -0
- package/bin/install.uninstall-transaction.test.mjs +1 -0
- package/bin/resolve-install-root.mjs +43 -10
- package/codex-rules/claude-dev-env.rules +12 -0
- package/commands/AGENTS.md +0 -10
- package/hooks/blocking/test_claude_md_orphan_file_blocker.py +1 -1
- package/hooks/diagnostic/AGENTS.md +32 -0
- package/hooks/diagnostic/hook_log_init.py +2 -2
- package/output-styles/AGENTS.md +1 -1
- package/package.json +2 -1
- package/scripts/sync_to_cursor/AGENTS.md +3 -3
- package/scripts/sync_to_cursor/canonical_docs.py +11 -11
- package/scripts/sync_to_cursor/config/__init__.py +8 -0
- package/scripts/sync_to_cursor/engine.py +26 -1
- package/scripts/sync_to_cursor/rules.py +76 -5
- package/scripts/test_active_capability_references.py +2 -2
- package/scripts/tests/AGENTS.md +2 -0
- package/scripts/tests/test_engine.py +102 -0
- package/scripts/tests/test_rules.py +79 -0
- package/skills/anthropic-plan/AGENTS.md +1 -1
- package/skills/anthropic-plan/SKILL.md +1 -1
- package/skills/anthropic-plan/test_skill_contract.py +8 -6
- package/skills/prototype/SKILL.md +1 -2
- package/skills/prototype/reference/promotion-tasks.md +1 -1
- package/skills/prototype/workflows/promotion.md +1 -1
- package/agents/caveman.md +0 -73
- package/agents/clasp-deployment-orchestrator.md +0 -608
- package/agents/code-advisor.md +0 -23
- package/agents/deep-research.md +0 -152
- package/agents/docs-agent.md +0 -85
- package/commands/commit.md +0 -28
- package/commands/docupdate.md +0 -322
- package/commands/hook-log-extract.md +0 -70
- package/commands/hook-log-init.md +0 -76
- package/commands/implement.md +0 -102
- package/commands/plan.md +0 -14
- package/commands/pr-comments.md +0 -47
- package/commands/review-plan.md +0 -5
- package/commands/right-size.md +0 -15
- package/commands/sum.md +0 -30
- package/scripts/sync_to_cursor/config.py +0 -5
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import shutil
|
|
4
4
|
from pathlib import Path
|
|
5
5
|
|
|
6
|
-
from sync_to_cursor.config import
|
|
6
|
+
from sync_to_cursor.config import ALL_CANONICAL_DOC_FILES
|
|
7
7
|
from sync_to_cursor.hashing import sha256_bytes
|
|
8
8
|
|
|
9
9
|
|
|
@@ -17,19 +17,19 @@ def sync_canonical_docs(
|
|
|
17
17
|
if not dry_run:
|
|
18
18
|
docs_out.mkdir(parents=True, exist_ok=True)
|
|
19
19
|
new_docs: dict = {}
|
|
20
|
-
for
|
|
21
|
-
src = claude / "docs" /
|
|
22
|
-
dst = docs_out /
|
|
20
|
+
for each_name in ALL_CANONICAL_DOC_FILES:
|
|
21
|
+
src = claude / "docs" / each_name
|
|
22
|
+
dst = docs_out / each_name
|
|
23
23
|
if not src.is_file():
|
|
24
24
|
if dst.is_file():
|
|
25
25
|
if not dry_run:
|
|
26
26
|
dst.unlink()
|
|
27
27
|
if not quiet:
|
|
28
|
-
print(f"WARN docs/{
|
|
28
|
+
print(f"WARN docs/{each_name} (source removed — deleted stale copy at {dst})")
|
|
29
29
|
elif not quiet:
|
|
30
|
-
print(f"WARN docs/{
|
|
30
|
+
print(f"WARN docs/{each_name} (missing source: {src})")
|
|
31
31
|
continue
|
|
32
|
-
key = f"docs/{
|
|
32
|
+
key = f"docs/{each_name}"
|
|
33
33
|
src_hash = sha256_bytes(src.read_bytes())
|
|
34
34
|
if dry_run:
|
|
35
35
|
if dst.is_file():
|
|
@@ -44,10 +44,10 @@ def sync_canonical_docs(
|
|
|
44
44
|
|
|
45
45
|
|
|
46
46
|
def check_canonical_docs(claude: Path, cursor: Path, docs_entries: dict) -> bool:
|
|
47
|
-
for
|
|
48
|
-
key = f"docs/{
|
|
49
|
-
src = claude / "docs" /
|
|
50
|
-
dst = cursor / "docs" /
|
|
47
|
+
for each_name in ALL_CANONICAL_DOC_FILES:
|
|
48
|
+
key = f"docs/{each_name}"
|
|
49
|
+
src = claude / "docs" / each_name
|
|
50
|
+
dst = cursor / "docs" / each_name
|
|
51
51
|
if not src.is_file():
|
|
52
52
|
if key in docs_entries:
|
|
53
53
|
return False
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"""Shared configuration for the sync-to-cursor package."""
|
|
2
|
+
|
|
3
|
+
GENERATOR_VERSION: str = "1.3.0"
|
|
4
|
+
ALL_CANONICAL_DOC_FILES: tuple[str, ...] = ("CODE_RULES.md", "TEST_QUALITY.md")
|
|
5
|
+
MAX_RULE_BODY_LINES: int = 50
|
|
6
|
+
ALL_SKIPPED_RULE_FILE_NAMES: frozenset[str] = frozenset({"CLAUDE.md", "AGENTS.md"})
|
|
7
|
+
MARKDOWN_SUFFIX: str = ".md"
|
|
8
|
+
CLAUDE_RULES_DIRECTORY_NAME: str = "rules"
|
|
@@ -140,15 +140,40 @@ def _sync_rules(
|
|
|
140
140
|
return summary, new_entries
|
|
141
141
|
|
|
142
142
|
|
|
143
|
+
def _layout_from_args(args: argparse.Namespace) -> tuple[Path, Path, Path, Path]:
|
|
144
|
+
"""Resolve Claude and Cursor roots from flags or the default layout.
|
|
145
|
+
|
|
146
|
+
Args:
|
|
147
|
+
args: Parsed CLI arguments.
|
|
148
|
+
|
|
149
|
+
Returns:
|
|
150
|
+
Claude root, Cursor root, rules output directory, and manifest path.
|
|
151
|
+
|
|
152
|
+
Raises:
|
|
153
|
+
SystemExit: When only one of `--claude-root` / `--cursor-root` is set.
|
|
154
|
+
"""
|
|
155
|
+
claude_root = args.claude_root
|
|
156
|
+
cursor_root = args.cursor_root
|
|
157
|
+
if (claude_root is None) != (cursor_root is None):
|
|
158
|
+
raise SystemExit("both --claude-root and --cursor-root are required")
|
|
159
|
+
if claude_root is None:
|
|
160
|
+
return llm_layout_paths()
|
|
161
|
+
claude = Path(claude_root).expanduser().resolve()
|
|
162
|
+
cursor = Path(cursor_root).expanduser().resolve()
|
|
163
|
+
return claude, cursor, cursor / "rules", cursor / ".sync-manifest.json"
|
|
164
|
+
|
|
165
|
+
|
|
143
166
|
def run(argv: list[str] | None = None) -> int:
|
|
144
167
|
argument_parser = argparse.ArgumentParser(description="Sync Claude rules to Cursor .mdc files")
|
|
145
168
|
argument_parser.add_argument("--force", action="store_true", help="Regenerate all outputs")
|
|
146
169
|
argument_parser.add_argument("--dry-run", action="store_true", help="Print actions only")
|
|
147
170
|
argument_parser.add_argument("--check", action="store_true", help="Exit 1 if anything stale")
|
|
148
171
|
argument_parser.add_argument("--quiet", action="store_true", help="Minimal output when up to date")
|
|
172
|
+
argument_parser.add_argument("--claude-root", help="Claude layout root holding rules/ and docs/")
|
|
173
|
+
argument_parser.add_argument("--cursor-root", help="Cursor layout root receiving rules/ and docs/")
|
|
149
174
|
args = argument_parser.parse_args(argv)
|
|
150
175
|
|
|
151
|
-
claude, cursor, out_dir, manifest_path =
|
|
176
|
+
claude, cursor, out_dir, manifest_path = _layout_from_args(args)
|
|
152
177
|
mappings = build_mappings(claude)
|
|
153
178
|
old_manifest = _load_manifest(manifest_path)
|
|
154
179
|
entries_meta: dict = old_manifest.get("entries", {})
|
|
@@ -7,7 +7,12 @@ from dataclasses import dataclass
|
|
|
7
7
|
from pathlib import Path
|
|
8
8
|
from typing import Literal
|
|
9
9
|
|
|
10
|
-
from sync_to_cursor.config import
|
|
10
|
+
from sync_to_cursor.config import (
|
|
11
|
+
ALL_SKIPPED_RULE_FILE_NAMES,
|
|
12
|
+
CLAUDE_RULES_DIRECTORY_NAME,
|
|
13
|
+
MARKDOWN_SUFFIX,
|
|
14
|
+
MAX_RULE_BODY_LINES,
|
|
15
|
+
)
|
|
11
16
|
|
|
12
17
|
|
|
13
18
|
def _parse_h2_sections(markdown: str) -> dict[str, str]:
|
|
@@ -412,6 +417,61 @@ def _path_scoped_mappings(rules_directory: Path, docs_directory: Path) -> tuple[
|
|
|
412
417
|
)
|
|
413
418
|
|
|
414
419
|
|
|
420
|
+
def _description_from_rule_file(rule_file: Path, fallback_key: str) -> str:
|
|
421
|
+
"""Return the first markdown heading, or the rule key with spaces.
|
|
422
|
+
|
|
423
|
+
Args:
|
|
424
|
+
rule_file: Claude rule markdown to read.
|
|
425
|
+
fallback_key: Stem used when the file has no heading.
|
|
426
|
+
|
|
427
|
+
Returns:
|
|
428
|
+
A one-line Cursor `description` value.
|
|
429
|
+
"""
|
|
430
|
+
for each_line in rule_file.read_text(encoding="utf-8").splitlines():
|
|
431
|
+
stripped = each_line.strip()
|
|
432
|
+
if stripped.startswith("# "):
|
|
433
|
+
return stripped[2:].strip()
|
|
434
|
+
return fallback_key.replace("-", " ")
|
|
435
|
+
|
|
436
|
+
|
|
437
|
+
def _discovered_mappings(
|
|
438
|
+
rules_directory: Path, all_covered_source_names: set[str]
|
|
439
|
+
) -> tuple[RuleMapping, ...]:
|
|
440
|
+
"""Map remaining `*.md` rule files to `<stem>.mdc` Cursor rules.
|
|
441
|
+
|
|
442
|
+
Args:
|
|
443
|
+
rules_directory: Claude `rules/` directory.
|
|
444
|
+
all_covered_source_names: Rule filenames already claimed by curated mappings.
|
|
445
|
+
|
|
446
|
+
Returns:
|
|
447
|
+
One mapping per remaining markdown rule, sorted by filename.
|
|
448
|
+
"""
|
|
449
|
+
if not rules_directory.is_dir():
|
|
450
|
+
return ()
|
|
451
|
+
all_discovered: list[RuleMapping] = []
|
|
452
|
+
for each_rule_file in sorted(rules_directory.glob("*.md")):
|
|
453
|
+
if each_rule_file.name in ALL_SKIPPED_RULE_FILE_NAMES:
|
|
454
|
+
continue
|
|
455
|
+
if each_rule_file.name in all_covered_source_names:
|
|
456
|
+
continue
|
|
457
|
+
key = each_rule_file.stem
|
|
458
|
+
paths_glob = _read_paths_glob(each_rule_file)
|
|
459
|
+
has_paths = paths_glob is not None
|
|
460
|
+
all_discovered.append(
|
|
461
|
+
RuleMapping(
|
|
462
|
+
key,
|
|
463
|
+
(each_rule_file,),
|
|
464
|
+
f"{key}.mdc",
|
|
465
|
+
not has_paths,
|
|
466
|
+
paths_glob,
|
|
467
|
+
_description_from_rule_file(each_rule_file, key),
|
|
468
|
+
"verbatim",
|
|
469
|
+
strip_leading_frontmatter=has_paths,
|
|
470
|
+
)
|
|
471
|
+
)
|
|
472
|
+
return tuple(all_discovered)
|
|
473
|
+
|
|
474
|
+
|
|
415
475
|
def build_mappings(claude: Path) -> tuple[RuleMapping, ...]:
|
|
416
476
|
"""Resolve every rule into a concrete Cursor mapping against a Claude layout.
|
|
417
477
|
|
|
@@ -420,14 +480,25 @@ def build_mappings(claude: Path) -> tuple[RuleMapping, ...]:
|
|
|
420
480
|
|
|
421
481
|
Returns:
|
|
422
482
|
One RuleMapping per rule, each path-scoped rule carrying a glob derived
|
|
423
|
-
from its source rule's `paths:` frontmatter.
|
|
483
|
+
from its source rule's `paths:` frontmatter. Curated mappings come first,
|
|
484
|
+
then one generated mapping per remaining `rules/*.md` file.
|
|
424
485
|
"""
|
|
425
486
|
rules_directory = claude / "rules"
|
|
426
487
|
docs_directory = claude / "docs"
|
|
427
|
-
|
|
488
|
+
all_curated = (
|
|
428
489
|
*_always_apply_mappings(rules_directory, docs_directory),
|
|
429
490
|
*_path_scoped_mappings(rules_directory, docs_directory),
|
|
430
491
|
)
|
|
431
|
-
mapping_by_key = {each_mapping.key: each_mapping for each_mapping in
|
|
492
|
+
mapping_by_key = {each_mapping.key: each_mapping for each_mapping in all_curated}
|
|
432
493
|
assert set(mapping_by_key) == set(_merged_mapping_key_order)
|
|
433
|
-
|
|
494
|
+
ordered_curated = tuple(mapping_by_key[each_key] for each_key in _merged_mapping_key_order)
|
|
495
|
+
all_covered_source_names = {
|
|
496
|
+
each_source.name
|
|
497
|
+
for each_mapping in ordered_curated
|
|
498
|
+
for each_source in each_mapping.sources
|
|
499
|
+
if each_source.suffix == MARKDOWN_SUFFIX
|
|
500
|
+
and each_source.parent.name == CLAUDE_RULES_DIRECTORY_NAME
|
|
501
|
+
}
|
|
502
|
+
return ordered_curated + _discovered_mappings(
|
|
503
|
+
rules_directory, all_covered_source_names
|
|
504
|
+
)
|
|
@@ -32,7 +32,7 @@ def test_inventory_includes_shipped_skill_and_agent_names() -> None:
|
|
|
32
32
|
|
|
33
33
|
def test_strip_inert_fenced_blocks_drops_historical_examples() -> None:
|
|
34
34
|
markdown = (
|
|
35
|
-
"Use /
|
|
35
|
+
"Use /sr-loop for cleanup.\n"
|
|
36
36
|
"```historical\n"
|
|
37
37
|
"Use /qbug for bugs.\n"
|
|
38
38
|
"```\n"
|
|
@@ -40,7 +40,7 @@ def test_strip_inert_fenced_blocks_drops_historical_examples() -> None:
|
|
|
40
40
|
)
|
|
41
41
|
stripped = strip_inert_fenced_blocks(markdown)
|
|
42
42
|
assert "/qbug" not in stripped
|
|
43
|
-
assert "/
|
|
43
|
+
assert "/sr-loop" in stripped
|
|
44
44
|
|
|
45
45
|
|
|
46
46
|
def test_extract_active_capability_names_finds_slash_and_backticks() -> None:
|
package/scripts/tests/AGENTS.md
CHANGED
|
@@ -10,6 +10,8 @@ pytest suite for the Python scripts and Pester suite for the PowerShell scripts
|
|
|
10
10
|
| `test_setup_project_paths_config.py` | Configuration constants used by `setup_project_paths.py` |
|
|
11
11
|
| `test_sweep_empty_dirs.py` | `sweep_empty_dirs.py` — age check, one-shot mode, and continuous-watch behavior |
|
|
12
12
|
| `test_sync_to_cursor.py` | `sync_to_cursor/` package — mapping, hashing, manifest, and path resolution |
|
|
13
|
+
| `test_rules.py` | Discovered Claude `rules/*.md` mappings to stem-named Cursor `.mdc` files |
|
|
14
|
+
| `test_engine.py` | `sync_to_cursor` engine `--claude-root` / `--cursor-root` layout flags |
|
|
13
15
|
| `test_grok_worker_constants.py` | `grok_worker_constants.py` — the accepted batch worker-key set stays in step with the worker key constants, and the unknown-key message names both its placeholders |
|
|
14
16
|
|
|
15
17
|
## PowerShell test files
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"""Tests for sync_to_cursor engine CLI roots."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import sys
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
import pytest
|
|
9
|
+
|
|
10
|
+
_SCRIPTS_DIR = Path(__file__).resolve().parent.parent
|
|
11
|
+
if str(_SCRIPTS_DIR) not in sys.path:
|
|
12
|
+
sys.path.insert(0, str(_SCRIPTS_DIR))
|
|
13
|
+
|
|
14
|
+
from sync_to_cursor.engine import run as run_sync_to_cursor
|
|
15
|
+
|
|
16
|
+
_CODE_STANDARDS_SECTION_ORDER = (
|
|
17
|
+
"COMMENT PRESERVATION",
|
|
18
|
+
"CORE PRINCIPLES",
|
|
19
|
+
"⚡ HOOK-ENFORCED RULES",
|
|
20
|
+
"3. REUSE CONSTANTS / 4. CONFIG LOCATIONS",
|
|
21
|
+
"5. NO ABBREVIATIONS",
|
|
22
|
+
"6. COMPLETE TYPE HINTS",
|
|
23
|
+
"9. SELF-CONTAINED COMPONENTS",
|
|
24
|
+
)
|
|
25
|
+
_TEST_QUALITY_SECTION_ORDER = (
|
|
26
|
+
"Delete Useless Tests",
|
|
27
|
+
"Test Dependencies MUST FAIL",
|
|
28
|
+
"Core Testing Principles",
|
|
29
|
+
"React Testing Patterns",
|
|
30
|
+
"Test File Organization",
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _write_minimal_curated_rules(rules_directory: Path) -> None:
|
|
35
|
+
rules_directory.mkdir(parents=True, exist_ok=True)
|
|
36
|
+
(rules_directory / "code-standards.md").write_text(
|
|
37
|
+
"# Code standards stub\n", encoding="utf-8"
|
|
38
|
+
)
|
|
39
|
+
(rules_directory / "tasklings-preferences.md").write_text(
|
|
40
|
+
'---\npaths:\n - "Y:/x/**"\n---\n\n# Tasklings\n',
|
|
41
|
+
encoding="utf-8",
|
|
42
|
+
)
|
|
43
|
+
(rules_directory / "bdd.md").write_text("# BDD\n", encoding="utf-8")
|
|
44
|
+
(rules_directory / "testing.md").write_text(
|
|
45
|
+
'---\npaths:\n - "**/test_*.py"\n---\n\n# Testing\n',
|
|
46
|
+
encoding="utf-8",
|
|
47
|
+
)
|
|
48
|
+
(rules_directory / "research-mode.md").write_text("# RM\n", encoding="utf-8")
|
|
49
|
+
(rules_directory / "conservative-action.md").write_text("# CA\n", encoding="utf-8")
|
|
50
|
+
(rules_directory / "explore-thoroughly.md").write_text("# ET\n", encoding="utf-8")
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _write_minimal_docs(docs_directory: Path) -> None:
|
|
54
|
+
docs_directory.mkdir(parents=True, exist_ok=True)
|
|
55
|
+
(docs_directory / "CODE_RULES.md").write_text(
|
|
56
|
+
"\n\n".join(f"## {title}\n\nalpha" for title in _CODE_STANDARDS_SECTION_ORDER)
|
|
57
|
+
+ "\n",
|
|
58
|
+
encoding="utf-8",
|
|
59
|
+
)
|
|
60
|
+
(docs_directory / "TEST_QUALITY.md").write_text(
|
|
61
|
+
"\n\n".join(f"## {title}\n\nbeta" for title in _TEST_QUALITY_SECTION_ORDER)
|
|
62
|
+
+ "\n",
|
|
63
|
+
encoding="utf-8",
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def test_explicit_roots_write_stem_named_mdc(
|
|
68
|
+
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
69
|
+
) -> None:
|
|
70
|
+
claude = tmp_path / "claude-home"
|
|
71
|
+
cursor = tmp_path / "cursor-home"
|
|
72
|
+
_write_minimal_curated_rules(claude / "rules")
|
|
73
|
+
_write_minimal_docs(claude / "docs")
|
|
74
|
+
(claude / "rules" / "plain-language.md").write_text(
|
|
75
|
+
"# Plain language\n\nUse short sentences.\n",
|
|
76
|
+
encoding="utf-8",
|
|
77
|
+
)
|
|
78
|
+
(claude / "rules" / "CLAUDE.md").write_text("# Inventory\n", encoding="utf-8")
|
|
79
|
+
monkeypatch.delenv("LLM_SETTINGS_ROOT", raising=False)
|
|
80
|
+
assert cursor.exists() is False
|
|
81
|
+
assert (
|
|
82
|
+
run_sync_to_cursor(
|
|
83
|
+
[
|
|
84
|
+
"--force",
|
|
85
|
+
"--claude-root",
|
|
86
|
+
str(claude),
|
|
87
|
+
"--cursor-root",
|
|
88
|
+
str(cursor),
|
|
89
|
+
]
|
|
90
|
+
)
|
|
91
|
+
== 0
|
|
92
|
+
)
|
|
93
|
+
generated = (cursor / "rules" / "plain-language.mdc").read_text(encoding="utf-8")
|
|
94
|
+
assert 'description: "Plain language"' in generated
|
|
95
|
+
assert "alwaysApply: true" in generated
|
|
96
|
+
assert "Use short sentences." in generated
|
|
97
|
+
assert not (cursor / "rules" / "CLAUDE.mdc").is_file()
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def test_explicit_roots_require_both_flags(tmp_path: Path) -> None:
|
|
101
|
+
with pytest.raises(SystemExit):
|
|
102
|
+
run_sync_to_cursor(["--force", "--claude-root", str(tmp_path)])
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"""Tests for discovered Claude-to-Cursor rule mappings."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import sys
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
_SCRIPTS_DIR = Path(__file__).resolve().parent.parent
|
|
9
|
+
if str(_SCRIPTS_DIR) not in sys.path:
|
|
10
|
+
sys.path.insert(0, str(_SCRIPTS_DIR))
|
|
11
|
+
|
|
12
|
+
from sync_to_cursor.rules import build_mappings
|
|
13
|
+
|
|
14
|
+
_PACKAGE_ROOT = _SCRIPTS_DIR.parent
|
|
15
|
+
_SKIPPED_RULE_FILE_NAMES = frozenset({"CLAUDE.md", "AGENTS.md"})
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _write_minimal_curated_rules(rules_directory: Path) -> None:
|
|
19
|
+
rules_directory.mkdir(parents=True, exist_ok=True)
|
|
20
|
+
(rules_directory / "code-standards.md").write_text(
|
|
21
|
+
"# Code standards stub\n", encoding="utf-8"
|
|
22
|
+
)
|
|
23
|
+
(rules_directory / "tasklings-preferences.md").write_text(
|
|
24
|
+
'---\npaths:\n - "Y:/x/**"\n---\n\n# Tasklings\n',
|
|
25
|
+
encoding="utf-8",
|
|
26
|
+
)
|
|
27
|
+
(rules_directory / "bdd.md").write_text("# BDD\n", encoding="utf-8")
|
|
28
|
+
(rules_directory / "testing.md").write_text(
|
|
29
|
+
'---\npaths:\n - "**/test_*.py"\n---\n\n# Testing\n',
|
|
30
|
+
encoding="utf-8",
|
|
31
|
+
)
|
|
32
|
+
(rules_directory / "research-mode.md").write_text("# RM\n", encoding="utf-8")
|
|
33
|
+
(rules_directory / "conservative-action.md").write_text("# CA\n", encoding="utf-8")
|
|
34
|
+
(rules_directory / "explore-thoroughly.md").write_text("# ET\n", encoding="utf-8")
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def test_build_mappings_emits_stem_mdc_for_remaining_claude_rules(
|
|
38
|
+
tmp_path: Path,
|
|
39
|
+
) -> None:
|
|
40
|
+
claude = tmp_path / ".claude"
|
|
41
|
+
_write_minimal_curated_rules(claude / "rules")
|
|
42
|
+
(claude / "docs").mkdir(parents=True, exist_ok=True)
|
|
43
|
+
(claude / "rules" / "plain-language.md").write_text(
|
|
44
|
+
"# Plain language\n\nBe brief.\n",
|
|
45
|
+
encoding="utf-8",
|
|
46
|
+
)
|
|
47
|
+
(claude / "rules" / "CLAUDE.md").write_text(
|
|
48
|
+
"# Package inventory\n", encoding="utf-8"
|
|
49
|
+
)
|
|
50
|
+
(claude / "rules" / "AGENTS.md").write_text("# Agent inventory\n", encoding="utf-8")
|
|
51
|
+
mappings = build_mappings(claude)
|
|
52
|
+
output_by_key = {each_mapping.key: each_mapping for each_mapping in mappings}
|
|
53
|
+
discovered = output_by_key["plain-language"]
|
|
54
|
+
assert discovered.output_name == "plain-language.mdc"
|
|
55
|
+
assert discovered.always_apply is True
|
|
56
|
+
assert discovered.description == "Plain language"
|
|
57
|
+
assert "CLAUDE.md" not in {
|
|
58
|
+
each_source.name
|
|
59
|
+
for each_mapping in mappings
|
|
60
|
+
for each_source in each_mapping.sources
|
|
61
|
+
}
|
|
62
|
+
assert "AGENTS.md" not in {
|
|
63
|
+
each_source.name
|
|
64
|
+
for each_mapping in mappings
|
|
65
|
+
for each_source in each_mapping.sources
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def test_every_shipped_claude_rule_maps_to_an_mdc() -> None:
|
|
70
|
+
mappings = build_mappings(_PACKAGE_ROOT)
|
|
71
|
+
output_name_by_rule_file = {}
|
|
72
|
+
for each_mapping in mappings:
|
|
73
|
+
for each_source in each_mapping.sources:
|
|
74
|
+
if each_source.parent.name == "rules" and each_source.suffix == ".md":
|
|
75
|
+
output_name_by_rule_file[each_source.name] = each_mapping.output_name
|
|
76
|
+
for each_rule_file in sorted((_PACKAGE_ROOT / "rules").glob("*.md")):
|
|
77
|
+
if each_rule_file.name in _SKIPPED_RULE_FILE_NAMES:
|
|
78
|
+
continue
|
|
79
|
+
assert each_rule_file.name in output_name_by_rule_file, each_rule_file.name
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# anthropic-plan
|
|
2
2
|
|
|
3
|
-
**Trigger:** `/anthropic-plan`,
|
|
3
|
+
**Trigger:** `/anthropic-plan`, "plan this first", "think before coding", "make a plan", "scope this out", "don't code yet", and non-trivial requests that need source-grounded design before build work.
|
|
4
4
|
|
|
5
5
|
Creates a repo-local plan packet under `docs/plans/<slug>/` by running the `plan-packet.mjs` workflow. The skill first drafts a short starting plan and gets the user's approval in plan mode (`EnterPlanMode` / `ExitPlanMode`); on approval it runs the workflow. The packet holds context, spec, implementation steps, validation, and a handoff prompt for the build agent. The skill stops before any production code changes.
|
|
6
6
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: anthropic-plan
|
|
3
|
-
description: Workflow-backed implementation planning that creates a deep repo-local packet under docs/plans/<slug>/ before any code changes. Use for /anthropic-plan,
|
|
3
|
+
description: Workflow-backed implementation planning that creates a deep repo-local packet under docs/plans/<slug>/ before any code changes. Use for /anthropic-plan, "plan this first", "think before coding", "make a plan", "scope this out", "don't code yet", and non-trivial implementation requests that need source-grounded design, TDD steps, and validator approval before build work.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Anthropic Plan
|
|
@@ -9,6 +9,7 @@ SKILL_DIRECTORY = Path(__file__).resolve().parent
|
|
|
9
9
|
CLAUDE_DIRECTORY = SKILL_DIRECTORY.parent.parent
|
|
10
10
|
SKILL_PATH = SKILL_DIRECTORY / "SKILL.md"
|
|
11
11
|
PLAN_COMMAND_PATH = CLAUDE_DIRECTORY / "commands" / "plan.md"
|
|
12
|
+
SR_LOOP_COMMAND_PATH = CLAUDE_DIRECTORY / "commands" / "sr-loop.md"
|
|
12
13
|
VALIDATOR_AGENT_PATH = CLAUDE_DIRECTORY / "agents" / "plan-packet-validator.md"
|
|
13
14
|
|
|
14
15
|
|
|
@@ -57,13 +58,14 @@ def test_skill_names_validator_and_stop_before_code_rules() -> None:
|
|
|
57
58
|
assert "stop before implementation" in skill_text.lower()
|
|
58
59
|
|
|
59
60
|
|
|
60
|
-
def
|
|
61
|
-
|
|
61
|
+
def test_skill_keeps_anthropic_plan_slash_without_plan_command() -> None:
|
|
62
|
+
skill_text = SKILL_PATH.read_text(encoding="utf-8")
|
|
62
63
|
|
|
63
|
-
assert "anthropic-plan" in
|
|
64
|
-
assert
|
|
65
|
-
assert
|
|
66
|
-
assert "plan
|
|
64
|
+
assert "/anthropic-plan" in skill_text
|
|
65
|
+
assert not PLAN_COMMAND_PATH.exists()
|
|
66
|
+
assert SR_LOOP_COMMAND_PATH.exists()
|
|
67
|
+
assert "write-plan" not in skill_text
|
|
68
|
+
assert "plan-executor" not in skill_text
|
|
67
69
|
|
|
68
70
|
|
|
69
71
|
def test_validator_agent_exists_and_is_read_only() -> None:
|
|
@@ -45,7 +45,7 @@ Follow `workflows/sandbox.md`. In short:
|
|
|
45
45
|
|
|
46
46
|
### Phase 2 — Promotion
|
|
47
47
|
|
|
48
|
-
Run only in the normal, fully-hooked session — never inside the sandbox. Follow `workflows/promotion.md`, which drives the clean-room task seeds in `reference/promotion-tasks.md`: fresh branch off live `origin/main`, POC content as an uncommitted diff, cleanup and privacy sweep, review and verification under the [review guide](../reviews/SKILL.md#review-workflow), then
|
|
48
|
+
Run only in the normal, fully-hooked session — never inside the sandbox. Follow `workflows/promotion.md`, which drives the clean-room task seeds in `reference/promotion-tasks.md`: fresh branch off live `origin/main`, POC content as an uncommitted diff, cleanup and privacy sweep, review and verification under the [review guide](../reviews/SKILL.md#review-workflow), then commit by hand per `git-workflow` and a draft PR handed to a PR-loop skill. State the two honest limitations from `reference/honest-limitations.md`.
|
|
49
49
|
|
|
50
50
|
## Task seeding
|
|
51
51
|
|
|
@@ -58,7 +58,6 @@ At the start of Phase 2, register every item in `reference/promotion-tasks.md` a
|
|
|
58
58
|
| `fresh-branch` | Sandbox step 1; Promotion step 2 | isolated worktree JSON (`worktree_path`, `base_commit`, `repo_root`) | Refuse — see refusal cases |
|
|
59
59
|
| `privacy-hygiene` | Promotion step 5 | personal-data and secret sweep of the diff | Warn; do a manual review before continuing |
|
|
60
60
|
| [Review guide](../reviews/SKILL.md#review-workflow) | Promotion step 6 | review and verification of the real diff | Stop and report the incomplete promotion |
|
|
61
|
-
| `/commit` (command) | Promotion step 7 | conventional commit + push | Commit and push by hand per `git-workflow` |
|
|
62
61
|
| `autoconverge` (default; `pr-converge` or `bugteam` as alternatives) | Promotion step 9 | the PR converged to ready | Stop after the draft PR; tell the user to converge manually |
|
|
63
62
|
|
|
64
63
|
## Degree of freedom
|
|
@@ -16,7 +16,7 @@ Promotion runs in the **normal, fully-hooked session** — never inside the `--b
|
|
|
16
16
|
|
|
17
17
|
6. **Review and verify the real diff.** Apply the [review guide](../../reviews/SKILL.md#review-workflow). Evidence: the checks run and each required finding repaired. Do not rely on sandbox testing as promotion evidence.
|
|
18
18
|
|
|
19
|
-
7. **Commit and open a draft PR.** After the review and verification record is complete,
|
|
19
|
+
7. **Commit and open a draft PR.** After the review and verification record is complete, commit by hand per the `git-workflow` rule, then open a draft PR. Evidence: the commit hash and the PR URL.
|
|
20
20
|
|
|
21
21
|
8. **State the honest limitations.** Post the two statements from `reference/honest-limitations.md` — write-time rules never ran; TDD ordering waived — in the PR body or to the user. Evidence: the text was included.
|
|
22
22
|
|
|
@@ -16,7 +16,7 @@ The task seeds carry the full ordered detail. The shape:
|
|
|
16
16
|
4. **Cleanup.** Remove scratch files, debug dumps, and temp helpers the POC created (`cleanup-temp-files` rule).
|
|
17
17
|
5. **Privacy sweep** via `privacy-hygiene` over the diff.
|
|
18
18
|
6. **Review and verify** the real diff against the [review guide](../../reviews/SKILL.md#review-workflow). Record the checks run and repair every required finding.
|
|
19
|
-
7. **Commit and PR.** After the review and verification record is complete,
|
|
19
|
+
7. **Commit and PR.** After the review and verification record is complete, commit by hand per the `git-workflow` rule, then open a draft PR.
|
|
20
20
|
8. **State the honest limitations** from `reference/honest-limitations.md` in the PR body or to the user.
|
|
21
21
|
9. **Converge** by handing the PR to `autoconverge` by default; use `pr-converge` for paced ticks or `bugteam` for an open-loop audit.
|
|
22
22
|
|
package/agents/caveman.md
DELETED
|
@@ -1,73 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: caveman
|
|
3
|
-
description: Trims noise from an artifact the main caller has already authored. Input is a draft (skill, doc, plan, response, README, prompt, PR description) — output is the same artifact with filler, hedging, preamble, recap, and restatement removed. Preserves structure, technical substance, frontmatter, and anything load-bearing. Does NOT redesign, restructure, or overrule the caller's scope decisions.
|
|
4
|
-
color: red
|
|
5
|
-
---
|
|
6
|
-
|
|
7
|
-
You are the caveman. You trim. You do not build. You do not restructure.
|
|
8
|
-
|
|
9
|
-
## What you are
|
|
10
|
-
|
|
11
|
-
A noise filter. The main caller has already decided *what* the artifact is, *how* it is structured, and *what lives in it*. Your job is to strip fluff off that artifact without touching the bones.
|
|
12
|
-
|
|
13
|
-
You are downstream of design decisions, not upstream.
|
|
14
|
-
|
|
15
|
-
## What you trim
|
|
16
|
-
|
|
17
|
-
| Noise type | Example |
|
|
18
|
-
|---|---|
|
|
19
|
-
| Preamble / recap | "As discussed above, this skill will..." |
|
|
20
|
-
| Hedging | "This might, in some cases, potentially..." |
|
|
21
|
-
| Filler transitions | "Now, moving on to..." / "It's worth noting that..." |
|
|
22
|
-
| Restatement | the same point made twice in different words |
|
|
23
|
-
| Empty future-proofing | parameters, sections, or fields with no current consumer |
|
|
24
|
-
| Dead examples | examples that duplicate another example without adding coverage |
|
|
25
|
-
| Pleasantries | "Hope this helps." / "Feel free to..." |
|
|
26
|
-
| Vague qualifiers | "various", "several", "a number of" — replace with the actual count or cut |
|
|
27
|
-
|
|
28
|
-
Rewrite prose into the caveman pattern only where it does not change meaning: `[thing] [action] [reason]. [next step].`
|
|
29
|
-
|
|
30
|
-
## What you do NOT touch
|
|
31
|
-
|
|
32
|
-
- **Structure the caller chose.** Four sections in, four sections out. Do not collapse or merge.
|
|
33
|
-
- **Frontmatter fields.** All fields stay. Tighten values if verbose; do not drop fields.
|
|
34
|
-
- **Technical substance.** Code, commands, paths, URLs, errors, JSON, schema — unchanged.
|
|
35
|
-
- **Trigger words / activation phrases.** Load-bearing for skill matching.
|
|
36
|
-
- **Safety / escape-hatch language.** Warnings about destructive ops, irreversible actions, credentials, money, production systems — preserve verbatim.
|
|
37
|
-
- **Caller-flagged content.** If the caller said "keep X verbatim", X is untouchable.
|
|
38
|
-
- **Counts and specifics.** Numbers, thresholds, version strings, identifiers — unchanged.
|
|
39
|
-
- **Register in examples and docstrings.** Unless caller asked for caveman voice throughout, keep the original register of user-facing copy.
|
|
40
|
-
|
|
41
|
-
## What you do NOT decide
|
|
42
|
-
|
|
43
|
-
You do not tell the caller:
|
|
44
|
-
- "Use the existing tool instead" — design call, caller's call.
|
|
45
|
-
- "Make this one file instead of three" — structure call, caller's call.
|
|
46
|
-
- "Drop this section" — scope call, caller's call.
|
|
47
|
-
- "Add tests" / "remove tests" — scope call, caller's call.
|
|
48
|
-
|
|
49
|
-
If you suspect a section is pure noise, flag it in the report. Leave it in place unless the caller told you to remove it.
|
|
50
|
-
|
|
51
|
-
## Process
|
|
52
|
-
|
|
53
|
-
1. Read the artifact end to end before touching it.
|
|
54
|
-
2. Mark the bones — frontmatter, structure, technical substance, trigger words, safety language. Off-limits.
|
|
55
|
-
3. Trim noise per the table above.
|
|
56
|
-
4. Return the trimmed artifact in the caller's original file format.
|
|
57
|
-
|
|
58
|
-
## Output shape
|
|
59
|
-
|
|
60
|
-
```
|
|
61
|
-
trimmed: <path or artifact name>
|
|
62
|
-
removed: <bullets — noise categories cut, with rough line counts>
|
|
63
|
-
preserved-verbatim: <what you refused to touch and why>
|
|
64
|
-
flagged: <content you suspect is noise but left in place for caller to decide>
|
|
65
|
-
```
|
|
66
|
-
|
|
67
|
-
No recap of the artifact itself. Caller has it.
|
|
68
|
-
|
|
69
|
-
## Escape hatch
|
|
70
|
-
|
|
71
|
-
If trimming would drop a safety warning, remove an irreversible-action caveat, collapse a distinction the caller made deliberately, or if you are unsure whether content is load-bearing — leave it in place and flag it. Ask before cutting.
|
|
72
|
-
|
|
73
|
-
Terse is for noise, not for substance.
|