@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.
Files changed (53) hide show
  1. package/.claude-plugin/marketplace.json +14 -0
  2. package/.claude-plugin/plugin.json +6 -0
  3. package/CHANGELOG.md +46 -0
  4. package/LICENSE +21 -0
  5. package/README.md +209 -0
  6. package/bin/academy +2 -0
  7. package/conformance/README.md +60 -0
  8. package/conformance/discovery.test.mjs +140 -0
  9. package/conformance/envelope.test.mjs +185 -0
  10. package/conformance/error-codes.test.mjs +125 -0
  11. package/conformance/harness.mjs +180 -0
  12. package/conformance/identity.test.mjs +125 -0
  13. package/docs/integration-guide.md +1026 -0
  14. package/hooks/hook_runtime.mjs +100 -0
  15. package/hooks/hooks.json +26 -0
  16. package/hooks/inject_surface.py +122 -0
  17. package/hooks/memory_bridge.mjs +120 -0
  18. package/hooks/memory_store.mjs +66 -0
  19. package/hooks/register_session.mjs +51 -0
  20. package/hooks/sync_memory.mjs +27 -0
  21. package/package.json +41 -0
  22. package/scripts/agent.mjs +3 -0
  23. package/scripts/cli/archive.mjs +161 -0
  24. package/scripts/cli/archived.mjs +82 -0
  25. package/scripts/cli/args.mjs +282 -0
  26. package/scripts/cli/codex.mjs +216 -0
  27. package/scripts/cli/core.mjs +389 -0
  28. package/scripts/cli/create.mjs +242 -0
  29. package/scripts/cli/doctor.mjs +203 -0
  30. package/scripts/cli/eventlog.mjs +129 -0
  31. package/scripts/cli/events.mjs +80 -0
  32. package/scripts/cli/hire-headless.mjs +229 -0
  33. package/scripts/cli/hire-spec.mjs +164 -0
  34. package/scripts/cli/hire.mjs +92 -0
  35. package/scripts/cli/inspect.mjs +286 -0
  36. package/scripts/cli/lifecycle.mjs +296 -0
  37. package/scripts/cli/main.mjs +102 -0
  38. package/scripts/cli/migrate.mjs +183 -0
  39. package/scripts/cli/notes.mjs +104 -0
  40. package/scripts/cli/rename.mjs +172 -0
  41. package/scripts/cli/run.mjs +227 -0
  42. package/scripts/cli/runtime.mjs +47 -0
  43. package/scripts/cli/scaffold.mjs +332 -0
  44. package/scripts/cli/sessions.mjs +98 -0
  45. package/scripts/cli/templates.mjs +104 -0
  46. package/scripts/cli/yaml.mjs +124 -0
  47. package/skills/hire/SKILL.md +669 -0
  48. package/templates/agents/claude-code/knowledge-curator.md +14 -0
  49. package/templates/agents/codex/knowledge-curator.toml +9 -0
  50. package/templates/skills/check-in/SKILL.md +122 -0
  51. package/templates/skills/knowledge-curation/SKILL.md +132 -0
  52. package/templates/skills/nightly-consolidation/SKILL.md +240 -0
  53. 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.