fraim 2.0.359 → 2.0.360-next.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.
@@ -0,0 +1,206 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.withRuleDeliveryTimeout = withRuleDeliveryTimeout;
4
+ exports.assembleDomainRules = assembleDomainRules;
5
+ exports.attachRuleAssembly = attachRuleAssembly;
6
+ const crypto_1 = require("crypto");
7
+ const catalog_1 = require("../../../registry/scripts/domains/catalog");
8
+ const inheritance_parser_1 = require("./inheritance-parser");
9
+ class RuleFetchTimeout extends Error {
10
+ }
11
+ class RuleSourceFault extends Error {
12
+ constructor(path, resolved) {
13
+ super('Rule source unavailable');
14
+ this.path = path;
15
+ this.resolved = resolved;
16
+ }
17
+ }
18
+ async function withRuleDeliveryTimeout(operation, timeoutMs = 5000) {
19
+ let timer;
20
+ try {
21
+ return await Promise.race([operation, new Promise((_, reject) => {
22
+ timer = setTimeout(() => reject(new RuleFetchTimeout()), timeoutMs);
23
+ })]);
24
+ }
25
+ finally {
26
+ if (timer)
27
+ clearTimeout(timer);
28
+ }
29
+ }
30
+ /** Independent loads deliberately retain available policy when any sibling fails. */
31
+ async function assembleDomainRules(options) {
32
+ const timeoutMs = Math.max(1, Math.min(options.timeoutMs ?? 5000, 30000));
33
+ const metadata = {
34
+ domain: options.jobPath ? null : 'global', catalogVersion: null,
35
+ selection: options.jobPath ? 'unknown' : 'connect', artifacts: [], warnings: [], complete: false,
36
+ };
37
+ const warn = (code, path, message, source) => {
38
+ metadata.warnings.push({ code, path, ...(options.jobPath ? { jobPath: options.jobPath } : {}),
39
+ domain: metadata.domain, message,
40
+ ...(source ? { source: source.scope ?? source.source, hash: (0, crypto_1.createHash)('sha256').update(source.content).digest('hex') } : {}),
41
+ recovery: `Restore or sync ${path} in its effective source, then refresh the current job instructions. Record this incident and its recovery in bound job evidence.`,
42
+ resolved: false });
43
+ };
44
+ let paths = ['rules/constitution.md'];
45
+ // Connect has no job/domain selection. Its fixed constitution must not wait
46
+ // on an unrelated catalog read (or report a catalog failure as missing policy).
47
+ if (options.jobPath) {
48
+ try {
49
+ const resolved = await withRuleDeliveryTimeout(options.resolveFile('domains.json'), timeoutMs);
50
+ const value = JSON.parse(resolved.content);
51
+ if (!value || typeof value !== 'object' || value.schemaVersion !== 2
52
+ || !value.domains || typeof value.domains !== 'object'
53
+ || Array.isArray(value.domains))
54
+ throw new Error('Invalid domain catalog');
55
+ const catalog = value;
56
+ metadata.catalogVersion = Number.isInteger(catalog.mapVersion) && catalog.mapVersion >= 3 ? catalog.mapVersion : null;
57
+ Object.assign(metadata, (0, catalog_1.selectCatalogDomain)(catalog, options.jobPath));
58
+ if ((0, catalog_1.validateDomainCatalog)(value).length)
59
+ warn('CATALOG_INVALID', 'domains.json', 'Some domain configuration is invalid; retain the constitution and available safe rule paths.', resolved);
60
+ for (const warning of resolved.resolutionWarnings ?? [])
61
+ warn('CATALOG_SOURCE_FALLBACK', 'domains.json', warning, resolved);
62
+ for (const domain of new Set(options.jobPath ? ['global', ...(metadata.domain ? [metadata.domain] : [])] : [])) {
63
+ const rules = catalog.domains[domain]?.rules;
64
+ if (Array.isArray(rules))
65
+ paths.push(...rules.filter(path => (0, catalog_1.isSafeRegistryPath)(path, 'rules')));
66
+ }
67
+ if (metadata.selection === 'unknown')
68
+ warn('DOMAIN_UNKNOWN', 'domains.json', 'The job folder has no configured domain; configure rules for that folder in domains.json.');
69
+ }
70
+ catch (error) {
71
+ warn(error instanceof RuleFetchTimeout ? 'CATALOG_FETCH_TIMEOUT' : 'CATALOG_UNAVAILABLE', 'domains.json', 'The effective domain catalog is unavailable or invalid; independently load the constitution.');
72
+ }
73
+ }
74
+ const resolveRule = async (path, ancestors = [], rootPath = path, deadline = Date.now() + timeoutMs) => {
75
+ if (!(0, catalog_1.isSafeRegistryPath)(path, 'rules') || ancestors.includes(path) || ancestors.length >= 10)
76
+ throw new RuleSourceFault(path);
77
+ let resolved;
78
+ try {
79
+ resolved = await withRuleDeliveryTimeout(options.resolveFile(path), Math.max(1, deadline - Date.now()));
80
+ }
81
+ catch (error) {
82
+ if (error instanceof RuleFetchTimeout)
83
+ throw error;
84
+ throw new RuleSourceFault(path);
85
+ }
86
+ if (!resolved.content.trim())
87
+ throw new RuleSourceFault(path, resolved);
88
+ if (resolved.content.includes('\u0000') || resolved.content.includes('\ufffd'))
89
+ throw new RuleSourceFault(path, resolved);
90
+ const frontmatter = resolved.content.match(/^---\r?\n([\s\S]+?)\r?\n---/);
91
+ if (frontmatter?.[1].trim().startsWith('{')) {
92
+ try {
93
+ JSON.parse(frontmatter[1]);
94
+ }
95
+ catch {
96
+ throw new RuleSourceFault(path, resolved);
97
+ }
98
+ }
99
+ if (new inheritance_parser_1.InheritanceParser().parse(resolved.content).hasImports)
100
+ throw new RuleSourceFault(path, resolved);
101
+ const includes = [...resolved.content.matchAll(/\{\{\s*include:\s*([^}]+)\}\}/g)];
102
+ // Reject malformed directives before traversing or certifying this source's
103
+ // children. Its caller can then retain valid sibling fragments independently.
104
+ const literalContent = resolved.content.replace(/\{\{\s*include:\s*([^}]+)\}\}/g, '');
105
+ if (includes.some(match => !match[1].trim()) || /\{\{\s*(?:include|import)\s*:/.test(literalContent)) {
106
+ throw new RuleSourceFault(path, resolved);
107
+ }
108
+ if (!includes.length)
109
+ return resolved;
110
+ const expanded = await Promise.all(includes.map(async (match) => {
111
+ const childPath = match[1].trim();
112
+ try {
113
+ const child = await resolveRule(childPath, [...ancestors, path], rootPath, deadline);
114
+ (metadata.includedArtifacts ??= []).push({ path: childPath, source: child.source,
115
+ ...(child.scope ? { scope: child.scope } : {}), inherited: child.inherited,
116
+ hash: (0, crypto_1.createHash)('sha256').update(child.content).digest('hex') });
117
+ for (const warning of child.resolutionWarnings ?? []) {
118
+ warn('RULE_SOURCE_FALLBACK', childPath, warning, child);
119
+ metadata.warnings[metadata.warnings.length - 1].affectedPath = rootPath;
120
+ }
121
+ return child.content;
122
+ }
123
+ catch (error) {
124
+ const fault = error instanceof RuleSourceFault ? error : undefined;
125
+ warn(error instanceof RuleFetchTimeout ? 'RULE_FETCH_TIMEOUT' : 'RULE_UNAVAILABLE', fault?.path ?? childPath, 'Included rule source is missing, empty, invalid, cyclic or unavailable; available sibling guidance remains in force.', fault?.resolved);
126
+ metadata.warnings[metadata.warnings.length - 1].affectedPath = rootPath;
127
+ return '';
128
+ }
129
+ }));
130
+ let index = 0;
131
+ const content = resolved.content.replace(/\{\{\s*include:\s*([^}]+)\}\}/g, () => expanded[index++]);
132
+ if (!content.trim())
133
+ throw new RuleSourceFault(path, resolved);
134
+ return { ...resolved, content };
135
+ };
136
+ const results = await Promise.all([...new Set(paths)].map(async (path) => {
137
+ let resolved;
138
+ try {
139
+ resolved = await resolveRule(path);
140
+ if (/\{\{\s*(?:include|import)\s*:/.test(resolved.content)) {
141
+ throw new Error('Unresolved rule source');
142
+ }
143
+ return { path, resolved };
144
+ }
145
+ catch (error) {
146
+ return { path, error, resolved };
147
+ }
148
+ }));
149
+ const bodies = [];
150
+ for (const result of results) {
151
+ if ('error' in result) {
152
+ const fault = result.error instanceof RuleSourceFault ? result.error : undefined;
153
+ warn(result.error instanceof RuleFetchTimeout ? 'RULE_FETCH_TIMEOUT' : 'RULE_UNAVAILABLE', fault?.path ?? result.path, 'Rule source is missing, empty, invalid, cyclic or unavailable; other available rules remain in force.', fault?.resolved ?? result.resolved);
154
+ if (fault && fault.path !== result.path)
155
+ metadata.warnings[metadata.warnings.length - 1].affectedPath = result.path;
156
+ continue;
157
+ }
158
+ const resolved = result.resolved;
159
+ metadata.artifacts.push({ path: result.path, source: resolved.source, ...(resolved.scope ? { scope: resolved.scope } : {}),
160
+ inherited: resolved.inherited, hash: (0, crypto_1.createHash)('sha256').update(resolved.content).digest('hex') });
161
+ for (const warning of resolved.resolutionWarnings ?? [])
162
+ warn('RULE_SOURCE_FALLBACK', result.path, warning, resolved);
163
+ bodies.push(`### ${result.path}\n\n${resolved.content}`);
164
+ }
165
+ metadata.complete = metadata.warnings.length === 0;
166
+ const warnings = metadata.warnings.map(warning => `- ${warning.code}: ${warning.message} ${warning.recovery}`);
167
+ return { text: ['## Applicable rules', ...bodies, ...(warnings.length ? ['### Rule delivery warnings', ...warnings] : [])].join('\n\n'), metadata };
168
+ }
169
+ /** Replace only the advertised rule block; keep phase content and warning history. */
170
+ function attachRuleAssembly(response, assembly) {
171
+ const content = [...(response.content ?? [])];
172
+ const previous = response.structuredContent?.ruleAssembly;
173
+ const warnings = [...assembly.metadata.warnings];
174
+ const incidentKey = (warning) => `${warning.code}:${warning.path}:${warning.hash ?? ''}`;
175
+ const priorWarnings = Array.isArray(previous?.warnings) ? previous.warnings : [];
176
+ const validWarning = (warning) => !!warning && typeof warning === 'object'
177
+ && ['code', 'path', 'message', 'recovery'].every(key => typeof warning[key] === 'string')
178
+ && typeof warning.resolved === 'boolean';
179
+ if (previous && (!Array.isArray(previous.warnings) || priorWarnings.some(warning => !validWarning(warning)))) {
180
+ warnings.push({ code: 'ASSEMBLY_METADATA_INVALID', path: 'ruleAssembly.warnings', domain: assembly.metadata.domain,
181
+ message: 'Prior rule delivery incident metadata is invalid; available rules remain in force.',
182
+ recovery: 'Refresh the current instructions and retain the invalid delivery incident in bound job evidence.', resolved: false });
183
+ }
184
+ for (const prior of priorWarnings.filter(validWarning)) {
185
+ if (warnings.some(warning => incidentKey(warning) === incidentKey(prior)))
186
+ continue;
187
+ const recovered = prior.path === 'domains.json'
188
+ ? assembly.metadata.catalogVersion !== null && assembly.metadata.selection !== 'unknown'
189
+ && !assembly.metadata.warnings.some(warning => warning.path === prior.path)
190
+ : (prior.affectedPath
191
+ ? assembly.metadata.artifacts.some(artifact => artifact.path === prior.affectedPath)
192
+ && (assembly.metadata.includedArtifacts ?? []).some(artifact => artifact.path === prior.path)
193
+ : assembly.metadata.artifacts.some(artifact => artifact.path === prior.path))
194
+ && !assembly.metadata.warnings.some(warning => warning.path === prior.path);
195
+ warnings.push({ ...prior, resolved: prior.resolved || recovered });
196
+ }
197
+ const previousIndex = previous?.contentBlockIndex;
198
+ const index = Number.isInteger(previousIndex) && previousIndex >= 0 && previousIndex < content.length ? previousIndex : content.length;
199
+ const retained = warnings.filter(warning => !assembly.metadata.warnings.includes(warning));
200
+ const history = retained.length ? '\n\n### Prior rule delivery incidents\n' + retained.map(warning => `- ${warning.code} (${warning.resolved ? 'recovered' : 'unresolved'}): ${warning.path}. ${warning.recovery}`).join('\n') : '';
201
+ content[index] = { type: 'text', text: assembly.text + history };
202
+ return { ...response, content, structuredContent: { ...response.structuredContent,
203
+ ruleAssembly: { ...assembly.metadata, warnings,
204
+ complete: assembly.metadata.complete && warnings.every(warning => warning.resolved),
205
+ contentBlockIndex: index } } };
206
+ }
@@ -46,6 +46,7 @@ const path_1 = require("path");
46
46
  const inheritance_parser_1 = require("./inheritance-parser");
47
47
  const job_parser_1 = require("./job-parser");
48
48
  const project_fraim_paths_1 = require("./project-fraim-paths");
49
+ const catalog_1 = require("../../../registry/scripts/domains/catalog");
49
50
  class LocalRegistryResolver {
50
51
  constructor(options) {
51
52
  this.workspaceRoot = options.workspaceRoot;
@@ -54,6 +55,18 @@ class LocalRegistryResolver {
54
55
  this.remoteContentResolver = options.remoteContentResolver;
55
56
  this.parser = new inheritance_parser_1.InheritanceParser(options.maxDepth);
56
57
  this.shouldFilter = options.shouldFilter;
58
+ if (options.onLocalDependency)
59
+ this.onLocalDependency = (...args) => {
60
+ if (this.dependencyFailure)
61
+ throw this.dependencyFailure.error;
62
+ try {
63
+ options.onLocalDependency(...args);
64
+ }
65
+ catch (error) {
66
+ this.dependencyFailure = { error };
67
+ throw error;
68
+ }
69
+ };
57
70
  // Issue #1002: default the manager's writable home to the configured contentRoot.
58
71
  // contentRoot = join(resolvePackHome('manager').root, 'personalized-employee') for all backends.
59
72
  this.managerLocalRoot = options.managerLocalRoot ?? (0, path_1.join)((0, project_fraim_paths_1.getUserFraimDirPath)(), 'personalized-employee');
@@ -82,12 +95,15 @@ class LocalRegistryResolver {
82
95
  if (this.effectiveFraimDir)
83
96
  return (0, path_1.join)(this.effectiveFraimDir, role, type);
84
97
  const machineRoot = (0, path_1.join)((0, project_fraim_paths_1.getUserFraimDirPath)(), role, type);
98
+ this.onLocalDependency?.(machineRoot, 'directory');
85
99
  return (0, fs_1.existsSync)(machineRoot) ? machineRoot : this.getFraimPath(role, type);
86
100
  }
87
101
  /**
88
102
  * Check if a local override exists for the given path
89
103
  */
90
104
  hasLocalOverride(path) {
105
+ if (!this.userLevelOnly)
106
+ this.onLocalDependency?.(this.getOverridePath(path), 'file');
91
107
  return !this.userLevelOnly && (0, fs_1.existsSync)(this.getOverridePath(path));
92
108
  }
93
109
  /** Whether an effective personalized layer should replace a remote baseline response. */
@@ -185,12 +201,20 @@ class LocalRegistryResolver {
185
201
  const normalizedPath = path.replace(/\\/g, '/').replace(/^\/+/, '');
186
202
  const parts = normalizedPath.split('/');
187
203
  const type = parts[0];
204
+ if (normalizedPath === 'domains.json') {
205
+ if (this.effectiveFraimDir)
206
+ return (0, path_1.join)(this.effectiveFraimDir, 'domains.json');
207
+ const machineCatalog = (0, path_1.join)((0, project_fraim_paths_1.getUserFraimDirPath)(), 'domains.json');
208
+ this.onLocalDependency?.(machineCatalog, 'file');
209
+ return (0, fs_1.existsSync)(machineCatalog) ? machineCatalog : this.getFraimPath('domains.json');
210
+ }
188
211
  if (type === 'jobs' || type === 'workflows') {
189
212
  if (parts.length >= 3 && (parts[1] === 'ai-employee' || parts[1] === 'ai-manager')) {
190
213
  return (0, path_1.join)(this.getSyncedCatalogRoot(parts[1], type), ...parts.slice(2));
191
214
  }
192
215
  for (const role of ['ai-employee', 'ai-manager']) {
193
216
  const candidate = (0, path_1.join)(this.getSyncedCatalogRoot(role, type), ...parts.slice(1));
217
+ this.onLocalDependency?.(candidate, 'file');
194
218
  if ((0, fs_1.existsSync)(candidate))
195
219
  return candidate;
196
220
  }
@@ -212,7 +236,10 @@ class LocalRegistryResolver {
212
236
  */
213
237
  readLocalOverride(path) {
214
238
  try {
215
- return (0, fs_1.readFileSync)(this.getOverridePath(path), 'utf-8');
239
+ const file = this.getOverridePath(path);
240
+ const content = (0, fs_1.readFileSync)(file, 'utf-8');
241
+ this.onLocalDependency?.(file, 'file', content);
242
+ return content;
216
243
  }
217
244
  catch (error) {
218
245
  throw new Error(`Failed to read local override: ${path}. ${error.message}`);
@@ -223,11 +250,14 @@ class LocalRegistryResolver {
223
250
  */
224
251
  readSyncedLocalFile(path) {
225
252
  const syncedPath = this.getSyncedFilePath(path);
253
+ if (syncedPath)
254
+ this.onLocalDependency?.(syncedPath, 'file');
226
255
  if (!syncedPath || !(0, fs_1.existsSync)(syncedPath)) {
227
256
  return null;
228
257
  }
229
258
  try {
230
259
  const content = (0, fs_1.readFileSync)(syncedPath, 'utf-8');
260
+ this.onLocalDependency?.(syncedPath, 'file', content);
231
261
  if (this.shouldFilter && this.shouldFilter(content)) {
232
262
  return null;
233
263
  }
@@ -251,15 +281,15 @@ class LocalRegistryResolver {
251
281
  * Security: same canonical-path containment guard as the other overlay
252
282
  * readers, so a crafted path can never escape the home directory.
253
283
  */
254
- readManagerLocalOverlayFile(path) {
255
- return this.readContainedOverlayFile(this.managerLocalRoot, path);
284
+ readManagerLocalOverlayFile(path, warnings) {
285
+ return this.readContainedOverlayFile(this.managerLocalRoot, path, warnings);
256
286
  }
257
287
  /**
258
288
  * Shared overlay read with a canonical-path containment guard. Extracted so
259
289
  * every overlay root uses one implementation of the guard rather than each
260
290
  * repeating it (the manager cache and org cache readers below delegate here).
261
291
  */
262
- readContainedOverlayFile(root, path) {
292
+ readContainedOverlayFile(root, path, warnings) {
263
293
  const normalized = path.replace(/\\/g, '/').replace(/^\/+/, '');
264
294
  const destination = (0, path_1.join)(root, ...normalized.split('/'));
265
295
  const resolved = (0, path_1.resolve)(destination);
@@ -267,15 +297,18 @@ class LocalRegistryResolver {
267
297
  if (!resolved.startsWith(rootResolved + path_1.sep) && resolved !== rootResolved) {
268
298
  return null;
269
299
  }
300
+ this.onLocalDependency?.(destination, 'file');
270
301
  if (!(0, fs_1.existsSync)(destination))
271
302
  return null;
272
303
  try {
273
304
  const content = (0, fs_1.readFileSync)(destination, 'utf-8');
305
+ this.onLocalDependency?.(destination, 'file', content);
274
306
  if (this.shouldFilter && this.shouldFilter(content))
275
307
  return null;
276
308
  return content;
277
309
  }
278
310
  catch {
311
+ warnings?.push('Manager local source could not be read; available lower-precedence content used.');
279
312
  return null;
280
313
  }
281
314
  }
@@ -287,7 +320,7 @@ class LocalRegistryResolver {
287
320
  * paths; we defensively resolve and check the canonical path stays inside
288
321
  * the cache root to guard against future path-traversal bugs in sync.
289
322
  */
290
- readManagerOverlayFile(path) {
323
+ readManagerOverlayFile(path, warnings) {
291
324
  const normalized = path.replace(/\\/g, '/').replace(/^\/+/, '');
292
325
  const destination = (0, path_1.join)(this.managerCacheRoot, ...normalized.split('/'));
293
326
  // Guard: resolved path must stay within the manager cache root.
@@ -296,15 +329,18 @@ class LocalRegistryResolver {
296
329
  if (!resolved.startsWith(cacheResolved + path_1.sep) && resolved !== cacheResolved) {
297
330
  return null;
298
331
  }
332
+ this.onLocalDependency?.(destination, 'file');
299
333
  if (!(0, fs_1.existsSync)(destination))
300
334
  return null;
301
335
  try {
302
336
  const content = (0, fs_1.readFileSync)(destination, 'utf-8');
337
+ this.onLocalDependency?.(destination, 'file', content);
303
338
  if (this.shouldFilter && this.shouldFilter(content))
304
339
  return null;
305
340
  return content;
306
341
  }
307
342
  catch {
343
+ warnings?.push('Manager source could not be read; available lower-precedence content used.');
308
344
  return null;
309
345
  }
310
346
  }
@@ -315,7 +351,7 @@ class LocalRegistryResolver {
315
351
  * Precedence: project > manager > org > synced baseline > registry > remote.
316
352
  * Security: same canonical-path containment guard as readManagerOverlayFile.
317
353
  */
318
- readOrgOverlayFile(path) {
354
+ readOrgOverlayFile(path, warnings) {
319
355
  const normalized = path.replace(/\\/g, '/').replace(/^\/+/, '');
320
356
  const destination = (0, path_1.join)(this.orgCacheRoot, ...normalized.split('/'));
321
357
  const resolved = (0, path_1.resolve)(destination);
@@ -323,15 +359,18 @@ class LocalRegistryResolver {
323
359
  if (!resolved.startsWith(cacheResolved + path_1.sep) && resolved !== cacheResolved) {
324
360
  return null;
325
361
  }
362
+ this.onLocalDependency?.(destination, 'file');
326
363
  if (!(0, fs_1.existsSync)(destination))
327
364
  return null;
328
365
  try {
329
366
  const content = (0, fs_1.readFileSync)(destination, 'utf-8');
367
+ this.onLocalDependency?.(destination, 'file', content);
330
368
  if (this.shouldFilter && this.shouldFilter(content))
331
369
  return null;
332
370
  return content;
333
371
  }
334
372
  catch {
373
+ warnings?.push('Organization source could not be read; available lower-precedence content used.');
335
374
  return null;
336
375
  }
337
376
  }
@@ -345,11 +384,13 @@ class LocalRegistryResolver {
345
384
  const rootResolved = (0, path_1.resolve)(registryRoot);
346
385
  if (!resolved.startsWith(rootResolved + path_1.sep) && resolved !== rootResolved)
347
386
  return null;
387
+ this.onLocalDependency?.(registryPath, 'file');
348
388
  if (!(0, fs_1.existsSync)(registryPath)) {
349
389
  return null;
350
390
  }
351
391
  try {
352
392
  const content = (0, fs_1.readFileSync)(registryPath, 'utf-8');
393
+ this.onLocalDependency?.(registryPath, 'file', content);
353
394
  if (this.shouldFilter && this.shouldFilter(content)) {
354
395
  return null;
355
396
  }
@@ -361,7 +402,7 @@ class LocalRegistryResolver {
361
402
  }
362
403
  normalizeRegistryPath(path) {
363
404
  const normalized = path.trim().replace(/\\/g, '/').replace(/^\/+/, '');
364
- return normalized.endsWith('.md') ? normalized : `${normalized}.md`;
405
+ return /\.(md|json)$/.test(normalized) ? normalized : `${normalized}.md`;
365
406
  }
366
407
  stripTypePrefix(path) {
367
408
  return path.replace(/^(jobs|workflows|skills|rules|templates)\//, '');
@@ -459,6 +500,8 @@ class LocalRegistryResolver {
459
500
  includeMetadata: false,
460
501
  stripMcpHeader: true
461
502
  }, false);
503
+ if (res.resolutionWarnings?.length)
504
+ throw new inheritance_parser_1.InheritanceError('Parent inheritance could not be resolved', targetPath);
462
505
  if (res.personalized)
463
506
  personalizedPaths.add(normalized);
464
507
  return res.content;
@@ -503,32 +546,104 @@ class LocalRegistryResolver {
503
546
  * @param path - Registry path (e.g., "workflows/product-building/spec.md")
504
547
  * @returns Resolved file with metadata
505
548
  */
549
+ async resolveDomainCatalog() {
550
+ const path = 'domains.json';
551
+ const localBaseline = this.readSyncedLocalFile(path) ?? this.readWorkspaceRegistryFile(path);
552
+ let content = localBaseline ?? await this.fetchRemoteWithFallback(path);
553
+ let source = localBaseline === null ? 'remote' : 'local';
554
+ let scope;
555
+ let catalog = JSON.parse(content);
556
+ const resolutionWarnings = [];
557
+ let projectContent = null;
558
+ if (this.hasLocalOverride(path)) {
559
+ try {
560
+ const candidate = this.readLocalOverride(path);
561
+ if (!this.shouldFilter || !this.shouldFilter(candidate))
562
+ projectContent = candidate;
563
+ }
564
+ catch {
565
+ resolutionWarnings.push('Project domain configuration could not be read; available earlier configuration retained.');
566
+ }
567
+ }
568
+ const layers = [
569
+ { content: this.readOrgOverlayFile(path, resolutionWarnings), scope: 'org' },
570
+ { content: this.readManagerOverlayFile(path, resolutionWarnings), scope: 'manager' },
571
+ ...(this.managerLocalRoot !== this.managerCacheRoot ? [{ content: this.readManagerLocalOverlayFile(path, resolutionWarnings), scope: 'manager' }] : []),
572
+ { content: projectContent, scope: 'project' },
573
+ ];
574
+ for (const layer of layers) {
575
+ if (layer.content === null)
576
+ continue;
577
+ try {
578
+ catalog = (0, catalog_1.mergeDomainCatalog)(catalog, JSON.parse(layer.content));
579
+ }
580
+ catch {
581
+ resolutionWarnings.push('Invalid ' + layer.scope + ' domain configuration; available earlier configuration retained.');
582
+ continue;
583
+ }
584
+ content = JSON.stringify(catalog);
585
+ source = 'local';
586
+ scope = layer.scope;
587
+ }
588
+ return { content, source, ...(scope ? { scope } : {}), personalized: !!scope, inherited: !!scope, ...(resolutionWarnings.length ? { resolutionWarnings } : {}) };
589
+ }
590
+ assertDependenciesObserved() {
591
+ if (this.dependencyFailure)
592
+ throw this.dependencyFailure.error;
593
+ }
506
594
  async resolveFile(path, options = {}) {
507
- return this.resolveFileInternal(path, options, true);
595
+ this.assertDependenciesObserved();
596
+ try {
597
+ return await this.resolveFileInternal(path, options, true);
598
+ }
599
+ finally {
600
+ this.assertDependenciesObserved();
601
+ }
508
602
  }
509
603
  async resolveFileInternal(path, options, applyInheritance) {
604
+ const warnings = [];
605
+ const resolved = await this.resolveFileWithWarnings(path, options, applyInheritance, warnings);
606
+ return warnings.length ? { ...resolved, resolutionWarnings: [...warnings, ...(resolved.resolutionWarnings ?? [])] } : resolved;
607
+ }
608
+ async resolveFileWithWarnings(path, options, applyInheritance, warnings) {
609
+ if (this.normalizeRegistryPath(path) === 'domains.json')
610
+ return this.resolveDomainCatalog();
510
611
  const includeMetadata = options.includeMetadata ?? true;
511
612
  const stripMcpHeader = options.stripMcpHeader ?? false;
512
613
  const resolveContent = (content, personalized = true) => applyInheritance
513
614
  ? this.resolveInheritance(content, path, personalized)
514
615
  : Promise.resolve({ content, imports: [] });
515
- const baselineResult = async (rawContent, source) => {
616
+ const baselineResult = async (rawContent, source, localPath) => {
516
617
  const content = stripMcpHeader ? this.stripMcpHeader(rawContent) : rawContent;
517
618
  const resolved = await resolveContent(content, false);
518
619
  return {
519
620
  content: resolved.content,
520
621
  source,
622
+ ...(localPath ? { localPath } : {}),
521
623
  personalized: false,
522
624
  inherited: resolved.imports.length > 0,
523
625
  imports: resolved.imports.length > 0 ? resolved.imports : undefined
524
626
  };
525
627
  };
526
- // Check for local override
527
- if (!this.hasLocalOverride(path)) {
628
+ // Unreadable or intentionally filtered Project content is unavailable,
629
+ // just like a missing override. Retain the normal precedence chain rather
630
+ // than skipping configured Manager/Organization and cached local sources.
631
+ let localContent = null;
632
+ if (this.hasLocalOverride(path)) {
633
+ try {
634
+ const candidate = this.readLocalOverride(path);
635
+ if (!this.shouldFilter || !this.shouldFilter(candidate))
636
+ localContent = candidate;
637
+ }
638
+ catch {
639
+ warnings.push('Project source could not be read; available lower-precedence content used.');
640
+ }
641
+ }
642
+ if (localContent === null) {
528
643
  // Issue #1002: the manager's writable home (precedence 2 — above the
529
644
  // synced manager cache, so a just-authored capability resolves before any
530
645
  // publish or sync and resolves at all under `single-machine`).
531
- const managerLocalContent = this.readManagerLocalOverlayFile(path);
646
+ const managerLocalContent = this.readManagerLocalOverlayFile(path, warnings);
532
647
  if (managerLocalContent !== null) {
533
648
  // Issue #1867: apply inheritance so an extends-based override merges with its parent.
534
649
  let resolvedManagerLocal;
@@ -548,7 +663,7 @@ class LocalRegistryResolver {
548
663
  };
549
664
  }
550
665
  // Issue #869: manager overlay (precedence 3 — the synced cross-machine copy)
551
- const managerOverlayContent = this.readManagerOverlayFile(path);
666
+ const managerOverlayContent = this.readManagerOverlayFile(path, warnings);
552
667
  if (managerOverlayContent !== null) {
553
668
  // Issue #1867: apply inheritance so an extends-based override merges with its parent.
554
669
  let resolvedManagerOverlay;
@@ -568,7 +683,7 @@ class LocalRegistryResolver {
568
683
  };
569
684
  }
570
685
  // Issue #869 Phase 2: org overlay (precedence 3 — after manager, before synced baseline)
571
- const orgOverlayContent = this.readOrgOverlayFile(path);
686
+ const orgOverlayContent = this.readOrgOverlayFile(path, warnings);
572
687
  if (orgOverlayContent !== null) {
573
688
  // Issue #1867: apply inheritance so an extends-based override merges with its parent.
574
689
  let resolvedOrgOverlay;
@@ -590,11 +705,11 @@ class LocalRegistryResolver {
590
705
  const syncedLocalContent = this.readSyncedLocalFile(path);
591
706
  if (syncedLocalContent !== null) {
592
707
  // Synced machine baseline, not personalized.
593
- return baselineResult(syncedLocalContent, 'local');
708
+ return baselineResult(syncedLocalContent, 'local', this.getSyncedFilePath(path));
594
709
  }
595
710
  const workspaceRegistryContent = this.readWorkspaceRegistryFile(path);
596
711
  if (workspaceRegistryContent !== null) {
597
- return baselineResult(workspaceRegistryContent, 'local');
712
+ return baselineResult(workspaceRegistryContent, 'local', (0, path_1.join)(this.workspaceRoot, 'registry', path.replace(/\\/g, '/').replace(/^\/+/, '')));
598
713
  }
599
714
  // No useful override or synced content, fetch from remote
600
715
  try {
@@ -605,21 +720,6 @@ class LocalRegistryResolver {
605
720
  throw new Error(`Failed to fetch from remote: ${path}. ${error.message}`);
606
721
  }
607
722
  }
608
- // Read local override
609
- let localContent;
610
- try {
611
- localContent = this.readLocalOverride(path);
612
- if (this.shouldFilter && this.shouldFilter(localContent)) {
613
- console.warn(`[LocalRegistryResolver] Local override for ${path} filtered (e.g. stub), falling back to remote.`);
614
- throw new Error('CONTENT_FILTERED');
615
- }
616
- }
617
- catch (error) {
618
- // If local read fails, fall back to remote
619
- console.warn(`Local override read failed, falling back to remote: ${path}`);
620
- const rawContent = await this.fetchRemoteWithFallback(path);
621
- return baselineResult(rawContent, 'remote');
622
- }
623
723
  // Resolve inheritance
624
724
  let resolved;
625
725
  try {
@@ -627,7 +727,10 @@ class LocalRegistryResolver {
627
727
  }
628
728
  catch (error) {
629
729
  const rawContent = await this.fetchRemoteWithFallback(path);
630
- return baselineResult(rawContent, 'remote');
730
+ return {
731
+ ...await baselineResult(rawContent, 'remote'),
732
+ resolutionWarnings: ['Personalized inheritance failed; available remote baseline used.']
733
+ };
631
734
  }
632
735
  // Build result — a local override hit means this came from the
633
736
  // personalized-employee layer (the manager taught/customized it).
@@ -9,7 +9,9 @@
9
9
  * `agent-token-prices.test.ts` lint fails CI if any entry exceeds 180 days
10
10
  * since `verifiedOn` so this table is reviewed at least quarterly.
11
11
  *
12
- * Prices are USD per million tokens.
12
+ * Prices are standard-tier USD per million tokens. Anthropic cache creation
13
+ * uses five-minute writes; long-context and other service-tier premiums are
14
+ * not represented by this fallback table.
13
15
  */
14
16
  Object.defineProperty(exports, "__esModule", { value: true });
15
17
  exports.AGENT_TOKEN_PRICES = void 0;
@@ -85,6 +87,28 @@ exports.AGENT_TOKEN_PRICES = [
85
87
  source: 'https://developers.openai.com/api/docs/models/gpt-5.5',
86
88
  verifiedOn: '2026-10-27',
87
89
  },
90
+ // Codex CLI ~0.155–0.162 emits `gpt-6-astra`; ~0.163+ emits `gpt-6.1-sol`.
91
+ // cacheCreationPerMTok is 0 because the CLI hardcodes cache_write_input_tokens=0.
92
+ {
93
+ agent: 'codex',
94
+ model: 'gpt-6-astra',
95
+ inputPerMTok: 10.00,
96
+ outputPerMTok: 50.00,
97
+ cacheReadPerMTok: 1.00,
98
+ cacheCreationPerMTok: 0,
99
+ source: 'https://developers.openai.com/api/docs/pricing',
100
+ verifiedOn: '2026-11-01',
101
+ },
102
+ {
103
+ agent: 'codex',
104
+ model: 'gpt-6.1-sol',
105
+ inputPerMTok: 2.00,
106
+ outputPerMTok: 10.00,
107
+ cacheReadPerMTok: 0.10,
108
+ cacheCreationPerMTok: 0,
109
+ source: 'https://developers.openai.com/api/docs/pricing',
110
+ verifiedOn: '2026-11-01',
111
+ },
88
112
  // Google — Gemini CLI. Standard paid-tier text/image/video token rates.
89
113
  // Context caching maps to cache reads; the published storage-hour price
90
114
  // cannot be derived from the CLI token stats and is intentionally omitted.