@event4u/agent-config 5.7.0 → 5.9.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/.agent-src/commands/agent-handoff.md +1 -1
- package/.agent-src/commands/agent-status.md +1 -1
- package/.agent-src/commands/agents/audit.md +1 -1
- package/.agent-src/commands/agents/init.md +1 -1
- package/.agent-src/commands/agents/user/accept.md +3 -3
- package/.agent-src/commands/agents/user/init.md +4 -4
- package/.agent-src/commands/agents/user/show.md +3 -3
- package/.agent-src/commands/agents/user/update.md +3 -3
- package/.agent-src/commands/agents/user.md +1 -1
- package/.agent-src/commands/agents.md +1 -1
- package/.agent-src/commands/analytics/prune.md +1 -1
- package/.agent-src/commands/analytics/show.md +1 -1
- package/.agent-src/commands/analytics.md +1 -1
- package/.agent-src/commands/bug-fix.md +1 -1
- package/.agent-src/commands/challenge-me.md +1 -1
- package/.agent-src/commands/chat-history/import.md +1 -1
- package/.agent-src/commands/chat-history/learn.md +1 -1
- package/.agent-src/commands/chat-history/show.md +1 -1
- package/.agent-src/commands/chat-history.md +1 -1
- package/.agent-src/commands/check-current-md.md +1 -1
- package/.agent-src/commands/condense.md +1 -1
- package/.agent-src/commands/context.md +1 -1
- package/.agent-src/commands/cost-report.md +1 -1
- package/.agent-src/commands/council.md +3 -3
- package/.agent-src/commands/create-pr/description-only.md +1 -1
- package/.agent-src/commands/create-pr.md +1 -1
- package/.agent-src/commands/e2e-heal.md +1 -1
- package/.agent-src/commands/e2e-plan.md +1 -1
- package/.agent-src/commands/feature.md +1 -1
- package/.agent-src/commands/fix/ci.md +1 -1
- package/.agent-src/commands/fix/portability.md +1 -1
- package/.agent-src/commands/fix/pr-bot-comments.md +1 -1
- package/.agent-src/commands/fix/pr-comments.md +1 -1
- package/.agent-src/commands/fix/pr-developer-comments.md +1 -1
- package/.agent-src/commands/fix/refs.md +1 -1
- package/.agent-src/commands/fix/seeder.md +1 -1
- package/.agent-src/commands/fix.md +1 -1
- package/.agent-src/commands/judge.md +1 -1
- package/.agent-src/commands/knowledge/cross-repo.md +1 -1
- package/.agent-src/commands/knowledge/forget.md +1 -1
- package/.agent-src/commands/knowledge/ingest.md +1 -1
- package/.agent-src/commands/knowledge/list.md +1 -1
- package/.agent-src/commands/knowledge.md +1 -1
- package/.agent-src/commands/memory/add.md +1 -1
- package/.agent-src/commands/memory/learn-low-impact.md +1 -1
- package/.agent-src/commands/memory/load.md +1 -1
- package/.agent-src/commands/memory/mine-session.md +1 -1
- package/.agent-src/commands/memory/promote.md +1 -1
- package/.agent-src/commands/memory/propose.md +1 -1
- package/.agent-src/commands/memory.md +1 -1
- package/.agent-src/commands/mode.md +1 -1
- package/.agent-src/commands/optimize/agents-dir.md +1 -1
- package/.agent-src/commands/optimize/augmentignore.md +1 -1
- package/.agent-src/commands/optimize/rtk.md +1 -1
- package/.agent-src/commands/optimize/skills.md +1 -1
- package/.agent-src/commands/optimize.md +1 -1
- package/.agent-src/commands/orchestrate.md +1 -1
- package/.agent-src/commands/override/create.md +1 -1
- package/.agent-src/commands/override/manage.md +1 -1
- package/.agent-src/commands/override.md +1 -1
- package/.agent-src/commands/package-reset.md +1 -1
- package/.agent-src/commands/prediction-pool.md +31 -12
- package/.agent-src/commands/profile/activate.md +81 -0
- package/.agent-src/commands/profile/deactivate.md +68 -0
- package/.agent-src/commands/profile/show.md +70 -0
- package/.agent-src/commands/profile.md +68 -0
- package/.agent-src/commands/project-health.md +1 -1
- package/.agent-src/commands/quality-fix.md +1 -1
- package/.agent-src/commands/roadmap/process-full.md +1 -1
- package/.agent-src/commands/roadmap/process-phase.md +1 -1
- package/.agent-src/commands/roadmap/process-step.md +1 -1
- package/.agent-src/commands/roadmap.md +1 -1
- package/.agent-src/commands/set-cost-profile.md +1 -1
- package/.agent-src/commands/skill/preview.md +3 -3
- package/.agent-src/commands/skill.md +1 -1
- package/.agent-src/commands/skills/discover.md +1 -1
- package/.agent-src/commands/skills.md +1 -1
- package/.agent-src/commands/sync-agent-settings.md +1 -1
- package/.agent-src/commands/sync-gitignore/fix.md +1 -1
- package/.agent-src/commands/sync-gitignore.md +1 -1
- package/.agent-src/commands/update-form-request-messages.md +1 -1
- package/.agent-src/skills/check-refs/SKILL.md +1 -1
- package/.agent-src/skills/finishing-a-development-branch/SKILL.md +1 -1
- package/.agent-src/skills/git-workflow/SKILL.md +1 -1
- package/.agent-src/skills/jira-integration/SKILL.md +1 -1
- package/.agent-src/skills/markitdown/SKILL.md +1 -1
- package/.agent-src/skills/prediction-pool-optimizer/SKILL.md +195 -77
- package/.agent-src/skills/prediction-pool-optimizer/evals/triggers.json +3 -1
- package/.agent-src/skills/prediction-pool-optimizer/reference/ev-fixtures.md +111 -16
- package/.agent-src/skills/prediction-pool-optimizer/reference/odds-and-bonus.md +109 -0
- package/.agent-src/skills/rtk-output-filtering/SKILL.md +1 -1
- package/.agent-src/skills/script-writing/SKILL.md +1 -1
- package/.agent-src/skills/token-optimizer/SKILL.md +1 -1
- package/.agent-src/skills/using-git-worktrees/SKILL.md +1 -1
- package/.agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/.agent-src/templates/scripts/work_engine/_lib/agent_settings.py +52 -5
- package/.claude-plugin/marketplace.json +370 -366
- package/CHANGELOG.md +77 -0
- package/README.md +2 -2
- package/config/discovery/session-profiles.yml +37 -0
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +183 -95
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +3 -3
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +9 -5
- package/dist/discovery/trust-report.md +2 -2
- package/dist/discovery/workspaces.json +8 -4
- package/dist/mcp/registry-manifest.json +3 -3
- package/docs/architecture.md +1 -1
- package/docs/catalog.md +7 -3
- package/docs/contracts/command-clusters.md +2 -0
- package/docs/contracts/session-profile-overlay.md +120 -0
- package/docs/customization.md +26 -0
- package/docs/decisions/ADR-010-profile-pack-preset-boundary.md +36 -0
- package/docs/decisions/ADR-038-canonical-settings-path.md +66 -0
- package/docs/decisions/ADR-039-claude-skills-untracked.md +139 -0
- package/docs/decisions/INDEX.md +2 -0
- package/docs/development.md +12 -0
- package/docs/getting-started.md +1 -1
- package/docs/guidelines/agent-infra/layered-settings.md +8 -2
- package/docs/skills-catalog.md +5 -1
- package/llms.txt +4 -0
- package/package.json +1 -1
- package/scripts/__pycache__/validate_frontmatter.cpython-312.pyc +0 -0
- package/scripts/_cli/cmd_doctor.py +180 -16
- package/scripts/_cli/cmd_versions.py +2 -2
- package/scripts/_lib/__pycache__/__init__.cpython-312.pyc +0 -0
- package/scripts/_lib/__pycache__/agent_src.cpython-312.pyc +0 -0
- package/scripts/_lib/agent_settings.py +52 -5
- package/scripts/_lib/agent_src.py +30 -0
- package/scripts/ai_council/session.py +5 -1
- package/scripts/audit_command_surface.py +7 -1
- package/scripts/audit_initial_context.py +10 -2
- package/scripts/check_gate_paths.py +117 -0
- package/scripts/check_references.py +51 -2
- package/scripts/check_release_published.py +145 -0
- package/scripts/check_test_coverage_diff.py +180 -0
- package/scripts/compile_router.py +5 -1
- package/scripts/condense.py +79 -2
- package/scripts/config/session_profiles.py +492 -0
- package/scripts/council_cli.py +5 -1
- package/scripts/hook_manifest.yaml +15 -7
- package/scripts/hooks/dispatch_hook.py +8 -0
- package/scripts/install-hooks.sh +2 -1
- package/scripts/install.py +76 -5
- package/scripts/inventory_abstraction_budget.py +6 -1
- package/scripts/lint_agents_md.py +11 -4
- package/scripts/lint_hook_concern_budget.py +5 -1
- package/scripts/lint_marketplace.py +18 -7
- package/scripts/lint_roadmap_ci_steps.py +5 -1
- package/scripts/lint_roadmap_complexity.py +5 -1
- package/scripts/mcp_server/prompts.py +5 -1
- package/scripts/prediction-pool/pool_winsim.py +236 -0
- package/scripts/prediction-pool/score_ev.py +188 -0
- package/scripts/profile_staleness_hook.py +69 -0
- package/scripts/release.py +54 -31
- package/scripts/roadmap_progress_hook.py +56 -6
- package/scripts/smoke_quickstart.py +3 -2
- package/scripts/sync_agent_settings.py +8 -3
- package/scripts/validate_agent_settings.py +5 -1
- package/scripts/validate_decision_engine.py +5 -1
- package/scripts/measure_roadmap_trajectory.py +0 -112
- package/scripts/verify_roadmap_closure.py +0 -327
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Gate path-integrity check (R2 of road-to-test-and-gate-integrity).
|
|
3
|
+
|
|
4
|
+
Asserts that every security/quality gate which enforces something against
|
|
5
|
+
a fixed ``packages/core/`` target still resolves that target on disk. A
|
|
6
|
+
``packages/core/`` move that desyncs a gate's hard-coded path fails CI here
|
|
7
|
+
instead of silently no-opping (the ``aab5755`` class: the Iron-Law SHA gate
|
|
8
|
+
pointed at a stale path and enforced nothing while CI stayed green).
|
|
9
|
+
|
|
10
|
+
Design (AI council, claude-sonnet-4-5 + gpt-4o, 2026-06-02):
|
|
11
|
+
|
|
12
|
+
- The check reads each gate's ACTUAL enforced paths via its module-level
|
|
13
|
+
``GATE_CORE_PATHS`` attribute — it does NOT re-declare a copy of the path
|
|
14
|
+
strings. A hand-maintained path registry would reintroduce the very
|
|
15
|
+
desync risk this guards against, one layer down.
|
|
16
|
+
- Scope is strictly the single-root hard-coders. Multi-root gates that
|
|
17
|
+
resolve via ``artefact_roots()`` (e.g. ``iron_law_sha``) are excluded:
|
|
18
|
+
asserting a single ``packages/core/`` path for them would false-pass on a
|
|
19
|
+
legacy layout or false-fail on a pack-only layout.
|
|
20
|
+
|
|
21
|
+
The input set is this gate list (no separate config file).
|
|
22
|
+
|
|
23
|
+
Usage:
|
|
24
|
+
python3 scripts/check_gate_paths.py
|
|
25
|
+
Exit codes: 0 = all enforced targets resolve under packages/core/ ·
|
|
26
|
+
1 = at least one missing / out-of-tree target · 2 = a gate failed to import.
|
|
27
|
+
"""
|
|
28
|
+
from __future__ import annotations
|
|
29
|
+
|
|
30
|
+
import importlib
|
|
31
|
+
import sys
|
|
32
|
+
from pathlib import Path
|
|
33
|
+
|
|
34
|
+
REPO_ROOT = Path(__file__).resolve().parent.parent
|
|
35
|
+
sys.path.insert(0, str(REPO_ROOT / "scripts"))
|
|
36
|
+
from _lib.agent_src import resolve_package_core_path # noqa: E402
|
|
37
|
+
|
|
38
|
+
PACKAGE_CORE = resolve_package_core_path("")
|
|
39
|
+
|
|
40
|
+
# Single-root gates that enforce against a fixed packages/core/ target and
|
|
41
|
+
# expose it via a module-level GATE_CORE_PATHS tuple. Adding a gate here is
|
|
42
|
+
# the only manual step; its paths are read from the gate, never copied.
|
|
43
|
+
GATES: tuple[str, ...] = (
|
|
44
|
+
"inventory_abstraction_budget",
|
|
45
|
+
"audit_command_surface",
|
|
46
|
+
"lint_agents_md",
|
|
47
|
+
"audit_initial_context",
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _is_under_core(p: Path) -> bool:
|
|
52
|
+
try:
|
|
53
|
+
p.resolve().relative_to(PACKAGE_CORE.resolve())
|
|
54
|
+
return True
|
|
55
|
+
except ValueError:
|
|
56
|
+
return False
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def collect_gate_paths(gate_modules: tuple[str, ...]) -> dict[str, list[Path]]:
|
|
60
|
+
"""Import each gate and read its declared ``GATE_CORE_PATHS``.
|
|
61
|
+
|
|
62
|
+
Raises ``ImportError`` (surfaced as exit 2 by ``main``) if a gate cannot
|
|
63
|
+
be imported — a gate whose path logic broke at import time is itself a
|
|
64
|
+
failure this check should not swallow.
|
|
65
|
+
"""
|
|
66
|
+
out: dict[str, list[Path]] = {}
|
|
67
|
+
for name in gate_modules:
|
|
68
|
+
mod = importlib.import_module(name)
|
|
69
|
+
paths = getattr(mod, "GATE_CORE_PATHS", None)
|
|
70
|
+
if not paths:
|
|
71
|
+
raise AttributeError(
|
|
72
|
+
f"{name} has no non-empty GATE_CORE_PATHS — gate cannot be "
|
|
73
|
+
f"checked. Declare the packages/core targets it enforces."
|
|
74
|
+
)
|
|
75
|
+
out[name] = [Path(p) for p in paths]
|
|
76
|
+
return out
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def check_paths(named: dict[str, list[Path]]) -> list[tuple[str, str, Path]]:
|
|
80
|
+
"""Return ``(gate, reason, path)`` for every target that fails.
|
|
81
|
+
|
|
82
|
+
Pure (no import side effects) so tests can drive it with fixtures.
|
|
83
|
+
A target fails when it does not resolve under ``packages/core/`` or
|
|
84
|
+
does not exist on disk.
|
|
85
|
+
"""
|
|
86
|
+
failures: list[tuple[str, str, Path]] = []
|
|
87
|
+
for gate, paths in named.items():
|
|
88
|
+
for p in paths:
|
|
89
|
+
if not _is_under_core(p):
|
|
90
|
+
failures.append((gate, "not under packages/core/", p))
|
|
91
|
+
elif not p.exists():
|
|
92
|
+
failures.append((gate, "target does not exist", p))
|
|
93
|
+
return failures
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def main() -> int:
|
|
97
|
+
try:
|
|
98
|
+
named = collect_gate_paths(GATES)
|
|
99
|
+
except (ImportError, AttributeError) as exc:
|
|
100
|
+
print(f"❌ check-gate-paths: {exc}", file=sys.stderr)
|
|
101
|
+
return 2
|
|
102
|
+
failures = check_paths(named)
|
|
103
|
+
if failures:
|
|
104
|
+
print("❌ check-gate-paths: gate target(s) do not resolve under packages/core/:")
|
|
105
|
+
for gate, reason, path in failures:
|
|
106
|
+
print(f" {gate}: {reason} → {path}")
|
|
107
|
+
print("\n A packages/core/ move likely desynced a gate. Fix the gate's")
|
|
108
|
+
print(" GATE_CORE_PATHS (built via resolve_package_core_path) or the move.")
|
|
109
|
+
return 1
|
|
110
|
+
total = sum(len(v) for v in named.values())
|
|
111
|
+
print(f"✅ check-gate-paths: {total} enforced target(s) across "
|
|
112
|
+
f"{len(named)} gate(s) resolve under packages/core/.")
|
|
113
|
+
return 0
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
if __name__ == "__main__":
|
|
117
|
+
raise SystemExit(main())
|
|
@@ -133,6 +133,53 @@ EXAMPLE_PATH_PATTERNS = [
|
|
|
133
133
|
]
|
|
134
134
|
|
|
135
135
|
|
|
136
|
+
@dataclass(frozen=True)
|
|
137
|
+
class AllowlistPattern:
|
|
138
|
+
"""A token-class allowlist entry. `reason` is mandatory and auditable."""
|
|
139
|
+
pattern: "re.Pattern[str]"
|
|
140
|
+
reason: str
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
# Content-class allowlist for known NON-reference token shapes.
|
|
144
|
+
#
|
|
145
|
+
# The skill/rule prose patterns (`X` skill / `X` rule) occasionally match
|
|
146
|
+
# a backtick token that is not an artifact id — an execution-type enum
|
|
147
|
+
# value, a pack identifier, or a bare meta-qualifier keyword. Historically
|
|
148
|
+
# each such false positive was dodged by *rewording the prose per file*
|
|
149
|
+
# (e.g. dc84ed01 "reword execution-type mentions to dodge check-refs
|
|
150
|
+
# false positive", bd02ef0b "avoid check-refs false-positive on pack
|
|
151
|
+
# name"), a treadmill that distorts natural wording release after release.
|
|
152
|
+
# This layer matches the token *class* centrally instead, so the natural
|
|
153
|
+
# wording passes without per-file edits. It is distinct from:
|
|
154
|
+
# - SKIP_DIRS (path-level, whole-directory)
|
|
155
|
+
# - FILE_SKIP_MARKER (file-level opt-out)
|
|
156
|
+
# - LINE_IGNORE_MARKER (per-line opt-out)
|
|
157
|
+
# Every entry carries a mandatory `reason` so the allowlist stays
|
|
158
|
+
# auditable and a future reader can tell why a class is exempt.
|
|
159
|
+
ALLOWLIST_PATTERNS: List[AllowlistPattern] = [
|
|
160
|
+
AllowlistPattern(
|
|
161
|
+
re.compile(r"^(?:manual|assisted|automated)$"),
|
|
162
|
+
"execution-type enum value (runtime-safety frontmatter), e.g. a "
|
|
163
|
+
"`manual` skill — not a skill/rule id (dc84ed01)",
|
|
164
|
+
),
|
|
165
|
+
AllowlistPattern(
|
|
166
|
+
re.compile(r"^pack-[\w-]+$"),
|
|
167
|
+
"pack / workspace identifier, e.g. `pack-ai-video` skills — not a "
|
|
168
|
+
"skill/rule id (bd02ef0b)",
|
|
169
|
+
),
|
|
170
|
+
AllowlistPattern(
|
|
171
|
+
re.compile(r"^(?:skill|rule|command|guideline|persona|context|pack|workspace)$"),
|
|
172
|
+
"bare meta-qualifier keyword used in prose (the `command` vs "
|
|
173
|
+
"`skill` distinction, etc.) — not an artifact id",
|
|
174
|
+
),
|
|
175
|
+
]
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def _is_allowlisted(name: str) -> bool:
|
|
179
|
+
"""True when `name` matches a known non-reference token class."""
|
|
180
|
+
return any(entry.pattern.match(name) for entry in ALLOWLIST_PATTERNS)
|
|
181
|
+
|
|
182
|
+
|
|
136
183
|
def collect_artifacts(root: Path) -> dict[str, set[str]]:
|
|
137
184
|
"""Build lookup sets for skills, rules, commands, guidelines, personas."""
|
|
138
185
|
arts: dict[str, set[str]] = {
|
|
@@ -345,7 +392,8 @@ def check_file(filepath: Path, artifacts: dict[str, set[str]], root: Path) -> Li
|
|
|
345
392
|
# Skill name references
|
|
346
393
|
for m in SKILL_REF_PATTERN.finditer(line):
|
|
347
394
|
name = m.group(1)
|
|
348
|
-
if name not in artifacts["skills"] and name not in _SKIP_NAMES
|
|
395
|
+
if name not in artifacts["skills"] and name not in _SKIP_NAMES \
|
|
396
|
+
and not _is_allowlisted(name):
|
|
349
397
|
broken.append(BrokenRef(
|
|
350
398
|
file=str(filepath), line=i, ref=name,
|
|
351
399
|
ref_type="skill", severity="warning",
|
|
@@ -355,7 +403,8 @@ def check_file(filepath: Path, artifacts: dict[str, set[str]], root: Path) -> Li
|
|
|
355
403
|
# Rule name references
|
|
356
404
|
for m in RULE_REF_PATTERN.finditer(line):
|
|
357
405
|
name = m.group(1)
|
|
358
|
-
if name not in artifacts["rules"] and name not in _SKIP_NAMES
|
|
406
|
+
if name not in artifacts["rules"] and name not in _SKIP_NAMES \
|
|
407
|
+
and not _is_allowlisted(name):
|
|
359
408
|
broken.append(BrokenRef(
|
|
360
409
|
file=str(filepath), line=i, ref=name,
|
|
361
410
|
ref_type="rule", severity="warning",
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Release-published drift gate.
|
|
4
|
+
|
|
5
|
+
Catches the "release merged to main but never tagged/published" failure
|
|
6
|
+
mode — where ``main``'s ``package.json`` claims a version that has no
|
|
7
|
+
matching git tag, and npm's ``latest`` therefore lags behind main. This
|
|
8
|
+
is the backstop that would have surfaced the 5.8.0-stuck-on-5.7.0 state
|
|
9
|
+
the moment it happened, instead of weeks later.
|
|
10
|
+
|
|
11
|
+
Two independent invariants:
|
|
12
|
+
|
|
13
|
+
1. **Tag invariant** (always checkable, no network): the version in
|
|
14
|
+
``package.json`` MUST have a matching git tag (local or remote).
|
|
15
|
+
2. **npm invariant** (``--check-npm``, network): ``npm view <pkg>
|
|
16
|
+
dist-tags.latest`` MUST equal the ``package.json`` version.
|
|
17
|
+
|
|
18
|
+
Scope guard: this only makes sense on the release trunk. Off ``main``
|
|
19
|
+
(e.g. a feature branch, or a ``release/X.Y.Z`` branch mid-flight where
|
|
20
|
+
the bump legitimately precedes the tag) the gate **no-ops** unless
|
|
21
|
+
``--strict`` is combined with an explicit on-main signal. The daily
|
|
22
|
+
scheduled workflow runs ``--strict --check-npm`` on ``main``; the local
|
|
23
|
+
``task check-release-published`` runs the tag invariant only.
|
|
24
|
+
|
|
25
|
+
Exit codes: 0 = pass / no-op · 1 = drift detected · 3 = internal error.
|
|
26
|
+
"""
|
|
27
|
+
from __future__ import annotations
|
|
28
|
+
|
|
29
|
+
import argparse
|
|
30
|
+
import json
|
|
31
|
+
import os
|
|
32
|
+
import re
|
|
33
|
+
import subprocess
|
|
34
|
+
import sys
|
|
35
|
+
from pathlib import Path
|
|
36
|
+
|
|
37
|
+
SEMVER_RE = re.compile(r"^(\d+)\.(\d+)\.(\d+)(?:[-+].*)?$")
|
|
38
|
+
REPO_ROOT = Path(__file__).resolve().parent.parent
|
|
39
|
+
PACKAGE_JSON = REPO_ROOT / "package.json"
|
|
40
|
+
MAIN_BRANCH = "main"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _git(*args: str) -> tuple[int, str]:
|
|
44
|
+
proc = subprocess.run(["git", *args], capture_output=True, text=True, check=False)
|
|
45
|
+
return proc.returncode, (proc.stdout or "").strip()
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _package_version() -> str:
|
|
49
|
+
data = json.loads(PACKAGE_JSON.read_text(encoding="utf-8"))
|
|
50
|
+
return str(data["version"])
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _package_name() -> str:
|
|
54
|
+
data = json.loads(PACKAGE_JSON.read_text(encoding="utf-8"))
|
|
55
|
+
return str(data["name"])
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _tag_exists(tag: str) -> bool:
|
|
59
|
+
rc, out = _git("tag", "-l", tag)
|
|
60
|
+
if rc == 0 and tag in out.splitlines():
|
|
61
|
+
return True
|
|
62
|
+
rc, _ = _git("ls-remote", "--exit-code", "--tags", "origin", tag)
|
|
63
|
+
return rc == 0
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _on_main() -> bool:
|
|
67
|
+
# Local checkout, CI push ref, or CI scheduled ref all map to main.
|
|
68
|
+
ref = os.environ.get("GITHUB_REF", "")
|
|
69
|
+
if ref in ("refs/heads/main", "refs/heads/master"):
|
|
70
|
+
return True
|
|
71
|
+
rc, head = _git("rev-parse", "--abbrev-ref", "HEAD")
|
|
72
|
+
return rc == 0 and head == MAIN_BRANCH
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _npm_latest(pkg: str) -> str | None:
|
|
76
|
+
proc = subprocess.run(
|
|
77
|
+
["npm", "view", pkg, "dist-tags.latest"],
|
|
78
|
+
capture_output=True, text=True, check=False,
|
|
79
|
+
)
|
|
80
|
+
if proc.returncode != 0:
|
|
81
|
+
return None
|
|
82
|
+
return (proc.stdout or "").strip() or None
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def main(argv: list[str] | None = None) -> int:
|
|
86
|
+
ap = argparse.ArgumentParser(description="Release-published drift gate.")
|
|
87
|
+
ap.add_argument("--strict", action="store_true",
|
|
88
|
+
help="Fail on drift (default: informational, exit 0).")
|
|
89
|
+
ap.add_argument("--check-npm", action="store_true",
|
|
90
|
+
help="Also assert npm dist-tags.latest == package.json version (network).")
|
|
91
|
+
ap.add_argument("--require-main", action="store_true",
|
|
92
|
+
help="Only enforce when on main; no-op elsewhere (default for scheduled).")
|
|
93
|
+
args = ap.parse_args(argv)
|
|
94
|
+
|
|
95
|
+
try:
|
|
96
|
+
version = _package_version()
|
|
97
|
+
except (OSError, KeyError, json.JSONDecodeError) as exc:
|
|
98
|
+
print(f"❌ cannot read package.json version: {exc}", file=sys.stderr)
|
|
99
|
+
return 3
|
|
100
|
+
if not SEMVER_RE.match(version):
|
|
101
|
+
print(f"❌ package.json version is not semver: {version!r}", file=sys.stderr)
|
|
102
|
+
return 3
|
|
103
|
+
|
|
104
|
+
if args.require_main and not _on_main():
|
|
105
|
+
print(f"ℹ️ not on {MAIN_BRANCH} — release-published gate skipped.")
|
|
106
|
+
return 0
|
|
107
|
+
|
|
108
|
+
problems: list[str] = []
|
|
109
|
+
|
|
110
|
+
if not _tag_exists(version):
|
|
111
|
+
problems.append(
|
|
112
|
+
f"package.json is {version} but no git tag {version} exists "
|
|
113
|
+
f"(local or origin) — the release was bumped/merged but never "
|
|
114
|
+
f"tagged. Complete it: tag the release-merge commit and push "
|
|
115
|
+
f"(triggers publish-npm.yml), e.g. `git tag {version} && git "
|
|
116
|
+
f"push origin {version}`."
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
if args.check_npm:
|
|
120
|
+
pkg = _package_name()
|
|
121
|
+
latest = _npm_latest(pkg)
|
|
122
|
+
if latest is None:
|
|
123
|
+
print(f"⚠️ could not read npm dist-tags.latest for {pkg} "
|
|
124
|
+
f"(network/registry) — npm invariant not checked.", file=sys.stderr)
|
|
125
|
+
elif latest != version:
|
|
126
|
+
problems.append(
|
|
127
|
+
f"npm {pkg}@latest is {latest} but package.json is {version} "
|
|
128
|
+
f"— the published release lags main. Check publish-npm.yml "
|
|
129
|
+
f"for tag {version}, or re-dispatch it."
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
if not problems:
|
|
133
|
+
suffix = " + npm" if args.check_npm else ""
|
|
134
|
+
print(f"✅ release-published: {version} is tagged{suffix} and in sync.")
|
|
135
|
+
return 0
|
|
136
|
+
|
|
137
|
+
header = "❌ Release-published drift:" if args.strict else "⚠️ Release-published drift (warn-only):"
|
|
138
|
+
print(header, file=sys.stderr)
|
|
139
|
+
for p in problems:
|
|
140
|
+
print(f" - {p}", file=sys.stderr)
|
|
141
|
+
return 1 if args.strict else 0
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
if __name__ == "__main__":
|
|
145
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Coverage forcing-function — WARN-only (R3 of road-to-test-and-gate-integrity).
|
|
3
|
+
|
|
4
|
+
A lightweight coverage-governance nudge: warn (never block) when a PR adds a
|
|
5
|
+
new gate script without a matching test, so the +0-tests pattern (three
|
|
6
|
+
releases shipped behaviour with no coverage) becomes visible at PR time. This
|
|
7
|
+
is partly a *social* problem — R3 is a nudge, not a cure.
|
|
8
|
+
|
|
9
|
+
Honest framing (per the roadmap critique): R3 IS a lightweight coverage-
|
|
10
|
+
governance layer, NOT pure hardening. It ships WARN-only, is calibrated from
|
|
11
|
+
the REAL Phase 2 backfill experience (not guessed up front), and carries a
|
|
12
|
+
sunset clause. The hard-fail flip is a SEPARATE, later decision gated on
|
|
13
|
+
warn-phase data.
|
|
14
|
+
|
|
15
|
+
----------------------------------------------------------------------------
|
|
16
|
+
TRIGGER SURFACE — calibrated from Phase 2 evidence + AI council
|
|
17
|
+
(claude-sonnet-4-5 + gpt-4o, analysis, 2026-06-02):
|
|
18
|
+
|
|
19
|
+
WARN when a NEW `scripts/check_*.py` or `scripts/lint_*.py` gate is added
|
|
20
|
+
with no matching new/changed test in the same diff (`tests/**/test_*.py`
|
|
21
|
+
or `tests/**/*_test.py` whose stem matches the gate's stem).
|
|
22
|
+
|
|
23
|
+
What is DELIBERATELY excluded (the council killed these to avoid
|
|
24
|
+
pragma-spam / signal-to-noise collapse — "over-broad triggers train people
|
|
25
|
+
to bypass it"):
|
|
26
|
+
- Edits to EXISTING gate scripts. Phase 2 migrated 4 gates with one-line
|
|
27
|
+
path-constant swaps that legitimately needed NO test; a trigger that
|
|
28
|
+
fired on edits would be pure noise. KNOWN RECALL LIMIT: a behaviour
|
|
29
|
+
change to an existing gate ships untracked in this warn-only phase —
|
|
30
|
+
accepted on purpose; revisit only if the warn-phase data justifies it.
|
|
31
|
+
- New skills with `requires_skills`. Skills are markdown capability docs,
|
|
32
|
+
usually test-less by design → would fire constantly. Dropped for Phase 1
|
|
33
|
+
(council: "no Phase 2 evidence; immediate pragma-spam").
|
|
34
|
+
- Taskfile wiring, docs/`*.md`, `agents/**`, `config/**`, logging/path-
|
|
35
|
+
constant edits.
|
|
36
|
+
|
|
37
|
+
PRAGMA — `# coverage-diff-ignore: <reason>` (reason mandatory) in the new
|
|
38
|
+
gate file suppresses its warning. Because the trigger is NEW files only, the
|
|
39
|
+
pragma lives in the added lines (it IS the diff), so it is naturally
|
|
40
|
+
commit-scoped — it cannot silently silence FUTURE edits to the file (those
|
|
41
|
+
do not trigger). Mirrors the `check-refs` `<!-- ref-ignore -->` allowlist.
|
|
42
|
+
|
|
43
|
+
SUCCESS METRIC + SUNSET (the fail-mode flip is a separate later decision):
|
|
44
|
+
- Track warn-rate and pragma-rate over the next release cycle (this script
|
|
45
|
+
emits `coverage-diff: warned=N suppressed=M` so the data is in CI logs —
|
|
46
|
+
no new telemetry surface).
|
|
47
|
+
- Consider fail-mode only at >= 3 legitimate catches per cycle AND
|
|
48
|
+
pragma-rate < 10%.
|
|
49
|
+
- If precision is poor after one cycle, REMOVE the check rather than
|
|
50
|
+
escalate it. A noisy nudge people route around is worse than none.
|
|
51
|
+
----------------------------------------------------------------------------
|
|
52
|
+
|
|
53
|
+
Usage:
|
|
54
|
+
python3 scripts/check_test_coverage_diff.py [--base-ref REF] [--files-status "A\tpath" ...]
|
|
55
|
+
Exit code: always 0 (warn-only by contract this phase).
|
|
56
|
+
"""
|
|
57
|
+
from __future__ import annotations
|
|
58
|
+
|
|
59
|
+
import argparse
|
|
60
|
+
import re
|
|
61
|
+
import subprocess
|
|
62
|
+
import sys
|
|
63
|
+
from pathlib import Path
|
|
64
|
+
|
|
65
|
+
REPO_ROOT = Path(__file__).resolve().parent.parent
|
|
66
|
+
|
|
67
|
+
GATE_RE = re.compile(r"^scripts/(?:check_|lint_)[A-Za-z0-9_]+\.py$")
|
|
68
|
+
PRAGMA_RE = re.compile(r"#\s*coverage-diff-ignore:\s*(\S.*?)\s*$")
|
|
69
|
+
_PRAGMA_SCAN_LINES = 60
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _is_test_file(path: str) -> bool:
|
|
73
|
+
if not (path.startswith("tests/") and path.endswith(".py")):
|
|
74
|
+
return False
|
|
75
|
+
stem = Path(path).stem
|
|
76
|
+
return stem.startswith("test_") or stem.endswith("_test")
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _test_matches_gate(gate_path: str, test_paths: list[str]) -> bool:
|
|
80
|
+
"""A test covers a gate when its stem shares the gate's stem.
|
|
81
|
+
|
|
82
|
+
Accepts naming variance: `tests/test_check_foo.py`, `tests/foo_test.py`,
|
|
83
|
+
`tests/sub/test_foo.py` all match gate `scripts/check_foo.py`.
|
|
84
|
+
"""
|
|
85
|
+
gate_stem = Path(gate_path).stem # e.g. check_foo
|
|
86
|
+
short = gate_stem.removeprefix("check_").removeprefix("lint_") # foo
|
|
87
|
+
for t in test_paths:
|
|
88
|
+
tstem = Path(t).stem.removeprefix("test_").removesuffix("_test")
|
|
89
|
+
if gate_stem in Path(t).stem or short and short in tstem:
|
|
90
|
+
return True
|
|
91
|
+
return False
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def evaluate(changed, pragma_reason):
|
|
95
|
+
"""Pure core (no git, no I/O): classify the changed-file set.
|
|
96
|
+
|
|
97
|
+
`changed` is a list of (status, path) where status is the git
|
|
98
|
+
name-status code ('A' added, 'M' modified, …). `pragma_reason(path)`
|
|
99
|
+
returns the in-file opt-out reason or None. Returns
|
|
100
|
+
`(warnings, suppressed)` where warnings is a list of new gate paths
|
|
101
|
+
with no matching test and no pragma, suppressed is [(path, reason)].
|
|
102
|
+
"""
|
|
103
|
+
new_gates = [p for (s, p) in changed if s == "A" and GATE_RE.match(p)]
|
|
104
|
+
test_changes = [p for (_s, p) in changed if _is_test_file(p)]
|
|
105
|
+
warnings: list[str] = []
|
|
106
|
+
suppressed: list[tuple[str, str]] = []
|
|
107
|
+
for gate in new_gates:
|
|
108
|
+
if _test_matches_gate(gate, test_changes):
|
|
109
|
+
continue
|
|
110
|
+
reason = pragma_reason(gate)
|
|
111
|
+
if reason:
|
|
112
|
+
suppressed.append((gate, reason))
|
|
113
|
+
else:
|
|
114
|
+
warnings.append(gate)
|
|
115
|
+
return warnings, suppressed
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _pragma_reason_from_tree(path: str) -> str | None:
|
|
119
|
+
f = REPO_ROOT / path
|
|
120
|
+
try:
|
|
121
|
+
head = f.read_text(encoding="utf-8", errors="ignore").splitlines()[:_PRAGMA_SCAN_LINES]
|
|
122
|
+
except OSError:
|
|
123
|
+
return None
|
|
124
|
+
for line in head:
|
|
125
|
+
m = PRAGMA_RE.search(line)
|
|
126
|
+
if m:
|
|
127
|
+
return m.group(1)
|
|
128
|
+
return None
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _resolve_base_ref(explicit: str | None) -> str:
|
|
132
|
+
if explicit:
|
|
133
|
+
return explicit
|
|
134
|
+
for candidate in ("origin/main", "origin/master", "main", "master"):
|
|
135
|
+
try:
|
|
136
|
+
subprocess.check_output(
|
|
137
|
+
["git", "rev-parse", "--verify", candidate], stderr=subprocess.DEVNULL,
|
|
138
|
+
)
|
|
139
|
+
return candidate
|
|
140
|
+
except subprocess.CalledProcessError:
|
|
141
|
+
continue
|
|
142
|
+
return "HEAD~1"
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def _git_name_status(base_ref: str) -> list[tuple[str, str]]:
|
|
146
|
+
try:
|
|
147
|
+
out = subprocess.check_output(
|
|
148
|
+
["git", "diff", "--name-status", f"{base_ref}...HEAD"],
|
|
149
|
+
stderr=subprocess.STDOUT, text=True,
|
|
150
|
+
)
|
|
151
|
+
except subprocess.CalledProcessError as exc:
|
|
152
|
+
print(f"⚠️ coverage-diff: git diff failed ({exc.output.strip()}); skipping.", file=sys.stderr)
|
|
153
|
+
return []
|
|
154
|
+
rows: list[tuple[str, str]] = []
|
|
155
|
+
for line in out.splitlines():
|
|
156
|
+
parts = line.split("\t")
|
|
157
|
+
if len(parts) >= 2:
|
|
158
|
+
rows.append((parts[0][:1], parts[-1]))
|
|
159
|
+
return rows
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def main(argv: list[str] | None = None) -> int:
|
|
163
|
+
ap = argparse.ArgumentParser()
|
|
164
|
+
ap.add_argument("--base-ref", default=None)
|
|
165
|
+
opts = ap.parse_args(argv)
|
|
166
|
+
changed = _git_name_status(_resolve_base_ref(opts.base_ref))
|
|
167
|
+
warnings, suppressed = evaluate(changed, _pragma_reason_from_tree)
|
|
168
|
+
if warnings:
|
|
169
|
+
print("⚠️ coverage-diff: new gate(s) added with no matching test (warn-only):")
|
|
170
|
+
for g in warnings:
|
|
171
|
+
print(f" {g} — add tests/test_{Path(g).stem}.py, or a "
|
|
172
|
+
f"`# coverage-diff-ignore: <reason>` line if no test is warranted.")
|
|
173
|
+
for g, reason in suppressed:
|
|
174
|
+
print(f" (suppressed: {g} — {reason})")
|
|
175
|
+
print(f"coverage-diff: warned={len(warnings)} suppressed={len(suppressed)}")
|
|
176
|
+
return 0 # warn-only by contract this phase
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
if __name__ == "__main__":
|
|
180
|
+
raise SystemExit(main())
|
|
@@ -13,6 +13,10 @@ from __future__ import annotations
|
|
|
13
13
|
import json
|
|
14
14
|
import sys
|
|
15
15
|
from pathlib import Path
|
|
16
|
+
try: # invocation-agnostic import (repo-root-on-path vs scripts-on-path)
|
|
17
|
+
from scripts._lib.agent_settings import project_settings_path
|
|
18
|
+
except ModuleNotFoundError: # pragma: no cover
|
|
19
|
+
from _lib.agent_settings import project_settings_path
|
|
16
20
|
|
|
17
21
|
ROOT = Path(__file__).resolve().parent.parent
|
|
18
22
|
# ADR-017: rules now live across multiple source roots. Legacy
|
|
@@ -20,7 +24,7 @@ ROOT = Path(__file__).resolve().parent.parent
|
|
|
20
24
|
# pure-condensed consumer projection.
|
|
21
25
|
RULES_DIR = ROOT / ".agent-src.uncondensed" / "rules"
|
|
22
26
|
OUT_PATH = ROOT / "dist" / "router.json"
|
|
23
|
-
SETTINGS_PATH = ROOT
|
|
27
|
+
SETTINGS_PATH = project_settings_path(ROOT)
|
|
24
28
|
SCHEMA_VERSION = 1
|
|
25
29
|
|
|
26
30
|
# Compile-time rule toggles. Maps rule-id → settings predicate.
|
package/scripts/condense.py
CHANGED
|
@@ -27,6 +27,10 @@ import re
|
|
|
27
27
|
import shutil
|
|
28
28
|
import sys
|
|
29
29
|
from pathlib import Path
|
|
30
|
+
try: # invocation-agnostic import (repo-root-on-path vs scripts-on-path)
|
|
31
|
+
from scripts._lib.agent_settings import project_settings_path
|
|
32
|
+
except ModuleNotFoundError: # pragma: no cover
|
|
33
|
+
from _lib.agent_settings import project_settings_path
|
|
30
34
|
|
|
31
35
|
import yaml
|
|
32
36
|
|
|
@@ -46,7 +50,7 @@ SOURCE_DIR = PROJECT_ROOT / ".agent-src.uncondensed"
|
|
|
46
50
|
TARGET_DIR = PROJECT_ROOT / ".agent-src"
|
|
47
51
|
AUGMENT_DIR = PROJECT_ROOT / ".augment"
|
|
48
52
|
HASH_FILE = PROJECT_ROOT / "internal" / ".condensation-hashes.json"
|
|
49
|
-
SETTINGS_FILE = PROJECT_ROOT
|
|
53
|
+
SETTINGS_FILE = project_settings_path(PROJECT_ROOT)
|
|
50
54
|
|
|
51
55
|
|
|
52
56
|
def _iter_sources():
|
|
@@ -415,6 +419,13 @@ COMMANDS_SOURCE = PROJECT_ROOT / ".agent-src" / "commands"
|
|
|
415
419
|
PERSONAS_SOURCE = PROJECT_ROOT / ".agent-src" / "personas"
|
|
416
420
|
USER_TYPES_SOURCE = PROJECT_ROOT / ".agent-src" / "user-types"
|
|
417
421
|
CLAUDE_SKILLS_DIR = PROJECT_ROOT / ".claude" / "skills"
|
|
422
|
+
# Committed plugin-marketplace projection for command-as-skill entries.
|
|
423
|
+
# The marketplace is consumed as a git repo, so every skills[] path must be
|
|
424
|
+
# committed. Real skills resolve to .agent-src/skills/<name> (already
|
|
425
|
+
# committed); commands have no <slug>/SKILL.md shape in source, so their
|
|
426
|
+
# committed projection lives here. .claude/skills/ stays a gitignored,
|
|
427
|
+
# generate-tools-rebuilt local auto-discovery channel.
|
|
428
|
+
PLUGIN_SKILLS_DIR = PROJECT_ROOT / ".claude-plugin" / "skills"
|
|
418
429
|
|
|
419
430
|
PERSONA_TOOL_DIRS = {
|
|
420
431
|
".claude/personas": "../../.agent-src/personas",
|
|
@@ -1156,6 +1167,70 @@ def generate_claude_commands() -> int:
|
|
|
1156
1167
|
return count
|
|
1157
1168
|
|
|
1158
1169
|
|
|
1170
|
+
def generate_plugin_command_skills() -> int:
|
|
1171
|
+
"""Mirror command-as-skill entries into the committed .claude-plugin/skills/.
|
|
1172
|
+
|
|
1173
|
+
The plugin marketplace references each command entry as a <slug>/SKILL.md
|
|
1174
|
+
path that must be committed (git-consumed marketplace). Commands have no
|
|
1175
|
+
such shape in source, so this projects them as symlinks:
|
|
1176
|
+
.claude-plugin/skills/<slug>/SKILL.md → ../../../.agent-src/commands/<rel>.
|
|
1177
|
+
|
|
1178
|
+
Symlink-only by design: the committed .claude/skills/ shape was always
|
|
1179
|
+
symlinks (ADR-034 model-rendered copies are local-only, never committed),
|
|
1180
|
+
so the distributed marketplace behaviour is preserved exactly. The local
|
|
1181
|
+
auto-discovery channel (.claude/skills/, gitignored) keeps model rendering
|
|
1182
|
+
for the dev loop.
|
|
1183
|
+
"""
|
|
1184
|
+
if not COMMANDS_SOURCE.exists():
|
|
1185
|
+
return 0
|
|
1186
|
+
|
|
1187
|
+
PLUGIN_SKILLS_DIR.mkdir(parents=True, exist_ok=True)
|
|
1188
|
+
|
|
1189
|
+
skill_names: set[str] = set()
|
|
1190
|
+
if SKILLS_SOURCE.exists():
|
|
1191
|
+
skill_names = {d.name for d in SKILLS_SOURCE.iterdir() if d.is_dir()}
|
|
1192
|
+
|
|
1193
|
+
current_slugs: set[str] = set()
|
|
1194
|
+
count = 0
|
|
1195
|
+
for source_file, slug in _iter_commands():
|
|
1196
|
+
# A real skill of the same name takes priority — skip the command.
|
|
1197
|
+
if slug in skill_names:
|
|
1198
|
+
continue
|
|
1199
|
+
current_slugs.add(slug)
|
|
1200
|
+
|
|
1201
|
+
skill_dir = PLUGIN_SKILLS_DIR / slug
|
|
1202
|
+
skill_dir.mkdir(parents=True, exist_ok=True)
|
|
1203
|
+
skill_file = skill_dir / "SKILL.md"
|
|
1204
|
+
if skill_file.exists() or skill_file.is_symlink():
|
|
1205
|
+
skill_file.unlink()
|
|
1206
|
+
|
|
1207
|
+
rel_path = source_file.relative_to(COMMANDS_SOURCE)
|
|
1208
|
+
rel_target = Path("../../../.agent-src/commands") / rel_path
|
|
1209
|
+
skill_file.symlink_to(rel_target)
|
|
1210
|
+
count += 1
|
|
1211
|
+
|
|
1212
|
+
# Reap stale command dirs from removed commands (exactly one SKILL.md).
|
|
1213
|
+
removed_dirs = 0
|
|
1214
|
+
for item in PLUGIN_SKILLS_DIR.iterdir():
|
|
1215
|
+
if not item.is_dir() or item.is_symlink():
|
|
1216
|
+
continue
|
|
1217
|
+
if item.name in current_slugs:
|
|
1218
|
+
continue
|
|
1219
|
+
skill_md = item / "SKILL.md"
|
|
1220
|
+
if skill_md.is_symlink() or skill_md.is_file():
|
|
1221
|
+
entries = list(item.iterdir())
|
|
1222
|
+
if len(entries) == 1 and entries[0].name == "SKILL.md":
|
|
1223
|
+
skill_md.unlink()
|
|
1224
|
+
item.rmdir()
|
|
1225
|
+
removed_dirs += 1
|
|
1226
|
+
|
|
1227
|
+
msg = f" ✅ Created {count} command entries in .claude-plugin/skills/"
|
|
1228
|
+
if removed_dirs:
|
|
1229
|
+
msg += f" ({removed_dirs} stale dirs removed)"
|
|
1230
|
+
info(msg)
|
|
1231
|
+
return count
|
|
1232
|
+
|
|
1233
|
+
|
|
1159
1234
|
def generate_persona_symlinks() -> int:
|
|
1160
1235
|
"""Create symlink directories for personas (.claude/personas/, .cursor/personas/).
|
|
1161
1236
|
|
|
@@ -1309,6 +1384,7 @@ def generate_tools() -> None:
|
|
|
1309
1384
|
generate_gemini_md()
|
|
1310
1385
|
skills = generate_claude_skills() if _tool_active("claude-code") else 0
|
|
1311
1386
|
commands = generate_claude_commands() if _tool_active("claude-code") else 0
|
|
1387
|
+
plugin_cmd_skills = generate_plugin_command_skills() if _tool_active("claude-code") else 0
|
|
1312
1388
|
plugin_hooks = generate_plugin_hooks() if _tool_active("claude-code") else 0
|
|
1313
1389
|
personas = generate_persona_symlinks()
|
|
1314
1390
|
user_types = generate_user_type_symlinks()
|
|
@@ -1318,7 +1394,8 @@ def generate_tools() -> None:
|
|
|
1318
1394
|
windsurf_wf = generate_windsurf_workflows() if _tool_active("windsurf") else 0
|
|
1319
1395
|
summary = (
|
|
1320
1396
|
f"✅ generate-tools — rules={rules} skills={skills} "
|
|
1321
|
-
f"commands={commands}
|
|
1397
|
+
f"commands={commands} plugin_cmd_skills={plugin_cmd_skills} "
|
|
1398
|
+
f"plugin_hooks={plugin_hooks} "
|
|
1322
1399
|
f"personas={personas} user_types={user_types} "
|
|
1323
1400
|
f"cursor_mdc={cursor_mdc} windsurf_rules={windsurf_modern} "
|
|
1324
1401
|
f"cursor_commands={cursor_cmds} windsurf_workflows={windsurf_wf} "
|