@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.
Files changed (252) hide show
  1. package/.agent-src/commands/agent-handoff.md +3 -0
  2. package/.agent-src/commands/agent-status.md +3 -0
  3. package/.agent-src/commands/agents-audit.md +4 -0
  4. package/.agent-src/commands/agents-cleanup.md +6 -1
  5. package/.agent-src/commands/agents-prepare.md +3 -0
  6. package/.agent-src/commands/analyze-reference-repo.md +4 -0
  7. package/.agent-src/commands/bug-fix.md +5 -1
  8. package/.agent-src/commands/bug-investigate.md +4 -0
  9. package/.agent-src/commands/chat-history-checkpoint.md +126 -0
  10. package/.agent-src/commands/chat-history-clear.md +5 -0
  11. package/.agent-src/commands/chat-history-resume.md +5 -0
  12. package/.agent-src/commands/chat-history.md +5 -0
  13. package/.agent-src/commands/check-current-md.md +126 -0
  14. package/.agent-src/commands/commit-in-chunks.md +98 -0
  15. package/.agent-src/commands/commit.md +4 -0
  16. package/.agent-src/commands/compress.md +3 -0
  17. package/.agent-src/commands/context-create.md +4 -0
  18. package/.agent-src/commands/context-refactor.md +4 -0
  19. package/.agent-src/commands/copilot-agents-init.md +3 -0
  20. package/.agent-src/commands/copilot-agents-optimize.md +3 -0
  21. package/.agent-src/commands/create-pr-description.md +4 -0
  22. package/.agent-src/commands/create-pr.md +4 -0
  23. package/.agent-src/commands/do-and-judge.md +4 -1
  24. package/.agent-src/commands/do-in-steps.md +3 -0
  25. package/.agent-src/commands/e2e-heal.md +4 -0
  26. package/.agent-src/commands/e2e-plan.md +4 -0
  27. package/.agent-src/commands/estimate-ticket.md +4 -1
  28. package/.agent-src/commands/feature-dev.md +4 -0
  29. package/.agent-src/commands/feature-explore.md +4 -0
  30. package/.agent-src/commands/feature-plan.md +4 -0
  31. package/.agent-src/commands/feature-refactor.md +4 -0
  32. package/.agent-src/commands/feature-roadmap.md +6 -0
  33. package/.agent-src/commands/fix-ci.md +4 -0
  34. package/.agent-src/commands/fix-portability.md +3 -0
  35. package/.agent-src/commands/fix-pr-bot-comments.md +4 -0
  36. package/.agent-src/commands/fix-pr-comments.md +4 -0
  37. package/.agent-src/commands/fix-pr-developer-comments.md +4 -0
  38. package/.agent-src/commands/fix-references.md +3 -0
  39. package/.agent-src/commands/fix-seeder.md +4 -0
  40. package/.agent-src/commands/implement-ticket.md +39 -13
  41. package/.agent-src/commands/jira-ticket.md +4 -0
  42. package/.agent-src/commands/judge.md +3 -0
  43. package/.agent-src/commands/memory-add.md +5 -3
  44. package/.agent-src/commands/memory-full.md +5 -2
  45. package/.agent-src/commands/memory-promote.md +7 -6
  46. package/.agent-src/commands/mode.md +3 -0
  47. package/.agent-src/commands/module-create.md +4 -0
  48. package/.agent-src/commands/module-explore.md +4 -0
  49. package/.agent-src/commands/onboard.md +24 -0
  50. package/.agent-src/commands/optimize-agents.md +4 -0
  51. package/.agent-src/commands/optimize-augmentignore.md +3 -0
  52. package/.agent-src/commands/optimize-rtk-filters.md +3 -0
  53. package/.agent-src/commands/optimize-skills.md +4 -0
  54. package/.agent-src/commands/override-create.md +4 -0
  55. package/.agent-src/commands/override-manage.md +4 -0
  56. package/.agent-src/commands/package-reset.md +3 -0
  57. package/.agent-src/commands/package-test.md +3 -0
  58. package/.agent-src/commands/prepare-for-review.md +4 -0
  59. package/.agent-src/commands/project-analyze.md +4 -0
  60. package/.agent-src/commands/project-health.md +4 -0
  61. package/.agent-src/commands/propose-memory.md +6 -8
  62. package/.agent-src/commands/quality-fix.md +4 -0
  63. package/.agent-src/commands/refine-ticket.md +4 -1
  64. package/.agent-src/commands/review-changes.md +4 -0
  65. package/.agent-src/commands/review-routing.md +4 -0
  66. package/.agent-src/commands/roadmap-create.md +7 -0
  67. package/.agent-src/commands/roadmap-execute.md +12 -1
  68. package/.agent-src/commands/rule-compliance-audit.md +4 -0
  69. package/.agent-src/commands/set-cost-profile.md +3 -0
  70. package/.agent-src/commands/sync-agent-settings.md +3 -0
  71. package/.agent-src/commands/sync-gitignore.md +3 -0
  72. package/.agent-src/commands/tests-create.md +4 -0
  73. package/.agent-src/commands/tests-execute.md +4 -0
  74. package/.agent-src/commands/threat-model.md +4 -0
  75. package/.agent-src/commands/update-form-request-messages.md +4 -0
  76. package/.agent-src/commands/upstream-contribute.md +4 -0
  77. package/.agent-src/commands/work.md +161 -0
  78. package/.agent-src/guidelines/agent-infra/engineering-memory-data-format.md +2 -6
  79. package/.agent-src/guidelines/agent-infra/layered-settings.md +0 -1
  80. package/.agent-src/guidelines/agent-infra/memory-access.md +0 -7
  81. package/.agent-src/guidelines/agent-infra/role-contracts.md +2 -4
  82. package/.agent-src/guidelines/agent-infra/self-improvement-pipeline.md +0 -1
  83. package/.agent-src/guidelines/php/patterns/strategy.md +180 -2
  84. package/.agent-src/personas/README.md +0 -1
  85. package/.agent-src/rules/artifact-drafting-protocol.md +7 -2
  86. package/.agent-src/rules/artifact-engagement-recording.md +133 -0
  87. package/.agent-src/rules/ask-when-uncertain.md +18 -13
  88. package/.agent-src/rules/augment-portability.md +8 -0
  89. package/.agent-src/rules/autonomous-execution.md +158 -0
  90. package/.agent-src/rules/chat-history.md +147 -118
  91. package/.agent-src/rules/cli-output-handling.md +26 -3
  92. package/.agent-src/rules/command-suggestion.md +133 -0
  93. package/.agent-src/rules/commit-policy.md +99 -0
  94. package/.agent-src/rules/direct-answers.md +114 -0
  95. package/.agent-src/rules/docs-sync.md +36 -0
  96. package/.agent-src/rules/downstream-changes.md +10 -9
  97. package/.agent-src/rules/improve-before-implement.md +9 -6
  98. package/.agent-src/rules/language-and-tone.md +81 -6
  99. package/.agent-src/rules/non-destructive-by-default.md +117 -0
  100. package/.agent-src/rules/package-ci-checks.md +4 -0
  101. package/.agent-src/rules/preservation-guard.md +20 -0
  102. package/.agent-src/rules/roadmap-progress-sync.md +103 -30
  103. package/.agent-src/rules/scope-control.md +42 -1
  104. package/.agent-src/rules/size-enforcement.md +1 -3
  105. package/.agent-src/rules/skill-quality.md +3 -8
  106. package/.agent-src/rules/ui-audit-before-build.md +106 -0
  107. package/.agent-src/rules/user-interaction.md +82 -50
  108. package/.agent-src/scripts/update_roadmap_progress.py +17 -5
  109. package/.agent-src/skills/blade-ui/SKILL.md +30 -5
  110. package/.agent-src/skills/command-routing/SKILL.md +32 -0
  111. package/.agent-src/skills/command-writing/SKILL.md +41 -2
  112. package/.agent-src/skills/description-assist/SKILL.md +21 -0
  113. package/.agent-src/skills/estimate-ticket/SKILL.md +0 -1
  114. package/.agent-src/skills/existing-ui-audit/SKILL.md +187 -0
  115. package/.agent-src/skills/fe-design/SKILL.md +72 -60
  116. package/.agent-src/skills/finishing-a-development-branch/SKILL.md +4 -0
  117. package/.agent-src/skills/flux/SKILL.md +31 -4
  118. package/.agent-src/skills/guideline-writing/SKILL.md +24 -2
  119. package/.agent-src/skills/learning-to-rule-or-skill/SKILL.md +51 -9
  120. package/.agent-src/skills/livewire/SKILL.md +30 -4
  121. package/.agent-src/skills/md-language-check/SKILL.md +103 -0
  122. package/.agent-src/skills/php-coder/SKILL.md +24 -0
  123. package/.agent-src/skills/react-shadcn-ui/SKILL.md +121 -0
  124. package/.agent-src/skills/refine-prompt/SKILL.md +220 -0
  125. package/.agent-src/skills/refine-ticket/SKILL.md +2 -4
  126. package/.agent-src/skills/roadmap-management/SKILL.md +10 -3
  127. package/.agent-src/skills/rule-writing/SKILL.md +23 -1
  128. package/.agent-src/skills/skill-writing/SKILL.md +1 -3
  129. package/.agent-src/skills/upstream-contribute/SKILL.md +1 -1
  130. package/.agent-src/skills/using-git-worktrees/SKILL.md +3 -1
  131. package/.agent-src/templates/AGENTS.md +24 -6
  132. package/.agent-src/templates/agent-settings.md +149 -0
  133. package/.agent-src/templates/roadmaps.md +8 -2
  134. package/.agent-src/templates/scripts/implement_ticket/__init__.py +63 -26
  135. package/.agent-src/templates/scripts/implement_ticket/__main__.py +8 -2
  136. package/.agent-src/templates/scripts/telemetry/__init__.py +42 -0
  137. package/.agent-src/templates/scripts/telemetry/aggregator.py +154 -0
  138. package/.agent-src/templates/scripts/telemetry/boundary.py +171 -0
  139. package/.agent-src/templates/scripts/telemetry/engagement.py +238 -0
  140. package/.agent-src/templates/scripts/telemetry/report_renderer.py +170 -0
  141. package/.agent-src/templates/scripts/telemetry/settings.py +112 -0
  142. package/.agent-src/templates/scripts/telemetry_record.py +166 -0
  143. package/.agent-src/templates/scripts/telemetry_report.py +161 -0
  144. package/.agent-src/templates/scripts/telemetry_status.py +142 -0
  145. package/.agent-src/templates/scripts/work_engine/__init__.py +58 -0
  146. package/.agent-src/templates/scripts/work_engine/__main__.py +9 -0
  147. package/.agent-src/templates/scripts/work_engine/cli.py +592 -0
  148. package/.agent-src/templates/scripts/{implement_ticket → work_engine}/delivery_state.py +7 -0
  149. package/.agent-src/templates/scripts/work_engine/directives/__init__.py +33 -0
  150. package/.agent-src/templates/scripts/work_engine/directives/backend/__init__.py +98 -0
  151. package/.agent-src/templates/scripts/{implement_ticket/steps → work_engine/directives/backend}/analyze.py +1 -1
  152. package/.agent-src/templates/scripts/{implement_ticket/steps → work_engine/directives/backend}/implement.py +2 -2
  153. package/.agent-src/templates/scripts/{implement_ticket/steps → work_engine/directives/backend}/memory.py +1 -1
  154. package/.agent-src/templates/scripts/{implement_ticket/steps → work_engine/directives/backend}/plan.py +1 -1
  155. package/.agent-src/templates/scripts/work_engine/directives/backend/refine.py +396 -0
  156. package/.agent-src/templates/scripts/{implement_ticket/steps → work_engine/directives/backend}/report.py +36 -4
  157. package/.agent-src/templates/scripts/{implement_ticket/steps → work_engine/directives/backend}/test.py +2 -2
  158. package/.agent-src/templates/scripts/{implement_ticket/steps → work_engine/directives/backend}/verify.py +2 -2
  159. package/.agent-src/templates/scripts/work_engine/directives/mixed/__init__.py +116 -0
  160. package/.agent-src/templates/scripts/work_engine/directives/mixed/contract.py +254 -0
  161. package/.agent-src/templates/scripts/work_engine/directives/mixed/stitch.py +229 -0
  162. package/.agent-src/templates/scripts/work_engine/directives/mixed/ui.py +231 -0
  163. package/.agent-src/templates/scripts/work_engine/directives/ui/__init__.py +113 -0
  164. package/.agent-src/templates/scripts/work_engine/directives/ui/_passthrough.py +44 -0
  165. package/.agent-src/templates/scripts/work_engine/directives/ui/apply.py +241 -0
  166. package/.agent-src/templates/scripts/work_engine/directives/ui/audit.py +414 -0
  167. package/.agent-src/templates/scripts/work_engine/directives/ui/design.py +335 -0
  168. package/.agent-src/templates/scripts/work_engine/directives/ui/polish.py +510 -0
  169. package/.agent-src/templates/scripts/work_engine/directives/ui/review.py +468 -0
  170. package/.agent-src/templates/scripts/work_engine/directives/ui_trivial/__init__.py +119 -0
  171. package/.agent-src/templates/scripts/work_engine/directives/ui_trivial/_skipped.py +37 -0
  172. package/.agent-src/templates/scripts/work_engine/directives/ui_trivial/apply.py +165 -0
  173. package/.agent-src/templates/scripts/work_engine/directives/ui_trivial/refine.py +66 -0
  174. package/.agent-src/templates/scripts/work_engine/directives/ui_trivial/report.py +62 -0
  175. package/.agent-src/templates/scripts/work_engine/directives/ui_trivial/test.py +115 -0
  176. package/.agent-src/templates/scripts/work_engine/dispatcher.py +331 -0
  177. package/.agent-src/templates/scripts/work_engine/hooks/__init__.py +54 -0
  178. package/.agent-src/templates/scripts/work_engine/hooks/builtin/__init__.py +32 -0
  179. package/.agent-src/templates/scripts/work_engine/hooks/builtin/_chat_history_base.py +103 -0
  180. package/.agent-src/templates/scripts/work_engine/hooks/builtin/chat_history_append.py +44 -0
  181. package/.agent-src/templates/scripts/work_engine/hooks/builtin/chat_history_halt_append.py +42 -0
  182. package/.agent-src/templates/scripts/work_engine/hooks/builtin/chat_history_heartbeat.py +50 -0
  183. package/.agent-src/templates/scripts/work_engine/hooks/builtin/chat_history_turn_check.py +49 -0
  184. package/.agent-src/templates/scripts/work_engine/hooks/builtin/directive_set_guard.py +53 -0
  185. package/.agent-src/templates/scripts/work_engine/hooks/builtin/halt_surface_audit.py +50 -0
  186. package/.agent-src/templates/scripts/work_engine/hooks/builtin/state_shape_validation.py +52 -0
  187. package/.agent-src/templates/scripts/work_engine/hooks/builtin/trace.py +84 -0
  188. package/.agent-src/templates/scripts/work_engine/hooks/context.py +66 -0
  189. package/.agent-src/templates/scripts/work_engine/hooks/events.py +44 -0
  190. package/.agent-src/templates/scripts/work_engine/hooks/exceptions.py +79 -0
  191. package/.agent-src/templates/scripts/work_engine/hooks/registry.py +60 -0
  192. package/.agent-src/templates/scripts/work_engine/hooks/runner.py +73 -0
  193. package/.agent-src/templates/scripts/work_engine/hooks/settings.py +141 -0
  194. package/.agent-src/templates/scripts/work_engine/intent/__init__.py +47 -0
  195. package/.agent-src/templates/scripts/work_engine/intent/classify.py +280 -0
  196. package/.agent-src/templates/scripts/work_engine/migration/__init__.py +8 -0
  197. package/.agent-src/templates/scripts/work_engine/migration/v0_to_v1.py +199 -0
  198. package/.agent-src/templates/scripts/work_engine/resolvers/__init__.py +22 -0
  199. package/.agent-src/templates/scripts/work_engine/resolvers/diff.py +106 -0
  200. package/.agent-src/templates/scripts/work_engine/resolvers/file.py +113 -0
  201. package/.agent-src/templates/scripts/work_engine/resolvers/prompt.py +90 -0
  202. package/.agent-src/templates/scripts/work_engine/scoring/__init__.py +14 -0
  203. package/.agent-src/templates/scripts/work_engine/scoring/confidence.py +300 -0
  204. package/.agent-src/templates/scripts/work_engine/stack/__init__.py +31 -0
  205. package/.agent-src/templates/scripts/work_engine/stack/detect.py +187 -0
  206. package/.agent-src/templates/scripts/work_engine/state.py +641 -0
  207. package/.claude-plugin/marketplace.json +105 -2
  208. package/AGENTS.md +36 -8
  209. package/CHANGELOG.md +534 -0
  210. package/README.md +125 -4
  211. package/config/agent-settings.template.yml +45 -0
  212. package/config/gitignore-block.txt +4 -0
  213. package/docs/architecture.md +28 -1
  214. package/docs/development.md +1 -1
  215. package/docs/getting-started.md +2 -2
  216. package/docs/installation.md +86 -0
  217. package/docs/showcase.md +204 -0
  218. package/package.json +1 -1
  219. package/scripts/agent-config +199 -0
  220. package/scripts/audit_cloud_compatibility.py +288 -0
  221. package/scripts/build_cloud_bundle.py +458 -0
  222. package/scripts/build_linear_digest.py +263 -0
  223. package/scripts/chat_history.py +796 -7
  224. package/scripts/check_compression.py +139 -0
  225. package/scripts/check_iron_law_prominence.py +143 -0
  226. package/scripts/check_md_language.py +159 -0
  227. package/scripts/check_portability.py +36 -0
  228. package/scripts/check_reply_consistency.py +140 -0
  229. package/scripts/command_suggester/__init__.py +51 -0
  230. package/scripts/command_suggester/cooldown.py +132 -0
  231. package/scripts/command_suggester/loader.py +70 -0
  232. package/scripts/command_suggester/match.py +180 -0
  233. package/scripts/command_suggester/rank.py +120 -0
  234. package/scripts/command_suggester/render.py +86 -0
  235. package/scripts/command_suggester/sanitize.py +113 -0
  236. package/scripts/command_suggester/settings.py +125 -0
  237. package/scripts/command_suggester/types.py +78 -0
  238. package/scripts/hooks/augment-chat-history.sh +56 -0
  239. package/scripts/install-hooks.sh +67 -0
  240. package/scripts/install.py +150 -33
  241. package/scripts/lint_marketplace.py +27 -0
  242. package/scripts/migrate_command_suggestions.py +151 -0
  243. package/scripts/schemas/command.schema.json +41 -0
  244. package/scripts/skill_linter.py +67 -0
  245. package/scripts/sync_agent_settings.py +42 -12
  246. package/templates/consumer-settings/augment-cli-hooks.json +54 -0
  247. package/templates/consumer-settings/claude-settings.json +55 -1
  248. package/.agent-src/templates/scripts/implement_ticket/cli.py +0 -171
  249. package/.agent-src/templates/scripts/implement_ticket/dispatcher.py +0 -134
  250. package/.agent-src/templates/scripts/implement_ticket/steps/__init__.py +0 -49
  251. package/.agent-src/templates/scripts/implement_ticket/steps/refine.py +0 -140
  252. /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
@@ -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"
@@ -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
- Strategy: walk the template lines, track the current top-level
255
- section, and replace the first matching line. Comments and indentation
256
- are preserved.
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 len(parts) == 1:
260
- section, key = None, parts[0]
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
- lines = template.splitlines()
267
- current_section: "str | None" = None
268
- section_re = re.compile(r"^([A-Za-z_][A-Za-z0-9_]*):\s*$")
269
- scalar_top_re = re.compile(r"^([A-Za-z_][A-Za-z0-9_]*):\s*.*$")
270
- scalar_sub_re = re.compile(r"^(\s+)([A-Za-z_][A-Za-z0-9_]*):\s*.*$")
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
- replacement = _yaml_scalar(value)
286
+ lines = template.splitlines()
273
287
  for idx, line in enumerate(lines):
274
- # Top-level section header
275
- m_section = section_re.match(line)
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
- if section is None:
280
- # Top-level scalar target
281
- m_top = scalar_top_re.match(line)
282
- if m_top and m_top.group(1) == key and not line.startswith((" ", "\t")):
283
- lines[idx] = f"{key}: {replacement}"
284
- return "\n".join(lines) + ("\n" if template.endswith("\n") else "")
285
- else:
286
- if current_section != section:
287
- continue
288
- m_sub = scalar_sub_re.match(line)
289
- if m_sub and m_sub.group(2) == key:
290
- indent = m_sub.group(1)
291
- lines[idx] = f"{indent}{key}: {replacement}"
292
- return "\n".join(lines) + ("\n" if template.endswith("\n") else "")
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.")