@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.
Files changed (53) hide show
  1. package/.claude-plugin/marketplace.json +14 -0
  2. package/.claude-plugin/plugin.json +6 -0
  3. package/CHANGELOG.md +46 -0
  4. package/LICENSE +21 -0
  5. package/README.md +209 -0
  6. package/bin/academy +2 -0
  7. package/conformance/README.md +60 -0
  8. package/conformance/discovery.test.mjs +140 -0
  9. package/conformance/envelope.test.mjs +185 -0
  10. package/conformance/error-codes.test.mjs +125 -0
  11. package/conformance/harness.mjs +180 -0
  12. package/conformance/identity.test.mjs +125 -0
  13. package/docs/integration-guide.md +1026 -0
  14. package/hooks/hook_runtime.mjs +100 -0
  15. package/hooks/hooks.json +26 -0
  16. package/hooks/inject_surface.py +122 -0
  17. package/hooks/memory_bridge.mjs +120 -0
  18. package/hooks/memory_store.mjs +66 -0
  19. package/hooks/register_session.mjs +51 -0
  20. package/hooks/sync_memory.mjs +27 -0
  21. package/package.json +41 -0
  22. package/scripts/agent.mjs +3 -0
  23. package/scripts/cli/archive.mjs +161 -0
  24. package/scripts/cli/archived.mjs +82 -0
  25. package/scripts/cli/args.mjs +282 -0
  26. package/scripts/cli/codex.mjs +216 -0
  27. package/scripts/cli/core.mjs +389 -0
  28. package/scripts/cli/create.mjs +242 -0
  29. package/scripts/cli/doctor.mjs +203 -0
  30. package/scripts/cli/eventlog.mjs +129 -0
  31. package/scripts/cli/events.mjs +80 -0
  32. package/scripts/cli/hire-headless.mjs +229 -0
  33. package/scripts/cli/hire-spec.mjs +164 -0
  34. package/scripts/cli/hire.mjs +92 -0
  35. package/scripts/cli/inspect.mjs +286 -0
  36. package/scripts/cli/lifecycle.mjs +296 -0
  37. package/scripts/cli/main.mjs +102 -0
  38. package/scripts/cli/migrate.mjs +183 -0
  39. package/scripts/cli/notes.mjs +104 -0
  40. package/scripts/cli/rename.mjs +172 -0
  41. package/scripts/cli/run.mjs +227 -0
  42. package/scripts/cli/runtime.mjs +47 -0
  43. package/scripts/cli/scaffold.mjs +332 -0
  44. package/scripts/cli/sessions.mjs +98 -0
  45. package/scripts/cli/templates.mjs +104 -0
  46. package/scripts/cli/yaml.mjs +124 -0
  47. package/skills/hire/SKILL.md +669 -0
  48. package/templates/agents/claude-code/knowledge-curator.md +14 -0
  49. package/templates/agents/codex/knowledge-curator.toml +9 -0
  50. package/templates/skills/check-in/SKILL.md +122 -0
  51. package/templates/skills/knowledge-curation/SKILL.md +132 -0
  52. package/templates/skills/nightly-consolidation/SKILL.md +240 -0
  53. 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
+ }