@event4u/agent-config 8.4.0 → 8.4.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/.claude-plugin/marketplace.json +1 -1
- package/CHANGELOG.md +6 -0
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +1 -1
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +1 -1
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +1 -1
- package/dist/discovery/trust-report.md +1 -1
- package/dist/discovery/workspaces.json +1 -1
- package/dist/mcp/registry-manifest.json +1 -1
- package/package.json +4 -1
- package/src/agent-src/README.md +64 -0
- package/src/agent-src/scripts/archive_completed_roadmaps.ts +497 -0
- package/src/agent-src/scripts/scan-seeder-violations.php +145 -0
- package/src/agent-src/scripts/update_roadmap_progress.ts +1087 -0
- package/src/agent-src/templates/AGENTS.md +34 -0
- package/src/agent-src/templates/scripts/README.md +195 -0
- package/src/agent-src/templates/scripts/check_memory.ts +662 -0
- package/src/agent-src/templates/scripts/check_memory_proposal.ts +370 -0
- package/src/agent-src/templates/scripts/historical-bug-patterns.example.yml +84 -0
- package/src/agent-src/templates/scripts/implement_ticket/__main__.ts +27 -0
- package/src/agent-src/templates/scripts/memory_hash.ts +355 -0
- package/src/agent-src/templates/scripts/memory_lookup.ts +1089 -0
- package/src/agent-src/templates/scripts/memory_report.ts +868 -0
- package/src/agent-src/templates/scripts/memory_signal.ts +444 -0
- package/src/agent-src/templates/scripts/memory_status.ts +215 -0
- package/src/agent-src/templates/scripts/ownership-map.example.yml +87 -0
- package/src/agent-src/templates/scripts/pr-risk-config.example.yml +76 -0
- package/src/agent-src/templates/scripts/pr_review_routing.ts +544 -0
- package/src/agent-src/templates/scripts/pr_risk_review.ts +0 -0
- package/src/agent-src/templates/scripts/telemetry/aggregator.ts +0 -0
- package/src/agent-src/templates/scripts/telemetry/boundary.ts +164 -0
- package/src/agent-src/templates/scripts/telemetry/engagement.ts +479 -0
- package/src/agent-src/templates/scripts/telemetry/report_renderer.ts +394 -0
- package/src/agent-src/templates/scripts/telemetry/settings.ts +210 -0
- package/src/agent-src/templates/scripts/telemetry_record.ts +276 -0
- package/src/agent-src/templates/scripts/telemetry_report.ts +211 -0
- package/src/agent-src/templates/scripts/telemetry_status.ts +333 -0
- package/src/agent-src/templates/scripts/tier_usage_report.ts +619 -0
- package/src/agent-src/templates/scripts/work_engine/__main__.ts +14 -0
- package/src/agent-src/templates/scripts/work_engine/_lib/agent_settings.ts +1261 -0
- package/src/agent-src/templates/scripts/work_engine/_lib/user_global_paths.ts +329 -0
- package/src/agent-src/templates/scripts/work_engine/cli.ts +206 -0
- package/src/agent-src/templates/scripts/work_engine/cli_args.ts +249 -0
- package/src/agent-src/templates/scripts/work_engine/delivery_state.ts +225 -0
- package/src/agent-src/templates/scripts/work_engine/directives/backend/analyze.ts +125 -0
- package/src/agent-src/templates/scripts/work_engine/directives/backend/implement.ts +189 -0
- package/src/agent-src/templates/scripts/work_engine/directives/backend/index.ts +94 -0
- package/src/agent-src/templates/scripts/work_engine/directives/backend/memory.ts +193 -0
- package/src/agent-src/templates/scripts/work_engine/directives/backend/plan.ts +267 -0
- package/src/agent-src/templates/scripts/work_engine/directives/backend/refine.ts +518 -0
- package/src/agent-src/templates/scripts/work_engine/directives/backend/report.ts +379 -0
- package/src/agent-src/templates/scripts/work_engine/directives/backend/test.ts +268 -0
- package/src/agent-src/templates/scripts/work_engine/directives/backend/verify.ts +258 -0
- package/src/agent-src/templates/scripts/work_engine/directives/index.ts +32 -0
- package/src/agent-src/templates/scripts/work_engine/directives/mixed/contract.ts +243 -0
- package/src/agent-src/templates/scripts/work_engine/directives/mixed/index.ts +108 -0
- package/src/agent-src/templates/scripts/work_engine/directives/mixed/stitch.ts +259 -0
- package/src/agent-src/templates/scripts/work_engine/directives/mixed/ui.ts +216 -0
- package/src/agent-src/templates/scripts/work_engine/directives/ui/_passthrough.ts +40 -0
- package/src/agent-src/templates/scripts/work_engine/directives/ui/app_spec.ts +241 -0
- package/src/agent-src/templates/scripts/work_engine/directives/ui/apply.ts +216 -0
- package/src/agent-src/templates/scripts/work_engine/directives/ui/audit.ts +506 -0
- package/src/agent-src/templates/scripts/work_engine/directives/ui/design.ts +325 -0
- package/src/agent-src/templates/scripts/work_engine/directives/ui/index.ts +102 -0
- package/src/agent-src/templates/scripts/work_engine/directives/ui/polish.ts +462 -0
- package/src/agent-src/templates/scripts/work_engine/directives/ui/review.ts +474 -0
- package/src/agent-src/templates/scripts/work_engine/directives/ui/scaffold.ts +352 -0
- package/src/agent-src/templates/scripts/work_engine/directives/ui_trivial/_skipped.ts +33 -0
- package/src/agent-src/templates/scripts/work_engine/directives/ui_trivial/apply.ts +213 -0
- package/src/agent-src/templates/scripts/work_engine/directives/ui_trivial/index.ts +111 -0
- package/src/agent-src/templates/scripts/work_engine/directives/ui_trivial/refine.ts +126 -0
- package/src/agent-src/templates/scripts/work_engine/directives/ui_trivial/report.ts +112 -0
- package/src/agent-src/templates/scripts/work_engine/directives/ui_trivial/test.ts +164 -0
- package/src/agent-src/templates/scripts/work_engine/dispatcher.ts +515 -0
- package/src/agent-src/templates/scripts/work_engine/emitters.ts +119 -0
- package/src/agent-src/templates/scripts/work_engine/errors.ts +24 -0
- package/src/agent-src/templates/scripts/work_engine/hook_bootstrap.ts +104 -0
- package/src/agent-src/templates/scripts/work_engine/hooks/builtin/_chat_history_base.ts +189 -0
- package/src/agent-src/templates/scripts/work_engine/hooks/builtin/chat_history_append.ts +41 -0
- package/src/agent-src/templates/scripts/work_engine/hooks/builtin/chat_history_halt_append.ts +89 -0
- package/src/agent-src/templates/scripts/work_engine/hooks/builtin/decision_gate.ts +193 -0
- package/src/agent-src/templates/scripts/work_engine/hooks/builtin/decision_trace.ts +304 -0
- package/src/agent-src/templates/scripts/work_engine/hooks/builtin/directive_set_guard.ts +110 -0
- package/src/agent-src/templates/scripts/work_engine/hooks/builtin/halt_surface_audit.ts +118 -0
- package/src/agent-src/templates/scripts/work_engine/hooks/builtin/index.ts +17 -0
- package/src/agent-src/templates/scripts/work_engine/hooks/builtin/memory_visibility.ts +161 -0
- package/src/agent-src/templates/scripts/work_engine/hooks/builtin/state_shape_validation.ts +45 -0
- package/src/agent-src/templates/scripts/work_engine/hooks/builtin/trace.ts +134 -0
- package/src/agent-src/templates/scripts/work_engine/hooks/context.ts +94 -0
- package/src/agent-src/templates/scripts/work_engine/hooks/events.ts +58 -0
- package/src/agent-src/templates/scripts/work_engine/hooks/exceptions.ts +85 -0
- package/src/agent-src/templates/scripts/work_engine/hooks/index.ts +27 -0
- package/src/agent-src/templates/scripts/work_engine/hooks/registry.ts +66 -0
- package/src/agent-src/templates/scripts/work_engine/hooks/runner.ts +90 -0
- package/src/agent-src/templates/scripts/work_engine/hooks/settings.ts +260 -0
- package/src/agent-src/templates/scripts/work_engine/input_builders.ts +260 -0
- package/src/agent-src/templates/scripts/work_engine/intent/classify.ts +466 -0
- package/src/agent-src/templates/scripts/work_engine/migration/v0_to_v1.ts +531 -0
- package/src/agent-src/templates/scripts/work_engine/orchestration.ts +366 -0
- package/src/agent-src/templates/scripts/work_engine/persona_policy.ts +97 -0
- package/src/agent-src/templates/scripts/work_engine/resolvers/diff.ts +135 -0
- package/src/agent-src/templates/scripts/work_engine/resolvers/file.ts +175 -0
- package/src/agent-src/templates/scripts/work_engine/resolvers/prompt.ts +115 -0
- package/src/agent-src/templates/scripts/work_engine/scoring/confidence.ts +415 -0
- package/src/agent-src/templates/scripts/work_engine/scoring/decision_engine.ts +466 -0
- package/src/agent-src/templates/scripts/work_engine/scoring/decision_trace.ts +298 -0
- package/src/agent-src/templates/scripts/work_engine/scoring/memory_visibility.ts +444 -0
- package/src/agent-src/templates/scripts/work_engine/stack/detect.ts +252 -0
- package/src/agent-src/templates/scripts/work_engine/stack/runner.ts +745 -0
- package/src/agent-src/templates/scripts/work_engine/state.ts +1151 -0
- package/src/agent-src/templates/scripts/work_engine/state_io.ts +413 -0
- package/src/install/atomic.ts +111 -0
- package/src/install/conflict.ts +226 -0
- package/src/install/detect.ts +280 -0
- package/src/install/paths.ts +90 -0
- package/src/install/plan.ts +205 -0
- package/src/install/preflight.ts +210 -0
- package/src/install/selectedTools.ts +52 -0
- package/src/install/toolDetection.ts +120 -0
- package/src/install/txlog.ts +161 -0
- package/src/install/types.ts +175 -0
- package/src/install/wizard-plan.ts +216 -0
- package/src/scripts/prepack-check.mjs +108 -5
|
@@ -0,0 +1,619 @@
|
|
|
1
|
+
#!/usr/bin/env tsx
|
|
2
|
+
/**
|
|
3
|
+
* Tier-usage report — aggregate the local tier-usage log into a frequency table.
|
|
4
|
+
*
|
|
5
|
+
* TypeScript twin of `src/agent-src/templates/scripts/tier_usage_report.py`
|
|
6
|
+
* (ADR-094, consumer-template surface-discipline telemetry). The CLI
|
|
7
|
+
* contract mirrors the Python original EXACTLY — same exit codes,
|
|
8
|
+
* stdout/stderr split, byte-identical messages, byte-identical table +
|
|
9
|
+
* `--json` output (matching json.dumps(indent=2)). No behaviour changes —
|
|
10
|
+
* latent Python bugs are replicated and flagged as divergence candidates.
|
|
11
|
+
*
|
|
12
|
+
* Phase 5 Step 3 of road-to-surface-discipline. Reads the JSONL log
|
|
13
|
+
* written by the dispatcher (default `.agent-tier-usage.jsonl`; override
|
|
14
|
+
* via `telemetry.tier_usage.output.path`) and emits a per-command
|
|
15
|
+
* frequency table grouped by tier, plus distinct `user_hash` counts.
|
|
16
|
+
* Run-local-only; no upload, no remote aggregation.
|
|
17
|
+
*
|
|
18
|
+
* Privacy floor mirrors the contract in
|
|
19
|
+
* `docs/contracts/command-clusters.md#tier-usage-signal-contract` and
|
|
20
|
+
* the four-layer enforcement model used by artefact-engagement telemetry.
|
|
21
|
+
* Records that carry any field outside the contract whitelist are dropped
|
|
22
|
+
* at the read gate — the report refuses to render leaked shapes rather
|
|
23
|
+
* than re-emit them.
|
|
24
|
+
*
|
|
25
|
+
* The `telemetry.settings` reader is inlined here (the `read_tier_usage_settings`
|
|
26
|
+
* slice + `DEFAULT_TIER_USAGE_LOG_PATH`) rather than imported across the
|
|
27
|
+
* still-unported `telemetry/settings.py` boundary — a `.ts` twin must not
|
|
28
|
+
* import a `.py`. The inlined coercion mirrors the Python source exactly.
|
|
29
|
+
*
|
|
30
|
+
* Usage:
|
|
31
|
+
* tier_usage_report # last 30d, table
|
|
32
|
+
* tier_usage_report --window-days 7 # last 7d
|
|
33
|
+
* tier_usage_report --window-days 0 # full log
|
|
34
|
+
* tier_usage_report --json # JSON for tooling
|
|
35
|
+
* tier_usage_report --log-path X.jsonl # archived snapshot
|
|
36
|
+
*
|
|
37
|
+
* Exit codes:
|
|
38
|
+
* 0 success or telemetry disabled (single header line)
|
|
39
|
+
* 1 no records survived the privacy floor on a non-empty file
|
|
40
|
+
* 2 IO error (permission denied; passed path missing)
|
|
41
|
+
*/
|
|
42
|
+
|
|
43
|
+
import * as fs from 'node:fs';
|
|
44
|
+
import * as path from 'node:path';
|
|
45
|
+
import process from 'node:process';
|
|
46
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
47
|
+
import { parse as parseYaml } from 'yaml';
|
|
48
|
+
|
|
49
|
+
// Contract whitelist (see `docs/contracts/command-clusters.md`).
|
|
50
|
+
const ALLOWED_FIELDS: ReadonlySet<string> = new Set(['ts_bucket', 'command', 'tier', 'outcome', 'user_hash']);
|
|
51
|
+
const ALLOWED_OUTCOMES: ReadonlySet<string> = new Set(['success', 'error', 'blocked']);
|
|
52
|
+
|
|
53
|
+
// Defaults for the tier-usage signal (mirrors telemetry/settings.py).
|
|
54
|
+
const DEFAULT_TIER_USAGE_LOG_PATH = '.agent-tier-usage.jsonl';
|
|
55
|
+
const DEFAULT_TIER_USAGE_RETIER = { window_days: 30, min_invocations: 20, min_distinct_users: 3 };
|
|
56
|
+
|
|
57
|
+
interface TierUsageSettings {
|
|
58
|
+
enabled: boolean;
|
|
59
|
+
log_path: string;
|
|
60
|
+
window_days: number;
|
|
61
|
+
min_invocations: number;
|
|
62
|
+
min_distinct_users: number;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function _isPlainObject(v: unknown): v is Record<string, unknown> {
|
|
66
|
+
return typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Mirror telemetry.settings._coerce_bool. */
|
|
70
|
+
function _coerce_bool(value: unknown, def: boolean): boolean {
|
|
71
|
+
if (typeof value === 'boolean') {
|
|
72
|
+
return value;
|
|
73
|
+
}
|
|
74
|
+
if (typeof value === 'string') {
|
|
75
|
+
const normalised = value.trim().toLowerCase();
|
|
76
|
+
if (['true', 'yes', 'on', '1'].includes(normalised)) {
|
|
77
|
+
return true;
|
|
78
|
+
}
|
|
79
|
+
if (['false', 'no', 'off', '0'].includes(normalised)) {
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return def;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Mirror telemetry.settings._coerce_path. */
|
|
87
|
+
function _coerce_path(value: unknown, def: string): string {
|
|
88
|
+
if (typeof value !== 'string' || !value.trim()) {
|
|
89
|
+
return def;
|
|
90
|
+
}
|
|
91
|
+
return value.trim();
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Mirror telemetry.settings.read_tier_usage_settings (the slice this report uses). */
|
|
95
|
+
function read_tier_usage_settings(p: string): TierUsageSettings {
|
|
96
|
+
let section: Record<string, unknown> = {};
|
|
97
|
+
if (_isFile(p)) {
|
|
98
|
+
// The `yaml` npm dependency is always present; the ImportError branch
|
|
99
|
+
// (PyYAML absent → all-defaults) is unreachable in the TS twin.
|
|
100
|
+
let raw: unknown = {};
|
|
101
|
+
try {
|
|
102
|
+
raw = parseYaml(fs.readFileSync(p, 'utf-8')) ?? {};
|
|
103
|
+
} catch {
|
|
104
|
+
raw = {};
|
|
105
|
+
}
|
|
106
|
+
if (_isPlainObject(raw)) {
|
|
107
|
+
const tele = raw['telemetry'];
|
|
108
|
+
if (_isPlainObject(tele)) {
|
|
109
|
+
const tu = tele['tier_usage'];
|
|
110
|
+
if (_isPlainObject(tu)) {
|
|
111
|
+
section = tu;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
const output = _isPlainObject(section['output']) ? (section['output'] as Record<string, unknown>) : {};
|
|
117
|
+
const retier = _isPlainObject(section['retier']) ? (section['retier'] as Record<string, unknown>) : {};
|
|
118
|
+
const defaults = DEFAULT_TIER_USAGE_RETIER;
|
|
119
|
+
|
|
120
|
+
const _coerce_int = (value: unknown, def: number): number => {
|
|
121
|
+
if (typeof value === 'boolean') {
|
|
122
|
+
return def;
|
|
123
|
+
}
|
|
124
|
+
if (typeof value === 'number' && Number.isInteger(value) && value >= 0) {
|
|
125
|
+
return value;
|
|
126
|
+
}
|
|
127
|
+
return def;
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
return {
|
|
131
|
+
enabled: _coerce_bool(section['enabled'], false),
|
|
132
|
+
log_path: _coerce_path(output['path'], DEFAULT_TIER_USAGE_LOG_PATH),
|
|
133
|
+
window_days: _coerce_int(retier['window_days'], defaults.window_days),
|
|
134
|
+
min_invocations: _coerce_int(retier['min_invocations'], defaults.min_invocations),
|
|
135
|
+
min_distinct_users: _coerce_int(retier['min_distinct_users'], defaults.min_distinct_users),
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function _isFile(p: string): boolean {
|
|
140
|
+
try {
|
|
141
|
+
return fs.statSync(p).isFile();
|
|
142
|
+
} catch {
|
|
143
|
+
return false;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
interface SanitizedRecord {
|
|
148
|
+
ts_bucket: string;
|
|
149
|
+
command: string;
|
|
150
|
+
tier: number;
|
|
151
|
+
outcome: string;
|
|
152
|
+
user_hash: string;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** Return a sanitized record or null when the line violates the floor. */
|
|
156
|
+
function _parse_record(raw: string): SanitizedRecord | null {
|
|
157
|
+
let obj: unknown;
|
|
158
|
+
try {
|
|
159
|
+
obj = JSON.parse(raw);
|
|
160
|
+
} catch {
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
if (!_isPlainObject(obj)) {
|
|
164
|
+
return null;
|
|
165
|
+
}
|
|
166
|
+
for (const k of Object.keys(obj)) {
|
|
167
|
+
if (!ALLOWED_FIELDS.has(k)) {
|
|
168
|
+
return null;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
const cmd = obj['command'];
|
|
172
|
+
if (typeof cmd !== 'string' || !cmd || cmd.includes('/') || cmd.includes('\\')) {
|
|
173
|
+
return null;
|
|
174
|
+
}
|
|
175
|
+
const tier = obj['tier'];
|
|
176
|
+
if (typeof tier !== 'number' || !Number.isInteger(tier) || typeof tier === 'boolean' || ![0, 1, 2, 3].includes(tier)) {
|
|
177
|
+
return null;
|
|
178
|
+
}
|
|
179
|
+
if (typeof obj['outcome'] !== 'string' || !ALLOWED_OUTCOMES.has(obj['outcome'])) {
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
const uh = obj['user_hash'];
|
|
183
|
+
if (typeof uh !== 'string' || uh.length !== 16) {
|
|
184
|
+
return null;
|
|
185
|
+
}
|
|
186
|
+
if (typeof obj['ts_bucket'] !== 'string') {
|
|
187
|
+
return null;
|
|
188
|
+
}
|
|
189
|
+
return obj as unknown as SanitizedRecord;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** Mirror datetime.fromisoformat(ts_bucket.replace("Z","+00:00")) → ms epoch, or null. */
|
|
193
|
+
function _parseIso(ts: string): number | null {
|
|
194
|
+
const s = ts.replace('Z', '+00:00');
|
|
195
|
+
// datetime.fromisoformat (3.10): "YYYY-MM-DD[THH:MM:SS[.ffffff]][+HH:MM]".
|
|
196
|
+
const m =
|
|
197
|
+
/^(\d{4})-(\d{2})-(\d{2})(?:[ T](\d{2}):(\d{2}):(\d{2})(?:\.(\d{1,6}))?(?:([+-]\d{2}:\d{2}))?)?$/.exec(s);
|
|
198
|
+
if (!m) {
|
|
199
|
+
return null;
|
|
200
|
+
}
|
|
201
|
+
const [, y, mo, d, h, mi, sec, frac, tz] = m;
|
|
202
|
+
let ms = Date.UTC(
|
|
203
|
+
Number(y),
|
|
204
|
+
Number(mo) - 1,
|
|
205
|
+
Number(d),
|
|
206
|
+
h ? Number(h) : 0,
|
|
207
|
+
mi ? Number(mi) : 0,
|
|
208
|
+
sec ? Number(sec) : 0,
|
|
209
|
+
frac ? Number((frac + '000000').slice(0, 6)) / 1000 : 0,
|
|
210
|
+
);
|
|
211
|
+
let hadOffset = false;
|
|
212
|
+
if (tz) {
|
|
213
|
+
hadOffset = true;
|
|
214
|
+
const sign = tz[0] === '-' ? -1 : 1;
|
|
215
|
+
const oh = Number(tz.slice(1, 3));
|
|
216
|
+
const om = Number(tz.slice(4, 6));
|
|
217
|
+
ms -= sign * (oh * 60 + om) * 60 * 1000;
|
|
218
|
+
}
|
|
219
|
+
// Python: a naive ts (no tzinfo) gets .replace(tzinfo=utc); offsetless input
|
|
220
|
+
// is therefore treated as UTC. Date.UTC already did that, so `hadOffset`
|
|
221
|
+
// distinction is not needed for the comparison value.
|
|
222
|
+
void hadOffset;
|
|
223
|
+
return ms;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function _within_window(ts_bucket: string, window_days: number | null): boolean {
|
|
227
|
+
if (window_days === null || window_days === 0) {
|
|
228
|
+
return true;
|
|
229
|
+
}
|
|
230
|
+
const ms = _parseIso(ts_bucket);
|
|
231
|
+
if (ms === null) {
|
|
232
|
+
return false;
|
|
233
|
+
}
|
|
234
|
+
return ms >= Date.now() - window_days * 24 * 60 * 60 * 1000;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
type BucketKey = string; // `${tier}${command}` — composite (tier, command) key.
|
|
238
|
+
interface RowStats {
|
|
239
|
+
count: number;
|
|
240
|
+
distinct_users: number;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
interface AggregateResult {
|
|
244
|
+
table: Map<BucketKey, RowStats>;
|
|
245
|
+
keyMeta: Map<BucketKey, { tier: number; command: string }>;
|
|
246
|
+
total: number;
|
|
247
|
+
kept: number;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/** Return ((tier, command) -> stats, total_lines, kept) over the window. */
|
|
251
|
+
function aggregate(log_path: string, window_days: number): AggregateResult {
|
|
252
|
+
const counts = new Map<BucketKey, number>();
|
|
253
|
+
const users = new Map<BucketKey, Set<string>>();
|
|
254
|
+
const keyMeta = new Map<BucketKey, { tier: number; command: string }>();
|
|
255
|
+
let total = 0;
|
|
256
|
+
let kept = 0;
|
|
257
|
+
if (!_exists(log_path)) {
|
|
258
|
+
return { table: new Map(), keyMeta: new Map(), total: 0, kept: 0 };
|
|
259
|
+
}
|
|
260
|
+
// Python opens the file; a permission error raises OSError → caller exit 2.
|
|
261
|
+
const text = fs.readFileSync(log_path, 'utf-8');
|
|
262
|
+
for (const rawLine of text.split('\n')) {
|
|
263
|
+
const line = rawLine.trim();
|
|
264
|
+
if (!line) {
|
|
265
|
+
continue;
|
|
266
|
+
}
|
|
267
|
+
total += 1;
|
|
268
|
+
const rec = _parse_record(line);
|
|
269
|
+
if (rec === null) {
|
|
270
|
+
continue;
|
|
271
|
+
}
|
|
272
|
+
if (!_within_window(rec.ts_bucket, window_days)) {
|
|
273
|
+
continue;
|
|
274
|
+
}
|
|
275
|
+
kept += 1;
|
|
276
|
+
const tier = Math.trunc(rec.tier);
|
|
277
|
+
const key = `${tier}${rec.command}`;
|
|
278
|
+
if (!keyMeta.has(key)) {
|
|
279
|
+
keyMeta.set(key, { tier, command: rec.command });
|
|
280
|
+
counts.set(key, 0);
|
|
281
|
+
users.set(key, new Set());
|
|
282
|
+
}
|
|
283
|
+
counts.set(key, (counts.get(key) ?? 0) + 1);
|
|
284
|
+
(users.get(key) as Set<string>).add(rec.user_hash);
|
|
285
|
+
}
|
|
286
|
+
const table = new Map<BucketKey, RowStats>();
|
|
287
|
+
for (const [k, c] of counts) {
|
|
288
|
+
table.set(k, { count: c, distinct_users: (users.get(k) as Set<string>).size });
|
|
289
|
+
}
|
|
290
|
+
return { table, keyMeta, total, kept };
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function _exists(p: string): boolean {
|
|
294
|
+
try {
|
|
295
|
+
fs.statSync(p);
|
|
296
|
+
return true;
|
|
297
|
+
} catch {
|
|
298
|
+
return false;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/** Left-pad to width with spaces (mirror f"{s:<width}"). */
|
|
303
|
+
function _ljust(s: string, width: number): string {
|
|
304
|
+
return s.length >= width ? s : s + ' '.repeat(width - s.length);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/** Right-justify to width with spaces (mirror f"{s:>width}"). */
|
|
308
|
+
function _rjust(s: string, width: number): string {
|
|
309
|
+
return s.length >= width ? s : ' '.repeat(width - s.length) + s;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function render(result: AggregateResult, window_days: number): string {
|
|
313
|
+
const suffix = window_days ? ` (last ${window_days}d)` : ' (full log)';
|
|
314
|
+
if (result.table.size === 0) {
|
|
315
|
+
return `(no tier-usage records${suffix})\n`;
|
|
316
|
+
}
|
|
317
|
+
// sorted by (tier, -count, command)
|
|
318
|
+
const rows = [...result.table.entries()].sort((a, b) => {
|
|
319
|
+
const ma = result.keyMeta.get(a[0]) as { tier: number; command: string };
|
|
320
|
+
const mb = result.keyMeta.get(b[0]) as { tier: number; command: string };
|
|
321
|
+
if (ma.tier !== mb.tier) {
|
|
322
|
+
return ma.tier - mb.tier;
|
|
323
|
+
}
|
|
324
|
+
if (b[1].count !== a[1].count) {
|
|
325
|
+
return b[1].count - a[1].count;
|
|
326
|
+
}
|
|
327
|
+
return ma.command < mb.command ? -1 : ma.command > mb.command ? 1 : 0;
|
|
328
|
+
});
|
|
329
|
+
const header = `${_ljust('Tier', 6)}${_ljust('Command', 32)}${_rjust('Calls', 8)}${_rjust('Users', 8)}`;
|
|
330
|
+
const lines: string[] = [header, '-'.repeat(header.length)];
|
|
331
|
+
for (const [key, stats] of rows) {
|
|
332
|
+
const meta = result.keyMeta.get(key) as { tier: number; command: string };
|
|
333
|
+
lines.push(
|
|
334
|
+
`${_ljust(String(meta.tier), 6)}${_ljust(meta.command, 32)}${_rjust(String(stats.count), 8)}${_rjust(
|
|
335
|
+
String(stats.distinct_users),
|
|
336
|
+
8,
|
|
337
|
+
)}`,
|
|
338
|
+
);
|
|
339
|
+
}
|
|
340
|
+
lines.push(`\n(window:${suffix.trim()})`);
|
|
341
|
+
return lines.join('\n') + '\n';
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
interface ParsedArgs {
|
|
345
|
+
window_days: number;
|
|
346
|
+
json: boolean;
|
|
347
|
+
log_path: string | null;
|
|
348
|
+
settings_file: string;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
const _PROG = 'tier_usage_report.py';
|
|
352
|
+
const _USAGE =
|
|
353
|
+
'usage: tier_usage_report.py [-h] [--window-days WINDOW_DAYS] [--json]\n' +
|
|
354
|
+
' [--log-path LOG_PATH]\n' +
|
|
355
|
+
' [--settings-file SETTINGS_FILE]\n';
|
|
356
|
+
|
|
357
|
+
/** Mirror argparse error: usage + "<prog>: error: <msg>" to stderr, exit 2. */
|
|
358
|
+
function _argError(msg: string): never {
|
|
359
|
+
process.stderr.write(_USAGE);
|
|
360
|
+
process.stderr.write(`${_PROG}: error: ${msg}\n`);
|
|
361
|
+
process.exit(2);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/** Mirror argparse `type=int` coercion + its error message. */
|
|
365
|
+
function _parseIntArg(value: string | undefined, flag: string): number {
|
|
366
|
+
if (value === undefined) {
|
|
367
|
+
_argError(`argument ${flag}: expected one argument`);
|
|
368
|
+
}
|
|
369
|
+
// Python int() accepts surrounding whitespace and a leading sign.
|
|
370
|
+
const trimmed = (value as string).trim();
|
|
371
|
+
if (!/^[+-]?\d+$/.test(trimmed)) {
|
|
372
|
+
_argError(`argument ${flag}: invalid int value: '${value as string}'`);
|
|
373
|
+
}
|
|
374
|
+
return Number(trimmed);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
function _parseArgs(argv: string[]): ParsedArgs {
|
|
378
|
+
const args: ParsedArgs = {
|
|
379
|
+
window_days: 30,
|
|
380
|
+
json: false,
|
|
381
|
+
log_path: null,
|
|
382
|
+
settings_file: '.agent-settings.yml',
|
|
383
|
+
};
|
|
384
|
+
const takeValue = (flag: string, i: number): [string, number] => {
|
|
385
|
+
const v = argv[i + 1];
|
|
386
|
+
if (v === undefined) {
|
|
387
|
+
_argError(`argument ${flag}: expected one argument`);
|
|
388
|
+
}
|
|
389
|
+
return [v as string, i + 1];
|
|
390
|
+
};
|
|
391
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
392
|
+
const a = argv[i] as string;
|
|
393
|
+
if (a === '--window-days') {
|
|
394
|
+
args.window_days = _parseIntArg(argv[++i], '--window-days');
|
|
395
|
+
} else if (a.startsWith('--window-days=')) {
|
|
396
|
+
args.window_days = _parseIntArg(a.slice('--window-days='.length), '--window-days');
|
|
397
|
+
} else if (a === '--json') {
|
|
398
|
+
args.json = true;
|
|
399
|
+
} else if (a === '--log-path') {
|
|
400
|
+
const [v, ni] = takeValue('--log-path', i);
|
|
401
|
+
args.log_path = v;
|
|
402
|
+
i = ni;
|
|
403
|
+
} else if (a.startsWith('--log-path=')) {
|
|
404
|
+
args.log_path = a.slice('--log-path='.length);
|
|
405
|
+
} else if (a === '--settings-file') {
|
|
406
|
+
const [v, ni] = takeValue('--settings-file', i);
|
|
407
|
+
args.settings_file = v;
|
|
408
|
+
i = ni;
|
|
409
|
+
} else if (a.startsWith('--settings-file=')) {
|
|
410
|
+
args.settings_file = a.slice('--settings-file='.length);
|
|
411
|
+
} else if (a === '-h' || a === '--help') {
|
|
412
|
+
process.stdout.write(_USAGE);
|
|
413
|
+
process.exit(0);
|
|
414
|
+
} else {
|
|
415
|
+
_argError(`unrecognized arguments: ${a}`);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
return args;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// --- JSON output (json.dumps(payload, indent=2)) ----------------------------
|
|
422
|
+
|
|
423
|
+
function pyJsonDumps(value: unknown, indent: number): string {
|
|
424
|
+
return _escapeNonAscii(_dumpsIndent(value, indent, 0));
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
function _dumpsIndent(value: unknown, indent: number, depth: number): string {
|
|
428
|
+
const pad = ' '.repeat(indent * (depth + 1));
|
|
429
|
+
const closePad = ' '.repeat(indent * depth);
|
|
430
|
+
if (value === null || value === undefined) {
|
|
431
|
+
return 'null';
|
|
432
|
+
}
|
|
433
|
+
if (typeof value === 'boolean') {
|
|
434
|
+
return value ? 'true' : 'false';
|
|
435
|
+
}
|
|
436
|
+
if (typeof value === 'number') {
|
|
437
|
+
return _jsonNum(value);
|
|
438
|
+
}
|
|
439
|
+
if (typeof value === 'string') {
|
|
440
|
+
return _jsonStrAscii(value);
|
|
441
|
+
}
|
|
442
|
+
if (Array.isArray(value)) {
|
|
443
|
+
if (value.length === 0) {
|
|
444
|
+
return '[]';
|
|
445
|
+
}
|
|
446
|
+
const items = value.map((v) => pad + _dumpsIndent(v, indent, depth + 1));
|
|
447
|
+
return `[\n${items.join(',\n')}\n${closePad}]`;
|
|
448
|
+
}
|
|
449
|
+
if (_isPlainObject(value)) {
|
|
450
|
+
const keys = Object.keys(value);
|
|
451
|
+
if (keys.length === 0) {
|
|
452
|
+
return '{}';
|
|
453
|
+
}
|
|
454
|
+
const items = keys.map((k) => `${pad}${_jsonStrAscii(k)}: ${_dumpsIndent(value[k], indent, depth + 1)}`);
|
|
455
|
+
return `{\n${items.join(',\n')}\n${closePad}}`;
|
|
456
|
+
}
|
|
457
|
+
return _jsonStrAscii(String(value));
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
function _jsonNum(n: number): string {
|
|
461
|
+
if (!Number.isFinite(n)) {
|
|
462
|
+
if (Number.isNaN(n)) {
|
|
463
|
+
return 'NaN';
|
|
464
|
+
}
|
|
465
|
+
return n > 0 ? 'Infinity' : '-Infinity';
|
|
466
|
+
}
|
|
467
|
+
return String(n);
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
function _jsonStrAscii(s: string): string {
|
|
471
|
+
let out = '"';
|
|
472
|
+
for (const ch of s) {
|
|
473
|
+
const code = ch.codePointAt(0) ?? 0;
|
|
474
|
+
switch (ch) {
|
|
475
|
+
case '"':
|
|
476
|
+
out += '\\"';
|
|
477
|
+
break;
|
|
478
|
+
case '\\':
|
|
479
|
+
out += '\\\\';
|
|
480
|
+
break;
|
|
481
|
+
case '\n':
|
|
482
|
+
out += '\\n';
|
|
483
|
+
break;
|
|
484
|
+
case '\r':
|
|
485
|
+
out += '\\r';
|
|
486
|
+
break;
|
|
487
|
+
case '\t':
|
|
488
|
+
out += '\\t';
|
|
489
|
+
break;
|
|
490
|
+
case '\b':
|
|
491
|
+
out += '\\b';
|
|
492
|
+
break;
|
|
493
|
+
case '\f':
|
|
494
|
+
out += '\\f';
|
|
495
|
+
break;
|
|
496
|
+
default:
|
|
497
|
+
if (code < 0x20) {
|
|
498
|
+
out += `\\u${code.toString(16).padStart(4, '0')}`;
|
|
499
|
+
} else {
|
|
500
|
+
out += ch;
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
return `${out}"`;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
function _escapeNonAscii(s: string): string {
|
|
508
|
+
let out = '';
|
|
509
|
+
for (const ch of s) {
|
|
510
|
+
const code = ch.codePointAt(0) ?? 0;
|
|
511
|
+
if (code > 0x7f) {
|
|
512
|
+
for (let i = 0; i < ch.length; i += 1) {
|
|
513
|
+
out += `\\u${ch.charCodeAt(i).toString(16).padStart(4, '0')}`;
|
|
514
|
+
}
|
|
515
|
+
} else {
|
|
516
|
+
out += ch;
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
return out;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
export function main(argv?: string[]): number {
|
|
523
|
+
const args = _parseArgs(argv ?? process.argv.slice(2));
|
|
524
|
+
|
|
525
|
+
const settings = read_tier_usage_settings(args.settings_file);
|
|
526
|
+
const log_path = args.log_path || settings.log_path || DEFAULT_TIER_USAGE_LOG_PATH;
|
|
527
|
+
|
|
528
|
+
if (args.log_path === null && !settings.enabled) {
|
|
529
|
+
process.stdout.write(
|
|
530
|
+
'(tier-usage telemetry disabled; set ' +
|
|
531
|
+
'`telemetry.tier_usage.enabled: true` in .agent-settings.yml)\n',
|
|
532
|
+
);
|
|
533
|
+
return 0;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
let result: AggregateResult;
|
|
537
|
+
try {
|
|
538
|
+
result = aggregate(log_path, args.window_days);
|
|
539
|
+
} catch (exc) {
|
|
540
|
+
process.stderr.write(`❌ ${_osErrorMessage(exc, log_path)}\n`);
|
|
541
|
+
return 2;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
if (result.total > 0 && result.kept === 0) {
|
|
545
|
+
process.stderr.write(
|
|
546
|
+
`❌ ${result.total} record(s) read; 0 survived the privacy floor — ` + 'report refused\n',
|
|
547
|
+
);
|
|
548
|
+
return 1;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
if (args.json) {
|
|
552
|
+
// sorted(table.items(), key=(tier, command))
|
|
553
|
+
const sortedKeys = [...result.table.entries()].sort((a, b) => {
|
|
554
|
+
const ma = result.keyMeta.get(a[0]) as { tier: number; command: string };
|
|
555
|
+
const mb = result.keyMeta.get(b[0]) as { tier: number; command: string };
|
|
556
|
+
if (ma.tier !== mb.tier) {
|
|
557
|
+
return ma.tier - mb.tier;
|
|
558
|
+
}
|
|
559
|
+
return ma.command < mb.command ? -1 : ma.command > mb.command ? 1 : 0;
|
|
560
|
+
});
|
|
561
|
+
const payload = {
|
|
562
|
+
window_days: args.window_days,
|
|
563
|
+
log_path: String(log_path),
|
|
564
|
+
records_total: result.total,
|
|
565
|
+
records_kept: result.kept,
|
|
566
|
+
rows: sortedKeys.map(([key, v]) => {
|
|
567
|
+
const meta = result.keyMeta.get(key) as { tier: number; command: string };
|
|
568
|
+
return { tier: meta.tier, command: meta.command, count: v.count, distinct_users: v.distinct_users };
|
|
569
|
+
}),
|
|
570
|
+
};
|
|
571
|
+
process.stdout.write(`${pyJsonDumps(payload, 2)}\n`);
|
|
572
|
+
} else {
|
|
573
|
+
process.stdout.write(render(result, args.window_days));
|
|
574
|
+
}
|
|
575
|
+
return 0;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
/** Mirror str(OSError) shapes for the exit-2 stderr line. */
|
|
579
|
+
function _osErrorMessage(exc: unknown, log_path: string): string {
|
|
580
|
+
const err = exc as NodeJS.ErrnoException;
|
|
581
|
+
if (err && err.code === 'ENOENT') {
|
|
582
|
+
return `[Errno 2] No such file or directory: '${log_path}'`;
|
|
583
|
+
}
|
|
584
|
+
if (err && err.code === 'EACCES') {
|
|
585
|
+
return `[Errno 13] Permission denied: '${log_path}'`;
|
|
586
|
+
}
|
|
587
|
+
if (err && err.code === 'EISDIR') {
|
|
588
|
+
return `[Errno 21] Is a directory: '${log_path}'`;
|
|
589
|
+
}
|
|
590
|
+
return err && err.message ? err.message : String(exc);
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
function _isCliEntry(): boolean {
|
|
594
|
+
if (process.argv[1] === undefined) {
|
|
595
|
+
return false;
|
|
596
|
+
}
|
|
597
|
+
const argvUrl = pathToFileURL(path.resolve(process.argv[1])).href;
|
|
598
|
+
if (import.meta.url === argvUrl) {
|
|
599
|
+
return true;
|
|
600
|
+
}
|
|
601
|
+
// A symlinked invocation (e.g. via an installed `.augment/` projection,
|
|
602
|
+
// or macOS /var → /private/var temp dirs) makes the raw URLs differ:
|
|
603
|
+
// import.meta.url is the resolved real path while argv[1] keeps the
|
|
604
|
+
// symlink path. Compare realpaths so the entry guard still fires
|
|
605
|
+
// (without this the CLI silently no-ops when run through a symlink).
|
|
606
|
+
try {
|
|
607
|
+
const here = fs.realpathSync(fileURLToPath(import.meta.url));
|
|
608
|
+
const argv = fs.realpathSync(path.resolve(process.argv[1]));
|
|
609
|
+
return here === argv;
|
|
610
|
+
} catch {
|
|
611
|
+
return false;
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
const _isMain =
|
|
616
|
+
_isCliEntry();
|
|
617
|
+
if (_isMain) {
|
|
618
|
+
process.exit(main());
|
|
619
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Module entry point — lets the work engine run as a script.
|
|
3
|
+
*
|
|
4
|
+
* TypeScript twin of `work_engine/__main__.py` (ADR-200 py2ts Phase 1 —
|
|
5
|
+
* work_engine TOP/integration layer). The Python source is the
|
|
6
|
+
* `python3 -m work_engine` shim (`from .cli import main; sys.exit(main())`).
|
|
7
|
+
* The twin mirrors it: import `main`, run it, and set `process.exitCode` to the
|
|
8
|
+
* returned code — never `process.exit`, per ADR-200 (so flushing + cleanup run
|
|
9
|
+
* exactly as Node's natural shutdown does).
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { main } from './cli.js';
|
|
13
|
+
|
|
14
|
+
process.exitCode = main();
|