@joenandez/academy 0.4.0 → 0.5.12

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 (121) hide show
  1. package/.claude-plugin/marketplace.json +5 -0
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/CHANGELOG.md +390 -0
  4. package/README.md +67 -122
  5. package/hooks/backstory/.claude-plugin/plugin.json +11 -0
  6. package/hooks/backstory/SOURCE +2 -0
  7. package/hooks/backstory/bin/backstory +9 -0
  8. package/hooks/backstory/hooks/hooks.json +45 -0
  9. package/hooks/backstory/scripts/knowledge.mjs +87 -0
  10. package/hooks/backstory/scripts/memory.mjs +685 -0
  11. package/hooks/backstory/scripts/tool-tick.mjs +1 -0
  12. package/hooks/backstory/skills/capture/SKILL.md +71 -0
  13. package/hooks/backstory/skills/capture/references/knowledge-capture-input.json +16 -0
  14. package/hooks/backstory/skills/capture/references/tagging-policy.md +29 -0
  15. package/hooks/backstory/skills/memory/SKILL.md +70 -0
  16. package/hooks/backstory/skills/setup/SKILL.md +85 -0
  17. package/hooks/context_metrics.mjs +67 -0
  18. package/hooks/hook_runtime.mjs +43 -2
  19. package/hooks/memory_bridge.mjs +286 -52
  20. package/hooks/memory_store.mjs +78 -26
  21. package/hooks/register_session.mjs +76 -14
  22. package/hooks/roque/.claude-plugin/plugin.json +6 -0
  23. package/hooks/roque/LICENSE +21 -0
  24. package/hooks/roque/SOURCE +1 -0
  25. package/hooks/roque/hooks.json +38 -0
  26. package/hooks/roque/package.json +3 -0
  27. package/hooks/roque/roque.js +1690 -0
  28. package/hooks/sync_memory.mjs +11 -7
  29. package/package.json +7 -3
  30. package/scripts/cli/agent-gate.mjs +70 -0
  31. package/scripts/cli/agent-id.mjs +37 -0
  32. package/scripts/cli/archive.mjs +39 -38
  33. package/scripts/cli/archived.mjs +4 -2
  34. package/scripts/cli/args-kinds.mjs +33 -0
  35. package/scripts/cli/args-notes.mjs +42 -0
  36. package/scripts/cli/args-owner.mjs +37 -0
  37. package/scripts/cli/args-run.mjs +38 -0
  38. package/scripts/cli/args.mjs +47 -104
  39. package/scripts/cli/beads.mjs +57 -0
  40. package/scripts/cli/codex.mjs +66 -12
  41. package/scripts/cli/core.mjs +117 -41
  42. package/scripts/cli/create.mjs +224 -76
  43. package/scripts/cli/curator-feed-codex.mjs +81 -0
  44. package/scripts/cli/curator-feed-observations.mjs +97 -0
  45. package/scripts/cli/curator-feed-scope.mjs +124 -0
  46. package/scripts/cli/curator-feed-sources.mjs +172 -0
  47. package/scripts/cli/curator-feed.mjs +318 -0
  48. package/scripts/cli/default-name.mjs +73 -0
  49. package/scripts/cli/doctor.mjs +67 -2
  50. package/scripts/cli/eventlog.mjs +6 -2
  51. package/scripts/cli/exit-code.mjs +9 -0
  52. package/scripts/cli/first-run.mjs +115 -0
  53. package/scripts/cli/heartbeat.mjs +135 -0
  54. package/scripts/cli/helm-skip.mjs +20 -0
  55. package/scripts/cli/hire-headless.mjs +37 -22
  56. package/scripts/cli/hire.mjs +8 -2
  57. package/scripts/cli/inspect.mjs +36 -18
  58. package/scripts/cli/lifecycle.mjs +95 -49
  59. package/scripts/cli/main.mjs +32 -2
  60. package/scripts/cli/memory-env.mjs +124 -0
  61. package/scripts/cli/migrate.mjs +13 -7
  62. package/scripts/cli/next-steps.mjs +13 -0
  63. package/scripts/cli/nightly-launch.mjs +118 -0
  64. package/scripts/cli/nightly.mjs +228 -0
  65. package/scripts/cli/notes.mjs +58 -8
  66. package/scripts/cli/owner-config.mjs +181 -0
  67. package/scripts/cli/owner-jobs.mjs +201 -0
  68. package/scripts/cli/prompt-append.mjs +135 -0
  69. package/scripts/cli/rename.mjs +90 -109
  70. package/scripts/cli/run-context.mjs +50 -0
  71. package/scripts/cli/run.mjs +236 -79
  72. package/scripts/cli/scaffold.mjs +119 -126
  73. package/scripts/cli/schedule-errors.mjs +70 -0
  74. package/scripts/cli/sessions.mjs +10 -3
  75. package/scripts/cli/skills-check.mjs +147 -0
  76. package/scripts/cli/skills.mjs +170 -0
  77. package/scripts/cli/start.mjs +201 -0
  78. package/scripts/cli/templates.mjs +119 -45
  79. package/scripts/cli/tldr.mjs +184 -0
  80. package/scripts/cli/toml-string.mjs +76 -0
  81. package/scripts/cli/user-work-view.mjs +48 -0
  82. package/scripts/cli/writable-dirs.mjs +23 -0
  83. package/scripts/cli/yaml.mjs +1 -1
  84. package/skills/hire/SKILL.md +112 -100
  85. package/skills/hire/references/owner.md +68 -0
  86. package/templates/agents/claude-code/knowledge-curator.md +5 -2
  87. package/templates/agents/codex/knowledge-curator.toml +2 -1
  88. package/templates/operating-model/base.md +78 -0
  89. package/templates/operating-model/owner.md +11 -0
  90. package/templates/operating-model/principal.md +9 -0
  91. package/templates/operating-model/specialist.md +9 -0
  92. package/templates/packs/owner/craft.md +32 -0
  93. package/templates/packs/owner/identity.md +13 -0
  94. package/templates/packs/owner/mandate.md +26 -0
  95. package/templates/packs/owner/owner/SKILL.md +118 -0
  96. package/templates/packs/owner/owner-feedback/SKILL.md +49 -0
  97. package/templates/packs/owner/owner-heartbeat/SKILL.md +35 -0
  98. package/templates/packs/owner/owner-nightly/SKILL.md +114 -0
  99. package/templates/packs/owner/owner-review/SKILL.md +70 -0
  100. package/templates/packs/owner/owner-work/SKILL.md +53 -0
  101. package/templates/packs/principal/craft.md +27 -0
  102. package/templates/packs/principal/identity.md +13 -0
  103. package/templates/packs/principal/mandate.md +23 -0
  104. package/templates/packs/principal/principal-brief/SKILL.md +44 -0
  105. package/templates/packs/principal/principal-intro/SKILL.md +95 -0
  106. package/templates/packs/principal/principal-nightly/SKILL.md +152 -0
  107. package/templates/skills/check-in/SKILL.md +34 -24
  108. package/templates/skills/knowledge-curation/SKILL.md +89 -48
  109. package/templates/skills/local-rules/SKILL.md +86 -0
  110. package/templates/skills/nightly-consolidation/SKILL.md +131 -100
  111. package/templates/skills/skill-method/SKILL.md +79 -0
  112. package/templates/skills/user-nightly/SKILL.md +143 -0
  113. package/templates/skills/work-graph/SKILL.md +75 -0
  114. package/conformance/README.md +0 -60
  115. package/conformance/discovery.test.mjs +0 -140
  116. package/conformance/envelope.test.mjs +0 -185
  117. package/conformance/error-codes.test.mjs +0 -125
  118. package/conformance/harness.mjs +0 -180
  119. package/conformance/identity.test.mjs +0 -125
  120. package/docs/integration-guide.md +0 -1042
  121. package/templates/skills/self-update/SKILL.md +0 -121
@@ -0,0 +1,143 @@
1
+ ---
2
+ name: user-nightly
3
+ layer: craft
4
+ description: Use only in the user-nightly run of the curator agent, to read what happened across all projects and keep user.md and user-work.md true for every agent.
5
+ ---
6
+
7
+ # User Nightly
8
+
9
+ {{agent_name}} is the curator. This run keeps the user's shared context true:
10
+ `user.md` (who the user is) and `user-work.md` (what the user works on, and
11
+ why). Every agent except a Monitor loads `user.md` at launch. Every agent
12
+ loads its own share of `user-work.md` at launch: the preamble and the sections
13
+ that name the agent. Only you write `user-work.md`, and only you edit `user.md`
14
+ beyond appending `[proposed]` lines.
15
+
16
+ ## Paths
17
+
18
+ - `user.md`: `{{user_path}}`
19
+ - `user-work.md`: `{{user_work_path}}`
20
+ - Reports: `{{dreams_dir}}/user/YYYY-MM-DD.md` (a subfolder your agent
21
+ nightly never reads)
22
+
23
+ ## Inputs
24
+
25
+ 1. **Watermark.** The date of your newest report in the reports folder. Read
26
+ at most 7 days. A missing report leaves the watermark old, so the next run
27
+ uses the 7-day window. No offsets.
28
+ 2. **Feed.** Run
29
+ `node "$(academy root)/scripts/cli/curator-feed.mjs" --since <watermark> --all --max-tokens 500000 --out {{dreams_dir}}/user-feed/<today>.md`.
30
+ Read it with Read offset and limit. If the script fails or a source is
31
+ `error`, sweep that source yourself. Look up with Subspace memory search,
32
+ then a transcript.
33
+ 3. **Beads.** Workstream changes since the watermark:
34
+ `bd list --type workstream --updated-after <watermark> --json`, and the
35
+ `ws:proposed` ones: `bd list --type workstream --label ws:proposed --json`.
36
+ 4. **Proposed facts.** The `[proposed]` lines at the end of `user.md`.
37
+ 5. **Explicit corrections.** Workstream notes that start `User said`, and the
38
+ user's own statements in the feed.
39
+
40
+ ## Run In The Foreground
41
+
42
+ A scheduled run is one print-mode turn. When it ends, unfinished work is lost.
43
+ Wait for every foreground subagent. Never use `run_in_background`, a
44
+ background task, or a monitor. Read the feed in full; look up only what it
45
+ cannot answer.
46
+
47
+ ## Steps
48
+
49
+ 1. Map the activity to Workstreams. Link or propose through `work-graph`. Find
50
+ gaps and duplicates.
51
+ 2. Resolve each `[proposed]` line: confirm it into the right section of
52
+ `user.md`, merge it with a duplicate, or remove it. A line about one agent
53
+ or one project, not the user, goes back to that agent with
54
+ `academy notes add <agent> "<line>"`. `<line>` is the text after the
55
+ `- [proposed] <stamp> <agent>:` prefix. Only when that command succeeds,
56
+ remove the line from `user.md`.
57
+ 3. Revise goals, priorities, and focus. **Explicit statements first.** An
58
+ explicit decision stands until the user changes it. A busy day is not a new
59
+ priority.
60
+ 4. **Drift is a question.** When attention moves away from a stated priority,
61
+ name it (aligned, possible drift, intentional shift, or exploration) and put
62
+ it under Open questions. Do not reorder priorities from activity alone. Do
63
+ not activate a Workstream on inference alone.
64
+ 5. Map each section of `user-work.md` to the agents that need it. Use the
65
+ mandates and the Beads `owner:` labels. Correct any name that is not in
66
+ `academy list --json`.
67
+ 6. Write `user.md` and `user-work.md`. Run `academy budget {{agent_name}} --json`
68
+ and read the `user` row. For each agent that `user-work.md` names, run
69
+ `academy budget <agent> --json` and read its `user-work` row. That row counts
70
+ the agent's share. Over cap: merge and remove finished or old detail first,
71
+ never an explicit user decision.
72
+ 7. Write the report.
73
+
74
+ ## File Shapes
75
+
76
+ `user.md`: one fact per line, in these `##` sections, in order: `## Identity`,
77
+ `## Preferences`, `## Feed` (last 7 days; promote or drop each item weekly),
78
+ `## Scoped rules`, `## Projects` (the lead-agent line for each project and a
79
+ pointer to `user-work.md`), `## Proposed`. No per-tier caps.
80
+ Keep `## Proposed` as the last section, because agents append below it. When you first restructure
81
+ a `user.md` that has the older sections, its scoped lines move to
82
+ `## Scoped rules` unchanged.
83
+
84
+ `user-work.md` is a preamble and sections. The preamble is every line before
85
+ the first `##`. It holds only the `Private:` header line, the title line
86
+ `# Work - <user>`, and an `Updated by the curator, <date time>` line. Every
87
+ agent loads it, so put no work in it. Each `##` section has one `agents:` line
88
+ right after its heading:
89
+
90
+ ```
91
+ ## <id> <title> (priority 2 of 4)
92
+ agents: <names>
93
+ ```
94
+
95
+ - `<names>` are agent names from `academy list --json`, comma-separated, or
96
+ `none`: write `agents: none` when no agent needs the section. Name an agent
97
+ only when its work needs it.
98
+ - A section without an `agents:` line goes to no agent. An agent that no
99
+ section names gets only the preamble.
100
+ - The sections, in order: `## Goal: <goal>` (each with a measure or the word
101
+ qualitative, and `(explicit)` or `(inferred; <why>)`), one
102
+ `## <id> <title> (priority N of 4)` per ranked Workstream, in priority order
103
+ and with a reason, `## <id> <title> (not ranked)` for the others, and
104
+ `## Open questions`.
105
+ - A rule that applies to every agent belongs in `user.md` under Preferences,
106
+ not in `user-work.md`.
107
+
108
+ ## Report
109
+
110
+ Write the report in the reports folder: sources read and gaps, changes to each
111
+ file, drift findings, questions asked. Add a `## Feed` line: the script's JSON
112
+ fields and any fallback.
113
+
114
+ Write the report before you send any email. If the write is refused or fails,
115
+ retry it once (with Bash if Write was refused). If it still cannot be written,
116
+ do not link it.
117
+
118
+ Do not end your turn until the report is written or its failure is stated.
119
+ End with one line: what changed, or that nothing did, or that the report
120
+ failed.
121
+
122
+ ## Email The User Every Run
123
+
124
+ Send one email every run, quiet nights included. The user expects it; a
125
+ missing email tells them the run failed. Use tldr messaging:
126
+ `tldr-agents messaging agent send --to user --channel email --subject "<subject>" --reason "<why now>" --body-file <file>`.
127
+
128
+ - Something material changed: a new or changed priority, a Workstream proposed
129
+ for confirmation, a drift question, a removed fact, or
130
+ each new or changed assignment of a section to an agent (kestrel → ac-aiw).
131
+ Lead with what changed and what you need.
132
+ - A quiet night: say so in the subject. In the body, list what you updated,
133
+ if anything, in a few short lines, or say that nothing changed.
134
+
135
+ Link the report only when it was written. If it failed, the email says the
136
+ report failed and why. A reply resumes this agent; treat it as an explicit
137
+ correction.
138
+
139
+ ## Do Not
140
+
141
+ - Edit another agent's home or `work.md`, except the one `academy notes add`
142
+ append in step 2.
143
+ - Write a fact from one noisy day, a mood, or a secret.
@@ -0,0 +1,75 @@
1
+ ---
2
+ name: work-graph
3
+ layer: craft
4
+ description: Use when work you see or do may belong to a Workstream - before you start a sustained effort, when the user decides a priority or pause, or when you must find, link, propose, or update a Workstream in Beads.
5
+ ---
6
+
7
+ # Work Graph
8
+
9
+ A Workstream is a durable body of purposeful work. It outlives sessions, epics,
10
+ and handoffs. It lives in Beads, in the one user database that `BEADS_DIR`
11
+ points to, next to the tasks that advance it. Call `bd` with `--json`.
12
+
13
+ A Workstream is not an epic (one bounded implementation) and not a Topic (a
14
+ conversation). Goals and priorities are not Beads items; they live in `work.md`
15
+ and `user-work.md`, which name Workstreams by Beads id.
16
+
17
+ ## Procedure
18
+
19
+ 1. **Identify.** Does this effort survive a session and serve a purpose? If it
20
+ is bounded execution work, it is a task, not a Workstream.
21
+ 2. **Match first.** List the Workstream titles with
22
+ `bd list --type workstream --json`. Open the full `user-work.md` only when
23
+ you need more. Then run `bd search "<two or three key words>" --json` and try a synonym. Run
24
+ `bd find-duplicates --json` when the area looks crowded. Search closed items
25
+ too: a `rejected` Workstream blocks a new proposal from the same evidence.
26
+ 3. **Link when one matches.** Connect the task or epic to the Workstream. This
27
+ link does not block: `bd dep add <workstream-id> <task-id> --type tracks --json`.
28
+ 4. **Propose when none matches.**
29
+ - `bd create "<title>" --type workstream --description "<purpose, intended outcome, evidence>" --labels owner:<who is accountable> --json`
30
+ - `bd set-state <id> ws=proposed --reason "<evidence>" --json`
31
+
32
+ Use `owner:{{agent_id}}` when you will drive it, else `owner:user`.
33
+ 5. **Update.** Add evidence with `bd update <id> --append-notes "<what changed, source>" --json`.
34
+ Change state with `bd set-state <id> ws=<state> --reason "<why>" --json`.
35
+ Label a task you create for a Workstream `owner:{{agent_id}}`.
36
+
37
+ ## State Rules
38
+
39
+ States: `proposed`, `active`, `paused`, `done`, `rejected`. Close a `done` or
40
+ `rejected` Workstream with `bd close <id> --reason "<evidence>" --json`.
41
+
42
+ - Any agent may propose a Workstream, add notes, and add links.
43
+ - A Workstream becomes `active` only when the user confirms it, or when the
44
+ user's own action shows it ("I'm implementing X this week"). Quote those
45
+ words in the reason.
46
+ - Never activate on inference alone: not on activity, not on mentions, not on
47
+ your own judgment. Ask the user instead.
48
+ - An Owner may make a Workstream `active` inside its mandate. It reports each
49
+ new one in its daily message.
50
+ - Pause or finish a Workstream on the user's word. An Owner may do it inside
51
+ its mandate.
52
+ - A `rejected` Workstream keeps a note with the reason. Do not propose it
53
+ again from the same evidence.
54
+ - A user correction ("not a priority", "pause it", "wrong Workstream") goes at
55
+ once on the Workstream as a note that starts `User said (<date>):`. Act on
56
+ it if it is a state change. The nightly gives it precedence.
57
+
58
+ ## Duplicates
59
+
60
+ Related Workstreams with different outcomes or owners are not duplicates. Close
61
+ a real duplicate and keep the history: `bd duplicate <id> --of <canonical-id> --json`
62
+ or `bd supersede <id> --with <new-id> --json`.
63
+
64
+ ## Keep Workstreams Out Of The Ready Queue
65
+
66
+ The ready query lists open Workstreams as if they were tasks. Every ready query
67
+ adds `--exclude-type workstream`, for example
68
+ `bd ready --json --exclude-type workstream`. Never claim or work a Workstream
69
+ as a task.
70
+
71
+ ## Do Not
72
+
73
+ - Edit `user-work.md`. Only the curator's user-nightly writes it.
74
+ - Rewrite `work.md` mid-session unless the user asks. Your nightly consolidates it.
75
+ - Open a Workstream for a one-off task.
@@ -1,60 +0,0 @@
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.
@@ -1,140 +0,0 @@
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
- });
@@ -1,185 +0,0 @@
1
- // The envelope, swept across every published command.
2
- //
3
- // success stdout, exit 0 { contract_version, ok: true, command, ...payload }
4
- // failure stderr, exit 1 { contract_version, ok: false, command, error }
5
- //
6
- // Exit status is 0 if and only if `ok` is true. The table is checked against
7
- // the command list `doctor` publishes, so a build that publishes a command this
8
- // suite has no case for fails here rather than going unchecked.
9
-
10
- import assert from 'node:assert/strict';
11
- import { symlinkSync, writeFileSync } from 'node:fs';
12
- import { join } from 'node:path';
13
- import test from 'node:test';
14
- import {
15
- academy,
16
- assertFailure,
17
- assertSuccess,
18
- createHost,
19
- hireAgent,
20
- publishedCommands,
21
- } from './harness.mjs';
22
-
23
- function agentHost() {
24
- const host = createHost();
25
- hireAgent(host, 'kai');
26
- return host;
27
- }
28
-
29
- function archivedHost() {
30
- const host = agentHost();
31
- assertSuccess(academy(host, ['archive', 'kai', '--json']), 'archive');
32
- return host;
33
- }
34
-
35
- // An agents root that resolves outside itself — the one state that fails every
36
- // command reading the root, and so the failure case for the two that address no
37
- // agent and reject no option.
38
- function escapedHost() {
39
- const host = createHost();
40
- symlinkSync(host.outside, host.agentsRoot);
41
- return host;
42
- }
43
-
44
- function specHost() {
45
- const host = createHost();
46
- const body = { name: 'kai', role: 'conformance subject', objective: 'exist' };
47
- writeFileSync(join(host.root, 'spec.json'), JSON.stringify(body));
48
- return host;
49
- }
50
-
51
- // `hire` without --spec execs into an interactive session with inherited stdio
52
- // and can never print an envelope, so the headless form is the one under test.
53
- const SPEC = (host) => join(host.root, 'spec.json');
54
-
55
- const CASES = {
56
- doctor: [createHost, () => ['doctor'], escapedHost, () => ['doctor'], 'unsafe_agent_path'],
57
- list: [createHost, () => ['list'], escapedHost, () => ['list'], 'unsafe_agent_path'],
58
- inspect: [
59
- agentHost,
60
- () => ['inspect', 'kai'],
61
- createHost,
62
- () => ['inspect', 'ghost'],
63
- 'agent_not_found',
64
- ],
65
- tokens: [
66
- agentHost,
67
- () => ['tokens', 'kai'],
68
- createHost,
69
- () => ['tokens', 'ghost'],
70
- 'agent_not_found',
71
- ],
72
- budget: [
73
- agentHost,
74
- () => ['budget', 'kai'],
75
- createHost,
76
- () => ['budget', 'ghost'],
77
- 'agent_not_found',
78
- ],
79
- sessions: [
80
- createHost,
81
- () => ['sessions'],
82
- createHost,
83
- () => ['sessions', '--nope'],
84
- 'invalid_spec',
85
- ],
86
- events: [
87
- createHost,
88
- () => ['events', '--since', '0'],
89
- createHost,
90
- () => ['events', '--since', '99999'],
91
- 'replay_unavailable',
92
- ],
93
- create: [createHost, () => ['create', 'kai'], agentHost, () => ['create', 'kai'], 'agent_exists'],
94
- hire: [
95
- specHost,
96
- (h) => ['hire', '--spec', SPEC(h)],
97
- createHost,
98
- (h) => ['hire', '--spec', SPEC(h)],
99
- 'invalid_spec',
100
- ],
101
- rename: [
102
- agentHost,
103
- () => ['rename', 'kai', 'nova'],
104
- createHost,
105
- () => ['rename', 'ghost', 'nova'],
106
- 'agent_not_found',
107
- ],
108
- archive: [
109
- agentHost,
110
- () => ['archive', 'kai'],
111
- createHost,
112
- () => ['archive', 'ghost'],
113
- 'agent_not_found',
114
- ],
115
- unarchive: [
116
- archivedHost,
117
- () => ['unarchive', 'kai'],
118
- createHost,
119
- () => ['unarchive', 'ghost'],
120
- 'agent_not_found',
121
- ],
122
- delete: [
123
- agentHost,
124
- () => ['delete', 'kai'],
125
- createHost,
126
- () => ['delete', 'ghost'],
127
- 'agent_not_found',
128
- ],
129
- migrate: [createHost, () => ['migrate'], createHost, () => ['migrate', '--nope'], 'invalid_spec'],
130
- };
131
-
132
- test('every published command has a success case and a failure case', () => {
133
- const published = publishedCommands(createHost());
134
-
135
- assert.deepEqual([...published].sort(), Object.keys(CASES).sort());
136
- });
137
-
138
- for (const [command, [okHost, okArgs, failHost, failArgs, code]] of Object.entries(CASES)) {
139
- test(`${command} answers success in the envelope with exit 0`, () => {
140
- const host = okHost();
141
-
142
- const result = academy(host, [...okArgs(host), '--json']);
143
-
144
- assertSuccess(result, command);
145
- assert.equal(result.status, 0);
146
- assert.equal(result.stderr, '');
147
- });
148
-
149
- test(`${command} answers failure in the envelope with a non-zero exit`, () => {
150
- const host = failHost();
151
-
152
- const result = academy(host, [...failArgs(host), '--json']);
153
-
154
- assertFailure(result, command, code);
155
- assert.notEqual(result.status, 0);
156
- assert.equal(result.stdout, '');
157
- });
158
- }
159
-
160
- // The one success that carries a false flag in its payload. The exit rule
161
- // follows `ok`, never the payload, so a client must not read a budget breach as
162
- // a failed command.
163
- test('a payload reporting a breach is still ok:true and exit 0', () => {
164
- const host = agentHost();
165
- writeFileSync(join(host.agentsRoot, 'kai', 'knowledge.md'), 'over the cap. '.repeat(4000));
166
-
167
- const result = academy(host, ['budget', 'kai', '--json']);
168
-
169
- const payload = assertSuccess(result, 'budget');
170
- assert.equal(result.status, 0);
171
- assert.equal(payload.withinBudget, false);
172
- });
173
-
174
- // The health channel is not the error channel: a degraded component is reported
175
- // beside an ok:true answer, and the exit rule still follows `ok`.
176
- test('doctor reporting health errors is still ok:true and exit 0', () => {
177
- const host = agentHost();
178
- writeFileSync(join(host.agentsRoot, 'kai', 'agent.yaml'), 'name: kai\nruntime: mainframe\n');
179
-
180
- const result = academy(host, ['doctor', '--json']);
181
-
182
- const payload = assertSuccess(result, 'doctor');
183
- assert.equal(result.status, 0);
184
- assert.deepEqual(payload.errors, [{ code: 'invalid_runtime_agents', count: 1 }]);
185
- });