@soederpop/luca 0.2.1 → 0.2.3

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 (35) hide show
  1. package/.github/workflows/release.yaml +2 -0
  2. package/CNAME +1 -0
  3. package/assistants/codingAssistant/ABOUT.md +3 -1
  4. package/assistants/codingAssistant/CORE.md +2 -4
  5. package/assistants/codingAssistant/hooks.ts +9 -10
  6. package/assistants/codingAssistant/tools.ts +9 -0
  7. package/assistants/inkbot/ABOUT.md +13 -2
  8. package/assistants/inkbot/CORE.md +278 -39
  9. package/assistants/inkbot/hooks.ts +0 -8
  10. package/assistants/inkbot/tools.ts +24 -18
  11. package/assistants/researcher/ABOUT.md +5 -0
  12. package/assistants/researcher/CORE.md +46 -0
  13. package/assistants/researcher/hooks.ts +16 -0
  14. package/assistants/researcher/tools.ts +237 -0
  15. package/commands/inkbot.ts +526 -194
  16. package/docs/CNAME +1 -0
  17. package/docs/examples/assistant-hooks-reference.ts +171 -0
  18. package/index.html +1430 -0
  19. package/package.json +1 -1
  20. package/public/slides-ai-native.html +902 -0
  21. package/public/slides-intro.html +974 -0
  22. package/src/agi/features/assistant.ts +432 -62
  23. package/src/agi/features/conversation.ts +170 -10
  24. package/src/bootstrap/generated.ts +1 -1
  25. package/src/cli/build-info.ts +2 -2
  26. package/src/helper.ts +12 -3
  27. package/src/introspection/generated.agi.ts +1663 -644
  28. package/src/introspection/generated.node.ts +1637 -870
  29. package/src/introspection/generated.web.ts +1 -1
  30. package/src/python/generated.ts +1 -1
  31. package/src/scaffolds/generated.ts +1 -1
  32. package/test/assistant-hooks.test.ts +306 -0
  33. package/test/assistant.test.ts +1 -1
  34. package/test/fork-and-research.test.ts +450 -0
  35. package/SPEC.md +0 -304
package/SPEC.md DELETED
@@ -1,304 +0,0 @@
1
- ---
2
- tags: [dx, registration, metaprogramming, luca, architecture, "1.0"]
3
- status: exploring
4
- goal: get-luca-to-1.0-release
5
- ---
6
-
7
- # Class Registration Refactor Possibilities
8
-
9
- Exploring how static initialization blocks and class-level hooks could simplify helper registration, inspired by Ruby's `self.inherited(subclass)` pattern and how ActiveRecord::Base uses it.
10
-
11
- ## Why This Matters for 1.0
12
-
13
- Luca currently has **114 `.register()` calls** spread across 44 features, ~23 clients, and 3 servers. Adding a new feature requires touching **4 separate locations** (see the [Adding a New Feature checklist](../../../luca/CLAUDE.md)). This ceremony is the kind of friction that kills adoption — the [Luca 1.0 goal](../../goals/get-luca-to-1.0-release.md) explicitly requires that external developers can build their own features easily.
14
-
15
- The 4-step checklist today:
16
- 1. Feature file with class + trailing `features.register()` call
17
- 2. Side-effect import in `container.ts` (lines 20-63 — currently 44 of these)
18
- 3. Type import + re-export in `container.ts` (lines 65-148)
19
- 4. `NodeFeatures` interface entry in `container.ts` (lines 170-215)
20
-
21
- Steps 2-4 exist because registration is **disconnected from the class**. If the class declared itself, the container wiring could be derived.
22
-
23
- ## The Ruby Inspiration
24
-
25
- In Ruby, when you write `class Post < ActiveRecord::Base`, the `inherited` hook fires automatically:
26
-
27
- ```ruby
28
- class ActiveRecord::Base
29
- def self.inherited(subclass)
30
- subclass.table_name = subclass.name.tableize
31
- subclass.establish_connection
32
- # The parent configures the child at definition time
33
- end
34
- end
35
-
36
- class Post < ActiveRecord::Base
37
- end
38
- # That's it. Full ORM-backed model. No registration call needed.
39
- ```
40
-
41
- The magic: `inherited` fires at **class definition time**, receives the subclass, and lets the parent **reach into the child and set it up**. The empty class that does everything.
42
-
43
- ## Current Luca Registration Pattern
44
-
45
- Today, every helper requires explicit registration at the bottom of the file:
46
-
47
- ```typescript
48
- // src/node/features/fs.ts
49
- import { features, Feature } from "../feature.js"
50
-
51
- export class FS extends Feature {
52
- static override shortcut = "features.fs" as const
53
- static override stateSchema = FeatureStateSchema
54
- static override optionsSchema = FeatureOptionsSchema
55
-
56
- // ... methods ...
57
- }
58
-
59
- export default features.register("fs", FS)
60
- ```
61
-
62
- And for extension features (AGI, etc), there's the `attach` pattern:
63
-
64
- ```typescript
65
- export class Assistant extends Feature<AssistantState, AssistantOptions> {
66
- static attach(container: Container<AvailableFeatures, any>) {
67
- features.register('assistant', Assistant)
68
- return container
69
- }
70
- }
71
-
72
- export default features.register('assistant', Assistant)
73
- ```
74
-
75
- The registration is always a separate, imperative call disconnected from the class definition itself. The class doesn't know it's been registered. The registry doesn't know until that line runs.
76
-
77
- ### What the registry actually does
78
-
79
- The core `Registry.register()` method (`luca/src/registry.ts:65-70`) does three things:
80
- 1. Stores the constructor in a private `members` Map
81
- 2. Calls `interceptRegistration()` for introspection metadata capture
82
- 3. Emits a `'helperRegistered'` event on the registry bus
83
-
84
- This is straightforward enough that the base class can own it entirely.
85
-
86
- ## The Static Block Approach
87
-
88
- ES2022 static initialization blocks let code run at **class definition time**, inside the class body. This is JavaScript's closest equivalent to Ruby's `inherited`:
89
-
90
- ```typescript
91
- class Feature {
92
- static registry: Registry
93
-
94
- // This is our "inherited" hook
95
- static __initSubclass(SubClass: typeof Feature, id: string) {
96
- this.registry.register(id, SubClass)
97
- }
98
- }
99
- ```
100
-
101
- ### What it could look like for authors
102
-
103
- ```typescript
104
- // The dream: define, register, and export in one declaration
105
- export default class FS extends Feature {
106
- static {
107
- Feature.register(this, "fs")
108
- }
109
-
110
- static override shortcut = "features.fs" as const
111
- static override stateSchema = FeatureStateSchema
112
- static override optionsSchema = FeatureOptionsSchema
113
-
114
- async readFile(path: string) { /* ... */ }
115
- }
116
- ```
117
-
118
- No trailing `features.register()` call. No disconnect between the class and its registration. The class **declares itself** as a registered member of the system.
119
-
120
- ### The id could be derived from convention
121
-
122
- If we adopt a naming convention (like ActiveRecord derives table names from class names), we could eliminate the explicit id entirely:
123
-
124
- ```typescript
125
- export default class DiskCache extends Feature {
126
- static {
127
- Feature.register(this) // id inferred as "diskCache" from class name
128
- }
129
- }
130
- ```
131
-
132
- The `register` implementation:
133
-
134
- ```typescript
135
- class Feature {
136
- static register(SubClass: typeof Feature, id?: string) {
137
- // Convention: PascalCase class name -> camelCase registry id
138
- const registryId = id ?? SubClass.name[0].toLowerCase() + SubClass.name.slice(1)
139
- features.register(registryId, SubClass)
140
- }
141
- }
142
- ```
143
-
144
- ## Going Further: The Base Class Does All The Work
145
-
146
- What if the base classes (`Feature`, `Client`, `Server`) provided a static `register` that also handled module augmentation hints and the `attach` pattern?
147
-
148
- ```typescript
149
- // Before: 15+ lines of boilerplate per feature
150
- import { z } from 'zod'
151
- import { features, Feature } from '@soederpop/luca/feature'
152
- import type { AvailableFeatures } from '@soederpop/luca/feature'
153
-
154
- declare module '@soederpop/luca/feature' {
155
- interface AvailableFeatures {
156
- conversation: typeof Conversation
157
- }
158
- }
159
-
160
- export class Conversation extends Feature<ConversationState, ConversationOptions> {
161
- static override shortcut = 'features.conversation' as const
162
- static attach(container: Container<AvailableFeatures, any>) {
163
- features.register('conversation', Conversation)
164
- return container
165
- }
166
- // ...
167
- }
168
-
169
- export default features.register('conversation', Conversation)
170
- ```
171
-
172
- ```typescript
173
- // After: the class IS the registration
174
- import { Feature } from '@soederpop/luca/feature'
175
-
176
- export default class Conversation extends Feature<ConversationState, ConversationOptions> {
177
- static {
178
- Feature.register(this, 'conversation')
179
- }
180
-
181
- // shortcut derived automatically from registry + id
182
- // attach() provided by base class using the registered id
183
- // ...
184
- }
185
- ```
186
-
187
- The base `Feature.register()` could:
188
- 1. Call `features.register(id, SubClass)`
189
- 2. Set `SubClass.shortcut` automatically (`features.${id}`)
190
- 3. Generate a default `attach()` that registers and returns the container
191
- 4. Emit a `"subclass:registered"` event on the registry for any other wiring
192
-
193
- ## Codebase Reality Check
194
-
195
- **Current scale of the problem** (from `luca/src/`):
196
- - 44 feature files, each with a trailing `features.register()` call
197
- - 44 side-effect imports in `node/container.ts` lines 20-63
198
- - 44 type imports in `node/container.ts` lines 65-108
199
- - 44 entries in the `NodeFeatures` interface at lines 170-215
200
- - ~23 client files with `clients.register()` calls
201
- - 3 server files with `servers.register()` calls
202
- - 0 files currently using static initialization blocks
203
-
204
- **The `attach()` pattern is mostly ceremonial**: most `attach()` methods in features just call `register()` and return the container. The base classes (`Client`, `Server`, `Command`, `Endpoint`) all have their own `attach()` at the helper-type level (`luca/src/client.ts:41-69`, `luca/src/server.ts:49-73`, etc.) which wire up the registry and factory onto the container. Individual helpers rarely need custom `attach()` logic.
205
-
206
- **The 4-step checklist creates coupling**: every new feature requires editing `container.ts` in three separate places. This is a maintenance burden and a contributor friction point — exactly the kind of thing that needs solving before 1.0.
207
-
208
- ## Connection to Related Ideas
209
-
210
- This refactor sits at the intersection of several exploring ideas:
211
-
212
- - **[Feature Stacks](./feature-stacks.md)**: Stacks group features into lazy-loaded bundles. If features self-register via static blocks, stacks become simpler — a stack is just an async module that imports a set of feature files, and each feature registers itself on import. No stack-level registration wiring needed.
213
-
214
- - **[Container `.use()` API](./container-use-api.md)**: The `.use()` API loads stacks/plugins via dynamic import. Static block registration means `container.use(import('./my-feature'))` just works — the import triggers the static block, which calls `Feature.register(this)`, and the feature is available. No `attach()` ceremony.
215
-
216
- - **[Feature Authoring DX](./luca-feature-authoring-dx.md)**: Complementary, not competing. `defineFeature()` is for simple bag-of-methods features; static block registration is for full class-based helpers. Both reduce boilerplate, both feed into the "one file, one command" authoring goal.
217
-
218
- - **[Introspection Enhancement](./introspection-enhancement.md)**: `interceptRegistration()` already fires during `register()`. If registration moves into the class body, introspection metadata can be co-located too — the static block could declare description, category, and other metadata that introspection currently scrapes from JSDoc.
219
-
220
- ## Comparison With Other Approaches
221
-
222
- | Approach | When it runs | Cooperation needed | Ceremony |
223
- |----------|-------------|-------------------|----------|
224
- | Ruby `inherited` | Class definition | None | Zero — just `extends` |
225
- | Static block + `register` | Class definition | One line in static block | Minimal — inside the class |
226
- | Trailing `registry.register()` | Module evaluation | Separate call after class | Moderate — disconnected |
227
- | `defineFeature()` factory | Module evaluation | Wrap entire definition | Different paradigm entirely |
228
- | Decorators (`@tracked`) | Class definition | One line above class | Minimal — but external |
229
-
230
- The static block approach is the sweet spot for Luca: it's **standard JavaScript**, runs at **definition time**, lives **inside the class body**, and keeps the class as the primary authoring unit (unlike `defineFeature()` which replaces the class with a factory call).
231
-
232
- ## The `defineFeature` Relationship
233
-
234
- This is complementary to the [`defineFeature()` idea](./luca-feature-authoring-dx.md), not a replacement. They solve different problems:
235
-
236
- - **`defineFeature()`** reduces boilerplate for simple features that are mostly a bag of methods — you skip writing a class entirely
237
- - **Static block registration** reduces boilerplate for full class-based helpers — you still write the class, but registration is self-contained
238
-
239
- For complex features that need class inheritance, lifecycle hooks, and full OOP — the static block pattern is cleaner. For simple features that are essentially a named collection of functions — `defineFeature()` is cleaner.
240
-
241
- ## Implementation Sketch
242
-
243
- ### Phase 1: Add `Helper.register()` to the base class
244
-
245
- Each helper base class (`Feature`, `Client`, `Server`) gets a static `register()` that dispatches to its registry:
246
-
247
- ```typescript
248
- // In feature.ts
249
- class Feature {
250
- static register(SubClass: typeof Feature, id?: string) {
251
- const registryId = id ?? SubClass.name[0].toLowerCase() + SubClass.name.slice(1)
252
-
253
- // 1. Register in the features registry
254
- features.register(registryId, SubClass as any)
255
-
256
- // 2. Auto-set shortcut if not overridden
257
- if (!Object.getOwnPropertyDescriptor(SubClass, 'shortcut')?.value) {
258
- ;(SubClass as any).shortcut = `features.${registryId}` as const
259
- }
260
-
261
- // 3. Generate default attach() if not overridden
262
- if (!Object.getOwnPropertyDescriptor(SubClass, 'attach')) {
263
- ;(SubClass as any).attach = (container: any) => {
264
- features.register(registryId, SubClass as any)
265
- return container
266
- }
267
- }
268
- }
269
- }
270
- ```
271
-
272
- This is backward-compatible — existing `features.register()` calls still work. New features can opt into the static block pattern.
273
-
274
- ### Phase 2: Migrate one feature as proof of concept
275
-
276
- Pick a simple feature like `dns` or `yaml` and convert it:
277
-
278
- ```typescript
279
- // Before (dns.ts)
280
- export class DNS extends Feature { /* ... */ }
281
- export default features.register("dns", DNS)
282
-
283
- // After (dns.ts)
284
- export default class DNS extends Feature {
285
- static { Feature.register(this, "dns") }
286
- /* ... */
287
- }
288
- ```
289
-
290
- ### Phase 3: Codegen for container.ts wiring
291
-
292
- Build a `luca gen:features` command that scans `src/node/features/` and generates the side-effect imports, type imports, and `NodeFeatures` interface entries. This eliminates steps 2-4 of the checklist entirely — the only manual step is writing the feature file itself.
293
-
294
- ### Phase 4: Gradual migration
295
-
296
- Convert remaining features at leisure. The old and new patterns coexist since `features.register()` is the underlying mechanism either way.
297
-
298
- ## Open Questions
299
-
300
- - ~~**Registry detection**~~: **Yes.** `Helper.register(this)` walks the prototype chain to find which base class (`Feature`, `Client`, `Server`) owns the registry, then dispatches automatically. Verified in Bun. The `register` call also accepts an options bag (`{ id, stateSchema, optionsSchema }`) so schemas are set on the class *before* the registry call fires — meaning `interceptRegistration` sees a fully-decorated class. Id inference from class name works for most cases; acronym-style names (DNS, REST, SSH) need an explicit `id` passed.
301
- - ~~**Module augmentation**~~: The `declare module` blocks are necessary boilerplate — they're the one piece that can't be eliminated by runtime mechanics. Codegen is off the table, and TS language service plugins only affect editor intellisense (not `tsc`), so they'd require non-standard toolchain hacks. Not worth pursuing. One `declare module` block per feature file, co-located with the class, is acceptable.
302
- - ~~**Import side effects**~~: Not worth changing right now. Registration on import is fine — the static block just makes it more visible and intentional. Lazy registration via `container.use()` is a separate concern for later.
303
- - ~~**`attach()` consolidation**~~: **Yes, do it.** `Helper.register()` should generate a default `attach()` that registers and returns the container. The vast majority of `attach()` methods are just that. The few that do more (wiring up other features) can still override.
304
- - ~~**Bun compatibility**~~: **Verified on Bun 1.2.15** — `this` inside `static { }` correctly binds to the subclass being defined, not the parent. Name inference from `SubClass.name`, coexistence with other static properties, and registration dispatch all work as expected. No issues found.