fraim 2.0.131 → 2.0.133
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.
|
@@ -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.' });
|
|
@@ -660,6 +660,7 @@ const runSetup = async (options) => {
|
|
|
660
660
|
console.log(chalk_1.default.gray(' legal, product, hiring, customer development, and more.'));
|
|
661
661
|
// Ask how the user wants to work with their AI employees.
|
|
662
662
|
// Skip prompt when the user already chose on a previous run (R2.2/R3.2/R4.6).
|
|
663
|
+
// Also skip in non-interactive environments (CI, tests, automated installs).
|
|
663
664
|
const { writeSetupHandoffChoice, readSetupHandoffChoice } = await Promise.resolve().then(() => __importStar(require('../../core/utils/setup-preferences')));
|
|
664
665
|
const storedChoice = readSetupHandoffChoice();
|
|
665
666
|
let choice;
|
|
@@ -667,6 +668,11 @@ const runSetup = async (options) => {
|
|
|
667
668
|
console.log(chalk_1.default.gray(`\n Using your saved preference: ${storedChoice === 'ide' ? 'In my IDE' : 'In FRAIM Hub'}`));
|
|
668
669
|
choice = storedChoice;
|
|
669
670
|
}
|
|
671
|
+
else if (process.env.FRAIM_NON_INTERACTIVE || process.env.CI) {
|
|
672
|
+
// In non-interactive environments, default to IDE mode (no browser launch)
|
|
673
|
+
console.log(chalk_1.default.gray('\n Non-interactive mode: defaulting to IDE setup'));
|
|
674
|
+
choice = 'ide';
|
|
675
|
+
}
|
|
670
676
|
else {
|
|
671
677
|
const userTypeResponse = await (0, prompts_1.default)({
|
|
672
678
|
type: 'select',
|
package/package.json
CHANGED
package/public/ai-hub/script.js
CHANGED
|
@@ -951,18 +951,19 @@ 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 agentMessage = buildAgentMessage(employeeId, job.id, 'start', instructions);
|
|
966
|
+
const agentMessage = buildAgentMessage(employeeId, job.id, 'start', instructions, job.stubPath);
|
|
966
967
|
const conv = {
|
|
967
968
|
id: newConversationId(),
|
|
968
969
|
projectPath: state.projectPath,
|
|
@@ -1266,6 +1267,16 @@ function wireEvents() {
|
|
|
1266
1267
|
return;
|
|
1267
1268
|
}
|
|
1268
1269
|
|
|
1270
|
+
// Auto-onboard: if the project hasn't been set up with FRAIM and there's no
|
|
1271
|
+
// existing conversation for it, kick off the onboarding job automatically.
|
|
1272
|
+
if (state.bootstrap && state.bootstrap.project && state.bootstrap.project.needsOnboarding) {
|
|
1273
|
+
const existing = activeConversation();
|
|
1274
|
+
if (!existing || existing.projectPath !== state.projectPath) {
|
|
1275
|
+
await autoOnboardProject();
|
|
1276
|
+
return;
|
|
1277
|
+
}
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1269
1280
|
// If an active conversation belongs to the loaded project and is still running, resume polling.
|
|
1270
1281
|
const conv = activeConversation();
|
|
1271
1282
|
if (conv && conv.projectPath === state.projectPath) {
|
|
@@ -1277,6 +1288,53 @@ function wireEvents() {
|
|
|
1277
1288
|
}
|
|
1278
1289
|
})();
|
|
1279
1290
|
|
|
1291
|
+
async function autoOnboardProject() {
|
|
1292
|
+
const employeeId = state.selectedEmployeeId || 'claude';
|
|
1293
|
+
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.";
|
|
1294
|
+
const job = (state.bootstrap && state.bootstrap.jobs || []).find((j) => j.id === 'project-onboarding');
|
|
1295
|
+
if (job) {
|
|
1296
|
+
await startRun(job, onboardingMessage, employeeId);
|
|
1297
|
+
return;
|
|
1298
|
+
}
|
|
1299
|
+
// Fallback: project-onboarding job not in catalog (fraim/ doesn't exist yet) — POST directly.
|
|
1300
|
+
try {
|
|
1301
|
+
const run = await requestJson('/api/ai-hub/runs', {
|
|
1302
|
+
method: 'POST',
|
|
1303
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1304
|
+
body: JSON.stringify({
|
|
1305
|
+
projectPath: state.projectPath,
|
|
1306
|
+
hostId: employeeId,
|
|
1307
|
+
jobId: 'project-onboarding',
|
|
1308
|
+
message: onboardingMessage,
|
|
1309
|
+
}),
|
|
1310
|
+
});
|
|
1311
|
+
const conv = {
|
|
1312
|
+
id: newConversationId(),
|
|
1313
|
+
projectPath: state.projectPath,
|
|
1314
|
+
title: 'Project Onboarding',
|
|
1315
|
+
jobId: 'project-onboarding',
|
|
1316
|
+
jobTitle: 'Project Onboarding',
|
|
1317
|
+
employeeId,
|
|
1318
|
+
runId: run.id,
|
|
1319
|
+
sessionId: run.sessionId || null,
|
|
1320
|
+
status: 'running',
|
|
1321
|
+
messages: [{ role: 'manager', text: onboardingMessage, at: Date.now() }],
|
|
1322
|
+
events: [],
|
|
1323
|
+
artifacts: [],
|
|
1324
|
+
lastUpdatedAt: Date.now(),
|
|
1325
|
+
};
|
|
1326
|
+
foldRunIntoConversation(conv, run);
|
|
1327
|
+
upsertConversation(conv);
|
|
1328
|
+
state.activeId = conv.id;
|
|
1329
|
+
persistConversations();
|
|
1330
|
+
renderRail();
|
|
1331
|
+
renderActive();
|
|
1332
|
+
startPolling();
|
|
1333
|
+
} catch (err) {
|
|
1334
|
+
showStatus(err.message, true);
|
|
1335
|
+
}
|
|
1336
|
+
}
|
|
1337
|
+
|
|
1280
1338
|
function renderFirstRunLanding() {
|
|
1281
1339
|
// Show first-run onboarding screen over the normal Hub UI.
|
|
1282
1340
|
// The user picks a project folder here; clicking Start fires the
|