@event4u/agent-config 1.16.0 → 1.17.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/{agents-audit.md → agents/audit.md} +4 -3
- package/.agent-src/commands/{agents-cleanup.md → agents/cleanup.md} +12 -6
- package/.agent-src/commands/{agents-prepare.md → agents/prepare.md} +4 -3
- package/.agent-src/commands/agents.md +46 -0
- package/.agent-src/commands/{chat-history-checkpoint.md → chat-history/checkpoint.md} +4 -4
- package/.agent-src/commands/{chat-history-clear.md → chat-history/clear.md} +4 -4
- package/.agent-src/commands/{chat-history-resume.md → chat-history/resume.md} +4 -4
- package/.agent-src/commands/chat-history/show.md +107 -0
- package/.agent-src/commands/chat-history.md +33 -89
- package/.agent-src/commands/{commit-in-chunks.md → commit/in-chunks.md} +15 -13
- package/.agent-src/commands/commit.md +22 -2
- package/.agent-src/commands/{context-create.md → context/create.md} +4 -3
- package/.agent-src/commands/{context-refactor.md → context/refactor.md} +4 -3
- package/.agent-src/commands/context.md +44 -0
- package/.agent-src/commands/{copilot-agents-init.md → copilot-agents/init.md} +4 -3
- package/.agent-src/commands/{copilot-agents-optimize.md → copilot-agents/optimize.md} +4 -3
- package/.agent-src/commands/copilot-agents.md +44 -0
- package/.agent-src/commands/council/default.md +221 -0
- package/.agent-src/commands/{council-design.md → council/design.md} +6 -5
- package/.agent-src/commands/{council-optimize.md → council/optimize.md} +7 -6
- package/.agent-src/commands/{council-pr.md → council/pr.md} +6 -5
- package/.agent-src/commands/council.md +47 -212
- package/.agent-src/commands/{create-pr-description.md → create-pr/description-only.md} +4 -2
- package/.agent-src/commands/create-pr.md +26 -5
- package/.agent-src/commands/{feature-dev.md → feature/dev.md} +5 -10
- package/.agent-src/commands/{feature-explore.md → feature/explore.md} +4 -8
- package/.agent-src/commands/{feature-plan.md → feature/plan.md} +4 -8
- package/.agent-src/commands/{feature-refactor.md → feature/refactor.md} +4 -8
- package/.agent-src/commands/{feature-roadmap.md → feature/roadmap.md} +6 -10
- package/.agent-src/commands/feature.md +6 -12
- package/.agent-src/commands/{fix-ci.md → fix/ci.md} +4 -8
- package/.agent-src/commands/{fix-portability.md → fix/portability.md} +4 -8
- package/.agent-src/commands/{fix-pr-bot-comments.md → fix/pr-bots.md} +4 -8
- package/.agent-src/commands/{fix-pr-developer-comments.md → fix/pr-developers.md} +4 -8
- package/.agent-src/commands/{fix-pr-comments.md → fix/pr.md} +7 -11
- package/.agent-src/commands/{fix-references.md → fix/refs.md} +4 -8
- package/.agent-src/commands/{fix-seeder.md → fix/seeder.md} +4 -8
- package/.agent-src/commands/fix.md +7 -13
- package/.agent-src/commands/{do-and-judge.md → judge/on-diff.md} +4 -3
- package/.agent-src/commands/judge/solo.md +90 -0
- package/.agent-src/commands/{do-in-steps.md → judge/steps.md} +4 -3
- package/.agent-src/commands/judge.md +35 -70
- package/.agent-src/commands/{memory-add.md → memory/add.md} +4 -3
- package/.agent-src/commands/{memory-full.md → memory/load.md} +4 -3
- package/.agent-src/commands/{memory-promote.md → memory/promote.md} +4 -3
- package/.agent-src/commands/{propose-memory.md → memory/propose.md} +4 -3
- package/.agent-src/commands/memory.md +48 -0
- package/.agent-src/commands/{module-create.md → module/create.md} +4 -3
- package/.agent-src/commands/{module-explore.md → module/explore.md} +4 -3
- package/.agent-src/commands/module.md +44 -0
- package/.agent-src/commands/{optimize-agents.md → optimize/agents.md} +4 -8
- package/.agent-src/commands/{optimize-augmentignore.md → optimize/augmentignore.md} +4 -9
- package/.agent-src/commands/{optimize-rtk-filters.md → optimize/rtk.md} +4 -8
- package/.agent-src/commands/{optimize-skills.md → optimize/skills.md} +4 -8
- package/.agent-src/commands/optimize.md +4 -10
- package/.agent-src/commands/{override-create.md → override/create.md} +4 -3
- package/.agent-src/commands/{override-manage.md → override/manage.md} +4 -3
- package/.agent-src/commands/override.md +44 -0
- package/.agent-src/commands/{roadmap-create.md → roadmap/create.md} +4 -3
- package/.agent-src/commands/{roadmap-execute.md → roadmap/execute.md} +4 -3
- package/.agent-src/commands/roadmap.md +44 -0
- package/.agent-src/commands/{tests-create.md → tests/create.md} +4 -3
- package/.agent-src/commands/{tests-execute.md → tests/execute.md} +4 -3
- package/.agent-src/commands/tests.md +44 -0
- package/.agent-src/contexts/communication/rules-auto/artifact-engagement-recording-mechanics.md +72 -0
- package/.agent-src/contexts/communication/rules-auto/augment-portability-mechanics.md +79 -0
- package/.agent-src/contexts/communication/rules-auto/augment-source-of-truth-mechanics.md +98 -0
- package/.agent-src/contexts/communication/rules-auto/cli-output-handling-mechanics.md +87 -0
- package/.agent-src/contexts/communication/rules-auto/command-suggestion-policy-mechanics.md +62 -0
- package/.agent-src/contexts/communication/rules-auto/docs-sync-mechanics.md +78 -0
- package/.agent-src/contexts/communication/rules-auto/package-ci-checks-mechanics.md +85 -0
- package/.agent-src/contexts/communication/rules-auto/review-routing-awareness-mechanics.md +65 -0
- package/.agent-src/contexts/communication/rules-auto/roadmap-progress-sync-mechanics.md +78 -0
- package/.agent-src/contexts/communication/rules-auto/skill-quality-mechanics.md +62 -0
- package/.agent-src/contexts/communication/rules-auto/slash-command-routing-policy-mechanics.md +55 -0
- package/.agent-src/contexts/communication/rules-auto/ui-audit-gate-mechanics.md +53 -0
- package/.agent-src/contexts/communication/rules-auto/user-interaction-mechanics.md +77 -0
- package/.agent-src/contexts/judges/no-consolidate-rationale.md +102 -0
- package/.agent-src/contexts/judges/persona-voice-rubric.md +140 -0
- package/.agent-src/rules/artifact-engagement-recording.md +13 -69
- package/.agent-src/rules/ask-when-uncertain.md +27 -42
- package/.agent-src/rules/augment-portability.md +15 -61
- package/.agent-src/rules/augment-source-of-truth.md +27 -93
- package/.agent-src/rules/cli-output-handling.md +10 -76
- package/.agent-src/rules/command-suggestion-policy.md +18 -59
- package/.agent-src/rules/commit-conventions.md +17 -14
- package/.agent-src/rules/direct-answers.md +34 -49
- package/.agent-src/rules/docker-commands.md +5 -5
- package/.agent-src/rules/docs-sync.md +15 -69
- package/.agent-src/rules/language-and-tone.md +48 -72
- package/.agent-src/rules/missing-tool-handling.md +28 -22
- package/.agent-src/rules/no-cheap-questions.md +45 -52
- package/.agent-src/rules/no-roadmap-references.md +73 -0
- package/.agent-src/rules/package-ci-checks.md +21 -61
- package/.agent-src/rules/preservation-guard.md +64 -29
- package/.agent-src/rules/review-routing-awareness.md +24 -43
- package/.agent-src/rules/roadmap-progress-sync.md +10 -71
- package/.agent-src/rules/security-sensitive-stop.md +8 -8
- package/.agent-src/rules/skill-quality.md +16 -48
- package/.agent-src/rules/slash-command-routing-policy.md +7 -4
- package/.agent-src/rules/think-before-action.md +52 -42
- package/.agent-src/rules/tool-safety.md +19 -16
- package/.agent-src/rules/ui-audit-gate.md +24 -38
- package/.agent-src/rules/user-interaction.md +13 -68
- package/.agent-src/skills/ai-council/SKILL.md +2 -0
- package/.agent-src/skills/api-testing/SKILL.md +1 -1
- package/.agent-src/skills/check-refs/SKILL.md +59 -40
- package/.agent-src/skills/conventional-commits-writing/SKILL.md +86 -28
- package/.agent-src/skills/copilot-agents-optimization/SKILL.md +5 -5
- package/.agent-src/skills/developer-like-execution/SKILL.md +4 -4
- package/.agent-src/skills/finishing-a-development-branch/SKILL.md +101 -65
- package/.agent-src/skills/flux/SKILL.md +30 -10
- package/.agent-src/skills/github-ci/SKILL.md +2 -2
- package/.agent-src/skills/judge-code-quality/SKILL.md +7 -8
- package/.agent-src/skills/judge-security-auditor/SKILL.md +4 -5
- package/.agent-src/skills/judge-test-coverage/SKILL.md +3 -4
- package/.agent-src/skills/lint-skills/SKILL.md +57 -39
- package/.agent-src/skills/md-language-check/SKILL.md +61 -39
- package/.agent-src/skills/override-management/SKILL.md +5 -5
- package/.agent-src/skills/quality-tools/SKILL.md +2 -2
- package/.agent-src/skills/react-shadcn-ui/SKILL.md +116 -43
- package/.agent-src/skills/readme-reviewer/SKILL.md +30 -29
- package/.agent-src/skills/readme-writing/SKILL.md +78 -53
- package/.agent-src/skills/readme-writing-package/SKILL.md +50 -47
- package/.agent-src/skills/receiving-code-review/SKILL.md +52 -47
- package/.agent-src/skills/refine-prompt/SKILL.md +0 -1
- package/.agent-src/skills/requesting-code-review/SKILL.md +35 -30
- package/.agent-src/skills/security/SKILL.md +7 -2
- package/.agent-src/skills/security-audit/SKILL.md +7 -3
- package/.agent-src/skills/systematic-debugging/SKILL.md +68 -60
- package/.agent-src/skills/test-driven-development/SKILL.md +59 -57
- package/.agent-src/skills/test-performance/SKILL.md +0 -1
- package/.agent-src/skills/traefik/SKILL.md +4 -4
- package/.agent-src/skills/verify-completion-evidence/SKILL.md +28 -26
- package/.claude-plugin/marketplace.json +22 -11
- package/AGENTS.md +2 -2
- package/CHANGELOG.md +90 -1
- package/README.md +18 -17
- package/docs/architecture.md +4 -6
- package/docs/catalog.md +67 -39
- package/docs/contracts/STABILITY.md +13 -7
- package/docs/contracts/adr-chat-history-split.md +1 -3
- package/docs/contracts/adr-command-suggestion.md +0 -2
- package/docs/contracts/adr-implement-ticket-runtime.md +1 -2
- package/docs/contracts/adr-product-ui-track.md +3 -6
- package/docs/contracts/adr-prompt-driven-execution.md +3 -4
- package/docs/contracts/agent-memory-contract.md +6 -11
- package/docs/contracts/artifact-engagement-flow.md +6 -9
- package/docs/contracts/command-clusters.md +56 -46
- package/docs/contracts/command-suggestion-flow.md +1 -3
- package/docs/contracts/context-paths.md +99 -0
- package/docs/contracts/file-ownership-matrix.json +6722 -0
- package/docs/contracts/file-ownership-matrix.md +134 -0
- package/docs/contracts/implement-ticket-flow.md +6 -9
- package/docs/contracts/linear-ai-rules-inclusion.md +0 -1
- package/docs/contracts/linear-ai-three-layers.md +0 -2
- package/docs/contracts/load-context-budget-model.md +178 -0
- package/docs/contracts/load-context-schema.md +1 -3
- package/docs/contracts/rule-interactions.md +0 -1
- package/docs/contracts/rule-priority-hierarchy.md +1 -1
- package/docs/contracts/ui-track-flow.md +7 -17
- package/docs/customization.md +2 -0
- package/docs/getting-started.md +5 -4
- package/docs/guidelines/agent-infra/asking-and-brevity-examples.md +100 -0
- package/package.json +1 -1
- package/scripts/_one_off_phase4_dispatch_latency.py +108 -0
- package/scripts/_one_off_phase6_trigger_jaccard.py +92 -0
- package/scripts/_phase2_shim_helper.py +109 -0
- package/scripts/agent-config +10 -0
- package/scripts/ai_council/_one_off_2a4_acceptance.py +208 -0
- package/scripts/ai_council/_one_off_context_layer_v1_estimate.py +67 -0
- package/scripts/ai_council/_one_off_context_layer_v1_review.py +292 -0
- package/scripts/ai_council/_one_off_followups_review.py +259 -0
- package/scripts/ai_council/_one_off_nondestructive_inline_audit.py +209 -0
- package/scripts/ai_council/_one_off_phase_2a_budget_rebalance.py +257 -0
- package/scripts/ai_council/_one_off_phase_2a_post_revert.py +197 -0
- package/scripts/ai_council/_one_off_rule_hardening_v1.py +251 -0
- package/scripts/ai_council/_one_off_structural_open_questions.py +232 -0
- package/scripts/ai_council/_one_off_structural_optimization.py +144 -0
- package/scripts/ai_council/_one_off_structural_v3_gaps.py +252 -0
- package/scripts/ai_council/_one_off_structural_v3_review.py +240 -0
- package/scripts/check_always_budget.py +363 -45
- package/scripts/check_cluster_patterns.py +159 -0
- package/scripts/check_command_count_messaging.py +14 -7
- package/scripts/check_context_paths.py +201 -0
- package/scripts/check_no_roadmap_refs.py +155 -0
- package/scripts/check_phase_coupling.py +148 -0
- package/scripts/check_portability.py +2 -0
- package/scripts/check_references.py +29 -2
- package/scripts/check_safety_floor_untouched.py +125 -0
- package/scripts/command_suggester/loader.py +4 -1
- package/scripts/compress.py +59 -13
- package/scripts/generate_index.py +6 -2
- package/scripts/generate_ownership_matrix.py +323 -0
- package/scripts/hooks/augment-roadmap-progress.sh +57 -0
- package/scripts/install.py +49 -28
- package/scripts/lint_no_new_atomic_commands.py +12 -11
- package/scripts/requirements-evals.txt +1 -0
- package/scripts/roadmap_progress_hook.py +159 -0
- package/scripts/schemas/command.schema.json +4 -3
- package/scripts/skill_linter.py +1 -0
- package/scripts/sync_agent_settings.py +25 -2
- package/scripts/update_counts.py +7 -0
|
@@ -26,7 +26,10 @@ def load_commands(commands_dir: Path) -> list[CommandSpec]:
|
|
|
26
26
|
this loader.
|
|
27
27
|
"""
|
|
28
28
|
specs: list[CommandSpec] = []
|
|
29
|
-
for path in sorted(commands_dir.
|
|
29
|
+
for path in sorted(commands_dir.rglob("*.md")):
|
|
30
|
+
# Skip cluster authoring docs — not commands.
|
|
31
|
+
if path.name == "AGENTS.md":
|
|
32
|
+
continue
|
|
30
33
|
text = path.read_text(encoding="utf-8")
|
|
31
34
|
data, _offset = parse_frontmatter(text)
|
|
32
35
|
if data is None:
|
package/scripts/compress.py
CHANGED
|
@@ -312,6 +312,29 @@ def generate_gemini_md() -> None:
|
|
|
312
312
|
print(" ✅ Created GEMINI.md → AGENTS.md symlink")
|
|
313
313
|
|
|
314
314
|
|
|
315
|
+
def _command_slug(source_file: Path) -> str:
|
|
316
|
+
"""Return the flat .claude/skills/ slug for a command source file.
|
|
317
|
+
|
|
318
|
+
Top-level commands keep their stem (`commit.md` → `commit`). Nested
|
|
319
|
+
commands flatten the relative path with `-` (`council/default.md` →
|
|
320
|
+
`council-default`). Keeps slug collisions out of `.claude/skills/`
|
|
321
|
+
while preserving native nested invocation in `.agent-src/commands/`.
|
|
322
|
+
"""
|
|
323
|
+
rel = source_file.relative_to(COMMANDS_SOURCE)
|
|
324
|
+
return "-".join(rel.with_suffix("").parts)
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
def _iter_commands():
|
|
328
|
+
"""Yield (source_file, slug) for every command .md file (recursive)."""
|
|
329
|
+
if not COMMANDS_SOURCE.exists():
|
|
330
|
+
return
|
|
331
|
+
for source_file in sorted(COMMANDS_SOURCE.rglob("*.md")):
|
|
332
|
+
# Skip the cluster AGENTS.md authoring doc (not a command).
|
|
333
|
+
if source_file.name == "AGENTS.md":
|
|
334
|
+
continue
|
|
335
|
+
yield source_file, _command_slug(source_file)
|
|
336
|
+
|
|
337
|
+
|
|
315
338
|
def generate_claude_skills() -> None:
|
|
316
339
|
"""Create .claude/skills/ symlinks for ALL skills in .agent-src/skills/.
|
|
317
340
|
"""
|
|
@@ -321,16 +344,14 @@ def generate_claude_skills() -> None:
|
|
|
321
344
|
|
|
322
345
|
# All skill directories in .agent-src/skills/
|
|
323
346
|
skills = sorted([d.name for d in SKILLS_SOURCE.iterdir() if d.is_dir()])
|
|
324
|
-
# All command
|
|
325
|
-
|
|
326
|
-
if COMMANDS_SOURCE.exists():
|
|
327
|
-
command_names = {f.stem for f in COMMANDS_SOURCE.glob("*.md")}
|
|
347
|
+
# All command slugs (to protect from stale cleanup)
|
|
348
|
+
command_slugs = {slug for _, slug in _iter_commands()}
|
|
328
349
|
|
|
329
350
|
CLAUDE_SKILLS_DIR.mkdir(parents=True, exist_ok=True)
|
|
330
351
|
|
|
331
352
|
# Clean stale symlinks (but not converted commands or README)
|
|
332
353
|
for item in CLAUDE_SKILLS_DIR.iterdir():
|
|
333
|
-
if item.is_symlink() and item.name not in skills and item.name not in
|
|
354
|
+
if item.is_symlink() and item.name not in skills and item.name not in command_slugs and item.name != "README.md":
|
|
334
355
|
item.unlink()
|
|
335
356
|
|
|
336
357
|
count = 0
|
|
@@ -357,11 +378,15 @@ def extract_description_from_md(content: str) -> str:
|
|
|
357
378
|
|
|
358
379
|
|
|
359
380
|
def generate_claude_commands() -> None:
|
|
360
|
-
"""Create .claude/skills/{
|
|
381
|
+
"""Create .claude/skills/{slug}/SKILL.md symlinks for ALL Augment commands.
|
|
361
382
|
|
|
362
383
|
Commands in .agent-src/commands/ are the single source of truth.
|
|
363
384
|
They must include name: and disable-model-invocation: true in frontmatter
|
|
364
385
|
(added once, then maintained as part of the command file).
|
|
386
|
+
|
|
387
|
+
Top-level commands use their filename stem as the slug. Nested
|
|
388
|
+
cluster commands (e.g. `commands/council/default.md`) are flattened
|
|
389
|
+
to `council-default` so directories never collide in `.claude/skills/`.
|
|
365
390
|
"""
|
|
366
391
|
if not COMMANDS_SOURCE.exists():
|
|
367
392
|
print(" ⚠️ .agent-src/commands/ not found — skipping commands")
|
|
@@ -374,32 +399,53 @@ def generate_claude_commands() -> None:
|
|
|
374
399
|
if SKILLS_SOURCE.exists():
|
|
375
400
|
skill_names = {d.name for d in SKILLS_SOURCE.iterdir() if d.is_dir()}
|
|
376
401
|
|
|
402
|
+
# Track current command slugs for stale-directory cleanup
|
|
403
|
+
current_slugs: set[str] = set()
|
|
377
404
|
count = 0
|
|
378
405
|
skipped = 0
|
|
379
|
-
for source_file in
|
|
380
|
-
name = source_file.stem
|
|
381
|
-
|
|
406
|
+
for source_file, slug in _iter_commands():
|
|
382
407
|
# Skip if a real skill with the same name exists — skill takes priority
|
|
383
|
-
if
|
|
408
|
+
if slug in skill_names:
|
|
384
409
|
skipped += 1
|
|
385
410
|
continue
|
|
386
411
|
|
|
412
|
+
current_slugs.add(slug)
|
|
413
|
+
|
|
387
414
|
# Create skill directory (real dir, symlinked SKILL.md inside)
|
|
388
|
-
skill_dir = CLAUDE_SKILLS_DIR /
|
|
415
|
+
skill_dir = CLAUDE_SKILLS_DIR / slug
|
|
389
416
|
skill_dir.mkdir(parents=True, exist_ok=True)
|
|
390
417
|
|
|
391
418
|
skill_file = skill_dir / "SKILL.md"
|
|
392
419
|
if skill_file.exists() or skill_file.is_symlink():
|
|
393
420
|
skill_file.unlink()
|
|
394
421
|
|
|
395
|
-
# Symlink: .claude/skills/{
|
|
396
|
-
|
|
422
|
+
# Symlink: .claude/skills/{slug}/SKILL.md → ../../../.agent-src/commands/<rel-path>
|
|
423
|
+
rel_path = source_file.relative_to(COMMANDS_SOURCE)
|
|
424
|
+
rel_target = Path("../../../.agent-src/commands") / rel_path
|
|
397
425
|
skill_file.symlink_to(rel_target)
|
|
398
426
|
count += 1
|
|
399
427
|
|
|
428
|
+
# Clean stale command skill directories — real dirs from removed commands.
|
|
429
|
+
# Only delete if the directory contains exactly the SKILL.md symlink we created.
|
|
430
|
+
removed_dirs = 0
|
|
431
|
+
for item in CLAUDE_SKILLS_DIR.iterdir():
|
|
432
|
+
if not item.is_dir() or item.is_symlink():
|
|
433
|
+
continue
|
|
434
|
+
if item.name in skill_names or item.name in current_slugs:
|
|
435
|
+
continue
|
|
436
|
+
skill_md = item / "SKILL.md"
|
|
437
|
+
if skill_md.is_symlink():
|
|
438
|
+
entries = list(item.iterdir())
|
|
439
|
+
if len(entries) == 1 and entries[0].name == "SKILL.md":
|
|
440
|
+
skill_md.unlink()
|
|
441
|
+
item.rmdir()
|
|
442
|
+
removed_dirs += 1
|
|
443
|
+
|
|
400
444
|
msg = f" ✅ Created {count} command symlinks in .claude/skills/"
|
|
401
445
|
if skipped:
|
|
402
446
|
msg += f" ({skipped} skipped — same-name skill exists)"
|
|
447
|
+
if removed_dirs:
|
|
448
|
+
msg += f" ({removed_dirs} stale dirs removed)"
|
|
403
449
|
print(msg)
|
|
404
450
|
|
|
405
451
|
|
|
@@ -95,7 +95,10 @@ def _collect_rules() -> list[Entry]:
|
|
|
95
95
|
|
|
96
96
|
def _collect_commands() -> list[Entry]:
|
|
97
97
|
out = []
|
|
98
|
-
|
|
98
|
+
cmd_dir = SRC / "commands"
|
|
99
|
+
for cmd_md in sorted(cmd_dir.rglob("*.md")):
|
|
100
|
+
if cmd_md.name == "AGENTS.md":
|
|
101
|
+
continue
|
|
99
102
|
fm = _parse_frontmatter(cmd_md.read_text(encoding="utf-8"))
|
|
100
103
|
is_shim = bool(fm.get("superseded_by"))
|
|
101
104
|
extra = ""
|
|
@@ -103,12 +106,13 @@ def _collect_commands() -> list[Entry]:
|
|
|
103
106
|
extra = f"shim → /{fm['superseded_by']}"
|
|
104
107
|
elif fm.get("cluster"):
|
|
105
108
|
extra = f"cluster: {fm['cluster']}"
|
|
109
|
+
rel = cmd_md.relative_to(cmd_dir)
|
|
106
110
|
out.append(Entry(
|
|
107
111
|
kind="shim" if is_shim else "command",
|
|
108
112
|
name=fm.get("name") or cmd_md.stem,
|
|
109
113
|
description=_truncate(fm.get("description", "")),
|
|
110
114
|
extra=extra,
|
|
111
|
-
path=f".agent-src.uncompressed/commands/{
|
|
115
|
+
path=f".agent-src.uncompressed/commands/{rel}",
|
|
112
116
|
))
|
|
113
117
|
return out
|
|
114
118
|
|
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Generate the file-ownership matrix.
|
|
3
|
+
|
|
4
|
+
Produces:
|
|
5
|
+
|
|
6
|
+
* docs/contracts/file-ownership-matrix.json (machine, internal-locked)
|
|
7
|
+
* agents/contexts/structural/file-ownership-matrix.md (human-readable)
|
|
8
|
+
|
|
9
|
+
Walks `.agent-src.uncompressed/{rules,skills,commands,contexts,personas}/`,
|
|
10
|
+
parses frontmatter for `load_context:` / `load_context_eager:`, scans
|
|
11
|
+
markdown bodies for inline links to `.md` files inside the scanned roots,
|
|
12
|
+
and emits READ_ONLY edges plus depth-2 transitive closure of load_context
|
|
13
|
+
chains. Depth-3 chains abort the build (matches the 0.2.4 nesting cap).
|
|
14
|
+
|
|
15
|
+
Contract: docs/contracts/file-ownership-matrix.md
|
|
16
|
+
Roadmap: road-to-structural-optimization.md § 0.1
|
|
17
|
+
|
|
18
|
+
Modes:
|
|
19
|
+
--check Regenerate to memory and diff against committed JSON.
|
|
20
|
+
Exit 0 if identical, 1 if drifted.
|
|
21
|
+
(default) Regenerate JSON + MD in place; exit 0 on success.
|
|
22
|
+
|
|
23
|
+
Exit codes: 0 = ok, 1 = drift (--check), 2 = depth-3 chain, 3 = internal.
|
|
24
|
+
"""
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
import argparse
|
|
28
|
+
import json
|
|
29
|
+
import re
|
|
30
|
+
import sys
|
|
31
|
+
from dataclasses import dataclass, field
|
|
32
|
+
from pathlib import Path
|
|
33
|
+
from typing import Iterable
|
|
34
|
+
|
|
35
|
+
import yaml
|
|
36
|
+
|
|
37
|
+
ROOT = Path(__file__).resolve().parent.parent
|
|
38
|
+
SRC_ROOT = ROOT / ".agent-src.uncompressed"
|
|
39
|
+
|
|
40
|
+
SCAN_DIRS = ("rules", "skills", "commands", "contexts", "personas")
|
|
41
|
+
|
|
42
|
+
JSON_OUT = ROOT / "docs" / "contracts" / "file-ownership-matrix.json"
|
|
43
|
+
MD_OUT = ROOT / "agents" / "contexts" / "structural" / "file-ownership-matrix.md"
|
|
44
|
+
|
|
45
|
+
LINK_RE = re.compile(r"\]\(([^)]+\.md)(?:#[^)]*)?\)")
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@dataclass
|
|
49
|
+
class FileEntry:
|
|
50
|
+
path: str
|
|
51
|
+
kind: str
|
|
52
|
+
rule_type: str | None = None
|
|
53
|
+
load_context: list[str] = field(default_factory=list)
|
|
54
|
+
load_context_eager: list[str] = field(default_factory=list)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@dataclass
|
|
58
|
+
class Edge:
|
|
59
|
+
source: str
|
|
60
|
+
target: str
|
|
61
|
+
type: str
|
|
62
|
+
via: str
|
|
63
|
+
depth: int
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _rel(p: Path) -> str:
|
|
67
|
+
return p.relative_to(ROOT).as_posix()
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _kind_for(rel: str) -> str:
|
|
71
|
+
parts = rel.split("/")
|
|
72
|
+
if len(parts) >= 3 and parts[0] == ".agent-src.uncompressed":
|
|
73
|
+
return parts[1].rstrip("s") if parts[1] != "personas" else "persona"
|
|
74
|
+
return "unknown"
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _parse_frontmatter(p: Path) -> dict:
|
|
78
|
+
text = p.read_text(encoding="utf-8")
|
|
79
|
+
if not text.startswith("---\n"):
|
|
80
|
+
return {}
|
|
81
|
+
end = text.find("\n---\n", 4)
|
|
82
|
+
if end == -1:
|
|
83
|
+
return {}
|
|
84
|
+
try:
|
|
85
|
+
data = yaml.safe_load(text[4:end])
|
|
86
|
+
except yaml.YAMLError:
|
|
87
|
+
return {}
|
|
88
|
+
return data if isinstance(data, dict) else {}
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _collect_files(src_root: Path) -> list[Path]:
|
|
92
|
+
out: list[Path] = []
|
|
93
|
+
for sub in SCAN_DIRS:
|
|
94
|
+
d = src_root / sub
|
|
95
|
+
if d.exists():
|
|
96
|
+
out.extend(sorted(d.rglob("*.md")))
|
|
97
|
+
return out
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _resolve(target: str, src_root: Path) -> Path | None:
|
|
101
|
+
"""Resolve a path string (repo-relative or short) into an absolute Path
|
|
102
|
+
under src_root or the repo root. Return None if not under a scanned root."""
|
|
103
|
+
cand = src_root.parent / target if "/" in target else src_root / target
|
|
104
|
+
try:
|
|
105
|
+
rel = cand.resolve().relative_to(src_root.parent)
|
|
106
|
+
except ValueError:
|
|
107
|
+
return None
|
|
108
|
+
parts = rel.parts
|
|
109
|
+
if len(parts) >= 3 and parts[0] == ".agent-src.uncompressed" and parts[1] in SCAN_DIRS:
|
|
110
|
+
return cand if cand.exists() else None
|
|
111
|
+
return None
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def build_matrix(src_root: Path) -> tuple[dict[str, FileEntry], list[Edge], list[str]]:
|
|
115
|
+
"""Build the file map + edge list. Returns (files, edges, depth3_chains).
|
|
116
|
+
|
|
117
|
+
depth3_chains is non-empty iff the depth invariant is violated; the
|
|
118
|
+
caller must abort with exit code 2.
|
|
119
|
+
"""
|
|
120
|
+
files: dict[str, FileEntry] = {}
|
|
121
|
+
for f in _collect_files(src_root):
|
|
122
|
+
rel = f.relative_to(src_root.parent).as_posix()
|
|
123
|
+
fm = _parse_frontmatter(f)
|
|
124
|
+
rtype = fm.get("type")
|
|
125
|
+
if isinstance(rtype, str):
|
|
126
|
+
rtype = rtype.strip('"').strip("'")
|
|
127
|
+
else:
|
|
128
|
+
rtype = None
|
|
129
|
+
lazy = fm.get("load_context") or []
|
|
130
|
+
eager = fm.get("load_context_eager") or []
|
|
131
|
+
if not isinstance(lazy, list):
|
|
132
|
+
lazy = []
|
|
133
|
+
if not isinstance(eager, list):
|
|
134
|
+
eager = []
|
|
135
|
+
files[rel] = FileEntry(
|
|
136
|
+
path=rel,
|
|
137
|
+
kind=_kind_for(rel),
|
|
138
|
+
rule_type=rtype,
|
|
139
|
+
load_context=[str(x) for x in lazy if isinstance(x, str)],
|
|
140
|
+
load_context_eager=[str(x) for x in eager if isinstance(x, str)],
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
edges: list[Edge] = []
|
|
144
|
+
for rel, entry in files.items():
|
|
145
|
+
for tgt in entry.load_context:
|
|
146
|
+
edges.append(Edge(rel, tgt, "READ_ONLY", "load_context", 1))
|
|
147
|
+
for tgt in entry.load_context_eager:
|
|
148
|
+
edges.append(Edge(rel, tgt, "READ_ONLY", "load_context_eager", 1))
|
|
149
|
+
|
|
150
|
+
# Body markdown links — only count edges to files we know about
|
|
151
|
+
for rel, entry in files.items():
|
|
152
|
+
body = (src_root.parent / rel).read_text(encoding="utf-8")
|
|
153
|
+
body = body.split("\n---\n", 1)[-1] if body.startswith("---\n") else body
|
|
154
|
+
seen_targets: set[str] = set()
|
|
155
|
+
for m in LINK_RE.finditer(body):
|
|
156
|
+
href = m.group(1).strip()
|
|
157
|
+
if href.startswith("http"):
|
|
158
|
+
continue
|
|
159
|
+
resolved = _resolve_link(rel, href, src_root)
|
|
160
|
+
if resolved is None or resolved == rel or resolved in seen_targets:
|
|
161
|
+
continue
|
|
162
|
+
if resolved in files:
|
|
163
|
+
seen_targets.add(resolved)
|
|
164
|
+
edges.append(Edge(rel, resolved, "READ_ONLY", "body_link", 1))
|
|
165
|
+
|
|
166
|
+
# Transitive closure on load_context* edges, depth 2; depth 3 aborts.
|
|
167
|
+
lc_edges_by_src: dict[str, list[str]] = {}
|
|
168
|
+
for e in edges:
|
|
169
|
+
if e.via in ("load_context", "load_context_eager"):
|
|
170
|
+
lc_edges_by_src.setdefault(e.source, []).append(e.target)
|
|
171
|
+
|
|
172
|
+
transitive: list[Edge] = []
|
|
173
|
+
depth3: list[str] = []
|
|
174
|
+
for src, lvl1_targets in lc_edges_by_src.items():
|
|
175
|
+
for t1 in lvl1_targets:
|
|
176
|
+
for t2 in lc_edges_by_src.get(t1, []):
|
|
177
|
+
if t2 == src or t2 == t1:
|
|
178
|
+
continue
|
|
179
|
+
transitive.append(Edge(src, t2, "READ_ONLY", "load_context_transitive", 2))
|
|
180
|
+
# depth-3 probe
|
|
181
|
+
for t3 in lc_edges_by_src.get(t2, []):
|
|
182
|
+
if t3 in (src, t1, t2):
|
|
183
|
+
continue
|
|
184
|
+
depth3.append(f"{src} → {t1} → {t2} → {t3}")
|
|
185
|
+
|
|
186
|
+
edges.extend(transitive)
|
|
187
|
+
for rel in files:
|
|
188
|
+
edges.append(Edge(rel, rel, "WRITE", "self", 0))
|
|
189
|
+
|
|
190
|
+
edges.sort(key=lambda e: (e.source, e.target, e.via, e.depth))
|
|
191
|
+
return files, edges, depth3
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def _resolve_link(source_rel: str, href: str, src_root: Path) -> str | None:
|
|
195
|
+
"""Resolve a markdown link href (relative to source file) to a repo-relative
|
|
196
|
+
path inside a scanned root, or None."""
|
|
197
|
+
if href.startswith(".agent-src.uncompressed/") or href.startswith("agents/"):
|
|
198
|
+
cand = (src_root.parent / href).resolve()
|
|
199
|
+
else:
|
|
200
|
+
base = (src_root.parent / source_rel).parent
|
|
201
|
+
cand = (base / href).resolve()
|
|
202
|
+
try:
|
|
203
|
+
rel = cand.relative_to(src_root.parent).as_posix()
|
|
204
|
+
except ValueError:
|
|
205
|
+
return None
|
|
206
|
+
parts = rel.split("/")
|
|
207
|
+
if len(parts) >= 3 and parts[0] == ".agent-src.uncompressed" and parts[1] in SCAN_DIRS:
|
|
208
|
+
return rel if cand.exists() else None
|
|
209
|
+
return None
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def _to_json(files: dict[str, FileEntry], edges: list[Edge]) -> dict:
|
|
213
|
+
return {
|
|
214
|
+
"version": 1,
|
|
215
|
+
"generated_by": "scripts/generate_ownership_matrix.py",
|
|
216
|
+
"source_of_truth": ".agent-src.uncompressed/",
|
|
217
|
+
"files": {
|
|
218
|
+
rel: {
|
|
219
|
+
"kind": e.kind,
|
|
220
|
+
"rule_type": e.rule_type,
|
|
221
|
+
"load_context": e.load_context,
|
|
222
|
+
"load_context_eager": e.load_context_eager,
|
|
223
|
+
}
|
|
224
|
+
for rel, e in sorted(files.items())
|
|
225
|
+
},
|
|
226
|
+
"edges": [
|
|
227
|
+
{
|
|
228
|
+
"source": e.source,
|
|
229
|
+
"target": e.target,
|
|
230
|
+
"type": e.type,
|
|
231
|
+
"via": e.via,
|
|
232
|
+
"depth": e.depth,
|
|
233
|
+
}
|
|
234
|
+
for e in edges
|
|
235
|
+
],
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def _to_markdown(payload: dict) -> str:
|
|
240
|
+
lines: list[str] = [
|
|
241
|
+
"# File-ownership matrix (regenerated)",
|
|
242
|
+
"",
|
|
243
|
+
"> **Do not edit.** Regenerated by `scripts/generate_ownership_matrix.py`.",
|
|
244
|
+
"> Schema: [`docs/contracts/file-ownership-matrix.md`](../../../docs/contracts/file-ownership-matrix.md).",
|
|
245
|
+
"",
|
|
246
|
+
f"- Schema version: `{payload['version']}`",
|
|
247
|
+
f"- Source of truth: `{payload['source_of_truth']}`",
|
|
248
|
+
f"- Files indexed: **{len(payload['files'])}**",
|
|
249
|
+
f"- Edges (incl. self-WRITE): **{len(payload['edges'])}**",
|
|
250
|
+
"",
|
|
251
|
+
"## READ_ONLY edges",
|
|
252
|
+
"",
|
|
253
|
+
"| Source | Target | Via | Depth |",
|
|
254
|
+
"|---|---|---|---:|",
|
|
255
|
+
]
|
|
256
|
+
ro = [e for e in payload["edges"] if e["type"] == "READ_ONLY"]
|
|
257
|
+
for e in ro:
|
|
258
|
+
lines.append(f"| `{e['source']}` | `{e['target']}` | `{e['via']}` | {e['depth']} |")
|
|
259
|
+
if not ro:
|
|
260
|
+
lines.append("| _(none)_ | | | |")
|
|
261
|
+
lines += [
|
|
262
|
+
"",
|
|
263
|
+
"## Files by kind",
|
|
264
|
+
"",
|
|
265
|
+
"| Kind | Count |",
|
|
266
|
+
"|---|---:|",
|
|
267
|
+
]
|
|
268
|
+
counts: dict[str, int] = {}
|
|
269
|
+
for f in payload["files"].values():
|
|
270
|
+
counts[f["kind"]] = counts.get(f["kind"], 0) + 1
|
|
271
|
+
for k in sorted(counts):
|
|
272
|
+
lines.append(f"| `{k}` | {counts[k]} |")
|
|
273
|
+
lines.append("")
|
|
274
|
+
return "\n".join(lines)
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def _write_outputs(payload: dict, json_out: Path, md_out: Path) -> None:
|
|
278
|
+
json_out.parent.mkdir(parents=True, exist_ok=True)
|
|
279
|
+
md_out.parent.mkdir(parents=True, exist_ok=True)
|
|
280
|
+
json_out.write_text(json.dumps(payload, indent=2, sort_keys=False) + "\n", encoding="utf-8")
|
|
281
|
+
md_out.write_text(_to_markdown(payload) + "\n", encoding="utf-8")
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def main(argv: Iterable[str] | None = None) -> int:
|
|
285
|
+
ap = argparse.ArgumentParser(description=__doc__)
|
|
286
|
+
ap.add_argument("--check", action="store_true",
|
|
287
|
+
help="Regenerate to memory and diff against committed JSON.")
|
|
288
|
+
args = ap.parse_args(list(argv) if argv is not None else None)
|
|
289
|
+
|
|
290
|
+
if not SRC_ROOT.is_dir():
|
|
291
|
+
print(f"❌ source dir missing: {SRC_ROOT}", file=sys.stderr)
|
|
292
|
+
return 3
|
|
293
|
+
|
|
294
|
+
files, edges, depth3 = build_matrix(SRC_ROOT)
|
|
295
|
+
if depth3:
|
|
296
|
+
print("❌ load_context depth-3 chain detected (limit is 2):", file=sys.stderr)
|
|
297
|
+
for chain in depth3:
|
|
298
|
+
print(f" 🔴 {chain}", file=sys.stderr)
|
|
299
|
+
return 2
|
|
300
|
+
|
|
301
|
+
payload = _to_json(files, edges)
|
|
302
|
+
|
|
303
|
+
if args.check:
|
|
304
|
+
if not JSON_OUT.exists():
|
|
305
|
+
print(f"❌ {JSON_OUT.relative_to(ROOT)} not committed; run `task generate-ownership-matrix`",
|
|
306
|
+
file=sys.stderr)
|
|
307
|
+
return 1
|
|
308
|
+
committed = json.loads(JSON_OUT.read_text(encoding="utf-8"))
|
|
309
|
+
if committed != payload:
|
|
310
|
+
print("❌ ownership matrix is stale — run `task generate-ownership-matrix` and commit",
|
|
311
|
+
file=sys.stderr)
|
|
312
|
+
return 1
|
|
313
|
+
print(f"✅ ownership matrix in sync ({len(files)} files, {len(edges)} edges)")
|
|
314
|
+
return 0
|
|
315
|
+
|
|
316
|
+
_write_outputs(payload, JSON_OUT, MD_OUT)
|
|
317
|
+
print(f"✅ wrote {JSON_OUT.relative_to(ROOT)} ({len(files)} files, {len(edges)} edges)")
|
|
318
|
+
print(f"✅ wrote {MD_OUT.relative_to(ROOT)}")
|
|
319
|
+
return 0
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
if __name__ == "__main__":
|
|
323
|
+
sys.exit(main())
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Augment Code lifecycle-hook trampoline for roadmap-progress-sync.
|
|
3
|
+
#
|
|
4
|
+
# Augment requires hook scripts to use the .sh extension and live at
|
|
5
|
+
# either a system path (/etc/augment/...) or user scope
|
|
6
|
+
# (~/.augment/...). This trampoline lives at user scope and dispatches
|
|
7
|
+
# every PostToolUse event to whichever workspace fired it, so a single
|
|
8
|
+
# install covers every project that has ./agent-config available.
|
|
9
|
+
#
|
|
10
|
+
# Behaviour:
|
|
11
|
+
# - Read the JSON event from stdin into a buffer.
|
|
12
|
+
# - Extract workspace_roots[0]; bail silently when missing.
|
|
13
|
+
# - cd into that workspace; bail silently when it is not a directory
|
|
14
|
+
# or does not contain ./agent-config.
|
|
15
|
+
# - Re-pipe the original JSON into
|
|
16
|
+
# ./agent-config roadmap-progress:hook --platform augment
|
|
17
|
+
# so roadmap_progress_hook.py runs the path filter and decides
|
|
18
|
+
# whether to regenerate the dashboard.
|
|
19
|
+
# - Always exit 0 — PostToolUse hooks must never block.
|
|
20
|
+
|
|
21
|
+
set -u
|
|
22
|
+
|
|
23
|
+
EVENT_DATA="$(cat)"
|
|
24
|
+
|
|
25
|
+
# Extract workspace_roots[0] using whichever JSON tool is available.
|
|
26
|
+
WORKSPACE=""
|
|
27
|
+
if command -v jq >/dev/null 2>&1; then
|
|
28
|
+
WORKSPACE="$(printf '%s' "$EVENT_DATA" \
|
|
29
|
+
| jq -r '.workspace_roots[0] // empty' 2>/dev/null)"
|
|
30
|
+
elif command -v python3 >/dev/null 2>&1; then
|
|
31
|
+
WORKSPACE="$(printf '%s' "$EVENT_DATA" | python3 -c '
|
|
32
|
+
import json, sys
|
|
33
|
+
try:
|
|
34
|
+
data = json.load(sys.stdin)
|
|
35
|
+
except Exception:
|
|
36
|
+
sys.exit(0)
|
|
37
|
+
roots = data.get("workspace_roots") or []
|
|
38
|
+
if roots:
|
|
39
|
+
print(roots[0])
|
|
40
|
+
' 2>/dev/null)"
|
|
41
|
+
fi
|
|
42
|
+
|
|
43
|
+
if [ -z "$WORKSPACE" ] || [ ! -d "$WORKSPACE" ]; then
|
|
44
|
+
exit 0
|
|
45
|
+
fi
|
|
46
|
+
|
|
47
|
+
cd "$WORKSPACE" 2>/dev/null || exit 0
|
|
48
|
+
|
|
49
|
+
if [ ! -x ./agent-config ]; then
|
|
50
|
+
exit 0
|
|
51
|
+
fi
|
|
52
|
+
|
|
53
|
+
printf '%s' "$EVENT_DATA" \
|
|
54
|
+
| ./agent-config roadmap-progress:hook --platform augment \
|
|
55
|
+
>/dev/null 2>&1 || true
|
|
56
|
+
|
|
57
|
+
exit 0
|
package/scripts/install.py
CHANGED
|
@@ -464,44 +464,65 @@ def ensure_augment_bridge(project_root: Path, force: bool) -> None:
|
|
|
464
464
|
# .augment/settings.json is plugin enablement, not hooks.
|
|
465
465
|
AUGMENT_USER_DIR = Path.home() / ".augment"
|
|
466
466
|
AUGMENT_USER_HOOKS_DIR = AUGMENT_USER_DIR / "hooks"
|
|
467
|
-
|
|
468
|
-
|
|
467
|
+
AUGMENT_CHAT_HISTORY_TRAMPOLINE = "augment-chat-history.sh"
|
|
468
|
+
AUGMENT_ROADMAP_PROGRESS_TRAMPOLINE = "augment-roadmap-progress.sh"
|
|
469
|
+
# (trampoline name, list of events it should fire on). Each trampoline
|
|
470
|
+
# is a self-contained workspace router; mapping them per-event keeps the
|
|
471
|
+
# wiring explicit and lets a future hook bind to a different surface
|
|
472
|
+
# without touching the chat-history one.
|
|
473
|
+
AUGMENT_HOOK_BINDINGS = (
|
|
474
|
+
(AUGMENT_CHAT_HISTORY_TRAMPOLINE,
|
|
475
|
+
("SessionStart", "SessionEnd", "Stop", "PostToolUse")),
|
|
476
|
+
(AUGMENT_ROADMAP_PROGRESS_TRAMPOLINE,
|
|
477
|
+
("PostToolUse",)),
|
|
478
|
+
)
|
|
479
|
+
|
|
480
|
+
|
|
481
|
+
def _deploy_augment_trampoline(package_root: Path, name: str, force: bool) -> Path | None:
|
|
482
|
+
src = package_root / "scripts" / "hooks" / name
|
|
483
|
+
if not src.exists():
|
|
484
|
+
skip(f"augment trampoline missing in package: {src}")
|
|
485
|
+
return None
|
|
486
|
+
AUGMENT_USER_HOOKS_DIR.mkdir(parents=True, exist_ok=True)
|
|
487
|
+
dst = AUGMENT_USER_HOOKS_DIR / name
|
|
488
|
+
src_text = src.read_text(encoding="utf-8")
|
|
489
|
+
if dst.exists() and dst.read_text(encoding="utf-8") == src_text and not force:
|
|
490
|
+
skip(f"~/.augment/hooks/{name} already up to date")
|
|
491
|
+
else:
|
|
492
|
+
dst.write_text(src_text, encoding="utf-8")
|
|
493
|
+
dst.chmod(0o755)
|
|
494
|
+
success(f"~/.augment/hooks/{name} installed")
|
|
495
|
+
return dst
|
|
469
496
|
|
|
470
497
|
|
|
471
498
|
def ensure_augment_user_hooks(package_root: Path, force: bool) -> None:
|
|
472
|
-
"""Deploy the Augment lifecycle-hook
|
|
499
|
+
"""Deploy the Augment lifecycle-hook trampolines at user scope.
|
|
473
500
|
|
|
474
501
|
Augment hook scripts must use the .sh extension and be referenced by
|
|
475
502
|
absolute path; user scope is the only surface that fires for both the
|
|
476
503
|
CLI and the IDE plugins. This installs once per developer (not per
|
|
477
|
-
project) —
|
|
478
|
-
and dispatches into whichever project is active at hook-fire
|
|
504
|
+
project) — each trampoline reads workspace_roots from the event
|
|
505
|
+
payload and dispatches into whichever project is active at hook-fire
|
|
506
|
+
time.
|
|
507
|
+
|
|
508
|
+
Two trampolines are deployed:
|
|
509
|
+
- augment-chat-history.sh → SessionStart/SessionEnd/Stop/PostToolUse
|
|
510
|
+
- augment-roadmap-progress.sh → PostToolUse (path-filtered to
|
|
511
|
+
agents/roadmaps/ — see scripts/roadmap_progress_hook.py)
|
|
479
512
|
"""
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
513
|
+
per_event: dict[str, list] = {}
|
|
514
|
+
for name, events in AUGMENT_HOOK_BINDINGS:
|
|
515
|
+
dst = _deploy_augment_trampoline(package_root, name, force)
|
|
516
|
+
if dst is None:
|
|
517
|
+
continue
|
|
518
|
+
entry = {"hooks": [{"type": "command", "command": str(dst)}]}
|
|
519
|
+
for event in events:
|
|
520
|
+
per_event.setdefault(event, []).append(entry)
|
|
487
521
|
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
skip(f"~/.augment/hooks/{AUGMENT_TRAMPOLINE_NAME} already up to date")
|
|
491
|
-
else:
|
|
492
|
-
dst.write_text(src_text, encoding="utf-8")
|
|
493
|
-
dst.chmod(0o755)
|
|
494
|
-
success(f"~/.augment/hooks/{AUGMENT_TRAMPOLINE_NAME} installed")
|
|
522
|
+
if not per_event:
|
|
523
|
+
return
|
|
495
524
|
|
|
496
|
-
|
|
497
|
-
"hooks": [
|
|
498
|
-
{
|
|
499
|
-
"type": "command",
|
|
500
|
-
"command": str(dst),
|
|
501
|
-
},
|
|
502
|
-
],
|
|
503
|
-
}
|
|
504
|
-
settings_patch: dict = {"hooks": {event: [hook_entry] for event in AUGMENT_HOOK_EVENTS}}
|
|
525
|
+
settings_patch: dict = {"hooks": per_event}
|
|
505
526
|
merge_json_file(
|
|
506
527
|
AUGMENT_USER_DIR / "settings.json",
|
|
507
528
|
settings_patch,
|