@joenandez/academy 0.4.0-rc.1
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/.claude-plugin/marketplace.json +14 -0
- package/.claude-plugin/plugin.json +6 -0
- package/CHANGELOG.md +46 -0
- package/LICENSE +21 -0
- package/README.md +209 -0
- package/bin/academy +2 -0
- package/conformance/README.md +60 -0
- package/conformance/discovery.test.mjs +140 -0
- package/conformance/envelope.test.mjs +185 -0
- package/conformance/error-codes.test.mjs +125 -0
- package/conformance/harness.mjs +180 -0
- package/conformance/identity.test.mjs +125 -0
- package/docs/integration-guide.md +1026 -0
- package/hooks/hook_runtime.mjs +100 -0
- package/hooks/hooks.json +26 -0
- package/hooks/inject_surface.py +122 -0
- package/hooks/memory_bridge.mjs +120 -0
- package/hooks/memory_store.mjs +66 -0
- package/hooks/register_session.mjs +51 -0
- package/hooks/sync_memory.mjs +27 -0
- package/package.json +41 -0
- package/scripts/agent.mjs +3 -0
- package/scripts/cli/archive.mjs +161 -0
- package/scripts/cli/archived.mjs +82 -0
- package/scripts/cli/args.mjs +282 -0
- package/scripts/cli/codex.mjs +216 -0
- package/scripts/cli/core.mjs +389 -0
- package/scripts/cli/create.mjs +242 -0
- package/scripts/cli/doctor.mjs +203 -0
- package/scripts/cli/eventlog.mjs +129 -0
- package/scripts/cli/events.mjs +80 -0
- package/scripts/cli/hire-headless.mjs +229 -0
- package/scripts/cli/hire-spec.mjs +164 -0
- package/scripts/cli/hire.mjs +92 -0
- package/scripts/cli/inspect.mjs +286 -0
- package/scripts/cli/lifecycle.mjs +296 -0
- package/scripts/cli/main.mjs +102 -0
- package/scripts/cli/migrate.mjs +183 -0
- package/scripts/cli/notes.mjs +104 -0
- package/scripts/cli/rename.mjs +172 -0
- package/scripts/cli/run.mjs +227 -0
- package/scripts/cli/runtime.mjs +47 -0
- package/scripts/cli/scaffold.mjs +332 -0
- package/scripts/cli/sessions.mjs +98 -0
- package/scripts/cli/templates.mjs +104 -0
- package/scripts/cli/yaml.mjs +124 -0
- package/skills/hire/SKILL.md +669 -0
- package/templates/agents/claude-code/knowledge-curator.md +14 -0
- package/templates/agents/codex/knowledge-curator.toml +9 -0
- package/templates/skills/check-in/SKILL.md +122 -0
- package/templates/skills/knowledge-curation/SKILL.md +132 -0
- package/templates/skills/nightly-consolidation/SKILL.md +240 -0
- package/templates/skills/self-update/SKILL.md +121 -0
|
@@ -0,0 +1,1026 @@
|
|
|
1
|
+
# Academy client integration guide
|
|
2
|
+
|
|
3
|
+
**Contract version 1.**
|
|
4
|
+
|
|
5
|
+
This is the whole published contract between Academy and a client that drives
|
|
6
|
+
it. It is written for an author of a client product — a desktop app, a web
|
|
7
|
+
service, another CLI — who will integrate Academy without reading Academy's
|
|
8
|
+
source. Everything a client may depend on is here. Anything not here is
|
|
9
|
+
internal and may change in any release.
|
|
10
|
+
|
|
11
|
+
Academy emits data. Clients render it. A client asks; it never guesses and
|
|
12
|
+
never reads what is not published.
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## Contents
|
|
17
|
+
|
|
18
|
+
1. [Install and provision](#1-install-and-provision)
|
|
19
|
+
2. [The response envelope](#2-the-response-envelope)
|
|
20
|
+
3. [Versions and the compatibility floor](#3-versions-and-the-compatibility-floor)
|
|
21
|
+
4. [Discovery: call `doctor` first](#4-discovery-call-doctor-first)
|
|
22
|
+
5. [The fourteen published commands](#5-the-fourteen-published-commands)
|
|
23
|
+
6. [Commands Academy has but does not publish](#6-commands-academy-has-but-does-not-publish)
|
|
24
|
+
7. [The fifteen error codes](#7-the-fifteen-error-codes)
|
|
25
|
+
8. [The event log](#8-the-event-log)
|
|
26
|
+
9. [The agent directory layout](#9-the-agent-directory-layout)
|
|
27
|
+
10. [The document contract: eight boot surfaces](#10-the-document-contract-eight-boot-surfaces)
|
|
28
|
+
11. [External dependencies](#11-external-dependencies)
|
|
29
|
+
12. [The one client-specific exception](#12-the-one-client-specific-exception)
|
|
30
|
+
13. [Verify your integration](#13-verify-your-integration)
|
|
31
|
+
14. [What is not contract](#14-what-is-not-contract)
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## 1. Install and provision
|
|
36
|
+
|
|
37
|
+
Academy is a Node package with no npm dependencies. It needs Node 18 or
|
|
38
|
+
later, and two external binaries described in §11. The launcher is `bin/academy`.
|
|
39
|
+
|
|
40
|
+
Academy stores agents in an **agents root**. The root is
|
|
41
|
+
`$AGENTS_ROOT` when that variable is set, and `~/.academy/agents` otherwise. A
|
|
42
|
+
client that manages its own root sets `AGENTS_ROOT` on every invocation. Two
|
|
43
|
+
installs with different roots are fully independent.
|
|
44
|
+
|
|
45
|
+
Never assume a path. `doctor` reports `agentsRoot` and `eventLog`, and
|
|
46
|
+
`sessions` reports `sessionIndex`. Read them from the payload.
|
|
47
|
+
|
|
48
|
+
**Environment variables a client may set**
|
|
49
|
+
|
|
50
|
+
| Variable | Effect |
|
|
51
|
+
| --- | --- |
|
|
52
|
+
| `AGENTS_ROOT` | The agents root. Default `~/.academy/agents`. |
|
|
53
|
+
| `ACADEMY_CLI_NAME` | The name Academy calls itself in human help text. Default `academy`. It does not change any command, flag, or payload. |
|
|
54
|
+
| `ACADEMY_HELM_TASKS_BIN` | Absolute path of the `helm-tasks` scheduler. See §11. |
|
|
55
|
+
| `ACADEMY_CLAUDE_BIN` | Absolute path of the `claude` runtime. |
|
|
56
|
+
| `ACADEMY_CODEX_BIN` | Absolute path of the `codex` runtime. |
|
|
57
|
+
| `ACADEMY_HIRE_TIMEOUT_MS` | Milliseconds `hire --spec` waits for the runtime. Default `600000`. |
|
|
58
|
+
| `ACADEMY_MEMORY_BRIDGE` | `1` enables the memory bridge. See §12. Off by default. |
|
|
59
|
+
|
|
60
|
+
A missing agents root is not an error. Academy creates the default root on
|
|
61
|
+
first use. A client provisions Academy silently and calls `doctor` before it
|
|
62
|
+
renders anything.
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
## 2. The response envelope
|
|
67
|
+
|
|
68
|
+
Every published command accepts `--json`. With `--json`, the command answers
|
|
69
|
+
with exactly one JSON document and nothing else.
|
|
70
|
+
|
|
71
|
+
**Success — on stdout, exit 0:**
|
|
72
|
+
|
|
73
|
+
```json
|
|
74
|
+
{
|
|
75
|
+
"contract_version": 1,
|
|
76
|
+
"ok": true,
|
|
77
|
+
"command": "list",
|
|
78
|
+
"agents": [],
|
|
79
|
+
"archived": [],
|
|
80
|
+
"lastSeq": 0,
|
|
81
|
+
"logId": null
|
|
82
|
+
}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
**Failure — on stderr, exit 1:**
|
|
86
|
+
|
|
87
|
+
```json
|
|
88
|
+
{
|
|
89
|
+
"contract_version": 1,
|
|
90
|
+
"ok": false,
|
|
91
|
+
"command": "inspect",
|
|
92
|
+
"error": {
|
|
93
|
+
"code": "agent_not_found",
|
|
94
|
+
"message": "Agent \"ghost\" not found at /home/u/.academy/agents/ghost",
|
|
95
|
+
"name": "ghost"
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
**The rules, all of them:**
|
|
101
|
+
|
|
102
|
+
- `contract_version`, `ok` and `command` are always the first three keys.
|
|
103
|
+
- The payload is spread at the top level on success. There is no `data`
|
|
104
|
+
wrapper.
|
|
105
|
+
- `error.code` is one of the fifteen strings in §7. Switch on it.
|
|
106
|
+
- `error.message` is human text. It is never stable. Do not parse it and never
|
|
107
|
+
switch on it.
|
|
108
|
+
- `error` carries extra context keys per code. They are listed in §7.
|
|
109
|
+
- **Exit status is 0 if and only if `ok` is true.**
|
|
110
|
+
- Success writes only to stdout. Failure writes only to stderr.
|
|
111
|
+
- One command breaks the stream rule in a defined way: a `doctor` failure
|
|
112
|
+
carries its whole payload *and* an `error` object. See §4.
|
|
113
|
+
|
|
114
|
+
Without `--json`, every command prints human text instead. Human text is not
|
|
115
|
+
contract. A client always passes `--json`.
|
|
116
|
+
|
|
117
|
+
**`budget` needs one extra note.** An agent over its token cap is a report, not
|
|
118
|
+
a failure: `budget --json` answers `ok: true` and exits 0, and `withinBudget`
|
|
119
|
+
is `false`. The human form of `budget` exits 1 in that case. A client reads
|
|
120
|
+
`withinBudget`, never the exit status.
|
|
121
|
+
|
|
122
|
+
---
|
|
123
|
+
|
|
124
|
+
## 3. Versions and the compatibility floor
|
|
125
|
+
|
|
126
|
+
Two version numbers move independently.
|
|
127
|
+
|
|
128
|
+
- **`contract_version`** is the shape of the envelope and the meaning of every
|
|
129
|
+
payload and error code in this guide. It is `1`, and it is frozen.
|
|
130
|
+
- **The package version** is the npm semver of the build. It moves on every
|
|
131
|
+
release.
|
|
132
|
+
|
|
133
|
+
**Pin the contract, not the package.** A client asserts
|
|
134
|
+
`doctor.contracts.includes(1)` and refuses anything else. It must not compare
|
|
135
|
+
package versions.
|
|
136
|
+
|
|
137
|
+
`doctor` answers both:
|
|
138
|
+
|
|
139
|
+
```json
|
|
140
|
+
{ "contracts": [1], "version": "0.3.0-phase0+4c97d8d" }
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
`version` is the package version. A **source checkout** appends
|
|
144
|
+
`+<git describe --always --dirty>` as semver build metadata, so two checkouts
|
|
145
|
+
at different commits never report the same string. An **install** has no
|
|
146
|
+
checkout to describe and reports the published version bare, with no `+`.
|
|
147
|
+
Treat `version` as an opaque identity string for support and telemetry. Never
|
|
148
|
+
branch on it.
|
|
149
|
+
|
|
150
|
+
**The compatibility floor.** A build whose `doctor` payload carries no
|
|
151
|
+
`contract_version` is incompatible. There is no migration path and no shim. A
|
|
152
|
+
client that finds no `contract_version` reports a setup problem and reprovisions
|
|
153
|
+
Academy. It must not attempt to parse the response.
|
|
154
|
+
|
|
155
|
+
There is no `academy --version` command. `doctor --json` is the only published
|
|
156
|
+
source of the version.
|
|
157
|
+
|
|
158
|
+
---
|
|
159
|
+
|
|
160
|
+
## 4. Discovery: call `doctor` first
|
|
161
|
+
|
|
162
|
+
`doctor --json` is the availability gate. It is the one call a client may make
|
|
163
|
+
against an Academy it knows nothing about, and it is read-only. **Call it
|
|
164
|
+
before rendering any UI.**
|
|
165
|
+
|
|
166
|
+
```sh
|
|
167
|
+
academy doctor --json
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
```json
|
|
171
|
+
{
|
|
172
|
+
"contract_version": 1,
|
|
173
|
+
"ok": true,
|
|
174
|
+
"command": "doctor",
|
|
175
|
+
"contracts": [1],
|
|
176
|
+
"version": "0.3.0-phase0+4c97d8d",
|
|
177
|
+
"packageRoot": "/opt/academy",
|
|
178
|
+
"agentsRoot": "/home/u/.academy/agents",
|
|
179
|
+
"eventLog": "/home/u/.academy/events.jsonl",
|
|
180
|
+
"commands": ["doctor", "list", "inspect", "tokens", "budget", "sessions",
|
|
181
|
+
"events", "create", "hire", "rename", "archive", "unarchive",
|
|
182
|
+
"delete", "migrate"],
|
|
183
|
+
"runtimes": { "claude_code": { "available": true },
|
|
184
|
+
"codex": { "available": false } },
|
|
185
|
+
"errors": []
|
|
186
|
+
}
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
**Exactly eight payload keys, in this order:**
|
|
190
|
+
|
|
191
|
+
| Key | Meaning |
|
|
192
|
+
| --- | --- |
|
|
193
|
+
| `contracts` | Contract versions this build speaks. `[1]` today. |
|
|
194
|
+
| `version` | Package version, plus build metadata for a checkout. §3. |
|
|
195
|
+
| `packageRoot` | Absolute path of the Academy installation. |
|
|
196
|
+
| `agentsRoot` | Absolute path of the agents root in use. |
|
|
197
|
+
| `eventLog` | Absolute path of the lifecycle event log. §8. |
|
|
198
|
+
| `commands` | The published command list. §5. Ordered; the order is stable. |
|
|
199
|
+
| `runtimes` | `{ <provider>: { available: boolean } }` for `claude_code` and `codex`. |
|
|
200
|
+
| `errors` | The **health** channel. See below. |
|
|
201
|
+
|
|
202
|
+
**A degraded install is still `ok: true`.** An unavailable runtime, agents
|
|
203
|
+
missing an ownership marker, an agent with an unreadable runtime — none of these
|
|
204
|
+
fail `doctor`. The client renders its Academy experience and repairs what it
|
|
205
|
+
can.
|
|
206
|
+
|
|
207
|
+
**The one state answered `ok: false`** is an agents root that fails its audit:
|
|
208
|
+
a symlinked root, a root that is not a directory, a root resolving outside its
|
|
209
|
+
parent, or a named root whose parent does not exist. Every agent-addressed
|
|
210
|
+
command raises `unsafe_agent_path` on such a root, so a client told `ok: true`
|
|
211
|
+
would render an interface whose first call fails. The failure envelope still
|
|
212
|
+
carries the whole payload beside the `error` object, because a client parsing
|
|
213
|
+
the failure still needs the version and contract it is talking to:
|
|
214
|
+
|
|
215
|
+
```json
|
|
216
|
+
{
|
|
217
|
+
"contract_version": 1, "ok": false, "command": "doctor",
|
|
218
|
+
"contracts": [1], "version": "...", "packageRoot": "...",
|
|
219
|
+
"agentsRoot": "...", "eventLog": "...", "commands": [...],
|
|
220
|
+
"runtimes": {...}, "errors": [],
|
|
221
|
+
"error": { "code": "unsafe_agent_path",
|
|
222
|
+
"message": "AGENTS_ROOT must not be a symlink: /tmp/x/agents",
|
|
223
|
+
"agentsRoot": "/tmp/x/agents" }
|
|
224
|
+
}
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
This is the only command whose failure envelope carries a payload.
|
|
228
|
+
|
|
229
|
+
### `errors[]` is a health channel, not the error channel
|
|
230
|
+
|
|
231
|
+
This is the single thing client authors get wrong. Academy has **two separate
|
|
232
|
+
channels** and they never mix.
|
|
233
|
+
|
|
234
|
+
| | The error channel | The health channel |
|
|
235
|
+
| --- | --- | --- |
|
|
236
|
+
| Where | `error.code` in a failure envelope | `doctor.errors[]` |
|
|
237
|
+
| Shape | `{ code, message, ...context }` | `{ code, count }` |
|
|
238
|
+
| Means | This command failed | This install has *n* degraded things |
|
|
239
|
+
| Values | The fifteen codes in §7 | The three codes below |
|
|
240
|
+
| Effect on `ok` | `ok: false` | none — `ok` stays `true` |
|
|
241
|
+
|
|
242
|
+
**No code from one channel ever appears in the other.** None of the fifteen
|
|
243
|
+
error codes can appear in `doctor.errors[]`, and none of these three health
|
|
244
|
+
codes can appear in an `error` object.
|
|
245
|
+
|
|
246
|
+
| Health code | Counts | Repair |
|
|
247
|
+
| --- | --- | --- |
|
|
248
|
+
| `unowned_agents` | Agent directories inside the root with no valid `.academy-agent.json`. Every lifecycle command fails on them. | `academy migrate --json` |
|
|
249
|
+
| `invalid_runtime_agents` | Agents whose `agent.yaml` `runtime:` is outside `claude_code` / `codex`. | Rewrite `runtime:` to a valid value. |
|
|
250
|
+
| `unattributable_sessions` | Session index records written before the index carried an agent directory. They are historical, never rewritten. | None. They are excluded from `sessions`. |
|
|
251
|
+
|
|
252
|
+
Zero counts are omitted, so a healthy install reports `"errors": []`.
|
|
253
|
+
|
|
254
|
+
---
|
|
255
|
+
|
|
256
|
+
## 5. The fourteen published commands
|
|
257
|
+
|
|
258
|
+
These are exactly the commands in `doctor.commands`, in that order. Nothing
|
|
259
|
+
else is contract. Every one accepts `--json`.
|
|
260
|
+
|
|
261
|
+
Two shapes recur.
|
|
262
|
+
|
|
263
|
+
**`<agentRecord>`** — the roster record for one agent:
|
|
264
|
+
|
|
265
|
+
```json
|
|
266
|
+
{
|
|
267
|
+
"name": "kai",
|
|
268
|
+
"dir": "/home/u/.academy/agents/kai",
|
|
269
|
+
"displayName": "Kai",
|
|
270
|
+
"runtimeProvider": "claude_code",
|
|
271
|
+
"role": "Data analyst"
|
|
272
|
+
}
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
`name`, `dir`, `displayName` and `runtimeProvider` are always present. `role`
|
|
276
|
+
is present only when `agent.yaml` declares a non-empty one. `dir` is absolute
|
|
277
|
+
and is the joined path, not a resolved symlink target, so a client may key
|
|
278
|
+
agents on it and match it against the record `create` gave it.
|
|
279
|
+
`runtimeProvider` is `claude_code`, `codex`, or `null` when `agent.yaml`
|
|
280
|
+
declares a value Academy does not recognise.
|
|
281
|
+
|
|
282
|
+
**`<jobId>`** — the identifier of an agent's nightly consolidation job, always
|
|
283
|
+
`<name>-nightly-consolidation`, or `null` when no job was registered.
|
|
284
|
+
|
|
285
|
+
### `doctor [--json]`
|
|
286
|
+
|
|
287
|
+
See §4.
|
|
288
|
+
|
|
289
|
+
### `list [--json]`
|
|
290
|
+
|
|
291
|
+
The roster, and the atomic re-sync point for the event log.
|
|
292
|
+
|
|
293
|
+
```json
|
|
294
|
+
{
|
|
295
|
+
"contract_version": 1, "ok": true, "command": "list",
|
|
296
|
+
"agents": [ "<agentRecord>" ],
|
|
297
|
+
"archived": [ "<agentRecord>" ],
|
|
298
|
+
"lastSeq": 42,
|
|
299
|
+
"logId": "52892036-6c02-47c9-8ea3-ff811663a893"
|
|
300
|
+
}
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
`agents[]` is the working roster, sorted by name. `archived[]` names what the
|
|
304
|
+
roster leaves out, with each `dir` pointing inside the holding area. An archived
|
|
305
|
+
agent that simply vanished would be indistinguishable from a deleted one.
|
|
306
|
+
|
|
307
|
+
`lastSeq` and `logId` are the event-log watermark, read **before** the roster.
|
|
308
|
+
Taking both in one call is what makes re-sync safe — see §8.
|
|
309
|
+
|
|
310
|
+
`list` degrades rather than fails: one agent with an unreadable `runtime:`
|
|
311
|
+
reports `runtimeProvider: null` and every healthy agent beside it still
|
|
312
|
+
appears. This is the documented recovery path, and a client locked out of it
|
|
313
|
+
has no recovery left.
|
|
314
|
+
|
|
315
|
+
Errors: `unsafe_agent_path`.
|
|
316
|
+
|
|
317
|
+
### `inspect <name> [--json]`
|
|
318
|
+
|
|
319
|
+
One agent, in detail.
|
|
320
|
+
|
|
321
|
+
```json
|
|
322
|
+
{
|
|
323
|
+
"contract_version": 1, "ok": true, "command": "inspect",
|
|
324
|
+
"name": "kai", "dir": "...", "displayName": "kai",
|
|
325
|
+
"runtimeProvider": "claude_code",
|
|
326
|
+
"surfaces": {
|
|
327
|
+
"identity": true, "role": true, "knowledge": true, "goals": true,
|
|
328
|
+
"priorities": true, "threads": true, "notes": true, "dailys": true
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
`surfaces` reports which of the eight boot files exist on disk. Unlike `list`,
|
|
334
|
+
`inspect` **raises** `invalid_runtime` rather than reporting `null`: a direct
|
|
335
|
+
question about one agent must not be answered with a provider Academy did not
|
|
336
|
+
resolve.
|
|
337
|
+
|
|
338
|
+
Errors: `invalid_name`, `agent_archived`, `agent_not_found`,
|
|
339
|
+
`unsafe_agent_path`, `invalid_runtime`.
|
|
340
|
+
|
|
341
|
+
### `tokens <name> [--json]`
|
|
342
|
+
|
|
343
|
+
The estimated prompt token cost of the agent's compiled boot context, by
|
|
344
|
+
surface.
|
|
345
|
+
|
|
346
|
+
```json
|
|
347
|
+
{
|
|
348
|
+
"contract_version": 1, "ok": true, "command": "tokens",
|
|
349
|
+
"agent": "kai",
|
|
350
|
+
"dir": "/home/u/.academy/agents/kai",
|
|
351
|
+
"promptPath": ".../.academy/generated/academy-system-prompt.md",
|
|
352
|
+
"tokenizer": "estimated:chars-and-words-v1",
|
|
353
|
+
"total": { "estimatedTokens": 756, "chars": 2714 },
|
|
354
|
+
"overhead": { "estimatedTokens": 237, "chars": 937, "percent": 31.3 },
|
|
355
|
+
"surfaces": [
|
|
356
|
+
{ "name": "identity", "file": "identity.md", "path": "...",
|
|
357
|
+
"exists": true, "estimatedTokens": 53, "chars": 171, "percent": 7 }
|
|
358
|
+
]
|
|
359
|
+
}
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
`tokenizer` names the estimator. It is an estimate, not a model tokenizer
|
|
363
|
+
count. `surfaces[]` is in boot order: identity, role, knowledge, goals,
|
|
364
|
+
priorities, threads, notes, dailys.
|
|
365
|
+
|
|
366
|
+
`tokens` reads only the surface files, so an agent with an unrecognised
|
|
367
|
+
`runtime:` still reports normally here.
|
|
368
|
+
|
|
369
|
+
Errors: `invalid_name`, `agent_archived`, `agent_not_found`,
|
|
370
|
+
`unsafe_agent_path`, `internal_error`.
|
|
371
|
+
|
|
372
|
+
### `budget <name> [--json]`
|
|
373
|
+
|
|
374
|
+
The same estimate, judged against per-surface caps.
|
|
375
|
+
|
|
376
|
+
```json
|
|
377
|
+
{
|
|
378
|
+
"contract_version": 1, "ok": true, "command": "budget",
|
|
379
|
+
"agent": "kai", "dir": "...",
|
|
380
|
+
"withinBudget": true,
|
|
381
|
+
"total": { "estimatedTokens": 520, "cap": 7000, "overBy": 0 },
|
|
382
|
+
"surfaces": [
|
|
383
|
+
{ "name": "identity", "estimatedTokens": 53, "cap": 400,
|
|
384
|
+
"overBy": 0, "withinCap": true, "enforced": false }
|
|
385
|
+
],
|
|
386
|
+
"violations": []
|
|
387
|
+
}
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
`enforced` distinguishes a cap Academy holds an agent to from an advisory one.
|
|
391
|
+
`withinBudget` is false only when an **enforced** surface is over. `violations`
|
|
392
|
+
repeats the surfaces with `withinCap: false`.
|
|
393
|
+
|
|
394
|
+
The cap values are not contract; they may change. The field names are.
|
|
395
|
+
|
|
396
|
+
**An over-budget agent is `ok: true` with exit 0.** Read `withinBudget`.
|
|
397
|
+
|
|
398
|
+
Errors: as `tokens` — `invalid_name`, `agent_archived`, `agent_not_found`,
|
|
399
|
+
`unsafe_agent_path`, `internal_error`.
|
|
400
|
+
|
|
401
|
+
### `sessions [--agent <name>] [--json]`
|
|
402
|
+
|
|
403
|
+
Agent sessions recorded by Academy's session hook.
|
|
404
|
+
|
|
405
|
+
```json
|
|
406
|
+
{
|
|
407
|
+
"contract_version": 1, "ok": true, "command": "sessions",
|
|
408
|
+
"agentsRoot": "/home/u/.academy/agents",
|
|
409
|
+
"sessionIndex": "/home/u/.academy/sessions.jsonl",
|
|
410
|
+
"sessions": [
|
|
411
|
+
{ "sessionId": "…", "agentName": "kai",
|
|
412
|
+
"agentDir": "/home/u/.academy/agents/kai",
|
|
413
|
+
"cwd": "/home/u/work/report", "startedAt": "2026-08-31T18:04:02.113Z" }
|
|
414
|
+
]
|
|
415
|
+
}
|
|
416
|
+
```
|
|
417
|
+
|
|
418
|
+
The session index is **global across installs** — one file, whichever root a
|
|
419
|
+
client drives. Attribution is therefore made at read time by `agentDir`
|
|
420
|
+
containment inside the resolved agents root, never by agent name: two roots can
|
|
421
|
+
each hold a `kai`. `--agent <name>` narrows further by `agentName`.
|
|
422
|
+
|
|
423
|
+
Rows written before the index carried `agentDir` are unattributable. They are
|
|
424
|
+
never rewritten, never returned by any root, and counted by `doctor` under
|
|
425
|
+
`unattributable_sessions`.
|
|
426
|
+
|
|
427
|
+
`sessions[]` is in index order, oldest first. `agentName`, `cwd` and
|
|
428
|
+
`startedAt` may be `null`; `sessionId` and `agentDir` are always present.
|
|
429
|
+
|
|
430
|
+
Errors: `invalid_name`, `unsafe_agent_path`, `invalid_spec` (an unknown
|
|
431
|
+
option).
|
|
432
|
+
|
|
433
|
+
### `events --since <seq> [--logid <id>] [--json]`
|
|
434
|
+
|
|
435
|
+
Replay lifecycle change. See §8 for the delivery contract.
|
|
436
|
+
|
|
437
|
+
```json
|
|
438
|
+
{
|
|
439
|
+
"contract_version": 1, "ok": true, "command": "events",
|
|
440
|
+
"firstSeq": 1, "lastSeq": 7,
|
|
441
|
+
"logId": "52892036-6c02-47c9-8ea3-ff811663a893",
|
|
442
|
+
"events": [ { "seq": 3, "event": "agent_created", "agentName": "rho",
|
|
443
|
+
"agentDir": "...", "ts": "2026-09-01T02:13:56.599Z" } ]
|
|
444
|
+
}
|
|
445
|
+
```
|
|
446
|
+
|
|
447
|
+
`firstSeq` and `lastSeq` are the bounds of **the whole log**, not of the
|
|
448
|
+
returned page. `events[]` holds every record with `seq` strictly greater than
|
|
449
|
+
`--since`. `--since 0` means "I have applied nothing" and returns the whole log.
|
|
450
|
+
|
|
451
|
+
`--logid` is the log identity the client stored with its watermark. Pass it on
|
|
452
|
+
every call. Omitting it means Academy cannot tell a rebuilt log from the
|
|
453
|
+
original.
|
|
454
|
+
|
|
455
|
+
A `--since` value that is not a non-negative integer is answered with
|
|
456
|
+
`replay_unavailable`, not with a separate code, and `error.requestedSeq` echoes
|
|
457
|
+
back verbatim what was sent.
|
|
458
|
+
|
|
459
|
+
Errors: `unsafe_agent_path`, `replay_unavailable`, `invalid_spec` (an unknown
|
|
460
|
+
option).
|
|
461
|
+
|
|
462
|
+
### `create <name> [--json]`
|
|
463
|
+
|
|
464
|
+
Scaffold a new agent with the eight boot surfaces at their templates, register
|
|
465
|
+
its nightly job, and append `agent_created`.
|
|
466
|
+
|
|
467
|
+
```json
|
|
468
|
+
{
|
|
469
|
+
"contract_version": 1, "ok": true, "command": "create",
|
|
470
|
+
"created": true,
|
|
471
|
+
"name": "kai", "dir": "...", "displayName": "kai",
|
|
472
|
+
"runtimeProvider": "claude_code",
|
|
473
|
+
"scheduledJobId": "kai-nightly-consolidation"
|
|
474
|
+
}
|
|
475
|
+
```
|
|
476
|
+
|
|
477
|
+
The payload is `{ created: true, ...<agentRecord>, scheduledJobId }`.
|
|
478
|
+
|
|
479
|
+
`create` is all-or-nothing. A failure to register the nightly job or to append
|
|
480
|
+
the event removes the directory before answering, so a failed `create` leaves
|
|
481
|
+
nothing behind and the same name can be retried.
|
|
482
|
+
|
|
483
|
+
Names are kebab-case: `^[a-z][a-z0-9-]{0,31}$`.
|
|
484
|
+
|
|
485
|
+
Errors: `invalid_name`, `agent_archived`, `agent_exists`,
|
|
486
|
+
`unsafe_agent_path`, `runtime_unavailable`, `log_corrupt`, `lock_timeout`.
|
|
487
|
+
|
|
488
|
+
### `hire --spec <path> [--json]`
|
|
489
|
+
|
|
490
|
+
The headless hire. It scaffolds the agent exactly as `create` does, then drives
|
|
491
|
+
the Claude Code runtime once to write the eight surfaces from a written brief.
|
|
492
|
+
|
|
493
|
+
```json
|
|
494
|
+
{
|
|
495
|
+
"contract_version": 1, "ok": true, "command": "hire",
|
|
496
|
+
"hired": true,
|
|
497
|
+
"name": "nova", "dir": "...", "displayName": "nova",
|
|
498
|
+
"runtimeProvider": "codex", "role": "Data analyst",
|
|
499
|
+
"scheduledJobId": "nova-nightly-consolidation"
|
|
500
|
+
}
|
|
501
|
+
```
|
|
502
|
+
|
|
503
|
+
The payload is `{ hired: true, ...<agentRecord>, scheduledJobId }`.
|
|
504
|
+
|
|
505
|
+
**The specification file is contract.** It is JSON, and the key set is closed:
|
|
506
|
+
|
|
507
|
+
```json
|
|
508
|
+
{
|
|
509
|
+
"name": "nova",
|
|
510
|
+
"role": "Data analyst for the growth team",
|
|
511
|
+
"objective": "Report weekly metrics and flag anomalies",
|
|
512
|
+
"runtime": "codex"
|
|
513
|
+
}
|
|
514
|
+
```
|
|
515
|
+
|
|
516
|
+
| Key | Required | Rule |
|
|
517
|
+
| --- | --- | --- |
|
|
518
|
+
| `name` | yes | Kebab-case, `^[a-z][a-z0-9-]{0,31}$`, and free. |
|
|
519
|
+
| `role` | yes | Non-empty string, ≤2000 characters, single line, no `"` and no `\`. |
|
|
520
|
+
| `objective` | yes | Same rule as `role`. |
|
|
521
|
+
| `runtime` | no | `claude_code` or `codex`. Note the **underscore**; the `claude-code` hyphen form is rejected. |
|
|
522
|
+
|
|
523
|
+
Any other key is rejected with `invalid_spec`, not silently dropped. The file
|
|
524
|
+
must be a regular file of at most 64 KiB.
|
|
525
|
+
|
|
526
|
+
A hire whose runtime fails or times out is rolled back: the nightly job is
|
|
527
|
+
unregistered, the directory removed, and a compensating `agent_deleted` event
|
|
528
|
+
appended after the `agent_created` that was already published. The envelope
|
|
529
|
+
then answers `runtime_unavailable`.
|
|
530
|
+
|
|
531
|
+
`hire` without `--spec` is the **interactive** form. It spawns a terminal
|
|
532
|
+
session with inherited stdio and can never emit an envelope. It is not part of
|
|
533
|
+
this contract.
|
|
534
|
+
|
|
535
|
+
Errors: `invalid_spec`, `invalid_runtime`, `invalid_name`, `agent_exists`,
|
|
536
|
+
`agent_archived`, `unsafe_agent_path`, `runtime_unavailable`, `log_corrupt`,
|
|
537
|
+
`lock_timeout`.
|
|
538
|
+
|
|
539
|
+
### `rename <old> <new> [--json]`
|
|
540
|
+
|
|
541
|
+
Move an agent to a new name. The directory, the ownership marker, the
|
|
542
|
+
`agent.yaml` `name:` scalar, and the nightly job all move together, inside one
|
|
543
|
+
lock.
|
|
544
|
+
|
|
545
|
+
```json
|
|
546
|
+
{
|
|
547
|
+
"contract_version": 1, "ok": true, "command": "rename",
|
|
548
|
+
"renamed": true,
|
|
549
|
+
"name": "vera", "dir": "…/agents/vera", "displayName": "vera",
|
|
550
|
+
"runtimeProvider": "codex", "role": "Data analyst",
|
|
551
|
+
"previousName": "nova",
|
|
552
|
+
"previousDir": "…/agents/nova",
|
|
553
|
+
"unscheduledJobId": "nova-nightly-consolidation",
|
|
554
|
+
"scheduledJobId": "vera-nightly-consolidation"
|
|
555
|
+
}
|
|
556
|
+
```
|
|
557
|
+
|
|
558
|
+
The payload is `{ renamed: true, ...<agentRecord>, previousName, previousDir,
|
|
559
|
+
unscheduledJobId, scheduledJobId }`. A client keying agents on `dir` rewrites
|
|
560
|
+
its key from `previousDir` to `dir`.
|
|
561
|
+
|
|
562
|
+
`scheduledJobId` is `null` when the new nightly job could not be registered;
|
|
563
|
+
the rename still stands.
|
|
564
|
+
|
|
565
|
+
Errors: `invalid_name`, `agent_not_found`, `agent_archived`,
|
|
566
|
+
`not_academy_owned`, `agent_exists`, `unsafe_agent_path`,
|
|
567
|
+
`runtime_unavailable`, `unschedule_failed`, `invalid_spec`, `log_corrupt`,
|
|
568
|
+
`lock_timeout`, `internal_error`.
|
|
569
|
+
|
|
570
|
+
### `archive <name> [--json]`
|
|
571
|
+
|
|
572
|
+
Move an agent into the holding area and unregister its nightly job. Nothing is
|
|
573
|
+
destroyed and the move is reversible.
|
|
574
|
+
|
|
575
|
+
```json
|
|
576
|
+
{
|
|
577
|
+
"contract_version": 1, "ok": true, "command": "archive",
|
|
578
|
+
"archived": true,
|
|
579
|
+
"name": "vera",
|
|
580
|
+
"dir": "…/agents/.archived/vera",
|
|
581
|
+
"previousDir": "…/agents/vera",
|
|
582
|
+
"unscheduledJobId": "vera-nightly-consolidation"
|
|
583
|
+
}
|
|
584
|
+
```
|
|
585
|
+
|
|
586
|
+
An archived agent leaves `list.agents[]` and appears in `list.archived[]`.
|
|
587
|
+
Every published command except `unarchive` answers `agent_archived` for it — it
|
|
588
|
+
is never reported as missing, because a client told an archived specialist does
|
|
589
|
+
not exist would offer to hire a replacement for somebody who is still there.
|
|
590
|
+
|
|
591
|
+
Errors: `invalid_name`, `agent_not_found`, `agent_archived`,
|
|
592
|
+
`not_academy_owned`, `agent_exists`, `unsafe_agent_path`,
|
|
593
|
+
`runtime_unavailable`, `unschedule_failed`, `log_corrupt`, `lock_timeout`.
|
|
594
|
+
|
|
595
|
+
### `unarchive <name> [--json]`
|
|
596
|
+
|
|
597
|
+
Restore an archived agent to its canonical slot and re-register its nightly job.
|
|
598
|
+
|
|
599
|
+
```json
|
|
600
|
+
{
|
|
601
|
+
"contract_version": 1, "ok": true, "command": "unarchive",
|
|
602
|
+
"unarchived": true,
|
|
603
|
+
"name": "vera",
|
|
604
|
+
"dir": "…/agents/vera",
|
|
605
|
+
"previousDir": "…/agents/.archived/vera",
|
|
606
|
+
"scheduledJobId": "vera-nightly-consolidation"
|
|
607
|
+
}
|
|
608
|
+
```
|
|
609
|
+
|
|
610
|
+
`unarchive` is the one lifecycle command that may address an archived agent.
|
|
611
|
+
It is also the one that does **not** require the scheduler: with no scheduler
|
|
612
|
+
present it still succeeds and answers `scheduledJobId: null`.
|
|
613
|
+
|
|
614
|
+
Errors: `invalid_name`, `agent_not_found`, `not_academy_owned`,
|
|
615
|
+
`agent_exists`, `unsafe_agent_path`, `log_corrupt`, `lock_timeout`.
|
|
616
|
+
|
|
617
|
+
### `delete <name> [--json]`
|
|
618
|
+
|
|
619
|
+
Remove an agent and unregister its nightly job.
|
|
620
|
+
|
|
621
|
+
```json
|
|
622
|
+
{
|
|
623
|
+
"contract_version": 1, "ok": true, "command": "delete",
|
|
624
|
+
"deleted": true,
|
|
625
|
+
"name": "vera",
|
|
626
|
+
"dir": "…/agents/vera",
|
|
627
|
+
"unscheduledJobId": "vera-nightly-consolidation"
|
|
628
|
+
}
|
|
629
|
+
```
|
|
630
|
+
|
|
631
|
+
The directory is moved aside before anything is removed. If the nightly job
|
|
632
|
+
cannot be unregistered, the directory is put back and the command answers
|
|
633
|
+
`unschedule_failed` — Academy refuses to leave a scheduled job pointing at an
|
|
634
|
+
agent that no longer exists. If it cannot be put back because the slot has been
|
|
635
|
+
refilled, the answer is `unschedule_failed_restore_blocked` and the `error`
|
|
636
|
+
object names the `quarantine` path an operator must recover by hand.
|
|
637
|
+
|
|
638
|
+
Errors: `invalid_name`, `agent_not_found`, `agent_archived`,
|
|
639
|
+
`not_academy_owned`, `unsafe_agent_path`, `runtime_unavailable`,
|
|
640
|
+
`unschedule_failed`, `unschedule_failed_restore_blocked`, `log_corrupt`,
|
|
641
|
+
`lock_timeout`.
|
|
642
|
+
|
|
643
|
+
### `migrate [--dry-run] [--json]`
|
|
644
|
+
|
|
645
|
+
Write the missing ownership marker for agent directories that predate it. It is
|
|
646
|
+
the repair for `doctor`'s `unowned_agents` count, and it is operator-invoked,
|
|
647
|
+
never implicit.
|
|
648
|
+
|
|
649
|
+
```json
|
|
650
|
+
{
|
|
651
|
+
"contract_version": 1, "ok": true, "command": "migrate",
|
|
652
|
+
"agentsRoot": "/home/u/.academy/agents",
|
|
653
|
+
"dryRun": false,
|
|
654
|
+
"migrated": [ { "name": "kai", "dir": "…/agents/kai" } ],
|
|
655
|
+
"refused": [ { "name": "zed", "dir": "…/agents/zed",
|
|
656
|
+
"reason": "resolves outside AGENTS_ROOT: /tmp/elsewhere" } ]
|
|
657
|
+
}
|
|
658
|
+
```
|
|
659
|
+
|
|
660
|
+
`migrate` never creates an agent, never edits agent content, and never touches
|
|
661
|
+
a directory it cannot prove is that agent's own slot inside the root. It marks
|
|
662
|
+
only directories that already contain an `agent.yaml`.
|
|
663
|
+
|
|
664
|
+
**A refusal keeps `ok: true`.** One unwritable directory is one entry in
|
|
665
|
+
`refused[]`, not an abandoned sweep — the operator asked for every outstanding
|
|
666
|
+
marker and has to learn which ones landed. `refused[]` reasons are human text
|
|
667
|
+
and are not stable. Only a lock timeout fails the whole command.
|
|
668
|
+
|
|
669
|
+
`--dry-run` reports what it would write and writes nothing. `migrated[]` then
|
|
670
|
+
lists the planned repairs.
|
|
671
|
+
|
|
672
|
+
Errors: `unsafe_agent_path`, `lock_timeout`, `invalid_spec` (an unknown
|
|
673
|
+
option).
|
|
674
|
+
|
|
675
|
+
---
|
|
676
|
+
|
|
677
|
+
## 6. Commands Academy has but does not publish
|
|
678
|
+
|
|
679
|
+
Academy implements more commands than it publishes. The unpublished ones keep
|
|
680
|
+
working, and **no client may build on them**: they are absent from
|
|
681
|
+
`doctor.commands`, they have no envelope guarantee, and they may change or
|
|
682
|
+
disappear in any release.
|
|
683
|
+
|
|
684
|
+
| Command | Why it is not published |
|
|
685
|
+
| --- | --- |
|
|
686
|
+
| `notes` | It writes a boot surface. Writing a surface is the *document* contract — a client edits `notes.md` on disk directly (§10). A second, CLI-shaped way to do the same thing would be a second definition of the same surface. |
|
|
687
|
+
| `nightly` | Invoked by the scheduler Academy registers, never by a client. |
|
|
688
|
+
| `clean` | Traces to no client requirement. |
|
|
689
|
+
| `root` | Duplicates `doctor`'s `packageRoot` and `agentsRoot`. |
|
|
690
|
+
| `run` | The launch verb. It spawns a runtime with inherited stdio and exits on the child's status, so it can never emit an envelope. It remains how an agent is launched interactively; it is not a machine contract. |
|
|
691
|
+
| `destroy` | Not contract. Use `delete`. |
|
|
692
|
+
|
|
693
|
+
`hire` is published; the **interactive** form of `hire` is not (§5).
|
|
694
|
+
|
|
695
|
+
---
|
|
696
|
+
|
|
697
|
+
## 7. The fifteen error codes
|
|
698
|
+
|
|
699
|
+
The set is **closed** at contract version 1. A client may switch on
|
|
700
|
+
`error.code` exhaustively. A sixteenth code would be a contract change.
|
|
701
|
+
|
|
702
|
+
| Code | Meaning | Extra `error` keys | An invocation that produces it |
|
|
703
|
+
| --- | --- | --- | --- |
|
|
704
|
+
| `agent_not_found` | No agent by that name in the root. | `name` | `inspect ghost --json` |
|
|
705
|
+
| `unsafe_agent_path` | The agents root, or the agent directory, is not a path Academy can safely use: a symlink, not a directory, or resolving outside its parent or outside the root. | `agentsRoot` for a root fault, `name` for an agent fault | `list --json` with `AGENTS_ROOT` a symlink |
|
|
706
|
+
| `not_academy_owned` | The directory has no valid `.academy-agent.json`, or the marker names another agent. | `name` | `rename kai nova --json` after deleting `kai/.academy-agent.json` |
|
|
707
|
+
| `invalid_name` | The name is not kebab-case `^[a-z][a-z0-9-]{0,31}$`. | `name` | `inspect "Not A Name" --json` |
|
|
708
|
+
| `agent_exists` | The target name already has a directory. | `name`, `dir` | `create kai --json` twice |
|
|
709
|
+
| `agent_archived` | The agent is in the holding area. Only `unarchive` may address it. | `name`, `dir` | `archive kai --json` then `inspect kai --json` |
|
|
710
|
+
| `replay_unavailable` | The requested watermark cannot be served exactly. §8. | `requestedSeq`, `firstSeq`, `lastSeq`, `logId` | `events --since 999999 --json` |
|
|
711
|
+
| `log_corrupt` | The event log holds bytes but no parseable record, and a lifecycle command is about to append to it. Operator repair, not a client re-sync. | `eventLog` | write one unparseable line into the event log, then `create kai --json` |
|
|
712
|
+
| `invalid_runtime` | A `runtime` value outside `claude_code` / `codex`, in `agent.yaml` or in a hire spec. | `runtime`, and `dir` or `specPath` | set `runtime: mainframe` in `agent.yaml`, then `inspect kai --json` |
|
|
713
|
+
| `invalid_spec` | A request Academy will not accept: an unreadable or schema-invalid hire spec, an `agent.yaml` key in a form Academy cannot rewrite, or an unknown command option. | `specPath` and `field`/`keys`, or `option`, or `path` | `hire --spec ./not-json.json --json` |
|
|
714
|
+
| `runtime_unavailable` | An executable Academy needs is missing or failed. §11. | `name` when a nightly job could not be registered; `executable` when a binary could not be resolved, plus `status` and `timedOut` when a hire runtime failed | `create kai --json` with no `helm-tasks` on `PATH` |
|
|
715
|
+
| `lock_timeout` | An agent's lifecycle lock could not be taken within five seconds. | `lockDir` | hold `<root>/.kai.lifecycle.lock`, then `delete kai --json` |
|
|
716
|
+
| `internal_error` | The floor under the envelope: a fault no command anticipated. A `--json` caller always gets a parseable failure. | none | replace `kai/notes.md` with a directory, then `budget kai --json` |
|
|
717
|
+
| `unschedule_failed` | An agent's nightly job could not be unregistered, so the lifecycle command refused rather than orphan the job. Nothing moved. | `name` | `delete kai --json` with a scheduler that fails to remove the job |
|
|
718
|
+
| `unschedule_failed_restore_blocked` | The same failure, and the agent's slot was refilled before the directory could be put back. The directory is in a named quarantine and needs an operator. | `name`, `quarantine` | `delete kai --json` with a scheduler that fails *and* recreates the slot |
|
|
719
|
+
|
|
720
|
+
Every row above is exercised by the conformance suite (§13), one invocation per
|
|
721
|
+
code.
|
|
722
|
+
|
|
723
|
+
`internal_error` is the floor and can answer any command. The other fourteen
|
|
724
|
+
are raised only by the commands whose sections list them in §5.
|
|
725
|
+
|
|
726
|
+
**Handling guidance.** `agent_not_found`, `agent_exists`, `invalid_name`,
|
|
727
|
+
`agent_archived` and `invalid_spec` are user-facing and should be rendered as
|
|
728
|
+
such. `unsafe_agent_path`, `runtime_unavailable`, `log_corrupt`, `lock_timeout`
|
|
729
|
+
and the two `unschedule_*` codes are setup or environment problems and should be
|
|
730
|
+
surfaced to whoever administers the install. `replay_unavailable` has a defined
|
|
731
|
+
recovery, in §8. `internal_error` is a bug report.
|
|
732
|
+
|
|
733
|
+
---
|
|
734
|
+
|
|
735
|
+
## 8. The event log
|
|
736
|
+
|
|
737
|
+
Academy appends every agent lifecycle change to one file. `doctor` reports its
|
|
738
|
+
path as `eventLog`; it sits beside the agents root, at
|
|
739
|
+
`<agentsRoot>/../events.jsonl`. It is JSON Lines, append-only, and **never
|
|
740
|
+
trimmed**.
|
|
741
|
+
|
|
742
|
+
### Records
|
|
743
|
+
|
|
744
|
+
The first record of every log declares the log's own identity:
|
|
745
|
+
|
|
746
|
+
```json
|
|
747
|
+
{"seq":1,"event":"log_created","logId":"52892036-…","ts":"2026-09-01T02:12:24.253Z"}
|
|
748
|
+
```
|
|
749
|
+
|
|
750
|
+
Real lifecycle events start at `seq: 2`. Each carries
|
|
751
|
+
`{ seq, event, agentName, agentDir, ts }` plus per-event extras:
|
|
752
|
+
|
|
753
|
+
| `event` | Extra keys |
|
|
754
|
+
| --- | --- |
|
|
755
|
+
| `agent_created` | — |
|
|
756
|
+
| `agent_deleted` | — |
|
|
757
|
+
| `agent_renamed` | `previousName`, `previousDir` |
|
|
758
|
+
| `agent_archived` | `previousDir` |
|
|
759
|
+
| `agent_unarchived` | `previousDir` |
|
|
760
|
+
|
|
761
|
+
`seq` is a monotonic integer with no gaps. Because nothing trims, `firstSeq` is
|
|
762
|
+
permanently `1`.
|
|
763
|
+
|
|
764
|
+
### Delivery is at-least-once, and `seq` is the dedup key
|
|
765
|
+
|
|
766
|
+
**A client must apply each `seq` exactly once.** Academy may deliver the same
|
|
767
|
+
record more than once — most obviously because the re-sync in `list` reads its
|
|
768
|
+
watermark before its roster, so an agent created between the two reads appears
|
|
769
|
+
in both the roster and the next replay. Replaying an overlapping range must
|
|
770
|
+
therefore produce identical derived state in the client. Key your applied set
|
|
771
|
+
on `seq`, and make every handler idempotent.
|
|
772
|
+
|
|
773
|
+
Store `logId` beside your watermark. The pair `(logId, seq)` is your resume
|
|
774
|
+
point; `seq` alone is not.
|
|
775
|
+
|
|
776
|
+
### Polling
|
|
777
|
+
|
|
778
|
+
```sh
|
|
779
|
+
academy events --since <lastAppliedSeq> --logid <storedLogId> --json
|
|
780
|
+
```
|
|
781
|
+
|
|
782
|
+
Apply `events[]` in order. Adopt the response's `lastSeq` as your new
|
|
783
|
+
watermark. There is no daemon, no watcher and no push channel: a client polls at
|
|
784
|
+
whatever interval it chooses.
|
|
785
|
+
|
|
786
|
+
### `replay_unavailable`, and the one-call re-sync
|
|
787
|
+
|
|
788
|
+
An empty success would be indistinguishable from "you are up to date", so any
|
|
789
|
+
watermark that cannot be served exactly fails loudly with `replay_unavailable`
|
|
790
|
+
and `{ requestedSeq, firstSeq, lastSeq, logId }`.
|
|
791
|
+
|
|
792
|
+
**Three causes:**
|
|
793
|
+
|
|
794
|
+
1. The log is absent while the client holds a watermark above zero.
|
|
795
|
+
2. `--logid` does not match `logId` — the log was deleted and recreated, so
|
|
796
|
+
sequence numbers restarted and the client's watermark belongs to a different
|
|
797
|
+
epoch.
|
|
798
|
+
3. `requestedSeq` is above `lastSeq`.
|
|
799
|
+
|
|
800
|
+
**Recovery is one call, and it must be one call:**
|
|
801
|
+
|
|
802
|
+
```sh
|
|
803
|
+
academy list --json
|
|
804
|
+
```
|
|
805
|
+
|
|
806
|
+
`list` carries `lastSeq` and `logId` beside `agents[]` and `archived[]`, so the
|
|
807
|
+
roster snapshot and the watermark arrive **atomically**. Replace your roster
|
|
808
|
+
with what it returned, store its `lastSeq` and `logId`, and resume polling.
|
|
809
|
+
|
|
810
|
+
Do not re-sync with two calls. A `list` followed by a separate `events` loses
|
|
811
|
+
or double-applies anything that lands between them, and Academy publishes no
|
|
812
|
+
way to detect that.
|
|
813
|
+
|
|
814
|
+
### The log is client-readable
|
|
815
|
+
|
|
816
|
+
The event log path is contract, so an operator or a backup restore can replace
|
|
817
|
+
the file. That is exactly why `logId` exists, and why a client stores it.
|
|
818
|
+
|
|
819
|
+
---
|
|
820
|
+
|
|
821
|
+
## 9. The agent directory layout
|
|
822
|
+
|
|
823
|
+
An agent is a directory. Its layout is versioned by `contract_version`.
|
|
824
|
+
|
|
825
|
+
### Contract paths — a client may read and write these
|
|
826
|
+
|
|
827
|
+
| Path | Notes |
|
|
828
|
+
| --- | --- |
|
|
829
|
+
| `<agentsRoot>/<name>/agent.yaml` | Top-level scalars only. See below. |
|
|
830
|
+
| `<agentsRoot>/<name>/identity.md` | Boot surface. §10. |
|
|
831
|
+
| `<agentsRoot>/<name>/role.md` | Boot surface. |
|
|
832
|
+
| `<agentsRoot>/<name>/knowledge.md` | Boot surface. |
|
|
833
|
+
| `<agentsRoot>/<name>/goals.md` | Boot surface. |
|
|
834
|
+
| `<agentsRoot>/<name>/priorities.md` | Boot surface. |
|
|
835
|
+
| `<agentsRoot>/<name>/threads.md` | Boot surface. |
|
|
836
|
+
| `<agentsRoot>/<name>/notes.md` | Boot surface. |
|
|
837
|
+
| `<agentsRoot>/<name>/dailys.md` | Boot surface. |
|
|
838
|
+
| `<agentsRoot>/../events.jsonl` | The lifecycle event log. §8. Read only; Academy owns writes. Path from `doctor.eventLog`. |
|
|
839
|
+
| `~/.academy/sessions.jsonl` | The session index. Read only; the session hook owns writes. Path from `sessions.sessionIndex`. |
|
|
840
|
+
|
|
841
|
+
**`agent.yaml` is contract, with one rule.** Only *top-level scalars* are
|
|
842
|
+
published: `name`, `created`, `runtime`, `role`, `objective`, and
|
|
843
|
+
`displayName`, which sets the `displayName` field of every agent record and
|
|
844
|
+
falls back to `name` when absent. A client may edit
|
|
845
|
+
`runtime:` by hand, and the value must be `claude_code` or `codex` — anything
|
|
846
|
+
else makes the agent report `runtimeProvider: null` in `list` and raise
|
|
847
|
+
`invalid_runtime` in `inspect`. Academy reads the file with a line-oriented
|
|
848
|
+
reader and writes it with line surgery, never re-serialisation, so comments and
|
|
849
|
+
the `surfaces:` block survive untouched. Keep every published key as a
|
|
850
|
+
`key: value` line at column zero. A key restated indented, quoted or
|
|
851
|
+
space-padded is one Academy will refuse to rewrite, with `invalid_spec`.
|
|
852
|
+
|
|
853
|
+
### Internal paths — a client must not read or write these
|
|
854
|
+
|
|
855
|
+
They may change in any release, with no contract bump and no notice.
|
|
856
|
+
|
|
857
|
+
| Path | What it is |
|
|
858
|
+
| --- | --- |
|
|
859
|
+
| `<agentsRoot>/<name>/.academy-agent.json` | Ownership marker. Removing it breaks every lifecycle command; `migrate` restores it. |
|
|
860
|
+
| `<agentsRoot>/<name>/.academy/generated/` | The compiled system prompt. Derived from the eight surfaces; regenerated. |
|
|
861
|
+
| `<agentsRoot>/<name>/.claude/` | Claude Code skills, sub-agent definitions and local settings. |
|
|
862
|
+
| `<agentsRoot>/<name>/.claude-plugin` | Symlink into the Academy package. |
|
|
863
|
+
| `<agentsRoot>/<name>/hooks` | Symlink into the Academy package. |
|
|
864
|
+
| `<agentsRoot>/<name>/.agents/` | Codex skill surface. |
|
|
865
|
+
| `<agentsRoot>/<name>/.codex/` | Codex sub-agent definitions. |
|
|
866
|
+
| `<agentsRoot>/<name>/CLAUDE.md` | Runtime instruction file. |
|
|
867
|
+
| `<agentsRoot>/<name>/memory/` | Observation memory and the agent's own session log. |
|
|
868
|
+
| `<agentsRoot>/<name>/dreams/` | Nightly consolidation working area. |
|
|
869
|
+
| `<agentsRoot>/.archived/` | The holding area. Reach it through `archive` / `unarchive` and `list.archived[]`, never by path. |
|
|
870
|
+
| `<agentsRoot>/.<name>.lifecycle.lock` | Lifecycle lock. |
|
|
871
|
+
| `<agentsRoot>/.<name>.delete-quarantine.*` | Delete staging. |
|
|
872
|
+
| `<agentsRoot>/../events.lock`, `~/.academy/sessions.lock` | Append locks. |
|
|
873
|
+
|
|
874
|
+
The rule is simple: **the eight `.md` surfaces and `agent.yaml` are yours;
|
|
875
|
+
every dot-path is Academy's.**
|
|
876
|
+
|
|
877
|
+
---
|
|
878
|
+
|
|
879
|
+
## 10. The document contract: eight boot surfaces
|
|
880
|
+
|
|
881
|
+
Every agent has exactly eight boot surfaces, always these filenames:
|
|
882
|
+
|
|
883
|
+
```
|
|
884
|
+
identity.md role.md knowledge.md goals.md
|
|
885
|
+
priorities.md threads.md notes.md dailys.md
|
|
886
|
+
```
|
|
887
|
+
|
|
888
|
+
They are the agent's durable context, compiled into its system prompt on every
|
|
889
|
+
launch.
|
|
890
|
+
|
|
891
|
+
**Clients read and write them directly on disk.** There are deliberately no CLI
|
|
892
|
+
commands to write a surface. Direct file access *is* the document contract: a
|
|
893
|
+
client opens `<agentsRoot>/<name>/notes.md`, edits it, and saves. Academy picks
|
|
894
|
+
the change up on the next launch. Nothing needs to be told.
|
|
895
|
+
|
|
896
|
+
**What is contract:** the eight filenames, their location in the agent
|
|
897
|
+
directory, and that they are UTF-8 Markdown.
|
|
898
|
+
|
|
899
|
+
**What is not contract:** their headings, their internal conventions, the cap
|
|
900
|
+
values `budget` judges them against, and the templates `create` writes. A
|
|
901
|
+
client renders and edits what it finds; it must not require a particular
|
|
902
|
+
structure inside a surface.
|
|
903
|
+
|
|
904
|
+
Use `inspect` to learn which surfaces exist, and `tokens` or `budget` to show a
|
|
905
|
+
user the cost of what they have written.
|
|
906
|
+
|
|
907
|
+
---
|
|
908
|
+
|
|
909
|
+
## 11. External dependencies
|
|
910
|
+
|
|
911
|
+
Two executables are resolved from `PATH`. Neither is part of the response
|
|
912
|
+
contract, and both matter to a client author because their absence changes what
|
|
913
|
+
Academy can do.
|
|
914
|
+
|
|
915
|
+
### `helm-tasks` — required for the agent lifecycle
|
|
916
|
+
|
|
917
|
+
Academy registers each agent's nightly consolidation job through a scheduler
|
|
918
|
+
binary named `helm-tasks`, resolved from `PATH` or from
|
|
919
|
+
`ACADEMY_HELM_TASKS_BIN`.
|
|
920
|
+
|
|
921
|
+
**This is a hard dependency, and `doctor` does not report it.** `doctor`'s
|
|
922
|
+
`runtimes` names only `claude_code` and `codex`; there is no `helm-tasks` entry
|
|
923
|
+
and no health code for it. Without the binary:
|
|
924
|
+
|
|
925
|
+
| Command | Behaviour with no `helm-tasks` |
|
|
926
|
+
| --- | --- |
|
|
927
|
+
| `create` | fails, `runtime_unavailable` |
|
|
928
|
+
| `hire --spec` | fails, `runtime_unavailable` |
|
|
929
|
+
| `delete` | fails, `runtime_unavailable` |
|
|
930
|
+
| `rename` | fails, `runtime_unavailable` |
|
|
931
|
+
| `archive` | fails, `runtime_unavailable` |
|
|
932
|
+
| `unarchive` | succeeds, `scheduledJobId: null` |
|
|
933
|
+
| `doctor`, `list`, `inspect`, `tokens`, `budget`, `sessions`, `events`, `migrate` | unaffected |
|
|
934
|
+
|
|
935
|
+
A client that provisions Academy must provision the scheduler with it, or
|
|
936
|
+
present a setup problem to the operator. Detect it by attempting a `create` and
|
|
937
|
+
reading the code, not by probing `doctor`.
|
|
938
|
+
|
|
939
|
+
### `claude` and `codex` — the agent runtimes
|
|
940
|
+
|
|
941
|
+
`doctor.runtimes` reports each provider's availability. `hire --spec` needs
|
|
942
|
+
`claude`; a missing one answers `runtime_unavailable` with
|
|
943
|
+
`error.executable: "claude"`. Overrides are `ACADEMY_CLAUDE_BIN` and
|
|
944
|
+
`ACADEMY_CODEX_BIN`.
|
|
945
|
+
|
|
946
|
+
An unavailable runtime does not fail `doctor` and does not stop any read
|
|
947
|
+
command. It stops launching an agent, which is `run` — not a published command.
|
|
948
|
+
|
|
949
|
+
---
|
|
950
|
+
|
|
951
|
+
## 12. The one client-specific exception
|
|
952
|
+
|
|
953
|
+
Academy is client-agnostic. No identifier in Academy's code names a specific
|
|
954
|
+
client product, with exactly one deliberate exception, retained by decision and
|
|
955
|
+
documented here so the client-agnostic claim stays honest rather than quietly
|
|
956
|
+
becoming untrue. A case-insensitive search of the published package for either
|
|
957
|
+
client name returns one source file, named below.
|
|
958
|
+
|
|
959
|
+
**The exception is the memory sync bridge**, in Academy's `hooks/` directory.
|
|
960
|
+
`hooks/memory_bridge.mjs` is the only file in Academy that names a client. It
|
|
961
|
+
copies observation memory from one specific host product into an agent's
|
|
962
|
+
`memory/observations/`, and it declares the client-named environment keys, the
|
|
963
|
+
tool permission, and the nightly guidance prose that go with it.
|
|
964
|
+
|
|
965
|
+
**It is off by default.** The bridge activates only when
|
|
966
|
+
`ACADEMY_MEMORY_BRIDGE=1` is set in Academy's environment. Unset, a scaffolded
|
|
967
|
+
agent names no client at all: no client-named environment key is read, no
|
|
968
|
+
client-named tool permission is written into the agent's settings, and no
|
|
969
|
+
client-named prose appears in any skill.
|
|
970
|
+
|
|
971
|
+
**Treat it as an exception, not as a pattern.** It is not a plugin point, not
|
|
972
|
+
an extension mechanism, and not a template for a second client. A client
|
|
973
|
+
integrating Academy does not enable it, does not read it, and does not need it.
|
|
974
|
+
Academy publishes no other client-specific behaviour and will not add one.
|
|
975
|
+
|
|
976
|
+
---
|
|
977
|
+
|
|
978
|
+
## 13. Verify your integration
|
|
979
|
+
|
|
980
|
+
Academy ships a conformance suite. It is written for client authors, not for
|
|
981
|
+
Academy's developers: it never imports Academy source and asserts only what a
|
|
982
|
+
client can observe — the envelope, the exit status, the published command list,
|
|
983
|
+
the identity boundary, and the fifteen error codes.
|
|
984
|
+
|
|
985
|
+
```sh
|
|
986
|
+
# against the build in this package
|
|
987
|
+
node --test conformance/*.test.mjs
|
|
988
|
+
|
|
989
|
+
# against any other build: an install, an unpacked tarball, a checkout
|
|
990
|
+
ACADEMY_BIN=/usr/local/bin/academy node --test conformance/*.test.mjs
|
|
991
|
+
```
|
|
992
|
+
|
|
993
|
+
It needs Node 18, a POSIX shell, and nothing else. No packages are installed
|
|
994
|
+
and no test framework is used. Every test builds its own throwaway install with
|
|
995
|
+
a fresh temporary `HOME` and `AGENTS_ROOT` and a `PATH` built from nothing, so
|
|
996
|
+
it can drive the full lifecycle — including `delete` — with no way to reach any
|
|
997
|
+
agent on the machine running it. The scheduler and the runtime are stubbed,
|
|
998
|
+
because neither is part of the response contract.
|
|
999
|
+
|
|
1000
|
+
Run it against the exact build you ship. `conformance/README.md` explains what
|
|
1001
|
+
each file asserts and how to read a failure.
|
|
1002
|
+
|
|
1003
|
+
---
|
|
1004
|
+
|
|
1005
|
+
## 14. What is not contract
|
|
1006
|
+
|
|
1007
|
+
Depending on any of these will break.
|
|
1008
|
+
|
|
1009
|
+
- **Human output.** Every command without `--json` prints text for a person.
|
|
1010
|
+
Nothing about it is stable.
|
|
1011
|
+
- **`error.message` text.** Switch on `error.code`. Render `message` if you
|
|
1012
|
+
want, but never parse it.
|
|
1013
|
+
- **`refused[]` reasons in `migrate`.** Human text.
|
|
1014
|
+
- **The unpublished commands** in §6, and the interactive form of `hire`.
|
|
1015
|
+
- **Internal paths** in §9, including everything under an agent's dot-
|
|
1016
|
+
directories and the `.archived/` holding area.
|
|
1017
|
+
- **Surface content conventions** — headings, section structure, the token cap
|
|
1018
|
+
values.
|
|
1019
|
+
- **Academy's prompts.** What Academy says to a runtime is Academy's to change,
|
|
1020
|
+
including the hire prompt behind `hire --spec`.
|
|
1021
|
+
- **The package version.** Pin `contract_version`.
|
|
1022
|
+
- **Anything absent from `doctor.commands`.**
|
|
1023
|
+
|
|
1024
|
+
If you need something that is not published, say so rather than reading around
|
|
1025
|
+
the contract. Reading Academy's internals is the failure this contract exists to
|
|
1026
|
+
end.
|