fraim-framework 2.0.170 → 2.0.173
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/src/ai-hub/hosts.js +227 -6
- package/dist/src/ai-hub/server.js +1014 -35
- package/dist/src/cli/commands/add-ide.js +4 -2
- package/dist/src/cli/commands/cleanup-artifacts.js +38 -0
- package/dist/src/cli/commands/init-project.js +12 -5
- package/dist/src/cli/commands/setup.js +1 -1
- package/dist/src/cli/commands/sync.js +74 -7
- package/dist/src/cli/doctor/checks/ide-config-checks.js +2 -2
- package/dist/src/cli/fraim.js +2 -0
- package/dist/src/cli/mcp/ide-formats.js +10 -2
- package/dist/src/cli/setup/auto-mcp-setup.js +4 -2
- package/dist/src/cli/setup/ide-detector.js +26 -0
- package/dist/src/cli/setup/ide-global-integration.js +6 -2
- package/dist/src/cli/setup/ide-invocation-surfaces.js +12 -4
- package/dist/src/cli/setup/mcp-config-generator.js +12 -1
- package/dist/src/cli/utils/agent-adapters.js +42 -17
- package/dist/src/cli/utils/fraim-gitignore.js +13 -0
- package/dist/src/cli/utils/remote-sync.js +129 -53
- package/dist/src/cli/utils/user-config.js +12 -0
- package/dist/src/config/ai-manager-hiring.js +121 -0
- package/dist/src/config/compat.js +16 -0
- package/dist/src/config/feature-flags.js +25 -0
- package/dist/src/config/persona-capability-bundles.js +273 -0
- package/dist/src/config/persona-hiring.js +270 -0
- package/dist/src/config/portfolio-slug-overrides.js +17 -0
- package/dist/src/config/pricing.js +37 -0
- package/dist/src/config/stripe.js +43 -0
- package/dist/src/core/fraim-config-schema.generated.js +8 -2
- package/dist/src/core/utils/local-registry-resolver.js +26 -0
- package/dist/src/core/utils/project-fraim-paths.js +89 -2
- package/dist/src/first-run/session-service.js +12 -3
- package/dist/src/local-mcp-server/artifact-retention-cleanup.js +255 -0
- package/dist/src/local-mcp-server/learning-context-builder.js +41 -81
- package/dist/src/local-mcp-server/stdio-server.js +42 -7
- package/package.json +5 -1
- package/public/ai-hub/index.html +205 -89
- package/public/ai-hub/review.css +12 -0
- package/public/ai-hub/script.js +1734 -253
- package/public/ai-hub/styles.css +473 -6
|
@@ -269,7 +269,8 @@ exports.FRAIM_CONFIG_SCHEMA = {
|
|
|
269
269
|
"kind": "enum",
|
|
270
270
|
"values": [
|
|
271
271
|
"salesforce",
|
|
272
|
-
"customereq"
|
|
272
|
+
"customereq",
|
|
273
|
+
"attio"
|
|
273
274
|
]
|
|
274
275
|
}
|
|
275
276
|
}
|
|
@@ -457,7 +458,8 @@ exports.FRAIM_CONFIG_SCHEMA = {
|
|
|
457
458
|
"kind": "enum",
|
|
458
459
|
"values": [
|
|
459
460
|
"servicenow",
|
|
460
|
-
"fixture"
|
|
461
|
+
"fixture",
|
|
462
|
+
"facebook_messenger"
|
|
461
463
|
]
|
|
462
464
|
},
|
|
463
465
|
"table": {
|
|
@@ -489,6 +491,9 @@ exports.FRAIM_CONFIG_SCHEMA = {
|
|
|
489
491
|
},
|
|
490
492
|
"closeState": {
|
|
491
493
|
"kind": "string"
|
|
494
|
+
},
|
|
495
|
+
"accessScript": {
|
|
496
|
+
"kind": "string"
|
|
492
497
|
}
|
|
493
498
|
}
|
|
494
499
|
},
|
|
@@ -678,6 +683,7 @@ exports.SUPPORTED_FRAIM_CONFIG_PATHS = [
|
|
|
678
683
|
"automation.support.queue.successState",
|
|
679
684
|
"automation.support.queue.failureState",
|
|
680
685
|
"automation.support.queue.closeState",
|
|
686
|
+
"automation.support.queue.accessScript",
|
|
681
687
|
"automation.support.requestTypes",
|
|
682
688
|
"automation.support.requestTypes.password_reset",
|
|
683
689
|
"automation.support.requestTypes.password_reset.mode",
|
|
@@ -225,6 +225,23 @@ class LocalRegistryResolver {
|
|
|
225
225
|
return null;
|
|
226
226
|
}
|
|
227
227
|
}
|
|
228
|
+
readWorkspaceRegistryFile(path) {
|
|
229
|
+
const normalizedPath = path.replace(/\\/g, '/').replace(/^\/+/, '');
|
|
230
|
+
const registryPath = (0, path_1.join)(this.workspaceRoot, 'registry', ...normalizedPath.split('/'));
|
|
231
|
+
if (!(0, fs_1.existsSync)(registryPath)) {
|
|
232
|
+
return null;
|
|
233
|
+
}
|
|
234
|
+
try {
|
|
235
|
+
const content = (0, fs_1.readFileSync)(registryPath, 'utf-8');
|
|
236
|
+
if (this.shouldFilter && this.shouldFilter(content)) {
|
|
237
|
+
return null;
|
|
238
|
+
}
|
|
239
|
+
return content;
|
|
240
|
+
}
|
|
241
|
+
catch {
|
|
242
|
+
return null;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
228
245
|
normalizeRegistryPath(path) {
|
|
229
246
|
const normalized = path.trim().replace(/\\/g, '/').replace(/^\/+/, '');
|
|
230
247
|
return normalized.endsWith('.md') ? normalized : `${normalized}.md`;
|
|
@@ -379,6 +396,15 @@ class LocalRegistryResolver {
|
|
|
379
396
|
inherited: false
|
|
380
397
|
};
|
|
381
398
|
}
|
|
399
|
+
const workspaceRegistryContent = this.readWorkspaceRegistryFile(path);
|
|
400
|
+
if (workspaceRegistryContent !== null) {
|
|
401
|
+
return {
|
|
402
|
+
content: workspaceRegistryContent,
|
|
403
|
+
source: 'local',
|
|
404
|
+
personalized: false,
|
|
405
|
+
inherited: false
|
|
406
|
+
};
|
|
407
|
+
}
|
|
382
408
|
// No useful override or synced content, fetch from remote
|
|
383
409
|
try {
|
|
384
410
|
const rawContent = await this.fetchRemoteWithFallback(path);
|
|
@@ -8,6 +8,10 @@ exports.getWorkspaceFraimDir = getWorkspaceFraimDir;
|
|
|
8
8
|
exports.workspaceFraimExists = workspaceFraimExists;
|
|
9
9
|
exports.getWorkspaceConfigPath = getWorkspaceConfigPath;
|
|
10
10
|
exports.getWorkspaceFraimPath = getWorkspaceFraimPath;
|
|
11
|
+
exports.assertWorkspaceFraimRoot = assertWorkspaceFraimRoot;
|
|
12
|
+
exports.assertWorkspaceFraimPath = assertWorkspaceFraimPath;
|
|
13
|
+
exports.createWorkspaceFraimPathAsserter = createWorkspaceFraimPathAsserter;
|
|
14
|
+
exports.assertExactWorkspaceFraimPath = assertExactWorkspaceFraimPath;
|
|
11
15
|
exports.getWorkspaceFraimDisplayPath = getWorkspaceFraimDisplayPath;
|
|
12
16
|
exports.getUserFraimDisplayPath = getUserFraimDisplayPath;
|
|
13
17
|
exports.getUserFraimDirPath = getUserFraimDirPath;
|
|
@@ -35,8 +39,38 @@ function normalizeDisplayPath(fullPath) {
|
|
|
35
39
|
function getWorkspaceFraimDir(projectRoot = process.cwd()) {
|
|
36
40
|
return (0, path_1.join)(projectRoot, exports.WORKSPACE_FRAIM_DIRNAME);
|
|
37
41
|
}
|
|
42
|
+
function isPathInsideOrEqual(parentPath, childPath) {
|
|
43
|
+
const relativePath = (0, path_1.relative)(parentPath, childPath);
|
|
44
|
+
return relativePath === '' || (relativePath.length > 0 &&
|
|
45
|
+
!relativePath.startsWith('..') &&
|
|
46
|
+
!(0, path_1.isAbsolute)(relativePath));
|
|
47
|
+
}
|
|
48
|
+
function realpathIfExists(fullPath) {
|
|
49
|
+
try {
|
|
50
|
+
return fs_1.realpathSync.native(fullPath);
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
function deepestExistingAncestor(fullPath) {
|
|
57
|
+
let current = (0, path_1.resolve)(fullPath);
|
|
58
|
+
while (!(0, fs_1.existsSync)(current)) {
|
|
59
|
+
const parent = (0, path_1.dirname)(current);
|
|
60
|
+
if (parent === current) {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
current = parent;
|
|
64
|
+
}
|
|
65
|
+
return current;
|
|
66
|
+
}
|
|
38
67
|
function workspaceFraimExists(projectRoot = process.cwd()) {
|
|
39
|
-
|
|
68
|
+
try {
|
|
69
|
+
return (0, fs_1.readdirSync)(projectRoot, { withFileTypes: true }).some((entry) => entry.isDirectory() && entry.name === exports.WORKSPACE_FRAIM_DIRNAME);
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
40
74
|
}
|
|
41
75
|
function getWorkspaceConfigPath(projectRoot = process.cwd()) {
|
|
42
76
|
return (0, path_1.join)(getWorkspaceFraimDir(projectRoot), 'config.json');
|
|
@@ -44,6 +78,59 @@ function getWorkspaceConfigPath(projectRoot = process.cwd()) {
|
|
|
44
78
|
function getWorkspaceFraimPath(projectRoot = process.cwd(), ...parts) {
|
|
45
79
|
return (0, path_1.join)(getWorkspaceFraimDir(projectRoot), ...parts);
|
|
46
80
|
}
|
|
81
|
+
function assertWorkspaceFraimRoot(projectRoot = process.cwd()) {
|
|
82
|
+
const resolvedProjectRoot = (0, path_1.resolve)(projectRoot);
|
|
83
|
+
const fraimDir = getWorkspaceFraimDir(resolvedProjectRoot);
|
|
84
|
+
if (!workspaceFraimExists(resolvedProjectRoot)) {
|
|
85
|
+
throw new Error(`Refusing to use FRAIM workspace root "${resolvedProjectRoot}": expected exact lowercase fraim directory at "${fraimDir}".`);
|
|
86
|
+
}
|
|
87
|
+
const projectRootRealPath = realpathIfExists(resolvedProjectRoot);
|
|
88
|
+
const fraimRealPath = realpathIfExists(fraimDir);
|
|
89
|
+
if (projectRootRealPath &&
|
|
90
|
+
fraimRealPath &&
|
|
91
|
+
!isPathInsideOrEqual(projectRootRealPath, fraimRealPath)) {
|
|
92
|
+
throw new Error(`Refusing to use FRAIM workspace root "${resolvedProjectRoot}": "${fraimDir}" resolves outside the project root.`);
|
|
93
|
+
}
|
|
94
|
+
return fraimDir;
|
|
95
|
+
}
|
|
96
|
+
function assertWorkspaceFraimPath(projectRoot = process.cwd(), targetPath) {
|
|
97
|
+
createWorkspaceFraimPathAsserter(projectRoot)(targetPath);
|
|
98
|
+
}
|
|
99
|
+
function createWorkspaceFraimPathAsserter(projectRoot = process.cwd()) {
|
|
100
|
+
const fraimDir = assertWorkspaceFraimRoot(projectRoot);
|
|
101
|
+
const resolvedFraimDir = (0, path_1.resolve)(fraimDir);
|
|
102
|
+
const fraimRealPath = realpathIfExists(fraimDir);
|
|
103
|
+
const realpathCache = new Map();
|
|
104
|
+
const cachedRealpath = (fullPath) => {
|
|
105
|
+
const resolvedPath = (0, path_1.resolve)(fullPath);
|
|
106
|
+
if (!realpathCache.has(resolvedPath)) {
|
|
107
|
+
realpathCache.set(resolvedPath, realpathIfExists(resolvedPath));
|
|
108
|
+
}
|
|
109
|
+
return realpathCache.get(resolvedPath) ?? null;
|
|
110
|
+
};
|
|
111
|
+
return (targetPath) => {
|
|
112
|
+
const resolvedTargetPath = (0, path_1.resolve)(targetPath);
|
|
113
|
+
if (resolvedTargetPath === resolvedFraimDir ||
|
|
114
|
+
!isPathInsideOrEqual(resolvedFraimDir, resolvedTargetPath)) {
|
|
115
|
+
throw new Error(`Refusing to use FRAIM workspace path "${resolvedTargetPath}": expected a path under "${resolvedFraimDir}".`);
|
|
116
|
+
}
|
|
117
|
+
const existingAncestor = deepestExistingAncestor(resolvedTargetPath);
|
|
118
|
+
const ancestorRealPath = existingAncestor ? cachedRealpath(existingAncestor) : null;
|
|
119
|
+
if (fraimRealPath &&
|
|
120
|
+
ancestorRealPath &&
|
|
121
|
+
!isPathInsideOrEqual(fraimRealPath, ancestorRealPath)) {
|
|
122
|
+
throw new Error(`Refusing to use FRAIM workspace path "${resolvedTargetPath}": existing path "${existingAncestor}" resolves outside "${fraimDir}".`);
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
function assertExactWorkspaceFraimPath(projectRoot = process.cwd(), targetPath, ...parts) {
|
|
127
|
+
const expectedPath = (0, path_1.resolve)(getWorkspaceFraimDir((0, path_1.resolve)(projectRoot)), ...parts);
|
|
128
|
+
const resolvedTargetPath = (0, path_1.resolve)(targetPath);
|
|
129
|
+
if (resolvedTargetPath !== expectedPath) {
|
|
130
|
+
throw new Error(`Refusing to use FRAIM workspace path "${resolvedTargetPath}": expected exact managed path "${expectedPath}".`);
|
|
131
|
+
}
|
|
132
|
+
assertWorkspaceFraimPath(projectRoot, targetPath);
|
|
133
|
+
}
|
|
47
134
|
function getWorkspaceFraimDisplayPath(relativePath = '') {
|
|
48
135
|
const normalized = normalizeRelativePath(relativePath);
|
|
49
136
|
return normalized.length > 0
|
|
@@ -120,7 +207,7 @@ function getConfiguredPortableLearningsDisplayPath(projectRoot = process.cwd())
|
|
|
120
207
|
function getEffectiveFraimDir(projectRoot = process.cwd(), userFraimDir) {
|
|
121
208
|
// 1. Check for project-level fraim/ directory
|
|
122
209
|
const projectFraimDir = getWorkspaceFraimDir(projectRoot);
|
|
123
|
-
if ((
|
|
210
|
+
if (workspaceFraimExists(projectRoot)) {
|
|
124
211
|
return projectFraimDir;
|
|
125
212
|
}
|
|
126
213
|
// 2. Fall back to user-level ~/.fraim/
|
|
@@ -174,8 +174,8 @@ function normalizeRows(rows) {
|
|
|
174
174
|
...(existingById.get(canonical.id) || {}),
|
|
175
175
|
}));
|
|
176
176
|
}
|
|
177
|
-
function
|
|
178
|
-
const ides = (0, ide_detector_1.detectInstalledIDEs)();
|
|
177
|
+
function buildRunnableAgentSurfaces() {
|
|
178
|
+
const ides = (0, ide_detector_1.detectInstalledIDEs)('cli-runnable');
|
|
179
179
|
const hints = (0, ide_global_integration_1.describeOnboardingInvocationSurfaces)(ides);
|
|
180
180
|
return ides.map((ide, index) => ({
|
|
181
181
|
id: ide.configType,
|
|
@@ -333,7 +333,7 @@ class FirstRunSessionService {
|
|
|
333
333
|
byId.set(surface.id, surface);
|
|
334
334
|
}
|
|
335
335
|
if (this.fakeMode !== 'no-agents') {
|
|
336
|
-
for (const surface of
|
|
336
|
+
for (const surface of buildRunnableAgentSurfaces()) {
|
|
337
337
|
byId.set(surface.id, surface);
|
|
338
338
|
}
|
|
339
339
|
}
|
|
@@ -547,6 +547,15 @@ class FirstRunSessionService {
|
|
|
547
547
|
this.persist();
|
|
548
548
|
return this.respond('Fake-mode fraim ok.', true);
|
|
549
549
|
}
|
|
550
|
+
// FRAIM_SKIP_SYNC signals an isolated/test environment. Skip all external
|
|
551
|
+
// writes (MCP config files, global config.json, slash commands) so tests
|
|
552
|
+
// that auto-trigger this path cannot corrupt the real user config.
|
|
553
|
+
if (process.env.FRAIM_SKIP_SYNC) {
|
|
554
|
+
row.status = 'ok';
|
|
555
|
+
row.verb = 'Ready.';
|
|
556
|
+
this.persist();
|
|
557
|
+
return this.respond('FRAIM configured.', true);
|
|
558
|
+
}
|
|
550
559
|
try {
|
|
551
560
|
if (!forceConfigure && !commandVersion('fraim')) {
|
|
552
561
|
const prefix = path_1.default.join((0, script_sync_utils_1.getUserFraimDir)(), 'node');
|
|
@@ -0,0 +1,255 @@
|
|
|
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.ARTIFACT_RETENTION_CATEGORIES = void 0;
|
|
7
|
+
exports.resolveArtifactRetentionConfig = resolveArtifactRetentionConfig;
|
|
8
|
+
exports.runArtifactCleanup = runArtifactCleanup;
|
|
9
|
+
exports.planArtifactCleanup = planArtifactCleanup;
|
|
10
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
11
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
12
|
+
const project_fraim_paths_1 = require("../core/utils/project-fraim-paths");
|
|
13
|
+
exports.ARTIFACT_RETENTION_CATEGORIES = [
|
|
14
|
+
'learning_archive',
|
|
15
|
+
'retrospectives',
|
|
16
|
+
'evidence',
|
|
17
|
+
'feedback',
|
|
18
|
+
'cleanup_manifests',
|
|
19
|
+
];
|
|
20
|
+
function readWorkspaceConfig(workspaceRoot) {
|
|
21
|
+
try {
|
|
22
|
+
const configPath = (0, project_fraim_paths_1.getWorkspaceConfigPath)(workspaceRoot);
|
|
23
|
+
if (!node_fs_1.default.existsSync(configPath))
|
|
24
|
+
return {};
|
|
25
|
+
return JSON.parse(node_fs_1.default.readFileSync(configPath, 'utf8'));
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return {};
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
function validRetentionValue(value) {
|
|
32
|
+
return Number.isInteger(value) && value >= -1;
|
|
33
|
+
}
|
|
34
|
+
function normalizeRelative(workspaceRoot, absolutePath) {
|
|
35
|
+
return node_path_1.default.relative(workspaceRoot, absolutePath).replace(/\\/g, '/');
|
|
36
|
+
}
|
|
37
|
+
function parseRetentionValue(category, value, fallback, invalidValues) {
|
|
38
|
+
if (validRetentionValue(value))
|
|
39
|
+
return value;
|
|
40
|
+
invalidValues.push({
|
|
41
|
+
category,
|
|
42
|
+
value,
|
|
43
|
+
fallback,
|
|
44
|
+
reason: 'Retention values must be integers greater than or equal to -1.',
|
|
45
|
+
});
|
|
46
|
+
return fallback;
|
|
47
|
+
}
|
|
48
|
+
function resolveArtifactRetentionConfig(workspaceRoot) {
|
|
49
|
+
const config = readWorkspaceConfig(workspaceRoot);
|
|
50
|
+
const configuredRetention = config?.artifact_retention_period;
|
|
51
|
+
const rawRetention = configuredRetention && typeof configuredRetention === 'object' && !Array.isArray(configuredRetention)
|
|
52
|
+
? configuredRetention
|
|
53
|
+
: {};
|
|
54
|
+
const invalidValues = [];
|
|
55
|
+
const rawDefault = rawRetention?.default;
|
|
56
|
+
const defaultValue = validRetentionValue(rawDefault)
|
|
57
|
+
? rawDefault
|
|
58
|
+
: parseRetentionValue('default', rawDefault, -1, invalidValues);
|
|
59
|
+
const values = {};
|
|
60
|
+
for (const category of exports.ARTIFACT_RETENTION_CATEGORIES) {
|
|
61
|
+
values[category] = category in rawRetention
|
|
62
|
+
? parseRetentionValue(category, rawRetention[category], defaultValue, invalidValues)
|
|
63
|
+
: defaultValue;
|
|
64
|
+
}
|
|
65
|
+
return { values, defaultValue, invalidValues };
|
|
66
|
+
}
|
|
67
|
+
function listFiles(dirPath) {
|
|
68
|
+
if (!node_fs_1.default.existsSync(dirPath))
|
|
69
|
+
return [];
|
|
70
|
+
const out = [];
|
|
71
|
+
const stack = [dirPath];
|
|
72
|
+
while (stack.length) {
|
|
73
|
+
const current = stack.pop();
|
|
74
|
+
let entries;
|
|
75
|
+
try {
|
|
76
|
+
entries = node_fs_1.default.readdirSync(current, { withFileTypes: true });
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
for (const entry of entries) {
|
|
82
|
+
const fullPath = node_path_1.default.join(current, entry.name);
|
|
83
|
+
if (entry.isDirectory())
|
|
84
|
+
stack.push(fullPath);
|
|
85
|
+
else if (entry.isFile())
|
|
86
|
+
out.push(fullPath);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return out;
|
|
90
|
+
}
|
|
91
|
+
function fileMtime(absolutePath) {
|
|
92
|
+
try {
|
|
93
|
+
return node_fs_1.default.statSync(absolutePath).mtime;
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
return undefined;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
function readFrontmatter(content) {
|
|
100
|
+
const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
101
|
+
if (!match)
|
|
102
|
+
return {};
|
|
103
|
+
const values = {};
|
|
104
|
+
for (const line of match[1].split(/\r?\n/)) {
|
|
105
|
+
const kv = line.match(/^([A-Za-z0-9_-]+):\s*(.*)$/);
|
|
106
|
+
if (kv)
|
|
107
|
+
values[kv[1]] = kv[2].trim();
|
|
108
|
+
}
|
|
109
|
+
return values;
|
|
110
|
+
}
|
|
111
|
+
function synthesizedDate(absolutePath) {
|
|
112
|
+
try {
|
|
113
|
+
const frontmatter = readFrontmatter(node_fs_1.default.readFileSync(absolutePath, 'utf8'));
|
|
114
|
+
const value = frontmatter.synthesized;
|
|
115
|
+
if (!value)
|
|
116
|
+
return undefined;
|
|
117
|
+
const parsed = new Date(value);
|
|
118
|
+
return Number.isNaN(parsed.getTime()) ? undefined : parsed;
|
|
119
|
+
}
|
|
120
|
+
catch {
|
|
121
|
+
return undefined;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
function userScoped(fileName, activeUserEmail) {
|
|
125
|
+
return fileName.startsWith(`${activeUserEmail}-`);
|
|
126
|
+
}
|
|
127
|
+
function collectCandidates(workspaceRoot, activeUserEmail) {
|
|
128
|
+
const candidates = [];
|
|
129
|
+
const add = (category, absolutePath, eligible, reason, eligibilityDate) => {
|
|
130
|
+
candidates.push({
|
|
131
|
+
category,
|
|
132
|
+
absolutePath,
|
|
133
|
+
relativePath: normalizeRelative(workspaceRoot, absolutePath),
|
|
134
|
+
eligible,
|
|
135
|
+
reason,
|
|
136
|
+
eligibilityDate,
|
|
137
|
+
});
|
|
138
|
+
};
|
|
139
|
+
const archiveDir = node_path_1.default.join(workspaceRoot, 'fraim', 'personalized-employee', 'learnings', 'archive');
|
|
140
|
+
for (const filePath of listFiles(archiveDir)) {
|
|
141
|
+
const fileName = node_path_1.default.basename(filePath);
|
|
142
|
+
if (!fileName.endsWith('.md') || !userScoped(fileName, activeUserEmail))
|
|
143
|
+
continue;
|
|
144
|
+
add('learning_archive', filePath, true, 'Archived learning source for active user.', fileMtime(filePath));
|
|
145
|
+
}
|
|
146
|
+
const retrospectivesDir = node_path_1.default.join(workspaceRoot, 'docs', 'retrospectives');
|
|
147
|
+
for (const filePath of listFiles(retrospectivesDir)) {
|
|
148
|
+
const fileName = node_path_1.default.basename(filePath);
|
|
149
|
+
if (!fileName.endsWith('.md') || !userScoped(fileName, activeUserEmail))
|
|
150
|
+
continue;
|
|
151
|
+
const date = synthesizedDate(filePath);
|
|
152
|
+
if (date)
|
|
153
|
+
add('retrospectives', filePath, true, 'Retrospective has synthesized frontmatter.', date);
|
|
154
|
+
else
|
|
155
|
+
add('retrospectives', filePath, false, 'Retrospective has no synthesized frontmatter.');
|
|
156
|
+
}
|
|
157
|
+
const evidenceDir = node_path_1.default.join(workspaceRoot, 'docs', 'evidence');
|
|
158
|
+
for (const filePath of listFiles(evidenceDir)) {
|
|
159
|
+
const fileName = node_path_1.default.basename(filePath);
|
|
160
|
+
const isMarkdown = fileName.endsWith('.md');
|
|
161
|
+
const isJson = fileName.endsWith('.json');
|
|
162
|
+
if (fileName.includes('cleanup-manifest') || fileName.includes('cleanup_manifest')) {
|
|
163
|
+
if (!isMarkdown && !isJson)
|
|
164
|
+
continue;
|
|
165
|
+
add('cleanup_manifests', filePath, true, 'Cleanup manifest file.', fileMtime(filePath));
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
if (!isMarkdown)
|
|
169
|
+
continue;
|
|
170
|
+
if (fileName.endsWith('-feedback.md')) {
|
|
171
|
+
add('feedback', filePath, true, 'Feedback evidence file.', fileMtime(filePath));
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
add('evidence', filePath, true, 'Evidence file.', fileMtime(filePath));
|
|
175
|
+
}
|
|
176
|
+
return candidates;
|
|
177
|
+
}
|
|
178
|
+
function daysBetween(from, to) {
|
|
179
|
+
return Math.floor((to.getTime() - from.getTime()) / 86_400_000);
|
|
180
|
+
}
|
|
181
|
+
function shouldDelete(candidate, retentionValue, now) {
|
|
182
|
+
if (!candidate.eligible)
|
|
183
|
+
return { delete: false, reason: candidate.reason };
|
|
184
|
+
if (retentionValue === -1)
|
|
185
|
+
return { delete: false, reason: 'Retention value -1 means never cleanup.' };
|
|
186
|
+
if (!candidate.eligibilityDate || Number.isNaN(candidate.eligibilityDate.getTime())) {
|
|
187
|
+
return { delete: false, reason: 'Eligibility date is unavailable or invalid.' };
|
|
188
|
+
}
|
|
189
|
+
if (retentionValue === 0)
|
|
190
|
+
return { delete: true, reason: 'Retention value 0 means cleanup immediately when eligible.' };
|
|
191
|
+
const ageDays = daysBetween(candidate.eligibilityDate, now);
|
|
192
|
+
if (ageDays >= retentionValue) {
|
|
193
|
+
return { delete: true, reason: `Eligible artifact age ${ageDays} days meets retention ${retentionValue} days.` };
|
|
194
|
+
}
|
|
195
|
+
return { delete: false, reason: `Eligible artifact age ${ageDays} days is below retention ${retentionValue} days.` };
|
|
196
|
+
}
|
|
197
|
+
function incrementCount(report, action) {
|
|
198
|
+
if (action === 'would_delete')
|
|
199
|
+
report.counts.wouldDelete++;
|
|
200
|
+
else
|
|
201
|
+
report.counts[action]++;
|
|
202
|
+
}
|
|
203
|
+
function runArtifactCleanup(workspaceRoot, options) {
|
|
204
|
+
const now = options.now ?? new Date();
|
|
205
|
+
const retention = resolveArtifactRetentionConfig(workspaceRoot);
|
|
206
|
+
const candidates = collectCandidates(workspaceRoot, options.activeUserEmail);
|
|
207
|
+
const report = {
|
|
208
|
+
generatedAt: now.toISOString(),
|
|
209
|
+
apply: options.apply,
|
|
210
|
+
retention: retention.values,
|
|
211
|
+
invalidConfig: retention.invalidValues,
|
|
212
|
+
counts: { retained: 0, wouldDelete: 0, deleted: 0, skipped: 0, failed: 0 },
|
|
213
|
+
items: [],
|
|
214
|
+
};
|
|
215
|
+
for (const candidate of candidates) {
|
|
216
|
+
const retentionValue = retention.values[candidate.category];
|
|
217
|
+
const decision = shouldDelete(candidate, retentionValue, now);
|
|
218
|
+
let action;
|
|
219
|
+
let error;
|
|
220
|
+
if (!candidate.eligible) {
|
|
221
|
+
action = 'skipped';
|
|
222
|
+
}
|
|
223
|
+
else if (!decision.delete) {
|
|
224
|
+
action = 'retained';
|
|
225
|
+
}
|
|
226
|
+
else if (!options.apply) {
|
|
227
|
+
action = 'would_delete';
|
|
228
|
+
}
|
|
229
|
+
else {
|
|
230
|
+
try {
|
|
231
|
+
node_fs_1.default.unlinkSync(candidate.absolutePath);
|
|
232
|
+
action = 'deleted';
|
|
233
|
+
}
|
|
234
|
+
catch (err) {
|
|
235
|
+
action = 'failed';
|
|
236
|
+
error = err instanceof Error ? err.message : String(err);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
incrementCount(report, action);
|
|
240
|
+
report.items.push({
|
|
241
|
+
category: candidate.category,
|
|
242
|
+
path: candidate.relativePath,
|
|
243
|
+
action,
|
|
244
|
+
reason: error ? `Deletion failed: ${decision.reason}` : decision.reason,
|
|
245
|
+
retentionValue,
|
|
246
|
+
eligibilityDate: candidate.eligibilityDate?.toISOString().slice(0, 10),
|
|
247
|
+
cleanupDate: action === 'deleted' || action === 'would_delete' ? now.toISOString().slice(0, 10) : undefined,
|
|
248
|
+
error,
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
return report;
|
|
252
|
+
}
|
|
253
|
+
function planArtifactCleanup(workspaceRoot, options) {
|
|
254
|
+
return runArtifactCleanup(workspaceRoot, { ...options, apply: false });
|
|
255
|
+
}
|
|
@@ -25,8 +25,7 @@ const REPO_LEARNINGS_REL = (0, project_fraim_paths_1.getWorkspaceFraimDisplayPat
|
|
|
25
25
|
const DEFAULT_THRESHOLD = 3.0;
|
|
26
26
|
const AGING_HORIZON_DAYS = 7;
|
|
27
27
|
const MAX_ENTRIES_SCANNED = 200;
|
|
28
|
-
const
|
|
29
|
-
const OLDEST_AGE_DAYS_TRIGGER = 3;
|
|
28
|
+
const L0_SLEEP_ON_LEARNINGS_PROMPT_MIN = 5;
|
|
30
29
|
// ── Single source of truth for the learning-entry format contract (#533) ──────
|
|
31
30
|
// The `## [P-…] <title>` entry format is a tight contract shared by three sides:
|
|
32
31
|
// 1. EMIT — the synthesis jobs (sleep-on-learnings, organizational-learning-
|
|
@@ -309,51 +308,49 @@ function isUnsynthesizedRetrospective(filePath) {
|
|
|
309
308
|
return false;
|
|
310
309
|
}
|
|
311
310
|
}
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
const
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
const ageDays = Math.floor((now - st.mtimeMs) / (1000 * 60 * 60 * 24));
|
|
321
|
-
if (ageDays > oldest)
|
|
322
|
-
oldest = ageDays;
|
|
323
|
-
}
|
|
324
|
-
catch {
|
|
325
|
-
// ignore
|
|
326
|
-
}
|
|
327
|
-
};
|
|
328
|
-
const rawDir = (0, path_1.join)(learningsBase, 'raw');
|
|
329
|
-
if ((0, fs_1.existsSync)(rawDir)) {
|
|
311
|
+
function pendingL0SortKey(fileName) {
|
|
312
|
+
const timestamp = fileName.match(/\d{4}-\d{2}-\d{2}(?:T\d{2}-\d{2}-\d{2})?/);
|
|
313
|
+
return timestamp?.[0] || fileName;
|
|
314
|
+
}
|
|
315
|
+
function collectPendingL0SourceFiles(workspaceRoot, resolvedUserId, roots) {
|
|
316
|
+
const sources = [];
|
|
317
|
+
const rawPath = (0, path_1.join)(roots.repoLearningsBase, 'raw');
|
|
318
|
+
if ((0, fs_1.existsSync)(rawPath)) {
|
|
330
319
|
try {
|
|
331
|
-
for (const
|
|
332
|
-
if (!
|
|
320
|
+
for (const fileName of (0, fs_1.readdirSync)(rawPath)) {
|
|
321
|
+
if (!fileName.startsWith(`${resolvedUserId}-`))
|
|
333
322
|
continue;
|
|
334
|
-
|
|
323
|
+
sources.push({
|
|
324
|
+
kind: 'coaching-moment',
|
|
325
|
+
displayPath: `${REPO_LEARNINGS_REL}/raw/${fileName}`,
|
|
326
|
+
sortKey: pendingL0SortKey(fileName)
|
|
327
|
+
});
|
|
335
328
|
}
|
|
336
329
|
}
|
|
337
330
|
catch {
|
|
338
|
-
//
|
|
331
|
+
// Ignore read failures.
|
|
339
332
|
}
|
|
340
333
|
}
|
|
341
|
-
const
|
|
342
|
-
if ((0, fs_1.existsSync)(
|
|
334
|
+
const retrospectivesPath = (0, path_1.join)(workspaceRoot, 'docs', 'retrospectives');
|
|
335
|
+
if ((0, fs_1.existsSync)(retrospectivesPath)) {
|
|
343
336
|
try {
|
|
344
|
-
for (const
|
|
345
|
-
if (!
|
|
337
|
+
for (const fileName of (0, fs_1.readdirSync)(retrospectivesPath)) {
|
|
338
|
+
if (!fileName.startsWith(`${resolvedUserId}-`) || !fileName.endsWith('.md'))
|
|
346
339
|
continue;
|
|
347
|
-
if (!isUnsynthesizedRetrospective((0, path_1.join)(
|
|
340
|
+
if (!isUnsynthesizedRetrospective((0, path_1.join)(retrospectivesPath, fileName)))
|
|
348
341
|
continue;
|
|
349
|
-
|
|
342
|
+
sources.push({
|
|
343
|
+
kind: 'retrospective',
|
|
344
|
+
displayPath: `docs/retrospectives/${fileName}`,
|
|
345
|
+
sortKey: pendingL0SortKey(fileName)
|
|
346
|
+
});
|
|
350
347
|
}
|
|
351
348
|
}
|
|
352
349
|
catch {
|
|
353
|
-
//
|
|
350
|
+
// Ignore read failures.
|
|
354
351
|
}
|
|
355
352
|
}
|
|
356
|
-
return
|
|
353
|
+
return sources.sort((a, b) => b.sortKey.localeCompare(a.sortKey) || a.displayPath.localeCompare(b.displayPath));
|
|
357
354
|
}
|
|
358
355
|
/**
|
|
359
356
|
* Resolve an L2 org-scope learning file (issue #563): a repo-local override
|
|
@@ -395,28 +392,9 @@ function buildLearningContextSection(workspaceRoot, userId, forJob) {
|
|
|
395
392
|
const l1Validated = resolvePersonalLearningFile(roots.repoLearningsBase, roots.managerCacheBase, roots.managerCacheDisplayBase, roots.globalPersonalBase, roots.globalPersonalDisplayBase, `${resolvedUserId}-validated-patterns.md`);
|
|
396
393
|
const l1MistakeStats = l1Mistake.present ? scanMistakePatternFile(l1Mistake.path, threshold, 'mistake-patterns') : null;
|
|
397
394
|
const l1ValidatedStats = l1Validated.present ? scanMistakePatternFile(l1Validated.path, threshold, 'validated-patterns') : null;
|
|
398
|
-
|
|
399
|
-
const
|
|
400
|
-
|
|
401
|
-
try {
|
|
402
|
-
l0CoachingCount = (0, fs_1.readdirSync)(rawPath).filter(f => f.startsWith(`${resolvedUserId}-`)).length;
|
|
403
|
-
}
|
|
404
|
-
catch {
|
|
405
|
-
// Ignore read failures.
|
|
406
|
-
}
|
|
407
|
-
}
|
|
408
|
-
let l0RetroCount = 0;
|
|
409
|
-
const retrospectivesPath = (0, path_1.join)(workspaceRoot, 'docs', 'retrospectives');
|
|
410
|
-
if ((0, fs_1.existsSync)(retrospectivesPath)) {
|
|
411
|
-
try {
|
|
412
|
-
l0RetroCount = (0, fs_1.readdirSync)(retrospectivesPath)
|
|
413
|
-
.filter(f => f.startsWith(`${resolvedUserId}-`) && f.endsWith('.md'))
|
|
414
|
-
.filter(f => isUnsynthesizedRetrospective((0, path_1.join)(retrospectivesPath, f))).length;
|
|
415
|
-
}
|
|
416
|
-
catch {
|
|
417
|
-
// Ignore read failures.
|
|
418
|
-
}
|
|
419
|
-
}
|
|
395
|
+
const pendingL0Sources = collectPendingL0SourceFiles(workspaceRoot, resolvedUserId, roots);
|
|
396
|
+
const l0CoachingCount = pendingL0Sources.filter(source => source.kind === 'coaching-moment').length;
|
|
397
|
+
const l0RetroCount = pendingL0Sources.filter(source => source.kind === 'retrospective').length;
|
|
420
398
|
const hasL2 = l2MistakePresent || l2PrefPresent || l2CoachPresent || l2ValidatedPresent;
|
|
421
399
|
const hasL1 = l1Mistake.present || l1Pref.present || l1Coach.present || l1Validated.present;
|
|
422
400
|
const hasContent = hasL2 || hasL1 || l0CoachingCount > 0 || l0RetroCount > 0;
|
|
@@ -458,6 +436,7 @@ function buildLearningContextSection(workspaceRoot, userId, forJob) {
|
|
|
458
436
|
section += '\n';
|
|
459
437
|
}
|
|
460
438
|
if (l0CoachingCount > 0 || l0RetroCount > 0) {
|
|
439
|
+
const totalL0 = pendingL0Sources.length;
|
|
461
440
|
section += '### L0 - Your unprocessed signals\n';
|
|
462
441
|
if (l0CoachingCount > 0) {
|
|
463
442
|
section += `${l0CoachingCount} coaching moment${l0CoachingCount !== 1 ? 's' : ''} in \`${REPO_LEARNINGS_REL}/raw/${resolvedUserId}-*\`\n`;
|
|
@@ -465,12 +444,16 @@ function buildLearningContextSection(workspaceRoot, userId, forJob) {
|
|
|
465
444
|
if (l0RetroCount > 0) {
|
|
466
445
|
section += `${l0RetroCount} retrospective${l0RetroCount !== 1 ? 's' : ''} in \`docs/retrospectives/${resolvedUserId}-*\` with unsynthesized or missing \`synthesized\` frontmatter\n`;
|
|
467
446
|
}
|
|
447
|
+
section += 'Pending L0 Source Files:\n';
|
|
448
|
+
for (const source of pendingL0Sources) {
|
|
449
|
+
section += `- \`${source.displayPath}\`\n`;
|
|
450
|
+
}
|
|
451
|
+
section += 'Read these pending L0 source files before continuing; they are unsynthesized learnings for the current agent.\n';
|
|
452
|
+
if (totalL0 >= L0_SLEEP_ON_LEARNINGS_PROMPT_MIN) {
|
|
453
|
+
section += `This is ${totalL0} pending L0 file${totalL0 !== 1 ? 's' : ''}; to speed up future starts, run \`sleep-on-learnings\` to synthesize or archive them.\n`;
|
|
454
|
+
}
|
|
468
455
|
section += '\n';
|
|
469
456
|
}
|
|
470
|
-
const totalL0 = l0CoachingCount + l0RetroCount;
|
|
471
|
-
const oldestAgeDays = totalL0 > 0 ? computeOldestL0AgeDays(workspaceRoot, resolvedUserId) : 0;
|
|
472
|
-
const agingRisk = l1MistakeStats?.agingRisk ?? 0;
|
|
473
|
-
const backlogTriggered = totalL0 >= BACKLOG_MIN || (oldestAgeDays >= OLDEST_AGE_DAYS_TRIGGER && totalL0 > 0);
|
|
474
457
|
if (forJob) {
|
|
475
458
|
if (hasL2 || hasL1) {
|
|
476
459
|
section += 'Use the relevant patterns and preferences in this job.\n';
|
|
@@ -478,23 +461,12 @@ function buildLearningContextSection(workspaceRoot, userId, forJob) {
|
|
|
478
461
|
section += 'Treat manager-coaching as feedback for how the manager should continue or improve managing AI, not as agent instruction.\n';
|
|
479
462
|
}
|
|
480
463
|
}
|
|
481
|
-
if (backlogTriggered) {
|
|
482
|
-
section += '\n';
|
|
483
|
-
section += `Warning: ${totalL0} unprocessed signals pending. Consider running \`sleep-on-learnings\` before starting today's work.\n`;
|
|
484
|
-
section += renderBacklogDetail(oldestAgeDays, agingRisk);
|
|
485
|
-
}
|
|
486
464
|
}
|
|
487
465
|
else {
|
|
488
466
|
section += 'Use this synthesized learning context throughout the session.\n';
|
|
489
467
|
if (l1Coach.present || l2CoachPresent) {
|
|
490
468
|
section += 'Manager-coaching entries are manager-facing feedback, not instructions for the AI to follow.\n';
|
|
491
469
|
}
|
|
492
|
-
if (backlogTriggered) {
|
|
493
|
-
section += '\n';
|
|
494
|
-
section += `Warning: synthesis overdue with ${totalL0} unprocessed signals.\n`;
|
|
495
|
-
section += 'Run `sleep-on-learnings` before starting today\'s work.\n';
|
|
496
|
-
section += renderBacklogDetail(oldestAgeDays, agingRisk);
|
|
497
|
-
}
|
|
498
470
|
}
|
|
499
471
|
return section;
|
|
500
472
|
}
|
|
@@ -960,15 +932,3 @@ function isTruthyFlag(value) {
|
|
|
960
932
|
return false;
|
|
961
933
|
return normalized === 'true' || normalized === 'yes' || normalized === '1';
|
|
962
934
|
}
|
|
963
|
-
function renderBacklogDetail(oldestAgeDays, agingRisk) {
|
|
964
|
-
if (oldestAgeDays <= 0 && agingRisk <= 0)
|
|
965
|
-
return '';
|
|
966
|
-
const parts = [];
|
|
967
|
-
if (oldestAgeDays > 0)
|
|
968
|
-
parts.push(`oldest ${oldestAgeDays}d`);
|
|
969
|
-
parts.push('debrief takes ~3 minutes');
|
|
970
|
-
if (agingRisk > 0) {
|
|
971
|
-
parts.push(`${agingRisk} high-score pattern${agingRisk !== 1 ? 's' : ''} aging out within ${AGING_HORIZON_DAYS}d`);
|
|
972
|
-
}
|
|
973
|
-
return `Detail: ${parts.join('; ')}.\n`;
|
|
974
|
-
}
|