fraim-hub 2.0.346 → 2.0.349
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/scripts/learning/completion-state.js +80 -4
- package/dist/registry/scripts/learning/corpus-audit.js +7 -7
- package/dist/registry/scripts/learning/entry-format.js +10 -1
- package/dist/registry/scripts/learning/outcome-types.js +92 -0
- package/dist/registry/scripts/learning/owner-identity.js +11 -0
- package/dist/registry/scripts/learning/proposal-queue.js +81 -0
- package/dist/src/ai-hub/hosts.js +43 -1
- package/dist/src/ai-hub/hub-app-materializer.js +20 -0
- package/dist/src/ai-hub/server.js +9 -3
- package/dist/src/core/utils/local-registry-resolver.js +13 -3
- package/dist/src/local-mcp-server/learning-context-builder.js +109 -379
- package/dist/src/local-mcp-server/learning-entry-format.js +25 -0
- package/dist/src/local-mcp-server/learning-usage-eligibility.js +8 -14
- package/dist/src/local-mcp-server/learning-usage-projection.js +5 -22
- package/dist/src/local-mcp-server/learning-usage-store.js +93 -393
- package/package.json +7 -2
- package/public/ai-hub/review.css +8 -0
- package/public/ai-hub/script.js +219 -279
- package/public/ai-hub/styles.css +1 -0
|
@@ -4,10 +4,13 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.COMPLETION_END = exports.COMPLETION_START = exports.UnsupportedRunHistoryError = exports.sourcePathKey = void 0;
|
|
7
|
+
exports.validateDeferredAttempt = validateDeferredAttempt;
|
|
8
|
+
exports.validateDeferredWork = validateDeferredWork;
|
|
7
9
|
exports.cycleRetrospectivePath = cycleRetrospectivePath;
|
|
8
10
|
exports.validateProcessedSources = validateProcessedSources;
|
|
9
11
|
exports.canonicalJournal = canonicalJournal;
|
|
10
12
|
exports.readRunJournal = readRunJournal;
|
|
13
|
+
exports.readRunReservations = readRunReservations;
|
|
11
14
|
exports.readSourceReservations = readSourceReservations;
|
|
12
15
|
exports.normalizeSourceParticipants = normalizeSourceParticipants;
|
|
13
16
|
exports.sourceAuthorAllowed = sourceAuthorAllowed;
|
|
@@ -17,6 +20,33 @@ const node_fs_1 = __importDefault(require("node:fs"));
|
|
|
17
20
|
const node_crypto_1 = require("node:crypto");
|
|
18
21
|
const artifact_paths_1 = require("../artifact-paths");
|
|
19
22
|
const corpus_audit_1 = require("./corpus-audit");
|
|
23
|
+
const outcome_types_1 = require("./outcome-types");
|
|
24
|
+
function validateDeferredAttempt(value) {
|
|
25
|
+
if (!keys(value, ['at', 'evidenceHash', 'deferred', 'reason']) || !text(value.at) || !Number.isFinite(Date.parse(value.at)) || !hash(value.evidenceHash))
|
|
26
|
+
throw new Error('invalid-deferred-attempt');
|
|
27
|
+
// Older journals embedded the checkpoint snapshot; keep accepting those exact records.
|
|
28
|
+
if (value.deferred !== undefined && value.reason === undefined)
|
|
29
|
+
validateDeferredWork(value.deferred);
|
|
30
|
+
else if (value.deferred !== undefined || !text(value.reason))
|
|
31
|
+
throw new Error('invalid-deferred-attempt');
|
|
32
|
+
}
|
|
33
|
+
function validateDeferredWork(value) {
|
|
34
|
+
if (!keys(value, ['actions', 'sources', 'corpusComplete', 'corpusErrors']) || value.corpusComplete !== false || !Array.isArray(value.actions) || !Array.isArray(value.sources))
|
|
35
|
+
throw new Error('invalid-deferred-work');
|
|
36
|
+
if (value.corpusErrors !== undefined && (!Array.isArray(value.corpusErrors) || value.corpusErrors.some(item => !keys(item, ['path', 'reason']) || !text(item.path) || !node_path_1.default.isAbsolute(item.path) || !text(item.reason))))
|
|
37
|
+
throw new Error('invalid-deferred-work');
|
|
38
|
+
const actions = new Set(), sources = new Set();
|
|
39
|
+
for (const item of value.actions) {
|
|
40
|
+
if (!keys(item, ['actionId', 'revision', 'digest', 'reason']) || !text(item.actionId) || !Number.isInteger(item.revision) || item.revision < 1 || !hash(item.digest) || !text(item.reason) || actions.has(item.actionId))
|
|
41
|
+
throw new Error('invalid-deferred-work');
|
|
42
|
+
actions.add(item.actionId);
|
|
43
|
+
}
|
|
44
|
+
for (const item of value.sources) {
|
|
45
|
+
if (!keys(item, ['path', 'owner', 'reason', 'sha256']) || !text(item.path) || !node_path_1.default.isAbsolute(item.path) || /[\x00-\x1f]/.test(item.path) || !ownerValid(item.owner) || !text(item.reason) || (item.sha256 !== undefined && !hash(item.sha256)) || sources.has((0, exports.sourcePathKey)(item.path)))
|
|
46
|
+
throw new Error('invalid-deferred-work');
|
|
47
|
+
sources.add((0, exports.sourcePathKey)(item.path));
|
|
48
|
+
}
|
|
49
|
+
}
|
|
20
50
|
const sourcePathKey = (file) => {
|
|
21
51
|
const value = node_path_1.default.resolve(file).replace(/\\/g, '/');
|
|
22
52
|
return process.platform === 'win32' ? value.toLowerCase() : value;
|
|
@@ -145,9 +175,10 @@ function readRunJournal(file) {
|
|
|
145
175
|
}
|
|
146
176
|
return { events, lastHash, format: 'current' };
|
|
147
177
|
}
|
|
148
|
-
/**
|
|
149
|
-
function
|
|
178
|
+
/** Current archive plans and prepared assessments share one verified reservation inventory. */
|
|
179
|
+
function readRunReservations(runDirectories, excludingRunFile) {
|
|
150
180
|
const reserved = new Set();
|
|
181
|
+
const assessmentKeys = new Set();
|
|
151
182
|
for (const directory of new Set(runDirectories.map(dir => node_path_1.default.resolve(dir)))) {
|
|
152
183
|
let names;
|
|
153
184
|
try {
|
|
@@ -168,6 +199,23 @@ function readSourceReservations(runDirectories, excludingRunFile) {
|
|
|
168
199
|
const journal = readRunJournal(file);
|
|
169
200
|
if (journal.format === 'historical')
|
|
170
201
|
continue;
|
|
202
|
+
const assessmentSources = new Map();
|
|
203
|
+
for (const event of journal.events) {
|
|
204
|
+
if (event.payload.type === 'assessment-prepared') {
|
|
205
|
+
const { record, sourcePaths } = event.payload.data;
|
|
206
|
+
(0, outcome_types_1.validateOutcomeAssessmentRecord)(record);
|
|
207
|
+
if (!Array.isArray(sourcePaths) || sourcePaths.length !== record.sourceRefs.length || sourcePaths.some(file => typeof file !== 'string' || !node_path_1.default.isAbsolute(file)))
|
|
208
|
+
throw new Error('invalid-assessment-sources');
|
|
209
|
+
assessmentSources.set(record.digest, { paths: sourcePaths, key: (0, outcome_types_1.assessmentKey)(record) });
|
|
210
|
+
}
|
|
211
|
+
else if (event.payload.type === 'assessment-committed')
|
|
212
|
+
assessmentSources.delete(event.payload.data.digest);
|
|
213
|
+
}
|
|
214
|
+
for (const reservation of assessmentSources.values()) {
|
|
215
|
+
assessmentKeys.add(reservation.key);
|
|
216
|
+
for (const source of reservation.paths)
|
|
217
|
+
reserved.add((0, exports.sourcePathKey)(source));
|
|
218
|
+
}
|
|
171
219
|
for (const event of journal.events)
|
|
172
220
|
if (event.payload.type === 'source-completion') {
|
|
173
221
|
for (const source of event.payload.data) {
|
|
@@ -183,7 +231,10 @@ function readSourceReservations(runDirectories, excludingRunFile) {
|
|
|
183
231
|
}
|
|
184
232
|
}
|
|
185
233
|
}
|
|
186
|
-
return [...reserved].sort();
|
|
234
|
+
return { sources: [...reserved].sort(), assessmentKeys: [...assessmentKeys].sort() };
|
|
235
|
+
}
|
|
236
|
+
function readSourceReservations(runDirectories, excludingRunFile) {
|
|
237
|
+
return readRunReservations(runDirectories, excludingRunFile).sources;
|
|
187
238
|
}
|
|
188
239
|
exports.COMPLETION_START = '<!-- fraim-sleep-decisions:v1 -->';
|
|
189
240
|
exports.COMPLETION_END = '<!-- /fraim-sleep-decisions -->';
|
|
@@ -234,7 +285,7 @@ function decodeCompletion(markdown, expected = {}) {
|
|
|
234
285
|
catch {
|
|
235
286
|
fail();
|
|
236
287
|
}
|
|
237
|
-
if (!keys(state, ['schemaVersion', 'runId', 'owner', 'finalizationState', 'decisions', 'sourceDispositions', 'cleanup', 'reconciliations', 'retrospective', 'processedSources', 'sourceParticipants', 'corpusAudit', 'reconciliationHistory']) || state.schemaVersion !== 1 || !text(state.runId) || !/^[a-zA-Z0-9][a-zA-Z0-9._-]*$/.test(state.runId) || !ownerValid(state.owner)
|
|
288
|
+
if (!keys(state, ['schemaVersion', 'runId', 'owner', 'finalizationState', 'decisions', 'sourceDispositions', 'cleanup', 'reconciliations', 'retrospective', 'processedSources', 'sourceParticipants', 'corpusAudit', 'reconciliationHistory', 'deferred', 'deferredHistory', 'deferredAttempts', 'outcomeAssessments']) || state.schemaVersion !== 1 || !text(state.runId) || !/^[a-zA-Z0-9][a-zA-Z0-9._-]*$/.test(state.runId) || !ownerValid(state.owner)
|
|
238
289
|
|| !['pending', 'complete'].includes(state.finalizationState) || !Array.isArray(state.decisions) || !Array.isArray(state.sourceDispositions) || !Array.isArray(state.cleanup))
|
|
239
290
|
fail();
|
|
240
291
|
if ((expected.runId && expected.runId !== state.runId) || (expected.owner && !sameOwner(expected.owner, state.owner)))
|
|
@@ -249,8 +300,31 @@ function decodeCompletion(markdown, expected = {}) {
|
|
|
249
300
|
fail();
|
|
250
301
|
if (state.processedSources !== undefined)
|
|
251
302
|
validateProcessedSources(state.processedSources);
|
|
303
|
+
if (state.outcomeAssessments !== undefined) {
|
|
304
|
+
if (!Array.isArray(state.outcomeAssessments))
|
|
305
|
+
fail();
|
|
306
|
+
const digests = new Set();
|
|
307
|
+
for (const assessment of state.outcomeAssessments) {
|
|
308
|
+
(0, outcome_types_1.validateOutcomeAssessmentRecord)(assessment);
|
|
309
|
+
if (assessment.runId !== state.runId || digests.has(assessment.digest))
|
|
310
|
+
fail();
|
|
311
|
+
digests.add(assessment.digest);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
252
314
|
if (state.sourceParticipants !== undefined)
|
|
253
315
|
normalizeSourceParticipants(state.sourceParticipants, state.owner);
|
|
316
|
+
if (state.deferred !== undefined)
|
|
317
|
+
validateDeferredWork(state.deferred);
|
|
318
|
+
if (state.deferredHistory !== undefined) {
|
|
319
|
+
if (!Array.isArray(state.deferredHistory))
|
|
320
|
+
fail();
|
|
321
|
+
state.deferredHistory.forEach(validateDeferredWork);
|
|
322
|
+
}
|
|
323
|
+
if (state.deferredAttempts !== undefined) {
|
|
324
|
+
if (!Array.isArray(state.deferredAttempts))
|
|
325
|
+
fail();
|
|
326
|
+
state.deferredAttempts.forEach(validateDeferredAttempt);
|
|
327
|
+
}
|
|
254
328
|
const ids = new Set();
|
|
255
329
|
for (const item of state.decisions) {
|
|
256
330
|
if (!keys(item, ['actionId', 'revision', 'digest', 'owner', 'claimKey', 'evidenceFingerprint', 'sourceRefs', 'decision', 'publicationRef', 'target', 'dependencies']) || !text(item.actionId) || ids.has(item.actionId) || !Number.isInteger(item.revision) || item.revision < 1 || !hash(item.digest) || !ownerValid(item.owner)
|
|
@@ -282,6 +356,8 @@ function decodeCompletion(markdown, expected = {}) {
|
|
|
282
356
|
fail();
|
|
283
357
|
}
|
|
284
358
|
const decisions = new Map(state.decisions.map((item) => [item.actionId, item]));
|
|
359
|
+
if (state.deferred?.actions.some((item) => decisions.has(item.actionId)))
|
|
360
|
+
fail();
|
|
285
361
|
if (state.corpusAudit !== undefined) {
|
|
286
362
|
try {
|
|
287
363
|
(0, corpus_audit_1.validateCorpusAudit)(state.corpusAudit);
|
|
@@ -10,7 +10,7 @@ const node_fs_1 = __importDefault(require("node:fs"));
|
|
|
10
10
|
const node_path_1 = __importDefault(require("node:path"));
|
|
11
11
|
const node_crypto_1 = require("node:crypto");
|
|
12
12
|
const owner_identity_1 = require("./owner-identity");
|
|
13
|
-
const { learningEntryBlocks } = require('./entry-format');
|
|
13
|
+
const { learningEntryBlocks, learningEntryIdentity, isLearningEntryId } = require('./entry-format');
|
|
14
14
|
const stable = (value) => JSON.stringify(value, (_key, item) => item && typeof item === 'object' && !Array.isArray(item)
|
|
15
15
|
? Object.fromEntries(Object.keys(item).sort().map(key => [key, item[key]])) : item);
|
|
16
16
|
const hash = (value) => (0, node_crypto_1.createHash)('sha256').update(value).digest('hex');
|
|
@@ -63,9 +63,8 @@ function collectLearningCorpus(homes, subjects, readerRoots = []) {
|
|
|
63
63
|
for (const block of blocks) {
|
|
64
64
|
// Match the runtime reader's handwritten identity contract.
|
|
65
65
|
// Approved mutations still use the author's narrower generated-ID schema.
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
-
if ((declarations.length && (!id || declarations.length !== 1)) || (id && ids.has(id))
|
|
66
|
+
const { id, ambiguous } = learningEntryIdentity(block.text);
|
|
67
|
+
if (ambiguous || (id && ids.has(id))
|
|
69
68
|
|| !/^#{2,6}\s+\[P-(CRITICAL|HIGH|MED|LOW)\]\s+\S/.test(block.text)) {
|
|
70
69
|
errors.push({ path: file.replace(/\\/g, '/'), reason: 'ambiguous-corpus-entry' });
|
|
71
70
|
continue;
|
|
@@ -157,8 +156,9 @@ function collectLearningCorpus(homes, subjects, readerRoots = []) {
|
|
|
157
156
|
/** Shared by journal replay and the strict compact receipt decoder. No filesystem access. */
|
|
158
157
|
function validateCorpusAudit(audit, reviewer) {
|
|
159
158
|
const fail = () => { throw new Error('invalid-corpus-audit'); };
|
|
160
|
-
if (!keys(audit, ['snapshot', 'reviewer', 'dispositions']) || typeof audit.reviewer !== 'string' || !audit.reviewer.trim() || (reviewer && audit.reviewer !== reviewer)
|
|
161
|
-
|| !keys(audit.snapshot, ['schemaVersion', 'homes', 'entries', 'duplicates', 'fingerprint', 'complete', 'errors']) || audit.snapshot.schemaVersion !== 1 || audit.snapshot.complete !==
|
|
159
|
+
if (!keys(audit, ['snapshot', 'reviewer', 'dispositions', 'partial']) || typeof audit.reviewer !== 'string' || !audit.reviewer.trim() || (reviewer && audit.reviewer !== reviewer) || (audit.partial !== undefined && audit.partial !== true)
|
|
160
|
+
|| !keys(audit.snapshot, ['schemaVersion', 'homes', 'entries', 'duplicates', 'fingerprint', 'complete', 'errors']) || audit.snapshot.schemaVersion !== 1 || typeof audit.snapshot.complete !== 'boolean' || (!audit.partial && !audit.snapshot.complete) || !Array.isArray(audit.snapshot.errors) || (!audit.partial && audit.snapshot.errors.length)
|
|
161
|
+
|| audit.snapshot.errors.some(item => !keys(item, ['path', 'reason']) || typeof item.path !== 'string' || !node_path_1.default.isAbsolute(item.path) || typeof item.reason !== 'string' || !item.reason.trim())
|
|
162
162
|
|| !Array.isArray(audit.snapshot.homes) || !Array.isArray(audit.snapshot.entries) || !Array.isArray(audit.dispositions))
|
|
163
163
|
fail();
|
|
164
164
|
const snapshot = audit.snapshot;
|
|
@@ -172,7 +172,7 @@ function validateCorpusAudit(audit, reviewer) {
|
|
|
172
172
|
fail();
|
|
173
173
|
(0, owner_identity_1.validateOwner)(entry.ref?.owner);
|
|
174
174
|
if (!snapshot.homes.some(owner => (0, owner_identity_1.ownerKey)(owner) === (0, owner_identity_1.ownerKey)(entry.ref.owner))
|
|
175
|
-
|| (entry.ref.entryId === undefined ? !Number.isSafeInteger(entry.occurrence) || entry.occurrence < 0 :
|
|
175
|
+
|| (entry.ref.entryId === undefined ? !Number.isSafeInteger(entry.occurrence) || entry.occurrence < 0 : !isLearningEntryId(entry.ref.entryId) || entry.occurrence !== undefined)
|
|
176
176
|
|| !/^learnings\//.test(entry.ref.relativePath) || /[\\:]/.test(entry.ref.relativePath) || entry.ref.relativePath.split('/').some(part => !part || part === '.' || part === '..')
|
|
177
177
|
|| !/^[a-f0-9]{64}$/.test(entry.sha256) || entries.has(corpusEntryKey(entry)))
|
|
178
178
|
fail();
|
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
const LEARNING_ENTRY_ID_LINE = /^\*\*Id\*\*:\s*(L-[0-9A-Za-z]{6,24})\s*$/;
|
|
4
|
+
function isLearningEntryId(value) { return typeof value === 'string' && /^L-[0-9A-Za-z]{6,24}$/.test(value); }
|
|
5
|
+
/** Reader/corpus identity accepts stable handwritten IDs; approved mutation IDs remain narrower below. */
|
|
6
|
+
function learningEntryIdentity(content) {
|
|
7
|
+
const declarations = content.split(/\r?\n/).map(line => line.trim()).filter(line => /^\*\*Id\*\*:/i.test(line));
|
|
8
|
+
const id = declarations[0]?.match(LEARNING_ENTRY_ID_LINE)?.[1] ?? null;
|
|
9
|
+
return { id: declarations.length === 1 ? id : null, ambiguous: declarations.length > 0 && (!id || declarations.length !== 1) };
|
|
10
|
+
}
|
|
11
|
+
|
|
3
12
|
// Read the shared author's current entry format for identity and update guards.
|
|
4
13
|
function learningEntryBlocks(body) {
|
|
5
14
|
const headings = [...body.matchAll(/^#{2,6}\s+\[P-[A-Z]+\].*$/gm)];
|
|
@@ -24,4 +33,4 @@ function learningEntryMetadata(content) {
|
|
|
24
33
|
return metadata;
|
|
25
34
|
}
|
|
26
35
|
|
|
27
|
-
module.exports = { learningEntryMetadata, learningEntryBlocks };
|
|
36
|
+
module.exports = { learningEntryMetadata, learningEntryBlocks, learningEntryIdentity, isLearningEntryId, LEARNING_ENTRY_ID_LINE };
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.matchesOutcomeScope = matchesOutcomeScope;
|
|
4
|
+
exports.assessmentKey = assessmentKey;
|
|
5
|
+
exports.assessmentContent = assessmentContent;
|
|
6
|
+
exports.outcomeAssessmentDigest = outcomeAssessmentDigest;
|
|
7
|
+
exports.validateOutcomeAssessmentInput = validateOutcomeAssessmentInput;
|
|
8
|
+
exports.validateOutcomeAssessmentRecord = validateOutcomeAssessmentRecord;
|
|
9
|
+
exports.summarizeLearningOutcomes = summarizeLearningOutcomes;
|
|
10
|
+
const node_crypto_1 = require("node:crypto");
|
|
11
|
+
const owner_identity_1 = require("./owner-identity");
|
|
12
|
+
const { isLearningEntryId } = require('./entry-format');
|
|
13
|
+
function matchesOutcomeScope(record, scope) {
|
|
14
|
+
return (scope.key === undefined || assessmentKey(record) === scope.key)
|
|
15
|
+
&& (scope.principal === undefined || record.learning.learningSubject === scope.principal)
|
|
16
|
+
&& (scope.owners === undefined || scope.owners.some(owner => (0, owner_identity_1.ownerKey)(owner) === (0, owner_identity_1.ownerKey)(record.learning.owner)));
|
|
17
|
+
}
|
|
18
|
+
const canonical = (value) => JSON.stringify(value, (_key, item) => item && typeof item === 'object' && !Array.isArray(item)
|
|
19
|
+
? Object.fromEntries(Object.keys(item).sort().map(key => [key, item[key]])) : item);
|
|
20
|
+
const text = (value) => typeof value === 'string' && !!value.trim() && !/[\x00-\x08\x0b\x0c\x0e-\x1f]/.test(value);
|
|
21
|
+
const hash = (value) => typeof value === 'string' && /^[a-f0-9]{64}$/.test(value);
|
|
22
|
+
const relative = (value) => text(value) && !/[\\:\x00-\x1f]/.test(value) && value.split('/').every(part => !!part && part !== '.' && part !== '..');
|
|
23
|
+
const date = (value) => typeof value === 'string' && /^\d{4}-\d{2}-\d{2}(?:T\d{2}:\d{2}:\d{2}(?:\.\d{3})?Z)?$/.test(value)
|
|
24
|
+
&& Number.isFinite(Date.parse(value)) && new Date(value).toISOString().slice(0, 10) === value.slice(0, 10);
|
|
25
|
+
/** Semantic identity comes from Sleep; paths are provenance, never a title or move-sensitive join. */
|
|
26
|
+
function assessmentKey(input) {
|
|
27
|
+
return canonical([(0, owner_identity_1.ownerKey)(input.learning.owner), input.learning.learningSubject, input.learning.entryId, input.incidentId]);
|
|
28
|
+
}
|
|
29
|
+
function assessmentContent(input) {
|
|
30
|
+
return canonical({ learning: input.learning, incidentId: input.incidentId, observedAt: input.observedAt, outcome: input.outcome,
|
|
31
|
+
reportedHelp: input.reportedHelp, explicitlySkipped: input.explicitlySkipped,
|
|
32
|
+
sourceRefs: [...input.sourceRefs].sort((a, b) => canonical(a).localeCompare(canonical(b))), rationale: input.rationale,
|
|
33
|
+
guidanceExistedAt: input.guidanceExistedAt });
|
|
34
|
+
}
|
|
35
|
+
function outcomeAssessmentDigest(record) {
|
|
36
|
+
return (0, node_crypto_1.createHash)('sha256').update(canonical({ content: assessmentContent(record), revision: record.revision,
|
|
37
|
+
expectedPriorDigest: record.expectedPriorDigest, reviewer: record.reviewer })).digest('hex');
|
|
38
|
+
}
|
|
39
|
+
function validateOutcomeAssessmentInput(input) {
|
|
40
|
+
if (!input || !input.learning)
|
|
41
|
+
throw new Error('invalid-outcome-assessment');
|
|
42
|
+
(0, owner_identity_1.validateOwner)(input.learning.owner);
|
|
43
|
+
if (!relative(input.learning.relativePath) || !isLearningEntryId(input.learning.entryId)
|
|
44
|
+
|| !text(input.learning.learningSubject) || !text(input.incidentId) || input.incidentId.length > 512
|
|
45
|
+
|| !date(input.observedAt) || !date(input.guidanceExistedAt)
|
|
46
|
+
|| !['success', 'miss', 'unknown'].includes(input.outcome) || typeof input.reportedHelp !== 'boolean'
|
|
47
|
+
|| typeof input.explicitlySkipped !== 'boolean' || !text(input.rationale)
|
|
48
|
+
|| (input.expectedPriorDigest !== null && !hash(input.expectedPriorDigest))
|
|
49
|
+
|| !Array.isArray(input.sourceRefs) || !input.sourceRefs.length)
|
|
50
|
+
throw new Error('invalid-outcome-assessment');
|
|
51
|
+
if (input.reportedHelp && input.outcome !== 'success' || input.explicitlySkipped && input.outcome !== 'miss')
|
|
52
|
+
throw new Error('invalid-outcome-qualification');
|
|
53
|
+
if (Date.parse(input.guidanceExistedAt) > Date.parse(input.observedAt))
|
|
54
|
+
throw new Error('guidance-after-incident');
|
|
55
|
+
for (const source of input.sourceRefs) {
|
|
56
|
+
if (!source)
|
|
57
|
+
throw new Error('invalid-assessment-source');
|
|
58
|
+
(0, owner_identity_1.validateOwner)(source.owner);
|
|
59
|
+
if (!relative(source.relativePath) || !/^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/.test(source.sourceId)
|
|
60
|
+
|| !hash(source.sha256))
|
|
61
|
+
throw new Error('invalid-assessment-source');
|
|
62
|
+
}
|
|
63
|
+
if (new Set(input.sourceRefs.map(source => canonical([(0, owner_identity_1.ownerKey)(source.owner), source.sourceId]))).size !== input.sourceRefs.length)
|
|
64
|
+
throw new Error('duplicate-assessment-source');
|
|
65
|
+
}
|
|
66
|
+
function validateOutcomeAssessmentRecord(record) {
|
|
67
|
+
validateOutcomeAssessmentInput(record);
|
|
68
|
+
if (!Number.isInteger(record.revision) || record.revision < 1 || !date(record.assessedAt)
|
|
69
|
+
|| !text(record.reviewer) || !text(record.runId) || record.digest !== outcomeAssessmentDigest(record))
|
|
70
|
+
throw new Error('invalid-outcome-record');
|
|
71
|
+
}
|
|
72
|
+
function summarizeLearningOutcomes(records, learning) {
|
|
73
|
+
const selected = new Map();
|
|
74
|
+
for (const record of records) {
|
|
75
|
+
if ((0, owner_identity_1.ownerKey)(record.learning.owner) !== (0, owner_identity_1.ownerKey)(learning.owner) || record.learning.entryId !== learning.entryId
|
|
76
|
+
|| record.learning.learningSubject !== learning.learningSubject)
|
|
77
|
+
continue;
|
|
78
|
+
validateOutcomeAssessmentRecord(record);
|
|
79
|
+
const key = assessmentKey(record), prior = selected.get(key);
|
|
80
|
+
if (!prior || record.revision > prior.revision)
|
|
81
|
+
selected.set(key, record);
|
|
82
|
+
else if (record.revision === prior.revision && record.digest !== prior.digest)
|
|
83
|
+
throw new Error('assessment-history-conflict');
|
|
84
|
+
}
|
|
85
|
+
if (!selected.size)
|
|
86
|
+
return undefined;
|
|
87
|
+
const current = [...selected.values()], observed = current.map(record => record.observedAt).sort();
|
|
88
|
+
return { success: current.filter(record => record.outcome === 'success').length,
|
|
89
|
+
reportedHelp: current.filter(record => record.outcome === 'success' && record.reportedHelp).length,
|
|
90
|
+
miss: current.filter(record => record.outcome === 'miss').length, unknown: current.filter(record => record.outcome === 'unknown').length,
|
|
91
|
+
firstObservedAt: observed[0], lastObservedAt: observed[observed.length - 1], assessedAt: current.map(record => record.assessedAt).sort().at(-1) };
|
|
92
|
+
}
|
|
@@ -8,6 +8,7 @@ exports.ownerPath = ownerPath;
|
|
|
8
8
|
exports.validateOwner = validateOwner;
|
|
9
9
|
exports.ownerKey = ownerKey;
|
|
10
10
|
exports.assertSameOwner = assertSameOwner;
|
|
11
|
+
exports.assertLearningTargetPath = assertLearningTargetPath;
|
|
11
12
|
exports.resolveOwnerHome = resolveOwnerHome;
|
|
12
13
|
exports.ownerArtifactPath = ownerArtifactPath;
|
|
13
14
|
exports.configuredArtifactPath = configuredArtifactPath;
|
|
@@ -34,6 +35,16 @@ function assertSameOwner(expected, actual) {
|
|
|
34
35
|
if (ownerKey(expected) !== ownerKey(actual))
|
|
35
36
|
throw new Error('owner-path-conflict: the approved home differs from the configured path');
|
|
36
37
|
}
|
|
38
|
+
/** Shared authored-family identity check. Handwritten entries can omit Domain metadata. */
|
|
39
|
+
function assertLearningTargetPath(ref, domain) {
|
|
40
|
+
const subject = ref.owner.level === 'Org' ? 'org' : ref.learningSubject;
|
|
41
|
+
if (!subject || !/^[a-zA-Z0-9@._+-]+$/.test(subject))
|
|
42
|
+
throw new Error('learning-subject-required');
|
|
43
|
+
const escaped = subject.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
44
|
+
const match = ref.relativePath.match(new RegExp(`^learnings/${escaped}-((?:[a-z][a-z-]*-)?)(preferences|mistake-patterns|validated-patterns|manager-coaching)(\\.cold)?\\.md$`));
|
|
45
|
+
if (!match || (domain !== undefined && match[1] !== (domain === 'global' ? '' : domain + '-')))
|
|
46
|
+
throw new Error('domain-target-mismatch: learning subject, domain and family must match its file');
|
|
47
|
+
}
|
|
37
48
|
/** Pure read: resolve existing configured homes without migration or identity configuration. */
|
|
38
49
|
function resolveOwnerHome(projectPath, level, options) {
|
|
39
50
|
const load = (relative) => options.runtimeModule(projectPath, relative);
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.hashContent = void 0;
|
|
7
|
+
exports.atomicWrite = atomicWrite;
|
|
8
|
+
exports.withFileLock = withFileLock;
|
|
9
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
10
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
11
|
+
const node_crypto_1 = require("node:crypto");
|
|
12
|
+
const node_os_1 = require("node:os");
|
|
13
|
+
const hashContent = (text) => (0, node_crypto_1.createHash)('sha256').update(text).digest('hex');
|
|
14
|
+
exports.hashContent = hashContent;
|
|
15
|
+
function atomicWrite(file, text) {
|
|
16
|
+
node_fs_1.default.mkdirSync(node_path_1.default.dirname(file), { recursive: true });
|
|
17
|
+
const temporary = file + '.' + (0, node_crypto_1.randomUUID)() + '.tmp';
|
|
18
|
+
try {
|
|
19
|
+
node_fs_1.default.writeFileSync(temporary, text, { encoding: 'utf8', flag: 'wx' });
|
|
20
|
+
const fd = node_fs_1.default.openSync(temporary, 'r+');
|
|
21
|
+
try {
|
|
22
|
+
node_fs_1.default.fsyncSync(fd);
|
|
23
|
+
}
|
|
24
|
+
finally {
|
|
25
|
+
node_fs_1.default.closeSync(fd);
|
|
26
|
+
}
|
|
27
|
+
node_fs_1.default.renameSync(temporary, file);
|
|
28
|
+
}
|
|
29
|
+
finally {
|
|
30
|
+
if (node_fs_1.default.existsSync(temporary))
|
|
31
|
+
node_fs_1.default.unlinkSync(temporary);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
function withFileLock(file, fn) {
|
|
35
|
+
node_fs_1.default.mkdirSync(node_path_1.default.dirname(file), { recursive: true });
|
|
36
|
+
let fd;
|
|
37
|
+
try {
|
|
38
|
+
fd = node_fs_1.default.openSync(file + '.lock', 'wx');
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
// Serialize recovery so two contenders cannot unlink each other's new lock.
|
|
42
|
+
let recovery;
|
|
43
|
+
try {
|
|
44
|
+
recovery = node_fs_1.default.openSync(file + '.lock.recovery', 'wx');
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
throw new Error('busy: lock recovery is already in progress');
|
|
48
|
+
}
|
|
49
|
+
try {
|
|
50
|
+
const owner = JSON.parse(node_fs_1.default.readFileSync(file + '.lock', 'utf8'));
|
|
51
|
+
let stale = false;
|
|
52
|
+
if (owner.host === (0, node_os_1.hostname)() && Number.isInteger(owner.pid) && owner.pid > 0) {
|
|
53
|
+
try {
|
|
54
|
+
process.kill(owner.pid, 0);
|
|
55
|
+
}
|
|
56
|
+
catch (error) {
|
|
57
|
+
stale = error.code === 'ESRCH';
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
if (!stale)
|
|
61
|
+
throw new Error('busy');
|
|
62
|
+
node_fs_1.default.unlinkSync(file + '.lock');
|
|
63
|
+
fd = node_fs_1.default.openSync(file + '.lock', 'wx');
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
throw new Error('busy: reconcile outstanding lock before retry');
|
|
67
|
+
}
|
|
68
|
+
finally {
|
|
69
|
+
node_fs_1.default.closeSync(recovery);
|
|
70
|
+
node_fs_1.default.unlinkSync(file + '.lock.recovery');
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
node_fs_1.default.writeFileSync(fd, JSON.stringify({ pid: process.pid, host: (0, node_os_1.hostname)(), at: new Date().toISOString() }));
|
|
74
|
+
try {
|
|
75
|
+
return fn();
|
|
76
|
+
}
|
|
77
|
+
finally {
|
|
78
|
+
node_fs_1.default.closeSync(fd);
|
|
79
|
+
node_fs_1.default.unlinkSync(file + '.lock');
|
|
80
|
+
}
|
|
81
|
+
}
|
package/dist/src/ai-hub/hosts.js
CHANGED
|
@@ -31,6 +31,7 @@ exports.buildDirectStartPlan = buildDirectStartPlan;
|
|
|
31
31
|
exports.buildDirectContinuePlan = buildDirectContinuePlan;
|
|
32
32
|
exports.parseHostLine = parseHostLine;
|
|
33
33
|
exports.detectCodexCompactTaskFailure = detectCodexCompactTaskFailure;
|
|
34
|
+
exports.wireHostProcess = wireHostProcess;
|
|
34
35
|
exports.describeSpawnError = describeSpawnError;
|
|
35
36
|
exports.hostDefaultUnsetEnv = hostDefaultUnsetEnv;
|
|
36
37
|
exports.buildHostSpawnEnv = buildHostSpawnEnv;
|
|
@@ -2398,6 +2399,39 @@ function detectCodexCompactTaskFailure(line) {
|
|
|
2398
2399
|
return null;
|
|
2399
2400
|
}
|
|
2400
2401
|
function wireHostProcess(hostId, child, handlers, spawnCwd) {
|
|
2402
|
+
// Exec's JSON error has no auth code. Refresh failures need corroborating
|
|
2403
|
+
// stderr; workspace-routing 401 needs the exact terminal provider event.
|
|
2404
|
+
// Both require a failed process exit; never scan tool/prose text.
|
|
2405
|
+
const codexRefreshFailure = 'Your access token could not be refreshed because your refresh token was already used. Please log out and sign in again.';
|
|
2406
|
+
const codexWorkspaceRoutingFailure = 'workspace routing discovery unauthorized (401)';
|
|
2407
|
+
let codexAuthDiagnostic = false;
|
|
2408
|
+
let codexTerminalAuthFailure = false;
|
|
2409
|
+
let codexTerminalWorkspaceAuthFailure = false;
|
|
2410
|
+
const observeCodexAuth = (line, channel) => {
|
|
2411
|
+
if (hostId !== 'codex')
|
|
2412
|
+
return;
|
|
2413
|
+
if (channel === 'stderr') {
|
|
2414
|
+
const clean = line.replace(/\x1b\[[0-9;]*m/g, '').trim();
|
|
2415
|
+
const diagnostic = clean.match(/^(?:\d{4}-\d{2}-\d{2}T[\d:.]+Z\s+)?ERROR\s+codex_login::auth::manager:\s+Failed to refresh token: (.+)$/);
|
|
2416
|
+
if (diagnostic?.[1] === codexRefreshFailure)
|
|
2417
|
+
codexAuthDiagnostic = true;
|
|
2418
|
+
return;
|
|
2419
|
+
}
|
|
2420
|
+
try {
|
|
2421
|
+
const event = JSON.parse(line);
|
|
2422
|
+
if (event.type === 'turn.failed') {
|
|
2423
|
+
codexTerminalWorkspaceAuthFailure = event.error?.message === codexWorkspaceRoutingFailure;
|
|
2424
|
+
}
|
|
2425
|
+
if (event.type === 'turn.failed' || event.type === 'error') {
|
|
2426
|
+
codexTerminalAuthFailure = (event.type === 'turn.failed' ? event.error?.message : event.message) === codexRefreshFailure;
|
|
2427
|
+
}
|
|
2428
|
+
else if (event.type === 'turn.started' || event.type === 'turn.completed') {
|
|
2429
|
+
codexTerminalAuthFailure = false;
|
|
2430
|
+
codexTerminalWorkspaceAuthFailure = false;
|
|
2431
|
+
}
|
|
2432
|
+
}
|
|
2433
|
+
catch { /* Plain stdout cannot establish authentication failure. */ }
|
|
2434
|
+
};
|
|
2401
2435
|
const wire = (buffer, channel) => {
|
|
2402
2436
|
let pending = '';
|
|
2403
2437
|
let suppressCodexModelRefreshStderr = false;
|
|
@@ -2406,6 +2440,7 @@ function wireHostProcess(hostId, child, handlers, spawnCwd) {
|
|
|
2406
2440
|
const lines = pending.split(/\r?\n/);
|
|
2407
2441
|
pending = lines.pop() || '';
|
|
2408
2442
|
for (const line of lines) {
|
|
2443
|
+
observeCodexAuth(line, channel);
|
|
2409
2444
|
if (hostId === 'codex' && channel === 'stderr') {
|
|
2410
2445
|
if (line.includes('codex_models_manager::manager: failed to refresh available models:')) {
|
|
2411
2446
|
suppressCodexModelRefreshStderr = true;
|
|
@@ -2431,6 +2466,7 @@ function wireHostProcess(hostId, child, handlers, spawnCwd) {
|
|
|
2431
2466
|
});
|
|
2432
2467
|
buffer.on('end', () => {
|
|
2433
2468
|
if (pending.trim().length > 0) {
|
|
2469
|
+
observeCodexAuth(pending, channel);
|
|
2434
2470
|
const parsed = parseHostLine(hostId, pending);
|
|
2435
2471
|
if (parsed.message || parsed.sessionId || parsed.raw) {
|
|
2436
2472
|
handlers.onEvent(parsed, channel);
|
|
@@ -2444,7 +2480,13 @@ function wireHostProcess(hostId, child, handlers, spawnCwd) {
|
|
|
2444
2480
|
// signal='SIGSEGV') are NOT treated as clean exits by classifyExit's
|
|
2445
2481
|
// `exitCode ?? 0` guard. Pass -1 for any signal-terminated child so the
|
|
2446
2482
|
// non-zero branch fires and recovery is attempted.
|
|
2447
|
-
child.on('close', (code, signal) =>
|
|
2483
|
+
child.on('close', (code, signal) => {
|
|
2484
|
+
if (!signal && code !== null && code !== 0 && ((codexAuthDiagnostic && codexTerminalAuthFailure) || codexTerminalWorkspaceAuthFailure)) {
|
|
2485
|
+
const failure = codexTerminalWorkspaceAuthFailure ? codexWorkspaceRoutingFailure : codexRefreshFailure;
|
|
2486
|
+
handlers.onEvent({ hostAuthenticationRequired: true, hostError: failure, raw: failure }, 'system');
|
|
2487
|
+
}
|
|
2488
|
+
handlers.onExit(signal ? -1 : code);
|
|
2489
|
+
});
|
|
2448
2490
|
// Issue #1327: without this listener, an OS-level spawn failure (EAGAIN,
|
|
2449
2491
|
// ENOENT, EACCES) fires an 'error' event on the ChildProcess. An unhandled
|
|
2450
2492
|
// EventEmitter 'error' throws synchronously and crashes the Hub process.
|
|
@@ -10,6 +10,7 @@ exports.resolveHubAppCacheDir = resolveHubAppCacheDir;
|
|
|
10
10
|
exports.isHubAppCacheComplete = isHubAppCacheComplete;
|
|
11
11
|
exports.listCachedHubAppVersions = listCachedHubAppVersions;
|
|
12
12
|
exports.runNpmInstall = runNpmInstall;
|
|
13
|
+
exports.pruneHubAppCache = pruneHubAppCache;
|
|
13
14
|
exports.ensureMaterializedHubApp = ensureMaterializedHubApp;
|
|
14
15
|
exports.resolveHubAppEntry = resolveHubAppEntry;
|
|
15
16
|
const fs_1 = __importDefault(require("fs"));
|
|
@@ -209,6 +210,24 @@ const defaultInstallPackage = async (version, destDir) => {
|
|
|
209
210
|
}
|
|
210
211
|
fs_1.default.rmSync(installedRoot, { recursive: true, force: true });
|
|
211
212
|
};
|
|
213
|
+
const HUB_APP_CACHE_MAX_VERSIONS = 3;
|
|
214
|
+
/** Prune the hub-app cache to at most HUB_APP_CACHE_MAX_VERSIONS entries, always preserving keepVersion. Non-fatal. */
|
|
215
|
+
function pruneHubAppCache(fraimDir, keepVersion) {
|
|
216
|
+
try {
|
|
217
|
+
const all = listCachedHubAppVersions(fraimDir);
|
|
218
|
+
const sorted = all.map((e) => e.version).sort((a, b) => (semver_1.default.gt(a, b) ? -1 : 1));
|
|
219
|
+
const toKeep = new Set(sorted.slice(0, HUB_APP_CACHE_MAX_VERSIONS));
|
|
220
|
+
toKeep.add(keepVersion);
|
|
221
|
+
for (const v of sorted) {
|
|
222
|
+
if (!toKeep.has(v)) {
|
|
223
|
+
fs_1.default.rmSync(resolveHubAppCacheDir(v, fraimDir), { recursive: true, force: true });
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
catch {
|
|
228
|
+
// Prune failure must never break a launch.
|
|
229
|
+
}
|
|
230
|
+
}
|
|
212
231
|
/**
|
|
213
232
|
* Return the entry path for `version`, materializing it first if it is not already a complete
|
|
214
233
|
* cache entry. Returns immediately (no npm invocation) when a complete entry already exists.
|
|
@@ -248,6 +267,7 @@ async function ensureMaterializedHubApp(version, options = {}) {
|
|
|
248
267
|
finally {
|
|
249
268
|
fs_1.default.rmSync(stagingDir, { recursive: true, force: true });
|
|
250
269
|
}
|
|
270
|
+
pruneHubAppCache(fraimDir, version);
|
|
251
271
|
return entryPath;
|
|
252
272
|
}
|
|
253
273
|
/**
|
|
@@ -2596,7 +2596,9 @@ function summarizeHostErrorForThread(error) {
|
|
|
2596
2596
|
// noise that the system handled on its own.
|
|
2597
2597
|
function pushTerminalFailureToThread(run, exitCode) {
|
|
2598
2598
|
if (exitCode !== 0 && run.lastHostError) {
|
|
2599
|
-
|
|
2599
|
+
const detail = detectLoginFailureSignature(run, exitCode)
|
|
2600
|
+
? run.lastHostError : summarizeHostErrorForThread(run.lastHostError);
|
|
2601
|
+
run.messages.push((0, hosts_1.createHubMessage)('system', `This run stopped: ${detail}`));
|
|
2600
2602
|
}
|
|
2601
2603
|
}
|
|
2602
2604
|
function createHubRecoveryEvent(run, exitCode, attempt) {
|
|
@@ -3017,6 +3019,7 @@ class AiHubServer {
|
|
|
3017
3019
|
this.latestVersionPollTimer = null;
|
|
3018
3020
|
this.catalogSyncChild = null;
|
|
3019
3021
|
this.attachmentPolicy = (0, attachment_store_1.attachmentPolicy)();
|
|
3022
|
+
this.configureAgentSurface = options.configureAgentSurface ?? configureFraimForHubAgent;
|
|
3020
3023
|
this.openTerminal = options.openTerminal ?? hubOpenTerminal;
|
|
3021
3024
|
this.preferencesStore = options.preferencesStore || new preferences_1.AiHubPreferencesStore();
|
|
3022
3025
|
const launchResolution = options.projectPath
|
|
@@ -4254,7 +4257,10 @@ class AiHubServer {
|
|
|
4254
4257
|
const option = hubAgentOption(run.hostId);
|
|
4255
4258
|
run.status = 'failed';
|
|
4256
4259
|
run.pauseReason = 'awaiting_user';
|
|
4257
|
-
run.loginRequired = option ? {
|
|
4260
|
+
run.loginRequired = option ? {
|
|
4261
|
+
agentLabel: option.label,
|
|
4262
|
+
...(run.hostId === 'codex' && run.lastHostError ? { remediation: run.lastHostError } : {}),
|
|
4263
|
+
} : undefined;
|
|
4258
4264
|
}
|
|
4259
4265
|
if (run.status !== 'running') {
|
|
4260
4266
|
run.eventLogRefs = (run.eventLogRefs || []).map((ref) => {
|
|
@@ -7013,7 +7019,7 @@ class AiHubServer {
|
|
|
7013
7019
|
return res.json({ ok: true, configured: [], message: 'No ready CLI agents to configure.' });
|
|
7014
7020
|
}
|
|
7015
7021
|
const results = await Promise.all(readyIds.map(async (hubId) => {
|
|
7016
|
-
const result = await
|
|
7022
|
+
const result = await this.configureAgentSurface(hubId);
|
|
7017
7023
|
return { hubId, ...result };
|
|
7018
7024
|
}));
|
|
7019
7025
|
const failed = results.filter((r) => !r.configured);
|
|
@@ -50,6 +50,7 @@ class LocalRegistryResolver {
|
|
|
50
50
|
constructor(options) {
|
|
51
51
|
this.workspaceRoot = options.workspaceRoot;
|
|
52
52
|
this.effectiveFraimDir = options.effectiveFraimDir;
|
|
53
|
+
this.userLevelOnly = options.userLevelOnly ?? false;
|
|
53
54
|
this.remoteContentResolver = options.remoteContentResolver;
|
|
54
55
|
this.parser = new inheritance_parser_1.InheritanceParser(options.maxDepth);
|
|
55
56
|
this.shouldFilter = options.shouldFilter;
|
|
@@ -87,7 +88,14 @@ class LocalRegistryResolver {
|
|
|
87
88
|
* Check if a local override exists for the given path
|
|
88
89
|
*/
|
|
89
90
|
hasLocalOverride(path) {
|
|
90
|
-
return (0, fs_1.existsSync)(this.getOverridePath(path));
|
|
91
|
+
return !this.userLevelOnly && (0, fs_1.existsSync)(this.getOverridePath(path));
|
|
92
|
+
}
|
|
93
|
+
/** Whether an effective personalized layer should replace a remote baseline response. */
|
|
94
|
+
hasPersonalizedOverride(path) {
|
|
95
|
+
return this.hasLocalOverride(path)
|
|
96
|
+
|| this.readManagerLocalOverlayFile(path) !== null
|
|
97
|
+
|| this.readManagerOverlayFile(path) !== null
|
|
98
|
+
|| this.readOrgOverlayFile(path) !== null;
|
|
91
99
|
}
|
|
92
100
|
/**
|
|
93
101
|
* Find the actual relative path for a registry item by searching subdirectories.
|
|
@@ -108,11 +116,11 @@ class LocalRegistryResolver {
|
|
|
108
116
|
// `jobs/<category>/<name>.md`, so the job was reported not found even
|
|
109
117
|
// though the file existed.
|
|
110
118
|
const searchRoots = [
|
|
111
|
-
this.getFraimPath('personalized-employee', dir),
|
|
119
|
+
...(!this.userLevelOnly ? [this.getFraimPath('personalized-employee', dir)] : []),
|
|
112
120
|
(0, path_1.join)(this.managerLocalRoot, dir),
|
|
113
121
|
(0, path_1.join)(this.managerCacheRoot, dir),
|
|
114
122
|
(0, path_1.join)(this.orgCacheRoot, dir),
|
|
115
|
-
(0, path_1.join)(this.workspaceRoot, 'registry', dir),
|
|
123
|
+
...(!this.userLevelOnly ? [(0, path_1.join)(this.workspaceRoot, 'registry', dir)] : []),
|
|
116
124
|
// Installed projects discover categorized capabilities in machine catalogs.
|
|
117
125
|
this.getSyncedCatalogRoot('ai-employee', dir),
|
|
118
126
|
...(type !== 'skills' ? [this.getSyncedCatalogRoot('ai-manager', dir)] : []),
|
|
@@ -326,6 +334,8 @@ class LocalRegistryResolver {
|
|
|
326
334
|
}
|
|
327
335
|
}
|
|
328
336
|
readWorkspaceRegistryFile(path) {
|
|
337
|
+
if (this.userLevelOnly)
|
|
338
|
+
return null;
|
|
329
339
|
const normalizedPath = path.replace(/\\/g, '/').replace(/^\/+/, '');
|
|
330
340
|
const registryRoot = (0, path_1.join)(this.workspaceRoot, 'registry');
|
|
331
341
|
const registryPath = (0, path_1.join)(registryRoot, ...normalizedPath.split('/'));
|