fraim 2.0.360 → 2.0.361-next.1

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.
@@ -46,20 +46,25 @@ function directory(root, value, field) {
46
46
  throw new Error(`Invalid artifactPaths.${field}: target is not a directory`);
47
47
  return target.replace(/\\/g, '/');
48
48
  }
49
- function resolveArtifactPaths(homeRoot, contentRoot) {
49
+ function resolveArtifactPaths(homeRoot, contentRoot, configPath, configOverrides) {
50
50
  if (!node_path_1.default.isAbsolute(homeRoot) || !node_path_1.default.isAbsolute(contentRoot))
51
51
  throw new Error('Absolute owning homeRoot and contentRoot are required');
52
52
  homeRoot = node_path_1.default.resolve(homeRoot);
53
53
  contentRoot = node_path_1.default.resolve(contentRoot);
54
54
  if (!within(homeRoot, contentRoot))
55
55
  throw new Error('Personalized contentRoot escapes owning homeRoot');
56
- const configPath = node_path_1.default.join(node_path_1.default.dirname(contentRoot), 'config.json');
56
+ const resolvedConfigPath = configPath ?? node_path_1.default.join(node_path_1.default.dirname(contentRoot), 'config.json');
57
57
  let config = {};
58
- try {
59
- config = (0, local_runtime_1.readJsonIfObject)(configPath, { strict: true });
58
+ if (configOverrides !== undefined) {
59
+ config = configOverrides;
60
60
  }
61
- catch {
62
- throw new Error(`Cannot read artifact configuration: ${configPath}`);
61
+ else if (node_fs_1.default.existsSync(resolvedConfigPath)) {
62
+ try {
63
+ config = (0, local_runtime_1.readJsonIfObject)(resolvedConfigPath, { strict: true });
64
+ }
65
+ catch {
66
+ throw new Error(`Cannot read artifact configuration: ${resolvedConfigPath}`);
67
+ }
63
68
  }
64
69
  const configured = config.artifactPaths === undefined ? {} : config.artifactPaths;
65
70
  // Defaults follow the configured home layout, but every value has the same root-relative basis.
@@ -90,7 +95,7 @@ function resolveArtifactPaths(homeRoot, contentRoot) {
90
95
  if (within(target, other) || within(other, target))
91
96
  throw new Error(`Artifact directories overlap: ${field} and ${otherField}`);
92
97
  }
93
- return { configPath: configPath.replace(/\\/g, '/'), evidence: outputs.evidence, raw: outputs.raw,
98
+ return { configPath: resolvedConfigPath.replace(/\\/g, '/'), evidence: outputs.evidence, raw: outputs.raw,
94
99
  retrospectives: outputs.retrospectives,
95
100
  archiveRaw: directory(homeRoot, values.learningArchive + '/raw', 'learningArchive'),
96
101
  archiveRetrospectives: directory(homeRoot, values.learningArchive + '/retrospectives', 'learningArchive') };
@@ -12,7 +12,7 @@ const learning_usage_store_1 = require("../../../src/local-mcp-server/learning-u
12
12
  const owner_identity_1 = require("./owner-identity");
13
13
  const proposal_queue_1 = require("./proposal-queue");
14
14
  const completion_state_1 = require("./completion-state");
15
- const artifact_paths_1 = require("../artifact-paths");
15
+ const pack_home_1 = require("../../../src/cli/utils/pack-home");
16
16
  const learning_source_record_1 = require("../learning-usage/learning-source-record");
17
17
  const corpus_audit_1 = require("./corpus-audit");
18
18
  const outcome_types_1 = require("./outcome-types");
@@ -34,7 +34,7 @@ function resolveAssessmentSources(input, homes) {
34
34
  const originalFile = (0, owner_identity_1.configuredArtifactPath)(home, node_path_1.default.join((0, owner_identity_1.ownerArtifactRoot)(home), ref.relativePath));
35
35
  let file = originalFile;
36
36
  if (!node_fs_1.default.existsSync(file)) {
37
- const paths = (0, artifact_paths_1.resolveArtifactPaths)((0, owner_identity_1.ownerArtifactRoot)(home), home.contentRoot);
37
+ const paths = (0, pack_home_1.resolvePackArtifactPaths)(home);
38
38
  const archive = (0, completion_state_1.sourcePathKey)(node_path_1.default.dirname(file)) === (0, completion_state_1.sourcePathKey)(paths.raw) ? paths.archiveRaw
39
39
  : (0, completion_state_1.sourcePathKey)(node_path_1.default.dirname(file)) === (0, completion_state_1.sourcePathKey)(paths.retrospectives) ? paths.archiveRetrospectives : undefined;
40
40
  if (archive)
@@ -83,7 +83,8 @@ function verifyAssessmentSources(sources, state) {
83
83
  const originalBody = source.archived ? body.replace(/^(synthesized:)[^\r\n]*/m, '$1') : body;
84
84
  if ((0, proposal_queue_1.hashContent)(body) !== source.ref.sha256 && (0, proposal_queue_1.hashContent)(originalBody) !== source.ref.sha256)
85
85
  throw new Error('assessment-source-hash-conflict');
86
- const parsed = (0, learning_source_record_1.parseLearningSourceRecord)(body, source.file, (0, owner_identity_1.ownerArtifactRoot)(source.home), source.home.contentRoot);
86
+ const locations = (0, pack_home_1.resolvePackArtifactPaths)(source.home);
87
+ const parsed = (0, learning_source_record_1.parseLearningSourceRecord)(body, source.file, (0, owner_identity_1.ownerArtifactRoot)(source.home), source.home.contentRoot, locations);
87
88
  if (parsed.status !== 'valid')
88
89
  throw new Error(`invalid-assessment-source: ${parsed.reason}`);
89
90
  if (parsed.record.sourceId !== source.ref.sourceId
@@ -17,7 +17,7 @@ const proposal_queue_1 = require("./proposal-queue");
17
17
  const entry_authoring_1 = require("./entry-authoring");
18
18
  const run_publication_1 = require("./run-publication");
19
19
  const completion_state_2 = require("./completion-state");
20
- const artifact_paths_1 = require("../artifact-paths");
20
+ const pack_home_1 = require("../../../src/cli/utils/pack-home");
21
21
  const learning_source_record_1 = require("../learning-usage/learning-source-record");
22
22
  const corpus_audit_1 = require("./corpus-audit");
23
23
  const { learningEntryBlocks, learningEntryMetadata } = require('./entry-format');
@@ -453,14 +453,14 @@ function sourceCompletion(run, homes, options, afterStage) {
453
453
  if (new Set(normalized.map(item => process.platform === 'win32' ? item.path.toLowerCase() : item.path)).size !== normalized.length)
454
454
  throw new Error('duplicate-analyzed-source');
455
455
  const runHome = options.resolveHome(run.owner);
456
- const retrospectiveFile = (0, owner_identity_1.configuredArtifactPath)(runHome, node_path_1.default.join((0, artifact_paths_1.resolveArtifactPaths)((0, owner_identity_1.ownerArtifactRoot)(runHome), runHome.contentRoot).retrospectives, node_path_1.default.basename(options.retrospective.relativePath)));
456
+ const retrospectiveFile = (0, owner_identity_1.configuredArtifactPath)(runHome, node_path_1.default.join((0, pack_home_1.resolvePackArtifactPaths)(runHome).retrospectives, node_path_1.default.basename(options.retrospective.relativePath)));
457
457
  const allowed = (source) => {
458
458
  for (const home of homes) {
459
459
  const configured = options.resolveHome(home.owner);
460
460
  (0, owner_identity_1.assertSameOwner)(home.owner, configured.owner);
461
461
  if ((0, completion_state_2.sourcePathKey)(home.contentRoot) !== (0, completion_state_2.sourcePathKey)(configured.contentRoot) || (0, completion_state_2.sourcePathKey)(home.root) !== (0, completion_state_2.sourcePathKey)(configured.root) || (0, completion_state_2.sourcePathKey)((0, owner_identity_1.ownerArtifactRoot)(home)) !== (0, completion_state_2.sourcePathKey)((0, owner_identity_1.ownerArtifactRoot)(configured)))
462
462
  throw new Error('analyzed-source-outside-home');
463
- const locations = (0, artifact_paths_1.resolveArtifactPaths)((0, owner_identity_1.ownerArtifactRoot)(configured), configured.contentRoot);
463
+ const locations = (0, pack_home_1.resolvePackArtifactPaths)(configured);
464
464
  const sourceDirectory = source.kind === 'raw' ? locations.raw : locations.retrospectives;
465
465
  const archiveDirectory = source.kind === 'raw' ? locations.archiveRaw : locations.archiveRetrospectives;
466
466
  if ((0, completion_state_2.sourcePathKey)(node_path_1.default.dirname(source.path)) !== (0, completion_state_2.sourcePathKey)(sourceDirectory))
@@ -471,7 +471,7 @@ function sourceCompletion(run, homes, options, afterStage) {
471
471
  const relative = node_path_1.default.relative(configured.contentRoot, directory);
472
472
  return relative === '..' || relative.startsWith('..' + node_path_1.default.sep) || node_path_1.default.isAbsolute(relative);
473
473
  });
474
- return { root: (0, owner_identity_1.ownerArtifactRoot)(configured), contentRoot: configured.contentRoot, owner: configured.owner, destination,
474
+ return { root: (0, owner_identity_1.ownerArtifactRoot)(configured), contentRoot: configured.contentRoot, owner: configured.owner, destination, locations,
475
475
  layout: { contentRoot: node_path_1.default.resolve(configured.contentRoot).replace(/\\/g, '/'), sourceDirectory, archiveDirectory, ...(outsideContent ? { homeRoot: (0, owner_identity_1.ownerArtifactRoot)(configured) } : {}) } };
476
476
  }
477
477
  throw new Error('analyzed-source-outside-home');
@@ -510,7 +510,7 @@ function sourceCompletion(run, homes, options, afterStage) {
510
510
  if (!node_path_1.default.isAbsolute(ref))
511
511
  continue;
512
512
  const isInput = homes.some(home => {
513
- const locations = (0, artifact_paths_1.resolveArtifactPaths)((0, owner_identity_1.ownerArtifactRoot)(home), home.contentRoot);
513
+ const locations = (0, pack_home_1.resolvePackArtifactPaths)(home);
514
514
  return [locations.raw, locations.retrospectives].some(directory => (0, completion_state_2.sourcePathKey)(node_path_1.default.dirname(ref)) === (0, completion_state_2.sourcePathKey)(directory));
515
515
  });
516
516
  if (isInput && !normalized.some(item => (0, completion_state_2.sourcePathKey)(item.path) === (0, completion_state_2.sourcePathKey)(ref) && item.sha256 === action.sourceHashes?.[ref])
@@ -536,8 +536,8 @@ function sourceCompletion(run, homes, options, afterStage) {
536
536
  && !options.deferredSources?.some(item => (0, completion_state_2.sourcePathKey)(source.path) === (0, completion_state_2.sourcePathKey)(item.path)))))
537
537
  throw new Error('completion-sources-conflict');
538
538
  const date = new Date().toISOString().slice(0, 10);
539
- const validateInput = (body, source, root, contentRoot, owner, archivePath) => {
540
- const parsed = (0, learning_source_record_1.parseLearningSourceRecord)(body, archivePath ?? source.path, root, contentRoot);
539
+ const validateInput = (body, source, root, contentRoot, owner, configuredPaths, archivePath) => {
540
+ const parsed = (0, learning_source_record_1.parseLearningSourceRecord)(body, archivePath ?? source.path, root, contentRoot, configuredPaths);
541
541
  if (parsed.status !== 'valid' || parsed.record.kind !== source.kind
542
542
  || !(0, completion_state_1.sourceAuthorAllowed)(state, parsed.record.author, owner)
543
543
  || parsed.record.state !== (archivePath ? 'archived' : 'pending')
@@ -551,13 +551,13 @@ function sourceCompletion(run, homes, options, afterStage) {
551
551
  throw new Error(`completion-sources-conflict: ${source.path}: original plan changed`);
552
552
  return recorded;
553
553
  }
554
- const { root, destination, contentRoot, owner, layout } = allowed(source);
554
+ const { root, destination, contentRoot, owner, layout, locations } = allowed(source);
555
555
  regular(source.path, root);
556
556
  regular(destination, root);
557
557
  const body = node_fs_1.default.readFileSync(source.path, 'utf8');
558
558
  if ((0, proposal_queue_1.hashContent)(body) !== source.sha256)
559
559
  throw new Error('analyzed-source-changed');
560
- validateInput(body, source, root, contentRoot, owner);
560
+ validateInput(body, source, root, contentRoot, owner, locations);
561
561
  return { ...source, destination, resultHash: (0, proposal_queue_1.hashContent)(stamp(body, date)), synthesizedAt: date, layout };
562
562
  });
563
563
  const reservationsAndPlans = [...(prior ?? []), ...plans.filter(source => !prior?.some(item => (0, completion_state_2.sourcePathKey)(item.path) === (0, completion_state_2.sourcePathKey)(source.path)))];
@@ -637,7 +637,7 @@ function sourceCompletion(run, homes, options, afterStage) {
637
637
  throw new Error('source-participants-conflict');
638
638
  persistRejections(false);
639
639
  for (const source of plans) {
640
- const { root, destination, contentRoot, owner, layout } = allowed(source);
640
+ const { root, destination, contentRoot, owner, layout, locations } = allowed(source);
641
641
  regular(source.path, root);
642
642
  regular(destination, root);
643
643
  if ((0, completion_state_2.sourcePathKey)(destination) !== (0, completion_state_2.sourcePathKey)(source.destination) || (source.layout && Object.keys(layout).some(key => typeof source.layout[key] !== 'string' || (0, completion_state_2.sourcePathKey)(layout[key]) !== (0, completion_state_2.sourcePathKey)(source.layout[key]))))
@@ -646,7 +646,7 @@ function sourceCompletion(run, homes, options, afterStage) {
646
646
  if (body !== undefined) {
647
647
  if ((0, proposal_queue_1.hashContent)(body) !== source.sha256)
648
648
  throw new Error('analyzed-source-changed');
649
- validateInput(body, source, root, contentRoot, owner);
649
+ validateInput(body, source, root, contentRoot, owner, locations);
650
650
  }
651
651
  if (!node_fs_1.default.existsSync(destination)) {
652
652
  if (body === undefined)
@@ -668,12 +668,12 @@ function sourceCompletion(run, homes, options, afterStage) {
668
668
  const archivedBody = node_fs_1.default.readFileSync(destination, 'utf8');
669
669
  if ((0, proposal_queue_1.hashContent)(archivedBody) !== source.resultHash)
670
670
  throw new Error('source-archive-conflict');
671
- validateInput(archivedBody, source, root, contentRoot, owner, destination);
671
+ validateInput(archivedBody, source, root, contentRoot, owner, locations, destination);
672
672
  afterStage?.('source-verified');
673
673
  if (body !== undefined) {
674
674
  const currentRunHome = options.resolveHome(run.owner);
675
675
  (0, owner_identity_1.assertSameOwner)(run.owner, currentRunHome.owner);
676
- const currentRunPaths = (0, artifact_paths_1.resolveArtifactPaths)((0, owner_identity_1.ownerArtifactRoot)(currentRunHome), currentRunHome.contentRoot);
676
+ const currentRunPaths = (0, pack_home_1.resolvePackArtifactPaths)(currentRunHome);
677
677
  if ((0, completion_state_2.sourcePathKey)((0, owner_identity_1.ownerArtifactRoot)(currentRunHome)) !== (0, completion_state_2.sourcePathKey)((0, owner_identity_1.ownerArtifactRoot)(runHome))
678
678
  || (0, completion_state_2.sourcePathKey)(currentRunHome.contentRoot) !== (0, completion_state_2.sourcePathKey)(runHome.contentRoot)
679
679
  || (0, completion_state_2.sourcePathKey)(node_path_1.default.join(currentRunPaths.evidence, 'sleep')) !== (0, completion_state_2.sourcePathKey)(run.completionDirectory)
@@ -736,7 +736,7 @@ function completeRun(run, summary, homes, options, afterStage) {
736
736
  sources.push(source);
737
737
  }
738
738
  const sourceHome = (file) => homes.find(home => {
739
- const locations = (0, artifact_paths_1.resolveArtifactPaths)((0, owner_identity_1.ownerArtifactRoot)(home), home.contentRoot);
739
+ const locations = (0, pack_home_1.resolvePackArtifactPaths)(home);
740
740
  return [locations.raw, locations.retrospectives].some(directory => (0, completion_state_2.sourcePathKey)(node_path_1.default.dirname(file)) === (0, completion_state_2.sourcePathKey)(directory));
741
741
  });
742
742
  for (const action of actions)
@@ -774,7 +774,7 @@ function completeRun(run, summary, homes, options, afterStage) {
774
774
  const recorded = state.processedSources?.find(item => (0, completion_state_2.sourcePathKey)(item.path) === (0, completion_state_2.sourcePathKey)(source.path));
775
775
  if (recorded && (recorded.sha256 !== source.sha256 || recorded.kind !== source.kind))
776
776
  throw new Error('original-source-plan-changed');
777
- const locations = (0, artifact_paths_1.resolveArtifactPaths)((0, owner_identity_1.ownerArtifactRoot)(home), home.contentRoot);
777
+ const locations = (0, pack_home_1.resolvePackArtifactPaths)(home);
778
778
  const destination = (0, owner_identity_1.configuredArtifactPath)(home, node_path_1.default.join(source.kind === 'raw' ? locations.archiveRaw : locations.archiveRetrospectives, node_path_1.default.basename(source.path)));
779
779
  if (reserved.includes((0, completion_state_2.sourcePathKey)(source.path)) || reserved.includes((0, completion_state_2.sourcePathKey)(destination)))
780
780
  throw new Error('source-reserved-by-pending-run');
@@ -787,7 +787,7 @@ function completeRun(run, summary, homes, options, afterStage) {
787
787
  const body = node_fs_1.default.readFileSync(source.path, 'utf8');
788
788
  if ((0, proposal_queue_1.hashContent)(body) !== source.sha256)
789
789
  throw new Error('analyzed-source-changed');
790
- const parsed = (0, learning_source_record_1.parseLearningSourceRecord)(body, source.path, (0, owner_identity_1.ownerArtifactRoot)(home), home.contentRoot);
790
+ const parsed = (0, learning_source_record_1.parseLearningSourceRecord)(body, source.path, (0, owner_identity_1.ownerArtifactRoot)(home), home.contentRoot, locations);
791
791
  if (parsed.status !== 'valid' || parsed.record.kind !== source.kind || !(0, completion_state_1.sourceAuthorAllowed)(state, parsed.record.author, home.owner) || parsed.record.state !== 'pending' || parsed.record.synthesized)
792
792
  throw new Error('invalid-learning-source');
793
793
  // References may use immutable identity, content-relative or home-relative path, or
@@ -835,7 +835,7 @@ function completeRun(run, summary, homes, options, afterStage) {
835
835
  const retrospective = options.retrospective;
836
836
  const retrospectiveHome = options.resolveHome(run.owner);
837
837
  (0, owner_identity_1.assertSameOwner)(run.owner, retrospectiveHome.owner);
838
- const locations = (0, artifact_paths_1.resolveArtifactPaths)((0, owner_identity_1.ownerArtifactRoot)(retrospectiveHome), retrospectiveHome.contentRoot);
838
+ const locations = (0, pack_home_1.resolvePackArtifactPaths)(retrospectiveHome);
839
839
  const retrospectiveDirectory = node_path_1.default.relative((0, owner_identity_1.ownerArtifactRoot)(retrospectiveHome), locations.retrospectives).replace(/\\/g, '/');
840
840
  const legacyDirectory = node_path_1.default.relative(retrospectiveHome.contentRoot, locations.retrospectives).replace(/\\/g, '/');
841
841
  const expectedPaths = [retrospectiveDirectory, legacyDirectory].filter(directory => directory.split('/').every(part => part && part !== '.' && part !== '..'))
@@ -852,7 +852,7 @@ function completeRun(run, summary, homes, options, afterStage) {
852
852
  const verifyRetrospective = () => {
853
853
  // Meaning, attribution and template fidelity are reviewed by the agent.
854
854
  // This guard binds the actual document, including on interruption retries.
855
- const currentLocations = (0, artifact_paths_1.resolveArtifactPaths)((0, owner_identity_1.ownerArtifactRoot)(retrospectiveHome), retrospectiveHome.contentRoot);
855
+ const currentLocations = (0, pack_home_1.resolvePackArtifactPaths)(retrospectiveHome);
856
856
  if ((0, completion_state_2.sourcePathKey)(node_path_1.default.join(currentLocations.evidence, 'sleep')) !== (0, completion_state_2.sourcePathKey)(run.completionDirectory)
857
857
  || (0, completion_state_2.sourcePathKey)(currentLocations.retrospectives) !== (0, completion_state_2.sourcePathKey)(node_path_1.default.dirname(retrospectiveFile)))
858
858
  throw new Error('completion-artifact-paths-changed');
@@ -862,7 +862,7 @@ function completeRun(run, summary, homes, options, afterStage) {
862
862
  const body = node_fs_1.default.readFileSync(retrospectiveFile, 'utf8');
863
863
  if (!body.trim() || (0, proposal_queue_1.hashContent)(body) !== retrospective.sha256)
864
864
  throw new Error('completion-retrospective-changed');
865
- const parsed = (0, learning_source_record_1.parseLearningSourceRecord)(body, retrospectiveFile, (0, owner_identity_1.ownerArtifactRoot)(retrospectiveHome), retrospectiveHome.contentRoot);
865
+ const parsed = (0, learning_source_record_1.parseLearningSourceRecord)(body, retrospectiveFile, (0, owner_identity_1.ownerArtifactRoot)(retrospectiveHome), retrospectiveHome.contentRoot, currentLocations);
866
866
  if (parsed.status !== 'valid' || parsed.record.kind !== 'retrospective' || parsed.record.author !== run.reviewer || parsed.record.runId !== run.runId || parsed.record.synthesized)
867
867
  throw new Error('completion-retrospective-invalid');
868
868
  };
@@ -326,6 +326,7 @@ exports.syncCommand = new commander_1.Command('sync')
326
326
  .option('--skip-updates', 'Skip checking for CLI updates (legacy)')
327
327
  .option('--local', 'Sync from local development server (port derived from git branch)')
328
328
  .option('--global', 'Sync machine-level FRAIM content only (~/.fraim/), skip project maintenance')
329
+ .option('--automatic', 'Refresh catalogs without changing the selected MCP launcher version')
329
330
  .option('--project-root <path>', 'Explicit project root for configuration and local exclusion maintenance')
330
331
  .option('--project-adapters', 'Deprecated compatibility option (no effect; use machine-level FRAIM setup)')
331
332
  .action(exports.runSync);
@@ -1,22 +1,13 @@
1
1
  "use strict";
2
- /**
3
- * Agent CLI PATH/version health checks for FRAIM doctor command
4
- * Issue #1284/#1285: detect drift between FRAIM's own managed-PATH resolution,
5
- * the ambient shell PATH, and the actual npm-global install for agent CLIs
6
- * FRAIM installs through its managed-Node fallback.
7
- */
8
- var __importDefault = (this && this.__importDefault) || function (mod) {
9
- return (mod && mod.__esModule) ? mod : { "default": mod };
10
- };
11
2
  Object.defineProperty(exports, "__esModule", { value: true });
12
3
  exports.getAgentCliHealthChecks = getAgentCliHealthChecks;
13
4
  exports.checkAgentCliHealthByCommand = checkAgentCliHealthByCommand;
14
- const child_process_1 = require("child_process");
15
- const path_1 = __importDefault(require("path"));
16
- const redact_paths_1 = require("../redact-paths");
17
5
  const managed_agent_paths_1 = require("../../utils/managed-agent-paths");
18
- const machine_path_1 = require("../../utils/machine-path");
6
+ const agent_command_probe_1 = require("../../utils/agent-command-probe");
19
7
  const command_resolution_1 = require("../../mcp/command-resolution");
8
+ const operation_scope_1 = require("../../utils/operation-scope");
9
+ const redact_paths_1 = require("../redact-paths");
10
+ const command_resolution_2 = require("../../mcp/command-resolution");
20
11
  // CLIs with a managed-install fallback (npm install -g into FRAIM's portable
21
12
  // Node when no system install is found). Add gemini/copilot here when their
22
13
  // installManagedAgent wiring lands.
@@ -24,50 +15,6 @@ const MANAGED_CLIS = [
24
15
  { id: 'codex', label: 'Codex', command: 'codex' },
25
16
  { id: 'claude-code', label: 'Claude Code', command: 'claude' },
26
17
  ];
27
- // Windows cannot CreateProcess a `.cmd`/`.bat` file directly (spawnSync on a
28
- // resolved absolute `.cmd` path throws EINVAL) — it must go through cmd.exe,
29
- // matching the same wrapping `hosts.ts`'s `resolveHostInvocation()` and
30
- // `server.ts`'s `hubCommandVersion()` already use. Duplicated here (not
31
- // imported from `hosts.ts`) because `src/cli` is a pure layer that cannot
32
- // import the server-layer `src/ai-hub` module (`scripts/validate-purity.ts`).
33
- function quoteWindowsArg(value) {
34
- if (value.length === 0)
35
- return '""';
36
- return `"${value.replace(/(\\*)"/g, '$1$1\\"').replace(/(\\+)$/g, '$1$1')}"`;
37
- }
38
- function escapeWindowsArg(value) {
39
- return /[\s"]/u.test(value) ? quoteWindowsArg(value) : value;
40
- }
41
- function probeVersion(commandPath) {
42
- try {
43
- const executable = process.platform === 'win32' ? 'cmd.exe' : commandPath;
44
- const args = process.platform === 'win32'
45
- ? ['/d', '/s', '/c', `${escapeWindowsArg(commandPath)} --version`]
46
- : ['--version'];
47
- const result = (0, child_process_1.spawnSync)(executable, args, { encoding: 'utf8', timeout: 5000, windowsHide: true });
48
- if (result.status !== 0 || result.error)
49
- return null;
50
- // Issue #1256 (AC-D1/D2): only stdout counts as a version — see versionFromProbeOutput.
51
- return (0, managed_agent_paths_1.versionFromProbeOutput)(result.stdout, result.stderr);
52
- }
53
- catch {
54
- return null;
55
- }
56
- }
57
- // The persisted Windows User PATH (what a brand-new terminal reads) can
58
- // disagree with this process's own inherited `process.env.PATH` (what a
59
- // shell already open before the fix landed still uses) — issue #1285's
60
- // exact "update said success but --version looked wrong" ambiguity.
61
- // Windows-only; there is no equivalent persisted-PATH registry on macOS/Linux.
62
- // Issue #1748: this used to shell out to PowerShell for HKCU Environment Path on its own,
63
- // a second independently-written Windows registry PATH reader. Two implementations of the
64
- // same read drift apart, and a parsing fix applied to one is easy to miss on the other,
65
- // which is the same PATH-resolution-drift bug class this check exists to report. Delegates
66
- // to the shared reader instead, which also covers HKLM rather than only the user hive and
67
- // expands REG_EXPAND_SZ references.
68
- function readPersistedUserPath() {
69
- return (0, machine_path_1.resolveMachinePathCached)();
70
- }
71
18
  // Issue #1482 (Privacy PRIV05 / GDPR Art. 5(1)(c)): the single exit point for every branch
72
19
  // below. `message` and `suggestion` are forwarded off this process — to the browser as
73
20
  // `driftWarning`/`driftSuggestion` by `POST /api/ai-hub/check-agent`, and to the Hub's server
@@ -75,132 +22,36 @@ function readPersistedUserPath() {
75
22
  // Redacting here rather than at each branch that happens to build a path into its message is
76
23
  // deliberate: a branch added to `computeAgentCliHealth()` later is covered whether or not its
77
24
  // author remembers this constraint. `details` is not redacted; see `redact-paths.ts` for why.
78
- async function runAgentCliHealthCheck(cli) {
79
- return (0, redact_paths_1.redactCheckResultPaths)(await computeAgentCliHealth(cli));
25
+ async function runAgentCliHealthCheck(cli, options = {}) {
26
+ return (0, operation_scope_1.withOperation)(options, 30000, async (signal) => (0, redact_paths_1.redactCheckResultPaths)(await computeAgentCliHealth(cli, signal)));
80
27
  }
81
- async function computeAgentCliHealth(cli) {
82
- // "Ambient" is this process's own raw, unmodified PATH — exactly what a
83
- // shell already open before a PATH fix landed still sees (issue #1285's
84
- // "any shell the user already has open keeps its own stale in-memory copy
85
- // until restarted" applies to the process running this check too, not
86
- // only the user's terminal). "Managed" strips any stale managed directory
87
- // already sitting on that ambient PATH and re-appends FRAIM's current,
88
- // correctly-ordered managed dirs — so it always reflects the current
89
- // versioned build, independent of whatever the ambient PATH happens to
90
- // still contain. Deliberately NOT `resolveManagedCommand()`: that helper
91
- // is system-PATH-first for launch purposes and would silently agree with
92
- // a stale ambient entry instead of surfacing the drift this check exists
93
- // to catch.
94
- // Issue #1748: process.env.PATH no longer carries FRAIM's managed bin dirs (the
95
- // module-load mutation that put them there was the defect). Without a fallback,
96
- // `ambientPath` is null whenever the only install is FRAIM-managed, and both clauses
97
- // of `versionMismatch` below short-circuit on it, so this check would return
98
- // "consistent" while the #1285 stale-shim drift it exists to catch went unreported.
99
- // The machine PATH is what a newly opened shell resolves, which is the notion of
100
- // "ambient" this check actually wants once the process is no longer self-mutating.
101
- const machinePathForAmbient = (0, machine_path_1.resolveMachinePathCached)();
102
- const ambientPath = (0, command_resolution_1.getSystemCommandPath)(cli.command)
103
- || (machinePathForAmbient ? (0, command_resolution_1.getSystemCommandPath)(cli.command, machinePathForAmbient) : null);
104
- const managedSearchPath = (0, managed_agent_paths_1.buildPathWithManagedAgentBins)(process.env.PATH);
105
- const managedPath = (0, command_resolution_1.getSystemCommandPath)(cli.command, managedSearchPath);
106
- if (!ambientPath && !managedPath) {
107
- // Issue #1256 (AC-A3): before concluding the CLI is absent, try the same shared
108
- // location-agnostic recovery the Hub's version probe and check-agent route use. This
109
- // only changes the answer for a CLI this process's raw/managed PATH cannot see at all
110
- // (e.g. doctor itself invoked from a PATH-truncated context) — it never overrides a
111
- // drift verdict computed from a real ambient/managed hit above.
112
- const recoveredSearchPath = (0, managed_agent_paths_1.buildRecoveredAgentPath)(process.env.PATH);
113
- const recoveredPath = (0, command_resolution_1.getSystemCommandPath)(cli.command, recoveredSearchPath);
114
- if (!recoveredPath) {
115
- return {
116
- status: 'passed',
117
- message: `${cli.label} is not installed; nothing to check.`,
118
- };
119
- }
120
- const recoveredVersion = probeVersion(recoveredPath);
121
- if (!recoveredVersion) {
122
- return {
123
- status: 'error',
124
- // Issue #1482: basename only. The full path stays in `details` for local/self-heal use.
125
- message: `${cli.label} is installed (launcher \`${(0, redact_paths_1.redactPathForDisplay)(recoveredPath)}\`) but failed to run (\`${cli.command} --version\` produced no output).`,
126
- suggestion: `Reinstall ${cli.label}, then run "${cli.command} --version" again to confirm it's fixed.`,
127
- details: { recoveredPath },
128
- };
28
+ async function computeAgentCliHealth(cli, signal) {
29
+ const env = await (0, agent_command_probe_1.agentProbeEnvironmentAsync)(undefined, process.env, { signal });
30
+ const failures = [];
31
+ const selected = await (0, agent_command_probe_1.selectAgentCommand)(cli.command, { env, signal, onFailure: reason => failures.push(reason) });
32
+ const ambientPath = (0, command_resolution_2.getSystemCommandPath)(cli.command);
33
+ const candidate = (0, command_resolution_1.getPortableManagedCommandPath)(cli.command);
34
+ const managedPath = candidate && (0, managed_agent_paths_1.isOwnedManagedAgentTarget)(candidate) ? candidate : null;
35
+ let repairable = false;
36
+ const managed = managedPath ? await (0, agent_command_probe_1.selectAgentCommand)(managedPath, { env, signal, onFailure: reason => { repairable = reason === 'exit'; } }) : null;
37
+ const ambient = ambientPath ? await (0, agent_command_probe_1.selectAgentCommand)(ambientPath, { env: { ...process.env }, signal }) : null;
38
+ const details = { repairable, launchFailure: selected ? undefined : failures.at(-1), ambientPath, ambientVersion: ambient?.version ?? null,
39
+ managedPath, managedVersion: managed?.version ?? null,
40
+ recoveredPath: selected?.target ?? (0, command_resolution_2.getSystemCommandPath)(cli.command, env.PATH), recoveredVersion: selected?.version ?? null };
41
+ if (!selected) {
42
+ if (!failures.length && !ambientPath && !managedPath && !(0, command_resolution_2.getSystemCommandPath)(cli.command, env.PATH)) {
43
+ return { status: 'passed', message: cli.label + ' is not installed; nothing to check.', details };
129
44
  }
130
- return {
131
- status: 'passed',
132
- message: `${cli.label} is consistent (${recoveredVersion}).`,
133
- details: { recoveredPath, recoveredVersion },
134
- };
45
+ const explanation = (0, agent_command_probe_1.agentProbeFailureMessage)(failures.at(-1));
46
+ const boundary = explanation.indexOf('. ');
47
+ return { status: failures.at(-1) === 'timeout' || failures.at(-1) === 'cancelled' ? 'warning' : 'error',
48
+ message: cli.label + ': ' + explanation.slice(0, boundary + 1), suggestion: explanation.slice(boundary + 2), details };
135
49
  }
136
- const ambientVersion = ambientPath ? probeVersion(ambientPath) : null;
137
- const managedVersion = !managedPath
138
- ? null
139
- : managedPath === ambientPath
140
- ? ambientVersion
141
- : probeVersion(managedPath);
142
- // Issue #1412: a resolved file that fails to execute at all (e.g. an
143
- // interrupted update left a non-executable fallback launcher) is a harder
144
- // failure than drift — `getSystemCommandPath()` only proves the file
145
- // exists, not that it can run. Without this check, a single broken install
146
- // with nothing else on PATH to disagree with (ambientPath === managedPath,
147
- // both versions null) falls straight through the mismatch check below and
148
- // reads as "consistent". Check this before computing npm-global/drift
149
- // details that don't matter once the CLI can't run at all.
150
- const brokenPath = ambientPath && !ambientVersion
151
- ? ambientPath
152
- : managedPath && !managedVersion
153
- ? managedPath
154
- : null;
155
- if (brokenPath) {
156
- return {
157
- status: 'error',
158
- // Issue #1482: basename only, same as the recovery branch above.
159
- message: `${cli.label} is installed (launcher \`${(0, redact_paths_1.redactPathForDisplay)(brokenPath)}\`) but failed to run (\`${cli.command} --version\` produced no output). An interrupted or partial update may have left a non-functional launcher.`,
160
- suggestion: `Reinstall ${cli.label}, then run "${cli.command} --version" again to confirm it's fixed.`,
161
- details: { ambientPath, ambientVersion, managedPath, managedVersion },
162
- };
50
+ if (ambientPath && (ambientPath !== selected.target || !ambient)) {
51
+ return { status: 'warning', message: cli.label + ' works in Hub (' + selected.version + '), but the current terminal resolves a different or broken launcher (' + (ambient?.version || 'unavailable') + ').',
52
+ suggestion: 'Open a new terminal and check the agent version.', details };
163
53
  }
164
- const npmGlobalBinDirs = (0, managed_agent_paths_1.resolveNpmGlobalBinDirs)();
165
- const npmGlobalPath = npmGlobalBinDirs.length > 0
166
- ? (0, command_resolution_1.getSystemCommandPath)(cli.command, npmGlobalBinDirs.join(path_1.default.delimiter))
167
- : null;
168
- const npmGlobalVersion = npmGlobalPath ? probeVersion(npmGlobalPath) : null;
169
- // Drift means either: the ambient shell resolves a different version than
170
- // the actual npm-global install, or this process's own ambient PATH
171
- // resolves a *different file* than FRAIM's freshly-recomputed managed-dir
172
- // resolution would — the exact "stale shim still shadows the current one"
173
- // signature from #1285.
174
- const versionMismatch = Boolean((ambientVersion && npmGlobalVersion && ambientVersion !== npmGlobalVersion)
175
- || (ambientPath && managedPath && ambientPath !== managedPath));
176
- if (!versionMismatch) {
177
- return {
178
- status: 'passed',
179
- message: `${cli.label} is consistent (${ambientVersion || managedVersion || 'unknown version'}).`,
180
- details: { ambientPath, managedPath, npmGlobalPath },
181
- };
182
- }
183
- const persistedUserPath = readPersistedUserPath();
184
- const persistedResolved = persistedUserPath ? (0, command_resolution_1.getSystemCommandPath)(cli.command, persistedUserPath) : null;
185
- const persistedVersion = persistedResolved ? probeVersion(persistedResolved) : null;
186
- const persistedMatchesManaged = Boolean(persistedResolved && managedPath && persistedResolved === managedPath);
187
- const suggestion = persistedUserPath === null
188
- ? `Restart your terminal, then run "${cli.command} --version" again to confirm.`
189
- : persistedMatchesManaged
190
- ? `Your saved PATH is already correct — open a new terminal window so it takes effect, then run "${cli.command} --version" again.`
191
- : `Your saved PATH still resolves the old ${cli.label} build. Run "${cli.command} update" (or reinstall ${cli.label}), then open a new terminal.`;
192
- return {
193
- status: 'warning',
194
- message: `${cli.label} PATH drift detected: ambient ${ambientVersion || 'not found'} vs managed ${managedVersion || 'not found'} vs npm-global ${npmGlobalVersion || 'not found'}.`,
195
- suggestion,
196
- details: {
197
- ambientPath, ambientVersion,
198
- managedPath, managedVersion,
199
- npmGlobalPath, npmGlobalVersion,
200
- persistedUserPathResolvedTo: persistedResolved,
201
- persistedUserPathVersion: persistedVersion,
202
- },
203
- };
54
+ return { status: 'passed', message: cli.label + ' is consistent (' + selected.version + ').', details };
204
55
  }
205
56
  function agentCliHealthCheck(cli) {
206
57
  return {
@@ -217,12 +68,12 @@ function getAgentCliHealthChecks() {
217
68
  // manager who clicks "Check if Ready" right after `codex update` sees PATH
218
69
  // drift immediately instead of a bare pass/fail. Returns null for a CLI this
219
70
  // module does not track.
220
- async function checkAgentCliHealthByCommand(command) {
71
+ async function checkAgentCliHealthByCommand(command, options = {}) {
221
72
  const cli = MANAGED_CLIS.find((entry) => entry.command === command);
222
73
  if (!cli)
223
74
  return null;
224
75
  try {
225
- return await runAgentCliHealthCheck(cli);
76
+ return await runAgentCliHealthCheck(cli, options);
226
77
  }
227
78
  catch {
228
79
  return null;
@@ -15,6 +15,7 @@ const user_config_1 = require("../utils/user-config");
15
15
  const run_record_1 = require("../../../registry/scripts/learning/run-record");
16
16
  const completion_state_1 = require("../../../registry/scripts/learning/completion-state");
17
17
  const artifact_paths_1 = require("../../../registry/scripts/artifact-paths");
18
+ const pack_home_2 = require("../utils/pack-home");
18
19
  const local_runtime_1 = require("../../../registry/scripts/learning-usage/local-runtime");
19
20
  const learning_source_record_1 = require("../../../registry/scripts/learning-usage/learning-source-record");
20
21
  const corpus_audit_1 = require("../../../registry/scripts/learning/corpus-audit");
@@ -76,7 +77,7 @@ function discoverLearningSources(projectPath, owner, orgRunId) {
76
77
  reservationHomes.push(record);
77
78
  if (record.status !== 'unavailable') {
78
79
  try {
79
- record.artifactPaths = (0, artifact_paths_1.resolveArtifactPaths)(record.root, record.contentRoot);
80
+ record.artifactPaths = (0, pack_home_2.resolvePackArtifactPaths)(record);
80
81
  }
81
82
  catch (error) {
82
83
  record.status = 'unavailable';
@@ -13,6 +13,7 @@ const node_crypto_1 = require("node:crypto");
13
13
  const runtime_1 = require("./runtime");
14
14
  const completion_state_1 = require("../../../registry/scripts/learning/completion-state");
15
15
  const artifact_paths_1 = require("../../../registry/scripts/artifact-paths");
16
+ const pack_home_1 = require("../utils/pack-home");
16
17
  const entry_authoring_1 = require("../../../registry/scripts/learning/entry-authoring");
17
18
  const owner_identity_1 = require("../../../registry/scripts/learning/owner-identity");
18
19
  const run_record_1 = require("../../../registry/scripts/learning/run-record");
@@ -21,14 +22,14 @@ const run_publication_1 = require("../../../registry/scripts/learning/run-public
21
22
  const corpus_audit_1 = require("../../../registry/scripts/learning/corpus-audit");
22
23
  const outcome_assessment_1 = require("../../../registry/scripts/learning/outcome-assessment");
23
24
  const project_fraim_paths_1 = require("../../core/utils/project-fraim-paths");
24
- const pack_home_1 = require("../utils/pack-home");
25
+ const pack_home_2 = require("../utils/pack-home");
25
26
  const user_config_1 = require("../utils/user-config");
26
27
  const local_registry_resolver_1 = require("../../core/utils/local-registry-resolver");
27
28
  function resolveQueueHome(projectPath, level) {
28
29
  return (0, owner_identity_1.resolveOwnerHome)(projectPath, level, { runtimeModule: runtime_1.runtimeModuleStatic });
29
30
  }
30
31
  function resolveReaderCorpusRoots(projectPath) {
31
- const manager = (0, pack_home_1.resolvePackHome)('manager', { migrateLegacy: false });
32
+ const manager = (0, pack_home_2.resolvePackHome)('manager', { migrateLegacy: false });
32
33
  const portable = (0, project_fraim_paths_1.getConfiguredPortableLearningsDir)(projectPath);
33
34
  const same = (left, right) => node_path_1.default.resolve(left) === node_path_1.default.resolve(right);
34
35
  const roots = manager.synced ? [] : [node_path_1.default.join((0, project_fraim_paths_1.getUserFraimDirPath)(), 'manager', 'learnings')];
@@ -37,7 +38,7 @@ function resolveReaderCorpusRoots(projectPath) {
37
38
  return [...new Set(roots)];
38
39
  }
39
40
  function evidenceDirectory(home) {
40
- const directory = node_path_1.default.join((0, artifact_paths_1.resolveArtifactPaths)((0, owner_identity_1.ownerArtifactRoot)(home), home.contentRoot).evidence, 'sleep');
41
+ const directory = node_path_1.default.join((0, pack_home_1.resolvePackArtifactPaths)(home).evidence, 'sleep');
41
42
  (0, owner_identity_1.configuredArtifactPath)(home, directory);
42
43
  return directory;
43
44
  }
@@ -115,8 +116,8 @@ async function runQueueCommand(argv) {
115
116
  if (input.kind === 'learning' && input.operation !== 'remove') {
116
117
  if (!input.target.entryId)
117
118
  input.target.entryId = 'L-' + (0, node_crypto_1.randomUUID)().replace(/-/g, '').slice(0, 10);
118
- const managerHome = (0, pack_home_1.resolvePackHome)('manager', { migrateLegacy: false });
119
- const orgHome = (0, pack_home_1.resolvePackHome)('org', { migrateLegacy: false });
119
+ const managerHome = (0, pack_home_2.resolvePackHome)('manager', { migrateLegacy: false });
120
+ const orgHome = (0, pack_home_2.resolvePackHome)('org', { migrateLegacy: false });
120
121
  const resolver = new local_registry_resolver_1.LocalRegistryResolver({ workspaceRoot: projectPath, managerLocalRoot: managerHome.contentRoot, managerCacheRoot: managerHome.contentRoot, orgCacheRoot: orgHome.contentRoot, remoteContentResolver: async () => { throw new Error('job-definition-unavailable'); } });
121
122
  if (input.content?.domain?.basis !== 'manager') {
122
123
  for (const origin of input.content.trace.originatingJobs) {
@@ -3,7 +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.resolveManagedCommand = exports.getPortableManagedCommandPath = exports.getSystemCommandPath = void 0;
6
+ exports.resolveManagedCommand = exports.getPortableManagedCommandPath = exports.getSystemCommandPath = exports.getSystemCommandCandidates = void 0;
7
7
  const fs_1 = __importDefault(require("fs"));
8
8
  const path_1 = __importDefault(require("path"));
9
9
  const managed_agent_paths_1 = require("../utils/managed-agent-paths");
@@ -20,25 +20,30 @@ const getPathEntries = (basePath) => {
20
20
  .map((entry) => entry.trim())
21
21
  .filter(Boolean);
22
22
  };
23
- const getSystemCommandCandidates = (command, basePath) => {
23
+ const getSystemCommandCandidates = (command, basePath, pathExt = process.env.PATHEXT) => {
24
24
  if (!command || path_1.default.isAbsolute(command)) {
25
25
  return command ? [command] : [];
26
26
  }
27
27
  const commandNames = process.platform === 'win32'
28
28
  ? command.includes('.')
29
29
  ? [command]
30
- : [command, `${command}.cmd`, `${command}.exe`, `${command}.bat`, `${command}.com`]
30
+ : (pathExt || '.COM;.EXE;.BAT;.CMD').split(';')
31
+ .filter(ext => /^\.(com|exe|bat|cmd)$/i.test(ext)).map(ext => command + ext.toLowerCase())
31
32
  : [command];
32
33
  return getPathEntries(basePath).flatMap((entry) => commandNames.map((name) => path_1.default.join(entry, name)));
33
34
  };
35
+ exports.getSystemCommandCandidates = getSystemCommandCandidates;
34
36
  // `basePath` defaults to the current process PATH; pass an explicit PATH
35
37
  // string to resolve a command against a different candidate list (e.g. the
36
38
  // npm-global bin dirs computed by `resolveNpmGlobalBinDirs`) without mutating
37
39
  // `process.env.PATH`.
38
40
  const getSystemCommandPath = (command, basePath) => {
39
- for (const candidate of getSystemCommandCandidates(command, basePath)) {
41
+ for (const candidate of (0, exports.getSystemCommandCandidates)(command, basePath)) {
40
42
  try {
41
43
  const stats = fs_1.default.statSync(candidate);
44
+ fs_1.default.accessSync(candidate, process.platform === 'win32' ? fs_1.default.constants.R_OK : fs_1.default.constants.X_OK);
45
+ if (process.platform === 'win32')
46
+ fs_1.default.closeSync(fs_1.default.openSync(candidate, 'r'));
42
47
  if (stats.isFile()) {
43
48
  return candidate;
44
49
  }