@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/create.mjs
CHANGED
|
@@ -1,26 +1,36 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { join, resolve } from 'node:path';
|
|
1
|
+
import { realpathSync, rmSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { basename, join, resolve } from 'node:path';
|
|
3
3
|
import { spawnSync } from 'node:child_process';
|
|
4
4
|
import { pendingMarkerPath } from '../../hooks/memory_store.mjs';
|
|
5
5
|
import {
|
|
6
6
|
ACADEMY_ROOT,
|
|
7
7
|
AGENTS_ROOT,
|
|
8
8
|
CLI_NAME,
|
|
9
|
+
KIND_PROFILES,
|
|
9
10
|
activeCommandName,
|
|
10
11
|
NIGHTLY_JOB_CRON,
|
|
11
12
|
agentDir,
|
|
12
13
|
contractError,
|
|
13
14
|
contractOk,
|
|
15
|
+
exitJsonError,
|
|
16
|
+
kindOf,
|
|
14
17
|
LockTimeoutError,
|
|
15
18
|
RuntimeUnavailableError,
|
|
16
19
|
resolveExecutable,
|
|
17
20
|
validateAgentsRoot,
|
|
18
21
|
validateName,
|
|
19
22
|
} from './core.mjs';
|
|
23
|
+
import { resolveTldrAgentsBinary, spawnTldrTasksArgs, tldrSpawnOptions } from './tldr.mjs';
|
|
24
|
+
import { scheduleFailureReason } from './schedule-errors.mjs';
|
|
20
25
|
import { assertNotArchived } from './archived.mjs';
|
|
26
|
+
import { claimDefaultName, claimName, isAgentNameFree } from './default-name.mjs';
|
|
21
27
|
import { LogCorruptError, appendLifecycleEvent } from './eventlog.mjs';
|
|
22
28
|
import { agentRecord } from './inspect.mjs';
|
|
23
|
-
import {
|
|
29
|
+
import { nextSteps } from './next-steps.mjs';
|
|
30
|
+
import { isStopped } from './agent-gate.mjs';
|
|
31
|
+
import { deleteAcademyJobs } from './lifecycle.mjs';
|
|
32
|
+
import { nightlyRoute } from './nightly.mjs';
|
|
33
|
+
import { academyRuntimeArgs, registerKindTasks } from './owner-jobs.mjs';
|
|
24
34
|
import {
|
|
25
35
|
renderAcademySystemPrompt,
|
|
26
36
|
scaffoldBootFiles,
|
|
@@ -36,9 +46,50 @@ import {
|
|
|
36
46
|
// raises RuntimeUnavailableError instead of exiting mid-operation.
|
|
37
47
|
export const MUST_EXIST = { throwOnMissing: true };
|
|
38
48
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
49
|
+
const DEFAULT_KIND = 'specialist';
|
|
50
|
+
export const INTRO_PENDING_MARKER = 'intro-pending.md';
|
|
51
|
+
|
|
52
|
+
// `guided` is the caller saying a person at a terminal made this create:
|
|
53
|
+
// 'opening' (a first session starts right after, so the report says that) or
|
|
54
|
+
// 'later' (--no-start, so the report names `run`). Either replaces the Next:
|
|
55
|
+
// block. `main.mjs` starts the session: this module cannot import run.mjs,
|
|
56
|
+
// which imports this one. With no name, the first free default name is claimed
|
|
57
|
+
// and returned.
|
|
58
|
+
export function createAgent(
|
|
59
|
+
name,
|
|
60
|
+
json = false,
|
|
61
|
+
{ kind = DEFAULT_KIND, invalidOption, guided } = {},
|
|
62
|
+
) {
|
|
63
|
+
assertCreateOptions(kind, invalidOption, json);
|
|
64
|
+
const created = provisionAgent(name, json, kind);
|
|
65
|
+
reportCreated(created.name, created.dir, created.jobs, json, guided);
|
|
66
|
+
return created.name;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Both refusals come before the name is asked about, so a create Academy will
|
|
70
|
+
// not perform writes nothing. `invalid_spec` is the published code for an
|
|
71
|
+
// unknown option; there is no sixteenth code for it.
|
|
72
|
+
function assertCreateOptions(kind, invalidOption, json) {
|
|
73
|
+
if (invalidOption !== undefined) {
|
|
74
|
+
refuseCreate(
|
|
75
|
+
`Unknown create option: ${invalidOption}. Use create <name> [--kind <kind>] [--no-start] [--json].`,
|
|
76
|
+
{ option: invalidOption },
|
|
77
|
+
json,
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
if (Object.hasOwn(KIND_PROFILES, kind)) return;
|
|
81
|
+
const kinds = Object.keys(KIND_PROFILES).join(', ');
|
|
82
|
+
refuseCreate(
|
|
83
|
+
`Agent kind "${kind}" is not a kind Academy supports. Use ${kinds}.`,
|
|
84
|
+
{ kind },
|
|
85
|
+
json,
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function refuseCreate(message, fields, json) {
|
|
90
|
+
if (json) exitJsonError('invalid_spec', message, fields);
|
|
91
|
+
console.error(`Error: ${message}`);
|
|
92
|
+
process.exit(1);
|
|
42
93
|
}
|
|
43
94
|
|
|
44
95
|
// The one creation path. `create` reports what it returns as a create and the
|
|
@@ -47,22 +98,48 @@ export function createAgent(name, json = false) {
|
|
|
47
98
|
// events. A second scaffolder would be a second definition of what an Academy
|
|
48
99
|
// agent is, and the two would disagree the first time one of them learned a
|
|
49
100
|
// rule the other did not.
|
|
50
|
-
export function provisionAgent(name, json = false) {
|
|
101
|
+
export function provisionAgent(name, json = false, kind = DEFAULT_KIND) {
|
|
102
|
+
const claimed = name === undefined ? claimDefaultName(kind, json) : claimNamedDir(name, json);
|
|
103
|
+
return provisionAgentDir(claimed.dir, claimed.name, json, kind);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// The mkdir is the one point where this request comes to own the directory. It
|
|
107
|
+
// shares the root event lock with every other name claim, and the final check
|
|
108
|
+
// runs inside it, where nothing exits. The loser refuses after release and
|
|
109
|
+
// removes nothing, because what is there belongs to the winner. Rollback is
|
|
110
|
+
// for a directory this request made.
|
|
111
|
+
function claimNamedDir(name, json) {
|
|
51
112
|
const dir = assertAgentNameFree(name, json);
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
113
|
+
const claimed = claimName(name, json);
|
|
114
|
+
if (claimed instanceof Error) failScaffold(name, claimed, json);
|
|
115
|
+
// The same checks, asked again after release, exit with the code that fits.
|
|
116
|
+
if (!claimed) refuseTakenName(name, assertAgentNameFree(name, json), json);
|
|
117
|
+
return { name, dir };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function provisionAgentDir(dir, name, json, kind) {
|
|
121
|
+
scaffoldAgentDir(dir, name, json, kind);
|
|
122
|
+
const jobs = registerAcademyJobs(dir, name);
|
|
123
|
+
const failed = jobs.find((job) => !job.registered && !job.skipped);
|
|
124
|
+
if (failed) {
|
|
125
|
+
unregisterJobs(dir, jobs);
|
|
126
|
+
failJobRegistration(name, failed, json);
|
|
57
127
|
}
|
|
58
128
|
try {
|
|
59
129
|
appendLifecycleEvent('agent_created', name, dir);
|
|
60
130
|
} catch (error) {
|
|
61
|
-
|
|
62
|
-
rmSync(dir, { recursive: true, force: true });
|
|
131
|
+
unregisterJobs(dir, jobs);
|
|
63
132
|
failLifecyclePublish(name, error, json);
|
|
64
133
|
}
|
|
65
|
-
return { dir,
|
|
134
|
+
return { name, dir, jobs };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// The rollback of a create that will not stand: every job it registered goes
|
|
138
|
+
// with the directory, so a retry of the name starts from nothing.
|
|
139
|
+
function unregisterJobs(dir, jobs) {
|
|
140
|
+
const ids = registeredJobIds(jobs);
|
|
141
|
+
if (ids.length > 0) deleteAcademyJobs(realpathSync(dir), ids);
|
|
142
|
+
rmSync(dir, { recursive: true, force: true });
|
|
66
143
|
}
|
|
67
144
|
|
|
68
145
|
// The name question, asked before anything is written. The headless hire asks
|
|
@@ -71,11 +148,15 @@ export function provisionAgent(name, json = false) {
|
|
|
71
148
|
export function assertAgentNameFree(name, json = false) {
|
|
72
149
|
validateName(name);
|
|
73
150
|
validateAgentsRoot();
|
|
151
|
+
const dir = agentDir(name);
|
|
152
|
+
if (isAgentNameFree(name)) return dir;
|
|
74
153
|
// A fresh agent must not take a name the holding area still owns: unarchiving
|
|
75
154
|
// it afterwards could only ever collide.
|
|
76
|
-
assertNotArchived(
|
|
77
|
-
|
|
78
|
-
|
|
155
|
+
assertNotArchived(basename(dir), json);
|
|
156
|
+
return refuseTakenName(name, dir, json);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function refuseTakenName(name, dir, json) {
|
|
79
160
|
const message = `Agent "${name}" already exists at ${dir}`;
|
|
80
161
|
if (json) {
|
|
81
162
|
contractError(activeCommandName(), 'agent_exists', message, { name, dir: resolve(dir) });
|
|
@@ -84,30 +165,45 @@ export function assertAgentNameFree(name, json = false) {
|
|
|
84
165
|
process.exit(1);
|
|
85
166
|
}
|
|
86
167
|
|
|
87
|
-
function scaffoldAgentDir(dir, name, json) {
|
|
168
|
+
function scaffoldAgentDir(dir, name, json, kind) {
|
|
88
169
|
try {
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
writeAgentYaml(dir, name, today);
|
|
170
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
171
|
+
// agent.yaml first: the kind it names selects the surfaces scaffolded next.
|
|
172
|
+
writeAgentYaml(dir, name, today, kind);
|
|
173
|
+
scaffoldBootFiles(dir, name, today);
|
|
92
174
|
writeAgentClaudeMd(dir, name);
|
|
93
175
|
writeSkillsScaffold(dir, name);
|
|
94
176
|
writeSettingsLocal(dir);
|
|
95
177
|
writePluginSymlink(dir);
|
|
96
178
|
writeOwnershipMarker(dir, name);
|
|
179
|
+
if (kind === 'principal') writeIntroPendingMarker(dir);
|
|
97
180
|
renderAcademySystemPrompt(dir, name);
|
|
98
181
|
} catch (error) {
|
|
99
182
|
rmSync(dir, { recursive: true, force: true });
|
|
100
|
-
|
|
101
|
-
console.error(`Error: failed to create agent "${name}": ${error.message}`);
|
|
102
|
-
process.exit(1);
|
|
183
|
+
failScaffold(name, error, json);
|
|
103
184
|
}
|
|
104
185
|
}
|
|
105
186
|
|
|
187
|
+
// Not a boot surface and never in the prompt: `academy run` reads it to seed the
|
|
188
|
+
// first message, and the intro skill deletes it once the first work has started.
|
|
189
|
+
function writeIntroPendingMarker(dir) {
|
|
190
|
+
writeFileSync(
|
|
191
|
+
join(dir, INTRO_PENDING_MARKER),
|
|
192
|
+
'The first-session intro is not finished. The principal-intro skill removes this file.\n',
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function failScaffold(name, error, json) {
|
|
197
|
+
if (json) throw error;
|
|
198
|
+
console.error(`Error: failed to create agent "${name}": ${error.message}`);
|
|
199
|
+
process.exit(1);
|
|
200
|
+
}
|
|
201
|
+
|
|
106
202
|
// A host with no scheduler binary used to exit from inside the resolver, which
|
|
107
203
|
// skipped the rollback, the event, and the envelope, and left an orphan agent
|
|
108
204
|
// that no client could see and no retry could replace.
|
|
109
|
-
function
|
|
110
|
-
const message = `failed to register
|
|
205
|
+
function failJobRegistration(name, failed, json) {
|
|
206
|
+
const message = `failed to register ${jobLabel(failed.id)} "${failed.id}" for "${name}": ${failed.reason}`;
|
|
111
207
|
if (json) contractError(activeCommandName(), 'runtime_unavailable', message, { name });
|
|
112
208
|
console.error(`Error: ${message}`);
|
|
113
209
|
process.exit(1);
|
|
@@ -127,16 +223,19 @@ function failLifecyclePublish(name, error, json) {
|
|
|
127
223
|
process.exit(1);
|
|
128
224
|
}
|
|
129
225
|
|
|
130
|
-
function reportCreated(name, dir,
|
|
226
|
+
function reportCreated(name, dir, jobs, json, guided) {
|
|
131
227
|
if (json) {
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
scheduledJobId: nightlyTask.registered ? nightlyTask.id : null,
|
|
136
|
-
});
|
|
228
|
+
// A new Owner is a draft until its mandate is signed (`start`), so it is never started here.
|
|
229
|
+
const fields = { created: true, ...agentRecord(name), ...scheduledJobFields(jobs) };
|
|
230
|
+
contractOk('create', kindOf(dir) === 'owner' ? { ...fields, started: false } : fields);
|
|
137
231
|
return;
|
|
138
232
|
}
|
|
139
233
|
|
|
234
|
+
const [nightlyTask, ...ownerTasks] = jobs;
|
|
235
|
+
if (guided) {
|
|
236
|
+
reportGuidedCreated(name, nightlyTask, guided);
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
140
239
|
console.log(`Created agent "${name}" at ${dir}`);
|
|
141
240
|
if (nightlyTask.registered) {
|
|
142
241
|
console.log(
|
|
@@ -145,23 +244,42 @@ function reportCreated(name, dir, nightlyTask, json) {
|
|
|
145
244
|
} else {
|
|
146
245
|
console.log(`Nightly consolidation job not registered: ${nightlyTask.reason}`);
|
|
147
246
|
}
|
|
247
|
+
for (const task of ownerTasks) console.log(`Registered ${jobLabel(task.id)} "${task.id}".`);
|
|
148
248
|
console.log('');
|
|
149
249
|
console.log('Next:');
|
|
150
|
-
console.log(
|
|
151
|
-
console.log(` ${CLI_NAME} run ${name} # launch Claude Code in the agent's home`);
|
|
250
|
+
console.log(nextSteps(name, kindOf(dir)).join('\n'));
|
|
152
251
|
}
|
|
153
252
|
|
|
154
|
-
|
|
253
|
+
// The guided path is a person at a terminal making a Principal: short lines, no paths.
|
|
254
|
+
function reportGuidedCreated(name, nightlyTask, guided) {
|
|
255
|
+
console.log(`✓ Created ${name}`);
|
|
256
|
+
if (nightlyTask.registered) {
|
|
257
|
+
const [minute, hour] = NIGHTLY_JOB_CRON.split(' ');
|
|
258
|
+
const time = `${hour.padStart(2, '0')}:${minute.padStart(2, '0')}`;
|
|
259
|
+
console.log(`✓ Nightly run at ${time} ${nightlyTask.timezone}`);
|
|
260
|
+
} else {
|
|
261
|
+
console.log(`Nightly consolidation job not registered: ${nightlyTask.reason}`);
|
|
262
|
+
}
|
|
263
|
+
if (guided === 'opening') {
|
|
264
|
+
console.log(`Opening your first session with ${name}…`);
|
|
265
|
+
console.log(`Next time: ${CLI_NAME} run ${name}`);
|
|
266
|
+
} else {
|
|
267
|
+
console.log(`Start it when you are ready: ${CLI_NAME} run ${name}`);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
export function localTimezone() {
|
|
155
272
|
return (
|
|
156
273
|
process.env.TZ || Intl.DateTimeFormat().resolvedOptions().timeZone || 'America/Los_Angeles'
|
|
157
274
|
);
|
|
158
275
|
}
|
|
159
276
|
|
|
160
|
-
// The job is registered once and then runs unattended for months, so it
|
|
161
|
-
// no runtime: a token here would be a snapshot of the scalar as it stood
|
|
162
|
-
// registration day, and the child run would rewrite every later operator
|
|
163
|
-
// back to it. `nightly` reads the persisted scalar instead.
|
|
164
|
-
|
|
277
|
+
// The command job is registered once and then runs unattended for months, so it
|
|
278
|
+
// carries no runtime: a token here would be a snapshot of the scalar as it stood
|
|
279
|
+
// on registration day, and the child run would rewrite every later operator
|
|
280
|
+
// edit back to it. `nightly` reads the persisted scalar instead. An Owner's
|
|
281
|
+
// provider job must name its runtime; `run --agent` re-registers it on change.
|
|
282
|
+
function registerNightlyConsolidationTask(dir, name) {
|
|
165
283
|
const id = `${name}-nightly-consolidation`;
|
|
166
284
|
if (process.env.ACADEMY_SKIP_NIGHTLY_TASK === '1') {
|
|
167
285
|
return {
|
|
@@ -171,20 +289,20 @@ export function registerNightlyConsolidationTask(dir, name) {
|
|
|
171
289
|
reason: 'skipped by ACADEMY_SKIP_NIGHTLY_TASK=1',
|
|
172
290
|
};
|
|
173
291
|
}
|
|
292
|
+
if (isStopped(dir)) return { id, registered: false, skipped: true, reason: 'agent is stopped' };
|
|
174
293
|
|
|
175
294
|
const timezone = localTimezone();
|
|
176
|
-
const runArgs = ['nightly', name];
|
|
177
295
|
let academyBin;
|
|
178
|
-
let
|
|
296
|
+
let tldrAgentsBin;
|
|
179
297
|
try {
|
|
180
298
|
academyBin = resolveExecutable('ACADEMY_BIN', join(ACADEMY_ROOT, 'bin', 'academy'), MUST_EXIST);
|
|
181
|
-
|
|
299
|
+
tldrAgentsBin = resolveTldrAgentsBinary();
|
|
182
300
|
} catch (error) {
|
|
183
301
|
if (!(error instanceof RuntimeUnavailableError)) throw error;
|
|
184
302
|
return { id, registered: false, reason: error.message };
|
|
185
303
|
}
|
|
186
304
|
|
|
187
|
-
const args = [
|
|
305
|
+
const args = spawnTldrTasksArgs([
|
|
188
306
|
'schedule',
|
|
189
307
|
'--cwd',
|
|
190
308
|
dir,
|
|
@@ -196,45 +314,75 @@ export function registerNightlyConsolidationTask(dir, name) {
|
|
|
196
314
|
timezone,
|
|
197
315
|
'--condition-file-exists',
|
|
198
316
|
pendingMarkerPath(dir),
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
'--env-json',
|
|
202
|
-
JSON.stringify({ AGENTS_ROOT: resolve(AGENTS_ROOT) }),
|
|
203
|
-
'--replace',
|
|
317
|
+
...nightlyJobLaunch(dir, name, academyBin),
|
|
318
|
+
'--merge',
|
|
204
319
|
'--tags',
|
|
205
320
|
'academy,nightly,consolidation',
|
|
206
321
|
'--timeout-sec',
|
|
207
322
|
'3600',
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
];
|
|
323
|
+
...nightlyJobCommandArgs(dir, name),
|
|
324
|
+
]);
|
|
211
325
|
|
|
212
|
-
const
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
env: process.env,
|
|
216
|
-
});
|
|
326
|
+
const task = runSchedule(tldrAgentsBin, dir, id, args);
|
|
327
|
+
return task.registered ? { ...task, timezone } : task;
|
|
328
|
+
}
|
|
217
329
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
return { id, registered: false, reason };
|
|
222
|
-
}
|
|
223
|
-
return { id, registered: true, timezone };
|
|
330
|
+
export function runSchedule(tldrAgentsBin, dir, id, args) {
|
|
331
|
+
const result = spawnSync(tldrAgentsBin, args, tldrSpawnOptions(dir));
|
|
332
|
+
if (result.error || result.status !== 0)
|
|
333
|
+
return { id, registered: false, reason: scheduleFailureReason(result) };
|
|
334
|
+
return { id, registered: true };
|
|
224
335
|
}
|
|
225
336
|
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
337
|
+
// Every Academy job an agent of its kind runs, nightly first. The skip flag
|
|
338
|
+
// that silences the nightly silences the set, and an Owner whose nightly did
|
|
339
|
+
// not register does not go on to half a schedule. A stopped agent's nightly is
|
|
340
|
+
// skipped, so it registers nothing.
|
|
341
|
+
export function registerAcademyJobs(dir, name) {
|
|
342
|
+
const nightly = registerNightlyConsolidationTask(dir, name);
|
|
343
|
+
return nightly.registered ? registerKindTasks(dir, name, nightly) : [nightly];
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
export const registeredJobIds = (jobs) => jobs.filter((job) => job.registered).map((job) => job.id);
|
|
347
|
+
|
|
348
|
+
export const jobLabel = (id) =>
|
|
349
|
+
id.endsWith('-nightly-consolidation')
|
|
350
|
+
? 'nightly consolidation job'
|
|
351
|
+
: id.endsWith('-principal-brief')
|
|
352
|
+
? 'Principal brief job'
|
|
353
|
+
: 'Owner job';
|
|
354
|
+
|
|
355
|
+
// `scheduledJobId` keeps its published meaning, the nightly; the set is
|
|
356
|
+
// additive, so a client that knows only the nightly reads what it always did.
|
|
357
|
+
export function scheduledJobFields(jobs) {
|
|
358
|
+
const [nightly] = jobs;
|
|
359
|
+
return {
|
|
360
|
+
scheduledJobId: nightly.registered ? nightly.id : null,
|
|
361
|
+
scheduledJobIds: registeredJobIds(jobs),
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// An Owner's nightly runs through the Academy provider, like its other Academy
|
|
366
|
+
// jobs; `academy run` derives ACADEMY_NIGHTLY_JOB from the job id and enters
|
|
367
|
+
// the same routine as `academy nightly`. A provider job carries no env: the
|
|
368
|
+
// scheduler refuses it there. Every other kind keeps the command job, which
|
|
369
|
+
// carries its env and already records the real exit.
|
|
370
|
+
function nightlyJobLaunch(dir, name, academyBin) {
|
|
371
|
+
const env = { AGENTS_ROOT: resolve(AGENTS_ROOT) };
|
|
372
|
+
if (kindOf(dir) !== 'owner') return ['--command', academyBin, '--env-json', JSON.stringify(env)];
|
|
373
|
+
return [
|
|
374
|
+
'--provider',
|
|
375
|
+
'academy',
|
|
376
|
+
'--academy-agent',
|
|
377
|
+
name,
|
|
378
|
+
...academyRuntimeArgs(dir),
|
|
379
|
+
'--prompt',
|
|
380
|
+
nightlyRoute('owner'),
|
|
381
|
+
];
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
function nightlyJobCommandArgs(dir, name) {
|
|
385
|
+
return kindOf(dir) === 'owner' ? [] : ['--', 'nightly', name];
|
|
238
386
|
}
|
|
239
387
|
|
|
240
388
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
// Codex prompt reader for curator-feed.mjs. Read-only.
|
|
2
|
+
import { readdirSync } from 'node:fs';
|
|
3
|
+
import { basename, join } from 'node:path';
|
|
4
|
+
import { MEMORY_BRIDGE_CODEX_ORIGINATOR } from '../../hooks/memory_bridge.mjs';
|
|
5
|
+
import {
|
|
6
|
+
inWindow,
|
|
7
|
+
isTempCwd,
|
|
8
|
+
jsonLines,
|
|
9
|
+
mtimeMs,
|
|
10
|
+
stripEnvelopes,
|
|
11
|
+
} from './curator-feed-sources.mjs';
|
|
12
|
+
|
|
13
|
+
const CODEX_ORIGINATORS = [
|
|
14
|
+
MEMORY_BRIDGE_CODEX_ORIGINATOR,
|
|
15
|
+
'codex_cli_rs',
|
|
16
|
+
'codex-tui',
|
|
17
|
+
'codex_tui',
|
|
18
|
+
'codex_vscode',
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
function codexText(p) {
|
|
22
|
+
return (p.content ?? [])
|
|
23
|
+
.filter((b) => ['input_text', 'output_text'].includes(b?.type))
|
|
24
|
+
.map((b) => b.text ?? '')
|
|
25
|
+
.join('\n')
|
|
26
|
+
.trim();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const codexPrompt = ({ p, meta, n, ts, text, prev }) => ({
|
|
30
|
+
runtime: 'codex',
|
|
31
|
+
ts,
|
|
32
|
+
cwd: meta.cwd || '',
|
|
33
|
+
branch: meta.git?.branch || null,
|
|
34
|
+
sessionId: meta.id ?? basename(n, '.jsonl'),
|
|
35
|
+
turnId: p.internal_chat_message_metadata_passthrough?.turn_id ?? null,
|
|
36
|
+
text,
|
|
37
|
+
prev,
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
function codexFilePrompts(lines, meta, n, win) {
|
|
41
|
+
const prompts = [];
|
|
42
|
+
let last = '';
|
|
43
|
+
for (const e of lines) {
|
|
44
|
+
const p = e.payload ?? {};
|
|
45
|
+
if (e.type !== 'response_item' || p.type !== 'message') continue;
|
|
46
|
+
const tx = codexText(p);
|
|
47
|
+
if (p.role === 'assistant') {
|
|
48
|
+
last = tx || last;
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
const ts = Date.parse(e.timestamp);
|
|
52
|
+
if (p.role !== 'user' || !inWindow(ts, win)) continue;
|
|
53
|
+
const text = stripEnvelopes(tx);
|
|
54
|
+
if (text) prompts.push(codexPrompt({ p, meta, n, ts, text, prev: last }));
|
|
55
|
+
}
|
|
56
|
+
return prompts;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const isCodexUserSession = (meta) =>
|
|
60
|
+
CODEX_ORIGINATORS.includes(meta?.originator) && !isTempCwd(meta.cwd || '');
|
|
61
|
+
|
|
62
|
+
export function codexPrompts(home, win) {
|
|
63
|
+
const base = join(home, '.codex', 'sessions');
|
|
64
|
+
let names;
|
|
65
|
+
try {
|
|
66
|
+
names = readdirSync(base, { recursive: true });
|
|
67
|
+
} catch (e) {
|
|
68
|
+
if (e.code === 'ENOENT') return null;
|
|
69
|
+
throw e;
|
|
70
|
+
}
|
|
71
|
+
const prompts = [];
|
|
72
|
+
for (const n of names) {
|
|
73
|
+
const file = join(base, n);
|
|
74
|
+
if (!n.endsWith('.jsonl') || mtimeMs(file) < win.since) continue;
|
|
75
|
+
const lines = jsonLines(file);
|
|
76
|
+
const meta = lines[0]?.payload;
|
|
77
|
+
if (!isCodexUserSession(meta)) continue;
|
|
78
|
+
prompts.push(...codexFilePrompts(lines, meta, n, win));
|
|
79
|
+
}
|
|
80
|
+
return prompts;
|
|
81
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
// Memory-bridge observations and the previous curator run's token use, for curator-feed.mjs. Read-only.
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { bridgeHomes } from '../../hooks/memory_bridge.mjs';
|
|
5
|
+
import { DAY, ifPresent, jsonLines, ls, mtimeMs, textOf } from './curator-feed-sources.mjs';
|
|
6
|
+
|
|
7
|
+
// --- C. Observations ---
|
|
8
|
+
const observationRows = (file) => ifPresent(() => jsonLines(file), []);
|
|
9
|
+
|
|
10
|
+
function observationFiles(homes, win) {
|
|
11
|
+
const dates = [];
|
|
12
|
+
for (let d = Math.floor((win.since - DAY) / DAY) * DAY; d <= win.until + DAY; d += DAY)
|
|
13
|
+
dates.push(new Date(d).toISOString().slice(0, 10));
|
|
14
|
+
const files = [];
|
|
15
|
+
for (const h of homes)
|
|
16
|
+
for (const project of ls(h) ?? [])
|
|
17
|
+
for (const ws of ls(join(h, project)) ?? [])
|
|
18
|
+
for (const date of dates)
|
|
19
|
+
files.push(join(h, project, ws, 'memory', 'observations', `${date}.jsonl`));
|
|
20
|
+
return files;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Rows without an id cannot be told apart, so only rows with an id dedupe.
|
|
24
|
+
function isNewObservation(o, seen) {
|
|
25
|
+
if (!o.sessionId || (o.id && seen.has(o.id))) return false;
|
|
26
|
+
if (o.id) seen.add(o.id);
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function observations(env, win) {
|
|
31
|
+
const homes = bridgeHomes(env).filter((h) => existsSync(h));
|
|
32
|
+
if (!homes.length) return { homes, byTurn: null };
|
|
33
|
+
const seen = new Set();
|
|
34
|
+
const byTurn = new Map();
|
|
35
|
+
for (const file of observationFiles(homes, win)) {
|
|
36
|
+
for (const o of observationRows(file)) {
|
|
37
|
+
if (!isNewObservation(o, seen)) continue;
|
|
38
|
+
const key = `${o.sessionId}|${o.turnId}`;
|
|
39
|
+
if (!(Date.parse(byTurn.get(key)?.timestamp) >= Date.parse(o.timestamp))) byTurn.set(key, o);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return { homes, byTurn };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// A malformed observation row throws here, apart from its prompt: only its Result is skipped.
|
|
46
|
+
export function checkedObservation(obs) {
|
|
47
|
+
for (const k of ['tags', 'files'])
|
|
48
|
+
if (obs[k] != null && !(Array.isArray(obs[k]) && obs[k].every((s) => typeof s === 'string')))
|
|
49
|
+
throw new Error(`${k} is not a list of strings`);
|
|
50
|
+
for (const k of ['actionStatus', 'summary', 'branch'])
|
|
51
|
+
if (obs[k] != null && typeof obs[k] !== 'string') throw new Error(`${k} is not a string`);
|
|
52
|
+
return obs;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// --- D. Previous run tokens ---
|
|
56
|
+
function isCuratorRun(lines) {
|
|
57
|
+
const first = textOf(
|
|
58
|
+
lines.find((e) => e.type === 'user' && textOf(e.message?.content) != null)?.message?.content,
|
|
59
|
+
);
|
|
60
|
+
return Boolean(
|
|
61
|
+
first?.startsWith('<helm-context>') && first.includes('HELM_JOB_ID=curator-user-nightly'),
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function tokensOf(lines) {
|
|
66
|
+
const byId = new Map();
|
|
67
|
+
for (const e of lines)
|
|
68
|
+
if (e.type === 'assistant' && e.message?.id && e.message.usage)
|
|
69
|
+
byId.set(e.message.id, e.message.usage);
|
|
70
|
+
const sum = (k) => [...byId.values()].reduce((n, u) => n + (u[k] ?? 0), 0);
|
|
71
|
+
return {
|
|
72
|
+
calls: byId.size,
|
|
73
|
+
cacheRead: sum('cache_read_input_tokens'),
|
|
74
|
+
cacheWrite: sum('cache_creation_input_tokens'),
|
|
75
|
+
output: sum('output_tokens'),
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// shortcut: Claude transcripts only; add Codex token_count when a curator runs on Codex.
|
|
80
|
+
export function previousRun(home, agentsRoot) {
|
|
81
|
+
const dir = join(
|
|
82
|
+
home,
|
|
83
|
+
'.claude',
|
|
84
|
+
'projects',
|
|
85
|
+
join(agentsRoot, 'curator').replace(/[^A-Za-z0-9]/g, '-'),
|
|
86
|
+
);
|
|
87
|
+
const current = process.env.CLAUDE_CODE_SESSION_ID;
|
|
88
|
+
const files = (ls(dir) ?? [])
|
|
89
|
+
.filter((f) => f.endsWith('.jsonl') && f !== `${current}.jsonl`)
|
|
90
|
+
.map((f) => ({ file: join(dir, f), at: mtimeMs(join(dir, f)) }))
|
|
91
|
+
.sort((a, b) => b.at - a.at);
|
|
92
|
+
for (const { file } of files) {
|
|
93
|
+
const lines = jsonLines(file);
|
|
94
|
+
if (isCuratorRun(lines)) return tokensOf(lines);
|
|
95
|
+
}
|
|
96
|
+
return null;
|
|
97
|
+
}
|