create-open-autonomy 2.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/README.md +75 -0
- package/package.json +38 -0
- package/src/cli.ts +35 -0
- package/src/kit.ts +139 -0
- package/template/.github/workflows/ci.yml +18 -0
- package/template/.github/workflows/land.yml +30 -0
- package/template/.open-autonomy/config.yaml +27 -0
- package/template/.open-autonomy/mint-key.ts +49 -0
- package/template/.open-autonomy/package.json +10 -0
- package/template/.open-autonomy/reporter.ts +320 -0
- package/template/.open-autonomy/setup.ts +91 -0
- package/template/AGENTS.md +9 -0
- package/template/CHANGELOG.md +4 -0
- package/template/CONSTITUTION.md +23 -0
- package/template/CONTRIBUTING.md +13 -0
- package/template/LICENSE +55 -0
- package/template/README.md +24 -0
- package/template/container/Dockerfile +13 -0
- package/template/container/Dockerfile.reporter +10 -0
- package/template/container/Dockerfile.valve +5 -0
- package/template/container/README.md +45 -0
- package/template/container/build-hermes.sh +21 -0
- package/template/container/compose.yml +99 -0
- package/template/container/hermes.pin +6 -0
- package/template/container/key-valve.ts +73 -0
- package/template/hermes/.no-bundled-skills +0 -0
- package/template/hermes/README.md +19 -0
- package/template/hermes/SOUL.md +7 -0
- package/template/hermes/config.yaml +32 -0
- package/template/hermes/cron/jobs.seed.json +12 -0
- package/template/hermes/hooks/seed/HOOK.yaml +4 -0
- package/template/hermes/hooks/seed/handler.py +220 -0
- package/template/hermes/kanban.seed.json +13 -0
- package/template/hermes/profiles/treasurer/.no-bundled-skills +0 -0
- package/template/hermes/profiles/treasurer/SOUL.md +13 -0
- package/template/hermes/profiles/treasurer/config.yaml +33 -0
- package/template/hermes/skills/open-autonomy/develop/SKILL.md +69 -0
- package/template/hermes/skills/open-autonomy/pm/SKILL.md +30 -0
- package/template/package.json +8 -0
- package/template/test/project.test.ts +7 -0
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
// The reporter: the project's development stream, published as it happens. An SDK-to-SDK bridge —
|
|
3
|
+
// supercode's harness SDK in (it discovers the agent's Hermes sessions and follows each transcript), the
|
|
4
|
+
// Open Autonomy SDK out (sessions, turns, updates on the project's page). It runs as the stack's keyless
|
|
5
|
+
// third service: it authenticates through the key valve's forwarded narration route and never sees the
|
|
6
|
+
// project's key. Nothing here drives the agent; it only reads.
|
|
7
|
+
//
|
|
8
|
+
// OPEN_AUTONOMY_BASE_URL=http://valve:8787/v1 bun .open-autonomy/reporter.ts [--config .open-autonomy/config.yaml]
|
|
9
|
+
//
|
|
10
|
+
// Supercode's contract, as its SDK documents it: `subscribeSessionIndex` lists sessions and streams
|
|
11
|
+
// index changes (`sessionIndexEvent`); `session(locator).follow()` yields a snapshot then appended
|
|
12
|
+
// messages; `subscribeSessionActivity` reports presence and turn state. A Hermes home is named by the
|
|
13
|
+
// path of its state.db in `homes.hermes`.
|
|
14
|
+
import { existsSync, readFileSync, readdirSync, writeFileSync } from 'node:fs';
|
|
15
|
+
import { resolve } from 'node:path';
|
|
16
|
+
import { SupercodeHarnessClient, type NormalizedMessage, type SessionActivity, type SessionDescriptor, type SessionLocator } from '@volter-ai-dev/supercode-harness-sdk';
|
|
17
|
+
import { ROADMAP_SCHEMA, type RoadmapItem } from './sdk/roadmap.ts';
|
|
18
|
+
import { OpenAutonomy, type Session, type Turn } from './sdk/client.ts';
|
|
19
|
+
|
|
20
|
+
const arg = (name: string): string | undefined => { const i = process.argv.indexOf(name); return i >= 0 ? process.argv[i + 1] : undefined; };
|
|
21
|
+
const configPath = resolve(arg('--config') ?? resolve(import.meta.dir, 'config.yaml'));
|
|
22
|
+
const cfg = readConfig(configPath);
|
|
23
|
+
const baseUrl = process.env.OPEN_AUTONOMY_BASE_URL ?? `${cfg.platform}/v1`;
|
|
24
|
+
const oa = new OpenAutonomy({ baseUrl, key: process.env.OPEN_AUTONOMY_KEY ?? 'valve' });
|
|
25
|
+
const stateFile = resolve(cfg.state_file);
|
|
26
|
+
const IDLE_END_MS = Number(process.env.OPEN_AUTONOMY_IDLE_END_MS ?? 5 * 60_000);
|
|
27
|
+
const TURN_END_MS = Number(process.env.OPEN_AUTONOMY_TURN_END_MS ?? 15_000);
|
|
28
|
+
const log = (m: string) => console.log(`reporter: ${m}`);
|
|
29
|
+
// The valve holds the key; its health line says when the key expires. Logged once at start so a reader of
|
|
30
|
+
// either log sees the expiry.
|
|
31
|
+
fetch(`${baseUrl.replace(/\/v1\/?$/, '')}/healthz`).then(async (r) => log(`valve: ${(await r.text()).trim()}`)).catch((e: Error) => log(`valve unreachable at start: ${e.message}`));
|
|
32
|
+
|
|
33
|
+
interface Config { account: string; platform: string; publish: { runs: boolean; chats: boolean; private: string[] }; hermes_home: string; state_file: string }
|
|
34
|
+
// The config's shape is small and fixed, so a line reader suffices: top-level `key: value` and the
|
|
35
|
+
// `publish:` block's own keys and list.
|
|
36
|
+
function readConfig(path: string): Config {
|
|
37
|
+
const text = existsSync(path) ? readFileSync(path, 'utf8') : '';
|
|
38
|
+
const top: Record<string, string> = {};
|
|
39
|
+
const publish: Record<string, string> = {};
|
|
40
|
+
const priv: string[] = [];
|
|
41
|
+
let block = '';
|
|
42
|
+
for (const raw of text.split('\n')) {
|
|
43
|
+
const line = raw.replace(/\s+#.*$/, '').trimEnd();
|
|
44
|
+
if (!line.trim() || line.trim().startsWith('#')) continue;
|
|
45
|
+
const topKey = /^([a-z_]+):\s*(.*)$/.exec(line);
|
|
46
|
+
if (topKey) { block = topKey[2] === '' ? topKey[1] : ''; if (topKey[2]) top[topKey[1]] = topKey[2].trim(); continue; }
|
|
47
|
+
if (block === 'publish') {
|
|
48
|
+
const kv = /^\s+([a-z_]+):\s*(.*)$/.exec(line);
|
|
49
|
+
if (kv) { if (kv[2]) publish[kv[1]] = kv[2].trim(); else if (kv[1] === 'private') publish.private = ''; continue; }
|
|
50
|
+
const item = /^\s+-\s+(.+)$/.exec(line);
|
|
51
|
+
if (item && item[1] !== '[]') priv.push(item[1].trim());
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return {
|
|
55
|
+
account: top.account ?? '', platform: (top.platform ?? 'https://open-autonomy.org').replace(/\/$/, ''),
|
|
56
|
+
publish: { runs: (publish.runs ?? 'true') !== 'false', chats: (publish.chats ?? 'false') === 'true', private: priv },
|
|
57
|
+
hermes_home: top.hermes_home ?? process.env.HERMES_HOME ?? '/opt/data',
|
|
58
|
+
state_file: top.state_file ?? resolve(import.meta.dir, 'reporter-state.json'),
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// What has been published: ended sessions are never reopened; open ones resume at the platform's offset.
|
|
63
|
+
interface State { ended: Record<string, string> }
|
|
64
|
+
const state: State = existsSync(stateFile) ? JSON.parse(readFileSync(stateFile, 'utf8')) as State : { ended: {} };
|
|
65
|
+
const saveState = () => { try { writeFileSync(stateFile, `${JSON.stringify(state, null, 2)}\n`); } catch (e) { log(`cannot write ${stateFile}: ${(e as Error).message}`); } };
|
|
66
|
+
|
|
67
|
+
// A run: the schedule fired it, or the board's dispatcher spawned it for a task (a worker or a reviewer).
|
|
68
|
+
const kindOf = (d: SessionDescriptor): 'run' | 'chat' => (d.trigger === 'cron' || d.trigger === 'heartbeat' || d.trigger === 'task' ? 'run' : 'chat');
|
|
69
|
+
// A run's source is its job's name. supercode's job model carries the job's id, not its name; Hermes keeps
|
|
70
|
+
// the name beside the id in its own schedule store in the home the reporter reads, so that is where the
|
|
71
|
+
// name comes from (read-only, refreshed whenever an id is new), falling back to the id.
|
|
72
|
+
const jobNames = new Map<string, string>();
|
|
73
|
+
function jobName(id: string): string {
|
|
74
|
+
if (!jobNames.has(id)) {
|
|
75
|
+
try {
|
|
76
|
+
const store = JSON.parse(readFileSync(resolve(cfg.hermes_home, 'cron', 'jobs.json'), 'utf8')) as { jobs?: Array<{ id?: string; name?: string }> } | Array<{ id?: string; name?: string }>;
|
|
77
|
+
for (const j of Array.isArray(store) ? store : store.jobs ?? []) if (j.id && j.name) jobNames.set(j.id, j.name);
|
|
78
|
+
} catch { /* no schedule store yet */ }
|
|
79
|
+
}
|
|
80
|
+
return jobNames.get(id) ?? id;
|
|
81
|
+
}
|
|
82
|
+
const sourceOf = (d: SessionDescriptor): string => (d.recurrence?.job_id ? jobName(d.recurrence.job_id) : d.trigger === 'task' ? 'board' : d.surface?.platform ?? kindOf(d));
|
|
83
|
+
function publishes(d: SessionDescriptor): boolean {
|
|
84
|
+
const id = d.locator.session_id;
|
|
85
|
+
if (cfg.publish.private.includes(id) || (d.recurrence?.job_id && cfg.publish.private.includes(d.recurrence.job_id))) return false;
|
|
86
|
+
return kindOf(d) === 'run' ? cfg.publish.runs : cfg.publish.chats;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// A transcript message as the platform's turns: a tool call is one turn, its result another.
|
|
90
|
+
function turnsOf(m: NormalizedMessage): Turn[] {
|
|
91
|
+
const text = typeof m.content === 'string' ? m.content : Array.isArray(m.content) ? m.content.map((p) => (typeof p === 'string' ? p : (p as { text?: string })?.text ?? '')).join('') : '';
|
|
92
|
+
const ts = m.metadata?.timestamp ?? m.metadata?.ts;
|
|
93
|
+
if (m.role === 'tool') return [{ ts, role: 'tool', tool: m.name ?? 'tool', result: text.slice(0, 600) }];
|
|
94
|
+
if (m.role === 'assistant') {
|
|
95
|
+
const out: Turn[] = [];
|
|
96
|
+
if (text.trim()) out.push({ ts, role: 'assistant', text: text.slice(0, 2000) });
|
|
97
|
+
for (const c of m.tool_calls ?? []) out.push({ ts, role: 'assistant', tool: c.function.name, args: c.function.arguments.slice(0, 600) });
|
|
98
|
+
return out;
|
|
99
|
+
}
|
|
100
|
+
if (m.role === 'user') return [{ ts, role: 'user', text: text.slice(0, 2000) }];
|
|
101
|
+
return [];
|
|
102
|
+
}
|
|
103
|
+
// The task a session serves is the board's task id: the dispatcher's own prompt names it (`work kanban task <id>`), and
|
|
104
|
+
// so does the agent branch (agent/<task id>) in what the session says, runs, or reads back.
|
|
105
|
+
const itemIn = (turns: Turn[]): string | undefined => turns.map((t) => (t.role === 'user' ? /\bkanban task (\S+)/.exec(t.text ?? '')?.[1] : undefined) ?? /\bagent\/([A-Za-z0-9][A-Za-z0-9._-]*)/.exec(`${t.args ?? ''} ${t.text ?? ''} ${t.result ?? ''}`)?.[1]).find(Boolean);
|
|
106
|
+
const shaIn = (turns: Turn[]): string | undefined => turns.map((t) => /PUSHED_BRANCH=agent\/[A-Za-z0-9._-]+ ([0-9a-f]{7,40})/.exec(t.result ?? '')?.[1]).find(Boolean);
|
|
107
|
+
|
|
108
|
+
// supercode synthesizes this result for a tool call whose answer is not recorded yet; a live follow sees it
|
|
109
|
+
// before the real result lands in its place. Such a turn is held back until it resolves.
|
|
110
|
+
const PLACEHOLDER = '[no tool result recorded — turn interrupted]';
|
|
111
|
+
const contentOf = (m: NormalizedMessage): string => (typeof m.content === 'string' ? m.content : Array.isArray(m.content) ? m.content.map((p) => (typeof p === 'string' ? p : (p as { text?: string })?.text ?? '')).join('') : '');
|
|
112
|
+
const isPlaceholder = (m: NormalizedMessage): boolean => m.role === 'tool' && contentOf(m) === PLACEHOLDER;
|
|
113
|
+
|
|
114
|
+
class Followed {
|
|
115
|
+
session?: Session;
|
|
116
|
+
seq = 0;
|
|
117
|
+
// How many of the transcript's messages have been published (the platform counts turns; a message may
|
|
118
|
+
// be several).
|
|
119
|
+
sentMessages = 0;
|
|
120
|
+
item?: string;
|
|
121
|
+
sha?: string;
|
|
122
|
+
lastAt = Date.now();
|
|
123
|
+
ended = false;
|
|
124
|
+
private timer?: ReturnType<typeof setTimeout>;
|
|
125
|
+
private syncing = false;
|
|
126
|
+
private dirty = false;
|
|
127
|
+
constructor(readonly d: SessionDescriptor) {}
|
|
128
|
+
get key(): string { return this.d.locator.session_id; }
|
|
129
|
+
async open(): Promise<void> {
|
|
130
|
+
const start = { key: this.key, kind: kindOf(this.d), source: sourceOf(this.d), title: this.d.title ?? undefined, startedAt: this.d.updated_at_ms ? new Date(this.d.updated_at_ms).toISOString() : undefined };
|
|
131
|
+
this.session = await oa.resume(this.key, cfg.account, start);
|
|
132
|
+
this.seq = this.session.seq;
|
|
133
|
+
// Resuming at the platform's turn offset: the message index it corresponds to.
|
|
134
|
+
if (this.seq > 0) {
|
|
135
|
+
const { session } = await sc.loadWindow(this.d.locator, { message_limit: 5000 });
|
|
136
|
+
let counted = 0;
|
|
137
|
+
for (const m of session.messages) { if (counted >= this.seq) break; counted += turnsOf(m).length; this.sentMessages += 1; }
|
|
138
|
+
}
|
|
139
|
+
log(`${this.key}: ${kindOf(this.d)} (${sourceOf(this.d)}) open at turn ${this.seq}`);
|
|
140
|
+
}
|
|
141
|
+
// Publish what the transcript holds beyond what was sent, read through supercode's window: everything up
|
|
142
|
+
// to the first unresolved tool result, all of it once the session is ending.
|
|
143
|
+
async sync(final = false): Promise<void> {
|
|
144
|
+
if (this.syncing) { this.dirty = true; return; }
|
|
145
|
+
this.syncing = true;
|
|
146
|
+
try {
|
|
147
|
+
do {
|
|
148
|
+
this.dirty = false;
|
|
149
|
+
const { session } = await sc.loadWindow(this.d.locator, { message_offset: this.sentMessages, message_limit: 500 });
|
|
150
|
+
const msgs = session.messages;
|
|
151
|
+
let n = final ? msgs.length : msgs.findIndex(isPlaceholder);
|
|
152
|
+
if (n < 0) n = msgs.length;
|
|
153
|
+
const ready = msgs.slice(0, n);
|
|
154
|
+
const turns = ready.flatMap(turnsOf);
|
|
155
|
+
if (turns.length) {
|
|
156
|
+
this.item ??= itemIn(turns);
|
|
157
|
+
this.sha ??= shaIn(turns);
|
|
158
|
+
await this.session!.turns(turns, this.item);
|
|
159
|
+
this.seq = this.session!.seq;
|
|
160
|
+
this.sentMessages += n;
|
|
161
|
+
this.lastAt = Date.now();
|
|
162
|
+
}
|
|
163
|
+
// The shape of a turn's end: the last message is the assistant's own text with no tool call pending.
|
|
164
|
+
// The timer is (re)armed only when the transcript moved; a quiet re-read leaves it running.
|
|
165
|
+
const last = msgs[msgs.length - 1];
|
|
166
|
+
if (!this.ended && (turns.length || !this.timer)) this.arm(n === msgs.length && last?.role === 'assistant' && !(last.tool_calls?.length));
|
|
167
|
+
} while (this.dirty);
|
|
168
|
+
} catch (e) { log(`${this.key}: sync failed (${(e as Error).message})`); }
|
|
169
|
+
finally { this.syncing = false; }
|
|
170
|
+
}
|
|
171
|
+
// A session ends when its transcript has ended: a closing assistant text followed by fifteen seconds of silence
|
|
172
|
+
// (a tool call in flight is never silence, its result is still to come), else the idle fallback.
|
|
173
|
+
arm(turnEnded = false): void {
|
|
174
|
+
clearTimeout(this.timer);
|
|
175
|
+
this.timer = setTimeout(() => void this.end(turnEnded ? 'turn ended' : 'idle'), turnEnded ? TURN_END_MS : IDLE_END_MS);
|
|
176
|
+
}
|
|
177
|
+
async end(why: string): Promise<void> {
|
|
178
|
+
if (this.ended) return;
|
|
179
|
+
this.ended = true;
|
|
180
|
+
clearTimeout(this.timer);
|
|
181
|
+
await this.sync(true);
|
|
182
|
+
let outcome: 'done' | 'failed' | undefined;
|
|
183
|
+
let report: string | undefined;
|
|
184
|
+
try {
|
|
185
|
+
const { summary } = await sc.loadWindow(this.d.locator, { message_tail: 20 });
|
|
186
|
+
report = summary.last_assistant_text?.slice(0, 4000) || undefined;
|
|
187
|
+
if (kindOf(this.d) === 'run') outcome = summary.end_of_turn && !!report ? 'done' : 'failed';
|
|
188
|
+
} catch (e) { log(`${this.key}: summary unavailable (${(e as Error).message})`); if (kindOf(this.d) === 'run') outcome = 'failed'; }
|
|
189
|
+
await this.session?.end({ outcome, report, commit: this.sha, item: this.item, endedAt: new Date().toISOString() });
|
|
190
|
+
state.ended[this.key] = new Date().toISOString();
|
|
191
|
+
saveState();
|
|
192
|
+
log(`${this.key}: ended (${why}${outcome ? `, ${outcome}` : ''})`);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// supercode is the reporter's own dependency (its npm package carries the binary), so it is found beside
|
|
197
|
+
// this file before anywhere on PATH; SUPERCODE_BIN names another build outright.
|
|
198
|
+
const supercode = [process.env.SUPERCODE_BIN, resolve(import.meta.dir, 'node_modules', '.bin', 'supercode')].filter((p): p is string => !!p).find(existsSync) ?? Bun.which('supercode') ?? 'supercode';
|
|
199
|
+
const sc = new SupercodeHarnessClient({ command: supercode, env: { ...process.env, HERMES_HOME: cfg.hermes_home } as Record<string, string> });
|
|
200
|
+
const homes = { hermes: resolve(cfg.hermes_home, 'state.db') };
|
|
201
|
+
const followed = new Map<string, Followed>();
|
|
202
|
+
const activitySubs = new Map<string, string>();
|
|
203
|
+
|
|
204
|
+
async function consider(d: SessionDescriptor): Promise<void> {
|
|
205
|
+
const key = d.locator.session_id;
|
|
206
|
+
if (d.locator.harness !== 'hermes' || followed.has(key) || state.ended[key]) return;
|
|
207
|
+
if (!publishes(d)) { log(`${key}: ${kindOf(d)} (${sourceOf(d)}) is private; not published`); state.ended[key] = 'private'; saveState(); return; }
|
|
208
|
+
const f = new Followed(d);
|
|
209
|
+
followed.set(key, f);
|
|
210
|
+
try {
|
|
211
|
+
await f.open();
|
|
212
|
+
f.arm();
|
|
213
|
+
const act = await sc.subscribeSessionActivity([d.locator], homes);
|
|
214
|
+
activitySubs.set(act.subscription, key);
|
|
215
|
+
for (const a of act.initial) activity(key, a);
|
|
216
|
+
void follow(f);
|
|
217
|
+
} catch (e) { log(`${key}: cannot open (${(e as Error).message})`); followed.delete(key); }
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// The follow stream is the trigger: every event means the transcript moved, and the window read is the
|
|
221
|
+
// truth of what it now holds. A slow tick covers a tool result landing in place without an event.
|
|
222
|
+
async function follow(f: Followed): Promise<void> {
|
|
223
|
+
const tick = setInterval(() => { if (!f.ended) void f.sync(); }, 5000);
|
|
224
|
+
try {
|
|
225
|
+
await f.sync();
|
|
226
|
+
for await (const ev of sc.session(f.d.locator).follow({ view: { tailMessages: 50, maxMessageChars: 200, includeSubagents: false } })) {
|
|
227
|
+
if (f.ended) break;
|
|
228
|
+
// `runtime_state` describes a supercode-managed runtime; a Hermes session never has one, so `persisted`
|
|
229
|
+
// says nothing about whether the run is over. Only a shutdown of one is an end.
|
|
230
|
+
if (ev.type === 'runtime_state' && ev.state === 'shutting_down' && f.seq > 0) await f.end(`runtime ${ev.state}`);
|
|
231
|
+
else if (ev.type === 'session_snapshot' || ev.type === 'messages_appended') await f.sync();
|
|
232
|
+
}
|
|
233
|
+
} catch (e) { log(`${f.key}: follow ended (${(e as Error).message})`); }
|
|
234
|
+
finally { clearInterval(tick); }
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
let seenWorking = new Set<string>();
|
|
238
|
+
function activity(key: string, a: SessionActivity): void {
|
|
239
|
+
const f = followed.get(key);
|
|
240
|
+
if (!f || f.ended) return;
|
|
241
|
+
if (a.turn === 'working' || a.presence === 'running') seenWorking.add(key);
|
|
242
|
+
else if (a.presence === 'persisted' && a.turn === 'idle' && seenWorking.has(key) && f.seq > 0) void f.end('idle after working');
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
sc.on('sessionIndexEvent', (ev) => { if ('changes' in ev) for (const c of ev.changes) if (c.kind !== 'removed') void consider(c.descriptor); });
|
|
246
|
+
sc.on('sessionActivityEvent', (ev) => { const key = activitySubs.get(ev.subscription); if (key) for (const a of ev.activities) activity(key, a); });
|
|
247
|
+
sc.on('exit', (code) => { log(`supercode harness serve exited (${code}); stopping`); process.exit(1); });
|
|
248
|
+
|
|
249
|
+
await sc.start();
|
|
250
|
+
// The board, through supercode's workflow layer, is the project's roadmap and its work record, both published
|
|
251
|
+
// through the SDK. Every task is a roadmap item — its id, its title, its lane as the item's status, the `- `
|
|
252
|
+
// lines of its body as the acceptance — and each task's board state (lane, attempts, handoff, review verdicts)
|
|
253
|
+
// is published under that item whenever it changes. A task the board marks done after a review it requested
|
|
254
|
+
// was approved by that review.
|
|
255
|
+
type BoardTask = { id: string; title?: string; body?: string; assignee?: string; lane: string; priority?: number; created_at?: string; completed_at?: string; attempts?: Array<{ id: string; profile?: string; status: string; started_at?: string; ended_at?: string; outcome?: string; handoff?: { summary?: string; branch?: string; commit?: string } }>; reviews?: Array<{ verdict: string; by?: string; reason?: string; at?: string }> };
|
|
256
|
+
// The lanes as the roadmap's four words: done; running or review is active; blocked or parked (scheduled) waits on a
|
|
257
|
+
// decision, so proposed; the rest is planned.
|
|
258
|
+
const statusOf = (lane: string): RoadmapItem['status'] => (lane === 'done' ? 'done' : lane === 'running' || lane === 'review' ? 'active' : lane === 'blocked' || lane === 'scheduled' ? 'proposed' : 'planned');
|
|
259
|
+
const boardDigests = new Map<string, string>();
|
|
260
|
+
let roadmapDigest = '';
|
|
261
|
+
async function board(): Promise<void> {
|
|
262
|
+
let read: { workflow?: { boards?: Record<string, { tasks?: Record<string, BoardTask> }> } };
|
|
263
|
+
try { read = await sc.workflowLoad({ from: 'hermes', home: cfg.hermes_home }) as typeof read; } catch (e) { log(`board unreadable: ${(e as Error).message}`); return; }
|
|
264
|
+
// The developer's tasks are the roadmap. Tasks assigned to another profile (a purchase request for the treasurer)
|
|
265
|
+
// are the board's own bookkeeping: their spend shows on the trail under the developer's task, not as items.
|
|
266
|
+
const tasks = Object.values(read.workflow?.boards ?? {}).flatMap((b) => Object.values(b.tasks ?? {})).filter((t) => t.lane !== 'archived' && (t.assignee ?? 'default') === 'default').sort((a, b) => (a.created_at ?? '').localeCompare(b.created_at ?? '') || a.id.localeCompare(b.id));
|
|
267
|
+
// A read that found no board at all (the database mid-write) is not an empty board.
|
|
268
|
+
if (!tasks.length) return;
|
|
269
|
+
const items: RoadmapItem[] = tasks.map((t) => ({ id: t.id, title: t.title ?? t.id, status: statusOf(t.lane), acceptance: (t.body ?? '').split('\n').filter((l) => /^- /.test(l)).map((l) => l.slice(2).trim()) }));
|
|
270
|
+
const digest = JSON.stringify(items);
|
|
271
|
+
if (digest !== roadmapDigest) {
|
|
272
|
+
try { const r = await oa.pushRoadmap({ schema: ROADMAP_SCHEMA, items }, 'kanban', 'reporter'); if (r.ok) { roadmapDigest = digest; if (!r.unchanged) log(`roadmap published from the board (${items.length} task(s))`); } else log(`roadmap publish refused: ${r.error ?? r.status}`); } catch (e) { log(`roadmap publish failed: ${(e as Error).message}`); }
|
|
273
|
+
}
|
|
274
|
+
for (const t of tasks) {
|
|
275
|
+
const item = t.id;
|
|
276
|
+
const attempts = (t.attempts ?? []).map((a) => ({ id: a.id, profile: a.profile, status: a.status, started_at: a.started_at, ended_at: a.ended_at, outcome: a.outcome, summary: a.handoff?.summary }));
|
|
277
|
+
const reviews = (t.reviews ?? []).map((r) => ({ verdict: r.verdict as 'requested', by: r.by, reason: r.reason, at: r.at }));
|
|
278
|
+
const requested = [...reviews].reverse().find((r) => r.verdict === 'requested');
|
|
279
|
+
if (t.lane === 'done' && requested && !reviews.some((r) => r.verdict === 'changes_requested' && (r.at ?? '') > (requested.at ?? ''))) reviews.push({ verdict: 'approved' as 'requested', by: attempts[attempts.length - 1]?.profile, at: t.completed_at ?? attempts[attempts.length - 1]?.ended_at });
|
|
280
|
+
const last = [...(t.attempts ?? [])].reverse().find((a) => a.handoff);
|
|
281
|
+
const state = { item, task_id: t.id, lane: t.lane, title: t.title, assignee: t.assignee, attempts, reviews, handoff: last?.handoff, updated_at: new Date().toISOString() };
|
|
282
|
+
const taskDigest = JSON.stringify([state.lane, attempts.map((a) => [a.id, a.status, a.ended_at]), reviews.length, last?.handoff?.summary]);
|
|
283
|
+
if (boardDigests.get(t.id) === taskDigest) continue;
|
|
284
|
+
try { if (await oa.task(state)) { boardDigests.set(t.id, taskDigest); log(`board: ${t.id} (${t.lane}, ${attempts.length} attempt(s), ${reviews.length} review(s))`); } } catch (e) { log(`board publish failed for ${t.id}: ${(e as Error).message}`); }
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
// The agent's setup, from the home it runs with — the identity text, the model, the schedule seed, the
|
|
288
|
+
// skills — published whenever they change. The platform reads no harness file.
|
|
289
|
+
const readText = (p: string): string | undefined => { try { return readFileSync(p, 'utf8'); } catch { return undefined; } };
|
|
290
|
+
// The project's documents, from the checkout: CONSTITUTION.md is what the project is (the page leads with its first
|
|
291
|
+
// paragraph), CHANGELOG.md is what shipped. Published when they change. The platform reads no file for either.
|
|
292
|
+
const projectDir = resolve(stateFile, '..', '..');
|
|
293
|
+
let docsDigest = '';
|
|
294
|
+
async function docs(): Promise<void> {
|
|
295
|
+
const d = { about_md: readText(resolve(projectDir, 'CONSTITUTION.md')), shipped_md: readText(resolve(projectDir, 'CHANGELOG.md')) };
|
|
296
|
+
const digest = JSON.stringify(d);
|
|
297
|
+
if (digest === docsDigest || (!d.about_md && !d.shipped_md)) return;
|
|
298
|
+
try { if (await oa.docs(d)) { docsDigest = digest; log(`documents published (${[d.about_md && 'about', d.shipped_md && 'shipped'].filter(Boolean).join(', ')})`); } } catch (e) { log(`documents publish failed: ${(e as Error).message}`); }
|
|
299
|
+
}
|
|
300
|
+
let setupDigest = '';
|
|
301
|
+
async function setup(): Promise<void> {
|
|
302
|
+
const home = cfg.hermes_home;
|
|
303
|
+
const config = readText(resolve(home, 'config.yaml')) ?? '';
|
|
304
|
+
const model = /^\s+default:\s*(\S+)/m.exec(config)?.[1];
|
|
305
|
+
const provider = /^\s+provider:\s*(\S+)/m.exec(config)?.[1];
|
|
306
|
+
let schedule: Array<{ name: string; schedule: string; description?: string }> = [];
|
|
307
|
+
try { const seed = JSON.parse(readText(resolve(home, 'cron', 'jobs.seed.json')) ?? '{}') as { jobs?: Array<{ name?: string; schedule?: string; prompt?: string; script?: string }> }; schedule = (seed.jobs ?? []).filter((j) => j.name && j.schedule).map((j) => ({ name: j.name!, schedule: j.schedule!, description: j.prompt ?? (j.script ? `runs ${j.script}` : undefined) })); } catch { /* no seed */ }
|
|
308
|
+
const skills: string[] = [];
|
|
309
|
+
try { for (const cat of readdirSync(resolve(home, 'skills'))) { try { for (const name of readdirSync(resolve(home, 'skills', cat))) if (existsSync(resolve(home, 'skills', cat, name, 'SKILL.md'))) skills.push(name); } catch { /* a file */ } } } catch { /* no skills */ }
|
|
310
|
+
const s = { harness: 'hermes', persona: readText(resolve(home, 'SOUL.md')), model, provider, schedule, skills: skills.sort(), setup_md: readText(resolve(home, 'README.md')) };
|
|
311
|
+
const digest = JSON.stringify(s);
|
|
312
|
+
if (digest === setupDigest) return;
|
|
313
|
+
try { if (await oa.setup(s)) { setupDigest = digest; log(`setup published (${model ?? 'no model'}, ${schedule.length} job(s), ${skills.length} skill(s))`); } } catch (e) { log(`setup publish failed: ${(e as Error).message}`); }
|
|
314
|
+
}
|
|
315
|
+
const index = await sc.subscribeSessionIndex({ harnesses: ['hermes'], homes });
|
|
316
|
+
await setup(); await docs(); await board();
|
|
317
|
+
setInterval(() => { void setup(); void docs(); void board(); }, 10_000);
|
|
318
|
+
log(`watching ${cfg.hermes_home} for ${cfg.account} → ${baseUrl} (${index.initial.length} session(s) on the index)`);
|
|
319
|
+
for (const d of index.initial) await consider(d);
|
|
320
|
+
process.on('SIGTERM', () => { void sc.close().then(() => process.exit(0)); });
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
// The host, set up by one command, idempotently: what container/README.md asks of the owner before
|
|
3
|
+
// `docker compose up`, done or found done, and what it cannot do said plainly. Safe to run again.
|
|
4
|
+
//
|
|
5
|
+
// bun .open-autonomy/setup.ts [--context <docker context>] [--secrets <dir>] [--origin <url>]
|
|
6
|
+
// [--origin-in-container <url>] [--env KEY=VALUE ...] [--uid N --gid N] [--fresh]
|
|
7
|
+
//
|
|
8
|
+
// 1. the key files <secrets>/agent.env (the developer's: spend + narrate) and <secrets>/treasurer.env (the
|
|
9
|
+
// treasurer's: spend + narrate + pay), default ~/.config/open-autonomy, from mint-key.ts — found or named
|
|
10
|
+
// 2. the image hermes-agent:<tag> from container/hermes.pin — present, copied from another Docker host
|
|
11
|
+
// that has it, or built (container/build-hermes.sh, ~10 minutes)
|
|
12
|
+
// 3. the volumes oa-home from hermes/ (its .env: the valve's address, the dummy key, every --env), oa-repo
|
|
13
|
+
// a clone of --origin (default: this repository's origin, cloned with your own git and
|
|
14
|
+
// keys); --fresh recreates both
|
|
15
|
+
// 4. what is yours the deploy key and the ssh-agent that forwards it, the Discord token; then compose up
|
|
16
|
+
//
|
|
17
|
+
// The world's stack step calls this same file, so what an adopter runs and what the world proves never drift.
|
|
18
|
+
import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync } from 'node:fs';
|
|
19
|
+
import { homedir } from 'node:os';
|
|
20
|
+
import { dirname, join, resolve } from 'node:path';
|
|
21
|
+
|
|
22
|
+
const here = resolve(import.meta.dir, '..');
|
|
23
|
+
const argv = process.argv.slice(2);
|
|
24
|
+
const arg = (name: string): string | undefined => { const i = argv.indexOf(name); return i >= 0 ? argv[i + 1] : undefined; };
|
|
25
|
+
const args = (name: string): string[] => argv.flatMap((a, i) => (a === name && argv[i + 1] ? [argv[i + 1]] : []));
|
|
26
|
+
const context = arg('--context') ?? process.env.DOCKER_CONTEXT;
|
|
27
|
+
const secrets = resolve(arg('--secrets') ?? join(homedir(), '.config', 'open-autonomy'));
|
|
28
|
+
const uid = arg('--uid') ?? String(process.getuid?.() ?? 501);
|
|
29
|
+
const gid = arg('--gid') ?? String(process.getgid?.() ?? 20);
|
|
30
|
+
const fresh = argv.includes('--fresh');
|
|
31
|
+
const docker = ['docker', ...(context ? ['--context', context] : [])];
|
|
32
|
+
const say = (m: string) => console.log(`setup: ${m}`);
|
|
33
|
+
const run = (cmd: string[], opts: { quiet?: boolean; check?: boolean; env?: Record<string, string>; cwd?: string } = {}) => {
|
|
34
|
+
const r = Bun.spawnSync({ cmd, cwd: opts.cwd ?? here, stdout: 'pipe', stderr: opts.quiet ? 'pipe' : 'inherit', env: { ...process.env, ...opts.env } });
|
|
35
|
+
if (opts.check !== false && r.exitCode !== 0) throw new Error(`${cmd.slice(0, 3).join(' ')} … failed (${r.exitCode})${opts.quiet ? `\n${r.stderr.toString().slice(-600)}` : ''}`);
|
|
36
|
+
return { code: r.exitCode, out: r.stdout.toString() };
|
|
37
|
+
};
|
|
38
|
+
const todo: string[] = [];
|
|
39
|
+
|
|
40
|
+
// 1. The key file.
|
|
41
|
+
const keyFile = join(secrets, 'agent.env');
|
|
42
|
+
const token = existsSync(keyFile) ? /^OPEN_AUTONOMY_KEY=(.+)$/m.exec(readFileSync(keyFile, 'utf8'))?.[1] : undefined;
|
|
43
|
+
if (token) {
|
|
44
|
+
try { const c = JSON.parse(Buffer.from(token.split('.')[0], 'base64url').toString('utf8')) as { kid?: string; exp?: string }; say(`key: ${c.kid} in ${keyFile}, expires ${c.exp}`); } catch { say(`key: present in ${keyFile}`); }
|
|
45
|
+
} else {
|
|
46
|
+
say(`no key in ${keyFile}`);
|
|
47
|
+
todo.push(`mint the developer's key: bun .open-autonomy/mint-key.ts${secrets === join(homedir(), '.config', 'open-autonomy') ? '' : ` --out ${keyFile}`}`);
|
|
48
|
+
}
|
|
49
|
+
const payFile = join(secrets, 'treasurer.env');
|
|
50
|
+
if (existsSync(payFile) && /^OPEN_AUTONOMY_KEY=/m.test(readFileSync(payFile, 'utf8'))) say(`treasurer's key: in ${payFile}`);
|
|
51
|
+
else { say(`no treasurer's key in ${payFile}`); todo.push(`mint the treasurer's key (the only one that pays): bun .open-autonomy/mint-key.ts --scopes spend,narrate,pay --out ${payFile}`); }
|
|
52
|
+
|
|
53
|
+
// 2. The image.
|
|
54
|
+
const pin = Object.fromEntries(readFileSync(join(here, 'container', 'hermes.pin'), 'utf8').split('\n').map((l) => l.trim().split('=') as [string, string]).filter(([k]) => k && !k.startsWith('#')));
|
|
55
|
+
const image = `hermes-agent:${pin.HERMES_TAG}`;
|
|
56
|
+
if (run([...docker, 'image', 'inspect', image], { quiet: true, check: false }).code === 0) say(`image: ${image} present`);
|
|
57
|
+
else {
|
|
58
|
+
const other = run(['docker', 'context', 'ls', '-q'], { quiet: true }).out.split('\n').map((c) => c.trim()).filter((c) => c && c !== context)
|
|
59
|
+
.find((c) => run(['docker', '--context', c, 'image', 'inspect', image], { quiet: true, check: false }).code === 0);
|
|
60
|
+
if (other) { say(`image: copying ${image} from Docker host ${other}`); run(['sh', '-c', `docker --context '${other}' save '${image}' | ${docker.join(' ')} load`]); }
|
|
61
|
+
else { say(`image: building ${image} (container/build-hermes.sh, ~10 minutes)`); run(['sh', join(here, 'container', 'build-hermes.sh')], { env: context ? { DOCKER_CONTEXT: context } : {} }); }
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// 3. The volumes.
|
|
65
|
+
const have = (v: string) => run([...docker, 'volume', 'inspect', v], { quiet: true, check: false }).code === 0;
|
|
66
|
+
if (fresh) for (const v of ['oa-home', 'oa-repo']) run([...docker, 'volume', 'rm', '-f', v], { quiet: true, check: false });
|
|
67
|
+
if (have('oa-home') && have('oa-repo')) say('volumes: oa-home and oa-repo present (compose re-syncs the home from hermes/ on every start; --fresh recreates both)');
|
|
68
|
+
else {
|
|
69
|
+
const origin = arg('--origin') ?? run(['git', 'remote', 'get-url', 'origin'], { quiet: true }).out.trim();
|
|
70
|
+
const originInside = arg('--origin-in-container') ?? origin;
|
|
71
|
+
for (const v of ['oa-home', 'oa-repo']) if (!have(v)) run([...docker, 'volume', 'create', v], { quiet: true });
|
|
72
|
+
const env = [`OPEN_AUTONOMY_BASE_URL=http://valve:8787/v1`, `OPEN_AUTONOMY_KEY=valve`, ...args('--env')];
|
|
73
|
+
run([...docker, 'run', '--rm', '-v', 'oa-home:/opt/data', '-v', `${join(here, 'hermes')}:/src:ro`, 'alpine:3', 'sh', '-c',
|
|
74
|
+
`cp -a /src/. /opt/data/ && printf '%s\\n' ${env.map((e) => `'${e.replace(/'/g, "'\\''")}'`).join(' ')} > /opt/data/.env && chown -R ${uid}:${gid} /opt/data`], { quiet: true });
|
|
75
|
+
say(`home: oa-home seeded from hermes/ (.env: the valve's address, the dummy key${args('--env').length ? `, ${args('--env').map((e) => e.split('=')[0]).join(', ')}` : ''})`);
|
|
76
|
+
// The clone is made on the host with your own git (and so your own keys), then carried into the volume through a
|
|
77
|
+
// directory under your home: a Docker host mounts the home directory, not the system's temporary one.
|
|
78
|
+
mkdirSync(join(homedir(), '.config', 'open-autonomy'), { recursive: true });
|
|
79
|
+
const tmp = mkdtempSync(join(homedir(), '.config', 'open-autonomy', 'setup-'));
|
|
80
|
+
try {
|
|
81
|
+
run(['git', 'clone', '-q', origin, join(tmp, 'repo')], { quiet: true });
|
|
82
|
+
if (originInside !== origin) run(['git', '-C', join(tmp, 'repo'), 'remote', 'set-url', 'origin', originInside], { quiet: true });
|
|
83
|
+
run([...docker, 'run', '--rm', '-v', 'oa-repo:/work', '-v', `${join(tmp, 'repo')}:/src:ro`, 'alpine:3', 'sh', '-c', `cp -a /src/. /work/ && chown -R ${uid}:${gid} /work`], { quiet: true });
|
|
84
|
+
} finally { rmSync(tmp, { recursive: true, force: true }); }
|
|
85
|
+
say(`repo: oa-repo cloned from ${origin}${originInside !== origin ? ` (origin inside the container: ${originInside})` : ''}`);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// 4. What is the owner's, and what is next.
|
|
89
|
+
say('yours: the deploy key and the ssh-agent that forwards it into the Docker host (container/README.md), and the Discord bot token if you deliver there');
|
|
90
|
+
for (const t of todo) say(`next: ${t}`);
|
|
91
|
+
say(`next: AGENT_SECRETS=${secrets} ${docker.join(' ')} compose -f container/compose.yml up -d --build`);
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# __PROJECT__ — rules for the agent working this repository
|
|
2
|
+
|
|
3
|
+
- **What this is.** __PROJECT__, a project that builds itself through Open Autonomy. `CONSTITUTION.md` is what it is and must remain. The board is what you build next, in order. `CONTRIBUTING.md` is how code is written here. `hermes/` is you.
|
|
4
|
+
- **Checks.** `bun run check` from the repository root is the project's definition of green. It must pass before every push.
|
|
5
|
+
- **Verify.** State here where the project is verified: its check, and any local or twinned surface. You cannot reach production and must not try. Where an acceptance line names a surface, exercise the surface.
|
|
6
|
+
- **Git.** You cannot push to `main` and must not try. Work on `agent/<task id>` off a fresh `origin/main`, commit small with the task id first in the subject, and push the branch; the landing workflow opens the pull request and it merges itself when the checks pass. Never rewrite history, never force-push.
|
|
7
|
+
- **Secrets.** There are none for you to use: your model calls and your pushes are authorized outside your reach. Never read or print `.env` files or key material; your sessions are published live.
|
|
8
|
+
- **Do not edit** `LICENSE`, `.github/workflows/`, `container/`, `.open-autonomy/reporter.ts`, or anything under `hermes/` except a skill a task asks you to improve.
|
|
9
|
+
- **Cost.** Your calls are metered and public. Read before writing; run the check once; stop when verified.
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# __PROJECT__ — constitution
|
|
2
|
+
|
|
3
|
+
__PROJECT__ is built by its own agent, in the open. Say here, in a paragraph, what the project is, for whom,
|
|
4
|
+
and what it will be when it is done.
|
|
5
|
+
|
|
6
|
+
What this project is and what it must remain. Its opening paragraph is the project's north star and leads its
|
|
7
|
+
page; the invariants below bind every task, and a review that finds one violated sends the work back whatever
|
|
8
|
+
else it got right. Changing this file is the owner's act, never a task's.
|
|
9
|
+
|
|
10
|
+
## Invariants
|
|
11
|
+
|
|
12
|
+
- **The board is the promise.** What will be built, in what order, is filed by the owner. The agent works it and
|
|
13
|
+
never invents work.
|
|
14
|
+
- **The agent is readable.** Its identity, skills and schedule live in `hermes/`; changing what it does is a
|
|
15
|
+
commit anyone can read.
|
|
16
|
+
- **Every spend is on the books.** Every model call and every purchase is metered to this project's account on
|
|
17
|
+
the platform and published, with what it was for.
|
|
18
|
+
- **Done means true in the running system.** A task is done when its acceptance lines hold where the project is
|
|
19
|
+
verified, not when code exists.
|
|
20
|
+
|
|
21
|
+
## Out of scope
|
|
22
|
+
|
|
23
|
+
Name what this project will not become, so a task that drifts there is refused rather than built.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Contributing to __PROJECT__
|
|
2
|
+
|
|
3
|
+
How code is written here, for people and for the agent alike. The bar every diff is reviewed against, beside
|
|
4
|
+
the constitution's invariants. Short on purpose; the reviewer reads it whole.
|
|
5
|
+
|
|
6
|
+
- **Language and tooling.** TypeScript on Bun. `bun run check` is the definition of green and runs in seconds.
|
|
7
|
+
- **Shape.** Small modules with one job each, named for what they hold. No layer that exists only to forward.
|
|
8
|
+
- **Tests.** A test proves an acceptance line or guards a bug that happened. No tests for their own sake, no
|
|
9
|
+
mocks of our own code, no fixtures larger than the thing they test.
|
|
10
|
+
- **Errors.** Fail loudly with the cause in the message. No silent fallbacks.
|
|
11
|
+
- **Docs.** A file's header says what it is for. The README says how to run it. Nothing else is documented twice.
|
|
12
|
+
- **Dependencies.** Add one only when writing it would be more code than reading it. Pin what you add.
|
|
13
|
+
- **History.** One change per commit, the task id first in the subject, signed as the agent.
|
package/template/LICENSE
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
Apache License
|
|
2
|
+
Version 2.0, January 2004
|
|
3
|
+
http://www.apache.org/licenses/
|
|
4
|
+
|
|
5
|
+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
|
6
|
+
|
|
7
|
+
1. Definitions.
|
|
8
|
+
|
|
9
|
+
"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document.
|
|
10
|
+
|
|
11
|
+
"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License.
|
|
12
|
+
|
|
13
|
+
"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity.
|
|
14
|
+
|
|
15
|
+
"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License.
|
|
16
|
+
|
|
17
|
+
"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files.
|
|
18
|
+
|
|
19
|
+
"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types.
|
|
20
|
+
|
|
21
|
+
"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below).
|
|
22
|
+
|
|
23
|
+
"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof.
|
|
24
|
+
|
|
25
|
+
"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution."
|
|
26
|
+
|
|
27
|
+
"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work.
|
|
28
|
+
|
|
29
|
+
2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form.
|
|
30
|
+
|
|
31
|
+
3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed.
|
|
32
|
+
|
|
33
|
+
4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions:
|
|
34
|
+
|
|
35
|
+
(a) You must give any other recipients of the Work or Derivative Works a copy of this License; and
|
|
36
|
+
|
|
37
|
+
(b) You must cause any modified files to carry prominent notices stating that You changed the files; and
|
|
38
|
+
|
|
39
|
+
(c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and
|
|
40
|
+
|
|
41
|
+
(d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License.
|
|
42
|
+
|
|
43
|
+
You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License.
|
|
44
|
+
|
|
45
|
+
5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions.
|
|
46
|
+
|
|
47
|
+
6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file.
|
|
48
|
+
|
|
49
|
+
7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License.
|
|
50
|
+
|
|
51
|
+
8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages.
|
|
52
|
+
|
|
53
|
+
9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability.
|
|
54
|
+
|
|
55
|
+
END OF TERMS AND CONDITIONS
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# __PROJECT__
|
|
2
|
+
|
|
3
|
+
[](https://open-autonomy.org/p/__ACCOUNT_ENC__)
|
|
4
|
+
[](https://open-autonomy.org/p/__ACCOUNT_ENC__)
|
|
5
|
+
[](https://open-autonomy.org/p/__ACCOUNT_ENC__)
|
|
6
|
+
[](https://open-autonomy.org/v1/accounts/__ACCOUNT_ENC__/calls)
|
|
7
|
+
|
|
8
|
+
This project builds itself. Its agent, a checked-in Hermes home under `hermes/`, works its board top to
|
|
9
|
+
bottom, funded through [Open Autonomy](https://open-autonomy.org/p/__ACCOUNT_ENC__), where
|
|
10
|
+
every session it works, every cent it spends and everything it ships is public.
|
|
11
|
+
|
|
12
|
+
- `CONSTITUTION.md` says what the project is and must remain; the agent's board says what gets built, in order; `CONTRIBUTING.md` is how code is written here, the bar every change is reviewed against.
|
|
13
|
+
- `AGENTS.md` is the agent's rules for this repository; `hermes/` is the agent.
|
|
14
|
+
- `.open-autonomy/` is the project's connection to the platform: its config, the reporter that publishes
|
|
15
|
+
the agent's sessions, and the record of the kit that made this repository.
|
|
16
|
+
- `container/` runs it: the agent, the key valve that holds the project's key, and the reporter.
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
bun run check # the project's own definition of green
|
|
20
|
+
bun .open-autonomy/mint-key.ts # prove control of this repository, get the project's key
|
|
21
|
+
AGENT_SECRETS=~/.config/open-autonomy docker compose -f container/compose.yml up -d --build
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Made with the Open Autonomy Hermes kit; `create-open-autonomy check .` says whether the kit's files are current.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# The agent's container: stock Hermes at the pinned tag (container/hermes.pin, built by
|
|
2
|
+
# container/build-hermes.sh) plus what a worker runs: git over ssh to push, bun for the
|
|
3
|
+
# project's checks. It holds no secret: the key lives in the valve, the push key in an ssh-agent forwarded
|
|
4
|
+
# from the host, and the only thing in its environment is an optional Discord token.
|
|
5
|
+
ARG HERMES_IMAGE=hermes-agent:v2026.8.31
|
|
6
|
+
FROM ${HERMES_IMAGE}
|
|
7
|
+
USER root
|
|
8
|
+
RUN apt-get update && apt-get install -y --no-install-recommends openssh-client git ca-certificates curl unzip \
|
|
9
|
+
&& rm -rf /var/lib/apt/lists/*
|
|
10
|
+
ARG BUN_VERSION=1.3.10
|
|
11
|
+
RUN curl -fsSL https://bun.sh/install | BUN_INSTALL=/usr/local bash -s "bun-v${BUN_VERSION}" \
|
|
12
|
+
&& ln -sf /usr/local/bin/bun /usr/local/bin/bunx && bun --version
|
|
13
|
+
RUN mkdir -p /work
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# The reporter: keyless. bun for the reporter itself and its two npm dependencies: supercode (the harness
|
|
2
|
+
# it reads the agent's sessions through, a prebuilt binary per platform) and supercode's harness SDK. The
|
|
3
|
+
# reporter's code is baked at build time; its config and its cursor live in the checkout.
|
|
4
|
+
FROM oven/bun:1.3.10
|
|
5
|
+
COPY .open-autonomy /opt/reporter
|
|
6
|
+
WORKDIR /opt/reporter
|
|
7
|
+
RUN bun install --no-save && chown -R bun:bun /opt/reporter && ./node_modules/.bin/supercode --version
|
|
8
|
+
ENV PATH="/opt/reporter/node_modules/.bin:${PATH}"
|
|
9
|
+
USER bun
|
|
10
|
+
CMD ["bun", "/opt/reporter/reporter.ts", "--config", "/work/project/.open-autonomy/config.yaml"]
|