@joenandez/academy 0.4.0 → 0.5.13

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 (122) hide show
  1. package/.claude-plugin/marketplace.json +5 -0
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/CHANGELOG.md +415 -0
  4. package/README.md +67 -122
  5. package/hooks/backstory/.claude-plugin/plugin.json +11 -0
  6. package/hooks/backstory/LICENSE +21 -0
  7. package/hooks/backstory/SOURCE +2 -0
  8. package/hooks/backstory/bin/backstory +9 -0
  9. package/hooks/backstory/hooks/hooks.json +45 -0
  10. package/hooks/backstory/scripts/knowledge.mjs +87 -0
  11. package/hooks/backstory/scripts/memory.mjs +685 -0
  12. package/hooks/backstory/scripts/tool-tick.mjs +1 -0
  13. package/hooks/backstory/skills/capture/SKILL.md +71 -0
  14. package/hooks/backstory/skills/capture/references/knowledge-capture-input.json +16 -0
  15. package/hooks/backstory/skills/capture/references/tagging-policy.md +29 -0
  16. package/hooks/backstory/skills/memory/SKILL.md +70 -0
  17. package/hooks/backstory/skills/setup/SKILL.md +85 -0
  18. package/hooks/context_metrics.mjs +67 -0
  19. package/hooks/hook_runtime.mjs +43 -2
  20. package/hooks/memory_bridge.mjs +286 -52
  21. package/hooks/memory_store.mjs +78 -26
  22. package/hooks/register_session.mjs +76 -14
  23. package/hooks/roque/.claude-plugin/plugin.json +6 -0
  24. package/hooks/roque/LICENSE +21 -0
  25. package/hooks/roque/SOURCE +1 -0
  26. package/hooks/roque/hooks.json +38 -0
  27. package/hooks/roque/package.json +3 -0
  28. package/hooks/roque/roque.js +1690 -0
  29. package/hooks/sync_memory.mjs +11 -7
  30. package/package.json +10 -3
  31. package/scripts/cli/agent-gate.mjs +70 -0
  32. package/scripts/cli/agent-id.mjs +37 -0
  33. package/scripts/cli/archive.mjs +39 -38
  34. package/scripts/cli/archived.mjs +4 -2
  35. package/scripts/cli/args-kinds.mjs +33 -0
  36. package/scripts/cli/args-notes.mjs +42 -0
  37. package/scripts/cli/args-owner.mjs +37 -0
  38. package/scripts/cli/args-run.mjs +38 -0
  39. package/scripts/cli/args.mjs +47 -104
  40. package/scripts/cli/beads.mjs +57 -0
  41. package/scripts/cli/codex.mjs +66 -12
  42. package/scripts/cli/core.mjs +117 -41
  43. package/scripts/cli/create.mjs +224 -76
  44. package/scripts/cli/curator-feed-codex.mjs +81 -0
  45. package/scripts/cli/curator-feed-observations.mjs +97 -0
  46. package/scripts/cli/curator-feed-scope.mjs +124 -0
  47. package/scripts/cli/curator-feed-sources.mjs +172 -0
  48. package/scripts/cli/curator-feed.mjs +318 -0
  49. package/scripts/cli/default-name.mjs +73 -0
  50. package/scripts/cli/doctor.mjs +67 -2
  51. package/scripts/cli/eventlog.mjs +6 -2
  52. package/scripts/cli/exit-code.mjs +9 -0
  53. package/scripts/cli/first-run.mjs +115 -0
  54. package/scripts/cli/heartbeat.mjs +135 -0
  55. package/scripts/cli/helm-skip.mjs +20 -0
  56. package/scripts/cli/hire-headless.mjs +37 -22
  57. package/scripts/cli/hire.mjs +8 -2
  58. package/scripts/cli/inspect.mjs +36 -18
  59. package/scripts/cli/lifecycle.mjs +95 -49
  60. package/scripts/cli/main.mjs +32 -2
  61. package/scripts/cli/memory-env.mjs +124 -0
  62. package/scripts/cli/migrate.mjs +13 -7
  63. package/scripts/cli/next-steps.mjs +13 -0
  64. package/scripts/cli/nightly-launch.mjs +118 -0
  65. package/scripts/cli/nightly.mjs +228 -0
  66. package/scripts/cli/notes.mjs +58 -8
  67. package/scripts/cli/owner-config.mjs +181 -0
  68. package/scripts/cli/owner-jobs.mjs +201 -0
  69. package/scripts/cli/prompt-append.mjs +135 -0
  70. package/scripts/cli/rename.mjs +90 -109
  71. package/scripts/cli/run-context.mjs +50 -0
  72. package/scripts/cli/run.mjs +236 -79
  73. package/scripts/cli/scaffold.mjs +119 -126
  74. package/scripts/cli/schedule-errors.mjs +70 -0
  75. package/scripts/cli/sessions.mjs +10 -3
  76. package/scripts/cli/skills-check.mjs +147 -0
  77. package/scripts/cli/skills.mjs +170 -0
  78. package/scripts/cli/start.mjs +201 -0
  79. package/scripts/cli/templates.mjs +119 -45
  80. package/scripts/cli/tldr.mjs +184 -0
  81. package/scripts/cli/toml-string.mjs +76 -0
  82. package/scripts/cli/user-work-view.mjs +48 -0
  83. package/scripts/cli/writable-dirs.mjs +23 -0
  84. package/scripts/cli/yaml.mjs +1 -1
  85. package/skills/hire/SKILL.md +112 -100
  86. package/skills/hire/references/owner.md +68 -0
  87. package/templates/agents/claude-code/knowledge-curator.md +5 -2
  88. package/templates/agents/codex/knowledge-curator.toml +2 -1
  89. package/templates/operating-model/base.md +78 -0
  90. package/templates/operating-model/owner.md +11 -0
  91. package/templates/operating-model/principal.md +9 -0
  92. package/templates/operating-model/specialist.md +9 -0
  93. package/templates/packs/owner/craft.md +32 -0
  94. package/templates/packs/owner/identity.md +13 -0
  95. package/templates/packs/owner/mandate.md +26 -0
  96. package/templates/packs/owner/owner/SKILL.md +118 -0
  97. package/templates/packs/owner/owner-feedback/SKILL.md +49 -0
  98. package/templates/packs/owner/owner-heartbeat/SKILL.md +35 -0
  99. package/templates/packs/owner/owner-nightly/SKILL.md +114 -0
  100. package/templates/packs/owner/owner-review/SKILL.md +70 -0
  101. package/templates/packs/owner/owner-work/SKILL.md +53 -0
  102. package/templates/packs/principal/craft.md +27 -0
  103. package/templates/packs/principal/identity.md +13 -0
  104. package/templates/packs/principal/mandate.md +23 -0
  105. package/templates/packs/principal/principal-brief/SKILL.md +44 -0
  106. package/templates/packs/principal/principal-intro/SKILL.md +95 -0
  107. package/templates/packs/principal/principal-nightly/SKILL.md +152 -0
  108. package/templates/skills/check-in/SKILL.md +34 -24
  109. package/templates/skills/knowledge-curation/SKILL.md +89 -48
  110. package/templates/skills/local-rules/SKILL.md +86 -0
  111. package/templates/skills/nightly-consolidation/SKILL.md +131 -100
  112. package/templates/skills/skill-method/SKILL.md +79 -0
  113. package/templates/skills/user-nightly/SKILL.md +143 -0
  114. package/templates/skills/work-graph/SKILL.md +75 -0
  115. package/conformance/README.md +0 -60
  116. package/conformance/discovery.test.mjs +0 -140
  117. package/conformance/envelope.test.mjs +0 -185
  118. package/conformance/error-codes.test.mjs +0 -125
  119. package/conformance/harness.mjs +0 -180
  120. package/conformance/identity.test.mjs +0 -125
  121. package/docs/integration-guide.md +0 -1042
  122. package/templates/skills/self-update/SKILL.md +0 -121
@@ -1,1042 +0,0 @@
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
- **Committed-mutation failure.** Academy moves the directory before it appends
592
- the lifecycle event. If `archive --json` answers `log_corrupt` or
593
- `internal_error`, the move can already have landed. Recover with one
594
- `academy list --json` call and replace the client roster and watermark from that
595
- response.
596
-
597
- Errors: `invalid_name`, `agent_not_found`, `agent_archived`,
598
- `not_academy_owned`, `agent_exists`, `unsafe_agent_path`,
599
- `runtime_unavailable`, `unschedule_failed`, `log_corrupt`, `lock_timeout`.
600
-
601
- ### `unarchive <name> [--json]`
602
-
603
- Restore an archived agent to its canonical slot and re-register its nightly job.
604
-
605
- ```json
606
- {
607
- "contract_version": 1, "ok": true, "command": "unarchive",
608
- "unarchived": true,
609
- "name": "vera",
610
- "dir": "…/agents/vera",
611
- "previousDir": "…/agents/.archived/vera",
612
- "scheduledJobId": "vera-nightly-consolidation"
613
- }
614
- ```
615
-
616
- `unarchive` is the one lifecycle command that may address an archived agent.
617
- It is also the one that does **not** require the scheduler: with no scheduler
618
- present it still succeeds and answers `scheduledJobId: null`.
619
-
620
- Errors: `invalid_name`, `agent_not_found`, `not_academy_owned`,
621
- `agent_exists`, `unsafe_agent_path`, `log_corrupt`, `lock_timeout`.
622
-
623
- ### `delete <name> [--json]`
624
-
625
- Remove an agent and unregister its nightly job.
626
-
627
- ```json
628
- {
629
- "contract_version": 1, "ok": true, "command": "delete",
630
- "deleted": true,
631
- "name": "vera",
632
- "dir": "…/agents/vera",
633
- "unscheduledJobId": "vera-nightly-consolidation"
634
- }
635
- ```
636
-
637
- The directory is moved aside before anything is removed. If the nightly job
638
- cannot be unregistered, the directory is put back and the command answers
639
- `unschedule_failed` — Academy refuses to leave a scheduled job pointing at an
640
- agent that no longer exists. If it cannot be put back because the slot has been
641
- refilled, the answer is `unschedule_failed_restore_blocked` and the `error`
642
- object names the `quarantine` path an operator must recover by hand.
643
-
644
- **Committed-mutation failure.** Academy removes the quarantined directory before
645
- it appends the lifecycle event. If `delete --json` answers `log_corrupt` or
646
- `internal_error`, the removal can already have landed. Recover with one
647
- `academy list --json` call and replace the client roster and watermark from that
648
- response.
649
-
650
- Errors: `invalid_name`, `agent_not_found`, `agent_archived`,
651
- `not_academy_owned`, `unsafe_agent_path`, `runtime_unavailable`,
652
- `unschedule_failed`, `unschedule_failed_restore_blocked`, `log_corrupt`,
653
- `lock_timeout`.
654
-
655
- ### `migrate [--dry-run] [--json]`
656
-
657
- Write the missing ownership marker for agent directories that predate it. It is
658
- the repair for `doctor`'s `unowned_agents` count, and it is operator-invoked,
659
- never implicit.
660
-
661
- ```json
662
- {
663
- "contract_version": 1, "ok": true, "command": "migrate",
664
- "agentsRoot": "/home/u/.academy/agents",
665
- "dryRun": false,
666
- "migrated": [ { "name": "kai", "dir": "…/agents/kai" } ],
667
- "refused": [ { "name": "zed", "dir": "…/agents/zed",
668
- "reason": "resolves outside AGENTS_ROOT: /tmp/elsewhere" } ]
669
- }
670
- ```
671
-
672
- `migrate` never creates an agent, never edits agent content, and never touches
673
- a directory it cannot prove is that agent's own slot inside the root. It marks
674
- only directories that already contain an `agent.yaml`.
675
-
676
- **A refusal keeps `ok: true`.** One unwritable directory is one entry in
677
- `refused[]`, not an abandoned sweep — the operator asked for every outstanding
678
- marker and has to learn which ones landed. `refused[]` reasons are human text
679
- and are not stable. Only a lock timeout fails the whole command.
680
-
681
- `--dry-run` reports what it would write and writes nothing. `migrated[]` then
682
- lists the planned repairs.
683
-
684
- Errors: `unsafe_agent_path`, `lock_timeout`, `invalid_spec` (an unknown
685
- option).
686
-
687
- ---
688
-
689
- ## 6. Commands Academy has but does not publish
690
-
691
- Academy implements more commands than it publishes. The unpublished ones keep
692
- working, and **no client may build on them**: they are absent from
693
- `doctor.commands`, they have no envelope guarantee, and they may change or
694
- disappear in any release.
695
-
696
- | Command | Why it is not published |
697
- | --- | --- |
698
- | `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. |
699
- | `nightly` | Invoked by the scheduler Academy registers, never by a client. |
700
- | `clean` | Traces to no client requirement. |
701
- | `root` | Duplicates `doctor`'s `packageRoot` and `agentsRoot`. |
702
- | `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. |
703
- | `destroy` | Not contract. Use `delete`. |
704
-
705
- `hire` is published; the **interactive** form of `hire` is not (§5).
706
-
707
- ---
708
-
709
- ## 7. The fifteen error codes
710
-
711
- The set is **closed** at contract version 1. A client may switch on
712
- `error.code` exhaustively. A sixteenth code would be a contract change.
713
-
714
- | Code | Meaning | Extra `error` keys | An invocation that produces it |
715
- | --- | --- | --- | --- |
716
- | `agent_not_found` | No agent by that name in the root. | `name` | `inspect ghost --json` |
717
- | `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 |
718
- | `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` |
719
- | `invalid_name` | The name is not kebab-case `^[a-z][a-z0-9-]{0,31}$`. | `name` | `inspect "Not A Name" --json` |
720
- | `agent_exists` | The target name already has a directory. | `name`, `dir` | `create kai --json` twice |
721
- | `agent_archived` | The agent is in the holding area. Only `unarchive` may address it. | `name`, `dir` | `archive kai --json` then `inspect kai --json` |
722
- | `replay_unavailable` | The requested watermark cannot be served exactly. §8. | `requestedSeq`, `firstSeq`, `lastSeq`, `logId` | `events --since 999999 --json` |
723
- | `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` |
724
- | `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` |
725
- | `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` |
726
- | `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` |
727
- | `lock_timeout` | An agent's lifecycle lock could not be taken within five seconds. | `lockDir` | hold `<root>/.kai.lifecycle.lock`, then `delete kai --json` |
728
- | `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` |
729
- | `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 |
730
- | `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 |
731
-
732
- Every row above is exercised by the conformance suite (§13), one invocation per
733
- code.
734
-
735
- `internal_error` is the floor and can answer any command. The other fourteen
736
- are raised only by the commands whose sections list them in §5.
737
-
738
- For `archive` and `delete`, `log_corrupt` or `internal_error` can follow a
739
- committed directory move or removal. Use `list --json` as the atomic roster and
740
- watermark recovery call described in §8.
741
-
742
- **Handling guidance.** `agent_not_found`, `agent_exists`, `invalid_name`,
743
- `agent_archived` and `invalid_spec` are user-facing and should be rendered as
744
- such. `unsafe_agent_path`, `runtime_unavailable`, `log_corrupt`, `lock_timeout`
745
- and the two `unschedule_*` codes are setup or environment problems and should be
746
- surfaced to whoever administers the install. `replay_unavailable` has a defined
747
- recovery, in §8. `internal_error` is a bug report.
748
-
749
- ---
750
-
751
- ## 8. The event log
752
-
753
- Academy appends every agent lifecycle change to one file. `doctor` reports its
754
- path as `eventLog`; it sits beside the agents root, at
755
- `<agentsRoot>/../events.jsonl`. It is JSON Lines, append-only, and **never
756
- trimmed**.
757
-
758
- ### Records
759
-
760
- The first record of every log declares the log's own identity:
761
-
762
- ```json
763
- {"seq":1,"event":"log_created","logId":"52892036-…","ts":"2026-09-01T02:12:24.253Z"}
764
- ```
765
-
766
- Real lifecycle events start at `seq: 2`. Each carries
767
- `{ seq, event, agentName, agentDir, ts }` plus per-event extras:
768
-
769
- | `event` | Extra keys |
770
- | --- | --- |
771
- | `agent_created` | — |
772
- | `agent_deleted` | — |
773
- | `agent_renamed` | `previousName`, `previousDir` |
774
- | `agent_archived` | `previousDir` |
775
- | `agent_unarchived` | `previousDir` |
776
-
777
- `seq` is a monotonic integer with no gaps. Because nothing trims, `firstSeq` is
778
- permanently `1`.
779
-
780
- ### Delivery is at-least-once, and `seq` is the dedup key
781
-
782
- **A client must apply each `seq` exactly once.** Academy may deliver the same
783
- record more than once — most obviously because the re-sync in `list` reads its
784
- watermark before its roster, so an agent created between the two reads appears
785
- in both the roster and the next replay. Replaying an overlapping range must
786
- therefore produce identical derived state in the client. Key your applied set
787
- on `seq`, and make every handler idempotent.
788
-
789
- Store `logId` beside your watermark. The pair `(logId, seq)` is your resume
790
- point; `seq` alone is not.
791
-
792
- ### Polling
793
-
794
- ```sh
795
- academy events --since <lastAppliedSeq> --logid <storedLogId> --json
796
- ```
797
-
798
- Apply `events[]` in order. Adopt the response's `lastSeq` as your new
799
- watermark. There is no daemon, no watcher and no push channel: a client polls at
800
- whatever interval it chooses.
801
-
802
- ### `replay_unavailable`, and the one-call re-sync
803
-
804
- An empty success would be indistinguishable from "you are up to date", so any
805
- watermark that cannot be served exactly fails loudly with `replay_unavailable`
806
- and `{ requestedSeq, firstSeq, lastSeq, logId }`.
807
-
808
- **Three causes:**
809
-
810
- 1. The log is absent while the client holds a watermark above zero.
811
- 2. `--logid` does not match `logId` — the log was deleted and recreated, so
812
- sequence numbers restarted and the client's watermark belongs to a different
813
- epoch.
814
- 3. `requestedSeq` is above `lastSeq`.
815
-
816
- **Recovery is one call, and it must be one call:**
817
-
818
- ```sh
819
- academy list --json
820
- ```
821
-
822
- `list` carries `lastSeq` and `logId` beside `agents[]` and `archived[]`, so the
823
- roster snapshot and the watermark arrive **atomically**. Replace your roster
824
- with what it returned, store its `lastSeq` and `logId`, and resume polling.
825
-
826
- Do not re-sync with two calls. A `list` followed by a separate `events` loses
827
- or double-applies anything that lands between them, and Academy publishes no
828
- way to detect that.
829
-
830
- ### The log is client-readable
831
-
832
- The event log path is contract, so an operator or a backup restore can replace
833
- the file. That is exactly why `logId` exists, and why a client stores it.
834
-
835
- ---
836
-
837
- ## 9. The agent directory layout
838
-
839
- An agent is a directory. Its layout is versioned by `contract_version`.
840
-
841
- ### Contract paths — a client may read and write these
842
-
843
- | Path | Notes |
844
- | --- | --- |
845
- | `<agentsRoot>/<name>/agent.yaml` | Top-level scalars only. See below. |
846
- | `<agentsRoot>/<name>/identity.md` | Boot surface. §10. |
847
- | `<agentsRoot>/<name>/role.md` | Boot surface. |
848
- | `<agentsRoot>/<name>/knowledge.md` | Boot surface. |
849
- | `<agentsRoot>/<name>/goals.md` | Boot surface. |
850
- | `<agentsRoot>/<name>/priorities.md` | Boot surface. |
851
- | `<agentsRoot>/<name>/threads.md` | Boot surface. |
852
- | `<agentsRoot>/<name>/notes.md` | Boot surface. |
853
- | `<agentsRoot>/<name>/dailys.md` | Boot surface. |
854
- | `<agentsRoot>/../events.jsonl` | The lifecycle event log. §8. Read only; Academy owns writes. Path from `doctor.eventLog`. |
855
- | `~/.academy/sessions.jsonl` | The session index. Read only; the session hook owns writes. Path from `sessions.sessionIndex`. |
856
-
857
- **`agent.yaml` is contract, with one rule.** Only *top-level scalars* are
858
- published: `name`, `created`, `runtime`, `role`, `objective`, and
859
- `displayName`, which sets the `displayName` field of every agent record and
860
- falls back to `name` when absent. A client may edit
861
- `runtime:` by hand, and the value must be `claude_code` or `codex` — anything
862
- else makes the agent report `runtimeProvider: null` in `list` and raise
863
- `invalid_runtime` in `inspect`. Academy reads the file with a line-oriented
864
- reader and writes it with line surgery, never re-serialisation, so comments and
865
- the `surfaces:` block survive untouched. Keep every published key as a
866
- `key: value` line at column zero. A key restated indented, quoted or
867
- space-padded is one Academy will refuse to rewrite, with `invalid_spec`.
868
-
869
- ### Internal paths — a client must not read or write these
870
-
871
- They may change in any release, with no contract bump and no notice.
872
-
873
- | Path | What it is |
874
- | --- | --- |
875
- | `<agentsRoot>/<name>/.academy-agent.json` | Ownership marker. Removing it breaks every lifecycle command; `migrate` restores it. |
876
- | `<agentsRoot>/<name>/.academy/generated/` | The compiled system prompt. Derived from the eight surfaces; regenerated. |
877
- | `<agentsRoot>/<name>/.claude/` | Claude Code skills, sub-agent definitions and local settings. |
878
- | `<agentsRoot>/<name>/.claude-plugin` | Symlink into the Academy package. |
879
- | `<agentsRoot>/<name>/hooks` | Symlink into the Academy package. |
880
- | `<agentsRoot>/<name>/.agents/` | Codex skill surface. |
881
- | `<agentsRoot>/<name>/.codex/` | Codex sub-agent definitions. |
882
- | `<agentsRoot>/<name>/CLAUDE.md` | Runtime instruction file. |
883
- | `<agentsRoot>/<name>/memory/` | Observation memory and the agent's own session log. |
884
- | `<agentsRoot>/<name>/dreams/` | Nightly consolidation working area. |
885
- | `<agentsRoot>/.archived/` | The holding area. Reach it through `archive` / `unarchive` and `list.archived[]`, never by path. |
886
- | `<agentsRoot>/.<name>.lifecycle.lock` | Lifecycle lock. |
887
- | `<agentsRoot>/.<name>.delete-quarantine.*` | Delete staging. |
888
- | `<agentsRoot>/../events.lock`, `~/.academy/sessions.lock` | Append locks. |
889
-
890
- The rule is simple: **the eight `.md` surfaces and `agent.yaml` are yours;
891
- every dot-path is Academy's.**
892
-
893
- ---
894
-
895
- ## 10. The document contract: eight boot surfaces
896
-
897
- Every agent has exactly eight boot surfaces, always these filenames:
898
-
899
- ```
900
- identity.md role.md knowledge.md goals.md
901
- priorities.md threads.md notes.md dailys.md
902
- ```
903
-
904
- They are the agent's durable context, compiled into its system prompt on every
905
- launch.
906
-
907
- **Clients read and write them directly on disk.** There are deliberately no CLI
908
- commands to write a surface. Direct file access *is* the document contract: a
909
- client opens `<agentsRoot>/<name>/notes.md`, edits it, and saves. Academy picks
910
- the change up on the next launch. Nothing needs to be told.
911
-
912
- **What is contract:** the eight filenames, their location in the agent
913
- directory, and that they are UTF-8 Markdown.
914
-
915
- **What is not contract:** their headings, their internal conventions, the cap
916
- values `budget` judges them against, and the templates `create` writes. A
917
- client renders and edits what it finds; it must not require a particular
918
- structure inside a surface.
919
-
920
- Use `inspect` to learn which surfaces exist, and `tokens` or `budget` to show a
921
- user the cost of what they have written.
922
-
923
- ---
924
-
925
- ## 11. External dependencies
926
-
927
- Two executables are resolved from `PATH`. Neither is part of the response
928
- contract, and both matter to a client author because their absence changes what
929
- Academy can do.
930
-
931
- ### `helm-tasks` — required for the agent lifecycle
932
-
933
- Academy registers each agent's nightly consolidation job through a scheduler
934
- binary named `helm-tasks`, resolved from `PATH` or from
935
- `ACADEMY_HELM_TASKS_BIN`.
936
-
937
- **This is a hard dependency, and `doctor` does not report it.** `doctor`'s
938
- `runtimes` names only `claude_code` and `codex`; there is no `helm-tasks` entry
939
- and no health code for it. Without the binary:
940
-
941
- | Command | Behaviour with no `helm-tasks` |
942
- | --- | --- |
943
- | `create` | fails, `runtime_unavailable` |
944
- | `hire --spec` | fails, `runtime_unavailable` |
945
- | `delete` | fails, `runtime_unavailable` |
946
- | `rename` | fails, `runtime_unavailable` |
947
- | `archive` | fails, `runtime_unavailable` |
948
- | `unarchive` | succeeds, `scheduledJobId: null` |
949
- | `doctor`, `list`, `inspect`, `tokens`, `budget`, `sessions`, `events`, `migrate` | unaffected |
950
-
951
- A client that provisions Academy must provision the scheduler with it, or
952
- present a setup problem to the operator. Detect it by attempting a `create` and
953
- reading the code, not by probing `doctor`.
954
-
955
- ### `claude` and `codex` — the agent runtimes
956
-
957
- `doctor.runtimes` reports each provider's availability. `hire --spec` needs
958
- `claude`; a missing one answers `runtime_unavailable` with
959
- `error.executable: "claude"`. Overrides are `ACADEMY_CLAUDE_BIN` and
960
- `ACADEMY_CODEX_BIN`.
961
-
962
- An unavailable runtime does not fail `doctor` and does not stop any read
963
- command. It stops launching an agent, which is `run` — not a published command.
964
-
965
- ---
966
-
967
- ## 12. The one client-specific exception
968
-
969
- Academy is client-agnostic. No identifier in Academy's code names a specific
970
- client product, with exactly one deliberate exception, retained by decision and
971
- documented here so the client-agnostic claim stays honest rather than quietly
972
- becoming untrue. A case-insensitive search of the published package for either
973
- client name returns one source file, named below.
974
-
975
- **The exception is the memory sync bridge**, in Academy's `hooks/` directory.
976
- `hooks/memory_bridge.mjs` is the only file in Academy that names a client. It
977
- copies observation memory from one specific host product into an agent's
978
- `memory/observations/`, and it declares the client-named environment keys, the
979
- tool permission, and the nightly guidance prose that go with it.
980
-
981
- **It is off by default.** The bridge activates only when
982
- `ACADEMY_MEMORY_BRIDGE=1` is set in Academy's environment. Unset, a scaffolded
983
- agent names no client at all: no client-named environment key is read, no
984
- client-named tool permission is written into the agent's settings, and no
985
- client-named prose appears in any skill.
986
-
987
- **Treat it as an exception, not as a pattern.** It is not a plugin point, not
988
- an extension mechanism, and not a template for a second client. A client
989
- integrating Academy does not enable it, does not read it, and does not need it.
990
- Academy publishes no other client-specific behaviour and will not add one.
991
-
992
- ---
993
-
994
- ## 13. Verify your integration
995
-
996
- Academy ships a conformance suite. It is written for client authors, not for
997
- Academy's developers: it never imports Academy source and asserts only what a
998
- client can observe — the envelope, the exit status, the published command list,
999
- the identity boundary, and the fifteen error codes.
1000
-
1001
- ```sh
1002
- # against the build in this package
1003
- node --test conformance/*.test.mjs
1004
-
1005
- # against any other build: an install, an unpacked tarball, a checkout
1006
- ACADEMY_BIN=/usr/local/bin/academy node --test conformance/*.test.mjs
1007
- ```
1008
-
1009
- It needs Node 18, a POSIX shell, and nothing else. No packages are installed
1010
- and no test framework is used. Every test builds its own throwaway install with
1011
- a fresh temporary `HOME` and `AGENTS_ROOT` and a `PATH` built from nothing, so
1012
- it can drive the full lifecycle — including `delete` — with no way to reach any
1013
- agent on the machine running it. The scheduler and the runtime are stubbed,
1014
- because neither is part of the response contract.
1015
-
1016
- Run it against the exact build you ship. `conformance/README.md` explains what
1017
- each file asserts and how to read a failure.
1018
-
1019
- ---
1020
-
1021
- ## 14. What is not contract
1022
-
1023
- Depending on any of these will break.
1024
-
1025
- - **Human output.** Every command without `--json` prints text for a person.
1026
- Nothing about it is stable.
1027
- - **`error.message` text.** Switch on `error.code`. Render `message` if you
1028
- want, but never parse it.
1029
- - **`refused[]` reasons in `migrate`.** Human text.
1030
- - **The unpublished commands** in §6, and the interactive form of `hire`.
1031
- - **Internal paths** in §9, including everything under an agent's dot-
1032
- directories and the `.archived/` holding area.
1033
- - **Surface content conventions** — headings, section structure, the token cap
1034
- values.
1035
- - **Academy's prompts.** What Academy says to a runtime is Academy's to change,
1036
- including the hire prompt behind `hire --spec`.
1037
- - **The package version.** Pin `contract_version`.
1038
- - **Anything absent from `doctor.commands`.**
1039
-
1040
- If you need something that is not published, say so rather than reading around
1041
- the contract. Reading Academy's internals is the failure this contract exists to
1042
- end.