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/dist/recipe.js CHANGED
@@ -1,55 +1,55 @@
1
- // Frame Recipe (F-LOOP-8, ADR-0008): a VENDOR-NEUTRAL task protocol (feature/bugfix/ship) that we
2
- // COMPILE/PROJECT onto each CLI's native features — NOT "run a Claude skill inside Codex" (that's a
3
- // category error, ADR-0002/0004). The same numbered protocol body is emitted for every agent; only
4
- // the header naming the agent's native mechanism differs. Shared state stays in framein MCP + ledger.
5
- import { PLAIN } from './ui/theme.js';
6
- export const RECIPES = [
7
- { name: 'feature', trigger: 'feature', steps: [
8
- { role: 'lead', action: 'define_contract' },
9
- { role: 'implementer', action: 'implement' },
10
- { action: 'run_validation', required: ['tests', 'build'] },
11
- { role: 'reviewer', action: 'blocker_review', readOnly: true },
12
- { role: 'implementer', action: 'resolve_findings' },
13
- { action: 'human_approval' },
14
- ] },
15
- { name: 'bugfix', trigger: 'bugfix', steps: [
16
- { role: 'implementer', action: 'reproduce' },
17
- { role: 'reviewer', action: 'root_cause', readOnly: true },
18
- { role: 'implementer', action: 'minimal_fix' },
19
- { action: 'run_validation', required: ['regression_test'] },
20
- { action: 'human_approval' },
21
- ] },
22
- { name: 'ship', trigger: 'ship', steps: [
23
- { action: 'verify_changes', required: ['tests', 'build'] },
24
- { action: 'risk_check' },
25
- { role: 'explainer', action: 'ownership_brief' },
26
- { action: 'human_approval' },
27
- ] },
28
- ];
29
- // How each CLI expresses a recipe natively (we project onto these; we do not cross-execute).
30
- const NATIVE_MECHANISM = {
31
- claude: 'a Claude Skill + hooks + subagent guidance',
32
- codex: 'a Codex skill / plugin / workflow',
33
- gemini: 'a Gemini extension / skill / hooks',
34
- };
35
- export function listRecipes() { return RECIPES; }
36
- export function getRecipe(name) { return RECIPES.find((r) => r.name === name); }
37
- function stepLines(r) {
38
- return r.steps.map((s, i) => {
39
- const who = s.role ? `[${s.role}] ` : '';
40
- const ro = s.readOnly ? ' (read-only)' : '';
41
- const req = s.required ? ` — required: ${s.required.join(', ')}` : '';
42
- return `${i + 1}. ${who}${s.action}${ro}${req}`;
43
- });
44
- }
45
- export function renderRecipe(r, ui = PLAIN) {
46
- return [ui.tone(`recipe: ${r.name} (trigger: ${r.trigger})`, 'muted'), ...stepLines(r)].join('\n');
47
- }
48
- /** Project a recipe onto one agent's native mechanism. Body (numbered steps) is identical per agent. */
49
- export function compileRecipe(r, agent) {
50
- return [
51
- `# ${r.name} — compiled for ${agent} as ${NATIVE_MECHANISM[agent]}`,
52
- ...stepLines(r),
53
- 'Shared state: framein MCP + task ledger (the contract, decisions, and validation results all agents read).',
54
- ].join('\n');
55
- }
1
+ // Frame Recipe (F-LOOP-8, ADR-0008): a VENDOR-NEUTRAL task protocol (feature/bugfix/ship) that we
2
+ // COMPILE/PROJECT onto each CLI's native features — NOT "run a Claude skill inside Codex" (that's a
3
+ // category error, ADR-0002/0004). The same numbered protocol body is emitted for every agent; only
4
+ // the header naming the agent's native mechanism differs. Shared state stays in framein MCP + ledger.
5
+ import { PLAIN } from './ui/theme.js';
6
+ export const RECIPES = [
7
+ { name: 'feature', trigger: 'feature', steps: [
8
+ { role: 'lead', action: 'define_contract' },
9
+ { role: 'implementer', action: 'implement' },
10
+ { action: 'run_validation', required: ['tests', 'build'] },
11
+ { role: 'reviewer', action: 'blocker_review', readOnly: true },
12
+ { role: 'implementer', action: 'resolve_findings' },
13
+ { action: 'human_approval' },
14
+ ] },
15
+ { name: 'bugfix', trigger: 'bugfix', steps: [
16
+ { role: 'implementer', action: 'reproduce' },
17
+ { role: 'reviewer', action: 'root_cause', readOnly: true },
18
+ { role: 'implementer', action: 'minimal_fix' },
19
+ { action: 'run_validation', required: ['regression_test'] },
20
+ { action: 'human_approval' },
21
+ ] },
22
+ { name: 'ship', trigger: 'ship', steps: [
23
+ { action: 'verify_changes', required: ['tests', 'build'] },
24
+ { action: 'risk_check' },
25
+ { role: 'explainer', action: 'ownership_brief' },
26
+ { action: 'human_approval' },
27
+ ] },
28
+ ];
29
+ // How each CLI expresses a recipe natively (we project onto these; we do not cross-execute).
30
+ const NATIVE_MECHANISM = {
31
+ claude: 'a Claude Skill + hooks + subagent guidance',
32
+ codex: 'a Codex skill / plugin / workflow',
33
+ gemini: 'a Gemini extension / skill / hooks',
34
+ };
35
+ export function listRecipes() { return RECIPES; }
36
+ export function getRecipe(name) { return RECIPES.find((r) => r.name === name); }
37
+ function stepLines(r) {
38
+ return r.steps.map((s, i) => {
39
+ const who = s.role ? `[${s.role}] ` : '';
40
+ const ro = s.readOnly ? ' (read-only)' : '';
41
+ const req = s.required ? ` — required: ${s.required.join(', ')}` : '';
42
+ return `${i + 1}. ${who}${s.action}${ro}${req}`;
43
+ });
44
+ }
45
+ export function renderRecipe(r, ui = PLAIN) {
46
+ return [ui.tone(`recipe: ${r.name} (trigger: ${r.trigger})`, 'muted'), ...stepLines(r)].join('\n');
47
+ }
48
+ /** Project a recipe onto one agent's native mechanism. Body (numbered steps) is identical per agent. */
49
+ export function compileRecipe(r, agent) {
50
+ return [
51
+ `# ${r.name} — compiled for ${agent} as ${NATIVE_MECHANISM[agent]}`,
52
+ ...stepLines(r),
53
+ 'Shared state: framein MCP + task ledger (the contract, decisions, and validation results all agents read).',
54
+ ].join('\n');
55
+ }
package/dist/rescue.js CHANGED
@@ -1,38 +1,38 @@
1
- // Rescue Mode (F-LOOP-3, ADR-0008): promote the anomaly detector from a side feature to a
2
- // flagship. When the ledger shows a repair loop, assemble a rescue report — the signals, the last
3
- // green checkpoint, and three options (diagnose / rewind / continue) — and NEVER act automatically.
4
- // Pure logic; the actual model diagnosis (option A) and the git rewind (option B) live in cli.ts.
5
- import { PLAIN } from './ui/theme.js';
6
- const short = (sha) => sha.slice(0, 7);
7
- export function buildRescue(signals, opts = {}) {
8
- const triggered = signals.length > 0;
9
- const options = [];
10
- if (triggered) {
11
- const who = opts.reviewer ?? 'the reviewer role';
12
- options.push({ key: 'A', label: `Ask ${who} to diagnose without editing` });
13
- if (opts.lastGreen) {
14
- options.push({ key: 'B', label: `Rewind to checkpoint ${short(opts.lastGreen.sha)}${opts.lastGreen.label ? ` (${opts.lastGreen.label})` : ''}` });
15
- }
16
- options.push({ key: opts.lastGreen ? 'C' : 'B', label: 'Continue with the current agent' });
17
- }
18
- return { triggered, signals, lastGreen: opts.lastGreen, reviewer: opts.reviewer, options };
19
- }
20
- export function renderRescue(report, ui = PLAIN) {
21
- if (!report.triggered)
22
- return 'No repair loop detected. (rescue is anomaly-triggered — ADR-0005)';
23
- const lines = [ui.tone('Framein detected a repair loop.', 'danger'), ''];
24
- for (const s of report.signals)
25
- lines.push(` ${ui.tone(ui.sym.warn, 'warning')} ${s.message}`);
26
- lines.push('');
27
- if (report.lastGreen) {
28
- lines.push(`Last green checkpoint: ${short(report.lastGreen.sha)}${report.lastGreen.label ? ` (${report.lastGreen.label})` : ''}`);
29
- }
30
- else {
31
- lines.push('No green checkpoint recorded (run `frame checkpoint` at a known-good state).');
32
- }
33
- lines.push('', 'Recommended:');
34
- for (const o of report.options)
35
- lines.push(` ${ui.tone(o.key, 'brand')}. ${o.label}`);
36
- lines.push('', ui.tone('No action taken automatically.', 'muted'));
37
- return lines.join('\n');
38
- }
1
+ // Rescue Mode (F-LOOP-3, ADR-0008): promote the anomaly detector from a side feature to a
2
+ // flagship. When the ledger shows a repair loop, assemble a rescue report — the signals, the last
3
+ // green checkpoint, and three options (diagnose / rewind / continue) — and NEVER act automatically.
4
+ // Pure logic; the actual model diagnosis (option A) and the git rewind (option B) live in cli.ts.
5
+ import { PLAIN } from './ui/theme.js';
6
+ const short = (sha) => sha.slice(0, 7);
7
+ export function buildRescue(signals, opts = {}) {
8
+ const triggered = signals.length > 0;
9
+ const options = [];
10
+ if (triggered) {
11
+ const who = opts.reviewer ?? 'the reviewer role';
12
+ options.push({ key: 'A', label: `Ask ${who} to diagnose without editing` });
13
+ if (opts.lastGreen) {
14
+ options.push({ key: 'B', label: `Rewind to checkpoint ${short(opts.lastGreen.sha)}${opts.lastGreen.label ? ` (${opts.lastGreen.label})` : ''}` });
15
+ }
16
+ options.push({ key: opts.lastGreen ? 'C' : 'B', label: 'Continue with the current agent' });
17
+ }
18
+ return { triggered, signals, lastGreen: opts.lastGreen, reviewer: opts.reviewer, options };
19
+ }
20
+ export function renderRescue(report, ui = PLAIN) {
21
+ if (!report.triggered)
22
+ return 'No repair loop detected. (rescue is anomaly-triggered — ADR-0005)';
23
+ const lines = [ui.tone('Framein detected a repair loop.', 'danger'), ''];
24
+ for (const s of report.signals)
25
+ lines.push(` ${ui.tone(ui.sym.warn, 'warning')} ${s.message}`);
26
+ lines.push('');
27
+ if (report.lastGreen) {
28
+ lines.push(`Last green checkpoint: ${short(report.lastGreen.sha)}${report.lastGreen.label ? ` (${report.lastGreen.label})` : ''}`);
29
+ }
30
+ else {
31
+ lines.push('No green checkpoint recorded (run `frame checkpoint` at a known-good state).');
32
+ }
33
+ lines.push('', 'Recommended:');
34
+ for (const o of report.options)
35
+ lines.push(` ${ui.tone(o.key, 'brand')}. ${o.label}`);
36
+ lines.push('', ui.tone('No action taken automatically.', 'muted'));
37
+ return lines.join('\n');
38
+ }
package/dist/roles.js CHANGED
@@ -1,61 +1,61 @@
1
- // Role presets + routing score function (PRD section 5.2).
2
- // Vendor-role mapping is NOT hardcoded: these are defaults the user can override.
3
- import { AGENTS, ROLES } from './types.js';
4
- /** Runtime guard: is `x` one of the three first-class agents? */
5
- export function isAgent(x) {
6
- return AGENTS.includes(x);
7
- }
8
- /** Runtime guard: is `x` one of the five role presets? */
9
- export function isRole(x) {
10
- return ROLES.includes(x);
11
- }
12
- export const DEFAULT_ROLE_PRIORITY = {
13
- lead: ['claude', 'codex'],
14
- implementer: ['claude', 'codex'],
15
- reviewer: ['codex', 'claude'],
16
- explainer: ['gemini', 'claude'],
17
- researcher: ['gemini', 'claude'],
18
- };
19
- /** Compliance rule (PRD non-goal): consumer Gemini login is not permitted. */
20
- export function isForbiddenAuth(agent, auth) {
21
- return agent === 'gemini' && auth === 'consumer-login';
22
- }
23
- /** Repo-local routing bonus: reward local success, penalize local quota trouble (F-LOOP-7). */
24
- export function repoBonus(st) {
25
- if (!st || st.delegations === 0)
26
- return 0;
27
- const successRate = (st.delegations - st.failures) / st.delegations; // 0..1
28
- return successRate * 2 - st.quotaHits * 0.5;
29
- }
30
- export const POLICY_PENALTY = Number.POSITIVE_INFINITY;
31
- // score = roleFit + quotaScore - failurePenalty - costPenalty
32
- // forbidden auth combos => -Infinity (never selected).
33
- export function scoreAgent(agent, ctx) {
34
- const auth = ctx.authMode[agent];
35
- if (auth && isForbiddenAuth(agent, auth))
36
- return -POLICY_PENALTY;
37
- if (ctx.unavailable?.[agent])
38
- return -POLICY_PENALTY; // quota-exhausted / down => fail over to another agent
39
- const priority = (ctx.rolePriority ?? DEFAULT_ROLE_PRIORITY)[ctx.role] ?? [];
40
- const idx = priority.indexOf(agent);
41
- const roleFit = idx === -1 ? 0 : priority.length - idx;
42
- const quota = ctx.remainingQuota?.[agent];
43
- const quotaScore = quota === undefined ? 1 : quota * 2;
44
- const failurePenalty = (ctx.recentFailures?.[agent] ?? 0) * 1.5;
45
- const costPenalty = ctx.costBand?.[agent] ?? 0;
46
- return roleFit + quotaScore + repoBonus(ctx.repoStats?.[agent]) - failurePenalty - costPenalty;
47
- }
48
- export function selectAgent(candidates, ctx) {
49
- let best = null;
50
- let bestScore = -POLICY_PENALTY;
51
- for (const a of candidates) {
52
- const s = scoreAgent(a, ctx);
53
- if (s === -POLICY_PENALTY)
54
- continue;
55
- if (best === null || s > bestScore) {
56
- best = a;
57
- bestScore = s;
58
- }
59
- }
60
- return best;
61
- }
1
+ // Role presets + routing score function (PRD section 5.2).
2
+ // Vendor-role mapping is NOT hardcoded: these are defaults the user can override.
3
+ import { AGENTS, ROLES } from './types.js';
4
+ /** Runtime guard: is `x` one of the three first-class agents? */
5
+ export function isAgent(x) {
6
+ return AGENTS.includes(x);
7
+ }
8
+ /** Runtime guard: is `x` one of the five role presets? */
9
+ export function isRole(x) {
10
+ return ROLES.includes(x);
11
+ }
12
+ export const DEFAULT_ROLE_PRIORITY = {
13
+ lead: ['claude', 'codex'],
14
+ implementer: ['claude', 'codex'],
15
+ reviewer: ['codex', 'claude'],
16
+ explainer: ['gemini', 'claude'],
17
+ researcher: ['gemini', 'claude'],
18
+ };
19
+ /** Compliance rule (PRD non-goal): consumer Gemini login is not permitted. */
20
+ export function isForbiddenAuth(agent, auth) {
21
+ return agent === 'gemini' && auth === 'consumer-login';
22
+ }
23
+ /** Repo-local routing bonus: reward local success, penalize local quota trouble (F-LOOP-7). */
24
+ export function repoBonus(st) {
25
+ if (!st || st.delegations === 0)
26
+ return 0;
27
+ const successRate = (st.delegations - st.failures) / st.delegations; // 0..1
28
+ return successRate * 2 - st.quotaHits * 0.5;
29
+ }
30
+ export const POLICY_PENALTY = Number.POSITIVE_INFINITY;
31
+ // score = roleFit + quotaScore - failurePenalty - costPenalty
32
+ // forbidden auth combos => -Infinity (never selected).
33
+ export function scoreAgent(agent, ctx) {
34
+ const auth = ctx.authMode[agent];
35
+ if (auth && isForbiddenAuth(agent, auth))
36
+ return -POLICY_PENALTY;
37
+ if (ctx.unavailable?.[agent])
38
+ return -POLICY_PENALTY; // quota-exhausted / down => fail over to another agent
39
+ const priority = (ctx.rolePriority ?? DEFAULT_ROLE_PRIORITY)[ctx.role] ?? [];
40
+ const idx = priority.indexOf(agent);
41
+ const roleFit = idx === -1 ? 0 : priority.length - idx;
42
+ const quota = ctx.remainingQuota?.[agent];
43
+ const quotaScore = quota === undefined ? 1 : quota * 2;
44
+ const failurePenalty = (ctx.recentFailures?.[agent] ?? 0) * 1.5;
45
+ const costPenalty = ctx.costBand?.[agent] ?? 0;
46
+ return roleFit + quotaScore + repoBonus(ctx.repoStats?.[agent]) - failurePenalty - costPenalty;
47
+ }
48
+ export function selectAgent(candidates, ctx) {
49
+ let best = null;
50
+ let bestScore = -POLICY_PENALTY;
51
+ for (const a of candidates) {
52
+ const s = scoreAgent(a, ctx);
53
+ if (s === -POLICY_PENALTY)
54
+ continue;
55
+ if (best === null || s > bestScore) {
56
+ best = a;
57
+ bestScore = s;
58
+ }
59
+ }
60
+ return best;
61
+ }
package/dist/select.js CHANGED
@@ -1,50 +1,50 @@
1
- // Pure core for the zero-dep arrow-key picker. The raw-mode stdin loop + ANSI redraw live in cli.ts
2
- // (promptSelect); this module is the testable reducer: (state, keypress) -> next state OR a terminal
3
- // action. No npm deps, no I/O. `index` always refers to the CURRENTLY VISIBLE (filtered) list.
4
- /** Case-insensitive substring filter over label + hint + value. Order preserved. */
5
- export function filterItems(items, query) {
6
- const q = query.trim().toLowerCase();
7
- if (!q)
8
- return items;
9
- return items.filter((it) => `${it.label} ${it.hint ?? ''} ${it.value}`.toLowerCase().includes(q));
10
- }
11
- export function initSelect(items) {
12
- return { items, query: '', index: 0 };
13
- }
14
- /** Pure reducer. Returns the next state (kind 'move') or a terminal action ('accept' | 'cancel'). */
15
- export function reduceSelectKey(state, key) {
16
- const visible = filterItems(state.items, state.query);
17
- const n = visible.length;
18
- const idx = n ? Math.min(state.index, n - 1) : 0;
19
- if (key.ctrl && key.name === 'c')
20
- return { kind: 'cancel' };
21
- if (key.name === 'escape')
22
- return { kind: 'cancel' };
23
- if (key.name === 'return' || key.name === 'enter') {
24
- return n ? { kind: 'accept', value: visible[idx].value } : { kind: 'move', state };
25
- }
26
- if (key.name === 'up')
27
- return { kind: 'move', state: { ...state, index: n ? (idx - 1 + n) % n : 0 } };
28
- if (key.name === 'down')
29
- return { kind: 'move', state: { ...state, index: n ? (idx + 1) % n : 0 } };
30
- if (key.name === 'backspace')
31
- return { kind: 'move', state: { ...state, query: state.query.slice(0, -1), index: 0 } };
32
- // A single printable character extends the type-to-filter query (control sequences are ignored).
33
- const ch = key.sequence;
34
- if (ch && ch.length === 1 && ch >= ' ' && !key.ctrl) {
35
- return { kind: 'move', state: { ...state, query: state.query + ch, index: 0 } };
36
- }
37
- return { kind: 'move', state };
38
- }
39
- /** Pure renderer: the lines to draw (no ANSI; the I/O layer colors them). First line is the label. */
40
- export function renderSelectLines(label, state, marker = '>') {
41
- const visible = filterItems(state.items, state.query);
42
- const head = state.query ? `${label} (filter: ${state.query})` : label;
43
- if (!visible.length)
44
- return [head, ' (no matches — backspace to clear)'];
45
- const idx = Math.min(state.index, visible.length - 1);
46
- return [head, ...visible.map((it, i) => {
47
- const hint = it.hint ? ` ${it.hint}` : '';
48
- return `${i === idx ? `${marker} ` : ' '}${it.label}${hint}`;
49
- })];
50
- }
1
+ // Pure core for the zero-dep arrow-key picker. The raw-mode stdin loop + ANSI redraw live in cli.ts
2
+ // (promptSelect); this module is the testable reducer: (state, keypress) -> next state OR a terminal
3
+ // action. No npm deps, no I/O. `index` always refers to the CURRENTLY VISIBLE (filtered) list.
4
+ /** Case-insensitive substring filter over label + hint + value. Order preserved. */
5
+ export function filterItems(items, query) {
6
+ const q = query.trim().toLowerCase();
7
+ if (!q)
8
+ return items;
9
+ return items.filter((it) => `${it.label} ${it.hint ?? ''} ${it.value}`.toLowerCase().includes(q));
10
+ }
11
+ export function initSelect(items) {
12
+ return { items, query: '', index: 0 };
13
+ }
14
+ /** Pure reducer. Returns the next state (kind 'move') or a terminal action ('accept' | 'cancel'). */
15
+ export function reduceSelectKey(state, key) {
16
+ const visible = filterItems(state.items, state.query);
17
+ const n = visible.length;
18
+ const idx = n ? Math.min(state.index, n - 1) : 0;
19
+ if (key.ctrl && key.name === 'c')
20
+ return { kind: 'cancel' };
21
+ if (key.name === 'escape')
22
+ return { kind: 'cancel' };
23
+ if (key.name === 'return' || key.name === 'enter') {
24
+ return n ? { kind: 'accept', value: visible[idx].value } : { kind: 'move', state };
25
+ }
26
+ if (key.name === 'up')
27
+ return { kind: 'move', state: { ...state, index: n ? (idx - 1 + n) % n : 0 } };
28
+ if (key.name === 'down')
29
+ return { kind: 'move', state: { ...state, index: n ? (idx + 1) % n : 0 } };
30
+ if (key.name === 'backspace')
31
+ return { kind: 'move', state: { ...state, query: state.query.slice(0, -1), index: 0 } };
32
+ // A single printable character extends the type-to-filter query (control sequences are ignored).
33
+ const ch = key.sequence;
34
+ if (ch && ch.length === 1 && ch >= ' ' && !key.ctrl) {
35
+ return { kind: 'move', state: { ...state, query: state.query + ch, index: 0 } };
36
+ }
37
+ return { kind: 'move', state };
38
+ }
39
+ /** Pure renderer: the lines to draw (no ANSI; the I/O layer colors them). First line is the label. */
40
+ export function renderSelectLines(label, state, marker = '>') {
41
+ const visible = filterItems(state.items, state.query);
42
+ const head = state.query ? `${label} (filter: ${state.query})` : label;
43
+ if (!visible.length)
44
+ return [head, ' (no matches — backspace to clear)'];
45
+ const idx = Math.min(state.index, visible.length - 1);
46
+ return [head, ...visible.map((it, i) => {
47
+ const hint = it.hint ? ` ${it.hint}` : '';
48
+ return `${i === idx ? `${marker} ` : ' '}${it.label}${hint}`;
49
+ })];
50
+ }