fraim-hub 2.0.297 → 2.0.299

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.
@@ -42,12 +42,12 @@ function parseConfiguredCommand(runner, commandText) {
42
42
  // the only way to catch this class of error is to check, at readiness time,
43
43
  // that what the user typed there actually was that same base binary — not
44
44
  // to attempt to execute or otherwise interpret it.
45
- function describeConfiguredCommandInvocationProblem(parsed, baseHostId) {
45
+ function describeConfiguredCommandInvocationProblem(parsed, baseHostId, agentLabel = 'Configured agent') {
46
46
  if (!parsed.invocationHead) {
47
- return 'Command has no invocation to run after the environment prefix.';
47
+ return `${agentLabel} command has no invocation after the environment prefix. Run the create-hub-configured-agent job to repair this profile with an environment prefix followed by "${baseHostId}".`;
48
48
  }
49
49
  if (parsed.invocationHead !== baseHostId) {
50
- return `Command must invoke "${baseHostId}" (found "${parsed.invocationHead}"). A setup script should be registered as a setup script, not composed into this field — see hub-configured-agent-setup.md.`;
50
+ return `Configured agent "${agentLabel}" invokes "${parsed.invocationHead}", but Hub launches the "${baseHostId}" base CLI and cannot apply that wrapper's environment. Run the create-hub-configured-agent job to repair this profile with an environment prefix followed by "${baseHostId}".`;
51
51
  }
52
52
  return null;
53
53
  }
@@ -178,6 +178,17 @@ function checkConfiguredAgentAvailability(agent, employees, env = process.env) {
178
178
  if (!agent.command.command.trim()) {
179
179
  reasons.push('Command is empty.');
180
180
  }
181
+ else {
182
+ try {
183
+ const parsed = (0, configured_agent_command_1.parseConfiguredCommand)(agent.command.runner, agent.command.command);
184
+ const problem = (0, configured_agent_command_1.describeConfiguredCommandInvocationProblem)(parsed, agent.baseHostId, agent.label);
185
+ if (problem)
186
+ reasons.push(problem);
187
+ }
188
+ catch (error) {
189
+ reasons.push(error instanceof Error ? error.message : 'Configured agent command could not be parsed.');
190
+ }
191
+ }
181
192
  }
182
193
  else if (agent.setupScript) {
183
194
  const command = agent.setupScript.command;
@@ -195,26 +206,8 @@ function checkConfiguredAgentReadiness(agent, employees, env = process.env) {
195
206
  const check = checkConfiguredAgentAvailability(agent, employees, env);
196
207
  if (!check.available)
197
208
  return check;
198
- if (agent.command) {
199
- try {
200
- const parsed = (0, configured_agent_command_1.parseConfiguredCommand)(agent.command.runner, agent.command.command);
201
- const problem = (0, configured_agent_command_1.describeConfiguredCommandInvocationProblem)(parsed, agent.baseHostId);
202
- if (problem) {
203
- return { ...check, available: false, reasons: [...check.reasons, problem] };
204
- }
205
- return check;
206
- }
207
- catch (error) {
208
- return {
209
- ...check,
210
- available: false,
211
- reasons: [
212
- ...check.reasons,
213
- error instanceof Error ? error.message : 'Configured agent command could not be parsed.',
214
- ],
215
- };
216
- }
217
- }
209
+ if (agent.command)
210
+ return check;
218
211
  if (!agent.setupScript)
219
212
  return check;
220
213
  try {
@@ -630,12 +630,24 @@ async function bootstrap() {
630
630
  });
631
631
  await launchDesktopShell(options);
632
632
  }
633
- // Issue #1415: self-execution is now gated on `require.main === module` in addition to the
633
+ function isDirectElectronEntry() {
634
+ if (require.main === module)
635
+ return true;
636
+ const currentFile = path_1.default.resolve(__filename).toLowerCase();
637
+ return process.argv.some((arg) => {
638
+ try {
639
+ return path_1.default.resolve(arg).toLowerCase() === currentFile;
640
+ }
641
+ catch {
642
+ return false;
643
+ }
644
+ });
645
+ }
646
+ // Issue #1415: self-execution is gated on a direct Electron entry check in addition to the
634
647
  // existing Electron-main-process check, so `desktop-launcher.ts` requiring this file (bundled or
635
- // materialized) does not double-run bootstrap the launcher calls the exported `bootstrap`
636
- // explicitly instead. Direct invocation (`npm run hub:desktop`, Electron-launching test suites)
637
- // is unaffected: this file is still `require.main` in that case.
638
- if (require.main === module && process.versions.electron && process.type !== 'renderer') {
648
+ // materialized) does not double-run bootstrap. Electron's loader can own `require.main`, so
649
+ // direct test launches also match the resolved script path in argv.
650
+ if (isDirectElectronEntry() && process.versions.electron && process.type !== 'renderer') {
639
651
  bootstrap().catch(async (error) => {
640
652
  console.error(error instanceof Error ? error.message : error);
641
653
  await stopServerOnce();
@@ -43,6 +43,7 @@ const command_resolution_1 = require("../cli/mcp/command-resolution");
43
43
  const agent_token_prices_1 = require("../local-mcp-server/agent-token-prices");
44
44
  const configured_agents_1 = require("./configured-agents");
45
45
  const pack_home_1 = require("../cli/utils/pack-home");
46
+ const run_working_directory_1 = require("./run-working-directory");
46
47
  // Parse a single line of host stdout looking for a seekMentoring tool-use
47
48
  // signal. Returns null if the line does not contain one. Supports both
48
49
  // hosts FRAIM ships against today:
@@ -1916,7 +1917,7 @@ function detectCodexCompactTaskFailure(line) {
1916
1917
  }
1917
1918
  return null;
1918
1919
  }
1919
- function wireHostProcess(hostId, child, handlers) {
1920
+ function wireHostProcess(hostId, child, handlers, spawnCwd) {
1920
1921
  const wire = (buffer, channel) => {
1921
1922
  let pending = '';
1922
1923
  let suppressCodexModelRefreshStderr = false;
@@ -1971,12 +1972,23 @@ function wireHostProcess(hostId, child, handlers) {
1971
1972
  // message names the cause. 'close' still fires after 'error' for spawn
1972
1973
  // failures, so handlers.onExit is called by the close listener above.
1973
1974
  child.on('error', (err) => {
1974
- handlers.onEvent({ hostError: describeSpawnError(err) }, 'system');
1975
+ const spawnError = err;
1976
+ console.warn('[ai-hub] hub.host_spawn_error', {
1977
+ hostId,
1978
+ cwd: spawnCwd,
1979
+ cwdExists: spawnCwd ? (0, run_working_directory_1.workingDirectoryExists)(spawnCwd) : undefined,
1980
+ errorCode: spawnError.code,
1981
+ path: spawnError.path,
1982
+ });
1983
+ handlers.onEvent({ hostError: describeSpawnError(spawnError, spawnCwd) }, 'system');
1975
1984
  });
1976
1985
  return child;
1977
1986
  }
1978
- function describeSpawnError(err) {
1987
+ function describeSpawnError(err, cwd) {
1979
1988
  if (err.code === 'ENOENT') {
1989
+ if (cwd && !(0, run_working_directory_1.workingDirectoryExists)(cwd)) {
1990
+ return `Agent working directory not found (${cwd}). The conversation scope may have been used as a path.`;
1991
+ }
1980
1992
  return `Agent binary not found${err.path ? ` (${err.path})` : ''}. Ensure the agent CLI is installed and on PATH.`;
1981
1993
  }
1982
1994
  if (err.code === 'EACCES') {
@@ -2003,9 +2015,10 @@ function buildHostSpawnEnv(plan) {
2003
2015
  }
2004
2016
  function spawnHostProcess(hostId, plan, projectPath, handlers) {
2005
2017
  const invocation = resolveHostInvocation(plan);
2018
+ const spawnCwd = (0, run_working_directory_1.resolveHostWorkingDirectory)(projectPath);
2006
2019
  const startedAtMs = Date.now();
2007
2020
  const child = (0, child_process_1.spawn)(invocation.command, invocation.args, {
2008
- cwd: projectPath,
2021
+ cwd: spawnCwd,
2009
2022
  stdio: ['pipe', 'pipe', 'pipe'],
2010
2023
  env: buildHostSpawnEnv(plan),
2011
2024
  });
@@ -2015,13 +2028,13 @@ function spawnHostProcess(hostId, plan, projectPath, handlers) {
2015
2028
  child.stdin.end();
2016
2029
  if (typeof plan.stdin === 'string' && !plan.args.includes('--resume')) {
2017
2030
  child.once('close', () => {
2018
- const sessionId = discoverSessionIdAfterStart(hostId, projectPath, plan.stdin || '', startedAtMs);
2031
+ const sessionId = discoverSessionIdAfterStart(hostId, spawnCwd, plan.stdin || '', startedAtMs);
2019
2032
  if (sessionId) {
2020
2033
  handlers.onEvent({ sessionId, raw: `${hostId}-session:${sessionId}` }, 'system');
2021
2034
  }
2022
2035
  });
2023
2036
  }
2024
- return wireHostProcess(hostId, child, handlers);
2037
+ return wireHostProcess(hostId, child, handlers, spawnCwd);
2025
2038
  }
2026
2039
  function discoverSessionIdAfterStart(hostId, projectPath, prompt, startedAtMs) {
2027
2040
  if (hostId !== 'gemini')
@@ -2257,13 +2270,15 @@ class FakeHostRuntime {
2257
2270
  detectEmployees() {
2258
2271
  return this.employees;
2259
2272
  }
2260
- startRun(hostId, _projectPath, message, handlers, _sessionId) {
2273
+ startRun(hostId, _projectPath, message, handlers, _sessionId, launchContext) {
2261
2274
  this.lastStartMessage = message;
2275
+ this.lastLaunchContext = launchContext;
2262
2276
  const reply = this.startReply ?? this.fakeEmployeeReply('start', message);
2263
2277
  return this.fakeProcess(hostId, reply, handlers);
2264
2278
  }
2265
- continueRun(hostId, _projectPath, sessionId, message, handlers) {
2279
+ continueRun(hostId, _projectPath, sessionId, message, handlers, launchContext) {
2266
2280
  this.lastContinueMessage = message;
2281
+ this.lastLaunchContext = launchContext;
2267
2282
  return this.fakeProcess(hostId, this.fakeEmployeeReply('continue', message), handlers);
2268
2283
  }
2269
2284
  startDirectRun(hostId, _message, _projectPath, handlers, _sessionId) {
@@ -3,6 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.BADGE_POLL_INTERVAL_MS = void 0;
6
7
  exports.getLatestPublishedVersion = getLatestPublishedVersion;
7
8
  exports.getCachedLatestPublishedVersion = getCachedLatestPublishedVersion;
8
9
  exports.refreshLatestPublishedVersionInBackground = refreshLatestPublishedVersionInBackground;
@@ -21,6 +22,16 @@ const https_1 = __importDefault(require("https"));
21
22
  const TTL_MS = 5 * 60 * 1000;
22
23
  const FAILED_RETRY_MS = 5 * 60 * 1000;
23
24
  const REGISTRY_URL = 'https://registry.npmjs.org/fraim/latest';
25
+ // A resident Hub (hidden in the tray, login-item autostart) can go days without any of
26
+ // this module's other callers ever firing: they're all reactive (a page load, a
27
+ // persona-identity retry, a tray-click relaunch decision), and none of them are timer-driven.
28
+ // If nothing calls in, this cache never refreshes and the badge never learns about a new
29
+ // release, no matter how far behind it drifts. AiHubServer.start() below runs this on its
30
+ // own interval, independent of any request or window, so the cache — and therefore the
31
+ // badge, on its next client-side poll — stays within 15 minutes of the truth even when the
32
+ // Hub is otherwise fully idle. Lives here (not in the Electron shell) so it ships to
33
+ // existing installs via a normal npm publish, not a new signed installer.
34
+ exports.BADGE_POLL_INTERVAL_MS = 15 * 60 * 1000;
24
35
  let cache = { value: null, at: 0 };
25
36
  let lastAttemptAt = 0;
26
37
  let inFlight = null;
@@ -50,7 +50,7 @@ function buildCommunicationStyleNote() {
50
50
  function buildBackgroundTaskPolicyNote() {
51
51
  return [
52
52
  '',
53
- '[Background task policy] You are running in Hub headless mode. Never end your turn while background tasks (bash commands started with run_in_background=true, or sub-agents) are still active. Instead: monitor them, poll for results, and provide periodic progress commentary to the manager. Only end your turn when all background tasks have finished, or when you genuinely need a user decision or input to continue. If you exit while a background task is active, the Hub loses the link to that task permanently.',
53
+ '[Background task policy] You are running in Hub headless mode. Never end your turn while background tasks (bash commands started with run_in_background=true, or sub-agents) are still active. Instead: monitor them, poll for results, and provide periodic progress commentary to the manager. Only end your turn when all background tasks have finished, or when you genuinely need a user decision or input to continue. If you exit while a background task is active, the Hub loses the link to that task permanently. Background tasks and Monitor watchers cannot survive this turn ending: the next turn is a new process with no link to the old one (Windows: hard-killed; Unix: orphaned). The two things that reliably work: run the command synchronously in the foreground within this turn, or supervise it with `npx tsx ~/.fraim/scripts/exec-with-timeout.ts --supervise <name> --timeout <n> -- <command>` and poll `--status`/`--wait` (or `--stop` to cancel) without ending your turn.',
54
54
  ].join('\n');
55
55
  }
56
56
  // Issue #732: a plain continue of the SAME active job must not re-load the job.
@@ -0,0 +1,68 @@
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.resolveRunWorkingDirectory = resolveRunWorkingDirectory;
7
+ exports.executionScopeFromConversation = executionScopeFromConversation;
8
+ exports.resolveHostWorkingDirectory = resolveHostWorkingDirectory;
9
+ exports.workingDirectoryExists = workingDirectoryExists;
10
+ const fs_1 = __importDefault(require("fs"));
11
+ const path_1 = __importDefault(require("path"));
12
+ const pack_home_1 = require("../cli/utils/pack-home");
13
+ const conversation_store_1 = require("./conversation-store");
14
+ function ensureExistingDirectory(directoryPath, label) {
15
+ const trimmed = (directoryPath || '').trim();
16
+ if (!trimmed) {
17
+ throw new Error(label === 'Project' ? 'Project path is required.' : `${label} working directory is required.`);
18
+ }
19
+ const resolved = path_1.default.resolve(trimmed);
20
+ const stat = fs_1.default.existsSync(resolved) ? fs_1.default.statSync(resolved) : null;
21
+ if (!stat || !stat.isDirectory()) {
22
+ throw new Error(label === 'Project'
23
+ ? 'Project path must point to an existing directory.'
24
+ : `${label} working directory is not available: ${resolved}`);
25
+ }
26
+ return resolved;
27
+ }
28
+ function scopeHome(scope) {
29
+ return scope === 'manager'
30
+ ? (0, pack_home_1.resolvePackHome)('manager').contentRoot
31
+ : (0, pack_home_1.resolvePackHome)('org').contentRoot;
32
+ }
33
+ function resolveRunWorkingDirectory(scope, candidateProjectPath, defaultProjectPath) {
34
+ if (scope === 'manager') {
35
+ return ensureExistingDirectory(scopeHome('manager'), 'Manager');
36
+ }
37
+ if (scope === 'company') {
38
+ return ensureExistingDirectory(scopeHome('company'), 'Company');
39
+ }
40
+ return ensureExistingDirectory(candidateProjectPath || defaultProjectPath || '', 'Project');
41
+ }
42
+ function executionScopeFromConversation(scope, projectPath) {
43
+ if (scope === 'manager' || projectPath === conversation_store_1.MANAGER_SCOPE_KEY)
44
+ return 'manager';
45
+ if (scope === 'company' || projectPath === conversation_store_1.COMPANY_SCOPE_KEY)
46
+ return 'company';
47
+ return 'project';
48
+ }
49
+ function resolveHostWorkingDirectory(candidateProjectPath) {
50
+ const trimmed = (candidateProjectPath || '').trim();
51
+ if (trimmed === conversation_store_1.MANAGER_SCOPE_KEY) {
52
+ return ensureExistingDirectory(scopeHome('manager'), 'Manager');
53
+ }
54
+ if (trimmed === conversation_store_1.COMPANY_SCOPE_KEY) {
55
+ return ensureExistingDirectory(scopeHome('company'), 'Company');
56
+ }
57
+ return ensureExistingDirectory(trimmed, 'Agent');
58
+ }
59
+ function workingDirectoryExists(directoryPath) {
60
+ if (!directoryPath)
61
+ return false;
62
+ try {
63
+ return fs_1.default.statSync(path_1.default.resolve(directoryPath)).isDirectory();
64
+ }
65
+ catch {
66
+ return false;
67
+ }
68
+ }