@soleri/forge 5.2.0 → 5.5.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 (62) hide show
  1. package/dist/scaffolder.js +166 -3
  2. package/dist/scaffolder.js.map +1 -1
  3. package/dist/skills/brain-debrief.md +186 -0
  4. package/dist/skills/brainstorming.md +170 -0
  5. package/dist/skills/code-patrol.md +176 -0
  6. package/dist/skills/context-resume.md +143 -0
  7. package/dist/skills/executing-plans.md +201 -0
  8. package/dist/skills/fix-and-learn.md +164 -0
  9. package/dist/skills/health-check.md +225 -0
  10. package/dist/skills/knowledge-harvest.md +178 -0
  11. package/dist/skills/onboard-me.md +197 -0
  12. package/dist/skills/retrospective.md +189 -0
  13. package/dist/skills/second-opinion.md +142 -0
  14. package/dist/skills/systematic-debugging.md +230 -0
  15. package/dist/skills/test-driven-development.md +266 -0
  16. package/dist/skills/vault-capture.md +154 -0
  17. package/dist/skills/vault-navigator.md +129 -0
  18. package/dist/skills/verification-before-completion.md +170 -0
  19. package/dist/skills/writing-plans.md +207 -0
  20. package/dist/templates/claude-md-template.js +90 -1
  21. package/dist/templates/claude-md-template.js.map +1 -1
  22. package/dist/templates/domain-facade.d.ts +4 -0
  23. package/dist/templates/domain-facade.js +4 -0
  24. package/dist/templates/domain-facade.js.map +1 -1
  25. package/dist/templates/entry-point.js +32 -0
  26. package/dist/templates/entry-point.js.map +1 -1
  27. package/dist/templates/readme.js +38 -0
  28. package/dist/templates/readme.js.map +1 -1
  29. package/dist/templates/setup-script.js +52 -1
  30. package/dist/templates/setup-script.js.map +1 -1
  31. package/dist/templates/skills.d.ts +16 -0
  32. package/dist/templates/skills.js +73 -0
  33. package/dist/templates/skills.js.map +1 -0
  34. package/dist/templates/test-facades.js +173 -3
  35. package/dist/templates/test-facades.js.map +1 -1
  36. package/package.json +2 -2
  37. package/src/__tests__/scaffolder.test.ts +115 -2
  38. package/src/scaffolder.ts +171 -3
  39. package/src/skills/brain-debrief.md +186 -0
  40. package/src/skills/brainstorming.md +170 -0
  41. package/src/skills/code-patrol.md +176 -0
  42. package/src/skills/context-resume.md +143 -0
  43. package/src/skills/executing-plans.md +201 -0
  44. package/src/skills/fix-and-learn.md +164 -0
  45. package/src/skills/health-check.md +225 -0
  46. package/src/skills/knowledge-harvest.md +178 -0
  47. package/src/skills/onboard-me.md +197 -0
  48. package/src/skills/retrospective.md +189 -0
  49. package/src/skills/second-opinion.md +142 -0
  50. package/src/skills/systematic-debugging.md +230 -0
  51. package/src/skills/test-driven-development.md +266 -0
  52. package/src/skills/vault-capture.md +154 -0
  53. package/src/skills/vault-navigator.md +129 -0
  54. package/src/skills/verification-before-completion.md +170 -0
  55. package/src/skills/writing-plans.md +207 -0
  56. package/src/templates/claude-md-template.ts +181 -0
  57. package/src/templates/domain-facade.ts +4 -0
  58. package/src/templates/entry-point.ts +32 -0
  59. package/src/templates/readme.ts +38 -0
  60. package/src/templates/setup-script.ts +54 -1
  61. package/src/templates/skills.ts +82 -0
  62. package/src/templates/test-facades.ts +173 -3
@@ -101,7 +101,60 @@ else
101
101
  fi
102
102
  fi
103
103
 
104
- echo ""
104
+ # Install skills to ~/.claude/commands/
105
+ SKILLS_DIR="$AGENT_DIR/skills"
106
+ COMMANDS_DIR="$HOME/.claude/commands"
107
+
108
+ if [ -d "$SKILLS_DIR" ]; then
109
+ echo ""
110
+ echo "Installing skills..."
111
+ mkdir -p "$COMMANDS_DIR"
112
+ skill_installed=0
113
+ skill_skipped=0
114
+ for skill_dir in "$SKILLS_DIR"/*/; do
115
+ [ -d "$skill_dir" ] || continue
116
+ skill_file="$skill_dir/SKILL.md"
117
+ [ -f "$skill_file" ] || continue
118
+ skill_name="$(basename "$skill_dir")"
119
+ dest="$COMMANDS_DIR/$skill_name.md"
120
+ if [ -f "$dest" ]; then
121
+ skill_skipped=$((skill_skipped + 1))
122
+ else
123
+ cp "$skill_file" "$dest"
124
+ skill_installed=$((skill_installed + 1))
125
+ fi
126
+ done
127
+ echo "[ok] Skills: $skill_installed installed, $skill_skipped already present"
128
+ fi
129
+
130
+ ${
131
+ config.hookPacks?.length
132
+ ? `# Install hook packs to global ~/.claude/
133
+ AGENT_CLAUDE_DIR="$AGENT_DIR/.claude"
134
+ GLOBAL_CLAUDE_DIR="$HOME/.claude"
135
+
136
+ if [ -d "$AGENT_CLAUDE_DIR" ]; then
137
+ echo ""
138
+ echo "Installing hook packs..."
139
+ mkdir -p "$GLOBAL_CLAUDE_DIR"
140
+ installed=0
141
+ skipped=0
142
+ for hook_file in "$AGENT_CLAUDE_DIR"/hookify.*.local.md; do
143
+ [ -f "$hook_file" ] || continue
144
+ dest="$GLOBAL_CLAUDE_DIR/$(basename "$hook_file")"
145
+ if [ -f "$dest" ]; then
146
+ skipped=$((skipped + 1))
147
+ else
148
+ cp "$hook_file" "$dest"
149
+ installed=$((installed + 1))
150
+ fi
151
+ done
152
+ echo "[ok] Hooks: $installed installed, $skipped already present"
153
+ fi
154
+
155
+ `
156
+ : ''
157
+ }echo ""
105
158
  echo "=== Setup Complete ==="
106
159
  echo ""
107
160
  echo "Next:"
@@ -0,0 +1,82 @@
1
+ import { readFileSync, readdirSync } from 'node:fs';
2
+ import { join, dirname } from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ import type { AgentConfig } from '../types.js';
5
+
6
+ const __dirname = dirname(fileURLToPath(import.meta.url));
7
+ const SKILLS_DIR = join(__dirname, '..', 'skills');
8
+
9
+ /** Skills that use YOUR_AGENT_core placeholder and need agent-specific substitution. */
10
+ const AGENT_SPECIFIC_SKILLS = new Set([
11
+ 'brain-debrief',
12
+ 'brainstorming',
13
+ 'code-patrol',
14
+ 'context-resume',
15
+ 'executing-plans',
16
+ 'fix-and-learn',
17
+ 'health-check',
18
+ 'knowledge-harvest',
19
+ 'onboard-me',
20
+ 'retrospective',
21
+ 'second-opinion',
22
+ 'systematic-debugging',
23
+ 'test-driven-development',
24
+ 'vault-capture',
25
+ 'vault-navigator',
26
+ 'verification-before-completion',
27
+ 'writing-plans',
28
+ ]);
29
+
30
+ /**
31
+ * Generate skill files for the scaffolded agent.
32
+ * Returns [relativePath, content] tuples for each skill.
33
+ *
34
+ * - Superpowers-adapted skills (MIT): copied as-is
35
+ * - Engine-adapted skills: YOUR_AGENT_core → {config.id}_core
36
+ */
37
+ export function generateSkills(config: AgentConfig): Array<[string, string]> {
38
+ const files: Array<[string, string]> = [];
39
+ let skillFiles: string[];
40
+
41
+ try {
42
+ skillFiles = readdirSync(SKILLS_DIR).filter((f) => f.endsWith('.md'));
43
+ } catch {
44
+ return files;
45
+ }
46
+
47
+ for (const file of skillFiles) {
48
+ const skillName = file.replace('.md', '');
49
+ let content = readFileSync(join(SKILLS_DIR, file), 'utf-8');
50
+
51
+ if (AGENT_SPECIFIC_SKILLS.has(skillName)) {
52
+ content = content.replace(/YOUR_AGENT_core/g, `${config.id}_core`);
53
+ }
54
+
55
+ files.push([`skills/${skillName}/SKILL.md`, content]);
56
+ }
57
+
58
+ return files;
59
+ }
60
+
61
+ /**
62
+ * List all bundled skill names with their descriptions (from YAML frontmatter).
63
+ */
64
+ export function listSkillDescriptions(): Array<{ name: string; description: string }> {
65
+ let skillFiles: string[];
66
+
67
+ try {
68
+ skillFiles = readdirSync(SKILLS_DIR).filter((f) => f.endsWith('.md'));
69
+ } catch {
70
+ return [];
71
+ }
72
+
73
+ return skillFiles.map((file) => {
74
+ const content = readFileSync(join(SKILLS_DIR, file), 'utf-8');
75
+ const nameMatch = content.match(/^name:\s*(.+)$/m);
76
+ const descMatch = content.match(/^description:\s*"?(.+?)"?\s*$/m);
77
+ return {
78
+ name: nameMatch?.[1]?.trim() ?? file.replace('.md', ''),
79
+ description: descMatch?.[1]?.trim() ?? '',
80
+ };
81
+ });
82
+ }
@@ -126,6 +126,29 @@ ${domainDescribes}
126
126
  if (stats.totalEntries === 0) {
127
127
  recommendations.push('Vault is empty');
128
128
  }
129
+ // Check hook status
130
+ const { readdirSync } = await import('node:fs');
131
+ const agentClaudeDir = joinPath(__dirname, '..', '.claude');
132
+ const globalClaudeDir = joinPath(homedir(), '.claude');
133
+ const hookStatus = { agent: [] as string[], global: [] as string[], missing: [] as string[] };
134
+ if (exists(agentClaudeDir)) {
135
+ try {
136
+ const agentHooks = readdirSync(agentClaudeDir)
137
+ .filter((f: string) => f.startsWith('hookify.') && f.endsWith('.local.md'))
138
+ .map((f: string) => f.replace('hookify.', '').replace('.local.md', ''));
139
+ hookStatus.agent = agentHooks;
140
+ for (const hook of agentHooks) {
141
+ if (exists(joinPath(globalClaudeDir, \`hookify.\${hook}.local.md\`))) {
142
+ hookStatus.global.push(hook);
143
+ } else {
144
+ hookStatus.missing.push(hook);
145
+ }
146
+ }
147
+ } catch { /* ignore */ }
148
+ }
149
+ if (hookStatus.missing.length > 0) {
150
+ recommendations.push(\`\${hookStatus.missing.length} hook(s) not installed globally — run scripts/setup.sh\`);
151
+ }
129
152
  if (recommendations.length === 0) {
130
153
  recommendations.push('${config.name} is fully set up and ready!');
131
154
  }
@@ -136,6 +159,7 @@ ${domainDescribes}
136
159
  global: { exists: exists(globalClaudeMd), has_agent_section: hasAgentMarker(globalClaudeMd) },
137
160
  },
138
161
  vault: { entries: stats.totalEntries, domains: Object.keys(stats.byDomain) },
162
+ hooks: hookStatus,
139
163
  recommendations,
140
164
  };
141
165
  },
@@ -172,6 +196,10 @@ ${domainDescribes}
172
196
  expect(opNames).toContain('brain_archive_sessions');
173
197
  expect(opNames).toContain('brain_promote_proposals');
174
198
  expect(opNames).toContain('brain_lifecycle');
199
+ // Enhanced brain ops (3)
200
+ expect(opNames).toContain('brain_feedback');
201
+ expect(opNames).toContain('brain_feedback_stats');
202
+ expect(opNames).toContain('brain_reset_extracted');
175
203
  // Agent-specific ops (5)
176
204
  expect(opNames).toContain('health');
177
205
  expect(opNames).toContain('identity');
@@ -187,8 +215,122 @@ ${domainDescribes}
187
215
  expect(opNames).toContain('route_intent');
188
216
  expect(opNames).toContain('morph');
189
217
  expect(opNames).toContain('get_behavior_rules');
190
- // Total: 50
191
- expect(facade.ops.length).toBe(50);
218
+ // Cognee ops (5)
219
+ expect(opNames).toContain('cognee_status');
220
+ expect(opNames).toContain('cognee_search');
221
+ expect(opNames).toContain('cognee_add');
222
+ expect(opNames).toContain('cognee_cognify');
223
+ expect(opNames).toContain('cognee_config');
224
+ // LLM ops (2)
225
+ expect(opNames).toContain('llm_rotate');
226
+ expect(opNames).toContain('llm_call');
227
+ // Governance ops (5)
228
+ expect(opNames).toContain('governance_policy');
229
+ expect(opNames).toContain('governance_proposals');
230
+ expect(opNames).toContain('governance_stats');
231
+ expect(opNames).toContain('governance_expire');
232
+ expect(opNames).toContain('governance_dashboard');
233
+ // Planning Extra ops (9)
234
+ expect(opNames).toContain('plan_iterate');
235
+ expect(opNames).toContain('plan_split');
236
+ expect(opNames).toContain('plan_reconcile');
237
+ expect(opNames).toContain('plan_complete_lifecycle');
238
+ expect(opNames).toContain('plan_dispatch');
239
+ expect(opNames).toContain('plan_review');
240
+ expect(opNames).toContain('plan_archive');
241
+ expect(opNames).toContain('plan_list_tasks');
242
+ expect(opNames).toContain('plan_stats');
243
+ // Memory Extra ops (8)
244
+ expect(opNames).toContain('memory_delete');
245
+ expect(opNames).toContain('memory_stats');
246
+ expect(opNames).toContain('memory_export');
247
+ expect(opNames).toContain('memory_import');
248
+ expect(opNames).toContain('memory_prune');
249
+ expect(opNames).toContain('memory_deduplicate');
250
+ expect(opNames).toContain('memory_topics');
251
+ expect(opNames).toContain('memory_by_project');
252
+ // Vault Extra ops (12)
253
+ expect(opNames).toContain('vault_get');
254
+ expect(opNames).toContain('vault_update');
255
+ expect(opNames).toContain('vault_remove');
256
+ expect(opNames).toContain('vault_bulk_add');
257
+ expect(opNames).toContain('vault_bulk_remove');
258
+ expect(opNames).toContain('vault_tags');
259
+ expect(opNames).toContain('vault_domains');
260
+ expect(opNames).toContain('vault_recent');
261
+ expect(opNames).toContain('vault_import');
262
+ expect(opNames).toContain('vault_seed');
263
+ expect(opNames).toContain('vault_backup');
264
+ expect(opNames).toContain('vault_age_report');
265
+ // Admin ops (8)
266
+ expect(opNames).toContain('admin_health');
267
+ expect(opNames).toContain('admin_tool_list');
268
+ expect(opNames).toContain('admin_config');
269
+ expect(opNames).toContain('admin_vault_size');
270
+ expect(opNames).toContain('admin_uptime');
271
+ expect(opNames).toContain('admin_version');
272
+ expect(opNames).toContain('admin_reset_cache');
273
+ expect(opNames).toContain('admin_diagnostic');
274
+ // Loop ops (7)
275
+ expect(opNames).toContain('loop_start');
276
+ expect(opNames).toContain('loop_iterate');
277
+ expect(opNames).toContain('loop_status');
278
+ expect(opNames).toContain('loop_cancel');
279
+ expect(opNames).toContain('loop_history');
280
+ expect(opNames).toContain('loop_is_active');
281
+ expect(opNames).toContain('loop_complete');
282
+ // Orchestrate ops (5)
283
+ expect(opNames).toContain('orchestrate_plan');
284
+ expect(opNames).toContain('orchestrate_execute');
285
+ expect(opNames).toContain('orchestrate_complete');
286
+ expect(opNames).toContain('orchestrate_status');
287
+ expect(opNames).toContain('orchestrate_quick_capture');
288
+ // Capture ops (4)
289
+ expect(opNames).toContain('capture_knowledge');
290
+ expect(opNames).toContain('capture_quick');
291
+ expect(opNames).toContain('search_intelligent');
292
+ expect(opNames).toContain('search_feedback');
293
+ // Grading ops (5)
294
+ expect(opNames).toContain('plan_grade');
295
+ expect(opNames).toContain('plan_check_history');
296
+ expect(opNames).toContain('plan_latest_check');
297
+ expect(opNames).toContain('plan_meets_grade');
298
+ expect(opNames).toContain('plan_auto_improve');
299
+ // Admin Extra ops (10)
300
+ expect(opNames).toContain('admin_telemetry');
301
+ expect(opNames).toContain('admin_telemetry_recent');
302
+ expect(opNames).toContain('admin_telemetry_reset');
303
+ expect(opNames).toContain('admin_permissions');
304
+ expect(opNames).toContain('admin_vault_analytics');
305
+ expect(opNames).toContain('admin_search_insights');
306
+ expect(opNames).toContain('admin_module_status');
307
+ expect(opNames).toContain('admin_env');
308
+ expect(opNames).toContain('admin_gc');
309
+ expect(opNames).toContain('admin_export_config');
310
+ // Curator Extra ops (4)
311
+ expect(opNames).toContain('curator_entry_history');
312
+ expect(opNames).toContain('curator_record_snapshot');
313
+ expect(opNames).toContain('curator_queue_stats');
314
+ expect(opNames).toContain('curator_enrich');
315
+ // Project ops (12)
316
+ expect(opNames).toContain('project_get');
317
+ expect(opNames).toContain('project_list');
318
+ expect(opNames).toContain('project_unregister');
319
+ expect(opNames).toContain('project_get_rules');
320
+ expect(opNames).toContain('project_list_rules');
321
+ expect(opNames).toContain('project_add_rule');
322
+ expect(opNames).toContain('project_remove_rule');
323
+ expect(opNames).toContain('project_link');
324
+ expect(opNames).toContain('project_unlink');
325
+ expect(opNames).toContain('project_get_links');
326
+ expect(opNames).toContain('project_linked_projects');
327
+ expect(opNames).toContain('project_touch');
328
+ // Cross-project memory ops (3)
329
+ expect(opNames).toContain('memory_promote_to_global');
330
+ expect(opNames).toContain('memory_configure');
331
+ expect(opNames).toContain('memory_cross_project_search');
332
+ // Total: 152 (147 core + 5 agent-specific)
333
+ expect(facade.ops.length).toBe(152);
192
334
  });
193
335
 
194
336
  it('search should query across all domains with ranked results', async () => {
@@ -280,7 +422,9 @@ ${domainDescribes}
280
422
  const setupOp = facade.ops.find((o) => o.name === 'setup')!;
281
423
  const result = (await setupOp.handler({ projectPath: '/tmp/nonexistent-test' })) as {
282
424
  agent: { name: string };
425
+ claude_md: { project: { exists: boolean; has_agent_section: boolean }; global: { exists: boolean; has_agent_section: boolean } };
283
426
  vault: { entries: number };
427
+ hooks: { agent: string[]; global: string[]; missing: string[] };
284
428
  recommendations: string[];
285
429
  };
286
430
  expect(result.agent.name).toBe('${escapeQuotes(config.name)}');
@@ -330,6 +474,32 @@ ${domainDescribes}
330
474
  const result = (await healthOp.handler({})) as { score: number };
331
475
  expect(result.score).toBeGreaterThan(0);
332
476
  });
477
+
478
+ it('governance_policy get should return default policy', async () => {
479
+ const facade = buildCoreFacade();
480
+ const policyOp = facade.ops.find((o) => o.name === 'governance_policy')!;
481
+ const result = (await policyOp.handler({ action: 'get', projectPath: '/test' })) as {
482
+ projectPath: string;
483
+ quotas: { maxEntriesTotal: number };
484
+ autoCapture: { enabled: boolean };
485
+ };
486
+ expect(result.projectPath).toBe('/test');
487
+ expect(result.quotas.maxEntriesTotal).toBe(500);
488
+ expect(result.autoCapture.enabled).toBe(true);
489
+ });
490
+
491
+ it('governance_dashboard should return combined view', async () => {
492
+ const facade = buildCoreFacade();
493
+ const dashOp = facade.ops.find((o) => o.name === 'governance_dashboard')!;
494
+ const result = (await dashOp.handler({ projectPath: '/test' })) as {
495
+ vaultSize: number;
496
+ quotaPercent: number;
497
+ pendingProposals: number;
498
+ };
499
+ expect(typeof result.vaultSize).toBe('number');
500
+ expect(typeof result.quotaPercent).toBe('number');
501
+ expect(result.pendingProposals).toBe(0);
502
+ });
333
503
  });
334
504
  });
335
505
  `;
@@ -387,7 +557,7 @@ function generateDomainDescribe(agentId: string, domain: string): string {
387
557
  severity: 'warning',
388
558
  description: 'A captured pattern.',
389
559
  tags: ['captured'],
390
- })) as { captured: boolean };
560
+ })) as { captured: boolean; governance?: { action: string } };
391
561
  expect(result.captured).toBe(true);
392
562
  const entry = runtime.vault.get('${domain}-cap1');
393
563
  expect(entry).not.toBeNull();