fraim 2.0.136 → 2.0.138

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.
@@ -251,14 +251,14 @@ function buildStartPlan(hostId, message) {
251
251
  if (hostId === 'codex') {
252
252
  return {
253
253
  command: executableName('codex'),
254
- args: ['exec', '--json', '--skip-git-repo-check', '--dangerously-bypass-approvals-and-sandbox'],
254
+ args: ['exec', '--json', '--skip-git-repo-check', '--dangerously-bypass-approvals-and-sandbox', '--model', 'gpt-4o'],
255
255
  stdin: message,
256
256
  };
257
257
  }
258
258
  if (hostId === 'gemini') {
259
259
  return {
260
260
  command: executableName('gemini'),
261
- args: ['--yolo'],
261
+ args: ['--yolo', '--skip-trust'],
262
262
  stdin: message,
263
263
  };
264
264
  }
@@ -272,7 +272,7 @@ function buildContinuePlan(hostId, sessionId, message) {
272
272
  if (hostId === 'codex') {
273
273
  return {
274
274
  command: executableName('codex'),
275
- args: ['exec', 'resume', '--json', '--skip-git-repo-check', '--dangerously-bypass-approvals-and-sandbox', sessionId],
275
+ args: ['exec', 'resume', '--json', '--skip-git-repo-check', '--dangerously-bypass-approvals-and-sandbox', '--model', 'gpt-4o', sessionId],
276
276
  stdin: message,
277
277
  };
278
278
  }
@@ -281,7 +281,7 @@ function buildContinuePlan(hostId, sessionId, message) {
281
281
  // is sent as a fresh invocation. The Hub still tracks state client-side.
282
282
  return {
283
283
  command: executableName('gemini'),
284
- args: ['--yolo'],
284
+ args: ['--yolo', '--skip-trust'],
285
285
  stdin: message,
286
286
  };
287
287
  }
@@ -261,8 +261,10 @@ function hubCommandVersion(command) {
261
261
  }
262
262
  function hubRunProcess(command, args, env) {
263
263
  return new Promise((resolve, reject) => {
264
- const executable = process.platform === 'win32' && command === 'npm' ? 'npm.cmd' : command;
265
- const child = (0, child_process_1.spawn)(executable, args, {
264
+ const [realCmd, realArgs] = process.platform === 'win32'
265
+ ? ['cmd.exe', ['/d', '/s', '/c', command, ...args]]
266
+ : [command, args];
267
+ const child = (0, child_process_1.spawn)(realCmd, realArgs, {
266
268
  env: { ...process.env, ...env },
267
269
  stdio: ['ignore', 'pipe', 'pipe'],
268
270
  });
@@ -62,13 +62,10 @@ const detectWindsurf = () => {
62
62
  return checkMultiplePaths(paths);
63
63
  };
64
64
  const detectGeminiCli = () => {
65
- const paths = [
66
- '~/.gemini/settings.json',
67
- '~/.gemini',
68
- '~/AppData/Roaming/gemini/settings.json',
69
- '~/.config/gemini/settings.json'
70
- ];
71
- return checkMultiplePaths(paths) || availableByVersionProbe('gemini');
65
+ // Require the binary to be in PATH. The ~/.gemini directory alone is not
66
+ // sufficient — it exists when only Antigravity or other Gemini-adjacent
67
+ // tools are installed, which would produce a false positive.
68
+ return availableByVersionProbe('gemini');
72
69
  };
73
70
  exports.IDE_CONFIGS = [
74
71
  {
@@ -168,7 +165,7 @@ exports.IDE_CONFIGS = [
168
165
  configFormat: 'toml',
169
166
  configType: 'codex',
170
167
  invocationProfile: 'codex-skill',
171
- detectMethod: () => fs_1.default.existsSync(expandPath('~/.codex')),
168
+ detectMethod: () => availableByVersionProbe('codex'),
172
169
  description: 'Codex AI development environment'
173
170
  },
174
171
  {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fraim",
3
- "version": "2.0.136",
3
+ "version": "2.0.138",
4
4
  "description": "FRAIM CLI - Framework for Rigor-based AI Management (alias for fraim-framework)",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -96,7 +96,13 @@
96
96
  <div class="messages" id="messages"></div>
97
97
 
98
98
  <div class="coach">
99
- <div class="section-title">Coach the employee</div>
99
+ <div class="coach-title-row">
100
+ <span class="section-title">Coach the employee</span>
101
+ <span class="active-employee-row">
102
+ <label for="active-employee-select" class="active-employee-label">via</label>
103
+ <select id="active-employee-select" class="employee-select inline"></select>
104
+ </span>
105
+ </div>
100
106
  <textarea id="coach-text" placeholder="Tell the employee what to do next…"></textarea>
101
107
  <div class="coach-actions">
102
108
  <!-- Issue #347 R2: template picker. Hidden when the project
@@ -138,6 +144,7 @@
138
144
  <span class="left" id="job-pick-status">Choose a job to continue</span>
139
145
  <div class="right">
140
146
  <button class="ghost" type="button" id="cancel1">Cancel</button>
147
+ <button class="ghost" type="button" id="freeform-btn">Just describe what you need</button>
141
148
  <button class="send-button" type="button" id="next1" disabled>Next →</button>
142
149
  </div>
143
150
  </div>
@@ -40,7 +40,8 @@ function gatherElements() {
40
40
  'cancel1', 'next1', 'back2', 'start',
41
41
  'job-search', 'job-catalog', 'job-pick-status',
42
42
  'picked-name', 'picked-desc', 'instructions',
43
- 'employee-select', 'agent-install-panel',
43
+ 'employee-select', 'agent-install-panel', 'active-employee-select',
44
+ 'freeform-btn',
44
45
  // Issue #347 additions: tracker, template picker, totals.
45
46
  'tracker', 'tracker-rows', 'tracker-note',
46
47
  'template-picker-btn', 'template-popover',
@@ -349,6 +350,9 @@ function renderActive() {
349
350
  // Coaching state — only enable Send when there's text and the run is resumable.
350
351
  syncSendButton();
351
352
 
353
+ // Render the active employee selector in the coach section.
354
+ renderActiveEmployeeSelect(conv);
355
+
352
356
  // Issue #347 — tracker, totals, picker. The data lives on conv.run
353
357
  // (latest server snapshot folded into the conversation by
354
358
  // foldRunIntoConversation); for runs that have not yet polled we
@@ -360,6 +364,31 @@ function renderActive() {
360
364
  document.title = conv.title ? conv.title : 'AI Hub';
361
365
  }
362
366
 
367
+ // Render the inline employee selector shown in the coach section of an
368
+ // active conversation. Allows switching agents without reopening the modal.
369
+ function renderActiveEmployeeSelect(conv) {
370
+ const sel = els['active-employee-select'];
371
+ if (!sel) return;
372
+ const employees = state.bootstrap?.employees || [];
373
+ if (employees.length === 0) { sel.hidden = true; return; }
374
+ sel.hidden = false;
375
+ // Only rebuild the options when the employee list changes (avoid reset on every poll).
376
+ const newKey = employees.map((e) => `${e.id}:${e.available}`).join('|');
377
+ if (sel.dataset.optionsKey !== newKey) {
378
+ sel.innerHTML = '';
379
+ for (const emp of employees) {
380
+ const opt = document.createElement('option');
381
+ opt.value = emp.id;
382
+ opt.textContent = emp.label + (emp.available ? '' : ' (unavailable)');
383
+ if (!emp.available) opt.disabled = true;
384
+ sel.appendChild(opt);
385
+ }
386
+ sel.dataset.optionsKey = newKey;
387
+ }
388
+ const current = (conv && conv.employeeId) || state.selectedEmployeeId || 'claude';
389
+ if (sel.value !== current) sel.value = current;
390
+ }
391
+
363
392
  // Issue #347 R1 — render the pizza tracker. Reads conv.run.stages and
364
393
  // conv.run.currentPhase from the most recent backend snapshot. Hidden
365
394
  // when the active job has no declared phases.
@@ -1088,6 +1117,7 @@ const FRAIM_INVOCATION_SYMBOL = {
1088
1117
  };
1089
1118
 
1090
1119
  function fraimInvocationFor(employeeId, jobId, kind) {
1120
+ if (jobId === '__freeform__') return null;
1091
1121
  const symbol = FRAIM_INVOCATION_SYMBOL[employeeId] || '/fraim';
1092
1122
  if (kind === 'start') {
1093
1123
  return `${symbol} ${jobId}`;
@@ -1098,11 +1128,14 @@ function fraimInvocationFor(employeeId, jobId, kind) {
1098
1128
  // Wrap the manager's typed instructions with the host-appropriate FRAIM
1099
1129
  // invocation. The wrapped text is what we ACTUALLY send to the host CLI
1100
1130
  // AND what we show in the timeline so the manager sees what the agent
1101
- // received.
1131
+ // received. For freeform jobs (no FRAIM job assigned), the instructions
1132
+ // are sent verbatim with no invocation prefix.
1102
1133
  function buildAgentMessage(employeeId, jobId, kind, instructions, stubPath) {
1134
+ const trimmed = (instructions || '').trim();
1103
1135
  const invocation = fraimInvocationFor(employeeId, jobId, kind);
1136
+ // Freeform: no FRAIM prefix, no stub reference — just the raw instructions.
1137
+ if (!invocation) return trimmed;
1104
1138
  const stub = (kind === 'start' && stubPath) ? `\n[Job stub: ${stubPath}]` : '';
1105
- const trimmed = (instructions || '').trim();
1106
1139
  if (!trimmed) return `${invocation}${stub}`;
1107
1140
  // For continue turns, applyTemplateInvocation already writes the full
1108
1141
  // FRAIM invocation (e.g. "/fraim follow-your-mentor") into the textarea.
@@ -1118,14 +1151,16 @@ async function startRun(job, instructions, employeeId) {
1118
1151
  // Prefix the manager's typed instructions with the FRAIM invocation so
1119
1152
  // the underlying host actually launches the right job. The prefixed
1120
1153
  // text is what the host receives AND what we show in the timeline.
1121
- const absoluteStubPath = (job.stubPath && state.projectPath)
1154
+ // Freeform jobs carry no job stub and no FRAIM invocation prefix.
1155
+ const isFreeform = job.id === '__freeform__';
1156
+ const absoluteStubPath = (!isFreeform && job.stubPath && state.projectPath)
1122
1157
  ? [state.projectPath, job.stubPath].join('/').replace(/\\/g, '/').replace(/\/+/g, '/')
1123
- : job.stubPath;
1158
+ : undefined;
1124
1159
  const agentMessage = buildAgentMessage(employeeId, job.id, 'start', instructions, absoluteStubPath);
1125
1160
  const conv = {
1126
1161
  id: newConversationId(),
1127
1162
  projectPath: state.projectPath,
1128
- title: deriveTitle(job.title, instructions),
1163
+ title: isFreeform ? deriveTitle('', instructions) : deriveTitle(job.title, instructions),
1129
1164
  jobId: job.id,
1130
1165
  jobTitle: job.title,
1131
1166
  employeeId,
@@ -1170,6 +1205,43 @@ async function startRun(job, instructions, employeeId) {
1170
1205
  }
1171
1206
  }
1172
1207
 
1208
+ // Start a NEW run on an existing conversation with a different agent.
1209
+ // The conversation's message history stays intact; only the run restarts.
1210
+ async function restartConvWithAgent(conv, newAgentId, text) {
1211
+ const agentMessage = buildAgentMessage(newAgentId, conv.jobId, 'start', text);
1212
+ conv.status = 'running';
1213
+ conv.sessionId = null;
1214
+ conv.messages.push({ role: 'manager', text: agentMessage, at: Date.now() });
1215
+ upsertConversation(conv);
1216
+ renderRail();
1217
+ renderActive();
1218
+ try {
1219
+ const run = await requestJson('/api/ai-hub/runs', {
1220
+ method: 'POST',
1221
+ headers: { 'Content-Type': 'application/json' },
1222
+ body: JSON.stringify({
1223
+ projectPath: state.projectPath,
1224
+ hostId: newAgentId,
1225
+ jobId: conv.jobId,
1226
+ message: agentMessage,
1227
+ }),
1228
+ });
1229
+ conv.runId = run.id;
1230
+ foldRunIntoConversation(conv, run);
1231
+ upsertConversation(conv);
1232
+ renderRail();
1233
+ renderActive();
1234
+ startPolling();
1235
+ } catch (error) {
1236
+ conv.status = 'failed';
1237
+ conv.events.push({ channel: 'system', text: error.message });
1238
+ upsertConversation(conv);
1239
+ renderRail();
1240
+ renderActive();
1241
+ showStatus(error.message, true);
1242
+ }
1243
+ }
1244
+
1173
1245
  async function continueRun(text) {
1174
1246
  const conv = activeConversation();
1175
1247
  if (!conv || !conv.runId) return;
@@ -1320,6 +1392,31 @@ async function pickProject() {
1320
1392
  }
1321
1393
  }
1322
1394
 
1395
+ // Show step 2 of the new-job modal. Handles both regular jobs and freeform.
1396
+ function showStep2(job) {
1397
+ const isFreeform = job.id === '__freeform__';
1398
+ const assignedJobDiv = document.querySelector('#step2 .assigned-job');
1399
+ if (assignedJobDiv) assignedJobDiv.hidden = isFreeform;
1400
+ const h = document.querySelector('#step2 .modal-header h2');
1401
+ const p = document.querySelector('#step2 .modal-header p');
1402
+ if (isFreeform) {
1403
+ if (h) h.textContent = 'What do you need done?';
1404
+ if (p) p.textContent = 'Describe the task in plain language. No FRAIM job required.';
1405
+ els['instructions'].placeholder = 'Describe what you need…';
1406
+ } else {
1407
+ if (h) h.textContent = 'Tell the employee what you need';
1408
+ if (p) p.textContent = 'A few sentences is enough. The employee will ask if anything is unclear.';
1409
+ els['instructions'].placeholder = 'What outcome do you want? Any context the employee should know?';
1410
+ els['picked-name'].textContent = job.title;
1411
+ els['picked-desc'].textContent = job.intent || '';
1412
+ }
1413
+ els['instructions'].value = '';
1414
+ els['start'].disabled = true;
1415
+ els['step1'].hidden = true;
1416
+ els['step2'].hidden = false;
1417
+ setTimeout(() => els['instructions'].focus(), 50);
1418
+ }
1419
+
1323
1420
  // ---------------------------------------------------------------------------
1324
1421
  // Wire up
1325
1422
  // ---------------------------------------------------------------------------
@@ -1334,14 +1431,15 @@ function wireEvents() {
1334
1431
  });
1335
1432
  els['next1'].addEventListener('click', () => {
1336
1433
  if (!state.selectedJob) return;
1337
- els['picked-name'].textContent = state.selectedJob.title;
1338
- els['picked-desc'].textContent = state.selectedJob.intent || '';
1339
- els['instructions'].value = '';
1340
- els['start'].disabled = true;
1341
- els['step1'].hidden = true;
1342
- els['step2'].hidden = false;
1343
- setTimeout(() => els['instructions'].focus(), 50);
1434
+ showStep2(state.selectedJob);
1344
1435
  });
1436
+
1437
+ if (els['freeform-btn']) {
1438
+ els['freeform-btn'].addEventListener('click', () => {
1439
+ state.selectedJob = { id: '__freeform__', title: 'Freeform task', intent: '' };
1440
+ showStep2(state.selectedJob);
1441
+ });
1442
+ }
1345
1443
  els['instructions'].addEventListener('input', () => {
1346
1444
  els['start'].disabled = els['instructions'].value.trim().length === 0;
1347
1445
  });
@@ -1365,9 +1463,32 @@ function wireEvents() {
1365
1463
  if (!text) return;
1366
1464
  els['coach-text'].value = '';
1367
1465
  syncSendButton();
1368
- await continueRun(text);
1466
+ // If the user changed the agent via the inline selector, restart with the
1467
+ // new agent (new run) instead of continuing on the old one. This lets them
1468
+ // recover from a failed run by switching agents without opening the modal.
1469
+ const conv = activeConversation();
1470
+ const chosenAgent = els['active-employee-select'] && els['active-employee-select'].value;
1471
+ if (conv && chosenAgent && chosenAgent !== conv.employeeId && conv.status !== 'running') {
1472
+ conv.employeeId = chosenAgent;
1473
+ state.selectedEmployeeId = chosenAgent;
1474
+ await restartConvWithAgent(conv, chosenAgent, text);
1475
+ } else {
1476
+ await continueRun(text);
1477
+ }
1369
1478
  });
1370
1479
 
1480
+ if (els['active-employee-select']) {
1481
+ els['active-employee-select'].addEventListener('change', () => {
1482
+ const conv = activeConversation();
1483
+ const newAgent = els['active-employee-select'].value;
1484
+ state.selectedEmployeeId = newAgent;
1485
+ if (conv) {
1486
+ conv.employeeId = newAgent;
1487
+ persistConversations();
1488
+ }
1489
+ });
1490
+ }
1491
+
1371
1492
  // Issue #347 R2 — template picker.
1372
1493
  if (els['template-picker-btn']) {
1373
1494
  els['template-picker-btn'].addEventListener('click', (e) => {
@@ -315,6 +315,28 @@ button { font: inherit; cursor: pointer; }
315
315
  margin-bottom: 8px;
316
316
  }
317
317
 
318
+ .coach-title-row {
319
+ display: flex;
320
+ align-items: center;
321
+ justify-content: space-between;
322
+ margin-bottom: 8px;
323
+ }
324
+ .coach-title-row .section-title { margin-bottom: 0; }
325
+ .active-employee-row {
326
+ display: flex;
327
+ align-items: center;
328
+ gap: 6px;
329
+ }
330
+ .active-employee-label {
331
+ font-size: 12px;
332
+ color: var(--muted);
333
+ }
334
+ .employee-select.inline {
335
+ font-size: 12px;
336
+ padding: 3px 8px;
337
+ border-radius: 6px;
338
+ }
339
+
318
340
  /* Progress is now a single-line status strip (was a tall card) so the
319
341
  messages region gets the vertical space it needs. */
320
342
  .progress {