@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.
- package/.claude-plugin/marketplace.json +5 -0
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +415 -0
- package/README.md +67 -122
- package/hooks/backstory/.claude-plugin/plugin.json +11 -0
- package/hooks/backstory/LICENSE +21 -0
- package/hooks/backstory/SOURCE +2 -0
- package/hooks/backstory/bin/backstory +9 -0
- package/hooks/backstory/hooks/hooks.json +45 -0
- package/hooks/backstory/scripts/knowledge.mjs +87 -0
- package/hooks/backstory/scripts/memory.mjs +685 -0
- package/hooks/backstory/scripts/tool-tick.mjs +1 -0
- package/hooks/backstory/skills/capture/SKILL.md +71 -0
- package/hooks/backstory/skills/capture/references/knowledge-capture-input.json +16 -0
- package/hooks/backstory/skills/capture/references/tagging-policy.md +29 -0
- package/hooks/backstory/skills/memory/SKILL.md +70 -0
- package/hooks/backstory/skills/setup/SKILL.md +85 -0
- package/hooks/context_metrics.mjs +67 -0
- package/hooks/hook_runtime.mjs +43 -2
- package/hooks/memory_bridge.mjs +286 -52
- package/hooks/memory_store.mjs +78 -26
- package/hooks/register_session.mjs +76 -14
- package/hooks/roque/.claude-plugin/plugin.json +6 -0
- package/hooks/roque/LICENSE +21 -0
- package/hooks/roque/SOURCE +1 -0
- package/hooks/roque/hooks.json +38 -0
- package/hooks/roque/package.json +3 -0
- package/hooks/roque/roque.js +1690 -0
- package/hooks/sync_memory.mjs +11 -7
- package/package.json +10 -3
- package/scripts/cli/agent-gate.mjs +70 -0
- package/scripts/cli/agent-id.mjs +37 -0
- package/scripts/cli/archive.mjs +39 -38
- package/scripts/cli/archived.mjs +4 -2
- package/scripts/cli/args-kinds.mjs +33 -0
- package/scripts/cli/args-notes.mjs +42 -0
- package/scripts/cli/args-owner.mjs +37 -0
- package/scripts/cli/args-run.mjs +38 -0
- package/scripts/cli/args.mjs +47 -104
- package/scripts/cli/beads.mjs +57 -0
- package/scripts/cli/codex.mjs +66 -12
- package/scripts/cli/core.mjs +117 -41
- package/scripts/cli/create.mjs +224 -76
- package/scripts/cli/curator-feed-codex.mjs +81 -0
- package/scripts/cli/curator-feed-observations.mjs +97 -0
- package/scripts/cli/curator-feed-scope.mjs +124 -0
- package/scripts/cli/curator-feed-sources.mjs +172 -0
- package/scripts/cli/curator-feed.mjs +318 -0
- package/scripts/cli/default-name.mjs +73 -0
- package/scripts/cli/doctor.mjs +67 -2
- package/scripts/cli/eventlog.mjs +6 -2
- package/scripts/cli/exit-code.mjs +9 -0
- package/scripts/cli/first-run.mjs +115 -0
- package/scripts/cli/heartbeat.mjs +135 -0
- package/scripts/cli/helm-skip.mjs +20 -0
- package/scripts/cli/hire-headless.mjs +37 -22
- package/scripts/cli/hire.mjs +8 -2
- package/scripts/cli/inspect.mjs +36 -18
- package/scripts/cli/lifecycle.mjs +95 -49
- package/scripts/cli/main.mjs +32 -2
- package/scripts/cli/memory-env.mjs +124 -0
- package/scripts/cli/migrate.mjs +13 -7
- package/scripts/cli/next-steps.mjs +13 -0
- package/scripts/cli/nightly-launch.mjs +118 -0
- package/scripts/cli/nightly.mjs +228 -0
- package/scripts/cli/notes.mjs +58 -8
- package/scripts/cli/owner-config.mjs +181 -0
- package/scripts/cli/owner-jobs.mjs +201 -0
- package/scripts/cli/prompt-append.mjs +135 -0
- package/scripts/cli/rename.mjs +90 -109
- package/scripts/cli/run-context.mjs +50 -0
- package/scripts/cli/run.mjs +236 -79
- package/scripts/cli/scaffold.mjs +119 -126
- package/scripts/cli/schedule-errors.mjs +70 -0
- package/scripts/cli/sessions.mjs +10 -3
- package/scripts/cli/skills-check.mjs +147 -0
- package/scripts/cli/skills.mjs +170 -0
- package/scripts/cli/start.mjs +201 -0
- package/scripts/cli/templates.mjs +119 -45
- package/scripts/cli/tldr.mjs +184 -0
- package/scripts/cli/toml-string.mjs +76 -0
- package/scripts/cli/user-work-view.mjs +48 -0
- package/scripts/cli/writable-dirs.mjs +23 -0
- package/scripts/cli/yaml.mjs +1 -1
- package/skills/hire/SKILL.md +112 -100
- package/skills/hire/references/owner.md +68 -0
- package/templates/agents/claude-code/knowledge-curator.md +5 -2
- package/templates/agents/codex/knowledge-curator.toml +2 -1
- package/templates/operating-model/base.md +78 -0
- package/templates/operating-model/owner.md +11 -0
- package/templates/operating-model/principal.md +9 -0
- package/templates/operating-model/specialist.md +9 -0
- package/templates/packs/owner/craft.md +32 -0
- package/templates/packs/owner/identity.md +13 -0
- package/templates/packs/owner/mandate.md +26 -0
- package/templates/packs/owner/owner/SKILL.md +118 -0
- package/templates/packs/owner/owner-feedback/SKILL.md +49 -0
- package/templates/packs/owner/owner-heartbeat/SKILL.md +35 -0
- package/templates/packs/owner/owner-nightly/SKILL.md +114 -0
- package/templates/packs/owner/owner-review/SKILL.md +70 -0
- package/templates/packs/owner/owner-work/SKILL.md +53 -0
- package/templates/packs/principal/craft.md +27 -0
- package/templates/packs/principal/identity.md +13 -0
- package/templates/packs/principal/mandate.md +23 -0
- package/templates/packs/principal/principal-brief/SKILL.md +44 -0
- package/templates/packs/principal/principal-intro/SKILL.md +95 -0
- package/templates/packs/principal/principal-nightly/SKILL.md +152 -0
- package/templates/skills/check-in/SKILL.md +34 -24
- package/templates/skills/knowledge-curation/SKILL.md +89 -48
- package/templates/skills/local-rules/SKILL.md +86 -0
- package/templates/skills/nightly-consolidation/SKILL.md +131 -100
- package/templates/skills/skill-method/SKILL.md +79 -0
- package/templates/skills/user-nightly/SKILL.md +143 -0
- package/templates/skills/work-graph/SKILL.md +75 -0
- package/conformance/README.md +0 -60
- package/conformance/discovery.test.mjs +0 -140
- package/conformance/envelope.test.mjs +0 -185
- package/conformance/error-codes.test.mjs +0 -125
- package/conformance/harness.mjs +0 -180
- package/conformance/identity.test.mjs +0 -125
- package/docs/integration-guide.md +0 -1042
- 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.
|
package/conformance/README.md
DELETED
|
@@ -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
|
-
});
|