@joenandez/academy 0.4.0-rc.1
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 +14 -0
- package/.claude-plugin/plugin.json +6 -0
- package/CHANGELOG.md +46 -0
- package/LICENSE +21 -0
- package/README.md +209 -0
- package/bin/academy +2 -0
- package/conformance/README.md +60 -0
- package/conformance/discovery.test.mjs +140 -0
- package/conformance/envelope.test.mjs +185 -0
- package/conformance/error-codes.test.mjs +125 -0
- package/conformance/harness.mjs +180 -0
- package/conformance/identity.test.mjs +125 -0
- package/docs/integration-guide.md +1026 -0
- package/hooks/hook_runtime.mjs +100 -0
- package/hooks/hooks.json +26 -0
- package/hooks/inject_surface.py +122 -0
- package/hooks/memory_bridge.mjs +120 -0
- package/hooks/memory_store.mjs +66 -0
- package/hooks/register_session.mjs +51 -0
- package/hooks/sync_memory.mjs +27 -0
- package/package.json +41 -0
- package/scripts/agent.mjs +3 -0
- package/scripts/cli/archive.mjs +161 -0
- package/scripts/cli/archived.mjs +82 -0
- package/scripts/cli/args.mjs +282 -0
- package/scripts/cli/codex.mjs +216 -0
- package/scripts/cli/core.mjs +389 -0
- package/scripts/cli/create.mjs +242 -0
- package/scripts/cli/doctor.mjs +203 -0
- package/scripts/cli/eventlog.mjs +129 -0
- package/scripts/cli/events.mjs +80 -0
- package/scripts/cli/hire-headless.mjs +229 -0
- package/scripts/cli/hire-spec.mjs +164 -0
- package/scripts/cli/hire.mjs +92 -0
- package/scripts/cli/inspect.mjs +286 -0
- package/scripts/cli/lifecycle.mjs +296 -0
- package/scripts/cli/main.mjs +102 -0
- package/scripts/cli/migrate.mjs +183 -0
- package/scripts/cli/notes.mjs +104 -0
- package/scripts/cli/rename.mjs +172 -0
- package/scripts/cli/run.mjs +227 -0
- package/scripts/cli/runtime.mjs +47 -0
- package/scripts/cli/scaffold.mjs +332 -0
- package/scripts/cli/sessions.mjs +98 -0
- package/scripts/cli/templates.mjs +104 -0
- package/scripts/cli/yaml.mjs +124 -0
- package/skills/hire/SKILL.md +669 -0
- package/templates/agents/claude-code/knowledge-curator.md +14 -0
- package/templates/agents/codex/knowledge-curator.toml +9 -0
- package/templates/skills/check-in/SKILL.md +122 -0
- package/templates/skills/knowledge-curation/SKILL.md +132 -0
- package/templates/skills/nightly-consolidation/SKILL.md +240 -0
- package/templates/skills/self-update/SKILL.md +121 -0
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { readFileSync, renameSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { agentLifecycleLockPath, withFileLock } from './core.mjs';
|
|
4
|
+
|
|
5
|
+
// agent.yaml is hand-authored: it carries comments and a `surfaces:` list that
|
|
6
|
+
// the CLI's line-regex reader cannot represent. Writing is therefore line
|
|
7
|
+
// surgery, never re-serialization — anything the writer does not touch keeps
|
|
8
|
+
// its exact bytes.
|
|
9
|
+
|
|
10
|
+
/** The reader's key line: a top-level scalar, value optional. */
|
|
11
|
+
const scalarLine = (key) => new RegExp(`^${key}:[ \\t]*(.*)$`);
|
|
12
|
+
/** Any line naming the same key in a form neither end of this module handles. */
|
|
13
|
+
const looseKeyLine = (key) => new RegExp(`^\\s*(["']?)${key}\\1\\s*:`);
|
|
14
|
+
/** A top-level scalar carrying a non-empty value. `surfaces:` does not match. */
|
|
15
|
+
const VALUED_SCALAR_RE = /^[a-z_][a-z0-9_]*:[ \t]*\S/;
|
|
16
|
+
|
|
17
|
+
// Raised when agent.yaml already names the key in a shape the line surgery
|
|
18
|
+
// cannot rewrite. Inserting a second top-level key instead would leave a
|
|
19
|
+
// duplicate mapping key in a file §3.7 publishes for clients to parse, and a
|
|
20
|
+
// strict parser rejects that outright. Thrown, not exited, so a --json caller
|
|
21
|
+
// can answer in the envelope; `invalid_spec` is the published code for an agent
|
|
22
|
+
// spec Academy will not accept.
|
|
23
|
+
export class AgentSpecError extends Error {
|
|
24
|
+
constructor(message, fields) {
|
|
25
|
+
super(message);
|
|
26
|
+
this.code = 'invalid_spec';
|
|
27
|
+
this.fields = fields;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function lastIndexMatching(lines, test) {
|
|
32
|
+
for (let i = lines.length - 1; i >= 0; i -= 1) {
|
|
33
|
+
if (test(lines[i])) return i;
|
|
34
|
+
}
|
|
35
|
+
return -1;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// The read, the edit and the commit belong to one critical section. agent.yaml
|
|
39
|
+
// is published as client-writable, `delete` moves the whole directory, and a
|
|
40
|
+
// nightly child can run at the same instant as an interactive one — so the
|
|
41
|
+
// scalar write takes the same lock every lifecycle command takes.
|
|
42
|
+
export function writeAgentYamlScalar(dir, key, value) {
|
|
43
|
+
withFileLock(agentLifecycleLockPath(dir), () => rewriteAgentYamlScalar(dir, key, value));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// The same write for a caller that already holds the agent's lifecycle lock.
|
|
47
|
+
// `rename` moves the directory and rewrites `name:` in one critical section, so
|
|
48
|
+
// taking the lock a second time here would block against its own holder until
|
|
49
|
+
// the timeout and report `lock_timeout` for work that was never contended.
|
|
50
|
+
export function rewriteAgentYamlScalar(dir, key, value) {
|
|
51
|
+
const path = join(dir, 'agent.yaml');
|
|
52
|
+
const text = readFileSync(path, 'utf8');
|
|
53
|
+
const eol = text.includes('\r\n') ? '\r\n' : '\n';
|
|
54
|
+
const lines = splitLines(text.replace(/\r?\n$/, ''));
|
|
55
|
+
const line = `${key}: ${value}`;
|
|
56
|
+
|
|
57
|
+
// Rewrite the last occurrence: the reader takes the last key line too, so a
|
|
58
|
+
// hand-edited duplicate cannot shadow what was just written.
|
|
59
|
+
const present = lastIndexMatching(lines, (candidate) => scalarLine(key).test(candidate));
|
|
60
|
+
if (present !== -1) lines[present] = line;
|
|
61
|
+
else lines.splice(insertionIndex(lines, key, path) + 1, 0, line);
|
|
62
|
+
|
|
63
|
+
commit(path, lines.join(eol) + eol);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// The strict key line is absent. Inserting is safe only when nothing else in
|
|
67
|
+
// the file claims the key: an indented, quoted or space-padded one is invisible
|
|
68
|
+
// to this module's reader and writer alike, so adding a second is a duplicate
|
|
69
|
+
// no client should have to resolve.
|
|
70
|
+
function insertionIndex(lines, key, path) {
|
|
71
|
+
const loose = lastIndexMatching(lines, (candidate) => looseKeyLine(key).test(candidate));
|
|
72
|
+
if (loose !== -1) {
|
|
73
|
+
throw new AgentSpecError(
|
|
74
|
+
`agent.yaml at ${path} already declares "${key}" on line ${loose + 1} in a form Academy cannot rewrite. Restate it as a top-level \`${key}: <value>\` and retry.`,
|
|
75
|
+
{ path, key, line: loose + 1 },
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
return anchorIndex(lines);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Truncating the published file in place has a window where a crash leaves it
|
|
82
|
+
// empty and a concurrent reader sees no scalars at all — `name`, `created`,
|
|
83
|
+
// `role`, the `surfaces:` block, gone. Rename is atomic, so every reader sees
|
|
84
|
+
// either the whole old file or the whole new one.
|
|
85
|
+
function commit(path, contents) {
|
|
86
|
+
const temp = `${path}.tmp`;
|
|
87
|
+
writeFileSync(temp, contents);
|
|
88
|
+
renameSync(temp, path);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Insert after the last scalar carrying a value. Inserting after the last line
|
|
92
|
+
// the reader recognizes would land inside `surfaces:` and split the list from
|
|
93
|
+
// its key. With no valued scalar anywhere, append at end of file.
|
|
94
|
+
function anchorIndex(lines) {
|
|
95
|
+
const anchor = lastIndexMatching(lines, (line) => VALUED_SCALAR_RE.test(line));
|
|
96
|
+
return anchor === -1 ? lines.length - 1 : anchor;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// One line splitter for both ends. `.` does not match `\r`, so a `$`-anchored
|
|
100
|
+
// key regex matched nothing at all on a CRLF file and Academy reported the
|
|
101
|
+
// default runtime for an agent that declared another one.
|
|
102
|
+
function splitLines(text) {
|
|
103
|
+
return text.split(/\r?\n/);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// The matching reader: only the top-level scalars the CLI needs, taking the
|
|
107
|
+
// last occurrence of a key so it agrees with the writer above. A missing or
|
|
108
|
+
// unreadable agent.yaml reads as an empty record, never as an error.
|
|
109
|
+
const READ_SCALAR_RE = /^([a-z_][a-z0-9_]*):\s*(.*)$/i;
|
|
110
|
+
|
|
111
|
+
export function readAgentYaml(dir) {
|
|
112
|
+
const out = {};
|
|
113
|
+
let text;
|
|
114
|
+
try {
|
|
115
|
+
text = readFileSync(join(dir, 'agent.yaml'), 'utf8');
|
|
116
|
+
} catch {
|
|
117
|
+
return out;
|
|
118
|
+
}
|
|
119
|
+
for (const line of splitLines(text)) {
|
|
120
|
+
const match = line.match(READ_SCALAR_RE);
|
|
121
|
+
if (match) out[match[1]] = match[2].trim().replace(/^["']|["']$/g, '');
|
|
122
|
+
}
|
|
123
|
+
return out;
|
|
124
|
+
}
|