@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.
Files changed (121) hide show
  1. package/.claude-plugin/marketplace.json +5 -0
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/CHANGELOG.md +390 -0
  4. package/README.md +67 -122
  5. package/hooks/backstory/.claude-plugin/plugin.json +11 -0
  6. package/hooks/backstory/SOURCE +2 -0
  7. package/hooks/backstory/bin/backstory +9 -0
  8. package/hooks/backstory/hooks/hooks.json +45 -0
  9. package/hooks/backstory/scripts/knowledge.mjs +87 -0
  10. package/hooks/backstory/scripts/memory.mjs +685 -0
  11. package/hooks/backstory/scripts/tool-tick.mjs +1 -0
  12. package/hooks/backstory/skills/capture/SKILL.md +71 -0
  13. package/hooks/backstory/skills/capture/references/knowledge-capture-input.json +16 -0
  14. package/hooks/backstory/skills/capture/references/tagging-policy.md +29 -0
  15. package/hooks/backstory/skills/memory/SKILL.md +70 -0
  16. package/hooks/backstory/skills/setup/SKILL.md +85 -0
  17. package/hooks/context_metrics.mjs +67 -0
  18. package/hooks/hook_runtime.mjs +43 -2
  19. package/hooks/memory_bridge.mjs +286 -52
  20. package/hooks/memory_store.mjs +78 -26
  21. package/hooks/register_session.mjs +76 -14
  22. package/hooks/roque/.claude-plugin/plugin.json +6 -0
  23. package/hooks/roque/LICENSE +21 -0
  24. package/hooks/roque/SOURCE +1 -0
  25. package/hooks/roque/hooks.json +38 -0
  26. package/hooks/roque/package.json +3 -0
  27. package/hooks/roque/roque.js +1690 -0
  28. package/hooks/sync_memory.mjs +11 -7
  29. package/package.json +7 -3
  30. package/scripts/cli/agent-gate.mjs +70 -0
  31. package/scripts/cli/agent-id.mjs +37 -0
  32. package/scripts/cli/archive.mjs +39 -38
  33. package/scripts/cli/archived.mjs +4 -2
  34. package/scripts/cli/args-kinds.mjs +33 -0
  35. package/scripts/cli/args-notes.mjs +42 -0
  36. package/scripts/cli/args-owner.mjs +37 -0
  37. package/scripts/cli/args-run.mjs +38 -0
  38. package/scripts/cli/args.mjs +47 -104
  39. package/scripts/cli/beads.mjs +57 -0
  40. package/scripts/cli/codex.mjs +66 -12
  41. package/scripts/cli/core.mjs +117 -41
  42. package/scripts/cli/create.mjs +224 -76
  43. package/scripts/cli/curator-feed-codex.mjs +81 -0
  44. package/scripts/cli/curator-feed-observations.mjs +97 -0
  45. package/scripts/cli/curator-feed-scope.mjs +124 -0
  46. package/scripts/cli/curator-feed-sources.mjs +172 -0
  47. package/scripts/cli/curator-feed.mjs +318 -0
  48. package/scripts/cli/default-name.mjs +73 -0
  49. package/scripts/cli/doctor.mjs +67 -2
  50. package/scripts/cli/eventlog.mjs +6 -2
  51. package/scripts/cli/exit-code.mjs +9 -0
  52. package/scripts/cli/first-run.mjs +115 -0
  53. package/scripts/cli/heartbeat.mjs +135 -0
  54. package/scripts/cli/helm-skip.mjs +20 -0
  55. package/scripts/cli/hire-headless.mjs +37 -22
  56. package/scripts/cli/hire.mjs +8 -2
  57. package/scripts/cli/inspect.mjs +36 -18
  58. package/scripts/cli/lifecycle.mjs +95 -49
  59. package/scripts/cli/main.mjs +32 -2
  60. package/scripts/cli/memory-env.mjs +124 -0
  61. package/scripts/cli/migrate.mjs +13 -7
  62. package/scripts/cli/next-steps.mjs +13 -0
  63. package/scripts/cli/nightly-launch.mjs +118 -0
  64. package/scripts/cli/nightly.mjs +228 -0
  65. package/scripts/cli/notes.mjs +58 -8
  66. package/scripts/cli/owner-config.mjs +181 -0
  67. package/scripts/cli/owner-jobs.mjs +201 -0
  68. package/scripts/cli/prompt-append.mjs +135 -0
  69. package/scripts/cli/rename.mjs +90 -109
  70. package/scripts/cli/run-context.mjs +50 -0
  71. package/scripts/cli/run.mjs +236 -79
  72. package/scripts/cli/scaffold.mjs +119 -126
  73. package/scripts/cli/schedule-errors.mjs +70 -0
  74. package/scripts/cli/sessions.mjs +10 -3
  75. package/scripts/cli/skills-check.mjs +147 -0
  76. package/scripts/cli/skills.mjs +170 -0
  77. package/scripts/cli/start.mjs +201 -0
  78. package/scripts/cli/templates.mjs +119 -45
  79. package/scripts/cli/tldr.mjs +184 -0
  80. package/scripts/cli/toml-string.mjs +76 -0
  81. package/scripts/cli/user-work-view.mjs +48 -0
  82. package/scripts/cli/writable-dirs.mjs +23 -0
  83. package/scripts/cli/yaml.mjs +1 -1
  84. package/skills/hire/SKILL.md +112 -100
  85. package/skills/hire/references/owner.md +68 -0
  86. package/templates/agents/claude-code/knowledge-curator.md +5 -2
  87. package/templates/agents/codex/knowledge-curator.toml +2 -1
  88. package/templates/operating-model/base.md +78 -0
  89. package/templates/operating-model/owner.md +11 -0
  90. package/templates/operating-model/principal.md +9 -0
  91. package/templates/operating-model/specialist.md +9 -0
  92. package/templates/packs/owner/craft.md +32 -0
  93. package/templates/packs/owner/identity.md +13 -0
  94. package/templates/packs/owner/mandate.md +26 -0
  95. package/templates/packs/owner/owner/SKILL.md +118 -0
  96. package/templates/packs/owner/owner-feedback/SKILL.md +49 -0
  97. package/templates/packs/owner/owner-heartbeat/SKILL.md +35 -0
  98. package/templates/packs/owner/owner-nightly/SKILL.md +114 -0
  99. package/templates/packs/owner/owner-review/SKILL.md +70 -0
  100. package/templates/packs/owner/owner-work/SKILL.md +53 -0
  101. package/templates/packs/principal/craft.md +27 -0
  102. package/templates/packs/principal/identity.md +13 -0
  103. package/templates/packs/principal/mandate.md +23 -0
  104. package/templates/packs/principal/principal-brief/SKILL.md +44 -0
  105. package/templates/packs/principal/principal-intro/SKILL.md +95 -0
  106. package/templates/packs/principal/principal-nightly/SKILL.md +152 -0
  107. package/templates/skills/check-in/SKILL.md +34 -24
  108. package/templates/skills/knowledge-curation/SKILL.md +89 -48
  109. package/templates/skills/local-rules/SKILL.md +86 -0
  110. package/templates/skills/nightly-consolidation/SKILL.md +131 -100
  111. package/templates/skills/skill-method/SKILL.md +79 -0
  112. package/templates/skills/user-nightly/SKILL.md +143 -0
  113. package/templates/skills/work-graph/SKILL.md +75 -0
  114. package/conformance/README.md +0 -60
  115. package/conformance/discovery.test.mjs +0 -140
  116. package/conformance/envelope.test.mjs +0 -185
  117. package/conformance/error-codes.test.mjs +0 -125
  118. package/conformance/harness.mjs +0 -180
  119. package/conformance/identity.test.mjs +0 -125
  120. package/docs/integration-guide.md +0 -1042
  121. 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
- `# Identity
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
- 'role.md': (name, today) =>
15
- `# Role
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
- _(What you do — job, responsibilities, scope, deliverable shape, cadence.)_
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
- 'knowledge.md': (name, today) =>
25
- `# Knowledge
67
+ 'mandate.md': (name, today) =>
68
+ `${HEADERS.mandate}
26
69
 
27
- _(What you know — domain expertise, mental models, frameworks, learned patterns.)_
70
+ # Mandate
28
71
 
29
- Lightweight sections by domain (max 5–8). Dated entries within sections.
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 from research. See \`academy hire\`.)_
74
+ _(Hire flow will populate this. See \`academy hire\`.)_
33
75
 
34
76
  ---
35
77
  _Created: ${today}._
36
78
  `,
37
- 'goals.md': (name, today) =>
38
- `# Goals
79
+ 'work.md': (name, today) =>
80
+ `${HEADERS.work}
39
81
 
40
- _(Strategic direction — quarterly horizon, hard cap of 3.)_
82
+ # Work
41
83
 
42
- 1. _(goal one)_
43
- 2. _(goal two)_
44
- 3. _(goal three)_
84
+ ${WORK_SECTIONS}
45
85
 
46
86
  ---
47
- _Created: ${today}. Re-affirm every 14 days._
87
+ _Created: ${today}._
48
88
  `,
49
- 'priorities.md': (name, today) =>
50
- `# Priorities
89
+ 'knowledge.md': (name, today) =>
90
+ `${HEADERS.knowledge}
51
91
 
52
- _(Weekly direction — WIP-limited, 3–5 visible.)_
92
+ # Knowledge
53
93
 
54
- - _(priority one)_
55
- - _(priority two)_
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
- 'threads.md': (name, today) =>
61
- `# Threads
104
+ 'notes.md': (name, today) =>
105
+ `${HEADERS.notes}
106
+
107
+ # Notes
62
108
 
63
- _(Active work pursuits — 5 active visible / 8 idle. Auto-demote by \`last_touched\`.)_
109
+ _(Micro-steering staging area. No daytime limit. Must be at 500 after each nightly: graduate or expire.)_
64
110
 
65
- ## Active
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
- ## Idle
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
- _(none yet)_
123
+ # Local Rules
72
124
 
73
- ## Parked
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
- 'notes.md': (name, today) =>
81
- `# Notes
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
- _(Micro-steering staging area — 8–12 visible cap. Graduate or expire.)_
149
+ # User
84
150
 
85
- _Capture temporary steering, corrections, stakeholder facts, caveats, and raw
86
- learnings here with_ \`academy notes add "..."\` _— it appends a timestamped
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
- _(none yet)_
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
- _Created: ${today}. Long-staying notes are a smell — they should graduate to knowledge / role / identity / skill, or expire._
93
- `,
94
- 'dailys.md': (name, today) =>
95
- `# Recent Days
160
+ ## Feed
161
+ - _(Last 7 days. Promote or drop each item weekly.)_
96
162
 
97
- _(Tight summaries, last 7 working days, FIFO.)_
163
+ ## Scoped rules
164
+ - _(A rule the user set for some agents or one project.)_
98
165
 
99
- _(none yet — populated by daily primitive in Phase 3.)_
166
+ ## Projects
167
+ - _(The lead agent for each project. Details are in user-work.md.)_
100
168
 
101
- ---
102
- _Created: ${today}._
169
+ ## Proposed
170
+ `,
171
+ 'user-work.md': () =>
172
+ `${HEADERS['user-work']}
173
+
174
+ # User work
175
+
176
+ ${WORK_SECTIONS}
103
177
  `,
104
178
  };