fraim 2.0.360-next.0 → 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.
- package/dist/registry/domains.json +159 -0
- package/dist/registry/scripts/domains/catalog.js +112 -0
- package/dist/registry/scripts/learning/entry-authoring.js +63 -25
- package/dist/registry/scripts/learning/run-application.js +12 -2
- package/dist/registry/scripts/learning/run-publication.js +169 -16
- package/dist/registry/scripts/learning-usage/local-runtime.js +1 -3
- package/dist/src/cli/learning/runtime.js +2 -0
- package/dist/src/cli/utils/remote-sync.js +7 -0
- package/dist/src/cli/utils/script-sync-utils.js +4 -0
- package/dist/src/config/learning-domains.js +24 -108
- package/dist/src/core/ai-mentor.js +7 -0
- package/dist/src/core/utils/domain-rule-assembler.js +206 -0
- package/dist/src/core/utils/local-registry-resolver.js +135 -32
- package/dist/src/local-mcp-server/agent-token-prices.js +25 -1
- package/dist/src/local-mcp-server/learning-context-builder.js +94 -36
- package/dist/src/local-mcp-server/stdio-server.js +122 -39
- package/dist/src/mcp/tool-schemas.js +2 -2
- package/dist/src/services/mcp-service.js +35 -16
- package/dist/src/services/registry-service.js +4 -0
- package/package.json +3 -2
- package/dist/registry/scripts/learning/learning-domains.json +0 -80
|
@@ -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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
//
|
|
527
|
-
|
|
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
|
|
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.
|