@event4u/agent-config 1.13.0 → 1.14.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/agent-handoff.md +3 -0
- package/.agent-src/commands/agent-status.md +3 -0
- package/.agent-src/commands/agents-audit.md +4 -0
- package/.agent-src/commands/agents-cleanup.md +6 -1
- package/.agent-src/commands/agents-prepare.md +3 -0
- package/.agent-src/commands/analyze-reference-repo.md +4 -0
- package/.agent-src/commands/bug-fix.md +5 -1
- package/.agent-src/commands/bug-investigate.md +4 -0
- package/.agent-src/commands/chat-history-checkpoint.md +126 -0
- package/.agent-src/commands/chat-history-clear.md +5 -0
- package/.agent-src/commands/chat-history-resume.md +5 -0
- package/.agent-src/commands/chat-history.md +5 -0
- package/.agent-src/commands/check-current-md.md +126 -0
- package/.agent-src/commands/commit-in-chunks.md +98 -0
- package/.agent-src/commands/commit.md +4 -0
- package/.agent-src/commands/compress.md +3 -0
- package/.agent-src/commands/context-create.md +4 -0
- package/.agent-src/commands/context-refactor.md +4 -0
- package/.agent-src/commands/copilot-agents-init.md +3 -0
- package/.agent-src/commands/copilot-agents-optimize.md +3 -0
- package/.agent-src/commands/create-pr-description.md +4 -0
- package/.agent-src/commands/create-pr.md +4 -0
- package/.agent-src/commands/do-and-judge.md +4 -1
- package/.agent-src/commands/do-in-steps.md +3 -0
- package/.agent-src/commands/e2e-heal.md +4 -0
- package/.agent-src/commands/e2e-plan.md +4 -0
- package/.agent-src/commands/estimate-ticket.md +4 -1
- package/.agent-src/commands/feature-dev.md +4 -0
- package/.agent-src/commands/feature-explore.md +4 -0
- package/.agent-src/commands/feature-plan.md +4 -0
- package/.agent-src/commands/feature-refactor.md +4 -0
- package/.agent-src/commands/feature-roadmap.md +6 -0
- package/.agent-src/commands/fix-ci.md +4 -0
- package/.agent-src/commands/fix-portability.md +3 -0
- package/.agent-src/commands/fix-pr-bot-comments.md +4 -0
- package/.agent-src/commands/fix-pr-comments.md +4 -0
- package/.agent-src/commands/fix-pr-developer-comments.md +4 -0
- package/.agent-src/commands/fix-references.md +3 -0
- package/.agent-src/commands/fix-seeder.md +4 -0
- package/.agent-src/commands/implement-ticket.md +39 -13
- package/.agent-src/commands/jira-ticket.md +4 -0
- package/.agent-src/commands/judge.md +3 -0
- package/.agent-src/commands/memory-add.md +5 -3
- package/.agent-src/commands/memory-full.md +5 -2
- package/.agent-src/commands/memory-promote.md +7 -6
- package/.agent-src/commands/mode.md +3 -0
- package/.agent-src/commands/module-create.md +4 -0
- package/.agent-src/commands/module-explore.md +4 -0
- package/.agent-src/commands/onboard.md +24 -0
- package/.agent-src/commands/optimize-agents.md +4 -0
- package/.agent-src/commands/optimize-augmentignore.md +3 -0
- package/.agent-src/commands/optimize-rtk-filters.md +3 -0
- package/.agent-src/commands/optimize-skills.md +4 -0
- package/.agent-src/commands/override-create.md +4 -0
- package/.agent-src/commands/override-manage.md +4 -0
- package/.agent-src/commands/package-reset.md +3 -0
- package/.agent-src/commands/package-test.md +3 -0
- package/.agent-src/commands/prepare-for-review.md +4 -0
- package/.agent-src/commands/project-analyze.md +4 -0
- package/.agent-src/commands/project-health.md +4 -0
- package/.agent-src/commands/propose-memory.md +6 -8
- package/.agent-src/commands/quality-fix.md +4 -0
- package/.agent-src/commands/refine-ticket.md +4 -1
- package/.agent-src/commands/review-changes.md +4 -0
- package/.agent-src/commands/review-routing.md +4 -0
- package/.agent-src/commands/roadmap-create.md +7 -0
- package/.agent-src/commands/roadmap-execute.md +12 -1
- package/.agent-src/commands/rule-compliance-audit.md +4 -0
- package/.agent-src/commands/set-cost-profile.md +3 -0
- package/.agent-src/commands/sync-agent-settings.md +3 -0
- package/.agent-src/commands/sync-gitignore.md +3 -0
- package/.agent-src/commands/tests-create.md +4 -0
- package/.agent-src/commands/tests-execute.md +4 -0
- package/.agent-src/commands/threat-model.md +4 -0
- package/.agent-src/commands/update-form-request-messages.md +4 -0
- package/.agent-src/commands/upstream-contribute.md +4 -0
- package/.agent-src/commands/work.md +161 -0
- package/.agent-src/guidelines/agent-infra/engineering-memory-data-format.md +2 -6
- package/.agent-src/guidelines/agent-infra/layered-settings.md +0 -1
- package/.agent-src/guidelines/agent-infra/memory-access.md +0 -7
- package/.agent-src/guidelines/agent-infra/role-contracts.md +2 -4
- package/.agent-src/guidelines/agent-infra/self-improvement-pipeline.md +0 -1
- package/.agent-src/guidelines/php/patterns/strategy.md +180 -2
- package/.agent-src/personas/README.md +0 -1
- package/.agent-src/rules/artifact-drafting-protocol.md +7 -2
- package/.agent-src/rules/artifact-engagement-recording.md +133 -0
- package/.agent-src/rules/ask-when-uncertain.md +18 -13
- package/.agent-src/rules/augment-portability.md +8 -0
- package/.agent-src/rules/autonomous-execution.md +158 -0
- package/.agent-src/rules/chat-history.md +147 -118
- package/.agent-src/rules/cli-output-handling.md +26 -3
- package/.agent-src/rules/command-suggestion.md +133 -0
- package/.agent-src/rules/commit-policy.md +99 -0
- package/.agent-src/rules/direct-answers.md +114 -0
- package/.agent-src/rules/docs-sync.md +36 -0
- package/.agent-src/rules/downstream-changes.md +10 -9
- package/.agent-src/rules/improve-before-implement.md +9 -6
- package/.agent-src/rules/language-and-tone.md +81 -6
- package/.agent-src/rules/non-destructive-by-default.md +117 -0
- package/.agent-src/rules/package-ci-checks.md +4 -0
- package/.agent-src/rules/preservation-guard.md +20 -0
- package/.agent-src/rules/roadmap-progress-sync.md +103 -30
- package/.agent-src/rules/scope-control.md +42 -1
- package/.agent-src/rules/size-enforcement.md +1 -3
- package/.agent-src/rules/skill-quality.md +3 -8
- package/.agent-src/rules/ui-audit-before-build.md +106 -0
- package/.agent-src/rules/user-interaction.md +82 -50
- package/.agent-src/scripts/update_roadmap_progress.py +17 -5
- package/.agent-src/skills/blade-ui/SKILL.md +30 -5
- package/.agent-src/skills/command-routing/SKILL.md +32 -0
- package/.agent-src/skills/command-writing/SKILL.md +41 -2
- package/.agent-src/skills/description-assist/SKILL.md +21 -0
- package/.agent-src/skills/estimate-ticket/SKILL.md +0 -1
- package/.agent-src/skills/existing-ui-audit/SKILL.md +187 -0
- package/.agent-src/skills/fe-design/SKILL.md +72 -60
- package/.agent-src/skills/finishing-a-development-branch/SKILL.md +4 -0
- package/.agent-src/skills/flux/SKILL.md +31 -4
- package/.agent-src/skills/guideline-writing/SKILL.md +24 -2
- package/.agent-src/skills/learning-to-rule-or-skill/SKILL.md +51 -9
- package/.agent-src/skills/livewire/SKILL.md +30 -4
- package/.agent-src/skills/md-language-check/SKILL.md +103 -0
- package/.agent-src/skills/php-coder/SKILL.md +24 -0
- package/.agent-src/skills/react-shadcn-ui/SKILL.md +121 -0
- package/.agent-src/skills/refine-prompt/SKILL.md +220 -0
- package/.agent-src/skills/refine-ticket/SKILL.md +2 -4
- package/.agent-src/skills/roadmap-management/SKILL.md +10 -3
- package/.agent-src/skills/rule-writing/SKILL.md +23 -1
- package/.agent-src/skills/skill-writing/SKILL.md +1 -3
- package/.agent-src/skills/upstream-contribute/SKILL.md +1 -1
- package/.agent-src/skills/using-git-worktrees/SKILL.md +3 -1
- package/.agent-src/templates/AGENTS.md +24 -6
- package/.agent-src/templates/agent-settings.md +149 -0
- package/.agent-src/templates/roadmaps.md +8 -2
- package/.agent-src/templates/scripts/implement_ticket/__init__.py +63 -26
- package/.agent-src/templates/scripts/implement_ticket/__main__.py +8 -2
- package/.agent-src/templates/scripts/telemetry/__init__.py +42 -0
- package/.agent-src/templates/scripts/telemetry/aggregator.py +154 -0
- package/.agent-src/templates/scripts/telemetry/boundary.py +171 -0
- package/.agent-src/templates/scripts/telemetry/engagement.py +238 -0
- package/.agent-src/templates/scripts/telemetry/report_renderer.py +170 -0
- package/.agent-src/templates/scripts/telemetry/settings.py +112 -0
- package/.agent-src/templates/scripts/telemetry_record.py +166 -0
- package/.agent-src/templates/scripts/telemetry_report.py +161 -0
- package/.agent-src/templates/scripts/telemetry_status.py +142 -0
- package/.agent-src/templates/scripts/work_engine/__init__.py +58 -0
- package/.agent-src/templates/scripts/work_engine/__main__.py +9 -0
- package/.agent-src/templates/scripts/work_engine/cli.py +592 -0
- package/.agent-src/templates/scripts/{implement_ticket → work_engine}/delivery_state.py +7 -0
- package/.agent-src/templates/scripts/work_engine/directives/__init__.py +33 -0
- package/.agent-src/templates/scripts/work_engine/directives/backend/__init__.py +98 -0
- package/.agent-src/templates/scripts/{implement_ticket/steps → work_engine/directives/backend}/analyze.py +1 -1
- package/.agent-src/templates/scripts/{implement_ticket/steps → work_engine/directives/backend}/implement.py +2 -2
- package/.agent-src/templates/scripts/{implement_ticket/steps → work_engine/directives/backend}/memory.py +1 -1
- package/.agent-src/templates/scripts/{implement_ticket/steps → work_engine/directives/backend}/plan.py +1 -1
- package/.agent-src/templates/scripts/work_engine/directives/backend/refine.py +396 -0
- package/.agent-src/templates/scripts/{implement_ticket/steps → work_engine/directives/backend}/report.py +36 -4
- package/.agent-src/templates/scripts/{implement_ticket/steps → work_engine/directives/backend}/test.py +2 -2
- package/.agent-src/templates/scripts/{implement_ticket/steps → work_engine/directives/backend}/verify.py +2 -2
- package/.agent-src/templates/scripts/work_engine/directives/mixed/__init__.py +116 -0
- package/.agent-src/templates/scripts/work_engine/directives/mixed/contract.py +254 -0
- package/.agent-src/templates/scripts/work_engine/directives/mixed/stitch.py +229 -0
- package/.agent-src/templates/scripts/work_engine/directives/mixed/ui.py +231 -0
- package/.agent-src/templates/scripts/work_engine/directives/ui/__init__.py +113 -0
- package/.agent-src/templates/scripts/work_engine/directives/ui/_passthrough.py +44 -0
- package/.agent-src/templates/scripts/work_engine/directives/ui/apply.py +241 -0
- package/.agent-src/templates/scripts/work_engine/directives/ui/audit.py +414 -0
- package/.agent-src/templates/scripts/work_engine/directives/ui/design.py +335 -0
- package/.agent-src/templates/scripts/work_engine/directives/ui/polish.py +510 -0
- package/.agent-src/templates/scripts/work_engine/directives/ui/review.py +468 -0
- package/.agent-src/templates/scripts/work_engine/directives/ui_trivial/__init__.py +119 -0
- package/.agent-src/templates/scripts/work_engine/directives/ui_trivial/_skipped.py +37 -0
- package/.agent-src/templates/scripts/work_engine/directives/ui_trivial/apply.py +165 -0
- package/.agent-src/templates/scripts/work_engine/directives/ui_trivial/refine.py +66 -0
- package/.agent-src/templates/scripts/work_engine/directives/ui_trivial/report.py +62 -0
- package/.agent-src/templates/scripts/work_engine/directives/ui_trivial/test.py +115 -0
- package/.agent-src/templates/scripts/work_engine/dispatcher.py +331 -0
- package/.agent-src/templates/scripts/work_engine/hooks/__init__.py +54 -0
- package/.agent-src/templates/scripts/work_engine/hooks/builtin/__init__.py +32 -0
- package/.agent-src/templates/scripts/work_engine/hooks/builtin/_chat_history_base.py +103 -0
- package/.agent-src/templates/scripts/work_engine/hooks/builtin/chat_history_append.py +44 -0
- package/.agent-src/templates/scripts/work_engine/hooks/builtin/chat_history_halt_append.py +42 -0
- package/.agent-src/templates/scripts/work_engine/hooks/builtin/chat_history_heartbeat.py +50 -0
- package/.agent-src/templates/scripts/work_engine/hooks/builtin/chat_history_turn_check.py +49 -0
- package/.agent-src/templates/scripts/work_engine/hooks/builtin/directive_set_guard.py +53 -0
- package/.agent-src/templates/scripts/work_engine/hooks/builtin/halt_surface_audit.py +50 -0
- package/.agent-src/templates/scripts/work_engine/hooks/builtin/state_shape_validation.py +52 -0
- package/.agent-src/templates/scripts/work_engine/hooks/builtin/trace.py +84 -0
- package/.agent-src/templates/scripts/work_engine/hooks/context.py +66 -0
- package/.agent-src/templates/scripts/work_engine/hooks/events.py +44 -0
- package/.agent-src/templates/scripts/work_engine/hooks/exceptions.py +79 -0
- package/.agent-src/templates/scripts/work_engine/hooks/registry.py +60 -0
- package/.agent-src/templates/scripts/work_engine/hooks/runner.py +73 -0
- package/.agent-src/templates/scripts/work_engine/hooks/settings.py +141 -0
- package/.agent-src/templates/scripts/work_engine/intent/__init__.py +47 -0
- package/.agent-src/templates/scripts/work_engine/intent/classify.py +280 -0
- package/.agent-src/templates/scripts/work_engine/migration/__init__.py +8 -0
- package/.agent-src/templates/scripts/work_engine/migration/v0_to_v1.py +199 -0
- package/.agent-src/templates/scripts/work_engine/resolvers/__init__.py +22 -0
- package/.agent-src/templates/scripts/work_engine/resolvers/diff.py +106 -0
- package/.agent-src/templates/scripts/work_engine/resolvers/file.py +113 -0
- package/.agent-src/templates/scripts/work_engine/resolvers/prompt.py +90 -0
- package/.agent-src/templates/scripts/work_engine/scoring/__init__.py +14 -0
- package/.agent-src/templates/scripts/work_engine/scoring/confidence.py +300 -0
- package/.agent-src/templates/scripts/work_engine/stack/__init__.py +31 -0
- package/.agent-src/templates/scripts/work_engine/stack/detect.py +187 -0
- package/.agent-src/templates/scripts/work_engine/state.py +641 -0
- package/.claude-plugin/marketplace.json +105 -2
- package/AGENTS.md +36 -8
- package/CHANGELOG.md +534 -0
- package/README.md +125 -4
- package/config/agent-settings.template.yml +45 -0
- package/config/gitignore-block.txt +4 -0
- package/docs/architecture.md +28 -1
- package/docs/development.md +1 -1
- package/docs/getting-started.md +2 -2
- package/docs/installation.md +86 -0
- package/docs/showcase.md +204 -0
- package/package.json +1 -1
- package/scripts/agent-config +199 -0
- package/scripts/audit_cloud_compatibility.py +288 -0
- package/scripts/build_cloud_bundle.py +458 -0
- package/scripts/build_linear_digest.py +263 -0
- package/scripts/chat_history.py +796 -7
- package/scripts/check_compression.py +139 -0
- package/scripts/check_iron_law_prominence.py +143 -0
- package/scripts/check_md_language.py +159 -0
- package/scripts/check_portability.py +36 -0
- package/scripts/check_reply_consistency.py +140 -0
- package/scripts/command_suggester/__init__.py +51 -0
- package/scripts/command_suggester/cooldown.py +132 -0
- package/scripts/command_suggester/loader.py +70 -0
- package/scripts/command_suggester/match.py +180 -0
- package/scripts/command_suggester/rank.py +120 -0
- package/scripts/command_suggester/render.py +86 -0
- package/scripts/command_suggester/sanitize.py +113 -0
- package/scripts/command_suggester/settings.py +125 -0
- package/scripts/command_suggester/types.py +78 -0
- package/scripts/hooks/augment-chat-history.sh +56 -0
- package/scripts/install-hooks.sh +67 -0
- package/scripts/install.py +150 -33
- package/scripts/lint_marketplace.py +27 -0
- package/scripts/migrate_command_suggestions.py +151 -0
- package/scripts/schemas/command.schema.json +41 -0
- package/scripts/skill_linter.py +67 -0
- package/scripts/sync_agent_settings.py +42 -12
- package/templates/consumer-settings/augment-cli-hooks.json +54 -0
- package/templates/consumer-settings/claude-settings.json +55 -1
- package/.agent-src/templates/scripts/implement_ticket/cli.py +0 -171
- package/.agent-src/templates/scripts/implement_ticket/dispatcher.py +0 -134
- package/.agent-src/templates/scripts/implement_ticket/steps/__init__.py +0 -49
- package/.agent-src/templates/scripts/implement_ticket/steps/refine.py +0 -140
- /package/.agent-src/templates/scripts/{implement_ticket → work_engine}/persona_policy.py +0 -0
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"""Sanitize matcher input to prevent self-echo and quoted-code triggering.
|
|
2
|
+
|
|
3
|
+
The suggestion engine scores against the user's raw message and the
|
|
4
|
+
last 2 turns of context. Two adversarial inputs would otherwise
|
|
5
|
+
re-trigger the engine on its own output or on user-pasted code:
|
|
6
|
+
|
|
7
|
+
* **Self-echo** — the previous turn's suggestion block (`> 1. /commit
|
|
8
|
+
— …`) is part of the conversation context. Scoring against it
|
|
9
|
+
re-surfaces the same commands turn after turn.
|
|
10
|
+
* **Quoted code** — user-pasted snippets that mention a command
|
|
11
|
+
(`` `/implement-ticket` ``, fenced ``` ```bash\ngit commit``` ```)
|
|
12
|
+
read like real intent signals to the substring matcher.
|
|
13
|
+
|
|
14
|
+
Both patterns are stripped here before the matcher sees them. The
|
|
15
|
+
sanitiser is **conservative**: only well-formed Markdown fences,
|
|
16
|
+
inline-code spans, and the engine's own suggestion-line shape are
|
|
17
|
+
removed. Plain prose is untouched so legitimate intent ("commit my
|
|
18
|
+
changes please") still scores.
|
|
19
|
+
"""
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import re
|
|
23
|
+
from typing import Iterable
|
|
24
|
+
|
|
25
|
+
# Triple-backtick fence — handles language hints (```bash …```) and
|
|
26
|
+
# unhinted blocks alike. Non-greedy so adjacent fences don't merge.
|
|
27
|
+
_CODE_FENCE_RE = re.compile(r"```.*?```", re.DOTALL)
|
|
28
|
+
# Inline code span. Excludes empty `` `` `` and respects single-line scope.
|
|
29
|
+
_INLINE_CODE_RE = re.compile(r"`[^`\n]+`")
|
|
30
|
+
# Suggestion-block line shape from `render.py`:
|
|
31
|
+
# > 1. /implement-ticket — drive ticket end-to-end…
|
|
32
|
+
# > 2. /refine-ticket — tighten the AC…
|
|
33
|
+
# Numbered-options lines starting with `>` and a `/command` token.
|
|
34
|
+
_SUGGESTION_LINE_RE = re.compile(
|
|
35
|
+
r"^\s*>\s*\d+\.\s*/[A-Za-z][A-Za-z0-9_-]*\b.*$",
|
|
36
|
+
re.MULTILINE,
|
|
37
|
+
)
|
|
38
|
+
# As-is escape hatch line — recognisable suffix from render.py.
|
|
39
|
+
_AS_IS_LINE_RE = re.compile(
|
|
40
|
+
r"^\s*>\s*\d+\.\s*Just run the prompt as-is.*$",
|
|
41
|
+
re.MULTILINE | re.IGNORECASE,
|
|
42
|
+
)
|
|
43
|
+
# Header line emitted by render.py.
|
|
44
|
+
_SUGGESTION_HEADER_RE = re.compile(
|
|
45
|
+
r"^\s*>\s*💡\s*Your request matches a command.*$",
|
|
46
|
+
re.MULTILINE,
|
|
47
|
+
)
|
|
48
|
+
# Recommendation line right after the block.
|
|
49
|
+
_RECOMMENDATION_LINE_RE = re.compile(
|
|
50
|
+
r"^\s*\*\*Recommendation:\s*\d+\b.*$",
|
|
51
|
+
re.MULTILINE,
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def strip_code_blocks(text: str) -> str:
|
|
56
|
+
"""Remove fenced and inline code spans.
|
|
57
|
+
|
|
58
|
+
Fenced blocks first (greedy across newlines, non-greedy across
|
|
59
|
+
fences), then inline backticks. Plain text outside code is left
|
|
60
|
+
bit-identical.
|
|
61
|
+
"""
|
|
62
|
+
if not text:
|
|
63
|
+
return text
|
|
64
|
+
out = _CODE_FENCE_RE.sub(" ", text)
|
|
65
|
+
out = _INLINE_CODE_RE.sub(" ", out)
|
|
66
|
+
return out
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def strip_suggestion_echo(text: str) -> str:
|
|
70
|
+
"""Remove lines that look like the engine's own previous output.
|
|
71
|
+
|
|
72
|
+
Matches the four shapes `render.py` emits:
|
|
73
|
+
|
|
74
|
+
* the `> 💡 …` header
|
|
75
|
+
* `> N. /command — …` numbered options
|
|
76
|
+
* the `> N. Just run the prompt as-is …` escape hatch
|
|
77
|
+
* the `**Recommendation: N — …` follow-up line
|
|
78
|
+
|
|
79
|
+
Anything else (including user-authored quotes that happen to
|
|
80
|
+
mention a command) is preserved — only the engine's distinctive
|
|
81
|
+
block shape is filtered.
|
|
82
|
+
"""
|
|
83
|
+
if not text:
|
|
84
|
+
return text
|
|
85
|
+
out = _SUGGESTION_HEADER_RE.sub("", text)
|
|
86
|
+
out = _SUGGESTION_LINE_RE.sub("", out)
|
|
87
|
+
out = _AS_IS_LINE_RE.sub("", out)
|
|
88
|
+
out = _RECOMMENDATION_LINE_RE.sub("", out)
|
|
89
|
+
return out
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def sanitize_message(message: str) -> str:
|
|
93
|
+
"""Apply both filters to a single user message.
|
|
94
|
+
|
|
95
|
+
Order matters: strip code first (a `/command` inside a fence is
|
|
96
|
+
code, not an echo), then strip echoes from what remains.
|
|
97
|
+
"""
|
|
98
|
+
return strip_suggestion_echo(strip_code_blocks(message))
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def sanitize_context(context_lines: Iterable[str]) -> list[str]:
|
|
102
|
+
"""Apply `sanitize_message` to each line of recent-turn context.
|
|
103
|
+
|
|
104
|
+
Returns a new list — the caller's list is untouched. Empty strings
|
|
105
|
+
after sanitising are kept out of the result so they don't dilute
|
|
106
|
+
token-overlap scoring.
|
|
107
|
+
"""
|
|
108
|
+
out: list[str] = []
|
|
109
|
+
for line in context_lines:
|
|
110
|
+
cleaned = sanitize_message(line)
|
|
111
|
+
if cleaned and cleaned.strip():
|
|
112
|
+
out.append(cleaned)
|
|
113
|
+
return out
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
"""Read `commands.suggestion.*` from `.agent-settings.yml` into `Settings`.
|
|
2
|
+
|
|
3
|
+
Mirror of the chat-history pattern (`scripts/chat_history.py`):
|
|
4
|
+
|
|
5
|
+
* Lazy PyYAML import — the engine works without yaml installed when no
|
|
6
|
+
settings file is present (test fixtures, cloud bundles).
|
|
7
|
+
* Default-permissive: a missing file or missing section returns
|
|
8
|
+
`Settings()` defaults (suggestion layer enabled). Only an explicit
|
|
9
|
+
`enabled: false` flips the master switch off.
|
|
10
|
+
* Malformed YAML / unreadable file → defaults; the suggester degrades
|
|
11
|
+
silently rather than crashing the turn.
|
|
12
|
+
* Type-coerces with bounded fallbacks (floors clamped 0.0-1.0, ints
|
|
13
|
+
non-negative, blocklist forced to a tuple of strings).
|
|
14
|
+
"""
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
from typing import Any, Iterable
|
|
19
|
+
|
|
20
|
+
from .types import Settings
|
|
21
|
+
|
|
22
|
+
DEFAULT_SETTINGS_FILE = ".agent-settings.yml"
|
|
23
|
+
|
|
24
|
+
_DEFAULT = Settings()
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def load_settings(settings_path: Path | str | None = None) -> Settings:
|
|
28
|
+
"""Return a `Settings` instance hydrated from `.agent-settings.yml`.
|
|
29
|
+
|
|
30
|
+
Parameters
|
|
31
|
+
----------
|
|
32
|
+
settings_path:
|
|
33
|
+
Explicit override. ``None`` resolves to ``./.agent-settings.yml``
|
|
34
|
+
relative to the current working directory — same convention as
|
|
35
|
+
``chat_history``.
|
|
36
|
+
"""
|
|
37
|
+
path = Path(settings_path) if settings_path else Path(DEFAULT_SETTINGS_FILE)
|
|
38
|
+
raw = _read_section(path)
|
|
39
|
+
if raw is None:
|
|
40
|
+
return _DEFAULT
|
|
41
|
+
return _settings_from_raw(raw)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _read_section(path: Path) -> dict[str, Any] | None:
|
|
45
|
+
"""Return the ``commands.suggestion`` mapping or ``None`` on any miss."""
|
|
46
|
+
if not path.is_file():
|
|
47
|
+
return None
|
|
48
|
+
try:
|
|
49
|
+
import yaml # type: ignore[import-untyped]
|
|
50
|
+
except ImportError:
|
|
51
|
+
return None
|
|
52
|
+
try:
|
|
53
|
+
with path.open(encoding="utf-8") as fh:
|
|
54
|
+
data = yaml.safe_load(fh) or {}
|
|
55
|
+
except (OSError, yaml.YAMLError):
|
|
56
|
+
return None
|
|
57
|
+
if not isinstance(data, dict):
|
|
58
|
+
return None
|
|
59
|
+
commands = data.get("commands")
|
|
60
|
+
if not isinstance(commands, dict):
|
|
61
|
+
return None
|
|
62
|
+
section = commands.get("suggestion")
|
|
63
|
+
if not isinstance(section, dict):
|
|
64
|
+
return None
|
|
65
|
+
return section
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _settings_from_raw(raw: dict[str, Any]) -> Settings:
|
|
69
|
+
return Settings(
|
|
70
|
+
enabled=_coerce_bool(raw.get("enabled"), _DEFAULT.enabled),
|
|
71
|
+
confidence_floor=_coerce_floor(
|
|
72
|
+
raw.get("confidence_floor"), _DEFAULT.confidence_floor
|
|
73
|
+
),
|
|
74
|
+
cooldown_seconds=_coerce_nonneg_int(
|
|
75
|
+
raw.get("cooldown_seconds"), _DEFAULT.cooldown_seconds
|
|
76
|
+
),
|
|
77
|
+
max_options=_coerce_nonneg_int(
|
|
78
|
+
raw.get("max_options"), _DEFAULT.max_options
|
|
79
|
+
),
|
|
80
|
+
blocklist=_coerce_str_tuple(raw.get("blocklist")),
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _coerce_bool(value: Any, default: bool) -> bool:
|
|
85
|
+
if isinstance(value, bool):
|
|
86
|
+
return value
|
|
87
|
+
if value is None:
|
|
88
|
+
return default
|
|
89
|
+
if isinstance(value, str):
|
|
90
|
+
s = value.strip().lower()
|
|
91
|
+
if s in ("true", "yes", "on", "1"):
|
|
92
|
+
return True
|
|
93
|
+
if s in ("false", "no", "off", "0"):
|
|
94
|
+
return False
|
|
95
|
+
return default
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _coerce_floor(value: Any, default: float) -> float:
|
|
99
|
+
try:
|
|
100
|
+
f = float(value) # type: ignore[arg-type]
|
|
101
|
+
except (TypeError, ValueError):
|
|
102
|
+
return default
|
|
103
|
+
if f < 0.0:
|
|
104
|
+
return 0.0
|
|
105
|
+
if f > 1.0:
|
|
106
|
+
return 1.0
|
|
107
|
+
return f
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _coerce_nonneg_int(value: Any, default: int) -> int:
|
|
111
|
+
try:
|
|
112
|
+
i = int(value) # type: ignore[arg-type]
|
|
113
|
+
except (TypeError, ValueError):
|
|
114
|
+
return default
|
|
115
|
+
return i if i >= 0 else default
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _coerce_str_tuple(value: Any) -> tuple[str, ...]:
|
|
119
|
+
if not isinstance(value, Iterable) or isinstance(value, (str, bytes)):
|
|
120
|
+
return ()
|
|
121
|
+
out: list[str] = []
|
|
122
|
+
for item in value:
|
|
123
|
+
if isinstance(item, str) and item.strip():
|
|
124
|
+
out.append(item.strip())
|
|
125
|
+
return tuple(out)
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"""Type definitions for the command suggestion engine.
|
|
2
|
+
|
|
3
|
+
Plain dataclasses — no third-party deps. Kept in a sibling module so
|
|
4
|
+
match/rank/cooldown/render can import without cycles.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from dataclasses import dataclass, field
|
|
9
|
+
from typing import Optional
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass(frozen=True)
|
|
13
|
+
class CommandSpec:
|
|
14
|
+
"""Loaded command metadata that drives matching.
|
|
15
|
+
|
|
16
|
+
Fields mirror the `suggestion:` frontmatter block plus the
|
|
17
|
+
command's `name` and `description`. Ineligible commands are
|
|
18
|
+
represented with `eligible=False` and are never returned by the
|
|
19
|
+
matcher; the loader keeps them so cross-referencing stays simple.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
name: str
|
|
23
|
+
description: str
|
|
24
|
+
eligible: bool
|
|
25
|
+
trigger_description: str = ""
|
|
26
|
+
trigger_context: str = ""
|
|
27
|
+
rationale: str = ""
|
|
28
|
+
confidence_floor: Optional[float] = None
|
|
29
|
+
cooldown: Optional[str] = None
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass(frozen=True)
|
|
33
|
+
class Match:
|
|
34
|
+
"""A scored candidate. `score` is 0.0–1.0 inclusive.
|
|
35
|
+
|
|
36
|
+
`matched_trigger` is "description" | "context" | "both" and lets
|
|
37
|
+
the renderer surface why a command surfaced. `evidence` is the
|
|
38
|
+
short substring that fired (debugging / golden tests / explain).
|
|
39
|
+
`has_structural_bonus` is True when a heavy-signal pattern (ticket
|
|
40
|
+
key, file path, glob) co-occurred in the message — the ranker
|
|
41
|
+
treats those as specific enough to bypass vague-input suppression.
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
command: str
|
|
45
|
+
score: float
|
|
46
|
+
matched_trigger: str
|
|
47
|
+
evidence: str
|
|
48
|
+
has_structural_bonus: bool = False
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@dataclass(frozen=True)
|
|
52
|
+
class Settings:
|
|
53
|
+
"""Runtime knobs read from `.agent-settings.yml`.
|
|
54
|
+
|
|
55
|
+
Defaults match the "open decisions" leans in the roadmap.
|
|
56
|
+
Per-command frontmatter values override the global floor /
|
|
57
|
+
cooldown.
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
enabled: bool = True
|
|
61
|
+
confidence_floor: float = 0.6
|
|
62
|
+
cooldown_seconds: int = 600 # 10m
|
|
63
|
+
max_options: int = 4
|
|
64
|
+
blocklist: tuple[str, ...] = ()
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@dataclass
|
|
68
|
+
class CooldownState:
|
|
69
|
+
"""Per-conversation cooldown tracker — mutable on purpose."""
|
|
70
|
+
|
|
71
|
+
last_shown: dict[tuple[str, str], float] = field(default_factory=dict)
|
|
72
|
+
"""Key: (command_name, trigger_evidence). Value: unix timestamp."""
|
|
73
|
+
|
|
74
|
+
explicit_invocations: dict[str, float] = field(default_factory=dict)
|
|
75
|
+
"""Commands the user explicitly typed; clears their cooldown."""
|
|
76
|
+
|
|
77
|
+
disabled_for_conversation: bool = False
|
|
78
|
+
"""Set by the `/command-suggestion-off` directive (Phase 5)."""
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Augment Code lifecycle-hook trampoline for chat-history.
|
|
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 event to whichever workspace fired it, so a single install
|
|
8
|
+
# 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 chat-history:hook --platform augment
|
|
17
|
+
# so chat_history.py can run the platform mapping.
|
|
18
|
+
# - Always exit 0 — chat-history must never block the agent loop.
|
|
19
|
+
|
|
20
|
+
set -u
|
|
21
|
+
|
|
22
|
+
EVENT_DATA="$(cat)"
|
|
23
|
+
|
|
24
|
+
# Extract workspace_roots[0] using whichever JSON tool is available.
|
|
25
|
+
WORKSPACE=""
|
|
26
|
+
if command -v jq >/dev/null 2>&1; then
|
|
27
|
+
WORKSPACE="$(printf '%s' "$EVENT_DATA" \
|
|
28
|
+
| jq -r '.workspace_roots[0] // empty' 2>/dev/null)"
|
|
29
|
+
elif command -v python3 >/dev/null 2>&1; then
|
|
30
|
+
WORKSPACE="$(printf '%s' "$EVENT_DATA" | python3 -c '
|
|
31
|
+
import json, sys
|
|
32
|
+
try:
|
|
33
|
+
data = json.load(sys.stdin)
|
|
34
|
+
except Exception:
|
|
35
|
+
sys.exit(0)
|
|
36
|
+
roots = data.get("workspace_roots") or []
|
|
37
|
+
if roots:
|
|
38
|
+
print(roots[0])
|
|
39
|
+
' 2>/dev/null)"
|
|
40
|
+
fi
|
|
41
|
+
|
|
42
|
+
if [ -z "$WORKSPACE" ] || [ ! -d "$WORKSPACE" ]; then
|
|
43
|
+
exit 0
|
|
44
|
+
fi
|
|
45
|
+
|
|
46
|
+
cd "$WORKSPACE" 2>/dev/null || exit 0
|
|
47
|
+
|
|
48
|
+
if [ ! -x ./agent-config ]; then
|
|
49
|
+
exit 0
|
|
50
|
+
fi
|
|
51
|
+
|
|
52
|
+
printf '%s' "$EVENT_DATA" \
|
|
53
|
+
| ./agent-config chat-history:hook --platform augment \
|
|
54
|
+
>/dev/null 2>&1 || true
|
|
55
|
+
|
|
56
|
+
exit 0
|
package/scripts/install-hooks.sh
CHANGED
|
@@ -27,3 +27,70 @@ EOF
|
|
|
27
27
|
|
|
28
28
|
chmod +x "$HOOKS_DIR/pre-push"
|
|
29
29
|
echo "✅ Pre-push hook installed."
|
|
30
|
+
|
|
31
|
+
# Pre-commit: marketplace consistency -----------------------------------------
|
|
32
|
+
#
|
|
33
|
+
# Distribution manifests (.claude-plugin/marketplace.json) drift silently —
|
|
34
|
+
# adding a skill on disk without updating the manifest renders it invisible to
|
|
35
|
+
# Claude Code Plugin Marketplace consumers. CI catches it, but a structural
|
|
36
|
+
# pre-commit gate stops the bad commit from landing in the first place.
|
|
37
|
+
# Runtime is ~40 ms; always-on is cheaper than scoped detection.
|
|
38
|
+
|
|
39
|
+
cat > "$HOOKS_DIR/pre-commit" << 'EOF'
|
|
40
|
+
#!/usr/bin/env bash
|
|
41
|
+
# Pre-commit hook: verify .claude-plugin/marketplace.json lists every skill
|
|
42
|
+
# that exists on disk under .claude/skills/.
|
|
43
|
+
|
|
44
|
+
python3 scripts/lint_marketplace.py
|
|
45
|
+
status=$?
|
|
46
|
+
|
|
47
|
+
if [ $status -ne 0 ]; then
|
|
48
|
+
echo ""
|
|
49
|
+
echo "❌ Commit blocked — .claude-plugin/marketplace.json is out of sync."
|
|
50
|
+
echo " Add the missing skill to the manifest (or remove the stale entry),"
|
|
51
|
+
echo " then re-stage and commit. To bypass for an unrelated WIP commit:"
|
|
52
|
+
echo " git commit --no-verify"
|
|
53
|
+
exit 1
|
|
54
|
+
fi
|
|
55
|
+
EOF
|
|
56
|
+
|
|
57
|
+
chmod +x "$HOOKS_DIR/pre-commit"
|
|
58
|
+
echo "✅ Pre-commit hook installed."
|
|
59
|
+
|
|
60
|
+
# Chat-history bridge hooks ----------------------------------------------------
|
|
61
|
+
#
|
|
62
|
+
# Augment IDE plugin (and any other agent surface without native chat
|
|
63
|
+
# lifecycle hooks) cannot fire SessionStart/Stop/PostToolUse. Git hooks
|
|
64
|
+
# are the platform-agnostic lifecycle surface that fires regardless of
|
|
65
|
+
# IDE — every commit, merge, checkout, and rewrite turns into a phase
|
|
66
|
+
# boundary in .agent-chat-history when an agent session is active.
|
|
67
|
+
#
|
|
68
|
+
# The hooks are silent no-ops when no agent session is active (the
|
|
69
|
+
# chat_history.py hook-append script returns "skipped_no_sidecar" with
|
|
70
|
+
# exit 0) and `|| true` belt-and-suspenders ensures git operations are
|
|
71
|
+
# never blocked.
|
|
72
|
+
|
|
73
|
+
write_chat_history_hook() {
|
|
74
|
+
local name="$1"
|
|
75
|
+
local phase_tag="$2"
|
|
76
|
+
cat > "$HOOKS_DIR/$name" << EOF
|
|
77
|
+
#!/usr/bin/env bash
|
|
78
|
+
# $name: append a phase boundary to .agent-chat-history when an agent
|
|
79
|
+
# session is active. Silent no-op otherwise. Never blocks git.
|
|
80
|
+
|
|
81
|
+
if [ -x ./agent-config ]; then
|
|
82
|
+
ref="\$(git rev-parse --short HEAD 2>/dev/null || echo unknown)"
|
|
83
|
+
payload="{\"phase\":\"$phase_tag\",\"source\":\"git-hook:\$ref\"}"
|
|
84
|
+
./agent-config chat-history:checkpoint --payload "\$payload" \
|
|
85
|
+
>/dev/null 2>&1 || true
|
|
86
|
+
fi
|
|
87
|
+
exit 0
|
|
88
|
+
EOF
|
|
89
|
+
chmod +x "$HOOKS_DIR/$name"
|
|
90
|
+
echo "✅ $name hook installed."
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
write_chat_history_hook "post-commit" "git:post-commit"
|
|
94
|
+
write_chat_history_hook "post-merge" "git:post-merge"
|
|
95
|
+
write_chat_history_hook "post-checkout" "git:post-checkout"
|
|
96
|
+
write_chat_history_hook "post-rewrite" "git:post-rewrite"
|
package/scripts/install.py
CHANGED
|
@@ -251,45 +251,67 @@ def _yaml_scalar(value: str) -> str:
|
|
|
251
251
|
def _replace_template_value(template: str, dotted_path: str, value: str) -> str:
|
|
252
252
|
"""Replace the default value for a dotted-path key in the YAML template.
|
|
253
253
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
254
|
+
Convenience wrapper: formats *value* as a YAML scalar (via
|
|
255
|
+
:func:`_yaml_scalar`) and delegates to :func:`_replace_template_value_raw`.
|
|
256
|
+
"""
|
|
257
|
+
return _replace_template_value_raw(template, dotted_path, _yaml_scalar(value))
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def _replace_template_value_raw(template: str, dotted_path: str, raw_yaml: str) -> str:
|
|
261
|
+
"""Replace the value at *dotted_path* with the pre-formatted *raw_yaml*.
|
|
262
|
+
|
|
263
|
+
Handles arbitrary nesting depth. The template uses 2-space indents;
|
|
264
|
+
parent sections are tracked by indent level so the leaf scalar is
|
|
265
|
+
only replaced when every parent matches the dotted path.
|
|
266
|
+
|
|
267
|
+
Comments and indentation are preserved. Returns *template* unchanged
|
|
268
|
+
if the path cannot be located.
|
|
257
269
|
"""
|
|
258
270
|
parts = dotted_path.split(".")
|
|
259
|
-
if
|
|
260
|
-
|
|
261
|
-
elif len(parts) == 2:
|
|
262
|
-
section, key = parts[0], parts[1]
|
|
263
|
-
else:
|
|
264
|
-
return template # deeper nesting not supported in current schema
|
|
271
|
+
if not parts:
|
|
272
|
+
return template
|
|
265
273
|
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
274
|
+
sections = parts[:-1]
|
|
275
|
+
key = parts[-1]
|
|
276
|
+
target_indent = " " * len(sections)
|
|
277
|
+
|
|
278
|
+
header_re = re.compile(r"^(\s*)([A-Za-z_][A-Za-z0-9_]*):\s*$")
|
|
279
|
+
scalar_re = re.compile(r"^(\s*)([A-Za-z_][A-Za-z0-9_]*):\s*\S.*$")
|
|
280
|
+
|
|
281
|
+
# Stack of section names by depth; None entries mean "not yet seen
|
|
282
|
+
# at this depth" or "left this section". For path a.b.c we need
|
|
283
|
+
# current_path == ['a', 'b'] when scanning for key 'c' at indent 4.
|
|
284
|
+
current_path: list[str | None] = [None] * len(sections)
|
|
271
285
|
|
|
272
|
-
|
|
286
|
+
lines = template.splitlines()
|
|
273
287
|
for idx, line in enumerate(lines):
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
if m_section:
|
|
277
|
-
current_section = m_section.group(1)
|
|
288
|
+
stripped = line.strip()
|
|
289
|
+
if not stripped or stripped.startswith("#"):
|
|
278
290
|
continue
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
291
|
+
|
|
292
|
+
m_header = header_re.match(line)
|
|
293
|
+
if m_header:
|
|
294
|
+
indent = m_header.group(1)
|
|
295
|
+
name = m_header.group(2)
|
|
296
|
+
depth = len(indent) // 2
|
|
297
|
+
if depth < len(sections):
|
|
298
|
+
current_path[depth] = name
|
|
299
|
+
# Reset deeper levels — we just entered a new sub-tree.
|
|
300
|
+
for d in range(depth + 1, len(sections)):
|
|
301
|
+
current_path[d] = None
|
|
302
|
+
continue
|
|
303
|
+
|
|
304
|
+
m_scalar = scalar_re.match(line)
|
|
305
|
+
if not m_scalar:
|
|
306
|
+
continue
|
|
307
|
+
indent = m_scalar.group(1)
|
|
308
|
+
name = m_scalar.group(2)
|
|
309
|
+
if name != key or indent != target_indent:
|
|
310
|
+
continue
|
|
311
|
+
if current_path != list(sections):
|
|
312
|
+
continue
|
|
313
|
+
lines[idx] = f"{indent}{key}: {raw_yaml}"
|
|
314
|
+
return "\n".join(lines) + ("\n" if template.endswith("\n") else "")
|
|
293
315
|
return template
|
|
294
316
|
|
|
295
317
|
|
|
@@ -436,6 +458,92 @@ def ensure_augment_bridge(project_root: Path, force: bool) -> None:
|
|
|
436
458
|
merge_json_file(project_root / ".augment" / "settings.json", bridge, force, ".augment/settings.json")
|
|
437
459
|
|
|
438
460
|
|
|
461
|
+
# Augment lifecycle hooks live at user scope (~/.augment/settings.json) per
|
|
462
|
+
# https://docs.augmentcode.com/cli/hooks — that is the only path read by both
|
|
463
|
+
# the CLI and the IDE plugins (VSCode, IntelliJ). Project-local
|
|
464
|
+
# .augment/settings.json is plugin enablement, not hooks.
|
|
465
|
+
AUGMENT_USER_DIR = Path.home() / ".augment"
|
|
466
|
+
AUGMENT_USER_HOOKS_DIR = AUGMENT_USER_DIR / "hooks"
|
|
467
|
+
AUGMENT_TRAMPOLINE_NAME = "augment-chat-history.sh"
|
|
468
|
+
AUGMENT_HOOK_EVENTS = ("SessionStart", "SessionEnd", "Stop", "PostToolUse")
|
|
469
|
+
|
|
470
|
+
|
|
471
|
+
def ensure_augment_user_hooks(package_root: Path, force: bool) -> None:
|
|
472
|
+
"""Deploy the Augment lifecycle-hook trampoline at user scope.
|
|
473
|
+
|
|
474
|
+
Augment hook scripts must use the .sh extension and be referenced by
|
|
475
|
+
absolute path; user scope is the only surface that fires for both the
|
|
476
|
+
CLI and the IDE plugins. This installs once per developer (not per
|
|
477
|
+
project) — the trampoline reads workspace_roots from the event payload
|
|
478
|
+
and dispatches into whichever project is active at hook-fire time.
|
|
479
|
+
"""
|
|
480
|
+
src = package_root / "scripts" / "hooks" / AUGMENT_TRAMPOLINE_NAME
|
|
481
|
+
if not src.exists():
|
|
482
|
+
skip(f"augment trampoline missing in package: {src}")
|
|
483
|
+
return
|
|
484
|
+
|
|
485
|
+
AUGMENT_USER_HOOKS_DIR.mkdir(parents=True, exist_ok=True)
|
|
486
|
+
dst = AUGMENT_USER_HOOKS_DIR / AUGMENT_TRAMPOLINE_NAME
|
|
487
|
+
|
|
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/{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")
|
|
495
|
+
|
|
496
|
+
hook_entry = {
|
|
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}}
|
|
505
|
+
merge_json_file(
|
|
506
|
+
AUGMENT_USER_DIR / "settings.json",
|
|
507
|
+
settings_patch,
|
|
508
|
+
force,
|
|
509
|
+
"~/.augment/settings.json",
|
|
510
|
+
)
|
|
511
|
+
|
|
512
|
+
|
|
513
|
+
def _chat_history_hook_block(platform: str) -> dict:
|
|
514
|
+
"""Single hook entry that calls ./agent-config chat-history:hook --platform <name>."""
|
|
515
|
+
return {
|
|
516
|
+
"hooks": [
|
|
517
|
+
{
|
|
518
|
+
"type": "command",
|
|
519
|
+
"command": f"./agent-config chat-history:hook --platform {platform}",
|
|
520
|
+
},
|
|
521
|
+
],
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
|
|
525
|
+
def ensure_claude_bridge(project_root: Path, force: bool) -> None:
|
|
526
|
+
"""Deploy .claude/settings.json with plugin enablement and chat-history hooks.
|
|
527
|
+
|
|
528
|
+
Hooks dispatch to scripts/chat_history.py via the project-root ./agent-config
|
|
529
|
+
wrapper. They are no-ops when chat_history.enabled is false in
|
|
530
|
+
.agent-settings.yml. Idempotent: reruns merge cleanly without duplicating
|
|
531
|
+
entries (deep_merge replaces hook arrays rather than appending).
|
|
532
|
+
"""
|
|
533
|
+
claude_hook = _chat_history_hook_block("claude")
|
|
534
|
+
bridge = {
|
|
535
|
+
"enabledPlugins": {"agent-conf@event4u": True},
|
|
536
|
+
"hooks": {
|
|
537
|
+
"SessionStart": [claude_hook],
|
|
538
|
+
"UserPromptSubmit": [claude_hook],
|
|
539
|
+
"PostToolUse": [claude_hook],
|
|
540
|
+
"Stop": [claude_hook],
|
|
541
|
+
"SessionEnd": [claude_hook],
|
|
542
|
+
},
|
|
543
|
+
}
|
|
544
|
+
merge_json_file(project_root / ".claude" / "settings.json", bridge, force, ".claude/settings.json")
|
|
545
|
+
|
|
546
|
+
|
|
439
547
|
def ensure_copilot_bridge(project_root: Path, force: bool) -> None:
|
|
440
548
|
target = project_root / ".github" / "plugin" / "marketplace.json"
|
|
441
549
|
|
|
@@ -474,6 +582,11 @@ def parse_options(argv: list[str]) -> argparse.Namespace:
|
|
|
474
582
|
)
|
|
475
583
|
parser.add_argument("--force", action="store_true", help="overwrite existing files")
|
|
476
584
|
parser.add_argument("--skip-bridges", action="store_true", help="only create .agent-settings.yml")
|
|
585
|
+
parser.add_argument(
|
|
586
|
+
"--augment-user-hooks",
|
|
587
|
+
action="store_true",
|
|
588
|
+
help="also deploy ~/.augment/settings.json + ~/.augment/hooks/ (user-scope, all projects)",
|
|
589
|
+
)
|
|
477
590
|
parser.add_argument("--project", default=None, help="project root (default: cwd or PROJECT_ROOT env)")
|
|
478
591
|
parser.add_argument("--package", default=None, help="package root (default: auto-detect under project)")
|
|
479
592
|
parser.add_argument("--quiet", action="store_true", help="suppress info/success output (warnings/errors still shown)")
|
|
@@ -516,8 +629,12 @@ def main(argv: list[str]) -> int:
|
|
|
516
629
|
if not opts.skip_bridges:
|
|
517
630
|
ensure_vscode_bridge(project_root, package_type, opts.force)
|
|
518
631
|
ensure_augment_bridge(project_root, opts.force)
|
|
632
|
+
ensure_claude_bridge(project_root, opts.force)
|
|
519
633
|
ensure_copilot_bridge(project_root, opts.force)
|
|
520
634
|
|
|
635
|
+
if opts.augment_user_hooks:
|
|
636
|
+
ensure_augment_user_hooks(package_root, opts.force)
|
|
637
|
+
|
|
521
638
|
if not QUIET:
|
|
522
639
|
print()
|
|
523
640
|
success("Done.")
|