@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/inspect.mjs
CHANGED
|
@@ -3,15 +3,14 @@ import { join, resolve } from 'node:path';
|
|
|
3
3
|
import {
|
|
4
4
|
AGENTS_ROOT,
|
|
5
5
|
CLI_NAME,
|
|
6
|
-
|
|
6
|
+
KIND_PROFILES,
|
|
7
7
|
NAME_RE,
|
|
8
|
-
SURFACES,
|
|
9
|
-
SURFACE_CAPS,
|
|
10
|
-
TOTAL_SURFACE_CAP,
|
|
11
8
|
agentDir,
|
|
12
9
|
checkAgentsRoot,
|
|
13
10
|
contractOk,
|
|
14
11
|
exitJsonError,
|
|
12
|
+
kindOf,
|
|
13
|
+
surfacesFor,
|
|
15
14
|
validateName,
|
|
16
15
|
} from './core.mjs';
|
|
17
16
|
import { archivedAgentDir, archivedAgentNames, assertNotArchived } from './archived.mjs';
|
|
@@ -20,6 +19,7 @@ import { readEventLogWatermark } from './eventlog.mjs';
|
|
|
20
19
|
import { readRuntimeProvider, runtimeProviderOrNull } from './runtime.mjs';
|
|
21
20
|
import { academySystemPromptPath, buildAcademySystemPrompt, tokenRecord } from './scaffold.mjs';
|
|
22
21
|
import { readAgentYaml } from './yaml.mjs';
|
|
22
|
+
import { currentAgentName } from './agent-id.mjs';
|
|
23
23
|
|
|
24
24
|
export function listAgents() {
|
|
25
25
|
const entries = agentNames();
|
|
@@ -33,9 +33,12 @@ export function listAgents() {
|
|
|
33
33
|
}
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
|
|
36
|
+
// The current name, and the fixed id after it when a rename made them differ.
|
|
37
|
+
function printRosterLine(id, dir, prefix) {
|
|
37
38
|
const role = readAgentYaml(dir).role || '(no role set)';
|
|
38
|
-
|
|
39
|
+
const name = currentAgentName(dir);
|
|
40
|
+
const label = name === id ? name : `${name} (id: ${id})`;
|
|
41
|
+
console.log(` ${(prefix + label).padEnd(20)} ${role}`);
|
|
39
42
|
}
|
|
40
43
|
|
|
41
44
|
// The watermark is read before the roster, and that order is the contract. Read
|
|
@@ -104,20 +107,26 @@ export function agentRecord(
|
|
|
104
107
|
) {
|
|
105
108
|
const yaml = readAgentYaml(dir);
|
|
106
109
|
const record = {
|
|
107
|
-
name,
|
|
110
|
+
name: yaml.name || name,
|
|
108
111
|
dir: resolve(dir),
|
|
109
112
|
displayName: yaml.displayName || yaml.display_name || yaml.name || name,
|
|
110
113
|
runtimeProvider: strictRuntime ? readRuntimeProvider(dir) : runtimeProviderOrNull(dir),
|
|
114
|
+
kind: strictRuntime ? kindOf(dir) : rosterKind(yaml),
|
|
111
115
|
};
|
|
112
116
|
if (yaml.role) record.role = yaml.role;
|
|
113
117
|
if (includeSurfaces) record.surfaces = surfacePresence(dir);
|
|
114
118
|
return record;
|
|
115
119
|
}
|
|
116
120
|
|
|
121
|
+
// The roster degrades an unsupported kind to null the way it degrades the
|
|
122
|
+
// runtime; a missing one is a Specialist, as `kindOf` reads it.
|
|
123
|
+
function rosterKind(yaml) {
|
|
124
|
+
const kind = yaml.kind || 'specialist';
|
|
125
|
+
return Object.hasOwn(KIND_PROFILES, kind) ? kind : null;
|
|
126
|
+
}
|
|
127
|
+
|
|
117
128
|
function surfacePresence(dir) {
|
|
118
|
-
return Object.fromEntries(
|
|
119
|
-
SURFACES.map((surface) => [surface, existsSync(join(dir, `${surface}.md`))]),
|
|
120
|
-
);
|
|
129
|
+
return Object.fromEntries(surfacesFor(dir).map(({ name, path }) => [name, existsSync(path)]));
|
|
121
130
|
}
|
|
122
131
|
|
|
123
132
|
export function inspectAgent(name, json) {
|
|
@@ -145,12 +154,17 @@ export function inspectAgent(name, json) {
|
|
|
145
154
|
if (record.role) console.log(` role: ${record.role}`);
|
|
146
155
|
}
|
|
147
156
|
|
|
157
|
+
// Staged `## Proposed` lines reach the prompt but do not count toward the user cap.
|
|
158
|
+
function withoutProposed({ name, content }) {
|
|
159
|
+
return name === 'user' ? content.split(/^## Proposed\s*$/m)[0] : content;
|
|
160
|
+
}
|
|
161
|
+
|
|
148
162
|
function roundPercent(part, total) {
|
|
149
163
|
if (!total) return 0;
|
|
150
164
|
return Math.round((part / total) * 1000) / 10;
|
|
151
165
|
}
|
|
152
166
|
|
|
153
|
-
function promptTokenReport(name) {
|
|
167
|
+
function promptTokenReport(name, budgetOnly = false) {
|
|
154
168
|
validateName(name);
|
|
155
169
|
checkAgentsRoot();
|
|
156
170
|
assertNotArchived(name);
|
|
@@ -158,7 +172,7 @@ function promptTokenReport(name) {
|
|
|
158
172
|
if (!existsSync(dir)) return { missing: true, dir };
|
|
159
173
|
assertContainedAgentDir(name, dir);
|
|
160
174
|
|
|
161
|
-
const built = buildAcademySystemPrompt(dir,
|
|
175
|
+
const built = buildAcademySystemPrompt(dir, currentAgentName(dir));
|
|
162
176
|
const total = tokenRecord(built.prompt);
|
|
163
177
|
const overheadText = [
|
|
164
178
|
...built.intro,
|
|
@@ -166,7 +180,7 @@ function promptTokenReport(name) {
|
|
|
166
180
|
].join('\n');
|
|
167
181
|
const overhead = tokenRecord(overheadText);
|
|
168
182
|
const surfaces = built.surfaces.map((surface) => {
|
|
169
|
-
const count = tokenRecord(surface.content);
|
|
183
|
+
const count = tokenRecord(budgetOnly ? withoutProposed(surface) : surface.content);
|
|
170
184
|
return {
|
|
171
185
|
name: surface.name,
|
|
172
186
|
file: surface.file,
|
|
@@ -223,7 +237,7 @@ export function printPromptTokens(name, json) {
|
|
|
223
237
|
}
|
|
224
238
|
|
|
225
239
|
export function printSurfaceBudget(name, json) {
|
|
226
|
-
const tokenReport = promptTokenReport(name);
|
|
240
|
+
const tokenReport = promptTokenReport(name, true);
|
|
227
241
|
if (tokenReport.missing) {
|
|
228
242
|
const message = `Agent "${name}" not found at ${tokenReport.dir}`;
|
|
229
243
|
if (json) exitJsonError('agent_not_found', message, { name });
|
|
@@ -231,8 +245,11 @@ export function printSurfaceBudget(name, json) {
|
|
|
231
245
|
process.exit(1);
|
|
232
246
|
}
|
|
233
247
|
|
|
248
|
+
// Both the token report and the limits come from surfacesFor, so a surface
|
|
249
|
+
// is joined by its name, never by its position in the list.
|
|
250
|
+
const limits = new Map(surfacesFor(tokenReport.dir).map((limit) => [limit.name, limit]));
|
|
234
251
|
const surfaces = tokenReport.surfaces.map((surface) => {
|
|
235
|
-
const cap =
|
|
252
|
+
const { cap, enforced } = limits.get(surface.name);
|
|
236
253
|
const overBy = Math.max(0, surface.estimatedTokens - cap);
|
|
237
254
|
return {
|
|
238
255
|
name: surface.name,
|
|
@@ -240,10 +257,11 @@ export function printSurfaceBudget(name, json) {
|
|
|
240
257
|
cap,
|
|
241
258
|
overBy,
|
|
242
259
|
withinCap: overBy === 0,
|
|
243
|
-
enforced
|
|
260
|
+
enforced,
|
|
244
261
|
};
|
|
245
262
|
});
|
|
246
263
|
const totalEstimatedTokens = surfaces.reduce((sum, surface) => sum + surface.estimatedTokens, 0);
|
|
264
|
+
const totalCap = surfaces.reduce((sum, surface) => sum + surface.cap, 0);
|
|
247
265
|
const violations = surfaces.filter((surface) => !surface.withinCap);
|
|
248
266
|
const report = {
|
|
249
267
|
agent: tokenReport.agent,
|
|
@@ -251,8 +269,8 @@ export function printSurfaceBudget(name, json) {
|
|
|
251
269
|
withinBudget: !violations.some((surface) => surface.enforced),
|
|
252
270
|
total: {
|
|
253
271
|
estimatedTokens: totalEstimatedTokens,
|
|
254
|
-
cap:
|
|
255
|
-
overBy: Math.max(0, totalEstimatedTokens -
|
|
272
|
+
cap: totalCap,
|
|
273
|
+
overBy: Math.max(0, totalEstimatedTokens - totalCap),
|
|
256
274
|
},
|
|
257
275
|
surfaces,
|
|
258
276
|
violations,
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { existsSync, realpathSync, renameSync, rmSync, writeFileSync } from 'node:fs';
|
|
2
|
-
import { dirname, join, resolve } from 'node:path';
|
|
2
|
+
import { basename, dirname, join, resolve } from 'node:path';
|
|
3
3
|
import { spawnSync } from 'node:child_process';
|
|
4
4
|
import { readJsonFile } from './codex.mjs';
|
|
5
5
|
import {
|
|
6
6
|
AGENTS_ROOT,
|
|
7
|
+
CLI_NAME,
|
|
7
8
|
agentDir,
|
|
8
9
|
agentLifecycleLockPath,
|
|
9
10
|
contractOk,
|
|
@@ -13,16 +14,18 @@ import {
|
|
|
13
14
|
jsonMode,
|
|
14
15
|
LockTimeoutError,
|
|
15
16
|
RuntimeUnavailableError,
|
|
16
|
-
resolveExecutable,
|
|
17
17
|
validateAgentsRoot,
|
|
18
18
|
validateName,
|
|
19
19
|
withFileLock,
|
|
20
20
|
} from './core.mjs';
|
|
21
21
|
import { assertNotArchived, holdingAreaOrNull } from './archived.mjs';
|
|
22
22
|
import { LogCorruptError, appendLifecycleEvent } from './eventlog.mjs';
|
|
23
|
-
import { AgentSpecError } from './yaml.mjs';
|
|
24
|
-
import {
|
|
23
|
+
import { AgentSpecError, readAgentYaml } from './yaml.mjs';
|
|
24
|
+
import { agentIdOf, currentAgentName } from './agent-id.mjs';
|
|
25
|
+
import { helmFailureReason } from './schedule-errors.mjs';
|
|
26
|
+
import { ownerJobIds, ownerSessionJobIds, principalJobIds } from './owner-jobs.mjs';
|
|
25
27
|
import { TEMPLATES } from './templates.mjs';
|
|
28
|
+
import { resolveTldrAgentsBinary, spawnTldrTasksArgs, tldrSpawnOptions } from './tldr.mjs';
|
|
26
29
|
|
|
27
30
|
export function cleanAgent(name) {
|
|
28
31
|
validateName(name);
|
|
@@ -31,19 +34,20 @@ export function cleanAgent(name) {
|
|
|
31
34
|
console.error(`Agent "${name}" not found at ${dir}`);
|
|
32
35
|
process.exit(1);
|
|
33
36
|
}
|
|
34
|
-
// Reset notes.md
|
|
35
|
-
//
|
|
37
|
+
// Reset notes.md only. It is the one transient surface. work.md is durable:
|
|
38
|
+
// it holds the goals and Workstreams the nightly run keeps, so clean never
|
|
39
|
+
// resets it.
|
|
36
40
|
const today = new Date().toISOString().slice(0, 10);
|
|
37
|
-
|
|
38
|
-
writeFileSync(join(dir, '
|
|
39
|
-
console.log(`Cleaned transient surfaces for "${
|
|
41
|
+
const current = currentAgentName(dir);
|
|
42
|
+
writeFileSync(join(dir, 'notes.md'), TEMPLATES['notes.md'](current, today));
|
|
43
|
+
console.log(`Cleaned transient surfaces for "${current}" (notes.md).`);
|
|
40
44
|
}
|
|
41
45
|
|
|
42
46
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
43
47
|
// `destroy` — nuke an agent (--force required)
|
|
44
48
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
45
49
|
|
|
46
|
-
|
|
50
|
+
function deleteJsonError(code, message, fields = {}, json = false) {
|
|
47
51
|
if (json) exitJsonError(code, message, fields);
|
|
48
52
|
console.error(message);
|
|
49
53
|
process.exit(1);
|
|
@@ -57,17 +61,25 @@ export class DeleteAgentError extends Error {
|
|
|
57
61
|
}
|
|
58
62
|
}
|
|
59
63
|
|
|
64
|
+
const MIGRATE_HINT = `Run \`${CLI_NAME} migrate\` to write it.`;
|
|
65
|
+
|
|
60
66
|
// Ownership is one question, asked by every command that acts on an agent
|
|
61
67
|
// directory and by `doctor` when it counts what `migrate` still has to fix.
|
|
62
68
|
// It answers with the fault text, or null when the directory is owned.
|
|
69
|
+
// A marker fault on a directory holding agent.yaml is exactly what `migrate`
|
|
70
|
+
// repairs, so the text names it; with no agent.yaml there is no agent to
|
|
71
|
+
// repair, and pointing at `migrate` would send the operator nowhere.
|
|
63
72
|
export function ownershipFault(name, dir) {
|
|
64
|
-
|
|
65
|
-
if (!existsSync(markerPath) || !existsSync(join(dir, 'agent.yaml'))) {
|
|
73
|
+
if (!existsSync(join(dir, 'agent.yaml'))) {
|
|
66
74
|
return `Agent "${name}" is missing Academy ownership metadata`;
|
|
67
75
|
}
|
|
76
|
+
const markerPath = join(dir, '.academy-agent.json');
|
|
77
|
+
if (!existsSync(markerPath)) {
|
|
78
|
+
return `Agent "${name}" is missing Academy ownership metadata. ${MIGRATE_HINT}`;
|
|
79
|
+
}
|
|
68
80
|
const marker = readJsonFile(markerPath, {});
|
|
69
81
|
if (marker.capability !== 'academy-agent' || marker.name !== name) {
|
|
70
|
-
return `Agent "${name}" ownership metadata is invalid`;
|
|
82
|
+
return `Agent "${name}" ownership metadata is invalid. ${MIGRATE_HINT}`;
|
|
71
83
|
}
|
|
72
84
|
return null;
|
|
73
85
|
}
|
|
@@ -144,7 +156,8 @@ function ownedAgentFault(name, dir, slot) {
|
|
|
144
156
|
// landed, because a read that follows one out of the root publishes content
|
|
145
157
|
// from outside it under a `dir` field claiming to be inside.
|
|
146
158
|
export function assertContainedAgentDir(name, dir, json = jsonMode()) {
|
|
147
|
-
|
|
159
|
+
// The slot is the id folder `dir` names, whichever name addressed it.
|
|
160
|
+
const fault = containmentFault(basename(resolve(dir)), dir, SLOT.canonical);
|
|
148
161
|
if (fault.code) deleteJsonError(fault.code, fault.message, { name }, json);
|
|
149
162
|
}
|
|
150
163
|
|
|
@@ -157,11 +170,14 @@ export function validateOwnedAgentDir(name, dir, json = false, slot = SLOT.canon
|
|
|
157
170
|
// The shared preflight for `delete`, `rename` and `archive`. `unarchive` is the
|
|
158
171
|
// one lifecycle command that may address an archived agent, so it preflights
|
|
159
172
|
// against the holding area instead of coming through here.
|
|
173
|
+
// Every check after the name resolves asks about the fixed id: the folder,
|
|
174
|
+
// its slot and its marker are all keyed by it, never by the current name.
|
|
160
175
|
export function preflightOwnedAgent(name, json = false) {
|
|
161
176
|
validateName(name);
|
|
162
177
|
validateAgentsRoot();
|
|
163
|
-
|
|
164
|
-
|
|
178
|
+
const id = agentIdOf(name);
|
|
179
|
+
assertNotArchived(id, json);
|
|
180
|
+
return { ...validateOwnedAgentDir(id, agentDir(id), json), id };
|
|
165
181
|
}
|
|
166
182
|
|
|
167
183
|
export function assertOwnedAgentForDelete(name, dir, slot = SLOT.canonical) {
|
|
@@ -170,30 +186,61 @@ export function assertOwnedAgentForDelete(name, dir, slot = SLOT.canonical) {
|
|
|
170
186
|
return outcome;
|
|
171
187
|
}
|
|
172
188
|
|
|
173
|
-
|
|
174
|
-
|
|
189
|
+
// Every Academy job the agent's kind registers, nightly first. Read before the
|
|
190
|
+
// directory moves, and read leniently: an unreadable kind must not exit inside
|
|
191
|
+
// the lifecycle lock, and only an Owner has jobs beyond the nightly.
|
|
192
|
+
export function academyJobIds(name, dir) {
|
|
193
|
+
const ids = kindJobIds(name, dir);
|
|
194
|
+
return readAgentYaml(dir).kind === 'owner' ? [...ids, ...ownerSessionJobIds(name)] : ids;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// The jobs Academy itself registers for the agent's kind, nightly first; the
|
|
198
|
+
// Owner's self-scheduled sessions are not among them.
|
|
199
|
+
export function kindJobIds(name, dir) {
|
|
200
|
+
const nightly = `${name}-nightly-consolidation`;
|
|
201
|
+
const { kind } = readAgentYaml(dir);
|
|
202
|
+
if (kind === 'owner') return [nightly, ...ownerJobIds(name)];
|
|
203
|
+
return kind === 'principal' ? [nightly, ...principalJobIds(name)] : [nightly];
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Each id is deleted by its exact (cwd, id) pair, so no other agent's job can
|
|
207
|
+
// match. Stops at the first failure and names it; `id` on success is the
|
|
208
|
+
// nightly, the published `unscheduledJobId`.
|
|
209
|
+
export function deleteAcademyJobs(dir, ids) {
|
|
175
210
|
// Resolved inside the lock, so exiting here would strand the lock directory
|
|
176
211
|
// and make every later delete of this agent unanswerable.
|
|
177
|
-
let
|
|
212
|
+
let tldrAgentsBin;
|
|
178
213
|
try {
|
|
179
|
-
|
|
214
|
+
tldrAgentsBin = resolveTldrAgentsBinary();
|
|
180
215
|
} catch (error) {
|
|
181
216
|
if (!(error instanceof RuntimeUnavailableError)) throw error;
|
|
182
|
-
return { ok: false, id, code: error.code, reason: error.message };
|
|
217
|
+
return { ok: false, id: ids[0], code: error.code, reason: error.message };
|
|
183
218
|
}
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
219
|
+
for (const id of ids) {
|
|
220
|
+
const failure = deleteJob(tldrAgentsBin, dir, id);
|
|
221
|
+
if (failure) return { ok: false, id, reason: failure };
|
|
222
|
+
}
|
|
223
|
+
return { ok: true, id: ids[0], ids };
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function deleteJob(tldrAgentsBin, dir, id) {
|
|
227
|
+
const result = spawnSync(
|
|
228
|
+
tldrAgentsBin,
|
|
229
|
+
spawnTldrTasksArgs(['delete', '--cwd', dir, '--id', id]),
|
|
230
|
+
tldrSpawnOptions(dirname(dir)),
|
|
231
|
+
);
|
|
232
|
+
if (result.error || result.status !== 0) return helmFailureReason(result);
|
|
233
|
+
return null;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// The refusal `delete` and `archive` raise when a job of the set
|
|
237
|
+
// would not go.
|
|
238
|
+
export function unscheduleError(verb, name, unscheduled) {
|
|
239
|
+
return new DeleteAgentError(
|
|
240
|
+
unscheduled.code ?? 'unschedule_failed',
|
|
241
|
+
`Refusing to ${verb} "${name}" because unscheduling "${unscheduled.id}" failed: ${unscheduled.reason}`,
|
|
242
|
+
{ name },
|
|
243
|
+
);
|
|
197
244
|
}
|
|
198
245
|
|
|
199
246
|
// The one failure path `delete`, `rename` and `archive` share. Each stated the
|
|
@@ -219,40 +266,38 @@ export function reportLifecycleFailure(error, verb, name, json) {
|
|
|
219
266
|
}
|
|
220
267
|
|
|
221
268
|
export function deleteAgent(name, json) {
|
|
222
|
-
preflightOwnedAgent(name, json);
|
|
223
|
-
const lockDir = agentLifecycleLockPath(agentDir(
|
|
269
|
+
const { id } = preflightOwnedAgent(name, json);
|
|
270
|
+
const lockDir = agentLifecycleLockPath(agentDir(id));
|
|
224
271
|
try {
|
|
225
|
-
withFileLock(lockDir, () => deleteAgentLocked(
|
|
272
|
+
withFileLock(lockDir, () => deleteAgentLocked(id, json));
|
|
226
273
|
} catch (error) {
|
|
227
|
-
reportLifecycleFailure(error, 'delete',
|
|
274
|
+
reportLifecycleFailure(error, 'delete', id, json);
|
|
228
275
|
}
|
|
229
276
|
}
|
|
230
277
|
|
|
231
|
-
function deleteAgentLocked(
|
|
232
|
-
const { dir, dirReal } = assertOwnedAgentForDelete(
|
|
278
|
+
function deleteAgentLocked(id, _json) {
|
|
279
|
+
const { dir, dirReal } = assertOwnedAgentForDelete(id, agentDir(id));
|
|
280
|
+
const name = currentAgentName(dirReal);
|
|
281
|
+
const jobIds = academyJobIds(id, dirReal);
|
|
233
282
|
const quarantine = join(
|
|
234
283
|
resolve(AGENTS_ROOT),
|
|
235
|
-
`.${
|
|
284
|
+
`.${id}.delete-quarantine.${process.pid}.${Date.now()}`,
|
|
236
285
|
);
|
|
237
286
|
renameSync(dir, quarantine);
|
|
238
|
-
assertOwnedAgentForDelete(
|
|
287
|
+
assertOwnedAgentForDelete(id, quarantine, SLOT.contained);
|
|
239
288
|
|
|
240
|
-
const unscheduled =
|
|
289
|
+
const unscheduled = deleteAcademyJobs(dirReal, jobIds);
|
|
241
290
|
if (!unscheduled.ok) {
|
|
242
291
|
if (!existsSync(dir)) {
|
|
243
292
|
renameSync(quarantine, dir);
|
|
244
293
|
} else {
|
|
245
294
|
throw new DeleteAgentError(
|
|
246
295
|
'unschedule_failed_restore_blocked',
|
|
247
|
-
`Refusing to delete "${name}" because
|
|
296
|
+
`Refusing to delete "${name}" because unscheduling "${unscheduled.id}" failed and ${dir} is occupied: ${unscheduled.reason}`,
|
|
248
297
|
{ name, quarantine },
|
|
249
298
|
);
|
|
250
299
|
}
|
|
251
|
-
throw
|
|
252
|
-
unscheduled.code ?? 'unschedule_failed',
|
|
253
|
-
`Refusing to delete "${name}" because nightly unschedule failed: ${unscheduled.reason}`,
|
|
254
|
-
{ name },
|
|
255
|
-
);
|
|
300
|
+
throw unscheduleError('delete', name, unscheduled);
|
|
256
301
|
}
|
|
257
302
|
rmSync(quarantine, { recursive: true, force: false });
|
|
258
303
|
appendLifecycleEvent('agent_deleted', name, dir);
|
|
@@ -264,6 +309,7 @@ function deleteAgentLocked(name, _json) {
|
|
|
264
309
|
name,
|
|
265
310
|
dir: resolve(dir),
|
|
266
311
|
unscheduledJobId: unscheduled.id,
|
|
312
|
+
unscheduledJobIds: unscheduled.ids,
|
|
267
313
|
});
|
|
268
314
|
return;
|
|
269
315
|
}
|
package/scripts/cli/main.mjs
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { resolve } from 'node:path';
|
|
2
|
+
import { isatty } from 'node:tty';
|
|
2
3
|
import { parseArgs, printUsage } from './args.mjs';
|
|
3
4
|
import {
|
|
4
5
|
ACADEMY_ROOT,
|
|
5
6
|
AGENTS_ROOT,
|
|
7
|
+
CLI_NAME,
|
|
6
8
|
CONTRACT_VERSION,
|
|
7
9
|
contractOk,
|
|
8
10
|
exitJsonError,
|
|
@@ -28,6 +30,8 @@ import { notesAdd, notesList } from './notes.mjs';
|
|
|
28
30
|
import { renameAgent } from './rename.mjs';
|
|
29
31
|
import { runAgent, runNightly } from './run.mjs';
|
|
30
32
|
import { readSessions } from './sessions.mjs';
|
|
33
|
+
import { checkSkills } from './skills-check.mjs';
|
|
34
|
+
import { startAgent, stopAgent } from './start.mjs';
|
|
31
35
|
|
|
32
36
|
const COMMAND_HANDLERS = {
|
|
33
37
|
...Object.fromEntries(UNIMPLEMENTED_COMMANDS.map((command) => [command, unimplementedCommand])),
|
|
@@ -36,7 +40,7 @@ const COMMAND_HANDLERS = {
|
|
|
36
40
|
printUsage();
|
|
37
41
|
process.exit(parsed.exitCode ?? 0);
|
|
38
42
|
},
|
|
39
|
-
create:
|
|
43
|
+
create: createCommand,
|
|
40
44
|
// `--spec` is the only thing that selects the headless form; without it the
|
|
41
45
|
// interactive launch runs exactly as it always has.
|
|
42
46
|
hire: (parsed) =>
|
|
@@ -45,6 +49,8 @@ const COMMAND_HANDLERS = {
|
|
|
45
49
|
nightly: (parsed) => runNightly(parsed.name),
|
|
46
50
|
events: (parsed) => readEvents(parsed),
|
|
47
51
|
migrate: (parsed) => backfillOwnership(parsed),
|
|
52
|
+
start: (parsed) => startAgent(parsed),
|
|
53
|
+
stop: (parsed) => stopAgent(parsed.name, parsed.json),
|
|
48
54
|
rename: (parsed) => renameAgent(parsed.name, parsed.newName, parsed.json, parsed.invalidOption),
|
|
49
55
|
archive: (parsed) => archiveAgent(parsed.name, parsed.json),
|
|
50
56
|
unarchive: (parsed) => unarchiveAgent(parsed.name, parsed.json),
|
|
@@ -53,6 +59,7 @@ const COMMAND_HANDLERS = {
|
|
|
53
59
|
inspect: (parsed) => inspectAgent(parsed.name, parsed.json),
|
|
54
60
|
tokens: (parsed) => printPromptTokens(parsed.name, parsed.json),
|
|
55
61
|
budget: (parsed) => printSurfaceBudget(parsed.name, parsed.json),
|
|
62
|
+
skills: (parsed) => checkSkills(parsed),
|
|
56
63
|
clean: (parsed) => cleanAgent(parsed.name),
|
|
57
64
|
delete: (parsed) => deleteAgent(parsed.name, parsed.json),
|
|
58
65
|
destroy: (parsed) => destroyAgent(parsed.name, parsed.force),
|
|
@@ -62,11 +69,34 @@ const COMMAND_HANDLERS = {
|
|
|
62
69
|
else console.log(ACADEMY_ROOT);
|
|
63
70
|
},
|
|
64
71
|
notes: (parsed) => {
|
|
65
|
-
if (parsed.action === 'add') notesAdd(parsed.name, parsed.text);
|
|
72
|
+
if (parsed.action === 'add') notesAdd(parsed.name, parsed.text, { user: parsed.user });
|
|
66
73
|
else notesList(parsed.name, parsed.last);
|
|
67
74
|
},
|
|
68
75
|
};
|
|
69
76
|
|
|
77
|
+
const PRINCIPAL_PITCH = [
|
|
78
|
+
'New Principal',
|
|
79
|
+
'Your main agent. You direct the work; it does the work with you.',
|
|
80
|
+
'It keeps its job, its memory, and your work between sessions.',
|
|
81
|
+
];
|
|
82
|
+
|
|
83
|
+
// Only a person at a terminal has a session opened for them. Every other
|
|
84
|
+
// create (--json, a pipe, a script, another kind) is the plain create. No
|
|
85
|
+
// create asks a question: a missing name is the first free default name.
|
|
86
|
+
function createCommand(parsed) {
|
|
87
|
+
const atTerminal = isatty(0) && isatty(1);
|
|
88
|
+
const guided = parsed.kind === 'principal' && !parsed.json && !parsed.invalidOption && atTerminal;
|
|
89
|
+
if (!guided) return createAgent(parsed.name, parsed.json, parsed);
|
|
90
|
+
if (parsed.name === undefined) console.log(`${PRINCIPAL_PITCH.join('\n')}\n`);
|
|
91
|
+
const name = createAgent(parsed.name, false, {
|
|
92
|
+
...parsed,
|
|
93
|
+
guided: parsed.noStart ? 'later' : 'opening',
|
|
94
|
+
});
|
|
95
|
+
if (parsed.name === undefined)
|
|
96
|
+
console.log(`Rename it any time: ${CLI_NAME} rename ${name} <new-name>`);
|
|
97
|
+
if (!parsed.noStart) runAgent(name, null, []);
|
|
98
|
+
}
|
|
99
|
+
|
|
70
100
|
// `doctor` advertises the whole contract_version 1 command set, including the
|
|
71
101
|
// commands later phases build. Answering those in the envelope is what keeps
|
|
72
102
|
// the capability list honest: a client that calls what doctor named gets a
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
import { statSync } from 'node:fs';
|
|
3
|
+
import { resolve } from 'node:path';
|
|
4
|
+
import {
|
|
5
|
+
bridgeClaudeSettingsArgs,
|
|
6
|
+
bridgeEnvPresent,
|
|
7
|
+
bridgeHooksInUserSettings,
|
|
8
|
+
bridgeLaunch,
|
|
9
|
+
bridgeLauncher,
|
|
10
|
+
} from '../../hooks/memory_bridge.mjs';
|
|
11
|
+
import { ACADEMY_CLI_PATH, isInside } from './core.mjs';
|
|
12
|
+
import { childExitCode } from './exit-code.mjs';
|
|
13
|
+
import { readAgentYaml } from './yaml.mjs';
|
|
14
|
+
|
|
15
|
+
// Set on the relaunched run so it launches its provider directly instead of
|
|
16
|
+
// wrapping itself again. Cleared before the provider starts.
|
|
17
|
+
export const MEMORY_RELAUNCH_FLAG = 'ACADEMY_MEMORY_RELAUNCH';
|
|
18
|
+
|
|
19
|
+
function isDirectory(path) {
|
|
20
|
+
try {
|
|
21
|
+
return statSync(path).isDirectory();
|
|
22
|
+
} catch {
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// The agent home is not a memory workspace, so a run started there falls
|
|
28
|
+
// through to the workspace the agent declares, if any.
|
|
29
|
+
function memoryWorkspace(dir, cwd, env) {
|
|
30
|
+
const declared = readAgentYaml(dir).memory_workspace;
|
|
31
|
+
const candidates = [
|
|
32
|
+
env.ACADEMY_PROJECT_DIR,
|
|
33
|
+
isInside(cwd, dir) ? null : cwd,
|
|
34
|
+
declared ? resolve(dir, declared) : null,
|
|
35
|
+
];
|
|
36
|
+
const found = candidates.find((path) => path && isDirectory(path));
|
|
37
|
+
return found ? resolve(found) : null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Whether a run of the agent in `dir` from `cwd` launches with a memory workspace. */
|
|
41
|
+
export function resolveMemoryEnv(dir, { cwd, env }) {
|
|
42
|
+
if (bridgeEnvPresent(env)) return { status: 'on', reason: 'inherited', workspace: null };
|
|
43
|
+
const launcher = bridgeLauncher(env);
|
|
44
|
+
if (!launcher) return { status: 'off', reason: 'wrapper_missing', workspace: null };
|
|
45
|
+
const workspace = memoryWorkspace(dir, cwd, env);
|
|
46
|
+
if (!workspace) return { status: 'off', reason: 'no_memory_workspace', workspace: null };
|
|
47
|
+
return { status: 'on', reason: null, workspace, launcher };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** The receipt form of a decision: no launcher internals. */
|
|
51
|
+
export function memoryEnvRecord({ status, reason, workspace }) {
|
|
52
|
+
return { status, reason, workspace };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function logMemoryEnv(name, decision) {
|
|
56
|
+
const event = decision.status === 'on' ? 'memory.env_on' : 'memory.env_off';
|
|
57
|
+
const params =
|
|
58
|
+
decision.status === 'on'
|
|
59
|
+
? {
|
|
60
|
+
agent: name,
|
|
61
|
+
workspace: decision.workspace,
|
|
62
|
+
source: decision.reason ?? 'resolved',
|
|
63
|
+
}
|
|
64
|
+
: { agent: name, reason: decision.reason };
|
|
65
|
+
console.error(JSON.stringify({ event, params }));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// The client's settings load only when the user's own Claude settings do not
|
|
69
|
+
// already run its hooks: both sets would fire on every turn otherwise.
|
|
70
|
+
function claudeSettingsArgs({ name, runtime, passthrough, launcher }) {
|
|
71
|
+
if (runtime === 'codex' || passthrough.includes('--settings')) return [];
|
|
72
|
+
const source = bridgeHooksInUserSettings(process.env) ? 'global' : 'injected';
|
|
73
|
+
console.error(JSON.stringify({ event: 'memory.settings', params: { agent: name, source } }));
|
|
74
|
+
return source === 'global' ? [] : bridgeClaudeSettingsArgs(launcher);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function relaunchArgs({ name, requestedRuntime, runtime, passthrough, launcher }) {
|
|
78
|
+
const providerArgs = [
|
|
79
|
+
...claudeSettingsArgs({ name, runtime, passthrough, launcher }),
|
|
80
|
+
...passthrough,
|
|
81
|
+
];
|
|
82
|
+
return [
|
|
83
|
+
ACADEMY_CLI_PATH,
|
|
84
|
+
'run',
|
|
85
|
+
name,
|
|
86
|
+
...(requestedRuntime ? ['--agent', requestedRuntime] : []),
|
|
87
|
+
...(providerArgs.length > 0 ? ['--', ...providerArgs] : []),
|
|
88
|
+
];
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Re-runs this same `academy run` under the memory wrapper, so the provider,
|
|
92
|
+
// its hooks, and any runtime context all see the resolved memory env. Returns
|
|
93
|
+
// only when the run should launch directly: memory resolved nothing, the env
|
|
94
|
+
// was inherited, or the wrapper could not start. A user's run always launches.
|
|
95
|
+
export function relaunchWithMemoryEnv(run) {
|
|
96
|
+
if (process.env[MEMORY_RELAUNCH_FLAG] === '1') return;
|
|
97
|
+
const decision = resolveMemoryEnv(run.dir, {
|
|
98
|
+
cwd: process.cwd(),
|
|
99
|
+
env: process.env,
|
|
100
|
+
});
|
|
101
|
+
if (!decision.launcher) {
|
|
102
|
+
logMemoryEnv(run.name, decision);
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
const launch = bridgeLaunch(decision.launcher, {
|
|
106
|
+
command: process.execPath,
|
|
107
|
+
args: relaunchArgs({ ...run, launcher: decision.launcher }),
|
|
108
|
+
env: { ...process.env, [MEMORY_RELAUNCH_FLAG]: '1' },
|
|
109
|
+
workspaceCwd: decision.workspace,
|
|
110
|
+
});
|
|
111
|
+
logMemoryEnv(run.name, decision);
|
|
112
|
+
const result = spawnSync(launch.command, launch.args, {
|
|
113
|
+
stdio: 'inherit',
|
|
114
|
+
env: launch.env,
|
|
115
|
+
});
|
|
116
|
+
if (result.error) {
|
|
117
|
+
logMemoryEnv(run.name, {
|
|
118
|
+
status: 'off',
|
|
119
|
+
reason: `wrapper_failed: ${result.error.code}`,
|
|
120
|
+
});
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
process.exit(childExitCode(result));
|
|
124
|
+
}
|
package/scripts/cli/migrate.mjs
CHANGED
|
@@ -59,27 +59,33 @@ function markerFault(dirReal) {
|
|
|
59
59
|
return null;
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
+
function refusal(name, dir, reason) {
|
|
63
|
+
return { refused: { name, dir, reason } };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function holdsAgent(dirReal) {
|
|
67
|
+
return statSync(dirReal).isDirectory() && existsSync(join(dirReal, 'agent.yaml'));
|
|
68
|
+
}
|
|
69
|
+
|
|
62
70
|
function classify(root, rootReal, name) {
|
|
63
71
|
const dir = join(root, name);
|
|
64
72
|
let dirReal;
|
|
65
73
|
try {
|
|
66
74
|
dirReal = realpathSync(dir);
|
|
67
75
|
} catch (error) {
|
|
68
|
-
return
|
|
76
|
+
return refusal(name, dir, `cannot be resolved: ${error.message}`);
|
|
69
77
|
}
|
|
70
78
|
const fault = slotFault(dirReal, rootReal, name);
|
|
71
|
-
if (fault) return
|
|
79
|
+
if (fault) return refusal(name, dir, fault);
|
|
72
80
|
// Not an agent directory, so not a repair. Marking a directory holding no
|
|
73
81
|
// agent.yaml would create an agent, which the backfill must never do.
|
|
74
|
-
if (!
|
|
75
|
-
if (!existsSync(join(dirReal, 'agent.yaml'))) return {};
|
|
82
|
+
if (!holdsAgent(dirReal)) return {};
|
|
76
83
|
const marker = markerFault(dirReal);
|
|
77
|
-
if (marker) return
|
|
78
|
-
if (ownershipFault(name, dirReal) === null) return {};
|
|
84
|
+
if (marker) return refusal(name, dir, marker);
|
|
79
85
|
// dirReal is carried, not recomputed at the write: joining the unvalidated
|
|
80
86
|
// path again would traverse the same symlink chain a second time and land
|
|
81
87
|
// somewhere the classification never proved.
|
|
82
|
-
return { repair: { name, dir, dirReal } };
|
|
88
|
+
return ownershipFault(name, dirReal) === null ? {} : { repair: { name, dir, dirReal } };
|
|
83
89
|
}
|
|
84
90
|
|
|
85
91
|
// The one enumeration. `migrate` repairs exactly this set and `doctor` counts
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { CLI_NAME } from './core.mjs';
|
|
2
|
+
|
|
3
|
+
// What `create` prints under "Next:". A new Owner is a draft: it starts once its mandate is signed.
|
|
4
|
+
export function nextSteps(name, kind) {
|
|
5
|
+
const hire = ` ${CLI_NAME} hire # interactive hire flow to populate the boot surfaces`;
|
|
6
|
+
if (kind !== 'owner') {
|
|
7
|
+
return [hire, ` ${CLI_NAME} run ${name} # launch Claude Code in the agent's home`];
|
|
8
|
+
}
|
|
9
|
+
return [
|
|
10
|
+
` ${CLI_NAME} start ${name} --mandate <file> # sign the mandate and start the draft`,
|
|
11
|
+
` ${CLI_NAME} hire # drafts the mandate with you`,
|
|
12
|
+
];
|
|
13
|
+
}
|