@joenandez/academy 0.4.0 → 0.5.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +5 -0
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +390 -0
- package/README.md +67 -122
- package/hooks/backstory/.claude-plugin/plugin.json +11 -0
- package/hooks/backstory/SOURCE +2 -0
- package/hooks/backstory/bin/backstory +9 -0
- package/hooks/backstory/hooks/hooks.json +45 -0
- package/hooks/backstory/scripts/knowledge.mjs +87 -0
- package/hooks/backstory/scripts/memory.mjs +685 -0
- package/hooks/backstory/scripts/tool-tick.mjs +1 -0
- package/hooks/backstory/skills/capture/SKILL.md +71 -0
- package/hooks/backstory/skills/capture/references/knowledge-capture-input.json +16 -0
- package/hooks/backstory/skills/capture/references/tagging-policy.md +29 -0
- package/hooks/backstory/skills/memory/SKILL.md +70 -0
- package/hooks/backstory/skills/setup/SKILL.md +85 -0
- package/hooks/context_metrics.mjs +67 -0
- package/hooks/hook_runtime.mjs +43 -2
- package/hooks/memory_bridge.mjs +286 -52
- package/hooks/memory_store.mjs +78 -26
- package/hooks/register_session.mjs +76 -14
- package/hooks/roque/.claude-plugin/plugin.json +6 -0
- package/hooks/roque/LICENSE +21 -0
- package/hooks/roque/SOURCE +1 -0
- package/hooks/roque/hooks.json +38 -0
- package/hooks/roque/package.json +3 -0
- package/hooks/roque/roque.js +1690 -0
- package/hooks/sync_memory.mjs +11 -7
- package/package.json +7 -3
- package/scripts/cli/agent-gate.mjs +70 -0
- package/scripts/cli/agent-id.mjs +37 -0
- package/scripts/cli/archive.mjs +39 -38
- package/scripts/cli/archived.mjs +4 -2
- package/scripts/cli/args-kinds.mjs +33 -0
- package/scripts/cli/args-notes.mjs +42 -0
- package/scripts/cli/args-owner.mjs +37 -0
- package/scripts/cli/args-run.mjs +38 -0
- package/scripts/cli/args.mjs +47 -104
- package/scripts/cli/beads.mjs +57 -0
- package/scripts/cli/codex.mjs +66 -12
- package/scripts/cli/core.mjs +117 -41
- package/scripts/cli/create.mjs +224 -76
- package/scripts/cli/curator-feed-codex.mjs +81 -0
- package/scripts/cli/curator-feed-observations.mjs +97 -0
- package/scripts/cli/curator-feed-scope.mjs +124 -0
- package/scripts/cli/curator-feed-sources.mjs +172 -0
- package/scripts/cli/curator-feed.mjs +318 -0
- package/scripts/cli/default-name.mjs +73 -0
- package/scripts/cli/doctor.mjs +67 -2
- package/scripts/cli/eventlog.mjs +6 -2
- package/scripts/cli/exit-code.mjs +9 -0
- package/scripts/cli/first-run.mjs +115 -0
- package/scripts/cli/heartbeat.mjs +135 -0
- package/scripts/cli/helm-skip.mjs +20 -0
- package/scripts/cli/hire-headless.mjs +37 -22
- package/scripts/cli/hire.mjs +8 -2
- package/scripts/cli/inspect.mjs +36 -18
- package/scripts/cli/lifecycle.mjs +95 -49
- package/scripts/cli/main.mjs +32 -2
- package/scripts/cli/memory-env.mjs +124 -0
- package/scripts/cli/migrate.mjs +13 -7
- package/scripts/cli/next-steps.mjs +13 -0
- package/scripts/cli/nightly-launch.mjs +118 -0
- package/scripts/cli/nightly.mjs +228 -0
- package/scripts/cli/notes.mjs +58 -8
- package/scripts/cli/owner-config.mjs +181 -0
- package/scripts/cli/owner-jobs.mjs +201 -0
- package/scripts/cli/prompt-append.mjs +135 -0
- package/scripts/cli/rename.mjs +90 -109
- package/scripts/cli/run-context.mjs +50 -0
- package/scripts/cli/run.mjs +236 -79
- package/scripts/cli/scaffold.mjs +119 -126
- package/scripts/cli/schedule-errors.mjs +70 -0
- package/scripts/cli/sessions.mjs +10 -3
- package/scripts/cli/skills-check.mjs +147 -0
- package/scripts/cli/skills.mjs +170 -0
- package/scripts/cli/start.mjs +201 -0
- package/scripts/cli/templates.mjs +119 -45
- package/scripts/cli/tldr.mjs +184 -0
- package/scripts/cli/toml-string.mjs +76 -0
- package/scripts/cli/user-work-view.mjs +48 -0
- package/scripts/cli/writable-dirs.mjs +23 -0
- package/scripts/cli/yaml.mjs +1 -1
- package/skills/hire/SKILL.md +112 -100
- package/skills/hire/references/owner.md +68 -0
- package/templates/agents/claude-code/knowledge-curator.md +5 -2
- package/templates/agents/codex/knowledge-curator.toml +2 -1
- package/templates/operating-model/base.md +78 -0
- package/templates/operating-model/owner.md +11 -0
- package/templates/operating-model/principal.md +9 -0
- package/templates/operating-model/specialist.md +9 -0
- package/templates/packs/owner/craft.md +32 -0
- package/templates/packs/owner/identity.md +13 -0
- package/templates/packs/owner/mandate.md +26 -0
- package/templates/packs/owner/owner/SKILL.md +118 -0
- package/templates/packs/owner/owner-feedback/SKILL.md +49 -0
- package/templates/packs/owner/owner-heartbeat/SKILL.md +35 -0
- package/templates/packs/owner/owner-nightly/SKILL.md +114 -0
- package/templates/packs/owner/owner-review/SKILL.md +70 -0
- package/templates/packs/owner/owner-work/SKILL.md +53 -0
- package/templates/packs/principal/craft.md +27 -0
- package/templates/packs/principal/identity.md +13 -0
- package/templates/packs/principal/mandate.md +23 -0
- package/templates/packs/principal/principal-brief/SKILL.md +44 -0
- package/templates/packs/principal/principal-intro/SKILL.md +95 -0
- package/templates/packs/principal/principal-nightly/SKILL.md +152 -0
- package/templates/skills/check-in/SKILL.md +34 -24
- package/templates/skills/knowledge-curation/SKILL.md +89 -48
- package/templates/skills/local-rules/SKILL.md +86 -0
- package/templates/skills/nightly-consolidation/SKILL.md +131 -100
- package/templates/skills/skill-method/SKILL.md +79 -0
- package/templates/skills/user-nightly/SKILL.md +143 -0
- package/templates/skills/work-graph/SKILL.md +75 -0
- package/conformance/README.md +0 -60
- package/conformance/discovery.test.mjs +0 -140
- package/conformance/envelope.test.mjs +0 -185
- package/conformance/error-codes.test.mjs +0 -125
- package/conformance/harness.mjs +0 -180
- package/conformance/identity.test.mjs +0 -125
- package/docs/integration-guide.md +0 -1042
- package/templates/skills/self-update/SKILL.md +0 -121
package/scripts/cli/run.mjs
CHANGED
|
@@ -1,19 +1,19 @@
|
|
|
1
|
-
import { existsSync
|
|
2
|
-
import { basename, resolve } from 'node:path';
|
|
3
|
-
import {
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { basename, join, resolve } from 'node:path';
|
|
3
|
+
import { isatty } from 'node:tty';
|
|
4
|
+
import { bridgeEnvPresent, bridgeLauncher, MEMORY_BRIDGE_ENV } from '../../hooks/memory_bridge.mjs';
|
|
4
5
|
import { pendingMarkerPath } from '../../hooks/memory_store.mjs';
|
|
5
6
|
import {
|
|
6
7
|
codexRuntimeContextPath,
|
|
7
8
|
launchCodex,
|
|
8
9
|
readJsonFile,
|
|
10
|
+
withRepoCheck,
|
|
9
11
|
writeCodexHooks,
|
|
10
12
|
writeCodexProfile,
|
|
11
13
|
} from './codex.mjs';
|
|
12
14
|
import {
|
|
15
|
+
ACADEMY_ROOT,
|
|
13
16
|
CLI_NAME,
|
|
14
|
-
ACADEMY_CLI_PATH,
|
|
15
|
-
SCHEDULED_CLAUDE_PERMISSION_ARGS,
|
|
16
|
-
SCHEDULED_CODEX_PERMISSION_ARGS,
|
|
17
17
|
LockTimeoutError,
|
|
18
18
|
agentDir,
|
|
19
19
|
delegateLegacyRun,
|
|
@@ -21,17 +21,56 @@ import {
|
|
|
21
21
|
legacyAcademyRoot,
|
|
22
22
|
validateName,
|
|
23
23
|
} from './core.mjs';
|
|
24
|
-
import {
|
|
24
|
+
import { agentIdOf, currentAgentName } from './agent-id.mjs';
|
|
25
|
+
import { refuseNotStarted } from './agent-gate.mjs';
|
|
26
|
+
import { beadsEnv } from './beads.mjs';
|
|
27
|
+
import { INTRO_PENDING_MARKER, jobLabel, registerAcademyJobs } from './create.mjs';
|
|
25
28
|
import { launchClaude } from './hire.mjs';
|
|
29
|
+
import {
|
|
30
|
+
MEMORY_RELAUNCH_FLAG,
|
|
31
|
+
memoryEnvRecord,
|
|
32
|
+
relaunchWithMemoryEnv,
|
|
33
|
+
resolveMemoryEnv,
|
|
34
|
+
} from './memory-env.mjs';
|
|
35
|
+
import {
|
|
36
|
+
NIGHTLY_JOB_FLAG,
|
|
37
|
+
nightlySkillFor,
|
|
38
|
+
recordNightlyOutcome,
|
|
39
|
+
removeMarkerIfUnchanged,
|
|
40
|
+
skipNightlyWithoutMarker,
|
|
41
|
+
skipNightlyWithoutWork,
|
|
42
|
+
sweepBeforeNightly,
|
|
43
|
+
} from './nightly.mjs';
|
|
44
|
+
import { launchNightlyChild, nightlyChildEnv } from './nightly-launch.mjs';
|
|
45
|
+
import { appendedPromptLaunch } from './prompt-append.mjs';
|
|
46
|
+
import { printHelmSkip } from './helm-skip.mjs';
|
|
47
|
+
import { guardOwnerHeartbeat } from './heartbeat.mjs';
|
|
48
|
+
import { applyRunContext } from './run-context.mjs';
|
|
26
49
|
import { readRuntimeProvider, toRuntimeProvider, toRuntimeToken } from './runtime.mjs';
|
|
50
|
+
import { bundledTldrPluginRoot, schedulerJobId } from './tldr.mjs';
|
|
51
|
+
import { logUserWorkView } from './user-work-view.mjs';
|
|
52
|
+
import { codexWritableDirArgs, sharedWritableDirArgs } from './writable-dirs.mjs';
|
|
27
53
|
import { AgentSpecError, writeAgentYamlScalar } from './yaml.mjs';
|
|
28
54
|
import {
|
|
29
55
|
renderAcademySystemPrompt,
|
|
56
|
+
scaffoldLocalRules,
|
|
30
57
|
writeProjectCodexSkillBridge,
|
|
31
58
|
writeProjectPluginInstance,
|
|
32
59
|
writeSkillsScaffold,
|
|
33
60
|
} from './scaffold.mjs';
|
|
34
61
|
|
|
62
|
+
// The `roque` plugin replaces any installed `roque` for the session, so Roque
|
|
63
|
+
// never runs twice next to a normal-session install. Two more plugins load only
|
|
64
|
+
// on a machine without the Mac's installs: the bundled tldr plugin (tldr did not
|
|
65
|
+
// resolve from a Claude plugin install) and Backstory (no memory wrapper).
|
|
66
|
+
export function claudePluginArgs(pluginDir, dir, env = process.env, academyRoot = ACADEMY_ROOT) {
|
|
67
|
+
const dirs = [pluginDir, join(academyRoot, 'hooks', 'roque')];
|
|
68
|
+
const tldr = bundledTldrPluginRoot(env, academyRoot);
|
|
69
|
+
if (tldr) dirs.push(tldr);
|
|
70
|
+
if (!bridgeLauncher(env)) dirs.push(join(academyRoot, 'hooks', 'backstory'));
|
|
71
|
+
return [...dirs.flatMap((d) => ['--plugin-dir', d]), ...sharedWritableDirArgs(dir)];
|
|
72
|
+
}
|
|
73
|
+
|
|
35
74
|
// The agent's runtime is only knowable once the agent is resolved. A bare run
|
|
36
75
|
// reads what the agent already declared, so it can neither launch the wrong
|
|
37
76
|
// provider nor re-register the nightly job under one. Only an explicit --agent
|
|
@@ -56,50 +95,130 @@ function persistRuntime(dir, requested) {
|
|
|
56
95
|
}
|
|
57
96
|
}
|
|
58
97
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
98
|
+
// A scheduler job whose agent is not here (a job registered under another
|
|
99
|
+
// AGENTS_ROOT) is a decision to skip, not a failed start: a failed start makes
|
|
100
|
+
// the scheduler launch a fallback session that lacks Academy's context. A
|
|
101
|
+
// person's run has no job id and still fails with the not-found message.
|
|
102
|
+
function skipMissingAgentUnderJob(name) {
|
|
103
|
+
const jobId = schedulerJobId();
|
|
104
|
+
if (!jobId) return;
|
|
105
|
+
console.error(
|
|
106
|
+
JSON.stringify({
|
|
107
|
+
event: 'run.agent_missing',
|
|
108
|
+
status: 'warn',
|
|
109
|
+
params: { agent: name, jobId },
|
|
110
|
+
result: { skip: 'agent_not_found' },
|
|
111
|
+
}),
|
|
112
|
+
);
|
|
113
|
+
printHelmSkip('agent_not_found', name);
|
|
114
|
+
process.exit(0);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// A new Principal's first interactive session starts with one plain sentence
|
|
118
|
+
// that points at its own rendered intro skill by path: a command name resolves
|
|
119
|
+
// differently in Claude project mode and Codex project mode. Any passthrough
|
|
120
|
+
// at all means a caller (a schedule, a resume, a host) already chose the first
|
|
121
|
+
// message, so only an empty one is seeded. No terminal, no seed.
|
|
122
|
+
export function introSeed({ dir, runtime, passthrough, isTTY }) {
|
|
123
|
+
if (!isTTY || passthrough.length > 0 || !existsSync(join(dir, INTRO_PENDING_MARKER))) return;
|
|
124
|
+
const surface = runtime === 'codex' ? '.agents' : '.claude';
|
|
125
|
+
return [
|
|
126
|
+
`Start your first-session intro: read and follow ${dir}/${surface}/skills/principal-intro/SKILL.md`,
|
|
127
|
+
];
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function seedFirstSession(dir, name, runtime, passthrough) {
|
|
131
|
+
const seed = introSeed({ dir, runtime, passthrough, isTTY: isatty(0) });
|
|
132
|
+
if (!seed) return passthrough;
|
|
133
|
+
console.error(JSON.stringify({ event: 'run.intro_seed', params: { agent: name, runtime } }));
|
|
134
|
+
return seed;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// `id` keys everything that must survive a name change: the heartbeat's Beads
|
|
138
|
+
// label, the Beads actor, job registration, the relaunch and per-agent paths.
|
|
139
|
+
// The current name is what the session is told it is.
|
|
140
|
+
function prepareAgentRun(id, requestedRuntime, passthrough) {
|
|
141
|
+
validateName(id);
|
|
142
|
+
const dir = agentDir(id);
|
|
62
143
|
if (!existsSync(dir)) {
|
|
63
|
-
|
|
64
|
-
console.error(`
|
|
144
|
+
skipMissingAgentUnderJob(id);
|
|
145
|
+
console.error(`Agent "${id}" not found at ${dir}`);
|
|
146
|
+
console.error(`Run \`${CLI_NAME} create ${id}\` or \`${CLI_NAME} hire\` first.`);
|
|
65
147
|
process.exit(1);
|
|
66
148
|
}
|
|
67
149
|
|
|
68
150
|
const legacyRoot = legacyAcademyRoot(dir);
|
|
69
151
|
if (legacyRoot) {
|
|
70
|
-
delegateLegacyRun(
|
|
152
|
+
delegateLegacyRun(id, passthrough, legacyRoot);
|
|
71
153
|
return null;
|
|
72
154
|
}
|
|
155
|
+
const name = currentAgentName(dir);
|
|
73
156
|
|
|
157
|
+
// Before any model session, and once: the memory relaunch's child inherits
|
|
158
|
+
// the parent's decision. Runs of one agent may overlap; only a scheduled
|
|
159
|
+
// Owner heartbeat checks first whether it has anything to do.
|
|
160
|
+
const relaunchRuntime = requestedRuntime ?? toRuntimeToken(readRuntimeProvider(dir));
|
|
161
|
+
if (process.env[MEMORY_RELAUNCH_FLAG] !== '1') {
|
|
162
|
+
guardOwnerHeartbeat(dir, id);
|
|
163
|
+
// Seeded before the relaunch so the child receives it as its passthrough
|
|
164
|
+
// and, under the flag, does not decide again.
|
|
165
|
+
passthrough = seedFirstSession(dir, id, relaunchRuntime, passthrough);
|
|
166
|
+
}
|
|
167
|
+
relaunchWithMemoryEnv({
|
|
168
|
+
dir,
|
|
169
|
+
name: id,
|
|
170
|
+
passthrough,
|
|
171
|
+
requestedRuntime,
|
|
172
|
+
runtime: relaunchRuntime,
|
|
173
|
+
});
|
|
74
174
|
const runtime = resolveRuntime(dir, requestedRuntime);
|
|
175
|
+
scaffoldLocalRules(dir, name);
|
|
75
176
|
writeSkillsScaffold(dir, name);
|
|
76
|
-
const systemPromptPath = renderAcademySystemPrompt(dir, name);
|
|
177
|
+
const { promptPath: systemPromptPath, surfaces } = renderAcademySystemPrompt(dir, name);
|
|
178
|
+
logUserWorkView(name, surfaces);
|
|
77
179
|
const projectDir = resolve(process.cwd());
|
|
78
180
|
const inAgentHome = isInside(projectDir, dir);
|
|
79
181
|
const codex =
|
|
80
182
|
runtime === 'codex'
|
|
81
|
-
? { ...writeCodexProfile(dir,
|
|
183
|
+
? { ...writeCodexProfile(dir, id), runtimeContextPath: codexRuntimeContextPath() }
|
|
82
184
|
: undefined;
|
|
83
185
|
if (codex) writeCodexHooks(codex.runtimeContextPath);
|
|
84
|
-
const
|
|
85
|
-
|
|
86
|
-
console.error(
|
|
87
|
-
`Warning: nightly consolidation job "${nightlyTask.id}" not updated: ${nightlyTask.reason}`,
|
|
88
|
-
);
|
|
186
|
+
const jobs = requestedRuntime ? registerAcademyJobs(dir, id) : [];
|
|
187
|
+
for (const job of jobs.filter(({ registered, skipped }) => !registered && !skipped)) {
|
|
188
|
+
console.error(`Warning: ${jobLabel(job.id)} "${job.id}" not updated: ${job.reason}`);
|
|
89
189
|
}
|
|
90
190
|
|
|
191
|
+
const { [MEMORY_RELAUNCH_FLAG]: _relaunched, ...inherited } = process.env;
|
|
192
|
+
// A session's Beads actor names this process, which waits on the provider,
|
|
193
|
+
// so the actor lives exactly as long as the session.
|
|
91
194
|
const env = {
|
|
92
|
-
...
|
|
195
|
+
...inherited,
|
|
93
196
|
ACADEMY_AGENT_DIR: dir,
|
|
94
197
|
ACADEMY_AGENT_NAME: name,
|
|
198
|
+
// One compaction point per agent session; Roque's hints fire at fractions of it.
|
|
199
|
+
CLAUDE_CODE_AUTO_COMPACT_WINDOW: inherited.CLAUDE_CODE_AUTO_COMPACT_WINDOW || '430000',
|
|
200
|
+
// The client's memory is always on in a topic, but a scheduled job's
|
|
201
|
+
// wrapper sets only the topic; without the flag no turn is captured or recorded.
|
|
202
|
+
...(bridgeEnvPresent(inherited) && {
|
|
203
|
+
[MEMORY_BRIDGE_ENV.enabled]: inherited[MEMORY_BRIDGE_ENV.enabled] || '1',
|
|
204
|
+
}),
|
|
205
|
+
...beadsEnv(`${id}@${process.pid}`),
|
|
95
206
|
};
|
|
96
207
|
return { codex, dir, env, inAgentHome, name, passthrough, projectDir, runtime, systemPromptPath };
|
|
97
208
|
}
|
|
98
209
|
|
|
99
210
|
function launchInAgentHome(context) {
|
|
100
211
|
const { codex, dir, env, name, passthrough, systemPromptPath } = context;
|
|
212
|
+
const { promptArgs, passthrough: rest } = appendedPromptLaunch({
|
|
213
|
+
runtime: codex ? 'codex' : 'claude',
|
|
214
|
+
promptPath: systemPromptPath,
|
|
215
|
+
passthrough,
|
|
216
|
+
});
|
|
101
217
|
if (!codex) {
|
|
102
|
-
|
|
218
|
+
// The home is its own plugin root: `create` links `.claude-plugin` and
|
|
219
|
+
// `hooks` into it. Claude Code loads a plugin only when told to, so without
|
|
220
|
+
// this flag a home run registers no session and syncs no memory.
|
|
221
|
+
launchClaude([...claudePluginArgs(dir, dir), ...promptArgs, ...rest], {
|
|
103
222
|
cwd: dir,
|
|
104
223
|
env,
|
|
105
224
|
message: `Launching ${name} with Claude Code in agent home (${dir})`,
|
|
@@ -112,11 +231,9 @@ function launchInAgentHome(context) {
|
|
|
112
231
|
codex.profileName,
|
|
113
232
|
'-C',
|
|
114
233
|
dir,
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
`model_instructions_file=${JSON.stringify(systemPromptPath)}`,
|
|
119
|
-
...passthrough,
|
|
234
|
+
...codexWritableDirArgs(dir),
|
|
235
|
+
...promptArgs,
|
|
236
|
+
...withRepoCheck(rest),
|
|
120
237
|
],
|
|
121
238
|
{ cwd: dir, env, message: `Launching ${name} with Codex in agent home (${dir})`, ...codex },
|
|
122
239
|
);
|
|
@@ -124,6 +241,11 @@ function launchInAgentHome(context) {
|
|
|
124
241
|
|
|
125
242
|
function launchInProject(context) {
|
|
126
243
|
const { codex, dir, env, name, passthrough, projectDir, systemPromptPath } = context;
|
|
244
|
+
const { promptArgs, passthrough: rest } = appendedPromptLaunch({
|
|
245
|
+
runtime: codex ? 'codex' : 'claude',
|
|
246
|
+
promptPath: systemPromptPath,
|
|
247
|
+
passthrough,
|
|
248
|
+
});
|
|
127
249
|
if (codex) {
|
|
128
250
|
writeProjectCodexSkillBridge(projectDir, dir);
|
|
129
251
|
launchCodex(
|
|
@@ -132,11 +254,9 @@ function launchInProject(context) {
|
|
|
132
254
|
codex.profileName,
|
|
133
255
|
'-C',
|
|
134
256
|
projectDir,
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
`model_instructions_file=${JSON.stringify(systemPromptPath)}`,
|
|
139
|
-
...passthrough,
|
|
257
|
+
...codexWritableDirArgs(dir),
|
|
258
|
+
...promptArgs,
|
|
259
|
+
...rest,
|
|
140
260
|
],
|
|
141
261
|
{
|
|
142
262
|
cwd: projectDir,
|
|
@@ -148,78 +268,115 @@ function launchInProject(context) {
|
|
|
148
268
|
return;
|
|
149
269
|
}
|
|
150
270
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
},
|
|
159
|
-
);
|
|
271
|
+
// Keyed by the id folder, so a name change does not orphan the instance.
|
|
272
|
+
const pluginDir = writeProjectPluginInstance(projectDir, basename(dir), dir);
|
|
273
|
+
launchClaude([...claudePluginArgs(pluginDir, dir), ...promptArgs, ...rest], {
|
|
274
|
+
cwd: projectDir,
|
|
275
|
+
env: { ...env, ACADEMY_PROJECT_DIR: projectDir },
|
|
276
|
+
message: `Launching ${name} with Claude Code for project ${basename(projectDir)} (plugin: ${pluginDir})`,
|
|
277
|
+
});
|
|
160
278
|
}
|
|
161
279
|
|
|
162
280
|
export function runAgent(name, runtime, passthrough) {
|
|
163
|
-
|
|
281
|
+
// First: an Academy job's flags come from its job id, which is keyed by the
|
|
282
|
+
// fixed id, and the nightly dispatch below is one of them. The name is
|
|
283
|
+
// checked before that, so no path it names is read.
|
|
284
|
+
validateName(name);
|
|
285
|
+
const id = agentIdOf(name);
|
|
286
|
+
applyRunContext(id);
|
|
287
|
+
refuseNotStarted(agentDir(id), id);
|
|
288
|
+
if (process.env[NIGHTLY_JOB_FLAG] === '1') {
|
|
289
|
+
dispatchNightlyJob(id, passthrough);
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
const context = prepareAgentRun(id, runtime, passthrough);
|
|
164
293
|
if (!context) return;
|
|
165
294
|
if (context.inAgentHome) launchInAgentHome(context);
|
|
166
295
|
else launchInProject(context);
|
|
167
296
|
}
|
|
168
297
|
|
|
298
|
+
// An Academy-provider nightly job reaches `academy run` with the provider's own
|
|
299
|
+
// argv. The flag routes it into the routine `academy nightly` runs, so both
|
|
300
|
+
// share one preflight and one verdict; the routine's child runs without the
|
|
301
|
+
// flag and is an ordinary run. The provider argv (prompt, runtime token) is
|
|
302
|
+
// not launched: the routine builds its prompt and reads the persisted runtime.
|
|
303
|
+
// Only its permission and output flags reach the child: the job approved the
|
|
304
|
+
// one, and its scheduler reads the provider's session id through the other.
|
|
305
|
+
function dispatchNightlyJob(name, passthrough) {
|
|
306
|
+
console.log(
|
|
307
|
+
JSON.stringify({
|
|
308
|
+
event: 'nightly.dispatch',
|
|
309
|
+
params: { agent: name, source: 'academy_provider', providerArgs: passthrough.length },
|
|
310
|
+
}),
|
|
311
|
+
);
|
|
312
|
+
runNightly(name, { providerArgv: passthrough });
|
|
313
|
+
}
|
|
314
|
+
|
|
169
315
|
// Runtime-agnostic by construction. An `--agent` on this command line comes
|
|
170
316
|
// from a job an earlier build registered; it is accepted and ignored, because
|
|
171
317
|
// the persisted scalar is the only source of truth a scheduled run may read.
|
|
172
318
|
// The child is spawned bare, so it persists nothing and cannot revert the
|
|
173
319
|
// operator's edit — editing agent.yaml is the published way to change a
|
|
174
320
|
// runtime, and a nightly that carried a snapshot undid that edit every night.
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
321
|
+
// `providerArgv` is a provider job's own argv; `templatesRoot` serves tests.
|
|
322
|
+
//
|
|
323
|
+
// The pending marker is the one input check: any session activity since the
|
|
324
|
+
// last nightly that exited 0. The skill decides what in the archive is new.
|
|
325
|
+
export function runNightly(nameOrId, { providerArgv, templatesRoot } = {}) {
|
|
326
|
+
validateName(nameOrId);
|
|
327
|
+
// The nightly is keyed by the fixed id: its job, its child run, its skip line.
|
|
328
|
+
const id = agentIdOf(nameOrId);
|
|
329
|
+
const dir = agentDir(id);
|
|
178
330
|
if (!existsSync(dir)) {
|
|
179
|
-
|
|
331
|
+
skipMissingAgentUnderJob(id);
|
|
332
|
+
console.error(`Agent "${id}" not found at ${dir}`);
|
|
180
333
|
process.exit(1);
|
|
181
334
|
}
|
|
182
|
-
|
|
335
|
+
refuseNotStarted(dir, id);
|
|
336
|
+
const provider = readRuntimeProvider(dir);
|
|
337
|
+
const attempt = {
|
|
338
|
+
dir,
|
|
339
|
+
name: id,
|
|
340
|
+
provider,
|
|
341
|
+
startedAt: new Date(),
|
|
342
|
+
memory: memoryEnvRecord(resolveMemoryEnv(dir, { cwd: dir, env: nightlyChildEnv() })),
|
|
343
|
+
};
|
|
183
344
|
|
|
345
|
+
sweepBeforeNightly(dir, id);
|
|
184
346
|
const markerPath = pendingMarkerPath(dir);
|
|
185
347
|
if (!existsSync(markerPath)) {
|
|
186
|
-
|
|
187
|
-
`Skipping nightly consolidation for ${name}: no new session activity since the last run.`,
|
|
188
|
-
);
|
|
348
|
+
skipNightlyWithoutMarker(attempt);
|
|
189
349
|
return;
|
|
190
350
|
}
|
|
191
351
|
|
|
352
|
+
// Read the revision before the run, so activity that lands during it bumps
|
|
353
|
+
// the revision and keeps the marker.
|
|
192
354
|
const revision = Number(readJsonFile(markerPath, {}).revision);
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
const
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
cwd: dir,
|
|
208
|
-
env: {
|
|
209
|
-
...process.env,
|
|
210
|
-
ACADEMY_SKIP_NIGHTLY_TASK: '1',
|
|
211
|
-
ACADEMY_NIGHTLY_RUN: '1',
|
|
212
|
-
},
|
|
213
|
-
},
|
|
214
|
-
);
|
|
215
|
-
const status = result.status ?? 1;
|
|
216
|
-
|
|
217
|
-
if (status === 0 && existsSync(markerPath)) {
|
|
218
|
-
const currentRevision = Number(readJsonFile(markerPath, {}).revision);
|
|
219
|
-
if (currentRevision === revision) unlinkSync(markerPath);
|
|
220
|
-
}
|
|
355
|
+
if (skipNightlyWithoutWork(attempt, markerPath, revision)) return;
|
|
356
|
+
const skill = requireNightlySkill(attempt, templatesRoot);
|
|
357
|
+
const runtime = toRuntimeToken(provider);
|
|
358
|
+
const exitCode = launchNightlyChild(dir, id, { runtime, skill, providerArgv });
|
|
359
|
+
// A pass takes its gate mark under the marker lock; a failure keeps the marker.
|
|
360
|
+
const gate = exitCode === 0 ? removeMarkerIfUnchanged(markerPath, revision) : {};
|
|
361
|
+
recordNightlyOutcome(attempt, {
|
|
362
|
+
status: exitCode === 0 ? 'pass' : 'failed',
|
|
363
|
+
reason: exitCode === 0 ? 'exit_0' : `exit_code_${exitCode}`,
|
|
364
|
+
exitCode,
|
|
365
|
+
...gate,
|
|
366
|
+
});
|
|
367
|
+
process.exit(exitCode);
|
|
368
|
+
}
|
|
221
369
|
|
|
222
|
-
|
|
370
|
+
// A kind whose nightly skill is not installed fails before launch: running the
|
|
371
|
+
// universal skill instead would consolidate against the wrong surfaces.
|
|
372
|
+
function requireNightlySkill(attempt, templatesRoot) {
|
|
373
|
+
const { kind, skill, present } = nightlySkillFor(attempt.dir, templatesRoot);
|
|
374
|
+
if (present) return skill;
|
|
375
|
+
console.error(
|
|
376
|
+
`Error: nightly skill "${skill}" is not installed for ${kind} agent "${attempt.name}".`,
|
|
377
|
+
);
|
|
378
|
+
recordNightlyOutcome(attempt, { status: 'failed', reason: 'nightly_skill_missing' });
|
|
379
|
+
process.exit(1);
|
|
223
380
|
}
|
|
224
381
|
|
|
225
382
|
// ─────────────────────────────────────────────────────────────────────────────
|