@joenandez/academy 0.5.17 → 0.5.18

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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "academy",
3
3
  "description": "Academy v3 — portable AI agents.",
4
- "version": "0.5.17",
4
+ "version": "0.5.18",
5
5
  "hooks": "./hooks/hooks.json"
6
6
  }
package/CHANGELOG.md CHANGED
@@ -3,6 +3,16 @@
3
3
  This project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
4
4
  The heading at the top of this file is the version in `package.json`.
5
5
 
6
+ ## 0.5.18 — 2026-11-11
7
+
8
+ ### Fixed
9
+
10
+ - **`academy create --runtime claude-code|codex|copilot`.** `create` stores
11
+ the agent's launch runtime, the same value `run --agent` persists, before
12
+ its jobs register. Before, `create` refused the flag, so a client that hired
13
+ a Codex agent (Subspace's Hire a Principal) failed. An unknown value is
14
+ refused with `invalid_spec` and writes nothing.
15
+
6
16
  ## 0.5.17 — 2026-11-11
7
17
 
8
18
  This release collects the 0.5.17 release candidates (rc.1 to rc.6, below) and
package/README.md CHANGED
@@ -80,7 +80,7 @@ academy tokens <name> [--json] # compiled prompt size by surface
80
80
  academy budget <name> [--json] # surfaces over their cap
81
81
  academy sessions [--agent <name>] [--json]
82
82
  academy events --since <seq> [--logid <id>] [--json]
83
- academy create [<name>] [--kind <kind>] [--model <id>] [--display-name <text>] [--json]
83
+ academy create [<name>] [--kind <kind>] [--model <id>] [--display-name <text>] [--runtime claude-code|codex|copilot] [--no-start] [--json]
84
84
  academy hire # interactive
85
85
  academy hire --spec <file> [--json] # headless
86
86
  academy rename <old> [<new>] [--display-name <text>] [--json] # no folder or job changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@joenandez/academy",
3
- "version": "0.5.17",
3
+ "version": "0.5.18",
4
4
  "description": "Academy v3 — portable AI agents with 8 boot surfaces and a skills primitive.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,29 +1,42 @@
1
- // `create <name> [--kind <kind>] [--model <id>] [--display-name <text>] [--no-start]
2
- // [--json]`. Like
1
+ // `create <name> [--kind <kind>] [--model <id>] [--display-name <text>]
2
+ // [--runtime <runtime>] [--no-start] [--json]`. Like
3
3
  // `rename`, an option Academy does not know stays on the command so the handler
4
4
  // answers it inside the envelope the caller asked for. The kind and model stay
5
5
  // raw strings: only `create` can answer an unsupported one in the envelope.
6
+ const CREATE_VALUE_FLAGS = {
7
+ '--kind': 'kind',
8
+ '--model': 'model',
9
+ '--display-name': 'displayName',
10
+ '--runtime': 'runtime',
11
+ };
12
+
6
13
  export function parseCreateArgs(command, rest) {
7
14
  const json = rest.includes('--json');
15
+ const values = {};
8
16
  let name;
9
- let kind;
10
- let model;
11
- let displayName;
12
17
  let noStart = false;
13
18
  for (let i = 0; i < rest.length; i++) {
14
19
  const arg = rest[i];
20
+ const value = createValueFlag(rest, i);
15
21
  if (arg === '--json') continue;
16
22
  else if (arg === '--no-start') noStart = true;
17
- else if (arg === '--kind') kind = rest[++i] ?? '';
18
- else if (arg.startsWith('--kind=')) kind = arg.slice('--kind='.length);
19
- else if (arg === '--model') model = rest[++i] ?? '';
20
- else if (arg.startsWith('--model=')) model = arg.slice('--model='.length);
21
- else if (arg === '--display-name') displayName = rest[++i] ?? '';
22
- else if (arg.startsWith('--display-name=')) displayName = arg.slice('--display-name='.length);
23
+ else if (value) [values[value.key], i] = [value.value, value.next];
23
24
  else if (arg.startsWith('--')) return { command, json, name, invalidOption: arg };
24
25
  else name ??= arg;
25
26
  }
26
- return { command, json, name, kind, model, displayName, noStart };
27
+ const { kind, model, displayName, runtime } = values;
28
+ return { command, json, name, kind, model, displayName, runtime, noStart };
29
+ }
30
+
31
+ // `--flag value` or `--flag=value` for a create option that takes a value.
32
+ function createValueFlag(rest, i) {
33
+ const arg = rest[i];
34
+ const eq = arg.indexOf('=');
35
+ const flag = arg.startsWith('--') && eq > 0 ? arg.slice(0, eq) : arg;
36
+ const key = CREATE_VALUE_FLAGS[flag];
37
+ if (!key) return null;
38
+ if (flag !== arg) return { key, value: arg.slice(eq + 1), next: i };
39
+ return { key, value: rest[i + 1] ?? '', next: i + 1 };
27
40
  }
28
41
 
29
42
  // `skills check <agent> [--json]`. Like `create`, an option Academy does not
@@ -175,11 +175,13 @@ Usage: ${CLI_NAME} <command> [options]
175
175
  Commands:
176
176
  doctor [--json] Report Academy's version, roots, runtimes, and capability
177
177
  create [<name>] [--kind specialist|principal|owner] [--model <id>]
178
- [--display-name <text>] [--no-start] [--json]
178
+ [--display-name <text>] [--runtime claude-code|codex|copilot]
179
+ [--no-start] [--json]
179
180
  Scaffold a portable agent at ~/.academy/agents/<name>/ (default
180
181
  specialist); a terminal Principal opens its first session;
181
182
  --model stores its default model; --display-name stores a
182
- person-facing name ("Principal Two"); <name> stays kebab-case
183
+ person-facing name ("Principal Two"); <name> stays kebab-case;
184
+ --runtime stores the launch runtime, as run --agent does
183
185
  set-model <name> <model> [--json]
184
186
  Change an agent's default model; run passes it to the
185
187
  runtime (Claude --model, Codex -m) unless the caller does
@@ -0,0 +1,43 @@
1
+ import { KIND_PROFILES, exitJsonError } from './core.mjs';
2
+ import { isModelId } from './model.mjs';
3
+ import { toRuntimeProvider } from './runtime.mjs';
4
+ import { DISPLAY_NAME_RULE, isDisplayName } from './yaml.mjs';
5
+
6
+ // Every refusal comes before the name is asked about, so a create Academy will
7
+ // not perform writes nothing. `invalid_spec` is the published code for an
8
+ // unknown option; there is no sixteenth code for it.
9
+ export function assertCreateOptions(kind, invalidOption, json, { model, displayName, runtime }) {
10
+ if (invalidOption !== undefined) {
11
+ refuseCreate(
12
+ `Unknown create option: ${invalidOption}. Use create <name> [--kind <kind>] [--model <id>] [--display-name <text>] [--runtime claude-code|codex|copilot] [--no-start] [--json].`,
13
+ { option: invalidOption },
14
+ json,
15
+ );
16
+ }
17
+ if (model !== undefined && !isModelId(model)) {
18
+ refuseCreate(`Model "${model}" is not a model id Academy can store.`, { model }, json);
19
+ }
20
+ if (runtime !== undefined && !toRuntimeProvider(runtime)) {
21
+ refuseCreate(
22
+ `Runtime "${runtime}" is not a runtime Academy supports. Use claude-code, codex, or copilot.`,
23
+ { runtime },
24
+ json,
25
+ );
26
+ }
27
+ if (displayName !== undefined && !isDisplayName(displayName)) {
28
+ refuseCreate(DISPLAY_NAME_RULE, { displayName }, json);
29
+ }
30
+ if (Object.hasOwn(KIND_PROFILES, kind)) return;
31
+ const kinds = Object.keys(KIND_PROFILES).join(', ');
32
+ refuseCreate(
33
+ `Agent kind "${kind}" is not a kind Academy supports. Use ${kinds}.`,
34
+ { kind },
35
+ json,
36
+ );
37
+ }
38
+
39
+ function refuseCreate(message, fields, json) {
40
+ if (json) exitJsonError('invalid_spec', message, fields);
41
+ console.error(`Error: ${message}`);
42
+ process.exit(1);
43
+ }
@@ -5,13 +5,11 @@ import { pendingMarkerPath } from '../../hooks/memory_store.mjs';
5
5
  import {
6
6
  ACADEMY_ROOT,
7
7
  CLI_NAME,
8
- KIND_PROFILES,
9
8
  activeCommandName,
10
9
  NIGHTLY_JOB_CRON,
11
10
  agentDir,
12
11
  contractError,
13
12
  contractOk,
14
- exitJsonError,
15
13
  kindOf,
16
14
  LockTimeoutError,
17
15
  RuntimeUnavailableError,
@@ -26,8 +24,8 @@ import { claimDefaultName, claimName, isAgentNameFree } from './default-name.mjs
26
24
  import { LogCorruptError, appendLifecycleEvent } from './eventlog.mjs';
27
25
  import { agentRecord } from './inspect.mjs';
28
26
  import { creatorOf } from './leads.mjs';
29
- import { isModelId } from './model.mjs';
30
- import { DISPLAY_NAME_RULE, isDisplayName } from './yaml.mjs';
27
+ import { assertCreateOptions } from './create-options.mjs';
28
+ import { toRuntimeProvider } from './runtime.mjs';
31
29
  import { nextSteps } from './next-steps.mjs';
32
30
  import { deleteAcademyJobs } from './lifecycle.mjs';
33
31
  import {
@@ -65,46 +63,16 @@ export const INTRO_PENDING_MARKER = 'intro-pending.md';
65
63
  export function createAgent(
66
64
  name,
67
65
  json = false,
68
- { kind = DEFAULT_KIND, model, displayName, invalidOption, guided } = {},
66
+ { kind = DEFAULT_KIND, model, displayName, runtime, invalidOption, guided } = {},
69
67
  ) {
70
- assertCreateOptions(kind, invalidOption, json, { model, displayName });
71
- const created = provisionAgent(name, json, kind, { model, displayName });
68
+ assertCreateOptions(kind, invalidOption, json, { model, displayName, runtime });
69
+ // Stored in the provider form `run --agent` writes, before the jobs register.
70
+ const yamlOptions = { model, displayName, runtime: runtime && toRuntimeProvider(runtime) };
71
+ const created = provisionAgent(name, json, kind, yamlOptions);
72
72
  reportCreated(created.name, created.dir, created.jobs, json, guided);
73
73
  return created.name;
74
74
  }
75
75
 
76
- // Every refusal comes before the name is asked about, so a create Academy will
77
- // not perform writes nothing. `invalid_spec` is the published code for an
78
- // unknown option; there is no sixteenth code for it.
79
- function assertCreateOptions(kind, invalidOption, json, { model, displayName }) {
80
- if (invalidOption !== undefined) {
81
- refuseCreate(
82
- `Unknown create option: ${invalidOption}. Use create <name> [--kind <kind>] [--model <id>] [--display-name <text>] [--no-start] [--json].`,
83
- { option: invalidOption },
84
- json,
85
- );
86
- }
87
- if (model !== undefined && !isModelId(model)) {
88
- refuseCreate(`Model "${model}" is not a model id Academy can store.`, { model }, json);
89
- }
90
- if (displayName !== undefined && !isDisplayName(displayName)) {
91
- refuseCreate(DISPLAY_NAME_RULE, { displayName }, json);
92
- }
93
- if (Object.hasOwn(KIND_PROFILES, kind)) return;
94
- const kinds = Object.keys(KIND_PROFILES).join(', ');
95
- refuseCreate(
96
- `Agent kind "${kind}" is not a kind Academy supports. Use ${kinds}.`,
97
- { kind },
98
- json,
99
- );
100
- }
101
-
102
- function refuseCreate(message, fields, json) {
103
- if (json) exitJsonError('invalid_spec', message, fields);
104
- console.error(`Error: ${message}`);
105
- process.exit(1);
106
- }
107
-
108
76
  // The one creation path. `create` reports what it returns as a create and the
109
77
  // headless hire reports the same thing as a hire, so an agent hired without a
110
78
  // terminal is indistinguishable from a created one in ownership, scheduling and
@@ -62,7 +62,7 @@ function bootTemplate(kind, surface) {
62
62
  // `displayName`, `model` and `createdBy` are written only when known, so an agent without them
63
63
  // reads as null, the same as an agent created before either existed.
64
64
  export function writeAgentYaml(dir, name, today, kind = 'specialist', options = {}) {
65
- const { displayName, model, createdBy } = options;
65
+ const { displayName, model, createdBy, runtime = 'claude_code' } = options;
66
66
  const { surfaces } = KIND_PROFILES[kind];
67
67
  const totalCap = surfaces.reduce((sum, { cap }) => sum + cap, 0).toLocaleString('en-US');
68
68
  const surfaceList = surfaces.map(({ name: surface }) => ` - ${surface}.md`).join('\n');
@@ -70,7 +70,7 @@ export function writeAgentYaml(dir, name, today, kind = 'specialist', options =
70
70
  name: ${name}
71
71
  ${displayName ? `displayName: ${displayNameScalar(displayName)}\n` : ''}created: ${today}
72
72
  ${createdBy ? `created_by: ${createdBy}\n` : ''}# Launch runtime — claude_code, codex, or copilot. \`academy run --agent\` rewrites it.
73
- runtime: claude_code
73
+ runtime: ${runtime}
74
74
  ${model ? `model: ${model}\n` : ''}# Launch permissions. yolo skips approvals and the sandbox; delete it for the runtime's default.
75
75
  permission_mode: yolo
76
76
  # Memory systems that are on — list them, e.g. "memory: [backstory]". Leave it out for the default.