fraim 2.0.132 → 2.0.134
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/catalog.js +25 -18
- package/dist/src/ai-hub/server.js +8 -0
- package/package.json +1 -1
- package/public/ai-hub/script.js +65 -4
|
@@ -27,13 +27,11 @@ const project_fraim_paths_1 = require("../core/utils/project-fraim-paths");
|
|
|
27
27
|
// (per-project override layer; CLAUDE.md says it
|
|
28
28
|
// "takes precedence over synced baseline content")
|
|
29
29
|
const EMPLOYEE_JOB_LAYERS = [
|
|
30
|
-
{ base: 'registry', segments: ['jobs', 'ai-employee'] },
|
|
31
30
|
{ base: 'fraim', segments: ['ai-employee', 'jobs'] },
|
|
32
31
|
{ base: 'fraim', segments: ['personalized-employee', 'jobs'] },
|
|
33
32
|
];
|
|
34
|
-
// Manager templates use the matching
|
|
33
|
+
// Manager templates use the matching two-layer model.
|
|
35
34
|
const MANAGER_JOB_LAYERS = [
|
|
36
|
-
{ base: 'registry', segments: ['jobs', 'ai-manager'] },
|
|
37
35
|
{ base: 'fraim', segments: ['ai-manager', 'jobs'] },
|
|
38
36
|
{ base: 'fraim', segments: ['personalized-employee', 'manager-jobs'] },
|
|
39
37
|
];
|
|
@@ -139,16 +137,14 @@ function summarizeProject(projectPath) {
|
|
|
139
137
|
};
|
|
140
138
|
}
|
|
141
139
|
const fraimDir = (0, project_fraim_paths_1.getWorkspaceFraimDir)(projectPath);
|
|
142
|
-
const registryEmployeeDir = path_1.default.join(projectPath, 'registry', 'jobs', 'ai-employee');
|
|
143
|
-
const registryManagerDir = path_1.default.join(projectPath, 'registry', 'jobs', 'ai-manager');
|
|
144
140
|
const hasFraim = fs_1.default.existsSync(fraimDir);
|
|
145
|
-
|
|
146
|
-
if (!hasFraim && !hasRegistry) {
|
|
141
|
+
if (!hasFraim) {
|
|
147
142
|
return {
|
|
148
143
|
path: projectPath,
|
|
149
144
|
exists: true,
|
|
150
145
|
hasFraim: false,
|
|
151
|
-
|
|
146
|
+
needsOnboarding: true,
|
|
147
|
+
message: 'This folder has not been set up with FRAIM yet. Onboarding will get your AI employee started.',
|
|
152
148
|
};
|
|
153
149
|
}
|
|
154
150
|
return {
|
|
@@ -158,13 +154,6 @@ function summarizeProject(projectPath) {
|
|
|
158
154
|
};
|
|
159
155
|
}
|
|
160
156
|
function resolveLayerRoot(projectPath, layer) {
|
|
161
|
-
if (layer.base === 'registry') {
|
|
162
|
-
// The registry is the FRAIM source-of-truth catalog. It lives at the
|
|
163
|
-
// project root in the FRAIM source repo (this repo) and in any project
|
|
164
|
-
// that ships dist/registry alongside its source. Not under fraim/.
|
|
165
|
-
return path_1.default.join(projectPath, 'registry', ...layer.segments);
|
|
166
|
-
}
|
|
167
|
-
// 'fraim' layers live inside <projectRoot>/fraim/.
|
|
168
157
|
return path_1.default.join((0, project_fraim_paths_1.getWorkspaceFraimDir)(projectPath), ...layer.segments);
|
|
169
158
|
}
|
|
170
159
|
function discoverLayers(projectPath, layers) {
|
|
@@ -300,13 +289,29 @@ function nextPhase(edge, discriminant) {
|
|
|
300
289
|
}
|
|
301
290
|
return null;
|
|
302
291
|
}
|
|
292
|
+
// Parse the ordered phase list from a job stub's ## Steps section.
|
|
293
|
+
// Real FRAIM job stubs use Markdown steps rather than JSON frontmatter;
|
|
294
|
+
// this is the fallback parser that makes the pizza tracker work for them.
|
|
295
|
+
function loadJobPhasesFromSteps(filePath) {
|
|
296
|
+
const raw = fs_1.default.readFileSync(filePath, 'utf8');
|
|
297
|
+
const stepsMatch = raw.match(/## Steps\r?\n([\s\S]*?)(?:\r?\n## |\r?\n---|$)/);
|
|
298
|
+
if (!stepsMatch)
|
|
299
|
+
return [];
|
|
300
|
+
const phases = [];
|
|
301
|
+
for (const line of stepsMatch[1].split(/\r?\n/)) {
|
|
302
|
+
const m = line.match(/`([a-z][a-z0-9-]*)`/);
|
|
303
|
+
if (m)
|
|
304
|
+
phases.push({ id: m[1], label: friendlyPhaseLabel(m[1]) });
|
|
305
|
+
}
|
|
306
|
+
return phases;
|
|
307
|
+
}
|
|
303
308
|
function loadJobPhases(jobId, projectPath, discriminant = 'feature') {
|
|
304
309
|
const stubPath = findJobStubPath(projectPath, jobId);
|
|
305
310
|
if (!stubPath)
|
|
306
311
|
return [];
|
|
307
312
|
const fm = readJobFrontmatter(stubPath);
|
|
308
313
|
if (!fm || !fm.initialPhase || !fm.phases)
|
|
309
|
-
return
|
|
314
|
+
return loadJobPhasesFromSteps(stubPath);
|
|
310
315
|
const visited = new Set();
|
|
311
316
|
const ordered = [];
|
|
312
317
|
let cursor = fm.initialPhase;
|
|
@@ -334,8 +339,10 @@ function loadAllJobPhaseIds(jobId, projectPath) {
|
|
|
334
339
|
if (!stubPath)
|
|
335
340
|
return new Set();
|
|
336
341
|
const fm = readJobFrontmatter(stubPath);
|
|
337
|
-
if (!fm || !fm.phases)
|
|
338
|
-
|
|
342
|
+
if (!fm || !fm.phases) {
|
|
343
|
+
const phases = loadJobPhasesFromSteps(stubPath);
|
|
344
|
+
return new Set(phases.map((p) => p.id));
|
|
345
|
+
}
|
|
339
346
|
return new Set(Object.keys(fm.phases));
|
|
340
347
|
}
|
|
341
348
|
// Issue #347 — public exposure of the friendly-label rule so callers
|
|
@@ -386,6 +386,14 @@ class AiHubServer {
|
|
|
386
386
|
recentJobIds: existingPreferences.recentJobIds,
|
|
387
387
|
}, jobId);
|
|
388
388
|
res.status(201).json(this.enrichRunForResponse(run));
|
|
389
|
+
// Background sync: refresh the local FRAIM catalog so the next job
|
|
390
|
+
// picker load sees any jobs that were added or updated since last run.
|
|
391
|
+
try {
|
|
392
|
+
const syncChild = (0, child_process_1.spawn)('npx', ['fraim', 'sync'], { cwd: projectPath, detached: true, stdio: 'ignore' });
|
|
393
|
+
syncChild.on('error', () => { });
|
|
394
|
+
syncChild.unref();
|
|
395
|
+
}
|
|
396
|
+
catch { /* ignore if spawn itself throws synchronously */ }
|
|
389
397
|
}
|
|
390
398
|
catch (error) {
|
|
391
399
|
res.status(400).json({ error: error instanceof Error ? error.message : 'Could not start run.' });
|
package/package.json
CHANGED
package/public/ai-hub/script.js
CHANGED
|
@@ -951,18 +951,22 @@ function fraimInvocationFor(employeeId, jobId, kind) {
|
|
|
951
951
|
// invocation. The wrapped text is what we ACTUALLY send to the host CLI
|
|
952
952
|
// AND what we show in the timeline so the manager sees what the agent
|
|
953
953
|
// received.
|
|
954
|
-
function buildAgentMessage(employeeId, jobId, kind, instructions) {
|
|
954
|
+
function buildAgentMessage(employeeId, jobId, kind, instructions, stubPath) {
|
|
955
955
|
const invocation = fraimInvocationFor(employeeId, jobId, kind);
|
|
956
|
+
const stub = (kind === 'start' && stubPath) ? `\n[Job stub: ${stubPath}]` : '';
|
|
956
957
|
const trimmed = (instructions || '').trim();
|
|
957
|
-
if (!trimmed) return invocation
|
|
958
|
-
return `${invocation}\n\n${trimmed}`;
|
|
958
|
+
if (!trimmed) return `${invocation}${stub}`;
|
|
959
|
+
return `${invocation}${stub}\n\n${trimmed}`;
|
|
959
960
|
}
|
|
960
961
|
|
|
961
962
|
async function startRun(job, instructions, employeeId) {
|
|
962
963
|
// Prefix the manager's typed instructions with the FRAIM invocation so
|
|
963
964
|
// the underlying host actually launches the right job. The prefixed
|
|
964
965
|
// text is what the host receives AND what we show in the timeline.
|
|
965
|
-
const
|
|
966
|
+
const absoluteStubPath = (job.stubPath && state.projectPath)
|
|
967
|
+
? [state.projectPath, job.stubPath].join('/').replace(/\\/g, '/').replace(/\/+/g, '/')
|
|
968
|
+
: job.stubPath;
|
|
969
|
+
const agentMessage = buildAgentMessage(employeeId, job.id, 'start', instructions, absoluteStubPath);
|
|
966
970
|
const conv = {
|
|
967
971
|
id: newConversationId(),
|
|
968
972
|
projectPath: state.projectPath,
|
|
@@ -1266,6 +1270,16 @@ function wireEvents() {
|
|
|
1266
1270
|
return;
|
|
1267
1271
|
}
|
|
1268
1272
|
|
|
1273
|
+
// Auto-onboard: if the project hasn't been set up with FRAIM and there's no
|
|
1274
|
+
// existing conversation for it, kick off the onboarding job automatically.
|
|
1275
|
+
if (state.bootstrap && state.bootstrap.project && state.bootstrap.project.needsOnboarding) {
|
|
1276
|
+
const existing = activeConversation();
|
|
1277
|
+
if (!existing || existing.projectPath !== state.projectPath) {
|
|
1278
|
+
await autoOnboardProject();
|
|
1279
|
+
return;
|
|
1280
|
+
}
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1269
1283
|
// If an active conversation belongs to the loaded project and is still running, resume polling.
|
|
1270
1284
|
const conv = activeConversation();
|
|
1271
1285
|
if (conv && conv.projectPath === state.projectPath) {
|
|
@@ -1277,6 +1291,53 @@ function wireEvents() {
|
|
|
1277
1291
|
}
|
|
1278
1292
|
})();
|
|
1279
1293
|
|
|
1294
|
+
async function autoOnboardProject() {
|
|
1295
|
+
const employeeId = state.selectedEmployeeId || 'claude';
|
|
1296
|
+
const onboardingMessage = "Your AI employee is onboarding to this project. They'll explore the codebase, learn how it's structured, and may ask a few questions so they can contribute meaningfully.";
|
|
1297
|
+
const job = (state.bootstrap && state.bootstrap.jobs || []).find((j) => j.id === 'project-onboarding');
|
|
1298
|
+
if (job) {
|
|
1299
|
+
await startRun(job, onboardingMessage, employeeId);
|
|
1300
|
+
return;
|
|
1301
|
+
}
|
|
1302
|
+
// Fallback: project-onboarding job not in catalog (fraim/ doesn't exist yet) — POST directly.
|
|
1303
|
+
try {
|
|
1304
|
+
const run = await requestJson('/api/ai-hub/runs', {
|
|
1305
|
+
method: 'POST',
|
|
1306
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1307
|
+
body: JSON.stringify({
|
|
1308
|
+
projectPath: state.projectPath,
|
|
1309
|
+
hostId: employeeId,
|
|
1310
|
+
jobId: 'project-onboarding',
|
|
1311
|
+
message: onboardingMessage,
|
|
1312
|
+
}),
|
|
1313
|
+
});
|
|
1314
|
+
const conv = {
|
|
1315
|
+
id: newConversationId(),
|
|
1316
|
+
projectPath: state.projectPath,
|
|
1317
|
+
title: 'Project Onboarding',
|
|
1318
|
+
jobId: 'project-onboarding',
|
|
1319
|
+
jobTitle: 'Project Onboarding',
|
|
1320
|
+
employeeId,
|
|
1321
|
+
runId: run.id,
|
|
1322
|
+
sessionId: run.sessionId || null,
|
|
1323
|
+
status: 'running',
|
|
1324
|
+
messages: [{ role: 'manager', text: onboardingMessage, at: Date.now() }],
|
|
1325
|
+
events: [],
|
|
1326
|
+
artifacts: [],
|
|
1327
|
+
lastUpdatedAt: Date.now(),
|
|
1328
|
+
};
|
|
1329
|
+
foldRunIntoConversation(conv, run);
|
|
1330
|
+
upsertConversation(conv);
|
|
1331
|
+
state.activeId = conv.id;
|
|
1332
|
+
persistConversations();
|
|
1333
|
+
renderRail();
|
|
1334
|
+
renderActive();
|
|
1335
|
+
startPolling();
|
|
1336
|
+
} catch (err) {
|
|
1337
|
+
showStatus(err.message, true);
|
|
1338
|
+
}
|
|
1339
|
+
}
|
|
1340
|
+
|
|
1280
1341
|
function renderFirstRunLanding() {
|
|
1281
1342
|
// Show first-run onboarding screen over the normal Hub UI.
|
|
1282
1343
|
// The user picks a project folder here; clicking Start fires the
|