@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
package/scripts/cli/scaffold.mjs
CHANGED
|
@@ -1,53 +1,82 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
|
-
import { dirname, join, resolve } from 'node:path';
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { basename, dirname, join, resolve } from 'node:path';
|
|
3
3
|
import {
|
|
4
4
|
ACADEMY_ROOT,
|
|
5
5
|
ACADEMY_SYSTEM_PROMPT,
|
|
6
|
-
|
|
7
|
-
UNIVERSAL_SKILLS,
|
|
6
|
+
KIND_PROFILES,
|
|
8
7
|
ensureAcademyGitignore,
|
|
9
8
|
ensureSymlink,
|
|
10
|
-
|
|
9
|
+
kindOf,
|
|
11
10
|
projectPluginDir,
|
|
11
|
+
surfacesFor,
|
|
12
12
|
} from './core.mjs';
|
|
13
|
+
import { ownerYamlBlock } from './owner-config.mjs';
|
|
14
|
+
import { renderPackageSkills, renderTemplate, universalSkillValues } from './skills.mjs';
|
|
13
15
|
import { TEMPLATES } from './templates.mjs';
|
|
14
|
-
import {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
16
|
+
import { filterUserWork } from './user-work-view.mjs';
|
|
17
|
+
import { tokenRecord } from '../../hooks/context_metrics.mjs';
|
|
18
|
+
import { MEMORY_BRIDGE_PERMISSIONS } from '../../hooks/memory_bridge.mjs';
|
|
19
|
+
|
|
20
|
+
export function scaffoldBootFiles(dir, name, today = new Date().toISOString().slice(0, 10)) {
|
|
21
|
+
// Only surfaces with a scaffold template are written, and never over an
|
|
22
|
+
// existing file: the shared ACADEMY_HOME surfaces belong to the user once
|
|
23
|
+
// they exist. The `wx` flag closes the gap between the check and the write,
|
|
24
|
+
// so a racing create or `academy notes add --user` is never overwritten.
|
|
25
|
+
const kind = kindOf(dir);
|
|
26
|
+
for (const { name: surface, path } of surfacesFor(dir)) {
|
|
27
|
+
const template = bootTemplate(kind, surface);
|
|
28
|
+
if (!template || existsSync(path)) continue;
|
|
29
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
30
|
+
writeSeedOnce(path, template(name, today));
|
|
26
31
|
}
|
|
27
32
|
return today;
|
|
28
33
|
}
|
|
29
34
|
|
|
30
|
-
|
|
35
|
+
function writeSeedOnce(path, text) {
|
|
36
|
+
try {
|
|
37
|
+
writeFileSync(path, text, { flag: 'wx' });
|
|
38
|
+
} catch (error) {
|
|
39
|
+
if (error.code !== 'EEXIST') throw error;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Agents created before local rules get the empty file on their next run. An
|
|
44
|
+
// existing file holds the user's rules and is never rewritten.
|
|
45
|
+
export function scaffoldLocalRules(dir, name) {
|
|
46
|
+
const path = join(dir, 'local-rules.md');
|
|
47
|
+
if (existsSync(path)) return;
|
|
48
|
+
writeFileSync(path, TEMPLATES['local-rules.md'](name, new Date().toISOString().slice(0, 10)));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// A kind's pack may supply a surface's first content (a Principal's craft.md).
|
|
52
|
+
// It is a seed: written at create, then the agent's own, never re-rendered.
|
|
53
|
+
function bootTemplate(kind, surface) {
|
|
54
|
+
const packTemplate = join(ACADEMY_ROOT, 'templates', 'packs', kind, `${surface}.md`);
|
|
55
|
+
if (!existsSync(packTemplate)) return TEMPLATES[`${surface}.md`];
|
|
56
|
+
const template = readFileSync(packTemplate, 'utf8');
|
|
57
|
+
return (name, today) => renderTemplate(template, { agent_name: name, created: today });
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function writeAgentYaml(dir, name, today, kind = 'specialist') {
|
|
61
|
+
const { surfaces } = KIND_PROFILES[kind];
|
|
62
|
+
const totalCap = surfaces.reduce((sum, { cap }) => sum + cap, 0).toLocaleString('en-US');
|
|
63
|
+
const surfaceList = surfaces.map(({ name: surface }) => ` - ${surface}.md`).join('\n');
|
|
31
64
|
const yaml = `# Agent metadata. Hand-edit role and objective once the hire flow runs.
|
|
32
65
|
name: ${name}
|
|
33
66
|
created: ${today}
|
|
34
67
|
# Launch runtime — claude_code or codex. \`academy run --agent\` rewrites it.
|
|
35
68
|
runtime: claude_code
|
|
36
|
-
|
|
69
|
+
# Agent kind — specialist, principal or owner. Selects its surfaces and skills.
|
|
70
|
+
kind: ${kind}
|
|
71
|
+
${kind === 'owner' ? ownerYamlBlock() : ''}role: ""
|
|
37
72
|
objective: ""
|
|
38
73
|
|
|
39
|
-
# Boot context —
|
|
40
|
-
#
|
|
41
|
-
# .academy/generated/${ACADEMY_SYSTEM_PROMPT}
|
|
74
|
+
# Boot context — the ${kind} surfaces, capped at ${totalCap} estimated tokens combined.
|
|
75
|
+
# Agent files live alongside this yaml, shared ones under ACADEMY_HOME; academy run
|
|
76
|
+
# compiles them into the generated .academy/generated/${ACADEMY_SYSTEM_PROMPT}
|
|
77
|
+
# before launching an agent runtime.
|
|
42
78
|
surfaces:
|
|
43
|
-
|
|
44
|
-
- role.md
|
|
45
|
-
- knowledge.md
|
|
46
|
-
- goals.md
|
|
47
|
-
- priorities.md
|
|
48
|
-
- threads.md
|
|
49
|
-
- notes.md
|
|
50
|
-
- dailys.md
|
|
79
|
+
${surfaceList}
|
|
51
80
|
`;
|
|
52
81
|
writeFileSync(join(dir, 'agent.yaml'), yaml);
|
|
53
82
|
}
|
|
@@ -57,10 +86,9 @@ export function writeAgentClaudeMd(dir, name) {
|
|
|
57
86
|
// user instructions channel can grow over time but starts effectively empty.
|
|
58
87
|
const md = `# ${name}
|
|
59
88
|
|
|
60
|
-
User instructions for this agent. The
|
|
61
|
-
|
|
62
|
-
\`
|
|
63
|
-
when \`academy run ${name}\` launches an agent runtime.
|
|
89
|
+
User instructions for this agent. The boot surfaces listed in \`agent.yaml\` are
|
|
90
|
+
compiled into \`.academy/generated/${ACADEMY_SYSTEM_PROMPT}\` when
|
|
91
|
+
\`academy run ${name}\` launches an agent runtime.
|
|
64
92
|
|
|
65
93
|
Add user-driven instructions below as they come up.
|
|
66
94
|
`;
|
|
@@ -129,7 +157,7 @@ export function writeProjectCodexSkillBridge(projectDir, dir) {
|
|
|
129
157
|
const projectSkillsDir = join(projectDir, '.agents', 'skills');
|
|
130
158
|
mkdirSync(projectSkillsDir, { recursive: true });
|
|
131
159
|
|
|
132
|
-
for (const skillName of
|
|
160
|
+
for (const skillName of KIND_PROFILES[kindOf(dir)].skills) {
|
|
133
161
|
const source = join(agentSkillsDir, skillName);
|
|
134
162
|
const target = join(projectSkillsDir, skillName);
|
|
135
163
|
if (!existsSync(source) || existsSync(target)) continue;
|
|
@@ -137,68 +165,17 @@ export function writeProjectCodexSkillBridge(projectDir, dir) {
|
|
|
137
165
|
}
|
|
138
166
|
}
|
|
139
167
|
|
|
140
|
-
function renderTemplate(template, values) {
|
|
141
|
-
const rendered = template.replace(/\{\{([a-z_]+)\}\}/g, (match, key) => {
|
|
142
|
-
if (!(key in values)) throw new Error(`Unknown template variable: ${match}`);
|
|
143
|
-
return values[key];
|
|
144
|
-
});
|
|
145
|
-
// An optional section that renders empty must not leave a hole in a surface
|
|
146
|
-
// Academy charges a token budget for.
|
|
147
|
-
return rendered.replace(/\n{3,}/g, '\n\n');
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
function universalSkillValues(dir, name, skillsDir) {
|
|
151
|
-
const dreamsDir = join(dir, 'dreams');
|
|
152
|
-
return {
|
|
153
|
-
agent_name: name,
|
|
154
|
-
agent_dir: dir,
|
|
155
|
-
identity_path: join(dir, 'identity.md'),
|
|
156
|
-
role_path: join(dir, 'role.md'),
|
|
157
|
-
knowledge_path: join(dir, 'knowledge.md'),
|
|
158
|
-
goals_path: join(dir, 'goals.md'),
|
|
159
|
-
priorities_path: join(dir, 'priorities.md'),
|
|
160
|
-
threads_path: join(dir, 'threads.md'),
|
|
161
|
-
notes_path: join(dir, 'notes.md'),
|
|
162
|
-
dailys_path: join(dir, 'dailys.md'),
|
|
163
|
-
memory_observations_path: join(dir, 'memory', 'observations'),
|
|
164
|
-
memory_bridge_guidance: memoryBridgeEnabled() ? MEMORY_BRIDGE_SKILL_GUIDANCE : '',
|
|
165
|
-
dreams_dir: dreamsDir,
|
|
166
|
-
skills_surface: skillsDir.endsWith(join('.agents', 'skills'))
|
|
167
|
-
? '.agents/skills'
|
|
168
|
-
: '.claude/skills',
|
|
169
|
-
skills_dir: skillsDir,
|
|
170
|
-
check_in_path: join(skillsDir, 'check-in', 'SKILL.md'),
|
|
171
|
-
self_update_path: join(skillsDir, 'self-update', 'SKILL.md'),
|
|
172
|
-
nightly_consolidation_path: join(skillsDir, 'nightly-consolidation', 'SKILL.md'),
|
|
173
|
-
knowledge_curation_path: join(skillsDir, 'knowledge-curation', 'SKILL.md'),
|
|
174
|
-
};
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
function writeUniversalSkill(dir, name, skillName, skillsDir) {
|
|
178
|
-
const skillDir = join(skillsDir, skillName);
|
|
179
|
-
const templatePath = join(ACADEMY_ROOT, 'templates', 'skills', skillName, 'SKILL.md');
|
|
180
|
-
const template = readFileSync(templatePath, 'utf8');
|
|
181
|
-
|
|
182
|
-
mkdirSync(skillDir, { recursive: true });
|
|
183
|
-
writeFileSync(
|
|
184
|
-
join(skillDir, 'SKILL.md'),
|
|
185
|
-
renderTemplate(template, universalSkillValues(dir, name, skillsDir)),
|
|
186
|
-
);
|
|
187
|
-
}
|
|
188
|
-
|
|
189
168
|
export function writeSkillsScaffold(dir, name) {
|
|
190
|
-
//
|
|
191
|
-
// paths. Agent/domain skills can be added later by hire or
|
|
169
|
+
// The kind's package skills are rendered into each agent with agent-specific
|
|
170
|
+
// paths. Agent/domain skills can be added later by hire or the skill-method skill.
|
|
192
171
|
writeDreamsDir(dir);
|
|
193
172
|
writeMemoryScaffold(dir);
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
}
|
|
198
|
-
writeKnowledgeCuratorAgents(dir, name);
|
|
173
|
+
const kind = kindOf(dir);
|
|
174
|
+
renderPackageSkills(dir, name, { kind, skills: KIND_PROFILES[kind].skills });
|
|
175
|
+
writeKnowledgeCuratorAgents(dir, name, kind);
|
|
199
176
|
}
|
|
200
177
|
|
|
201
|
-
function writeKnowledgeCuratorAgents(dir, name) {
|
|
178
|
+
function writeKnowledgeCuratorAgents(dir, name, kind) {
|
|
202
179
|
const definitions = [
|
|
203
180
|
{
|
|
204
181
|
source: join(ACADEMY_ROOT, 'templates', 'agents', 'claude-code', 'knowledge-curator.md'),
|
|
@@ -212,11 +189,21 @@ function writeKnowledgeCuratorAgents(dir, name) {
|
|
|
212
189
|
},
|
|
213
190
|
];
|
|
214
191
|
|
|
192
|
+
// The curator exists to run the knowledge-curation skill. A kind without that
|
|
193
|
+
// skill has no curator, and one left from an earlier kind is removed.
|
|
194
|
+
const curates = KIND_PROFILES[kind].skills.includes('knowledge-curation');
|
|
215
195
|
for (const { source, target, skillsDir } of definitions) {
|
|
196
|
+
if (!curates) {
|
|
197
|
+
rmSync(target, { force: true });
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
216
200
|
mkdirSync(dirname(target), { recursive: true });
|
|
217
201
|
writeFileSync(
|
|
218
202
|
target,
|
|
219
|
-
renderTemplate(
|
|
203
|
+
renderTemplate(
|
|
204
|
+
readFileSync(source, 'utf8'),
|
|
205
|
+
universalSkillValues(dir, name, skillsDir, kind),
|
|
206
|
+
),
|
|
220
207
|
);
|
|
221
208
|
}
|
|
222
209
|
}
|
|
@@ -225,41 +212,50 @@ export function academySystemPromptPath(dir) {
|
|
|
225
212
|
return join(dir, '.academy', 'generated', ACADEMY_SYSTEM_PROMPT);
|
|
226
213
|
}
|
|
227
214
|
|
|
215
|
+
export { userWorkView } from './user-work-view.mjs';
|
|
216
|
+
|
|
228
217
|
function surfaceTitle(surface) {
|
|
229
218
|
return `${surface[0].toUpperCase()}${surface.slice(1)}`;
|
|
230
219
|
}
|
|
231
220
|
|
|
232
|
-
function readSurface(
|
|
233
|
-
const surfaceFile = join(dir, `${surface}.md`);
|
|
221
|
+
function readSurface(surface, surfaceFile) {
|
|
234
222
|
if (!existsSync(surfaceFile))
|
|
235
223
|
return `# ${surfaceTitle(surface)}\n\n_(No ${surface}.md present yet.)_`;
|
|
236
224
|
const body = readFileSync(surfaceFile, 'utf8').trimEnd();
|
|
237
225
|
return body || `# ${surfaceTitle(surface)}\n\n_(${surface}.md is empty.)_`;
|
|
238
226
|
}
|
|
239
227
|
|
|
228
|
+
// The operating model is package-owned text, not an agent file: one base for
|
|
229
|
+
// every agent and one section for its kind, rendered with the same values the
|
|
230
|
+
// skills read. It opens the prompt so the harness can cache it.
|
|
231
|
+
function operatingModel(dir, name, kind) {
|
|
232
|
+
const values = universalSkillValues(dir, name, join(dir, '.claude', 'skills'), kind);
|
|
233
|
+
return ['base', kind].map((part) =>
|
|
234
|
+
renderTemplate(
|
|
235
|
+
readFileSync(join(ACADEMY_ROOT, 'templates', 'operating-model', `${part}.md`), 'utf8'),
|
|
236
|
+
values,
|
|
237
|
+
).trimEnd(),
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
|
|
240
241
|
export function buildAcademySystemPrompt(dir, name) {
|
|
241
242
|
const intro = [
|
|
242
243
|
'<!-- Generated by `academy`. Do not edit this file directly. Edit the source surfaces instead. -->',
|
|
243
244
|
'',
|
|
244
|
-
|
|
245
|
-
'',
|
|
246
|
-
'You are an Academy v3 agent. Treat the following surfaces as your durable identity, role, knowledge, goals, priorities, active threads, notes, and recent daily context.',
|
|
247
|
-
'',
|
|
248
|
-
'Capture transient steering cheaply with `academy notes add "..."` — it appends a timestamped bullet to your notes.md without reading the whole file. Use it for corrections, stakeholder facts, caveats, and raw learnings before they are durable enough for another surface; review with `academy notes list`. The self-update skill explains when a note should graduate or expire.',
|
|
249
|
-
'',
|
|
245
|
+
...operatingModel(dir, name, kindOf(dir)).flatMap((part) => [part, '']),
|
|
250
246
|
];
|
|
251
|
-
const surfaces =
|
|
252
|
-
const
|
|
253
|
-
const marker = `<!-- academy:surface:${surface} -->`;
|
|
254
|
-
const content = readSurface(dir, surface);
|
|
255
|
-
return {
|
|
247
|
+
const surfaces = surfacesFor(dir).map(({ name: surface, path }) => {
|
|
248
|
+
const entry = {
|
|
256
249
|
name: surface,
|
|
257
250
|
file: `${surface}.md`,
|
|
258
251
|
path,
|
|
259
252
|
exists: existsSync(path),
|
|
260
|
-
marker
|
|
261
|
-
content,
|
|
253
|
+
marker: `<!-- academy:surface:${surface} -->`,
|
|
254
|
+
content: readSurface(surface, path),
|
|
262
255
|
};
|
|
256
|
+
if (surface !== 'user-work') return entry;
|
|
257
|
+
const { view, ...counts } = filterUserWork(entry.content, name, basename(dir));
|
|
258
|
+
return { ...entry, content: view, ...counts };
|
|
263
259
|
});
|
|
264
260
|
const prompt = [
|
|
265
261
|
...intro,
|
|
@@ -274,25 +270,22 @@ export function buildAcademySystemPrompt(dir, name) {
|
|
|
274
270
|
|
|
275
271
|
export function renderAcademySystemPrompt(dir, name) {
|
|
276
272
|
mkdirSync(dirname(academySystemPromptPath(dir)), { recursive: true });
|
|
277
|
-
const { prompt } = buildAcademySystemPrompt(dir, name);
|
|
273
|
+
const { prompt, surfaces } = buildAcademySystemPrompt(dir, name);
|
|
278
274
|
const promptPath = academySystemPromptPath(dir);
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
275
|
+
// Replaced whole: a provider that starts during a concurrent render of the
|
|
276
|
+
// same agent reads the old prompt or the new one, never half of one.
|
|
277
|
+
const tempPath = join(dirname(promptPath), `.${ACADEMY_SYSTEM_PROMPT}.${process.pid}.tmp`);
|
|
278
|
+
try {
|
|
279
|
+
writeFileSync(tempPath, prompt);
|
|
280
|
+
renameSync(tempPath, promptPath);
|
|
281
|
+
} catch (err) {
|
|
282
|
+
rmSync(tempPath, { force: true });
|
|
283
|
+
throw err;
|
|
284
|
+
}
|
|
285
|
+
return { promptPath, surfaces };
|
|
288
286
|
}
|
|
289
287
|
|
|
290
|
-
export
|
|
291
|
-
return {
|
|
292
|
-
estimatedTokens: estimateTokens(text),
|
|
293
|
-
chars: text.length,
|
|
294
|
-
};
|
|
295
|
-
}
|
|
288
|
+
export { tokenRecord };
|
|
296
289
|
|
|
297
290
|
function writeDreamsDir(dir) {
|
|
298
291
|
mkdirSync(join(dir, 'dreams'), { recursive: true });
|
|
@@ -312,8 +305,8 @@ export function writeSettingsLocal(dir) {
|
|
|
312
305
|
allow: [
|
|
313
306
|
'Bash(academy:*)',
|
|
314
307
|
'Bash(helm:*)',
|
|
315
|
-
'Bash(
|
|
316
|
-
...
|
|
308
|
+
'Bash(tldr-agents tasks:*)',
|
|
309
|
+
...MEMORY_BRIDGE_PERMISSIONS,
|
|
317
310
|
'Bash(date:*)',
|
|
318
311
|
'Read',
|
|
319
312
|
'Edit',
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { tldrTimeoutMs } from './tldr.mjs';
|
|
2
|
+
|
|
3
|
+
// tldr writes info log lines to stderr and its JSON envelope to stdout, so a
|
|
4
|
+
// failure is read from the envelope first — stdout, then stderr — and only
|
|
5
|
+
// falls back to raw text when neither stream carries one.
|
|
6
|
+
|
|
7
|
+
// No --replace fallback: --replace overwrites the operator's pause, conditions,
|
|
8
|
+
// env, and misfire grace, which is the loss --merge exists to prevent.
|
|
9
|
+
const TLDR_WITHOUT_MERGE_REASON =
|
|
10
|
+
'tldr-agents does not support "schedule --merge"; install tldr 0.1.1-local.f6cac003eb94 or later';
|
|
11
|
+
|
|
12
|
+
export function scheduleFailureReason(result) {
|
|
13
|
+
const reason = helmFailureReason(result);
|
|
14
|
+
return rejectsMerge(result) ? `${TLDR_WITHOUT_MERGE_REASON} (${reason})` : reason;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// An older tldr parses unknown flags loosely, so it ignores --merge and then
|
|
18
|
+
// refuses the existing id as a duplicate; a current tldr never returns
|
|
19
|
+
// duplicate_id under --merge. A strict parser names the flag instead.
|
|
20
|
+
function rejectsMerge({ stdout, stderr }) {
|
|
21
|
+
const text = `${stdout ?? ''}\n${stderr ?? ''}`;
|
|
22
|
+
return text.includes('duplicate_id') || text.includes('--merge');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// A spawn `error` (a timeout is ETIMEDOUT, with a null status) is the failure.
|
|
26
|
+
export function helmFailureReason({ stdout, stderr, status, error }) {
|
|
27
|
+
if (error) {
|
|
28
|
+
return error.code === 'ETIMEDOUT'
|
|
29
|
+
? `tldr-agents timed out after ${tldrTimeoutMs()} ms`
|
|
30
|
+
: error.message;
|
|
31
|
+
}
|
|
32
|
+
const envelopeReason = reasonFromEnvelope(parseEnvelope(stdout) ?? parseEnvelope(stderr));
|
|
33
|
+
if (envelopeReason) return envelopeReason;
|
|
34
|
+
const text = (stderr || stdout)?.trim();
|
|
35
|
+
if (!text) return `tldr-agents tasks exited ${status}`;
|
|
36
|
+
return text.length > 240 ? `${text.slice(0, 237)}...` : text;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function reasonFromEnvelope(envelope) {
|
|
40
|
+
if (!envelope) return null;
|
|
41
|
+
const message = envelope.errors?.[0]?.message || envelope.data?.activation?.health?.reason;
|
|
42
|
+
const validationErrors = Array.isArray(envelope.validation_errors)
|
|
43
|
+
? envelope.validation_errors.join(', ')
|
|
44
|
+
: '';
|
|
45
|
+
if (!message) return validationErrors ? `validation errors: ${validationErrors}` : null;
|
|
46
|
+
return validationErrors ? `${message} (validation errors: ${validationErrors})` : message;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// The whole stream first; else the last line that parses, since a stream may
|
|
50
|
+
// carry log lines ahead of the envelope.
|
|
51
|
+
function parseEnvelope(output) {
|
|
52
|
+
const text = output?.trim();
|
|
53
|
+
if (!text) return null;
|
|
54
|
+
const candidates = [text, ...text.split('\n').reverse()];
|
|
55
|
+
for (const candidate of candidates) {
|
|
56
|
+
const parsed = parseObject(candidate.trim());
|
|
57
|
+
if (parsed) return parsed;
|
|
58
|
+
}
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function parseObject(text) {
|
|
63
|
+
if (!text.startsWith('{')) return null;
|
|
64
|
+
try {
|
|
65
|
+
const parsed = JSON.parse(text);
|
|
66
|
+
return parsed && typeof parsed === 'object' ? parsed : null;
|
|
67
|
+
} catch {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
}
|
package/scripts/cli/sessions.mjs
CHANGED
|
@@ -9,6 +9,8 @@ import {
|
|
|
9
9
|
isInside,
|
|
10
10
|
validateName,
|
|
11
11
|
} from './core.mjs';
|
|
12
|
+
import { resolvedAgentDir } from '../../hooks/hook_runtime.mjs';
|
|
13
|
+
import { agentIdOf } from './agent-id.mjs';
|
|
12
14
|
|
|
13
15
|
// The session index is global across installs — the specialists work needs one
|
|
14
16
|
// file a client can read whichever root it drives. Attribution is therefore a
|
|
@@ -55,9 +57,11 @@ export function unattributableSessionCount() {
|
|
|
55
57
|
return readIndex().filter((row) => !row.agentDir).length;
|
|
56
58
|
}
|
|
57
59
|
|
|
58
|
-
|
|
60
|
+
// `--agent` filters by the directory the id or current name resolves to, so a
|
|
61
|
+
// session recorded under an earlier name still belongs to the agent.
|
|
62
|
+
function attributed(row, rootReal, agentHome) {
|
|
59
63
|
if (!row.agentDir) return false;
|
|
60
|
-
if (
|
|
64
|
+
if (agentHome && resolvedAgentDir(row.agentDir) !== agentHome) return false;
|
|
61
65
|
return isInside(row.agentDir, rootReal);
|
|
62
66
|
}
|
|
63
67
|
|
|
@@ -76,8 +80,11 @@ export function readSessions({ json, agent, invalidOption }) {
|
|
|
76
80
|
if (agent !== undefined) validateName(agent, json);
|
|
77
81
|
checkAgentsRoot(json);
|
|
78
82
|
const agentsRoot = resolve(AGENTS_ROOT);
|
|
83
|
+
// The hook records a realpath; the agent's folder may have moved since.
|
|
84
|
+
const agentHome =
|
|
85
|
+
agent === undefined ? null : join(resolvedAgentDir(AGENTS_ROOT), agentIdOf(agent));
|
|
79
86
|
const sessions = readIndex()
|
|
80
|
-
.filter((row) => attributed(row, agentsRoot,
|
|
87
|
+
.filter((row) => attributed(row, agentsRoot, agentHome))
|
|
81
88
|
.map(publishedRow);
|
|
82
89
|
|
|
83
90
|
if (json) {
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
|
|
2
|
+
import { join, resolve } from 'node:path';
|
|
3
|
+
import { agentDir, checkAgentsRoot, contractOk, exitJsonError, validateName } from './core.mjs';
|
|
4
|
+
import { assertNotArchived } from './archived.mjs';
|
|
5
|
+
import { assertContainedAgentDir } from './lifecycle.mjs';
|
|
6
|
+
import { tokenRecord } from './scaffold.mjs';
|
|
7
|
+
|
|
8
|
+
// The skill method's hard ceiling per file. A file at 80% of it must be split
|
|
9
|
+
// before it grows further, so the check warns there and fails only over it.
|
|
10
|
+
export const SKILL_TOKEN_CEILING = 2000;
|
|
11
|
+
const SKILL_WARN_AT = Math.round(SKILL_TOKEN_CEILING * 0.8);
|
|
12
|
+
const SKILL_SURFACES = ['.claude/skills', '.agents/skills'];
|
|
13
|
+
const TOKENIZER = 'estimated:chars-and-words-v1';
|
|
14
|
+
|
|
15
|
+
function skillFileStatus(estimatedTokens) {
|
|
16
|
+
if (estimatedTokens > SKILL_TOKEN_CEILING) return 'fail';
|
|
17
|
+
if (estimatedTokens >= SKILL_WARN_AT) return 'warn';
|
|
18
|
+
return 'ok';
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function markdownFiles(skillDir) {
|
|
22
|
+
return readdirSync(skillDir, { recursive: true })
|
|
23
|
+
.filter((file) => file.endsWith('.md') && statSync(join(skillDir, file)).isFile())
|
|
24
|
+
.sort();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Every SKILL.md and reference file under each skill in one skills dir. A skills
|
|
28
|
+
// dir that does not exist measures as empty, not as an error.
|
|
29
|
+
export function measureSkillsRoot(skillsRoot) {
|
|
30
|
+
if (!existsSync(skillsRoot)) return [];
|
|
31
|
+
const skills = readdirSync(skillsRoot)
|
|
32
|
+
.filter((skill) => statSync(join(skillsRoot, skill)).isDirectory())
|
|
33
|
+
.sort();
|
|
34
|
+
return skills.flatMap((skill) =>
|
|
35
|
+
markdownFiles(join(skillsRoot, skill)).map((file) =>
|
|
36
|
+
measureFile(skill, file, join(skillsRoot, skill, file)),
|
|
37
|
+
),
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function measureFile(skill, file, path) {
|
|
42
|
+
const { estimatedTokens, chars } = tokenRecord(readFileSync(path, 'utf8'));
|
|
43
|
+
return {
|
|
44
|
+
skill,
|
|
45
|
+
file,
|
|
46
|
+
path,
|
|
47
|
+
estimatedTokens,
|
|
48
|
+
chars,
|
|
49
|
+
status: skillFileStatus(estimatedTokens),
|
|
50
|
+
overBy: Math.max(0, estimatedTokens - SKILL_TOKEN_CEILING),
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// <home>/references/*.md: linked facts held at the same ceiling. Top level only;
|
|
55
|
+
// a home without the dir measures as empty.
|
|
56
|
+
function measureReferences(dir) {
|
|
57
|
+
const root = join(dir, 'references');
|
|
58
|
+
if (!existsSync(root)) return [];
|
|
59
|
+
return readdirSync(root)
|
|
60
|
+
.filter((file) => file.endsWith('.md') && statSync(join(root, file)).isFile())
|
|
61
|
+
.sort()
|
|
62
|
+
.map((file) => ({ surface: 'references', ...measureFile('', file, join(root, file)) }));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function refuseSkills(message, fields, json) {
|
|
66
|
+
if (json) exitJsonError('invalid_spec', message, fields);
|
|
67
|
+
console.error(`Error: ${message}`);
|
|
68
|
+
process.exit(1);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function resolveCheckedAgent(name, json) {
|
|
72
|
+
validateName(name);
|
|
73
|
+
checkAgentsRoot();
|
|
74
|
+
assertNotArchived(name);
|
|
75
|
+
const dir = resolve(agentDir(name));
|
|
76
|
+
if (!existsSync(dir)) {
|
|
77
|
+
const message = `Agent "${name}" not found at ${dir}`;
|
|
78
|
+
if (json) exitJsonError('agent_not_found', message, { name });
|
|
79
|
+
console.error(message);
|
|
80
|
+
process.exit(1);
|
|
81
|
+
}
|
|
82
|
+
assertContainedAgentDir(name, dir);
|
|
83
|
+
return dir;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function skillsReport(name, dir) {
|
|
87
|
+
const files = SKILL_SURFACES.flatMap((surface) =>
|
|
88
|
+
measureSkillsRoot(join(dir, surface)).map((file) => ({ surface, ...file })),
|
|
89
|
+
).concat(measureReferences(dir));
|
|
90
|
+
const counts = { ok: 0, warn: 0, fail: 0 };
|
|
91
|
+
for (const file of files) counts[file.status] += 1;
|
|
92
|
+
return {
|
|
93
|
+
agent: name,
|
|
94
|
+
dir,
|
|
95
|
+
tokenizer: TOKENIZER,
|
|
96
|
+
ceiling: SKILL_TOKEN_CEILING,
|
|
97
|
+
warnAt: SKILL_WARN_AT,
|
|
98
|
+
passed: counts.fail === 0,
|
|
99
|
+
counts,
|
|
100
|
+
files,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function statusLabel(file) {
|
|
105
|
+
if (file.status === 'fail') return `FAIL over by ${file.overBy}`;
|
|
106
|
+
if (file.status === 'warn') return `WARN split before ${SKILL_TOKEN_CEILING}`;
|
|
107
|
+
return 'OK';
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function printSkillsReport(report) {
|
|
111
|
+
console.log(`${report.agent} skills check`);
|
|
112
|
+
console.log(`tokenizer: ${report.tokenizer}`);
|
|
113
|
+
console.log(`ceiling: ${report.ceiling} tokens per file, warn at ${report.warnAt}`);
|
|
114
|
+
console.log('');
|
|
115
|
+
for (const file of report.files) {
|
|
116
|
+
const label = [file.surface, file.skill, file.file].filter(Boolean).join('/');
|
|
117
|
+
console.log(
|
|
118
|
+
`${label.padEnd(48)} ${String(file.estimatedTokens).padStart(5)} / ${report.ceiling} tokens ${statusLabel(file)}`,
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
console.log('');
|
|
122
|
+
console.log(report.passed ? 'PASS' : 'FAIL');
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// `skills check <agent> [--json]`. A failing file is a finding, not a fault of
|
|
126
|
+
// the command: the report still publishes with ok: true, and the exit code
|
|
127
|
+
// carries the verdict so a nightly run can branch on it.
|
|
128
|
+
export function checkSkills({ action, name, json, invalidOption }) {
|
|
129
|
+
if (invalidOption !== undefined) {
|
|
130
|
+
refuseSkills(
|
|
131
|
+
`Unknown skills option: ${invalidOption}. Use skills check <agent> [--json].`,
|
|
132
|
+
{ option: invalidOption },
|
|
133
|
+
json,
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
if (action !== 'check') {
|
|
137
|
+
refuseSkills(
|
|
138
|
+
`Unknown skills action: ${action ?? '(none)'}. Use skills check <agent> [--json].`,
|
|
139
|
+
{ action: action ?? null },
|
|
140
|
+
json,
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
const report = skillsReport(name, resolveCheckedAgent(name, json));
|
|
144
|
+
if (json) contractOk('skills', report);
|
|
145
|
+
else printSkillsReport(report);
|
|
146
|
+
process.exitCode = report.passed ? 0 : 1;
|
|
147
|
+
}
|