@event4u/agent-config 3.0.0 → 3.1.1
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/install-via-agent.md +129 -0
- package/.agent-src/commands/video/from-script.md +1 -1
- package/.agent-src/commands/video.md +1 -1
- package/.agent-src/contexts/execution/cheap-question-mechanics.md +81 -0
- package/.agent-src/rules/caveman-speak.md +2 -2
- package/.agent-src/rules/context-hygiene.md +36 -0
- package/.agent-src/rules/engineering-safety-floor.md +102 -0
- package/.agent-src/rules/finance-safety-floor.md +114 -0
- package/.agent-src/rules/git-history-discipline.md +1 -1
- package/.agent-src/rules/no-cheap-questions.md +34 -32
- package/.agent-src/rules/provider-lifecycle-discipline.md +4 -4
- package/.agent-src/rules/strategy-safety-floor.md +114 -0
- package/.agent-src/skills/agents-md-thin-root/SKILL.md +15 -9
- package/.agent-src/skills/async-python-patterns/SKILL.md +1 -1
- package/.agent-src/skills/project-analysis-node-express/SKILL.md +1 -1
- package/.agent-src/skills/readme-reviewer/SKILL.md +52 -3
- package/.agent-src/skills/readme-writing/SKILL.md +52 -4
- package/.agent-src/skills/readme-writing-package/SKILL.md +48 -5
- package/.agent-src/skills/systematic-debugging/SKILL.md +41 -0
- package/.agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/.agent-src/templates/hooks/pre-commit-frontmatter +66 -0
- package/.agent-src/templates/hooks/pre-commit-roadmap-progress +78 -39
- package/.agent-src/templates/scripts/work_engine/_lib/agent_settings.py +4 -1
- package/.agent-src/templates/scripts/work_engine/orchestration.py +25 -11
- package/.claude-plugin/marketplace.json +2 -1
- package/AGENTS.md +10 -8
- package/CHANGELOG.md +233 -123
- package/README.md +165 -553
- package/config/agent-settings.template.yml +0 -7
- package/config/discovery/packs.yml +20 -0
- package/config/discovery/unassigned-artefacts.yml +2 -0
- package/config/gitignore-block.txt +19 -3
- package/dist/cli/commands/uiServe.js +13 -4
- package/dist/cli/commands/uiServe.js.map +1 -1
- package/dist/cli/registry.js +2 -0
- package/dist/cli/registry.js.map +1 -1
- package/dist/discovery/deprecation-report.md +7 -0
- package/dist/discovery/discovery-manifest.json +2107 -1409
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +9 -9
- package/dist/discovery/orphan-report.md +10 -0
- package/dist/discovery/packs.json +1002 -0
- package/dist/discovery/trust-report.md +26 -0
- package/dist/discovery/workspaces.json +705 -0
- package/dist/mcp/registry-manifest.json +4 -4
- package/dist/router.json +1623 -0
- package/dist/server/app.js +11 -3
- package/dist/server/app.js.map +1 -1
- package/dist/server/io/atomicMultiWrite.js +3 -1
- package/dist/server/io/atomicMultiWrite.js.map +1 -1
- package/dist/server/io/yamlIO.js +22 -0
- package/dist/server/io/yamlIO.js.map +1 -1
- package/dist/server/routes/ping.js +8 -0
- package/dist/server/routes/ping.js.map +1 -1
- package/dist/server/routes/schema.js +2 -2
- package/dist/server/routes/schema.js.map +1 -1
- package/dist/server/routes/settings.js +104 -23
- package/dist/server/routes/settings.js.map +1 -1
- package/dist/server/routes/userMd.js +37 -27
- package/dist/server/routes/userMd.js.map +1 -1
- package/dist/server/routes/wizard.js +256 -20
- package/dist/server/routes/wizard.js.map +1 -1
- package/dist/server/schemas/settings.js +0 -1
- package/dist/server/schemas/settings.js.map +1 -1
- package/dist/server/token.js +10 -3
- package/dist/server/token.js.map +1 -1
- package/dist/server/writeRoot.js +28 -11
- package/dist/server/writeRoot.js.map +1 -1
- package/dist/server/writeRoot.test.js +22 -4
- package/dist/server/writeRoot.test.js.map +1 -1
- package/dist/shared/userMd/formAdapter.js +29 -51
- package/dist/shared/userMd/formAdapter.js.map +1 -1
- package/dist/shared/userMd/schema.js +32 -104
- package/dist/shared/userMd/schema.js.map +1 -1
- package/dist/shared/userMd/utils.js +64 -50
- package/dist/shared/userMd/utils.js.map +1 -1
- package/dist/ui/assets/index-D-DY1ywI.js +35 -0
- package/dist/ui/assets/index-D-DY1ywI.js.map +1 -0
- package/dist/ui/index.html +1 -1
- package/docs/adrs/router/0001-three-tier-routing.md +5 -5
- package/docs/adrs/smoke/0001-per-tier-smoke-scripts.md +1 -1
- package/docs/architecture.md +3 -3
- package/docs/archive/CHANGELOG-pre-3.1.0.md +167 -0
- package/docs/catalog.md +30 -26
- package/docs/contracts/CHANGELOG-conventions.md +1 -1
- package/docs/contracts/agent-user-schema.md +6 -9
- package/docs/contracts/consumer-bridge.md +79 -0
- package/docs/contracts/discovery-manifest.md +209 -0
- package/docs/contracts/discovery-manifest.schema.json +77 -4
- package/docs/contracts/explain-trace.schema.json +1 -1
- package/docs/contracts/file-ownership-matrix.json +197 -13
- package/docs/contracts/frontmatter-contract.md +140 -0
- package/docs/contracts/gui-wizard.md +223 -0
- package/docs/contracts/installer-agent-mode.md +137 -0
- package/docs/contracts/kernel-membership.md +1 -1
- package/docs/contracts/mcp-tool-inventory.md +9 -9
- package/docs/contracts/namespace.md +6 -6
- package/docs/contracts/provider-lifecycle.md +5 -5
- package/docs/contracts/rule-router.md +4 -4
- package/docs/contracts/settings-api.md +53 -6
- package/docs/contracts/smoke-contracts.md +3 -3
- package/docs/contracts/trust-and-safety.md +144 -0
- package/docs/customization.md +2 -2
- package/docs/decisions/ADR-007-agent-discovery-scopes.md +12 -0
- package/docs/decisions/ADR-013-discovery-frontmatter-contract.md +24 -0
- package/docs/decisions/ADR-015-discovery-manifest-contract.md +146 -0
- package/docs/decisions/ADR-016-installer-architecture.md +189 -0
- package/docs/decisions/ADR-017-monorepo-physical-layout.md +261 -0
- package/docs/decisions/ADR-018-trust-and-safety-layer.md +159 -0
- package/docs/decisions/ADR-019-router-json-dist-location.md +124 -0
- package/docs/decisions/ADR-020-global-only-consumer-scope.md +123 -0
- package/docs/decisions/ADR-021-deployment-shape.md +153 -0
- package/docs/decisions/INDEX.md +7 -0
- package/docs/deploy/connector-setup.md +129 -0
- package/docs/deploy/env-vars.md +70 -0
- package/docs/deploy/policy-cookbook.md +130 -0
- package/docs/deploy/quickstart.md +112 -0
- package/docs/distribution/public-install-smoke.md +68 -0
- package/docs/distribution/registries.md +55 -0
- package/docs/distribution/telemetry-privacy.md +128 -0
- package/docs/distribution/telemetry-schema.md +174 -0
- package/docs/featured-skills.md +95 -0
- package/docs/getting-started-by-role.md +19 -1
- package/docs/getting-started.md +2 -2
- package/docs/guidelines/agent-infra/installed-tools-manifest.md +11 -8
- package/docs/guidelines/docs/readme-size-and-splitting.md +53 -1
- package/docs/installation.md +27 -14
- package/docs/maintainers/dev-mode.md +105 -0
- package/docs/setup/per-ide/claude-desktop.md +3 -2
- package/docs/wizard.md +39 -4
- package/package.json +18 -1
- package/scripts/__pycache__/validate_frontmatter.cpython-312.pyc +0 -0
- package/scripts/_cli/cmd_doctor.py +150 -2
- package/scripts/_cli/cmd_explain.py +2 -1
- package/scripts/_cli/cmd_migrate_to_global.py +415 -0
- package/scripts/_cli/cmd_settings_migrate.py +146 -0
- package/scripts/_cli/explain_last/route.py +2 -1
- package/scripts/_dispatch.bash +36 -3
- 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 +4 -1
- package/scripts/_lib/agent_src.py +157 -0
- package/scripts/agent-config +17 -6
- package/scripts/audit_skill_descriptions.py +18 -6
- package/scripts/build_discovery_manifest.py +373 -17
- package/scripts/check_artefact_checksums.py +104 -0
- package/scripts/check_cluster_patterns.py +20 -4
- package/scripts/check_command_count_messaging.py +33 -14
- package/scripts/check_council_references.py +43 -4
- package/scripts/check_overlay_cascade_subdirs.py +7 -3
- package/scripts/check_references.py +5 -2
- package/scripts/check_reply_consistency.py +32 -9
- package/scripts/check_template_pin_drift.py +24 -7
- package/scripts/check_token_optimizer_freshness.py +18 -3
- package/scripts/compile_router.py +34 -2
- package/scripts/compress.py +162 -44
- package/scripts/config/presets.py +19 -1
- package/scripts/config/profiles.py +16 -1
- package/scripts/discovery_stats.py +70 -0
- package/scripts/expected_perms.json +47 -0
- package/scripts/generate_index.py +78 -46
- package/scripts/generate_ownership_matrix.py +98 -43
- package/scripts/generate_pack_manifests.py +183 -0
- package/scripts/install +18 -1
- package/scripts/install.py +934 -59
- package/scripts/install.sh +27 -9
- package/scripts/lint_agents_layout.py +93 -13
- package/scripts/lint_agents_md.py +1 -1
- package/scripts/lint_archived_skills.py +32 -16
- package/scripts/lint_bench_corpus.py +14 -2
- package/scripts/lint_command_tiers.py +15 -2
- package/scripts/lint_featured_skills.py +139 -0
- package/scripts/lint_framework_leakage.py +33 -6
- package/scripts/lint_global_paths.py +147 -0
- package/scripts/lint_orchestration_dsl.py +6 -3
- package/scripts/lint_pack_boundaries.py +147 -0
- package/scripts/lint_pack_first_win.py +103 -0
- package/scripts/lint_readme_jargon.py +131 -0
- package/scripts/lint_readme_size.py +33 -0
- package/scripts/lint_rule_interactions.py +23 -5
- package/scripts/lint_rule_tiers.py +12 -3
- package/scripts/lint_trust_coherence.py +212 -0
- package/scripts/measure_rule_budget.py +22 -4
- package/scripts/move_artefact.py +143 -0
- package/scripts/new_skill.py +148 -0
- package/scripts/plan_physical_move.py +353 -0
- package/scripts/refine_ticket_detect.py +30 -7
- package/scripts/release.py +22 -2
- package/scripts/schemas/command.schema.json +4 -0
- package/scripts/skill_linter.py +248 -118
- package/scripts/skill_trigger_eval.py +28 -8
- package/scripts/smoke/kernel.sh +1 -1
- package/scripts/smoke/router.sh +24 -5
- package/scripts/smoke/skills.sh +15 -7
- package/scripts/smoke_quickstart.py +11 -2
- package/scripts/snapshot_agent_outputs.py +144 -0
- package/scripts/update_counts.py +45 -17
- package/scripts/validate_decision_engine.py +9 -1
- package/scripts/validate_discovery_manifest.py +94 -0
- package/scripts/validate_frontmatter.py +39 -20
- package/scripts/verify_physical_move.py +185 -0
- package/templates/agent-user.md +0 -1
- package/templates/agent-user.yml +21 -0
- package/templates/minimal/agents-overrides-readme.md +46 -0
- package/templates/minimal/overrides-gitkeep +2 -0
- package/dist/ui/assets/index-BTRcKDlB.js +0 -39
- package/dist/ui/assets/index-BTRcKDlB.js.map +0 -1
- package/templates/minimal/agents-gitkeep +0 -2
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Lint trust/safety coherence across the discovery manifest.
|
|
3
|
+
|
|
4
|
+
Phase 5.4 of the monorepo trust-and-safety layer. Walks the freshly
|
|
5
|
+
built `dist/discovery/discovery-manifest.json` and asserts three
|
|
6
|
+
invariants:
|
|
7
|
+
|
|
8
|
+
1. Every pack whose ``trust_summary`` declares ``advisory`` or
|
|
9
|
+
``restricted`` artefacts ships at least one ``*safety-floor*``
|
|
10
|
+
rule in the same pack.
|
|
11
|
+
2. Every artefact with ``trust.human_review_required: true`` carries
|
|
12
|
+
the ``_HRR_BANNER_MARKER`` in its compiled output under
|
|
13
|
+
``.agent-src/<logical>``.
|
|
14
|
+
3. Every rule listed in ``router.json`` ``kernel[]`` declares
|
|
15
|
+
``trust.level: core`` (no escalation to advisory/restricted,
|
|
16
|
+
no demotion to experimental).
|
|
17
|
+
|
|
18
|
+
Exits 0 clean, 1 on any violation. Stdlib + pyyaml. Cap: ≤ 200 LOC.
|
|
19
|
+
"""
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import argparse
|
|
23
|
+
import json
|
|
24
|
+
import sys
|
|
25
|
+
from pathlib import Path
|
|
26
|
+
from typing import Any
|
|
27
|
+
|
|
28
|
+
ROOT = Path(__file__).resolve().parents[1]
|
|
29
|
+
MANIFEST = ROOT / "dist" / "discovery" / "discovery-manifest.json"
|
|
30
|
+
ROUTER = ROOT / "dist" / "router.json"
|
|
31
|
+
COMPILED_SRC = ROOT / ".agent-src"
|
|
32
|
+
|
|
33
|
+
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
34
|
+
from _lib.agent_src import strip_source_prefix # noqa: E402
|
|
35
|
+
|
|
36
|
+
# Imported lazily inside _banner_marker() to keep the cap loose if compress.py
|
|
37
|
+
# grows additional top-level side effects.
|
|
38
|
+
_BANNER_MARKER = "<!-- agent-config:human-review-banner -->"
|
|
39
|
+
|
|
40
|
+
# Trust levels that demand a domain-safety floor in the same pack.
|
|
41
|
+
_FLAGGED_LEVELS = ("advisory", "restricted")
|
|
42
|
+
_SAFETY_FLOOR_FRAGMENT = "safety-floor"
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _load_manifest(path: Path) -> dict[str, Any]:
|
|
46
|
+
if not path.exists():
|
|
47
|
+
raise SystemExit(
|
|
48
|
+
f"ERROR: manifest not found: {path}\n"
|
|
49
|
+
" Run `task build-discovery` first."
|
|
50
|
+
)
|
|
51
|
+
return json.loads(path.read_text("utf-8"))
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _load_kernel(path: Path) -> set[str]:
|
|
55
|
+
if not path.exists():
|
|
56
|
+
raise SystemExit(f"ERROR: router.json not found: {path}")
|
|
57
|
+
data = json.loads(path.read_text("utf-8"))
|
|
58
|
+
kernel = data.get("kernel") or []
|
|
59
|
+
if not isinstance(kernel, list):
|
|
60
|
+
raise SystemExit("ERROR: router.json `kernel` must be a list")
|
|
61
|
+
return {str(name) for name in kernel}
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _check_pack_safety_floors(manifest: dict[str, Any]) -> list[str]:
|
|
65
|
+
"""Check 1: advisory/restricted packs ship a *safety-floor* rule."""
|
|
66
|
+
errs: list[str] = []
|
|
67
|
+
# Build pack -> [artefact path] index from the artefact list so we can
|
|
68
|
+
# spot the safety-floor regardless of how trust_summary was computed.
|
|
69
|
+
pack_paths: dict[str, list[str]] = {}
|
|
70
|
+
for art in manifest.get("artefacts", []):
|
|
71
|
+
for pack in art.get("packs", []) or []:
|
|
72
|
+
pack_paths.setdefault(pack, []).append(art["path"])
|
|
73
|
+
|
|
74
|
+
for pack in manifest.get("packs", []):
|
|
75
|
+
summary = pack.get("trust_summary", {}) or {}
|
|
76
|
+
flagged_total = sum(int(summary.get(lvl, 0)) for lvl in _FLAGGED_LEVELS)
|
|
77
|
+
if flagged_total == 0:
|
|
78
|
+
continue
|
|
79
|
+
paths = pack_paths.get(pack["id"], [])
|
|
80
|
+
has_floor = any(_SAFETY_FLOOR_FRAGMENT in p for p in paths)
|
|
81
|
+
if not has_floor:
|
|
82
|
+
counts = ", ".join(
|
|
83
|
+
f"{lvl}={int(summary.get(lvl, 0))}" for lvl in _FLAGGED_LEVELS
|
|
84
|
+
)
|
|
85
|
+
errs.append(
|
|
86
|
+
f"pack `{pack['id']}` declares flagged artefacts ({counts})"
|
|
87
|
+
f" but ships no `*{_SAFETY_FLOOR_FRAGMENT}*` rule"
|
|
88
|
+
)
|
|
89
|
+
return errs
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _check_human_review_banners(
|
|
93
|
+
manifest: dict[str, Any], compiled_src: Path
|
|
94
|
+
) -> list[str]:
|
|
95
|
+
"""Check 2: every human_review_required artefact has the banner."""
|
|
96
|
+
errs: list[str] = []
|
|
97
|
+
for art in manifest.get("artefacts", []):
|
|
98
|
+
trust = art.get("trust", {}) or {}
|
|
99
|
+
if not trust.get("human_review_required"):
|
|
100
|
+
continue
|
|
101
|
+
rel = art["path"]
|
|
102
|
+
logical = strip_source_prefix(rel)
|
|
103
|
+
if logical is None:
|
|
104
|
+
errs.append(
|
|
105
|
+
f"{rel}: human_review_required=true but path is not under"
|
|
106
|
+
" any known source root"
|
|
107
|
+
)
|
|
108
|
+
continue
|
|
109
|
+
compiled = compiled_src / logical
|
|
110
|
+
if not compiled.exists():
|
|
111
|
+
errs.append(
|
|
112
|
+
f"{rel}: human_review_required=true but compiled output"
|
|
113
|
+
f" missing at `{compiled.relative_to(ROOT)}`"
|
|
114
|
+
)
|
|
115
|
+
continue
|
|
116
|
+
body = compiled.read_text("utf-8", errors="replace")
|
|
117
|
+
if _BANNER_MARKER not in body:
|
|
118
|
+
errs.append(
|
|
119
|
+
f"{rel}: human_review_required=true but compiled output"
|
|
120
|
+
f" `{compiled.relative_to(ROOT)}` is missing the HRR banner"
|
|
121
|
+
f" (`{_BANNER_MARKER}`) — re-run `task compress`."
|
|
122
|
+
)
|
|
123
|
+
return errs
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _check_kernel_trust(
|
|
127
|
+
manifest: dict[str, Any], kernel: set[str]
|
|
128
|
+
) -> list[str]:
|
|
129
|
+
"""Check 3: every kernel rule declares trust.level=core."""
|
|
130
|
+
errs: list[str] = []
|
|
131
|
+
# name -> artefact for category=rule entries. Manifest does not always
|
|
132
|
+
# populate `name` for rules, so fall back to the logical filename stem.
|
|
133
|
+
rule_by_name: dict[str, dict[str, Any]] = {}
|
|
134
|
+
for art in manifest.get("artefacts", []):
|
|
135
|
+
if art.get("category") != "rule":
|
|
136
|
+
continue
|
|
137
|
+
name = art.get("name")
|
|
138
|
+
if not name:
|
|
139
|
+
logical = strip_source_prefix(art.get("path", ""))
|
|
140
|
+
if logical is None:
|
|
141
|
+
continue
|
|
142
|
+
stem = Path(logical).stem
|
|
143
|
+
name = stem
|
|
144
|
+
rule_by_name[name] = art
|
|
145
|
+
|
|
146
|
+
for kname in sorted(kernel):
|
|
147
|
+
art = rule_by_name.get(kname)
|
|
148
|
+
if art is None:
|
|
149
|
+
errs.append(
|
|
150
|
+
f"kernel rule `{kname}` listed in router.json but no"
|
|
151
|
+
" matching artefact in manifest"
|
|
152
|
+
)
|
|
153
|
+
continue
|
|
154
|
+
level = (art.get("trust", {}) or {}).get("level")
|
|
155
|
+
if level != "core":
|
|
156
|
+
errs.append(
|
|
157
|
+
f"kernel rule `{kname}` has trust.level=`{level}`"
|
|
158
|
+
" — must be `core` (router.json kernel guarantees Iron-Law"
|
|
159
|
+
" floor)"
|
|
160
|
+
)
|
|
161
|
+
return errs
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def main(argv: list[str] | None = None) -> int:
|
|
165
|
+
parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
|
|
166
|
+
parser.add_argument("--quiet", action="store_true")
|
|
167
|
+
parser.add_argument(
|
|
168
|
+
"--manifest", type=Path, default=MANIFEST, help="discovery manifest"
|
|
169
|
+
)
|
|
170
|
+
parser.add_argument(
|
|
171
|
+
"--router", type=Path, default=ROUTER, help="router.json with kernel[]"
|
|
172
|
+
)
|
|
173
|
+
parser.add_argument(
|
|
174
|
+
"--compiled-src",
|
|
175
|
+
type=Path,
|
|
176
|
+
default=COMPILED_SRC,
|
|
177
|
+
help="compiled output root (.agent-src/)",
|
|
178
|
+
)
|
|
179
|
+
args = parser.parse_args(argv)
|
|
180
|
+
|
|
181
|
+
manifest = _load_manifest(args.manifest)
|
|
182
|
+
kernel = _load_kernel(args.router)
|
|
183
|
+
|
|
184
|
+
errs: list[str] = []
|
|
185
|
+
errs.extend(_check_pack_safety_floors(manifest))
|
|
186
|
+
errs.extend(_check_human_review_banners(manifest, args.compiled_src))
|
|
187
|
+
errs.extend(_check_kernel_trust(manifest, kernel))
|
|
188
|
+
|
|
189
|
+
if errs:
|
|
190
|
+
for e in errs:
|
|
191
|
+
print(f"ERROR: {e}", file=sys.stderr)
|
|
192
|
+
print(
|
|
193
|
+
f"\n{len(errs)} trust-coherence violation(s) across"
|
|
194
|
+
f" {len(manifest.get('packs', []))} pack(s) and"
|
|
195
|
+
f" {len(manifest.get('artefacts', []))} artefact(s).",
|
|
196
|
+
file=sys.stderr,
|
|
197
|
+
)
|
|
198
|
+
return 1
|
|
199
|
+
|
|
200
|
+
if not args.quiet:
|
|
201
|
+
print(
|
|
202
|
+
"✅ lint-trust-coherence:"
|
|
203
|
+
f" {len(manifest.get('packs', []))} pack(s),"
|
|
204
|
+
f" {len(kernel)} kernel rule(s),"
|
|
205
|
+
f" {sum(1 for a in manifest.get('artefacts', []) if (a.get('trust') or {}).get('human_review_required'))}"
|
|
206
|
+
" HRR artefact(s) clean."
|
|
207
|
+
)
|
|
208
|
+
return 0
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
if __name__ == "__main__":
|
|
212
|
+
raise SystemExit(main())
|
|
@@ -26,7 +26,9 @@ import sys
|
|
|
26
26
|
from pathlib import Path
|
|
27
27
|
|
|
28
28
|
REPO_ROOT = Path(__file__).resolve().parent.parent
|
|
29
|
-
|
|
29
|
+
sys.path.insert(0, str(REPO_ROOT / "scripts"))
|
|
30
|
+
from _lib.agent_src import artefact_roots # noqa: E402
|
|
31
|
+
|
|
30
32
|
OVERRIDES_FILE = REPO_ROOT / "docs" / "contracts" / "iron-law-overrides.txt"
|
|
31
33
|
TREND_FILE = REPO_ROOT / "agents" / "runtime" / ".rule-budget-history.jsonl"
|
|
32
34
|
|
|
@@ -93,8 +95,24 @@ def measure_rule(path: Path) -> dict[str, object]:
|
|
|
93
95
|
|
|
94
96
|
|
|
95
97
|
def collect() -> list[dict[str, object]]:
|
|
96
|
-
|
|
97
|
-
|
|
98
|
+
"""Collect rule measurements from every source root (multi-root aware).
|
|
99
|
+
|
|
100
|
+
Pre-move: reads .agent-src.uncompressed/rules/*.md.
|
|
101
|
+
Post-move (ADR-017): reads packages/*/.agent-src.uncompressed/rules/*.md.
|
|
102
|
+
Deduplicates on logical rule id (stem) — first root wins.
|
|
103
|
+
"""
|
|
104
|
+
seen: set[str] = set()
|
|
105
|
+
rules: list[dict[str, object]] = []
|
|
106
|
+
for root in artefact_roots():
|
|
107
|
+
rules_dir = root / "rules"
|
|
108
|
+
if not rules_dir.is_dir():
|
|
109
|
+
continue
|
|
110
|
+
for p in sorted(rules_dir.glob("*.md")):
|
|
111
|
+
if p.stem in seen:
|
|
112
|
+
continue
|
|
113
|
+
seen.add(p.stem)
|
|
114
|
+
rules.append(measure_rule(p))
|
|
115
|
+
return sorted(rules, key=lambda r: r["id"])
|
|
98
116
|
|
|
99
117
|
|
|
100
118
|
def load_overrides() -> set[str]:
|
|
@@ -138,7 +156,7 @@ def aggregate(rules: list[dict[str, object]]) -> dict[str, object]:
|
|
|
138
156
|
|
|
139
157
|
def render_table(rules: list[dict[str, object]], agg: dict[str, object]) -> str:
|
|
140
158
|
lines: list[str] = []
|
|
141
|
-
lines.append("Rule budget — source:
|
|
159
|
+
lines.append("Rule budget — source: rules/ under every artefact root (multi-root aware, ADR-017)")
|
|
142
160
|
lines.append("")
|
|
143
161
|
lines.append(f"{'id':<40} {'type':<7} {'tier':<5} {'chars':>7}")
|
|
144
162
|
lines.append("-" * 62)
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Move a single artefact between packs via ``git mv`` (history-preserving).
|
|
3
|
+
|
|
4
|
+
Phase 4.5 of the monorepo migration (ADR-017). Locates the artefact by
|
|
5
|
+
slug or logical path, computes its destination under the requested
|
|
6
|
+
pack, runs ``git mv`` for the artefact directory (skills/commands) or
|
|
7
|
+
the single file (rules), and rewrites the ``packs:`` frontmatter so the
|
|
8
|
+
discovery manifest stays in sync.
|
|
9
|
+
|
|
10
|
+
CLI:
|
|
11
|
+
--id ID artefact slug (skill/command name or rule stem)
|
|
12
|
+
--type TYPE skill | rule | command (required when --id ambiguous)
|
|
13
|
+
--to PACK target pack id (e.g. ``laravel``, ``core``)
|
|
14
|
+
--dry-run print the planned move and frontmatter edit, no FS changes
|
|
15
|
+
"""
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import argparse
|
|
19
|
+
import subprocess
|
|
20
|
+
import sys
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
|
|
23
|
+
import yaml
|
|
24
|
+
|
|
25
|
+
ROOT = Path(__file__).resolve().parents[1]
|
|
26
|
+
PACKAGES = ROOT / "packages"
|
|
27
|
+
PACKS_VOCAB = ROOT / "config" / "discovery" / "packs.yml"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _list_pack_ids() -> set[str]:
|
|
31
|
+
data = yaml.safe_load(PACKS_VOCAB.read_text(encoding="utf-8")) or []
|
|
32
|
+
return {p["id"] for p in data} | {"core"}
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _pack_dir(pack_id: str) -> Path:
|
|
36
|
+
return PACKAGES / ("core" if pack_id == "core" else f"pack-{pack_id}")
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _find_artefact(slug: str, kind: str | None) -> tuple[Path, str, str]:
|
|
40
|
+
"""Return (physical_path, detected_kind, current_pack_id)."""
|
|
41
|
+
hits: list[tuple[Path, str, str]] = []
|
|
42
|
+
for pkg in sorted(PACKAGES.iterdir()):
|
|
43
|
+
src = pkg / ".agent-src.uncompressed"
|
|
44
|
+
if not src.is_dir():
|
|
45
|
+
continue
|
|
46
|
+
pid = "core" if pkg.name == "core" else pkg.name.removeprefix("pack-")
|
|
47
|
+
for k, rel in (("skill", f"skills/{slug}/SKILL.md"),
|
|
48
|
+
("rule", f"rules/{slug}.md"),
|
|
49
|
+
("command", f"commands/{slug}.md")):
|
|
50
|
+
p = src / rel
|
|
51
|
+
if p.exists() and (kind is None or kind == k):
|
|
52
|
+
hits.append((p, k, pid))
|
|
53
|
+
if not hits:
|
|
54
|
+
raise SystemExit(f"error: artefact '{slug}' not found under any pack")
|
|
55
|
+
if len(hits) > 1 and kind is None:
|
|
56
|
+
kinds = ", ".join(sorted({h[1] for h in hits}))
|
|
57
|
+
raise SystemExit(f"error: '{slug}' ambiguous (found as: {kinds}); pass --type")
|
|
58
|
+
return hits[0]
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _move_root(path: Path, kind: str) -> Path:
|
|
62
|
+
"""Return the path to git-mv (directory for skills, file for rule/command)."""
|
|
63
|
+
return path.parent if kind == "skill" else path
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _rewrite_packs(md_path: Path, new_pack: str, dry_run: bool) -> bool:
|
|
67
|
+
text = md_path.read_text(encoding="utf-8")
|
|
68
|
+
if not text.startswith("---"):
|
|
69
|
+
return False
|
|
70
|
+
end = text.find("\n---", 4)
|
|
71
|
+
if end == -1:
|
|
72
|
+
return False
|
|
73
|
+
head = text[4:end]
|
|
74
|
+
body = text[end:]
|
|
75
|
+
fm = yaml.safe_load(head) or {}
|
|
76
|
+
if not isinstance(fm, dict):
|
|
77
|
+
return False
|
|
78
|
+
current = fm.get("packs") or []
|
|
79
|
+
desired = [] if new_pack == "core" else [new_pack]
|
|
80
|
+
if current == desired:
|
|
81
|
+
return False
|
|
82
|
+
if desired:
|
|
83
|
+
fm["packs"] = desired
|
|
84
|
+
else:
|
|
85
|
+
fm.pop("packs", None)
|
|
86
|
+
new_text = "---\n" + yaml.safe_dump(fm, sort_keys=False, allow_unicode=True) + body[1:]
|
|
87
|
+
if dry_run:
|
|
88
|
+
print(f" would rewrite frontmatter packs: {current} -> {desired}")
|
|
89
|
+
else:
|
|
90
|
+
md_path.write_text(new_text, encoding="utf-8")
|
|
91
|
+
print(f" rewrote frontmatter packs: {current} -> {desired}")
|
|
92
|
+
return True
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def main() -> int:
|
|
96
|
+
ap = argparse.ArgumentParser(description=__doc__)
|
|
97
|
+
ap.add_argument("--id", required=True, help="artefact slug")
|
|
98
|
+
ap.add_argument("--to", required=True, help="target pack id")
|
|
99
|
+
ap.add_argument("--type", choices=["skill", "rule", "command"])
|
|
100
|
+
ap.add_argument("--dry-run", action="store_true")
|
|
101
|
+
args = ap.parse_args()
|
|
102
|
+
|
|
103
|
+
vocab = _list_pack_ids()
|
|
104
|
+
if args.to not in vocab:
|
|
105
|
+
print(f"error: target pack '{args.to}' not in {sorted(vocab)}", file=sys.stderr)
|
|
106
|
+
return 2
|
|
107
|
+
|
|
108
|
+
src_md, kind, current_pack = _find_artefact(args.id, args.type)
|
|
109
|
+
if current_pack == args.to:
|
|
110
|
+
print(f"no-op: '{args.id}' already lives in pack '{args.to}'")
|
|
111
|
+
return 0
|
|
112
|
+
|
|
113
|
+
src_root = _move_root(src_md, kind)
|
|
114
|
+
dest_pkg_src = _pack_dir(args.to) / ".agent-src.uncompressed"
|
|
115
|
+
rel_under_pack = src_root.relative_to(_pack_dir(current_pack) / ".agent-src.uncompressed")
|
|
116
|
+
dest_root = dest_pkg_src / rel_under_pack
|
|
117
|
+
|
|
118
|
+
print(f"plan: {kind} '{args.id}' : {current_pack} -> {args.to}")
|
|
119
|
+
print(f" git mv {src_root.relative_to(ROOT)} {dest_root.relative_to(ROOT)}")
|
|
120
|
+
|
|
121
|
+
# Frontmatter must be rewritten BEFORE the move so the new physical
|
|
122
|
+
# location matches the declared pack. Discovery scanner cross-checks.
|
|
123
|
+
_rewrite_packs(src_md, args.to, args.dry_run)
|
|
124
|
+
|
|
125
|
+
if args.dry_run:
|
|
126
|
+
print("dry-run: no FS changes")
|
|
127
|
+
return 0
|
|
128
|
+
|
|
129
|
+
dest_root.parent.mkdir(parents=True, exist_ok=True)
|
|
130
|
+
result = subprocess.run(
|
|
131
|
+
["git", "mv", str(src_root.relative_to(ROOT)), str(dest_root.relative_to(ROOT))],
|
|
132
|
+
cwd=ROOT, capture_output=True, text=True,
|
|
133
|
+
)
|
|
134
|
+
if result.returncode != 0:
|
|
135
|
+
print(f"git mv failed: {result.stderr}", file=sys.stderr)
|
|
136
|
+
return result.returncode
|
|
137
|
+
print(f"moved: {src_root.relative_to(ROOT)} -> {dest_root.relative_to(ROOT)}")
|
|
138
|
+
print("next: run `task sync` and `task lint-pack-boundaries`")
|
|
139
|
+
return 0
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
if __name__ == "__main__":
|
|
143
|
+
sys.exit(main())
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Interactive scaffolder for new skills under the packages/ layout.
|
|
3
|
+
|
|
4
|
+
Phase 4.5 of the monorepo migration (ADR-017). Asks for pack, type,
|
|
5
|
+
name, workspaces, and description, then drops a templated artefact
|
|
6
|
+
into ``packages/<pack-dir>/.agent-src.uncompressed/<type>s/<name>/SKILL.md``.
|
|
7
|
+
|
|
8
|
+
Type → directory mapping:
|
|
9
|
+
- skill → skills/<name>/SKILL.md
|
|
10
|
+
- rule → rules/<name>.md
|
|
11
|
+
- command → commands/<name>.md
|
|
12
|
+
|
|
13
|
+
CLI (non-interactive overrides):
|
|
14
|
+
--pack PACK pack id (e.g. ``laravel`` or ``core``)
|
|
15
|
+
--type TYPE skill | rule | command (default: skill)
|
|
16
|
+
--name NAME artefact slug (kebab-case)
|
|
17
|
+
--description TEXT one-line description (trigger phrasing)
|
|
18
|
+
--workspace WS repeatable; defaults to pack's owner list
|
|
19
|
+
--force overwrite if file exists
|
|
20
|
+
"""
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import argparse
|
|
24
|
+
import sys
|
|
25
|
+
from pathlib import Path
|
|
26
|
+
from typing import Any
|
|
27
|
+
|
|
28
|
+
import yaml
|
|
29
|
+
|
|
30
|
+
ROOT = Path(__file__).resolve().parents[1]
|
|
31
|
+
PACKAGES = ROOT / "packages"
|
|
32
|
+
PACKS_VOCAB = ROOT / "config" / "discovery" / "packs.yml"
|
|
33
|
+
|
|
34
|
+
TEMPLATES = {
|
|
35
|
+
"skill": "skills/{name}/SKILL.md",
|
|
36
|
+
"rule": "rules/{name}.md",
|
|
37
|
+
"command": "commands/{name}.md",
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _load_vocab() -> dict[str, dict[str, Any]]:
|
|
42
|
+
if not PACKS_VOCAB.exists():
|
|
43
|
+
return {}
|
|
44
|
+
data = yaml.safe_load(PACKS_VOCAB.read_text(encoding="utf-8")) or []
|
|
45
|
+
return {p["id"]: p for p in data}
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _list_packs() -> list[str]:
|
|
49
|
+
if not PACKAGES.exists():
|
|
50
|
+
return []
|
|
51
|
+
return sorted([
|
|
52
|
+
("core" if p.name == "core" else p.name.removeprefix("pack-"))
|
|
53
|
+
for p in PACKAGES.iterdir() if p.is_dir()
|
|
54
|
+
])
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _pack_dir(pack_id: str) -> Path:
|
|
58
|
+
return PACKAGES / ("core" if pack_id == "core" else f"pack-{pack_id}")
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _prompt(label: str, default: str | None = None, choices: list[str] | None = None) -> str:
|
|
62
|
+
suffix = f" [{default}]" if default else ""
|
|
63
|
+
if choices:
|
|
64
|
+
suffix = f" ({'/'.join(choices)})" + suffix
|
|
65
|
+
while True:
|
|
66
|
+
raw = input(f"{label}{suffix}: ").strip()
|
|
67
|
+
if not raw and default is not None:
|
|
68
|
+
return default
|
|
69
|
+
if choices and raw not in choices:
|
|
70
|
+
print(f" must be one of: {', '.join(choices)}")
|
|
71
|
+
continue
|
|
72
|
+
if raw:
|
|
73
|
+
return raw
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _frontmatter(name: str, description: str, workspaces: list[str], pack: str) -> str:
|
|
77
|
+
fm: dict[str, Any] = {
|
|
78
|
+
"name": name,
|
|
79
|
+
"description": description,
|
|
80
|
+
"source": "package",
|
|
81
|
+
"workspaces": workspaces,
|
|
82
|
+
"packs": [pack] if pack != "core" else [],
|
|
83
|
+
"lifecycle": "active",
|
|
84
|
+
"trust": {"level": "professional", "confidence": "medium", "human_review_required": False},
|
|
85
|
+
"install": {"default": False, "removable": True},
|
|
86
|
+
}
|
|
87
|
+
if not fm["packs"]:
|
|
88
|
+
del fm["packs"]
|
|
89
|
+
return "---\n" + yaml.safe_dump(fm, sort_keys=False, allow_unicode=True) + "---\n"
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _body(kind: str, name: str, description: str) -> str:
|
|
93
|
+
if kind == "skill":
|
|
94
|
+
return (
|
|
95
|
+
f"\n# {name}\n\n## When to use\n\n{description}\n\n## Procedure\n\n"
|
|
96
|
+
"1. _TODO: replace with the real step-by-step._\n\n"
|
|
97
|
+
"## Examples\n\n_TODO: copy-pasteable example._\n"
|
|
98
|
+
)
|
|
99
|
+
if kind == "rule":
|
|
100
|
+
return f"\n# {name}\n\n{description}\n\n## Iron Law\n\n```\nTODO\n```\n"
|
|
101
|
+
return f"\n# {name}\n\n{description}\n\n## Steps\n\n1. _TODO_\n"
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def main() -> int:
|
|
105
|
+
ap = argparse.ArgumentParser(description=__doc__)
|
|
106
|
+
ap.add_argument("--pack")
|
|
107
|
+
ap.add_argument("--type", dest="kind", choices=list(TEMPLATES))
|
|
108
|
+
ap.add_argument("--name")
|
|
109
|
+
ap.add_argument("--description")
|
|
110
|
+
ap.add_argument("--workspace", action="append", default=[])
|
|
111
|
+
ap.add_argument("--force", action="store_true")
|
|
112
|
+
args = ap.parse_args()
|
|
113
|
+
|
|
114
|
+
packs = _list_packs()
|
|
115
|
+
if not packs:
|
|
116
|
+
print("error: no packages/ tree found", file=sys.stderr)
|
|
117
|
+
return 2
|
|
118
|
+
vocab = _load_vocab()
|
|
119
|
+
interactive = sys.stdin.isatty()
|
|
120
|
+
pack = args.pack or (_prompt("pack", default="core", choices=packs) if interactive else "core")
|
|
121
|
+
if pack not in packs:
|
|
122
|
+
print(f"error: pack '{pack}' not in {packs}", file=sys.stderr)
|
|
123
|
+
return 2
|
|
124
|
+
kind = args.kind or (_prompt("type", default="skill", choices=list(TEMPLATES)) if interactive else "skill")
|
|
125
|
+
name = args.name or (_prompt("name (kebab-case)") if interactive else "")
|
|
126
|
+
if not name or " " in name or name != name.lower():
|
|
127
|
+
print(f"error: name '{name}' must be lowercase kebab-case", file=sys.stderr)
|
|
128
|
+
return 2
|
|
129
|
+
description = args.description or (_prompt("description (one line)") if interactive else "TODO: describe trigger")
|
|
130
|
+
workspaces = args.workspace or vocab.get(pack, {}).get("workspaces") or ["engineering"]
|
|
131
|
+
|
|
132
|
+
rel = TEMPLATES[kind].format(name=name)
|
|
133
|
+
out = _pack_dir(pack) / ".agent-src.uncompressed" / rel
|
|
134
|
+
if out.exists() and not args.force:
|
|
135
|
+
print(f"error: {out.relative_to(ROOT)} exists (use --force)", file=sys.stderr)
|
|
136
|
+
return 1
|
|
137
|
+
out.parent.mkdir(parents=True, exist_ok=True)
|
|
138
|
+
out.write_text(_frontmatter(name, description, workspaces, pack) + _body(kind, name, description), encoding="utf-8")
|
|
139
|
+
print(f"created: {out.relative_to(ROOT)}")
|
|
140
|
+
print("next steps:")
|
|
141
|
+
print(" 1. flesh out the body")
|
|
142
|
+
print(" 2. run `task sync` to project into .agent-src/ and .augment/")
|
|
143
|
+
print(" 3. run `task lint-skills` for validation")
|
|
144
|
+
return 0
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
if __name__ == "__main__":
|
|
148
|
+
sys.exit(main())
|