@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/hooks/sync_memory.mjs
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { mkdirSync } from 'node:fs';
|
|
3
3
|
import { join, resolve } from 'node:path';
|
|
4
4
|
import { isMainModule, readPayload, runtimeEnvFor } from './hook_runtime.mjs';
|
|
5
|
-
import {
|
|
5
|
+
import { sweepBridgeObservations } from './memory_bridge.mjs';
|
|
6
6
|
import { markPendingConsolidation, recordSession } from './memory_store.mjs';
|
|
7
7
|
|
|
8
8
|
export function syncAcademyMemory(payload, env = process.env) {
|
|
@@ -12,15 +12,19 @@ export function syncAcademyMemory(payload, env = process.env) {
|
|
|
12
12
|
if (!agentDir || !sessionId) return { synced: 0 };
|
|
13
13
|
|
|
14
14
|
const memoryDir = join(resolve(agentDir), 'memory');
|
|
15
|
-
|
|
16
|
-
mkdirSync(observationsDir, { recursive: true });
|
|
15
|
+
mkdirSync(join(memoryDir, 'observations'), { recursive: true });
|
|
17
16
|
recordSession(memoryDir, payload, env);
|
|
17
|
+
// Observations land after Stop, so this copies what earlier stops of every
|
|
18
|
+
// recorded session could not yet see (the store is one turn behind).
|
|
18
19
|
// Eligibility tracks Academy session activity, not the bridge copy, so an
|
|
19
|
-
// agent still consolidates when the memory bridge is unavailable.
|
|
20
|
-
|
|
20
|
+
// agent still consolidates when the memory bridge is unavailable. A
|
|
21
|
+
// nightly's own Stop marks only when it swept in earlier sessions' input.
|
|
22
|
+
let synced = 0;
|
|
23
|
+
markPendingConsolidation(memoryDir, () => {
|
|
24
|
+
synced = sweepBridgeObservations(resolve(agentDir), { env });
|
|
25
|
+
return env.ACADEMY_NIGHTLY_RUN !== '1' || synced > 0;
|
|
26
|
+
});
|
|
21
27
|
|
|
22
|
-
const synced =
|
|
23
|
-
env.ACADEMY_MEMORY_BRIDGE === '1' ? syncBridgeObservations(env, observationsDir, sessionId) : 0;
|
|
24
28
|
return { synced };
|
|
25
29
|
}
|
|
26
30
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@joenandez/academy",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.12",
|
|
4
4
|
"description": "Academy v3 — portable AI agents with 8 boot surfaces and a skills primitive.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -21,12 +21,12 @@
|
|
|
21
21
|
"templates/",
|
|
22
22
|
"hooks/",
|
|
23
23
|
".claude-plugin/",
|
|
24
|
-
"conformance/",
|
|
25
|
-
"docs/integration-guide.md",
|
|
26
24
|
"CHANGELOG.md"
|
|
27
25
|
],
|
|
28
26
|
"scripts": {
|
|
29
27
|
"test": "node --test tests/*.test.mjs",
|
|
28
|
+
"sync:roque": "node scripts/sync-roque.mjs",
|
|
29
|
+
"sync:backstory": "node scripts/sync-backstory.mjs",
|
|
30
30
|
"quality:setup": "node quality/run.mjs setup",
|
|
31
31
|
"quality:commit": "node quality/run.mjs commit",
|
|
32
32
|
"quality:push": "node quality/run.mjs push",
|
|
@@ -35,7 +35,11 @@
|
|
|
35
35
|
"keywords": ["academy", "ai-agent", "claude-code", "portable-agent"],
|
|
36
36
|
"author": "Joe Fernandez",
|
|
37
37
|
"license": "MIT",
|
|
38
|
+
"engines": {
|
|
39
|
+
"node": ">=24.20.0"
|
|
40
|
+
},
|
|
38
41
|
"devDependencies": {
|
|
42
|
+
"js-yaml": "4.3.2",
|
|
39
43
|
"knip": "5.70.2"
|
|
40
44
|
}
|
|
41
45
|
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { currentAgentName } from './agent-id.mjs';
|
|
4
|
+
import { CLI_NAME, kindOf } from './core.mjs';
|
|
5
|
+
import { printHelmSkip } from './helm-skip.mjs';
|
|
6
|
+
import { schedulerJobId } from './tldr.mjs';
|
|
7
|
+
import { readAgentYaml } from './yaml.mjs';
|
|
8
|
+
|
|
9
|
+
// Decision D1: nothing runs until the user signs the mandate, and a stopped
|
|
10
|
+
// agent runs nothing. Job registration and every run entry ask these two.
|
|
11
|
+
|
|
12
|
+
// The hire placeholder the Owner mandate template carries until a person
|
|
13
|
+
// writes the mandate. Matched as a substring, so the template may change around it.
|
|
14
|
+
const MANDATE_PLACEHOLDER = 'Hire flow will populate this';
|
|
15
|
+
|
|
16
|
+
export const isStopped = (dir) => readAgentYaml(dir).stopped === 'true';
|
|
17
|
+
|
|
18
|
+
export function readMandate(dir) {
|
|
19
|
+
try {
|
|
20
|
+
return readFileSync(join(dir, 'mandate.md'), 'utf8');
|
|
21
|
+
} catch {
|
|
22
|
+
return '';
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Mandate text that is blank or still holds the hire placeholder. */
|
|
27
|
+
export const isDraftMandate = (text) => text.trim() === '' || text.includes(MANDATE_PLACEHOLDER);
|
|
28
|
+
|
|
29
|
+
/** An Owner whose mandate.md is missing, blank, or still holds the hire placeholder. */
|
|
30
|
+
export function isDraftOwner(dir) {
|
|
31
|
+
return kindOf(dir) === 'owner' && isDraftMandate(readMandate(dir));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Of a kind's job ids, nightly first, the ones registered while the agent is
|
|
35
|
+
// in this state: none when stopped, only the nightly for a draft Owner.
|
|
36
|
+
export function liveJobIds(dir, ids) {
|
|
37
|
+
if (isStopped(dir)) return [];
|
|
38
|
+
return isDraftOwner(dir) ? ids.slice(0, 1) : ids;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function notStartedReason(dir) {
|
|
42
|
+
if (isStopped(dir)) return 'agent_stopped';
|
|
43
|
+
if (isDraftOwner(dir)) return 'owner_draft';
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// The run entry gate, before any model. A scheduled run, a stale job left by a
|
|
48
|
+
// failed delete included, ends as a skip the scheduler records; a person is
|
|
49
|
+
// told to start the agent.
|
|
50
|
+
export function refuseNotStarted(dir, id) {
|
|
51
|
+
const reason = notStartedReason(dir);
|
|
52
|
+
if (!reason) return;
|
|
53
|
+
const jobId = schedulerJobId();
|
|
54
|
+
console.error(
|
|
55
|
+
JSON.stringify({
|
|
56
|
+
event: 'run.not_started',
|
|
57
|
+
status: 'warn',
|
|
58
|
+
params: { agent: id, jobId },
|
|
59
|
+
result: { skip: reason },
|
|
60
|
+
}),
|
|
61
|
+
);
|
|
62
|
+
if (jobId) {
|
|
63
|
+
printHelmSkip(reason, id);
|
|
64
|
+
process.exit(0);
|
|
65
|
+
}
|
|
66
|
+
const name = currentAgentName(dir);
|
|
67
|
+
const state = reason === 'agent_stopped' ? 'is stopped' : 'has no signed mandate';
|
|
68
|
+
console.error(`Agent "${name}" ${state}. Run \`${CLI_NAME} start ${name}\` to start it.`);
|
|
69
|
+
process.exit(1);
|
|
70
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { existsSync, readdirSync } from 'node:fs';
|
|
2
|
+
import { basename, join } from 'node:path';
|
|
3
|
+
import { AGENTS_ROOT, NAME_RE } from './core.mjs';
|
|
4
|
+
import { holdingAreaOrNull, isArchivedAgent } from './archived.mjs';
|
|
5
|
+
import { readAgentYaml } from './yaml.mjs';
|
|
6
|
+
|
|
7
|
+
// An agent's folder name is its fixed id; agent.yaml `name:` is its current
|
|
8
|
+
// name. `agentDir` in core.mjs resolves either through here.
|
|
9
|
+
|
|
10
|
+
/** The id an id or current name resolves to, live or archived, or null. Never exits. */
|
|
11
|
+
export function agentIdOrNull(nameOrId) {
|
|
12
|
+
if (typeof nameOrId !== 'string' || !NAME_RE.test(nameOrId)) return null;
|
|
13
|
+
if (existsSync(join(AGENTS_ROOT, nameOrId)) || isArchivedAgent(nameOrId)) return nameOrId;
|
|
14
|
+
// Read only on a folder miss, so a lookup by id never opens a YAML file.
|
|
15
|
+
const holding = holdingAreaOrNull();
|
|
16
|
+
for (const home of holding === null ? [AGENTS_ROOT] : [AGENTS_ROOT, holding]) {
|
|
17
|
+
const id = listIds(home).find((entry) => readAgentYaml(join(home, entry)).name === nameOrId);
|
|
18
|
+
if (id) return id;
|
|
19
|
+
}
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** The current name: agent.yaml `name:`, or the id when it names none. */
|
|
24
|
+
export function currentAgentName(dir) {
|
|
25
|
+
return readAgentYaml(dir).name || basename(dir);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** The fixed id an id or current name addresses; an unknown or invalid input as given. */
|
|
29
|
+
export const agentIdOf = (nameOrId) => agentIdOrNull(nameOrId) ?? nameOrId;
|
|
30
|
+
|
|
31
|
+
function listIds(home) {
|
|
32
|
+
try {
|
|
33
|
+
return readdirSync(home).filter((entry) => NAME_RE.test(entry));
|
|
34
|
+
} catch {
|
|
35
|
+
return [];
|
|
36
|
+
}
|
|
37
|
+
}
|
package/scripts/cli/archive.mjs
CHANGED
|
@@ -11,17 +11,21 @@ import {
|
|
|
11
11
|
withFileLock,
|
|
12
12
|
} from './core.mjs';
|
|
13
13
|
import { ARCHIVED_DIR, archivedAgentDir, holdingAreaOrNull } from './archived.mjs';
|
|
14
|
-
import {
|
|
14
|
+
import { jobLabel, registerAcademyJobs, scheduledJobFields } from './create.mjs';
|
|
15
|
+
import { agentIdOf, currentAgentName } from './agent-id.mjs';
|
|
15
16
|
import { appendLifecycleEvent } from './eventlog.mjs';
|
|
16
17
|
import {
|
|
17
18
|
DeleteAgentError,
|
|
18
19
|
SLOT,
|
|
20
|
+
academyJobIds,
|
|
19
21
|
assertOwnedAgentForDelete,
|
|
20
|
-
|
|
22
|
+
deleteAcademyJobs,
|
|
21
23
|
preflightOwnedAgent,
|
|
22
24
|
reportLifecycleFailure,
|
|
25
|
+
unscheduleError,
|
|
23
26
|
validateOwnedAgentDir,
|
|
24
27
|
} from './lifecycle.mjs';
|
|
28
|
+
import { reportRegisteredJobs } from './rename.mjs';
|
|
25
29
|
|
|
26
30
|
// `archive` / `unarchive`. Archived agents live at AGENTS_ROOT/.archived/<name>,
|
|
27
31
|
// which is not the canonical slot, so they are validated against the holding
|
|
@@ -29,16 +33,19 @@ import {
|
|
|
29
33
|
// Neither command destroys anything, so neither quarantines: the move itself is
|
|
30
34
|
// the whole operation and it is reversible by the other command.
|
|
31
35
|
|
|
36
|
+
// Both commands act on the fixed id: its folder, slot, marker and lock. The
|
|
37
|
+
// current name is what they report.
|
|
32
38
|
export function archiveAgent(name, json) {
|
|
33
|
-
preflightOwnedAgent(name, json);
|
|
34
|
-
runLocked(
|
|
39
|
+
const { id } = preflightOwnedAgent(name, json);
|
|
40
|
+
runLocked(id, json, 'archive', () => archiveAgentLocked(id, json));
|
|
35
41
|
}
|
|
36
42
|
|
|
37
43
|
export function unarchiveAgent(name, json) {
|
|
38
44
|
validateName(name);
|
|
39
45
|
validateAgentsRoot();
|
|
40
|
-
|
|
41
|
-
|
|
46
|
+
const id = agentIdOf(name);
|
|
47
|
+
validateOwnedAgentDir(id, archivedAgentDir(id), json, SLOT.archived);
|
|
48
|
+
runLocked(id, json, 'unarchive', () => unarchiveAgentLocked(id, json));
|
|
42
49
|
}
|
|
43
50
|
|
|
44
51
|
// Both commands take the canonical lifecycle lock, the same one `delete` and
|
|
@@ -94,26 +101,20 @@ function refuseOccupiedTarget(name, target) {
|
|
|
94
101
|
);
|
|
95
102
|
}
|
|
96
103
|
|
|
97
|
-
function archiveAgentLocked(
|
|
98
|
-
const { dirReal } = assertOwnedAgentForDelete(
|
|
99
|
-
const
|
|
104
|
+
function archiveAgentLocked(id, json) {
|
|
105
|
+
const { dirReal } = assertOwnedAgentForDelete(id, agentDir(id));
|
|
106
|
+
const name = currentAgentName(dirReal);
|
|
107
|
+
const previousDir = resolve(agentDir(id));
|
|
100
108
|
ensureHoldingArea();
|
|
101
|
-
assertContainedHoldingArea(
|
|
102
|
-
const target = archivedAgentDir(
|
|
103
|
-
assertContainedSlot(
|
|
104
|
-
refuseOccupiedTarget(
|
|
105
|
-
|
|
106
|
-
// Unscheduled before the move
|
|
107
|
-
//
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
if (!unscheduled.ok) {
|
|
111
|
-
throw new DeleteAgentError(
|
|
112
|
-
unscheduled.code ?? 'unschedule_failed',
|
|
113
|
-
`Refusing to archive "${name}" because nightly unschedule failed: ${unscheduled.reason}`,
|
|
114
|
-
{ name },
|
|
115
|
-
);
|
|
116
|
-
}
|
|
109
|
+
assertContainedHoldingArea(id);
|
|
110
|
+
const target = archivedAgentDir(id);
|
|
111
|
+
assertContainedSlot(id, target);
|
|
112
|
+
refuseOccupiedTarget(id, target);
|
|
113
|
+
|
|
114
|
+
// Unscheduled before the move: the job's --cwd names a directory that is
|
|
115
|
+
// about to move, and an archived agent must not keep firing any of its jobs.
|
|
116
|
+
const unscheduled = deleteAcademyJobs(dirReal, academyJobIds(id, dirReal));
|
|
117
|
+
if (!unscheduled.ok) throw unscheduleError('archive', name, unscheduled);
|
|
117
118
|
|
|
118
119
|
renameSync(dirReal, target);
|
|
119
120
|
appendLifecycleEvent('agent_archived', name, resolve(target), { previousDir });
|
|
@@ -125,37 +126,37 @@ function archiveAgentLocked(name, json) {
|
|
|
125
126
|
dir: resolve(target),
|
|
126
127
|
previousDir,
|
|
127
128
|
unscheduledJobId: unscheduled.id,
|
|
129
|
+
unscheduledJobIds: unscheduled.ids,
|
|
128
130
|
});
|
|
129
131
|
return;
|
|
130
132
|
}
|
|
131
133
|
console.log(`Archived agent "${name}" (${previousDir} → ${resolve(target)}).`);
|
|
132
|
-
console.log(`Unregistered
|
|
134
|
+
for (const id of unscheduled.ids) console.log(`Unregistered ${jobLabel(id)} "${id}".`);
|
|
133
135
|
}
|
|
134
136
|
|
|
135
|
-
function unarchiveAgentLocked(
|
|
136
|
-
assertContainedHoldingArea(
|
|
137
|
-
const { dirReal } = assertOwnedAgentForDelete(
|
|
138
|
-
const
|
|
139
|
-
const
|
|
140
|
-
|
|
141
|
-
|
|
137
|
+
function unarchiveAgentLocked(id, json) {
|
|
138
|
+
assertContainedHoldingArea(id);
|
|
139
|
+
const { dirReal } = assertOwnedAgentForDelete(id, archivedAgentDir(id), SLOT.archived);
|
|
140
|
+
const name = currentAgentName(dirReal);
|
|
141
|
+
const previousDir = resolve(archivedAgentDir(id));
|
|
142
|
+
const target = agentDir(id);
|
|
143
|
+
assertContainedSlot(id, target);
|
|
144
|
+
refuseOccupiedTarget(id, target);
|
|
142
145
|
|
|
143
146
|
renameSync(dirReal, target);
|
|
144
|
-
const
|
|
147
|
+
const jobs = registerAcademyJobs(target, id);
|
|
145
148
|
appendLifecycleEvent('agent_unarchived', name, resolve(target), { previousDir });
|
|
146
149
|
|
|
147
|
-
const scheduledJobId = nightly.registered ? nightly.id : null;
|
|
148
150
|
if (json) {
|
|
149
151
|
contractOk('unarchive', {
|
|
150
152
|
unarchived: true,
|
|
151
153
|
name,
|
|
152
154
|
dir: resolve(target),
|
|
153
155
|
previousDir,
|
|
154
|
-
|
|
156
|
+
...scheduledJobFields(jobs),
|
|
155
157
|
});
|
|
156
158
|
return;
|
|
157
159
|
}
|
|
158
160
|
console.log(`Unarchived agent "${name}" (${previousDir} → ${resolve(target)}).`);
|
|
159
|
-
|
|
160
|
-
else console.log(`Nightly consolidation job not registered: ${nightly.reason}`);
|
|
161
|
+
reportRegisteredJobs(jobs);
|
|
161
162
|
}
|
package/scripts/cli/archived.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { lstatSync, readdirSync, realpathSync, statSync } from 'node:fs';
|
|
2
2
|
import { join, resolve } from 'node:path';
|
|
3
3
|
import { AGENTS_ROOT, NAME_RE, activeCommandName, contractError, jsonMode } from './core.mjs';
|
|
4
|
+
import { agentIdOf } from './agent-id.mjs';
|
|
4
5
|
|
|
5
6
|
// The holding area, as one module. Archived agents live in a dot-directory
|
|
6
7
|
// beside their canonical slots, so the roster's NAME_RE filter already keeps
|
|
@@ -73,8 +74,9 @@ function isRealDirectory(path) {
|
|
|
73
74
|
// replacement for somebody who is still there. `unarchive` is the one command
|
|
74
75
|
// allowed to address one, so it is the one command that does not ask.
|
|
75
76
|
export function assertNotArchived(name, json = jsonMode()) {
|
|
76
|
-
|
|
77
|
-
|
|
77
|
+
const id = agentIdOf(name);
|
|
78
|
+
if (!isArchivedAgent(id)) return;
|
|
79
|
+
const dir = resolve(archivedAgentDir(id));
|
|
78
80
|
const message = `Agent "${name}" is archived at ${dir}. Unarchive it first.`;
|
|
79
81
|
if (json) contractError(activeCommandName(), 'agent_archived', message, { name, dir });
|
|
80
82
|
console.error(`Error: ${message}`);
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// `create <name> [--kind <kind>] [--no-start] [--json]`. Like `rename`, an option Academy
|
|
2
|
+
// does not know stays on the command so the handler answers it inside the
|
|
3
|
+
// envelope the caller asked for. The kind stays a raw string: only `create` can
|
|
4
|
+
// answer an unsupported one in the envelope.
|
|
5
|
+
export function parseCreateArgs(command, rest) {
|
|
6
|
+
const json = rest.includes('--json');
|
|
7
|
+
let name;
|
|
8
|
+
let kind;
|
|
9
|
+
let noStart = false;
|
|
10
|
+
for (let i = 0; i < rest.length; i++) {
|
|
11
|
+
const arg = rest[i];
|
|
12
|
+
if (arg === '--json') continue;
|
|
13
|
+
else if (arg === '--no-start') noStart = true;
|
|
14
|
+
else if (arg === '--kind') kind = rest[++i] ?? '';
|
|
15
|
+
else if (arg.startsWith('--kind=')) kind = arg.slice('--kind='.length);
|
|
16
|
+
else if (arg.startsWith('--')) return { command, json, name, invalidOption: arg };
|
|
17
|
+
else name ??= arg;
|
|
18
|
+
}
|
|
19
|
+
return { command, json, name, kind, noStart };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// `skills check <agent> [--json]`. Like `create`, an option Academy does not
|
|
23
|
+
// know stays on the command so the handler answers it in the envelope.
|
|
24
|
+
export function parseSkillsArgs(command, rest) {
|
|
25
|
+
const json = rest.includes('--json');
|
|
26
|
+
const positional = [];
|
|
27
|
+
for (const arg of rest) {
|
|
28
|
+
if (arg === '--json') continue;
|
|
29
|
+
else if (arg.startsWith('--')) return { command, json, invalidOption: arg };
|
|
30
|
+
else positional.push(arg);
|
|
31
|
+
}
|
|
32
|
+
return { command, json, action: positional[0], name: positional[1] };
|
|
33
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { NAME_RE } from './core.mjs';
|
|
2
|
+
|
|
3
|
+
// `notes add [<agent>] [--user] "text"` and `notes list [<agent>] [--last N]`. The first
|
|
4
|
+
// positional token is treated as an agent only when it looks like an agent name
|
|
5
|
+
// (NAME_RE) AND there is more to follow — so quoted single-arg text stays text.
|
|
6
|
+
export function parseNotesArgs(rest) {
|
|
7
|
+
const action = rest[0];
|
|
8
|
+
if (action !== 'add' && action !== 'list') {
|
|
9
|
+
console.error(`Unknown notes action: ${action ?? '(none)'}. Use 'add' or 'list'.`);
|
|
10
|
+
return { command: 'help', exitCode: 1 };
|
|
11
|
+
}
|
|
12
|
+
const args = rest.slice(1);
|
|
13
|
+
|
|
14
|
+
if (action === 'add') {
|
|
15
|
+
// `--user` proposes a fact about the user rather than noting for the agent.
|
|
16
|
+
const user = args.includes('--user');
|
|
17
|
+
const words = args.filter((arg) => arg !== '--user');
|
|
18
|
+
let name;
|
|
19
|
+
let textParts = words;
|
|
20
|
+
if (words.length >= 2 && NAME_RE.test(words[0])) {
|
|
21
|
+
name = words[0];
|
|
22
|
+
textParts = words.slice(1);
|
|
23
|
+
}
|
|
24
|
+
return { command: 'notes', action, name, text: textParts.join(' '), user };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// list — pull out --last N (or --last=N), the rest is an optional agent name.
|
|
28
|
+
let last = 12;
|
|
29
|
+
const positional = [];
|
|
30
|
+
for (let i = 0; i < args.length; i++) {
|
|
31
|
+
if (args[i] === '--last') {
|
|
32
|
+
last = Number.parseInt(args[++i], 10);
|
|
33
|
+
} else if (args[i].startsWith('--last=')) {
|
|
34
|
+
last = Number.parseInt(args[i].slice('--last='.length), 10);
|
|
35
|
+
} else {
|
|
36
|
+
positional.push(args[i]);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
if (!Number.isInteger(last) || last <= 0) last = 12;
|
|
40
|
+
const name = positional[0] && NAME_RE.test(positional[0]) ? positional[0] : undefined;
|
|
41
|
+
return { command: 'notes', action, name, last };
|
|
42
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// `rename <old> <new> [--json]`. Like `events`, `migrate` and `sessions`, an
|
|
2
|
+
// option Academy does not know stays on the command so the handler answers it
|
|
3
|
+
// inside the envelope the caller asked for.
|
|
4
|
+
export function parseRenameArgs(command, rest) {
|
|
5
|
+
const json = rest.includes('--json');
|
|
6
|
+
const positional = [];
|
|
7
|
+
for (const arg of rest) {
|
|
8
|
+
if (arg === '--json') continue;
|
|
9
|
+
else if (arg.startsWith('--')) return { command, json, invalidOption: arg };
|
|
10
|
+
else positional.push(arg);
|
|
11
|
+
}
|
|
12
|
+
return { command, json, name: positional[0], newName: positional[1] };
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const START_OPTIONS = {
|
|
16
|
+
'--mandate': 'mandate',
|
|
17
|
+
'--hours': 'hours',
|
|
18
|
+
'--contact': 'contact',
|
|
19
|
+
'--trust': 'trust',
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
// `start <name> [--mandate <file>] [--hours <h>] [--contact <c>] [--trust 1-4]
|
|
23
|
+
// [--json]`. Like `rename`, an option Academy does not know stays on the
|
|
24
|
+
// command so the handler answers it inside the envelope.
|
|
25
|
+
export function parseStartArgs(command, rest) {
|
|
26
|
+
const parsed = { command, json: rest.includes('--json') };
|
|
27
|
+
for (let i = 0; i < rest.length; i++) {
|
|
28
|
+
const arg = rest[i];
|
|
29
|
+
const [flag, inline] = arg.split(/=(.*)/s);
|
|
30
|
+
if (arg === '--json') continue;
|
|
31
|
+
else if (Object.hasOwn(START_OPTIONS, flag))
|
|
32
|
+
parsed[START_OPTIONS[flag]] = inline ?? rest[++i] ?? '';
|
|
33
|
+
else if (arg.startsWith('--') || parsed.name !== undefined) parsed.invalidOption ??= arg;
|
|
34
|
+
else parsed.name = arg;
|
|
35
|
+
}
|
|
36
|
+
return parsed;
|
|
37
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { RUNTIMES } from './core.mjs';
|
|
2
|
+
|
|
3
|
+
export function parseRunArgs(rest) {
|
|
4
|
+
const name = rest[0];
|
|
5
|
+
const afterName = rest.slice(1);
|
|
6
|
+
const dashIdx = afterName.indexOf('--');
|
|
7
|
+
const optionArgs = dashIdx >= 0 ? afterName.slice(0, dashIdx) : afterName;
|
|
8
|
+
const passthrough = dashIdx >= 0 ? afterName.slice(dashIdx + 1) : [];
|
|
9
|
+
// No default here. The agent is not resolved yet, and defaulting before it is
|
|
10
|
+
// what re-registered a codex agent's nightly job as claude-code for ten
|
|
11
|
+
// nights. A null runtime means "not explicit"; `run` reads the persisted
|
|
12
|
+
// scalar instead.
|
|
13
|
+
let runtime = null;
|
|
14
|
+
|
|
15
|
+
for (let i = 0; i < optionArgs.length; i++) {
|
|
16
|
+
const arg = optionArgs[i];
|
|
17
|
+
if (arg === '--agent') {
|
|
18
|
+
const value = optionArgs[++i];
|
|
19
|
+
if (!RUNTIMES.has(value))
|
|
20
|
+
return invalidRunOption(`Invalid --agent value: ${value ?? '(none)'}`);
|
|
21
|
+
runtime = value;
|
|
22
|
+
} else if (arg?.startsWith('--agent=')) {
|
|
23
|
+
const value = arg.slice('--agent='.length);
|
|
24
|
+
if (!RUNTIMES.has(value))
|
|
25
|
+
return invalidRunOption(`Invalid --agent value: ${value || '(none)'}`);
|
|
26
|
+
runtime = value;
|
|
27
|
+
} else if (arg) {
|
|
28
|
+
return invalidRunOption(`Unknown run option: ${arg}`);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return { command: 'run', name, runtime, passthrough };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function invalidRunOption(message) {
|
|
36
|
+
console.error(`${message}. Use --agent claude-code or --agent codex before --.`);
|
|
37
|
+
return { command: 'help', exitCode: 1 };
|
|
38
|
+
}
|