@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
package/scripts/compress.py
CHANGED
|
@@ -32,14 +32,43 @@ import yaml
|
|
|
32
32
|
|
|
33
33
|
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
34
34
|
from _lib.script_output import info, success, flush_summary, resolve_level # noqa: E402
|
|
35
|
+
from _lib.agent_src import ( # noqa: E402
|
|
36
|
+
artefact_roots,
|
|
37
|
+
iter_all_sources,
|
|
38
|
+
resolve_logical,
|
|
39
|
+
)
|
|
35
40
|
|
|
36
41
|
PROJECT_ROOT = Path(__file__).resolve().parent.parent
|
|
42
|
+
# Legacy single-root anchor — kept for backward compatibility with callers
|
|
43
|
+
# that pass it explicitly. Multi-root iteration (post-ADR-017 physical
|
|
44
|
+
# move) goes through `_lib.agent_src` helpers.
|
|
37
45
|
SOURCE_DIR = PROJECT_ROOT / ".agent-src.uncompressed"
|
|
38
46
|
TARGET_DIR = PROJECT_ROOT / ".agent-src"
|
|
39
47
|
AUGMENT_DIR = PROJECT_ROOT / ".augment"
|
|
40
48
|
HASH_FILE = PROJECT_ROOT / ".compression-hashes.json"
|
|
41
49
|
SETTINGS_FILE = PROJECT_ROOT / ".agent-settings.yml"
|
|
42
50
|
|
|
51
|
+
|
|
52
|
+
def _iter_sources():
|
|
53
|
+
"""Yield (physical_path, logical_relpath) for every source artefact.
|
|
54
|
+
|
|
55
|
+
Wraps `_lib.agent_src.iter_all_sources` so the compressor walks every
|
|
56
|
+
active source root (legacy `.agent-src.uncompressed/` plus any
|
|
57
|
+
`packages/*/.agent-src.uncompressed/`) and keys outputs by the
|
|
58
|
+
logical relative path that survives the physical move (ADR-017).
|
|
59
|
+
"""
|
|
60
|
+
yield from iter_all_sources()
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _resolve_source(relative: str) -> Path | None:
|
|
64
|
+
"""Find the physical path that backs a logical relative path."""
|
|
65
|
+
return resolve_logical(relative)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _any_source_root_exists() -> bool:
|
|
69
|
+
"""True if at least one artefact source root contains files."""
|
|
70
|
+
return bool(artefact_roots())
|
|
71
|
+
|
|
43
72
|
# Self-projection tool toggle — see .agent-tools.yml. When the file is
|
|
44
73
|
# absent (e.g. tests run in tmp dirs, consumer projects), `_active_tools`
|
|
45
74
|
# returns ``None`` which is treated as "emit every tool".
|
|
@@ -148,8 +177,8 @@ def mark_done(relative_path: str) -> None:
|
|
|
148
177
|
relative paths in the shipped layer (P1 of road-to-path-fixes.md).
|
|
149
178
|
Idempotent — re-running is a no-op.
|
|
150
179
|
"""
|
|
151
|
-
source_file =
|
|
152
|
-
if not source_file.exists():
|
|
180
|
+
source_file = _resolve_source(relative_path)
|
|
181
|
+
if source_file is None or not source_file.exists():
|
|
153
182
|
print(f"❌ Source file not found: {relative_path}")
|
|
154
183
|
sys.exit(1)
|
|
155
184
|
apply_path_rewriter(relative_path)
|
|
@@ -181,12 +210,9 @@ def mark_all_done() -> None:
|
|
|
181
210
|
"""Mark ALL .md files as compressed (e.g. after initial full compression)."""
|
|
182
211
|
hashes = load_hashes()
|
|
183
212
|
count = 0
|
|
184
|
-
for source_file in
|
|
185
|
-
if source_file.is_dir():
|
|
186
|
-
continue
|
|
213
|
+
for source_file, relative in _iter_sources():
|
|
187
214
|
if not should_compress(source_file):
|
|
188
215
|
continue
|
|
189
|
-
relative = str(source_file.relative_to(SOURCE_DIR))
|
|
190
216
|
hashes[relative] = file_hash(source_file)
|
|
191
217
|
count += 1
|
|
192
218
|
save_hashes(hashes)
|
|
@@ -194,15 +220,17 @@ def mark_all_done() -> None:
|
|
|
194
220
|
|
|
195
221
|
|
|
196
222
|
def list_changed_md(source_dir: Path) -> list:
|
|
197
|
-
"""List .md files whose source hash differs from stored hash (= need recompression).
|
|
223
|
+
"""List .md files whose source hash differs from stored hash (= need recompression).
|
|
224
|
+
|
|
225
|
+
The ``source_dir`` parameter is retained for backward compatibility but
|
|
226
|
+
ignored — iteration walks every active source root (ADR-017).
|
|
227
|
+
"""
|
|
228
|
+
del source_dir # multi-root: ignored, kept for signature stability
|
|
198
229
|
hashes = load_hashes()
|
|
199
230
|
changed = []
|
|
200
|
-
for source_file in
|
|
201
|
-
if source_file.is_dir():
|
|
202
|
-
continue
|
|
231
|
+
for source_file, relative in _iter_sources():
|
|
203
232
|
if not should_compress(source_file):
|
|
204
233
|
continue
|
|
205
|
-
relative = str(source_file.relative_to(source_dir))
|
|
206
234
|
current_hash = file_hash(source_file)
|
|
207
235
|
stored_hash = hashes.get(relative)
|
|
208
236
|
if stored_hash != current_hash:
|
|
@@ -211,12 +239,12 @@ def list_changed_md(source_dir: Path) -> list:
|
|
|
211
239
|
|
|
212
240
|
|
|
213
241
|
def find_stale_hashes(source_dir: Path) -> list:
|
|
214
|
-
"""Find hashes stored for source files that no longer exist."""
|
|
242
|
+
"""Find hashes stored for source files that no longer exist in any root."""
|
|
243
|
+
del source_dir # multi-root: ignored, kept for signature stability
|
|
215
244
|
hashes = load_hashes()
|
|
216
245
|
stale = []
|
|
217
246
|
for relative in sorted(hashes.keys()):
|
|
218
|
-
|
|
219
|
-
if not source_file.exists():
|
|
247
|
+
if _resolve_source(relative) is None:
|
|
220
248
|
stale.append(relative)
|
|
221
249
|
return stale
|
|
222
250
|
|
|
@@ -240,10 +268,16 @@ def should_compress(filepath: Path) -> bool:
|
|
|
240
268
|
return False
|
|
241
269
|
if filepath.name in COPY_AS_IS:
|
|
242
270
|
return False
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
271
|
+
# Determine the logical relative path so the COPY_AS_IS_DIRS check
|
|
272
|
+
# works for both legacy (`.agent-src.uncompressed/`) and post-move
|
|
273
|
+
# (`packages/*/.agent-src.uncompressed/`) source roots.
|
|
274
|
+
rel_parts: tuple[str, ...] = filepath.parts
|
|
275
|
+
for root in artefact_roots():
|
|
276
|
+
try:
|
|
277
|
+
rel_parts = filepath.relative_to(root).parts
|
|
278
|
+
break
|
|
279
|
+
except ValueError:
|
|
280
|
+
continue
|
|
247
281
|
if rel_parts and rel_parts[0] in COPY_AS_IS_DIRS:
|
|
248
282
|
return False
|
|
249
283
|
return True
|
|
@@ -256,7 +290,8 @@ def copy_file(source: Path, target: Path) -> None:
|
|
|
256
290
|
|
|
257
291
|
|
|
258
292
|
def cleanup_stale(source_dir: Path, target_dir: Path) -> int:
|
|
259
|
-
"""Delete files in target that don't exist in source. Returns count."""
|
|
293
|
+
"""Delete files in target that don't exist in any source root. Returns count."""
|
|
294
|
+
del source_dir # multi-root: ignored, kept for signature stability
|
|
260
295
|
deleted = 0
|
|
261
296
|
if not target_dir.exists():
|
|
262
297
|
return 0
|
|
@@ -265,8 +300,7 @@ def cleanup_stale(source_dir: Path, target_dir: Path) -> int:
|
|
|
265
300
|
if target_file.is_dir():
|
|
266
301
|
continue
|
|
267
302
|
relative = target_file.relative_to(target_dir)
|
|
268
|
-
|
|
269
|
-
if not source_file.exists():
|
|
303
|
+
if _resolve_source(str(relative)) is None:
|
|
270
304
|
print(f" Deleting stale: {relative}")
|
|
271
305
|
target_file.unlink()
|
|
272
306
|
deleted += 1
|
|
@@ -281,14 +315,17 @@ def cleanup_stale(source_dir: Path, target_dir: Path) -> int:
|
|
|
281
315
|
|
|
282
316
|
|
|
283
317
|
def sync_non_md(source_dir: Path, target_dir: Path) -> int:
|
|
284
|
-
"""Copy all non-.md files (and COPY_AS_IS .md files) from source
|
|
318
|
+
"""Copy all non-.md files (and COPY_AS_IS .md files) from every source
|
|
319
|
+
root to target. Returns count."""
|
|
320
|
+
del source_dir # multi-root: ignored, kept for signature stability
|
|
285
321
|
copied = 0
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
continue
|
|
322
|
+
seen: set[str] = set()
|
|
323
|
+
for source_file, relative in _iter_sources():
|
|
289
324
|
if should_compress(source_file):
|
|
290
325
|
continue # .md files are compressed by the agent, not copied here
|
|
291
|
-
relative
|
|
326
|
+
if relative in seen:
|
|
327
|
+
continue
|
|
328
|
+
seen.add(relative)
|
|
292
329
|
target_file = target_dir / relative
|
|
293
330
|
copy_file(source_file, target_file)
|
|
294
331
|
print(f" Copied: {relative}")
|
|
@@ -298,36 +335,41 @@ def sync_non_md(source_dir: Path, target_dir: Path) -> int:
|
|
|
298
335
|
|
|
299
336
|
def list_md_files(source_dir: Path) -> list:
|
|
300
337
|
"""List all .md files that need compression by the agent."""
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
338
|
+
del source_dir # multi-root: ignored, kept for signature stability
|
|
339
|
+
files: list[str] = []
|
|
340
|
+
seen: set[str] = set()
|
|
341
|
+
for source_file, relative in _iter_sources():
|
|
342
|
+
if not should_compress(source_file):
|
|
304
343
|
continue
|
|
305
|
-
if
|
|
306
|
-
|
|
307
|
-
|
|
344
|
+
if relative in seen:
|
|
345
|
+
continue
|
|
346
|
+
seen.add(relative)
|
|
347
|
+
files.append(relative)
|
|
348
|
+
return sorted(files)
|
|
308
349
|
|
|
309
350
|
|
|
310
351
|
def check_sync(source_dir: Path, target_dir: Path) -> tuple:
|
|
311
|
-
"""Check if target is in sync with source. Returns (missing, stale) lists."""
|
|
352
|
+
"""Check if target is in sync with source(s). Returns (missing, stale) lists."""
|
|
353
|
+
del source_dir # multi-root: ignored, kept for signature stability
|
|
312
354
|
missing = []
|
|
313
355
|
stale = []
|
|
314
356
|
|
|
315
|
-
# Files in source but not in target
|
|
316
|
-
|
|
317
|
-
|
|
357
|
+
# Files in any source root but not in target
|
|
358
|
+
seen: set[str] = set()
|
|
359
|
+
for _source_file, relative in _iter_sources():
|
|
360
|
+
if relative in seen:
|
|
318
361
|
continue
|
|
319
|
-
|
|
362
|
+
seen.add(relative)
|
|
320
363
|
if not (target_dir / relative).exists():
|
|
321
364
|
missing.append(relative)
|
|
322
365
|
|
|
323
|
-
# Files in target but not in source (stale)
|
|
366
|
+
# Files in target but not in any source root (stale)
|
|
324
367
|
if target_dir.exists():
|
|
325
368
|
for target_file in sorted(target_dir.rglob("*")):
|
|
326
369
|
if target_file.is_dir():
|
|
327
370
|
continue
|
|
328
371
|
relative = str(target_file.relative_to(target_dir))
|
|
329
|
-
|
|
330
|
-
if not (source_dir / relative).exists():
|
|
372
|
+
if _resolve_source(relative) is None:
|
|
331
373
|
stale.append(relative)
|
|
332
374
|
|
|
333
375
|
return missing, stale
|
|
@@ -512,11 +554,80 @@ def _rewrite_body_links(body: str, prefix: str) -> str:
|
|
|
512
554
|
return _BODY_DOCS_RE.sub(prefix + r"\1", body)
|
|
513
555
|
|
|
514
556
|
|
|
557
|
+
# ── Human-review banner injection (Phase 5.3 / ADR-018) ───────────────────
|
|
558
|
+
# Source artefacts may set `trust.human_review_required: true` in their
|
|
559
|
+
# frontmatter. The compressor injects a short, parser-stable banner block
|
|
560
|
+
# at the top of the projected body so every downstream surface (agent
|
|
561
|
+
# memory, .augment, .claude, etc.) surfaces the gate. Idempotent — the
|
|
562
|
+
# marker comment prevents double-injection on re-compress.
|
|
563
|
+
|
|
564
|
+
_HRR_BANNER_MARKER = "<!-- agent-config:human-review-banner -->"
|
|
565
|
+
|
|
566
|
+
# Plain YAML list item — any scalar, used to read `packs:` / `workspaces:`
|
|
567
|
+
# blocks where values are bare identifiers, not file paths.
|
|
568
|
+
_FM_PLAIN_LIST_RE = re.compile(r'^\s*-\s*(["\']?)([^"\'\n]+?)\1\s*$')
|
|
569
|
+
|
|
570
|
+
|
|
571
|
+
def _parse_trust_and_owner(fm_lines):
|
|
572
|
+
"""Extract `trust.level`, `human_review_required`, and an owner hint
|
|
573
|
+
from a frontmatter line list. Owner falls back to the first pack
|
|
574
|
+
prefix (`finance-basic` → `finance`), then the first workspace,
|
|
575
|
+
then `unknown`.
|
|
576
|
+
"""
|
|
577
|
+
level = None
|
|
578
|
+
hrr = False
|
|
579
|
+
packs: list[str] = []
|
|
580
|
+
workspaces: list[str] = []
|
|
581
|
+
in_trust = False
|
|
582
|
+
in_packs = False
|
|
583
|
+
in_workspaces = False
|
|
584
|
+
for line in fm_lines:
|
|
585
|
+
stripped = line.lstrip()
|
|
586
|
+
indent = len(line) - len(stripped)
|
|
587
|
+
if indent == 0 and stripped.endswith(":"):
|
|
588
|
+
key = stripped[:-1]
|
|
589
|
+
in_trust = key == "trust"
|
|
590
|
+
in_packs = key == "packs"
|
|
591
|
+
in_workspaces = key == "workspaces"
|
|
592
|
+
continue
|
|
593
|
+
if in_trust and stripped.startswith("level:"):
|
|
594
|
+
level = stripped.split(":", 1)[1].strip().strip('"').strip("'")
|
|
595
|
+
elif in_trust and stripped.startswith("human_review_required:"):
|
|
596
|
+
val = stripped.split(":", 1)[1].strip()
|
|
597
|
+
hrr = val.lower() == "true"
|
|
598
|
+
elif in_packs or in_workspaces:
|
|
599
|
+
m = _FM_PLAIN_LIST_RE.match(line)
|
|
600
|
+
if m:
|
|
601
|
+
value = m.group(2).strip()
|
|
602
|
+
(packs if in_packs else workspaces).append(value)
|
|
603
|
+
owner = "unknown"
|
|
604
|
+
if packs:
|
|
605
|
+
owner = packs[0].split("-")[0]
|
|
606
|
+
elif workspaces:
|
|
607
|
+
owner = workspaces[0]
|
|
608
|
+
return level, hrr, owner
|
|
609
|
+
|
|
610
|
+
|
|
611
|
+
def _inject_hrr_banner(body: str, level: str, owner: str) -> str:
|
|
612
|
+
"""Prepend the HUMAN_REVIEW banner block to `body`. Idempotent — a
|
|
613
|
+
body that already carries `_HRR_BANNER_MARKER` is returned unchanged.
|
|
614
|
+
"""
|
|
615
|
+
if _HRR_BANNER_MARKER in body:
|
|
616
|
+
return body
|
|
617
|
+
banner = (
|
|
618
|
+
f"{_HRR_BANNER_MARKER}\n"
|
|
619
|
+
f"> HUMAN REVIEW REQUIRED · trust: {level} · owner: {owner}\n\n"
|
|
620
|
+
)
|
|
621
|
+
return banner + body.lstrip("\n")
|
|
622
|
+
|
|
623
|
+
|
|
515
624
|
def _rewrite_paths(content: str, source_relative_path: str) -> str:
|
|
516
625
|
"""Rewrite logical / legacy paths in `content` for a file shipped at
|
|
517
626
|
`.agent-src/{source_relative_path}`. Idempotent.
|
|
518
627
|
|
|
519
628
|
See module-level comment above for the full pattern catalog.
|
|
629
|
+
Also injects the HUMAN_REVIEW banner when the source frontmatter
|
|
630
|
+
sets `trust.human_review_required: true` (Phase 5.3 / ADR-018).
|
|
520
631
|
"""
|
|
521
632
|
prefix = _depth_prefix(source_relative_path)
|
|
522
633
|
fm_lines, body = _split_frontmatter(content)
|
|
@@ -524,6 +635,9 @@ def _rewrite_paths(content: str, source_relative_path: str) -> str:
|
|
|
524
635
|
if fm_lines is None:
|
|
525
636
|
return body
|
|
526
637
|
new_fm = _rewrite_frontmatter_lines(fm_lines, prefix)
|
|
638
|
+
level, hrr, owner = _parse_trust_and_owner(fm_lines)
|
|
639
|
+
if hrr and level:
|
|
640
|
+
body = _inject_hrr_banner(body, level, owner)
|
|
527
641
|
return "---\n" + "\n".join(new_fm) + "\n---\n" + body
|
|
528
642
|
|
|
529
643
|
|
|
@@ -1096,8 +1210,11 @@ def clean_tools() -> None:
|
|
|
1096
1210
|
|
|
1097
1211
|
|
|
1098
1212
|
def main() -> None:
|
|
1099
|
-
|
|
1100
|
-
|
|
1213
|
+
# Multi-root awareness (ADR-017): tolerate a missing legacy
|
|
1214
|
+
# `.agent-src.uncompressed/` as long as at least one package-scoped
|
|
1215
|
+
# source root carries artefacts.
|
|
1216
|
+
if not SOURCE_DIR.exists() and not _any_source_root_exists():
|
|
1217
|
+
print(f"❌ No source directory found (looked at {SOURCE_DIR} and packages/*/.agent-src.uncompressed)")
|
|
1101
1218
|
sys.exit(1)
|
|
1102
1219
|
|
|
1103
1220
|
arg = sys.argv[1] if len(sys.argv) > 1 else "--sync"
|
|
@@ -1149,9 +1266,10 @@ def main() -> None:
|
|
|
1149
1266
|
copied = sync_non_md(SOURCE_DIR, TARGET_DIR)
|
|
1150
1267
|
print(f"\n--- Cleanup stale files ---")
|
|
1151
1268
|
deleted = cleanup_stale(SOURCE_DIR, TARGET_DIR)
|
|
1152
|
-
# Also cleanup stale hashes
|
|
1269
|
+
# Also cleanup stale hashes (multi-root aware — resolve against
|
|
1270
|
+
# every artefact root, not just the legacy SOURCE_DIR).
|
|
1153
1271
|
hashes = load_hashes()
|
|
1154
|
-
stale_keys = [k for k in hashes if
|
|
1272
|
+
stale_keys = [k for k in hashes if _resolve_source(k) is None]
|
|
1155
1273
|
for k in stale_keys:
|
|
1156
1274
|
del hashes[k]
|
|
1157
1275
|
if stale_keys:
|
|
@@ -93,7 +93,25 @@ def _load_yaml(path: Path) -> dict[str, Any]:
|
|
|
93
93
|
|
|
94
94
|
|
|
95
95
|
def _preset_file(project_root: Path, preset_id: str) -> Path:
|
|
96
|
-
|
|
96
|
+
# Legacy single-root layout — honor when present so tests that mock a
|
|
97
|
+
# ``.agent-src.uncompressed/`` sub-tree under ``project_root`` keep working.
|
|
98
|
+
legacy = project_root / PRESETS_DIRNAME / f"{preset_id}.yml"
|
|
99
|
+
if legacy.exists():
|
|
100
|
+
return legacy
|
|
101
|
+
# Monorepo layout — scan every package root via the agent_src helper.
|
|
102
|
+
try:
|
|
103
|
+
import sys as _sys
|
|
104
|
+
scripts_root = Path(__file__).resolve().parents[1]
|
|
105
|
+
if str(scripts_root) not in _sys.path:
|
|
106
|
+
_sys.path.insert(0, str(scripts_root))
|
|
107
|
+
from _lib.agent_src import artefact_roots # type: ignore
|
|
108
|
+
except Exception:
|
|
109
|
+
return legacy
|
|
110
|
+
for root in artefact_roots():
|
|
111
|
+
candidate = root / "presets" / f"{preset_id}.yml"
|
|
112
|
+
if candidate.exists():
|
|
113
|
+
return candidate
|
|
114
|
+
return legacy
|
|
97
115
|
|
|
98
116
|
|
|
99
117
|
def _coerce_scalar(raw: str) -> Any:
|
|
@@ -89,7 +89,22 @@ def _load_yaml(path: Path) -> dict[str, Any]:
|
|
|
89
89
|
|
|
90
90
|
|
|
91
91
|
def _profile_file(project_root: Path, profile_id: str) -> Path:
|
|
92
|
-
|
|
92
|
+
legacy = project_root / PROFILES_DIRNAME / f"{profile_id}.yml"
|
|
93
|
+
if legacy.exists():
|
|
94
|
+
return legacy
|
|
95
|
+
try:
|
|
96
|
+
import sys as _sys
|
|
97
|
+
scripts_root = Path(__file__).resolve().parents[1]
|
|
98
|
+
if str(scripts_root) not in _sys.path:
|
|
99
|
+
_sys.path.insert(0, str(scripts_root))
|
|
100
|
+
from _lib.agent_src import artefact_roots # type: ignore
|
|
101
|
+
except Exception:
|
|
102
|
+
return legacy
|
|
103
|
+
for root in artefact_roots():
|
|
104
|
+
candidate = root / "profiles" / f"{profile_id}.yml"
|
|
105
|
+
if candidate.exists():
|
|
106
|
+
return candidate
|
|
107
|
+
return legacy
|
|
93
108
|
|
|
94
109
|
|
|
95
110
|
def _build_resolved(
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Pretty-print the ``stats`` block from the committed discovery manifest.
|
|
3
|
+
|
|
4
|
+
Cheap sanity surface for developers: counts by category, lifecycle, and
|
|
5
|
+
trust level — answers "how many skills are professional vs core?" in
|
|
6
|
+
one terminal line. Reads only the committed manifest; no scan, no
|
|
7
|
+
generation. See ADR-015.
|
|
8
|
+
|
|
9
|
+
CLI:
|
|
10
|
+
python scripts/discovery_stats.py [--manifest PATH]
|
|
11
|
+
|
|
12
|
+
Exit codes:
|
|
13
|
+
0 printed
|
|
14
|
+
1 manifest missing or malformed
|
|
15
|
+
"""
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import argparse
|
|
19
|
+
import json
|
|
20
|
+
import sys
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
|
|
23
|
+
ROOT = Path(__file__).resolve().parents[1]
|
|
24
|
+
DEFAULT_MANIFEST = ROOT / "dist" / "discovery" / "discovery-manifest.json"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _fmt_row(label: str, counts: dict[str, int]) -> str:
|
|
28
|
+
parts = [f"{k}={v}" for k, v in counts.items()]
|
|
29
|
+
return f" {label:<14} " + " ".join(parts)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def main(argv: list[str] | None = None) -> int:
|
|
33
|
+
parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
|
|
34
|
+
parser.add_argument("--manifest", type=Path, default=DEFAULT_MANIFEST)
|
|
35
|
+
args = parser.parse_args(argv)
|
|
36
|
+
|
|
37
|
+
if not args.manifest.exists():
|
|
38
|
+
print(
|
|
39
|
+
f"error: manifest not found at {args.manifest} "
|
|
40
|
+
"— run `task build-discovery` first.",
|
|
41
|
+
file=sys.stderr,
|
|
42
|
+
)
|
|
43
|
+
return 1
|
|
44
|
+
|
|
45
|
+
try:
|
|
46
|
+
manifest = json.loads(args.manifest.read_text(encoding="utf-8"))
|
|
47
|
+
except json.JSONDecodeError as exc:
|
|
48
|
+
print(f"error: invalid JSON: {exc}", file=sys.stderr)
|
|
49
|
+
return 1
|
|
50
|
+
|
|
51
|
+
stats = manifest.get("stats")
|
|
52
|
+
if not isinstance(stats, dict):
|
|
53
|
+
print("error: manifest has no `stats` block (regenerate)", file=sys.stderr)
|
|
54
|
+
return 1
|
|
55
|
+
|
|
56
|
+
rel = args.manifest.relative_to(ROOT) if args.manifest.is_absolute() else args.manifest
|
|
57
|
+
print(f"Discovery stats — {rel}")
|
|
58
|
+
print(f" total {stats.get('total_artefacts', 0)}")
|
|
59
|
+
print(_fmt_row("by category", stats.get("by_category", {})))
|
|
60
|
+
print(_fmt_row("by lifecycle", stats.get("by_lifecycle", {})))
|
|
61
|
+
print(_fmt_row("by trust", stats.get("by_trust_level", {})))
|
|
62
|
+
if stats.get("unassigned_count"):
|
|
63
|
+
print(f" unassigned {stats['unassigned_count']}")
|
|
64
|
+
if stats.get("documented_unassigned_count"):
|
|
65
|
+
print(f" documented {stats['documented_unassigned_count']}")
|
|
66
|
+
return 0
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
if __name__ == "__main__":
|
|
70
|
+
sys.exit(main())
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema_version": "expected-perms/v1",
|
|
3
|
+
"$comment": "Expected POSIX modes for the global install tree. Consumed by scripts/lint_global_paths.py as an entry-gate for the migrate-to-global subcommand (road-to-global-only-install Phase 5.0 / A7). Octal modes are strings so JSON tooling never widens 0700 → 700.",
|
|
4
|
+
"global_root": {
|
|
5
|
+
"path": "~/.event4u/agent-config",
|
|
6
|
+
"expected_mode": "0700",
|
|
7
|
+
"kind": "directory"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
{
|
|
11
|
+
"glob": "~/.event4u/agent-config/.agent-settings.yml",
|
|
12
|
+
"expected_mode": "0600",
|
|
13
|
+
"required": false
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
"glob": "~/.event4u/agent-config/.agent-user.yml",
|
|
17
|
+
"expected_mode": "0600",
|
|
18
|
+
"required": false
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"glob": "~/.event4u/agent-config/.wizard-state.json",
|
|
22
|
+
"expected_mode": "0600",
|
|
23
|
+
"required": false
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
"glob": "~/.event4u/agent-config/**/*.key",
|
|
27
|
+
"expected_mode": "0600",
|
|
28
|
+
"required": false
|
|
29
|
+
}
|
|
30
|
+
],
|
|
31
|
+
"directories": [
|
|
32
|
+
{
|
|
33
|
+
"glob": "~/.event4u/agent-config/state",
|
|
34
|
+
"expected_mode": "0700",
|
|
35
|
+
"required": false
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
"glob": "~/.event4u/agent-config/settings",
|
|
39
|
+
"expected_mode": "0700",
|
|
40
|
+
"required": false
|
|
41
|
+
}
|
|
42
|
+
],
|
|
43
|
+
"forbid_symlink_escape": {
|
|
44
|
+
"root": "~/.event4u/agent-config",
|
|
45
|
+
"$comment": "All symlinks under the global root MUST resolve to paths still under the global root. Cross-root escapes are a security finding (allows external writes via a writable symlink target)."
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -19,6 +19,12 @@ from dataclasses import dataclass
|
|
|
19
19
|
from pathlib import Path
|
|
20
20
|
|
|
21
21
|
ROOT = Path(__file__).resolve().parent.parent
|
|
22
|
+
sys.path.insert(0, str(ROOT / "scripts"))
|
|
23
|
+
from _lib.agent_src import artefact_roots # noqa: E402
|
|
24
|
+
|
|
25
|
+
# Legacy single-root anchor — kept as fallback for pure-compressed
|
|
26
|
+
# consumer projections. Multi-root discovery uses artefact_roots() so
|
|
27
|
+
# generate_index works across packages/* per ADR-017.
|
|
22
28
|
SRC = ROOT / ".agent-src.uncompressed"
|
|
23
29
|
GUIDELINES = ROOT / "docs" / "guidelines"
|
|
24
30
|
INDEX_PATH = ROOT / "agents" / "index.md"
|
|
@@ -62,59 +68,79 @@ def _truncate(text: str, limit: int = 200) -> str:
|
|
|
62
68
|
|
|
63
69
|
|
|
64
70
|
def _collect_skills() -> list[Entry]:
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
71
|
+
"""Walk every source root for skills; first root wins per logical id."""
|
|
72
|
+
seen: dict[str, Entry] = {}
|
|
73
|
+
for src_root in artefact_roots():
|
|
74
|
+
skills_root = src_root / "skills"
|
|
75
|
+
if not skills_root.is_dir():
|
|
69
76
|
continue
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
77
|
+
for skill_dir in sorted(skills_root.iterdir()):
|
|
78
|
+
if not skill_dir.is_dir() or skill_dir.name in seen:
|
|
79
|
+
continue
|
|
80
|
+
skill_md = skill_dir / "SKILL.md"
|
|
81
|
+
if not skill_md.exists():
|
|
82
|
+
continue
|
|
83
|
+
fm = _parse_frontmatter(skill_md.read_text(encoding="utf-8"))
|
|
84
|
+
name = fm.get("name") or skill_dir.name
|
|
85
|
+
seen[skill_dir.name] = Entry(
|
|
86
|
+
kind="skill",
|
|
87
|
+
name=name,
|
|
88
|
+
description=_truncate(fm.get("description", "")),
|
|
89
|
+
extra="",
|
|
90
|
+
path=skill_md.relative_to(ROOT).as_posix(),
|
|
91
|
+
)
|
|
92
|
+
return [seen[k] for k in sorted(seen)]
|
|
80
93
|
|
|
81
94
|
|
|
82
95
|
def _collect_rules() -> list[Entry]:
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
96
|
+
"""Walk every source root for rules; first root wins per logical id."""
|
|
97
|
+
seen: dict[str, Entry] = {}
|
|
98
|
+
for src_root in artefact_roots():
|
|
99
|
+
rules_root = src_root / "rules"
|
|
100
|
+
if not rules_root.is_dir():
|
|
101
|
+
continue
|
|
102
|
+
for rule_md in sorted(rules_root.glob("*.md")):
|
|
103
|
+
if rule_md.stem in seen:
|
|
104
|
+
continue
|
|
105
|
+
fm = _parse_frontmatter(rule_md.read_text(encoding="utf-8"))
|
|
106
|
+
seen[rule_md.stem] = Entry(
|
|
107
|
+
kind="rule",
|
|
108
|
+
name=rule_md.stem,
|
|
109
|
+
description=_truncate(fm.get("description", "")),
|
|
110
|
+
extra=fm.get("type", "?"),
|
|
111
|
+
path=rule_md.relative_to(ROOT).as_posix(),
|
|
112
|
+
)
|
|
113
|
+
return [seen[k] for k in sorted(seen)]
|
|
94
114
|
|
|
95
115
|
|
|
96
116
|
def _collect_commands() -> list[Entry]:
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
for
|
|
100
|
-
|
|
117
|
+
"""Walk every source root for commands; first root wins per logical id."""
|
|
118
|
+
seen: dict[str, Entry] = {}
|
|
119
|
+
for src_root in artefact_roots():
|
|
120
|
+
cmd_dir = src_root / "commands"
|
|
121
|
+
if not cmd_dir.is_dir():
|
|
101
122
|
continue
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
123
|
+
for cmd_md in sorted(cmd_dir.rglob("*.md")):
|
|
124
|
+
if cmd_md.name == "AGENTS.md":
|
|
125
|
+
continue
|
|
126
|
+
rel = cmd_md.relative_to(cmd_dir).as_posix()
|
|
127
|
+
if rel in seen:
|
|
128
|
+
continue
|
|
129
|
+
fm = _parse_frontmatter(cmd_md.read_text(encoding="utf-8"))
|
|
130
|
+
is_shim = bool(fm.get("superseded_by"))
|
|
131
|
+
extra = ""
|
|
132
|
+
if is_shim:
|
|
133
|
+
extra = f"shim → /{fm['superseded_by']}"
|
|
134
|
+
elif fm.get("cluster"):
|
|
135
|
+
extra = f"cluster: {fm['cluster']}"
|
|
136
|
+
seen[rel] = Entry(
|
|
137
|
+
kind="shim" if is_shim else "command",
|
|
138
|
+
name=fm.get("name") or cmd_md.stem,
|
|
139
|
+
description=_truncate(fm.get("description", "")),
|
|
140
|
+
extra=extra,
|
|
141
|
+
path=cmd_md.relative_to(ROOT).as_posix(),
|
|
142
|
+
)
|
|
143
|
+
return [seen[k] for k in sorted(seen)]
|
|
118
144
|
|
|
119
145
|
|
|
120
146
|
def _collect_guidelines() -> list[Entry]:
|
|
@@ -137,8 +163,14 @@ def _collect_guidelines() -> list[Entry]:
|
|
|
137
163
|
# Path rewriter for the public catalog: link to the shipped surface
|
|
138
164
|
# (`.agent-src/`) instead of the source-of-truth (`.agent-src.uncompressed/`),
|
|
139
165
|
# which is excluded from `package.json#files` and `composer.json` archives.
|
|
166
|
+
# Post-ADR-017 the source-of-truth lives at packages/*/.agent-src.uncompressed/,
|
|
167
|
+
# so we strip whichever prefix matches and pin to the flat .agent-src/ output.
|
|
140
168
|
def _to_shipped_path(path: str) -> str:
|
|
141
|
-
|
|
169
|
+
from _lib.agent_src import strip_source_prefix
|
|
170
|
+
logical = strip_source_prefix(path)
|
|
171
|
+
if logical is not None:
|
|
172
|
+
return f".agent-src/{logical}"
|
|
173
|
+
return path
|
|
142
174
|
|
|
143
175
|
|
|
144
176
|
def _render_table(
|