@inixiative/foundry-core 0.4.0 → 0.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.
@@ -0,0 +1,366 @@
1
+ import { createHash } from 'node:crypto';
2
+
3
+ // The Prospector's correction detection (moved from Oracle's session miner), shared by Oracle's
4
+ // `oracle mine`, Foundry's live feedback capture and `foundry repeats`. Data and pure functions only:
5
+ // callers bring the transcript turns and the model call.
6
+
7
+ /** Why a message became a candidate, strongest first. */
8
+ export type CorrectionSignal = 'repeat' | 'explicit' | 'interruption' | 'opener';
9
+
10
+ /** One turn of a transcript, whatever harness wrote it. */
11
+ export interface TranscriptTurn {
12
+ role: 'user' | 'assistant';
13
+ /** The harness's record id for the turn. */
14
+ id?: string;
15
+ /** ISO time, or '' when the record has none. */
16
+ timestamp: string;
17
+ /** What the person typed (user) or the agent's prose (assistant). */
18
+ text: string;
19
+ /** The person interrupted the agent just before this turn. */
20
+ interrupted?: boolean;
21
+ /** Where the turn sits in its source, e.g. `line:42`. */
22
+ sourceRef?: string;
23
+ }
24
+
25
+ export interface CorrectionEvent {
26
+ /** Stable identity: `<sessionId>:<messageId>`, or a content digest when the record has no id. */
27
+ readonly id: string;
28
+ readonly sessionId: string;
29
+ readonly messageId?: string;
30
+ readonly sourceRef?: string;
31
+ readonly timestamp: string;
32
+ /** What the agent said right before the correction ('' at the start of a session). */
33
+ readonly before: string;
34
+ /** The person's message. */
35
+ readonly correction: string;
36
+ /** The strongest signal, and every signal that fired. */
37
+ readonly signal: CorrectionSignal;
38
+ readonly signals: readonly CorrectionSignal[];
39
+ /** The matched phrase of the strongest pattern signal. */
40
+ readonly phrase?: string;
41
+ /** 0–1 rank among candidates. */
42
+ readonly score: number;
43
+ }
44
+
45
+ export interface CorrectionDetection {
46
+ hit: boolean;
47
+ signal?: CorrectionSignal;
48
+ signals: CorrectionSignal[];
49
+ phrase?: string;
50
+ score: number;
51
+ }
52
+
53
+ /** Data, so every caller filters with exactly these. Tuned on a 19.8k-message scan against an Opus read. */
54
+ export const CORRECTION_PATTERNS: Readonly<
55
+ Record<Exclude<CorrectionSignal, 'interruption'>, RegExp>
56
+ > = {
57
+ /** The person says this already failed. A bare "again" is handled by `AGAIN_CORRECTION`, not here. */
58
+ repeat:
59
+ /\b(?:i thought we|didn'?t we|did(?:n'?t| not) (?:i|we) (?:say|tell|ask|agree|decide)|i (?:already )?(?:told|asked) you|once and for all|how many times|(?:keeps?|kept) (?:doing|adding|making|asking|forgetting|happening)|i keep (?:saying|telling|having|asking)|same (?:mistake|lessons?)|as i said|like i said|we(?:'ve| have) (?:talked|been over|discussed|decided)|over and over|who keeps|still (?:seeing|doing|using|adding)|gentle reminder|reminder:)/i,
60
+ /** A standing instruction, a challenge, a reuse hint or a verdict on what the agent did. */
61
+ explicit:
62
+ /\b(?:from now on|going forward|in the future|always|never|remember (?:that|to|this)|make sure (?:you|to|that)|stop \w+ing|do not|don'?t (?:ever|make|add|do|use|ask|touch|create|build|write|need|be|put|bother)|please (?:don'?t|use|run|just)|you (?:should|shouldn'?t|need to|have to|must|can (?:just|always)|don'?t (?:need|have) to)|we (?:should(?:n'?t)?|don'?t|never|always)|should (?:just|not|never|always)|shouldn'?t|no need to|just (?:merge|use|do|call|fix|throw|delete|go|run|ship)|stop asking|why (?:would|did|are|do) (?:you|u)|why not just|why can'?t we just|get rid of|(?:is|are)n'?t needed|(?:does|do)n'?t make sense|i (?:really )?don'?t like|weird pattern|we (?:already )?have (?:a |an |the )?(?:native |existing |shared )?\w+(?: \w+)? (?:for|that|utils?|helpers?)|isn'?t there (?:already )?a|already (?:exists|have|has)|use (?:the )?(?:existing|internal|shared)|follow (?:what|how) \w+ (?:is doing|does)|are (?:you|u) assuming|did (?:you|u) (?:actually|read|check|test|run|verify)|i don'?t trust|have (?:you|u) (?:restarted|tested|run|checked|verified)|in both repos|backport|backprop|use (?:the )?logger)\b/i,
63
+ /** A message that opens by pushing back (the Prospector's corrective openers). */
64
+ opener:
65
+ /^\s*(?:no\b|no,|nope|don'?t|dont\b|stop\b|wait\b|wrong\b|not what|that'?s not|thats not|actually,?\s|instead\b|undo\b|revert\b|why (?:did|would|are|is|do) (?:you|u)|why not|what the (?:fuck|hell)|wtf|that'?s wrong|you'?re wrong|i said)/i,
66
+ };
67
+
68
+ /**
69
+ * "Again" marks a repeat only beside a negation or a challenge in the same sentence ("again, we
70
+ * shouldn't…", "not again"). Retries ("try it again", "run again") and filler don't count.
71
+ */
72
+ export const AGAIN_CORRECTION = {
73
+ word: /\bagain\b/i,
74
+ retry:
75
+ /\b(?:try|run|do|check|show|send|ask|say|test|start|restart|look|read|load|open|build|deploy|push|pull|merge|call|post|go)(?: (?:it|that|this|them|me|the \w+))? again\b|^\W*again\W*$/i,
76
+ opening: /^\W*(?:no,?\s+)?again\s*[,:;—-]\s*\S/i,
77
+ context:
78
+ /\b(?:don'?t|do not|not|never|shouldn'?t|stop|why|wrong|told|said|no longer|keep|still|same|we'?re|you'?re|you)\b/i,
79
+ } as const;
80
+
81
+ /** What a signal says, most telling first: the `signal` a caller acts on (a repeat can block). */
82
+ const MEANING: readonly CorrectionSignal[] = ['repeat', 'explicit', 'interruption', 'opener'];
83
+ /** Measured precision per signal, for `score`: openers hold best, interruptions worst. */
84
+ const WEIGHT: Record<CorrectionSignal, number> = {
85
+ opener: 0.8,
86
+ repeat: 0.7,
87
+ explicit: 0.6,
88
+ interruption: 0.5,
89
+ };
90
+ /** The agent ended on a question, so a bare "no" answers it rather than corrects it. */
91
+ const ASKED = /\?\s*[)*`]*\s*$/;
92
+ const INTERRUPTED = /^\s*\[Request interrupted by user/;
93
+ const INTERRUPTED_MARKER = /^\s*\[Request interrupted by user[^\]]*\]\s*/;
94
+
95
+ /** The person's own prose: pasted code, quotes and log lines are someone else's words. */
96
+ export function proseOf(text: string): string {
97
+ return text
98
+ .replace(/```[\s\S]*?(?:```|$)/g, ' ')
99
+ .split('\n')
100
+ .filter(
101
+ (line) =>
102
+ !/^\s*>/.test(line) &&
103
+ !/^\s*(?:\[?\d{4}-\d{2}-\d{2}[T ]\d|\d{1,2}:\d{2}(?::\d{2})?\s|[\w.-]+:\d+(?::\d+)?\s)/.test(
104
+ line,
105
+ ),
106
+ )
107
+ .join('\n');
108
+ }
109
+
110
+ function againPhrase(prose: string): string | undefined {
111
+ for (const sentence of prose.split(/(?<=[.!?\n])\s+/)) {
112
+ if (!AGAIN_CORRECTION.word.test(sentence) || AGAIN_CORRECTION.retry.test(sentence)) continue;
113
+ // "Again, merge it yourself." opens with the marker: a repeat whatever follows.
114
+ if (AGAIN_CORRECTION.opening.test(sentence)) return 'again';
115
+ if (AGAIN_CORRECTION.context.test(sentence.replace(AGAIN_CORRECTION.word, ''))) return 'again';
116
+ }
117
+ return undefined;
118
+ }
119
+
120
+ export function detectCorrection(
121
+ text: string,
122
+ before = '',
123
+ options: { interrupted?: boolean } = {},
124
+ ): CorrectionDetection {
125
+ const prose = proseOf(text);
126
+ const found = new Map<CorrectionSignal, string | undefined>();
127
+ for (const signal of ['opener', 'repeat', 'explicit'] as const) {
128
+ const match = CORRECTION_PATTERNS[signal].exec(prose);
129
+ if (match) found.set(signal, match[0].trim());
130
+ }
131
+ const again = againPhrase(prose);
132
+ if (again && !found.has('repeat')) found.set('repeat', again);
133
+ if (options.interrupted) found.set('interruption', undefined);
134
+ const signals = MEANING.filter((signal) => found.has(signal));
135
+ const [strongest] = signals;
136
+ if (!strongest) return { hit: false, signals, score: 0 };
137
+ let score = Math.max(...signals.map((signal) => WEIGHT[signal])) + 0.1 * (signals.length - 1);
138
+ if (signals.length === 1 && strongest === 'opener' && ASKED.test(before)) score -= 0.4;
139
+ const phrase = found.get(strongest) ?? signals.map((signal) => found.get(signal)).find(Boolean);
140
+ return {
141
+ hit: true,
142
+ signal: strongest,
143
+ signals,
144
+ ...(phrase ? { phrase } : {}),
145
+ score: Math.round(Math.min(1, score) * 100) / 100,
146
+ };
147
+ }
148
+
149
+ export function correctionId(
150
+ sessionId: string,
151
+ event: Pick<CorrectionEvent, 'messageId' | 'timestamp' | 'before' | 'correction'>,
152
+ ): string {
153
+ if (event.messageId) return `${sessionId}:${event.messageId}`;
154
+ const digest = createHash('sha256')
155
+ .update(`${event.timestamp}\0${event.before}\0${event.correction}`)
156
+ .digest('hex')
157
+ .slice(0, 16);
158
+ return `${sessionId}:sha256-${digest}`;
159
+ }
160
+
161
+ /** Correction candidates in one session's turns, secrets scrubbed. */
162
+ export function extractCorrections(
163
+ sessionId: string,
164
+ turns: Iterable<TranscriptTurn>,
165
+ options: { minScore?: number } = {},
166
+ ): { corrections: CorrectionEvent[]; secretsScrubbed: number } {
167
+ const minScore = options.minScore ?? 0;
168
+ const corrections: CorrectionEvent[] = [];
169
+ let before = '';
170
+ let pendingInterruption = false;
171
+ let secretsScrubbed = 0;
172
+ // An identical message resent within ten minutes is one correction, not a repeat.
173
+ const recent = new Map<string, number>();
174
+ for (const turn of turns) {
175
+ if (turn.role === 'assistant') {
176
+ if (turn.text.trim()) before = turn.text;
177
+ continue;
178
+ }
179
+ if (turn.interrupted || INTERRUPTED.test(turn.text)) pendingInterruption = true;
180
+ const text = turn.text.replace(INTERRUPTED_MARKER, '');
181
+ if (!text.trim()) continue;
182
+ const detection = detectCorrection(text, before, { interrupted: pendingInterruption });
183
+ pendingInterruption = false;
184
+ const key = text.toLowerCase().replace(/\s+/g, ' ').trim();
185
+ const at = Date.parse(turn.timestamp);
186
+ const sent = recent.get(key);
187
+ recent.set(key, at);
188
+ if (sent !== undefined && Number.isFinite(at) && at - sent < 600_000) continue;
189
+ if (!detection.hit || !detection.signal || detection.score < minScore) continue;
190
+ // Scrub before truncating, so a cut can't leave part of a secret unmatched.
191
+ const scrubbedBefore = scrubSecrets(before);
192
+ const scrubbedText = scrubSecrets(text.trim());
193
+ secretsScrubbed += scrubbedBefore.hits + scrubbedText.hits;
194
+ const event = {
195
+ ...(turn.id ? { messageId: turn.id } : {}),
196
+ ...(turn.sourceRef ? { sourceRef: turn.sourceRef } : {}),
197
+ timestamp: turn.timestamp,
198
+ before: scrubbedBefore.text.slice(-2000),
199
+ correction: scrubbedText.text.slice(0, 4000),
200
+ signal: detection.signal,
201
+ signals: detection.signals,
202
+ ...(detection.phrase ? { phrase: detection.phrase } : {}),
203
+ score: detection.score,
204
+ };
205
+ corrections.push({ id: correctionId(sessionId, event), sessionId, ...event });
206
+ }
207
+ return { corrections, secretsScrubbed };
208
+ }
209
+
210
+ const SECRET_PATTERNS: ReadonlyArray<readonly [RegExp, string]> = [
211
+ [/sk-ant-[A-Za-z0-9_-]{8,}/g, '[REDACTED:anthropic-key]'],
212
+ [/sk-[A-Za-z0-9_-]{20,}/g, '[REDACTED:api-key]'],
213
+ [/gh[pousr]_[A-Za-z0-9]{20,}/g, '[REDACTED:github-token]'],
214
+ [/github_pat_[A-Za-z0-9_]{20,}/g, '[REDACTED:github-token]'],
215
+ [/AIza[A-Za-z0-9_-]{30,}/g, '[REDACTED:google-key]'],
216
+ [/xox[baprs]-[A-Za-z0-9-]{10,}/g, '[REDACTED:slack-token]'],
217
+ [/AKIA[A-Z0-9]{16}/g, '[REDACTED:aws-key]'],
218
+ [
219
+ /-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z ]*PRIVATE KEY-----/g,
220
+ '[REDACTED:private-key]',
221
+ ],
222
+ [/eyJ[A-Za-z0-9_-]{10,}\.eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}/g, '[REDACTED:jwt]'],
223
+ [/\b[a-z]+_(?:live|test|prod|runtime)_[A-Za-z0-9]{16,}/g, '[REDACTED:token]'],
224
+ [/\b(?:npm_|glpat-|hf_)[A-Za-z0-9_-]{20,}/g, '[REDACTED:token]'],
225
+ [/hooks\.slack\.com\/services\/[A-Za-z0-9/]+/g, '[REDACTED:slack-webhook]'],
226
+ [/\bASIA[A-Z0-9]{16}/g, '[REDACTED:aws-key]'],
227
+ [/\b(Bearer|Basic|DPoP)\s+[A-Za-z0-9._~+/=-]{16,}/g, '$1 [REDACTED:credential]'],
228
+ [/\b([a-z][a-z0-9+.-]*:\/\/[^\s:/@]+:)[^\s@/]+@/gi, '$1[REDACTED:password]@'],
229
+ [
230
+ /\b([A-Z][A-Z0-9_]*(?:KEY|SECRET|TOKEN|PASSWORD|PASSWD)[A-Z0-9_]*\s*[=:]\s*)["']?[^\s"']{8,}["']?/g,
231
+ '$1[REDACTED:value]',
232
+ ],
233
+ [
234
+ /(["']?)\b(password|passwd|api[_-]?key|secret|(?:access|auth|refresh)?[_-]?token)\1(\s*[:=]\s*)["']?[^\s"',}]{8,}["']?/gi,
235
+ '$1$2$1$3[REDACTED:value]',
236
+ ],
237
+ ];
238
+
239
+ /** Replace credential-shaped substrings. Transcripts are exactly where leaked keys live. */
240
+ export function scrubSecrets(text: string): { text: string; hits: number } {
241
+ let hits = 0;
242
+ let out = text;
243
+ for (const [pattern, replacement] of SECRET_PATTERNS)
244
+ out = out.replace(pattern, (...match: unknown[]) => {
245
+ hits++;
246
+ return replacement.replace(/\$(\d)/g, (_, group: string) => {
247
+ const value = match[Number(group)];
248
+ return typeof value === 'string' ? value : '';
249
+ });
250
+ });
251
+ return { text: out, hits };
252
+ }
253
+
254
+ /** FEEDBACK-LOG's tags plus process: the closed list feedback items and repeat rules are tagged with. */
255
+ export const FEEDBACK_TAGS = [
256
+ 'hygiene',
257
+ 'naming',
258
+ 'merge',
259
+ 'reuse',
260
+ 'overbuild',
261
+ 'architecture',
262
+ 'data',
263
+ 'product',
264
+ 'coordination',
265
+ 'communication',
266
+ 'tooling',
267
+ 'security',
268
+ 'process',
269
+ ] as const;
270
+ export type FeedbackTag = (typeof FEEDBACK_TAGS)[number];
271
+
272
+ export const CORRECTION_CATEGORIES = ['approach', 'scope', 'convention', 'fact', 'other'] as const;
273
+ export type CorrectionCategory = (typeof CORRECTION_CATEGORIES)[number];
274
+
275
+ export interface CorrectionClassification {
276
+ isCorrection: boolean;
277
+ category: CorrectionCategory;
278
+ /** Imperative rules, each at most 160 characters and saying when it applies; empty when none. */
279
+ statements: string[];
280
+ /** False when it is a correction but no crisp rule can be stated. */
281
+ sharp: boolean;
282
+ }
283
+
284
+ export const CORRECTION_CLASSIFY_PROMPT = `You classify messages a person typed to an AI coding agent. Each message comes with the agent's previous message, and each is judged on its own.
285
+
286
+ A CORRECTION is feedback about how the agent works: the person redirects, fixes or rejects what the agent did, or states a rule or preference the agent should keep following beyond this task. These are NOT corrections: a new task or question, an answer to the agent's question, approving or reviewing one specific item, design discussion, status chatter.
287
+
288
+ For a correction, write each rule it carries as a statement: one imperative sentence of at most 160 characters that says when it applies ("When X, do Y." or "Never X."), general enough to hold next time, in plain words without quoting the message. At most three statements per message. Use names (products, tools, terms) only when the rule is about them.
289
+
290
+ Respond with JSON only, one result per message id:
291
+ { "results": { "<id>": { "isCorrection": boolean, "category": "approach" | "scope" | "convention" | "fact" | "other", "statements": string[], "sharp": boolean } } }
292
+ category: approach = how to do the work; scope = what to do or not do; convention = naming, style, process or tooling rules; fact = something about the world or the codebase the agent got wrong; other = anything else.
293
+ sharp: false when it is a correction but no crisp rule can be stated; then statements is empty.`;
294
+
295
+ /** The stable start of every classify input: providers that prime a session reuse it (`stablePrefix`). */
296
+ export const CORRECTION_CLASSIFY_PREFIX = 'Classify each message below.\n\n';
297
+
298
+ /** The messages every caller sends for a batch of candidates (one is a batch of one). Ids are M1..Mn. */
299
+ export function correctionClassifyMessages(
300
+ events: readonly Pick<CorrectionEvent, 'before' | 'correction'>[],
301
+ limits: { before?: number; correction?: number } = {},
302
+ ): { role: 'system' | 'user'; content: string }[] {
303
+ const before = limits.before ?? 1200;
304
+ const correction = limits.correction ?? 3000;
305
+ const body = events
306
+ .map(
307
+ (event, index) =>
308
+ `## M${index + 1}\n### Agent's prior message\n${event.before.slice(-before) || '(start of session)'}\n\n### Person's message\n${event.correction.slice(0, correction)}`,
309
+ )
310
+ .join('\n\n');
311
+ return [
312
+ { role: 'system', content: CORRECTION_CLASSIFY_PROMPT },
313
+ { role: 'user', content: `${CORRECTION_CLASSIFY_PREFIX}${body}` },
314
+ ];
315
+ }
316
+
317
+ function classification(value: unknown): CorrectionClassification | undefined {
318
+ if (!value || typeof value !== 'object') return undefined;
319
+ const item = value as Record<string, unknown>;
320
+ if (typeof item.isCorrection !== 'boolean') return undefined;
321
+ const category = CORRECTION_CATEGORIES.find((entry) => entry === item.category) ?? 'other';
322
+ const statements = Array.isArray(item.statements)
323
+ ? item.statements
324
+ .filter((entry): entry is string => typeof entry === 'string' && entry.trim().length > 0)
325
+ .map((entry) => entry.trim().slice(0, 160))
326
+ .slice(0, 3)
327
+ : [];
328
+ if (!item.isCorrection) return { isCorrection: false, category, statements: [], sharp: false };
329
+ const sharp = item.sharp !== false && statements.length > 0;
330
+ return { isCorrection: true, category, statements: sharp ? statements : [], sharp };
331
+ }
332
+
333
+ /** The first balanced `{...}` in a model answer, so prose or braces after it don't break parsing. */
334
+ function firstJsonObject(content: string): string | undefined {
335
+ const start = content.indexOf('{');
336
+ if (start < 0) return undefined;
337
+ let depth = 0;
338
+ let quoted = false;
339
+ for (let i = start; i < content.length; i++) {
340
+ const char = content[i];
341
+ if (quoted) {
342
+ if (char === '\\') i++;
343
+ else if (char === '"') quoted = false;
344
+ } else if (char === '"') quoted = true;
345
+ else if (char === '{') depth++;
346
+ else if (char === '}' && --depth === 0) return content.slice(start, i + 1);
347
+ }
348
+ return undefined;
349
+ }
350
+
351
+ /** One answer per message of the batch, in order; undefined where the answer is missing or malformed. */
352
+ export function parseCorrectionClassifications(
353
+ content: string,
354
+ count: number,
355
+ ): (CorrectionClassification | undefined)[] {
356
+ let raw: unknown;
357
+ try {
358
+ raw = JSON.parse(firstJsonObject(content) ?? '');
359
+ } catch {
360
+ return Array.from({ length: count }, () => undefined);
361
+ }
362
+ const results =
363
+ raw && typeof raw === 'object' ? (raw as { results?: unknown }).results : undefined;
364
+ const byId = results && typeof results === 'object' ? (results as Record<string, unknown>) : {};
365
+ return Array.from({ length: count }, (_, index) => classification(byId[`M${index + 1}`]));
366
+ }
package/src/harness.ts CHANGED
@@ -137,6 +137,13 @@ interface SendOptions {
137
137
  onDelta?: (text: string) => void;
138
138
  /** Final trace for this attempt, independent of the bounded history cache. */
139
139
  onTrace?: (trace: Trace) => void;
140
+ /**
141
+ * The executor has prepared its input after every middleware ran and is handing it to its
142
+ * provider. Fires once per message, before any provider call.
143
+ */
144
+ onExecutorInput?: () => void;
145
+ /** The payload the execute stage's agent receives instead of `message.payload` (see DispatchOptions.agentPayload). */
146
+ executorPayload?: unknown;
140
147
  }
141
148
 
142
149
  /**
@@ -480,6 +487,7 @@ export class Harness {
480
487
  const requestedAgents = new Set<string>();
481
488
  const requestedLayers = new Set<string>();
482
489
  const stageResults = new Map<string, ExecutionResult>();
490
+ let executorInput = false;
483
491
 
484
492
  let classification: Decision<Classification> | undefined;
485
493
  let route: Decision<Route> | undefined;
@@ -655,11 +663,19 @@ export class Harness {
655
663
  // classifier/router/enricher output isn't user-visible content.
656
664
  const result = await this.thread.dispatch(agentId, stagePayload, layerFilter, {
657
665
  onDelta: stage.role === 'execute' ? opts?.onDelta : undefined,
666
+ requestLayer: (layerId) => requestedLayers.add(layerId),
658
667
  role: stage.role,
659
668
  messageId: message.id,
660
669
  invocation: stage.invocation,
670
+ ...(stage.role === 'execute' && opts?.executorPayload !== undefined
671
+ ? { agentPayload: opts.executorPayload }
672
+ : {}),
661
673
  recordInjection: (artifact) => {
662
674
  stageSpan.annotations.injection = artifact;
675
+ if (stage.role === 'execute' && !executorInput) {
676
+ executorInput = true;
677
+ opts?.onExecutorInput?.();
678
+ }
663
679
  },
664
680
  nativeObservation: opts?.nativeObservation,
665
681
  recordNative: (evidence) => {
package/src/index.ts CHANGED
@@ -33,7 +33,45 @@ export {
33
33
  validateMemorySelection,
34
34
  } from './adapters/file-memory';
35
35
  export { HttpMemory } from './adapters/http-memory';
36
- export { claudemdSource, MarkdownDocs } from './adapters/markdown-docs';
36
+ export {
37
+ claudemdSource,
38
+ formatInstructionFiles,
39
+ type InstructionFile,
40
+ type InstructionReadOptions,
41
+ instructionImports,
42
+ MarkdownDocs,
43
+ readInstructionFile,
44
+ resolveInstructionImport,
45
+ secretInstructionPath,
46
+ stripInstructionComments,
47
+ withinAny,
48
+ withinPath,
49
+ } from './adapters/markdown-docs';
50
+ export {
51
+ dedupeHierarchy,
52
+ dedupeHierarchySlots,
53
+ discoverHierarchy,
54
+ expandHierarchyPath,
55
+ type HierarchyDedupeOptions,
56
+ type HierarchyDiscoverOptions,
57
+ type HierarchyDuplicate,
58
+ type HierarchyDuplicateKind,
59
+ type HierarchyFile,
60
+ type HierarchyFileSource,
61
+ type HierarchyLocation,
62
+ type HierarchyMatch,
63
+ type HierarchyNode,
64
+ type HierarchyRefusal,
65
+ type HierarchySlotDuplicate,
66
+ type HierarchySource,
67
+ type HierarchySpan,
68
+ type HierarchyStack,
69
+ type HierarchyWalkSource,
70
+ hierarchyFingerprint,
71
+ markdownSections,
72
+ type SelectedHierarchyNode,
73
+ selectHierarchyNodes,
74
+ } from './adapters/markdown-hierarchy';
37
75
  export { type SqliteEntry, SqliteMemory } from './adapters/sqlite-memory';
38
76
  // Agent primitives
39
77
  export {
@@ -60,6 +98,7 @@ export {
60
98
  type GateContext,
61
99
  type PermissionLevel,
62
100
  type PermissionPolicy,
101
+ permissionLevel,
63
102
  RESTRICTED_POLICY,
64
103
  } from './capability';
65
104
  export {
@@ -122,6 +161,29 @@ export {
122
161
  // Observability
123
162
  export { EventStream, type SessionEvent, type StreamEvent } from './event-stream';
124
163
  export { type ExecuteHandler, type ExecuteMeta, Executor, type ExecutorConfig } from './executor';
164
+ // Feedback: correction detection (the Prospector's), shared by Oracle, live capture and `foundry repeats`
165
+ export {
166
+ AGAIN_CORRECTION,
167
+ CORRECTION_CATEGORIES,
168
+ CORRECTION_CLASSIFY_PREFIX,
169
+ CORRECTION_CLASSIFY_PROMPT,
170
+ CORRECTION_PATTERNS,
171
+ type CorrectionCategory,
172
+ type CorrectionClassification,
173
+ type CorrectionDetection,
174
+ type CorrectionEvent,
175
+ type CorrectionSignal,
176
+ correctionClassifyMessages,
177
+ correctionId,
178
+ detectCorrection,
179
+ extractCorrections,
180
+ FEEDBACK_TAGS,
181
+ type FeedbackTag,
182
+ parseCorrectionClassifications,
183
+ proseOf,
184
+ scrubSecrets,
185
+ type TranscriptTurn,
186
+ } from './feedback/correction';
125
187
  export {
126
188
  type FlowConfig,
127
189
  type FlowStage,
@@ -132,7 +194,6 @@ export {
132
194
  type PipelineStep,
133
195
  type RequestContext,
134
196
  } from './harness';
135
-
136
197
  // Lifecycle Hooks (registry only — built-in hooks in @inixiative/foundry)
137
198
  export {
138
199
  type HookContext,
@@ -205,20 +266,23 @@ export {
205
266
  type RuntimeKind,
206
267
  registryModel,
207
268
  resolveDecisionModel,
269
+ SUBSCRIPTION_DECISION_MODELS,
208
270
  } from './model-registry';
209
271
  export * from './native-evidence';
210
- // Permission middleware
211
- export {
212
- type PermissionCheck,
213
- type PermissionCheckResult,
214
- type PermissionMiddlewareConfig,
215
- permissionMiddleware,
216
- } from './permission-middleware';
217
272
  // Retry middleware
218
273
  export { type RetryConfig, retryMiddleware } from './retry';
219
274
  export { type Route, type RouteHandler, Router, type RouterConfig } from './router';
220
275
  // Ownership scope — which thread/project owns a source read or tool call
221
276
  export { normalizeScope, type OwnershipScope } from './scope';
277
+ export {
278
+ type DeliveredPart,
279
+ layerPart,
280
+ type SessionDelivery,
281
+ type SessionDeliveryReport,
282
+ type StandingPart,
283
+ standingPart,
284
+ standingParts,
285
+ } from './session-delivery';
222
286
  // Signals
223
287
  export { type Signal, SignalBus, type SignalHandler, type SignalKind } from './signal';
224
288
  // Orchestration
package/src/messages.ts CHANGED
@@ -96,7 +96,7 @@ export interface DeliveryRecord {
96
96
  readonly assessedHash?: string;
97
97
  /** Owner revision the decorator assessed, for owner-versioned layers. */
98
98
  readonly assessedRevision?: number;
99
- /** Hash the executor assembled and sent. */
99
+ /** Hash the executor assembled; a session provider sends only what `session` lists. */
100
100
  readonly deliveredHash: string;
101
101
  /** Owner revision of the bytes actually assembled, from the layer's own version mark at assembly. */
102
102
  readonly deliveredRevision?: number;
@@ -110,6 +110,8 @@ export interface DeliveryRecord {
110
110
  }>;
111
111
  /** Layer ids committed to the delivery ledger. */
112
112
  readonly committed: string[];
113
+ /** What a session provider delivered (its base context and this turn's sent parts), when one ran. */
114
+ readonly session?: import('./session-delivery').SessionDeliveryReport;
113
115
  /**
114
116
  * How the turn treated learning still pending from earlier work:
115
117
  * `pending` means preparation continued immediately with committed knowledge.
@@ -143,6 +145,10 @@ export interface InjectionArtifact {
143
145
  readonly executorContext?: string;
144
146
  /** Initial provider messages, before native-runtime wrapping or tool followups. */
145
147
  readonly providerMessages?: LLMMessage[];
148
+ /** What a session provider delivered: its system prompt parts and the parts sent with this turn. */
149
+ readonly sessionDelivery?: import('./session-delivery').SessionDeliveryReport;
150
+ /** Every standing part the executor assembled (layer with its prompt), by id and hash. */
151
+ readonly standingParts?: Array<import('./session-delivery').DeliveredPart>;
146
152
  readonly layers?: Array<
147
153
  LayerInstanceState & {
148
154
  id: string;
package/src/middleware.ts CHANGED
@@ -18,6 +18,11 @@ export interface DispatchContext<TPayload = unknown> {
18
18
  readonly messageId?: string;
19
19
  /** Identity of this dispatch; tool observations for it carry the same id. */
20
20
  readonly dispatchId?: string;
21
+ /**
22
+ * Include an on-demand layer in this dispatch's context (the routing plan asked for it). Present
23
+ * when a Harness stage dispatched it; layers of other activation modes are unaffected.
24
+ */
25
+ readonly requestLayer?: (layerId: string) => void;
21
26
  }
22
27
 
23
28
  export interface DispatchOutcome<TResult = unknown> {
@@ -225,11 +225,17 @@ export interface ModelSweepOption {
225
225
  export const MODEL_REGISTRY_UPDATED_AT = '2026-09-24';
226
226
 
227
227
  /**
228
- * Decision default: the cheapest current model tagged `judgment`. Held here
229
- * rather than in an adapter so one map answers "what runs decisions".
228
+ * Decision default: Claude Haiku 5.5, the cheapest current Claude model tagged `judgment`. Held here
229
+ * rather than in an adapter so one map answers "what runs decisions". DECISION_PROVIDER is the
230
+ * API-token path; subscription decisions pick their model per runtime from SUBSCRIPTION_DECISION_MODELS.
230
231
  */
231
- export const DECISION_PROVIDER = 'openai';
232
- export const DECISION_MODEL = 'gpt-6-luna';
232
+ export const DECISION_PROVIDER = 'anthropic';
233
+ export const DECISION_MODEL = 'claude-haiku-5-5';
234
+ /** Subscription decision default per native runtime; settings choose the runtime. */
235
+ export const SUBSCRIPTION_DECISION_MODELS = {
236
+ claude: DECISION_MODEL,
237
+ codex: 'gpt-6-luna',
238
+ } as const;
233
239
 
234
240
  /** Tag rule: every model judges. Cheap models review and route; capable models execute and advise. */
235
241
  const FAST = ['judgment', 'learning-review', 'tool-use'] as ModelCapability[];
@@ -270,7 +276,7 @@ export const MODEL_REGISTRY: Record<string, FoundryProviderInfo> = {
270
276
  },
271
277
  {
272
278
  id: 'opus',
273
- label: 'Opus 5',
279
+ label: 'Opus 5.5',
274
280
  tier: 'powerful',
275
281
  costTier: 'high',
276
282
  contextWindow: 1_000_000,
@@ -278,10 +284,11 @@ export const MODEL_REGISTRY: Record<string, FoundryProviderInfo> = {
278
284
  nativeAlias: true,
279
285
  capabilities: POWERFUL_REASONING,
280
286
  reasoning: CLAUDE_REASONING,
287
+ notes: 'Claude Code alias for claude-opus-5-5.',
281
288
  },
282
289
  {
283
290
  id: 'sonnet',
284
- label: 'Sonnet 5',
291
+ label: 'Sonnet 5.5',
285
292
  tier: 'standard',
286
293
  costTier: 'medium',
287
294
  contextWindow: 1_000_000,
@@ -289,6 +296,39 @@ export const MODEL_REGISTRY: Record<string, FoundryProviderInfo> = {
289
296
  nativeAlias: true,
290
297
  capabilities: STANDARD_REASONING,
291
298
  reasoning: CLAUDE_REASONING,
299
+ notes: 'Claude Code alias for claude-sonnet-5-5.',
300
+ },
301
+ {
302
+ id: 'claude-haiku-5-5',
303
+ label: 'Haiku 5.5',
304
+ tier: 'fast',
305
+ costTier: 'low',
306
+ contextWindow: 1_000_000,
307
+ runtimeKind: 'native-harness',
308
+ capabilities: FAST_REASONING,
309
+ reasoning: { ...CLAUDE_REASONING, efforts: ['low', 'medium', 'high'] },
310
+ notes: "Foundry's decision model; the CLI reports this id as the served model.",
311
+ },
312
+ {
313
+ id: 'claude-opus-5-5',
314
+ label: 'Opus 5.5',
315
+ tier: 'powerful',
316
+ costTier: 'high',
317
+ contextWindow: 1_000_000,
318
+ runtimeKind: 'native-harness',
319
+ capabilities: POWERFUL_REASONING,
320
+ reasoning: CLAUDE_REASONING,
321
+ },
322
+ {
323
+ id: 'claude-fable-5-1',
324
+ label: 'Fable 5.1',
325
+ tier: 'powerful',
326
+ costTier: 'high',
327
+ contextWindow: 1_000_000,
328
+ maxOutputTokens: 128_000,
329
+ runtimeKind: 'native-harness',
330
+ capabilities: POWERFUL_REASONING,
331
+ reasoning: CLAUDE_REASONING,
292
332
  },
293
333
  {
294
334
  id: 'haiku',
@@ -325,6 +365,17 @@ export const MODEL_REGISTRY: Record<string, FoundryProviderInfo> = {
325
365
  reasoning: CLAUDE_REASONING,
326
366
  notes: 'Thinking is always on; steer with effort.',
327
367
  },
368
+ {
369
+ id: 'claude-opus-5-5',
370
+ label: 'Claude Opus 5.5',
371
+ tier: 'powerful',
372
+ costTier: 'high',
373
+ contextWindow: 1_000_000,
374
+ maxOutputTokens: 128_000,
375
+ runtimeKind: 'api',
376
+ capabilities: POWERFUL_REASONING,
377
+ reasoning: CLAUDE_REASONING,
378
+ },
328
379
  {
329
380
  id: 'claude-opus-5',
330
381
  label: 'Claude Opus 5',