@openlife/cli 1.7.6 → 1.7.9

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/dist/index.js CHANGED
@@ -338,6 +338,50 @@ program.command('init')
338
338
  }
339
339
  });
340
340
  // ============================================================================
341
+ // OBSERVABILITY — `openlife status`, `openlife logs`
342
+ // ============================================================================
343
+ program.command('status')
344
+ .description('Consolidated runtime state (heartbeat + executors + governance + queue) as JSON')
345
+ .option('--watch', 're-print every 5 seconds (Ctrl-C to stop)', false)
346
+ .action(async (options) => {
347
+ const { buildStatusReport, renderStatusReport } = require('./cli/StatusCommand');
348
+ const printOnce = () => {
349
+ const report = buildStatusReport(process.cwd());
350
+ console.log(renderStatusReport(report));
351
+ if (report.overall !== 'healthy')
352
+ process.exitCode = 1;
353
+ };
354
+ if (!options.watch) {
355
+ printOnce();
356
+ return;
357
+ }
358
+ printOnce();
359
+ const interval = setInterval(() => {
360
+ console.log('---');
361
+ printOnce();
362
+ }, 5000);
363
+ process.on('SIGINT', () => {
364
+ clearInterval(interval);
365
+ process.exit(0);
366
+ });
367
+ });
368
+ program.command('logs')
369
+ .description('Readable tail of .openlife/*.jsonl event streams')
370
+ .option('--tail <N>', 'number of entries to show', '20')
371
+ .option('--since <duration>', 'only entries newer than e.g. 5m | 1h | 2d')
372
+ .option('--filter <subsystem>', 'match subsystem (governance, media-routing, capability-lifecycle, canonization-log)')
373
+ .option('--json', 'emit one JSON object per line instead of human-readable text', false)
374
+ .action((options) => {
375
+ const { collectLogs, renderLogsHuman, renderLogsJson } = require('./cli/LogsCommand');
376
+ const tail = Number.parseInt(options.tail || '20', 10);
377
+ const entries = collectLogs({
378
+ tail: Number.isFinite(tail) && tail > 0 ? tail : 20,
379
+ since: options.since,
380
+ filter: options.filter,
381
+ });
382
+ console.log(options.json ? renderLogsJson(entries) : renderLogsHuman(entries));
383
+ });
384
+ // ============================================================================
341
385
  // AUTENTICAÇÃO (AUTH)
342
386
  // ============================================================================
343
387
  const authCmd = program.command('auth').description('Gerencia a autenticação nativa sem uso de API Keys');
@@ -2038,32 +2082,196 @@ aiobuilderCmd.command('mode')
2038
2082
  }
2039
2083
  console.log(JSON.stringify(r.setMode(options.set, options.profile), null, 2));
2040
2084
  });
2085
+ // Story 5.5 (M5) — real authoring pipeline. Replaces the 6-line placeholder
2086
+ // stub. Accepts rich flags + optional --interactive readline prompts, then
2087
+ // delegates to AgentCreator / SquadCreator for atomic write.
2088
+ const splitCsv = (s) => (s || '')
2089
+ .split(',')
2090
+ .map((v) => v.trim())
2091
+ .filter((v) => v.length > 0);
2041
2092
  aiobuilderCmd.command('create-agent <id>')
2042
- .option('--role <role>', 'papel', 'specialist')
2043
- .option('--notes <notes>', 'notas', '')
2044
- .action((id, options) => {
2045
- const base = path.join(process.cwd(), '.catalog', 'agents', id);
2046
- fs.mkdirSync(base, { recursive: true });
2047
- const out = path.join(base, 'AGENT.md');
2048
- fs.writeFileSync(out, `---\nid: ${id}\nname: ${id}\nrole: ${options.role}\nsource: aiobuilder\n---\n\n# ${id}\n\n${options.notes || 'Criado pelo AIOBUILDER.'}\n`, 'utf-8');
2049
- console.log(JSON.stringify({ ok: true, type: 'agent', path: out }, null, 2));
2093
+ .description('Author a new agent in .catalog/agents/<id>/AGENT.md (status: draft)')
2094
+ .option('--role <role>', 'role label (e.g. specialist, planner)', 'specialist')
2095
+ .option('--name <name>', 'display name (defaults to id)')
2096
+ .option('--domain <domain>', 'business domain')
2097
+ .option('--expertise <csv>', 'comma-separated expertise tags')
2098
+ .option('--skills <csv>', 'comma-separated primary skill ids (must exist in .catalog/skills/)')
2099
+ .option('--parent-squad <id>', 'parent squad id (must exist in .catalog/squads/)')
2100
+ .option('--persona <text>', 'free-form persona description')
2101
+ .option('--when-to-use <text>', 'goal / when-to-use sentence')
2102
+ .option('--status <status>', 'active | draft | archived', 'draft')
2103
+ .option('--interactive', 'prompt for missing fields via readline', false)
2104
+ .action(async (id, options) => {
2105
+ const { AgentCreator } = require('./orchestrator/AgentCreator');
2106
+ let name = options.name || id;
2107
+ let role = options.role;
2108
+ let domain = options.domain;
2109
+ let expertise = splitCsv(options.expertise);
2110
+ let primarySkills = splitCsv(options.skills);
2111
+ let parentSquad = options.parentSquad;
2112
+ let persona = options.persona;
2113
+ let whenToUse = options.whenToUse;
2114
+ if (options.interactive && process.stdin.isTTY) {
2115
+ const { ReadlineAnswerProvider } = require('./cli/InstallWizard');
2116
+ const provider = new ReadlineAnswerProvider();
2117
+ try {
2118
+ console.log(`\n📝 Authoring agent: ${id}\n`);
2119
+ name = await provider.text('Display name', name);
2120
+ role = await provider.text('Role (e.g. specialist, planner, reviewer)', role);
2121
+ domain = await provider.text('Business domain (optional, Enter to skip)', domain || '');
2122
+ const expertiseRaw = await provider.text('Expertise tags (comma-separated, optional)', expertise.join(','));
2123
+ expertise = splitCsv(expertiseRaw);
2124
+ const skillsRaw = await provider.text('Primary skill ids (comma-separated, optional)', primarySkills.join(','));
2125
+ primarySkills = splitCsv(skillsRaw);
2126
+ parentSquad = await provider.text('Parent squad id (optional)', parentSquad || '');
2127
+ whenToUse = await provider.text('When to use this agent (one sentence)', whenToUse || `Execute the ${role} role on demand.`);
2128
+ }
2129
+ finally {
2130
+ provider.close?.();
2131
+ }
2132
+ }
2133
+ else if (options.interactive) {
2134
+ console.error('⚠️ --interactive ignored (no TTY). Using flag values + defaults.');
2135
+ }
2136
+ const creator = new AgentCreator();
2137
+ const result = creator.create({
2138
+ id,
2139
+ name,
2140
+ role,
2141
+ domain: domain || undefined,
2142
+ expertise: expertise.length ? expertise : undefined,
2143
+ primarySkills: primarySkills.length ? primarySkills : undefined,
2144
+ parentSquad: parentSquad || undefined,
2145
+ persona,
2146
+ whenToUse,
2147
+ status: options.status,
2148
+ });
2149
+ if (!result.ok) {
2150
+ console.log(JSON.stringify({ ok: false, type: 'agent', ...result }, null, 2));
2151
+ process.exitCode = 1;
2152
+ return;
2153
+ }
2154
+ const validation = creator.validate(id);
2155
+ console.log(JSON.stringify({
2156
+ ok: true,
2157
+ type: 'agent',
2158
+ agentId: result.agentId,
2159
+ path: result.filePath,
2160
+ warnings: validation.warnings,
2161
+ }, null, 2));
2050
2162
  });
2051
2163
  aiobuilderCmd.command('create-squad <id>')
2052
- .option('--domain <domain>', 'domínio', 'general')
2053
- .option('--notes <notes>', 'notas', '')
2054
- .action((id, options) => {
2055
- const base = path.join(process.cwd(), '.catalog', 'squads', id);
2056
- fs.mkdirSync(base, { recursive: true });
2057
- const out = path.join(base, 'SQUAD.md');
2058
- fs.writeFileSync(out, `---\nid: ${id}\ndomain: ${options.domain}\nsource: aiobuilder\n---\n\n# ${id}\n\n${options.notes || 'Criada pelo AIOBUILDER.'}\n`, 'utf-8');
2059
- console.log(JSON.stringify({ ok: true, type: 'squad', path: out }, null, 2));
2164
+ .description('Author a new squad in .catalog/squads/<id>/ with at least one agent')
2165
+ .option('--name <name>', 'display name (defaults to id)')
2166
+ .option('--domain <domain>', 'business domain', 'general')
2167
+ .option('--description <text>', 'short description', '')
2168
+ .option('--agents <csv>', 'comma-separated initial agent ids', '')
2169
+ .option('--status <status>', 'active | draft', 'draft')
2170
+ .option('--interactive', 'prompt for missing fields via readline', false)
2171
+ .action(async (id, options) => {
2172
+ const { SquadCreator } = require('./orchestrator/SquadCreator');
2173
+ let name = options.name || id;
2174
+ let domain = options.domain;
2175
+ let description = options.description;
2176
+ let agentIds = splitCsv(options.agents);
2177
+ if (options.interactive && process.stdin.isTTY) {
2178
+ const { ReadlineAnswerProvider } = require('./cli/InstallWizard');
2179
+ const provider = new ReadlineAnswerProvider();
2180
+ try {
2181
+ console.log(`\n📝 Authoring squad: ${id}\n`);
2182
+ name = await provider.text('Display name', name);
2183
+ domain = await provider.text('Domain', domain);
2184
+ description = await provider.text('Description (one sentence)', description || `${name} squad — ${domain} domain`);
2185
+ const agentsRaw = await provider.text('Initial agent ids (comma-separated, at least one)', agentIds.join(','));
2186
+ agentIds = splitCsv(agentsRaw);
2187
+ }
2188
+ finally {
2189
+ provider.close?.();
2190
+ }
2191
+ }
2192
+ else if (options.interactive) {
2193
+ console.error('⚠️ --interactive ignored (no TTY). Using flag values + defaults.');
2194
+ }
2195
+ if (agentIds.length === 0) {
2196
+ agentIds = [`${id}-lead`];
2197
+ }
2198
+ const creator = new SquadCreator();
2199
+ const result = creator.create({
2200
+ id,
2201
+ name,
2202
+ description: description || `${name} squad`,
2203
+ agents: agentIds.map((agentId) => ({
2204
+ id: agentId,
2205
+ name: agentId,
2206
+ role: 'specialist',
2207
+ })),
2208
+ status: options.status,
2209
+ source: 'aiobuilder',
2210
+ });
2211
+ if (!result.ok) {
2212
+ console.log(JSON.stringify({ ok: false, type: 'squad', ...result }, null, 2));
2213
+ process.exitCode = 1;
2214
+ return;
2215
+ }
2216
+ console.log(JSON.stringify({
2217
+ ok: true,
2218
+ type: 'squad',
2219
+ squadId: result.squadId,
2220
+ squadDir: result.squadDir,
2221
+ filesCreated: result.filesCreated.length,
2222
+ }, null, 2));
2060
2223
  });
2061
- aiobuilderCmd.command('validate-catalog').action(() => {
2062
- const agents = countCatalogFiles('agents', 'AGENT.md');
2063
- const squads = countCatalogFiles('squads', 'SQUAD.md');
2064
- const ok = agents.total >= 300 && squads.total >= 2;
2065
- console.log(JSON.stringify({ ok, agents, squads }, null, 2));
2066
- if (!ok)
2224
+ aiobuilderCmd.command('validate-catalog')
2225
+ .description('Semantic validation of .catalog/ agents + squads (JSON by default)')
2226
+ .option('--human', 'emit human-readable text instead of JSON', false)
2227
+ .option('--json', 'emit machine-readable JSON only (backward-compatible no-op; JSON is default)', false)
2228
+ .action((options) => {
2229
+ const { AgentCreator } = require('./orchestrator/AgentCreator');
2230
+ const { SquadCreator } = require('./orchestrator/SquadCreator');
2231
+ const agentCreator = new AgentCreator();
2232
+ const squadCreator = new SquadCreator();
2233
+ const agentList = agentCreator.list();
2234
+ const squadList = squadCreator.list();
2235
+ const errors = [];
2236
+ const warnings = [];
2237
+ for (const a of agentList) {
2238
+ const v = agentCreator.validate(a.id);
2239
+ for (const err of v.errors)
2240
+ errors.push({ kind: 'agent', id: a.id, error: err });
2241
+ for (const w of v.warnings)
2242
+ warnings.push({ kind: 'agent', id: a.id, warning: w });
2243
+ }
2244
+ for (const s of squadList) {
2245
+ const v = squadCreator.validate(s.id);
2246
+ for (const err of v.errors)
2247
+ errors.push({ kind: 'squad', id: s.id, error: err });
2248
+ for (const w of v.warnings)
2249
+ warnings.push({ kind: 'squad', id: s.id, warning: w });
2250
+ }
2251
+ const report = {
2252
+ ok: errors.length === 0,
2253
+ parsed: { agents: agentList.length, squads: squadList.length },
2254
+ // Back-compat shape (consumed by test_openlife_evolution_surface and
2255
+ // any operator script that grew up under the old threshold output).
2256
+ agents: { total: agentList.length },
2257
+ squads: { total: squadList.length },
2258
+ errors,
2259
+ warnings,
2260
+ };
2261
+ if (options.human) {
2262
+ console.log(`Catalog validation — agents: ${agentList.length}, squads: ${squadList.length}`);
2263
+ console.log(` errors: ${errors.length}, warnings: ${warnings.length}`);
2264
+ for (const e of errors)
2265
+ console.log(` ❌ [${e.kind}/${e.id}] ${e.error}`);
2266
+ for (const w of warnings.slice(0, 10))
2267
+ console.log(` ⚠️ [${w.kind}/${w.id}] ${w.warning}`);
2268
+ if (warnings.length > 10)
2269
+ console.log(` ... and ${warnings.length - 10} more warnings`);
2270
+ }
2271
+ else {
2272
+ console.log(JSON.stringify(report, null, 2));
2273
+ }
2274
+ if (!report.ok)
2067
2275
  process.exitCode = 1;
2068
2276
  });
2069
2277
  aiobuilderCmd.command('generate-ui <featureName>')
@@ -2227,16 +2435,6 @@ aiobuilderCmd.command('test')
2227
2435
  if (r.status !== 0)
2228
2436
  process.exitCode = 1;
2229
2437
  });
2230
- aiobuilderCmd.command('preview')
2231
- .description('Prepare a preview URL for the current branch (v1.3 will wire to Vercel/Railway)')
2232
- .action(() => {
2233
- console.log(JSON.stringify({
2234
- ok: true,
2235
- action: 'aiobuilder.preview',
2236
- note: 'preview deployment integration deferred to v1.3 capability genesis epic',
2237
- next: 'manually push to feature branch + open PR for now',
2238
- }, null, 2));
2239
- });
2240
2438
  aiobuilderCmd.command('release')
2241
2439
  .description('Run QA loop + open PR (delegates to qa-loop workflow)')
2242
2440
  .action(async () => {
@@ -2253,16 +2451,6 @@ aiobuilderCmd.command('release')
2253
2451
  const state = await engine.run(r.workflow);
2254
2452
  console.log(JSON.stringify({ ok: state.status === 'completed', action: 'aiobuilder.release', state }, null, 2));
2255
2453
  });
2256
- aiobuilderCmd.command('infra')
2257
- .description('Infrastructure report (placeholder — real integration v1.3)')
2258
- .action(() => {
2259
- console.log(JSON.stringify({
2260
- ok: true,
2261
- action: 'aiobuilder.infra',
2262
- note: 'infra integration deferred to v1.3',
2263
- checks: ['supabase: TBD', 'railway: TBD', 'env: TBD'],
2264
- }, null, 2));
2265
- });
2266
2454
  aiobuilderCmd.command('score')
2267
2455
  .description('Show squad/skill scores from .artifacts/squad-scores.json')
2268
2456
  .action(() => {
@@ -0,0 +1,362 @@
1
+ "use strict";
2
+ /**
3
+ * AgentCreator — authoring system for standalone agents under `.catalog/agents/`.
4
+ *
5
+ * Story 5.5 — OpenLife v1.8 (M5 of Hit-10 sprint).
6
+ *
7
+ * Replaces the 6-line placeholder previously inlined in
8
+ * `aiobuilder create-agent` with a real authoring pipeline that produces
9
+ * an AGENT.md with complete frontmatter + structured sections (Goal,
10
+ * Commands, Dependencies, Persona, Guardrails). Mirrors SkillCreator
11
+ * (single-file output) rather than SquadCreator (multi-file directory),
12
+ * since standalone agents live as one AGENT.md inside their own directory.
13
+ *
14
+ * Methods:
15
+ * - create(proposal) — render full AGENT.md from a proposal
16
+ * - validate(id) — semantic frontmatter + cross-ref check
17
+ * - list(filter?) — direct read of `.catalog/agents/`
18
+ * - analyze(id) — surface missing/orphan references
19
+ */
20
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
21
+ if (k2 === undefined) k2 = k;
22
+ var desc = Object.getOwnPropertyDescriptor(m, k);
23
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
24
+ desc = { enumerable: true, get: function() { return m[k]; } };
25
+ }
26
+ Object.defineProperty(o, k2, desc);
27
+ }) : (function(o, m, k, k2) {
28
+ if (k2 === undefined) k2 = k;
29
+ o[k2] = m[k];
30
+ }));
31
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
32
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
33
+ }) : function(o, v) {
34
+ o["default"] = v;
35
+ });
36
+ var __importStar = (this && this.__importStar) || (function () {
37
+ var ownKeys = function(o) {
38
+ ownKeys = Object.getOwnPropertyNames || function (o) {
39
+ var ar = [];
40
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
41
+ return ar;
42
+ };
43
+ return ownKeys(o);
44
+ };
45
+ return function (mod) {
46
+ if (mod && mod.__esModule) return mod;
47
+ var result = {};
48
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
49
+ __setModuleDefault(result, mod);
50
+ return result;
51
+ };
52
+ })();
53
+ Object.defineProperty(exports, "__esModule", { value: true });
54
+ exports.AgentCreator = void 0;
55
+ const fs = __importStar(require("fs"));
56
+ const path = __importStar(require("path"));
57
+ const AtomicWriter_1 = require("./util/AtomicWriter");
58
+ const ID_PATTERN = /^[a-z0-9][a-z0-9._-]+$/i;
59
+ const VALID_STATUSES = new Set(['active', 'draft', 'archived']);
60
+ class AgentCreator {
61
+ catalogRoot;
62
+ skillsCatalogRoot;
63
+ squadsCatalogRoot;
64
+ constructor(opts = {}) {
65
+ const cwd = process.cwd();
66
+ this.catalogRoot = opts.catalogRoot || path.join(cwd, '.catalog', 'agents');
67
+ this.skillsCatalogRoot = opts.skillsCatalogRoot || path.join(cwd, '.catalog', 'skills');
68
+ this.squadsCatalogRoot = opts.squadsCatalogRoot || path.join(cwd, '.catalog', 'squads');
69
+ }
70
+ // ─────────────────────────────────────────────────────────
71
+ // create
72
+ // ─────────────────────────────────────────────────────────
73
+ create(proposal) {
74
+ if (!proposal.id || !ID_PATTERN.test(proposal.id)) {
75
+ return { ok: false, error: 'invalid_agent_id', detail: 'must match /^[a-z0-9][a-z0-9._-]+$/i' };
76
+ }
77
+ if (!proposal.name?.trim()) {
78
+ return { ok: false, error: 'missing_name' };
79
+ }
80
+ if (!proposal.role?.trim()) {
81
+ return { ok: false, error: 'missing_role' };
82
+ }
83
+ if (proposal.status && !VALID_STATUSES.has(proposal.status)) {
84
+ return { ok: false, error: 'invalid_status', detail: `must be one of: ${Array.from(VALID_STATUSES).join(', ')}` };
85
+ }
86
+ const agentDir = path.join(this.catalogRoot, proposal.id);
87
+ if (fs.existsSync(agentDir)) {
88
+ return { ok: false, error: 'agent_already_exists', detail: agentDir };
89
+ }
90
+ try {
91
+ fs.mkdirSync(agentDir, { recursive: true });
92
+ const content = this.renderAgentMd(proposal);
93
+ const filePath = path.join(agentDir, 'AGENT.md');
94
+ (0, AtomicWriter_1.writeStringAtomic)(filePath, content);
95
+ return { ok: true, agentId: proposal.id, agentDir, filePath };
96
+ }
97
+ catch (err) {
98
+ try {
99
+ fs.rmSync(agentDir, { recursive: true, force: true });
100
+ }
101
+ catch { /* ignore */ }
102
+ return { ok: false, error: 'render_failed', detail: err instanceof Error ? err.message : String(err) };
103
+ }
104
+ }
105
+ // ─────────────────────────────────────────────────────────
106
+ // validate
107
+ // ─────────────────────────────────────────────────────────
108
+ validate(agentId) {
109
+ const errors = [];
110
+ const warnings = [];
111
+ const agentDir = path.join(this.catalogRoot, agentId);
112
+ const agentFile = path.join(agentDir, 'AGENT.md');
113
+ if (!fs.existsSync(agentDir)) {
114
+ errors.push(`agent directory not found: ${agentDir}`);
115
+ return { ok: false, agentId, errors, warnings };
116
+ }
117
+ if (!fs.existsSync(agentFile)) {
118
+ errors.push('AGENT.md missing');
119
+ return { ok: false, agentId, errors, warnings };
120
+ }
121
+ const content = fs.readFileSync(agentFile, 'utf-8');
122
+ const fm = this.parseFrontmatter(content);
123
+ if (!fm.id)
124
+ errors.push('frontmatter missing id');
125
+ if (!fm.name)
126
+ errors.push('frontmatter missing name');
127
+ if (!fm.role)
128
+ errors.push('frontmatter missing role');
129
+ if (fm.id && fm.id !== agentId) {
130
+ errors.push(`frontmatter id '${fm.id}' does not match directory '${agentId}'`);
131
+ }
132
+ if (fm.status && !VALID_STATUSES.has(fm.status)) {
133
+ errors.push(`invalid status '${fm.status}' (must be active|draft|archived)`);
134
+ }
135
+ // Cross-reference: parent squad
136
+ if (fm.parentSquad) {
137
+ const squadPath = path.join(this.squadsCatalogRoot, fm.parentSquad);
138
+ if (!fs.existsSync(squadPath)) {
139
+ warnings.push(`parent squad '${fm.parentSquad}' not found in catalog`);
140
+ }
141
+ }
142
+ // Cross-reference: primary skills
143
+ const skillRefs = this.parseListField(content, 'primarySkills');
144
+ for (const skillId of skillRefs) {
145
+ const skillPath = path.join(this.skillsCatalogRoot, skillId);
146
+ if (!fs.existsSync(skillPath)) {
147
+ warnings.push(`primary skill '${skillId}' not found in catalog`);
148
+ }
149
+ }
150
+ return { ok: errors.length === 0, agentId, errors, warnings };
151
+ }
152
+ // ─────────────────────────────────────────────────────────
153
+ // list
154
+ // ─────────────────────────────────────────────────────────
155
+ list(filter) {
156
+ if (!fs.existsSync(this.catalogRoot))
157
+ return [];
158
+ const out = [];
159
+ for (const entry of fs.readdirSync(this.catalogRoot)) {
160
+ const agentFile = path.join(this.catalogRoot, entry, 'AGENT.md');
161
+ if (!fs.existsSync(agentFile))
162
+ continue;
163
+ const fm = this.parseFrontmatter(fs.readFileSync(agentFile, 'utf-8'));
164
+ const item = {
165
+ id: fm.id || entry,
166
+ name: fm.name || entry,
167
+ role: fm.role || 'unknown',
168
+ status: fm.status || 'unknown',
169
+ parentSquad: fm.parentSquad || null,
170
+ };
171
+ if (filter?.status && item.status !== filter.status)
172
+ continue;
173
+ if (filter?.squad && item.parentSquad !== filter.squad)
174
+ continue;
175
+ out.push(item);
176
+ }
177
+ return out;
178
+ }
179
+ // ─────────────────────────────────────────────────────────
180
+ // analyze
181
+ // ─────────────────────────────────────────────────────────
182
+ analyze(agentId) {
183
+ const agentDir = path.join(this.catalogRoot, agentId);
184
+ const agentFile = path.join(agentDir, 'AGENT.md');
185
+ if (!fs.existsSync(agentFile))
186
+ return null;
187
+ const content = fs.readFileSync(agentFile, 'utf-8');
188
+ const fm = this.parseFrontmatter(content);
189
+ const hasPersona = /^## Persona\b/m.test(content);
190
+ const commandLines = content.match(/^\s*-\s*name:\s*[\w-]+/gm) || [];
191
+ const skillRefs = this.parseListField(content, 'primarySkills');
192
+ const orphanSkills = [];
193
+ for (const skillId of skillRefs) {
194
+ if (!fs.existsSync(path.join(this.skillsCatalogRoot, skillId))) {
195
+ orphanSkills.push(skillId);
196
+ }
197
+ }
198
+ const squadReference = fm.parentSquad || null;
199
+ const squadExists = squadReference
200
+ ? fs.existsSync(path.join(this.squadsCatalogRoot, squadReference))
201
+ : null;
202
+ const suggestions = [];
203
+ if (!hasPersona)
204
+ suggestions.push('add a Persona section to clarify behavior under load');
205
+ if (commandLines.length === 0)
206
+ suggestions.push('declare at least one command so operators know how to invoke');
207
+ if (orphanSkills.length > 0)
208
+ suggestions.push(`${orphanSkills.length} skill reference(s) not in catalog`);
209
+ if (squadReference && squadExists === false)
210
+ suggestions.push(`parent squad '${squadReference}' not in catalog`);
211
+ return {
212
+ agentId,
213
+ hasPersona,
214
+ commandCount: commandLines.length,
215
+ skillReferences: skillRefs.length,
216
+ orphanSkills,
217
+ squadReference,
218
+ squadExists,
219
+ suggestions,
220
+ };
221
+ }
222
+ // ─────────────────────────────────────────────────────────
223
+ // render — AGENT.md body
224
+ // ─────────────────────────────────────────────────────────
225
+ renderAgentMd(p) {
226
+ const status = p.status || 'draft';
227
+ const version = p.version || '0.1.0';
228
+ const source = p.source || 'aiobuilder';
229
+ const createdAt = new Date().toISOString();
230
+ const frontmatterLines = [
231
+ '---',
232
+ `id: ${p.id}`,
233
+ `name: ${p.name}`,
234
+ `role: ${p.role}`,
235
+ ];
236
+ if (p.domain)
237
+ frontmatterLines.push(`domain: ${p.domain}`);
238
+ if (p.parentSquad)
239
+ frontmatterLines.push(`parentSquad: ${p.parentSquad}`);
240
+ if (p.expertise?.length) {
241
+ frontmatterLines.push('expertise:');
242
+ for (const tag of p.expertise)
243
+ frontmatterLines.push(` - ${tag}`);
244
+ }
245
+ if (p.primarySkills?.length) {
246
+ frontmatterLines.push('primarySkills:');
247
+ for (const skillId of p.primarySkills)
248
+ frontmatterLines.push(` - ${skillId}`);
249
+ }
250
+ frontmatterLines.push(`status: ${status}`);
251
+ frontmatterLines.push(`version: ${version}`);
252
+ frontmatterLines.push(`source: ${source}`);
253
+ frontmatterLines.push(`createdAt: ${createdAt}`);
254
+ frontmatterLines.push('---');
255
+ const commands = p.commands?.length
256
+ ? p.commands
257
+ : [{ name: 'help', description: 'Show available commands and capabilities' }];
258
+ const dependencies = p.dependencies?.length
259
+ ? p.dependencies
260
+ : ['(none declared yet — list skills, tools, or services this agent relies on)'];
261
+ const guardrails = p.guardrails?.length
262
+ ? p.guardrails
263
+ : [
264
+ 'Respect toolset boundaries from parent squad policies',
265
+ 'Emit audit events for state-changing actions',
266
+ 'Defer to human approval for actions outside autonomy budget',
267
+ ];
268
+ const sections = [];
269
+ sections.push(`# ${p.name}`);
270
+ sections.push('');
271
+ sections.push(`> Role: ${p.role}${p.domain ? ` · Domain: ${p.domain}` : ''}`);
272
+ sections.push('');
273
+ sections.push('## Goal');
274
+ sections.push('');
275
+ sections.push(p.whenToUse || `Define and execute work in the ${p.role} role.`);
276
+ sections.push('');
277
+ sections.push('## Commands');
278
+ sections.push('');
279
+ sections.push('```yaml');
280
+ sections.push('commands:');
281
+ for (const c of commands) {
282
+ sections.push(` - name: ${c.name}`);
283
+ sections.push(` description: ${JSON.stringify(c.description)}`);
284
+ }
285
+ sections.push('```');
286
+ sections.push('');
287
+ sections.push('## Dependencies');
288
+ sections.push('');
289
+ for (const d of dependencies)
290
+ sections.push(`- ${d}`);
291
+ sections.push('');
292
+ sections.push('## Persona');
293
+ sections.push('');
294
+ if (p.identity || p.style || p.focus || p.persona) {
295
+ if (p.identity)
296
+ sections.push(`- **Identity:** ${p.identity}`);
297
+ if (p.style)
298
+ sections.push(`- **Style:** ${p.style}`);
299
+ if (p.focus)
300
+ sections.push(`- **Focus:** ${p.focus}`);
301
+ if (p.persona) {
302
+ sections.push('');
303
+ sections.push(p.persona);
304
+ }
305
+ }
306
+ else {
307
+ sections.push(`Embodies the ${p.role} discipline with rigor and clarity. Speaks in concrete steps,`);
308
+ sections.push('not abstractions. Defers to operator judgment on irreversible decisions.');
309
+ }
310
+ sections.push('');
311
+ sections.push('## Guardrails');
312
+ sections.push('');
313
+ for (const g of guardrails)
314
+ sections.push(`- ${g}`);
315
+ sections.push('');
316
+ sections.push('---');
317
+ sections.push('*Generated by OpenLife AgentCreator.*');
318
+ return frontmatterLines.join('\n') + '\n\n' + sections.join('\n') + '\n';
319
+ }
320
+ // ─────────────────────────────────────────────────────────
321
+ // parsing helpers
322
+ // ─────────────────────────────────────────────────────────
323
+ parseFrontmatter(content) {
324
+ const match = content.match(/^---\n([\s\S]*?)\n---/);
325
+ if (!match)
326
+ return {};
327
+ const out = {};
328
+ for (const line of match[1].split('\n')) {
329
+ const m = line.match(/^([a-zA-Z][a-zA-Z0-9_]*):\s*(.+)$/);
330
+ if (m)
331
+ out[m[1]] = m[2].trim();
332
+ }
333
+ return out;
334
+ }
335
+ parseListField(content, field) {
336
+ const fmMatch = content.match(/^---\n([\s\S]*?)\n---/);
337
+ if (!fmMatch)
338
+ return [];
339
+ const lines = fmMatch[1].split('\n');
340
+ const out = [];
341
+ let capturing = false;
342
+ const fieldRe = new RegExp(`^${field}:\\s*$`);
343
+ for (const line of lines) {
344
+ if (fieldRe.test(line)) {
345
+ capturing = true;
346
+ continue;
347
+ }
348
+ if (capturing) {
349
+ const m = line.match(/^\s+-\s+(.+)$/);
350
+ if (m) {
351
+ out.push(m[1].trim());
352
+ continue;
353
+ }
354
+ // any non-list line ends the capture
355
+ if (/^[a-zA-Z]/.test(line))
356
+ capturing = false;
357
+ }
358
+ }
359
+ return out;
360
+ }
361
+ }
362
+ exports.AgentCreator = AgentCreator;