@event4u/agent-config 3.0.0 → 3.1.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/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 +223 -125
- 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/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,353 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Plan + apply the physical monorepo migration (Phase 4).
|
|
3
|
+
|
|
4
|
+
Reads every `.md` artefact under `.agent-src.uncompressed/`, decides
|
|
5
|
+
its destination under `packages/core/` or `packages/pack-<id>/` using
|
|
6
|
+
the deterministic rules from
|
|
7
|
+
`agents/roadmaps/monorepo-phase-4-physical-package-layout.md` § Mapping
|
|
8
|
+
rules, and emits `dist/migration/move-plan.json`.
|
|
9
|
+
|
|
10
|
+
CLI:
|
|
11
|
+
--dry-run (default) emit the plan JSON only; no FS changes
|
|
12
|
+
--apply execute the moves via `git mv` (history-preserving)
|
|
13
|
+
|
|
14
|
+
Schema: see docs/contracts/move-plan.schema.json (added in this phase).
|
|
15
|
+
"""
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import argparse
|
|
19
|
+
import json
|
|
20
|
+
import subprocess
|
|
21
|
+
import sys
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
from typing import Any
|
|
24
|
+
|
|
25
|
+
import yaml
|
|
26
|
+
|
|
27
|
+
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
28
|
+
from validate_frontmatter import parse_frontmatter # noqa: E402
|
|
29
|
+
|
|
30
|
+
ROOT = Path(__file__).resolve().parents[1]
|
|
31
|
+
SRC = ROOT / ".agent-src.uncompressed"
|
|
32
|
+
PACKAGES = ROOT / "packages"
|
|
33
|
+
CORE = PACKAGES / "core" / ".agent-src.uncompressed"
|
|
34
|
+
VOCAB_DIR = ROOT / "config" / "discovery"
|
|
35
|
+
PLAN_OUT = ROOT / "dist" / "migration" / "move-plan.json"
|
|
36
|
+
UNASSIGNED_YAML = VOCAB_DIR / "unassigned-artefacts.yml"
|
|
37
|
+
|
|
38
|
+
# Locked kernel — `docs/contracts/kernel-membership.md` § 4. Pinned to
|
|
39
|
+
# core regardless of frontmatter (sanity check, not duplication).
|
|
40
|
+
KERNEL_RULES = frozenset({
|
|
41
|
+
"agent-authority",
|
|
42
|
+
"ask-when-uncertain",
|
|
43
|
+
"commit-policy",
|
|
44
|
+
"direct-answers",
|
|
45
|
+
"language-and-tone",
|
|
46
|
+
"no-cheap-questions",
|
|
47
|
+
"non-destructive-by-default",
|
|
48
|
+
"scope-control",
|
|
49
|
+
"verify-before-complete",
|
|
50
|
+
"user-interrupt-priority", # admitted post-P2.2
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
# Non-frontmatter trees that follow the host package (core) by default.
|
|
54
|
+
# Scaffold templates, profiles, presets, contexts, user-types, scripts,
|
|
55
|
+
# ghostwriter, packs — none of these carry pack metadata.
|
|
56
|
+
CORE_DIRS = (
|
|
57
|
+
"templates",
|
|
58
|
+
"profiles",
|
|
59
|
+
"presets",
|
|
60
|
+
"contexts",
|
|
61
|
+
"user-types",
|
|
62
|
+
"scripts",
|
|
63
|
+
"ghostwriter",
|
|
64
|
+
"packs",
|
|
65
|
+
"personas",
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _load_pack_ids() -> set[str]:
|
|
70
|
+
packs = yaml.safe_load((VOCAB_DIR / "packs.yml").read_text(encoding="utf-8")) or []
|
|
71
|
+
return {p["id"] for p in packs}
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _load_unassigned() -> dict[str, str]:
|
|
75
|
+
raw = yaml.safe_load(UNASSIGNED_YAML.read_text(encoding="utf-8")) or []
|
|
76
|
+
return {e["path"]: e["reason"] for e in raw}
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _is_core(fm: dict[str, Any] | None, stem: str) -> bool:
|
|
80
|
+
if stem in KERNEL_RULES:
|
|
81
|
+
return True
|
|
82
|
+
if fm is None:
|
|
83
|
+
return False
|
|
84
|
+
trust = fm.get("trust") or {}
|
|
85
|
+
install = fm.get("install") or {}
|
|
86
|
+
return (
|
|
87
|
+
trust.get("level") == "core"
|
|
88
|
+
and install.get("removable") is False
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _primary_pack(fm: dict[str, Any] | None) -> str | None:
|
|
93
|
+
"""Round-2 council refinement R1: explicit ``primary_pack:`` wins over
|
|
94
|
+
``packs[0]`` fallback. The ``primary_pack`` lint lands in Phase 4.4.
|
|
95
|
+
"""
|
|
96
|
+
if not fm:
|
|
97
|
+
return None
|
|
98
|
+
explicit = fm.get("primary_pack")
|
|
99
|
+
if isinstance(explicit, str) and explicit.strip():
|
|
100
|
+
return explicit.strip()
|
|
101
|
+
packs = fm.get("packs")
|
|
102
|
+
if not isinstance(packs, list) or not packs:
|
|
103
|
+
return None
|
|
104
|
+
return packs[0]
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _dest_for(src: Path, fm: dict[str, Any] | None, pack_ids: set[str]) -> tuple[Path, str, str | None]:
|
|
108
|
+
"""Return (destination_path, reason, conflict_reason_or_None)."""
|
|
109
|
+
rel = src.relative_to(SRC)
|
|
110
|
+
parts = rel.parts
|
|
111
|
+
top = parts[0] if parts else ""
|
|
112
|
+
|
|
113
|
+
# Non-frontmatter trees → core verbatim.
|
|
114
|
+
if top in CORE_DIRS:
|
|
115
|
+
return CORE / rel, f"core dir: {top}/", None
|
|
116
|
+
|
|
117
|
+
stem = src.stem if src.name != "SKILL.md" else src.parent.name
|
|
118
|
+
|
|
119
|
+
if _is_core(fm, stem):
|
|
120
|
+
reason = "kernel rule" if stem in KERNEL_RULES else "trust.level=core + install.removable=false"
|
|
121
|
+
return CORE / rel, reason, None
|
|
122
|
+
|
|
123
|
+
primary = _primary_pack(fm)
|
|
124
|
+
if primary is None:
|
|
125
|
+
return CORE / rel, "no primary pack — falling back to core", "missing primary pack"
|
|
126
|
+
if primary not in pack_ids:
|
|
127
|
+
return CORE / rel, f"unknown pack '{primary}' — falling back to core", f"unknown pack: {primary}"
|
|
128
|
+
if primary == "meta":
|
|
129
|
+
# meta = package-internal scaffolding; lives in core alongside the kernel.
|
|
130
|
+
return CORE / rel, "primary pack: meta (package internals → core)", None
|
|
131
|
+
|
|
132
|
+
dest_root = PACKAGES / f"pack-{primary}" / ".agent-src.uncompressed"
|
|
133
|
+
return dest_root / rel, f"primary pack: {primary}", None
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
# Filesystem artefacts that must never be moved (runtime caches, scratch).
|
|
137
|
+
_SKIP_DIR_NAMES = frozenset({".pytest_cache", "__pycache__", ".mypy_cache",
|
|
138
|
+
".ruff_cache", "node_modules", ".DS_Store"})
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _should_skip(p: Path) -> bool:
|
|
142
|
+
return any(part in _SKIP_DIR_NAMES for part in p.parts)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def _gitignored(paths: list[Path]) -> set[Path]:
|
|
146
|
+
"""Return the subset of paths matched by .gitignore (runtime artefacts,
|
|
147
|
+
eval last-run.json, caches, …). These never participate in `git mv`."""
|
|
148
|
+
if not paths:
|
|
149
|
+
return set()
|
|
150
|
+
rel = [p.relative_to(ROOT).as_posix() for p in paths]
|
|
151
|
+
result = subprocess.run(
|
|
152
|
+
["git", "check-ignore", "--stdin"],
|
|
153
|
+
cwd=ROOT,
|
|
154
|
+
input="\n".join(rel),
|
|
155
|
+
capture_output=True,
|
|
156
|
+
text=True,
|
|
157
|
+
)
|
|
158
|
+
# check-ignore exits 0 when at least one path matched, 1 when none, >1 on error.
|
|
159
|
+
if result.returncode > 1:
|
|
160
|
+
return set()
|
|
161
|
+
ignored = {line.strip() for line in result.stdout.splitlines() if line.strip()}
|
|
162
|
+
return {ROOT / r for r in rel if r in ignored}
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def _iter_artefacts() -> list[Path]:
|
|
166
|
+
"""Every source file under SRC. Includes non-md so the plan covers
|
|
167
|
+
the full tree (templates/, scripts/, packs/, hooks, .gitattributes
|
|
168
|
+
fragments, …). Runtime caches and gitignored files are excluded.
|
|
169
|
+
"""
|
|
170
|
+
paths: list[Path] = []
|
|
171
|
+
for p in sorted(SRC.rglob("*")):
|
|
172
|
+
if not p.is_file():
|
|
173
|
+
continue
|
|
174
|
+
if _should_skip(p):
|
|
175
|
+
continue
|
|
176
|
+
paths.append(p)
|
|
177
|
+
ignored = _gitignored(paths)
|
|
178
|
+
return [p for p in paths if p not in ignored]
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def _find_owning_skill_fm(src: Path) -> dict[str, Any] | None:
|
|
182
|
+
"""For a non-SKILL.md file under skills/<name>/, return the sibling SKILL.md frontmatter.
|
|
183
|
+
|
|
184
|
+
Looks first at the source location (pre-move) and then under
|
|
185
|
+
``packages/*/.agent-src.uncompressed/skills/<name>/SKILL.md`` so the
|
|
186
|
+
planner can resume mid-migration when the parent SKILL.md has already
|
|
187
|
+
been relocated.
|
|
188
|
+
"""
|
|
189
|
+
if "skills" not in src.parts:
|
|
190
|
+
return None
|
|
191
|
+
idx = src.parts.index("skills")
|
|
192
|
+
if idx + 1 >= len(src.parts):
|
|
193
|
+
return None
|
|
194
|
+
skill_name = src.parts[idx + 1]
|
|
195
|
+
candidates = [ROOT / Path(*src.parts[: idx + 2]) / "SKILL.md"]
|
|
196
|
+
pkgs_root = ROOT / "packages"
|
|
197
|
+
if pkgs_root.exists():
|
|
198
|
+
for pkg in pkgs_root.iterdir():
|
|
199
|
+
cand = pkg / ".agent-src.uncompressed" / "skills" / skill_name / "SKILL.md"
|
|
200
|
+
if cand.exists():
|
|
201
|
+
candidates.append(cand)
|
|
202
|
+
for cand in candidates:
|
|
203
|
+
if cand.exists():
|
|
204
|
+
parsed, _ = parse_frontmatter(cand.read_text(encoding="utf-8", errors="replace"))
|
|
205
|
+
if isinstance(parsed, dict):
|
|
206
|
+
return parsed
|
|
207
|
+
return None
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def _build_plan() -> dict[str, Any]:
|
|
211
|
+
pack_ids = _load_pack_ids()
|
|
212
|
+
unassigned = _load_unassigned()
|
|
213
|
+
moves: list[dict[str, Any]] = []
|
|
214
|
+
stays_in_core: list[dict[str, Any]] = []
|
|
215
|
+
conflicts: list[dict[str, Any]] = []
|
|
216
|
+
|
|
217
|
+
for src in _iter_artefacts():
|
|
218
|
+
rel_src = src.relative_to(ROOT).as_posix()
|
|
219
|
+
fm: dict[str, Any] | None = None
|
|
220
|
+
# Only .md files carry artefact frontmatter. Non-md files (yaml,
|
|
221
|
+
# python, php, hooks, .gitattributes fragments, …) inherit their
|
|
222
|
+
# placement from the parent directory or sibling SKILL.md.
|
|
223
|
+
if src.suffix == ".md":
|
|
224
|
+
try:
|
|
225
|
+
text = src.read_text(encoding="utf-8", errors="replace")
|
|
226
|
+
parsed, _ = parse_frontmatter(text)
|
|
227
|
+
if isinstance(parsed, dict):
|
|
228
|
+
fm = parsed
|
|
229
|
+
except Exception as exc: # noqa: BLE001
|
|
230
|
+
conflicts.append({"path": rel_src, "reason": f"parse error: {exc}"})
|
|
231
|
+
continue
|
|
232
|
+
|
|
233
|
+
# Quarantined scaffolds → core, no conflict.
|
|
234
|
+
if rel_src in unassigned and fm is None:
|
|
235
|
+
dest = CORE / src.relative_to(SRC)
|
|
236
|
+
stays_in_core.append({
|
|
237
|
+
"from": rel_src,
|
|
238
|
+
"to": dest.relative_to(ROOT).as_posix(),
|
|
239
|
+
"reason": f"unassigned scaffold: {unassigned[rel_src]}",
|
|
240
|
+
})
|
|
241
|
+
continue
|
|
242
|
+
|
|
243
|
+
# Skill auxiliary files (sub-pages, prompt fragments) inherit the
|
|
244
|
+
# parent SKILL.md's pack/trust. They never carry their own frontmatter.
|
|
245
|
+
inherited = False
|
|
246
|
+
if fm is None and src.name != "SKILL.md":
|
|
247
|
+
owner_fm = _find_owning_skill_fm(src)
|
|
248
|
+
if owner_fm is not None:
|
|
249
|
+
fm = owner_fm
|
|
250
|
+
inherited = True
|
|
251
|
+
|
|
252
|
+
dest, reason, conflict = _dest_for(src, fm, pack_ids)
|
|
253
|
+
if inherited:
|
|
254
|
+
reason = f"inherits parent SKILL.md → {reason}"
|
|
255
|
+
entry = {
|
|
256
|
+
"from": rel_src,
|
|
257
|
+
"to": dest.relative_to(ROOT).as_posix(),
|
|
258
|
+
"reason": reason,
|
|
259
|
+
}
|
|
260
|
+
if conflict:
|
|
261
|
+
conflicts.append({"path": rel_src, "reason": conflict, "fallback_to": entry["to"]})
|
|
262
|
+
if dest.is_relative_to(CORE):
|
|
263
|
+
stays_in_core.append(entry)
|
|
264
|
+
else:
|
|
265
|
+
moves.append(entry)
|
|
266
|
+
|
|
267
|
+
return {
|
|
268
|
+
"schema_version": "1",
|
|
269
|
+
"source_root": SRC.relative_to(ROOT).as_posix(),
|
|
270
|
+
"packages_root": PACKAGES.relative_to(ROOT).as_posix(),
|
|
271
|
+
"totals": {
|
|
272
|
+
"moves": len(moves),
|
|
273
|
+
"stays_in_core": len(stays_in_core),
|
|
274
|
+
"conflicts": len(conflicts),
|
|
275
|
+
},
|
|
276
|
+
"moves": moves,
|
|
277
|
+
"stays_in_core": stays_in_core,
|
|
278
|
+
"conflicts": conflicts,
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def _write_plan(plan: dict[str, Any]) -> None:
|
|
283
|
+
PLAN_OUT.parent.mkdir(parents=True, exist_ok=True)
|
|
284
|
+
PLAN_OUT.write_text(
|
|
285
|
+
json.dumps(plan, indent=2, sort_keys=False, ensure_ascii=False) + "\n",
|
|
286
|
+
encoding="utf-8",
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def _apply(plan: dict[str, Any]) -> int:
|
|
291
|
+
"""Execute every move + stay via `git mv` so history follows.
|
|
292
|
+
|
|
293
|
+
Idempotent: entries whose source is already absent and whose
|
|
294
|
+
destination already exists are silently skipped. This lets a
|
|
295
|
+
partial run be resumed without rewinding the worktree.
|
|
296
|
+
"""
|
|
297
|
+
if plan["conflicts"]:
|
|
298
|
+
print(f"ERROR: {len(plan['conflicts'])} unresolved conflict(s); refusing --apply.", file=sys.stderr)
|
|
299
|
+
return 2
|
|
300
|
+
|
|
301
|
+
all_entries = plan["moves"] + plan["stays_in_core"]
|
|
302
|
+
moved = 0
|
|
303
|
+
skipped = 0
|
|
304
|
+
for entry in all_entries:
|
|
305
|
+
src = ROOT / entry["from"]
|
|
306
|
+
dst = ROOT / entry["to"]
|
|
307
|
+
if not src.exists():
|
|
308
|
+
if dst.exists():
|
|
309
|
+
skipped += 1
|
|
310
|
+
continue
|
|
311
|
+
print(f"ERROR: source missing AND destination missing: {entry['from']}", file=sys.stderr)
|
|
312
|
+
return 3
|
|
313
|
+
dst.parent.mkdir(parents=True, exist_ok=True)
|
|
314
|
+
result = subprocess.run(
|
|
315
|
+
["git", "mv", str(src), str(dst)],
|
|
316
|
+
cwd=ROOT,
|
|
317
|
+
capture_output=True,
|
|
318
|
+
text=True,
|
|
319
|
+
)
|
|
320
|
+
if result.returncode != 0:
|
|
321
|
+
print(f"ERROR: git mv failed: {entry['from']} -> {entry['to']}\n{result.stderr}", file=sys.stderr)
|
|
322
|
+
return 4
|
|
323
|
+
moved += 1
|
|
324
|
+
print(f"Applied {moved} moves, skipped {skipped} already-moved entries.")
|
|
325
|
+
return 0
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def main() -> int:
|
|
329
|
+
ap = argparse.ArgumentParser(description=__doc__)
|
|
330
|
+
ap.add_argument("--apply", action="store_true", help="Execute moves via git mv (default: dry-run only)")
|
|
331
|
+
ap.add_argument("--out", type=Path, default=PLAN_OUT, help="Plan JSON output path")
|
|
332
|
+
args = ap.parse_args()
|
|
333
|
+
|
|
334
|
+
plan = _build_plan()
|
|
335
|
+
PLAN_OUT.parent.mkdir(parents=True, exist_ok=True)
|
|
336
|
+
args.out.parent.mkdir(parents=True, exist_ok=True)
|
|
337
|
+
args.out.write_text(
|
|
338
|
+
json.dumps(plan, indent=2, sort_keys=False, ensure_ascii=False) + "\n",
|
|
339
|
+
encoding="utf-8",
|
|
340
|
+
)
|
|
341
|
+
print(f"Plan: {args.out.relative_to(ROOT)}")
|
|
342
|
+
print(f" moves : {plan['totals']['moves']}")
|
|
343
|
+
print(f" stays_in_core : {plan['totals']['stays_in_core']}")
|
|
344
|
+
print(f" conflicts : {plan['totals']['conflicts']}")
|
|
345
|
+
|
|
346
|
+
if args.apply:
|
|
347
|
+
return _apply(plan)
|
|
348
|
+
|
|
349
|
+
return 1 if plan["conflicts"] else 0
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
if __name__ == "__main__":
|
|
353
|
+
sys.exit(main())
|
|
@@ -32,13 +32,36 @@ except ImportError as exc:
|
|
|
32
32
|
) from exc
|
|
33
33
|
|
|
34
34
|
REPO_ROOT = Path(__file__).resolve().parent.parent
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
35
|
+
|
|
36
|
+
# Post-monorepo Phase 4 the detection map lives under any package's
|
|
37
|
+
# .agent-src.uncompressed/. Discover it via the shared helper; fall
|
|
38
|
+
# back to the legacy flat path so consumers and older sub-trees still
|
|
39
|
+
# work.
|
|
40
|
+
import sys as _sys # noqa: E402
|
|
41
|
+
|
|
42
|
+
_sys.path.insert(0, str(REPO_ROOT / "scripts"))
|
|
43
|
+
try:
|
|
44
|
+
from _lib.agent_src import artefact_roots as _artefact_roots
|
|
45
|
+
except ImportError:
|
|
46
|
+
_artefact_roots = None
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _discover_default_map() -> Path:
|
|
50
|
+
if _artefact_roots is not None:
|
|
51
|
+
for root in _artefact_roots():
|
|
52
|
+
candidate = root / "skills" / "refine-ticket" / "detection-map.yml"
|
|
53
|
+
if candidate.is_file():
|
|
54
|
+
return candidate
|
|
55
|
+
return (
|
|
56
|
+
REPO_ROOT
|
|
57
|
+
/ ".agent-src.uncompressed"
|
|
58
|
+
/ "skills"
|
|
59
|
+
/ "refine-ticket"
|
|
60
|
+
/ "detection-map.yml"
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
DEFAULT_MAP = _discover_default_map()
|
|
42
65
|
|
|
43
66
|
# Composite tokens that contain a sub-skill keyword as a substring but
|
|
44
67
|
# are not themselves triggers (Phase F2). Matched with word boundaries
|
|
@@ -157,6 +157,10 @@
|
|
|
157
157
|
"removable": {"type": "boolean"}
|
|
158
158
|
},
|
|
159
159
|
"description": "ADR-013 install hints (default-on at consumer install; user-removable)."
|
|
160
|
+
},
|
|
161
|
+
"gui_runnable": {
|
|
162
|
+
"type": "boolean",
|
|
163
|
+
"description": "AI OS Product UI allowlist (road-to-ai-os-product-ui.md Phase 1). When true, the command is exposed in the browser /tasks surface and may be spawned by POST /api/v1/task/run. Default false — commands that touch git, secrets, or run destructive migrations must stay false (terminal only). Per-command opt-in, never opt-out."
|
|
160
164
|
}
|
|
161
165
|
}
|
|
162
166
|
}
|