@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,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
+ }
@@ -0,0 +1,6 @@
1
+ {
2
+ "name": "academy",
3
+ "description": "Academy v3 — portable AI agents.",
4
+ "version": "0.4.0-rc.1",
5
+ "hooks": "./hooks/hooks.json"
6
+ }
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,2 @@
1
+ #!/usr/bin/env node
2
+ import('../scripts/agent.mjs');
@@ -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
+ });