@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.
- package/.agent-src/commands/agent-handoff.md +4 -1
- 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 +7 -3
- 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 +6 -1
- package/.agent-src/commands/chat-history-resume.md +7 -2
- package/.agent-src/commands/chat-history.md +7 -2
- package/.agent-src/commands/check-current-md.md +137 -0
- package/.agent-src/commands/commit-in-chunks.md +118 -0
- package/.agent-src/commands/commit.md +4 -0
- package/.agent-src/commands/compress.md +37 -2
- 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 +5 -2
- 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 +33 -0
- package/.agent-src/commands/optimize-agents.md +4 -0
- package/.agent-src/commands/optimize-augmentignore.md +12 -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 +12 -7
- package/.agent-src/commands/review-changes.md +39 -8
- package/.agent-src/commands/review-routing.md +4 -0
- package/.agent-src/commands/roadmap-create.md +18 -0
- package/.agent-src/commands/roadmap-execute.md +14 -1
- package/.agent-src/commands/rule-compliance-audit.md +4 -0
- package/.agent-src/commands/set-cost-profile.md +11 -0
- package/.agent-src/commands/sync-agent-settings.md +12 -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 +6 -3
- 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 +64 -37
- package/.agent-src/rules/autonomous-execution.md +158 -0
- package/.agent-src/rules/chat-history-cadence.md +109 -0
- package/.agent-src/rules/chat-history-ownership.md +123 -0
- package/.agent-src/rules/chat-history-visibility.md +96 -0
- package/.agent-src/rules/cli-output-handling.md +27 -4
- package/.agent-src/rules/command-suggestion.md +134 -0
- package/.agent-src/rules/commit-policy.md +109 -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 +85 -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 +159 -27
- package/.agent-src/rules/role-mode-adherence.md +1 -1
- package/.agent-src/rules/scope-control.md +42 -1
- package/.agent-src/rules/size-enforcement.md +2 -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 +107 -51
- package/.agent-src/scripts/update_roadmap_progress.py +73 -9
- package/.agent-src/skills/blade-ui/SKILL.md +47 -3
- package/.agent-src/skills/command-routing/SKILL.md +32 -0
- package/.agent-src/skills/command-writing/SKILL.md +52 -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 +202 -0
- package/.agent-src/skills/fe-design/SKILL.md +78 -61
- package/.agent-src/skills/file-editor/SKILL.md +9 -0
- 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 +49 -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 +32 -28
- package/.agent-src/skills/roadmap-management/SKILL.md +24 -11
- package/.agent-src/skills/rule-writing/SKILL.md +23 -1
- package/.agent-src/skills/skill-writing/SKILL.md +3 -5
- package/.agent-src/skills/upstream-contribute/SKILL.md +3 -3
- 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 +11 -4
- 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/memory_lookup.py +1 -1
- 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 +195 -0
- package/.agent-src/templates/scripts/work_engine/cli_args.py +116 -0
- package/.agent-src/templates/scripts/{implement_ticket → work_engine}/delivery_state.py +10 -3
- 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 +3 -3
- package/.agent-src/templates/scripts/{implement_ticket/steps → work_engine/directives/backend}/memory.py +2 -2
- package/.agent-src/templates/scripts/{implement_ticket/steps → work_engine/directives/backend}/plan.py +2 -2
- 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 +37 -5
- 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/emitters.py +43 -0
- package/.agent-src/templates/scripts/work_engine/errors.py +19 -0
- package/.agent-src/templates/scripts/work_engine/hook_bootstrap.py +76 -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/input_builders.py +163 -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 +231 -0
- package/.agent-src/templates/scripts/{implement_ticket → work_engine}/persona_policy.py +1 -1
- 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/.agent-src/templates/scripts/work_engine/state_io.py +202 -0
- package/.claude-plugin/marketplace.json +105 -2
- package/AGENTS.md +38 -8
- package/CHANGELOG.md +609 -0
- package/README.md +136 -14
- package/config/agent-settings.template.yml +45 -0
- package/config/gitignore-block.txt +4 -0
- package/docs/MIGRATION.md +122 -0
- package/docs/architecture.md +111 -35
- package/docs/contracts/STABILITY.md +95 -0
- package/docs/contracts/adr-chat-history-split.md +132 -0
- package/docs/contracts/adr-command-suggestion.md +146 -0
- package/docs/contracts/adr-implement-ticket-runtime.md +122 -0
- package/docs/contracts/adr-product-ui-track.md +384 -0
- package/docs/contracts/adr-prompt-driven-execution.md +187 -0
- package/docs/contracts/agent-memory-contract.md +149 -0
- package/docs/contracts/artifact-engagement-flow.md +262 -0
- package/docs/contracts/command-clusters.md +126 -0
- package/docs/contracts/command-suggestion-flow.md +148 -0
- package/docs/contracts/implement-ticket-flow.md +628 -0
- package/docs/contracts/linear-ai-rules-inclusion.md +143 -0
- package/docs/contracts/linear-ai-three-layers.md +131 -0
- package/docs/contracts/rule-interactions.md +107 -0
- package/docs/contracts/rule-interactions.yml +142 -0
- package/docs/contracts/ui-stack-extension.md +236 -0
- package/docs/contracts/ui-track-flow.md +338 -0
- package/docs/development.md +1 -1
- package/docs/getting-started.md +3 -3
- package/docs/installation.md +124 -2
- package/docs/migrations/commands-1.15.0.md +112 -0
- package/docs/showcase.md +204 -0
- package/docs/ui-track-mental-model.md +121 -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 +38 -0
- package/scripts/check_public_links.py +185 -0
- package/scripts/check_references.py +1 -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/lint_no_new_atomic_commands.py +179 -0
- package/scripts/lint_rule_interactions.py +149 -0
- package/scripts/memory_lookup.py +1 -1
- package/scripts/migrate_command_suggestions.py +151 -0
- package/scripts/release.py +297 -64
- package/scripts/schemas/command.schema.json +41 -0
- package/scripts/skill_linter.py +81 -0
- package/scripts/sync_agent_settings.py +42 -12
- package/scripts/update_counts.py +10 -0
- package/templates/consumer-settings/augment-cli-hooks.json +54 -0
- package/templates/consumer-settings/claude-settings.json +55 -1
- package/.agent-src/rules/chat-history.md +0 -171
- 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
|
@@ -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())
|