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/mcpServer.js CHANGED
@@ -1,138 +1,138 @@
1
- // Thin framein MCP surface (F-OWN-1): the minimum tools agents need to share the store.
2
- // `handleTool` maps a tool call to a store op (unit-tested); `dispatch` handles one JSON-RPC
3
- // message; `serve` is the stdio loop. The wire format is newline-delimited JSON (NDJSON) —
4
- // that IS the MCP stdio transport (ADR-0007). MCP stdio is NOT Content-Length framed (that's
5
- // LSP); headers only appear in MCP's separate Streamable HTTP transport. Live MCP-CLIENT
6
- // wiring (each CLI launching its own `frame mcp serve`) is the orchestration layer (B).
7
- import { createInterface } from 'node:readline';
8
- const obj = (properties, required = []) => ({ type: 'object', properties, required });
9
- const str = { type: 'string' };
10
- export const TOOLS = [
11
- { name: 'append_adr', description: 'record an ADR', inputSchema: obj({ title: str, decision: str, context: str, consequences: str }, ['title']) },
12
- { name: 'list_adr', description: 'list all ADRs', inputSchema: obj({}) },
13
- { name: 'get_adr', description: 'get one ADR', inputSchema: obj({ id: { type: 'number' } }, ['id']) },
14
- { name: 'read_memory', description: 'read scoped memory', inputSchema: obj({ scope: str, key: str }, ['scope', 'key']) },
15
- { name: 'write_memory', description: 'write scoped memory', inputSchema: obj({ scope: str, key: str, value: {} }, ['scope', 'key', 'value']) },
16
- { name: 'list_memory', description: 'list a memory scope', inputSchema: obj({ scope: str }, ['scope']) },
17
- { name: 'get_role', description: 'agent assigned to a role', inputSchema: obj({ role: str }, ['role']) },
18
- { name: 'get_roles', description: 'all role assignments', inputSchema: obj({}) },
19
- { name: 'acquire_lock', description: 'acquire the write lock', inputSchema: obj({ holder: str, scope: str }, ['holder']) },
20
- { name: 'release_lock', description: 'release the write lock', inputSchema: obj({ holder: str, scope: str }, ['holder']) },
21
- ];
22
- const TOOL_NAMES = new Set(TOOLS.map((t) => t.name));
23
- /** Protocol versions we accept, newest first. Negotiation echoes the client's if supported. */
24
- export const PROTOCOL_VERSIONS = ['2025-06-18', '2025-03-26', '2024-11-05'];
25
- // Advertised to clients; keep loosely in sync with package.json (a string is required by spec).
26
- const SERVER_INFO = { name: 'framein', version: '0.0.1' };
27
- export function handleTool(store, name, args = {}) {
28
- switch (name) {
29
- case 'append_adr': {
30
- const title = String(args.title ?? '');
31
- if (!title)
32
- throw new Error('append_adr requires a title');
33
- const a = store.appendAdr({
34
- title,
35
- decision: String(args.decision ?? title),
36
- context: args.context,
37
- consequences: args.consequences,
38
- });
39
- return { id: a.id };
40
- }
41
- case 'list_adr': return store.listAdrs();
42
- case 'get_adr': return store.getAdr(Number(args.id)) ?? null;
43
- case 'read_memory': return store.getMemory(String(args.scope), String(args.key)) ?? null;
44
- case 'write_memory':
45
- store.setMemory(String(args.scope), String(args.key), args.value);
46
- return { ok: true };
47
- case 'list_memory': return store.listMemory(String(args.scope));
48
- case 'get_role': return store.getRole(args.role) ?? null;
49
- case 'get_roles': return store.getRoles();
50
- case 'acquire_lock': return { acquired: store.acquireLock(String(args.holder), { scope: args.scope }) };
51
- case 'release_lock': return { released: store.releaseLock(String(args.holder), { scope: args.scope }) };
52
- default: throw new Error(`unknown tool: ${name}`);
53
- }
54
- }
55
- /**
56
- * Dispatch one parsed JSON-RPC value. Never throws. Returns a reply object for requests, or
57
- * `null` for notifications and for anything we can't reply to (no id). When a `session` is
58
- * supplied, the initialize-first lifecycle is enforced (ADR-0007); without one, dispatch stays
59
- * lenient (used by unit tests). `initialize` and `ping` are always allowed pre-init.
60
- */
61
- export function dispatch(store, req, session) {
62
- const isObj = typeof req === 'object' && req !== null && !Array.isArray(req);
63
- const r = (isObj ? req : {});
64
- const hasId = isObj && 'id' in r && r.id !== null && r.id !== undefined;
65
- const id = hasId ? r.id : null;
66
- const params = (typeof r.params === 'object' && r.params !== null ? r.params : {});
67
- const reply = (result) => (hasId ? { jsonrpc: '2.0', id, result } : null);
68
- const errReply = (code, message) => (hasId ? { jsonrpc: '2.0', id, error: { code, message } } : null);
69
- if (typeof r.method !== 'string')
70
- return errReply(-32600, 'invalid request');
71
- const method = r.method;
72
- // Lifecycle methods allowed before initialization completes.
73
- if (method === 'notifications/initialized') {
74
- if (session)
75
- session.initialized = true;
76
- return null;
77
- }
78
- if (method === 'initialize') {
79
- if (session)
80
- session.initialized = false; // re-init resets; the initialized notification re-arms it
81
- const want = params.protocolVersion;
82
- const protocolVersion = typeof want === 'string' && PROTOCOL_VERSIONS.includes(want) ? want : PROTOCOL_VERSIONS[0];
83
- return reply({ protocolVersion, serverInfo: SERVER_INFO, capabilities: { tools: { listChanged: false } } });
84
- }
85
- if (method === 'ping')
86
- return reply({});
87
- if (session && !session.initialized)
88
- return errReply(-32600, 'received request before initialization');
89
- try {
90
- switch (method) {
91
- case 'tools/list': return reply({ tools: TOOLS });
92
- case 'tools/call': {
93
- const name = params.name;
94
- if (typeof name !== 'string' || !TOOL_NAMES.has(name))
95
- return errReply(-32602, `unknown tool: ${String(name)}`);
96
- const args = (typeof params.arguments === 'object' && params.arguments !== null ? params.arguments : {});
97
- try {
98
- const out = handleTool(store, name, args);
99
- return reply({ content: [{ type: 'text', text: JSON.stringify(out) }], isError: false });
100
- }
101
- catch (e) {
102
- // Tool EXECUTION errors stay in-band (isError) so the model sees them and can recover.
103
- return reply({ content: [{ type: 'text', text: e.message }], isError: true });
104
- }
105
- }
106
- default: return errReply(-32601, `unknown method: ${method}`);
107
- }
108
- }
109
- catch (e) {
110
- return errReply(-32603, e.message);
111
- }
112
- }
113
- /**
114
- * MCP stdio loop: newline-delimited JSON-RPC over stdin/stdout (the MCP stdio transport — not
115
- * Content-Length framing, ADR-0007). A malformed line is answered with a JSON-RPC parse error
116
- * (-32700), not silently dropped. Only valid MCP messages are written to `output` (stdout
117
- * hygiene); diagnostics belong on stderr. Consumes input until EOF.
118
- */
119
- export async function serve(store, input = process.stdin, output = process.stdout) {
120
- const rl = createInterface({ input, crlfDelay: Infinity });
121
- const session = { initialized: false };
122
- for await (const line of rl) {
123
- const t = line.trim();
124
- if (!t)
125
- continue;
126
- let req;
127
- try {
128
- req = JSON.parse(t);
129
- }
130
- catch {
131
- output.write(JSON.stringify({ jsonrpc: '2.0', id: null, error: { code: -32700, message: 'parse error' } }) + '\n');
132
- continue;
133
- }
134
- const res = dispatch(store, req, session);
135
- if (res)
136
- output.write(JSON.stringify(res) + '\n');
137
- }
138
- }
1
+ // Thin framein MCP surface (F-OWN-1): the minimum tools agents need to share the store.
2
+ // `handleTool` maps a tool call to a store op (unit-tested); `dispatch` handles one JSON-RPC
3
+ // message; `serve` is the stdio loop. The wire format is newline-delimited JSON (NDJSON) —
4
+ // that IS the MCP stdio transport (ADR-0007). MCP stdio is NOT Content-Length framed (that's
5
+ // LSP); headers only appear in MCP's separate Streamable HTTP transport. Live MCP-CLIENT
6
+ // wiring (each CLI launching its own `frame mcp serve`) is the orchestration layer (B).
7
+ import { createInterface } from 'node:readline';
8
+ const obj = (properties, required = []) => ({ type: 'object', properties, required });
9
+ const str = { type: 'string' };
10
+ export const TOOLS = [
11
+ { name: 'append_adr', description: 'record an ADR', inputSchema: obj({ title: str, decision: str, context: str, consequences: str }, ['title']) },
12
+ { name: 'list_adr', description: 'list all ADRs', inputSchema: obj({}) },
13
+ { name: 'get_adr', description: 'get one ADR', inputSchema: obj({ id: { type: 'number' } }, ['id']) },
14
+ { name: 'read_memory', description: 'read scoped memory', inputSchema: obj({ scope: str, key: str }, ['scope', 'key']) },
15
+ { name: 'write_memory', description: 'write scoped memory', inputSchema: obj({ scope: str, key: str, value: {} }, ['scope', 'key', 'value']) },
16
+ { name: 'list_memory', description: 'list a memory scope', inputSchema: obj({ scope: str }, ['scope']) },
17
+ { name: 'get_role', description: 'agent assigned to a role', inputSchema: obj({ role: str }, ['role']) },
18
+ { name: 'get_roles', description: 'all role assignments', inputSchema: obj({}) },
19
+ { name: 'acquire_lock', description: 'acquire the write lock', inputSchema: obj({ holder: str, scope: str }, ['holder']) },
20
+ { name: 'release_lock', description: 'release the write lock', inputSchema: obj({ holder: str, scope: str }, ['holder']) },
21
+ ];
22
+ const TOOL_NAMES = new Set(TOOLS.map((t) => t.name));
23
+ /** Protocol versions we accept, newest first. Negotiation echoes the client's if supported. */
24
+ export const PROTOCOL_VERSIONS = ['2025-06-18', '2025-03-26', '2024-11-05'];
25
+ // Advertised to clients; keep loosely in sync with package.json (a string is required by spec).
26
+ const SERVER_INFO = { name: 'framein', version: '0.0.1' };
27
+ export function handleTool(store, name, args = {}) {
28
+ switch (name) {
29
+ case 'append_adr': {
30
+ const title = String(args.title ?? '');
31
+ if (!title)
32
+ throw new Error('append_adr requires a title');
33
+ const a = store.appendAdr({
34
+ title,
35
+ decision: String(args.decision ?? title),
36
+ context: args.context,
37
+ consequences: args.consequences,
38
+ });
39
+ return { id: a.id };
40
+ }
41
+ case 'list_adr': return store.listAdrs();
42
+ case 'get_adr': return store.getAdr(Number(args.id)) ?? null;
43
+ case 'read_memory': return store.getMemory(String(args.scope), String(args.key)) ?? null;
44
+ case 'write_memory':
45
+ store.setMemory(String(args.scope), String(args.key), args.value);
46
+ return { ok: true };
47
+ case 'list_memory': return store.listMemory(String(args.scope));
48
+ case 'get_role': return store.getRole(args.role) ?? null;
49
+ case 'get_roles': return store.getRoles();
50
+ case 'acquire_lock': return { acquired: store.acquireLock(String(args.holder), { scope: args.scope }) };
51
+ case 'release_lock': return { released: store.releaseLock(String(args.holder), { scope: args.scope }) };
52
+ default: throw new Error(`unknown tool: ${name}`);
53
+ }
54
+ }
55
+ /**
56
+ * Dispatch one parsed JSON-RPC value. Never throws. Returns a reply object for requests, or
57
+ * `null` for notifications and for anything we can't reply to (no id). When a `session` is
58
+ * supplied, the initialize-first lifecycle is enforced (ADR-0007); without one, dispatch stays
59
+ * lenient (used by unit tests). `initialize` and `ping` are always allowed pre-init.
60
+ */
61
+ export function dispatch(store, req, session) {
62
+ const isObj = typeof req === 'object' && req !== null && !Array.isArray(req);
63
+ const r = (isObj ? req : {});
64
+ const hasId = isObj && 'id' in r && r.id !== null && r.id !== undefined;
65
+ const id = hasId ? r.id : null;
66
+ const params = (typeof r.params === 'object' && r.params !== null ? r.params : {});
67
+ const reply = (result) => (hasId ? { jsonrpc: '2.0', id, result } : null);
68
+ const errReply = (code, message) => (hasId ? { jsonrpc: '2.0', id, error: { code, message } } : null);
69
+ if (typeof r.method !== 'string')
70
+ return errReply(-32600, 'invalid request');
71
+ const method = r.method;
72
+ // Lifecycle methods allowed before initialization completes.
73
+ if (method === 'notifications/initialized') {
74
+ if (session)
75
+ session.initialized = true;
76
+ return null;
77
+ }
78
+ if (method === 'initialize') {
79
+ if (session)
80
+ session.initialized = false; // re-init resets; the initialized notification re-arms it
81
+ const want = params.protocolVersion;
82
+ const protocolVersion = typeof want === 'string' && PROTOCOL_VERSIONS.includes(want) ? want : PROTOCOL_VERSIONS[0];
83
+ return reply({ protocolVersion, serverInfo: SERVER_INFO, capabilities: { tools: { listChanged: false } } });
84
+ }
85
+ if (method === 'ping')
86
+ return reply({});
87
+ if (session && !session.initialized)
88
+ return errReply(-32600, 'received request before initialization');
89
+ try {
90
+ switch (method) {
91
+ case 'tools/list': return reply({ tools: TOOLS });
92
+ case 'tools/call': {
93
+ const name = params.name;
94
+ if (typeof name !== 'string' || !TOOL_NAMES.has(name))
95
+ return errReply(-32602, `unknown tool: ${String(name)}`);
96
+ const args = (typeof params.arguments === 'object' && params.arguments !== null ? params.arguments : {});
97
+ try {
98
+ const out = handleTool(store, name, args);
99
+ return reply({ content: [{ type: 'text', text: JSON.stringify(out) }], isError: false });
100
+ }
101
+ catch (e) {
102
+ // Tool EXECUTION errors stay in-band (isError) so the model sees them and can recover.
103
+ return reply({ content: [{ type: 'text', text: e.message }], isError: true });
104
+ }
105
+ }
106
+ default: return errReply(-32601, `unknown method: ${method}`);
107
+ }
108
+ }
109
+ catch (e) {
110
+ return errReply(-32603, e.message);
111
+ }
112
+ }
113
+ /**
114
+ * MCP stdio loop: newline-delimited JSON-RPC over stdin/stdout (the MCP stdio transport — not
115
+ * Content-Length framing, ADR-0007). A malformed line is answered with a JSON-RPC parse error
116
+ * (-32700), not silently dropped. Only valid MCP messages are written to `output` (stdout
117
+ * hygiene); diagnostics belong on stderr. Consumes input until EOF.
118
+ */
119
+ export async function serve(store, input = process.stdin, output = process.stdout) {
120
+ const rl = createInterface({ input, crlfDelay: Infinity });
121
+ const session = { initialized: false };
122
+ for await (const line of rl) {
123
+ const t = line.trim();
124
+ if (!t)
125
+ continue;
126
+ let req;
127
+ try {
128
+ req = JSON.parse(t);
129
+ }
130
+ catch {
131
+ output.write(JSON.stringify({ jsonrpc: '2.0', id: null, error: { code: -32700, message: 'parse error' } }) + '\n');
132
+ continue;
133
+ }
134
+ const res = dispatch(store, req, session);
135
+ if (res)
136
+ output.write(JSON.stringify(res) + '\n');
137
+ }
138
+ }
package/dist/palette.js CHANGED
@@ -1,63 +1,63 @@
1
- // Pure core for the lobby's inline `/` command palette (Claude-style, non-modal). Unlike the modal
2
- // picker (select.ts), here the TYPED LINE is primary: a suggestion list appears *below* the line you're
3
- // editing as soon as it starts with `/`, filters as you type, and ⏎ runs exactly what you typed — it
4
- // never force-selects the top item. ↑/↓ opt into picking a suggestion; ⏎ then runs the highlighted one.
5
- // No npm deps, no I/O — the raw-mode stdin loop + ANSI redraw live in cli.ts (readLobbyLine).
6
- export function initPalette(buf = '') { return { buf, index: 0, navigated: false }; }
7
- /** Suggestions are shown ONLY while the buffer is a slash-command being typed (`/…`). Filtered by the
8
- * text after the leading `/` (substring over cmd + desc). Empty when the line isn't a slash command —
9
- * so bare verbs / agent names (`verify`, `codex fix bug`) type freely with no popup. */
10
- export function paletteSuggestions(buf, cmds) {
11
- if (!buf.startsWith('/'))
12
- return [];
13
- const q = buf.slice(1).trim().toLowerCase();
14
- if (!q)
15
- return cmds;
16
- return cmds.filter((c) => `${c.cmd} ${c.desc}`.toLowerCase().includes(q));
17
- }
18
- /** Pure reducer: (state, key) → next state OR a terminal step. Mirrors select.ts but the default ⏎
19
- * action is "run what I typed", not "accept the highlighted item". */
20
- export function reducePaletteKey(state, key, cmds) {
21
- const sugg = paletteSuggestions(state.buf, cmds);
22
- const n = sugg.length;
23
- const idx = n ? Math.min(state.index, n - 1) : 0;
24
- if (key.ctrl && key.name === 'c')
25
- return { kind: 'sigint' };
26
- if (key.ctrl && key.name === 'd')
27
- return state.buf ? { kind: 'edit', state } : { kind: 'exit' };
28
- if (key.name === 'escape')
29
- return { kind: 'edit', state: initPalette('') }; // close the palette / clear the line
30
- if (key.name === 'return' || key.name === 'enter') {
31
- if (n && state.navigated)
32
- return { kind: 'submit', line: sugg[idx].cmd }; // user picked one → run it
33
- const line = state.buf.trim();
34
- if (line === '' || line === '/')
35
- return { kind: 'edit', state }; // nothing meaningful typed → no-op
36
- return { kind: 'submit', line };
37
- }
38
- if (key.name === 'up')
39
- return { kind: 'edit', state: { ...state, index: n ? (idx - 1 + n) % n : 0, navigated: true } };
40
- if (key.name === 'down')
41
- return { kind: 'edit', state: { ...state, index: n ? (idx + 1) % n : 0, navigated: true } };
42
- if (key.name === 'tab')
43
- return n ? { kind: 'edit', state: { buf: `${sugg[idx].cmd} `, index: 0, navigated: false } } : { kind: 'edit', state };
44
- if (key.name === 'backspace')
45
- return { kind: 'edit', state: { ...state, buf: state.buf.slice(0, -1), index: 0, navigated: false } };
46
- // A single printable character extends the line (control sequences ignored). Typing resets the
47
- // highlight so ⏎ goes back to "run what I typed" until the user navigates again.
48
- const ch = key.sequence;
49
- if (ch && ch.length === 1 && ch >= ' ' && !key.ctrl) {
50
- return { kind: 'edit', state: { ...state, buf: state.buf + ch, index: 0, navigated: false } };
51
- }
52
- return { kind: 'edit', state };
53
- }
54
- /** Pure renderer for the suggestion list drawn below the input line (no ANSI; the I/O layer colors the
55
- * hints). Returns [] when no suggestions are visible. The input line itself is drawn by the caller. */
56
- export function renderPaletteSuggestions(state, cmds, marker = '>') {
57
- const sugg = paletteSuggestions(state.buf, cmds);
58
- if (!sugg.length)
59
- return [];
60
- const w = Math.max(...sugg.map((c) => c.cmd.length));
61
- const idx = Math.min(state.index, sugg.length - 1);
62
- return sugg.map((c, i) => `${i === idx ? `${marker} ` : ' '}${c.cmd.padEnd(w)} ${c.desc}`);
63
- }
1
+ // Pure core for the lobby's inline `/` command palette (Claude-style, non-modal). Unlike the modal
2
+ // picker (select.ts), here the TYPED LINE is primary: a suggestion list appears *below* the line you're
3
+ // editing as soon as it starts with `/`, filters as you type, and ⏎ runs exactly what you typed — it
4
+ // never force-selects the top item. ↑/↓ opt into picking a suggestion; ⏎ then runs the highlighted one.
5
+ // No npm deps, no I/O — the raw-mode stdin loop + ANSI redraw live in cli.ts (readLobbyLine).
6
+ export function initPalette(buf = '') { return { buf, index: 0, navigated: false }; }
7
+ /** Suggestions are shown ONLY while the buffer is a slash-command being typed (`/…`). Filtered by the
8
+ * text after the leading `/` (substring over cmd + desc). Empty when the line isn't a slash command —
9
+ * so bare verbs / agent names (`verify`, `codex fix bug`) type freely with no popup. */
10
+ export function paletteSuggestions(buf, cmds) {
11
+ if (!buf.startsWith('/'))
12
+ return [];
13
+ const q = buf.slice(1).trim().toLowerCase();
14
+ if (!q)
15
+ return cmds;
16
+ return cmds.filter((c) => `${c.cmd} ${c.desc}`.toLowerCase().includes(q));
17
+ }
18
+ /** Pure reducer: (state, key) → next state OR a terminal step. Mirrors select.ts but the default ⏎
19
+ * action is "run what I typed", not "accept the highlighted item". */
20
+ export function reducePaletteKey(state, key, cmds) {
21
+ const sugg = paletteSuggestions(state.buf, cmds);
22
+ const n = sugg.length;
23
+ const idx = n ? Math.min(state.index, n - 1) : 0;
24
+ if (key.ctrl && key.name === 'c')
25
+ return { kind: 'sigint' };
26
+ if (key.ctrl && key.name === 'd')
27
+ return state.buf ? { kind: 'edit', state } : { kind: 'exit' };
28
+ if (key.name === 'escape')
29
+ return { kind: 'edit', state: initPalette('') }; // close the palette / clear the line
30
+ if (key.name === 'return' || key.name === 'enter') {
31
+ if (n && state.navigated)
32
+ return { kind: 'submit', line: sugg[idx].cmd }; // user picked one → run it
33
+ const line = state.buf.trim();
34
+ if (line === '' || line === '/')
35
+ return { kind: 'edit', state }; // nothing meaningful typed → no-op
36
+ return { kind: 'submit', line };
37
+ }
38
+ if (key.name === 'up')
39
+ return { kind: 'edit', state: { ...state, index: n ? (idx - 1 + n) % n : 0, navigated: true } };
40
+ if (key.name === 'down')
41
+ return { kind: 'edit', state: { ...state, index: n ? (idx + 1) % n : 0, navigated: true } };
42
+ if (key.name === 'tab')
43
+ return n ? { kind: 'edit', state: { buf: `${sugg[idx].cmd} `, index: 0, navigated: false } } : { kind: 'edit', state };
44
+ if (key.name === 'backspace')
45
+ return { kind: 'edit', state: { ...state, buf: state.buf.slice(0, -1), index: 0, navigated: false } };
46
+ // A single printable character extends the line (control sequences ignored). Typing resets the
47
+ // highlight so ⏎ goes back to "run what I typed" until the user navigates again.
48
+ const ch = key.sequence;
49
+ if (ch && ch.length === 1 && ch >= ' ' && !key.ctrl) {
50
+ return { kind: 'edit', state: { ...state, buf: state.buf + ch, index: 0, navigated: false } };
51
+ }
52
+ return { kind: 'edit', state };
53
+ }
54
+ /** Pure renderer for the suggestion list drawn below the input line (no ANSI; the I/O layer colors the
55
+ * hints). Returns [] when no suggestions are visible. The input line itself is drawn by the caller. */
56
+ export function renderPaletteSuggestions(state, cmds, marker = '>') {
57
+ const sugg = paletteSuggestions(state.buf, cmds);
58
+ if (!sugg.length)
59
+ return [];
60
+ const w = Math.max(...sugg.map((c) => c.cmd.length));
61
+ const idx = Math.min(state.index, sugg.length - 1);
62
+ return sugg.map((c, i) => `${i === idx ? `${marker} ` : ' '}${c.cmd.padEnd(w)} ${c.desc}`);
63
+ }
package/dist/projector.js CHANGED
@@ -1,65 +1,65 @@
1
- // Projects the single source of truth into the three native context files.
2
- // The canonical CORE BLOCK is byte-identical across all three => guaranteed sync.
3
- import { buildAdrDigest } from './adr.js';
4
- import { renderContractDigest } from './task.js';
5
- import { wrapManaged, upsertManagedBlock } from './managedBlock.js';
6
- export function buildCoreBlock(state) {
7
- const rulesVal = state.config['rules'];
8
- const rules = typeof rulesVal === 'string' && rulesVal.trim() ? rulesVal.trim() : '_No project rules defined._';
9
- const roleEntries = Object.entries(state.roles);
10
- const roleLines = roleEntries.length === 0
11
- ? '_No roles assigned._'
12
- : roleEntries.map(([r, a]) => `- **${r}** → ${a}`).join('\n');
13
- const contract = state.taskContract ? renderContractDigest(state.taskContract) : '_No active task contract._';
14
- // Operating guide for the AGENT (ADR-0012): framein is driven from inside your normal flow, not by the
15
- // human typing long commands. Run any verb with the shell (`framein <verb>`) or the host slash command.
16
- const guide = [
17
- 'This project is kept aligned by **framein** (intent → validation → continuity). Drive it from your',
18
- 'normal flow — run a verb with the shell (`framein <verb>`) or the host command (`/fr:<verb>`, Codex',
19
- '`$fr-<verb>`). Automation-facing verbs expose `--json`; use the generated wrapper syntax when available.',
20
- 'On Windows PowerShell, use `framein.cmd <verb>` if the npm `.ps1` shim is blocked; generated wrappers already do this.',
21
- '',
22
- '- **At session start (and after any model switch)** — run `framein capsule` first to load where the',
23
- ' project left off (last-green commit, recent changes, open blocker, decisions). This is how context',
24
- ' follows you across models — don\'t ask the user to re-explain.',
25
- '- **Task start** — fix what "done" means before coding: `framein start "<goal>"`, then',
26
- ' `framein task amend acceptance|nongoal|protected "<…>"`. Changes auto-apply and show up in `git diff` —',
27
- ' keep them honest; the human reviews. Don\'t silently drift the contract.',
28
- '- **Before you claim done** — run the Validation Gate: `framein verify`. Never report "done" without it.',
29
- '- **Ship** — `framein ship` for commit/deploy readiness.',
30
- '- **Risky change** (auth · payments · migrations · deploy · secrets) — `framein risk`, then satisfy the required gates.',
31
- '- **Stuck in a repeat-fix loop** — `framein rescue` (it proposes options; it never auto-acts).',
32
- '- **Want a second opinion** — `framein challenge "<proposal>" --run` gets an INDEPENDENT model\'s verdict',
33
- ' (a *different* model reviews — don\'t write the critique yourself; you keep the lead and `decide`).',
34
- '- **Hand work to another model** — `framein capsule <agent>`, then exit; framein switches to that model,',
35
- ' which loads the capsule on arrival. The context follows the handoff automatically.',
36
- '',
37
- 'The **Task Contract** below is the definition of done — honor it. The human stays the final gate.',
38
- ].join('\n');
39
- return [
40
- '## Working with framein', guide, '',
41
- '## Task Contract', contract, '',
42
- '## Project Rules', rules, '',
43
- '_Project defaults — change them with `framein rules set "<…>"` (edits typed directly here are overwritten on sync). They guide the agent; the Validation Gate is what\'s enforced._', '',
44
- '## Agent Roles', roleLines, '',
45
- '## Architecture Decisions (digest)', buildAdrDigest(state.adrs),
46
- ].join('\n');
47
- }
48
- /** The managed block (markers + canonical core), byte-identical across all three files. */
49
- export function renderManagedBlock(state) {
50
- return wrapManaged(buildCoreBlock(state));
51
- }
52
- /** A from-scratch native file (heading + managed block); used when no file exists yet. */
53
- function projectFresh(title, state) {
54
- return upsertManagedBlock(null, title, renderManagedBlock(state));
55
- }
56
- export function projectClaudeMd(state) { return projectFresh('CLAUDE.md', state); }
57
- export function projectAgentsMd(state) { return projectFresh('AGENTS.md', state); }
58
- export function projectGeminiMd(state) { return projectFresh('GEMINI.md', state); }
59
- export function projectAll(state) {
60
- return {
61
- 'CLAUDE.md': projectClaudeMd(state),
62
- 'AGENTS.md': projectAgentsMd(state),
63
- 'GEMINI.md': projectGeminiMd(state),
64
- };
65
- }
1
+ // Projects the single source of truth into the three native context files.
2
+ // The canonical CORE BLOCK is byte-identical across all three => guaranteed sync.
3
+ import { buildAdrDigest } from './adr.js';
4
+ import { renderContractDigest } from './task.js';
5
+ import { wrapManaged, upsertManagedBlock } from './managedBlock.js';
6
+ export function buildCoreBlock(state) {
7
+ const rulesVal = state.config['rules'];
8
+ const rules = typeof rulesVal === 'string' && rulesVal.trim() ? rulesVal.trim() : '_No project rules defined._';
9
+ const roleEntries = Object.entries(state.roles);
10
+ const roleLines = roleEntries.length === 0
11
+ ? '_No roles assigned._'
12
+ : roleEntries.map(([r, a]) => `- **${r}** → ${a}`).join('\n');
13
+ const contract = state.taskContract ? renderContractDigest(state.taskContract) : '_No active task contract._';
14
+ // Operating guide for the AGENT (ADR-0012): framein is driven from inside your normal flow, not by the
15
+ // human typing long commands. Run any verb with the shell (`framein <verb>`) or the host slash command.
16
+ const guide = [
17
+ 'This project is kept aligned by **framein** (intent → validation → continuity). Drive it from your',
18
+ 'normal flow — run a verb with the shell (`framein <verb>`) or the host command (`/fr:<verb>`, Codex',
19
+ '`$fr-<verb>`). Automation-facing verbs expose `--json`; use the generated wrapper syntax when available.',
20
+ 'On Windows PowerShell, use `framein.cmd <verb>` if the npm `.ps1` shim is blocked; generated wrappers already do this.',
21
+ '',
22
+ '- **At session start (and after any model switch)** — run `framein capsule` first to load where the',
23
+ ' project left off (last-green commit, recent changes, open blocker, decisions). This is how context',
24
+ ' follows you across models — don\'t ask the user to re-explain.',
25
+ '- **Task start** — fix what "done" means before coding: `framein start "<goal>"`, then',
26
+ ' `framein task amend acceptance|nongoal|protected "<…>"`. Changes auto-apply and show up in `git diff` —',
27
+ ' keep them honest; the human reviews. Don\'t silently drift the contract.',
28
+ '- **Before you claim done** — run the Validation Gate: `framein verify`. Never report "done" without it.',
29
+ '- **Ship** — `framein ship` for commit/deploy readiness.',
30
+ '- **Risky change** (auth · payments · migrations · deploy · secrets) — `framein risk`, then satisfy the required gates.',
31
+ '- **Stuck in a repeat-fix loop** — `framein rescue` (it proposes options; it never auto-acts).',
32
+ '- **Want a second opinion** — `framein challenge "<proposal>" --run` gets an INDEPENDENT model\'s verdict',
33
+ ' (a *different* model reviews — don\'t write the critique yourself; you keep the lead and `decide`).',
34
+ '- **Hand work to another model** — `framein capsule <agent>`, then exit; framein switches to that model,',
35
+ ' which loads the capsule on arrival. The context follows the handoff automatically.',
36
+ '',
37
+ 'The **Task Contract** below is the definition of done — honor it. The human stays the final gate.',
38
+ ].join('\n');
39
+ return [
40
+ '## Working with framein', guide, '',
41
+ '## Task Contract', contract, '',
42
+ '## Project Rules', rules, '',
43
+ '_Project defaults — change them with `framein rules set "<…>"` (edits typed directly here are overwritten on sync). They guide the agent; the Validation Gate is what\'s enforced._', '',
44
+ '## Agent Roles', roleLines, '',
45
+ '## Architecture Decisions (digest)', buildAdrDigest(state.adrs),
46
+ ].join('\n');
47
+ }
48
+ /** The managed block (markers + canonical core), byte-identical across all three files. */
49
+ export function renderManagedBlock(state) {
50
+ return wrapManaged(buildCoreBlock(state));
51
+ }
52
+ /** A from-scratch native file (heading + managed block); used when no file exists yet. */
53
+ function projectFresh(title, state) {
54
+ return upsertManagedBlock(null, title, renderManagedBlock(state));
55
+ }
56
+ export function projectClaudeMd(state) { return projectFresh('CLAUDE.md', state); }
57
+ export function projectAgentsMd(state) { return projectFresh('AGENTS.md', state); }
58
+ export function projectGeminiMd(state) { return projectFresh('GEMINI.md', state); }
59
+ export function projectAll(state) {
60
+ return {
61
+ 'CLAUDE.md': projectClaudeMd(state),
62
+ 'AGENTS.md': projectAgentsMd(state),
63
+ 'GEMINI.md': projectGeminiMd(state),
64
+ };
65
+ }
package/dist/quota.js CHANGED
@@ -1,30 +1,30 @@
1
- // Reactive quota detection (F-ROLE-3). Parse a CLI's stderr/output for rate-limit / quota /
2
- // overload signals so the orchestrator can fail over to another agent. This is NOT "bypassing
3
- // limits" — it routes work within each subscription's normal use. Pure + fixture-tested; the
4
- // live capture of `output` (a real CLI run) is the B-layer spawn (see delegate.ts / M10).
5
- // Checked in order of severity: a hard quota wins over a transient rate-limit / overload.
6
- const QUOTA = /quota|usage limit|resource_exhausted|insufficient_quota/i;
7
- const RATE = /\b429\b|rate[\s_-]?limit|too many requests/i;
8
- const OVERLOAD = /overloaded|server is busy|try again later|\b529\b/i;
9
- function parseRetryAfter(text) {
10
- const m = text.match(/retry[\s-]?after[:\s]+(\d+)/i) ?? text.match(/try again in (\d+)\s*(seconds?|secs?|minutes?|mins?)/i);
11
- if (!m)
12
- return undefined;
13
- const n = Number(m[1]);
14
- if (!Number.isFinite(n))
15
- return undefined;
16
- const unit = (m[2] ?? 's').toLowerCase();
17
- return unit.startsWith('min') ? n * 60 : n;
18
- }
19
- /** Classify a single agent's output. `exhausted` is the failover trigger. */
20
- export function detectQuotaSignal(agent, output) {
21
- const text = output ?? '';
22
- let kind;
23
- if (QUOTA.test(text))
24
- kind = 'quota';
25
- else if (RATE.test(text))
26
- kind = 'rate-limit';
27
- else if (OVERLOAD.test(text))
28
- kind = 'overloaded';
29
- return { agent, exhausted: kind !== undefined, kind, retryAfterSec: parseRetryAfter(text) };
30
- }
1
+ // Reactive quota detection (F-ROLE-3). Parse a CLI's stderr/output for rate-limit / quota /
2
+ // overload signals so the orchestrator can fail over to another agent. This is NOT "bypassing
3
+ // limits" — it routes work within each subscription's normal use. Pure + fixture-tested; the
4
+ // live capture of `output` (a real CLI run) is the B-layer spawn (see delegate.ts / M10).
5
+ // Checked in order of severity: a hard quota wins over a transient rate-limit / overload.
6
+ const QUOTA = /quota|usage limit|resource_exhausted|insufficient_quota/i;
7
+ const RATE = /\b429\b|rate[\s_-]?limit|too many requests/i;
8
+ const OVERLOAD = /overloaded|server is busy|try again later|\b529\b/i;
9
+ function parseRetryAfter(text) {
10
+ const m = text.match(/retry[\s-]?after[:\s]+(\d+)/i) ?? text.match(/try again in (\d+)\s*(seconds?|secs?|minutes?|mins?)/i);
11
+ if (!m)
12
+ return undefined;
13
+ const n = Number(m[1]);
14
+ if (!Number.isFinite(n))
15
+ return undefined;
16
+ const unit = (m[2] ?? 's').toLowerCase();
17
+ return unit.startsWith('min') ? n * 60 : n;
18
+ }
19
+ /** Classify a single agent's output. `exhausted` is the failover trigger. */
20
+ export function detectQuotaSignal(agent, output) {
21
+ const text = output ?? '';
22
+ let kind;
23
+ if (QUOTA.test(text))
24
+ kind = 'quota';
25
+ else if (RATE.test(text))
26
+ kind = 'rate-limit';
27
+ else if (OVERLOAD.test(text))
28
+ kind = 'overloaded';
29
+ return { agent, exhausted: kind !== undefined, kind, retryAfterSec: parseRetryAfter(text) };
30
+ }