@rosh100yx/outlier 0.7.0 → 0.10.2
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/README.md +70 -54
- package/bin/outlier.js +388 -36
- package/package.json +1 -1
- package/src/aggregate.ts +59 -0
- package/src/capabilities.ts +5 -0
- package/src/carbon.ts +33 -4
- package/src/cli.ts +164 -12
- package/src/economics.ts +66 -0
- package/src/emissions.ts +7 -3
- package/src/insights.ts +109 -0
package/src/aggregate.ts
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// #8 Light team/fleet aggregation — local-first, no export.
|
|
2
|
+
//
|
|
3
|
+
// Each developer runs `outlier --json > me.json` and drops it in a shared folder (or CI
|
|
4
|
+
// collects them). `outlier aggregate <dir>` merges those JSON files into a team rollup.
|
|
5
|
+
// No machine talks to another; it only reads JSON the team already produced. This is the
|
|
6
|
+
// honest, minimal version of fleet view — real aggregation without a backend.
|
|
7
|
+
|
|
8
|
+
import { readdirSync, readFileSync } from 'fs';
|
|
9
|
+
import { join } from 'path';
|
|
10
|
+
|
|
11
|
+
export interface TeamRollup {
|
|
12
|
+
developers: number;
|
|
13
|
+
avgAiPercent: number | null;
|
|
14
|
+
maxAiPercent: number | null;
|
|
15
|
+
totalEstUsd: number;
|
|
16
|
+
worstBlastRadius: string;
|
|
17
|
+
overLimit: number; // how many devs are over their AI cap
|
|
18
|
+
reachWriteDeploy: number; // total write/deploy-capable tools across the team
|
|
19
|
+
notes: string[];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const BLAST_ORDER = ['LOW', 'MEDIUM', 'HIGH', 'CRITICAL'];
|
|
23
|
+
|
|
24
|
+
export function aggregateDir(dir: string): TeamRollup {
|
|
25
|
+
const files = readdirSync(dir).filter(f => f.endsWith('.json'));
|
|
26
|
+
const audits: any[] = [];
|
|
27
|
+
for (const f of files) {
|
|
28
|
+
try {
|
|
29
|
+
const j = JSON.parse(readFileSync(join(dir, f), 'utf-8'));
|
|
30
|
+
if (j?.tool === 'outlier') audits.push(j);
|
|
31
|
+
} catch {}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const aiPcts = audits.map(a => a.authorship?.aiPercent).filter((x: any) => typeof x === 'number');
|
|
35
|
+
const spends = audits.map(a => a.cost?.estUsd || 0);
|
|
36
|
+
const blasts = audits.map(a => a.reach?.blastRadius).filter(Boolean);
|
|
37
|
+
const overs = audits.filter(a => a.policy?.status === 'over').length;
|
|
38
|
+
const writeDeploy = audits.reduce((s, a) => s + (a.reach?.writeOrDeployCount || 0), 0);
|
|
39
|
+
|
|
40
|
+
const worst = blasts.reduce((m: string, b: string) =>
|
|
41
|
+
BLAST_ORDER.indexOf(b) > BLAST_ORDER.indexOf(m) ? b : m, 'LOW');
|
|
42
|
+
|
|
43
|
+
const notes: string[] = [];
|
|
44
|
+
if (aiPcts.length && Math.max(...aiPcts) > 70) notes.push('At least one developer is over 70% AI authorship.');
|
|
45
|
+
if (worst === 'HIGH' || worst === 'CRITICAL') notes.push(`Worst-case agent blast radius across the team is ${worst}.`);
|
|
46
|
+
if (overs > 0) notes.push(`${overs} developer(s) over their AI-authorship limit.`);
|
|
47
|
+
if (audits.length === 0) notes.push('No outlier --json files found in this folder.');
|
|
48
|
+
|
|
49
|
+
return {
|
|
50
|
+
developers: audits.length,
|
|
51
|
+
avgAiPercent: aiPcts.length ? +(aiPcts.reduce((a: number, b: number) => a + b, 0) / aiPcts.length).toFixed(1) : null,
|
|
52
|
+
maxAiPercent: aiPcts.length ? Math.max(...aiPcts) : null,
|
|
53
|
+
totalEstUsd: +spends.reduce((a: number, b: number) => a + b, 0).toFixed(2),
|
|
54
|
+
worstBlastRadius: worst,
|
|
55
|
+
overLimit: overs,
|
|
56
|
+
reachWriteDeploy: writeDeploy,
|
|
57
|
+
notes,
|
|
58
|
+
};
|
|
59
|
+
}
|
package/src/capabilities.ts
CHANGED
|
@@ -72,10 +72,15 @@ export async function getCapabilitiesStats(repoPath: string = process.cwd(), hom
|
|
|
72
72
|
const mcpNames = new Set<string>();
|
|
73
73
|
|
|
74
74
|
// MCP servers — the agent's actual tool reach. Read from every place they live.
|
|
75
|
+
// Read MCP servers from every place agents declare them (Claude, Cursor, Gemini, project).
|
|
75
76
|
for (const cfg of [
|
|
76
77
|
join(homeDirPath, '.claude.json'),
|
|
77
78
|
join(homeDirPath, '.claude', 'settings.json'),
|
|
79
|
+
join(homeDirPath, '.cursor', 'mcp.json'),
|
|
80
|
+
join(homeDirPath, '.gemini', 'config', 'mcp_config.json'),
|
|
81
|
+
join(homeDirPath, '.gemini', 'settings.json'),
|
|
78
82
|
join(repoPath, '.mcp.json'),
|
|
83
|
+
join(repoPath, '.cursor', 'mcp.json'),
|
|
79
84
|
join(repoPath, '.claude', 'settings.json'),
|
|
80
85
|
]) {
|
|
81
86
|
const j = readJson(cfg);
|
package/src/carbon.ts
CHANGED
|
@@ -117,6 +117,33 @@ function estimateUsd(output: number, cacheRead: number, total: number): number {
|
|
|
117
117
|
return (output / 1e6) * 9 + (cacheRead / 1e6) * 0.3 + (otherInput / 1e6) * 3;
|
|
118
118
|
}
|
|
119
119
|
|
|
120
|
+
// #6 CodeCarbon: if the developer runs CodeCarbon, it writes a real MEASURED
|
|
121
|
+
// emissions.csv (hardware energy, not a token estimate). Read the latest run.
|
|
122
|
+
// Columns (codecarbon >=2): timestamp,project_name,...,duration,emissions,...,energy_consumed,...
|
|
123
|
+
function readCodeCarbon(cwd: string, home: string): { energyKwh: number; co2Kg: number } | null {
|
|
124
|
+
const { readFileSync, existsSync } = require('fs');
|
|
125
|
+
const { join } = require('path');
|
|
126
|
+
for (const p of [join(cwd, 'emissions.csv'), join(home, '.codecarbon', 'emissions.csv')]) {
|
|
127
|
+
try {
|
|
128
|
+
if (!existsSync(p)) continue;
|
|
129
|
+
const lines = readFileSync(p, 'utf-8').trim().split('\n');
|
|
130
|
+
if (lines.length < 2) continue;
|
|
131
|
+
const header = lines[0].split(',');
|
|
132
|
+
const iEm = header.indexOf('emissions'); // kg CO2eq
|
|
133
|
+
const iEn = header.indexOf('energy_consumed'); // kWh
|
|
134
|
+
if (iEm === -1 && iEn === -1) continue;
|
|
135
|
+
let co2 = 0, kwh = 0;
|
|
136
|
+
for (const row of lines.slice(1)) {
|
|
137
|
+
const cols = row.split(',');
|
|
138
|
+
co2 += parseFloat(cols[iEm]) || 0;
|
|
139
|
+
kwh += parseFloat(cols[iEn]) || 0;
|
|
140
|
+
}
|
|
141
|
+
return { energyKwh: kwh, co2Kg: co2 };
|
|
142
|
+
} catch {}
|
|
143
|
+
}
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
|
|
120
147
|
function getLocalGridFactor(): { region: string, factor: number } {
|
|
121
148
|
try {
|
|
122
149
|
const tz = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
@@ -147,9 +174,11 @@ export async function getCarbonStats(): Promise<CarbonStats> {
|
|
|
147
174
|
}
|
|
148
175
|
}
|
|
149
176
|
|
|
150
|
-
//
|
|
151
|
-
const energyKwh = energyKwhByModel(outputByModel);
|
|
177
|
+
// Carbon: prefer CodeCarbon's measured hardware data; else model-aware estimate.
|
|
152
178
|
const localGrid = getLocalGridFactor();
|
|
179
|
+
const measured = readCodeCarbon(process.cwd(), homedir());
|
|
180
|
+
const energyKwh = measured ? measured.energyKwh : energyKwhByModel(outputByModel);
|
|
181
|
+
const measuredCo2 = measured ? measured.co2Kg : null;
|
|
153
182
|
|
|
154
183
|
// Source provenance for honest labelling in the UI.
|
|
155
184
|
const sources = detectSources();
|
|
@@ -165,8 +194,8 @@ export async function getCarbonStats(): Promise<CarbonStats> {
|
|
|
165
194
|
energyKwh,
|
|
166
195
|
co2KgVietnam: (energyKwh * gridFactors.vietnam) / 1000,
|
|
167
196
|
co2KgFrance: (energyKwh * gridFactors.france) / 1000,
|
|
168
|
-
localCo2Kg: (energyKwh * localGrid.factor) / 1000,
|
|
169
|
-
localRegion: localGrid.region,
|
|
197
|
+
localCo2Kg: measuredCo2 !== null ? measuredCo2 : (energyKwh * localGrid.factor) / 1000,
|
|
198
|
+
localRegion: measured ? 'CodeCarbon (measured)' : localGrid.region,
|
|
170
199
|
sessions,
|
|
171
200
|
estUsd,
|
|
172
201
|
costIsReal,
|
package/src/cli.ts
CHANGED
|
@@ -5,6 +5,9 @@ import pc from 'picocolors';
|
|
|
5
5
|
import { getAuthorshipStats } from './git';
|
|
6
6
|
import { getCarbonStats } from './carbon';
|
|
7
7
|
import { getCapabilitiesStats } from './capabilities';
|
|
8
|
+
import { deriveInsights, type Insight } from './insights';
|
|
9
|
+
import { projectEconomics } from './economics';
|
|
10
|
+
import { aggregateDir } from './aggregate';
|
|
8
11
|
import { writeFileSync, readFileSync, chmodSync, existsSync } from 'fs';
|
|
9
12
|
import { join } from 'path';
|
|
10
13
|
import { detectAgent } from './agent';
|
|
@@ -20,6 +23,40 @@ const ASCII_LOGO = `
|
|
|
20
23
|
|
|
21
24
|
let finalReceipt = '';
|
|
22
25
|
|
|
26
|
+
// Turn the left-rail receipt into a clean closed rectangle (adds the right border,
|
|
27
|
+
// padding each line to a fixed inner width). Width-aware: strips ANSI and counts a few
|
|
28
|
+
// known wide glyphs as 2 columns so the right edge lines up in a terminal and on GitHub.
|
|
29
|
+
function closeBox(s: string, W = 66): string {
|
|
30
|
+
const wide = new Set(['⚠', '🛑', '✈', '🌱', '📸', '🔬', '💾', '💡', '✅', '❌', '😾', '😀']);
|
|
31
|
+
const chW = (ch: string) => { const cp = ch.codePointAt(0)!; return (cp >= 0x1F000 || wide.has(ch)) ? 2 : 1; };
|
|
32
|
+
const rail = '\x1b[2m│\x1b[0m';
|
|
33
|
+
// Fit a (possibly ANSI-coloured) line to exactly `totalVis` visible columns: pad with
|
|
34
|
+
// spaces, or truncate with an ellipsis — preserving colour codes either way.
|
|
35
|
+
const fit = (line: string, totalVis: number) => {
|
|
36
|
+
const parts = line.split(/(\x1b\[[0-9;]*m)/);
|
|
37
|
+
let out = '', vis = 0, cut = false;
|
|
38
|
+
for (const p of parts) {
|
|
39
|
+
if (/^\x1b\[/.test(p)) { out += p; continue; }
|
|
40
|
+
for (const ch of p) {
|
|
41
|
+
const w = chW(ch);
|
|
42
|
+
if (vis + w > totalVis - 1) { cut = true; break; }
|
|
43
|
+
out += ch; vis += w;
|
|
44
|
+
}
|
|
45
|
+
if (cut) break;
|
|
46
|
+
}
|
|
47
|
+
if (cut) { out += '…'; vis += 1; }
|
|
48
|
+
return out + ' '.repeat(Math.max(0, totalVis - vis)) + '\x1b[0m';
|
|
49
|
+
};
|
|
50
|
+
return s.split('\n').map(line => {
|
|
51
|
+
const plain = line.replace(/\x1b\[[0-9;]*m/g, '');
|
|
52
|
+
if (/^\s*┌/.test(plain)) return ' \x1b[2m┌' + '─'.repeat(W) + '┐\x1b[0m';
|
|
53
|
+
if (/^\s*├/.test(plain)) return ' \x1b[2m├' + '─'.repeat(W) + '┤\x1b[0m';
|
|
54
|
+
if (/^\s*└/.test(plain)) return ' \x1b[2m└' + '─'.repeat(W) + '┘\x1b[0m';
|
|
55
|
+
if (/^\s*│/.test(plain)) return fit(line, 2 + W) + rail; // 2 = leading space + left rail
|
|
56
|
+
return line;
|
|
57
|
+
}).join('\n');
|
|
58
|
+
}
|
|
59
|
+
|
|
23
60
|
// Build a stable, machine-readable audit object. This is the contract agents,
|
|
24
61
|
// swarms, and CI parse — everything the human receipt shows, as plain JSON.
|
|
25
62
|
async function emitJson() {
|
|
@@ -82,6 +119,8 @@ async function emitJson() {
|
|
|
82
119
|
aiCapPercent: cap * 100,
|
|
83
120
|
status: aiRatio > cap ? 'over' : 'within',
|
|
84
121
|
},
|
|
122
|
+
insights: deriveInsights({ authorship: gitStats, carbon, caps, policyCap: cap }),
|
|
123
|
+
economics: projectEconomics({ aiRatio, estUsdSession: carbon ? carbon.estUsd : 0, teamSize: 1 }),
|
|
85
124
|
};
|
|
86
125
|
|
|
87
126
|
// Only JSON on stdout — nothing else.
|
|
@@ -143,7 +182,15 @@ async function main() {
|
|
|
143
182
|
} catch (e) {}
|
|
144
183
|
}
|
|
145
184
|
if (alreadyRun) process.exit(0);
|
|
146
|
-
|
|
185
|
+
// Compact once-per-day greeting (for the Claude Code plugin SessionStart hook) —
|
|
186
|
+
// a single line, fast, no full receipt.
|
|
187
|
+
const g = await getAuthorshipStats().catch(() => null);
|
|
188
|
+
const cp = await getCapabilitiesStats().catch(() => null);
|
|
189
|
+
const aiP = g ? (g.ratio * 100).toFixed(0) + '%' : '—';
|
|
190
|
+
const br = cp ? cp.blastRadius : '—';
|
|
191
|
+
const brc = br === 'HIGH' || br === 'CRITICAL' ? pc.red : br === 'MEDIUM' ? pc.yellow : pc.green;
|
|
192
|
+
console.log(`${pc.dim('[outlier]')} AI authorship ${pc.bold(aiP)} · agent reach ${brc(pc.bold(br))} ${pc.dim('· before you delegate, run: outlier preflight')}`);
|
|
193
|
+
process.exit(0);
|
|
147
194
|
}
|
|
148
195
|
|
|
149
196
|
// Agent / CI / swarm contract: --json emits a structured audit and nothing else
|
|
@@ -164,6 +211,7 @@ async function main() {
|
|
|
164
211
|
console.log(pc.dim(' how much of your code AI wrote, what it cost, and how to keep your skill.\n'));
|
|
165
212
|
console.log(pc.bold('COMMANDS:'));
|
|
166
213
|
console.log(` ${pc.cyan('outlier')} Run the audit (the default — same as 'status')`);
|
|
214
|
+
console.log(` ${pc.cyan('outlier preflight')} Quick briefing BEFORE you start an agent (reach + skill + spend)`);
|
|
167
215
|
console.log(` ${pc.cyan('outlier status')} Full audit: who wrote the code, what it cost, your limit`);
|
|
168
216
|
console.log(` ${pc.cyan('outlier status --save')} Save the audit to ./outlier-audit.txt`);
|
|
169
217
|
console.log(` ${pc.cyan('outlier --json')} Machine-readable audit (for agents, CI, swarms)`);
|
|
@@ -398,6 +446,16 @@ Conservative Floor: ${color(nmPct + '%')}`,
|
|
|
398
446
|
reachStr = `${col(pc.bold(rc))} · ${capabilities.mcps.length} tools` + (risky ? pc.dim(`, ${risky} can write/deploy`) : '');
|
|
399
447
|
}
|
|
400
448
|
|
|
449
|
+
// Insight engine: turn the numbers into the top thing to actually do.
|
|
450
|
+
const insights = deriveInsights({ authorship: gitStats, carbon, caps: capabilities, policyCap: 0.70 });
|
|
451
|
+
const sevColor = (s: string) => s === 'critical' ? pc.red : s === 'warn' ? pc.yellow : s === 'good' ? pc.green : pc.cyan;
|
|
452
|
+
const sevMark = (s: string) => s === 'critical' ? '✗' : s === 'warn' ? '⚠' : s === 'good' ? '✓' : 'i';
|
|
453
|
+
const insightLines = insights.slice(0, 2).map((ins: Insight) =>
|
|
454
|
+
` ${pc.dim('│')} ${sevColor(ins.severity)(sevMark(ins.severity))} ${pc.bold(ins.title)}\n` +
|
|
455
|
+
` ${pc.dim('│')} ${ins.detail.length > 56 ? ins.detail.slice(0, 55) + '…' : ins.detail}\n` +
|
|
456
|
+
` ${pc.dim('│')} ${pc.cyan('→ ' + ins.action)}`
|
|
457
|
+
).join(`\n ${pc.dim('│')}\n`);
|
|
458
|
+
|
|
401
459
|
// The thermal receipt below is the single canonical output for `status`.
|
|
402
460
|
// (The old @clack dashboard panel was removed: it duplicated the receipt's
|
|
403
461
|
// numbers in a second format, doubling the output on every run.)
|
|
@@ -471,6 +529,9 @@ Conservative Floor: ${color(nmPct + '%')}`,
|
|
|
471
529
|
${pc.dim('│')} ${pc.bold(pc.bgYellow(pc.black(' YOUR LIMIT ')))}
|
|
472
530
|
${pc.dim('│')} AI cap ${pc.bold('70%')} ${pc.dim('· change with: outlier policy')}
|
|
473
531
|
${pc.dim('│')} Status ${policyStatus} ${pc.dim('·')} ${policyAction}
|
|
532
|
+
${pc.dim('├────────────────────────────────────────────────────────')}
|
|
533
|
+
${pc.dim('│')} ${pc.bold(pc.bgGreen(pc.black(' WHAT TO DO ')))}
|
|
534
|
+
${insightLines}
|
|
474
535
|
${pc.dim('├────────────────────────────────────────────────────────')}
|
|
475
536
|
${pc.dim('│')} ${pc.dim('Numbers are local estimates — authorship is a proxy and')}
|
|
476
537
|
${pc.dim('│')} ${pc.dim('carbon is rough. How it works: outlier --help')}
|
|
@@ -529,6 +590,62 @@ ${pc.dim('This is your attack surface. Fewer write/deploy tools per session = sm
|
|
|
529
590
|
s.stop('Audit failed');
|
|
530
591
|
console.error(pc.red(e.message));
|
|
531
592
|
}
|
|
593
|
+
} else if (action === 'aggregate') {
|
|
594
|
+
// Team/fleet rollup from a folder of `outlier --json` files. Local-first, no export.
|
|
595
|
+
const dir = process.argv[3];
|
|
596
|
+
if (!dir || !existsSync(dir)) {
|
|
597
|
+
console.error(pc.red('Usage: outlier aggregate <folder-of-json-audits>'));
|
|
598
|
+
console.log(pc.dim(' Each dev: outlier --json > team/<name>.json then: outlier aggregate team/'));
|
|
599
|
+
process.exit(1);
|
|
600
|
+
}
|
|
601
|
+
const r = aggregateDir(dir);
|
|
602
|
+
note(
|
|
603
|
+
`Developers: ${pc.bold(String(r.developers))}
|
|
604
|
+
Avg AI authorship: ${pc.bold(r.avgAiPercent !== null ? r.avgAiPercent + '%' : '—')} Max: ${r.maxAiPercent !== null ? r.maxAiPercent + '%' : '—'}
|
|
605
|
+
Over their limit: ${r.overLimit > 0 ? pc.red(String(r.overLimit)) : pc.green('0')}
|
|
606
|
+
Team spend (est): ${pc.bold('$' + r.totalEstUsd)}
|
|
607
|
+
Worst blast radius:${' '}${r.worstBlastRadius === 'HIGH' || r.worstBlastRadius === 'CRITICAL' ? pc.red(r.worstBlastRadius) : pc.yellow(r.worstBlastRadius)} (${r.reachWriteDeploy} write/deploy tools across the team)
|
|
608
|
+
${r.notes.length ? '\n' + r.notes.map(n => `${pc.yellow('•')} ${n}`).join('\n') : ''}`,
|
|
609
|
+
'Team Rollup (local-first — nothing was exported)'
|
|
610
|
+
);
|
|
611
|
+
} else if (action === 'preflight') {
|
|
612
|
+
// Forward-looking briefing — the reason to run outlier BEFORE you start an agent.
|
|
613
|
+
// Same engine as status, framed for the session you are about to begin: reach,
|
|
614
|
+
// skill, spend, and the one thing to do, ending with the handoff to your agent.
|
|
615
|
+
s.start('Pre-flight check...');
|
|
616
|
+
const gitStats = await getAuthorshipStats().catch(() => null);
|
|
617
|
+
const carbon = await getCarbonStats().catch(() => null);
|
|
618
|
+
const caps = await getCapabilitiesStats().catch(() => null);
|
|
619
|
+
s.stop('Ready for take-off');
|
|
620
|
+
|
|
621
|
+
const aiPct = gitStats ? (gitStats.ratio * 100).toFixed(0) : '—';
|
|
622
|
+
const youPct = gitStats ? (100 - gitStats.ratio * 100).toFixed(0) : '—';
|
|
623
|
+
const blast = caps ? caps.blastRadius : 'UNKNOWN';
|
|
624
|
+
const blastCol = blast === 'CRITICAL' || blast === 'HIGH' ? pc.red : blast === 'MEDIUM' ? pc.yellow : pc.green;
|
|
625
|
+
const risky = caps ? caps.mcps.filter(m => ['money','exec','deploy','write-remote','write-local'].includes(m.reach)).length : 0;
|
|
626
|
+
const spend = carbon ? `$${carbon.estUsd.toFixed(0)}` : '—';
|
|
627
|
+
const cachePct = carbon && carbon.totalTokens ? ((carbon.cacheReadTokens / carbon.totalTokens) * 100).toFixed(0) + '%' : '—';
|
|
628
|
+
|
|
629
|
+
const insights = deriveInsights({ authorship: gitStats, carbon, caps, policyCap: 0.70 });
|
|
630
|
+
const sevCol = (sv: string) => sv === 'critical' ? pc.red : sv === 'warn' ? pc.yellow : sv === 'good' ? pc.green : pc.cyan;
|
|
631
|
+
const sevMk = (sv: string) => sv === 'critical' ? '✗' : sv === 'warn' ? '⚠' : sv === 'good' ? '✓' : 'i';
|
|
632
|
+
const actionLines = insights.slice(0, 3)
|
|
633
|
+
.map(ins => ` ${sevCol(ins.severity)(sevMk(ins.severity))} ${pc.cyan('→')} ${ins.action}`)
|
|
634
|
+
.join('\n');
|
|
635
|
+
|
|
636
|
+
console.log('');
|
|
637
|
+
console.log(pc.bold(pc.cyan(' ✈ PRE-FLIGHT')) + pc.dim(` · ${process.cwd().split('/').pop()}`));
|
|
638
|
+
console.log(pc.dim(' ────────────────────────────────────────────────────'));
|
|
639
|
+
console.log(` ${pc.bold('Reach')} ${blastCol(pc.bold(blast))}` + (caps ? pc.dim(` · ${caps.mcps.length} tools, ${risky} can write/deploy`) : ''));
|
|
640
|
+
console.log(` ${pc.bold('Skill')} AI wrote ${pc.bold(aiPct + '%')} · you own ${pc.bold(youPct + '%')}`);
|
|
641
|
+
console.log(` ${pc.bold('Spend')} ${pc.bold(spend)} · ${cachePct} re-sent context`);
|
|
642
|
+
console.log('');
|
|
643
|
+
console.log(pc.bold(' Before you delegate:'));
|
|
644
|
+
console.log(actionLines);
|
|
645
|
+
console.log('');
|
|
646
|
+
const agent = detectAgent();
|
|
647
|
+
console.log(pc.bold(pc.magenta(' ✓ Ready? ')) + 'Start your session: ' + pc.bold(agent || 'your AI agent'));
|
|
648
|
+
console.log('');
|
|
532
649
|
} else if (action === 'policy') {
|
|
533
650
|
const tier = await select({
|
|
534
651
|
message: 'Select the governance tier to configure:',
|
|
@@ -613,19 +730,34 @@ Enforcement: ${pc.cyan('Local pre-commit hook installed (backup created)')}`,
|
|
|
613
730
|
'Active Governance Policy'
|
|
614
731
|
);
|
|
615
732
|
} else if (tier === 'regulatory') {
|
|
616
|
-
s.start('Generating
|
|
617
|
-
|
|
618
|
-
|
|
733
|
+
s.start('Generating human-oversight audit record (Decree 142)...');
|
|
734
|
+
// A real, honest compliance record from the actual local numbers — not a stub.
|
|
735
|
+
const gitStats = await getAuthorshipStats().catch(() => null);
|
|
736
|
+
const caps = await getCapabilitiesStats().catch(() => null);
|
|
737
|
+
const humanReviewRate = gitStats ? +(1 - gitStats.ratio).toFixed(3) : null;
|
|
738
|
+
const oversightOk = humanReviewRate !== null && humanReviewRate >= 0.30; // ≥30% human-authored
|
|
739
|
+
const record = {
|
|
740
|
+
timestamp: new Date().toISOString(),
|
|
741
|
+
policy: 'Vietnam Decree 142/2026 — human oversight of high-risk AI',
|
|
742
|
+
repo: process.cwd().split('/').pop(),
|
|
743
|
+
humanAuthorshipRate: humanReviewRate,
|
|
744
|
+
aiAuthorshipRate: gitStats ? +gitStats.ratio.toFixed(3) : null,
|
|
745
|
+
humanOversight: oversightOk ? 'present' : 'insufficient',
|
|
746
|
+
agentBlastRadius: caps ? caps.blastRadius : 'unknown',
|
|
747
|
+
dataExported: false,
|
|
748
|
+
note: 'Derived from local git history only. No code, prompts, or citizen data leave the machine. Authorship is a proxy for human oversight.',
|
|
749
|
+
};
|
|
619
750
|
const reportPath = join(process.cwd(), 'outlier-audit-report.jsonl');
|
|
620
|
-
writeFileSync(reportPath, JSON.stringify(
|
|
621
|
-
s.stop('Audit
|
|
751
|
+
writeFileSync(reportPath, JSON.stringify(record) + '\n');
|
|
752
|
+
s.stop('Audit record written');
|
|
622
753
|
|
|
623
754
|
note(
|
|
624
|
-
`Jurisdiction: ${pc.bold('Vietnam (Decree 142)')}
|
|
625
|
-
|
|
626
|
-
|
|
755
|
+
`Jurisdiction: ${pc.bold('Vietnam (Decree 142/2026)')}
|
|
756
|
+
Human oversight: ${oversightOk ? pc.green('present') : pc.red('insufficient')} ${pc.dim(`(${humanReviewRate !== null ? (humanReviewRate * 100).toFixed(0) + '% human-authored' : 'no git history'})`)}
|
|
757
|
+
Agent reach: ${caps ? caps.blastRadius : 'unknown'}
|
|
758
|
+
Privacy: ${pc.green('preserved — nothing exported')}
|
|
627
759
|
Artifact: ${pc.cyan(reportPath)}`,
|
|
628
|
-
'
|
|
760
|
+
'Human-Oversight Audit Record'
|
|
629
761
|
);
|
|
630
762
|
}
|
|
631
763
|
} else if (action === 'participate') {
|
|
@@ -688,6 +820,25 @@ Artifact: ${pc.cyan(reportPath)}`,
|
|
|
688
820
|
console.log(` ${pc.red('Lose:')} Architectural intimacy. You become a reviewer.`);
|
|
689
821
|
console.log(pc.cyan('\n■ Next 5-10 Years (The 1M+ LOC Crisis)'));
|
|
690
822
|
console.log(` When an agent introduces a fatal state bug in a monolithic architecture, human reviewers will lack the muscle memory to debug it. Outlier measures this exact sovereignty erosion.\n`);
|
|
823
|
+
|
|
824
|
+
// Economic translation: the macro shadow of your individual number.
|
|
825
|
+
const gitStats = await getAuthorshipStats().catch(() => null);
|
|
826
|
+
const carbon = await getCarbonStats().catch(() => null);
|
|
827
|
+
if (gitStats || carbon) {
|
|
828
|
+
const { projectEconomics } = await import('./economics');
|
|
829
|
+
const teamSize = (() => { const i = process.argv.indexOf('--team'); return i > -1 ? (parseInt(process.argv[i + 1] || '1') || 1) : 1; })();
|
|
830
|
+
const econ = projectEconomics({
|
|
831
|
+
aiRatio: gitStats ? gitStats.ratio : 0,
|
|
832
|
+
estUsdSession: carbon ? carbon.estUsd : 0,
|
|
833
|
+
teamSize,
|
|
834
|
+
});
|
|
835
|
+
console.log(pc.bold(pc.bgMagenta(' THE MACRO SHADOW ')) + pc.dim(` (team of ${teamSize} — set with --team N)`));
|
|
836
|
+
for (const p of econ.projections) {
|
|
837
|
+
console.log(` ${pc.bold(p.label.padEnd(20))} ${pc.cyan(p.value)}`);
|
|
838
|
+
console.log(` ${pc.dim(' ' + p.note)}`);
|
|
839
|
+
}
|
|
840
|
+
console.log('\n' + pc.dim(' ' + econ.assumptions) + '\n');
|
|
841
|
+
}
|
|
691
842
|
} else if (action === 'knowledge') {
|
|
692
843
|
console.log('\n' + pc.bold(pc.bgBlue(' CORE LITERATURE & REFERENCES ')) + '\n');
|
|
693
844
|
console.log(`1. ${pc.cyan('METR (Measuring AI Ability)')} - Evaluating AI on long-horizon software tasks.`);
|
|
@@ -699,14 +850,15 @@ Artifact: ${pc.cyan(reportPath)}`,
|
|
|
699
850
|
outro('Done — nothing left your machine. (How it works: outlier --help)');
|
|
700
851
|
|
|
701
852
|
if (typeof finalReceipt !== 'undefined' && finalReceipt) {
|
|
702
|
-
|
|
853
|
+
const boxed = closeBox(finalReceipt);
|
|
854
|
+
console.log(boxed);
|
|
703
855
|
|
|
704
856
|
// --save: write a plain-text (no color) copy of the receipt next to the repo.
|
|
705
857
|
if (process.argv.includes('--save')) {
|
|
706
858
|
const stripAnsi = (s: string) => s.replace(/\x1b\[[0-9;]*m/g, '');
|
|
707
859
|
const savePath = join(process.cwd(), 'outlier-audit.txt');
|
|
708
860
|
try {
|
|
709
|
-
writeFileSync(savePath, stripAnsi(
|
|
861
|
+
writeFileSync(savePath, stripAnsi(boxed).trimStart() + '\n');
|
|
710
862
|
console.log(pc.dim(`\n 💾 Saved to ${savePath}`));
|
|
711
863
|
} catch {}
|
|
712
864
|
}
|
package/src/economics.ts
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// Economic translation layer.
|
|
2
|
+
//
|
|
3
|
+
// The paper's core claim is that an individual authorship/spend number has a macro
|
|
4
|
+
// shadow: at team and national scale it becomes skill-ladder erosion, offshore value
|
|
5
|
+
// capture, and a hard-currency (forex) drain. This module turns the local numbers into
|
|
6
|
+
// that projection — explicitly as a *projection with stated assumptions*, not a measurement.
|
|
7
|
+
|
|
8
|
+
export interface EconProjection {
|
|
9
|
+
label: string;
|
|
10
|
+
value: string;
|
|
11
|
+
note: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface EconInput {
|
|
15
|
+
aiRatio: number; // 0..1, share of authored output that is AI
|
|
16
|
+
estUsdSession: number; // measured/estimated spend reflected by local logs
|
|
17
|
+
teamSize?: number; // default 1 (solo); orgs override
|
|
18
|
+
workdaysPerYear?: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Assumptions are surfaced in the output so nothing is hidden.
|
|
22
|
+
const DEFAULTS = { teamSize: 1, workdaysPerYear: 230 };
|
|
23
|
+
|
|
24
|
+
export function projectEconomics(input: EconInput): { projections: EconProjection[]; assumptions: string } {
|
|
25
|
+
const team = input.teamSize ?? DEFAULTS.teamSize;
|
|
26
|
+
const days = input.workdaysPerYear ?? DEFAULTS.workdaysPerYear;
|
|
27
|
+
const ai = Math.max(0, Math.min(1, input.aiRatio));
|
|
28
|
+
|
|
29
|
+
// Treat the local spend as a per-active-period hard-currency outflow to the AI vendor.
|
|
30
|
+
// Annualize crudely (the local logs are a sample window, not a day) and scale by team.
|
|
31
|
+
const annualOutflowPerDev = input.estUsdSession * (days / 30); // sample window ≈ a month of work
|
|
32
|
+
const annualOutflowTeam = annualOutflowPerDev * team;
|
|
33
|
+
|
|
34
|
+
const projections: EconProjection[] = [
|
|
35
|
+
{
|
|
36
|
+
label: 'Authorship shift',
|
|
37
|
+
value: `${(ai * 100).toFixed(0)}% of output → AI`,
|
|
38
|
+
note: team > 1
|
|
39
|
+
? `≈ ${(ai * team).toFixed(1)} of ${team} dev-equivalents of authorship now machine-produced.`
|
|
40
|
+
: 'At team scale, this many dev-equivalents move to the machine.',
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
label: 'Value capture (offshore)',
|
|
44
|
+
value: `~$${Math.round(annualOutflowTeam).toLocaleString()}/yr`,
|
|
45
|
+
note: 'Hard-currency spend leaving to a foreign AI vendor — value captured offshore, not by the local worker.',
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
label: 'Skill ladder',
|
|
49
|
+
value: ai > 0.7 ? 'AT RISK' : ai > 0.4 ? 'watch' : 'intact',
|
|
50
|
+
note: ai > 0.7
|
|
51
|
+
? 'Above ~70%, juniors stop building the skill that makes seniors — premature deprofessionalization.'
|
|
52
|
+
: 'Humans still author enough core work to keep building expertise.',
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
label: 'Forex / tax base',
|
|
56
|
+
value: `$${Math.round(annualOutflowPerDev).toLocaleString()}/dev/yr imported`,
|
|
57
|
+
note: 'Locally-taxed wages give way to foreign-billed inference: income-tax erosion + a recurring forex import.',
|
|
58
|
+
},
|
|
59
|
+
];
|
|
60
|
+
|
|
61
|
+
const assumptions =
|
|
62
|
+
`Projection only. Assumes: team of ${team}, the local log window ≈ one month of work, ` +
|
|
63
|
+
`${days} workdays/yr. Spend is your measured/estimated local outflow scaled up — an order-of-magnitude shadow, not an audit.`;
|
|
64
|
+
|
|
65
|
+
return { projections, assumptions };
|
|
66
|
+
}
|
package/src/emissions.ts
CHANGED
|
@@ -10,9 +10,13 @@
|
|
|
10
10
|
// literature). We expose the method so the UI can label provenance honestly. We never
|
|
11
11
|
// claim precision we don't have.
|
|
12
12
|
|
|
13
|
-
// Energy per 1M OUTPUT tokens, by model class (kWh).
|
|
14
|
-
//
|
|
15
|
-
//
|
|
13
|
+
// Energy per 1M OUTPUT tokens, by model class (kWh).
|
|
14
|
+
// Grounded in: the paper's measured ~10 kWh / 15.1M output on Opus-class (~0.66);
|
|
15
|
+
// EcoLogits (genai-impact) per-call energy methodology; and the Hugging Face AI Energy
|
|
16
|
+
// Score (which ranks small/mid/large model inference energy). Larger models = more
|
|
17
|
+
// active parameters per token = more energy. These remain order-of-magnitude class
|
|
18
|
+
// estimates (the literature spread is ~4-20x), labelled "estimate" in the UI — not vendor
|
|
19
|
+
// figures. CodeCarbon, when present, overrides these with measured hardware energy.
|
|
16
20
|
const MODEL_ENERGY_KWH_PER_M_OUTPUT: Record<string, number> = {
|
|
17
21
|
'opus': 0.66, // large frontier (Claude Opus, GPT-4 class)
|
|
18
22
|
'sonnet': 0.30, // mid (Claude Sonnet, GPT-4o)
|
package/src/insights.ts
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
// Insight rules engine — turns the raw metrics into meaning + one action.
|
|
2
|
+
//
|
|
3
|
+
// The numbers (75% AI, $63, blast radius HIGH) are data. Insights are what they MEAN
|
|
4
|
+
// together: high AI authorship is only alarming if your agents can also deploy; a low
|
|
5
|
+
// AI% next to heavy token use usually means missing trailers, not human authorship.
|
|
6
|
+
// Each rule combines signals and returns a plain message + a concrete next step.
|
|
7
|
+
|
|
8
|
+
import type { AuthorshipStats } from './git';
|
|
9
|
+
import type { CarbonStats } from './carbon';
|
|
10
|
+
import type { CapabilitiesStats } from './capabilities';
|
|
11
|
+
|
|
12
|
+
export type Severity = 'critical' | 'warn' | 'info' | 'good';
|
|
13
|
+
|
|
14
|
+
export interface Insight {
|
|
15
|
+
severity: Severity;
|
|
16
|
+
title: string; // short headline
|
|
17
|
+
detail: string; // one plain sentence of why
|
|
18
|
+
action: string; // the one thing to do
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const RANK: Record<Severity, number> = { critical: 0, warn: 1, info: 2, good: 3 };
|
|
22
|
+
|
|
23
|
+
export interface InsightInput {
|
|
24
|
+
authorship: AuthorshipStats | null;
|
|
25
|
+
carbon: CarbonStats | null;
|
|
26
|
+
caps: CapabilitiesStats | null;
|
|
27
|
+
policyCap?: number; // 0..1, default 0.70
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function deriveInsights({ authorship, carbon, caps, policyCap = 0.70 }: InsightInput): Insight[] {
|
|
31
|
+
const out: Insight[] = [];
|
|
32
|
+
const ai = authorship ? authorship.ratio : null;
|
|
33
|
+
const cachePct = carbon && carbon.totalTokens ? (carbon.cacheReadTokens / carbon.totalTokens) * 100 : null;
|
|
34
|
+
const blast = caps ? caps.blastRadius : null;
|
|
35
|
+
const writeOrDeploy = caps ? caps.mcps.filter(m => ['money', 'exec', 'deploy', 'write-remote', 'write-local'].includes(m.reach)).length : 0;
|
|
36
|
+
const heavyTokens = carbon ? carbon.totalTokens > 1_000_000 : false;
|
|
37
|
+
|
|
38
|
+
// 1. The compound one: high reliance AND high reach = you may not own code that can ship.
|
|
39
|
+
if (ai !== null && ai > 0.7 && (blast === 'HIGH' || blast === 'CRITICAL')) {
|
|
40
|
+
out.push({
|
|
41
|
+
severity: 'critical',
|
|
42
|
+
title: 'High reliance + high reach',
|
|
43
|
+
detail: `AI wrote ${(ai * 100).toFixed(0)}% here and your agents can ${writeOrDeploy ? 'write/deploy' : 'reach external services'}. You may not own code that can ship to prod.`,
|
|
44
|
+
action: 'Review the core paths yourself before delegating more this session.',
|
|
45
|
+
});
|
|
46
|
+
} else if (ai !== null && ai > 0.7) {
|
|
47
|
+
// 2. High reliance alone.
|
|
48
|
+
out.push({
|
|
49
|
+
severity: 'warn',
|
|
50
|
+
title: 'You are mostly reviewing, not writing',
|
|
51
|
+
detail: `AI wrote ${(ai * 100).toFixed(0)}% of recent commits — you risk losing the skill to debug it unaided.`,
|
|
52
|
+
action: 'Read the AI-written code through, or hand-write the next core change.',
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// 3. Honesty rule: low AI% next to heavy token use = missing trailers, not human authorship.
|
|
57
|
+
if (ai !== null && ai < 0.1 && heavyTokens) {
|
|
58
|
+
out.push({
|
|
59
|
+
severity: 'info',
|
|
60
|
+
title: 'Low AI% may be misleading',
|
|
61
|
+
detail: 'Heavy token use but few AI-tagged commits — your agent probably is not writing Co-Authored-By trailers.',
|
|
62
|
+
action: 'Treat the authorship number as a floor, not the truth, until trailers are on.',
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// 4. Reach / blast radius, independent of authorship.
|
|
67
|
+
if (caps && (blast === 'CRITICAL' || blast === 'HIGH')) {
|
|
68
|
+
out.push({
|
|
69
|
+
severity: blast === 'CRITICAL' ? 'critical' : 'warn',
|
|
70
|
+
title: `Blast radius ${blast}`,
|
|
71
|
+
detail: `If an agent (or a prompt injection) drives your tools, it ${caps.blastReasons.slice(0, 2).join(' and ') || 'has broad reach'}.`,
|
|
72
|
+
action: 'Disable the write/deploy MCP tools you do not need this session.',
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// 5. Cache waste = where the money goes.
|
|
77
|
+
if (cachePct !== null && cachePct > 80) {
|
|
78
|
+
out.push({
|
|
79
|
+
severity: 'warn',
|
|
80
|
+
title: 'Most of your spend is re-sent context',
|
|
81
|
+
detail: `${cachePct.toFixed(0)}% of your tokens just re-read old context — that is most of the bill, not new work.`,
|
|
82
|
+
action: 'Start fresh sessions for new tasks; keep context tight.',
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// 6. Over the policy limit.
|
|
87
|
+
if (ai !== null && ai > policyCap) {
|
|
88
|
+
out.push({
|
|
89
|
+
severity: 'warn',
|
|
90
|
+
title: 'Over your AI-authorship limit',
|
|
91
|
+
detail: `AI authorship is ${(ai * 100).toFixed(0)}%, over your ${(policyCap * 100).toFixed(0)}% limit.`,
|
|
92
|
+
action: 'Either raise the cap deliberately, or write the next change yourself.',
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// 7. Nothing wrong — say so (don't manufacture alarm).
|
|
97
|
+
if (out.length === 0) {
|
|
98
|
+
out.push({
|
|
99
|
+
severity: 'good',
|
|
100
|
+
title: 'Low risk',
|
|
101
|
+
detail: ai !== null
|
|
102
|
+
? `You wrote most of this (${(100 - ai * 100).toFixed(0)}%) and your agents have limited reach.`
|
|
103
|
+
: 'No AI logs or git history found to flag.',
|
|
104
|
+
action: 'Carry on — re-run before your next big delegation.',
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return out.sort((a, b) => RANK[a.severity] - RANK[b.severity]);
|
|
109
|
+
}
|