@phuetz/code-buddy 1.2.0 → 1.3.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 (104) hide show
  1. package/README.md +119 -24
  2. package/dist/agent/autonomous/agentic-coding-contract.d.ts +6 -6
  3. package/dist/agent/base-agent.d.ts +4 -0
  4. package/dist/agent/base-agent.js +6 -0
  5. package/dist/agent/facades/infrastructure-facade.d.ts +9 -2
  6. package/dist/agent/facades/infrastructure-facade.js +15 -6
  7. package/dist/agent/self-improvement/authored-artifact-gate.d.ts +18 -0
  8. package/dist/agent/self-improvement/authored-artifact-gate.js +42 -0
  9. package/dist/agent/self-improvement/authored-tool-runtime.d.ts +27 -0
  10. package/dist/agent/self-improvement/authored-tool-runtime.js +57 -0
  11. package/dist/agent/self-improvement/authored-tool-store.d.ts +24 -0
  12. package/dist/agent/self-improvement/authored-tool-store.js +57 -0
  13. package/dist/agent/self-improvement/llm-tool-proposer.d.ts +41 -0
  14. package/dist/agent/self-improvement/llm-tool-proposer.js +136 -0
  15. package/dist/agent/self-improvement/sandbox-scorer.d.ts +17 -0
  16. package/dist/agent/self-improvement/sandbox-scorer.js +43 -0
  17. package/dist/agent/self-improvement/self-knowledge.d.ts +8 -0
  18. package/dist/agent/self-improvement/self-knowledge.js +24 -0
  19. package/dist/agent/self-improvement/skill-benchmark.d.ts +9 -0
  20. package/dist/agent/self-improvement/skill-benchmark.js +22 -0
  21. package/dist/agent/self-improvement/skill-consolidator.d.ts +71 -0
  22. package/dist/agent/self-improvement/skill-consolidator.js +137 -0
  23. package/dist/agent/self-improvement/skill-engine.d.ts +42 -0
  24. package/dist/agent/self-improvement/skill-engine.js +87 -0
  25. package/dist/agent/self-improvement/skill-gate.d.ts +19 -0
  26. package/dist/agent/self-improvement/skill-gate.js +62 -0
  27. package/dist/agent/self-improvement/skill-mutator.d.ts +74 -0
  28. package/dist/agent/self-improvement/skill-mutator.js +223 -0
  29. package/dist/agent/self-improvement/skill-proposer.d.ts +40 -0
  30. package/dist/agent/self-improvement/skill-proposer.js +82 -0
  31. package/dist/agent/self-improvement/skill-types.d.ts +41 -0
  32. package/dist/agent/self-improvement/skill-types.js +13 -0
  33. package/dist/agent/self-improvement/tool-benchmark.d.ts +10 -0
  34. package/dist/agent/self-improvement/tool-benchmark.js +37 -0
  35. package/dist/agent/self-improvement/tool-engine.d.ts +54 -0
  36. package/dist/agent/self-improvement/tool-engine.js +101 -0
  37. package/dist/agent/self-improvement/tool-gate.d.ts +20 -0
  38. package/dist/agent/self-improvement/tool-gate.js +78 -0
  39. package/dist/agent/self-improvement/tool-proposer.d.ts +31 -0
  40. package/dist/agent/self-improvement/tool-proposer.js +34 -0
  41. package/dist/agent/self-improvement/tool-skill-mutator.d.ts +40 -0
  42. package/dist/agent/self-improvement/tool-skill-mutator.js +79 -0
  43. package/dist/agent/self-improvement/tool-types.d.ts +48 -0
  44. package/dist/agent/self-improvement/tool-types.js +9 -0
  45. package/dist/agent/self-improvement/types.d.ts +3 -1
  46. package/dist/agent/tool-handler.js +3 -0
  47. package/dist/codebuddy/providers/provider-chatgpt-responses.js +6 -1
  48. package/dist/codebuddy/tools.d.ts +7 -0
  49. package/dist/codebuddy/tools.js +40 -0
  50. package/dist/commands/cli/improve-command.js +123 -0
  51. package/dist/commands/enhanced-command-handler.js +1 -1
  52. package/dist/commands/handlers/missing-handlers.d.ts +1 -1
  53. package/dist/commands/handlers/missing-handlers.js +26 -3
  54. package/dist/commands/skills-cli/index.js +123 -0
  55. package/dist/commands/slash/builtin-commands.js +1 -1
  56. package/dist/companion/percepts.js +11 -1
  57. package/dist/context/bootstrap-loader.js +6 -23
  58. package/dist/context/import-directive-parser.d.ts +4 -0
  59. package/dist/context/import-directive-parser.js +51 -6
  60. package/dist/context/instruction-excludes.d.ts +30 -1
  61. package/dist/context/instruction-excludes.js +71 -1
  62. package/dist/context/jit-context.d.ts +8 -10
  63. package/dist/context/jit-context.js +28 -106
  64. package/dist/context/project-context.d.ts +90 -0
  65. package/dist/context/project-context.js +295 -0
  66. package/dist/daemon/autonomous-loop.d.ts +31 -1
  67. package/dist/daemon/autonomous-loop.js +80 -2
  68. package/dist/harness/contract.d.ts +28 -28
  69. package/dist/identity/identity-manager.js +3 -2
  70. package/dist/index.js +17 -1
  71. package/dist/mcp/mcp-resources.js +2 -3
  72. package/dist/sensory/dreaming.d.ts +45 -0
  73. package/dist/sensory/dreaming.js +114 -0
  74. package/dist/sensory/heartbeat-scheduler.d.ts +38 -0
  75. package/dist/sensory/heartbeat-scheduler.js +72 -0
  76. package/dist/sensory/reactions.d.ts +24 -0
  77. package/dist/sensory/reactions.js +31 -0
  78. package/dist/sensory/screen-reaction.d.ts +23 -0
  79. package/dist/sensory/screen-reaction.js +59 -0
  80. package/dist/sensory/sensory-bridge.d.ts +23 -0
  81. package/dist/sensory/sensory-bridge.js +85 -0
  82. package/dist/sensory/sensory-memory.d.ts +20 -0
  83. package/dist/sensory/sensory-memory.js +39 -0
  84. package/dist/sensory/speech-reaction.d.ts +21 -0
  85. package/dist/sensory/speech-reaction.js +83 -0
  86. package/dist/sensory/vision-reaction.d.ts +31 -0
  87. package/dist/sensory/vision-reaction.js +74 -0
  88. package/dist/server/index.js +89 -0
  89. package/dist/services/prompt-builder.d.ts +10 -0
  90. package/dist/services/prompt-builder.js +75 -9
  91. package/dist/skills/parser.js +3 -0
  92. package/dist/skills/skill-importer.d.ts +58 -0
  93. package/dist/skills/skill-importer.js +261 -0
  94. package/dist/skills/skill-sources.d.ts +20 -0
  95. package/dist/skills/skill-sources.js +102 -0
  96. package/dist/skills/types.d.ts +6 -0
  97. package/dist/tools/register-tool-handler.d.ts +25 -0
  98. package/dist/tools/register-tool-handler.js +100 -0
  99. package/dist/tools/registry.d.ts +6 -0
  100. package/dist/tools/registry.js +8 -0
  101. package/dist/utils/init-project.d.ts +7 -0
  102. package/dist/utils/init-project.js +37 -0
  103. package/dist/utils/settings-manager.d.ts +12 -0
  104. package/package.json +2 -2
@@ -101,6 +101,129 @@ export function registerImproveCommands(program) {
101
101
  .join('\n');
102
102
  print({ ...result, committed }, options, text);
103
103
  });
104
+ improve
105
+ .command('tools')
106
+ .description('Author + behaviorally validate NEW tools for the agent (held-out gated, anti-gaming)')
107
+ .option('--json', 'output JSON')
108
+ .option('--apply', 'keep validated tools for this session (overrides propose-only)')
109
+ .action(async (options) => {
110
+ const { ToolImprovementEngine } = await import('../../agent/self-improvement/tool-engine.js');
111
+ const { LlmToolProposer } = await import('../../agent/self-improvement/llm-tool-proposer.js');
112
+ const { SEED_TOOL_SCENARIOS } = await import('../../agent/self-improvement/tool-benchmark.js');
113
+ const engine = new ToolImprovementEngine({
114
+ scenarios: SEED_TOOL_SCENARIOS,
115
+ proposer: new LlmToolProposer(),
116
+ ...(options.apply ? { autonomy: 'auto-apply' } : {}),
117
+ });
118
+ const results = await engine.runLoop();
119
+ const kept = results.map((r) => (r.applied ? r.gate?.appliedRef : null)).filter(Boolean);
120
+ const text = [
121
+ `Autonomy: ${results[0]?.autonomy ?? 'propose-only'}`,
122
+ `Cycles: ${results.length}`,
123
+ ...results.map((r) => ` ${r.selectedScenarioId ?? '—'}: ${r.applied
124
+ ? `AUTHORED + KEPT (${r.gate?.appliedRef})`
125
+ : r.gate?.accepted
126
+ ? 'accepted (propose-only) — re-run with --apply to keep'
127
+ : r.gate?.rejectionReason
128
+ ? `rejected (${r.gate.rejectionReason})`
129
+ : r.notes.join('; ')}`),
130
+ kept.length
131
+ ? `Kept: ${kept.join(', ')} (archived + persisted to .codebuddy/self-improvement; reloaded at next start when CODEBUDDY_SELF_IMPROVE=true)`
132
+ : 'No tool kept this run',
133
+ ].join('\n');
134
+ print({ kind: 'self_improvement_tools', cycles: results }, options, text);
135
+ });
136
+ improve
137
+ .command('skills')
138
+ .description('Author + safety-gate NEW skills for the agent (firewall + coverage)')
139
+ .option('--json', 'output JSON')
140
+ .option('--apply', 'install validated skills (overrides propose-only)')
141
+ .action(async (options) => {
142
+ const { SkillImprovementEngine } = await import('../../agent/self-improvement/skill-engine.js');
143
+ const { LlmSkillProposer } = await import('../../agent/self-improvement/skill-proposer.js');
144
+ const { SEED_SKILL_SCENARIOS } = await import('../../agent/self-improvement/skill-benchmark.js');
145
+ const engine = new SkillImprovementEngine({
146
+ scenarios: SEED_SKILL_SCENARIOS,
147
+ proposer: new LlmSkillProposer(),
148
+ ...(options.apply ? { autonomy: 'auto-apply' } : {}),
149
+ });
150
+ const results = await engine.runLoop();
151
+ const kept = results.map((r) => (r.applied ? r.gate?.appliedRef : null)).filter(Boolean);
152
+ const text = [
153
+ `Autonomy: ${results[0]?.autonomy ?? 'propose-only'}`,
154
+ `Cycles: ${results.length}`,
155
+ ...results.map((r) => ` ${r.selectedScenarioId ?? '—'}: ${r.applied
156
+ ? `AUTHORED + INSTALLED (${r.gate?.appliedRef})`
157
+ : r.gate?.accepted
158
+ ? 'accepted (propose-only) — re-run with --apply to install'
159
+ : r.gate?.rejectionReason
160
+ ? `rejected (${r.gate.rejectionReason})`
161
+ : r.notes.join('; ')}`),
162
+ kept.length ? `Installed: ${kept.join(', ')} (under .codebuddy/skills/authored)` : 'No skill installed this run',
163
+ ].join('\n');
164
+ print({ kind: 'self_improvement_skills', cycles: results }, options, text);
165
+ });
166
+ improve
167
+ .command('skills-list')
168
+ .description('List installed authored skills (with pinned status)')
169
+ .option('--json', 'output JSON')
170
+ .action(async (options) => {
171
+ const { LiveSkillMutator } = await import('../../agent/self-improvement/skill-mutator.js');
172
+ const m = new LiveSkillMutator();
173
+ const skills = m.listAuthored().map((name) => ({ name, pinned: m.isPinned(name) }));
174
+ const text = skills.length
175
+ ? skills.map((s) => ` ${s.pinned ? '📌' : ' '} ${s.name}`).join('\n')
176
+ : 'No authored skills installed';
177
+ print({ kind: 'authored_skills', skills }, options, text);
178
+ });
179
+ improve
180
+ .command('skills-pin <name>')
181
+ .description('Pin an authored skill (protect it from curation overwrite/remove/consolidation)')
182
+ .option('--json', 'output JSON')
183
+ .action(async (name, options) => {
184
+ const { LiveSkillMutator } = await import('../../agent/self-improvement/skill-mutator.js');
185
+ const ok = new LiveSkillMutator().pin(name);
186
+ print({ kind: 'skill_pin', name, ok }, options, ok ? `Pinned ${name}` : `Skill not found: ${name}`);
187
+ });
188
+ improve
189
+ .command('skills-unpin <name>')
190
+ .description('Unpin an authored skill')
191
+ .option('--json', 'output JSON')
192
+ .action(async (name, options) => {
193
+ const { LiveSkillMutator } = await import('../../agent/self-improvement/skill-mutator.js');
194
+ const ok = new LiveSkillMutator().unpin(name);
195
+ print({ kind: 'skill_unpin', name, ok }, options, ok ? `Unpinned ${name}` : `Skill not found: ${name}`);
196
+ });
197
+ improve
198
+ .command('skills-restore <name>')
199
+ .description('Restore a previously archived authored skill')
200
+ .option('--json', 'output JSON')
201
+ .action(async (name, options) => {
202
+ const { LiveSkillMutator } = await import('../../agent/self-improvement/skill-mutator.js');
203
+ const ok = new LiveSkillMutator().restore(name);
204
+ print({ kind: 'skill_restore', name, ok }, options, ok ? `Restored ${name}` : `No archived skill: ${name}`);
205
+ });
206
+ improve
207
+ .command('skills-consolidate')
208
+ .description('Merge overlapping authored skills into one umbrella (coverage-gated)')
209
+ .option('--json', 'output JSON')
210
+ .option('--apply', 'install the umbrella + archive merged siblings (else preview)')
211
+ .action(async (options) => {
212
+ const { LiveSkillMutator } = await import('../../agent/self-improvement/skill-mutator.js');
213
+ const { consolidateCluster, buildClusterFromInstalled, LlmUmbrellaProposer } = await import('../../agent/self-improvement/skill-consolidator.js');
214
+ const { SEED_SKILL_SCENARIOS } = await import('../../agent/self-improvement/skill-benchmark.js');
215
+ const mutator = new LiveSkillMutator();
216
+ const cluster = buildClusterFromInstalled(mutator, SEED_SKILL_SCENARIOS);
217
+ const out = await consolidateCluster(cluster, new LlmUmbrellaProposer(), mutator, new EvolutionaryArchive(), {
218
+ keepOnAccept: options.apply === true,
219
+ });
220
+ const text = out.accepted
221
+ ? out.absorbed.length
222
+ ? `Consolidated ${out.absorbed.join(', ')} into ${out.umbrellaName}` + (out.skippedPinned.length ? ` (kept pinned: ${out.skippedPinned.join(', ')})` : '')
223
+ : `Would consolidate into ${out.umbrellaName} — re-run with --apply` + (out.skippedPinned.length ? ` (would keep pinned: ${out.skippedPinned.join(', ')})` : '')
224
+ : `No consolidation: ${out.rejectionReason} — ${out.reasons.join('; ')}`;
225
+ print({ kind: 'skill_consolidation', ...out }, options, text);
226
+ });
104
227
  improve
105
228
  .command('loop')
106
229
  .description('Run improvement cycles until no further validated progress is made')
@@ -314,7 +314,7 @@ export class EnhancedCommandHandler {
314
314
  ['__ULTRAPLAN__', (args) => handleUltraplan(args)],
315
315
  ['__LIST_CHECKPOINTS__', (args) => handleListCheckpoints(args)],
316
316
  ['__RESTORE_CHECKPOINT__', (args) => handleRestoreCheckpoint(args)],
317
- ['__INIT_GROK__', () => handleInitGrok()],
317
+ ['__INIT_GROK__', (args) => handleInitGrok(args)],
318
318
  ['__REINIT_GROK__', () => handleReinitGrok()],
319
319
  ['__FEATURES__', () => handleFeatures()],
320
320
  ['__LESSONS__', (args) => handleLessonsCommand(args.join(' '))],
@@ -22,7 +22,7 @@ export declare function handleDiffCheckpoints(args: string[]): Promise<CommandHa
22
22
  export declare function handleFeatures(): CommandHandlerResult;
23
23
  export declare function handleListCheckpoints(_args: string[]): Promise<CommandHandlerResult>;
24
24
  export declare function handleRestoreCheckpoint(args: string[]): Promise<CommandHandlerResult>;
25
- export declare function handleInitGrok(): Promise<CommandHandlerResult>;
25
+ export declare function handleInitGrok(args?: string[]): Promise<CommandHandlerResult>;
26
26
  export declare function handleReinitGrok(): Promise<CommandHandlerResult>;
27
27
  export declare function handleStatus(): Promise<CommandHandlerResult>;
28
28
  export declare function handleNew(args: string[]): Promise<CommandHandlerResult>;
@@ -660,17 +660,40 @@ Use /checkpoints to see available checkpoints.`,
660
660
  // ============================================================================
661
661
  // /init - Initialize Code Buddy Project
662
662
  // ============================================================================
663
- export async function handleInitGrok() {
663
+ export async function handleInitGrok(args = []) {
664
664
  try {
665
- const { initCodeBuddyProject, formatInitResult } = await import('../../utils/init-project.js');
665
+ const fast = args.some((a) => a === '--fast' || a === '-f');
666
+ const { initCodeBuddyProject, formatInitResult, buildInitPrompt } = await import('../../utils/init-project.js');
667
+ // Deterministic scaffolding first (.codebuddy plumbing + template files,
668
+ // skip-if-exists). This always runs, so the config + a baseline AGENTS.md exist.
666
669
  const initResult = await initCodeBuddyProject();
670
+ if (fast) {
671
+ // --fast: deterministic template only (the legacy behavior).
672
+ return {
673
+ handled: true,
674
+ entry: { type: 'assistant', content: formatInitResult(initResult), timestamp: new Date() },
675
+ };
676
+ }
677
+ // Default: hand off to the agent to read the code and write a tailored
678
+ // AGENTS.md, grounded on the RepoProfiler's verified facts.
679
+ let contextPack = '';
680
+ try {
681
+ const { getRepoProfiler } = await import('../../agent/repo-profiler.js');
682
+ const profile = await getRepoProfiler().getProfile();
683
+ contextPack = profile.contextPack ?? '';
684
+ }
685
+ catch {
686
+ /* profiler optional — the prompt still works without the facts block */
687
+ }
667
688
  return {
668
689
  handled: true,
669
690
  entry: {
670
691
  type: 'assistant',
671
- content: formatInitResult(initResult),
692
+ content: `${formatInitResult(initResult)}\n\nNow analyzing the repository to write a tailored \`AGENTS.md\`… (run \`/init --fast\` for the deterministic template only)`,
672
693
  timestamp: new Date(),
673
694
  },
695
+ passToAI: true,
696
+ prompt: buildInitPrompt(contextPack),
674
697
  };
675
698
  }
676
699
  catch (error) {
@@ -688,6 +688,129 @@ export function registerSkillsCommands(program) {
688
688
  console.log(` ! ${error}`);
689
689
  }
690
690
  });
691
+ // ── Import external skills (Hermes / a repository), firewall-gated ──────────
692
+ skills
693
+ .command('import')
694
+ .description('Import external skills from a directory or a named source (firewall-gated)')
695
+ .option('--dir <path>', 'import from a local directory')
696
+ .option('--source <name>', 'import from a named source (see `skills sources`)')
697
+ .option('--apply', 'install (default is a dry run)')
698
+ .option('--include-review', "also import skills the firewall flags as 'review'")
699
+ .option('--overwrite', 'overwrite an already-imported skill')
700
+ .option('--category <c>', 'only import skills whose path contains this')
701
+ .option('--json', 'output JSON')
702
+ .action(async (opts) => {
703
+ const { importSkills } = await import('../../skills/skill-importer.js');
704
+ const { getSource, resolveSourceDir } = await import('../../skills/skill-sources.js');
705
+ let dir;
706
+ let label = 'import';
707
+ if (opts.dir) {
708
+ dir = opts.dir.startsWith('~') ? path.join(os.homedir(), opts.dir.slice(1)) : opts.dir;
709
+ label = path.basename(dir);
710
+ }
711
+ else if (opts.source) {
712
+ const src = getSource(opts.source);
713
+ if (!src) {
714
+ console.log(`Unknown source: ${opts.source} (see \`buddy skills sources list\`)`);
715
+ return;
716
+ }
717
+ dir = resolveSourceDir(src);
718
+ label = src.name;
719
+ }
720
+ else {
721
+ console.log('Specify --dir <path> or --source <name>.');
722
+ return;
723
+ }
724
+ const fs = await import('fs');
725
+ if (!fs.existsSync(dir)) {
726
+ console.log(`Directory not found: ${dir}`);
727
+ return;
728
+ }
729
+ const report = importSkills(dir, {
730
+ source: label,
731
+ dryRun: opts.apply !== true,
732
+ includeReview: opts.includeReview === true,
733
+ overwrite: opts.overwrite === true,
734
+ ...(opts.category ? { category: opts.category } : {}),
735
+ });
736
+ if (opts.json) {
737
+ console.log(JSON.stringify({ source: label, report }, null, 2));
738
+ return;
739
+ }
740
+ console.log(report.dryRun ? `Dry run (use --apply to install) — "${label}"` : `Imported from "${label}"`);
741
+ console.log(` ${report.dryRun ? 'would import' : 'imported'}: ${report.imported.length} · quarantined: ${report.quarantined.length} · review: ${report.review.length} · skipped: ${report.skipped.length}`);
742
+ if (report.quarantined.length) {
743
+ console.log(' ⚠️ quarantined by firewall:');
744
+ for (const q of report.quarantined.slice(0, 15))
745
+ console.log(` - ${q.sourcePath}`);
746
+ }
747
+ if (report.imported.length) {
748
+ console.log(` ✓ ${report.dryRun ? 'would import' : 'imported'}:`);
749
+ for (const s of report.imported.slice(0, 30))
750
+ console.log(` - ${s.name}`);
751
+ }
752
+ });
753
+ skills
754
+ .command('imported')
755
+ .description('List imported skills (with provenance + pinned status)')
756
+ .option('--json', 'output JSON')
757
+ .action(async (opts) => {
758
+ const fs = await import('fs');
759
+ const { parseSkillFile } = await import('../../skills/parser.js');
760
+ const root = path.join(os.homedir(), '.codebuddy', 'skills', 'managed');
761
+ const items = [];
762
+ if (fs.existsSync(root)) {
763
+ for (const e of fs.readdirSync(root, { withFileTypes: true })) {
764
+ if (!e.isDirectory() || !e.name.startsWith('imported-'))
765
+ continue;
766
+ const md = path.join(root, e.name, 'SKILL.md');
767
+ if (!fs.existsSync(md))
768
+ continue;
769
+ let pinned = false;
770
+ let source;
771
+ try {
772
+ const sk = parseSkillFile(fs.readFileSync(md, 'utf-8'), md, 'managed');
773
+ pinned = sk.metadata.pinned === true;
774
+ source = sk.metadata.source;
775
+ }
776
+ catch { /* ignore */ }
777
+ items.push({ name: e.name, pinned, ...(source ? { source } : {}) });
778
+ }
779
+ }
780
+ if (opts.json) {
781
+ console.log(JSON.stringify({ imported: items }, null, 2));
782
+ return;
783
+ }
784
+ console.log(items.length ? items.map((s) => ` ${s.pinned ? '📌' : ' '} ${s.name}${s.source ? ` (source: ${s.source})` : ''}`).join('\n') : 'No imported skills');
785
+ });
786
+ const sources = skills.command('sources').description('Manage skill sources (the import referential)');
787
+ sources
788
+ .command('list')
789
+ .option('--json', 'output JSON')
790
+ .action(async (opts) => {
791
+ const { listSources } = await import('../../skills/skill-sources.js');
792
+ const list = listSources();
793
+ if (opts.json) {
794
+ console.log(JSON.stringify({ sources: list }, null, 2));
795
+ return;
796
+ }
797
+ console.log(list.length ? list.map((s) => ` ${s.name} [${s.type}] ${s.location}`).join('\n') : 'No skill sources configured');
798
+ });
799
+ sources
800
+ .command('add <name> <location>')
801
+ .description('Register a skill source (a local dir or a git url)')
802
+ .option('--type <type>', "'dir' or 'git'")
803
+ .action(async (name, location, opts) => {
804
+ const { addSource } = await import('../../skills/skill-sources.js');
805
+ const src = addSource(name, location, opts.type);
806
+ console.log(`Added source ${src.name} [${src.type}] → ${src.location}`);
807
+ });
808
+ sources
809
+ .command('remove <name>')
810
+ .action(async (name) => {
811
+ const { removeSource } = await import('../../skills/skill-sources.js');
812
+ console.log(removeSource(name) ? `Removed source ${name}` : `No such source: ${name}`);
813
+ });
691
814
  }
692
815
  async function toggleSkill(name, enabled, opts) {
693
816
  const { getSkillsHub } = await import('../../skills/hub.js');
@@ -42,7 +42,7 @@ const coreCommands = [
42
42
  },
43
43
  {
44
44
  name: 'init',
45
- description: 'Initialize .codebuddy directory with templates',
45
+ description: 'Scaffold .codebuddy, then analyze the repo and write a tailored AGENTS.md (use `--fast` for the deterministic template only)',
46
46
  prompt: '__INIT_GROK__',
47
47
  filePath: '',
48
48
  isBuiltin: true
@@ -1,4 +1,4 @@
1
- import { appendFile, mkdir, readFile, stat } from 'fs/promises';
1
+ import { appendFile, mkdir, readFile, rename, stat } from 'fs/promises';
2
2
  import * as crypto from 'crypto';
3
3
  import * as path from 'path';
4
4
  const DEFAULT_RECENT_LIMIT = 10;
@@ -138,6 +138,16 @@ export async function recordCompanionPercept(input, options = {}) {
138
138
  }
139
139
  : percept;
140
140
  await mkdir(path.dirname(storePath), { recursive: true });
141
+ // Rotate when large (the disk-guard lesson, mirroring dreams.jsonl): a long-running
142
+ // companion appends a percept per event, so cap growth + keep one backup.
143
+ try {
144
+ const info = await stat(storePath);
145
+ if (info.size > 1024 * 1024)
146
+ await rename(storePath, `${storePath}.1`);
147
+ }
148
+ catch {
149
+ /* no file yet */
150
+ }
141
151
  await appendFile(storePath, `${JSON.stringify(storedPercept)}\n`, 'utf8');
142
152
  return percept;
143
153
  }
@@ -15,10 +15,12 @@ import { logger } from '../utils/logger.js';
15
15
  // ============================================================================
16
16
  // Defaults
17
17
  // ============================================================================
18
+ // AGENTS.md is intentionally NOT here — instruction files (AGENTS/CODEBUDDY/
19
+ // CONTEXT/INSTRUCTIONS) are owned by the unified project-context loader
20
+ // (src/context/project-context.ts). This loader covers soul/identity files.
18
21
  const DEFAULT_BOOTSTRAP_FILES = [
19
22
  'BOOT.md',
20
23
  'BOOTSTRAP.md',
21
- 'AGENTS.md',
22
24
  'SOUL.md',
23
25
  // TOOLS.md excluded: tool descriptions are already sent as function definitions
24
26
  'IDENTITY.md',
@@ -80,28 +82,9 @@ export class BootstrapLoader {
80
82
  sources.push(content.source);
81
83
  totalChars += text.length;
82
84
  }
83
- // 2. Hierarchical instruction files (Codex CLI pattern)
84
- if (totalChars < this.config.maxChars) {
85
- const hierarchicalResult = await this.loadHierarchical(cwd);
86
- for (const entry of hierarchicalResult) {
87
- if (totalChars >= this.config.maxChars) {
88
- truncated = true;
89
- break;
90
- }
91
- if (sources.includes(entry.source))
92
- continue;
93
- const remaining = this.config.maxChars - totalChars;
94
- let text = entry.text;
95
- if (text.length > remaining) {
96
- text = text.slice(0, remaining) + '\n\n... (truncated)';
97
- truncated = true;
98
- }
99
- const relSource = path.relative(cwd, entry.source) || entry.source;
100
- sections.push(`## ${entry.fileName} (${relSource})\n\n${text}`);
101
- sources.push(entry.source);
102
- totalChars += text.length;
103
- }
104
- }
85
+ // (Instruction-file hierarchy AGENTS/CODEBUDDY/CONTEXT/INSTRUCTIONS is
86
+ // now handled by the unified project-context loader, injected before this
87
+ // block in prompt-builder. `loadHierarchical` below is retained but unused.)
105
88
  // 3. Auto-generated project knowledge (from /docs generate --with-llm)
106
89
  if (totalChars < this.config.maxChars) {
107
90
  const knowledgePath = path.join(cwd, '.codebuddy', 'PROJECT_KNOWLEDGE.md');
@@ -27,6 +27,10 @@ export interface ImportResolveOptions {
27
27
  depth?: number;
28
28
  /** Set of already-visited absolute paths (cycle detection) */
29
29
  visited?: Set<string>;
30
+ /** Max recursion depth for nested imports (default 5) */
31
+ maxDepth?: number;
32
+ /** Max total content size after imports, in chars (default 50000) */
33
+ maxBytes?: number;
30
34
  }
31
35
  /**
32
36
  * Resolve all @import directives in the given content.
@@ -43,14 +43,23 @@ const IMPORT_DIRECTIVE_REGEX = /^@(~\/|\/\/|[^\s@].+?)$/gm;
43
43
  * @returns Content with all imports resolved inline
44
44
  */
45
45
  export function resolveImportDirectives(content, options) {
46
- const { baseDir, projectRoot = process.cwd(), homeDir = process.env.HOME || process.env.USERPROFILE || '', depth = 0, visited = new Set(), } = options;
47
- if (depth >= MAX_IMPORT_DEPTH) {
48
- logger.debug(`Import directive: max depth ${MAX_IMPORT_DEPTH} reached, skipping further imports`);
46
+ const { baseDir, projectRoot = process.cwd(), homeDir = process.env.HOME || process.env.USERPROFILE || '', depth = 0, visited = new Set(), maxDepth = MAX_IMPORT_DEPTH, maxBytes = MAX_IMPORT_CONTENT, } = options;
47
+ if (depth >= maxDepth) {
48
+ logger.debug(`Import directive: max depth ${maxDepth} reached, skipping further imports`);
49
49
  return content;
50
50
  }
51
51
  let result = content;
52
52
  let totalSize = content.length;
53
- result = result.replace(IMPORT_DIRECTIVE_REGEX, (fullMatch, importPath) => {
53
+ // Mask code regions so an @path directive that sits inside a fenced block or
54
+ // inline code span is left as literal content, not resolved as an import.
55
+ // Matches Claude Code / Gemini CLI semantics (a code example that mentions
56
+ // `@./foo.md` must not trigger a file read).
57
+ const codeRegions = findCodeRegions(content);
58
+ result = result.replace(IMPORT_DIRECTIVE_REGEX, (fullMatch, importPath, offset) => {
59
+ // Leave directives inside code spans/fences untouched.
60
+ if (isInsideRegion(offset, codeRegions)) {
61
+ return fullMatch;
62
+ }
54
63
  // Resolve the import path
55
64
  const resolvedPath = resolveImportPath(importPath.trim(), baseDir, projectRoot, homeDir);
56
65
  if (!resolvedPath) {
@@ -71,9 +80,9 @@ export function resolveImportDirectives(content, options) {
71
80
  try {
72
81
  let importedContent = fs.readFileSync(absolutePath, 'utf-8');
73
82
  // Size guard
74
- if (totalSize + importedContent.length > MAX_IMPORT_CONTENT) {
83
+ if (totalSize + importedContent.length > maxBytes) {
75
84
  logger.debug(`Import directive: size limit reached, truncating "${importPath}"`);
76
- const remaining = MAX_IMPORT_CONTENT - totalSize;
85
+ const remaining = maxBytes - totalSize;
77
86
  if (remaining <= 0) {
78
87
  return `<!-- import truncated: ${importPath} -->`;
79
88
  }
@@ -88,6 +97,8 @@ export function resolveImportDirectives(content, options) {
88
97
  homeDir,
89
98
  depth: depth + 1,
90
99
  visited: newVisited,
100
+ maxDepth,
101
+ maxBytes,
91
102
  });
92
103
  totalSize += resolved.length;
93
104
  logger.debug(`Import directive: resolved "${importPath}" (${resolved.length} chars)`);
@@ -129,4 +140,38 @@ function resolveImportPath(importPath, baseDir, projectRoot, homeDir) {
129
140
  return null;
130
141
  }
131
142
  }
143
+ // ============================================================================
144
+ // Code-region masking (so @imports inside code are inert)
145
+ // ============================================================================
146
+ /**
147
+ * Find all code spans/fences in the content as [start, end) ranges.
148
+ *
149
+ * Covers backtick code — inline `` `…` `` and fenced ```` ```…``` ```` (one or
150
+ * more backticks, closed by the same count) — plus `~~~` fenced blocks. An
151
+ * @import directive whose `@` falls inside any of these ranges is treated as
152
+ * literal text. Mirrors the gemini-cli memory import processor.
153
+ */
154
+ function findCodeRegions(content) {
155
+ const regions = [];
156
+ // Backtick inline + fenced: one or more backticks, lazily to the same run.
157
+ const backtick = /(`+)([\s\S]*?)\1/g;
158
+ let m;
159
+ while ((m = backtick.exec(content)) !== null) {
160
+ regions.push([m.index, m.index + m[0].length]);
161
+ }
162
+ // Tilde fenced blocks: a line of >=3 tildes, content, closing tilde line.
163
+ const tilde = /^[^\S\n]*(~{3,})[^\n]*\n[\s\S]*?\n[^\S\n]*\1[^\S\n]*$/gm;
164
+ while ((m = tilde.exec(content)) !== null) {
165
+ regions.push([m.index, m.index + m[0].length]);
166
+ }
167
+ return regions;
168
+ }
169
+ /** True if `offset` lies within any [start, end) code region. */
170
+ function isInsideRegion(offset, regions) {
171
+ for (const [start, end] of regions) {
172
+ if (offset >= start && offset < end)
173
+ return true;
174
+ }
175
+ return false;
176
+ }
132
177
  //# sourceMappingURL=import-directive-parser.js.map
@@ -7,9 +7,38 @@
7
7
  * Advanced enterprise architecture for claudeMdExcludes setting.
8
8
  */
9
9
  /**
10
- * Clear the excludes cache (for testing).
10
+ * Clear the excludes cache (for testing / config reload).
11
11
  */
12
12
  export declare function clearExcludesCache(): void;
13
+ /**
14
+ * Clear the context-config cache (for testing / `/context reload`).
15
+ */
16
+ export declare function clearContextConfigCache(): void;
17
+ /**
18
+ * Accepted project-instruction filenames, in precedence order. `AGENTS.md` is
19
+ * the cross-CLI primary (read by Codex/Cursor/Copilot/Claude Code); the rest
20
+ * are read for interop. All present names compose within a directory.
21
+ */
22
+ export declare const DEFAULT_CONTEXT_FILE_NAMES: readonly string[];
23
+ export interface ResolvedContextConfig {
24
+ /** Accepted instruction filenames, in precedence order. */
25
+ fileNames: string[];
26
+ /** Total byte budget for the startup hierarchy (Codex project_doc_max_bytes parity). */
27
+ maxBytes: number;
28
+ /** Per-touch incremental byte budget for JIT discovery. */
29
+ jitMaxBytes: number;
30
+ /** Byte budget passed to the @import resolver. */
31
+ importMaxBytes: number;
32
+ /** Max recursion depth for @import. */
33
+ importMaxDepth: number;
34
+ }
35
+ /**
36
+ * Load the `context` block from `.codebuddy/settings.json`, merged over
37
+ * defaults. Cached by settings path (cleared via `clearContextConfigCache()`).
38
+ * A missing `context` key returns the defaults, so existing projects are
39
+ * unaffected.
40
+ */
41
+ export declare function loadContextConfig(projectRoot?: string): ResolvedContextConfig;
13
42
  /**
14
43
  * Load exclude patterns from settings.json.
15
44
  * Returns an array of glob patterns (e.g., ["packages/legacy/**"]).
@@ -15,13 +15,83 @@ import { logger } from '../utils/logger.js';
15
15
  // ============================================================================
16
16
  let _excludePatterns = null;
17
17
  let _excludeCachePath = null;
18
+ let _contextConfig = null;
19
+ let _contextConfigCachePath = null;
18
20
  /**
19
- * Clear the excludes cache (for testing).
21
+ * Clear the excludes cache (for testing / config reload).
20
22
  */
21
23
  export function clearExcludesCache() {
22
24
  _excludePatterns = null;
23
25
  _excludeCachePath = null;
24
26
  }
27
+ /**
28
+ * Clear the context-config cache (for testing / `/context reload`).
29
+ */
30
+ export function clearContextConfigCache() {
31
+ _contextConfig = null;
32
+ _contextConfigCachePath = null;
33
+ }
34
+ // ============================================================================
35
+ // Context-file configuration (single source of truth for accepted filenames)
36
+ // ============================================================================
37
+ /**
38
+ * Accepted project-instruction filenames, in precedence order. `AGENTS.md` is
39
+ * the cross-CLI primary (read by Codex/Cursor/Copilot/Claude Code); the rest
40
+ * are read for interop. All present names compose within a directory.
41
+ */
42
+ export const DEFAULT_CONTEXT_FILE_NAMES = [
43
+ 'AGENTS.md',
44
+ 'CODEBUDDY.md',
45
+ 'CLAUDE.md',
46
+ 'GEMINI.md',
47
+ 'CONTEXT.md',
48
+ 'INSTRUCTIONS.md',
49
+ ];
50
+ const DEFAULT_CONTEXT_CONFIG = {
51
+ fileNames: [...DEFAULT_CONTEXT_FILE_NAMES],
52
+ maxBytes: 32_768,
53
+ jitMaxBytes: 4_096,
54
+ importMaxBytes: 50_000,
55
+ importMaxDepth: 5,
56
+ };
57
+ /**
58
+ * Load the `context` block from `.codebuddy/settings.json`, merged over
59
+ * defaults. Cached by settings path (cleared via `clearContextConfigCache()`).
60
+ * A missing `context` key returns the defaults, so existing projects are
61
+ * unaffected.
62
+ */
63
+ export function loadContextConfig(projectRoot = process.cwd()) {
64
+ const settingsPath = path.join(projectRoot, '.codebuddy', 'settings.json');
65
+ if (_contextConfig && _contextConfigCachePath === settingsPath) {
66
+ return _contextConfig;
67
+ }
68
+ const cfg = { ...DEFAULT_CONTEXT_CONFIG, fileNames: [...DEFAULT_CONTEXT_FILE_NAMES] };
69
+ if (fs.existsSync(settingsPath)) {
70
+ try {
71
+ const raw = fs.readFileSync(settingsPath, 'utf-8');
72
+ const ctx = JSON.parse(raw)?.context;
73
+ if (ctx && typeof ctx === 'object') {
74
+ if (Array.isArray(ctx.fileNames) && ctx.fileNames.every((n) => typeof n === 'string') && ctx.fileNames.length > 0) {
75
+ cfg.fileNames = ctx.fileNames;
76
+ }
77
+ if (Number.isFinite(ctx.maxBytes) && ctx.maxBytes > 0)
78
+ cfg.maxBytes = ctx.maxBytes;
79
+ if (Number.isFinite(ctx.jitMaxBytes) && ctx.jitMaxBytes > 0)
80
+ cfg.jitMaxBytes = ctx.jitMaxBytes;
81
+ if (Number.isFinite(ctx.importMaxBytes) && ctx.importMaxBytes > 0)
82
+ cfg.importMaxBytes = ctx.importMaxBytes;
83
+ if (Number.isFinite(ctx.importMaxDepth) && ctx.importMaxDepth > 0)
84
+ cfg.importMaxDepth = ctx.importMaxDepth;
85
+ }
86
+ }
87
+ catch (err) {
88
+ logger.debug(`Failed to load context config: ${err}`);
89
+ }
90
+ }
91
+ _contextConfig = cfg;
92
+ _contextConfigCachePath = settingsPath;
93
+ return cfg;
94
+ }
25
95
  // ============================================================================
26
96
  // Loader
27
97
  // ============================================================================