workflow-toolkit 1.0.1
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/.agents/skills/ponytail/SKILL.md +111 -0
- package/.agents/skills/ponytail-audit/SKILL.md +37 -0
- package/.agents/skills/ponytail-debt/SKILL.md +41 -0
- package/.agents/skills/ponytail-gain/SKILL.md +47 -0
- package/.agents/skills/ponytail-help/SKILL.md +70 -0
- package/.agents/skills/ponytail-review/SKILL.md +52 -0
- package/.agents/skills/prompt-review/SKILL.md +60 -0
- package/.agents/skills/wtk/SKILL.md +64 -0
- package/.agents/skills/wtk/references/artifacts.md +82 -0
- package/.agents/skills/wtk/references/evidence.md +68 -0
- package/.agents/skills/wtk/references/git.md +75 -0
- package/.agents/skills/wtk/references/test-contract.md +76 -0
- package/.agents/skills/wtk/references/validation.md +127 -0
- package/.agents/skills/wtk-config/SKILL.md +102 -0
- package/.agents/skills/wtk-config/assets/agents/claude/deep-reviewer.md +40 -0
- package/.agents/skills/wtk-config/assets/agents/claude/designer.md +57 -0
- package/.agents/skills/wtk-config/assets/agents/claude/explorer.md +41 -0
- package/.agents/skills/wtk-config/assets/agents/claude/implementer.md +57 -0
- package/.agents/skills/wtk-config/assets/agents/claude/planner.md +68 -0
- package/.agents/skills/wtk-config/assets/agents/claude/verifier.md +80 -0
- package/.agents/skills/wtk-config/assets/agents/codex/deep-reviewer.toml +41 -0
- package/.agents/skills/wtk-config/assets/agents/codex/designer.toml +55 -0
- package/.agents/skills/wtk-config/assets/agents/codex/explorer.toml +35 -0
- package/.agents/skills/wtk-config/assets/agents/codex/implementer.toml +52 -0
- package/.agents/skills/wtk-config/assets/agents/codex/planner.toml +66 -0
- package/.agents/skills/wtk-config/assets/agents/codex/verifier.toml +78 -0
- package/.agents/skills/wtk-config/assets/agents/cursor/deep-reviewer.md +38 -0
- package/.agents/skills/wtk-config/assets/agents/cursor/designer.md +55 -0
- package/.agents/skills/wtk-config/assets/agents/cursor/explorer.md +39 -0
- package/.agents/skills/wtk-config/assets/agents/cursor/implementer.md +55 -0
- package/.agents/skills/wtk-config/assets/agents/cursor/planner.md +66 -0
- package/.agents/skills/wtk-config/assets/agents/cursor/verifier.md +79 -0
- package/.agents/skills/wtk-config/scripts/ad-index.py +123 -0
- package/.agents/skills/wtk-config/scripts/repository_intelligence.py +671 -0
- package/.agents/skills/wtk-config/scripts/workflow_config.py +972 -0
- package/.agents/skills/wtk-deep-review/SKILL.md +164 -0
- package/.agents/skills/wtk-deep-review/assets/PROMPT.md +56 -0
- package/.agents/skills/wtk-deep-review/assets/REVIEW_UI.html +1340 -0
- package/.agents/skills/wtk-deep-review/assets/findings.schema.json +113 -0
- package/.agents/skills/wtk-deep-review/references/context-pack.md +88 -0
- package/.agents/skills/wtk-deep-review/references/orchestration.md +164 -0
- package/.agents/skills/wtk-deep-review/references/output-contracts.md +149 -0
- package/.agents/skills/wtk-deep-review/references/publish-github.md +83 -0
- package/.agents/skills/wtk-deep-review/references/state-and-learnings.md +64 -0
- package/.agents/skills/wtk-deep-review/references/subagent-runtimes.md +33 -0
- package/.agents/skills/wtk-deep-review/references/taxonomy.md +70 -0
- package/.agents/skills/wtk-deep-review/scripts/_common.py +371 -0
- package/.agents/skills/wtk-deep-review/scripts/build_jobs.py +561 -0
- package/.agents/skills/wtk-deep-review/scripts/build_knowledge.py +349 -0
- package/.agents/skills/wtk-deep-review/scripts/build_manifest.py +479 -0
- package/.agents/skills/wtk-deep-review/scripts/graft_context.py +160 -0
- package/.agents/skills/wtk-deep-review/scripts/graphify_context.py +75 -0
- package/.agents/skills/wtk-deep-review/scripts/merge_findings.py +313 -0
- package/.agents/skills/wtk-deep-review/scripts/render_html.py +240 -0
- package/.agents/skills/wtk-deep-review/scripts/render_review.py +282 -0
- package/.agents/skills/wtk-deep-review/scripts/run_jobs.py +436 -0
- package/.agents/skills/wtk-deep-review/scripts/token_metrics.py +369 -0
- package/.agents/skills/wtk-discover/SKILL.md +270 -0
- package/.agents/skills/wtk-discover/references/document-format.md +149 -0
- package/.agents/skills/wtk-implement/SKILL.md +95 -0
- package/.agents/skills/wtk-implement/references/checklist-format.md +116 -0
- package/.agents/skills/wtk-implement/references/screens.md +129 -0
- package/.agents/skills/wtk-implement/references/test-policy.md +152 -0
- package/.agents/skills/wtk-implement/references/verify.md +297 -0
- package/.agents/skills/wtk-knowledge-check/SKILL.md +16 -0
- package/.agents/skills/wtk-knowledge-check/scripts/check.ts +502 -0
- package/.agents/skills/wtk-knowledge-check/scripts/cli.ts +32 -0
- package/.agents/skills/wtk-knowledge-check/scripts/frontmatter.ts +44 -0
- package/.agents/skills/wtk-lean/.skill-meta.json +6 -0
- package/.agents/skills/wtk-lean/NOTICE.md +18 -0
- package/.agents/skills/wtk-lean/SKILL.md +137 -0
- package/.agents/skills/wtk-lean/references/build.md +168 -0
- package/.agents/skills/wtk-lean/references/checks.md +253 -0
- package/.agents/skills/wtk-lean/references/memory.md +156 -0
- package/.agents/skills/wtk-lean/references/plan.md +514 -0
- package/.agents/skills/wtk-lean/references/verify.md +336 -0
- package/.agents/skills/wtk-lean/scripts/check_commit.py +121 -0
- package/.agents/skills/wtk-lean/scripts/fixtures/checks.md +98 -0
- package/.agents/skills/wtk-lean/scripts/fixtures/plan.md +119 -0
- package/.agents/skills/wtk-lean/scripts/fixtures/verification.md +41 -0
- package/.agents/skills/wtk-lean/scripts/lessons.py +412 -0
- package/.agents/skills/wtk-lean/scripts/selftest.py +401 -0
- package/.agents/skills/wtk-lean/scripts/validate_checks.py +417 -0
- package/.agents/skills/wtk-lean/scripts/validate_plan.py +580 -0
- package/.agents/skills/wtk-lean/scripts/validate_verification.py +357 -0
- package/.agents/skills/wtk-plan/SKILL.md +102 -0
- package/.agents/skills/wtk-plan/references/document-format.md +222 -0
- package/.agents/skills/wtk-qa/SKILL.md +14 -0
- package/.agents/skills/wtk-qa-execute/SKILL.md +115 -0
- package/.agents/skills/wtk-qa-execute/references/fix-loop.md +23 -0
- package/.agents/skills/wtk-qa-execute/references/session-protocol.md +25 -0
- package/.agents/skills/wtk-qa-plan/SKILL.md +111 -0
- package/.agents/skills/wtk-qa-plan/references/profile.md +35 -0
- package/.agents/skills/wtk-ship/SKILL.md +49 -0
- package/.agents/skills/wtk-ship/remediation.py +170 -0
- package/.agents/skills/wtk-ship/scripts/close_feature.py +83 -0
- package/.agents/skills/wtk-ship/scripts/review_convergence.py +373 -0
- package/.wtk.toml.example +93 -0
- package/AGENTS.md +129 -0
- package/NOTICE.md +12 -0
- package/README.md +440 -0
- package/bin/wtk.js +25 -0
- package/docs/toolkit/README.md +56 -0
- package/docs/toolkit/decisions.md +61 -0
- package/docs/toolkit/guidelines/CONTEXT-BUDGET.md +64 -0
- package/docs/toolkit/guidelines/DX.md +67 -0
- package/docs/toolkit/guidelines/FRONTEND.md +73 -0
- package/docs/toolkit/guidelines/KNOWLEDGE-WIKI.md +128 -0
- package/docs/toolkit/guidelines/MODELING.md +134 -0
- package/docs/toolkit/guidelines/QA-EXECUTION.md +46 -0
- package/docs/toolkit/guidelines/QA-SCENARIOS.md +165 -0
- package/docs/toolkit/guidelines/REVIEW-ROUNDS.md +160 -0
- package/docs/toolkit/guidelines/SECURITY.md +160 -0
- package/docs/toolkit/guidelines/UI-UX.md +117 -0
- package/docs/toolkit/guidelines/WORKFLOW-MEMORY.md +73 -0
- package/docs/toolkit/guidelines.md +50 -0
- package/docs/toolkit/loop.md +77 -0
- package/docs/toolkit/purpose.md +51 -0
- package/docs/toolkit/repository-intelligence.md +53 -0
- package/docs/toolkit/reviews.md +77 -0
- package/knowledge/AGENTS.md +316 -0
- package/knowledge/raw/README.md +17 -0
- package/package.json +74 -0
- package/scripts/install_security_skills.py +816 -0
- package/scripts/installer/engine.js +254 -0
- package/scripts/installer/knowledge.js +28 -0
- package/scripts/installer/packets.js +77 -0
- package/scripts/installer/terminal.js +98 -0
- package/scripts/installer/transaction.js +75 -0
- package/skills-lock.json +92 -0
- package/templates/adoption/agents/core.md +23 -0
- package/templates/adoption/agents/quality.md +3 -0
- package/templates/adoption/knowledge/wiki/architecture/index.md +3 -0
- package/templates/adoption/knowledge/wiki/decisions/index.md +3 -0
- package/templates/adoption/knowledge/wiki/design/index.md +3 -0
- package/templates/adoption/knowledge/wiki/domain/index.md +3 -0
- package/templates/adoption/knowledge/wiki/index.md +3 -0
- package/templates/adoption/knowledge/wiki/log.md +3 -0
- package/templates/adoption/knowledge/wiki/open-questions/index.md +3 -0
- package/templates/adoption/knowledge/wiki/product/index.md +3 -0
- package/templates/adoption/knowledge/wiki/research/index.md +3 -0
- package/templates/adoption/product/AGENT-CONTEXT.md +25 -0
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Content-safe observational token metrics for compatible providers."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
8
|
+
import secrets
|
|
9
|
+
import sqlite3
|
|
10
|
+
from datetime import datetime, timezone
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
USAGE_FIELDS = (
|
|
14
|
+
"input_tokens",
|
|
15
|
+
"cached_input_tokens",
|
|
16
|
+
"output_tokens",
|
|
17
|
+
"reasoning_output_tokens",
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class TokenMetricsError(Exception):
|
|
22
|
+
"""Safe error used internally when a measurement cannot be trusted."""
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _empty_usage() -> dict[str, int | None]:
|
|
26
|
+
return {"total_tokens": 0, **{field: None for field in USAGE_FIELDS}}
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _safe_count(value: object) -> int | None:
|
|
30
|
+
return value if isinstance(value, int) and not isinstance(value, bool) and value >= 0 else None
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _usage(value: object) -> bool:
|
|
34
|
+
expected = {"total_tokens", *USAGE_FIELDS}
|
|
35
|
+
return (
|
|
36
|
+
isinstance(value, dict)
|
|
37
|
+
and set(value) == expected
|
|
38
|
+
and _safe_count(value["total_tokens"]) is not None
|
|
39
|
+
and all(value[field] is None or _safe_count(value[field]) is not None for field in USAGE_FIELDS)
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _timestamp(value: object) -> bool:
|
|
44
|
+
if not isinstance(value, str) or not value:
|
|
45
|
+
return False
|
|
46
|
+
try:
|
|
47
|
+
datetime.fromisoformat(value.replace("Z", "+00:00"))
|
|
48
|
+
return True
|
|
49
|
+
except ValueError:
|
|
50
|
+
return False
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _now() -> str:
|
|
54
|
+
return datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _rollout_usage(path: object) -> dict[str, int | None]:
|
|
58
|
+
if not isinstance(path, str) or not path or not Path(path).is_file():
|
|
59
|
+
return {field: None for field in USAGE_FIELDS}
|
|
60
|
+
latest: object = None
|
|
61
|
+
try:
|
|
62
|
+
with Path(path).open(encoding="utf-8", errors="replace") as stream:
|
|
63
|
+
for line in stream:
|
|
64
|
+
try:
|
|
65
|
+
event = json.loads(line)
|
|
66
|
+
except json.JSONDecodeError:
|
|
67
|
+
continue
|
|
68
|
+
payload = event.get("payload") if isinstance(event, dict) else None
|
|
69
|
+
if not isinstance(payload, dict) or payload.get("type") != "token_count":
|
|
70
|
+
continue
|
|
71
|
+
info = payload.get("info")
|
|
72
|
+
if not isinstance(info, dict):
|
|
73
|
+
latest = None
|
|
74
|
+
continue
|
|
75
|
+
candidate = info.get("total_token_usage", info.get("last_token_usage"))
|
|
76
|
+
latest = candidate if isinstance(candidate, dict) else None
|
|
77
|
+
except OSError:
|
|
78
|
+
return {field: None for field in USAGE_FIELDS}
|
|
79
|
+
if not isinstance(latest, dict):
|
|
80
|
+
return {field: None for field in USAGE_FIELDS}
|
|
81
|
+
return {field: _safe_count(latest.get(field)) for field in USAGE_FIELDS}
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def read_telemetry(db_path: str | Path, reviewer_prefix: str) -> dict[str, dict[str, int | None]]:
|
|
85
|
+
"""Read only allowlisted counters for the explicitly configured path."""
|
|
86
|
+
path = Path(db_path).expanduser()
|
|
87
|
+
prefix = reviewer_prefix.rstrip("/") if isinstance(reviewer_prefix, str) else ""
|
|
88
|
+
if not prefix or not path.is_file():
|
|
89
|
+
raise TokenMetricsError("telemetry")
|
|
90
|
+
db: sqlite3.Connection | None = None
|
|
91
|
+
try:
|
|
92
|
+
db = sqlite3.connect(path.as_posix())
|
|
93
|
+
db.execute("PRAGMA query_only = ON")
|
|
94
|
+
columns = {row[1] for row in db.execute("PRAGMA table_info(threads)")}
|
|
95
|
+
if not {"id", "rollout_path", "tokens_used", "agent_path"}.issubset(columns):
|
|
96
|
+
raise TokenMetricsError("telemetry")
|
|
97
|
+
result: dict[str, dict[str, int | None]] = {}
|
|
98
|
+
for thread_id, rollout_path, tokens_used, agent_path in db.execute(
|
|
99
|
+
"SELECT id, rollout_path, tokens_used, agent_path FROM threads"
|
|
100
|
+
):
|
|
101
|
+
if not isinstance(thread_id, str):
|
|
102
|
+
raise TokenMetricsError("telemetry")
|
|
103
|
+
if not isinstance(agent_path, str) or not (agent_path == prefix or agent_path.startswith(prefix + "/")):
|
|
104
|
+
continue
|
|
105
|
+
total = _safe_count(tokens_used)
|
|
106
|
+
if total is None:
|
|
107
|
+
raise TokenMetricsError("telemetry")
|
|
108
|
+
result[thread_id] = {"total_tokens": total, **_rollout_usage(rollout_path)}
|
|
109
|
+
return result
|
|
110
|
+
except TokenMetricsError:
|
|
111
|
+
raise
|
|
112
|
+
except (OSError, sqlite3.Error):
|
|
113
|
+
raise TokenMetricsError("telemetry") from None
|
|
114
|
+
finally:
|
|
115
|
+
if db is not None:
|
|
116
|
+
db.close()
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def delta_usage(baseline: dict[str, dict[str, int | None]], snapshot: dict[str, dict[str, int | None]]) -> dict[str, int | None]:
|
|
120
|
+
if not set(baseline).issubset(snapshot):
|
|
121
|
+
raise TokenMetricsError("telemetry")
|
|
122
|
+
rows = []
|
|
123
|
+
for thread_id, current in snapshot.items():
|
|
124
|
+
previous = baseline.get(thread_id, _empty_usage())
|
|
125
|
+
total = int(current["total_tokens"]) - int(previous["total_tokens"])
|
|
126
|
+
if total < 0:
|
|
127
|
+
raise TokenMetricsError("telemetry")
|
|
128
|
+
detail: dict[str, int | None] = {"total_tokens": total}
|
|
129
|
+
for field in USAGE_FIELDS:
|
|
130
|
+
current_value, baseline_value = current[field], previous[field]
|
|
131
|
+
if current_value is None:
|
|
132
|
+
detail[field] = None
|
|
133
|
+
elif baseline_value is None:
|
|
134
|
+
detail[field] = current_value
|
|
135
|
+
else:
|
|
136
|
+
detail[field] = current_value - baseline_value
|
|
137
|
+
if detail[field] < 0:
|
|
138
|
+
raise TokenMetricsError("telemetry")
|
|
139
|
+
rows.append(detail)
|
|
140
|
+
result: dict[str, int | None] = {"total_tokens": sum(int(row["total_tokens"]) for row in rows)}
|
|
141
|
+
for field in USAGE_FIELDS:
|
|
142
|
+
result[field] = None if any(row[field] is None for row in rows) else sum(int(row[field]) for row in rows)
|
|
143
|
+
return result
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _total_usage(snapshot: dict[str, dict[str, int | None]]) -> dict[str, int | None]:
|
|
147
|
+
"""Aggregate provider totals without retaining any provider content."""
|
|
148
|
+
rows = list(snapshot.values())
|
|
149
|
+
result: dict[str, int | None] = {
|
|
150
|
+
"total_tokens": sum(int(row["total_tokens"]) for row in rows),
|
|
151
|
+
}
|
|
152
|
+
for field in USAGE_FIELDS:
|
|
153
|
+
result[field] = None if any(row[field] is None for row in rows) else sum(int(row[field]) for row in rows)
|
|
154
|
+
return result
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def _write_json(path: Path, payload: dict) -> None:
|
|
158
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
159
|
+
temporary = path.with_name(f".{path.name}.{os.getpid()}.{secrets.token_hex(4)}.tmp")
|
|
160
|
+
try:
|
|
161
|
+
temporary.write_text(json.dumps(payload, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
|
|
162
|
+
os.chmod(temporary, 0o600)
|
|
163
|
+
os.replace(temporary, path)
|
|
164
|
+
finally:
|
|
165
|
+
temporary.unlink(missing_ok=True)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _scope(options: dict) -> dict:
|
|
169
|
+
return {
|
|
170
|
+
"repository": str(options.get("repository", "unknown")),
|
|
171
|
+
"round": int(options.get("round", 0)),
|
|
172
|
+
"base": str(options.get("base", "unknown")),
|
|
173
|
+
"head": str(options.get("head", "unknown")),
|
|
174
|
+
"selected_files": int(options.get("selected_files", 0)),
|
|
175
|
+
"carried_files": int(options.get("carried_files", 0)),
|
|
176
|
+
"jobs": int(options.get("jobs", 0)),
|
|
177
|
+
"model": str(options.get("model", "unknown")),
|
|
178
|
+
"reasoning_effort": str(options.get("reasoning", "unknown")),
|
|
179
|
+
"reviewer_prefix": str(options.get("reviewer_prefix", "")),
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def _unavailable(path: Path, reason: str, scope: dict | None = None, db_path: str | Path | None = None) -> dict:
|
|
184
|
+
payload = {
|
|
185
|
+
"schema_version": 1,
|
|
186
|
+
"kind": "review_token_metrics",
|
|
187
|
+
"started_at": _now(),
|
|
188
|
+
"finalized_at": _now(),
|
|
189
|
+
"runtime_db": str(Path(db_path).expanduser().resolve()) if db_path else "",
|
|
190
|
+
"scope": scope or _scope({}),
|
|
191
|
+
"status": "unavailable",
|
|
192
|
+
"reason": reason,
|
|
193
|
+
}
|
|
194
|
+
_write_json(path, payload)
|
|
195
|
+
return payload
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def _valid_scope(scope: object) -> bool:
|
|
199
|
+
required = {"repository", "round", "base", "head", "selected_files", "carried_files", "jobs", "model", "reasoning_effort", "reviewer_prefix"}
|
|
200
|
+
if not isinstance(scope, dict) or set(scope) != required or not isinstance(scope["reviewer_prefix"], str):
|
|
201
|
+
return False
|
|
202
|
+
if not all(isinstance(scope[key], str) and bool(scope[key]) for key in ("repository", "base", "head", "model", "reasoning_effort")):
|
|
203
|
+
return False
|
|
204
|
+
return all(isinstance(scope[key], int) and scope[key] >= 0 for key in ("round", "selected_files", "carried_files", "jobs"))
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def _valid_metrics(value: object) -> bool:
|
|
208
|
+
if not isinstance(value, dict):
|
|
209
|
+
return False
|
|
210
|
+
if value.get("status") == "unavailable":
|
|
211
|
+
return (
|
|
212
|
+
set(value) == {"schema_version", "kind", "started_at", "finalized_at", "runtime_db", "scope", "status", "reason"}
|
|
213
|
+
and value["schema_version"] == 1
|
|
214
|
+
and value["kind"] == "review_token_metrics"
|
|
215
|
+
and _timestamp(value["started_at"])
|
|
216
|
+
and _timestamp(value["finalized_at"])
|
|
217
|
+
and isinstance(value["runtime_db"], str)
|
|
218
|
+
and _valid_scope(value["scope"])
|
|
219
|
+
and isinstance(value["reason"], str)
|
|
220
|
+
)
|
|
221
|
+
expected = {"schema_version", "kind", "started_at", "finalized_at", "runtime_db", "scope", "baseline_by_thread", "reviewer_thread_count", "checkpoints", "usage", "final_snapshot_by_thread", "final_usage", "status"}
|
|
222
|
+
if set(value) != expected or value["schema_version"] != 1 or value["kind"] != "review_token_metrics" or not _timestamp(value["started_at"]):
|
|
223
|
+
return False
|
|
224
|
+
if value["finalized_at"] is not None and not _timestamp(value["finalized_at"]):
|
|
225
|
+
return False
|
|
226
|
+
scope = value["scope"]
|
|
227
|
+
if not _valid_scope(scope):
|
|
228
|
+
return False
|
|
229
|
+
if value["status"] == "running":
|
|
230
|
+
final_valid = value["final_snapshot_by_thread"] is None and value["final_usage"] is None
|
|
231
|
+
else:
|
|
232
|
+
final_snapshot = value["final_snapshot_by_thread"]
|
|
233
|
+
final_valid = (
|
|
234
|
+
isinstance(final_snapshot, dict)
|
|
235
|
+
and all(_usage(row) for row in final_snapshot.values())
|
|
236
|
+
and _usage(value["final_usage"])
|
|
237
|
+
)
|
|
238
|
+
if final_valid:
|
|
239
|
+
try:
|
|
240
|
+
final_valid = (
|
|
241
|
+
delta_usage(value["baseline_by_thread"], final_snapshot) == value["usage"]
|
|
242
|
+
and _total_usage(final_snapshot) == value["final_usage"]
|
|
243
|
+
)
|
|
244
|
+
except (KeyError, TypeError, TokenMetricsError):
|
|
245
|
+
final_valid = False
|
|
246
|
+
return (
|
|
247
|
+
isinstance(value["runtime_db"], str)
|
|
248
|
+
and isinstance(value["baseline_by_thread"], dict)
|
|
249
|
+
and all(_usage(row) for row in value["baseline_by_thread"].values())
|
|
250
|
+
and isinstance(value["reviewer_thread_count"], int)
|
|
251
|
+
and isinstance(value["checkpoints"], list)
|
|
252
|
+
and all(isinstance(row, dict) and set(row) == {"recorded_at", "completed_jobs", "usage"} and _timestamp(row["recorded_at"]) and isinstance(row["completed_jobs"], int) and row["completed_jobs"] >= 0 and _usage(row["usage"]) for row in value["checkpoints"])
|
|
253
|
+
and _usage(value["usage"])
|
|
254
|
+
and value["status"] in {"running", "complete"}
|
|
255
|
+
and (value["status"] == "running" or value["finalized_at"] is not None)
|
|
256
|
+
and final_valid
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def read_metrics(path: str | Path) -> dict:
|
|
261
|
+
try:
|
|
262
|
+
value = json.loads(Path(path).read_text(encoding="utf-8"))
|
|
263
|
+
except (OSError, json.JSONDecodeError):
|
|
264
|
+
raise TokenMetricsError("ledger") from None
|
|
265
|
+
if not _valid_metrics(value):
|
|
266
|
+
raise TokenMetricsError("ledger")
|
|
267
|
+
return value
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def start_metrics(path: str | Path, db_path: str | Path | None, reviewer_prefix: str | None, **options) -> dict:
|
|
271
|
+
state_path = Path(path).expanduser()
|
|
272
|
+
scope = _scope({**options, "reviewer_prefix": reviewer_prefix or ""})
|
|
273
|
+
if state_path.exists():
|
|
274
|
+
try:
|
|
275
|
+
metrics = read_metrics(state_path)
|
|
276
|
+
except TokenMetricsError:
|
|
277
|
+
return _unavailable(state_path, "persisted metrics unavailable", scope, db_path)
|
|
278
|
+
if metrics["status"] == "unavailable":
|
|
279
|
+
return metrics
|
|
280
|
+
if metrics["scope"] != scope or metrics["runtime_db"] != str(Path(db_path).expanduser().resolve() if db_path else ""):
|
|
281
|
+
return _unavailable(state_path, "metrics scope unavailable", scope, db_path)
|
|
282
|
+
if metrics["status"] == "complete":
|
|
283
|
+
return metrics
|
|
284
|
+
return validate_metrics(state_path)
|
|
285
|
+
if not db_path or not reviewer_prefix:
|
|
286
|
+
return _unavailable(state_path, "compatible telemetry unavailable", scope, db_path)
|
|
287
|
+
try:
|
|
288
|
+
snapshot = read_telemetry(db_path, reviewer_prefix)
|
|
289
|
+
except TokenMetricsError:
|
|
290
|
+
return _unavailable(state_path, "compatible telemetry unavailable", scope, db_path)
|
|
291
|
+
metrics = {
|
|
292
|
+
"schema_version": 1,
|
|
293
|
+
"kind": "review_token_metrics",
|
|
294
|
+
"started_at": _now(),
|
|
295
|
+
"finalized_at": None,
|
|
296
|
+
"runtime_db": str(Path(db_path).expanduser().resolve()),
|
|
297
|
+
"scope": scope,
|
|
298
|
+
"baseline_by_thread": snapshot,
|
|
299
|
+
"reviewer_thread_count": len(snapshot),
|
|
300
|
+
"checkpoints": [],
|
|
301
|
+
"usage": _empty_usage(),
|
|
302
|
+
"final_snapshot_by_thread": None,
|
|
303
|
+
"final_usage": None,
|
|
304
|
+
"status": "running",
|
|
305
|
+
}
|
|
306
|
+
_write_json(state_path, metrics)
|
|
307
|
+
return metrics
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def _measure(path: Path, metrics: dict) -> dict:
|
|
311
|
+
if metrics["status"] != "running":
|
|
312
|
+
return metrics
|
|
313
|
+
try:
|
|
314
|
+
snapshot = read_telemetry(metrics["runtime_db"], metrics["scope"]["reviewer_prefix"])
|
|
315
|
+
usage = delta_usage(metrics["baseline_by_thread"], snapshot)
|
|
316
|
+
if usage["total_tokens"] < metrics["usage"]["total_tokens"]:
|
|
317
|
+
raise TokenMetricsError("telemetry")
|
|
318
|
+
except TokenMetricsError:
|
|
319
|
+
return _unavailable(path, "runtime telemetry unavailable", metrics["scope"], metrics["runtime_db"])
|
|
320
|
+
return {**metrics, "reviewer_thread_count": len(snapshot), "usage": usage}
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
def validate_metrics(path: str | Path) -> dict:
|
|
324
|
+
state_path = Path(path)
|
|
325
|
+
metrics = read_metrics(state_path)
|
|
326
|
+
measured = _measure(state_path, metrics)
|
|
327
|
+
if measured is not metrics:
|
|
328
|
+
_write_json(state_path, measured)
|
|
329
|
+
return measured
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
def checkpoint_metrics(path: str | Path, completed_jobs: int) -> dict:
|
|
333
|
+
state_path = Path(path)
|
|
334
|
+
metrics = read_metrics(state_path)
|
|
335
|
+
measured = _measure(state_path, metrics)
|
|
336
|
+
if measured["status"] == "unavailable":
|
|
337
|
+
return measured
|
|
338
|
+
checkpoint = {"recorded_at": _now(), "completed_jobs": max(0, int(completed_jobs)), "usage": measured["usage"]}
|
|
339
|
+
next_metrics = {**measured, "checkpoints": [*measured["checkpoints"], checkpoint]}
|
|
340
|
+
_write_json(state_path, next_metrics)
|
|
341
|
+
return next_metrics
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
def finalize_metrics(path: str | Path) -> dict:
|
|
345
|
+
state_path = Path(path)
|
|
346
|
+
metrics = read_metrics(state_path)
|
|
347
|
+
if metrics["status"] in {"complete", "unavailable"}:
|
|
348
|
+
return metrics
|
|
349
|
+
measured = _measure(state_path, metrics)
|
|
350
|
+
if measured["status"] == "unavailable":
|
|
351
|
+
return measured
|
|
352
|
+
try:
|
|
353
|
+
final_snapshot = read_telemetry(measured["runtime_db"], measured["scope"]["reviewer_prefix"])
|
|
354
|
+
final_usage = _total_usage(final_snapshot)
|
|
355
|
+
except TokenMetricsError:
|
|
356
|
+
return _unavailable(state_path, "runtime telemetry unavailable", measured["scope"], measured["runtime_db"])
|
|
357
|
+
finalized = {
|
|
358
|
+
**measured,
|
|
359
|
+
"finalized_at": _now(),
|
|
360
|
+
"final_snapshot_by_thread": final_snapshot,
|
|
361
|
+
"final_usage": final_usage,
|
|
362
|
+
"status": "complete",
|
|
363
|
+
}
|
|
364
|
+
_write_json(state_path, finalized)
|
|
365
|
+
return finalized
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
def write_unavailable_metrics(path: str | Path, reason: str = "compatible telemetry unavailable") -> None:
|
|
369
|
+
_unavailable(Path(path), reason)
|