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
package/README.md
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# The Hermes kit
|
|
2
|
+
|
|
3
|
+
The default Open Autonomy starter kit: a complete repository that runs its own Hermes agent against the
|
|
4
|
+
platform, with the SDK wired in. One command scaffolds it from two identity parameters, the project's name
|
|
5
|
+
and its platform account; everything else is boilerplate the kit fills in.
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
bun create open-autonomy my-project --project my-project --account owner/my-project
|
|
9
|
+
create-open-autonomy adopt . --project my-project --account owner/my-project # into an existing repository
|
|
10
|
+
create-open-autonomy check . # the kit-owned files against the kit (exit 1 on drift)
|
|
11
|
+
create-open-autonomy upgrade . # check, then rewrite the kit-owned files
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## From npm
|
|
15
|
+
|
|
16
|
+
Both packages publish from this repository: `@open-autonomy/sdk` and `create-open-autonomy`, at one version.
|
|
17
|
+
`.github/workflows/release.yml` publishes them on a human-cut `release-v<version>` tag (or a manual dispatch)
|
|
18
|
+
after the `production` environment's reviewer approves, with egress locked to npm and the token it needs
|
|
19
|
+
(`NPM_TOKEN`) installed in that environment only. The world proves the same publish and a `bun create
|
|
20
|
+
open-autonomy` from it against the npm registry twin (`bun world/run.ts kit`) before any release is cut.
|
|
21
|
+
|
|
22
|
+
## What a generated repository contains
|
|
23
|
+
|
|
24
|
+
```text
|
|
25
|
+
README.md the project's front page, with the account's four widgets
|
|
26
|
+
CONSTITUTION.md what the project is and must remain; its first paragraph leads the page, its invariants bind every task
|
|
27
|
+
CONTRIBUTING.md how code is written here, the bar every diff is reviewed against
|
|
28
|
+
CHANGELOG.md what shipped
|
|
29
|
+
AGENTS.md the agent's rules for this repository
|
|
30
|
+
LICENSE Apache-2.0, seeded; the project's own
|
|
31
|
+
package.json, test/ the project's own check (`bun run check`), starting with one test
|
|
32
|
+
hermes/ the agent: SOUL.md, its two skills (develop, pm), profiles/treasurer (the second profile: the one that pays), kanban.seed.json (the board's first tasks, in order),
|
|
33
|
+
cron/jobs.seed.json (the PM, hourly), config.yaml (the model: the project's own choice), the seed hook
|
|
34
|
+
.open-autonomy/ the platform connection: config.yaml (account, publish policy, rail bounds), reporter.ts (the bridge:
|
|
35
|
+
sessions, the board, the setup), mint-key.ts (the key, the adopter way), setup.ts (the host, by one
|
|
36
|
+
command), the vendored SDK, kit.json (which kit, version and parameters made this repository)
|
|
37
|
+
container/ the stack: the agent, the key valve, the reporter; the pinned Hermes image
|
|
38
|
+
.github/workflows/ ci.yml (the project's check on every branch), land.yml (the landing convention)
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
**Kit-owned** files are kept current by `upgrade`: `hermes/` (except `config.yaml` and `kanban.seed.json`), the reporter,
|
|
42
|
+
the key tool, the vendored SDK, `container/`, the two workflows. A project that takes one over names it in
|
|
43
|
+
`kit.json`'s `divergences`. **Seeded** files are written once and never touched again: the README, the
|
|
44
|
+
board's seed, the constitution, `CONTRIBUTING.md`, the changelog, `AGENTS.md`, the license, the model config, the publish policy.
|
|
45
|
+
|
|
46
|
+
## How the repository runs itself
|
|
47
|
+
|
|
48
|
+
The agent is stock Hermes in a container, its home the committed `hermes/`, its checkout the repository,
|
|
49
|
+
its model calls forwarded by the key valve beside it (which alone holds the project's key) to the platform,
|
|
50
|
+
where each is metered to the project's account. The board is the roadmap: the owner files tasks (the seed
|
|
51
|
+
files the first ones, in order, on the first boot), the gateway's dispatcher pulls them down and runs each as
|
|
52
|
+
a worker session (the `develop` skill) that builds it, verifies it where `AGENTS.md` says the project is
|
|
53
|
+
verified, lands it on an `agent/<task id>` branch the landing workflow merges when the checks pass, and hands
|
|
54
|
+
off; the review lane, Hermes's own, verifies the handoff against the constitution and `CONTRIBUTING.md` in a session of its own; once an hour the PM job reads the whole board and unsticks what is stuck (the `pm` skill). Every
|
|
55
|
+
session shows on the project's page with its cost.
|
|
56
|
+
|
|
57
|
+
The reporter beside it is keyless: it discovers the agent's sessions through supercode's harness SDK
|
|
58
|
+
(`subscribeSessionIndex`, `follow`, `subscribeSessionActivity`) and publishes each one through the valve
|
|
59
|
+
with the Open Autonomy SDK, attaching it to the task it serves. It publishes the rest the same way: the board
|
|
60
|
+
(`workflowLoad`: every task as a roadmap item, and each task's lane, attempts, handoff and verdicts under it) and
|
|
61
|
+
the agent's setup (its persona, model, schedule and skills). The platform reads no file of
|
|
62
|
+
the agent's; everything a page shows about it came through the SDK. Scheduled runs publish by default;
|
|
63
|
+
`.open-autonomy/config.yaml` names the private exceptions. The project's page shows every session, update
|
|
64
|
+
and settled cent per item, live while a session runs.
|
|
65
|
+
|
|
66
|
+
**Rails.** The agent's model calls need no configuration beyond the key. `rails:` in
|
|
67
|
+
`.open-autonomy/config.yaml` opens the two others, off by default: a single-use card minted against the
|
|
68
|
+
balance for a bounded amount at the owner's merchant categories, and a partner service's metered charge
|
|
69
|
+
for a listed partner within a bound. Both leave records on the public audit trail naming the rail.
|
|
70
|
+
|
|
71
|
+
## Nothing in the agent's reach is a secret that matters
|
|
72
|
+
|
|
73
|
+
The agent's `.env` says `OPEN_AUTONOMY_KEY=valve`. Pushes sign through an ssh-agent forwarded from the host
|
|
74
|
+
holding one repository-scoped deploy key. Delivery uses at most a Discord bot token. Every session's turns
|
|
75
|
+
are published; the platform redacts secret-shaped text at intake as the second wall.
|
package/package.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "create-open-autonomy",
|
|
3
|
+
"version": "2.1.0",
|
|
4
|
+
"description": "The default Open Autonomy starter kit: a complete repository that runs its own Hermes agent against the platform, with the SDK wired in. `bun create open-autonomy <dir>` scaffolds one.",
|
|
5
|
+
"license": "Apache-2.0",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "https://github.com/open-autonomy-org/open-autonomy.git",
|
|
9
|
+
"directory": "packages/kit-hermes"
|
|
10
|
+
},
|
|
11
|
+
"type": "module",
|
|
12
|
+
"bin": {
|
|
13
|
+
"create-open-autonomy": "./src/cli.ts"
|
|
14
|
+
},
|
|
15
|
+
"exports": {
|
|
16
|
+
".": "./src/kit.ts"
|
|
17
|
+
},
|
|
18
|
+
"files": [
|
|
19
|
+
"src",
|
|
20
|
+
"template",
|
|
21
|
+
"README.md"
|
|
22
|
+
],
|
|
23
|
+
"publishConfig": {
|
|
24
|
+
"access": "public"
|
|
25
|
+
},
|
|
26
|
+
"scripts": {
|
|
27
|
+
"test": "bun test test/*.test.ts",
|
|
28
|
+
"typecheck": "bunx tsc --noEmit",
|
|
29
|
+
"check": "bun test test/*.test.ts && bunx tsc --noEmit"
|
|
30
|
+
},
|
|
31
|
+
"dependencies": {
|
|
32
|
+
"@open-autonomy/sdk": "2.0.0"
|
|
33
|
+
},
|
|
34
|
+
"devDependencies": {
|
|
35
|
+
"@types/bun": "^1.3.10",
|
|
36
|
+
"typescript": "^5.9.0"
|
|
37
|
+
}
|
|
38
|
+
}
|
package/src/cli.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
// create-open-autonomy: the Hermes kit's door.
|
|
3
|
+
//
|
|
4
|
+
// bun create open-autonomy <dir> --project <name> --account <owner/repo> # a complete new repository
|
|
5
|
+
// create-open-autonomy adopt <dir> --project <name> --account <owner/repo> # into an existing one; only what is missing
|
|
6
|
+
// create-open-autonomy check <dir> # the kit-owned files against the kit (exit 1 on drift)
|
|
7
|
+
// create-open-autonomy upgrade <dir> # check, then rewrite the kit-owned files
|
|
8
|
+
import { resolve } from 'node:path';
|
|
9
|
+
import { KIT, adopt, check, create, upgrade, validateParams } from './kit.ts';
|
|
10
|
+
|
|
11
|
+
const argv = process.argv.slice(2);
|
|
12
|
+
const flag = (name: string): string | undefined => { const i = argv.indexOf(name); return i >= 0 ? argv[i + 1] : undefined; };
|
|
13
|
+
const verbs = new Set(['create', 'adopt', 'check', 'upgrade']);
|
|
14
|
+
const verb = verbs.has(argv[0]) ? argv[0] : 'create';
|
|
15
|
+
const dir = argv.filter((a, i) => !a.startsWith('--') && argv[i - 1]?.startsWith('--') !== true && a !== verb)[0];
|
|
16
|
+
if (!dir) { console.error('usage: create-open-autonomy [create|adopt] <dir> --project <name> --account <owner/repo> | check <dir> | upgrade <dir>'); process.exit(2); }
|
|
17
|
+
const target = resolve(dir);
|
|
18
|
+
try {
|
|
19
|
+
if (verb === 'create' || verb === 'adopt') {
|
|
20
|
+
const params = validateParams({ project: flag('--project') ?? dir.split('/').filter(Boolean).pop(), account: flag('--account') });
|
|
21
|
+
const out = (verb === 'create' ? create : adopt)(target, params);
|
|
22
|
+
console.log(`${verb}: ${params.project} (${params.account}) → ${target}: ${out.written.length} file(s) written${out.skipped.length ? `, ${out.skipped.length} kept` : ''}`);
|
|
23
|
+
console.log(`next: commit it; mint the key (bun .open-autonomy/mint-key.ts); run the stack (container/README.md)`);
|
|
24
|
+
} else if (verb === 'check') {
|
|
25
|
+
const out = check(target);
|
|
26
|
+
if (out.drift.length) { for (const d of out.drift) console.error(d); console.error(`kit drift: ${out.drift.length} file(s). Run \`create-open-autonomy upgrade ${dir}\`; the kit is the source and its files are never edited in place (name a file in .open-autonomy/kit.json divergences to take it over).`); process.exit(1); }
|
|
27
|
+
console.log(`kit ${KIT.name} ${KIT.version}: ${target} matches`);
|
|
28
|
+
} else {
|
|
29
|
+
const out = upgrade(target);
|
|
30
|
+
console.log(`upgrade: ${out.written.length} file(s) rewritten${out.drift.length ? ` (${out.drift.length} had drifted)` : ''} → ${KIT.name} ${KIT.version}`);
|
|
31
|
+
}
|
|
32
|
+
} catch (e) {
|
|
33
|
+
console.error(`create-open-autonomy: ${(e as Error).message}`);
|
|
34
|
+
process.exit(1);
|
|
35
|
+
}
|
package/src/kit.ts
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
// The kit engine. A kit is a template directory rendered with the project's identity — its name and its
|
|
2
|
+
// platform account — into a complete repository. Two kinds of files come out of it:
|
|
3
|
+
// kit-owned the agent's home, the reporter, the container stack, the landing workflows: what the kit
|
|
4
|
+
// keeps current. `check` diffs them against a fresh render; `upgrade` rewrites them.
|
|
5
|
+
// seeded README, the board's seed, STANDARDS.md, vision, changelog, AGENTS.md, license, the model config, the publish
|
|
6
|
+
// policy: the project's own files, written once as a courtesy and never touched again.
|
|
7
|
+
// `.open-autonomy/kit.json` records which kit, which version and which parameters made the repo: the anchor
|
|
8
|
+
// `check` and `upgrade` read, so neither needs to be told anything twice.
|
|
9
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from 'node:fs';
|
|
10
|
+
import { dirname, join, relative, resolve } from 'node:path';
|
|
11
|
+
|
|
12
|
+
export const KIT = { name: 'hermes', version: '2.1.0' } as const;
|
|
13
|
+
export const KIT_FILE = '.open-autonomy/kit.json';
|
|
14
|
+
const TEMPLATE = resolve(import.meta.dir, '..', 'template');
|
|
15
|
+
|
|
16
|
+
export interface KitParams { project: string; account: string }
|
|
17
|
+
export interface KitRecord { kit: string; version: string; params: KitParams; divergences: string[] }
|
|
18
|
+
|
|
19
|
+
// What the kit keeps current. Everything else in the template is seeded once.
|
|
20
|
+
const OWNED = [/^hermes\/(?!config\.yaml$|kanban\.seed\.json$)/, /^\.open-autonomy\/(reporter\.ts|mint-key\.ts|setup\.ts|package\.json|sdk\/)/, /^container\//, /^\.github\/workflows\/(ci|land)\.yml$/];
|
|
21
|
+
export const isOwned = (rel: string): boolean => OWNED.some((re) => re.test(rel));
|
|
22
|
+
|
|
23
|
+
export function validateParams(p: Partial<KitParams>): KitParams {
|
|
24
|
+
if (!p.project || !/^[a-z0-9][a-z0-9._-]{0,63}$/i.test(p.project)) throw new Error('project: a short name (letters, digits, . _ -)');
|
|
25
|
+
if (!p.account || !/^[^/\s]+\/[^/\s]+$/.test(p.account)) throw new Error('account: owner/repo, the GitHub repository the platform funds');
|
|
26
|
+
return { project: p.project, account: p.account };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function walk(dir: string, base = dir): string[] {
|
|
30
|
+
const out: string[] = [];
|
|
31
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
32
|
+
const full = join(dir, entry.name);
|
|
33
|
+
if (entry.isDirectory()) out.push(...walk(full, base)); else out.push(relative(base, full));
|
|
34
|
+
}
|
|
35
|
+
return out.sort();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// The Open Autonomy SDK is vendored into the generated repository under .open-autonomy/sdk, kit-owned, so
|
|
39
|
+
// the reporter and the key tool run from a bare clone with no package to publish or install.
|
|
40
|
+
const SDK_SRC = resolve(dirname(Bun.resolveSync('@open-autonomy/sdk/package.json', import.meta.dir)), 'src');
|
|
41
|
+
const SDK_FILES = ['client.ts', 'roadmap.ts', 'drivers.ts', 'rails.ts'];
|
|
42
|
+
|
|
43
|
+
// Every template file, rendered. Placeholders are `__PROJECT__` and `__ACCOUNT__` (and `__ACCOUNT_ENC__`,
|
|
44
|
+
// the account as a URL path segment); binary-looking files pass through untouched.
|
|
45
|
+
export function render(params: KitParams): Map<string, Buffer> {
|
|
46
|
+
const out = new Map<string, Buffer>();
|
|
47
|
+
for (const rel of walk(TEMPLATE)) {
|
|
48
|
+
const raw = readFileSync(join(TEMPLATE, rel));
|
|
49
|
+
const text = raw.toString('utf8');
|
|
50
|
+
const rendered = /[\x00]/.test(text) ? raw : Buffer.from(text.replaceAll('__PROJECT__', params.project).replaceAll('__ACCOUNT_ENC__', encodeURIComponent(params.account)).replaceAll('__ACCOUNT__', params.account));
|
|
51
|
+
out.set(rel, rendered);
|
|
52
|
+
}
|
|
53
|
+
for (const f of SDK_FILES) out.set(`.open-autonomy/sdk/${f}`, readFileSync(join(SDK_SRC, f)));
|
|
54
|
+
out.set(KIT_FILE, Buffer.from(`${JSON.stringify({ kit: KIT.name, version: KIT.version, params, divergences: [] } satisfies KitRecord, null, 2)}\n`));
|
|
55
|
+
return out;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function readKit(dir: string): KitRecord {
|
|
59
|
+
const p = join(dir, KIT_FILE);
|
|
60
|
+
if (!existsSync(p)) throw new Error(`${p} is missing: not a repository this kit made (create or adopt it first)`);
|
|
61
|
+
const rec = JSON.parse(readFileSync(p, 'utf8')) as KitRecord;
|
|
62
|
+
if (rec.kit !== KIT.name) throw new Error(`${p} names kit ${rec.kit}, not ${KIT.name}`);
|
|
63
|
+
return { ...rec, params: validateParams(rec.params), divergences: Array.isArray(rec.divergences) ? rec.divergences : [] };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface Outcome { written: string[]; skipped: string[]; drift: string[] }
|
|
67
|
+
|
|
68
|
+
// create: every file, into an empty or new directory.
|
|
69
|
+
export function create(dir: string, params: KitParams): Outcome {
|
|
70
|
+
if (existsSync(dir) && readdirSync(dir).filter((n) => n !== '.git').length) throw new Error(`${dir} is not empty: use adopt for an existing repository`);
|
|
71
|
+
return write(dir, render(params), () => true);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// adopt: into an existing repository, writing only what is missing. The project's own files stay.
|
|
75
|
+
export function adopt(dir: string, params: KitParams): Outcome {
|
|
76
|
+
if (!existsSync(dir)) throw new Error(`${dir} does not exist`);
|
|
77
|
+
return write(dir, render(params), (rel) => !existsSync(join(dir, rel)));
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// check: the kit-owned files against a fresh render of the recorded parameters. A file the project has
|
|
81
|
+
// deliberately taken over is named in kit.json's `divergences` and is left out.
|
|
82
|
+
export function check(dir: string): Outcome {
|
|
83
|
+
const rec = readKit(dir);
|
|
84
|
+
const rendered = render(rec.params);
|
|
85
|
+
const drift: string[] = [];
|
|
86
|
+
for (const [rel, want] of rendered) {
|
|
87
|
+
if (!isOwned(rel) || rec.divergences.includes(rel)) continue;
|
|
88
|
+
const at = join(dir, rel);
|
|
89
|
+
const have = existsSync(at) ? readFileSync(at) : null;
|
|
90
|
+
if (!have || Buffer.compare(have, want) !== 0) drift.push(`${rel}: ${have ? 'differs from' : 'missing; in'} the kit`);
|
|
91
|
+
}
|
|
92
|
+
for (const rel of retired(dir, rendered, rec)) drift.push(`${rel}: retired from the kit`);
|
|
93
|
+
return { written: [], skipped: [], drift };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Kit-owned files the repository has that the kit no longer renders: what an earlier kit made and this one
|
|
97
|
+
// retired. `upgrade` removes them; a project that keeps one names it in `divergences`. Only the families the
|
|
98
|
+
// kit writes are looked at — never the agent's runtime state beside them (its database, sessions, logs, .env).
|
|
99
|
+
const RETIRABLE = [/^hermes\/(skills|hooks|scripts)\//, /^\.open-autonomy\/([a-z-]+\.ts|sdk\/)/, /^container\//];
|
|
100
|
+
function retired(dir: string, rendered: Map<string, Buffer>, rec: KitRecord): string[] {
|
|
101
|
+
const out: string[] = [];
|
|
102
|
+
for (const root of ['hermes/skills', 'hermes/hooks', 'hermes/scripts', '.open-autonomy', 'container']) {
|
|
103
|
+
const at = join(dir, root);
|
|
104
|
+
if (!existsSync(at)) continue;
|
|
105
|
+
for (const rel of walk(at).map((f) => `${root}/${f}`)) if (RETIRABLE.some((re) => re.test(rel)) && isOwned(rel) && !rendered.has(rel) && !rec.divergences.includes(rel)) out.push(rel);
|
|
106
|
+
}
|
|
107
|
+
return out;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// upgrade: check, then rewrite the drifted kit-owned files and stamp the kit version.
|
|
111
|
+
export function upgrade(dir: string): Outcome {
|
|
112
|
+
const rec = readKit(dir);
|
|
113
|
+
const rendered = render(rec.params);
|
|
114
|
+
const before = check(dir);
|
|
115
|
+
const out = write(dir, rendered, (rel) => (isOwned(rel) && !rec.divergences.includes(rel)) || rel === KIT_FILE);
|
|
116
|
+
const gone = retired(dir, rendered, rec);
|
|
117
|
+
for (const rel of gone) { rmSync(join(dir, rel), { force: true }); out.written.push(`${rel} (retired)`); }
|
|
118
|
+
// A directory the retirement emptied goes too.
|
|
119
|
+
for (const rel of [...new Set(gone.map((r) => dirname(r)))]) { try { if (!readdirSync(join(dir, rel)).length) rmSync(join(dir, rel), { recursive: true }); } catch { /* gone */ } }
|
|
120
|
+
// The record keeps the project's divergences; only the version moves.
|
|
121
|
+
writeFileSync(join(dir, KIT_FILE), `${JSON.stringify({ ...rec, version: KIT.version } satisfies KitRecord, null, 2)}\n`);
|
|
122
|
+
return { ...out, drift: before.drift };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function write(dir: string, files: Map<string, Buffer>, should: (rel: string) => boolean): Outcome {
|
|
126
|
+
const written: string[] = [];
|
|
127
|
+
const skipped: string[] = [];
|
|
128
|
+
for (const [rel, content] of files) {
|
|
129
|
+
if (!should(rel)) { skipped.push(rel); continue; }
|
|
130
|
+
const at = join(dir, rel);
|
|
131
|
+
if (existsSync(at) && Buffer.compare(readFileSync(at), content) === 0) { skipped.push(rel); continue; }
|
|
132
|
+
mkdirSync(dirname(at), { recursive: true });
|
|
133
|
+
writeFileSync(at, content, { mode: rel.endsWith('.sh') ? 0o755 : 0o644 });
|
|
134
|
+
written.push(rel);
|
|
135
|
+
}
|
|
136
|
+
return { written, skipped, drift: [] };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export const templateFiles = (): string[] => walk(TEMPLATE);
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
# Every change lands from a branch (agent/** the agent's, land/** a maintainer's): the push checks the head
|
|
3
|
+
# commit, which is what branch protection and auto-merge require.
|
|
4
|
+
on:
|
|
5
|
+
push:
|
|
6
|
+
branches: [main, 'agent/**', 'land/**']
|
|
7
|
+
workflow_dispatch:
|
|
8
|
+
permissions:
|
|
9
|
+
contents: read
|
|
10
|
+
jobs:
|
|
11
|
+
ci:
|
|
12
|
+
runs-on: ubuntu-latest
|
|
13
|
+
steps:
|
|
14
|
+
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
|
|
15
|
+
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
|
|
16
|
+
with:
|
|
17
|
+
bun-version: 1.3.10
|
|
18
|
+
- run: bun run check
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
name: Land a branch
|
|
2
|
+
# Nothing pushes to `main`: branch protection requires a pull request and the `ci` check, with no bypass.
|
|
3
|
+
# Work lands by pushing a branch: this workflow opens its pull request once and arms native auto-merge, so
|
|
4
|
+
# it merges exactly when the required checks pass on its head commit.
|
|
5
|
+
# agent/** the agent's own work land/** a maintainer's
|
|
6
|
+
on:
|
|
7
|
+
push:
|
|
8
|
+
branches: ['agent/**', 'land/**']
|
|
9
|
+
permissions:
|
|
10
|
+
contents: write
|
|
11
|
+
pull-requests: write
|
|
12
|
+
jobs:
|
|
13
|
+
land:
|
|
14
|
+
runs-on: ubuntu-latest
|
|
15
|
+
steps:
|
|
16
|
+
- name: Open the pull request once, then arm auto-merge
|
|
17
|
+
env:
|
|
18
|
+
GH_TOKEN: ${{ github.token }}
|
|
19
|
+
REPO: ${{ github.repository }}
|
|
20
|
+
BRANCH: ${{ github.ref_name }}
|
|
21
|
+
run: |
|
|
22
|
+
set -euo pipefail
|
|
23
|
+
existing=$(gh pr list -R "$REPO" --head "$BRANCH" --state open --json number -q '.[0].number')
|
|
24
|
+
if [ -z "$existing" ]; then
|
|
25
|
+
gh pr create -R "$REPO" --base main --head "$BRANCH" --title "$BRANCH" \
|
|
26
|
+
--body "Opened by the landing workflow for \`$BRANCH\`. It merges on its own when the required checks pass."
|
|
27
|
+
existing=$(gh pr list -R "$REPO" --head "$BRANCH" --state open --json number -q '.[0].number')
|
|
28
|
+
fi
|
|
29
|
+
gh pr merge -R "$REPO" "$existing" --auto --merge
|
|
30
|
+
echo "pull request #$existing: auto-merge armed"
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# The project's connection to Open Autonomy. Read by the reporter (.open-autonomy/reporter.ts) and the key
|
|
2
|
+
# tool (.open-autonomy/mint-key.ts). The kit seeds this file once; it is the project's own.
|
|
3
|
+
account: __ACCOUNT__
|
|
4
|
+
platform: https://open-autonomy.org
|
|
5
|
+
|
|
6
|
+
# What the reporter publishes. Work sessions (the scheduled runs, the funded work) publish by default; the
|
|
7
|
+
# private exceptions are named here. Anything listed under `private` is never sent.
|
|
8
|
+
publish:
|
|
9
|
+
runs: true # sessions the schedule started (trigger: cron)
|
|
10
|
+
chats: false # conversations on a channel (Discord, a terminal)
|
|
11
|
+
private: # session ids or job names that never publish, whatever their kind
|
|
12
|
+
- []
|
|
13
|
+
|
|
14
|
+
# Where sessions come from: the Hermes home the reporter reads through supercode, and where it keeps its
|
|
15
|
+
# own cursor. Inside the container stack these are the mounted volume and the checkout.
|
|
16
|
+
hermes_home: /opt/data
|
|
17
|
+
state_file: /work/project/.open-autonomy/reporter-state.json
|
|
18
|
+
|
|
19
|
+
# The rails the agent may spend through beyond model calls, and the owner's bounds on each. Off unless a
|
|
20
|
+
# bound is set. Every rail leaves a record on the public audit trail naming itself.
|
|
21
|
+
rails:
|
|
22
|
+
card: # a single-use virtual card minted against the balance (Stripe Issuing)
|
|
23
|
+
max_usd_cents: 0 # the most one card may be minted for
|
|
24
|
+
categories: [] # Stripe merchant categories a card may pay at, e.g. [computer_software_stores]
|
|
25
|
+
partner: # a partner service settling a metered charge against the balance
|
|
26
|
+
max_usd_cents: 0 # the most one charge may be
|
|
27
|
+
partners: [] # the partner ids allowed to settle
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
// Mint (or rotate) the project's key, the adopter way: prove control of the repository by committing the
|
|
3
|
+
// claim file the platform names, then mint. No admin token exists anywhere; the only authority this needs
|
|
4
|
+
// is the ability to push, which the maintainer running it already has. The key spends the project's balance
|
|
5
|
+
// and nothing else, and stops at zero.
|
|
6
|
+
//
|
|
7
|
+
// bun .open-autonomy/mint-key.ts [--models a,b] [--scopes spend,narrate,pay] [--out <file>] # commit the claim, mint
|
|
8
|
+
// bun .open-autonomy/mint-key.ts --rotate [--grace <seconds>] # with the current key; no commit
|
|
9
|
+
//
|
|
10
|
+
// The key is written to ~/.config/open-autonomy/agent.env (the file the key valve reads; created if
|
|
11
|
+
// absent), with OPEN_AUTONOMY_BASE_URL. The agent itself never sees it.
|
|
12
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
13
|
+
import { homedir } from 'node:os';
|
|
14
|
+
import { join, resolve } from 'node:path';
|
|
15
|
+
import { spawnSync } from 'node:child_process';
|
|
16
|
+
import { keyChallenge, keyMint, keyRotate } from './sdk/client.ts';
|
|
17
|
+
|
|
18
|
+
const arg = (name: string): string | undefined => { const i = process.argv.indexOf(name); return i >= 0 ? process.argv[i + 1] : undefined; };
|
|
19
|
+
const config = readFileSync(resolve(import.meta.dir, 'config.yaml'), 'utf8');
|
|
20
|
+
const account = arg('--account') ?? /^account:\s*(\S+)/m.exec(config)?.[1] ?? '';
|
|
21
|
+
const platform = (process.env.OPEN_AUTONOMY_URL ?? /^platform:\s*(\S+)/m.exec(config)?.[1] ?? 'https://open-autonomy.org').replace(/\/$/, '');
|
|
22
|
+
const base = `${platform}/v1`;
|
|
23
|
+
const models = (arg('--models') ?? 'zai/glm-5.3-flash').split(',').map((m) => m.trim()).filter(Boolean);
|
|
24
|
+
// The developer's key spends and narrates; the treasurer's adds `pay` (--scopes spend,narrate,pay --out …/treasurer.env).
|
|
25
|
+
const scopes = arg('--scopes')?.split(',').map((x) => x.trim()).filter(Boolean);
|
|
26
|
+
const dir = join(homedir(), '.config', 'open-autonomy');
|
|
27
|
+
const envPath = arg('--out') ?? join(dir, 'agent.env');
|
|
28
|
+
const git = (...args: string[]) => { const r = spawnSync('git', args, { stdio: ['ignore', 'pipe', 'inherit'] }); if (r.status !== 0) { console.error(`git ${args.join(' ')} failed`); process.exit(1); } return r.stdout.toString().trim(); };
|
|
29
|
+
if (!account) { console.error('no account: set it in .open-autonomy/config.yaml or pass --account owner/repo'); process.exit(2); }
|
|
30
|
+
|
|
31
|
+
let minted;
|
|
32
|
+
if (process.argv.includes('--rotate')) {
|
|
33
|
+
const current = existsSync(envPath) ? /^OPEN_AUTONOMY_KEY=(.+)$/m.exec(readFileSync(envPath, 'utf8'))?.[1] : undefined;
|
|
34
|
+
if (!current) { console.error(`no OPEN_AUTONOMY_KEY in ${envPath} to rotate`); process.exit(2); }
|
|
35
|
+
const grace = arg('--grace');
|
|
36
|
+
minted = await keyRotate(base, current, grace === undefined ? {} : { graceSeconds: Number(grace) });
|
|
37
|
+
} else {
|
|
38
|
+
const challenge = await keyChallenge(base, account);
|
|
39
|
+
if (!challenge.ok) { console.error(`challenge failed: ${JSON.stringify(challenge)}`); process.exit(1); }
|
|
40
|
+
writeFileSync(challenge.file, `${challenge.claim}\n`);
|
|
41
|
+
git('add', challenge.file);
|
|
42
|
+
if (git('status', '--porcelain', '--', challenge.file)) { git('commit', '-q', '-m', `claim: ${account} key`, '--', challenge.file); git('push', '-q'); }
|
|
43
|
+
minted = await keyMint(base, account, models, scopes);
|
|
44
|
+
}
|
|
45
|
+
if (!minted.ok || !minted.token) { console.error(`mint failed: ${JSON.stringify(minted)}`); process.exit(1); }
|
|
46
|
+
mkdirSync(dir, { recursive: true });
|
|
47
|
+
const keep = existsSync(envPath) ? readFileSync(envPath, 'utf8').split('\n').filter((l) => !/^(OPEN_AUTONOMY_KEY|OPEN_AUTONOMY_BASE_URL)=/.test(l) && l.trim()) : [];
|
|
48
|
+
writeFileSync(envPath, `${[...keep, `OPEN_AUTONOMY_BASE_URL=${base}`, `OPEN_AUTONOMY_KEY=${minted.token}`].join('\n')}\n`, { mode: 0o600 });
|
|
49
|
+
console.log(`key ${minted.key.kid} for ${minted.key.account} (models: ${minted.key.models.join(', ')}; expires ${minted.key.exp}) → ${envPath}`);
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "__PROJECT__-open-autonomy",
|
|
3
|
+
"private": true,
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "The project's connection to Open Autonomy: the reporter's two dependencies — supercode (the harness it reads the agent's sessions through) and supercode's harness SDK. The Open Autonomy SDK is vendored beside it under sdk/.",
|
|
6
|
+
"dependencies": {
|
|
7
|
+
"@volter-ai-dev/supercode": "^0.4.19",
|
|
8
|
+
"@volter-ai-dev/supercode-harness-sdk": "^0.3.21"
|
|
9
|
+
}
|
|
10
|
+
}
|