@event4u/agent-config 1.13.0 → 1.15.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 (291) hide show
  1. package/.agent-src/commands/agent-handoff.md +4 -1
  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 +7 -3
  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 +6 -1
  11. package/.agent-src/commands/chat-history-resume.md +7 -2
  12. package/.agent-src/commands/chat-history.md +7 -2
  13. package/.agent-src/commands/check-current-md.md +137 -0
  14. package/.agent-src/commands/commit-in-chunks.md +118 -0
  15. package/.agent-src/commands/commit.md +4 -0
  16. package/.agent-src/commands/compress.md +37 -2
  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 +5 -2
  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 +33 -0
  50. package/.agent-src/commands/optimize-agents.md +4 -0
  51. package/.agent-src/commands/optimize-augmentignore.md +12 -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 +12 -7
  64. package/.agent-src/commands/review-changes.md +39 -8
  65. package/.agent-src/commands/review-routing.md +4 -0
  66. package/.agent-src/commands/roadmap-create.md +18 -0
  67. package/.agent-src/commands/roadmap-execute.md +14 -1
  68. package/.agent-src/commands/rule-compliance-audit.md +4 -0
  69. package/.agent-src/commands/set-cost-profile.md +11 -0
  70. package/.agent-src/commands/sync-agent-settings.md +12 -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 +6 -3
  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 +64 -37
  89. package/.agent-src/rules/autonomous-execution.md +158 -0
  90. package/.agent-src/rules/chat-history-cadence.md +109 -0
  91. package/.agent-src/rules/chat-history-ownership.md +123 -0
  92. package/.agent-src/rules/chat-history-visibility.md +96 -0
  93. package/.agent-src/rules/cli-output-handling.md +27 -4
  94. package/.agent-src/rules/command-suggestion.md +134 -0
  95. package/.agent-src/rules/commit-policy.md +109 -0
  96. package/.agent-src/rules/direct-answers.md +114 -0
  97. package/.agent-src/rules/docs-sync.md +36 -0
  98. package/.agent-src/rules/downstream-changes.md +10 -9
  99. package/.agent-src/rules/improve-before-implement.md +9 -6
  100. package/.agent-src/rules/language-and-tone.md +85 -6
  101. package/.agent-src/rules/non-destructive-by-default.md +117 -0
  102. package/.agent-src/rules/package-ci-checks.md +4 -0
  103. package/.agent-src/rules/preservation-guard.md +20 -0
  104. package/.agent-src/rules/roadmap-progress-sync.md +159 -27
  105. package/.agent-src/rules/role-mode-adherence.md +1 -1
  106. package/.agent-src/rules/scope-control.md +42 -1
  107. package/.agent-src/rules/size-enforcement.md +2 -3
  108. package/.agent-src/rules/skill-quality.md +3 -8
  109. package/.agent-src/rules/ui-audit-before-build.md +106 -0
  110. package/.agent-src/rules/user-interaction.md +107 -51
  111. package/.agent-src/scripts/update_roadmap_progress.py +73 -9
  112. package/.agent-src/skills/blade-ui/SKILL.md +47 -3
  113. package/.agent-src/skills/command-routing/SKILL.md +32 -0
  114. package/.agent-src/skills/command-writing/SKILL.md +52 -2
  115. package/.agent-src/skills/description-assist/SKILL.md +21 -0
  116. package/.agent-src/skills/estimate-ticket/SKILL.md +0 -1
  117. package/.agent-src/skills/existing-ui-audit/SKILL.md +202 -0
  118. package/.agent-src/skills/fe-design/SKILL.md +78 -61
  119. package/.agent-src/skills/file-editor/SKILL.md +9 -0
  120. package/.agent-src/skills/finishing-a-development-branch/SKILL.md +4 -0
  121. package/.agent-src/skills/flux/SKILL.md +31 -4
  122. package/.agent-src/skills/guideline-writing/SKILL.md +24 -2
  123. package/.agent-src/skills/learning-to-rule-or-skill/SKILL.md +51 -9
  124. package/.agent-src/skills/livewire/SKILL.md +49 -4
  125. package/.agent-src/skills/md-language-check/SKILL.md +103 -0
  126. package/.agent-src/skills/php-coder/SKILL.md +24 -0
  127. package/.agent-src/skills/react-shadcn-ui/SKILL.md +121 -0
  128. package/.agent-src/skills/refine-prompt/SKILL.md +220 -0
  129. package/.agent-src/skills/refine-ticket/SKILL.md +32 -28
  130. package/.agent-src/skills/roadmap-management/SKILL.md +24 -11
  131. package/.agent-src/skills/rule-writing/SKILL.md +23 -1
  132. package/.agent-src/skills/skill-writing/SKILL.md +3 -5
  133. package/.agent-src/skills/upstream-contribute/SKILL.md +3 -3
  134. package/.agent-src/skills/using-git-worktrees/SKILL.md +3 -1
  135. package/.agent-src/templates/AGENTS.md +24 -6
  136. package/.agent-src/templates/agent-settings.md +149 -0
  137. package/.agent-src/templates/roadmaps.md +11 -4
  138. package/.agent-src/templates/scripts/implement_ticket/__init__.py +63 -26
  139. package/.agent-src/templates/scripts/implement_ticket/__main__.py +8 -2
  140. package/.agent-src/templates/scripts/memory_lookup.py +1 -1
  141. package/.agent-src/templates/scripts/telemetry/__init__.py +42 -0
  142. package/.agent-src/templates/scripts/telemetry/aggregator.py +154 -0
  143. package/.agent-src/templates/scripts/telemetry/boundary.py +171 -0
  144. package/.agent-src/templates/scripts/telemetry/engagement.py +238 -0
  145. package/.agent-src/templates/scripts/telemetry/report_renderer.py +170 -0
  146. package/.agent-src/templates/scripts/telemetry/settings.py +112 -0
  147. package/.agent-src/templates/scripts/telemetry_record.py +166 -0
  148. package/.agent-src/templates/scripts/telemetry_report.py +161 -0
  149. package/.agent-src/templates/scripts/telemetry_status.py +142 -0
  150. package/.agent-src/templates/scripts/work_engine/__init__.py +58 -0
  151. package/.agent-src/templates/scripts/work_engine/__main__.py +9 -0
  152. package/.agent-src/templates/scripts/work_engine/cli.py +195 -0
  153. package/.agent-src/templates/scripts/work_engine/cli_args.py +116 -0
  154. package/.agent-src/templates/scripts/{implement_ticket → work_engine}/delivery_state.py +10 -3
  155. package/.agent-src/templates/scripts/work_engine/directives/__init__.py +33 -0
  156. package/.agent-src/templates/scripts/work_engine/directives/backend/__init__.py +98 -0
  157. package/.agent-src/templates/scripts/{implement_ticket/steps → work_engine/directives/backend}/analyze.py +1 -1
  158. package/.agent-src/templates/scripts/{implement_ticket/steps → work_engine/directives/backend}/implement.py +3 -3
  159. package/.agent-src/templates/scripts/{implement_ticket/steps → work_engine/directives/backend}/memory.py +2 -2
  160. package/.agent-src/templates/scripts/{implement_ticket/steps → work_engine/directives/backend}/plan.py +2 -2
  161. package/.agent-src/templates/scripts/work_engine/directives/backend/refine.py +396 -0
  162. package/.agent-src/templates/scripts/{implement_ticket/steps → work_engine/directives/backend}/report.py +37 -5
  163. package/.agent-src/templates/scripts/{implement_ticket/steps → work_engine/directives/backend}/test.py +2 -2
  164. package/.agent-src/templates/scripts/{implement_ticket/steps → work_engine/directives/backend}/verify.py +2 -2
  165. package/.agent-src/templates/scripts/work_engine/directives/mixed/__init__.py +116 -0
  166. package/.agent-src/templates/scripts/work_engine/directives/mixed/contract.py +254 -0
  167. package/.agent-src/templates/scripts/work_engine/directives/mixed/stitch.py +229 -0
  168. package/.agent-src/templates/scripts/work_engine/directives/mixed/ui.py +231 -0
  169. package/.agent-src/templates/scripts/work_engine/directives/ui/__init__.py +113 -0
  170. package/.agent-src/templates/scripts/work_engine/directives/ui/_passthrough.py +44 -0
  171. package/.agent-src/templates/scripts/work_engine/directives/ui/apply.py +241 -0
  172. package/.agent-src/templates/scripts/work_engine/directives/ui/audit.py +414 -0
  173. package/.agent-src/templates/scripts/work_engine/directives/ui/design.py +335 -0
  174. package/.agent-src/templates/scripts/work_engine/directives/ui/polish.py +510 -0
  175. package/.agent-src/templates/scripts/work_engine/directives/ui/review.py +468 -0
  176. package/.agent-src/templates/scripts/work_engine/directives/ui_trivial/__init__.py +119 -0
  177. package/.agent-src/templates/scripts/work_engine/directives/ui_trivial/_skipped.py +37 -0
  178. package/.agent-src/templates/scripts/work_engine/directives/ui_trivial/apply.py +165 -0
  179. package/.agent-src/templates/scripts/work_engine/directives/ui_trivial/refine.py +66 -0
  180. package/.agent-src/templates/scripts/work_engine/directives/ui_trivial/report.py +62 -0
  181. package/.agent-src/templates/scripts/work_engine/directives/ui_trivial/test.py +115 -0
  182. package/.agent-src/templates/scripts/work_engine/dispatcher.py +331 -0
  183. package/.agent-src/templates/scripts/work_engine/emitters.py +43 -0
  184. package/.agent-src/templates/scripts/work_engine/errors.py +19 -0
  185. package/.agent-src/templates/scripts/work_engine/hook_bootstrap.py +76 -0
  186. package/.agent-src/templates/scripts/work_engine/hooks/__init__.py +54 -0
  187. package/.agent-src/templates/scripts/work_engine/hooks/builtin/__init__.py +32 -0
  188. package/.agent-src/templates/scripts/work_engine/hooks/builtin/_chat_history_base.py +103 -0
  189. package/.agent-src/templates/scripts/work_engine/hooks/builtin/chat_history_append.py +44 -0
  190. package/.agent-src/templates/scripts/work_engine/hooks/builtin/chat_history_halt_append.py +42 -0
  191. package/.agent-src/templates/scripts/work_engine/hooks/builtin/chat_history_heartbeat.py +50 -0
  192. package/.agent-src/templates/scripts/work_engine/hooks/builtin/chat_history_turn_check.py +49 -0
  193. package/.agent-src/templates/scripts/work_engine/hooks/builtin/directive_set_guard.py +53 -0
  194. package/.agent-src/templates/scripts/work_engine/hooks/builtin/halt_surface_audit.py +50 -0
  195. package/.agent-src/templates/scripts/work_engine/hooks/builtin/state_shape_validation.py +52 -0
  196. package/.agent-src/templates/scripts/work_engine/hooks/builtin/trace.py +84 -0
  197. package/.agent-src/templates/scripts/work_engine/hooks/context.py +66 -0
  198. package/.agent-src/templates/scripts/work_engine/hooks/events.py +44 -0
  199. package/.agent-src/templates/scripts/work_engine/hooks/exceptions.py +79 -0
  200. package/.agent-src/templates/scripts/work_engine/hooks/registry.py +60 -0
  201. package/.agent-src/templates/scripts/work_engine/hooks/runner.py +73 -0
  202. package/.agent-src/templates/scripts/work_engine/hooks/settings.py +141 -0
  203. package/.agent-src/templates/scripts/work_engine/input_builders.py +163 -0
  204. package/.agent-src/templates/scripts/work_engine/intent/__init__.py +47 -0
  205. package/.agent-src/templates/scripts/work_engine/intent/classify.py +280 -0
  206. package/.agent-src/templates/scripts/work_engine/migration/__init__.py +8 -0
  207. package/.agent-src/templates/scripts/work_engine/migration/v0_to_v1.py +231 -0
  208. package/.agent-src/templates/scripts/{implement_ticket → work_engine}/persona_policy.py +1 -1
  209. package/.agent-src/templates/scripts/work_engine/resolvers/__init__.py +22 -0
  210. package/.agent-src/templates/scripts/work_engine/resolvers/diff.py +106 -0
  211. package/.agent-src/templates/scripts/work_engine/resolvers/file.py +113 -0
  212. package/.agent-src/templates/scripts/work_engine/resolvers/prompt.py +90 -0
  213. package/.agent-src/templates/scripts/work_engine/scoring/__init__.py +14 -0
  214. package/.agent-src/templates/scripts/work_engine/scoring/confidence.py +300 -0
  215. package/.agent-src/templates/scripts/work_engine/stack/__init__.py +31 -0
  216. package/.agent-src/templates/scripts/work_engine/stack/detect.py +187 -0
  217. package/.agent-src/templates/scripts/work_engine/state.py +641 -0
  218. package/.agent-src/templates/scripts/work_engine/state_io.py +202 -0
  219. package/.claude-plugin/marketplace.json +105 -2
  220. package/AGENTS.md +38 -8
  221. package/CHANGELOG.md +609 -0
  222. package/README.md +136 -14
  223. package/config/agent-settings.template.yml +45 -0
  224. package/config/gitignore-block.txt +4 -0
  225. package/docs/MIGRATION.md +122 -0
  226. package/docs/architecture.md +111 -35
  227. package/docs/contracts/STABILITY.md +95 -0
  228. package/docs/contracts/adr-chat-history-split.md +132 -0
  229. package/docs/contracts/adr-command-suggestion.md +146 -0
  230. package/docs/contracts/adr-implement-ticket-runtime.md +122 -0
  231. package/docs/contracts/adr-product-ui-track.md +384 -0
  232. package/docs/contracts/adr-prompt-driven-execution.md +187 -0
  233. package/docs/contracts/agent-memory-contract.md +149 -0
  234. package/docs/contracts/artifact-engagement-flow.md +262 -0
  235. package/docs/contracts/command-clusters.md +126 -0
  236. package/docs/contracts/command-suggestion-flow.md +148 -0
  237. package/docs/contracts/implement-ticket-flow.md +628 -0
  238. package/docs/contracts/linear-ai-rules-inclusion.md +143 -0
  239. package/docs/contracts/linear-ai-three-layers.md +131 -0
  240. package/docs/contracts/rule-interactions.md +107 -0
  241. package/docs/contracts/rule-interactions.yml +142 -0
  242. package/docs/contracts/ui-stack-extension.md +236 -0
  243. package/docs/contracts/ui-track-flow.md +338 -0
  244. package/docs/development.md +1 -1
  245. package/docs/getting-started.md +3 -3
  246. package/docs/installation.md +124 -2
  247. package/docs/migrations/commands-1.15.0.md +112 -0
  248. package/docs/showcase.md +204 -0
  249. package/docs/ui-track-mental-model.md +121 -0
  250. package/package.json +1 -1
  251. package/scripts/agent-config +199 -0
  252. package/scripts/audit_cloud_compatibility.py +288 -0
  253. package/scripts/build_cloud_bundle.py +458 -0
  254. package/scripts/build_linear_digest.py +263 -0
  255. package/scripts/chat_history.py +796 -7
  256. package/scripts/check_compression.py +139 -0
  257. package/scripts/check_iron_law_prominence.py +143 -0
  258. package/scripts/check_md_language.py +159 -0
  259. package/scripts/check_portability.py +38 -0
  260. package/scripts/check_public_links.py +185 -0
  261. package/scripts/check_references.py +1 -0
  262. package/scripts/check_reply_consistency.py +140 -0
  263. package/scripts/command_suggester/__init__.py +51 -0
  264. package/scripts/command_suggester/cooldown.py +132 -0
  265. package/scripts/command_suggester/loader.py +70 -0
  266. package/scripts/command_suggester/match.py +180 -0
  267. package/scripts/command_suggester/rank.py +120 -0
  268. package/scripts/command_suggester/render.py +86 -0
  269. package/scripts/command_suggester/sanitize.py +113 -0
  270. package/scripts/command_suggester/settings.py +125 -0
  271. package/scripts/command_suggester/types.py +78 -0
  272. package/scripts/hooks/augment-chat-history.sh +56 -0
  273. package/scripts/install-hooks.sh +67 -0
  274. package/scripts/install.py +150 -33
  275. package/scripts/lint_marketplace.py +27 -0
  276. package/scripts/lint_no_new_atomic_commands.py +179 -0
  277. package/scripts/lint_rule_interactions.py +149 -0
  278. package/scripts/memory_lookup.py +1 -1
  279. package/scripts/migrate_command_suggestions.py +151 -0
  280. package/scripts/release.py +297 -64
  281. package/scripts/schemas/command.schema.json +41 -0
  282. package/scripts/skill_linter.py +81 -0
  283. package/scripts/sync_agent_settings.py +42 -12
  284. package/scripts/update_counts.py +10 -0
  285. package/templates/consumer-settings/augment-cli-hooks.json +54 -0
  286. package/templates/consumer-settings/claude-settings.json +55 -1
  287. package/.agent-src/rules/chat-history.md +0 -171
  288. package/.agent-src/templates/scripts/implement_ticket/cli.py +0 -171
  289. package/.agent-src/templates/scripts/implement_ticket/dispatcher.py +0 -134
  290. package/.agent-src/templates/scripts/implement_ticket/steps/__init__.py +0 -49
  291. package/.agent-src/templates/scripts/implement_ticket/steps/refine.py +0 -140
@@ -0,0 +1,170 @@
1
+ """Engagement report renderer (Phase 4 Step 2).
2
+
3
+ Two output formats sharing one quartile-bucketing pass:
4
+
5
+ - **markdown** — human-friendly table grouped into Essential (top 20 %),
6
+ Useful (mid 60 %), Retirement candidates (bottom 20 %).
7
+ - **json** — machine-readable summary; the same buckets, plus the
8
+ raw aggregate metadata, so downstream tooling never re-parses
9
+ the JSONL.
10
+
11
+ The bucketing is rank-based on ``applied`` count (the signal we care
12
+ about). Ties keep the deterministic order from
13
+ ``aggregator.rank_artefacts``. Empty inputs yield an empty-but-valid
14
+ report — the renderer never raises on an empty log.
15
+ """
16
+ from __future__ import annotations
17
+
18
+ import json
19
+ from dataclasses import dataclass
20
+ from typing import Any, Sequence
21
+
22
+ from .aggregator import AggregateResult, ArtefactStat, rank_artefacts
23
+ from .engagement import check_id_redaction
24
+
25
+ QUARTILE_TOP_RATIO = 0.20
26
+ QUARTILE_BOTTOM_RATIO = 0.20
27
+
28
+ BUCKET_TOP = "essential"
29
+ BUCKET_MID = "useful"
30
+ BUCKET_BOTTOM = "retirement_candidate"
31
+
32
+
33
+ @dataclass(frozen=True)
34
+ class BucketedStat:
35
+ stat: ArtefactStat
36
+ bucket: str
37
+
38
+
39
+ def bucketise(stats: Sequence[ArtefactStat]) -> list[BucketedStat]:
40
+ """Assign each stat to a quartile bucket.
41
+
42
+ Rank-based: indices ``[0, top_cut)`` → essential,
43
+ ``[top_cut, bottom_cut)`` → useful, ``[bottom_cut, n)`` →
44
+ retirement-candidate. Ranking from ``rank_artefacts`` is assumed.
45
+
46
+ For very small samples the cuts collapse:
47
+ n <= 1 → everything is essential
48
+ n <= 4 → top 1 essential, rest useful, none retirement
49
+ n >= 5 → at least 1 in each bucket
50
+ """
51
+ n = len(stats)
52
+ if n == 0:
53
+ return []
54
+ if n <= 1:
55
+ return [BucketedStat(stat=stats[0], bucket=BUCKET_TOP)]
56
+ top_cut = max(1, int(round(n * QUARTILE_TOP_RATIO)))
57
+ bottom_cut = n - max(1, int(round(n * QUARTILE_BOTTOM_RATIO))) if n >= 5 else n
58
+ if bottom_cut <= top_cut:
59
+ bottom_cut = n # mid takes the rest, no retirement bucket
60
+ out: list[BucketedStat] = []
61
+ for idx, stat in enumerate(stats):
62
+ if idx < top_cut:
63
+ bucket = BUCKET_TOP
64
+ elif idx < bottom_cut:
65
+ bucket = BUCKET_MID
66
+ else:
67
+ bucket = BUCKET_BOTTOM
68
+ out.append(BucketedStat(stat=stat, bucket=bucket))
69
+ return out
70
+
71
+
72
+ def render_markdown(
73
+ aggregate: AggregateResult,
74
+ *,
75
+ top: int | None = None,
76
+ since_label: str | None = None,
77
+ ) -> str:
78
+ """Render a markdown report. ``top`` truncates each bucket; ``None`` keeps all."""
79
+ ranked = rank_artefacts(aggregate.stats())
80
+ bucketed = bucketise(ranked)
81
+ grouped: dict[str, list[BucketedStat]] = {BUCKET_TOP: [], BUCKET_MID: [], BUCKET_BOTTOM: []}
82
+ for entry in bucketed:
83
+ grouped[entry.bucket].append(entry)
84
+
85
+ lines: list[str] = []
86
+ lines.append("# Artefact Engagement Report")
87
+ lines.append("")
88
+ lines.append(f"- events parsed: **{aggregate.parsed_events}**")
89
+ lines.append(f"- events skipped (malformed): **{aggregate.skipped_lines}**")
90
+ if since_label:
91
+ lines.append(f"- window: **{since_label}**")
92
+ if aggregate.earliest_ts and aggregate.latest_ts:
93
+ lines.append(f"- ts range: `{aggregate.earliest_ts}` → `{aggregate.latest_ts}`")
94
+ lines.append("")
95
+
96
+ titles = {
97
+ BUCKET_TOP: "Essential (top 20 %)",
98
+ BUCKET_MID: "Useful (mid 60 %)",
99
+ BUCKET_BOTTOM: "Retirement candidates (bottom 20 %)",
100
+ }
101
+ for bucket in (BUCKET_TOP, BUCKET_MID, BUCKET_BOTTOM):
102
+ rows = grouped[bucket]
103
+ if top is not None:
104
+ rows = rows[:top]
105
+ lines.append(f"## {titles[bucket]}")
106
+ lines.append("")
107
+ if not rows:
108
+ lines.append("_(none)_")
109
+ lines.append("")
110
+ continue
111
+ lines.append("| kind | id | consulted | applied | applied/consulted | last seen |")
112
+ lines.append("|---|---|---:|---:|---:|---|")
113
+ for entry in rows:
114
+ s = entry.stat
115
+ # Phase 5 export gate — applies to markdown too, not just JSON.
116
+ check_id_redaction(f"buckets.{s.kind}.id", s.artefact_id)
117
+ lines.append(
118
+ f"| {s.kind} | `{s.artefact_id}` | {s.consulted} | {s.applied} "
119
+ f"| {s.applied_ratio:.2f} | `{s.last_seen_ts}` |"
120
+ )
121
+ lines.append("")
122
+ return "\n".join(lines).rstrip() + "\n"
123
+
124
+
125
+ def render_json(
126
+ aggregate: AggregateResult,
127
+ *,
128
+ top: int | None = None,
129
+ since_label: str | None = None,
130
+ ) -> str:
131
+ ranked = rank_artefacts(aggregate.stats())
132
+ bucketed = bucketise(ranked)
133
+ grouped: dict[str, list[dict[str, Any]]] = {BUCKET_TOP: [], BUCKET_MID: [], BUCKET_BOTTOM: []}
134
+ for entry in bucketed:
135
+ grouped[entry.bucket].append(_stat_to_dict(entry.stat))
136
+ if top is not None:
137
+ for bucket in grouped:
138
+ grouped[bucket] = grouped[bucket][:top]
139
+ payload = {
140
+ "schema_version": 1,
141
+ "summary": {
142
+ "parsed_events": aggregate.parsed_events,
143
+ "skipped_lines": aggregate.skipped_lines,
144
+ "total_events": aggregate.total_events,
145
+ "earliest_ts": aggregate.earliest_ts,
146
+ "latest_ts": aggregate.latest_ts,
147
+ "since_label": since_label,
148
+ },
149
+ "buckets": grouped,
150
+ }
151
+ return json.dumps(payload, sort_keys=True, indent=2) + "\n"
152
+
153
+
154
+ def _stat_to_dict(stat: ArtefactStat) -> dict[str, Any]:
155
+ # Phase 5 export gate: every id leaving the renderer is re-validated
156
+ # against the same redaction floor that the schema enforces on
157
+ # write. A pre-validator log (or one hand-edited offline) can never
158
+ # leak path-shaped or free-text content into a shared report.
159
+ check_id_redaction(f"buckets.{stat.kind}.id", stat.artefact_id)
160
+ return {
161
+ "kind": stat.kind,
162
+ "id": stat.artefact_id,
163
+ "consulted": stat.consulted,
164
+ "applied": stat.applied,
165
+ "applied_ratio": round(stat.applied_ratio, 4),
166
+ "last_seen_ts": stat.last_seen_ts,
167
+ }
168
+
169
+
170
+ __all__ = ["BucketedStat", "bucketise", "render_markdown", "render_json"]
@@ -0,0 +1,112 @@
1
+ """Shared settings reader for the ``telemetry:*`` CLI commands.
2
+
3
+ Reads the ``telemetry.artifact_engagement`` namespace from
4
+ ``.agent-settings.yml``. Tolerates a missing file, a missing section,
5
+ and missing PyYAML — the default-off doctrine means "everything
6
+ unparseable means disabled".
7
+
8
+ Single source of truth so ``telemetry_record.py`` and
9
+ ``telemetry_status.py`` cannot drift on what counts as "enabled".
10
+ """
11
+ from __future__ import annotations
12
+
13
+ from dataclasses import dataclass
14
+ from pathlib import Path
15
+ from typing import Any
16
+
17
+ DEFAULT_LOG_PATH = Path(".agent-engagement.jsonl")
18
+ DEFAULT_GRANULARITY = "task"
19
+ ALLOWED_GRANULARITIES = ("task", "phase-step", "tool-call")
20
+
21
+
22
+ @dataclass(frozen=True)
23
+ class TelemetrySettings:
24
+ enabled: bool
25
+ granularity: str
26
+ log_path: Path
27
+ record_consulted: bool
28
+ record_applied: bool
29
+
30
+ @property
31
+ def section_present(self) -> bool:
32
+ # Distinguishes "disabled because section absent" from
33
+ # "disabled because someone wrote enabled: false". The status
34
+ # CLI uses this to render a different hint.
35
+ return self._section_present # type: ignore[attr-defined]
36
+
37
+
38
+ def _coerce_bool(value: Any, default: bool) -> bool:
39
+ if isinstance(value, bool):
40
+ return value
41
+ if isinstance(value, str):
42
+ normalised = value.strip().lower()
43
+ if normalised in ("true", "yes", "on", "1"):
44
+ return True
45
+ if normalised in ("false", "no", "off", "0"):
46
+ return False
47
+ return default
48
+
49
+
50
+ def _coerce_str(value: Any, default: str, allowed: tuple[str, ...] | None = None) -> str:
51
+ if not isinstance(value, str) or not value.strip():
52
+ return default
53
+ candidate = value.strip()
54
+ if allowed and candidate not in allowed:
55
+ return default
56
+ return candidate
57
+
58
+
59
+ def _coerce_path(value: Any, default: Path) -> Path:
60
+ if not isinstance(value, str) or not value.strip():
61
+ return default
62
+ return Path(value.strip())
63
+
64
+
65
+ def read_settings(path: Path) -> TelemetrySettings:
66
+ """Return parsed telemetry settings — never raises on missing data."""
67
+ section: dict[str, Any] = {}
68
+ section_present = False
69
+
70
+ if path.is_file():
71
+ try:
72
+ import yaml # type: ignore[import-not-found]
73
+ except ImportError:
74
+ yaml = None # type: ignore[assignment]
75
+ if yaml is not None:
76
+ try:
77
+ raw = yaml.safe_load(path.read_text(encoding="utf-8")) or {}
78
+ except Exception:
79
+ raw = {}
80
+ if isinstance(raw, dict):
81
+ tele = raw.get("telemetry")
82
+ if isinstance(tele, dict):
83
+ artefact = tele.get("artifact_engagement")
84
+ if isinstance(artefact, dict):
85
+ section = artefact
86
+ section_present = True
87
+
88
+ record = section.get("record") if isinstance(section.get("record"), dict) else {}
89
+ output = section.get("output") if isinstance(section.get("output"), dict) else {}
90
+
91
+ settings = TelemetrySettings(
92
+ enabled=_coerce_bool(section.get("enabled"), default=False),
93
+ granularity=_coerce_str(
94
+ section.get("granularity"),
95
+ default=DEFAULT_GRANULARITY,
96
+ allowed=ALLOWED_GRANULARITIES,
97
+ ),
98
+ log_path=_coerce_path(output.get("path"), DEFAULT_LOG_PATH),
99
+ record_consulted=_coerce_bool(record.get("consulted"), default=True),
100
+ record_applied=_coerce_bool(record.get("applied"), default=True),
101
+ )
102
+ # Carry the section-present flag without breaking dataclass frozen-ness.
103
+ object.__setattr__(settings, "_section_present", section_present)
104
+ return settings
105
+
106
+
107
+ __all__ = [
108
+ "DEFAULT_GRANULARITY",
109
+ "DEFAULT_LOG_PATH",
110
+ "TelemetrySettings",
111
+ "read_settings",
112
+ ]
@@ -0,0 +1,166 @@
1
+ #!/usr/bin/env python3
2
+ """``./agent-config telemetry:record`` — append one engagement event.
3
+
4
+ Reads the ``telemetry.artifact_engagement`` namespace from
5
+ ``.agent-settings.yml``. When ``enabled: false`` (default) the script
6
+ exits 0 silently and performs zero file IO — the default-off doctrine
7
+ for this whole feature.
8
+
9
+ Usage:
10
+ # JSON payload via --payload-file (consumed atomically)
11
+ ./agent-config telemetry:record --payload-file payload.json
12
+
13
+ # JSON payload via stdin
14
+ cat payload.json | ./agent-config telemetry:record --stdin
15
+
16
+ # Direct construction (idempotent within boundary if reused via
17
+ # the BoundarySession class — at the CLI layer, each call writes
18
+ # one line)
19
+ ./agent-config telemetry:record \\
20
+ --task-id ticket-PROJ-42 --boundary task \\
21
+ --consulted skills:php-coder --consulted rules:scope-control \\
22
+ --applied skills:php-coder
23
+
24
+ Exit codes:
25
+ 0 success or disabled (silent)
26
+ 1 schema-validation failure
27
+ 2 IO / settings parse error
28
+ """
29
+ from __future__ import annotations
30
+
31
+ import argparse
32
+ import json
33
+ import sys
34
+ from pathlib import Path
35
+
36
+ # Resolve sibling ``telemetry/`` package — Python adds the script's
37
+ # directory to sys.path automatically, so this import works whether
38
+ # the script is dispatched from the package or from a consumer copy.
39
+ from telemetry.boundary import record_event
40
+ from telemetry.engagement import (
41
+ EngagementEvent,
42
+ EngagementSchemaError,
43
+ now_utc_iso,
44
+ )
45
+ from telemetry.settings import TelemetrySettings, read_settings
46
+
47
+
48
+ def _parse_kv_list(values: list[str]) -> dict[str, list[str]]:
49
+ """Turn ``["skills:a", "skills:b", "rules:c"]`` into a kind→ids dict."""
50
+ out: dict[str, list[str]] = {}
51
+ for raw in values:
52
+ if ":" not in raw:
53
+ raise SystemExit(
54
+ f"❌ --consulted/--applied must be 'kind:id', got {raw!r}"
55
+ )
56
+ kind, _, art_id = raw.partition(":")
57
+ kind = kind.strip()
58
+ art_id = art_id.strip()
59
+ if not kind or not art_id:
60
+ raise SystemExit(
61
+ f"❌ empty kind or id in {raw!r}"
62
+ )
63
+ out.setdefault(kind, []).append(art_id)
64
+ return out
65
+
66
+
67
+ def _build_event_from_args(args: argparse.Namespace) -> EngagementEvent:
68
+ return EngagementEvent(
69
+ ts=args.ts or now_utc_iso(),
70
+ task_id=args.task_id,
71
+ boundary_kind=args.boundary,
72
+ consulted=_parse_kv_list(args.consulted or []),
73
+ applied=_parse_kv_list(args.applied or []),
74
+ )
75
+
76
+
77
+ def _build_event_from_payload(raw: str) -> EngagementEvent:
78
+ try:
79
+ data = json.loads(raw)
80
+ except json.JSONDecodeError as exc:
81
+ raise SystemExit(f"❌ payload is not valid JSON: {exc}")
82
+ if not isinstance(data, dict):
83
+ raise SystemExit("❌ payload must be a JSON object")
84
+ return EngagementEvent(
85
+ ts=data.get("ts") or now_utc_iso(),
86
+ task_id=data.get("task_id", ""),
87
+ boundary_kind=data.get("boundary_kind", ""),
88
+ consulted=data.get("consulted", {}) or {},
89
+ applied=data.get("applied", {}) or {},
90
+ tokens_estimate=data.get("tokens_estimate"),
91
+ )
92
+
93
+
94
+ def main(argv: list[str] | None = None) -> int:
95
+ parser = argparse.ArgumentParser(
96
+ prog="agent-config telemetry:record",
97
+ description=(
98
+ "Append one artefact-engagement event to the local JSONL log. "
99
+ "Default-off — silent exit 0 unless explicitly enabled."
100
+ ),
101
+ )
102
+ parser.add_argument("--task-id", default="")
103
+ parser.add_argument(
104
+ "--boundary",
105
+ default="task",
106
+ choices=("task", "phase-step", "tool-call"),
107
+ )
108
+ parser.add_argument("--consulted", action="append")
109
+ parser.add_argument("--applied", action="append")
110
+ parser.add_argument("--ts", default="")
111
+ parser.add_argument("--payload-file", type=Path)
112
+ parser.add_argument("--stdin", action="store_true")
113
+ parser.add_argument(
114
+ "--settings",
115
+ type=Path,
116
+ default=Path(".agent-settings.yml"),
117
+ help="Override settings path (tests).",
118
+ )
119
+ parser.add_argument(
120
+ "--force",
121
+ action="store_true",
122
+ help="Bypass the enabled-flag (tests + maintainer one-shots).",
123
+ )
124
+ args = parser.parse_args(argv)
125
+
126
+ try:
127
+ settings: TelemetrySettings = read_settings(args.settings)
128
+ except OSError as exc:
129
+ print(f"❌ cannot read settings: {exc}", file=sys.stderr)
130
+ return 2
131
+
132
+ if not settings.enabled and not args.force:
133
+ # Default-off: silent success. Crucially: no payload parsing,
134
+ # no schema construction — zero work attributable to telemetry.
135
+ return 0
136
+
137
+ if args.payload_file:
138
+ try:
139
+ raw = args.payload_file.read_text(encoding="utf-8")
140
+ except OSError as exc:
141
+ print(f"❌ cannot read --payload-file: {exc}", file=sys.stderr)
142
+ return 2
143
+ event = _build_event_from_payload(raw)
144
+ elif args.stdin:
145
+ event = _build_event_from_payload(sys.stdin.read())
146
+ else:
147
+ if not args.task_id:
148
+ print("❌ --task-id required (or pass --payload-file/--stdin)",
149
+ file=sys.stderr)
150
+ return 1
151
+ event = _build_event_from_args(args)
152
+
153
+ try:
154
+ record_event(settings.log_path, event)
155
+ except EngagementSchemaError as exc:
156
+ print(f"❌ schema validation failed: {exc}", file=sys.stderr)
157
+ return 1
158
+ except OSError as exc:
159
+ print(f"❌ cannot write engagement log: {exc}", file=sys.stderr)
160
+ return 2
161
+
162
+ return 0
163
+
164
+
165
+ if __name__ == "__main__":
166
+ raise SystemExit(main())
@@ -0,0 +1,161 @@
1
+ #!/usr/bin/env python3
2
+ """``./agent-config telemetry:report`` — aggregate the engagement log.
3
+
4
+ Reads ``.agent-engagement.jsonl``, groups events by ``(kind, id)``,
5
+ quartile-buckets the artefacts (essential / useful / retirement
6
+ candidate), and prints a markdown table or JSON document.
7
+
8
+ Usage:
9
+ # Defaults: --since 30d --top 20 markdown
10
+ ./agent-config telemetry:report
11
+
12
+ # Last 7 days, JSON, no truncation
13
+ ./agent-config telemetry:report --since 7d --format json --top 0
14
+
15
+ # Override log path (tests; consumer copies; reports on archived logs)
16
+ ./agent-config telemetry:report --log-path /tmp/snapshot.jsonl
17
+
18
+ Exit codes:
19
+ 0 success (empty log → empty-but-valid report)
20
+ 2 IO / settings parse error, unparseable --since, or
21
+ redaction-validator failure on a row sourced from the log
22
+ (a path-shaped or extension-shaped id slipped past the write
23
+ gate; the report is refused rather than shared)
24
+ """
25
+ from __future__ import annotations
26
+
27
+ import argparse
28
+ import re
29
+ import sys
30
+ from datetime import datetime, timedelta, timezone
31
+ from pathlib import Path
32
+
33
+ from telemetry.aggregator import aggregate
34
+ from telemetry.engagement import EngagementSchemaError
35
+ from telemetry.report_renderer import render_json, render_markdown
36
+ from telemetry.settings import read_settings
37
+
38
+ _DURATION_RE = re.compile(r"^\s*(\d+)\s*([dhm])\s*$")
39
+
40
+
41
+ def _parse_since(value: str | None) -> tuple[datetime | None, str | None]:
42
+ """Parse ``30d`` / ``7d`` / ``24h`` / ``60m`` into a UTC cutoff.
43
+
44
+ Returns ``(cutoff_or_None, human_label)``. ``value`` of ``None`` or
45
+ ``"all"`` means "no lower bound" — both cutoff and label are
46
+ ``None``. Raises ``ValueError`` on malformed input so the CLI can
47
+ surface a clean error and exit 2.
48
+ """
49
+ if value is None or value.strip().lower() == "all":
50
+ return None, None
51
+ match = _DURATION_RE.match(value)
52
+ if not match:
53
+ raise ValueError(
54
+ f"--since must be <int>{{d,h,m}} or 'all', got {value!r}"
55
+ )
56
+ qty = int(match.group(1))
57
+ unit = match.group(2)
58
+ delta = {
59
+ "d": timedelta(days=qty),
60
+ "h": timedelta(hours=qty),
61
+ "m": timedelta(minutes=qty),
62
+ }[unit]
63
+ cutoff = datetime.now(tz=timezone.utc) - delta
64
+ label = f"last {qty}{unit}"
65
+ return cutoff, label
66
+
67
+
68
+ def main(argv: list[str] | None = None) -> int:
69
+ parser = argparse.ArgumentParser(
70
+ prog="agent-config telemetry:report",
71
+ description=(
72
+ "Render an artefact-engagement report from the JSONL log. "
73
+ "Read-only; never mutates settings or the log."
74
+ ),
75
+ )
76
+ parser.add_argument(
77
+ "--since",
78
+ default="30d",
79
+ help=(
80
+ "Lower-bound time window: <int>{d,h,m} or 'all'. "
81
+ "Default: 30d. Events at or before the cutoff are excluded."
82
+ ),
83
+ )
84
+ parser.add_argument(
85
+ "--top",
86
+ type=int,
87
+ default=20,
88
+ help=(
89
+ "Truncate each bucket to N rows. Default: 20. Use 0 to "
90
+ "disable truncation."
91
+ ),
92
+ )
93
+ parser.add_argument(
94
+ "--format",
95
+ choices=("markdown", "json"),
96
+ default="markdown",
97
+ )
98
+ parser.add_argument(
99
+ "--log-path",
100
+ type=Path,
101
+ default=None,
102
+ help=(
103
+ "Override the log path. Default: read "
104
+ "telemetry.artifact_engagement.output.path from "
105
+ ".agent-settings.yml (falls back to .agent-engagement.jsonl)."
106
+ ),
107
+ )
108
+ parser.add_argument(
109
+ "--settings",
110
+ type=Path,
111
+ default=Path(".agent-settings.yml"),
112
+ help="Override settings path (tests).",
113
+ )
114
+ args = parser.parse_args(argv)
115
+
116
+ try:
117
+ cutoff, since_label = _parse_since(args.since)
118
+ except ValueError as exc:
119
+ print(f"❌ {exc}", file=sys.stderr)
120
+ return 2
121
+
122
+ if args.log_path is not None:
123
+ log_path = args.log_path
124
+ else:
125
+ try:
126
+ settings = read_settings(args.settings)
127
+ except OSError as exc:
128
+ print(f"❌ cannot read settings: {exc}", file=sys.stderr)
129
+ return 2
130
+ log_path = settings.log_path
131
+
132
+ try:
133
+ result = aggregate(log_path, since=cutoff)
134
+ except OSError as exc:
135
+ print(f"❌ cannot read log {log_path}: {exc}", file=sys.stderr)
136
+ return 2
137
+
138
+ top = None if args.top <= 0 else args.top
139
+ try:
140
+ if args.format == "json":
141
+ rendered = render_json(result, top=top, since_label=since_label)
142
+ else:
143
+ rendered = render_markdown(result, top=top, since_label=since_label)
144
+ except EngagementSchemaError as exc:
145
+ print(
146
+ f"❌ redaction validator refused report: {exc}",
147
+ file=sys.stderr,
148
+ )
149
+ return 2
150
+ sys.stdout.write(rendered)
151
+
152
+ if result.skipped_lines:
153
+ print(
154
+ f"⚠️ skipped {result.skipped_lines} malformed line(s)",
155
+ file=sys.stderr,
156
+ )
157
+ return 0
158
+
159
+
160
+ if __name__ == "__main__":
161
+ raise SystemExit(main())