@inixiative/foundry-core 0.5.11 → 0.5.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inixiative/foundry-core",
3
- "version": "0.5.11",
3
+ "version": "0.5.12",
4
4
  "type": "module",
5
5
  "main": "src/index.ts",
6
6
  "types": "src/index.ts",
@@ -37,7 +37,7 @@ export interface HierarchyMatch {
37
37
  readonly owners?: readonly string[];
38
38
  /** The harness the composed prompt is for. */
39
39
  readonly harness?: readonly string[];
40
- /** The role composed for (e.g. "main", a Warden's agent id). */
40
+ /** The role composed for (e.g. a thread role, a Warden's agent id), or one of its base roles. */
41
41
  readonly roles?: readonly string[];
42
42
  /** The slot composed (e.g. "instructions", "domainKnowledge"). */
43
43
  readonly slots?: readonly string[];
@@ -124,6 +124,11 @@ export interface HierarchyLocation {
124
124
  readonly harnessLoads?: readonly string[];
125
125
  /** The role composed for; also `{role}` in paths. */
126
126
  readonly role?: string;
127
+ /**
128
+ * Roles whose files come first: a source whose path names `{role}` is read once per base role, in
129
+ * order, then for `role` (e.g. a shared base role under each thread role).
130
+ */
131
+ readonly baseRoles?: readonly string[];
127
132
  /** The slot composed; also `{slot}` in paths. */
128
133
  readonly slot?: string;
129
134
  /**
@@ -221,7 +226,14 @@ function matches(when: HierarchyMatch | undefined, location: HierarchyLocation):
221
226
  if (when.projects && !(location.projectId && when.projects.includes(location.projectId)))
222
227
  return false;
223
228
  if (when.owners && !(location.owner && when.owners.includes(location.owner))) return false;
224
- if (when.roles && !(location.role && when.roles.includes(location.role))) return false;
229
+ // A base role counts as the role it sits under (a node for `main` serves every thread role).
230
+ if (
231
+ when.roles &&
232
+ ![...(location.baseRoles ?? []), ...(location.role ? [location.role] : [])].some((role) =>
233
+ when.roles!.includes(role),
234
+ )
235
+ )
236
+ return false;
225
237
  if (when.slots && !(location.slot && when.slots.includes(location.slot))) return false;
226
238
  if (when.paths && !matchedPath(when.paths, location)) return false;
227
239
  return true;
@@ -480,6 +492,22 @@ export async function discoverHierarchy(
480
492
  ) => {
481
493
  if (!matches(source.when, location) || (source.id && loadedNatively.has(source.id))) return;
482
494
  if (!slotted && !source.when?.slots && !unslottedApplies(entry.node, location)) return;
495
+ const templates = typeof source.path === 'string' ? [source.path] : source.path;
496
+ const roles =
497
+ location.baseRoles?.length && templates.some((path) => path.includes('{role}'))
498
+ ? [...new Set([...location.baseRoles, ...(location.role ? [location.role] : [])])]
499
+ : undefined;
500
+ for (const pass of roles ? roles.map((role) => ({ ...vars, role })) : [vars])
501
+ await collect(entry, source, position, base, pass);
502
+ };
503
+
504
+ const collect = async (
505
+ entry: SelectedHierarchyNode,
506
+ source: HierarchyFileSource,
507
+ position: number,
508
+ base: string | undefined,
509
+ vars: Readonly<Record<string, string | undefined>>,
510
+ ) => {
483
511
  const place = (path: string) => (isAbsolute(path) ? path : base ? join(base, path) : undefined);
484
512
  const literal = (template: string) => {
485
513
  const expanded = expandHierarchyPath(template, vars, location.home);
@@ -42,7 +42,31 @@ export type StreamEvent =
42
42
  message: string;
43
43
  severity: 'error' | 'warn';
44
44
  timestamp: number;
45
- };
45
+ }
46
+ | ({ kind: 'activity'; timestamp: number } & SystemActivity);
47
+
48
+ export type ActivityEvent = Extract<StreamEvent, { kind: 'activity' }>;
49
+
50
+ /**
51
+ * What the system is doing around a thread, not in it (Aron, 2026-11-11): a project's Wardens
52
+ * starting, a decision process restarting, a rate limit. The viewer keeps the latest per `key`
53
+ * and shows it above the composer; `error` events stay for what failed and needs the person.
54
+ */
55
+ export interface SystemActivity {
56
+ /** One thing that changes over time ("project-wardens:<projectId>"); a later event replaces it. */
57
+ readonly key: string;
58
+ readonly source: string;
59
+ /** The project it concerns; absent when it touches every thread (a login's rate limit). */
60
+ readonly projectId?: string;
61
+ /** `idle` is kept for the log only; the others show, in the status colours. */
62
+ readonly state: 'working' | 'ready' | 'degraded' | 'failed' | 'idle';
63
+ /** Shown until then unless a later event of the same key replaces it (a rate limit's end). */
64
+ readonly until?: number;
65
+ /** One plain sentence for the person. */
66
+ readonly summary: string;
67
+ /** The technical line, for the expanded log. */
68
+ readonly detail: string;
69
+ }
46
70
 
47
71
  /**
48
72
  * Unified event stream across all threads and sessions.
@@ -56,6 +80,8 @@ export type StreamEvent =
56
80
  export class EventStream {
57
81
  private _listeners: Array<(event: StreamEvent) => void> = [];
58
82
  private _history: StreamEvent[] = [];
83
+ /** The latest activity of each key, kept past the history bound: a failure stays until it clears. */
84
+ private _activities = new Map<string, ActivityEvent>();
59
85
  private _maxHistory: number;
60
86
 
61
87
  constructor(maxHistory: number = 5000) {
@@ -64,6 +90,10 @@ export class EventStream {
64
90
 
65
91
  /** Push an event into the stream. */
66
92
  push(event: StreamEvent): void {
93
+ if (event.kind === 'activity') {
94
+ this._activities.delete(event.key);
95
+ this._activities.set(event.key, event);
96
+ }
67
97
  this._history.push(event);
68
98
  if (this._history.length > this._maxHistory) {
69
99
  this._history.shift();
@@ -117,8 +147,19 @@ export class EventStream {
117
147
  this.push({ kind: 'error', source, message, severity, timestamp: Date.now() });
118
148
  }
119
149
 
150
+ /** Push what the system is doing (shown above the composer, never as a toast). */
151
+ pushActivity(activity: SystemActivity): void {
152
+ this.push({ kind: 'activity', ...activity, timestamp: Date.now() });
153
+ }
154
+
155
+ /** The latest activity of every key, oldest first. */
156
+ activities(): ActivityEvent[] {
157
+ return [...this._activities.values()];
158
+ }
159
+
120
160
  /** Clear history. */
121
161
  clear(): void {
122
162
  this._history = [];
163
+ this._activities.clear();
123
164
  }
124
165
  }
package/src/index.ts CHANGED
@@ -166,7 +166,13 @@ export {
166
166
  verifyExpertDelivery,
167
167
  } from './delivery-evidence';
168
168
  // Observability
169
- export { EventStream, type SessionEvent, type StreamEvent } from './event-stream';
169
+ export {
170
+ type ActivityEvent,
171
+ EventStream,
172
+ type SessionEvent,
173
+ type StreamEvent,
174
+ type SystemActivity,
175
+ } from './event-stream';
170
176
  export {
171
177
  type ExecuteHandler,
172
178
  type ExecuteMeta,
@@ -245,7 +245,7 @@ export interface ModelSweepOption {
245
245
  label: string;
246
246
  }
247
247
 
248
- export const MODEL_REGISTRY_UPDATED_AT = '2026-09-24';
248
+ export const MODEL_REGISTRY_UPDATED_AT = '2026-11-11';
249
249
 
250
250
  /**
251
251
  * Decision default: Claude Haiku 5.5, the cheapest current Claude model tagged `judgment`. Held here
@@ -292,7 +292,7 @@ export const MODEL_REGISTRY: Record<string, FoundryProviderInfo> = {
292
292
  models: [
293
293
  {
294
294
  id: 'fable',
295
- label: 'Fable 5.1',
295
+ label: 'Fable 5.2',
296
296
  tier: 'powerful',
297
297
  costTier: 'high',
298
298
  contextWindow: 1_000_000,
@@ -301,7 +301,7 @@ export const MODEL_REGISTRY: Record<string, FoundryProviderInfo> = {
301
301
  nativeAlias: true,
302
302
  capabilities: POWERFUL_REASONING,
303
303
  reasoning: CLAUDE_REASONING,
304
- notes: 'Claude Code alias for claude-fable-5-1.',
304
+ notes: 'Claude Code alias for claude-fable-5-2.',
305
305
  },
306
306
  {
307
307
  id: 'opus',
@@ -348,6 +348,17 @@ export const MODEL_REGISTRY: Record<string, FoundryProviderInfo> = {
348
348
  capabilities: POWERFUL_REASONING,
349
349
  reasoning: CLAUDE_REASONING,
350
350
  },
351
+ {
352
+ id: 'claude-fable-5-2',
353
+ label: 'Fable 5.2',
354
+ tier: 'powerful',
355
+ costTier: 'high',
356
+ contextWindow: 1_000_000,
357
+ maxOutputTokens: 128_000,
358
+ runtimeKind: 'native-harness',
359
+ capabilities: POWERFUL_REASONING,
360
+ reasoning: CLAUDE_REASONING,
361
+ },
351
362
  {
352
363
  id: 'claude-fable-5-1',
353
364
  label: 'Fable 5.1',
@@ -382,6 +393,18 @@ export const MODEL_REGISTRY: Record<string, FoundryProviderInfo> = {
382
393
  credential: 'api-key',
383
394
  enabledByDefault: true,
384
395
  models: [
396
+ {
397
+ id: 'claude-fable-5-2',
398
+ label: 'Claude Fable 5.2',
399
+ tier: 'powerful',
400
+ costTier: 'high',
401
+ contextWindow: 1_000_000,
402
+ maxOutputTokens: 128_000,
403
+ runtimeKind: 'api',
404
+ capabilities: POWERFUL_REASONING,
405
+ reasoning: CLAUDE_REASONING,
406
+ notes: 'Thinking is always on; steer with effort.',
407
+ },
385
408
  {
386
409
  id: 'claude-fable-5-1',
387
410
  label: 'Claude Fable 5.1',
@@ -416,6 +439,17 @@ export const MODEL_REGISTRY: Record<string, FoundryProviderInfo> = {
416
439
  capabilities: POWERFUL_REASONING,
417
440
  reasoning: CLAUDE_REASONING,
418
441
  },
442
+ {
443
+ id: 'claude-sonnet-5-5',
444
+ label: 'Claude Sonnet 5.5',
445
+ tier: 'standard',
446
+ costTier: 'medium',
447
+ contextWindow: 1_000_000,
448
+ maxOutputTokens: 128_000,
449
+ runtimeKind: 'api',
450
+ capabilities: STANDARD_REASONING,
451
+ reasoning: CLAUDE_REASONING,
452
+ },
419
453
  {
420
454
  id: 'claude-sonnet-5',
421
455
  label: 'Claude Sonnet 5',
package/src/scope.ts CHANGED
@@ -16,6 +16,10 @@ export interface OwnershipScope {
16
16
  readonly projectId?: string;
17
17
  /** Where the thread's native session runs. Not ownership: sources that read the workspace resolve against it. */
18
18
  readonly cwd?: string;
19
+ /** The thread role it composes as; absent: its worker's default. Not ownership. */
20
+ readonly role?: string;
21
+ /** The worker running its main session. Not ownership. */
22
+ readonly worker?: string;
19
23
  }
20
24
 
21
25
  /** Copy a scope, dropping undefined fields so equality checks are stable. */
package/src/thread.ts CHANGED
@@ -282,6 +282,9 @@ export interface ThreadMeta {
282
282
  /** The harness running this thread's main session (`claude-code`, `codex`). Set at creation; never changed silently. */
283
283
  worker?: string;
284
284
 
285
+ /** The thread role its instructions compose as (`coder`, `assistant`, …). Absent: its worker's default role. */
286
+ role?: string;
287
+
285
288
  /** When this thread was created. */
286
289
  readonly createdAt: number;
287
290
 
@@ -388,6 +391,8 @@ export interface ThreadConfig {
388
391
  projectId?: string;
389
392
  /** The harness running this thread's main session. */
390
393
  worker?: string;
394
+ /** The thread role its instructions compose as. */
395
+ role?: string;
391
396
  /** Optional token tracker — auto-records costs for every dispatch. */
392
397
  tokenTracker?: TokenTracker;
393
398
  }
@@ -441,6 +446,7 @@ export class Thread {
441
446
  ...(opts?.forkedFrom ? { forkedFrom: opts.forkedFrom } : {}),
442
447
  projectId: opts?.projectId,
443
448
  ...(opts?.worker ? { worker: opts.worker } : {}),
449
+ ...(opts?.role ? { role: opts.role } : {}),
444
450
  createdAt: now,
445
451
  lastActiveAt: now,
446
452
  };
@@ -219,18 +219,19 @@ export interface UsageSummary {
219
219
 
220
220
  // ---------------------------------------------------------------------------
221
221
  // Default cost table. Anthropic: first-party API list prices per 1M tokens, as published at
222
- // https://platform.claude.com/docs/en/about-claude/pricing (read 2026-11-01; cache writes are the
222
+ // https://platform.claude.com/docs/en/about-claude/pricing (read 2026-11-11; cache writes are the
223
223
  // 5-minute rate, 1.25x input). Haiku 5.5 is its rate up to 100K-token prompts.
224
224
  // ---------------------------------------------------------------------------
225
225
 
226
226
  export const DEFAULT_COST_TABLE: CostTable = {
227
227
  anthropic: {
228
+ 'claude-fable-5-2': { inputPer1M: 8, outputPer1M: 40, cacheReadPer1M: 0.2 },
228
229
  'claude-fable-5-1': { inputPer1M: 10, outputPer1M: 50, cacheReadPer1M: 0.25 },
229
230
  'claude-fable-5': { inputPer1M: 10, outputPer1M: 50 },
230
231
  'claude-opus-5-5': { inputPer1M: 4, outputPer1M: 20, cacheReadPer1M: 0.2 },
231
232
  'claude-opus-5': { inputPer1M: 5, outputPer1M: 25 },
232
233
  'claude-opus-4-8': { inputPer1M: 5, outputPer1M: 25 },
233
- 'claude-sonnet-5-5': { inputPer1M: 2, outputPer1M: 10, cacheReadPer1M: 0.2 },
234
+ 'claude-sonnet-5-5': { inputPer1M: 2, outputPer1M: 10, cacheReadPer1M: 0.1 },
234
235
  'claude-sonnet-5': { inputPer1M: 2, outputPer1M: 10 },
235
236
  'claude-haiku-5-5': { inputPer1M: 0.1, outputPer1M: 0.5 },
236
237
  'claude-haiku-4-5': { inputPer1M: 1, outputPer1M: 5 },