@idleware/ringi 0.0.0-stage → 0.1.0
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/LICENSE +21 -0
- package/README.md +83 -2
- package/package.json +53 -4
- package/src/main/resources/profiles/a2a-1.json +43 -0
- package/src/main/resources/skill/ringi/SKILL.md +72 -0
- package/target/dts/app/agent.d.ts +104 -0
- package/target/dts/app/budget.d.ts +36 -0
- package/target/dts/app/context.d.ts +19 -0
- package/target/dts/app/hubs.d.ts +38 -0
- package/target/dts/app/overview.d.ts +25 -0
- package/target/dts/app/room.d.ts +76 -0
- package/target/dts/app/rooms.d.ts +26 -0
- package/target/dts/app/session.d.ts +60 -0
- package/target/dts/app/view.d.ts +64 -0
- package/target/dts/cli/agent.d.ts +10 -0
- package/target/dts/cli/args.d.ts +13 -0
- package/target/dts/cli/context.d.ts +6 -0
- package/target/dts/cli/index.d.ts +5 -0
- package/target/dts/cli/operator.d.ts +8 -0
- package/target/dts/cli/session.d.ts +7 -0
- package/target/dts/cli/shared.d.ts +20 -0
- package/target/dts/cli/skill.d.ts +4 -0
- package/target/dts/cli.d.ts +3 -0
- package/target/dts/domain/decisions.d.ts +65 -0
- package/target/dts/domain/index.d.ts +2 -0
- package/target/dts/domain/profiles.d.ts +39 -0
- package/target/dts/domain/signals.d.ts +37 -0
- package/target/dts/home/index.d.ts +21 -0
- package/target/dts/hub/index.d.ts +66 -0
- package/target/dts/hub/signals.d.ts +4 -0
- package/target/dts/index.d.ts +8 -0
- package/target/dts/lib/index.d.ts +18 -0
- package/target/dts/protocol/files.d.ts +9 -0
- package/target/dts/protocol/index.d.ts +72 -0
- package/target/dts/runtime/reader.d.ts +34 -0
- package/target/dts/runtime/session.d.ts +42 -0
- package/target/dts/runtime/watch.d.ts +17 -0
- package/target/dts/store/git/index.d.ts +64 -0
- package/target/dts/store/git/remote.d.ts +70 -0
- package/target/dts/store/index.d.ts +102 -0
- package/target/dts/store/kafka/index.d.ts +2 -0
- package/target/dts/store/temporal/index.d.ts +2 -0
- package/target/esm/app/agent.js +84 -0
- package/target/esm/app/budget.js +95 -0
- package/target/esm/app/context.js +16 -0
- package/target/esm/app/hubs.js +76 -0
- package/target/esm/app/overview.js +39 -0
- package/target/esm/app/room.js +127 -0
- package/target/esm/app/rooms.js +120 -0
- package/target/esm/app/session.js +97 -0
- package/target/esm/app/view.js +52 -0
- package/target/esm/cli/agent.js +40 -0
- package/target/esm/cli/args.js +61 -0
- package/target/esm/cli/context.js +8 -0
- package/target/esm/cli/index.js +125 -0
- package/target/esm/cli/operator.js +147 -0
- package/target/esm/cli/session.js +39 -0
- package/target/esm/cli/shared.js +143 -0
- package/target/esm/cli/skill.js +54 -0
- package/target/esm/cli.js +14 -0
- package/target/esm/domain/decisions.js +178 -0
- package/target/esm/domain/index.js +16 -0
- package/target/esm/domain/profiles.js +96 -0
- package/target/esm/domain/signals.js +129 -0
- package/target/esm/home/index.js +88 -0
- package/target/esm/hub/index.js +212 -0
- package/target/esm/hub/signals.js +27 -0
- package/target/esm/index.js +9 -0
- package/target/esm/lib/index.js +202 -0
- package/target/esm/protocol/files.js +19 -0
- package/target/esm/protocol/index.js +217 -0
- package/target/esm/runtime/reader.js +64 -0
- package/target/esm/runtime/session.js +112 -0
- package/target/esm/runtime/watch.js +304 -0
- package/target/esm/store/git/index.js +346 -0
- package/target/esm/store/git/remote.js +700 -0
- package/target/esm/store/index.js +15 -0
- package/target/esm/store/kafka/index.js +3 -0
- package/target/esm/store/temporal/index.js +3 -0
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
// operator: what the human runs in a terminal: where things stand, sessions, hubs and rooms. Each command parses its
|
|
2
|
+
// arguments, calls app operations and prints the result.
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import * as Hubs from '../app/hubs.js';
|
|
5
|
+
import * as Overview from '../app/overview.js';
|
|
6
|
+
import * as Rooms from '../app/rooms.js';
|
|
7
|
+
import { oneLine, render } from '../app/view.js';
|
|
8
|
+
import { config } from '../app/context.js';
|
|
9
|
+
import { withSizes } from '../app/room.js';
|
|
10
|
+
import { fail, short, sleep } from '../lib/index.js';
|
|
11
|
+
import { attachments, dump, flag, list, opt, out, textOf } from './args.js';
|
|
12
|
+
import { taskLine } from './shared.js';
|
|
13
|
+
// --- where things stand, sessions, forget ---
|
|
14
|
+
export const sessionLine = (s) => `${s.agent.padEnd(12)} ${s.liveness.padEnd(8)} ${(s.product ?? '?').padEnd(7)} ${s.handle}${s.scope ? ` scope: ${s.scope}` : ''}`;
|
|
15
|
+
export const where = async (o) => {
|
|
16
|
+
const H = Hubs.hubOf(opt(o, 'hub'));
|
|
17
|
+
const w = Overview.where(H, opt(o, 'room') ?? config().room);
|
|
18
|
+
out(`hub ${H.name}: ${H.d.store.type} store, ${H.contract.trust} senders, contract ${H.contract.version}, protocol ${H.contract.protocol}${H.contract.capabilities?.length ? `, ${H.contract.capabilities.join(' ')}` : ''}`);
|
|
19
|
+
const R = w.room;
|
|
20
|
+
if (!R)
|
|
21
|
+
return out('no current room: ringi room create <id> | ringi room switch <id>');
|
|
22
|
+
out(`room ${R.name}${R.archived ? ' (archived, read-only)' : ''}, genesis ${short(R.genesis)}, profile ${R.profile.name}; members: ${Object.entries(R.members).map(([id, i]) => i.role === 'root' ? `${id} (root)` : id).join(', ')}`);
|
|
23
|
+
out(w.sessions.length ? `sessions:\n${w.sessions.map(s => ` ${sessionLine(s)}`).join('\n')}` : 'sessions: none joined');
|
|
24
|
+
out(w.work.length ? `open work:\n${w.work.map(t => ` ${taskLine(t)}`).join('\n')}` : 'open work: none');
|
|
25
|
+
if (w.proposals.length)
|
|
26
|
+
out(`open proposals:\n${w.proposals.map(x => ` ${short(x.proposal.id)} ${x.state} → ${x.decider} ${x.proposal.subject}`).join('\n')}`);
|
|
27
|
+
const n = Number(opt(o, 'n') ?? (flag(o, 'short') ? 10 : 5));
|
|
28
|
+
out(`last ${Math.min(n, w.messages.length)} of ${w.messages.length} messages:`);
|
|
29
|
+
const show = (all, m) => out(flag(o, 'short') ? ` ${oneLine(m)}` : `\n${render(all, m, { max: 2000, files: () => withSizes(R, m) })}`);
|
|
30
|
+
for (const m of w.messages.slice(-n))
|
|
31
|
+
show(w.all, m);
|
|
32
|
+
if (!flag(o, 'follow'))
|
|
33
|
+
return;
|
|
34
|
+
out('following; new messages appear as they land (Ctrl-C to stop)');
|
|
35
|
+
const seen = new Set(w.all.keys());
|
|
36
|
+
for (;;) {
|
|
37
|
+
await sleep(Number(opt(o, 'interval') ?? 2) * 1000);
|
|
38
|
+
const now = Overview.arrived(H, R, seen);
|
|
39
|
+
for (const m of now.messages)
|
|
40
|
+
show(now.all, m);
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
export const sessions = (o) => {
|
|
44
|
+
if (o._[0] === 'forget') {
|
|
45
|
+
const handle = o._[1] ?? fail('usage: ringi sessions forget <handle>');
|
|
46
|
+
Overview.forgetSession(handle);
|
|
47
|
+
return out(`forgot session ${handle}; its seat is free`);
|
|
48
|
+
}
|
|
49
|
+
const ss = Overview.sessions();
|
|
50
|
+
if (flag(o, 'json'))
|
|
51
|
+
return dump(ss);
|
|
52
|
+
if (!ss.length)
|
|
53
|
+
return out('no sessions');
|
|
54
|
+
for (const s of ss)
|
|
55
|
+
out(`${s.hub}/${s.room} ${sessionLine(s)}`);
|
|
56
|
+
};
|
|
57
|
+
export const forget = (o) => {
|
|
58
|
+
const [name, id] = (o._[0] ?? '').split('/');
|
|
59
|
+
if (!name || !id)
|
|
60
|
+
fail('usage: ringi forget <room>/<log>');
|
|
61
|
+
const H = Hubs.hubOf(opt(o, 'hub'));
|
|
62
|
+
const n = Rooms.forgetLog(H, Rooms.roomIn(H, name), id);
|
|
63
|
+
out(`forgot the accepted history of ${name}/${id} for ${n} reader(s); the next read accepts what is there now`);
|
|
64
|
+
};
|
|
65
|
+
// --- hubs and rooms ---
|
|
66
|
+
export const hub = (o) => {
|
|
67
|
+
const [sub, arg] = o._;
|
|
68
|
+
if (!sub || sub === 'list') {
|
|
69
|
+
for (const h of Hubs.hubs())
|
|
70
|
+
out(`${h.current ? '*' : ' '} ${h.name.padEnd(12)} ${h.store}, ${h.trust}, contract ${h.contract}, ${h.rooms} room(s)${h.remote ? ` remote ${h.remote}` : ''} ${h.dir}`);
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
if (sub === 'create') {
|
|
74
|
+
const r = Hubs.create(arg ?? '', { signed: flag(o, 'signed'), url: opt(o, 'url') });
|
|
75
|
+
if (!r.add)
|
|
76
|
+
return out(`created hub ${arg} (${r.H.dir})`);
|
|
77
|
+
return out(`created signed hub ${arg}${r.H.d.store.remote ? `, published to ${r.H.d.store.remote}` : ''}. Its root key stays on this machine (${r.rootKey}).
|
|
78
|
+
Give every machine that joins it, out of band:
|
|
79
|
+
${r.add}`);
|
|
80
|
+
}
|
|
81
|
+
if (sub === 'switch') {
|
|
82
|
+
Hubs.use(arg ?? fail('usage: ringi hub switch <name>'));
|
|
83
|
+
return out(`current hub: ${arg}; agents keep their own rooms`);
|
|
84
|
+
}
|
|
85
|
+
if (sub === 'add') {
|
|
86
|
+
const H = Hubs.add(arg ?? '', o._[2] ?? '', { root: opt(o, 'root'), genesis: opt(o, 'genesis') });
|
|
87
|
+
return out(`added signed hub ${arg} from ${o._[2]}: contract ${H.contract.version}, ${Rooms.rooms(H).length} room(s), root pinned`);
|
|
88
|
+
}
|
|
89
|
+
if (sub === 'amend') {
|
|
90
|
+
const H = Hubs.hubOf(opt(o, 'hub'));
|
|
91
|
+
const files = list(o, 'attach').flatMap(attachments).map(e => ({ path: e.path, content: fs.readFileSync(e.file) }));
|
|
92
|
+
const r = Hubs.amend(H, { text: textOf(o), set: list(o, 'set'), authority: opt(o, 'authority'), files });
|
|
93
|
+
return out(`amended hub ${H.name}: contract ${r.version}; rooms keep their base until ringi room rebase-contract <room>`);
|
|
94
|
+
}
|
|
95
|
+
fail(`unknown: hub ${sub}`);
|
|
96
|
+
};
|
|
97
|
+
export const roomCmd = (o) => {
|
|
98
|
+
const [sub, arg] = o._;
|
|
99
|
+
const H = Hubs.hubOf(opt(o, 'hub'));
|
|
100
|
+
if (!sub || sub === 'list') {
|
|
101
|
+
const rs = Rooms.rooms(H, { archived: flag(o, 'archived') });
|
|
102
|
+
for (const r of rs.filter(r => !r.archived))
|
|
103
|
+
out(`${r.current ? '*' : ' '} ${r.name.padEnd(16)} ${r.members} member(s), ${r.active} active session(s), profile ${r.profile} genesis ${short(r.genesis)}`);
|
|
104
|
+
for (const r of rs.filter(r => r.archived))
|
|
105
|
+
out(` ${r.name.padEnd(16)} archived, read-only`);
|
|
106
|
+
if (!rs.length && !flag(o, 'archived'))
|
|
107
|
+
out(`no rooms in hub ${H.name}: ringi room create <id>`);
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
if (sub === 'create') {
|
|
111
|
+
const r = Rooms.create(H, arg ?? '', { profile: opt(o, 'profile') });
|
|
112
|
+
return out(`created room ${arg} in hub ${H.name} with profile ${r.profile}; it is the current room. In each chat that should take part, say: "join ${arg} as <id>; <what to accept>"`);
|
|
113
|
+
}
|
|
114
|
+
if (sub === 'switch') {
|
|
115
|
+
const R = Rooms.use(H, arg ?? fail('usage: ringi room switch <id>'));
|
|
116
|
+
return out(`current room: ${H.name}/${R.name}; agents keep their own rooms`);
|
|
117
|
+
}
|
|
118
|
+
if (sub === 'admit') {
|
|
119
|
+
const R = Rooms.roomIn(H, opt(o, 'room'));
|
|
120
|
+
Rooms.admit(H, R, arg ?? fail('usage: ringi room admit <agent> <public key|file>'), o._[2] ?? fail('usage: ringi room admit <agent> <public key|file>'));
|
|
121
|
+
return out(`admitted ${arg} to ${H.name}/${R.name}: its signed messages are accepted from now on; it can join`);
|
|
122
|
+
}
|
|
123
|
+
if (sub === 'remove') {
|
|
124
|
+
const R = Rooms.roomIn(H, opt(o, 'room'));
|
|
125
|
+
Rooms.remove(H, R, arg ?? fail('usage: ringi room remove <agent>'));
|
|
126
|
+
return out(`removed ${arg} from ${R.name}: it can no longer write or be woken there; returning needs a fresh join`);
|
|
127
|
+
}
|
|
128
|
+
if (sub === 'archive') {
|
|
129
|
+
const R = Rooms.roomIn(H, arg ?? fail('usage: ringi room archive <id>'));
|
|
130
|
+
const name = Rooms.archive(H, R);
|
|
131
|
+
return out(`archived ${R.name} as ${name}: read-only, wakes nobody; its sessions end. Read it with --room ${name}`);
|
|
132
|
+
}
|
|
133
|
+
if (sub === 'delete') {
|
|
134
|
+
const R = Rooms.roomIn(H, arg ?? fail('usage: ringi room delete <id> --yes'));
|
|
135
|
+
if (!flag(o, 'yes'))
|
|
136
|
+
fail(`deleting ${R.name} removes its history from the hub for good. Run again with --yes, or archive it instead`);
|
|
137
|
+
Rooms.erase(H, R);
|
|
138
|
+
return out(`deleted room ${R.name}; sessions bound to it will be told it no longer exists`);
|
|
139
|
+
}
|
|
140
|
+
if (sub === 'rebase-contract') {
|
|
141
|
+
const R = Rooms.roomIn(H, arg ?? fail('usage: ringi room rebase-contract <id>'));
|
|
142
|
+
Rooms.rebase(H, R);
|
|
143
|
+
return out(`room ${R.name} now follows the hub contract at ${short(H.main)} (${H.contract.version})`);
|
|
144
|
+
}
|
|
145
|
+
fail(`unknown: room ${sub}`);
|
|
146
|
+
};
|
|
147
|
+
//# sourceMappingURL=operator.js.map
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// Session interface: the operator's words become typed app requests.
|
|
2
|
+
import * as Session from '../app/session.js';
|
|
3
|
+
import { fail } from '../lib/index.js';
|
|
4
|
+
import { opt, out } from './args.js';
|
|
5
|
+
import { showInbox } from './agent.js';
|
|
6
|
+
import { taskLine } from './shared.js';
|
|
7
|
+
export const briefing = (s) => {
|
|
8
|
+
const r = Session.briefing(s);
|
|
9
|
+
out(`${s.agent} in ${s.hub}/${s.room}; scope: ${s.scope ?? 'none recorded; ask the operator what to accept'}`);
|
|
10
|
+
out('agent commands: ringi agent inbox | reply <id> … | send … | tasks | proposals | whoami. A turn: inbox, your part, replies, end the turn. Never block on a peer.');
|
|
11
|
+
if (!r.tasks.length)
|
|
12
|
+
out(`no open work (profile ${r.actor.R.profile.name}; tasks --all for everything)`);
|
|
13
|
+
for (const task of r.tasks)
|
|
14
|
+
out(taskLine(task));
|
|
15
|
+
showInbox(r.actor, r.inbox, { _: [] });
|
|
16
|
+
};
|
|
17
|
+
export const join = (o) => {
|
|
18
|
+
const [hub, room] = o._;
|
|
19
|
+
if (!hub || !room)
|
|
20
|
+
fail('usage: ringi join <hub> <room> [--as <id>] [--scope "<what the operator delegates>"]');
|
|
21
|
+
const r = Session.join({ hub, room, agent: opt(o, 'as'), scope: opt(o, 'scope') });
|
|
22
|
+
if (r.key) {
|
|
23
|
+
out(`${r.agent}'s key on this machine (${r.key.fingerprint}). Ask the root to admit it, then join again:\n ringi room admit ${r.agent} '${r.key.publicKey}' --hub ${r.hub} --room ${r.room}`);
|
|
24
|
+
process.exitCode = 3;
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
out(`joined ${r.hub}/${r.room} as ${r.agent}`);
|
|
28
|
+
briefing(r.session);
|
|
29
|
+
};
|
|
30
|
+
export const resume = (o) => briefing(Session.resume(o._[0]));
|
|
31
|
+
export const stop = () => {
|
|
32
|
+
const s = Session.stop();
|
|
33
|
+
out(`stopped: ${s.agent} keeps its seat in ${s.hub}/${s.room} and reacts to nothing until the operator says "continue"`);
|
|
34
|
+
};
|
|
35
|
+
export const leave = () => {
|
|
36
|
+
const s = Session.leave();
|
|
37
|
+
out(`left ${s.hub}/${s.room}: the seat of ${s.agent} is free; ${s.agent} stays a member until the operator removes it`);
|
|
38
|
+
};
|
|
39
|
+
//# sourceMappingURL=session.js.map
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
// shared: commands the operator and agents both run: messages, threads, work, signals, proposals and grants. Each
|
|
2
|
+
// parses its arguments, calls one app operation and prints the result.
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import * as Budget from '../app/budget.js';
|
|
6
|
+
import * as Room from '../app/room.js';
|
|
7
|
+
import { checkPath, withSizes } from '../app/room.js';
|
|
8
|
+
import { asJson, oneLine, render } from '../app/view.js';
|
|
9
|
+
import { fail, note, short, when, words } from '../lib/index.js';
|
|
10
|
+
import { attachments, dump, flag, list, opt, out, textOf } from './args.js';
|
|
11
|
+
const sendInput = (o) => ({ text: textOf(o), to: words(opt(o, 'to')), all: flag(o, 'all'), task: flag(o, 'task'),
|
|
12
|
+
state: opt(o, 'state'), assign: opt(o, 'assign') ? words(opt(o, 'assign')) : undefined, roles: opt(o, 'roles'), grant: opt(o, 'grant'),
|
|
13
|
+
files: list(o, 'attach').flatMap(attachments) });
|
|
14
|
+
const sent = (r) => out(`sent ${short(r.id)} to ${r.to.join(', ')}${r.files ? ` with ${r.files} file(s)` : ''}`);
|
|
15
|
+
export const send = (a, o) => sent(Room.send(a, { ...sendInput(o), re: opt(o, 're') }));
|
|
16
|
+
export const reply = (a, o) => sent(Room.send(a, { ...sendInput(o), re: o._[0] ?? fail('usage: reply <msg> -m <text> …'), reply: true }));
|
|
17
|
+
export const read = (a, o) => {
|
|
18
|
+
const { all, m, replies, marks } = Room.message(a, o._[0] ?? fail('usage: read <msg>'));
|
|
19
|
+
if (flag(o, 'json'))
|
|
20
|
+
return dump(asJson(m));
|
|
21
|
+
out(render(all, m, { files: () => withSizes(a.R, m) }));
|
|
22
|
+
if (replies.length)
|
|
23
|
+
out(`replies: ${replies.map(x => `${short(x.id)} (${x.from})`).join(', ')}`);
|
|
24
|
+
if (marks)
|
|
25
|
+
out(Object.entries(marks).map(([mark, who]) => `${mark} by: ${who.join(', ')}`).join('; '));
|
|
26
|
+
};
|
|
27
|
+
export const thread = (a, o) => {
|
|
28
|
+
const { all, msgs } = Room.thread(a, o._[0] ?? fail('usage: thread <msg>'));
|
|
29
|
+
if (flag(o, 'json'))
|
|
30
|
+
return dump(msgs.map(asJson));
|
|
31
|
+
out(msgs.map(m => render(all, m, { files: () => withSizes(a.R, m) })).join('\n\n'));
|
|
32
|
+
};
|
|
33
|
+
export const taskLine = (t) => {
|
|
34
|
+
const roles = Object.keys(t.roles).length ? ` [${Object.entries(t.roles).map(([id, label]) => `${id}=${label}`).join(' ')}]` : '';
|
|
35
|
+
return `${short(t.id)} ${t.state.padEnd(14)} ${t.requester} → ${t.assignees.join(',') || '(open)'}${roles}${t.taken.length ? ` taken by ${t.taken.join(',')}` : ''}${t.grant ? ` grant ${short(t.grant)}` : ''} ${when(Date.parse(t.at) / 1000).slice(5)} by ${t.by} ${t.subject}`;
|
|
36
|
+
};
|
|
37
|
+
export const tasks = (a, o) => {
|
|
38
|
+
const { views } = Room.tasks(a, { all: flag(o, 'all') });
|
|
39
|
+
if (flag(o, 'json'))
|
|
40
|
+
return dump(views);
|
|
41
|
+
if (!views.length)
|
|
42
|
+
return out(flag(o, 'all') ? 'no work' : `no open work (profile ${a.R.profile.name}; tasks --all for everything)`);
|
|
43
|
+
for (const t of views)
|
|
44
|
+
out(taskLine(t));
|
|
45
|
+
};
|
|
46
|
+
export const log = (a, o) => {
|
|
47
|
+
const msgs = Room.log(a, { agent: opt(o, 'agent'), all: flag(o, 'all'), n: Number(opt(o, 'n') ?? 20) });
|
|
48
|
+
if (flag(o, 'json'))
|
|
49
|
+
return dump(msgs.map(asJson));
|
|
50
|
+
for (const m of msgs)
|
|
51
|
+
out(oneLine(m));
|
|
52
|
+
};
|
|
53
|
+
export const cat = (a, o) => {
|
|
54
|
+
const ref = o._[0] ?? fail('usage: cat <msg> [<path>]');
|
|
55
|
+
const p = o._[1] ?? (() => {
|
|
56
|
+
const { m } = Room.files(a, ref);
|
|
57
|
+
return m.files.length === 1 ? m.files[0].path : fail(`${short(m.id)} has ${m.files.length} files; name one: ${m.files.map(f => f.path).join(', ')}`);
|
|
58
|
+
})();
|
|
59
|
+
process.stdout.write(Room.files(a, ref, p).files[0].content);
|
|
60
|
+
};
|
|
61
|
+
export const get = (a, o) => {
|
|
62
|
+
const { m, files } = Room.files(a, o._[0] ?? fail('usage: get <msg> [-o <dir>]'));
|
|
63
|
+
const dir = path.resolve(opt(o, 'out') ?? short(m.id));
|
|
64
|
+
for (const f of files) {
|
|
65
|
+
const dest = path.join(dir, checkPath(f.path));
|
|
66
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
67
|
+
fs.writeFileSync(dest, f.content);
|
|
68
|
+
out(dest);
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
export const who = (a) => {
|
|
72
|
+
for (const x of Room.members(a))
|
|
73
|
+
out(`${x.id.padEnd(12)} ${x.role.padEnd(7)} ${String(x.messages).padStart(4)} msg${x.card ? ` ${x.card}` : ''}`);
|
|
74
|
+
};
|
|
75
|
+
export const signals = (a, o) => {
|
|
76
|
+
const res = Room.signals(a, { repair: flag(o, 'repair') });
|
|
77
|
+
if (res.repaired !== null)
|
|
78
|
+
out(`restored ${res.repaired} signal(s) of ${a.me}`);
|
|
79
|
+
for (const x of res.alarms)
|
|
80
|
+
note(`ALARM ${x}`);
|
|
81
|
+
if (flag(o, 'json'))
|
|
82
|
+
return dump(res.checked);
|
|
83
|
+
const at = (c) => res.all.get(c.target)?.time ?? 0;
|
|
84
|
+
const sorted = [...res.checked].sort((x, y) => at(x) - at(y));
|
|
85
|
+
const shown = flag(o, 'all') ? sorted : sorted.slice(-Number(opt(o, 'n') ?? 20));
|
|
86
|
+
if (!shown.length)
|
|
87
|
+
return out('no signals');
|
|
88
|
+
for (const c of shown)
|
|
89
|
+
out(`${short(c.target)} ${when(at(c)).slice(5)} ${c.actor.padEnd(10)} ${c.signal.padEnd(20)} ${c.ok ? res.all.get(c.target)?.subject ?? '' : `MISMATCH: ${c.why}`}`);
|
|
90
|
+
};
|
|
91
|
+
export const profileCmd = ({ R }, o) => {
|
|
92
|
+
if (flag(o, 'json'))
|
|
93
|
+
return dump(R.profile);
|
|
94
|
+
const P = R.profile;
|
|
95
|
+
out(`room ${R.name} follows profile ${P.name}${P.description ? `: ${P.description}` : ''}`);
|
|
96
|
+
out(`opens with Kind: ${P.opens}; states: ${P.states.join(', ')}; initial ${P.initial}; terminal ${P.terminal.join(', ')}${P.waiting ? `; waiting ${P.waiting}` : ''}`);
|
|
97
|
+
for (const [k, v] of Object.entries(P.rules))
|
|
98
|
+
out(` ${k.padEnd(18)} ${(v ?? []).join(', ')}`);
|
|
99
|
+
};
|
|
100
|
+
// --- decisions and budgets ---
|
|
101
|
+
export const propose = (a, o) => {
|
|
102
|
+
const decider = opt(o, 'decider') ?? fail('--decider <id> is required: who decides (operator, or someone with authority in the hub contract)');
|
|
103
|
+
const r = Budget.propose(a, { text: textOf(o), decider, reviewers: words(opt(o, 'reviewers')), budget: opt(o, 'budget'), due: opt(o, 'due'),
|
|
104
|
+
supersedes: opt(o, 'supersedes'), files: list(o, 'attach').flatMap(attachments) });
|
|
105
|
+
out(`proposed ${short(r.id)} to ${r.to.join(', ') || 'the room'}; decider ${decider}${r.budget ? `, budget ${Budget.budgetText(r.budget)}` : ''}`);
|
|
106
|
+
};
|
|
107
|
+
export const stamp_ = (a, o) => {
|
|
108
|
+
const s = o._[1] ?? '';
|
|
109
|
+
if (!Budget.STAMPS.includes(s))
|
|
110
|
+
fail(`usage: stamp <proposal> ${Budget.STAMPS.join('|')} -m <reason>`);
|
|
111
|
+
const r = Budget.stamp(a, o._[0] ?? fail('name the proposal'), s, textOf(o));
|
|
112
|
+
out(`stamped ${short(r.proposal.id)} ${s}: ${short(r.id)}`);
|
|
113
|
+
};
|
|
114
|
+
export const decide = (a, o) => {
|
|
115
|
+
const d = o._[1] ?? '';
|
|
116
|
+
if (!Budget.DECISIONS.includes(d))
|
|
117
|
+
fail(`usage: decide <proposal> ${Budget.DECISIONS.join('|')} -m <reason>`);
|
|
118
|
+
const r = Budget.decide(a, o._[0] ?? fail('name the proposal'), d, textOf(o));
|
|
119
|
+
out(`${d} ${short(r.proposal.id)}: ${short(r.id)}${r.budget ? `; grant ${short(r.id)} with ${Budget.budgetText(r.budget)}. Work spends it with --grant ${short(r.id)}` : ''}`);
|
|
120
|
+
};
|
|
121
|
+
export const proposals = (a, o) => {
|
|
122
|
+
const { proposals: ps, views } = Budget.proposals(a, { all: flag(o, 'all') });
|
|
123
|
+
if (flag(o, 'json'))
|
|
124
|
+
return dump(views);
|
|
125
|
+
if (!ps.length)
|
|
126
|
+
return out(flag(o, 'all') ? 'no proposals' : 'no open proposals (proposals --all for everything)');
|
|
127
|
+
for (const x of ps) {
|
|
128
|
+
const st = x.reviewers.map(r => `${r}:${x.stamps[r]?.stamp ?? '-'}`).join(' ');
|
|
129
|
+
out(`${short(x.proposal.id)} ${x.state.padEnd(10)} ${x.proposal.from} → ${x.decider}${x.budget ? ` [${Budget.budgetText(x.budget)}]` : ''}${st ? ` ${st}` : ''} ${x.proposal.subject}`);
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
export const grants = (a, o) => {
|
|
133
|
+
const gs = Budget.grants(a);
|
|
134
|
+
if (flag(o, 'json'))
|
|
135
|
+
return dump(gs);
|
|
136
|
+
if (!gs.length)
|
|
137
|
+
return out('no grants');
|
|
138
|
+
for (const g of gs) {
|
|
139
|
+
const u = g.usage;
|
|
140
|
+
out(`${short(g.grant)} ${g.revoked ? 'revoked ' : u.left ? 'active ' : 'spent '} runs ${u.runs}/${g.budget.runs}, open ${u.open}${u.uncertain ? `, uncertain ${u.uncertain}` : ''}, ${u.seconds}s used, ≤${g.budget['run-seconds']}s a run, concurrency ${g.budget.concurrency} ${g.subject}`);
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
//# sourceMappingURL=shared.js.map
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// skill: installs the agent skill for Claude Code, Codex and Cursor.
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { about } from '../app/context.js';
|
|
6
|
+
import { fail } from '../lib/index.js';
|
|
7
|
+
import { flag, out } from './args.js';
|
|
8
|
+
// --- skill ---
|
|
9
|
+
export const SKILL = path.join(about().resources, 'skill', 'ringi');
|
|
10
|
+
export const skillTargets = (project) => {
|
|
11
|
+
const base = project ? process.cwd() : os.homedir();
|
|
12
|
+
return [path.join(base, '.claude', 'skills', 'ringi'), path.join(base, '.agents', 'skills', 'ringi')];
|
|
13
|
+
};
|
|
14
|
+
export const skill = (o) => {
|
|
15
|
+
const sub = o._[0];
|
|
16
|
+
const same = (dir) => {
|
|
17
|
+
try {
|
|
18
|
+
return fs.readFileSync(path.join(dir, 'SKILL.md'), 'utf8') === fs.readFileSync(path.join(SKILL, 'SKILL.md'), 'utf8');
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
if (sub === 'install') {
|
|
25
|
+
for (const dir of skillTargets(flag(o, 'project'))) {
|
|
26
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
27
|
+
fs.cpSync(SKILL, dir, { recursive: true });
|
|
28
|
+
out(`installed ${dir}`);
|
|
29
|
+
}
|
|
30
|
+
return out(`ringi's home is ${about().home}. A sandboxed agent must be allowed to write it; for Codex, the narrowest grant is:
|
|
31
|
+
[sandbox_workspace_write]
|
|
32
|
+
writable_roots = ["${about().home}"]
|
|
33
|
+
in ~/.codex/config.toml. Otherwise agents re-run ringi commands with sandbox escalation, which you approve.`);
|
|
34
|
+
}
|
|
35
|
+
if (sub === 'status') {
|
|
36
|
+
for (const p of [false, true])
|
|
37
|
+
for (const dir of skillTargets(p))
|
|
38
|
+
out(`${same(dir) ? 'ok ' : fs.existsSync(dir) ? 'outdated' : 'missing '} ${dir}`);
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
if (sub === 'uninstall') {
|
|
42
|
+
for (const dir of skillTargets(flag(o, 'project'))) {
|
|
43
|
+
if (!fs.existsSync(dir))
|
|
44
|
+
continue;
|
|
45
|
+
fs.rmSync(dir, { recursive: true });
|
|
46
|
+
out(`removed ${dir}`);
|
|
47
|
+
}
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
if (sub === 'show')
|
|
51
|
+
return void process.stdout.write(fs.readFileSync(path.join(SKILL, 'SKILL.md'), 'utf8'));
|
|
52
|
+
fail('usage: ringi skill install [--project] | uninstall [--project] | status | show');
|
|
53
|
+
};
|
|
54
|
+
//# sourceMappingURL=skill.js.map
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// The `ringi` executable. The interface itself is cli/; this file only runs it when started as a program.
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { main } from './cli/index.js';
|
|
6
|
+
import { Fail } from './lib/index.js';
|
|
7
|
+
export { main };
|
|
8
|
+
if (process.argv[1] && fs.realpathSync(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
9
|
+
main().catch(e => {
|
|
10
|
+
process.stderr.write(`ringi: ${e instanceof Fail ? e.message : e.stack}\n`);
|
|
11
|
+
process.exitCode = 1;
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
//# sourceMappingURL=cli.js.map
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
// decide: the extension for decisions and budgets (capability `decisions/1`, docs/spec/decisions-1.md). A proposal circulates
|
|
2
|
+
// to named reviewers, who stamp it; only the named decider, holding authority from the hub's contract, decides. An
|
|
3
|
+
// approval that carries a budget is a grant; work spends against it, and the runner that starts model runs writes
|
|
4
|
+
// the usage receipts. Everything here is a pure fold over messages; nothing trusts a model's own report.
|
|
5
|
+
import { ID_RE, one, ordered } from '../protocol/index.js';
|
|
6
|
+
import { fail } from '../lib/index.js';
|
|
7
|
+
export const CAPABILITY = 'decisions/1';
|
|
8
|
+
export const ROOT = 'operator';
|
|
9
|
+
export const BUDGET_KEYS = ['runs', 'run-seconds', 'concurrency'];
|
|
10
|
+
export const STAMPS = ['approve', 'object', 'abstain'];
|
|
11
|
+
export const DECISIONS = ['approved', 'rejected', 'revoked'];
|
|
12
|
+
const ids = (v) => v === null ? [] : [...new Set(v.split(',').map(x => x.trim()).filter(Boolean))];
|
|
13
|
+
// `runs=5, run-seconds=600, concurrency=1`; undefined if malformed. `runs` is required: a budget without a run count
|
|
14
|
+
// bounds nothing; a zero run time or concurrency would disable the bound it names.
|
|
15
|
+
export const parseBudget = (s) => {
|
|
16
|
+
if (s === null || s === undefined)
|
|
17
|
+
return null;
|
|
18
|
+
const b = {};
|
|
19
|
+
for (const part of String(s).split(',').map(x => x.trim()).filter(Boolean)) {
|
|
20
|
+
const m = /^([a-z][a-z-]*)=(\d{1,9})$/.exec(part);
|
|
21
|
+
if (!m || !BUDGET_KEYS.includes(m[1]) || m[1] in b)
|
|
22
|
+
return undefined;
|
|
23
|
+
b[m[1]] = Number(m[2]);
|
|
24
|
+
}
|
|
25
|
+
if (!('runs' in b) || b['run-seconds'] === 0 || b.concurrency === 0)
|
|
26
|
+
return undefined;
|
|
27
|
+
return { 'run-seconds': 1800, concurrency: 1, ...b };
|
|
28
|
+
};
|
|
29
|
+
export const budgetText = (b) => BUDGET_KEYS.map(k => `${k}=${b[k]}`).join(', ');
|
|
30
|
+
// Relative (`90m`, `2h`, `3d`) or an ISO 8601 time → ISO UTC to the second.
|
|
31
|
+
export const parseDue = (s) => {
|
|
32
|
+
const rel = /^(\d{1,6})([mhd])$/.exec(s);
|
|
33
|
+
const t = rel ? Date.now() + Number(rel[1]) * { m: 60e3, h: 3600e3, d: 86400e3 }[rel[2]] : Date.parse(s);
|
|
34
|
+
if (!Number.isFinite(t))
|
|
35
|
+
fail(`--due takes 90m, 2h, 3d or an ISO time, not ${s}`);
|
|
36
|
+
return new Date(t).toISOString().slice(0, 19) + 'Z';
|
|
37
|
+
};
|
|
38
|
+
// Extension trailers are part of a record's validity: a malformed decision record routes nowhere. Returns the reason.
|
|
39
|
+
export const check = (m, isRef) => {
|
|
40
|
+
if (m.bad)
|
|
41
|
+
return null;
|
|
42
|
+
for (const k of ['decider', 'reviewers', 'due', 'budget', 'supersedes', 'stamp', 'decision', 'control', 'grant', 'usage', 'run', 'seconds', 'exit']) {
|
|
43
|
+
if ((m.trailers[k]?.length ?? 0) > 1)
|
|
44
|
+
return `duplicate ${k}`;
|
|
45
|
+
}
|
|
46
|
+
const ref = (k) => one(m, k) === null || isRef(one(m, k));
|
|
47
|
+
if (!ref('grant'))
|
|
48
|
+
return 'malformed Grant';
|
|
49
|
+
if (!ref('control'))
|
|
50
|
+
return 'malformed Control';
|
|
51
|
+
if (one(m, 'budget') !== null && !parseBudget(one(m, 'budget')))
|
|
52
|
+
return 'malformed Budget';
|
|
53
|
+
if (m.kind === 'proposal') {
|
|
54
|
+
if (!ID_RE.test(one(m, 'decider') ?? ''))
|
|
55
|
+
return 'proposal without a valid Decider';
|
|
56
|
+
if (!ids(one(m, 'reviewers')).every(x => ID_RE.test(x)))
|
|
57
|
+
return 'malformed Reviewers';
|
|
58
|
+
if (one(m, 'due') !== null && !Number.isFinite(Date.parse(one(m, 'due'))))
|
|
59
|
+
return 'malformed Due';
|
|
60
|
+
if (!ref('supersedes'))
|
|
61
|
+
return 'malformed Supersedes';
|
|
62
|
+
}
|
|
63
|
+
if (m.kind === 'stamp' && (!m.re || !STAMPS.includes(one(m, 'stamp') ?? '')))
|
|
64
|
+
return 'stamp without Re or a valid Stamp';
|
|
65
|
+
if (m.kind === 'decision' && (!m.re || !DECISIONS.includes(one(m, 'decision') ?? '')))
|
|
66
|
+
return 'decision without Re or a valid Decision';
|
|
67
|
+
if (m.kind === 'usage') {
|
|
68
|
+
if (!isRef(one(m, 'grant') ?? '') || !['reserve', 'settle'].includes(one(m, 'usage') ?? '') || !/^[A-Za-z0-9._:-]{1,128}$/.test(one(m, 'run') ?? ''))
|
|
69
|
+
return 'malformed usage receipt';
|
|
70
|
+
if (one(m, 'usage') === 'settle' && !/^\d{1,9}$/.test(one(m, 'seconds') ?? ''))
|
|
71
|
+
return 'usage settle without Seconds';
|
|
72
|
+
}
|
|
73
|
+
return null;
|
|
74
|
+
};
|
|
75
|
+
// The root always may; anyone else only within the authority table, and never on their own proposal.
|
|
76
|
+
export const mayDecide = (authority, decider, proposer, budget) => {
|
|
77
|
+
if (decider === ROOT)
|
|
78
|
+
return true;
|
|
79
|
+
if (decider === proposer)
|
|
80
|
+
return false;
|
|
81
|
+
const cap = authority?.decide?.[decider];
|
|
82
|
+
if (!cap)
|
|
83
|
+
return false;
|
|
84
|
+
if (!budget)
|
|
85
|
+
return true;
|
|
86
|
+
return BUDGET_KEYS.every(k => Number.isFinite(cap[k]) && budget[k] <= cap[k]);
|
|
87
|
+
};
|
|
88
|
+
// A decision names the room's control commit it was made under (`Control`); its authority is the table in effect
|
|
89
|
+
// there. A commit that does not exist yet cannot be named, so a later contract never promotes or demotes a decision.
|
|
90
|
+
const authorized = (m, p, decider, budget, authorityAt) => {
|
|
91
|
+
if (m.from === ROOT)
|
|
92
|
+
return true;
|
|
93
|
+
const table = authorityAt(one(m, 'control'));
|
|
94
|
+
return m.from === decider && !!table && mayDecide(table, decider, p.from, budget);
|
|
95
|
+
};
|
|
96
|
+
// Everything about one proposal, folded over the messages that answer it, in thread order.
|
|
97
|
+
export const proposalOf = (all, p, authorityAt, now = Date.now() / 1000) => {
|
|
98
|
+
const decider = one(p, 'decider');
|
|
99
|
+
const reviewers = ids(one(p, 'reviewers'));
|
|
100
|
+
const budget = parseBudget(one(p, 'budget')) ?? null;
|
|
101
|
+
const due = one(p, 'due') ? Date.parse(one(p, 'due')) / 1000 : null;
|
|
102
|
+
// A revision supersedes only when its author is the proposer or the root.
|
|
103
|
+
const revisions = ordered([...all.values()].filter(m => !m.bad && m.kind === 'proposal' && one(m, 'supersedes') === p.id && (m.from === p.from || m.from === ROOT)));
|
|
104
|
+
const answers = ordered([...all.values()].filter(m => !m.bad && m.reId === p.id && (m.kind === 'stamp' || m.kind === 'decision')));
|
|
105
|
+
const stamps = {};
|
|
106
|
+
let decision = null;
|
|
107
|
+
let revoked = null;
|
|
108
|
+
const superseded = revisions[0] ?? null;
|
|
109
|
+
const revocation = (m) => !revoked && one(m, 'decision') === 'revoked' && (m.from === decider || m.from === ROOT);
|
|
110
|
+
// A record after the superseding revision in thread order no longer applies to this revision.
|
|
111
|
+
for (const m of ordered([...answers, ...(superseded ? [superseded] : [])])) {
|
|
112
|
+
if (m === superseded)
|
|
113
|
+
break;
|
|
114
|
+
// A reviewer's latest stamp counts; its position relative to the decision is only a same-second tie.
|
|
115
|
+
if (m.kind === 'stamp' && reviewers.includes(m.from))
|
|
116
|
+
stamps[m.from] = { stamp: one(m, 'stamp'), id: m.id, reason: m.subject };
|
|
117
|
+
if (m.kind !== 'decision')
|
|
118
|
+
continue;
|
|
119
|
+
const d = one(m, 'decision');
|
|
120
|
+
if (!decision && d !== 'revoked' && m.from === decider && authorized(m, p, decider, budget, authorityAt))
|
|
121
|
+
decision = { decision: d, id: m.id, by: m.from, time: m.time };
|
|
122
|
+
else if (decision?.decision === 'approved' && revocation(m))
|
|
123
|
+
revoked = { id: m.id, by: m.from, time: m.time };
|
|
124
|
+
}
|
|
125
|
+
// After a superseding revision, only a revocation of an earlier approval still applies to this one.
|
|
126
|
+
if (superseded && decision?.decision === 'approved')
|
|
127
|
+
for (const m of answers)
|
|
128
|
+
if (revocation(m))
|
|
129
|
+
revoked = { id: m.id, by: m.from, time: m.time };
|
|
130
|
+
const missing = reviewers.filter(r => !stamps[r]);
|
|
131
|
+
const state = revoked ? 'revoked' : decision ? decision.decision : superseded ? 'superseded' : due && now > due ? 'overdue' : 'open';
|
|
132
|
+
const grant = decision?.decision === 'approved' && budget ? { id: decision.id, budget, proposal: p, revoked: !!revoked } : null;
|
|
133
|
+
return { proposal: p, decider, reviewers, budget, due, stamps, missing, decision, revoked, superseded, state, grant };
|
|
134
|
+
};
|
|
135
|
+
export const proposals = (all, authorityAt) => [...all.values()].filter(m => !m.bad && m.kind === 'proposal').map(p => proposalOf(all, p, authorityAt));
|
|
136
|
+
export const grantOf = (all, authorityAt, id) => {
|
|
137
|
+
const d = all.get(id);
|
|
138
|
+
if (!d || d.bad || d.kind !== 'decision' || !d.reId || !all.get(d.reId))
|
|
139
|
+
return null;
|
|
140
|
+
const g = proposalOf(all, all.get(d.reId), authorityAt).grant;
|
|
141
|
+
return g?.id === id ? g : null;
|
|
142
|
+
};
|
|
143
|
+
// The receipts a grant's ledger counts, in thread order: every reservation, and for each run (its runner and Run id)
|
|
144
|
+
// the first settlement after its reservation, as the first decision counts. A settlement of no reserved run counts
|
|
145
|
+
// nothing.
|
|
146
|
+
const ledger = (all, grant) => {
|
|
147
|
+
const reserves = [], settled = new Map(), reserved = new Set();
|
|
148
|
+
for (const m of ordered([...all.values()].filter(m => !m.bad && m.kind === 'usage' && one(m, 'grant') === grant.id))) {
|
|
149
|
+
const run = `${m.from}/${one(m, 'run')}`;
|
|
150
|
+
if (one(m, 'usage') === 'reserve') {
|
|
151
|
+
reserves.push(m);
|
|
152
|
+
reserved.add(run);
|
|
153
|
+
}
|
|
154
|
+
else if (reserved.has(run) && !settled.has(run))
|
|
155
|
+
settled.set(run, m);
|
|
156
|
+
}
|
|
157
|
+
return { reserves, settled };
|
|
158
|
+
};
|
|
159
|
+
// Whether a receipt counts: its Grant names a grant, and that grant's ledger holds it.
|
|
160
|
+
export const counted = (all, authorityAt, m) => {
|
|
161
|
+
const g = !m.bad && m.kind === 'usage' ? grantOf(all, authorityAt, one(m, 'grant') ?? '') : null;
|
|
162
|
+
if (!g)
|
|
163
|
+
return false;
|
|
164
|
+
const { reserves, settled } = ledger(all, g);
|
|
165
|
+
return reserves.includes(m) || [...settled.values()].includes(m);
|
|
166
|
+
};
|
|
167
|
+
// Usage from the runners' receipts: every reservation is a run, settled or not; an unsettled one is open while its
|
|
168
|
+
// run could still be going, and uncertain after that.
|
|
169
|
+
export const usageOf = (all, grant, now = Date.now() / 1000) => {
|
|
170
|
+
const { reserves, settled } = ledger(all, grant);
|
|
171
|
+
let seconds = 0;
|
|
172
|
+
for (const m of settled.values())
|
|
173
|
+
seconds += Number(one(m, 'seconds'));
|
|
174
|
+
const unsettled = reserves.filter(m => !settled.has(`${m.from}/${one(m, 'run')}`));
|
|
175
|
+
const open = unsettled.filter(m => now - m.time <= grant.budget['run-seconds'] + 60);
|
|
176
|
+
return { runs: reserves.length, open: open.length, uncertain: unsettled.length - open.length, seconds, left: Math.max(0, grant.budget.runs - reserves.length) };
|
|
177
|
+
};
|
|
178
|
+
//# sourceMappingURL=decisions.js.map
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// The capabilities this build enforces: `<name>/<major>` → the module whose folds apply it. A new major is a new module
|
|
2
|
+
// registered beside the old one. A hub contract selects at most one major of each name; a room applies what its
|
|
3
|
+
// contract base selects (docs/ARCHITECTURE.md, Evolution).
|
|
4
|
+
import * as decisions from './decisions.js';
|
|
5
|
+
import * as profiles from './profiles.js';
|
|
6
|
+
export const CAPABILITIES = { [decisions.CAPABILITY]: decisions, [profiles.PROFILES_CAPABILITY]: profiles };
|
|
7
|
+
// Why a contract's capability list cannot be enforced by this build, or nothing.
|
|
8
|
+
export const unenforceable = (ids) => {
|
|
9
|
+
const names = ids.map(id => id.split('/')[0]);
|
|
10
|
+
const twice = names.find((n, i) => names.indexOf(n) !== i);
|
|
11
|
+
if (twice)
|
|
12
|
+
return `selects two majors of ${twice}`;
|
|
13
|
+
const unknown = ids.filter(id => !CAPABILITIES[id]);
|
|
14
|
+
return unknown.length ? `requires ${unknown.join(', ')}` : null;
|
|
15
|
+
};
|
|
16
|
+
//# sourceMappingURL=index.js.map
|