@joenandez/academy 0.4.0 → 0.5.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +5 -0
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +415 -0
- package/README.md +67 -122
- package/hooks/backstory/.claude-plugin/plugin.json +11 -0
- package/hooks/backstory/LICENSE +21 -0
- package/hooks/backstory/SOURCE +2 -0
- package/hooks/backstory/bin/backstory +9 -0
- package/hooks/backstory/hooks/hooks.json +45 -0
- package/hooks/backstory/scripts/knowledge.mjs +87 -0
- package/hooks/backstory/scripts/memory.mjs +685 -0
- package/hooks/backstory/scripts/tool-tick.mjs +1 -0
- package/hooks/backstory/skills/capture/SKILL.md +71 -0
- package/hooks/backstory/skills/capture/references/knowledge-capture-input.json +16 -0
- package/hooks/backstory/skills/capture/references/tagging-policy.md +29 -0
- package/hooks/backstory/skills/memory/SKILL.md +70 -0
- package/hooks/backstory/skills/setup/SKILL.md +85 -0
- package/hooks/context_metrics.mjs +67 -0
- package/hooks/hook_runtime.mjs +43 -2
- package/hooks/memory_bridge.mjs +286 -52
- package/hooks/memory_store.mjs +78 -26
- package/hooks/register_session.mjs +76 -14
- package/hooks/roque/.claude-plugin/plugin.json +6 -0
- package/hooks/roque/LICENSE +21 -0
- package/hooks/roque/SOURCE +1 -0
- package/hooks/roque/hooks.json +38 -0
- package/hooks/roque/package.json +3 -0
- package/hooks/roque/roque.js +1690 -0
- package/hooks/sync_memory.mjs +11 -7
- package/package.json +10 -3
- package/scripts/cli/agent-gate.mjs +70 -0
- package/scripts/cli/agent-id.mjs +37 -0
- package/scripts/cli/archive.mjs +39 -38
- package/scripts/cli/archived.mjs +4 -2
- package/scripts/cli/args-kinds.mjs +33 -0
- package/scripts/cli/args-notes.mjs +42 -0
- package/scripts/cli/args-owner.mjs +37 -0
- package/scripts/cli/args-run.mjs +38 -0
- package/scripts/cli/args.mjs +47 -104
- package/scripts/cli/beads.mjs +57 -0
- package/scripts/cli/codex.mjs +66 -12
- package/scripts/cli/core.mjs +117 -41
- package/scripts/cli/create.mjs +224 -76
- package/scripts/cli/curator-feed-codex.mjs +81 -0
- package/scripts/cli/curator-feed-observations.mjs +97 -0
- package/scripts/cli/curator-feed-scope.mjs +124 -0
- package/scripts/cli/curator-feed-sources.mjs +172 -0
- package/scripts/cli/curator-feed.mjs +318 -0
- package/scripts/cli/default-name.mjs +73 -0
- package/scripts/cli/doctor.mjs +67 -2
- package/scripts/cli/eventlog.mjs +6 -2
- package/scripts/cli/exit-code.mjs +9 -0
- package/scripts/cli/first-run.mjs +115 -0
- package/scripts/cli/heartbeat.mjs +135 -0
- package/scripts/cli/helm-skip.mjs +20 -0
- package/scripts/cli/hire-headless.mjs +37 -22
- package/scripts/cli/hire.mjs +8 -2
- package/scripts/cli/inspect.mjs +36 -18
- package/scripts/cli/lifecycle.mjs +95 -49
- package/scripts/cli/main.mjs +32 -2
- package/scripts/cli/memory-env.mjs +124 -0
- package/scripts/cli/migrate.mjs +13 -7
- package/scripts/cli/next-steps.mjs +13 -0
- package/scripts/cli/nightly-launch.mjs +118 -0
- package/scripts/cli/nightly.mjs +228 -0
- package/scripts/cli/notes.mjs +58 -8
- package/scripts/cli/owner-config.mjs +181 -0
- package/scripts/cli/owner-jobs.mjs +201 -0
- package/scripts/cli/prompt-append.mjs +135 -0
- package/scripts/cli/rename.mjs +90 -109
- package/scripts/cli/run-context.mjs +50 -0
- package/scripts/cli/run.mjs +236 -79
- package/scripts/cli/scaffold.mjs +119 -126
- package/scripts/cli/schedule-errors.mjs +70 -0
- package/scripts/cli/sessions.mjs +10 -3
- package/scripts/cli/skills-check.mjs +147 -0
- package/scripts/cli/skills.mjs +170 -0
- package/scripts/cli/start.mjs +201 -0
- package/scripts/cli/templates.mjs +119 -45
- package/scripts/cli/tldr.mjs +184 -0
- package/scripts/cli/toml-string.mjs +76 -0
- package/scripts/cli/user-work-view.mjs +48 -0
- package/scripts/cli/writable-dirs.mjs +23 -0
- package/scripts/cli/yaml.mjs +1 -1
- package/skills/hire/SKILL.md +112 -100
- package/skills/hire/references/owner.md +68 -0
- package/templates/agents/claude-code/knowledge-curator.md +5 -2
- package/templates/agents/codex/knowledge-curator.toml +2 -1
- package/templates/operating-model/base.md +78 -0
- package/templates/operating-model/owner.md +11 -0
- package/templates/operating-model/principal.md +9 -0
- package/templates/operating-model/specialist.md +9 -0
- package/templates/packs/owner/craft.md +32 -0
- package/templates/packs/owner/identity.md +13 -0
- package/templates/packs/owner/mandate.md +26 -0
- package/templates/packs/owner/owner/SKILL.md +118 -0
- package/templates/packs/owner/owner-feedback/SKILL.md +49 -0
- package/templates/packs/owner/owner-heartbeat/SKILL.md +35 -0
- package/templates/packs/owner/owner-nightly/SKILL.md +114 -0
- package/templates/packs/owner/owner-review/SKILL.md +70 -0
- package/templates/packs/owner/owner-work/SKILL.md +53 -0
- package/templates/packs/principal/craft.md +27 -0
- package/templates/packs/principal/identity.md +13 -0
- package/templates/packs/principal/mandate.md +23 -0
- package/templates/packs/principal/principal-brief/SKILL.md +44 -0
- package/templates/packs/principal/principal-intro/SKILL.md +95 -0
- package/templates/packs/principal/principal-nightly/SKILL.md +152 -0
- package/templates/skills/check-in/SKILL.md +34 -24
- package/templates/skills/knowledge-curation/SKILL.md +89 -48
- package/templates/skills/local-rules/SKILL.md +86 -0
- package/templates/skills/nightly-consolidation/SKILL.md +131 -100
- package/templates/skills/skill-method/SKILL.md +79 -0
- package/templates/skills/user-nightly/SKILL.md +143 -0
- package/templates/skills/work-graph/SKILL.md +75 -0
- package/conformance/README.md +0 -60
- package/conformance/discovery.test.mjs +0 -140
- package/conformance/envelope.test.mjs +0 -185
- package/conformance/error-codes.test.mjs +0 -125
- package/conformance/harness.mjs +0 -180
- package/conformance/identity.test.mjs +0 -125
- package/docs/integration-guide.md +0 -1042
- package/templates/skills/self-update/SKILL.md +0 -121
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { basename, join } from 'node:path';
|
|
3
|
+
import { ACADEMY_HOME, ACADEMY_ROOT, KIND_PROFILES } from './core.mjs';
|
|
4
|
+
import { NIGHTLY_REPORT_EVIDENCE } from './nightly.mjs';
|
|
5
|
+
import { ownerSkillValues } from './owner-config.mjs';
|
|
6
|
+
import { proposalsDir } from './writable-dirs.mjs';
|
|
7
|
+
import { MEMORY_BRIDGE_SKILL_GUIDANCE } from '../../hooks/memory_bridge.mjs';
|
|
8
|
+
|
|
9
|
+
// Package skills are the skills Academy renders into an agent from its kind's
|
|
10
|
+
// list. The manifest records exactly what the last render wrote, so the next
|
|
11
|
+
// render can delete what left the list. It is the only ownership record: a
|
|
12
|
+
// skill the manifest never named belongs to the agent and is never deleted.
|
|
13
|
+
|
|
14
|
+
const PACKAGE_TEMPLATES_ROOT = join(ACADEMY_ROOT, 'templates');
|
|
15
|
+
const SKILL_SURFACES = ['.claude/skills', '.agents/skills'];
|
|
16
|
+
const MANIFEST_VERSION = 1;
|
|
17
|
+
// A manifest entry is deleted as `<skillsDir>/<name>`. Only a plain name can
|
|
18
|
+
// never resolve outside the skills dir, whatever the manifest file says.
|
|
19
|
+
const SKILL_NAME_RE = /^[a-z0-9][a-z0-9-]*$/;
|
|
20
|
+
|
|
21
|
+
const PACK_VERSION = JSON.parse(readFileSync(join(ACADEMY_ROOT, 'package.json'), 'utf8')).version;
|
|
22
|
+
|
|
23
|
+
function packageSkillsManifestPath(dir) {
|
|
24
|
+
return join(dir, '.academy', 'generated', 'package-skills.json');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function renderTemplate(template, values) {
|
|
28
|
+
const rendered = template.replace(/\{\{([a-z_]+)\}\}/g, (match, key) => {
|
|
29
|
+
if (!(key in values)) throw new Error(`Unknown template variable: ${match}`);
|
|
30
|
+
return values[key];
|
|
31
|
+
});
|
|
32
|
+
// An optional section that renders empty must not leave a hole in a surface
|
|
33
|
+
// Academy charges a token budget for.
|
|
34
|
+
return rendered.replace(/\n{3,}/g, '\n\n');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Where steering goes in each agent-owned surface, for the skills that route
|
|
38
|
+
// it. Surfaces absent here (local-rules and the two user files) have their own
|
|
39
|
+
// skills or their own writer. Every kind routes the same set.
|
|
40
|
+
const SURFACE_ROUTING = {
|
|
41
|
+
identity: `rare. Persona, voice, relationships. Update only when the
|
|
42
|
+
agent's character changes. Never method.`,
|
|
43
|
+
craft: `method, principles, definition of done, reusable domain judgment.
|
|
44
|
+
Written only by knowledge curation, in general terms, with no environment
|
|
45
|
+
names.`,
|
|
46
|
+
mandate: `the current job, scope, limits, and environment. The user writes
|
|
47
|
+
it. Propose a change; do not rewrite it.`,
|
|
48
|
+
work: `goals, priorities, focus, Workstreams, open questions. The nightly
|
|
49
|
+
run writes it. Link each Workstream by its Beads ID.`,
|
|
50
|
+
knowledge: `what the agent learned here: environment facts and raw domain
|
|
51
|
+
learning. Reusable judgment is promoted to craft.`,
|
|
52
|
+
notes: `temporary steering, observations, corrections, raw learnings,
|
|
53
|
+
pending curation. Append with \`academy notes add "..."\`; hand-edit only
|
|
54
|
+
to curate or graduate.`,
|
|
55
|
+
};
|
|
56
|
+
const AUTONOMOUS_SURFACES = ['work', 'notes'];
|
|
57
|
+
|
|
58
|
+
function routedSurfaceNames(kind) {
|
|
59
|
+
return KIND_PROFILES[kind].surfaces
|
|
60
|
+
.map((entry) => entry.name)
|
|
61
|
+
.filter((surfaceName) => surfaceName in SURFACE_ROUTING);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function surfaceRoutingValues(dir, kind) {
|
|
65
|
+
const names = routedSurfaceNames(kind);
|
|
66
|
+
return {
|
|
67
|
+
surface_paths: names.map((n) => `- \`${n}.md\`: \`${join(dir, `${n}.md`)}\``).join('\n'),
|
|
68
|
+
surface_routing: names.map((n) => `- \`${n}.md\`: ${SURFACE_ROUTING[n]}`).join('\n'),
|
|
69
|
+
autonomous_surfaces: names
|
|
70
|
+
.filter((n) => AUTONOMOUS_SURFACES.includes(n))
|
|
71
|
+
.map((n) => `\`${n}.md\``)
|
|
72
|
+
.join(', '),
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function universalSkillValues(dir, name, skillsDir, kind) {
|
|
77
|
+
const dreamsDir = join(dir, 'dreams');
|
|
78
|
+
return {
|
|
79
|
+
...surfaceRoutingValues(dir, kind),
|
|
80
|
+
// The shown name changes on rename; the id (the folder) never does, so
|
|
81
|
+
// Beads labels, job ids and tldr `academy_agent` render from it.
|
|
82
|
+
agent_name: name,
|
|
83
|
+
agent_id: basename(dir),
|
|
84
|
+
agent_dir: dir,
|
|
85
|
+
identity_path: join(dir, 'identity.md'),
|
|
86
|
+
craft_path: join(dir, 'craft.md'),
|
|
87
|
+
mandate_path: join(dir, 'mandate.md'),
|
|
88
|
+
work_path: join(dir, 'work.md'),
|
|
89
|
+
knowledge_path: join(dir, 'knowledge.md'),
|
|
90
|
+
notes_path: join(dir, 'notes.md'),
|
|
91
|
+
local_rules_path: join(dir, 'local-rules.md'),
|
|
92
|
+
user_path: join(ACADEMY_HOME, 'user.md'),
|
|
93
|
+
user_work_path: join(ACADEMY_HOME, 'user-work.md'),
|
|
94
|
+
proposals_dir: proposalsDir(kind),
|
|
95
|
+
pack_version: PACK_VERSION,
|
|
96
|
+
academy_home: ACADEMY_HOME,
|
|
97
|
+
memory_observations_path: join(dir, 'memory', 'observations'),
|
|
98
|
+
memory_bridge_guidance: MEMORY_BRIDGE_SKILL_GUIDANCE,
|
|
99
|
+
dreams_dir: dreamsDir,
|
|
100
|
+
report_evidence_section: NIGHTLY_REPORT_EVIDENCE.section,
|
|
101
|
+
report_consumed_field: NIGHTLY_REPORT_EVIDENCE.consumed,
|
|
102
|
+
report_changed_field: NIGHTLY_REPORT_EVIDENCE.changed,
|
|
103
|
+
report_no_change_field: NIGHTLY_REPORT_EVIDENCE.noChange,
|
|
104
|
+
skills_surface: skillsDir.endsWith(join('.agents', 'skills'))
|
|
105
|
+
? '.agents/skills'
|
|
106
|
+
: '.claude/skills',
|
|
107
|
+
skills_dir: skillsDir,
|
|
108
|
+
check_in_path: join(skillsDir, 'check-in', 'SKILL.md'),
|
|
109
|
+
nightly_consolidation_path: join(skillsDir, 'nightly-consolidation', 'SKILL.md'),
|
|
110
|
+
knowledge_curation_path: join(skillsDir, 'knowledge-curation', 'SKILL.md'),
|
|
111
|
+
...ownerSkillValues(dir),
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// A kind's pack template wins over a universal one of the same name.
|
|
116
|
+
function skillTemplatePath(templatesRoot, kind, skillName) {
|
|
117
|
+
const packTemplate = join(templatesRoot, 'packs', kind, skillName, 'SKILL.md');
|
|
118
|
+
if (existsSync(packTemplate)) return packTemplate;
|
|
119
|
+
return join(templatesRoot, 'skills', skillName, 'SKILL.md');
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Whether a kind's skill resolves to a pack or universal template. */
|
|
123
|
+
export function skillTemplateExists(kind, skillName, templatesRoot = PACKAGE_TEMPLATES_ROOT) {
|
|
124
|
+
return existsSync(skillTemplatePath(templatesRoot, kind, skillName));
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// No manifest, or one Academy cannot read, owns nothing: deleting on a guess
|
|
128
|
+
// could remove an agent-authored skill.
|
|
129
|
+
function readPreviousSkills(dir) {
|
|
130
|
+
try {
|
|
131
|
+
const { skills } = JSON.parse(readFileSync(packageSkillsManifestPath(dir), 'utf8'));
|
|
132
|
+
return skills && typeof skills === 'object' ? skills : {};
|
|
133
|
+
} catch {
|
|
134
|
+
return {};
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function staleSkills(previous, current) {
|
|
139
|
+
if (!Array.isArray(previous)) return [];
|
|
140
|
+
return previous.filter(
|
|
141
|
+
(skillName) => SKILL_NAME_RE.test(skillName) && !current.includes(skillName),
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export function renderPackageSkills(
|
|
146
|
+
dir,
|
|
147
|
+
name,
|
|
148
|
+
{ kind, skills, templatesRoot = PACKAGE_TEMPLATES_ROOT },
|
|
149
|
+
) {
|
|
150
|
+
const previous = readPreviousSkills(dir);
|
|
151
|
+
const manifest = { version: MANIFEST_VERSION, kind, skills: {} };
|
|
152
|
+
|
|
153
|
+
for (const surface of SKILL_SURFACES) {
|
|
154
|
+
const skillsDir = join(dir, surface);
|
|
155
|
+
const values = universalSkillValues(dir, name, skillsDir, kind);
|
|
156
|
+
mkdirSync(skillsDir, { recursive: true });
|
|
157
|
+
for (const skillName of skills) {
|
|
158
|
+
const template = readFileSync(skillTemplatePath(templatesRoot, kind, skillName), 'utf8');
|
|
159
|
+
mkdirSync(join(skillsDir, skillName), { recursive: true });
|
|
160
|
+
writeFileSync(join(skillsDir, skillName, 'SKILL.md'), renderTemplate(template, values));
|
|
161
|
+
}
|
|
162
|
+
for (const skillName of staleSkills(previous[surface], skills)) {
|
|
163
|
+
rmSync(join(skillsDir, skillName), { recursive: true, force: true });
|
|
164
|
+
}
|
|
165
|
+
manifest.skills[surface] = [...skills];
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
mkdirSync(join(dir, '.academy', 'generated'), { recursive: true });
|
|
169
|
+
writeFileSync(packageSkillsManifestPath(dir), JSON.stringify(manifest, null, 2) + '\n');
|
|
170
|
+
}
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { join, resolve } from 'node:path';
|
|
3
|
+
import { isDraftMandate, isStopped, liveJobIds, readMandate } from './agent-gate.mjs';
|
|
4
|
+
import { currentAgentName } from './agent-id.mjs';
|
|
5
|
+
import {
|
|
6
|
+
agentDir,
|
|
7
|
+
agentLifecycleLockPath,
|
|
8
|
+
contractOk,
|
|
9
|
+
exitJsonError,
|
|
10
|
+
kindOf,
|
|
11
|
+
withFileLock,
|
|
12
|
+
} from './core.mjs';
|
|
13
|
+
import { requestFirstRun } from './first-run.mjs';
|
|
14
|
+
import { jobLabel, registerAcademyJobs, registeredJobIds } from './create.mjs';
|
|
15
|
+
import {
|
|
16
|
+
DeleteAgentError,
|
|
17
|
+
academyJobIds,
|
|
18
|
+
assertOwnedAgentForDelete,
|
|
19
|
+
deleteAcademyJobs,
|
|
20
|
+
kindJobIds,
|
|
21
|
+
preflightOwnedAgent,
|
|
22
|
+
reportLifecycleFailure,
|
|
23
|
+
unscheduleError,
|
|
24
|
+
} from './lifecycle.mjs';
|
|
25
|
+
import { ownerConfig, ownerHeartbeatCron } from './owner-config.mjs';
|
|
26
|
+
import { rewriteAgentYamlScalar } from './yaml.mjs';
|
|
27
|
+
|
|
28
|
+
// `start` signs an agent in (decision D1): an Owner's mandate and terms are
|
|
29
|
+
// checked before any write, then written with `stopped` cleared, then its jobs
|
|
30
|
+
// register. Every check comes before the lock, where an exit would strand it.
|
|
31
|
+
|
|
32
|
+
// The trust level a person picks, as the two Owner terms it sets.
|
|
33
|
+
const TRUST_TERMS = {
|
|
34
|
+
1: { owner_gates: 'proposal, pr', owner_may_merge: 'false' },
|
|
35
|
+
2: { owner_gates: 'pr', owner_may_merge: 'false' },
|
|
36
|
+
3: { owner_gates: 'proposal', owner_may_merge: 'true' },
|
|
37
|
+
4: { owner_gates: 'none', owner_may_merge: 'true' },
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export function startAgent(parsed) {
|
|
41
|
+
const { name, json } = parsed;
|
|
42
|
+
if (parsed.invalidOption !== undefined) {
|
|
43
|
+
refuse(
|
|
44
|
+
`Unknown start option: ${parsed.invalidOption}. Use start <name> [--mandate <file>] [--hours <h>] [--contact <c>] [--trust 1-4] [--json].`,
|
|
45
|
+
{ option: parsed.invalidOption },
|
|
46
|
+
json,
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
const { id } = preflightOwnedAgent(name, json);
|
|
50
|
+
const dir = resolve(agentDir(id));
|
|
51
|
+
const kind = kindOf(dir);
|
|
52
|
+
const terms = kind === 'owner' ? ownerTerms(dir, parsed) : nonOwnerTerms(parsed, kind);
|
|
53
|
+
let jobs;
|
|
54
|
+
try {
|
|
55
|
+
jobs = withFileLock(agentLifecycleLockPath(dir), () => startLocked(dir, id, terms));
|
|
56
|
+
} catch (error) {
|
|
57
|
+
reportLifecycleFailure(error, 'start', name, json);
|
|
58
|
+
}
|
|
59
|
+
const scheduledJobIds = registeredJobIds(jobs);
|
|
60
|
+
// After the lock is released: the provider run writes its runtime scalar under it.
|
|
61
|
+
const firstRun = kind === 'owner' ? requestFirstRun(dir, id, scheduledJobIds) : null;
|
|
62
|
+
reportStarted({ name: currentAgentName(dir), kind, scheduledJobIds, firstRun }, json);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function refuse(message, fields, json) {
|
|
66
|
+
if (json) exitJsonError('invalid_spec', message, fields);
|
|
67
|
+
console.error(`Error: ${message}`);
|
|
68
|
+
process.exit(1);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function nonOwnerTerms({ mandate, hours, contact, trust, json }, kind) {
|
|
72
|
+
if ([mandate, hours, contact, trust].some((value) => value !== undefined)) {
|
|
73
|
+
refuse(`A ${kind} takes no mandate or Owner terms. Start it with no options.`, { kind }, json);
|
|
74
|
+
}
|
|
75
|
+
return { mandate: null, scalars: {} };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// The new or stored mandate, hours and trust, each checked before any write.
|
|
79
|
+
function ownerTerms(dir, { mandate, hours, contact, trust, json }) {
|
|
80
|
+
const text = mandate === undefined ? readMandate(dir) : readMandateFile(mandate, json);
|
|
81
|
+
if (isDraftMandate(text)) {
|
|
82
|
+
refuse(
|
|
83
|
+
'The Owner has no signed mandate. Write the mandate and pass it with --mandate <file>.',
|
|
84
|
+
{ reason: 'mandate_missing' },
|
|
85
|
+
json,
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
const scalars = {};
|
|
89
|
+
const stored = ownerConfig(dir);
|
|
90
|
+
const effectiveHours = hours ?? stored.owner_hours;
|
|
91
|
+
if (!ownerHeartbeatCron(effectiveHours, stored.owner_heartbeat)) {
|
|
92
|
+
refuse(
|
|
93
|
+
`owner_hours "${effectiveHours}" with owner_heartbeat "${stored.owner_heartbeat}" is not a schedule Academy reads. Use "24x7" or "daily|weekdays HH:MM-HH:MM".`,
|
|
94
|
+
{ reason: 'hours_invalid', hours: effectiveHours },
|
|
95
|
+
json,
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
if (hours !== undefined) scalars.owner_hours = `"${hours}"`;
|
|
99
|
+
if (trust !== undefined) {
|
|
100
|
+
if (!Object.hasOwn(TRUST_TERMS, trust)) {
|
|
101
|
+
refuse(
|
|
102
|
+
`--trust must be 1, 2, 3 or 4, not "${trust}".`,
|
|
103
|
+
{ reason: 'trust_invalid', trust },
|
|
104
|
+
json,
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
Object.assign(scalars, TRUST_TERMS[trust]);
|
|
108
|
+
}
|
|
109
|
+
if (contact !== undefined) {
|
|
110
|
+
// One agent.yaml line: a line break would write a second key.
|
|
111
|
+
if (/[\r\n]/.test(contact) || contact.trim() === '') {
|
|
112
|
+
refuse('--contact must be one non-empty line.', { reason: 'contact_invalid' }, json);
|
|
113
|
+
}
|
|
114
|
+
scalars.owner_contact = contact.trim();
|
|
115
|
+
}
|
|
116
|
+
return { mandate: mandate === undefined ? null : text, scalars };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function readMandateFile(path, json) {
|
|
120
|
+
try {
|
|
121
|
+
return readFileSync(path, 'utf8');
|
|
122
|
+
} catch (error) {
|
|
123
|
+
return refuse(`Cannot read mandate file ${path}: ${error.message}`, { path }, json);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// The writes and the registration, under the lifecycle lock. The unlocked
|
|
128
|
+
// scalar writer: `writeAgentYamlScalar` would take this same lock again.
|
|
129
|
+
function startLocked(dir, id, { mandate, scalars }) {
|
|
130
|
+
assertOwnedAgentForDelete(id, dir);
|
|
131
|
+
const before = { mandate: readMandate(dir), yaml: readFileSync(join(dir, 'agent.yaml'), 'utf8') };
|
|
132
|
+
const liveBefore = liveJobIds(dir, kindJobIds(id, dir));
|
|
133
|
+
if (mandate !== null) writeFileSync(join(dir, 'mandate.md'), mandate);
|
|
134
|
+
for (const [key, value] of Object.entries(scalars)) rewriteAgentYamlScalar(dir, key, value);
|
|
135
|
+
if (isStopped(dir)) rewriteAgentYamlScalar(dir, 'stopped', 'false');
|
|
136
|
+
const jobs = registerAcademyJobs(dir, id);
|
|
137
|
+
const failed = jobs.find((job) => !job.registered && !job.skipped);
|
|
138
|
+
if (!failed) return jobs;
|
|
139
|
+
// Back to the files from before this start: the old mandate, terms and `stopped`.
|
|
140
|
+
if (mandate !== null) writeFileSync(join(dir, 'mandate.md'), before.mandate);
|
|
141
|
+
writeFileSync(join(dir, 'agent.yaml'), before.yaml);
|
|
142
|
+
const name = currentAgentName(dir);
|
|
143
|
+
const reasons = [
|
|
144
|
+
`failed to register ${jobLabel(failed.id)} "${failed.id}" for "${name}": ${failed.reason}`,
|
|
145
|
+
...rollBackJobs(dir, id, registeredJobIds(jobs), liveBefore),
|
|
146
|
+
];
|
|
147
|
+
throw new DeleteAgentError('runtime_unavailable', reasons.join('; '), { name, jobId: failed.id });
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Deletes the jobs this start added, then registers the jobs live before it
|
|
151
|
+
// again from the restored files: --merge may already have changed them.
|
|
152
|
+
// Returns each rollback failure as a reason.
|
|
153
|
+
function rollBackJobs(dir, id, registered, liveBefore) {
|
|
154
|
+
const reasons = [];
|
|
155
|
+
const added = registered.filter((jobId) => !liveBefore.includes(jobId));
|
|
156
|
+
const deleted = added.length > 0 ? deleteAcademyJobs(dir, added) : { ok: true };
|
|
157
|
+
if (!deleted.ok) reasons.push(`rollback could not delete "${deleted.id}": ${deleted.reason}`);
|
|
158
|
+
if (liveBefore.length === 0) return reasons;
|
|
159
|
+
const again = registerAcademyJobs(dir, id).find((job) => !job.registered && !job.skipped);
|
|
160
|
+
if (again) {
|
|
161
|
+
reasons.push(
|
|
162
|
+
`rollback could not register "${again.id}" again with the old terms: ${again.reason}`,
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
return reasons;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function reportStarted({ name, kind, scheduledJobIds, firstRun }, json) {
|
|
169
|
+
if (json) {
|
|
170
|
+
contractOk('start', { name, kind, started: true, scheduledJobIds, firstRun });
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
console.log(`Started agent "${name}". Jobs: ${scheduledJobIds.join(', ') || 'none'}.`);
|
|
174
|
+
if (firstRun?.status === 'requested') console.log(`Requested the first daily run.`);
|
|
175
|
+
if (firstRun?.status === 'failed') console.error(firstRun.message);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// `stop` writes `stopped: true` first, so a job a failed delete leaves behind
|
|
179
|
+
// starts no model, then deletes every job by id. The files stay; a later
|
|
180
|
+
// `stop` retries a failed delete.
|
|
181
|
+
export function stopAgent(name, json) {
|
|
182
|
+
const { id, dirReal } = preflightOwnedAgent(name, json);
|
|
183
|
+
try {
|
|
184
|
+
withFileLock(agentLifecycleLockPath(agentDir(id)), () => stopLocked(dirReal, id, json));
|
|
185
|
+
} catch (error) {
|
|
186
|
+
reportLifecycleFailure(error, 'stop', name, json);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function stopLocked(dir, id, json) {
|
|
191
|
+
assertOwnedAgentForDelete(id, dir);
|
|
192
|
+
const name = currentAgentName(dir);
|
|
193
|
+
rewriteAgentYamlScalar(dir, 'stopped', 'true');
|
|
194
|
+
const unscheduled = deleteAcademyJobs(dir, academyJobIds(id, dir));
|
|
195
|
+
if (!unscheduled.ok) throw unscheduleError('stop', name, unscheduled);
|
|
196
|
+
// The Owner's own session jobs are deleted too, but only the jobs Academy
|
|
197
|
+
// registers are reported, the set `start` registers again.
|
|
198
|
+
const deletedJobIds = kindJobIds(id, dir);
|
|
199
|
+
if (json) contractOk('stop', { name, stopped: true, deletedJobIds });
|
|
200
|
+
else console.log(`Stopped agent "${name}". Deleted jobs: ${deletedJobIds.join(', ')}.`);
|
|
201
|
+
}
|
|
@@ -1,6 +1,45 @@
|
|
|
1
|
+
// The first line of every created file names its class: private (never copied
|
|
2
|
+
// to a clone) or craft (transferable). The operating model's "Your files"
|
|
3
|
+
// section states the same rule at length.
|
|
4
|
+
const HEADERS = {
|
|
5
|
+
identity: 'Private: not copied to a clone. Persona and voice. No method.',
|
|
6
|
+
craft: 'Craft: transferable. Do not name a user, repo, system, person, or product.',
|
|
7
|
+
mandate: 'Private: written by the user. Your job, scope, limits, and environment.',
|
|
8
|
+
work: 'Private: written by your nightly run. Goals, priorities, focus, Workstreams.',
|
|
9
|
+
knowledge:
|
|
10
|
+
'Private: what you learned here. Move reusable judgment to craft.md through knowledge curation.',
|
|
11
|
+
notes: 'Private: temporary. Graduate each note or let it expire.',
|
|
12
|
+
'local-rules': "Private: the user's rules for you. They override package skills.",
|
|
13
|
+
user: 'Private: shared by all agents. The curator writes it. Agents add only [proposed] lines.',
|
|
14
|
+
'user-work': 'Private: shared by all agents. Only the curator writes it.',
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
// An agent's work.md and the user's user-work.md use the same sections.
|
|
18
|
+
const WORK_SECTIONS = `## Goals
|
|
19
|
+
|
|
20
|
+
_(An outcome to pursue, with a measure or the word "qualitative".)_
|
|
21
|
+
|
|
22
|
+
## Priorities
|
|
23
|
+
|
|
24
|
+
_(The order of goals and Workstreams, with the reason for each.)_
|
|
25
|
+
|
|
26
|
+
## Focus
|
|
27
|
+
|
|
28
|
+
_(What deserves attention now.)_
|
|
29
|
+
|
|
30
|
+
## Workstreams
|
|
31
|
+
|
|
32
|
+
_(One line each: Beads ID, title, state, owner. Keep the detail in Beads.)_
|
|
33
|
+
|
|
34
|
+
## Open questions
|
|
35
|
+
|
|
36
|
+
_(None yet.)_`;
|
|
37
|
+
|
|
1
38
|
export const TEMPLATES = {
|
|
2
39
|
'identity.md': (name, today) =>
|
|
3
|
-
|
|
40
|
+
`${HEADERS.identity}
|
|
41
|
+
|
|
42
|
+
# Identity
|
|
4
43
|
|
|
5
44
|
_(Who you are — values, character, voice, persona/backstory.)_
|
|
6
45
|
|
|
@@ -11,94 +50,129 @@ _(Hire flow will populate this. See \`academy hire\`.)_
|
|
|
11
50
|
---
|
|
12
51
|
_Created: ${today}._
|
|
13
52
|
`,
|
|
14
|
-
'
|
|
15
|
-
|
|
53
|
+
'craft.md': (name, today) =>
|
|
54
|
+
`${HEADERS.craft}
|
|
55
|
+
|
|
56
|
+
# Craft
|
|
57
|
+
|
|
58
|
+
_(How you work — discipline, method, principles, definition of done, reusable domain judgment.)_
|
|
16
59
|
|
|
17
|
-
|
|
60
|
+
Write in general terms. After creation, text enters this file only through knowledge curation.
|
|
18
61
|
|
|
19
62
|
_(Hire flow will populate this. See \`academy hire\`.)_
|
|
20
63
|
|
|
21
64
|
---
|
|
22
65
|
_Created: ${today}._
|
|
23
66
|
`,
|
|
24
|
-
'
|
|
25
|
-
|
|
67
|
+
'mandate.md': (name, today) =>
|
|
68
|
+
`${HEADERS.mandate}
|
|
26
69
|
|
|
27
|
-
|
|
70
|
+
# Mandate
|
|
28
71
|
|
|
29
|
-
|
|
30
|
-
Curation is by reference + uniqueness + user signal — see scope §7.
|
|
72
|
+
_(Your current job — scope, limits, and the environment you work in: repos, systems, people, products.)_
|
|
31
73
|
|
|
32
|
-
_(Hire flow will populate this
|
|
74
|
+
_(Hire flow will populate this. See \`academy hire\`.)_
|
|
33
75
|
|
|
34
76
|
---
|
|
35
77
|
_Created: ${today}._
|
|
36
78
|
`,
|
|
37
|
-
'
|
|
38
|
-
|
|
79
|
+
'work.md': (name, today) =>
|
|
80
|
+
`${HEADERS.work}
|
|
39
81
|
|
|
40
|
-
|
|
82
|
+
# Work
|
|
41
83
|
|
|
42
|
-
|
|
43
|
-
2. _(goal two)_
|
|
44
|
-
3. _(goal three)_
|
|
84
|
+
${WORK_SECTIONS}
|
|
45
85
|
|
|
46
86
|
---
|
|
47
|
-
_Created: ${today}.
|
|
87
|
+
_Created: ${today}._
|
|
48
88
|
`,
|
|
49
|
-
'
|
|
50
|
-
|
|
89
|
+
'knowledge.md': (name, today) =>
|
|
90
|
+
`${HEADERS.knowledge}
|
|
51
91
|
|
|
52
|
-
|
|
92
|
+
# Knowledge
|
|
53
93
|
|
|
54
|
-
|
|
55
|
-
|
|
94
|
+
_(What you know here — environment facts, domain expertise, learned patterns.)_
|
|
95
|
+
|
|
96
|
+
Lightweight sections by domain (max 5–8). Dated entries within sections.
|
|
97
|
+
Curation is by reference, uniqueness, and user signal.
|
|
98
|
+
|
|
99
|
+
_(Hire flow will populate this from research. See \`academy hire\`.)_
|
|
56
100
|
|
|
57
101
|
---
|
|
58
102
|
_Created: ${today}._
|
|
59
103
|
`,
|
|
60
|
-
'
|
|
61
|
-
|
|
104
|
+
'notes.md': (name, today) =>
|
|
105
|
+
`${HEADERS.notes}
|
|
106
|
+
|
|
107
|
+
# Notes
|
|
62
108
|
|
|
63
|
-
_(
|
|
109
|
+
_(Micro-steering staging area. No daytime limit. Must be at 500 after each nightly: graduate or expire.)_
|
|
64
110
|
|
|
65
|
-
|
|
111
|
+
_Capture temporary steering, corrections, stakeholder facts, caveats, and raw
|
|
112
|
+
learnings here with_ \`academy notes add "..."\` _— it appends a timestamped
|
|
113
|
+
bullet without rewriting this file. Review with_ \`academy notes list\`_._
|
|
66
114
|
|
|
67
115
|
_(none yet)_
|
|
68
116
|
|
|
69
|
-
|
|
117
|
+
---
|
|
118
|
+
_Created: ${today}. Long-staying notes are a smell — they should graduate to knowledge, work, craft, or a skill, or expire._
|
|
119
|
+
`,
|
|
120
|
+
'local-rules.md': (name, today) =>
|
|
121
|
+
`${HEADERS['local-rules']}
|
|
70
122
|
|
|
71
|
-
|
|
123
|
+
# Local Rules
|
|
72
124
|
|
|
73
|
-
|
|
125
|
+
_(Your user's rules for how you work. Each one overrides any package skill
|
|
126
|
+
instruction. Write and edit them with the local-rules skill.)_
|
|
127
|
+
|
|
128
|
+
_Entry format:_
|
|
129
|
+
|
|
130
|
+
\`\`\`
|
|
131
|
+
## <rule text>
|
|
132
|
+
- Source: "<the user's message, quoted>"
|
|
133
|
+
- Date: <YYYY-MM-DD>
|
|
134
|
+
- Pack version: <Academy version when written>
|
|
135
|
+
\`\`\`
|
|
74
136
|
|
|
75
137
|
_(none yet)_
|
|
76
138
|
|
|
77
139
|
---
|
|
78
140
|
_Created: ${today}._
|
|
79
141
|
`,
|
|
80
|
-
|
|
81
|
-
|
|
142
|
+
// The two shared surfaces under ACADEMY_HOME. One file each for the user, so
|
|
143
|
+
// they carry no agent name. user.md grammar: `##` groups, `###` labels, `-`
|
|
144
|
+
// statements. Its last section is Proposed, because `academy notes add --user`
|
|
145
|
+
// appends one line at the end of the file.
|
|
146
|
+
'user.md': () =>
|
|
147
|
+
`${HEADERS.user}
|
|
82
148
|
|
|
83
|
-
|
|
149
|
+
# User
|
|
84
150
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
bullet without rewriting this file. Review with_ \`academy notes list\`_._
|
|
151
|
+
## Identity
|
|
152
|
+
- _(Who the user is, what the user does and owns, how and when the user works.)_
|
|
88
153
|
|
|
89
|
-
|
|
154
|
+
## Preferences
|
|
155
|
+
### Answers
|
|
156
|
+
- _(How the user wants answers, plans, and reports written.)_
|
|
157
|
+
### Changes
|
|
158
|
+
- _(What the user wants asked first, and what to just do.)_
|
|
90
159
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
`,
|
|
94
|
-
'dailys.md': (name, today) =>
|
|
95
|
-
`# Recent Days
|
|
160
|
+
## Feed
|
|
161
|
+
- _(Last 7 days. Promote or drop each item weekly.)_
|
|
96
162
|
|
|
97
|
-
|
|
163
|
+
## Scoped rules
|
|
164
|
+
- _(A rule the user set for some agents or one project.)_
|
|
98
165
|
|
|
99
|
-
|
|
166
|
+
## Projects
|
|
167
|
+
- _(The lead agent for each project. Details are in user-work.md.)_
|
|
100
168
|
|
|
101
|
-
|
|
102
|
-
|
|
169
|
+
## Proposed
|
|
170
|
+
`,
|
|
171
|
+
'user-work.md': () =>
|
|
172
|
+
`${HEADERS['user-work']}
|
|
173
|
+
|
|
174
|
+
# User work
|
|
175
|
+
|
|
176
|
+
${WORK_SECTIONS}
|
|
103
177
|
`,
|
|
104
178
|
};
|