@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,394 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Engagement report renderer (Phase 4 Step 2).
|
|
3
|
+
*
|
|
4
|
+
* TypeScript twin of `report_renderer.py` (ADR-200). Byte-for-byte parity on
|
|
5
|
+
* both formats: same quartile bucketing, same markdown table, same JSON
|
|
6
|
+
* (`json.dumps(sort_keys=True, indent=2)`), same redaction re-validation,
|
|
7
|
+
* same Python float / fixed-decimal rendering (round-half-to-even).
|
|
8
|
+
*/
|
|
9
|
+
import {
|
|
10
|
+
type ArtefactStat,
|
|
11
|
+
type AggregateResult,
|
|
12
|
+
rank_artefacts,
|
|
13
|
+
} from './aggregator.js';
|
|
14
|
+
import { ALLOWED_OUTCOMES, check_id_redaction } from './engagement.js';
|
|
15
|
+
|
|
16
|
+
export const QUARTILE_TOP_RATIO = 0.20;
|
|
17
|
+
export const QUARTILE_BOTTOM_RATIO = 0.20;
|
|
18
|
+
|
|
19
|
+
export const BUCKET_TOP = 'essential';
|
|
20
|
+
export const BUCKET_MID = 'useful';
|
|
21
|
+
export const BUCKET_BOTTOM = 'retirement_candidate';
|
|
22
|
+
|
|
23
|
+
export interface BucketedStat {
|
|
24
|
+
stat: ArtefactStat;
|
|
25
|
+
bucket: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Assign each stat to a quartile bucket. Ranking from rank_artefacts assumed. */
|
|
29
|
+
export function bucketise(stats: ArtefactStat[]): BucketedStat[] {
|
|
30
|
+
const n = stats.length;
|
|
31
|
+
if (n === 0) {
|
|
32
|
+
return [];
|
|
33
|
+
}
|
|
34
|
+
if (n <= 1) {
|
|
35
|
+
return [{ stat: stats[0] as ArtefactStat, bucket: BUCKET_TOP }];
|
|
36
|
+
}
|
|
37
|
+
const top_cut = Math.max(1, _intRound(n * QUARTILE_TOP_RATIO));
|
|
38
|
+
let bottom_cut = n >= 5 ? n - Math.max(1, _intRound(n * QUARTILE_BOTTOM_RATIO)) : n;
|
|
39
|
+
if (bottom_cut <= top_cut) {
|
|
40
|
+
bottom_cut = n; // mid takes the rest, no retirement bucket
|
|
41
|
+
}
|
|
42
|
+
const out: BucketedStat[] = [];
|
|
43
|
+
for (let idx = 0; idx < n; idx += 1) {
|
|
44
|
+
let bucket: string;
|
|
45
|
+
if (idx < top_cut) {
|
|
46
|
+
bucket = BUCKET_TOP;
|
|
47
|
+
} else if (idx < bottom_cut) {
|
|
48
|
+
bucket = BUCKET_MID;
|
|
49
|
+
} else {
|
|
50
|
+
bucket = BUCKET_BOTTOM;
|
|
51
|
+
}
|
|
52
|
+
out.push({ stat: stats[idx] as ArtefactStat, bucket });
|
|
53
|
+
}
|
|
54
|
+
return out;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface RenderOptions {
|
|
58
|
+
top?: number | null;
|
|
59
|
+
since_label?: string | null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Render a markdown report. `top` truncates each bucket; null keeps all. */
|
|
63
|
+
export function render_markdown(aggregate: AggregateResult, opts: RenderOptions = {}): string {
|
|
64
|
+
const top = opts.top ?? null;
|
|
65
|
+
const since_label = opts.since_label ?? null;
|
|
66
|
+
const ranked = rank_artefacts(aggregate.stats());
|
|
67
|
+
const bucketed = bucketise(ranked);
|
|
68
|
+
const grouped: Record<string, BucketedStat[]> = {
|
|
69
|
+
[BUCKET_TOP]: [],
|
|
70
|
+
[BUCKET_MID]: [],
|
|
71
|
+
[BUCKET_BOTTOM]: [],
|
|
72
|
+
};
|
|
73
|
+
for (const entry of bucketed) {
|
|
74
|
+
(grouped[entry.bucket] as BucketedStat[]).push(entry);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const lines: string[] = [];
|
|
78
|
+
lines.push('# Artefact Engagement Report');
|
|
79
|
+
lines.push('');
|
|
80
|
+
lines.push(`- events parsed: **${aggregate.parsed_events}**`);
|
|
81
|
+
lines.push(`- events skipped (malformed): **${aggregate.skipped_lines}**`);
|
|
82
|
+
if (since_label) {
|
|
83
|
+
lines.push(`- window: **${since_label}**`);
|
|
84
|
+
}
|
|
85
|
+
if (aggregate.earliest_ts && aggregate.latest_ts) {
|
|
86
|
+
lines.push(`- ts range: \`${aggregate.earliest_ts}\` → \`${aggregate.latest_ts}\``);
|
|
87
|
+
}
|
|
88
|
+
lines.push('');
|
|
89
|
+
|
|
90
|
+
const outcomes_total = _sumValues(aggregate.outcomes);
|
|
91
|
+
if (outcomes_total) {
|
|
92
|
+
lines.push('## Outcomes');
|
|
93
|
+
lines.push('');
|
|
94
|
+
lines.push('| outcome | count | share |');
|
|
95
|
+
lines.push('|---|---:|---:|');
|
|
96
|
+
for (const label of ALLOWED_OUTCOMES) {
|
|
97
|
+
const count = aggregate.outcomes.get(label) ?? 0;
|
|
98
|
+
if (!count) {
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
const share = count / outcomes_total;
|
|
102
|
+
lines.push(`| ${label} | ${count} | ${_pyFixed(share, 2)} |`);
|
|
103
|
+
}
|
|
104
|
+
lines.push('');
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const titles: Record<string, string> = {
|
|
108
|
+
[BUCKET_TOP]: 'Essential (top 20 %)',
|
|
109
|
+
[BUCKET_MID]: 'Useful (mid 60 %)',
|
|
110
|
+
[BUCKET_BOTTOM]: 'Retirement candidates (bottom 20 %)',
|
|
111
|
+
};
|
|
112
|
+
for (const bucket of [BUCKET_TOP, BUCKET_MID, BUCKET_BOTTOM]) {
|
|
113
|
+
let rows = grouped[bucket] as BucketedStat[];
|
|
114
|
+
if (top !== null) {
|
|
115
|
+
rows = rows.slice(0, top);
|
|
116
|
+
}
|
|
117
|
+
lines.push(`## ${titles[bucket]}`);
|
|
118
|
+
lines.push('');
|
|
119
|
+
if (rows.length === 0) {
|
|
120
|
+
lines.push('_(none)_');
|
|
121
|
+
lines.push('');
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
lines.push('| kind | id | consulted | applied | applied/consulted | last seen |');
|
|
125
|
+
lines.push('|---|---|---:|---:|---:|---|');
|
|
126
|
+
for (const entry of rows) {
|
|
127
|
+
const s = entry.stat;
|
|
128
|
+
check_id_redaction(`buckets.${s.kind}.id`, s.artefact_id);
|
|
129
|
+
lines.push(
|
|
130
|
+
`| ${s.kind} | \`${s.artefact_id}\` | ${s.consulted} | ${s.applied} `
|
|
131
|
+
+ `| ${_pyFixed(s.applied_ratio, 2)} | \`${s.last_seen_ts}\` |`,
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
lines.push('');
|
|
135
|
+
}
|
|
136
|
+
return `${_pyRStrip(lines.join('\n'))}\n`;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function render_json(aggregate: AggregateResult, opts: RenderOptions = {}): string {
|
|
140
|
+
const top = opts.top ?? null;
|
|
141
|
+
const since_label = opts.since_label ?? null;
|
|
142
|
+
const ranked = rank_artefacts(aggregate.stats());
|
|
143
|
+
const bucketed = bucketise(ranked);
|
|
144
|
+
const grouped: Record<string, Array<Record<string, unknown>>> = {
|
|
145
|
+
[BUCKET_TOP]: [],
|
|
146
|
+
[BUCKET_MID]: [],
|
|
147
|
+
[BUCKET_BOTTOM]: [],
|
|
148
|
+
};
|
|
149
|
+
for (const entry of bucketed) {
|
|
150
|
+
(grouped[entry.bucket] as Array<Record<string, unknown>>).push(_stat_to_dict(entry.stat));
|
|
151
|
+
}
|
|
152
|
+
if (top !== null) {
|
|
153
|
+
for (const bucket of Object.keys(grouped)) {
|
|
154
|
+
grouped[bucket] = (grouped[bucket] as Array<Record<string, unknown>>).slice(0, top);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
const outcomes_total = _sumValues(aggregate.outcomes);
|
|
158
|
+
const by_category: Record<string, number> = {};
|
|
159
|
+
for (const label of ALLOWED_OUTCOMES) {
|
|
160
|
+
const count = aggregate.outcomes.get(label) ?? 0;
|
|
161
|
+
if (count) {
|
|
162
|
+
by_category[label] = count;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
const outcomes_payload = { total: outcomes_total, by_category };
|
|
166
|
+
const payload = {
|
|
167
|
+
schema_version: 1,
|
|
168
|
+
summary: {
|
|
169
|
+
parsed_events: aggregate.parsed_events,
|
|
170
|
+
skipped_lines: aggregate.skipped_lines,
|
|
171
|
+
total_events: aggregate.total_events,
|
|
172
|
+
earliest_ts: aggregate.earliest_ts,
|
|
173
|
+
latest_ts: aggregate.latest_ts,
|
|
174
|
+
since_label,
|
|
175
|
+
},
|
|
176
|
+
outcomes: outcomes_payload,
|
|
177
|
+
buckets: grouped,
|
|
178
|
+
};
|
|
179
|
+
return `${_py_json_dumps_indent2_sorted(payload)}\n`;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function _stat_to_dict(stat: ArtefactStat): Record<string, unknown> {
|
|
183
|
+
check_id_redaction(`buckets.${stat.kind}.id`, stat.artefact_id);
|
|
184
|
+
return {
|
|
185
|
+
kind: stat.kind,
|
|
186
|
+
id: stat.artefact_id,
|
|
187
|
+
consulted: stat.consulted,
|
|
188
|
+
applied: stat.applied,
|
|
189
|
+
applied_ratio: new PyFloat(_pyRound(stat.applied_ratio, 4)),
|
|
190
|
+
last_seen_ts: stat.last_seen_ts,
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// ── Python-parity helpers ───────────────────────────────────────────────
|
|
195
|
+
|
|
196
|
+
function _sumValues(m: Map<string, number>): number {
|
|
197
|
+
let total = 0;
|
|
198
|
+
for (const v of m.values()) {
|
|
199
|
+
total += v;
|
|
200
|
+
}
|
|
201
|
+
return total;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/** Python `str.rstrip()` — strip trailing whitespace. */
|
|
205
|
+
function _pyRStrip(s: string): string {
|
|
206
|
+
return s.replace(/\s+$/u, '');
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/** `int(round(x))` with round-half-to-even on the exact IEEE value. */
|
|
210
|
+
function _intRound(x: number): number {
|
|
211
|
+
return _pyRound(x, 0);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* CPython `round(x, ndigits)` — round-half-to-even on the exact decimal
|
|
216
|
+
* expansion of the IEEE-754 double. Same algorithm as value_ladder.ts.
|
|
217
|
+
*/
|
|
218
|
+
export function _pyRound(value: number, ndigits = 0): number {
|
|
219
|
+
if (!Number.isFinite(value) || value === 0) {
|
|
220
|
+
return value;
|
|
221
|
+
}
|
|
222
|
+
const sign = value < 0 ? -1 : 1;
|
|
223
|
+
const abs = Math.abs(value);
|
|
224
|
+
const str = abs.toPrecision(17);
|
|
225
|
+
if (str.includes('e') || str.includes('E')) {
|
|
226
|
+
const factor = 10 ** ndigits;
|
|
227
|
+
return value > 0
|
|
228
|
+
? (Math.round(abs * factor) / factor) * sign
|
|
229
|
+
: -Math.round(abs * factor) / factor;
|
|
230
|
+
}
|
|
231
|
+
const dot = str.indexOf('.');
|
|
232
|
+
const intPart = dot === -1 ? str : str.slice(0, dot);
|
|
233
|
+
let fracPart = dot === -1 ? '' : str.slice(dot + 1);
|
|
234
|
+
while (fracPart.length <= ndigits) {
|
|
235
|
+
fracPart += '0';
|
|
236
|
+
}
|
|
237
|
+
const keepFrac = fracPart.slice(0, ndigits);
|
|
238
|
+
const deciderStr = fracPart.slice(ndigits);
|
|
239
|
+
const scaledIntStr = intPart + keepFrac;
|
|
240
|
+
let scaledInt = BigInt(scaledIntStr === '' ? '0' : scaledIntStr);
|
|
241
|
+
const firstDecider = deciderStr.charAt(0);
|
|
242
|
+
const restNonZero = /[1-9]/u.test(deciderStr.slice(1));
|
|
243
|
+
let roundUp = false;
|
|
244
|
+
if (firstDecider > '5' || (firstDecider === '5' && restNonZero)) {
|
|
245
|
+
roundUp = true;
|
|
246
|
+
} else if (firstDecider === '5' && !restNonZero) {
|
|
247
|
+
roundUp = scaledInt % 2n === 1n;
|
|
248
|
+
}
|
|
249
|
+
if (roundUp) {
|
|
250
|
+
scaledInt += 1n;
|
|
251
|
+
}
|
|
252
|
+
const result = Number(scaledInt) / 10 ** ndigits;
|
|
253
|
+
return result === 0 ? 0 : result * sign;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Python `format(x, '.Nf')` — fixed-point, N decimals, round-half-to-even on
|
|
258
|
+
* the exact IEEE value. Always emits exactly N fractional digits.
|
|
259
|
+
*/
|
|
260
|
+
export function _pyFixed(value: number, ndigits: number): string {
|
|
261
|
+
if (!Number.isFinite(value)) {
|
|
262
|
+
return String(value);
|
|
263
|
+
}
|
|
264
|
+
const sign = value < 0 ? '-' : '';
|
|
265
|
+
const abs = Math.abs(value);
|
|
266
|
+
const str = abs.toPrecision(17);
|
|
267
|
+
if (str.includes('e') || str.includes('E')) {
|
|
268
|
+
// Out-of-range magnitudes do not occur for ratio/share inputs.
|
|
269
|
+
return sign + abs.toFixed(ndigits);
|
|
270
|
+
}
|
|
271
|
+
const dot = str.indexOf('.');
|
|
272
|
+
const intPart = dot === -1 ? str : str.slice(0, dot);
|
|
273
|
+
let fracPart = dot === -1 ? '' : str.slice(dot + 1);
|
|
274
|
+
while (fracPart.length <= ndigits) {
|
|
275
|
+
fracPart += '0';
|
|
276
|
+
}
|
|
277
|
+
const keepFrac = fracPart.slice(0, ndigits);
|
|
278
|
+
const deciderStr = fracPart.slice(ndigits);
|
|
279
|
+
let scaledInt = BigInt((intPart + keepFrac) || '0');
|
|
280
|
+
const firstDecider = deciderStr.charAt(0);
|
|
281
|
+
const restNonZero = /[1-9]/u.test(deciderStr.slice(1));
|
|
282
|
+
let roundUp = false;
|
|
283
|
+
if (firstDecider > '5' || (firstDecider === '5' && restNonZero)) {
|
|
284
|
+
roundUp = true;
|
|
285
|
+
} else if (firstDecider === '5' && !restNonZero) {
|
|
286
|
+
roundUp = scaledInt % 2n === 1n;
|
|
287
|
+
}
|
|
288
|
+
if (roundUp) {
|
|
289
|
+
scaledInt += 1n;
|
|
290
|
+
}
|
|
291
|
+
let digits = scaledInt.toString();
|
|
292
|
+
while (digits.length <= ndigits) {
|
|
293
|
+
digits = `0${digits}`;
|
|
294
|
+
}
|
|
295
|
+
const outInt = ndigits === 0 ? digits : digits.slice(0, digits.length - ndigits);
|
|
296
|
+
const outFrac = ndigits === 0 ? '' : digits.slice(digits.length - ndigits);
|
|
297
|
+
const zeroValue = /^0*$/u.test(digits);
|
|
298
|
+
return `${zeroValue ? '' : sign}${outInt}${ndigits > 0 ? `.${outFrac}` : ''}`;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// ── Python-parity JSON (indent=2, sorted keys) ──────────────────────────
|
|
302
|
+
|
|
303
|
+
/** Box a float so the serializer renders it as a Python float (`0.0`, …). */
|
|
304
|
+
class PyFloat {
|
|
305
|
+
constructor(public readonly value: number) {}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function _isPlainObject(v: unknown): v is Record<string, unknown> {
|
|
309
|
+
return typeof v === 'object' && v !== null && !Array.isArray(v) && !(v instanceof PyFloat);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/** Mirror `json.dumps(obj, sort_keys=True, indent=2)` (+ ensure_ascii=True). */
|
|
313
|
+
function _py_json_dumps_indent2_sorted(value: unknown): string {
|
|
314
|
+
return _dumpIndent2(value, 0);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function _dumpIndent2(value: unknown, depth: number): string {
|
|
318
|
+
if (value instanceof PyFloat) {
|
|
319
|
+
return _pyFloatRepr(value.value);
|
|
320
|
+
}
|
|
321
|
+
if (value === null || value === undefined) {
|
|
322
|
+
return 'null';
|
|
323
|
+
}
|
|
324
|
+
if (typeof value === 'boolean') {
|
|
325
|
+
return value ? 'true' : 'false';
|
|
326
|
+
}
|
|
327
|
+
if (typeof value === 'number') {
|
|
328
|
+
return Number.isInteger(value) ? String(value) : _pyFloatRepr(value);
|
|
329
|
+
}
|
|
330
|
+
if (typeof value === 'string') {
|
|
331
|
+
return _pyJsonStringAscii(value);
|
|
332
|
+
}
|
|
333
|
+
const pad = ' '.repeat(depth + 1);
|
|
334
|
+
const closePad = ' '.repeat(depth);
|
|
335
|
+
if (Array.isArray(value)) {
|
|
336
|
+
if (value.length === 0) {
|
|
337
|
+
return '[]';
|
|
338
|
+
}
|
|
339
|
+
const items = value.map((v) => pad + _dumpIndent2(v, depth + 1));
|
|
340
|
+
return `[\n${items.join(',\n')}\n${closePad}]`;
|
|
341
|
+
}
|
|
342
|
+
const obj = value as Record<string, unknown>;
|
|
343
|
+
const keys = Object.keys(obj).sort((a, b) => (a < b ? -1 : (a > b ? 1 : 0)));
|
|
344
|
+
if (keys.length === 0) {
|
|
345
|
+
return '{}';
|
|
346
|
+
}
|
|
347
|
+
const items = keys.map(
|
|
348
|
+
(k) => `${pad}${_pyJsonStringAscii(k)}: ${_dumpIndent2(obj[k], depth + 1)}`,
|
|
349
|
+
);
|
|
350
|
+
return `{\n${items.join(',\n')}\n${closePad}}`;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/** Python `repr(float)` — shortest round-trip; integers get a trailing `.0`. */
|
|
354
|
+
function _pyFloatRepr(n: number): string {
|
|
355
|
+
if (Number.isInteger(n)) {
|
|
356
|
+
return `${n}.0`;
|
|
357
|
+
}
|
|
358
|
+
return String(n);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/** json.dumps default ensure_ascii=True string rendering. */
|
|
362
|
+
function _pyJsonStringAscii(s: string): string {
|
|
363
|
+
let out = '"';
|
|
364
|
+
for (const ch of s) {
|
|
365
|
+
const code = ch.codePointAt(0) as number;
|
|
366
|
+
if (ch === '"') {
|
|
367
|
+
out += '\\"';
|
|
368
|
+
} else if (ch === '\\') {
|
|
369
|
+
out += '\\\\';
|
|
370
|
+
} else if (ch === '\n') {
|
|
371
|
+
out += '\\n';
|
|
372
|
+
} else if (ch === '\r') {
|
|
373
|
+
out += '\\r';
|
|
374
|
+
} else if (ch === '\t') {
|
|
375
|
+
out += '\\t';
|
|
376
|
+
} else if (ch === '\b') {
|
|
377
|
+
out += '\\b';
|
|
378
|
+
} else if (ch === '\f') {
|
|
379
|
+
out += '\\f';
|
|
380
|
+
} else if (code < 0x20) {
|
|
381
|
+
out += `\\u${code.toString(16).padStart(4, '0')}`;
|
|
382
|
+
} else if (code < 0x7f) {
|
|
383
|
+
out += ch;
|
|
384
|
+
} else if (code <= 0xffff) {
|
|
385
|
+
out += `\\u${code.toString(16).padStart(4, '0')}`;
|
|
386
|
+
} else {
|
|
387
|
+
const c = code - 0x10000;
|
|
388
|
+
const hi = 0xd800 + (c >> 10);
|
|
389
|
+
const lo = 0xdc00 + (c & 0x3ff);
|
|
390
|
+
out += `\\u${hi.toString(16).padStart(4, '0')}\\u${lo.toString(16).padStart(4, '0')}`;
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
return `${out}"`;
|
|
394
|
+
}
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared settings reader for the `telemetry:*` CLI commands.
|
|
3
|
+
*
|
|
4
|
+
* TypeScript twin of `settings.py` (ADR-200). Reads the
|
|
5
|
+
* `telemetry.artifact_engagement` namespace from `.agent-settings.yml`.
|
|
6
|
+
* Tolerates a missing file, a missing section, and a missing YAML parser —
|
|
7
|
+
* the default-off doctrine means "everything unparseable means disabled".
|
|
8
|
+
*/
|
|
9
|
+
import { createRequire } from 'node:module';
|
|
10
|
+
import * as fs from 'node:fs';
|
|
11
|
+
import type * as YamlModule from 'yaml';
|
|
12
|
+
|
|
13
|
+
// Resolve `yaml` relative to this module (not cwd) so a consumer's cwd
|
|
14
|
+
// package.json never interferes; mirrors Python's lazy `import yaml`.
|
|
15
|
+
const YAML = createRequire(import.meta.url)('yaml') as typeof YamlModule;
|
|
16
|
+
|
|
17
|
+
export const DEFAULT_LOG_PATH = '.agent-engagement.jsonl';
|
|
18
|
+
export const DEFAULT_GRANULARITY = 'task';
|
|
19
|
+
export const ALLOWED_GRANULARITIES = ['task', 'phase-step', 'tool-call'] as const;
|
|
20
|
+
|
|
21
|
+
export const DEFAULT_TIER_USAGE_LOG_PATH = '.agent-tier-usage.jsonl';
|
|
22
|
+
export const DEFAULT_TIER_USAGE_RETIER = {
|
|
23
|
+
window_days: 30,
|
|
24
|
+
min_invocations: 20,
|
|
25
|
+
min_distinct_users: 3,
|
|
26
|
+
} as const;
|
|
27
|
+
|
|
28
|
+
export class TelemetrySettings {
|
|
29
|
+
readonly enabled: boolean;
|
|
30
|
+
readonly granularity: string;
|
|
31
|
+
readonly log_path: string;
|
|
32
|
+
readonly record_consulted: boolean;
|
|
33
|
+
readonly record_applied: boolean;
|
|
34
|
+
private readonly _section_present: boolean;
|
|
35
|
+
|
|
36
|
+
constructor(init: {
|
|
37
|
+
enabled: boolean;
|
|
38
|
+
granularity: string;
|
|
39
|
+
log_path: string;
|
|
40
|
+
record_consulted: boolean;
|
|
41
|
+
record_applied: boolean;
|
|
42
|
+
section_present: boolean;
|
|
43
|
+
}) {
|
|
44
|
+
this.enabled = init.enabled;
|
|
45
|
+
this.granularity = init.granularity;
|
|
46
|
+
this.log_path = init.log_path;
|
|
47
|
+
this.record_consulted = init.record_consulted;
|
|
48
|
+
this.record_applied = init.record_applied;
|
|
49
|
+
this._section_present = init.section_present;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
get section_present(): boolean {
|
|
53
|
+
return this._section_present;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function _isPlainObject(v: unknown): v is Record<string, unknown> {
|
|
58
|
+
return typeof v === 'object' && v !== null && !Array.isArray(v) && !(v instanceof Date);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function _coerce_bool(value: unknown, def: boolean): boolean {
|
|
62
|
+
if (typeof value === 'boolean') {
|
|
63
|
+
return value;
|
|
64
|
+
}
|
|
65
|
+
if (typeof value === 'string') {
|
|
66
|
+
const normalised = value.trim().toLowerCase();
|
|
67
|
+
if (['true', 'yes', 'on', '1'].includes(normalised)) {
|
|
68
|
+
return true;
|
|
69
|
+
}
|
|
70
|
+
if (['false', 'no', 'off', '0'].includes(normalised)) {
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return def;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function _coerce_str(value: unknown, def: string, allowed?: readonly string[]): string {
|
|
78
|
+
if (typeof value !== 'string' || !value.trim()) {
|
|
79
|
+
return def;
|
|
80
|
+
}
|
|
81
|
+
const candidate = value.trim();
|
|
82
|
+
if (allowed && !allowed.includes(candidate)) {
|
|
83
|
+
return def;
|
|
84
|
+
}
|
|
85
|
+
return candidate;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function _coerce_path(value: unknown, def: string): string {
|
|
89
|
+
if (typeof value !== 'string' || !value.trim()) {
|
|
90
|
+
return def;
|
|
91
|
+
}
|
|
92
|
+
return value.trim();
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function _isFile(p: string): boolean {
|
|
96
|
+
try {
|
|
97
|
+
return fs.statSync(p).isFile();
|
|
98
|
+
} catch {
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Return parsed telemetry settings — never raises on missing data. */
|
|
104
|
+
export function read_settings(p: string): TelemetrySettings {
|
|
105
|
+
let section: Record<string, unknown> = {};
|
|
106
|
+
let section_present = false;
|
|
107
|
+
|
|
108
|
+
if (_isFile(p)) {
|
|
109
|
+
let raw: unknown = {};
|
|
110
|
+
try {
|
|
111
|
+
raw = YAML.parse(fs.readFileSync(p, 'utf-8'), { version: '1.1' });
|
|
112
|
+
if (raw === null || raw === undefined) {
|
|
113
|
+
raw = {};
|
|
114
|
+
}
|
|
115
|
+
} catch {
|
|
116
|
+
raw = {};
|
|
117
|
+
}
|
|
118
|
+
if (_isPlainObject(raw)) {
|
|
119
|
+
const tele = raw['telemetry'];
|
|
120
|
+
if (_isPlainObject(tele)) {
|
|
121
|
+
const artefact = tele['artifact_engagement'];
|
|
122
|
+
if (_isPlainObject(artefact)) {
|
|
123
|
+
section = artefact;
|
|
124
|
+
section_present = true;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const record = _isPlainObject(section['record']) ? section['record'] : {};
|
|
131
|
+
const output = _isPlainObject(section['output']) ? section['output'] : {};
|
|
132
|
+
|
|
133
|
+
return new TelemetrySettings({
|
|
134
|
+
enabled: _coerce_bool(section['enabled'], false),
|
|
135
|
+
granularity: _coerce_str(section['granularity'], DEFAULT_GRANULARITY, ALLOWED_GRANULARITIES),
|
|
136
|
+
log_path: _coerce_path(output['path'], DEFAULT_LOG_PATH),
|
|
137
|
+
record_consulted: _coerce_bool(record['consulted'], true),
|
|
138
|
+
record_applied: _coerce_bool(record['applied'], true),
|
|
139
|
+
section_present,
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export class TierUsageSettings {
|
|
144
|
+
readonly enabled: boolean;
|
|
145
|
+
readonly log_path: string;
|
|
146
|
+
readonly window_days: number;
|
|
147
|
+
readonly min_invocations: number;
|
|
148
|
+
readonly min_distinct_users: number;
|
|
149
|
+
|
|
150
|
+
constructor(init: {
|
|
151
|
+
enabled: boolean;
|
|
152
|
+
log_path: string;
|
|
153
|
+
window_days: number;
|
|
154
|
+
min_invocations: number;
|
|
155
|
+
min_distinct_users: number;
|
|
156
|
+
}) {
|
|
157
|
+
this.enabled = init.enabled;
|
|
158
|
+
this.log_path = init.log_path;
|
|
159
|
+
this.window_days = init.window_days;
|
|
160
|
+
this.min_invocations = init.min_invocations;
|
|
161
|
+
this.min_distinct_users = init.min_distinct_users;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** Return parsed tier-usage settings — never raises on missing data. */
|
|
166
|
+
export function read_tier_usage_settings(p: string): TierUsageSettings {
|
|
167
|
+
let section: Record<string, unknown> = {};
|
|
168
|
+
if (_isFile(p)) {
|
|
169
|
+
let raw: unknown = {};
|
|
170
|
+
try {
|
|
171
|
+
raw = YAML.parse(fs.readFileSync(p, 'utf-8'), { version: '1.1' });
|
|
172
|
+
if (raw === null || raw === undefined) {
|
|
173
|
+
raw = {};
|
|
174
|
+
}
|
|
175
|
+
} catch {
|
|
176
|
+
raw = {};
|
|
177
|
+
}
|
|
178
|
+
if (_isPlainObject(raw)) {
|
|
179
|
+
const tele = raw['telemetry'];
|
|
180
|
+
if (_isPlainObject(tele)) {
|
|
181
|
+
const tu = tele['tier_usage'];
|
|
182
|
+
if (_isPlainObject(tu)) {
|
|
183
|
+
section = tu;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const output = _isPlainObject(section['output']) ? section['output'] : {};
|
|
190
|
+
const retier = _isPlainObject(section['retier']) ? section['retier'] : {};
|
|
191
|
+
const defaults = DEFAULT_TIER_USAGE_RETIER;
|
|
192
|
+
|
|
193
|
+
const _coerce_int = (value: unknown, def: number): number => {
|
|
194
|
+
if (typeof value === 'boolean') {
|
|
195
|
+
return def;
|
|
196
|
+
}
|
|
197
|
+
if (typeof value === 'number' && Number.isInteger(value) && value >= 0) {
|
|
198
|
+
return value;
|
|
199
|
+
}
|
|
200
|
+
return def;
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
return new TierUsageSettings({
|
|
204
|
+
enabled: _coerce_bool(section['enabled'], false),
|
|
205
|
+
log_path: _coerce_path(output['path'], DEFAULT_TIER_USAGE_LOG_PATH),
|
|
206
|
+
window_days: _coerce_int(retier['window_days'], defaults.window_days),
|
|
207
|
+
min_invocations: _coerce_int(retier['min_invocations'], defaults.min_invocations),
|
|
208
|
+
min_distinct_users: _coerce_int(retier['min_distinct_users'], defaults.min_distinct_users),
|
|
209
|
+
});
|
|
210
|
+
}
|