framein 0.0.4 → 0.0.6

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/delegate.js CHANGED
@@ -1,64 +1,71 @@
1
- // Headless delegation (ADR-0007 B-2). The PRIMARY way framein pulls another role into a session:
2
- // drive each CLI's non-interactive print mode over child_process pipes — NO PTY. This sidesteps the
3
- // Windows ConPTY/node-pty risk and keeps zero runtime deps. Human-in-the-loop attach uses Node's
4
- // built-in `stdio:'inherit'` (interactiveCommand below) — a programmatic PTY (node-pty) is
5
- // deliberately not used (ADR-0009). These builders are pure; the spawn lives in cli.ts and is
6
- // live-verified against real claude/codex/gemini.
7
- import { DEFAULT_ROLE_PRIORITY } from './roles.js';
8
- /**
9
- * Non-interactive one-shot invocation for each agent (prompt via stdin, response on stdout).
10
- * `opts.trustFlags` (from trustPlan, F-TRUST) are FIXED per-agent permission-bypass flags appended
11
- * to argv — still no user input there, so it stays shell-safe under shell:true.
12
- */
13
- export function buildInvocation(agent, prompt, opts = {}) {
14
- const trust = opts.trustFlags ?? [];
15
- switch (agent) {
16
- case 'claude': return { command: 'claude', args: ['-p', ...trust], stdin: prompt };
17
- case 'codex': return { command: 'codex', args: ['exec', '--skip-git-repo-check', ...trust], stdin: prompt }; // exec refuses untrusted/non-git dirs otherwise
18
- // gemini -p takes the prompt as a value and APPENDS stdin; `--prompt=` (empty) + stdin keeps the
19
- // prompt off argv (shell-safe), `--skip-trust` is required for headless untrusted dirs. Verified.
20
- case 'gemini': return { command: 'gemini', args: ['--prompt=', '--skip-trust', ...trust], stdin: prompt };
21
- default: {
22
- const _exhaustive = agent;
23
- throw new Error(`unknown agent: ${String(_exhaustive)}`);
24
- }
25
- }
26
- }
27
- /** Which agent runs a role: the explicit assignment, else the role's default-priority head. */
28
- export function resolveAgent(roles, role) {
29
- return roles[role] ?? DEFAULT_ROLE_PRIORITY[role][0];
30
- }
31
- /** The fixed shell command (flags only, no user input) that runDelegated runs with shell:true. */
32
- export function invocationCommand(inv) {
33
- return [inv.command, ...inv.args].join(' ');
34
- }
35
- /**
36
- * The bare interactive command for an agent — launched with stdio:'inherit' so the human drives the
37
- * agent's own TUI directly, inside the already-synced frame (ADR-0007 B-2 interactive path, zero-dep).
38
- * A true programmatic PTY (read/inject/resize the agent's terminal) would need node-pty a native
39
- * runtime dependency that breaks framein's zero-dep invariant (ADR-0003)and framein observes via
40
- * the store/ledger, not by screen-scraping a TTY, so it is deliberately NOT used.
41
- */
42
- // `resume` re-enters the agent's MOST RECENT session in this cwd (handoff continuity, F-CAPSULE/ADR-0009):
43
- // claude `--continue`, codex `resume --last`, gemini `--resume`. framein decides re-entry from its own
44
- // ledger (a prior enter/return for this agent) it never scrapes the printed session id (ADR-0009).
45
- export function interactiveCommand(agent, resume = false, trustFlags = []) {
46
- // F-TRUST placement: codex `resume` is a SUBCOMMAND, so top-level bypass flags must come BEFORE it
47
- // (`codex --full-auto resume --last`); appending after the subcommand makes codex reject them. claude
48
- // `--continue` and gemini `--resume` are plain flags, so trust flags can follow.
49
- const t = trustFlags.length ? ` ${trustFlags.join(' ')}` : '';
50
- switch (agent) {
51
- case 'claude': return `claude${resume ? ' --continue' : ''}${t}`;
52
- case 'codex': return `codex${t}${resume ? ' resume --last' : ''}`;
53
- case 'gemini': return `gemini${resume ? ' --resume' : ''}${t}`;
54
- default: {
55
- const _exhaustive = agent;
56
- throw new Error(`unknown agent: ${String(_exhaustive)}`);
57
- }
58
- }
59
- }
60
- /** Human-readable preview for `--show`: the fixed command + a peek at the stdin prompt. */
61
- export function renderInvocation(inv) {
62
- const peek = inv.stdin.length > 60 ? inv.stdin.slice(0, 60) + '…' : inv.stdin;
63
- return `${invocationCommand(inv)} ⟵ stdin: ${JSON.stringify(peek)}`;
64
- }
1
+ // Headless delegation (ADR-0007 B-2). The PRIMARY way framein pulls another role into a session:
2
+ // drive each CLI's non-interactive print mode over child_process pipes — NO PTY. This sidesteps the
3
+ // Windows ConPTY/node-pty risk and keeps zero runtime deps. Human-in-the-loop attach uses Node's
4
+ // built-in `stdio:'inherit'` (interactiveCommand below) — a programmatic PTY (node-pty) is
5
+ // deliberately not used (ADR-0009). These builders are pure; the spawn lives in cli.ts and is
6
+ // live-verified against real claude/codex/gemini.
7
+ import { DEFAULT_ROLE_PRIORITY } from './roles.js';
8
+ export const HANDOFF_START_PROMPT = 'Framein handoff: run `framein capsule` first, restate the current task contract briefly, then continue from the local facts. Do not ask the user to re-explain context.';
9
+ /**
10
+ * Non-interactive one-shot invocation for each agent (prompt via stdin, response on stdout).
11
+ * `opts.trustFlags` (from trustPlan, F-TRUST) are FIXED per-agent permission-bypass flags appended
12
+ * to argv — still no user input there, so it stays shell-safe under shell:true.
13
+ */
14
+ export function buildInvocation(agent, prompt, opts = {}) {
15
+ const trust = opts.trustFlags ?? [];
16
+ switch (agent) {
17
+ case 'claude': return { command: 'claude', args: ['-p', ...trust], stdin: prompt };
18
+ case 'codex': return { command: 'codex', args: ['exec', '--skip-git-repo-check', ...trust], stdin: prompt }; // exec refuses untrusted/non-git dirs otherwise
19
+ // gemini -p takes the prompt as a value and APPENDS stdin; `--prompt=` (empty) + stdin keeps the
20
+ // prompt off argv (shell-safe), `--skip-trust` is required for headless untrusted dirs. Verified.
21
+ case 'gemini': return { command: 'gemini', args: ['--prompt=', '--skip-trust', ...trust], stdin: prompt };
22
+ default: {
23
+ const _exhaustive = agent;
24
+ throw new Error(`unknown agent: ${String(_exhaustive)}`);
25
+ }
26
+ }
27
+ }
28
+ /** Which agent runs a role: the explicit assignment, else the role's default-priority head. */
29
+ export function resolveAgent(roles, role) {
30
+ return roles[role] ?? DEFAULT_ROLE_PRIORITY[role][0];
31
+ }
32
+ /** The fixed shell command (flags only, no user input) that runDelegated runs with shell:true. */
33
+ export function invocationCommand(inv) {
34
+ return [inv.command, ...inv.args].join(' ');
35
+ }
36
+ /**
37
+ * The bare interactive command for an agent launched with stdio:'inherit' so the human drives the
38
+ * agent's own TUI directly, inside the already-synced frame (ADR-0007 B-2 interactive path, zero-dep).
39
+ * A true programmatic PTY (read/inject/resize the agent's terminal) would need node-ptya native
40
+ * runtime dependency that breaks framein's zero-dep invariant (ADR-0003) and framein observes via
41
+ * the store/ledger, not by screen-scraping a TTY, so it is deliberately NOT used.
42
+ */
43
+ // `resume` re-enters the agent's MOST RECENT session in this cwd (handoff continuity, F-CAPSULE/ADR-0009):
44
+ // claude `--continue`, codex `resume --last`, gemini `--resume`. framein decides re-entry from its own
45
+ // ledger (a prior enter/return for this agent) it never scrapes the printed session id (ADR-0009).
46
+ function shellQuote(arg) {
47
+ if (process.platform === 'win32')
48
+ return `"${arg.replace(/"/g, '\\"')}"`;
49
+ return `'${arg.replace(/'/g, `'\\''`)}'`;
50
+ }
51
+ export function interactiveCommand(agent, resume = false, trustFlags = [], initialPrompt) {
52
+ // F-TRUST placement: codex `resume` is a SUBCOMMAND, so top-level bypass flags must come BEFORE it
53
+ // (`codex --full-auto resume --last`); appending after the subcommand makes codex reject them. claude
54
+ // `--continue` and gemini `--resume` are plain flags, so trust flags can follow.
55
+ const t = trustFlags.length ? ` ${trustFlags.join(' ')}` : '';
56
+ const p = initialPrompt ? ` ${shellQuote(initialPrompt)}` : '';
57
+ switch (agent) {
58
+ case 'claude': return `claude${resume ? ' --continue' : ''}${t}${p}`;
59
+ case 'codex': return `codex${t}${resume ? ' resume --last' : ''}${p}`;
60
+ case 'gemini': return `gemini${resume ? ' --resume' : ''}${t}${initialPrompt ? ` --prompt-interactive${p}` : ''}`;
61
+ default: {
62
+ const _exhaustive = agent;
63
+ throw new Error(`unknown agent: ${String(_exhaustive)}`);
64
+ }
65
+ }
66
+ }
67
+ /** Human-readable preview for `--show`: the fixed command + a peek at the stdin prompt. */
68
+ export function renderInvocation(inv) {
69
+ const peek = inv.stdin.length > 60 ? inv.stdin.slice(0, 60) + '…' : inv.stdin;
70
+ return `${invocationCommand(inv)} ⟵ stdin: ${JSON.stringify(peek)}`;
71
+ }
package/dist/disagree.js CHANGED
@@ -1,85 +1,106 @@
1
- // Disagreement Protocol (F-LOOP-5, ADR-0008): bound model-vs-model debate so it converges instead
2
- // of looping. A proposal is met with at most `maxRounds` blocking challenges; the reviewer returns
3
- // CLAIMS + a required change (never edits — the lead keeps control); no agreement after the cap
4
- // escalates to the human with exactly two options. Pure state machine; the model-authored content
5
- // of each turn is the deferred live path.
6
- import { PLAIN } from './ui/theme.js';
7
- export const MAX_ROUNDS = 2;
8
- export function newDebate(topic, proposal, maxRounds = MAX_ROUNDS) {
9
- return { topic, entries: [{ kind: 'proposal', proposal }], maxRounds };
10
- }
11
- export function challengeCount(d) {
12
- return d.entries.filter((e) => e.kind === 'challenge').length;
13
- }
14
- function leadPosition(d) {
15
- for (let i = d.entries.length - 1; i >= 0; i--) {
16
- const e = d.entries[i];
17
- if (e.kind === 'revision')
18
- return e.revision.text || d.topic;
19
- if (e.kind === 'proposal')
20
- return e.proposal.text;
21
- }
22
- return d.topic;
23
- }
24
- function reviewerRequirement(d) {
25
- for (let i = d.entries.length - 1; i >= 0; i--) {
26
- const e = d.entries[i];
27
- if (e.kind === 'challenge' && e.challenge.verdict === 'challenge')
28
- return e.challenge.requiredChange ?? e.challenge.claim;
29
- }
30
- return undefined;
31
- }
32
- export function debateStatus(d) {
33
- const last = d.entries[d.entries.length - 1];
34
- const rounds = challengeCount(d);
35
- const escalate = () => ({
36
- state: 'escalate',
37
- reason: `no agreement after ${rounds} round${rounds === 1 ? '' : 's'} (max ${d.maxRounds})`,
38
- options: [`A: ${leadPosition(d)}`, `B: ${reviewerRequirement(d) ?? 'reviewer change'}`],
39
- });
40
- if (!last || last.kind === 'proposal')
41
- return { state: 'awaiting-challenge' };
42
- if (last.kind === 'challenge') {
43
- if (last.challenge.verdict === 'accept')
44
- return { state: 'resolved', how: 'accepted-by-reviewer' };
45
- if (rounds >= d.maxRounds)
46
- return escalate();
47
- return { state: 'awaiting-revision', required: last.challenge.requiredChange };
48
- }
49
- // last is a revision
50
- if (last.revision.accepted)
51
- return { state: 'resolved', how: 'lead-accepted' };
52
- if (rounds >= d.maxRounds)
53
- return escalate();
54
- return { state: 'awaiting-challenge' };
55
- }
56
- export function renderDebate(d, ui = PLAIN) {
57
- const lines = [`Debate: ${d.topic}`, ''];
58
- for (const e of d.entries) {
59
- if (e.kind === 'proposal')
60
- lines.push(`proposal${e.proposal.by ? ` (${e.proposal.by})` : ''}: ${e.proposal.text}`);
61
- else if (e.kind === 'challenge') {
62
- const c = e.challenge;
63
- lines.push(c.verdict === 'accept'
64
- ? `challenge${c.by ? ` (${c.by})` : ''}: accept`
65
- : `challenge${c.by ? ` (${c.by})` : ''}: ${c.claim ?? ''}${c.requiredChange ? ` → require: ${c.requiredChange}` : ''}`);
66
- }
67
- else {
68
- lines.push(`revision: ${e.revision.accepted ? 'accepted' : 'rejected'}${e.revision.text ? ` — ${e.revision.text}` : ''}`);
69
- }
70
- }
71
- lines.push('');
72
- const st = debateStatus(d);
73
- if (st.state === 'resolved')
74
- lines.push(ui.tone(`Resolved (${st.how}).`, 'success'));
75
- else if (st.state === 'escalate') {
76
- lines.push(ui.tone(`Escalate to human — ${st.reason}:`, 'warning'));
77
- for (const o of st.options)
78
- lines.push(` ${o}`);
79
- }
80
- else if (st.state === 'awaiting-revision')
81
- lines.push(ui.tone(`Awaiting lead revision${st.required ? ` (required: ${st.required})` : ''}.`, 'info'));
82
- else
83
- lines.push(ui.tone('Awaiting reviewer challenge.', 'info'));
84
- return lines.join('\n');
85
- }
1
+ // Disagreement Protocol (F-LOOP-5, ADR-0008): bound model-vs-model debate so it converges instead
2
+ // of looping. A proposal is met with at most `maxRounds` blocking challenges; the reviewer returns
3
+ // CLAIMS + a required change (never edits — the lead keeps control); no agreement after the cap
4
+ // escalates to the human with exactly two options. Pure state machine; the model-authored content
5
+ // of each turn is the deferred live path.
6
+ import { PLAIN } from './ui/theme.js';
7
+ export const MAX_ROUNDS = 2;
8
+ export function newDebate(topic, proposal, maxRounds = MAX_ROUNDS) {
9
+ return { topic, entries: [{ kind: 'proposal', proposal }], maxRounds };
10
+ }
11
+ export function challengeCount(d) {
12
+ return d.entries.filter((e) => e.kind === 'challenge').length;
13
+ }
14
+ function leadPosition(d) {
15
+ for (let i = d.entries.length - 1; i >= 0; i--) {
16
+ const e = d.entries[i];
17
+ if (e.kind === 'revision')
18
+ return e.revision.text || d.topic;
19
+ if (e.kind === 'response')
20
+ return e.response.proposedRevision ?? e.response.text ?? d.topic;
21
+ if (e.kind === 'proposal')
22
+ return e.proposal.text;
23
+ }
24
+ return d.topic;
25
+ }
26
+ function reviewerRequirement(d) {
27
+ for (let i = d.entries.length - 1; i >= 0; i--) {
28
+ const e = d.entries[i];
29
+ if (e.kind === 'challenge' && e.challenge.verdict === 'challenge')
30
+ return e.challenge.requiredChange ?? e.challenge.claim;
31
+ }
32
+ return undefined;
33
+ }
34
+ export function debateStatus(d) {
35
+ const last = d.entries[d.entries.length - 1];
36
+ const rounds = challengeCount(d);
37
+ const escalate = () => ({
38
+ state: 'escalate',
39
+ reason: `no agreement after ${rounds} round${rounds === 1 ? '' : 's'} (max ${d.maxRounds})`,
40
+ options: [`A: ${leadPosition(d)}`, `B: ${reviewerRequirement(d) ?? 'reviewer change'}`],
41
+ });
42
+ if (!last || last.kind === 'proposal')
43
+ return { state: 'awaiting-challenge' };
44
+ if (last.kind === 'challenge') {
45
+ if (last.challenge.verdict === 'accept')
46
+ return { state: 'resolved', how: 'accepted-by-reviewer' };
47
+ if (rounds >= d.maxRounds)
48
+ return escalate();
49
+ return { state: 'awaiting-revision', required: last.challenge.requiredChange };
50
+ }
51
+ if (last.kind === 'response') {
52
+ if (rounds >= d.maxRounds)
53
+ return escalate();
54
+ return { state: 'awaiting-decision', required: reviewerRequirement(d) };
55
+ }
56
+ // last is a revision
57
+ if (last.revision.accepted)
58
+ return { state: 'resolved', how: 'lead-accepted' };
59
+ if (rounds >= d.maxRounds)
60
+ return escalate();
61
+ return { state: 'awaiting-challenge' };
62
+ }
63
+ export function renderDebate(d, ui = PLAIN) {
64
+ const lines = [`Debate: ${d.topic}`, ''];
65
+ for (const e of d.entries) {
66
+ if (e.kind === 'proposal')
67
+ lines.push(`proposal${e.proposal.by ? ` (${e.proposal.by})` : ''}: ${e.proposal.text}`);
68
+ else if (e.kind === 'challenge') {
69
+ const c = e.challenge;
70
+ lines.push(c.verdict === 'accept'
71
+ ? `challenge${c.by ? ` (${c.by})` : ''}: accept`
72
+ : `challenge${c.by ? ` (${c.by})` : ''}: ${c.claim ?? ''}${c.requiredChange ? ` → require: ${c.requiredChange}` : ''}`);
73
+ if (c.basis?.length)
74
+ lines.push(` basis: ${c.basis.join(', ')}`);
75
+ if (c.missingEvidence?.length)
76
+ lines.push(` missing_evidence: ${c.missingEvidence.join('; ')}`);
77
+ }
78
+ else if (e.kind === 'response') {
79
+ const r = e.response;
80
+ lines.push(`response${r.by ? ` (${r.by})` : ''}: ${r.text}`);
81
+ if (r.proposedRevision)
82
+ lines.push(` proposed_revision: ${r.proposedRevision}`);
83
+ if (r.acceptsRequiredChange !== undefined)
84
+ lines.push(` accepts_required_change: ${r.acceptsRequiredChange ? 'yes' : 'no'}`);
85
+ }
86
+ else {
87
+ lines.push(`revision: ${e.revision.accepted ? 'accepted' : 'rejected'}${e.revision.text ? ` — ${e.revision.text}` : ''}`);
88
+ }
89
+ }
90
+ lines.push('');
91
+ const st = debateStatus(d);
92
+ if (st.state === 'resolved')
93
+ lines.push(ui.tone(`Resolved (${st.how}).`, 'success'));
94
+ else if (st.state === 'escalate') {
95
+ lines.push(ui.tone(`Escalate to human — ${st.reason}:`, 'warning'));
96
+ for (const o of st.options)
97
+ lines.push(` ${o}`);
98
+ }
99
+ else if (st.state === 'awaiting-decision')
100
+ lines.push(ui.tone(`Awaiting lead decision${st.required ? ` (reviewer requires: ${st.required})` : ''}.`, 'info'));
101
+ else if (st.state === 'awaiting-revision')
102
+ lines.push(ui.tone(`Awaiting lead revision${st.required ? ` (required: ${st.required})` : ''}.`, 'info'));
103
+ else
104
+ lines.push(ui.tone('Awaiting reviewer challenge.', 'info'));
105
+ return lines.join('\n');
106
+ }
package/dist/wrappers.js CHANGED
@@ -1,63 +1,62 @@
1
- // Native command wrappers (ADR-0010/0011). LOGIC-LESS: each wrapper just runs `<bin> <verb> [--json]`
2
- // and presents the result — the single source of truth stays the framein engine (no per-host logic →
3
- // no drift). Pure generators; the CLI (`fr integrations`) writes them. Namespace = `fr` (ADR-0011):
4
- // Claude/Gemini `/fr:<verb>`, Codex `$fr-<verb>` (a SKILL.md skill Codex's `/prompts` are deprecated).
5
- // Each host's surface syntax differs by design (ADR-0010 rejected forcing one syntax); only the verb +
6
- // `fr` namespace are unified. Files carry PROVENANCE so uninstall only removes our own.
7
- export const WRAP_VERBS = [
8
- { verb: 'start', json: false, desc: 'Start a Task Contract (what "done" means)' },
9
- { verb: 'verify', json: true, desc: 'Validation Gate: build/test vs the contract' },
10
- { verb: 'ship', json: true, desc: 'Ship gate: readiness + commit/deploy guidance' },
11
- { verb: 'rescue', json: true, desc: 'Detect a repair loop and propose options' },
12
- { verb: 'status', json: true, desc: 'Show framein state' },
13
- { verb: 'challenge', json: false, run: true, independent: true, desc: 'Get an INDEPENDENT model\'s verdict on a proposal (a different model reviews)' },
14
- { verb: 'risk', json: true, desc: 'Blast Radius: risk level + required gates for the change' },
15
- { verb: 'task', json: false, desc: 'Task Contract: show / amend the definition of done' },
16
- { verb: 'capsule', json: false, desc: 'Task Capsule: handoff-free continuity snapshot' },
17
- { verb: 'decide', json: false, desc: 'Resolve an open reviewer debate (accept / reject)' },
18
- ];
19
- export const PROVENANCE = 'generated-by: framein (fr integrations) — do not edit; regenerate with `fr integrations install`';
20
- // host = the agent this wrapper runs inside; passed as `--by <host>` for `independent` verbs so framein
21
- // can pick a reviewer that is NOT this agent.
22
- const cmd = (bin, v, host) => `${bin} ${v.verb}${v.json ? ' --json' : ''}${v.run ? ' --run' : ''}${v.independent ? ` --by ${host}` : ''}`;
23
- export function genClaudeCommand(v, bin = 'framein') {
24
- const content = [
25
- '---',
26
- `description: ${v.desc}`,
27
- `allowed-tools: Bash(${bin}:*)`,
28
- `# ${PROVENANCE}`,
29
- '---',
30
- '',
31
- `Run \`!${cmd(bin, v, 'claude')} $ARGUMENTS\` and present the result to the user clearly.`,
32
- '',
33
- ].join('\n');
34
- return { path: `.claude/commands/fr/${v.verb}.md`, content };
35
- }
36
- export function genGeminiCommand(v, bin = 'framein') {
37
- const content = [
38
- `# ${PROVENANCE}`,
39
- `description = ${JSON.stringify(v.desc)}`,
40
- `prompt = ${JSON.stringify(`Run framein and present the result:\n!{${cmd(bin, v, 'gemini')} {{args}}}`)}`,
41
- '',
42
- ].join('\n');
43
- return { path: `.gemini/commands/fr/${v.verb}.toml`, content };
44
- }
45
- /** Codex skill — invoked as `$fr-<verb>` (Codex skills live in .codex/skills/<name>/SKILL.md and are
46
- * triggered with `$<name>`; the older `/prompts:` path is deprecated, ADR-0010). */
47
- export function genCodexSkill(v, bin = 'framein') {
48
- const content = [
49
- '---',
50
- `name: fr-${v.verb}`,
51
- `description: ${JSON.stringify(v.desc)}`,
52
- `# ${PROVENANCE}`,
53
- '---',
54
- '',
55
- `Run \`${cmd(bin, v, 'codex')} $ARGUMENTS\` and present the result to the user clearly.`,
56
- '',
57
- ].join('\n');
58
- return { path: `.codex/skills/fr-${v.verb}/SKILL.md`, content };
59
- }
60
- export function wrapperFiles(host, bin = 'framein') {
61
- const gen = host === 'claude' ? genClaudeCommand : host === 'gemini' ? genGeminiCommand : genCodexSkill;
62
- return WRAP_VERBS.map((v) => gen(v, bin));
63
- }
1
+ // Native command wrappers (ADR-0010/0011). LOGIC-LESS: each wrapper just runs `<bin> <verb> [--json]`
2
+ // and presents the result — the single source of truth stays the framein engine (no per-host logic →
3
+ // no drift). Pure generators; the CLI (`fr integrations`) writes them. Namespace = `fr` (ADR-0011):
4
+ // Claude/Gemini `/fr:<verb>`, Codex `$fr-<verb>` (a SKILL.md skill under `.agents/skills`).
5
+ // Each host's surface syntax differs by design (ADR-0010 rejected forcing one syntax); only the verb +
6
+ // `fr` namespace are unified. Files carry PROVENANCE so uninstall only removes our own.
7
+ export const WRAP_VERBS = [
8
+ { verb: 'start', json: false, desc: 'When beginning work: start or reset the Task Contract that defines done' },
9
+ { verb: 'verify', json: true, desc: 'Before claiming done: run build/test validation against the contract' },
10
+ { verb: 'ship', json: true, desc: 'Before ship: check readiness, risk, commit, and deploy guidance' },
11
+ { verb: 'rescue', json: true, desc: 'When stuck in a repair loop: detect thrash and show recovery options' },
12
+ { verb: 'status', json: true, desc: 'When reorienting: show current roles, lock, and Framein state' },
13
+ { verb: 'challenge', json: false, run: true, independent: true, desc: 'When stuck or before accepting a risky plan: ask a different model to challenge it' },
14
+ { verb: 'risk', json: true, desc: 'Before editing sensitive areas: inspect blast radius and required gates' },
15
+ { verb: 'task', json: false, desc: 'When scope changes: show or amend the task contract and definition of done' },
16
+ { verb: 'capsule', json: false, desc: 'For handoff, switch, session compaction, or quota: prepare the next lead' },
17
+ { verb: 'decide', json: false, desc: 'After challenge: accept or reject the reviewer objection and record the decision' },
18
+ ];
19
+ export const PROVENANCE = 'generated-by: framein (fr integrations) — do not edit; regenerate with `fr integrations install`';
20
+ // host = the agent this wrapper runs inside; passed as `--by <host>` for `independent` verbs so framein
21
+ // can pick a reviewer that is NOT this agent.
22
+ const cmd = (bin, v, host) => `${bin} ${v.verb}${v.json ? ' --json' : ''}${v.run ? ' --run' : ''}${v.independent ? ` --by ${host}` : ''}`;
23
+ export function genClaudeCommand(v, bin = 'framein') {
24
+ const content = [
25
+ '---',
26
+ `description: ${v.desc}`,
27
+ `allowed-tools: Bash(${bin}:*)`,
28
+ `# ${PROVENANCE}`,
29
+ '---',
30
+ '',
31
+ `Run \`!${cmd(bin, v, 'claude')} $ARGUMENTS\` and present the result to the user clearly.`,
32
+ '',
33
+ ].join('\n');
34
+ return { path: `.claude/commands/fr/${v.verb}.md`, content };
35
+ }
36
+ export function genGeminiCommand(v, bin = 'framein') {
37
+ const content = [
38
+ `# ${PROVENANCE}`,
39
+ `description = ${JSON.stringify(v.desc)}`,
40
+ `prompt = ${JSON.stringify(`Run framein and present the result:\n!{${cmd(bin, v, 'gemini')} {{args}}}`)}`,
41
+ '',
42
+ ].join('\n');
43
+ return { path: `.gemini/commands/fr/${v.verb}.toml`, content };
44
+ }
45
+ /** Codex skill — invoked as `$fr-<verb>` (repo skills live in .agents/skills/<name>/SKILL.md). */
46
+ export function genCodexSkill(v, bin = 'framein') {
47
+ const content = [
48
+ '---',
49
+ `name: fr-${v.verb}`,
50
+ `description: ${JSON.stringify(v.desc)}`,
51
+ `# ${PROVENANCE}`,
52
+ '---',
53
+ '',
54
+ `Run \`${cmd(bin, v, 'codex')} $ARGUMENTS\` and present the result to the user clearly.`,
55
+ '',
56
+ ].join('\n');
57
+ return { path: `.agents/skills/fr-${v.verb}/SKILL.md`, content };
58
+ }
59
+ export function wrapperFiles(host, bin = 'framein') {
60
+ const gen = host === 'claude' ? genClaudeCommand : host === 'gemini' ? genGeminiCommand : genCodexSkill;
61
+ return WRAP_VERBS.map((v) => gen(v, bin));
62
+ }
package/package.json CHANGED
@@ -1,49 +1,50 @@
1
- {
2
- "name": "framein",
3
- "version": "0.0.4",
4
- "description": "Local work frame for Claude, Codex, Gemini, and AI coding agents.",
5
- "type": "module",
6
- "homepage": "https://www.framein.dev",
7
- "repository": {
8
- "type": "git",
9
- "url": "git+https://github.com/framein-dev/framein.git"
10
- },
11
- "bugs": {
12
- "url": "https://github.com/framein-dev/framein/issues"
13
- },
14
- "keywords": [
15
- "ai",
16
- "agents",
17
- "cli",
18
- "claude",
19
- "codex",
20
- "gemini",
21
- "validation"
22
- ],
23
- "bin": {
24
- "framein": "dist/bin.js",
25
- "fr": "dist/bin.js",
26
- "frame": "dist/bin.js"
27
- },
1
+ {
2
+ "name": "framein",
3
+ "version": "0.0.6",
4
+ "description": "Local work-state layer beneath AI coding agents and harnesses.",
5
+ "type": "module",
6
+ "homepage": "https://www.framein.dev",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/framein-dev/framein.git"
10
+ },
11
+ "bugs": {
12
+ "url": "https://github.com/framein-dev/framein/issues"
13
+ },
14
+ "keywords": [
15
+ "ai",
16
+ "agents",
17
+ "cli",
18
+ "claude",
19
+ "codex",
20
+ "gemini",
21
+ "agent-harness",
22
+ "validation"
23
+ ],
24
+ "bin": {
25
+ "framein": "dist/bin.js",
26
+ "fr": "dist/bin.js",
27
+ "frame": "dist/bin.js"
28
+ },
28
29
  "files": [
29
30
  "dist/**/*.js",
30
31
  "!dist/**/*.test.js"
31
32
  ],
32
- "scripts": {
33
- "build": "tsc",
34
- "build:sea": "npm run build && node scripts/build-sea.mjs",
35
- "test": "npm run build && node --no-warnings --test \"dist/**/*.test.js\"",
36
- "framein": "node --no-warnings dist/cli.js",
37
- "frame": "node --no-warnings dist/cli.js"
38
- },
39
- "engines": {
40
- "node": ">=22.5.0"
41
- },
42
- "license": "MIT",
43
- "devDependencies": {
44
- "@types/node": "^26.0.0",
45
- "esbuild": "^0.28.1",
46
- "postject": "^1.0.0-alpha.6",
47
- "typescript": "^6.0.3"
48
- }
49
- }
33
+ "scripts": {
34
+ "build": "tsc",
35
+ "build:sea": "npm run build && node scripts/build-sea.mjs",
36
+ "test": "npm run build && node --no-warnings --test \"dist/**/*.test.js\"",
37
+ "framein": "node --no-warnings dist/cli.js",
38
+ "frame": "node --no-warnings dist/cli.js"
39
+ },
40
+ "engines": {
41
+ "node": ">=22.5.0"
42
+ },
43
+ "license": "MIT",
44
+ "devDependencies": {
45
+ "@types/node": "^26.0.0",
46
+ "esbuild": "^0.28.1",
47
+ "postject": "^1.0.0-alpha.6",
48
+ "typescript": "^6.0.3"
49
+ }
50
+ }