framein 0.0.4 → 0.0.5
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 +262 -226
- package/dist/adr.js +17 -17
- package/dist/anomaly.js +39 -39
- package/dist/bin.js +27 -27
- package/dist/blast.js +51 -51
- package/dist/brief.js +21 -21
- package/dist/capsule.js +91 -64
- package/dist/challenge.js +195 -0
- package/dist/cli.js +2150 -2090
- package/dist/db.js +7 -7
- package/dist/debt.js +42 -42
- package/dist/delegate.js +71 -64
- package/dist/detect.js +118 -118
- package/dist/disagree.js +106 -85
- package/dist/evidence.js +72 -72
- package/dist/fileWriter.js +35 -35
- package/dist/ingest.js +38 -38
- package/dist/managedBlock.js +101 -101
- package/dist/mcpRegister.js +85 -85
- package/dist/mcpServer.js +138 -138
- package/dist/palette.js +63 -63
- package/dist/projector.js +65 -65
- package/dist/quota.js +30 -30
- package/dist/recipe.js +55 -55
- package/dist/rescue.js +38 -38
- package/dist/roles.js +61 -61
- package/dist/select.js +50 -50
- package/dist/shell.js +127 -127
- package/dist/stats.js +74 -74
- package/dist/store.js +277 -277
- package/dist/task.js +94 -94
- package/dist/trust.js +39 -39
- package/dist/types.js +3 -3
- package/dist/ui/banner.js +32 -32
- package/dist/ui/capabilities.js +35 -35
- package/dist/ui/theme.js +49 -49
- package/dist/wrappers.js +62 -63
- package/package.json +46 -45
package/dist/shell.js
CHANGED
|
@@ -1,127 +1,127 @@
|
|
|
1
|
-
// Optional interactive `framein` lobby (ADR-0010, layer 4). A zero-dep readline *switchboard*: run framein
|
|
2
|
-
// verbs inline, switch the lead agent, and hand the terminal to a lead's NATIVE TUI via stdio:'inherit'
|
|
3
|
-
// (framein pauses while the lead drives, resumes on exit). Simultaneous overlay of framein + a live TUI
|
|
4
|
-
// would require node-pty (a native dep) — deferred/optional, never bundled (ADR-0010). This module is the
|
|
5
|
-
// PURE line router (fully unit-tested); the I/O loop (readline + spawn) is the thin wrapper in cli.ts.
|
|
6
|
-
import { AGENTS } from './types.js';
|
|
7
|
-
import { isAgent } from './roles.js';
|
|
8
|
-
/** Split a lobby line into tokens, honoring "double" and 'single' quotes (quotes stripped) so multi-word
|
|
9
|
-
* values like `start "add Google login"` survive as one token instead of leaking literal quotes. */
|
|
10
|
-
export function tokenizeLine(line) {
|
|
11
|
-
const tokens = [];
|
|
12
|
-
const re = /"([^"]*)"|'([^']*)'|(\S+)/g;
|
|
13
|
-
let m;
|
|
14
|
-
while ((m = re.exec(line)) !== null)
|
|
15
|
-
tokens.push(m[1] ?? m[2] ?? m[3]);
|
|
16
|
-
return tokens;
|
|
17
|
-
}
|
|
18
|
-
/** Map one input line to an action. Pure: no I/O, no agent launch — the loop performs the effect. */
|
|
19
|
-
export function routeShellLine(line, state) {
|
|
20
|
-
const trimmed = line.trim();
|
|
21
|
-
if (!trimmed)
|
|
22
|
-
return { kind: 'noop' };
|
|
23
|
-
const tokens = tokenizeLine(trimmed);
|
|
24
|
-
const head = tokens[0].toLowerCase();
|
|
25
|
-
const rest = tokens.slice(1);
|
|
26
|
-
if (head === 'exit' || head === 'quit' || head === '/exit' || head === '/quit')
|
|
27
|
-
return { kind: 'exit' };
|
|
28
|
-
if (head === 'help' || head === '/help' || head === '?')
|
|
29
|
-
return { kind: 'help' };
|
|
30
|
-
if (head === '/lead') {
|
|
31
|
-
if (rest.length === 0)
|
|
32
|
-
return { kind: 'pickLead' }; // bare /lead → interactive picker (TTY); printed fallback otherwise
|
|
33
|
-
const agent = rest[0].toLowerCase();
|
|
34
|
-
if (!isAgent(agent))
|
|
35
|
-
return { kind: 'error', message: `Unknown agent '${rest[0]}'. Valid: ${AGENTS.join(', ')}` };
|
|
36
|
-
return { kind: 'setLead', agent };
|
|
37
|
-
}
|
|
38
|
-
if (head === '/go') {
|
|
39
|
-
const prompt = rest.join(' ').trim();
|
|
40
|
-
return { kind: 'launchLead', agent: state.lead, prompt: prompt || undefined };
|
|
41
|
-
}
|
|
42
|
-
if (head === '/trust')
|
|
43
|
-
return { kind: 'toggleTrust' }; // arm/disarm permission-bypass for the next /go (time-boxed)
|
|
44
|
-
// A bare agent name (optionally with a prompt) switches to + launches that agent's native TUI.
|
|
45
|
-
if (isAgent(head)) {
|
|
46
|
-
const prompt = rest.join(' ').trim();
|
|
47
|
-
return { kind: 'launchLead', agent: head, prompt: prompt || undefined };
|
|
48
|
-
}
|
|
49
|
-
// Otherwise it's a framein command — `/verify` and `verify` both reach the engine.
|
|
50
|
-
const verb = head.startsWith('/') ? head.slice(1) : head;
|
|
51
|
-
if (verb === 'lobby' || verb === 'shell')
|
|
52
|
-
return { kind: 'error', message: 'Already in the lobby.' };
|
|
53
|
-
return { kind: 'engine', args: [verb, ...rest] };
|
|
54
|
-
}
|
|
55
|
-
/** Rows for the context card shown right before handing the terminal to a lead's native TUI (`/go`).
|
|
56
|
-
* Pure: the loop reads these from the store and renders them, so the user carries intent INTO the
|
|
57
|
-
* native UI. We surface state here; we never screen-scrape the TUI itself (ADR-0009). */
|
|
58
|
-
export function handoffCardRows(info) {
|
|
59
|
-
const rows = [['lead', info.lead]];
|
|
60
|
-
if (info.reviewer)
|
|
61
|
-
rows.push(['reviewer', info.reviewer]);
|
|
62
|
-
rows.push(['task', info.goal && info.goal.trim() ? info.goal : 'no active contract']);
|
|
63
|
-
if (info.lastGreen)
|
|
64
|
-
rows.push(['last green', info.lastGreen]);
|
|
65
|
-
if (info.blocker)
|
|
66
|
-
rows.push(['blocker', info.blocker]);
|
|
67
|
-
return rows;
|
|
68
|
-
}
|
|
69
|
-
export function renderShellHelp() {
|
|
70
|
-
return [
|
|
71
|
-
'framein lobby — your switchboard for AI coding. The leading / is optional. Every verb is',
|
|
72
|
-
'deterministic and LOCAL: you don’t need an agent to run one (the agent just decides when to).',
|
|
73
|
-
'',
|
|
74
|
-
' Lobby-only — choose who drives & hand off (these live in the lobby, not inside agents):',
|
|
75
|
-
' /lead switch the lead agent (↑↓ · type to filter · enter)',
|
|
76
|
-
' /go [task] hand the terminal to the lead — work there, exit (Ctrl-D) to return',
|
|
77
|
-
' /trust arm/disarm permission-bypass for /go (off by default · 30m)',
|
|
78
|
-
' /exit leave the lobby (or Ctrl-D on an empty line)',
|
|
79
|
-
` ${AGENTS.join(' · ')} shortcut: jump straight into that agent (e.g. \`codex fix the bug\`)`,
|
|
80
|
-
'',
|
|
81
|
-
' framein verbs — run here, inside your agent (/fr:verify · $fr-verify), or as `framein <verb>`:',
|
|
82
|
-
' start <goal> define what “done” means — the Task Contract',
|
|
83
|
-
' verify run build/test, check validation against the contract',
|
|
84
|
-
' ship deployment-readiness gate (blocks if not ready)',
|
|
85
|
-
' risk blast-radius of the current change',
|
|
86
|
-
' rescue stuck in a fix-loop? show the loop + safe options',
|
|
87
|
-
' challenge · decide have another model argue a proposal, then you rule',
|
|
88
|
-
' task · capsule show/amend the contract · carry state across a model switch',
|
|
89
|
-
' status project · contract · state',
|
|
90
|
-
'',
|
|
91
|
-
' Also in the lobby / terminal (not wrapped into agents): init · stats · explain',
|
|
92
|
-
'',
|
|
93
|
-
'Tip: type / to browse (filters as you type · ↑↓ to pick · ⏎ runs) · full list: framein --help · manual: docs/MANUAL.md',
|
|
94
|
-
].join('\n');
|
|
95
|
-
}
|
|
96
|
-
/** Verbs offered by lobby Tab-completion (a friendly subset of the full CLI surface). */
|
|
97
|
-
export const LOBBY_VERBS = ['init', 'start', 'verify', 'ship', 'rescue', 'status', 'stats', 'explain', 'risk', 'task', 'checkpoint', 'capsule'];
|
|
98
|
-
const LOBBY_COMMANDS = [...LOBBY_VERBS, ...LOBBY_VERBS.map((v) => `/${v}`), '/lead', '/go', '/trust', '/help', 'exit', 'quit'];
|
|
99
|
-
/** readline completer for the lobby line editor (pure). Completes agent names after `/lead `, otherwise
|
|
100
|
-
* the first token against the verb / slash-command list. Returns [matches, fragmentBeingCompleted]. */
|
|
101
|
-
export function lobbyCompleter(line) {
|
|
102
|
-
const lead = line.match(/^\/lead\s+(\S*)$/i);
|
|
103
|
-
if (lead) {
|
|
104
|
-
const frag = lead[1];
|
|
105
|
-
const hits = AGENTS.filter((a) => a.startsWith(frag.toLowerCase()));
|
|
106
|
-
return [hits.length ? hits : AGENTS.slice(), frag];
|
|
107
|
-
}
|
|
108
|
-
const hits = LOBBY_COMMANDS.filter((c) => c.startsWith(line));
|
|
109
|
-
return [hits, line];
|
|
110
|
-
}
|
|
111
|
-
/** Commands shown in the lobby's live `/` palette (the inline menu opened by typing `/`). All slash-
|
|
112
|
-
* prefixed for consistency; the leading `/` is what surfaces the menu. Curated to what's actually
|
|
113
|
-
* useful FROM the lobby — the deterministic checks ("local, no agent" → they run without an LLM) plus
|
|
114
|
-
* the switchboard verbs. The real coding happens after /go, inside the lead's own UI. (palette.ts) */
|
|
115
|
-
export const LOBBY_PALETTE = [
|
|
116
|
-
{ cmd: '/go', desc: 'hand the terminal to the lead agent — where the coding happens' },
|
|
117
|
-
{ cmd: '/lead', desc: 'switch the lead agent (claude · codex · gemini)' },
|
|
118
|
-
{ cmd: '/trust', desc: 'arm/disarm permission-bypass for /go (time-boxed)' },
|
|
119
|
-
{ cmd: '/status', desc: 'project · contract · state (local, no agent)' },
|
|
120
|
-
{ cmd: '/verify', desc: 'run build/test, check validation vs the contract (local, no agent)' },
|
|
121
|
-
{ cmd: '/ship', desc: 'deployment-readiness check (local, no agent)' },
|
|
122
|
-
{ cmd: '/risk', desc: 'blast-radius of the current changes (local, no agent)' },
|
|
123
|
-
{ cmd: '/rescue', desc: 'stuck? detect the loop + get options (local, no agent)' },
|
|
124
|
-
{ cmd: '/init', desc: 'set up framein in this folder' },
|
|
125
|
-
{ cmd: '/help', desc: 'all commands' },
|
|
126
|
-
{ cmd: '/exit', desc: 'leave the lobby' },
|
|
127
|
-
];
|
|
1
|
+
// Optional interactive `framein` lobby (ADR-0010, layer 4). A zero-dep readline *switchboard*: run framein
|
|
2
|
+
// verbs inline, switch the lead agent, and hand the terminal to a lead's NATIVE TUI via stdio:'inherit'
|
|
3
|
+
// (framein pauses while the lead drives, resumes on exit). Simultaneous overlay of framein + a live TUI
|
|
4
|
+
// would require node-pty (a native dep) — deferred/optional, never bundled (ADR-0010). This module is the
|
|
5
|
+
// PURE line router (fully unit-tested); the I/O loop (readline + spawn) is the thin wrapper in cli.ts.
|
|
6
|
+
import { AGENTS } from './types.js';
|
|
7
|
+
import { isAgent } from './roles.js';
|
|
8
|
+
/** Split a lobby line into tokens, honoring "double" and 'single' quotes (quotes stripped) so multi-word
|
|
9
|
+
* values like `start "add Google login"` survive as one token instead of leaking literal quotes. */
|
|
10
|
+
export function tokenizeLine(line) {
|
|
11
|
+
const tokens = [];
|
|
12
|
+
const re = /"([^"]*)"|'([^']*)'|(\S+)/g;
|
|
13
|
+
let m;
|
|
14
|
+
while ((m = re.exec(line)) !== null)
|
|
15
|
+
tokens.push(m[1] ?? m[2] ?? m[3]);
|
|
16
|
+
return tokens;
|
|
17
|
+
}
|
|
18
|
+
/** Map one input line to an action. Pure: no I/O, no agent launch — the loop performs the effect. */
|
|
19
|
+
export function routeShellLine(line, state) {
|
|
20
|
+
const trimmed = line.trim();
|
|
21
|
+
if (!trimmed)
|
|
22
|
+
return { kind: 'noop' };
|
|
23
|
+
const tokens = tokenizeLine(trimmed);
|
|
24
|
+
const head = tokens[0].toLowerCase();
|
|
25
|
+
const rest = tokens.slice(1);
|
|
26
|
+
if (head === 'exit' || head === 'quit' || head === '/exit' || head === '/quit')
|
|
27
|
+
return { kind: 'exit' };
|
|
28
|
+
if (head === 'help' || head === '/help' || head === '?')
|
|
29
|
+
return { kind: 'help' };
|
|
30
|
+
if (head === '/lead') {
|
|
31
|
+
if (rest.length === 0)
|
|
32
|
+
return { kind: 'pickLead' }; // bare /lead → interactive picker (TTY); printed fallback otherwise
|
|
33
|
+
const agent = rest[0].toLowerCase();
|
|
34
|
+
if (!isAgent(agent))
|
|
35
|
+
return { kind: 'error', message: `Unknown agent '${rest[0]}'. Valid: ${AGENTS.join(', ')}` };
|
|
36
|
+
return { kind: 'setLead', agent };
|
|
37
|
+
}
|
|
38
|
+
if (head === '/go') {
|
|
39
|
+
const prompt = rest.join(' ').trim();
|
|
40
|
+
return { kind: 'launchLead', agent: state.lead, prompt: prompt || undefined };
|
|
41
|
+
}
|
|
42
|
+
if (head === '/trust')
|
|
43
|
+
return { kind: 'toggleTrust' }; // arm/disarm permission-bypass for the next /go (time-boxed)
|
|
44
|
+
// A bare agent name (optionally with a prompt) switches to + launches that agent's native TUI.
|
|
45
|
+
if (isAgent(head)) {
|
|
46
|
+
const prompt = rest.join(' ').trim();
|
|
47
|
+
return { kind: 'launchLead', agent: head, prompt: prompt || undefined };
|
|
48
|
+
}
|
|
49
|
+
// Otherwise it's a framein command — `/verify` and `verify` both reach the engine.
|
|
50
|
+
const verb = head.startsWith('/') ? head.slice(1) : head;
|
|
51
|
+
if (verb === 'lobby' || verb === 'shell')
|
|
52
|
+
return { kind: 'error', message: 'Already in the lobby.' };
|
|
53
|
+
return { kind: 'engine', args: [verb, ...rest] };
|
|
54
|
+
}
|
|
55
|
+
/** Rows for the context card shown right before handing the terminal to a lead's native TUI (`/go`).
|
|
56
|
+
* Pure: the loop reads these from the store and renders them, so the user carries intent INTO the
|
|
57
|
+
* native UI. We surface state here; we never screen-scrape the TUI itself (ADR-0009). */
|
|
58
|
+
export function handoffCardRows(info) {
|
|
59
|
+
const rows = [['lead', info.lead]];
|
|
60
|
+
if (info.reviewer)
|
|
61
|
+
rows.push(['reviewer', info.reviewer]);
|
|
62
|
+
rows.push(['task', info.goal && info.goal.trim() ? info.goal : 'no active contract']);
|
|
63
|
+
if (info.lastGreen)
|
|
64
|
+
rows.push(['last green', info.lastGreen]);
|
|
65
|
+
if (info.blocker)
|
|
66
|
+
rows.push(['blocker', info.blocker]);
|
|
67
|
+
return rows;
|
|
68
|
+
}
|
|
69
|
+
export function renderShellHelp() {
|
|
70
|
+
return [
|
|
71
|
+
'framein lobby — your switchboard for AI coding. The leading / is optional. Every verb is',
|
|
72
|
+
'deterministic and LOCAL: you don’t need an agent to run one (the agent just decides when to).',
|
|
73
|
+
'',
|
|
74
|
+
' Lobby-only — choose who drives & hand off (these live in the lobby, not inside agents):',
|
|
75
|
+
' /lead switch the lead agent (↑↓ · type to filter · enter)',
|
|
76
|
+
' /go [task] hand the terminal to the lead — work there, exit (Ctrl-D) to return',
|
|
77
|
+
' /trust arm/disarm permission-bypass for /go (off by default · 30m)',
|
|
78
|
+
' /exit leave the lobby (or Ctrl-D on an empty line)',
|
|
79
|
+
` ${AGENTS.join(' · ')} shortcut: jump straight into that agent (e.g. \`codex fix the bug\`)`,
|
|
80
|
+
'',
|
|
81
|
+
' framein verbs — run here, inside your agent (/fr:verify · $fr-verify), or as `framein <verb>`:',
|
|
82
|
+
' start <goal> define what “done” means — the Task Contract',
|
|
83
|
+
' verify run build/test, check validation against the contract',
|
|
84
|
+
' ship deployment-readiness gate (blocks if not ready)',
|
|
85
|
+
' risk blast-radius of the current change',
|
|
86
|
+
' rescue stuck in a fix-loop? show the loop + safe options',
|
|
87
|
+
' challenge · decide have another model argue a proposal, then you rule',
|
|
88
|
+
' task · capsule show/amend the contract · carry state across a model switch',
|
|
89
|
+
' status project · contract · state',
|
|
90
|
+
'',
|
|
91
|
+
' Also in the lobby / terminal (not wrapped into agents): init · stats · explain',
|
|
92
|
+
'',
|
|
93
|
+
'Tip: type / to browse (filters as you type · ↑↓ to pick · ⏎ runs) · full list: framein --help · manual: docs/MANUAL.md',
|
|
94
|
+
].join('\n');
|
|
95
|
+
}
|
|
96
|
+
/** Verbs offered by lobby Tab-completion (a friendly subset of the full CLI surface). */
|
|
97
|
+
export const LOBBY_VERBS = ['init', 'start', 'verify', 'ship', 'rescue', 'status', 'stats', 'explain', 'risk', 'task', 'checkpoint', 'capsule'];
|
|
98
|
+
const LOBBY_COMMANDS = [...LOBBY_VERBS, ...LOBBY_VERBS.map((v) => `/${v}`), '/lead', '/go', '/trust', '/help', 'exit', 'quit'];
|
|
99
|
+
/** readline completer for the lobby line editor (pure). Completes agent names after `/lead `, otherwise
|
|
100
|
+
* the first token against the verb / slash-command list. Returns [matches, fragmentBeingCompleted]. */
|
|
101
|
+
export function lobbyCompleter(line) {
|
|
102
|
+
const lead = line.match(/^\/lead\s+(\S*)$/i);
|
|
103
|
+
if (lead) {
|
|
104
|
+
const frag = lead[1];
|
|
105
|
+
const hits = AGENTS.filter((a) => a.startsWith(frag.toLowerCase()));
|
|
106
|
+
return [hits.length ? hits : AGENTS.slice(), frag];
|
|
107
|
+
}
|
|
108
|
+
const hits = LOBBY_COMMANDS.filter((c) => c.startsWith(line));
|
|
109
|
+
return [hits, line];
|
|
110
|
+
}
|
|
111
|
+
/** Commands shown in the lobby's live `/` palette (the inline menu opened by typing `/`). All slash-
|
|
112
|
+
* prefixed for consistency; the leading `/` is what surfaces the menu. Curated to what's actually
|
|
113
|
+
* useful FROM the lobby — the deterministic checks ("local, no agent" → they run without an LLM) plus
|
|
114
|
+
* the switchboard verbs. The real coding happens after /go, inside the lead's own UI. (palette.ts) */
|
|
115
|
+
export const LOBBY_PALETTE = [
|
|
116
|
+
{ cmd: '/go', desc: 'hand the terminal to the lead agent — where the coding happens' },
|
|
117
|
+
{ cmd: '/lead', desc: 'switch the lead agent (claude · codex · gemini)' },
|
|
118
|
+
{ cmd: '/trust', desc: 'arm/disarm permission-bypass for /go (time-boxed)' },
|
|
119
|
+
{ cmd: '/status', desc: 'project · contract · state (local, no agent)' },
|
|
120
|
+
{ cmd: '/verify', desc: 'run build/test, check validation vs the contract (local, no agent)' },
|
|
121
|
+
{ cmd: '/ship', desc: 'deployment-readiness check (local, no agent)' },
|
|
122
|
+
{ cmd: '/risk', desc: 'blast-radius of the current changes (local, no agent)' },
|
|
123
|
+
{ cmd: '/rescue', desc: 'stuck? detect the loop + get options (local, no agent)' },
|
|
124
|
+
{ cmd: '/init', desc: 'set up framein in this folder' },
|
|
125
|
+
{ cmd: '/help', desc: 'all commands' },
|
|
126
|
+
{ cmd: '/exit', desc: 'leave the lobby' },
|
|
127
|
+
];
|
package/dist/stats.js
CHANGED
|
@@ -1,74 +1,74 @@
|
|
|
1
|
-
// Repo-local Routing (F-LOOP-7, ADR-0008): route by THIS repo's actual results, not generic model
|
|
2
|
-
// reputation, and — crucially — EXPLAIN the choice rather than auto-deciding silently (trust comes
|
|
3
|
-
// from "why", not magic). Pure: derive per-agent stats from the ledger, then score + explain via
|
|
4
|
-
// roles.scoreAgent. Accumulating richer signals (first-try pass rate, human reverts) comes later.
|
|
5
|
-
import { AGENTS } from './types.js';
|
|
6
|
-
import { scoreAgent, DEFAULT_ROLE_PRIORITY } from './roles.js';
|
|
7
|
-
import { PLAIN } from './ui/theme.js';
|
|
8
|
-
const isAgentName = (s) => AGENTS.includes(s);
|
|
9
|
-
/** The trailing token of a ledger target is the agent: "reviewer:codex" -> codex, "codex" -> codex. */
|
|
10
|
-
function agentOf(target) {
|
|
11
|
-
const tail = target.split(':').pop() ?? '';
|
|
12
|
-
return isAgentName(tail) ? tail : undefined;
|
|
13
|
-
}
|
|
14
|
-
export function computeRepoStats(ledger) {
|
|
15
|
-
const stats = {};
|
|
16
|
-
const ensure = (a) => (stats[a] ??= { delegations: 0, failures: 0, quotaHits: 0 });
|
|
17
|
-
for (const e of ledger) {
|
|
18
|
-
const a = agentOf(e.target);
|
|
19
|
-
if (!a)
|
|
20
|
-
continue;
|
|
21
|
-
if (e.kind === 'delegated')
|
|
22
|
-
ensure(a).delegations++;
|
|
23
|
-
else if (e.kind === 'delegate-fail') {
|
|
24
|
-
const s = ensure(a);
|
|
25
|
-
s.delegations++;
|
|
26
|
-
s.failures++;
|
|
27
|
-
}
|
|
28
|
-
else if (e.kind === 'quota')
|
|
29
|
-
ensure(a).quotaHits++;
|
|
30
|
-
}
|
|
31
|
-
return stats;
|
|
32
|
-
}
|
|
33
|
-
function routeReasons(st) {
|
|
34
|
-
if (!st || st.delegations === 0)
|
|
35
|
-
return ['no local track record yet (using role defaults)'];
|
|
36
|
-
const success = st.delegations - st.failures;
|
|
37
|
-
const reasons = [`+ ${Math.round((success / st.delegations) * 100)}% delegation success in this repo (${success}/${st.delegations})`];
|
|
38
|
-
if (st.failures)
|
|
39
|
-
reasons.push(`- ${st.failures} failure${st.failures > 1 ? 's' : ''}`);
|
|
40
|
-
reasons.push(st.quotaHits ? `- ${st.quotaHits} quota hit${st.quotaHits > 1 ? 's' : ''}` : '+ no quota issues');
|
|
41
|
-
return reasons;
|
|
42
|
-
}
|
|
43
|
-
export function explainRoute(role, ctx, stats) {
|
|
44
|
-
const priority = (ctx.rolePriority ?? DEFAULT_ROLE_PRIORITY)[role] ?? [...AGENTS];
|
|
45
|
-
const scored = priority
|
|
46
|
-
.map((agent) => ({ agent, score: scoreAgent(agent, { ...ctx, role, repoStats: stats }) }))
|
|
47
|
-
.filter((s) => Number.isFinite(s.score))
|
|
48
|
-
.sort((a, b) => b.score - a.score);
|
|
49
|
-
const best = scored[0];
|
|
50
|
-
const second = scored[1];
|
|
51
|
-
let alternative;
|
|
52
|
-
if (best && second) {
|
|
53
|
-
const total = best.score + second.score;
|
|
54
|
-
alternative = { agent: second.agent, confidence: total > 0 ? Number((second.score / total).toFixed(2)) : 0 };
|
|
55
|
-
}
|
|
56
|
-
return { role, agent: best?.agent ?? null, reasons: best ? routeReasons(stats[best.agent]) : ['no eligible agent'], alternative };
|
|
57
|
-
}
|
|
58
|
-
export function renderRouteExplain(e, ui = PLAIN) {
|
|
59
|
-
const lines = [e.agent ? `Selected ${ui.tone(e.agent, 'brand')} as ${e.role}.` : `No eligible agent for ${e.role}.`, ui.tone('Why:', 'muted')];
|
|
60
|
-
for (const r of e.reasons)
|
|
61
|
-
lines.push(` ${r}`);
|
|
62
|
-
if (e.alternative)
|
|
63
|
-
lines.push(ui.tone(`Alternative: ${e.alternative.agent}, confidence ${e.alternative.confidence}`, 'muted'));
|
|
64
|
-
return lines.join('\n');
|
|
65
|
-
}
|
|
66
|
-
export function renderStats(stats, ui = PLAIN) {
|
|
67
|
-
const entries = Object.entries(stats);
|
|
68
|
-
if (entries.length === 0)
|
|
69
|
-
return 'No repo-local stats yet. Delegations (`framein ask --run`) accumulate them.';
|
|
70
|
-
const lines = [ui.tone('Repo-local agent stats (from the ledger):', 'muted')];
|
|
71
|
-
for (const [a, st] of entries)
|
|
72
|
-
lines.push(` ${ui.tone(a, 'brand')}: ${st.delegations} delegations, ${st.failures} failed, ${st.quotaHits} quota`);
|
|
73
|
-
return lines.join('\n');
|
|
74
|
-
}
|
|
1
|
+
// Repo-local Routing (F-LOOP-7, ADR-0008): route by THIS repo's actual results, not generic model
|
|
2
|
+
// reputation, and — crucially — EXPLAIN the choice rather than auto-deciding silently (trust comes
|
|
3
|
+
// from "why", not magic). Pure: derive per-agent stats from the ledger, then score + explain via
|
|
4
|
+
// roles.scoreAgent. Accumulating richer signals (first-try pass rate, human reverts) comes later.
|
|
5
|
+
import { AGENTS } from './types.js';
|
|
6
|
+
import { scoreAgent, DEFAULT_ROLE_PRIORITY } from './roles.js';
|
|
7
|
+
import { PLAIN } from './ui/theme.js';
|
|
8
|
+
const isAgentName = (s) => AGENTS.includes(s);
|
|
9
|
+
/** The trailing token of a ledger target is the agent: "reviewer:codex" -> codex, "codex" -> codex. */
|
|
10
|
+
function agentOf(target) {
|
|
11
|
+
const tail = target.split(':').pop() ?? '';
|
|
12
|
+
return isAgentName(tail) ? tail : undefined;
|
|
13
|
+
}
|
|
14
|
+
export function computeRepoStats(ledger) {
|
|
15
|
+
const stats = {};
|
|
16
|
+
const ensure = (a) => (stats[a] ??= { delegations: 0, failures: 0, quotaHits: 0 });
|
|
17
|
+
for (const e of ledger) {
|
|
18
|
+
const a = agentOf(e.target);
|
|
19
|
+
if (!a)
|
|
20
|
+
continue;
|
|
21
|
+
if (e.kind === 'delegated')
|
|
22
|
+
ensure(a).delegations++;
|
|
23
|
+
else if (e.kind === 'delegate-fail') {
|
|
24
|
+
const s = ensure(a);
|
|
25
|
+
s.delegations++;
|
|
26
|
+
s.failures++;
|
|
27
|
+
}
|
|
28
|
+
else if (e.kind === 'quota')
|
|
29
|
+
ensure(a).quotaHits++;
|
|
30
|
+
}
|
|
31
|
+
return stats;
|
|
32
|
+
}
|
|
33
|
+
function routeReasons(st) {
|
|
34
|
+
if (!st || st.delegations === 0)
|
|
35
|
+
return ['no local track record yet (using role defaults)'];
|
|
36
|
+
const success = st.delegations - st.failures;
|
|
37
|
+
const reasons = [`+ ${Math.round((success / st.delegations) * 100)}% delegation success in this repo (${success}/${st.delegations})`];
|
|
38
|
+
if (st.failures)
|
|
39
|
+
reasons.push(`- ${st.failures} failure${st.failures > 1 ? 's' : ''}`);
|
|
40
|
+
reasons.push(st.quotaHits ? `- ${st.quotaHits} quota hit${st.quotaHits > 1 ? 's' : ''}` : '+ no quota issues');
|
|
41
|
+
return reasons;
|
|
42
|
+
}
|
|
43
|
+
export function explainRoute(role, ctx, stats) {
|
|
44
|
+
const priority = (ctx.rolePriority ?? DEFAULT_ROLE_PRIORITY)[role] ?? [...AGENTS];
|
|
45
|
+
const scored = priority
|
|
46
|
+
.map((agent) => ({ agent, score: scoreAgent(agent, { ...ctx, role, repoStats: stats }) }))
|
|
47
|
+
.filter((s) => Number.isFinite(s.score))
|
|
48
|
+
.sort((a, b) => b.score - a.score);
|
|
49
|
+
const best = scored[0];
|
|
50
|
+
const second = scored[1];
|
|
51
|
+
let alternative;
|
|
52
|
+
if (best && second) {
|
|
53
|
+
const total = best.score + second.score;
|
|
54
|
+
alternative = { agent: second.agent, confidence: total > 0 ? Number((second.score / total).toFixed(2)) : 0 };
|
|
55
|
+
}
|
|
56
|
+
return { role, agent: best?.agent ?? null, reasons: best ? routeReasons(stats[best.agent]) : ['no eligible agent'], alternative };
|
|
57
|
+
}
|
|
58
|
+
export function renderRouteExplain(e, ui = PLAIN) {
|
|
59
|
+
const lines = [e.agent ? `Selected ${ui.tone(e.agent, 'brand')} as ${e.role}.` : `No eligible agent for ${e.role}.`, ui.tone('Why:', 'muted')];
|
|
60
|
+
for (const r of e.reasons)
|
|
61
|
+
lines.push(` ${r}`);
|
|
62
|
+
if (e.alternative)
|
|
63
|
+
lines.push(ui.tone(`Alternative: ${e.alternative.agent}, confidence ${e.alternative.confidence}`, 'muted'));
|
|
64
|
+
return lines.join('\n');
|
|
65
|
+
}
|
|
66
|
+
export function renderStats(stats, ui = PLAIN) {
|
|
67
|
+
const entries = Object.entries(stats);
|
|
68
|
+
if (entries.length === 0)
|
|
69
|
+
return 'No repo-local stats yet. Delegations (`framein ask --run`) accumulate them.';
|
|
70
|
+
const lines = [ui.tone('Repo-local agent stats (from the ledger):', 'muted')];
|
|
71
|
+
for (const [a, st] of entries)
|
|
72
|
+
lines.push(` ${ui.tone(a, 'brand')}: ${st.delegations} delegations, ${st.failures} failed, ${st.quotaHits} quota`);
|
|
73
|
+
return lines.join('\n');
|
|
74
|
+
}
|