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/README.md +88 -51
- package/dist/capsule.js +91 -64
- package/dist/challenge.js +195 -0
- package/dist/cli.js +2150 -2090
- package/dist/delegate.js +71 -64
- package/dist/disagree.js +106 -85
- package/dist/wrappers.js +62 -63
- package/package.json +46 -45
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
|
-
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
case '
|
|
18
|
-
|
|
19
|
-
// prompt
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
//
|
|
44
|
-
//
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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-pty — a 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 === '
|
|
20
|
-
return e.
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
if (last.kind === '
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
if (
|
|
46
|
-
return
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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
|
|
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: '
|
|
9
|
-
{ verb: 'verify', json: true, desc: '
|
|
10
|
-
{ verb: 'ship', json: true, desc: '
|
|
11
|
-
{ verb: 'rescue', json: true, desc: '
|
|
12
|
-
{ verb: 'status', json: true, desc: '
|
|
13
|
-
{ verb: 'challenge', json: false, run: true, independent: true, desc: '
|
|
14
|
-
{ verb: 'risk', json: true, desc: '
|
|
15
|
-
{ verb: 'task', json: false, desc: '
|
|
16
|
-
{ verb: 'capsule', json: false, desc: '
|
|
17
|
-
{ verb: 'decide', json: false, desc: '
|
|
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>` (
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
`
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
'
|
|
54
|
-
''
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
-
"description": "Local work
|
|
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
|
-
"
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
"
|
|
26
|
-
"
|
|
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
|
+
}
|