@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
|
@@ -20,6 +20,7 @@ Exit codes: 0 = green; 1 = one or more checks failed; 2 = setup error.
|
|
|
20
20
|
"""
|
|
21
21
|
from __future__ import annotations
|
|
22
22
|
|
|
23
|
+
import os
|
|
23
24
|
import shutil
|
|
24
25
|
import subprocess
|
|
25
26
|
import sys
|
|
@@ -49,8 +50,10 @@ def _check_installer_runs(tmpdir: Path) -> tuple[int, Path | None]:
|
|
|
49
50
|
str(ROOT),
|
|
50
51
|
"--skip-bridges",
|
|
51
52
|
]
|
|
53
|
+
# ADR-020: --project is reserved for maintainers; CI is a maintainer context.
|
|
54
|
+
env = {**os.environ, "AGENT_CONFIG_DEV_MODE": "1"}
|
|
52
55
|
try:
|
|
53
|
-
result = subprocess.run(cmd, capture_output=True, text=True, timeout=60)
|
|
56
|
+
result = subprocess.run(cmd, capture_output=True, text=True, timeout=60, env=env)
|
|
54
57
|
except subprocess.TimeoutExpired:
|
|
55
58
|
return _fail("installer timed out after 60s"), None
|
|
56
59
|
if result.returncode != 0:
|
|
@@ -82,7 +85,13 @@ def _check_default_profile(settings: Path) -> int:
|
|
|
82
85
|
|
|
83
86
|
def _check_decision_engine_block(settings: Path) -> int:
|
|
84
87
|
"""Step 3 — decision_engine block parses through the engine parser."""
|
|
85
|
-
sys.path.insert(0, str(ROOT / "
|
|
88
|
+
sys.path.insert(0, str(ROOT / "scripts"))
|
|
89
|
+
from _lib.agent_src import resolve_logical # noqa: E402
|
|
90
|
+
|
|
91
|
+
template_scripts = resolve_logical("templates/scripts") or (
|
|
92
|
+
ROOT / ".agent-src.uncompressed" / "templates" / "scripts"
|
|
93
|
+
)
|
|
94
|
+
sys.path.insert(0, str(template_scripts))
|
|
86
95
|
try:
|
|
87
96
|
from work_engine.scoring.decision_engine import ( # type: ignore[import-not-found]
|
|
88
97
|
DecisionEngineSettings,
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Snapshot the agent-config build outputs for byte-identity verification.
|
|
3
|
+
|
|
4
|
+
Used by monorepo Phase 4 (physical layout move) to assert that the
|
|
5
|
+
pre-move and post-move `task sync` + `task build-discovery` outputs
|
|
6
|
+
match byte-for-byte except for `artefacts[].path` values.
|
|
7
|
+
|
|
8
|
+
Captures sha256 of every file under:
|
|
9
|
+
- .agent-src/
|
|
10
|
+
- .augment/
|
|
11
|
+
- dist/discovery/discovery-manifest.json (also stores parsed copy
|
|
12
|
+
with paths stripped so the post-move diff is path-only)
|
|
13
|
+
|
|
14
|
+
CLI:
|
|
15
|
+
--out PATH write JSON to this path (default: dist/migration/pre-move-snapshot.json)
|
|
16
|
+
"""
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import argparse
|
|
20
|
+
import hashlib
|
|
21
|
+
import json
|
|
22
|
+
import sys
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
from typing import Any
|
|
25
|
+
|
|
26
|
+
ROOT = Path(__file__).resolve().parents[1]
|
|
27
|
+
DEFAULT_OUT = ROOT / "dist" / "migration" / "pre-move-snapshot.json"
|
|
28
|
+
|
|
29
|
+
TARGETS = (
|
|
30
|
+
ROOT / ".agent-src",
|
|
31
|
+
ROOT / ".augment",
|
|
32
|
+
)
|
|
33
|
+
MANIFEST = ROOT / "dist" / "discovery" / "discovery-manifest.json"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _sha256(path: Path) -> str:
|
|
37
|
+
h = hashlib.sha256()
|
|
38
|
+
with path.open("rb") as f:
|
|
39
|
+
for chunk in iter(lambda: f.read(65536), b""):
|
|
40
|
+
h.update(chunk)
|
|
41
|
+
return h.hexdigest()
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
# Runtime artefacts that never participate in byte-identity verification.
|
|
45
|
+
# Eval last-run.json + pytest caches are gitignored; including them just
|
|
46
|
+
# adds noise when the worktree is clean.
|
|
47
|
+
_SKIP_NAMES = frozenset({"last-run.json"})
|
|
48
|
+
_SKIP_DIRS = frozenset({".pytest_cache", "__pycache__", ".mypy_cache",
|
|
49
|
+
".ruff_cache", "node_modules", ".DS_Store"})
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _hash_tree(root: Path) -> dict[str, str]:
|
|
53
|
+
if not root.exists():
|
|
54
|
+
return {}
|
|
55
|
+
hashes: dict[str, str] = {}
|
|
56
|
+
for p in sorted(root.rglob("*")):
|
|
57
|
+
if not p.is_file():
|
|
58
|
+
continue
|
|
59
|
+
if p.name in _SKIP_NAMES:
|
|
60
|
+
continue
|
|
61
|
+
if any(part in _SKIP_DIRS for part in p.parts):
|
|
62
|
+
continue
|
|
63
|
+
hashes[p.relative_to(ROOT).as_posix()] = _sha256(p)
|
|
64
|
+
return hashes
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _logical_path(rel: str) -> str:
|
|
68
|
+
"""Strip any source-root prefix (legacy or packages/*) so the diff
|
|
69
|
+
compares the artefact's logical identity, not its physical location.
|
|
70
|
+
Non-source paths are returned unchanged.
|
|
71
|
+
"""
|
|
72
|
+
posix = rel.replace("\\", "/")
|
|
73
|
+
if posix.startswith(".agent-src.uncompressed/"):
|
|
74
|
+
return posix[len(".agent-src.uncompressed/"):]
|
|
75
|
+
if posix.startswith("packages/"):
|
|
76
|
+
marker = "/.agent-src.uncompressed/"
|
|
77
|
+
idx = posix.find(marker)
|
|
78
|
+
if idx != -1:
|
|
79
|
+
return posix[idx + len(marker):]
|
|
80
|
+
return posix
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _manifest_path_stripped(manifest_path: Path) -> dict[str, Any] | None:
|
|
84
|
+
if not manifest_path.exists():
|
|
85
|
+
return None
|
|
86
|
+
data = json.loads(manifest_path.read_text(encoding="utf-8"))
|
|
87
|
+
# Strip `path` from every artefact so the diff is path-only, then
|
|
88
|
+
# re-sort by (category, checksum) so the list order is content-stable
|
|
89
|
+
# — the original sort is path-based, which shifts when files move
|
|
90
|
+
# between roots even though no artefact body changed.
|
|
91
|
+
artefacts = data.get("artefacts", []) or []
|
|
92
|
+
for a in artefacts:
|
|
93
|
+
a.pop("path", None)
|
|
94
|
+
artefacts.sort(key=lambda a: (a.get("category", ""), a.get("checksum", "")))
|
|
95
|
+
data["artefacts"] = artefacts
|
|
96
|
+
# Normalise unassigned / documented_unassigned to logical paths and
|
|
97
|
+
# re-sort so the post-move diff is content-only.
|
|
98
|
+
for key in ("unassigned", "documented_unassigned"):
|
|
99
|
+
entries = data.get(key) or []
|
|
100
|
+
for e in entries:
|
|
101
|
+
if isinstance(e, dict) and "path" in e:
|
|
102
|
+
e["path"] = _logical_path(e["path"])
|
|
103
|
+
entries.sort(key=lambda e: (e.get("path", ""), e.get("category", "")))
|
|
104
|
+
data[key] = entries
|
|
105
|
+
# Drop volatile fields: timestamp, the manifest's own checksum (which
|
|
106
|
+
# covers everything above and changes with any path text), and the
|
|
107
|
+
# scanner_version (sha of the build script — moves with code edits).
|
|
108
|
+
data.pop("generated_at", None)
|
|
109
|
+
data.pop("checksum", None)
|
|
110
|
+
data.pop("scanner_version", None)
|
|
111
|
+
return data
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _build_snapshot() -> dict[str, Any]:
|
|
115
|
+
snap: dict[str, Any] = {"schema_version": "1", "trees": {}}
|
|
116
|
+
for tgt in TARGETS:
|
|
117
|
+
key = tgt.relative_to(ROOT).as_posix()
|
|
118
|
+
snap["trees"][key] = _hash_tree(tgt)
|
|
119
|
+
snap["manifest_sha256"] = _sha256(MANIFEST) if MANIFEST.exists() else None
|
|
120
|
+
snap["manifest_path_stripped"] = _manifest_path_stripped(MANIFEST)
|
|
121
|
+
return snap
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def main() -> int:
|
|
125
|
+
ap = argparse.ArgumentParser(description=__doc__)
|
|
126
|
+
ap.add_argument("--out", type=Path, default=DEFAULT_OUT)
|
|
127
|
+
args = ap.parse_args()
|
|
128
|
+
|
|
129
|
+
snap = _build_snapshot()
|
|
130
|
+
args.out.parent.mkdir(parents=True, exist_ok=True)
|
|
131
|
+
args.out.write_text(
|
|
132
|
+
json.dumps(snap, indent=2, sort_keys=True, ensure_ascii=False) + "\n",
|
|
133
|
+
encoding="utf-8",
|
|
134
|
+
)
|
|
135
|
+
n_files = sum(len(t) for t in snap["trees"].values())
|
|
136
|
+
print(f"Snapshot: {args.out.relative_to(ROOT)}")
|
|
137
|
+
print(f" files hashed : {n_files}")
|
|
138
|
+
print(f" trees : {list(snap['trees'])}")
|
|
139
|
+
print(f" manifest sha256 : {snap['manifest_sha256'][:16] if snap['manifest_sha256'] else 'MISSING'}")
|
|
140
|
+
return 0
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
if __name__ == "__main__":
|
|
144
|
+
sys.exit(main())
|
package/scripts/update_counts.py
CHANGED
|
@@ -20,31 +20,59 @@ import re
|
|
|
20
20
|
import sys
|
|
21
21
|
from pathlib import Path
|
|
22
22
|
|
|
23
|
+
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
24
|
+
from _lib.agent_src import artefact_roots # noqa: E402
|
|
25
|
+
|
|
23
26
|
REPO_ROOT = Path(__file__).resolve().parent.parent
|
|
24
|
-
SRC = REPO_ROOT / ".agent-src.uncompressed"
|
|
25
27
|
|
|
26
28
|
|
|
27
29
|
def count(kind: str) -> int:
|
|
28
|
-
if kind == "skills":
|
|
29
|
-
return sum(1 for _ in (SRC / "skills").rglob("SKILL.md"))
|
|
30
30
|
if kind == "guidelines":
|
|
31
31
|
# Guidelines live under docs/guidelines/{topic}/ — they are reference
|
|
32
32
|
# material, not packaged artefacts. Recursive walk to count every .md.
|
|
33
33
|
return sum(1 for _ in (REPO_ROOT / "docs" / "guidelines").rglob("*.md"))
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
34
|
+
total = 0
|
|
35
|
+
seen: set[str] = set()
|
|
36
|
+
for root in artefact_roots():
|
|
37
|
+
subdir = root / kind
|
|
38
|
+
if not subdir.exists():
|
|
39
|
+
continue
|
|
40
|
+
if kind == "skills":
|
|
41
|
+
for f in subdir.rglob("SKILL.md"):
|
|
42
|
+
rel = f.relative_to(root).as_posix()
|
|
43
|
+
if rel in seen:
|
|
44
|
+
continue
|
|
45
|
+
seen.add(rel)
|
|
46
|
+
total += 1
|
|
47
|
+
elif kind == "personas":
|
|
48
|
+
# personas live as flat .md files, README excluded
|
|
49
|
+
for f in subdir.glob("*.md"):
|
|
50
|
+
if f.name == "README.md":
|
|
51
|
+
continue
|
|
52
|
+
rel = f.relative_to(root).as_posix()
|
|
53
|
+
if rel in seen:
|
|
54
|
+
continue
|
|
55
|
+
seen.add(rel)
|
|
56
|
+
total += 1
|
|
57
|
+
elif kind == "commands":
|
|
58
|
+
# Commands may be flat or nested under a cluster directory.
|
|
59
|
+
# Skip the AGENTS.md reference orchestrator.
|
|
60
|
+
for f in subdir.rglob("*.md"):
|
|
61
|
+
if f.name == "AGENTS.md":
|
|
62
|
+
continue
|
|
63
|
+
rel = f.relative_to(root).as_posix()
|
|
64
|
+
if rel in seen:
|
|
65
|
+
continue
|
|
66
|
+
seen.add(rel)
|
|
67
|
+
total += 1
|
|
68
|
+
else:
|
|
69
|
+
for f in subdir.glob("*.md"):
|
|
70
|
+
rel = f.relative_to(root).as_posix()
|
|
71
|
+
if rel in seen:
|
|
72
|
+
continue
|
|
73
|
+
seen.add(rel)
|
|
74
|
+
total += 1
|
|
75
|
+
return total
|
|
48
76
|
|
|
49
77
|
|
|
50
78
|
# file → list of (regex, kind)
|
|
@@ -27,7 +27,15 @@ except ImportError: # pragma: no cover — bootstrap guard
|
|
|
27
27
|
sys.exit(3)
|
|
28
28
|
|
|
29
29
|
REPO_ROOT = Path(__file__).resolve().parent.parent
|
|
30
|
-
|
|
30
|
+
sys.path.insert(0, str(REPO_ROOT / "scripts"))
|
|
31
|
+
from _lib.agent_src import resolve_logical # noqa: E402
|
|
32
|
+
|
|
33
|
+
# Post-ADR-017 the templates/ tree lives under packages/core/; fall back
|
|
34
|
+
# to the legacy root for pre-move checkouts.
|
|
35
|
+
_template_scripts = resolve_logical("templates/scripts")
|
|
36
|
+
TEMPLATE_SCRIPTS = _template_scripts or (
|
|
37
|
+
REPO_ROOT / ".agent-src.uncompressed" / "templates" / "scripts"
|
|
38
|
+
)
|
|
31
39
|
if str(TEMPLATE_SCRIPTS) not in sys.path:
|
|
32
40
|
sys.path.insert(0, str(TEMPLATE_SCRIPTS))
|
|
33
41
|
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Stale-manifest guard — re-builds the manifest in memory and diffs it
|
|
3
|
+
against the committed ``dist/discovery/discovery-manifest.json``.
|
|
4
|
+
|
|
5
|
+
CI runs this after a freshly-checked-out tree; non-zero diff = somebody
|
|
6
|
+
forgot to regenerate the manifest after touching artefact frontmatter.
|
|
7
|
+
|
|
8
|
+
The ``generated_at`` field is normalised on both sides (wall-clock).
|
|
9
|
+
Everything else MUST match byte-for-byte.
|
|
10
|
+
|
|
11
|
+
CLI:
|
|
12
|
+
python scripts/validate_discovery_manifest.py [--quiet]
|
|
13
|
+
|
|
14
|
+
Exit codes:
|
|
15
|
+
0 manifest on disk matches a fresh re-build
|
|
16
|
+
1 drift detected (committed manifest is stale)
|
|
17
|
+
"""
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import argparse
|
|
21
|
+
import json
|
|
22
|
+
import subprocess
|
|
23
|
+
import sys
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
|
|
26
|
+
ROOT = Path(__file__).resolve().parents[1]
|
|
27
|
+
SCANNER = ROOT / "scripts" / "build_discovery_manifest.py"
|
|
28
|
+
COMMITTED = ROOT / "dist" / "discovery" / "discovery-manifest.json"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _normalise(manifest: dict) -> str:
|
|
32
|
+
out = dict(manifest)
|
|
33
|
+
out["generated_at"] = "<normalised>"
|
|
34
|
+
return json.dumps(out, indent=2, sort_keys=True, ensure_ascii=False) + "\n"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _fresh_build() -> dict:
|
|
38
|
+
proc = subprocess.run(
|
|
39
|
+
[sys.executable, str(SCANNER)],
|
|
40
|
+
capture_output=True,
|
|
41
|
+
text=True,
|
|
42
|
+
check=False,
|
|
43
|
+
cwd=str(ROOT),
|
|
44
|
+
)
|
|
45
|
+
if proc.returncode != 0:
|
|
46
|
+
print(proc.stderr, file=sys.stderr)
|
|
47
|
+
raise SystemExit(f"scanner failed: exit {proc.returncode}")
|
|
48
|
+
return json.loads(proc.stdout)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def main(argv: list[str] | None = None) -> int:
|
|
52
|
+
parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
|
|
53
|
+
parser.add_argument("--quiet", action="store_true")
|
|
54
|
+
args = parser.parse_args(argv)
|
|
55
|
+
|
|
56
|
+
if not COMMITTED.exists():
|
|
57
|
+
print(
|
|
58
|
+
f"error: committed manifest not found at {COMMITTED.relative_to(ROOT)} "
|
|
59
|
+
"— run `task build-discovery` and commit the output.",
|
|
60
|
+
file=sys.stderr,
|
|
61
|
+
)
|
|
62
|
+
return 1
|
|
63
|
+
|
|
64
|
+
committed = json.loads(COMMITTED.read_text(encoding="utf-8"))
|
|
65
|
+
fresh = _fresh_build()
|
|
66
|
+
sa = _normalise(committed)
|
|
67
|
+
sb = _normalise(fresh)
|
|
68
|
+
if sa != sb:
|
|
69
|
+
print(
|
|
70
|
+
"DRIFT: committed discovery-manifest.json differs from a fresh re-build.",
|
|
71
|
+
file=sys.stderr,
|
|
72
|
+
)
|
|
73
|
+
print(
|
|
74
|
+
" Run `task build-discovery` and commit dist/discovery/.",
|
|
75
|
+
file=sys.stderr,
|
|
76
|
+
)
|
|
77
|
+
# first divergence — single most useful line
|
|
78
|
+
for i, (la, lb) in enumerate(zip(sa.splitlines(), sb.splitlines()), 1):
|
|
79
|
+
if la != lb:
|
|
80
|
+
print(f" first diff at line {i}:", file=sys.stderr)
|
|
81
|
+
print(f" committed: {la}", file=sys.stderr)
|
|
82
|
+
print(f" fresh: {lb}", file=sys.stderr)
|
|
83
|
+
break
|
|
84
|
+
return 1
|
|
85
|
+
if not args.quiet:
|
|
86
|
+
print(
|
|
87
|
+
f"OK {COMMITTED.relative_to(ROOT)} matches fresh re-build "
|
|
88
|
+
f"({committed['stats']['total_artefacts']} artefacts)."
|
|
89
|
+
)
|
|
90
|
+
return 0
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
if __name__ == "__main__":
|
|
94
|
+
sys.exit(main())
|
|
@@ -428,32 +428,51 @@ def _main() -> int:
|
|
|
428
428
|
)
|
|
429
429
|
parser.add_argument(
|
|
430
430
|
"--root",
|
|
431
|
-
default=
|
|
432
|
-
help=
|
|
431
|
+
default=None,
|
|
432
|
+
help=(
|
|
433
|
+
"Source root to scan. Default: every artefact root discovered by "
|
|
434
|
+
"scripts/_lib/agent_src.artefact_roots() (legacy + packages/*)."
|
|
435
|
+
),
|
|
433
436
|
)
|
|
434
437
|
args = parser.parse_args()
|
|
435
438
|
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
439
|
+
if args.root is not None:
|
|
440
|
+
root = Path(args.root)
|
|
441
|
+
if not root.is_dir():
|
|
442
|
+
print(f"error: source root not found: {root}", file=sys.stderr)
|
|
443
|
+
return 2
|
|
444
|
+
roots = [root]
|
|
445
|
+
else:
|
|
446
|
+
# Late import keeps the validator usable as a library without the
|
|
447
|
+
# monorepo-helper dependency on the import path.
|
|
448
|
+
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
449
|
+
from _lib.agent_src import artefact_roots # noqa: E402
|
|
450
|
+
roots = artefact_roots()
|
|
451
|
+
if not roots:
|
|
452
|
+
print(
|
|
453
|
+
"error: no artefact roots found "
|
|
454
|
+
"(checked .agent-src.uncompressed/ and packages/*/.agent-src.uncompressed/)",
|
|
455
|
+
file=sys.stderr,
|
|
456
|
+
)
|
|
457
|
+
return 2
|
|
440
458
|
|
|
441
459
|
total = 0
|
|
442
460
|
failing = 0
|
|
443
|
-
for
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
461
|
+
for root in roots:
|
|
462
|
+
for artefact_type, path in _iter_artefacts(root):
|
|
463
|
+
total += 1
|
|
464
|
+
text = path.read_text(encoding="utf-8")
|
|
465
|
+
data, _offset = parse_frontmatter(text)
|
|
466
|
+
if data is None:
|
|
467
|
+
# Other tooling flags missing frontmatter; don't double-report.
|
|
468
|
+
continue
|
|
469
|
+
schema = load_schema(artefact_type)
|
|
470
|
+
errors = validate(data, schema)
|
|
471
|
+
if errors:
|
|
472
|
+
failing += 1
|
|
473
|
+
for error in errors:
|
|
474
|
+
print(f"[{artefact_type}] {path}: {error.rule} at "
|
|
475
|
+
f"{error.path} – {error.message}")
|
|
457
476
|
|
|
458
477
|
print(f"\n== Frontmatter schema: {total} artefacts, "
|
|
459
478
|
f"{failing} failing ==")
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Verify the post-move state matches the pre-move snapshot byte-for-byte.
|
|
3
|
+
|
|
4
|
+
Re-runs `task sync` + `task build-discovery` (caller invokes them
|
|
5
|
+
ahead of this script), then loads the fresh outputs and compares them
|
|
6
|
+
against `dist/migration/pre-move-snapshot.json`. The contract:
|
|
7
|
+
|
|
8
|
+
- `.agent-src/` tree hashes must match exactly
|
|
9
|
+
- `.augment/` tree hashes must match exactly
|
|
10
|
+
- `dist/discovery/discovery-manifest.json` with `artefacts[].path`
|
|
11
|
+
stripped + `generated_at` dropped must match exactly
|
|
12
|
+
|
|
13
|
+
Anything else is a regression — exit non-zero with a diff summary.
|
|
14
|
+
|
|
15
|
+
CLI:
|
|
16
|
+
--snapshot PATH path to pre-move snapshot JSON
|
|
17
|
+
--json machine-readable verdict to stdout
|
|
18
|
+
"""
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import argparse
|
|
22
|
+
import json
|
|
23
|
+
import sys
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
from typing import Any
|
|
26
|
+
|
|
27
|
+
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
28
|
+
from snapshot_agent_outputs import ( # noqa: E402
|
|
29
|
+
_build_snapshot,
|
|
30
|
+
_logical_path,
|
|
31
|
+
_SKIP_DIRS,
|
|
32
|
+
_SKIP_NAMES,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _normalise_loaded_snapshot(snap: dict[str, Any]) -> None:
|
|
37
|
+
"""Re-apply current snapshot filters to a previously-captured snapshot.
|
|
38
|
+
|
|
39
|
+
The pre-move snapshot file is immutable history; this lets verify
|
|
40
|
+
compare it against a freshly-captured post-move snapshot whose
|
|
41
|
+
filters have evolved (runtime-cache exclusion, logical-path stripping,
|
|
42
|
+
volatile-field drop) without regenerating the reference.
|
|
43
|
+
"""
|
|
44
|
+
for key, tree in (snap.get("trees") or {}).items():
|
|
45
|
+
keep = {}
|
|
46
|
+
for path, sha in tree.items():
|
|
47
|
+
name = path.rsplit("/", 1)[-1]
|
|
48
|
+
if name in _SKIP_NAMES:
|
|
49
|
+
continue
|
|
50
|
+
if any(part in _SKIP_DIRS for part in path.split("/")):
|
|
51
|
+
continue
|
|
52
|
+
keep[path] = sha
|
|
53
|
+
snap["trees"][key] = keep
|
|
54
|
+
m = snap.get("manifest_path_stripped")
|
|
55
|
+
if isinstance(m, dict):
|
|
56
|
+
for k in ("unassigned", "documented_unassigned"):
|
|
57
|
+
entries = m.get(k) or []
|
|
58
|
+
normalised: list[dict[str, Any]] = []
|
|
59
|
+
for e in entries:
|
|
60
|
+
if not isinstance(e, dict):
|
|
61
|
+
normalised.append(e)
|
|
62
|
+
continue
|
|
63
|
+
if "path" in e:
|
|
64
|
+
e["path"] = _logical_path(e["path"])
|
|
65
|
+
path = e.get("path", "")
|
|
66
|
+
name = path.rsplit("/", 1)[-1]
|
|
67
|
+
if name in _SKIP_NAMES:
|
|
68
|
+
continue
|
|
69
|
+
if any(part in _SKIP_DIRS for part in path.split("/")):
|
|
70
|
+
continue
|
|
71
|
+
normalised.append(e)
|
|
72
|
+
normalised.sort(key=lambda e: (e.get("path", ""), e.get("category", "")))
|
|
73
|
+
m[k] = normalised
|
|
74
|
+
# Recompute the two counts that ride on the filtered lists so the
|
|
75
|
+
# stats block stays consistent with the normalised entries.
|
|
76
|
+
stats = m.get("stats")
|
|
77
|
+
if isinstance(stats, dict):
|
|
78
|
+
stats["documented_unassigned_count"] = len(m.get("documented_unassigned") or [])
|
|
79
|
+
stats["unassigned_count"] = len(m.get("unassigned") or [])
|
|
80
|
+
# Re-sort artefacts by (category, checksum) — pre-move snapshot
|
|
81
|
+
# was sorted by path; that order shifts when files move roots.
|
|
82
|
+
arts = m.get("artefacts") or []
|
|
83
|
+
for a in arts:
|
|
84
|
+
a.pop("path", None)
|
|
85
|
+
arts.sort(key=lambda a: (a.get("category", ""), a.get("checksum", "")))
|
|
86
|
+
m["artefacts"] = arts
|
|
87
|
+
m.pop("checksum", None)
|
|
88
|
+
m.pop("scanner_version", None)
|
|
89
|
+
|
|
90
|
+
ROOT = Path(__file__).resolve().parents[1]
|
|
91
|
+
DEFAULT_SNAPSHOT = ROOT / "dist" / "migration" / "pre-move-snapshot.json"
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _diff_tree(name: str, before: dict[str, str], after: dict[str, str]) -> list[str]:
|
|
95
|
+
issues: list[str] = []
|
|
96
|
+
keys = sorted(set(before) | set(after))
|
|
97
|
+
for k in keys:
|
|
98
|
+
b = before.get(k)
|
|
99
|
+
a = after.get(k)
|
|
100
|
+
if b is None:
|
|
101
|
+
issues.append(f" {name}: added {k}")
|
|
102
|
+
elif a is None:
|
|
103
|
+
issues.append(f" {name}: removed {k}")
|
|
104
|
+
elif a != b:
|
|
105
|
+
issues.append(f" {name}: changed {k} ({b[:12]}… → {a[:12]}…)")
|
|
106
|
+
return issues
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _diff_manifest(before: dict[str, Any] | None, after: dict[str, Any] | None) -> list[str]:
|
|
110
|
+
if before is None and after is None:
|
|
111
|
+
return []
|
|
112
|
+
if before is None:
|
|
113
|
+
return [" manifest: pre-move snapshot missing"]
|
|
114
|
+
if after is None:
|
|
115
|
+
return [" manifest: post-move manifest missing"]
|
|
116
|
+
before_str = json.dumps(before, sort_keys=True, ensure_ascii=False)
|
|
117
|
+
after_str = json.dumps(after, sort_keys=True, ensure_ascii=False)
|
|
118
|
+
if before_str == after_str:
|
|
119
|
+
return []
|
|
120
|
+
# Field-level diff for visibility.
|
|
121
|
+
issues = [" manifest: path-stripped content differs"]
|
|
122
|
+
b_arts = {a.get("name", "?"): a for a in (before.get("artefacts") or [])}
|
|
123
|
+
a_arts = {a.get("name", "?"): a for a in (after.get("artefacts") or [])}
|
|
124
|
+
only_b = sorted(set(b_arts) - set(a_arts))
|
|
125
|
+
only_a = sorted(set(a_arts) - set(b_arts))
|
|
126
|
+
for n in only_b[:10]:
|
|
127
|
+
issues.append(f" artefact removed: {n}")
|
|
128
|
+
for n in only_a[:10]:
|
|
129
|
+
issues.append(f" artefact added: {n}")
|
|
130
|
+
common_changed = []
|
|
131
|
+
for n in sorted(set(b_arts) & set(a_arts)):
|
|
132
|
+
if json.dumps(b_arts[n], sort_keys=True) != json.dumps(a_arts[n], sort_keys=True):
|
|
133
|
+
common_changed.append(n)
|
|
134
|
+
for n in common_changed[:10]:
|
|
135
|
+
issues.append(f" artefact changed: {n}")
|
|
136
|
+
return issues
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def main() -> int:
|
|
140
|
+
ap = argparse.ArgumentParser(description=__doc__)
|
|
141
|
+
ap.add_argument("--snapshot", type=Path, default=DEFAULT_SNAPSHOT)
|
|
142
|
+
ap.add_argument("--json", action="store_true", help="emit machine-readable verdict to stdout")
|
|
143
|
+
args = ap.parse_args()
|
|
144
|
+
|
|
145
|
+
if not args.snapshot.exists():
|
|
146
|
+
print(f"ERROR: snapshot missing: {args.snapshot}", file=sys.stderr)
|
|
147
|
+
return 2
|
|
148
|
+
|
|
149
|
+
before = json.loads(args.snapshot.read_text(encoding="utf-8"))
|
|
150
|
+
after = _build_snapshot()
|
|
151
|
+
|
|
152
|
+
# The pre-move snapshot was captured before _hash_tree / manifest
|
|
153
|
+
# stripping learned to filter runtime artefacts. Re-apply the current
|
|
154
|
+
# filter to the loaded snapshot so the diff is apples-to-apples.
|
|
155
|
+
_normalise_loaded_snapshot(before)
|
|
156
|
+
|
|
157
|
+
issues: list[str] = []
|
|
158
|
+
for key in (".agent-src", ".augment"):
|
|
159
|
+
issues.extend(_diff_tree(key, before["trees"].get(key, {}), after["trees"].get(key, {})))
|
|
160
|
+
issues.extend(_diff_manifest(before.get("manifest_path_stripped"), after.get("manifest_path_stripped")))
|
|
161
|
+
|
|
162
|
+
ok = not issues
|
|
163
|
+
if args.json:
|
|
164
|
+
print(json.dumps({
|
|
165
|
+
"ok": ok,
|
|
166
|
+
"issue_count": len(issues),
|
|
167
|
+
"issues": issues,
|
|
168
|
+
}, indent=2))
|
|
169
|
+
else:
|
|
170
|
+
if ok:
|
|
171
|
+
print("verify_physical_move: byte-identity OK")
|
|
172
|
+
print(f" .agent-src/ files: {len(after['trees'].get('.agent-src', {}))}")
|
|
173
|
+
print(f" .augment/ files: {len(after['trees'].get('.augment', {}))}")
|
|
174
|
+
print(f" manifest: path-stripped content matches")
|
|
175
|
+
else:
|
|
176
|
+
print(f"verify_physical_move: FAIL ({len(issues)} issue(s))")
|
|
177
|
+
for line in issues[:50]:
|
|
178
|
+
print(line)
|
|
179
|
+
if len(issues) > 50:
|
|
180
|
+
print(f" … and {len(issues) - 50} more")
|
|
181
|
+
return 0 if ok else 1
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
if __name__ == "__main__":
|
|
185
|
+
sys.exit(main())
|
package/templates/agent-user.md
CHANGED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
version: 1
|
|
2
|
+
identity:
|
|
3
|
+
name: ""
|
|
4
|
+
language: "en"
|
|
5
|
+
role:
|
|
6
|
+
- ""
|
|
7
|
+
style:
|
|
8
|
+
formality: "informal"
|
|
9
|
+
pace: "pragmatic"
|
|
10
|
+
voice_sample: |
|
|
11
|
+
Replace this block with one to three sentences in your own
|
|
12
|
+
writing style. The agent uses it as a tone anchor — paste the
|
|
13
|
+
way you would actually message a colleague, not a polished pitch.
|
|
14
|
+
last_updated: "1970-01-01"
|
|
15
|
+
# notes: |
|
|
16
|
+
# Optional free-form prose. Anything you want the agent to remember
|
|
17
|
+
# across sessions — preferred terminology, recurring projects,
|
|
18
|
+
# conventions you want enforced. Keep it short; everything here
|
|
19
|
+
# loads into every reply. Hard cap: 8 000 chars.
|
|
20
|
+
#
|
|
21
|
+
# Schema reference: docs/contracts/agent-user-schema.md
|