@soleri/cli 9.7.1 → 9.8.0

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 (61) hide show
  1. package/dist/commands/add-domain.js +1 -0
  2. package/dist/commands/add-domain.js.map +1 -1
  3. package/dist/commands/add-pack.js +7 -147
  4. package/dist/commands/add-pack.js.map +1 -1
  5. package/dist/commands/agent.js +130 -0
  6. package/dist/commands/agent.js.map +1 -1
  7. package/dist/commands/create.js +78 -2
  8. package/dist/commands/create.js.map +1 -1
  9. package/dist/commands/doctor.js +2 -0
  10. package/dist/commands/doctor.js.map +1 -1
  11. package/dist/commands/extend.js +17 -0
  12. package/dist/commands/extend.js.map +1 -1
  13. package/dist/commands/install-knowledge.js +1 -0
  14. package/dist/commands/install-knowledge.js.map +1 -1
  15. package/dist/commands/test.js +140 -1
  16. package/dist/commands/test.js.map +1 -1
  17. package/dist/hook-packs/flock-guard/manifest.json +2 -1
  18. package/dist/hook-packs/installer.js +12 -5
  19. package/dist/hook-packs/installer.js.map +1 -1
  20. package/dist/hook-packs/installer.ts +26 -7
  21. package/dist/hook-packs/marketing-research/manifest.json +2 -1
  22. package/dist/hook-packs/registry.d.ts +2 -0
  23. package/dist/hook-packs/registry.js.map +1 -1
  24. package/dist/hook-packs/registry.ts +2 -0
  25. package/dist/prompts/create-wizard.d.ts +16 -2
  26. package/dist/prompts/create-wizard.js +84 -11
  27. package/dist/prompts/create-wizard.js.map +1 -1
  28. package/dist/utils/checks.d.ts +8 -5
  29. package/dist/utils/checks.js +105 -10
  30. package/dist/utils/checks.js.map +1 -1
  31. package/dist/utils/format-paths.d.ts +14 -0
  32. package/dist/utils/format-paths.js +27 -0
  33. package/dist/utils/format-paths.js.map +1 -0
  34. package/dist/utils/git.d.ts +29 -0
  35. package/dist/utils/git.js +88 -0
  36. package/dist/utils/git.js.map +1 -0
  37. package/dist/utils/logger.d.ts +1 -0
  38. package/dist/utils/logger.js +4 -0
  39. package/dist/utils/logger.js.map +1 -1
  40. package/package.json +1 -1
  41. package/src/__tests__/create-wizard-git.test.ts +208 -0
  42. package/src/__tests__/git-utils.test.ts +268 -0
  43. package/src/__tests__/hook-packs.test.ts +5 -1
  44. package/src/__tests__/scaffold-git-e2e.test.ts +105 -0
  45. package/src/commands/add-domain.ts +1 -0
  46. package/src/commands/add-pack.ts +10 -163
  47. package/src/commands/agent.ts +161 -0
  48. package/src/commands/create.ts +89 -3
  49. package/src/commands/doctor.ts +1 -0
  50. package/src/commands/extend.ts +20 -1
  51. package/src/commands/install-knowledge.ts +1 -0
  52. package/src/commands/test.ts +141 -2
  53. package/src/hook-packs/flock-guard/manifest.json +2 -1
  54. package/src/hook-packs/installer.ts +26 -7
  55. package/src/hook-packs/marketing-research/manifest.json +2 -1
  56. package/src/hook-packs/registry.ts +2 -0
  57. package/src/prompts/create-wizard.ts +109 -14
  58. package/src/utils/checks.ts +122 -13
  59. package/src/utils/format-paths.ts +41 -0
  60. package/src/utils/git.ts +118 -0
  61. package/src/utils/logger.ts +5 -0
@@ -1,8 +1,139 @@
1
1
  import { spawn } from 'node:child_process';
2
+ import { existsSync, readFileSync, readdirSync } from 'node:fs';
3
+ import { join } from 'node:path';
2
4
  import type { Command } from 'commander';
3
5
  import * as p from '@clack/prompts';
6
+ import { parse as parseYaml } from 'yaml';
7
+ import { AgentYamlSchema } from '@soleri/forge/lib';
4
8
  import { detectAgent } from '../utils/agent-context.js';
5
9
 
10
+ /**
11
+ * Run validation checks for a file-tree agent (no vitest needed).
12
+ * Returns the process exit code (0 = all passed, 1 = failures).
13
+ */
14
+ function runFiletreeChecks(agentPath: string, _agentId: string): number {
15
+ let passed = 0;
16
+ let failed = 0;
17
+ const failures: string[] = [];
18
+
19
+ // ── 1. agent.yaml validation ───────────────────────
20
+ const yamlPath = join(agentPath, 'agent.yaml');
21
+ try {
22
+ const raw = readFileSync(yamlPath, 'utf-8');
23
+ const parsed = parseYaml(raw);
24
+ const result = AgentYamlSchema.safeParse(parsed);
25
+ if (result.success) {
26
+ p.log.success('agent.yaml — valid');
27
+ passed++;
28
+ } else {
29
+ const issues = result.error.issues
30
+ .map((i) => ` ${i.path.join('.')}: ${i.message}`)
31
+ .join('\n');
32
+ p.log.error(`agent.yaml — validation failed\n${issues}`);
33
+ failures.push('agent.yaml validation');
34
+ failed++;
35
+ }
36
+ } catch (err: unknown) {
37
+ const msg = err instanceof Error ? err.message : String(err);
38
+ p.log.error(`agent.yaml — could not read or parse: ${msg}`);
39
+ failures.push('agent.yaml read/parse');
40
+ failed++;
41
+ }
42
+
43
+ // ── 2. Skills syntax check ─────────────────────────
44
+ const skillsDir = join(agentPath, 'skills');
45
+ if (existsSync(skillsDir)) {
46
+ let validSkills = 0;
47
+ let invalidSkills = 0;
48
+ const invalidNames: string[] = [];
49
+
50
+ try {
51
+ const entries = readdirSync(skillsDir, { withFileTypes: true });
52
+ const skillDirs = entries.filter((e) => e.isDirectory());
53
+
54
+ for (const dir of skillDirs) {
55
+ const skillMd = join(skillsDir, dir.name, 'SKILL.md');
56
+ if (!existsSync(skillMd)) {
57
+ invalidSkills++;
58
+ invalidNames.push(`${dir.name}: missing SKILL.md`);
59
+ continue;
60
+ }
61
+
62
+ try {
63
+ const content = readFileSync(skillMd, 'utf-8');
64
+ const hasFrontmatter = content.startsWith('---');
65
+ const hasName = /^name:/m.test(content);
66
+ const hasDescription = /^description:/m.test(content);
67
+
68
+ if (hasFrontmatter && hasName && hasDescription) {
69
+ validSkills++;
70
+ } else {
71
+ invalidSkills++;
72
+ const missing: string[] = [];
73
+ if (!hasFrontmatter) missing.push('frontmatter (---)');
74
+ if (!hasName) missing.push('name:');
75
+ if (!hasDescription) missing.push('description:');
76
+ invalidNames.push(`${dir.name}: missing ${missing.join(', ')}`);
77
+ }
78
+ } catch {
79
+ invalidSkills++;
80
+ invalidNames.push(`${dir.name}: could not read SKILL.md`);
81
+ }
82
+ }
83
+
84
+ if (invalidSkills === 0) {
85
+ p.log.success(`skills — ${validSkills} valid, 0 invalid`);
86
+ passed++;
87
+ } else {
88
+ const details = invalidNames.map((n) => ` ${n}`).join('\n');
89
+ p.log.error(`skills — ${validSkills} valid, ${invalidSkills} invalid\n${details}`);
90
+ failures.push('skills syntax');
91
+ failed++;
92
+ }
93
+ } catch {
94
+ p.log.warn('skills — could not read skills/ directory');
95
+ // Not a failure — directory exists but unreadable is unusual, warn only
96
+ }
97
+ } else {
98
+ p.log.info('skills — no skills/ directory (skipped)');
99
+ }
100
+
101
+ // ── 3. Instructions check ──────────────────────────
102
+ const instructionsDir = join(agentPath, 'instructions');
103
+ if (existsSync(instructionsDir)) {
104
+ try {
105
+ const files = readdirSync(instructionsDir).filter((f) => f.endsWith('.md'));
106
+ if (files.length > 0) {
107
+ p.log.success(`instructions — ${files.length} .md file(s) found`);
108
+ passed++;
109
+ } else {
110
+ p.log.error('instructions — directory exists but contains no .md files');
111
+ failures.push('instructions empty');
112
+ failed++;
113
+ }
114
+ } catch {
115
+ p.log.error('instructions — could not read directory');
116
+ failures.push('instructions read');
117
+ failed++;
118
+ }
119
+ } else {
120
+ p.log.error('instructions — directory not found');
121
+ failures.push('instructions missing');
122
+ failed++;
123
+ }
124
+
125
+ // ── Summary ────────────────────────────────────────
126
+ if (failed === 0) {
127
+ p.log.success(`\n${passed} check(s) passed, 0 failed`);
128
+ } else {
129
+ p.log.error(
130
+ `\n${passed} check(s) passed, ${failed} failed:\n${failures.map((f) => ` - ${f}`).join('\n')}`,
131
+ );
132
+ }
133
+
134
+ return failed > 0 ? 1 : 0;
135
+ }
136
+
6
137
  export function registerTest(program: Command): void {
7
138
  program
8
139
  .command('test')
@@ -17,6 +148,16 @@ export function registerTest(program: Command): void {
17
148
  process.exit(1);
18
149
  }
19
150
 
151
+ p.log.info(`Running tests for ${ctx.agentId}...`);
152
+
153
+ // ── File-tree agents: run validation checks (no vitest) ──
154
+ if (ctx.format === 'filetree') {
155
+ const code = runFiletreeChecks(ctx.agentPath, ctx.agentId);
156
+ process.exit(code);
157
+ return;
158
+ }
159
+
160
+ // ── TypeScript agents: spawn vitest as before ──
20
161
  const args: string[] = [];
21
162
  if (opts.watch) {
22
163
  // vitest (no "run") enables watch mode
@@ -30,8 +171,6 @@ export function registerTest(program: Command): void {
30
171
  const extra = cmd.args as string[];
31
172
  if (extra.length > 0) args.push(...extra);
32
173
 
33
- p.log.info(`Running tests for ${ctx.agentId}...`);
34
-
35
174
  const child = spawn('npx', args, {
36
175
  cwd: ctx.agentPath,
37
176
  stdio: 'inherit',
@@ -32,5 +32,6 @@
32
32
  "timeout": 10,
33
33
  "statusMessage": "Releasing lockfile guard..."
34
34
  }
35
- ]
35
+ ],
36
+ "scaffoldDefault": false
36
37
  }
@@ -87,10 +87,16 @@ function resolveLifecycleHooks(
87
87
  return hooks;
88
88
  }
89
89
 
90
- interface SettingsHookEntry {
90
+ interface SettingsHookDef {
91
91
  type: 'command';
92
92
  command: string;
93
93
  timeout?: number;
94
+ statusMessage?: string;
95
+ }
96
+
97
+ interface SettingsHookEntry {
98
+ matcher?: string;
99
+ hooks: SettingsHookDef[];
94
100
  [key: string]: unknown;
95
101
  }
96
102
 
@@ -121,17 +127,24 @@ function addLifecycleHooks(
121
127
  const eventKey = hook.event;
122
128
  const eventHooks = (hooks[eventKey] ?? []) as SettingsHookEntry[];
123
129
  const alreadyExists = eventHooks.some(
124
- (h) => h.command === hook.command && h[PACK_MARKER] === sourcePack,
130
+ (h) => h[PACK_MARKER] === sourcePack && h.hooks?.some((hd) => hd.command === hook.command),
125
131
  );
126
132
  if (!alreadyExists) {
127
- const entry: SettingsHookEntry = {
133
+ const hookDef: SettingsHookDef = {
128
134
  type: hook.type,
129
135
  command: hook.command,
130
- [PACK_MARKER]: sourcePack,
131
136
  };
132
137
  if (hook.timeout) {
133
- entry.timeout = hook.timeout;
138
+ hookDef.timeout = hook.timeout;
139
+ }
140
+ if (hook.statusMessage) {
141
+ hookDef.statusMessage = hook.statusMessage;
134
142
  }
143
+ const entry: SettingsHookEntry = {
144
+ matcher: hook.matcher || '',
145
+ hooks: [hookDef],
146
+ [PACK_MARKER]: sourcePack,
147
+ };
135
148
  eventHooks.push(entry);
136
149
  hooks[eventKey] = eventHooks;
137
150
  added.push(`${eventKey}:${hook.matcher}`);
@@ -144,7 +157,10 @@ function addLifecycleHooks(
144
157
 
145
158
  function removeLifecycleHooks(claudeDir: string, packName: string): string[] {
146
159
  const settings = readClaudeSettings(claudeDir);
147
- const hooks = (settings['hooks'] ?? {}) as Record<string, SettingsHookEntry[]>;
160
+ const hooks = (settings['hooks'] ?? {}) as Record<
161
+ string,
162
+ (SettingsHookEntry | Record<string, unknown>)[]
163
+ >;
148
164
  const removed: string[] = [];
149
165
  for (const [eventKey, eventHooks] of Object.entries(hooks)) {
150
166
  if (!Array.isArray(eventHooks)) continue;
@@ -301,7 +317,10 @@ export function isPackInstalled(
301
317
  const eventHooks = hooksObj[hook.event];
302
318
  if (
303
319
  Array.isArray(eventHooks) &&
304
- eventHooks.some((h) => h.command === hook.command && h[PACK_MARKER] === sourcePack)
320
+ eventHooks.some(
321
+ (h) =>
322
+ h[PACK_MARKER] === sourcePack && h.hooks?.some((hd) => hd.command === hook.command),
323
+ )
305
324
  ) {
306
325
  present++;
307
326
  }
@@ -20,5 +20,6 @@
20
20
  "statusMessage": "Checking marketing context..."
21
21
  }
22
22
  ],
23
- "actionLevel": "remind"
23
+ "actionLevel": "remind",
24
+ "scaffoldDefault": false
24
25
  }
@@ -31,6 +31,8 @@ export interface HookPackManifest {
31
31
  lifecycleHooks?: HookPackLifecycleHook[];
32
32
  source?: 'built-in' | 'local';
33
33
  actionLevel?: 'remind' | 'warn' | 'block';
34
+ /** If false, pack is hidden from the scaffold picker but still installable via `hooks add-pack`. */
35
+ scaffoldDefault?: boolean;
34
36
  }
35
37
 
36
38
  const __filename = fileURLToPath(import.meta.url);
@@ -8,6 +8,23 @@
8
8
  import * as p from '@clack/prompts';
9
9
  import type { AgentConfigInput } from '@soleri/forge/lib';
10
10
  import { ITALIAN_CRAFTSPERSON } from '@soleri/core/personas';
11
+ import { isGhInstalled } from '../utils/git.js';
12
+
13
+ /** Git configuration collected from the wizard. */
14
+ export interface WizardGitConfig {
15
+ init: boolean;
16
+ remote?: {
17
+ type: 'gh' | 'manual';
18
+ url?: string;
19
+ visibility?: 'public' | 'private';
20
+ };
21
+ }
22
+
23
+ /** Full result from the create wizard. */
24
+ export interface CreateWizardResult {
25
+ config: AgentConfigInput;
26
+ git: WizardGitConfig;
27
+ }
11
28
 
12
29
  /** Slugify a display name into a kebab-case ID. */
13
30
  function slugify(name: string): string {
@@ -19,9 +36,9 @@ function slugify(name: string): string {
19
36
 
20
37
  /**
21
38
  * Run the simplified create wizard.
22
- * Returns an AgentConfigInput or null if cancelled.
39
+ * Returns a CreateWizardResult or null if cancelled.
23
40
  */
24
- export async function runCreateWizard(initialName?: string): Promise<AgentConfigInput | null> {
41
+ export async function runCreateWizard(initialName?: string): Promise<CreateWizardResult | null> {
25
42
  p.intro('Create a new Soleri agent');
26
43
 
27
44
  // ─── Step 1: Name ───────────────────────────────────────────
@@ -119,17 +136,95 @@ export async function runCreateWizard(initialName?: string): Promise<AgentConfig
119
136
 
120
137
  if (p.isCancel(confirm) || !confirm) return null;
121
138
 
139
+ // ─── Step 3: Git setup ──────────────────────────────────────
140
+ const gitInit = await p.confirm({
141
+ message: 'Initialize as a git repository?',
142
+ initialValue: true,
143
+ });
144
+
145
+ if (p.isCancel(gitInit)) return null;
146
+
147
+ const git: WizardGitConfig = { init: gitInit as boolean };
148
+
149
+ if (git.init) {
150
+ const pushRemote = await p.confirm({
151
+ message: 'Push to a remote repository?',
152
+ initialValue: false,
153
+ });
154
+
155
+ if (p.isCancel(pushRemote)) return null;
156
+
157
+ if (pushRemote) {
158
+ const ghAvailable = await isGhInstalled();
159
+
160
+ let remoteType: 'gh' | 'manual';
161
+
162
+ if (ghAvailable) {
163
+ const remoteChoice = await p.select({
164
+ message: 'How would you like to set up the remote?',
165
+ options: [
166
+ { value: 'gh' as const, label: 'Create a new GitHub repository' },
167
+ { value: 'manual' as const, label: 'Add an existing remote URL' },
168
+ ],
169
+ });
170
+
171
+ if (p.isCancel(remoteChoice)) return null;
172
+ remoteType = remoteChoice as 'gh' | 'manual';
173
+ } else {
174
+ remoteType = 'manual';
175
+ }
176
+
177
+ if (remoteType === 'gh') {
178
+ const visibility = await p.select({
179
+ message: 'Repository visibility?',
180
+ options: [
181
+ { value: 'private' as const, label: 'Private' },
182
+ { value: 'public' as const, label: 'Public' },
183
+ ],
184
+ initialValue: 'private' as const,
185
+ });
186
+
187
+ if (p.isCancel(visibility)) return null;
188
+
189
+ git.remote = {
190
+ type: 'gh',
191
+ visibility: visibility as 'public' | 'private',
192
+ };
193
+ } else {
194
+ const remoteUrl = await p.text({
195
+ message: 'Remote repository URL:',
196
+ placeholder: 'https://github.com/user/repo.git',
197
+ validate: (v) => {
198
+ if (!v || v.trim().length === 0) return 'URL is required';
199
+ if (!v.startsWith('https://') && !v.startsWith('git@'))
200
+ return 'URL must start with https:// or git@';
201
+ },
202
+ });
203
+
204
+ if (p.isCancel(remoteUrl)) return null;
205
+
206
+ git.remote = {
207
+ type: 'manual',
208
+ url: (remoteUrl as string).trim(),
209
+ };
210
+ }
211
+ }
212
+ }
213
+
122
214
  return {
123
- id,
124
- name: name.trim(),
125
- role: 'Your universal second brain — learns, remembers, improves',
126
- description:
127
- 'A universal assistant that learns from your projects, captures knowledge, and gets smarter with every session.',
128
- domains: [],
129
- principles: [],
130
- skills: [],
131
- tone: 'mentor',
132
- greeting,
133
- persona,
134
- } as AgentConfigInput;
215
+ config: {
216
+ id,
217
+ name: name.trim(),
218
+ role: 'Your universal second brain — learns, remembers, improves',
219
+ description:
220
+ 'A universal assistant that learns from your projects, captures knowledge, and gets smarter with every session.',
221
+ domains: [],
222
+ principles: [],
223
+ skills: [],
224
+ tone: 'mentor',
225
+ greeting,
226
+ persona,
227
+ } as AgentConfigInput,
228
+ git,
229
+ };
135
230
  }
@@ -1,15 +1,15 @@
1
1
  /**
2
2
  * Health check utilities for the doctor command.
3
3
  */
4
- import { existsSync, readFileSync } from 'node:fs';
4
+ import { existsSync, readFileSync, readdirSync } from 'node:fs';
5
5
  import { join } from 'node:path';
6
6
  import { execFileSync } from 'node:child_process';
7
7
  import { homedir } from 'node:os';
8
- import { detectAgent } from './agent-context.js';
8
+ import { detectAgent, type AgentFormat } from './agent-context.js';
9
9
  import { getInstalledPacks } from '../hook-packs/registry.js';
10
10
 
11
- interface CheckResult {
12
- status: 'pass' | 'fail' | 'warn';
11
+ export interface CheckResult {
12
+ status: 'pass' | 'fail' | 'warn' | 'skip';
13
13
  label: string;
14
14
  detail?: string;
15
15
  }
@@ -53,10 +53,19 @@ export function checkAgentProject(dir?: string): CheckResult {
53
53
  if (!ctx) {
54
54
  return { status: 'warn', label: 'Agent project', detail: 'not detected in current directory' };
55
55
  }
56
- return { status: 'pass', label: 'Agent project', detail: `${ctx.agentId} (${ctx.packageName})` };
56
+ const formatLabel = ctx.format === 'filetree' ? 'file-tree' : 'typescript';
57
+ return {
58
+ status: 'pass',
59
+ label: 'Agent project',
60
+ detail: `${ctx.agentId} (${ctx.packageName}, ${formatLabel})`,
61
+ };
57
62
  }
58
63
 
59
- export function checkAgentBuild(dir?: string): CheckResult {
64
+ export function checkAgentBuild(dir?: string, format?: AgentFormat): CheckResult {
65
+ if (format === 'filetree') {
66
+ return { status: 'skip', label: 'Agent build', detail: 'not applicable for file-tree agents' };
67
+ }
68
+
60
69
  const ctx = detectAgent(dir);
61
70
  if (!ctx) return { status: 'warn', label: 'Agent build', detail: 'no agent detected' };
62
71
 
@@ -73,7 +82,15 @@ export function checkAgentBuild(dir?: string): CheckResult {
73
82
  return { status: 'pass', label: 'Agent build', detail: 'dist/index.js exists' };
74
83
  }
75
84
 
76
- export function checkNodeModules(dir?: string): CheckResult {
85
+ export function checkNodeModules(dir?: string, format?: AgentFormat): CheckResult {
86
+ if (format === 'filetree') {
87
+ return {
88
+ status: 'skip',
89
+ label: 'Dependencies',
90
+ detail: 'not applicable for file-tree agents',
91
+ };
92
+ }
93
+
77
94
  const ctx = detectAgent(dir);
78
95
  if (!ctx) return { status: 'warn', label: 'Dependencies', detail: 'no agent detected' };
79
96
 
@@ -87,6 +104,80 @@ export function checkNodeModules(dir?: string): CheckResult {
87
104
  return { status: 'pass', label: 'Dependencies', detail: 'node_modules/ exists' };
88
105
  }
89
106
 
107
+ export function checkAgentYaml(agentPath: string): CheckResult {
108
+ const yamlPath = join(agentPath, 'agent.yaml');
109
+ if (!existsSync(yamlPath)) {
110
+ return { status: 'fail', label: 'agent.yaml', detail: 'not found' };
111
+ }
112
+
113
+ try {
114
+ const content = readFileSync(yamlPath, 'utf-8');
115
+ // Light validation: check for required fields without pulling in a YAML parser
116
+ // (detectAgent already parsed it, but we verify the raw content for diagnostics)
117
+ const hasId = /^id\s*:/m.test(content);
118
+ const hasName = /^name\s*:/m.test(content);
119
+
120
+ if (!hasId && !hasName) {
121
+ return {
122
+ status: 'fail',
123
+ label: 'agent.yaml',
124
+ detail: 'missing required fields: id, name',
125
+ };
126
+ }
127
+ if (!hasId) {
128
+ return { status: 'fail', label: 'agent.yaml', detail: 'missing required field: id' };
129
+ }
130
+ if (!hasName) {
131
+ return { status: 'fail', label: 'agent.yaml', detail: 'missing required field: name' };
132
+ }
133
+ return { status: 'pass', label: 'agent.yaml', detail: 'valid (id, name present)' };
134
+ } catch {
135
+ return { status: 'fail', label: 'agent.yaml', detail: 'failed to read file' };
136
+ }
137
+ }
138
+
139
+ export function checkInstructionsDir(agentPath: string): CheckResult {
140
+ const instrDir = join(agentPath, 'instructions');
141
+ if (!existsSync(instrDir)) {
142
+ return {
143
+ status: 'fail',
144
+ label: 'Instructions',
145
+ detail: 'instructions/ directory not found',
146
+ };
147
+ }
148
+
149
+ try {
150
+ const files = readdirSync(instrDir).filter((f) => f.endsWith('.md'));
151
+ if (files.length === 0) {
152
+ return {
153
+ status: 'warn',
154
+ label: 'Instructions',
155
+ detail: 'instructions/ exists but contains no .md files',
156
+ };
157
+ }
158
+ return {
159
+ status: 'pass',
160
+ label: 'Instructions',
161
+ detail: `${files.length} instruction file${files.length === 1 ? '' : 's'}`,
162
+ };
163
+ } catch {
164
+ return { status: 'fail', label: 'Instructions', detail: 'failed to read instructions/' };
165
+ }
166
+ }
167
+
168
+ export function checkEngineReachable(): CheckResult {
169
+ try {
170
+ require.resolve('@soleri/core/package.json');
171
+ return { status: 'pass', label: 'Engine', detail: '@soleri/core reachable' };
172
+ } catch {
173
+ return {
174
+ status: 'fail',
175
+ label: 'Engine',
176
+ detail: '@soleri/core not found — engine is required for file-tree agents',
177
+ };
178
+ }
179
+ }
180
+
90
181
  function checkMcpRegistration(dir?: string): CheckResult {
91
182
  const ctx = detectAgent(dir);
92
183
  if (!ctx) return { status: 'warn', label: 'MCP registration', detail: 'no agent detected' };
@@ -157,15 +248,33 @@ function checkHookPacks(): CheckResult {
157
248
  }
158
249
 
159
250
  export function runAllChecks(dir?: string): CheckResult[] {
160
- return [
251
+ const ctx = detectAgent(dir);
252
+ const format = ctx?.format;
253
+
254
+ // Common checks for all agent formats
255
+ const results: CheckResult[] = [
161
256
  checkNodeVersion(),
162
257
  checkNpm(),
163
258
  checkTsx(),
164
259
  checkAgentProject(dir),
165
- checkNodeModules(dir),
166
- checkAgentBuild(dir),
167
- checkMcpRegistration(dir),
168
- checkHookPacks(),
169
- checkCognee(),
170
260
  ];
261
+
262
+ if (format === 'filetree') {
263
+ // File-tree agent checks
264
+ results.push(
265
+ checkAgentYaml(ctx!.agentPath),
266
+ checkInstructionsDir(ctx!.agentPath),
267
+ checkEngineReachable(),
268
+ checkNodeModules(dir, format),
269
+ checkAgentBuild(dir, format),
270
+ );
271
+ } else {
272
+ // TypeScript agent checks (or no agent detected)
273
+ results.push(checkNodeModules(dir, format), checkAgentBuild(dir, format));
274
+ }
275
+
276
+ // Shared checks
277
+ results.push(checkMcpRegistration(dir), checkHookPacks(), checkCognee());
278
+
279
+ return results;
171
280
  }
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Format-aware path resolution for filetree vs typescript agents.
3
+ */
4
+ import { join } from 'node:path';
5
+
6
+ export interface FormatPaths {
7
+ knowledgeDir: string;
8
+ extensionsDir: string;
9
+ facadesDir: string;
10
+ agentConfigFile: string;
11
+ entryPoint: string;
12
+ }
13
+
14
+ export function getFormatPaths(ctx: {
15
+ format: 'filetree' | 'typescript';
16
+ agentPath: string;
17
+ }): FormatPaths {
18
+ const { format, agentPath } = ctx;
19
+
20
+ if (format === 'filetree') {
21
+ return {
22
+ knowledgeDir: join(agentPath, 'knowledge'),
23
+ extensionsDir: join(agentPath, 'extensions'),
24
+ facadesDir: '',
25
+ agentConfigFile: join(agentPath, 'agent.yaml'),
26
+ entryPoint: join(agentPath, 'agent.yaml'),
27
+ };
28
+ }
29
+
30
+ return {
31
+ knowledgeDir: join(agentPath, 'src', 'intelligence', 'data'),
32
+ extensionsDir: join(agentPath, 'src', 'extensions'),
33
+ facadesDir: join(agentPath, 'src', 'facades'),
34
+ agentConfigFile: join(agentPath, 'package.json'),
35
+ entryPoint: join(agentPath, 'src', 'index.ts'),
36
+ };
37
+ }
38
+
39
+ export function isFileTree(ctx: { format: string }): boolean {
40
+ return ctx.format === 'filetree';
41
+ }