@joenandez/academy 0.4.0-rc.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +14 -0
- package/.claude-plugin/plugin.json +6 -0
- package/CHANGELOG.md +46 -0
- package/LICENSE +21 -0
- package/README.md +209 -0
- package/bin/academy +2 -0
- package/conformance/README.md +60 -0
- package/conformance/discovery.test.mjs +140 -0
- package/conformance/envelope.test.mjs +185 -0
- package/conformance/error-codes.test.mjs +125 -0
- package/conformance/harness.mjs +180 -0
- package/conformance/identity.test.mjs +125 -0
- package/docs/integration-guide.md +1026 -0
- package/hooks/hook_runtime.mjs +100 -0
- package/hooks/hooks.json +26 -0
- package/hooks/inject_surface.py +122 -0
- package/hooks/memory_bridge.mjs +120 -0
- package/hooks/memory_store.mjs +66 -0
- package/hooks/register_session.mjs +51 -0
- package/hooks/sync_memory.mjs +27 -0
- package/package.json +41 -0
- package/scripts/agent.mjs +3 -0
- package/scripts/cli/archive.mjs +161 -0
- package/scripts/cli/archived.mjs +82 -0
- package/scripts/cli/args.mjs +282 -0
- package/scripts/cli/codex.mjs +216 -0
- package/scripts/cli/core.mjs +389 -0
- package/scripts/cli/create.mjs +242 -0
- package/scripts/cli/doctor.mjs +203 -0
- package/scripts/cli/eventlog.mjs +129 -0
- package/scripts/cli/events.mjs +80 -0
- package/scripts/cli/hire-headless.mjs +229 -0
- package/scripts/cli/hire-spec.mjs +164 -0
- package/scripts/cli/hire.mjs +92 -0
- package/scripts/cli/inspect.mjs +286 -0
- package/scripts/cli/lifecycle.mjs +296 -0
- package/scripts/cli/main.mjs +102 -0
- package/scripts/cli/migrate.mjs +183 -0
- package/scripts/cli/notes.mjs +104 -0
- package/scripts/cli/rename.mjs +172 -0
- package/scripts/cli/run.mjs +227 -0
- package/scripts/cli/runtime.mjs +47 -0
- package/scripts/cli/scaffold.mjs +332 -0
- package/scripts/cli/sessions.mjs +98 -0
- package/scripts/cli/templates.mjs +104 -0
- package/scripts/cli/yaml.mjs +124 -0
- package/skills/hire/SKILL.md +669 -0
- package/templates/agents/claude-code/knowledge-curator.md +14 -0
- package/templates/agents/codex/knowledge-curator.toml +9 -0
- package/templates/skills/check-in/SKILL.md +122 -0
- package/templates/skills/knowledge-curation/SKILL.md +132 -0
- package/templates/skills/nightly-consolidation/SKILL.md +240 -0
- package/templates/skills/self-update/SKILL.md +121 -0
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "academy-local",
|
|
3
|
+
"description": "Local marketplace for the Academy v3 plugin",
|
|
4
|
+
"owner": {
|
|
5
|
+
"name": "Joe Fernandez"
|
|
6
|
+
},
|
|
7
|
+
"plugins": [
|
|
8
|
+
{
|
|
9
|
+
"name": "academy",
|
|
10
|
+
"source": ".",
|
|
11
|
+
"description": "Academy v3 — portable AI agents."
|
|
12
|
+
}
|
|
13
|
+
]
|
|
14
|
+
}
|
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
This project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
4
|
+
The heading at the top of this file is the version in `package.json`.
|
|
5
|
+
|
|
6
|
+
## 0.4.0-rc.1 — 2026-08-31
|
|
7
|
+
|
|
8
|
+
First release candidate, and the first Academy artifact that can be installed
|
|
9
|
+
rather than cloned.
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- **A published client contract at `contract_version` 1.** Every command
|
|
14
|
+
answers a JSON envelope: `{ contract_version, ok, command, ... }` on stdout
|
|
15
|
+
with exit 0, or `{ contract_version, ok: false, command, error }` on stderr
|
|
16
|
+
with exit 1. Exit status is 0 if and only if `ok` is true.
|
|
17
|
+
- **`doctor`** — the discovery command. It reports the supported contracts, the
|
|
18
|
+
build version, the package root, the agents root, the event log path, the
|
|
19
|
+
published command list, runtime availability, and health counts.
|
|
20
|
+
- **Fifteen published error codes**, each reachable from outside the binary and
|
|
21
|
+
each documented in the integration guide.
|
|
22
|
+
- **An append-only event log**, so a client can follow lifecycle changes it did
|
|
23
|
+
not make.
|
|
24
|
+
- **`docs/integration-guide.md`** — the whole published contract, written for an
|
|
25
|
+
author of a client that drives Academy without reading Academy's source.
|
|
26
|
+
- **A client conformance suite** in `conformance/`. It imports no Academy
|
|
27
|
+
source, asserts only what a client can observe, and runs against any build
|
|
28
|
+
through `ACADEMY_BIN`.
|
|
29
|
+
- **Release tooling.** `scripts/release-check.mjs` is a read-only readiness
|
|
30
|
+
gate, `scripts/publish-tree.mjs` generates the public tree from an allow-list,
|
|
31
|
+
and `.agents/skills/release/SKILL.md` is the release procedure.
|
|
32
|
+
|
|
33
|
+
### Changed
|
|
34
|
+
|
|
35
|
+
- **Academy is client-agnostic.** No identifier in the source names a specific
|
|
36
|
+
client product, with one documented exception: the memory sync bridge in
|
|
37
|
+
`hooks/memory_bridge.mjs`, which is off unless `ACADEMY_MEMORY_BRIDGE=1`.
|
|
38
|
+
- **Package identity is `@joenandez/academy`.** The unscoped `academy` name is
|
|
39
|
+
taken on the registry. The command is still `academy`.
|
|
40
|
+
- **Every path a command reports is absolute and normalised**, so a client can
|
|
41
|
+
key an agent on `dir` across every record for that agent.
|
|
42
|
+
|
|
43
|
+
### Removed
|
|
44
|
+
|
|
45
|
+
- The Subspace-specific framing in `README.md`. Academy is described on its own
|
|
46
|
+
terms, with the memory bridge named as the single exception.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Joe Fernandez
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
# Academy
|
|
2
|
+
|
|
3
|
+
Portable, workspace-aware AI agents, and a published JSON contract that lets a
|
|
4
|
+
client product drive them.
|
|
5
|
+
|
|
6
|
+
An Academy agent is a directory of Markdown, not a framework object. Eight boot
|
|
7
|
+
surfaces describe who the agent is and what it is working on. Academy compiles
|
|
8
|
+
them into one system prompt and launches the agent through Claude Code or Codex
|
|
9
|
+
against whatever project you are in. Every command also answers JSON, so a
|
|
10
|
+
desktop app, a web service, or another CLI can render and manage agents without
|
|
11
|
+
reading Academy's source.
|
|
12
|
+
|
|
13
|
+
**Status: `0.4.0-rc.1`, contract version 1.** Academy is not on the npm registry
|
|
14
|
+
yet. Install it from a checkout, as below.
|
|
15
|
+
|
|
16
|
+
## Requirements
|
|
17
|
+
|
|
18
|
+
- **Node.js 18 or later.** The CLI is pure ESM Node with no npm dependencies.
|
|
19
|
+
- **`helm-tasks`** on your `PATH` — the scheduler Academy registers each agent's
|
|
20
|
+
nightly consolidation job with. This is a hard dependency of the agent
|
|
21
|
+
lifecycle: without it `create`, `hire --spec`, `delete`, `rename`, and
|
|
22
|
+
`archive` fail with `runtime_unavailable`. Read commands are unaffected.
|
|
23
|
+
- **A runtime.** `claude` for Claude Code, `codex` for Codex. You need the one
|
|
24
|
+
you intend to launch. `doctor` reports which are available.
|
|
25
|
+
|
|
26
|
+
## Install
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
git clone https://github.com/joenandez/academy.git
|
|
30
|
+
cd academy
|
|
31
|
+
npm link # makes `academy` available globally
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
There is no build step and no `npm install`. Then confirm the link resolved and
|
|
35
|
+
read the version Academy reports for itself:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
academy doctor --json
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
`doctor` is the discovery command and the version source; there is no
|
|
42
|
+
`academy --version`. Its payload names the supported contract versions, the
|
|
43
|
+
build version, the package root, the agents root, the event log, the published
|
|
44
|
+
command list, runtime availability, and health counts.
|
|
45
|
+
|
|
46
|
+
## Create and run an agent
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
academy create kai # scaffold ~/.academy/agents/kai/
|
|
50
|
+
academy run kai # launch Claude Code against the current project
|
|
51
|
+
academy run kai --agent codex -- exec "say hello"
|
|
52
|
+
academy list
|
|
53
|
+
academy inspect kai --json
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Agents live at `~/.academy/agents/<name>/` by default. Set `AGENTS_ROOT` to put
|
|
57
|
+
them anywhere else; two installs with different roots are fully independent.
|
|
58
|
+
|
|
59
|
+
`academy hire` is the interactive alternative to `create`: it runs a domain
|
|
60
|
+
research flow and writes all eight surfaces for you. `academy hire --spec
|
|
61
|
+
<file> --json` does the same headlessly from a JSON specification.
|
|
62
|
+
|
|
63
|
+
## The eight boot surfaces
|
|
64
|
+
|
|
65
|
+
Each agent directory holds eight editable Markdown files. `academy create` and
|
|
66
|
+
every `academy run` compile them into
|
|
67
|
+
`.academy/generated/academy-system-prompt.md` — roughly 5–6k tokens combined.
|
|
68
|
+
|
|
69
|
+
| Surface | Contains | Soft cap |
|
|
70
|
+
| --- | --- | --- |
|
|
71
|
+
| `identity.md` | Values, character, voice, persona | ~400 |
|
|
72
|
+
| `role.md` | Job, responsibilities, deliverable shape | ~400 |
|
|
73
|
+
| `knowledge.md` | Domain expertise, frameworks, patterns | ~1500–2500 |
|
|
74
|
+
| `goals.md` | Strategic objectives (cap 3) | ~150 |
|
|
75
|
+
| `priorities.md` | Weekly direction (3–5 visible) | ~250 |
|
|
76
|
+
| `threads.md` | Active work pursuits | ~700 |
|
|
77
|
+
| `notes.md` | Micro-steering staging area | ~500 |
|
|
78
|
+
| `dailys.md` | Last 7 working days | ~1000 |
|
|
79
|
+
|
|
80
|
+
`academy tokens <name>` estimates the compiled prompt by surface, and
|
|
81
|
+
`academy budget <name>` reports which surfaces are over their cap.
|
|
82
|
+
|
|
83
|
+
## Skills
|
|
84
|
+
|
|
85
|
+
Skills are Academy's one extension primitive, and the same file shape serves
|
|
86
|
+
both "a competency the agent has" and "how to use this tool". Academy renders
|
|
87
|
+
them for Claude Code under `.claude/skills/` and for Codex under
|
|
88
|
+
`.agents/skills/`, from one source.
|
|
89
|
+
|
|
90
|
+
Four universal skills ship with every agent: `check-in`, `self-update`,
|
|
91
|
+
`nightly-consolidation`, and `knowledge-curation`. Nightly consolidation
|
|
92
|
+
delegates evidence-backed graduation of notes into knowledge to a bounded
|
|
93
|
+
knowledge-curator subagent and writes its report under `dreams/`.
|
|
94
|
+
|
|
95
|
+
## Commands
|
|
96
|
+
|
|
97
|
+
Fourteen commands are contract. `doctor` publishes the list, and a client should
|
|
98
|
+
read it from there rather than hard-coding it.
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
academy doctor [--json] # discovery, health, and the version source
|
|
102
|
+
academy list [--json] # every agent, plus the archived ones
|
|
103
|
+
academy inspect <name> [--json] # one agent
|
|
104
|
+
academy tokens <name> [--json] # compiled prompt size by surface
|
|
105
|
+
academy budget <name> [--json] # surfaces over their soft cap
|
|
106
|
+
academy sessions [--agent <name>] [--json]
|
|
107
|
+
academy events --since <seq> [--logid <id>] [--json]
|
|
108
|
+
academy create <name> [--json]
|
|
109
|
+
academy hire # interactive
|
|
110
|
+
academy hire --spec <file> [--json] # headless
|
|
111
|
+
academy rename <old> <new> [--json]
|
|
112
|
+
academy archive <name> [--json]
|
|
113
|
+
academy unarchive <name> [--json]
|
|
114
|
+
academy delete <name> [--json]
|
|
115
|
+
academy migrate [--dry-run] [--json]
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
`run`, `nightly`, `notes`, `clean`, and `root` also work and are deliberately
|
|
119
|
+
not contract: `run` spawns with inherited stdio and can never emit an envelope,
|
|
120
|
+
`nightly` is called by the scheduler, and the rest are conveniences for a
|
|
121
|
+
person. See §6 of the integration guide.
|
|
122
|
+
|
|
123
|
+
## For client authors
|
|
124
|
+
|
|
125
|
+
Everything a client may depend on is in
|
|
126
|
+
[`docs/integration-guide.md`](docs/integration-guide.md): the response envelope,
|
|
127
|
+
the exit rule, the compatibility floor, all fourteen published commands, the
|
|
128
|
+
fifteen error codes, the event log, the agent directory layout, and an explicit
|
|
129
|
+
list of what is **not** contract. Anything not in that document is internal and
|
|
130
|
+
may change in any release.
|
|
131
|
+
|
|
132
|
+
The shape in one line: success is JSON on stdout with exit 0, failure is JSON on
|
|
133
|
+
stderr with exit 1, and exit status is 0 if and only if `ok` is true.
|
|
134
|
+
|
|
135
|
+
```json
|
|
136
|
+
{ "contract_version": 1, "ok": true, "command": "doctor", "version": "0.4.0-rc.1" }
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
Academy ships a conformance suite for client authors. It imports no Academy
|
|
140
|
+
source and asserts only what a client can observe:
|
|
141
|
+
|
|
142
|
+
```bash
|
|
143
|
+
node --test conformance/*.test.mjs # this build
|
|
144
|
+
ACADEMY_BIN=/path/to/academy node --test conformance/*.test.mjs # any build
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
Every test builds its own throwaway install with a fresh temporary `HOME` and
|
|
148
|
+
`AGENTS_ROOT`, so it can drive the full lifecycle without reaching any agent on
|
|
149
|
+
the machine running it. `conformance/README.md` explains each file.
|
|
150
|
+
|
|
151
|
+
## Academy is client-agnostic, with one documented exception
|
|
152
|
+
|
|
153
|
+
No identifier in Academy's source names a specific client product, with exactly
|
|
154
|
+
one deliberate exception: **the memory sync bridge**,
|
|
155
|
+
`hooks/memory_bridge.mjs`. It copies observation memory from one specific host
|
|
156
|
+
product into an agent's `memory/observations/` for nightly consolidation, and it
|
|
157
|
+
is **off unless `ACADEMY_MEMORY_BRIDGE=1` is set**. Unset, a scaffolded agent
|
|
158
|
+
names no client at all — no client-named environment key is read, no
|
|
159
|
+
client-named tool permission is written, and no client-named prose appears in
|
|
160
|
+
any skill.
|
|
161
|
+
|
|
162
|
+
Treat it as an exception, not as a pattern. It is not a plugin point and not a
|
|
163
|
+
template for a second client. Integration guide §12 has the full statement.
|
|
164
|
+
|
|
165
|
+
## Layout
|
|
166
|
+
|
|
167
|
+
```
|
|
168
|
+
academy/
|
|
169
|
+
├── .claude-plugin/ # plugin + marketplace manifests
|
|
170
|
+
├── bin/academy # CLI entry
|
|
171
|
+
├── scripts/cli/ # CLI implementation
|
|
172
|
+
├── scripts/release-check.mjs # read-only release readiness gate
|
|
173
|
+
├── scripts/publish-tree.mjs # allow-list publish tree generator
|
|
174
|
+
├── skills/hire/SKILL.md # the hire skill
|
|
175
|
+
├── templates/skills/ # universal skills copied into every agent
|
|
176
|
+
├── conformance/ # client conformance suite
|
|
177
|
+
├── docs/integration-guide.md # the published client contract
|
|
178
|
+
└── hooks/
|
|
179
|
+
├── hooks.json # lifecycle hook config
|
|
180
|
+
└── memory_bridge.mjs # the one client-specific file, off by default
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
## Design notes
|
|
184
|
+
|
|
185
|
+
- **No adapters.** External CLIs are used as-is. A missing capability is a
|
|
186
|
+
feature request against that project, not glue here.
|
|
187
|
+
- **Skills are the unifying primitive.** One file shape for competencies and for
|
|
188
|
+
tool documentation, rendered for both runtimes from one source.
|
|
189
|
+
- **Generated system prompt.** The eight surfaces stay editable Markdown.
|
|
190
|
+
Compilation at launch is local file IO only; the slow work stays nightly.
|
|
191
|
+
- **Lifecycle hooks only.** Hooks are for runtime side effects, not for startup
|
|
192
|
+
context transport.
|
|
193
|
+
- **Portable plugin layout.** Each agent has a `.claude-plugin/` symlink to this
|
|
194
|
+
package, so running Claude Code inside the agent directory keeps Academy's
|
|
195
|
+
lifecycle hooks available.
|
|
196
|
+
|
|
197
|
+
## Releasing
|
|
198
|
+
|
|
199
|
+
`node scripts/release-check.mjs` is the read-only readiness gate. It mutates
|
|
200
|
+
nothing, packs into a temporary directory, asserts the packed file list against
|
|
201
|
+
the declared published surface, runs the conformance suite against the unpacked
|
|
202
|
+
tarball, and proves `git status --porcelain` is byte-identical before and after.
|
|
203
|
+
`.agents/skills/release/SKILL.md` is the release procedure and calls it from
|
|
204
|
+
every mode.
|
|
205
|
+
|
|
206
|
+
## Licence
|
|
207
|
+
|
|
208
|
+
MIT. See [`LICENSE`](LICENSE). Changes are recorded in
|
|
209
|
+
[`CHANGELOG.md`](CHANGELOG.md).
|
package/bin/academy
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# Academy client conformance suite
|
|
2
|
+
|
|
3
|
+
This suite checks that an Academy build implements the client contract. It is
|
|
4
|
+
written for authors of clients that drive Academy, not for Academy's own
|
|
5
|
+
developers: it never imports Academy source and asserts only what a client can
|
|
6
|
+
observe — the response envelope, the exit status, and the published error codes.
|
|
7
|
+
|
|
8
|
+
## Run it
|
|
9
|
+
|
|
10
|
+
```sh
|
|
11
|
+
# against the build in this package
|
|
12
|
+
node --test conformance/*.test.mjs
|
|
13
|
+
|
|
14
|
+
# against any other build
|
|
15
|
+
ACADEMY_BIN=/usr/local/bin/academy node --test conformance/*.test.mjs
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
`ACADEMY_BIN` is the path of the Academy launcher to test. Unset, the suite
|
|
19
|
+
drives the `bin/academy` beside this directory. Point it at a global install, an
|
|
20
|
+
unpacked tarball, or a source checkout — the assertions are the same.
|
|
21
|
+
|
|
22
|
+
Requirements: Node 18 or later, a POSIX shell, and nothing else. No packages are
|
|
23
|
+
installed and no test framework is used.
|
|
24
|
+
|
|
25
|
+
## What it asserts
|
|
26
|
+
|
|
27
|
+
| File | Asserts |
|
|
28
|
+
| --- | --- |
|
|
29
|
+
| `discovery.test.mjs` | `doctor --json` — the published command list, the payload keys, the version rule, and the one state answered `ok:false` |
|
|
30
|
+
| `identity.test.mjs` | Every agent-addressed command refuses an out-of-root agents root and an out-of-root agent directory, and writes nothing outside the root |
|
|
31
|
+
| `error-codes.test.mjs` | An invocation for each of the fifteen published error codes |
|
|
32
|
+
| `envelope.test.mjs` | The envelope and the exit rule across every published command, on success and on failure |
|
|
33
|
+
|
|
34
|
+
## Safety
|
|
35
|
+
|
|
36
|
+
Every test builds its own throwaway install: a fresh temporary `HOME`, a fresh
|
|
37
|
+
temporary `AGENTS_ROOT`, and a `PATH` built from nothing. Values already
|
|
38
|
+
exported into your shell are **not** inherited by the build under test. The
|
|
39
|
+
suite drives the full lifecycle, including `delete`, `archive`, `rename` and
|
|
40
|
+
`migrate`, and can reach no agent outside its own temporary directories.
|
|
41
|
+
|
|
42
|
+
## Stubs
|
|
43
|
+
|
|
44
|
+
Two executables Academy uses are stubbed, because neither is part of the
|
|
45
|
+
response contract and a client author must not need either installed:
|
|
46
|
+
|
|
47
|
+
- `helm-tasks` — the scheduler Academy registers each agent's nightly job with.
|
|
48
|
+
Academy resolves it from `PATH`, or from `ACADEMY_HELM_TASKS_BIN`. Without it,
|
|
49
|
+
`create`, `delete`, `rename` and `archive` answer `runtime_unavailable`.
|
|
50
|
+
- `claude` — the runtime `hire --spec` drives. Academy resolves it from `PATH`,
|
|
51
|
+
or from `ACADEMY_CLAUDE_BIN`.
|
|
52
|
+
|
|
53
|
+
The scheduler stub is also the only way to reach `unschedule_failed` and
|
|
54
|
+
`unschedule_failed_restore_blocked` from outside the binary.
|
|
55
|
+
|
|
56
|
+
## Reading a failure
|
|
57
|
+
|
|
58
|
+
Each assertion prints the exit status and both streams of the invocation that
|
|
59
|
+
produced it, so a failure names the command, the envelope it returned, and the
|
|
60
|
+
field that disagreed.
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
// Discovery — what a client learns before it renders anything.
|
|
2
|
+
//
|
|
3
|
+
// `doctor --json` is the one call a client may make against an Academy it knows
|
|
4
|
+
// nothing about. Everything else in this suite is driven from what it answers.
|
|
5
|
+
|
|
6
|
+
import assert from 'node:assert/strict';
|
|
7
|
+
import { existsSync, rmSync, symlinkSync } from 'node:fs';
|
|
8
|
+
import { spawnSync } from 'node:child_process';
|
|
9
|
+
import { join } from 'node:path';
|
|
10
|
+
import test from 'node:test';
|
|
11
|
+
import { academy, assertFailure, assertSuccess, createHost, hireAgent } from './harness.mjs';
|
|
12
|
+
|
|
13
|
+
// The frozen answer at contract_version 1, in order. A client may key on the
|
|
14
|
+
// order, so this is asserted by equality rather than by membership.
|
|
15
|
+
const PUBLISHED_COMMANDS = [
|
|
16
|
+
'doctor',
|
|
17
|
+
'list',
|
|
18
|
+
'inspect',
|
|
19
|
+
'tokens',
|
|
20
|
+
'budget',
|
|
21
|
+
'sessions',
|
|
22
|
+
'events',
|
|
23
|
+
'create',
|
|
24
|
+
'hire',
|
|
25
|
+
'rename',
|
|
26
|
+
'archive',
|
|
27
|
+
'unarchive',
|
|
28
|
+
'delete',
|
|
29
|
+
'migrate',
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
// Commands an Academy build may implement but must not publish. A client that
|
|
33
|
+
// found one here would build on a surface with no contract behind it.
|
|
34
|
+
const UNPUBLISHED_COMMANDS = ['notes', 'nightly', 'clean', 'root', 'run', 'destroy'];
|
|
35
|
+
|
|
36
|
+
// The payload keys, in order, after the three envelope keys.
|
|
37
|
+
const DOCTOR_PAYLOAD_KEYS = [
|
|
38
|
+
'contracts',
|
|
39
|
+
'version',
|
|
40
|
+
'packageRoot',
|
|
41
|
+
'agentsRoot',
|
|
42
|
+
'eventLog',
|
|
43
|
+
'commands',
|
|
44
|
+
'runtimes',
|
|
45
|
+
'errors',
|
|
46
|
+
];
|
|
47
|
+
|
|
48
|
+
// `errors[]` is a health channel, not the error channel: it names a degraded
|
|
49
|
+
// part of an install and how much of it there is. None of the fifteen failure
|
|
50
|
+
// codes may appear here.
|
|
51
|
+
const HEALTH_CODES = ['unowned_agents', 'invalid_runtime_agents', 'unattributable_sessions'];
|
|
52
|
+
|
|
53
|
+
test('doctor publishes the whole command set, in order', () => {
|
|
54
|
+
const host = createHost();
|
|
55
|
+
|
|
56
|
+
const payload = assertSuccess(academy(host, ['doctor', '--json']), 'doctor');
|
|
57
|
+
|
|
58
|
+
assert.deepEqual(payload.commands, PUBLISHED_COMMANDS);
|
|
59
|
+
for (const command of UNPUBLISHED_COMMANDS) {
|
|
60
|
+
assert.equal(payload.commands.includes(command), false, `${command} must not be published`);
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test('doctor answers with exactly the published payload keys', () => {
|
|
65
|
+
const host = createHost();
|
|
66
|
+
|
|
67
|
+
const payload = assertSuccess(academy(host, ['doctor', '--json']), 'doctor');
|
|
68
|
+
|
|
69
|
+
assert.deepEqual(Object.keys(payload), [
|
|
70
|
+
'contract_version',
|
|
71
|
+
'ok',
|
|
72
|
+
'command',
|
|
73
|
+
...DOCTOR_PAYLOAD_KEYS,
|
|
74
|
+
]);
|
|
75
|
+
assert.deepEqual(payload.contracts, [1]);
|
|
76
|
+
assert.equal(typeof payload.packageRoot, 'string');
|
|
77
|
+
assert.equal(typeof payload.agentsRoot, 'string');
|
|
78
|
+
assert.equal(typeof payload.eventLog, 'string');
|
|
79
|
+
assert.deepEqual(Object.keys(payload.runtimes).sort(), ['claude_code', 'codex']);
|
|
80
|
+
for (const runtime of Object.values(payload.runtimes)) {
|
|
81
|
+
assert.equal(typeof runtime.available, 'boolean');
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
// Two builds with different capabilities must never report the same version. A
|
|
86
|
+
// checkout names its own commit as semver build metadata; an install has no
|
|
87
|
+
// checkout to name and reports the published version bare.
|
|
88
|
+
test('doctor reports build metadata only for a checkout', () => {
|
|
89
|
+
const host = createHost();
|
|
90
|
+
|
|
91
|
+
const payload = assertSuccess(academy(host, ['doctor', '--json']), 'doctor');
|
|
92
|
+
|
|
93
|
+
assert.equal(typeof payload.version, 'string');
|
|
94
|
+
assert.notEqual(payload.version, '');
|
|
95
|
+
if (describesItsOwnCheckout(host, payload.packageRoot)) {
|
|
96
|
+
assert.match(payload.version, /^[^+]+\+[0-9A-Za-z-]+$/);
|
|
97
|
+
} else {
|
|
98
|
+
assert.equal(payload.version.includes('+'), false);
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
function describesItsOwnCheckout(host, packageRoot) {
|
|
103
|
+
if (!existsSync(join(packageRoot, '.git'))) return false;
|
|
104
|
+
return spawnSync('git', ['--version'], { env: host.env, encoding: 'utf8' }).status === 0;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// A degraded component is not a failure. A client calls doctor before it renders
|
|
108
|
+
// anything, so an install with repairable faults must still be usable.
|
|
109
|
+
test('a degraded install stays ok:true and exits 0', () => {
|
|
110
|
+
const host = createHost();
|
|
111
|
+
hireAgent(host, 'kai');
|
|
112
|
+
rmSync(join(host.agentsRoot, 'kai', '.academy-agent.json'));
|
|
113
|
+
|
|
114
|
+
const result = academy(host, ['doctor', '--json']);
|
|
115
|
+
|
|
116
|
+
const payload = assertSuccess(result, 'doctor');
|
|
117
|
+
assert.equal(result.status, 0);
|
|
118
|
+
assert.deepEqual(payload.errors, [{ code: 'unowned_agents', count: 1 }]);
|
|
119
|
+
for (const entry of payload.errors) assert.equal(HEALTH_CODES.includes(entry.code), true);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
// The one state doctor answers with ok:false: every agent-addressed command
|
|
123
|
+
// would fail on this root, so a client told ok:true would render an interface
|
|
124
|
+
// whose first call fails. The payload still ships, beside the error.
|
|
125
|
+
test('doctor is ok:false only when the agents root fails its audit', () => {
|
|
126
|
+
const host = createHost();
|
|
127
|
+
symlinkSync(host.outside, host.agentsRoot);
|
|
128
|
+
|
|
129
|
+
const result = academy(host, ['doctor', '--json']);
|
|
130
|
+
|
|
131
|
+
const envelope = assertFailure(result, 'doctor', 'unsafe_agent_path');
|
|
132
|
+
assert.deepEqual(Object.keys(envelope), [
|
|
133
|
+
'contract_version',
|
|
134
|
+
'ok',
|
|
135
|
+
'command',
|
|
136
|
+
...DOCTOR_PAYLOAD_KEYS,
|
|
137
|
+
'error',
|
|
138
|
+
]);
|
|
139
|
+
assert.deepEqual(envelope.commands, PUBLISHED_COMMANDS);
|
|
140
|
+
});
|