@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.
- package/.claude-plugin/marketplace.json +5 -0
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +390 -0
- package/README.md +67 -122
- package/hooks/backstory/.claude-plugin/plugin.json +11 -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 +7 -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
package/scripts/cli/args.mjs
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { parseCreateArgs, parseSkillsArgs } from './args-kinds.mjs';
|
|
2
|
+
import { parseNotesArgs } from './args-notes.mjs';
|
|
3
|
+
import { parseRenameArgs, parseStartArgs } from './args-owner.mjs';
|
|
4
|
+
import { parseRunArgs } from './args-run.mjs';
|
|
5
|
+
import { CLI_NAME } from './core.mjs';
|
|
2
6
|
import { UNIMPLEMENTED_COMMANDS } from './doctor.mjs';
|
|
3
7
|
|
|
4
8
|
const COMMAND_PARSERS = {
|
|
@@ -6,7 +10,7 @@ const COMMAND_PARSERS = {
|
|
|
6
10
|
// dispatch can answer them in the envelope instead of a usage dump.
|
|
7
11
|
...Object.fromEntries(UNIMPLEMENTED_COMMANDS.map((command) => [command, jsonOnlyCommand])),
|
|
8
12
|
doctor: (command, rest) => jsonOnlyCommand(command, rest),
|
|
9
|
-
create: (command, rest) =>
|
|
13
|
+
create: (command, rest) => parseCreateArgs(command, rest),
|
|
10
14
|
hire: (command, rest) => parseHireArgs(command, rest),
|
|
11
15
|
run: (_command, rest) => parseRunArgs(rest),
|
|
12
16
|
nightly: (_command, rest) => {
|
|
@@ -15,6 +19,8 @@ const COMMAND_PARSERS = {
|
|
|
15
19
|
},
|
|
16
20
|
events: (command, rest) => parseEventsArgs(command, rest),
|
|
17
21
|
migrate: (command, rest) => parseMigrateArgs(command, rest),
|
|
22
|
+
start: (command, rest) => parseStartArgs(command, rest),
|
|
23
|
+
stop: (command, rest) => namedJsonCommand(command, rest),
|
|
18
24
|
rename: (command, rest) => parseRenameArgs(command, rest),
|
|
19
25
|
archive: (command, rest) => namedJsonCommand(command, rest),
|
|
20
26
|
unarchive: (command, rest) => namedJsonCommand(command, rest),
|
|
@@ -25,9 +31,14 @@ const COMMAND_PARSERS = {
|
|
|
25
31
|
budget: (command, rest) => namedJsonCommand(command, rest),
|
|
26
32
|
clean: (command, rest) => ({ command, name: rest[0] }),
|
|
27
33
|
delete: (command, rest) => namedJsonCommand(command, rest),
|
|
28
|
-
destroy: (command, rest) => ({
|
|
34
|
+
destroy: (command, rest) => ({
|
|
35
|
+
command,
|
|
36
|
+
name: rest[0],
|
|
37
|
+
force: rest.includes('--force'),
|
|
38
|
+
}),
|
|
29
39
|
root: (command, rest) => jsonOnlyCommand(command, rest),
|
|
30
40
|
notes: (_command, rest) => parseNotesArgs(rest),
|
|
41
|
+
skills: (command, rest) => parseSkillsArgs(command, rest),
|
|
31
42
|
};
|
|
32
43
|
|
|
33
44
|
export function parseArgs(argv) {
|
|
@@ -48,7 +59,11 @@ function jsonOnlyCommand(command, rest) {
|
|
|
48
59
|
}
|
|
49
60
|
|
|
50
61
|
function namedJsonCommand(command, rest) {
|
|
51
|
-
return {
|
|
62
|
+
return {
|
|
63
|
+
command,
|
|
64
|
+
name: rest.find((arg) => arg !== '--json'),
|
|
65
|
+
json: hasFlag(rest, '--json'),
|
|
66
|
+
};
|
|
52
67
|
}
|
|
53
68
|
|
|
54
69
|
// `hire [--spec <path>] [--json] [-- ...]`. `--spec` selects the headless form
|
|
@@ -69,44 +84,13 @@ function parseHireArgs(command, rest) {
|
|
|
69
84
|
else if (arg.startsWith('--spec=')) spec = arg.slice('--spec='.length);
|
|
70
85
|
else if (arg.startsWith('--')) invalidOption ??= arg;
|
|
71
86
|
}
|
|
72
|
-
return {
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
const optionArgs = dashIdx >= 0 ? afterName.slice(0, dashIdx) : afterName;
|
|
80
|
-
const passthrough = dashIdx >= 0 ? afterName.slice(dashIdx + 1) : [];
|
|
81
|
-
// No default here. The agent is not resolved yet, and defaulting before it is
|
|
82
|
-
// what re-registered a codex agent's nightly job as claude-code for ten
|
|
83
|
-
// nights. A null runtime means "not explicit"; `run` reads the persisted
|
|
84
|
-
// scalar instead.
|
|
85
|
-
let runtime = null;
|
|
86
|
-
|
|
87
|
-
for (let i = 0; i < optionArgs.length; i++) {
|
|
88
|
-
const arg = optionArgs[i];
|
|
89
|
-
if (arg === '--agent') {
|
|
90
|
-
const value = optionArgs[++i];
|
|
91
|
-
if (!RUNTIMES.has(value))
|
|
92
|
-
return invalidRunOption(`Invalid --agent value: ${value ?? '(none)'}`);
|
|
93
|
-
runtime = value;
|
|
94
|
-
} else if (arg?.startsWith('--agent=')) {
|
|
95
|
-
const value = arg.slice('--agent='.length);
|
|
96
|
-
if (!RUNTIMES.has(value))
|
|
97
|
-
return invalidRunOption(`Invalid --agent value: ${value || '(none)'}`);
|
|
98
|
-
runtime = value;
|
|
99
|
-
} else if (arg) {
|
|
100
|
-
return invalidRunOption(`Unknown run option: ${arg}`);
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
return { command: 'run', name, runtime, passthrough };
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
function invalidRunOption(message) {
|
|
108
|
-
console.error(`${message}. Use --agent claude-code or --agent codex before --.`);
|
|
109
|
-
return { command: 'help', exitCode: 1 };
|
|
87
|
+
return {
|
|
88
|
+
command,
|
|
89
|
+
json,
|
|
90
|
+
spec,
|
|
91
|
+
invalidOption,
|
|
92
|
+
passthrough: extractPassthrough(rest),
|
|
93
|
+
};
|
|
110
94
|
}
|
|
111
95
|
|
|
112
96
|
// `events --since <seq> [--logid <id>] [--json]`. `--since` stays a raw string:
|
|
@@ -160,71 +144,20 @@ function parseSessionsArgs(command, rest) {
|
|
|
160
144
|
return { command, json, agent };
|
|
161
145
|
}
|
|
162
146
|
|
|
163
|
-
// `rename <old> <new> [--json]`. Like `events`, `migrate` and `sessions`, an
|
|
164
|
-
// option Academy does not know stays on the command so the handler answers it
|
|
165
|
-
// inside the envelope the caller asked for.
|
|
166
|
-
function parseRenameArgs(command, rest) {
|
|
167
|
-
const json = hasFlag(rest, '--json');
|
|
168
|
-
const positional = [];
|
|
169
|
-
for (const arg of rest) {
|
|
170
|
-
if (arg === '--json') continue;
|
|
171
|
-
else if (arg.startsWith('--')) return { command, json, invalidOption: arg };
|
|
172
|
-
else positional.push(arg);
|
|
173
|
-
}
|
|
174
|
-
return { command, json, name: positional[0], newName: positional[1] };
|
|
175
|
-
}
|
|
176
|
-
|
|
177
147
|
function extractPassthrough(rest) {
|
|
178
148
|
const dashIdx = rest.indexOf('--');
|
|
179
149
|
return dashIdx >= 0 ? rest.slice(dashIdx + 1) : [];
|
|
180
150
|
}
|
|
181
151
|
|
|
182
|
-
//
|
|
183
|
-
|
|
184
|
-
// (NAME_RE) AND there is more to follow — so quoted single-arg text stays text.
|
|
185
|
-
function parseNotesArgs(rest) {
|
|
186
|
-
const action = rest[0];
|
|
187
|
-
if (action !== 'add' && action !== 'list') {
|
|
188
|
-
console.error(`Unknown notes action: ${action ?? '(none)'}. Use 'add' or 'list'.`);
|
|
189
|
-
return { command: 'help', exitCode: 1 };
|
|
190
|
-
}
|
|
191
|
-
const args = rest.slice(1);
|
|
192
|
-
|
|
193
|
-
if (action === 'add') {
|
|
194
|
-
let name;
|
|
195
|
-
let textParts = args;
|
|
196
|
-
if (args.length >= 2 && NAME_RE.test(args[0])) {
|
|
197
|
-
name = args[0];
|
|
198
|
-
textParts = args.slice(1);
|
|
199
|
-
}
|
|
200
|
-
return { command: 'notes', action, name, text: textParts.join(' ') };
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
// list — pull out --last N (or --last=N), the rest is an optional agent name.
|
|
204
|
-
let last = 12;
|
|
205
|
-
const positional = [];
|
|
206
|
-
for (let i = 0; i < args.length; i++) {
|
|
207
|
-
if (args[i] === '--last') {
|
|
208
|
-
last = Number.parseInt(args[++i], 10);
|
|
209
|
-
} else if (args[i].startsWith('--last=')) {
|
|
210
|
-
last = Number.parseInt(args[i].slice('--last='.length), 10);
|
|
211
|
-
} else {
|
|
212
|
-
positional.push(args[i]);
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
if (!Number.isInteger(last) || last <= 0) last = 12;
|
|
216
|
-
const name = positional[0] && NAME_RE.test(positional[0]) ? positional[0] : undefined;
|
|
217
|
-
return { command: 'notes', action, name, last };
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
export function printUsage() {
|
|
221
|
-
console.log(
|
|
222
|
-
`
|
|
152
|
+
// A module constant, so the command list can grow without growing a function.
|
|
153
|
+
const USAGE = `
|
|
223
154
|
Usage: ${CLI_NAME} <command> [options]
|
|
224
155
|
|
|
225
156
|
Commands:
|
|
226
157
|
doctor [--json] Report Academy's version, roots, runtimes, and capability
|
|
227
|
-
create <name> [--
|
|
158
|
+
create [<name>] [--kind specialist|principal|owner] [--no-start] [--json]
|
|
159
|
+
Scaffold a portable agent at ~/.academy/agents/<name>/ (default
|
|
160
|
+
specialist); a terminal Principal opens its first session
|
|
228
161
|
hire Interactive hire flow — produces 8 boot files via Claude Code
|
|
229
162
|
hire --spec <path> [--json]
|
|
230
163
|
Headless hire from a JSON specification file
|
|
@@ -243,8 +176,13 @@ Commands:
|
|
|
243
176
|
List sessions whose agent directory is inside
|
|
244
177
|
AGENTS_ROOT
|
|
245
178
|
rename <old> <new> [--json]
|
|
246
|
-
|
|
247
|
-
|
|
179
|
+
Change an agent's name: the name: field and the seeded
|
|
180
|
+
name lines. No folder moves and no job changes; safe
|
|
181
|
+
any time, also mid-session
|
|
182
|
+
start <name> [--mandate <file>] [--hours <h>] [--contact <c>] [--trust 1-4] [--json]
|
|
183
|
+
Start an agent and register its jobs; an Owner needs a
|
|
184
|
+
signed mandate and gets one first daily run
|
|
185
|
+
stop <name> [--json] Stop an agent and delete its jobs; its files stay
|
|
248
186
|
archive <name> [--json] Move an agent into the archived holding area
|
|
249
187
|
unarchive <name> [--json]
|
|
250
188
|
Restore an archived agent to its canonical slot
|
|
@@ -252,15 +190,16 @@ Commands:
|
|
|
252
190
|
inspect <name> Inspect one agent
|
|
253
191
|
tokens <name> Estimate generated prompt tokens by surface
|
|
254
192
|
budget <name> Check prompt token budget by surface
|
|
255
|
-
clean <name>
|
|
193
|
+
clean <name> Reset notes.md (work.md is kept)
|
|
256
194
|
destroy <name> --force Remove an agent and all its files
|
|
257
195
|
root Print Academy package root
|
|
258
|
-
notes add [<agent>] "…"
|
|
196
|
+
notes add [<agent>] [--user] "…" Append a note to notes.md; --user proposes it in user.md
|
|
259
197
|
notes list [<agent>] [--last N] Show recent notes (default last 12)
|
|
260
198
|
|
|
261
199
|
Examples:
|
|
262
200
|
${CLI_NAME} doctor --json
|
|
263
201
|
${CLI_NAME} create kai
|
|
202
|
+
${CLI_NAME} create ada --kind principal --json
|
|
264
203
|
${CLI_NAME} hire
|
|
265
204
|
${CLI_NAME} hire --spec ./kai.json --json
|
|
266
205
|
${CLI_NAME} run kai
|
|
@@ -271,10 +210,14 @@ Examples:
|
|
|
271
210
|
${CLI_NAME} migrate --dry-run --json
|
|
272
211
|
${CLI_NAME} sessions --agent kai --json
|
|
273
212
|
${CLI_NAME} rename kai nova --json
|
|
213
|
+
${CLI_NAME} start owner-one --mandate ./mandate.md --trust 3 --json
|
|
214
|
+
${CLI_NAME} stop owner-one --json
|
|
274
215
|
${CLI_NAME} notes add "User prefers short status updates before edits"
|
|
275
216
|
${CLI_NAME} notes list --last 20
|
|
276
|
-
|
|
277
|
-
|
|
217
|
+
`.trim();
|
|
218
|
+
|
|
219
|
+
export function printUsage() {
|
|
220
|
+
console.log(USAGE);
|
|
278
221
|
}
|
|
279
222
|
|
|
280
223
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { ACADEMY_HOME, resolveExecutable } from './core.mjs';
|
|
4
|
+
|
|
5
|
+
// Agents track work in Beads (`bd`). Academy never wraps it: skills call
|
|
6
|
+
// `bd … --json` directly, so Academy only pins the version range, points every
|
|
7
|
+
// agent at the one user database, and turns telemetry off.
|
|
8
|
+
|
|
9
|
+
const BEADS_VERSION_RANGE = '>=1.3.1 <1.4';
|
|
10
|
+
const RANGE_FLOOR = [1, 3, 1];
|
|
11
|
+
const RANGE_CEILING = [1, 4, 0];
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* The env a run adds: the one Beads database of the user (embedded, shared by
|
|
15
|
+
* every agent), no telemetry, and, for a session, its own actor. Beads refuses
|
|
16
|
+
* a second actor's claim on an item another holds, so two sessions can never
|
|
17
|
+
* work the same item, and the actor's pid tells the heartbeat whether the
|
|
18
|
+
* holder is still running.
|
|
19
|
+
*/
|
|
20
|
+
export function beadsEnv(actor) {
|
|
21
|
+
return {
|
|
22
|
+
BEADS_DIR: join(ACADEMY_HOME, '.beads'),
|
|
23
|
+
OTEL_SDK_DISABLED: 'true',
|
|
24
|
+
...(actor ? { BEADS_ACTOR: actor } : {}),
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function compareVersions(left, right) {
|
|
29
|
+
for (let index = 0; index < 3; index += 1) {
|
|
30
|
+
if (left[index] !== right[index]) return left[index] - right[index];
|
|
31
|
+
}
|
|
32
|
+
return 0;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function inPinnedRange(version) {
|
|
36
|
+
const parts = version.split('.').map(Number);
|
|
37
|
+
return compareVersions(parts, RANGE_FLOOR) >= 0 && compareVersions(parts, RANGE_CEILING) < 0;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// A probe, like doctor's runtime check: it never exits and never throws. An
|
|
41
|
+
// output it cannot read is reported out of range, because the pin cannot be
|
|
42
|
+
// confirmed.
|
|
43
|
+
export function beadsStatus() {
|
|
44
|
+
let bin;
|
|
45
|
+
try {
|
|
46
|
+
bin = resolveExecutable('ACADEMY_BD_BIN', 'bd', { throwOnMissing: true });
|
|
47
|
+
} catch {
|
|
48
|
+
return { status: 'missing', version: null, range: BEADS_VERSION_RANGE };
|
|
49
|
+
}
|
|
50
|
+
const probe = spawnSync(bin, ['version'], { encoding: 'utf8', timeout: 2000 });
|
|
51
|
+
if (probe.error || probe.status !== 0) {
|
|
52
|
+
return { status: 'missing', version: null, range: BEADS_VERSION_RANGE };
|
|
53
|
+
}
|
|
54
|
+
const version = /\b(\d+\.\d+\.\d+)\b/.exec(probe.stdout)?.[1] ?? null;
|
|
55
|
+
const status = version && inPinnedRange(version) ? 'ok' : 'out_of_range';
|
|
56
|
+
return { status, version, range: BEADS_VERSION_RANGE };
|
|
57
|
+
}
|
package/scripts/cli/codex.mjs
CHANGED
|
@@ -2,9 +2,13 @@ import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node
|
|
|
2
2
|
import { homedir } from 'node:os';
|
|
3
3
|
import { dirname, join } from 'node:path';
|
|
4
4
|
import { spawnSync } from 'node:child_process';
|
|
5
|
-
import { ACADEMY_ROOT,
|
|
5
|
+
import { ACADEMY_ROOT, resolveExecutable, shellSingleQuote } from './core.mjs';
|
|
6
|
+
import { childExitCode } from './exit-code.mjs';
|
|
6
7
|
import { MEMORY_BRIDGE_ENV_KEYS } from '../../hooks/memory_bridge.mjs';
|
|
7
8
|
|
|
9
|
+
const ROQUE_POST_TOOL_USE_TAG = 'academy-roque-post-tool-use-v1';
|
|
10
|
+
const ROQUE_COMPACT_TAG = 'academy-roque-compact-v1';
|
|
11
|
+
|
|
8
12
|
function codexHome() {
|
|
9
13
|
return process.env.CODEX_HOME || join(homedir(), '.codex');
|
|
10
14
|
}
|
|
@@ -30,6 +34,15 @@ function academyHookCommand(scriptName, contextPath) {
|
|
|
30
34
|
].join(' ');
|
|
31
35
|
}
|
|
32
36
|
|
|
37
|
+
// Only a kind that renders the curator agent may point Codex at its config.
|
|
38
|
+
function knowledgeCuratorProfileSection(configPath) {
|
|
39
|
+
return `
|
|
40
|
+
[agents.knowledge_curator]
|
|
41
|
+
description = "Apply evidence-gated note graduations during Academy nightly consolidation."
|
|
42
|
+
config_file = ${tomlString(configPath)}
|
|
43
|
+
`;
|
|
44
|
+
}
|
|
45
|
+
|
|
33
46
|
export function writeCodexProfile(dir, name) {
|
|
34
47
|
const profileName = codexProfileName(name);
|
|
35
48
|
const profilePath = join(codexHome(), `${profileName}.config.toml`);
|
|
@@ -44,11 +57,7 @@ writable_roots = [${tomlString(dir)}]
|
|
|
44
57
|
|
|
45
58
|
[features]
|
|
46
59
|
hooks = true
|
|
47
|
-
|
|
48
|
-
[agents.knowledge_curator]
|
|
49
|
-
description = "Apply evidence-gated note graduations during Academy nightly consolidation."
|
|
50
|
-
config_file = ${tomlString(knowledgeCuratorConfigPath)}
|
|
51
|
-
`;
|
|
60
|
+
${existsSync(knowledgeCuratorConfigPath) ? knowledgeCuratorProfileSection(knowledgeCuratorConfigPath) : ''}`;
|
|
52
61
|
writeFileSync(profilePath, toml);
|
|
53
62
|
return { profileName, profilePath };
|
|
54
63
|
}
|
|
@@ -139,19 +148,55 @@ export function writeCodexHooks(contextPath) {
|
|
|
139
148
|
|
|
140
149
|
const sessionStartIndex = upsertCodexHookGroup(config.hooks.SessionStart, sessionStartGroup);
|
|
141
150
|
const stopIndex = upsertCodexHookGroup(config.hooks.Stop, stopGroup);
|
|
151
|
+
config.hooks.PostToolUse = Array.isArray(config.hooks.PostToolUse)
|
|
152
|
+
? config.hooks.PostToolUse
|
|
153
|
+
: [];
|
|
154
|
+
const roquePostToolUseIndex = upsertCodexHookGroup(
|
|
155
|
+
config.hooks.PostToolUse,
|
|
156
|
+
roqueGroup(ROQUE_POST_TOOL_USE_TAG),
|
|
157
|
+
ROQUE_POST_TOOL_USE_TAG,
|
|
158
|
+
);
|
|
159
|
+
const roqueCompactIndex = upsertCodexHookGroup(
|
|
160
|
+
config.hooks.SessionStart,
|
|
161
|
+
roqueGroup(ROQUE_COMPACT_TAG, 'compact'),
|
|
162
|
+
ROQUE_COMPACT_TAG,
|
|
163
|
+
);
|
|
142
164
|
|
|
143
165
|
mkdirSync(dirname(hooksPath), { recursive: true });
|
|
144
166
|
writeFileSync(hooksPath, JSON.stringify(config, null, 2) + '\n');
|
|
145
167
|
writeCodexHookState([
|
|
146
168
|
`${hooksPath}:session_start:${sessionStartIndex}:0`,
|
|
147
169
|
`${hooksPath}:stop:${stopIndex}:0`,
|
|
170
|
+
`${hooksPath}:post_tool_use:${roquePostToolUseIndex}:0`,
|
|
171
|
+
`${hooksPath}:session_start:${roqueCompactIndex}:0`,
|
|
148
172
|
]);
|
|
149
173
|
return hooksPath;
|
|
150
174
|
}
|
|
151
175
|
|
|
152
|
-
|
|
176
|
+
// The guard keeps a checkout without hooks/roque from failing Codex tool calls.
|
|
177
|
+
function roqueCommand() {
|
|
178
|
+
const bundle = shellSingleQuote(join(ACADEMY_ROOT, 'hooks', 'roque', 'roque.js'));
|
|
179
|
+
return `[ ! -f ${bundle} ] || node ${bundle} --tool codex`;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// One hook per group: Codex trust is keyed by group index plus hook index :0.
|
|
183
|
+
function roqueGroup(tag, matcher) {
|
|
184
|
+
return {
|
|
185
|
+
...(matcher ? { matcher } : {}),
|
|
186
|
+
hooks: [
|
|
187
|
+
{
|
|
188
|
+
_academy: tag,
|
|
189
|
+
type: 'command',
|
|
190
|
+
command: roqueCommand(),
|
|
191
|
+
timeout: 10,
|
|
192
|
+
},
|
|
193
|
+
],
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function upsertCodexHookGroup(groups, nextGroup, tag = 'academy-runtime-hooks-v1') {
|
|
153
198
|
const index = groups.findIndex((group) => {
|
|
154
|
-
return (group?.hooks || []).some((hook) => hook?._academy ===
|
|
199
|
+
return (group?.hooks || []).some((hook) => hook?._academy === tag);
|
|
155
200
|
});
|
|
156
201
|
if (index >= 0) {
|
|
157
202
|
groups[index] = nextGroup;
|
|
@@ -163,14 +208,15 @@ function upsertCodexHookGroup(groups, nextGroup) {
|
|
|
163
208
|
|
|
164
209
|
function writeCodexRuntimeContext(env, contextPath) {
|
|
165
210
|
// The Codex sandbox strips the ambient environment, so forward Academy's own
|
|
166
|
-
// keys — plus whatever the memory bridge declares it needs
|
|
167
|
-
//
|
|
211
|
+
// keys — plus whatever the memory bridge declares it needs. The nightly flag
|
|
212
|
+
// must reach the Stop hook, or a nightly re-arms its own pending marker.
|
|
168
213
|
const keys = [
|
|
169
214
|
'ACADEMY_AGENT_DIR',
|
|
170
215
|
'ACADEMY_AGENT_NAME',
|
|
216
|
+
'ACADEMY_NIGHTLY_RUN',
|
|
171
217
|
'ACADEMY_PROJECT_DIR',
|
|
172
218
|
'HOME',
|
|
173
|
-
...
|
|
219
|
+
...MEMORY_BRIDGE_ENV_KEYS,
|
|
174
220
|
];
|
|
175
221
|
const context = {
|
|
176
222
|
expiresAt: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(),
|
|
@@ -180,6 +226,14 @@ function writeCodexRuntimeContext(env, contextPath) {
|
|
|
180
226
|
writeFileSync(contextPath, JSON.stringify(context, null, 2) + '\n');
|
|
181
227
|
}
|
|
182
228
|
|
|
229
|
+
// `codex exec` refuses a folder that is not a git repository unless told
|
|
230
|
+
// otherwise, and an agent home is not one. The flag belongs right after `exec`.
|
|
231
|
+
export function withRepoCheck(argv) {
|
|
232
|
+
const exec = argv.indexOf('exec');
|
|
233
|
+
if (exec === -1 || argv.slice(exec + 1).includes('--skip-git-repo-check')) return argv;
|
|
234
|
+
return [...argv.slice(0, exec + 1), '--skip-git-repo-check', ...argv.slice(exec + 1)];
|
|
235
|
+
}
|
|
236
|
+
|
|
183
237
|
export function launchCodex(
|
|
184
238
|
args,
|
|
185
239
|
{ cwd, env, message, profileName, profilePath, runtimeContextPath },
|
|
@@ -208,7 +262,7 @@ export function launchCodex(
|
|
|
208
262
|
);
|
|
209
263
|
process.exit(1);
|
|
210
264
|
}
|
|
211
|
-
process.exit(result
|
|
265
|
+
process.exit(childExitCode(result));
|
|
212
266
|
}
|
|
213
267
|
|
|
214
268
|
// ─────────────────────────────────────────────────────────────────────────────
|
package/scripts/cli/core.mjs
CHANGED
|
@@ -13,8 +13,11 @@ import {
|
|
|
13
13
|
} from 'node:fs';
|
|
14
14
|
import { homedir } from 'node:os';
|
|
15
15
|
import { basename, dirname, join, resolve } from 'node:path';
|
|
16
|
+
import { childExitCode } from './exit-code.mjs';
|
|
16
17
|
import { spawnSync } from 'node:child_process';
|
|
17
18
|
import { fileURLToPath } from 'node:url';
|
|
19
|
+
import { readAgentYaml } from './yaml.mjs';
|
|
20
|
+
import { agentIdOf } from './agent-id.mjs';
|
|
18
21
|
|
|
19
22
|
const __filename = fileURLToPath(import.meta.url);
|
|
20
23
|
const __dirname = dirname(__filename);
|
|
@@ -28,6 +31,11 @@ const DEFAULT_AGENTS_ROOT = join(homedir(), '.academy', 'agents');
|
|
|
28
31
|
const NAMED_AGENTS_ROOT = process.env.AGENTS_ROOT;
|
|
29
32
|
export const AGENTS_ROOT = NAMED_AGENTS_ROOT || DEFAULT_AGENTS_ROOT;
|
|
30
33
|
|
|
34
|
+
/** Shared home for files no one agent owns: user.md, user-work.md, proposals/. */
|
|
35
|
+
// Resolved, because every kind now reports shared surface paths and a relative
|
|
36
|
+
// AGENTS_ROOT would otherwise leave `user.md` relative.
|
|
37
|
+
export const ACADEMY_HOME = resolve(process.env.ACADEMY_HOME || dirname(AGENTS_ROOT));
|
|
38
|
+
|
|
31
39
|
// Whether the root is Academy's own or one an operator named. The two states
|
|
32
40
|
// disagree about exactly one thing — a missing parent — and about nothing else.
|
|
33
41
|
const AGENTS_ROOT_IS_DEFAULT = !NAMED_AGENTS_ROOT;
|
|
@@ -38,42 +46,23 @@ export const CLI_NAME = process.env.ACADEMY_CLI_NAME?.trim() || 'academy';
|
|
|
38
46
|
/** Allowed agent name pattern: kebab-case, 1–32 chars. */
|
|
39
47
|
export const NAME_RE = /^[a-z][a-z0-9-]{0,31}$/;
|
|
40
48
|
|
|
41
|
-
export const SURFACES = [
|
|
42
|
-
'identity',
|
|
43
|
-
'role',
|
|
44
|
-
'knowledge',
|
|
45
|
-
'goals',
|
|
46
|
-
'priorities',
|
|
47
|
-
'threads',
|
|
48
|
-
'notes',
|
|
49
|
-
'dailys',
|
|
50
|
-
];
|
|
51
|
-
export const SURFACE_CAPS = {
|
|
52
|
-
identity: 400,
|
|
53
|
-
role: 400,
|
|
54
|
-
knowledge: 2500,
|
|
55
|
-
goals: 150,
|
|
56
|
-
priorities: 250,
|
|
57
|
-
threads: 1750,
|
|
58
|
-
notes: 500,
|
|
59
|
-
dailys: 1050,
|
|
60
|
-
};
|
|
61
|
-
export const TOTAL_SURFACE_CAP = Object.values(SURFACE_CAPS).reduce((sum, cap) => sum + cap, 0);
|
|
62
|
-
export const ENFORCED_SURFACES = new Set(['knowledge', 'priorities', 'threads', 'notes', 'dailys']);
|
|
63
49
|
export const UNIVERSAL_SKILLS = [
|
|
64
50
|
'check-in',
|
|
65
|
-
'
|
|
51
|
+
'work-graph',
|
|
66
52
|
'nightly-consolidation',
|
|
67
53
|
'knowledge-curation',
|
|
54
|
+
'local-rules',
|
|
55
|
+
'skill-method',
|
|
68
56
|
];
|
|
69
57
|
export const NIGHTLY_JOB_CRON = '0 22 * * *';
|
|
70
|
-
|
|
58
|
+
// Unattended runs bypass permissions: auto mode refused every memory write
|
|
59
|
+
// with "no verdict", so nightly runs could not save their work.
|
|
60
|
+
export const SCHEDULED_CLAUDE_PERMISSION_ARGS = ['--permission-mode', 'bypassPermissions'];
|
|
71
61
|
export const SCHEDULED_CODEX_PERMISSION_ARGS = [
|
|
72
|
-
'--
|
|
73
|
-
'never',
|
|
74
|
-
'--sandbox',
|
|
75
|
-
'workspace-write',
|
|
62
|
+
'--dangerously-bypass-approvals-and-sandbox',
|
|
76
63
|
'exec',
|
|
64
|
+
// An agent home is not a git repository; without this, exec refuses to run.
|
|
65
|
+
'--skip-git-repo-check',
|
|
77
66
|
];
|
|
78
67
|
export const ACADEMY_SYSTEM_PROMPT = 'academy-system-prompt.md';
|
|
79
68
|
export const RUNTIMES = new Set(['claude-code', 'codex']);
|
|
@@ -99,9 +88,10 @@ function invalidName(message, name, json) {
|
|
|
99
88
|
process.exit(1);
|
|
100
89
|
}
|
|
101
90
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
91
|
+
// The folder name is an agent's fixed id; agent.yaml `name:` is its current
|
|
92
|
+
// name, so a name change never moves a running session's directory. Every
|
|
93
|
+
// command accepts either. A folder answers first; YAML is read only on a miss.
|
|
94
|
+
export const agentDir = (nameOrId) => join(AGENTS_ROOT, agentIdOf(nameOrId));
|
|
105
95
|
|
|
106
96
|
// The holding area — `.archived`, its containment rule and every command's
|
|
107
97
|
// question about it — lives in archived.mjs, which imports from here.
|
|
@@ -115,6 +105,97 @@ export function agentLifecycleLockPath(dir) {
|
|
|
115
105
|
return join(dirname(resolved), `.${basename(resolved)}.lifecycle.lock`);
|
|
116
106
|
}
|
|
117
107
|
|
|
108
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
109
|
+
// Kind registry
|
|
110
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
111
|
+
|
|
112
|
+
// The registry is the one surface and skill source for compile (scaffold),
|
|
113
|
+
// measurement (inspect, tokens, budget), skills and create. Its contract:
|
|
114
|
+
//
|
|
115
|
+
// KIND_PROFILES[kind] = {
|
|
116
|
+
// surfaces: [{ name, path: (dir) => absolutePath, cap, enforced }],
|
|
117
|
+
// skills: [skillName],
|
|
118
|
+
// }
|
|
119
|
+
//
|
|
120
|
+
// `surfaces` is ordered, and the order is the compile order. `path` takes the
|
|
121
|
+
// agent dir; shared surfaces ignore it and resolve under ACADEMY_HOME. `cap` is
|
|
122
|
+
// an estimated-token cap; a kind's total cap is the sum of its surface caps.
|
|
123
|
+
// `skills` lists only names whose templates exist — a later name enters this
|
|
124
|
+
// list together with its template, because rendering throws on a missing one.
|
|
125
|
+
// Each kind owns its own copy of the list, so a pack skill added to one kind
|
|
126
|
+
// never reaches another. A name renders from `templates/packs/<kind>/<name>`
|
|
127
|
+
// when that exists, else from `templates/skills/<name>`.
|
|
128
|
+
// `surfacesFor(dir)` is the per-agent view with `path` already resolved.
|
|
129
|
+
|
|
130
|
+
// A shared surface lives under ACADEMY_HOME, not in the agent dir.
|
|
131
|
+
const surface = (name, cap, enforced, { shared = false } = {}) => ({
|
|
132
|
+
name,
|
|
133
|
+
path: (dir) => join(shared ? ACADEMY_HOME : dir, `${name}.md`),
|
|
134
|
+
cap,
|
|
135
|
+
enforced,
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
// Every kind compiles the same nine surfaces, in this order: the agent's own
|
|
139
|
+
// seven, then the user's two. Only the cap of `work` differs by kind: a
|
|
140
|
+
// Principal's work.md is thin (only work unique to it); an Owner's is full.
|
|
141
|
+
const surfaceSet = (workCap) => [
|
|
142
|
+
surface('identity', 400, false),
|
|
143
|
+
surface('craft', 600, false),
|
|
144
|
+
surface('mandate', 600, true),
|
|
145
|
+
surface('work', workCap, true),
|
|
146
|
+
surface('knowledge', 2500, true),
|
|
147
|
+
surface('notes', 500, true),
|
|
148
|
+
surface('local-rules', 500, true),
|
|
149
|
+
surface('user', 1000, true, { shared: true }),
|
|
150
|
+
surface('user-work', 1000, true, { shared: true }),
|
|
151
|
+
];
|
|
152
|
+
|
|
153
|
+
const OWNER_PACK = [
|
|
154
|
+
'owner',
|
|
155
|
+
'owner-work',
|
|
156
|
+
'owner-review',
|
|
157
|
+
'owner-heartbeat',
|
|
158
|
+
'owner-feedback',
|
|
159
|
+
'owner-nightly',
|
|
160
|
+
];
|
|
161
|
+
|
|
162
|
+
export const KIND_PROFILES = {
|
|
163
|
+
// The curator is an ordinary Specialist, so `user-nightly` is the one skill a
|
|
164
|
+
// Specialist has that no other kind has.
|
|
165
|
+
specialist: { surfaces: surfaceSet(1000), skills: [...UNIVERSAL_SKILLS, 'user-nightly'] },
|
|
166
|
+
principal: {
|
|
167
|
+
surfaces: surfaceSet(500),
|
|
168
|
+
skills: [
|
|
169
|
+
'principal-nightly',
|
|
170
|
+
'principal-brief',
|
|
171
|
+
'principal-intro',
|
|
172
|
+
'work-graph',
|
|
173
|
+
'check-in',
|
|
174
|
+
'knowledge-curation',
|
|
175
|
+
'local-rules',
|
|
176
|
+
'skill-method',
|
|
177
|
+
],
|
|
178
|
+
},
|
|
179
|
+
owner: { surfaces: surfaceSet(1000), skills: [...OWNER_PACK, ...UNIVERSAL_SKILLS] },
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
// A missing `kind:` is a Specialist: every agent created before kinds. An
|
|
183
|
+
// unknown one is raised, not defaulted, the way the runtime is: compiling it as
|
|
184
|
+
// a Specialist would hand the agent the wrong prompt.
|
|
185
|
+
export function kindOf(dir) {
|
|
186
|
+
const kind = readAgentYaml(dir).kind || 'specialist';
|
|
187
|
+
if (Object.hasOwn(KIND_PROFILES, kind)) return kind;
|
|
188
|
+
|
|
189
|
+
const message = `Agent kind "${kind}" is not a kind Academy supports. Use ${Object.keys(KIND_PROFILES).join(', ')}.`;
|
|
190
|
+
if (jsonMode()) exitJsonError('invalid_spec', message, { kind, dir: resolve(dir) });
|
|
191
|
+
console.error(`Error: ${message}`);
|
|
192
|
+
process.exit(1);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export function surfacesFor(dir) {
|
|
196
|
+
return KIND_PROFILES[kindOf(dir)].surfaces.map((entry) => ({ ...entry, path: entry.path(dir) }));
|
|
197
|
+
}
|
|
198
|
+
|
|
118
199
|
// The read-only root audit, as an answer rather than an exit. `doctor` reports
|
|
119
200
|
// on a root it cannot use and must never exit from inside a probe, so the
|
|
120
201
|
// finding and the reaction to it live apart. It never creates the root: `list`
|
|
@@ -166,11 +247,12 @@ export function checkAgentsRoot(json = jsonMode()) {
|
|
|
166
247
|
}
|
|
167
248
|
|
|
168
249
|
// The default root creates its own parent, because on a first run there is none
|
|
169
|
-
// and nothing else will make it. A named root
|
|
170
|
-
//
|
|
250
|
+
// and nothing else will make it. A named root's parent was proved to exist by
|
|
251
|
+
// the audit above, so for it `recursive` still creates one level. It is set for
|
|
252
|
+
// both because without it two first creates race on the mkdir and one fails.
|
|
171
253
|
export function validateAgentsRoot(json = jsonMode()) {
|
|
172
254
|
const { root, exists } = checkAgentsRoot(json);
|
|
173
|
-
if (!exists) mkdirSync(root, { recursive:
|
|
255
|
+
if (!exists) mkdirSync(root, { recursive: true });
|
|
174
256
|
return root;
|
|
175
257
|
}
|
|
176
258
|
|
|
@@ -246,12 +328,6 @@ export function exitJsonError(code, message, fields = {}) {
|
|
|
246
328
|
contractError(activeCommandName(), code, message, fields);
|
|
247
329
|
}
|
|
248
330
|
|
|
249
|
-
// The memory bridge is Academy's one client-specific surface, and it is opt-in.
|
|
250
|
-
// Unset, a scaffolded agent names no client at all.
|
|
251
|
-
export function memoryBridgeEnabled() {
|
|
252
|
-
return process.env.ACADEMY_MEMORY_BRIDGE === '1';
|
|
253
|
-
}
|
|
254
|
-
|
|
255
331
|
export function isSymlink(path) {
|
|
256
332
|
try {
|
|
257
333
|
return lstatSync(path).isSymbolicLink();
|
|
@@ -334,7 +410,7 @@ export function delegateLegacyRun(name, passthrough, legacyRoot) {
|
|
|
334
410
|
cwd: process.cwd(),
|
|
335
411
|
env: process.env,
|
|
336
412
|
});
|
|
337
|
-
process.exit(result
|
|
413
|
+
process.exit(childExitCode(result));
|
|
338
414
|
}
|
|
339
415
|
|
|
340
416
|
export function isInside(childPath, parentPath) {
|